diff options
author | Bradley Taunt <bt@btxx.org> | 2024-02-02 13:05:54 -0500 |
---|---|---|
committer | Bradley Taunt <bt@btxx.org> | 2024-02-02 13:05:54 -0500 |
commit | 13cec3d0fc257d0e65c9a1c06bfc71648722a506 (patch) | |
tree | aaf959aa898357abe14c45016a9071ce4d0587c0 /_phpetite/dependencies/ParsedownExtraPlugin.php |
Diffstat (limited to '_phpetite/dependencies/ParsedownExtraPlugin.php')
-rwxr-xr-x | _phpetite/dependencies/ParsedownExtraPlugin.php | 585 |
1 files changed, 585 insertions, 0 deletions
diff --git a/_phpetite/dependencies/ParsedownExtraPlugin.php b/_phpetite/dependencies/ParsedownExtraPlugin.php new file mode 100755 index 0000000..55e9091 --- /dev/null +++ b/_phpetite/dependencies/ParsedownExtraPlugin.php @@ -0,0 +1,585 @@ +<?php + +# +# +# Parsedown Extra Plugin +# https://github.com/tovic/parsedown-extra-plugin +# +# (c) Emanuil Rusev +# http://erusev.com +# +# (c) Taufik Nurrohman +# https://mecha-cms.com +# +# For the full license information, view the LICENSE file that was distributed +# with this source code. +# +# + +class ParsedownExtraPlugin extends ParsedownExtra { + + const version = '1.3.1'; + + + # config + + public $abbreviationData = array(); + + public $blockCodeAttributes = array(); + + public $blockCodeClassFormat = 'language-%s'; + + public $blockCodeHtml = null; + + public $blockQuoteAttributes = array(); + + public $blockQuoteText = null; + + public $codeAttributes = array(); + + public $codeAttributesOnParent = false; + + public $codeHtml = null; + + public $figureAttributes = array(); + + public $figuresEnabled = false; + + public $footnoteAttributes = array(); + + public $footnoteBackLinkAttributes = array(); + + public $footnoteBackLinkHtml = null; + + public $footnoteBackReferenceAttributes = array(); + + public $footnoteLinkAttributes = array(); + + public $footnoteLinkHtml = null; + + public $footnoteReferenceAttributes = array(); + + public $headerAttributes = array(); + + public $headerText = null; + + public $imageAttributes = array(); + + public $imageAttributesOnParent = false; + + public $linkAttributes = array(); + + public $referenceData = array(); + + public $tableAttributes = array(); + + public $tableColumnAttributes = array(); + + public $voidElementSuffix = ' />'; + + # config + + + protected $regexAttribute = '(?:[#.][-\w:\\\]+[ ]*|[-\w:\\\]+(?:=(?:["\'][^\n]*?["\']|[^\s]+)?)?[ ]*)'; + + # Method aliases for every configuration property + public function __call($key, array $arguments = array()) { + $property = lcfirst(substr($key, 3)); + if (strpos($key, 'set') === 0 && property_exists($this, $property)) { + $this->{$property} = $arguments[0]; + return $this; + } + throw new Exception('Method ' . $key . ' does not exists.'); + } + + public function __construct() { + if (version_compare(parent::version, '0.8.0-beta-1') < 0) { + throw new Exception('ParsedownExtraPlugin requires a later version of Parsedown'); + } + $this->BlockTypes['!'][] = 'Image'; + parent::__construct(); + } + + protected function blockAbbreviation($Line) { + // Allow empty abbreviations + if (preg_match('/^\*\[(.+?)\]:[ ]*$/', $Line['text'], $matches)) { + $this->DefinitionData['Abbreviation'][$matches[1]] = null; + return array('hidden' => true); + } + return parent::blockAbbreviation($Line); + } + + protected function blockCodeComplete($Block) { + $this->doSetAttributes($Block['element']['element'], $this->blockCodeAttributes); + $this->doSetContent($Block['element']['element'], $this->blockCodeHtml, true); + // Put code attributes on parent element + if ($this->codeAttributesOnParent) { + if ($this->codeAttributesOnParent === true) { + // $this->codeAttributesOnParent = array_keys($Block['element']['element']['attributes']); + $this->codeAttributesOnParent = array('class', 'id'); + } + foreach ((array) $this->codeAttributesOnParent as $Name) { + if (isset($Block['element']['element']['attributes'][$Name])) { + $Block['element']['attributes'][$Name] = $Block['element']['element']['attributes'][$Name]; + unset($Block['element']['element']['attributes'][$Name]); + } + } + } + $Block['element']['element']['rawHtml'] = $Block['element']['element']['text']; + $Block['element']['element']['allowRawHtmlInSafeMode'] = true; + unset($Block['element']['element']['text']); + return $Block; + } + + protected function blockFencedCode($Line) { + // Re-enable the multiple class name feature + $Line['text'] = strtr(trim($Line['text']), array( + ' ' => "\x1A", + '.' => "\x1A." + )); + // Enable custom attribute syntax on code block + $Attributes = array(); + if (strpos($Line['text'], '{') !== false && substr($Line['text'], -1) === '}') { + $Parts = explode('{', $Line['text'], 2); + $Attributes = $this->parseAttributeData(strtr(substr($Parts[1], 0, -1), "\x1A", ' ')); + $Line['text'] = trim($Parts[0]); + } + if (!$Block = parent::blockFencedCode($Line)) { + return; + } + if ($Attributes) { + $Block['element']['element']['attributes'] = $Attributes; + } else if (isset($Block['element']['element']['attributes']['class'])) { + $Classes = explode("\x1A", strtr($Block['element']['element']['attributes']['class'], ' ', "\x1A")); + // `~~~ php` → `<pre><code class="language-php">` + // `~~~ php html` → `<pre><code class="language-php language-html">` + // `~~~ .php` → `<pre><code class="php">` + // `~~~ .php.html` → `<pre><code class="php html">` + // `~~~ .php html` → `<pre><code class="php language-html">` + // `~~~ {.php #foo}` → `<pre><code id="foo" class="php">` + $Results = []; + foreach ($Classes as $Class) { + if ($Class === "" || $Class === str_replace('%s', "", $this->blockCodeClassFormat)) { + continue; + } + if ($Class[0] === '.') { + $Results[] = substr($Class, 1); + } else { + $Results[] = sprintf($this->blockCodeClassFormat, $Class); + } + } + $Block['element']['element']['attributes']['class'] = implode(' ', array_unique($Results)); + } + return $Block; + } + + protected function blockFencedCodeComplete($Block) { + return $this->blockCodeComplete($Block); + } + + protected function blockHeader($Line) { + if (!$Block = parent::blockHeader($Line)) { + return; + } + $Level = strspn($Line['text'], '#'); + $this->doSetAttributes($Block['element'], $this->headerAttributes, array($Level)); + $this->doSetContent($Block['element'], $this->headerText, false, 'argument', array($Level)); + return $Block; + } + + protected function blockImage($Line) { + if (!$this->figuresEnabled) { + return; + } + // Match exactly an image syntax in a paragraph (with optional custom attributes, and optional hard break marker) + if (preg_match('/^\!\[[^\n]*?\](\[[^\n]*?\]|\([^\n]*?\))(\s*\{' . $this->regexAttribute . '+?\})?([ ]{2})?$/', $Line['text'])) { + $Block = array( + 'description' => "", + 'element' => array( + 'name' => 'figure', + 'attributes' => array(), + 'elements' => array( + $this->inlineImage($Line) + ) + ) + ); + $this->doSetAttributes($Block['element'], $this->figureAttributes); + return $Block; + } + return; + } + + protected function blockImageComplete($Block) { + if (!empty($Block['description'])) { + $Description = $Block['description']; + $Block['element']['elements'][] = array( + 'name' => 'figcaption', + 'rawHtml' => $this->{strpos($Description, "\n\n") === false ? 'line' : 'text'}(trim($Description, "\n")) + ); + // unset($Block['description']); + } + if ($this->imageAttributesOnParent) { + $Inline = $Block['element']['elements'][0]; + if ($this->imageAttributesOnParent === true) { + $this->imageAttributesOnParent = array_keys($Inline['element']['attributes']); + } + foreach ((array) $this->imageAttributesOnParent as $Name) { + if (isset($Inline['element']['attributes'][$Name])) { + // Merge class names + if ( + $Name === 'class' && + isset($Block['element']['attributes'][$Name]) && + isset($Inline['element']['attributes'][$Name]) + ) { + $Classes = array_merge( + explode(' ', $Block['element']['attributes'][$Name]), + explode(' ', $Inline['element']['attributes'][$Name]) + ); + sort($Classes); + $Block['element']['attributes']['class'] = implode(' ', array_unique(array_filter($Classes))); + unset($Block['element']['elements'][0]['element']['attributes'][$Name]); + continue; + } + $Block['element']['attributes'][$Name] = $Inline['element']['attributes'][$Name]; + unset($Block['element']['elements'][0]['element']['attributes'][$Name]); + } + } + } + return $Block; + } + + protected function blockImageContinue($Line, array $Block) { + if (isset($Block['complete'])) { + return; + } + if (isset($Block['interrupted'])) { + $Block['description'] .= "\n"; + unset($Block['interrupted']); + } + if ($Line['indent'] === 0) { + $Block['complete'] = true; + return; + } + if ($Line['indent'] > 0 && $Line['indent'] < 4) { + $Block['description'] .= "\n" . $Line['text']; + return $Block; + } + return; + } + + protected function blockQuoteComplete($Block) { + $this->doSetAttributes($Block['element'], $this->blockQuoteAttributes); + $this->doSetContent($Block['element'], $this->blockQuoteText, false, 'arguments'); + return $Block; + } + + protected function blockSetextHeader($Line, array $Block = null) { + if (!$Block = parent::blockSetextHeader($Line, $Block)) { + return; + } + $Level = $Line['text'][0] === '=' ? 1 : 2; + $this->doSetAttributes($Block['element'], $this->headerAttributes, array($Level)); + $this->doSetContent($Block['element'], $this->headerText, false, 'argument', array($Level)); + return $Block; + } + + protected function blockTableContinue($Line, array $Block) { + if (!$Block = parent::blockTableContinue($Line, $Block)) { + return; + } + $Aligns = $Block['alignments']; + // `<thead>` or `<tbody>` + foreach ($Block['element']['elements'] as $Index0 => &$Element0) { + // `<tr>` + foreach ($Element0['elements'] as $Index1 => &$Element1) { + // `<th>` or `<td>` + foreach ($Element1['elements'] as $Index2 => &$Element2) { + $this->doSetAttributes($Element2, $this->tableColumnAttributes, array($Aligns[$Index2], $Index2, $Index1)); + } + } + } + return $Block; + } + + protected function blockTableComplete($Block) { + $this->doSetAttributes($Block['element'], $this->tableAttributes); + return $Block; + } + + protected function buildFootnoteElement() { + $DefinitionData = $this->DefinitionData['Footnote']; + if (!$Footnotes = parent::buildFootnoteElement()) { + return; + } + $DefinitionKey = array_keys($DefinitionData); + $DefinitionData = array_values($DefinitionData); + $this->doSetAttributes($Footnotes, $this->footnoteAttributes); + foreach ($Footnotes['elements'][1]['elements'] as $Index0 => &$Element0) { + $Name = $DefinitionKey[$Index0]; + $Count = $DefinitionData[$Index0]['count']; + $Args = array(is_numeric($Name) ? (float) $Name : $Name, $Count); + $this->doSetAttributes($Element0, $this->footnoteBackReferenceAttributes, $Args); + foreach ($Element0['elements'] as $Index1 => &$Element1) { + $Count = 0; + foreach ($Element1['elements'] as $Index2 => &$Element2) { + if (!isset($Element2['name']) || $Element2['name'] !== 'a') { + continue; + } + $Args[1] = ++$Count; + $this->doSetAttributes($Element2, $this->footnoteBackLinkAttributes, $Args); + $this->doSetContent($Element2, $this->footnoteBackLinkHtml, false, 'rawHtml'); + } + } + } + return $Footnotes; + } + + protected function doGetAttributes($Element) { + if (isset($Element['attributes'])) { + return (array) $Element['attributes']; + } + return array(); + } + + protected function doGetContent($Element) { + if (isset($Element['text'])) { + return $Element['text']; + } + if (isset($Element['rawHtml'])) { + return $Element['rawHtml']; + } + if (isset($Element['handler']['argument'])) { + return implode("\n", (array) $Element['handler']['argument']); + } + return null; + } + + private function doSetLink($Excerpt, $Function) { + if (!$Inline = parent::$Function($Excerpt)) { + return; + } + $this->doSetAttributes($Inline['element'], $this->linkAttributes, array($this->isLocal($Inline['element'], 'href'))); + $this->doSetData($this->DefinitionData['Reference'], $this->referenceData); + return $Inline; + } + + protected function doSetAttributes(&$Element, $From, $Args = array()) { + $Attributes = $this->doGetAttributes($Element); + $Content = $this->doGetContent($Element); + if (is_callable($From)) { + $Args = array_merge(array($Content, $Attributes, &$Element), $Args); + $Element['attributes'] = array_replace($Attributes, (array) call_user_func_array($From, $Args)); + } else { + $Element['attributes'] = array_replace($Attributes, (array) $From); + } + } + + protected function doSetContent(&$Element, $From, $Esc = false, $Mode = 'text', $Args = array()) { + $Attributes = $this->doGetAttributes($Element); + $Content = $this->doGetContent($Element); + if ($Esc) { + $Content = parent::escape($Content, true); + } + if (is_callable($From)) { + $Args = array_merge(array($Content, $Attributes, &$Element), $Args); + $Content = call_user_func_array($From, $Args); + } else if (!empty($From)) { + $Content = sprintf($From, $Content); + } + if ($Mode === 'arguments') { + $Element['handler']['argument'] = explode("\n", $Content); + } else if ($Mode === 'argument') { + $Element['handler']['argument'] = $Content; + } else { + $Element[$Mode] = $Content; + } + } + + protected function doSetData(&$To, $From) { + $To = array_replace((array) $To, (array) $From); + } + + protected function element(array $Element) { + if (!$Any = parent::element($Element)) { + return; + } + if (substr($Any, -3) === ' />') { + if (is_callable($this->voidElementSuffix)) { + $Attributes = $this->doGetAttributes($Element); + $Content = $this->doGetContent($Element); + $Suffix = call_user_func_array($this->voidElementSuffix, [$Content, $Attributes, &$Element]); + } else { + $Suffix = $this->voidElementSuffix; + } + $Any = substr_replace($Any, $Suffix, -3); + } + return $Any; + } + + protected function inlineCode($Excerpt) { + if (!$Inline = parent::inlineCode($Excerpt)) { + return; + } + $this->doSetAttributes($Inline['element'], $this->codeAttributes); + $this->doSetContent($Inline['element'], $this->codeHtml, true); + $Inline['element']['rawHtml'] = $Inline['element']['text']; + $Inline['element']['allowRawHtmlInSafeMode'] = true; + unset($Inline['element']['text']); + return $Inline; + } + + protected function inlineFootnoteMarker($Excerpt) { + if (!$Inline = parent::inlineFootnoteMarker($Excerpt)) { + return; + } + $Name = null; + if (preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches)) { + $Name = $matches[1]; + } + $Args = array(is_numeric($Name) ? (float) $Name : $Name, $this->DefinitionData['Footnote'][$Name]['count']); + $this->doSetAttributes($Inline['element'], $this->footnoteReferenceAttributes, $Args); + $this->doSetAttributes($Inline['element']['element'], $this->footnoteLinkAttributes, $Args); + $this->doSetContent($Inline['element']['element'], $this->footnoteLinkHtml, false, 'text', $Args); + $Inline['element']['element']['rawHtml'] = $Inline['element']['element']['text']; + $Inline['element']['element']['allowRawHtmlInSafeMode'] = true; + unset($Inline['element']['element']['text']); + return $Inline; + } + + protected function inlineImage($Excerpt) { + if (!$Inline = parent::inlineImage($Excerpt)) { + return; + } + $this->doSetAttributes($Inline['element'], $this->imageAttributes, array($this->isLocal($Inline['element'], 'src'))); + return $Inline; + } + + protected function inlineLink($Excerpt) { + return $this->doSetLink($Excerpt, __FUNCTION__); + } + + protected function inlineText($Text) { + $this->doSetData($this->DefinitionData['Abbreviation'], $this->abbreviationData); + return parent::inlineText($Text); + } + + protected function inlineUrl($Excerpt) { + return $this->doSetLink($Excerpt, __FUNCTION__); + } + + protected function inlineUrlTag($Excerpt) { + return $this->doSetLink($Excerpt, __FUNCTION__); + } + + protected function isLocal($Element, $Key) { + $Link = isset($Element['attributes'][$Key]) ? (string) $Element['attributes'][$Key] : null; + if ( + // `<a href="">` + $Link === "" || + // `<a href="../foo/bar">` + // `<a href="/foo/bar">` + // `<a href="?foo=bar">` + // `<a href="&foo=bar">` + // `<a href="#foo">` + strpos('./?&#', $Link[0]) !== false && strpos($Link, '//') !== 0 || + // `<a href="data:text/html,asdf">` + strpos($Link, 'data:') === 0 || + // `<a href="javascript:;">` + strpos($Link, 'javascript:') === 0 || + // `<a href="mailto:as@df">` + strpos($Link, 'mailto:') === 0 + ) { + return true; + } + if (isset($_SERVER['HTTP_HOST'])) { + $Host = $_SERVER['HTTP_HOST']; + } else if (isset($_SERVER['SERVER_NAME'])) { + $Host = $_SERVER['SERVER_NAME']; + } else { + $Host = ""; + } + // `<a href="//example.com">` + if (strpos($Link, '//') === 0 && strpos($Link, '//' . $Host) !== 0) { + return false; + } + if ( + // `<a href="https://127.0.0.1">` + strpos($Link, 'https://' . $Host) === 0 || + // `<a href="http://127.0.0.1">` + strpos($Link, 'http://' . $Host) === 0 + ) { + return true; + } + // `<a href="foo/bar">` + return strpos($Link, '://') === false; + } + + protected function parseAttributeData($attributeString) { + // Allow compact attributes + $attributeString = strtr($attributeString, array( + '#' => ' #', + '.' => ' .' + )); + if (strpos($attributeString, '="') !== false || strpos($attributeString, "='") !== false) { + $attributeString = preg_replace_callback('#([-\w]+=)(["\'])([^\n]*?)\2#', function($matches) { + $value = strtr($matches[3], array( + ' #' => '#', + ' .' => '.', + ' ' => "\x1A" + )); + return $matches[1] . $matches[2] . $value . $matches[2]; + }, $attributeString); + } + $Attributes = array(); + foreach (explode(' ', $attributeString) as $v) { + if (!$v) { + continue; + } + // `{#foo}` + if ($v[0] === '#' && isset($v[1])) { + $Attributes['id'] = substr($v, 1); + // `{.foo}` + } else if ($v[0] === '.' && isset($v[1])) { + $Attributes['class'][] = substr($v, 1); + // ~ + } else if (strpos($v, '=') !== false) { + $vv = explode('=', $v, 2); + // `{foo=}` + if ($vv[1] === "") { + if ($vv[0] === 'class') { + continue; + } + $Attributes[$vv[0]] = ""; + // `{foo="bar baz"}` + // `{foo='bar baz'}` + } else if ($vv[1][0] === '"' && substr($vv[1], -1) === '"' || $vv[1][0] === "'" && substr($vv[1], -1) === "'") { + $values = stripslashes(strtr(substr(substr($vv[1], 1), 0, -1), "\x1A", ' ')); + if ($vv[0] === 'class' && isset($Attributes[$vv[0]])) { + $values = explode(' ', $values); + $Attributes[$vv[0]] = array_merge($Attributes[$vv[0]], $values); + } else { + $Attributes[$vv[0]] = $values; + } + // `{foo=bar}` + } else { + if ($vv[0] === 'class' && isset($Attributes[$vv[0]])) { + $Attributes[$vv[0]] = array_merge($Attributes[$vv[0]], [$vv[1]]); + } else { + $Attributes[$vv[0]] = $vv[1]; + } + } + // `{foo}` + } else { + if ($v === 'class' && isset($Attributes[$v])) { + continue; + } + $Attributes[$v] = $v; + } + } + if (isset($Attributes['class'])) { + $Attributes['class'] = implode(' ', array_unique((array) $Attributes['class'])); + } + return $Attributes; + } + +} |