diff --git a/lib/parsedown/LICENSE.txt b/lib/parsedownextraplugin/LICENSE.txt similarity index 100% rename from lib/parsedown/LICENSE.txt rename to lib/parsedownextraplugin/LICENSE.txt diff --git a/lib/parsedown/parsedown.class.php b/lib/parsedownextraplugin/parsedown.php similarity index 100% rename from lib/parsedown/parsedown.class.php rename to lib/parsedownextraplugin/parsedown.php diff --git a/lib/parsedownextraplugin/parsedownextra.php b/lib/parsedownextraplugin/parsedownextra.php new file mode 100644 index 0000000000..2eeeda6e0b --- /dev/null +++ b/lib/parsedownextraplugin/parsedownextra.php @@ -0,0 +1,686 @@ +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+
`
+ // `~~~ php html` → ``
+ // `~~~ .php` → ``
+ // `~~~ .php.html` → ``
+ // `~~~ .php html` → ``
+ // `~~~ {.php #foo}` → ``
+ $Results = [];
+ foreach ($Classes as $Class) {
+ if ($Class === "" || $Class === strtr($this->blockCodeClassFormat, array('%s' => ""))) {
+ continue;
+ }
+ if ($Class[0] === '.') {
+ $Results[] = substr($Class, 1);
+ } else if (preg_match('/^' . strtr(preg_quote($this->blockCodeClassFormat), array('%s' => '\S+')) . '$/', $Class)) {
+ $Results[] = $Class; // Do nothing!
+ } else {
+ $Results[] = sprintf($this->blockCodeClassFormat, $Class);
+ }
+ }
+ if ($Results = array_unique($Results)) {
+ $Block['element']['element']['attributes']['class'] = implode(' ', $Results);
+ } else {
+ unset($Block['element']['element']['attributes']['class']);
+ }
+ }
+ 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'];
+ // `` or ``
+ foreach ($Block['element']['elements'] as $Index0 => &$Element0) {
+ // ``
+ foreach ($Element0['elements'] as $Index1 => &$Element1) {
+ // `` or ` `
+ 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) {
+ if (!isset($Element1['elements'])) {
+ continue;
+ }
+ $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 = call_user_func('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 (
+ // ``
+ $Link === "" ||
+ // ``
+ // ``
+ // ``
+ // ``
+ // ``
+ strpos('./?', $Link[0]) !== false && strpos($Link, '//') !== 0 ||
+ // ``
+ strpos($Link, 'data:') === 0 ||
+ // ``
+ strpos($Link, 'javascript:') === 0 ||
+ // ``
+ 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 = "";
+ }
+ // ``
+ if (strpos($Link, '//') === 0 && strpos($Link, '//' . $Host) !== 0) {
+ return false;
+ }
+ if (
+ // ``
+ strpos($Link, 'https://' . $Host) === 0 ||
+ // ``
+ strpos($Link, 'http://' . $Host) === 0
+ ) {
+ return true;
+ }
+ // ``
+ 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;
+ }
+
+}
diff --git a/module/common/model.php b/module/common/model.php
index 4954c27ade..d95a7dc862 100644
--- a/module/common/model.php
+++ b/module/common/model.php
@@ -3056,8 +3056,13 @@ EOD;
if(empty($markdown)) return false;
global $app;
- $app->loadClass('htmlup');
- return new htmlup($markdown);
+ $app->loadClass('parsedownextraplugin');
+
+ $Parsedown = new parsedownextraplugin;
+
+ $Parsedown->voidElementSuffix = '>'; // HTML5
+
+ return $Parsedown->text($markdown);
}
}