Subversion Repositories web.active

Rev

Rev 1 | Blame | Compare with Previous | Last modification | View Log | Download

<?php namespace ProcessWire;

/**
 * ProcessWire Markdown Textformatter
 *
 * ProcessWire 3.x, Copyright 2021 by Ryan Cramer

 * https://processwire.com
 *
 * Parsedown copyright Emanuil Rusev.
 * Markdown invented by John Gruber.
 * 
 * @property int $flavor Markdown flavor (see flavor constants)
 * @property bool|int $safeMode Whether or not we are in safe mode (https://github.com/erusev/parsedown#security)
 *
 */

class TextformatterMarkdownExtra extends Textformatter implements ConfigurableModule {

  public static function getModuleInfo() {
    return array(
      'title'   => 'Markdown/Parsedown Extra',
      'version' => 180,
      'summary' => "Markdown/Parsedown extra lightweight markup language by Emanuil Rusev. Based on Markdown by John Gruber.",
    );
  }

  const flavorDefault = 2;
  const flavorParsedownExtra = 2;
  const flavorParsedown = 4;
  const flavorMarkdownExtra = 2;
  const flavorRCD = 16; // add RCD extensions to one of above, @see markdownExtensions() method (@deprecated)
  
  public function __construct() {
    parent::__construct();
    $this->set('flavor', self::flavorDefault); 
    $this->set('safeMode', false);
  }

  public function format(&$str) {
    $str = $this->markdown($str, (int) $this->flavor); 
  }
  
  public function formatValue(Page $page, Field $field, &$value) {
    $value = $this->markdown($value, (int) $this->flavor);
  }

  /**
   * Get or set safe mode
   * 
   * “Parsedown is capable of escaping user-input within the HTML that it generates. 
   * Additionally Parsedown will apply sanitisation to additional scripting vectors 
   * (such as scripting link destinations) that are introduced by the markdown syntax 
   * itself. To tell Parsedown that it is processing untrusted user-input, use the 
   * following...” See: <https://github.com/erusev/parsedown#security>
   * 
   * @param bool|null $safeMode
   * @return bool
   * 
   */
  public function safeMode($safeMode = null) {
    if($safeMode !== null) $this->safeMode = $safeMode ? true : false;
    return (bool) $this->safeMode; 
  }

  /**
   * Given a string, return a version processed with markdown
   * 
   * @param $str String to process
   * @param int|null $flavor Flavor of markdown (null=use module configured setting)
   * @param bool|null $safeMode Safe mode ON or OFF (null=use module configured setting)
   * @return string Processed string
   * 
   */
  public function markdown($str, $flavor = null, $safeMode = null) {
    $parsedown = $this->getParsedown($flavor);
    if(is_bool($safeMode)) $parsedown->setSafeMode($safeMode);
    $str = $parsedown->text($str);
    if($flavor & self::flavorRCD) $this->markdownExtensions($str);
    return $str; 
  }

  /**
   * Given a string, return a version processed with markdown in safe mode
   * 
   * https://github.com/erusev/parsedown#security
   *
   * @param $str String to process
   * @param int $flavor Flavor of markdown (default=parsedown extra)
   * @return string Processed string
   *
   */
  public function markdownSafe($str, $flavor = 0) {
    return $this->markdown($str, $flavor, true);
  }
  
  /**
   * @param int|null $flavor
   * @return \ParsedownExtra|\Parsedown
   *
   */
  public function getParsedown($flavor = null) {
    if($flavor === null) $flavor = (int) $this->flavor;
    if(!class_exists("\\Parsedown")) require_once(dirname(__FILE__) . "/parsedown/Parsedown.php");
    if(!class_exists("\\ParsedownExtra")) require_once(dirname(__FILE__) . "/parsedown-extra/ParsedownExtra.php");
    if($flavor & self::flavorParsedown) {
      // regular parsedown
      $parsedown = new \Parsedown();
    } else {
      // parsedown extra 
      $parsedown = new \ParsedownExtra();
    }
    if($this->safeMode) $parsedown->setSafeMode(true);
    return $parsedown;
  }

  /**
   * A few RCD extentions to MarkDown syntax, to be executed after Markdown has already had it's way with the text
   * 
   * @param string $str
   * @deprecated
   *
   */
  public function markdownExtensions(&$str) {

    // add id attribute to a tag, when followed by a #myid
    if(strpos($str, '>#')) $str = preg_replace('/<([a-z][a-z0-9]*)([^>]*>.*?)(<\/\\1>)#([a-z][-_a-z0-9]*)\b/', '<$1 id="$4"$2$3', $str); 

    // add class attribute to tag when followed by a .myclass
    if(strpos($str, '>.')) $str = preg_replace('/<([a-z][a-z0-9]*)([^>]*>.*?)(<\/\\1>)\.([a-z][-_a-z0-9]*)\b/', '<$1 class="$4"$2$3', $str); 

    // href links open in new window when followed by a plus sign, i.e. [google](http://google.com)+
    if(strpos($str, '</a>+')) $str = preg_replace('/<a ([^>]+>.+?<\/a>)\+/', '<a target="_blank" $1', $str); 

    // strip out comments
    if(strpos($str, '/*') !== false) $str = preg_replace('{/\*.*?\*/}s', '', $str); 
    if(strpos($str, '//') !== false) $str = preg_replace('{^//.*$}m', '', $str); 
  }

  /**
   * @param InputfieldWrapper $inputfields
   *
   */
  public function getModuleConfigInputfields(InputfieldWrapper $inputfields) {
    $f = $inputfields->InputfieldRadios;
    $f->attr('name', 'flavor');
    $f->label = $this->_('Markdown flavor to use');
    $f->addOption(self::flavorParsedownExtra, 'Parsedown Extra');
    $f->addOption(self::flavorParsedown, 'Parsedown'); 
    $f->attr('value', (int) $this->flavor);
    $inputfields->add($f);
    
    $f = $inputfields->InputfieldToggle;
    $f->attr('name', 'safeMode'); 
    $f->label = $this->_('Safe mode?'); 
    $f->description = $this->_('Enable this to Markdown that it is processing untrusted user-input.') . ' ' . 
      "[" . $this->_('Details') . "](https://github.com/erusev/parsedown#security)";
    $f->val((int) $this->safeMode);
    $inputfields->add($f);
    
    $f = $inputfields->InputfieldTextarea;
    $f->attr('name', '_test_markdown'); 
    $f->label = $this->_('Test markdown'); 
    $f->description = $this->_('Enter some markdown and click submit to test the result with your current settings.'); 
    $f->collapsed = Inputfield::collapsedBlank;
    $f->icon = 'flask';
    $inputfields->add($f);
  
    $session = $this->wire()->session;
    $text = $this->wire()->input->post('_test_markdown');
    if($text) {
      $session->setFor($this, 'text', $text);
    } else {
      $text = $session->getFor($this, 'text');
      $session->removeFor($this, 'text');
      $f->val($text);
      if($text) {
        $markup = $this->markdown($text);
        $this->message("<strong>$f->label results:</strong><br />" . 
          "<pre>" . $this->wire()->sanitizer->entities($markup) . '</pre>', 
          Notice::allowMarkup | Notice::noGroup
        ); 
      }
    }
    
  }
}