123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688 |
- <?php
- #
- #
- # Parsedown Extra
- # https://github.com/erusev/parsedown-extra
- #
- # (c) Emanuil Rusev
- # http://erusev.com
- #
- # For the full license information, view the LICENSE file that was distributed
- # with this source code.
- #
- #
- class ParsedownExtra extends Parsedown
- {
- # ~
- const version = '0.8.0-beta-1';
- # ~
- function __construct()
- {
- if (version_compare(parent::version, '1.7.1') < 0)
- {
- throw new Exception('ParsedownExtra requires a later version of Parsedown');
- }
- $this->BlockTypes[':'] []= 'DefinitionList';
- $this->BlockTypes['*'] []= 'Abbreviation';
- # identify footnote definitions before reference definitions
- array_unshift($this->BlockTypes['['], 'Footnote');
- # identify footnote markers before before links
- array_unshift($this->InlineTypes['['], 'FootnoteMarker');
- }
- #
- # ~
- function text($text)
- {
- $Elements = $this->textElements($text);
- # convert to markup
- $markup = $this->elements($Elements);
- # trim line breaks
- $markup = trim($markup, "\n");
- # merge consecutive dl elements
- $markup = preg_replace('/<\/dl>\s+<dl>\s+/', '', $markup);
- # add footnotes
- if (isset($this->DefinitionData['Footnote']))
- {
- $Element = $this->buildFootnoteElement();
- $markup .= "\n" . $this->element($Element);
- }
- return $markup;
- }
- #
- # Blocks
- #
- #
- # Abbreviation
- protected function blockAbbreviation($Line)
- {
- if (preg_match('/^\*\[(.+?)\]:[ ]*(.+?)[ ]*$/', $Line['text'], $matches))
- {
- $this->DefinitionData['Abbreviation'][$matches[1]] = $matches[2];
- $Block = array(
- 'hidden' => true,
- );
- return $Block;
- }
- }
- #
- # Footnote
- protected function blockFootnote($Line)
- {
- if (preg_match('/^\[\^(.+?)\]:[ ]?(.*)$/', $Line['text'], $matches))
- {
- $Block = array(
- 'label' => $matches[1],
- 'text' => $matches[2],
- 'hidden' => true,
- );
- return $Block;
- }
- }
- protected function blockFootnoteContinue($Line, $Block)
- {
- if ($Line['text'][0] === '[' and preg_match('/^\[\^(.+?)\]:/', $Line['text']))
- {
- return;
- }
- if (isset($Block['interrupted']))
- {
- if ($Line['indent'] >= 4)
- {
- $Block['text'] .= "\n\n" . $Line['text'];
- return $Block;
- }
- }
- else
- {
- $Block['text'] .= "\n" . $Line['text'];
- return $Block;
- }
- }
- protected function blockFootnoteComplete($Block)
- {
- $this->DefinitionData['Footnote'][$Block['label']] = array(
- 'text' => $Block['text'],
- 'count' => null,
- 'number' => null,
- );
- return $Block;
- }
- #
- # Definition List
- protected function blockDefinitionList($Line, $Block)
- {
- if ( ! isset($Block) or $Block['type'] !== 'Paragraph')
- {
- return;
- }
- $Element = array(
- 'name' => 'dl',
- 'elements' => array(),
- );
- $terms = explode("\n", $Block['element']['handler']['argument']);
- foreach ($terms as $term)
- {
- $Element['elements'] []= array(
- 'name' => 'dt',
- 'handler' => array(
- 'function' => 'lineElements',
- 'argument' => $term,
- 'destination' => 'elements'
- ),
- );
- }
- $Block['element'] = $Element;
- $Block = $this->addDdElement($Line, $Block);
- return $Block;
- }
- protected function blockDefinitionListContinue($Line, array $Block)
- {
- if ($Line['text'][0] === ':')
- {
- $Block = $this->addDdElement($Line, $Block);
- return $Block;
- }
- else
- {
- if (isset($Block['interrupted']) and $Line['indent'] === 0)
- {
- return;
- }
- if (isset($Block['interrupted']))
- {
- $Block['dd']['handler']['function'] = 'textElements';
- $Block['dd']['handler']['argument'] .= "\n\n";
- $Block['dd']['handler']['destination'] = 'elements';
- unset($Block['interrupted']);
- }
- $text = substr($Line['body'], min($Line['indent'], 4));
- $Block['dd']['handler']['argument'] .= "\n" . $text;
- return $Block;
- }
- }
- #
- # Header
- protected function blockHeader($Line)
- {
- $Block = parent::blockHeader($Line);
- if (preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE))
- {
- $attributeString = $matches[1][0];
- $Block['element']['attributes'] = $this->parseAttributeData($attributeString);
- $Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]);
- }
- return $Block;
- }
- #
- # Markup
- protected function blockMarkup($Line)
- {
- if ($this->markupEscaped or $this->safeMode)
- {
- return;
- }
- if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
- {
- $element = strtolower($matches[1]);
- if (in_array($element, $this->textLevelElements))
- {
- return;
- }
- $Block = array(
- 'name' => $matches[1],
- 'depth' => 0,
- 'element' => array(
- 'rawHtml' => $Line['text'],
- 'autobreak' => true,
- ),
- );
- $length = strlen($matches[0]);
- $remainder = substr($Line['text'], $length);
- if (trim($remainder) === '')
- {
- if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
- {
- $Block['closed'] = true;
- $Block['void'] = true;
- }
- }
- else
- {
- if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
- {
- return;
- }
- if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
- {
- $Block['closed'] = true;
- }
- }
- return $Block;
- }
- }
- protected function blockMarkupContinue($Line, array $Block)
- {
- if (isset($Block['closed']))
- {
- return;
- }
- if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
- {
- $Block['depth'] ++;
- }
- if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
- {
- if ($Block['depth'] > 0)
- {
- $Block['depth'] --;
- }
- else
- {
- $Block['closed'] = true;
- }
- }
- if (isset($Block['interrupted']))
- {
- $Block['element']['rawHtml'] .= "\n";
- unset($Block['interrupted']);
- }
- $Block['element']['rawHtml'] .= "\n".$Line['body'];
- return $Block;
- }
- protected function blockMarkupComplete($Block)
- {
- if ( ! isset($Block['void']))
- {
- $Block['element']['rawHtml'] = $this->processTag($Block['element']['rawHtml']);
- }
- return $Block;
- }
- #
- # Setext
- protected function blockSetextHeader($Line, array $Block = null)
- {
- $Block = parent::blockSetextHeader($Line, $Block);
-
- //Yiming: prevent error
- if(!$Block) return NULL;
- if (preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE))
- {
- $attributeString = $matches[1][0];
- $Block['element']['attributes'] = $this->parseAttributeData($attributeString);
- $Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]);
- }
- return $Block;
- }
- #
- # Inline Elements
- #
- #
- # Footnote Marker
- protected function inlineFootnoteMarker($Excerpt)
- {
- if (preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches))
- {
- $name = $matches[1];
- if ( ! isset($this->DefinitionData['Footnote'][$name]))
- {
- return;
- }
- $this->DefinitionData['Footnote'][$name]['count'] ++;
- if ( ! isset($this->DefinitionData['Footnote'][$name]['number']))
- {
- $this->DefinitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount; # » &
- }
- $Element = array(
- 'name' => 'sup',
- 'attributes' => array('id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name),
- 'element' => array(
- 'name' => 'a',
- 'attributes' => array('href' => '#fn:'.$name, 'class' => 'footnote-ref'),
- 'text' => $this->DefinitionData['Footnote'][$name]['number'],
- ),
- );
- return array(
- 'extent' => strlen($matches[0]),
- 'element' => $Element,
- );
- }
- }
- private $footnoteCount = 0;
- #
- # Link
- protected function inlineLink($Excerpt)
- {
- $Link = parent::inlineLink($Excerpt);
- $remainder = substr($Excerpt['text'], $Link['extent']);
- if (preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches))
- {
- $Link['element']['attributes'] += $this->parseAttributeData($matches[1]);
- $Link['extent'] += strlen($matches[0]);
- }
- return $Link;
- }
- #
- # ~
- #
- private $currentAbreviation;
- private $currentMeaning;
- protected function insertAbreviation(array $Element)
- {
- if (isset($Element['text']))
- {
- $Element['elements'] = self::pregReplaceElements(
- '/\b'.preg_quote($this->currentAbreviation, '/').'\b/',
- array(
- array(
- 'name' => 'abbr',
- 'attributes' => array(
- 'title' => $this->currentMeaning,
- ),
- 'text' => $this->currentAbreviation,
- )
- ),
- $Element['text']
- );
- unset($Element['text']);
- }
- return $Element;
- }
- protected function inlineText($text)
- {
- $Inline = parent::inlineText($text);
- if (isset($this->DefinitionData['Abbreviation']))
- {
- foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning)
- {
- $this->currentAbreviation = $abbreviation;
- $this->currentMeaning = $meaning;
- $Inline['element'] = $this->elementApplyRecursiveDepthFirst(
- array($this, 'insertAbreviation'),
- $Inline['element']
- );
- }
- }
- return $Inline;
- }
- #
- # Util Methods
- #
- protected function addDdElement(array $Line, array $Block)
- {
- $text = substr($Line['text'], 1);
- $text = trim($text);
- unset($Block['dd']);
- $Block['dd'] = array(
- 'name' => 'dd',
- 'handler' => array(
- 'function' => 'lineElements',
- 'argument' => $text,
- 'destination' => 'elements'
- ),
- );
- if (isset($Block['interrupted']))
- {
- $Block['dd']['handler']['function'] = 'textElements';
- unset($Block['interrupted']);
- }
- $Block['element']['elements'] []= & $Block['dd'];
- return $Block;
- }
- protected function buildFootnoteElement()
- {
- $Element = array(
- 'name' => 'div',
- 'attributes' => array('class' => 'footnotes'),
- 'elements' => array(
- array('name' => 'hr'),
- array(
- 'name' => 'ol',
- 'elements' => array(),
- ),
- ),
- );
- uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes');
- foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData)
- {
- if ( ! isset($DefinitionData['number']))
- {
- continue;
- }
- $text = $DefinitionData['text'];
- $textElements = parent::textElements($text);
- $numbers = range(1, $DefinitionData['count']);
- $backLinkElements = array();
- foreach ($numbers as $number)
- {
- $backLinkElements[] = array('text' => ' ');
- $backLinkElements[] = array(
- 'name' => 'a',
- 'attributes' => array(
- 'href' => "#fnref$number:$definitionId",
- 'rev' => 'footnote',
- 'class' => 'footnote-backref',
- ),
- 'rawHtml' => '↩',
- 'allowRawHtmlInSafeMode' => true,
- 'autobreak' => false,
- );
- }
- unset($backLinkElements[0]);
- $n = count($textElements) -1;
- if ($textElements[$n]['name'] === 'p')
- {
- $backLinkElements = array_merge(
- array(
- array(
- 'rawHtml' => ' ',
- 'allowRawHtmlInSafeMode' => true,
- ),
- ),
- $backLinkElements
- );
- unset($textElements[$n]['name']);
- $textElements[$n] = array(
- 'name' => 'p',
- 'elements' => array_merge(
- array($textElements[$n]),
- $backLinkElements
- ),
- );
- }
- else
- {
- $textElements[] = array(
- 'name' => 'p',
- 'elements' => $backLinkElements
- );
- }
- $Element['elements'][1]['elements'] []= array(
- 'name' => 'li',
- 'attributes' => array('id' => 'fn:'.$definitionId),
- 'elements' => array_merge(
- $textElements
- ),
- );
- }
- return $Element;
- }
- # ~
- protected function parseAttributeData($attributeString)
- {
- $Data = array();
- $attributes = preg_split('/[ ]+/', $attributeString, - 1, PREG_SPLIT_NO_EMPTY);
- foreach ($attributes as $attribute)
- {
- if ($attribute[0] === '#')
- {
- $Data['id'] = substr($attribute, 1);
- }
- else # "."
- {
- $classes []= substr($attribute, 1);
- }
- }
- if (isset($classes))
- {
- $Data['class'] = implode(' ', $classes);
- }
- return $Data;
- }
- # ~
- protected function processTag($elementMarkup) # recursive
- {
- # http://stackoverflow.com/q/1148928/200145
- libxml_use_internal_errors(true);
- $DOMDocument = new DOMDocument;
- # http://stackoverflow.com/q/11309194/200145
- $elementMarkup = mb_convert_encoding($elementMarkup, 'HTML-ENTITIES', 'UTF-8');
- # http://stackoverflow.com/q/4879946/200145
- $DOMDocument->loadHTML($elementMarkup);
- $DOMDocument->removeChild($DOMDocument->doctype);
- $DOMDocument->replaceChild($DOMDocument->firstChild->firstChild->firstChild, $DOMDocument->firstChild);
- $elementText = '';
- if ($DOMDocument->documentElement->getAttribute('markdown') === '1')
- {
- foreach ($DOMDocument->documentElement->childNodes as $Node)
- {
- $elementText .= $DOMDocument->saveHTML($Node);
- }
- $DOMDocument->documentElement->removeAttribute('markdown');
- $elementText = "\n".$this->text($elementText)."\n";
- }
- else
- {
- foreach ($DOMDocument->documentElement->childNodes as $Node)
- {
- $nodeMarkup = $DOMDocument->saveHTML($Node);
- if ($Node instanceof DOMElement and ! in_array($Node->nodeName, $this->textLevelElements))
- {
- $elementText .= $this->processTag($nodeMarkup);
- }
- else
- {
- $elementText .= $nodeMarkup;
- }
- }
- }
- # because we don't want for markup to get encoded
- $DOMDocument->documentElement->nodeValue = 'placeholder\x1A';
- $markup = $DOMDocument->saveHTML($DOMDocument->documentElement);
- $markup = str_replace('placeholder\x1A', $elementText, $markup);
- return $markup;
- }
- # ~
- protected function sortFootnotes($A, $B) # callback
- {
- return $A['number'] - $B['number'];
- }
- #
- # Fields
- #
- protected $regexAttribute = '(?:[#.][-\w]+[ ]*)';
- }
|