diff --git a/include/LICENSE.PARSEDOWN b/include/LICENSE.PARSEDOWN
index b5a9d32..e6253d1 100644
--- a/include/LICENSE.PARSEDOWN
+++ b/include/LICENSE.PARSEDOWN
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2013 Emanuil Rusev, erusev.com
+Copyright (c) 2013-2018 Emanuil Rusev, erusev.com
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
@@ -17,4 +17,4 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/include/Parsedown.php b/include/Parsedown.php
index 7e34540..926f2a5 100644
--- a/include/Parsedown.php
+++ b/include/Parsedown.php
@@ -8,35 +8,26 @@
# (c) Emanuil Rusev
# http://erusev.com
#
-# For the full license information, view the LICENSE.PARSEDOWN file that was distributed
+# For the full license information, view the LICENSE file that was distributed
# with this source code.
#
#
class Parsedown
{
- #
- # Philosophy
+ # ~
- # Parsedown recognises that the Markdown syntax is optimised for humans so
- # it tries to read like one. It goes through text line by line. It looks at
- # how lines start to identify blocks. It looks for special characters to
- # identify inline elements.
+ const version = '1.7.4';
- #
# ~
function text($text)
{
# make sure no definitions are set
- $this->Definitions = array();
+ $this->DefinitionData = array();
# standardize line breaks
- $text = str_replace("\r\n", "\n", $text);
- $text = str_replace("\r", "\n", $text);
-
- # replace tabs with spaces
- $text = str_replace("\t", ' ', $text);
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
# remove surrounding line breaks
$text = trim($text, "\n");
@@ -57,8 +48,6 @@ class Parsedown
# Setters
#
- private $breaksEnabled;
-
function setBreaksEnabled($breaksEnabled)
{
$this->breaksEnabled = $breaksEnabled;
@@ -66,15 +55,61 @@ class Parsedown
return $this;
}
+ protected $breaksEnabled;
+
+ function setMarkupEscaped($markupEscaped)
+ {
+ $this->markupEscaped = $markupEscaped;
+
+ return $this;
+ }
+
+ protected $markupEscaped;
+
+ function setUrlsLinked($urlsLinked)
+ {
+ $this->urlsLinked = $urlsLinked;
+
+ return $this;
+ }
+
+ protected $urlsLinked = true;
+
+ function setSafeMode($safeMode)
+ {
+ $this->safeMode = (bool) $safeMode;
+
+ return $this;
+ }
+
+ protected $safeMode;
+
+ protected $safeLinksWhitelist = array(
+ 'http://',
+ 'https://',
+ 'ftp://',
+ 'ftps://',
+ 'mailto:',
+ 'data:image/png;base64,',
+ 'data:image/gif;base64,',
+ 'data:image/jpeg;base64,',
+ 'irc:',
+ 'ircs:',
+ 'git:',
+ 'ssh:',
+ 'news:',
+ 'steam:',
+ );
+
#
# Lines
#
protected $BlockTypes = array(
- '#' => array('Atx'),
+ '#' => array('Header'),
'*' => array('Rule', 'List'),
'+' => array('List'),
- '-' => array('Setext', 'Table', 'Rule', 'List'),
+ '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
'0' => array('List'),
'1' => array('List'),
'2' => array('List'),
@@ -87,8 +122,9 @@ class Parsedown
'9' => array('List'),
':' => array('Table'),
'<' => array('Comment', 'Markup'),
- '=' => array('Setext'),
+ '=' => array('SetextHeader'),
'>' => array('Quote'),
+ '[' => array('Reference'),
'_' => array('Rule'),
'`' => array('FencedCode'),
'|' => array('Table'),
@@ -97,21 +133,15 @@ class Parsedown
# ~
- protected $DefinitionTypes = array(
- '[' => array('Reference'),
- );
-
- # ~
-
protected $unmarkedBlockTypes = array(
- 'CodeBlock',
+ 'Code',
);
#
# Blocks
#
- private function lines(array $lines)
+ protected function lines(array $lines)
{
$CurrentBlock = null;
@@ -127,6 +157,23 @@ class Parsedown
continue;
}
+ if (strpos($line, "\t") !== false)
+ {
+ $parts = explode("\t", $line);
+
+ $line = $parts[0];
+
+ unset($parts[0]);
+
+ foreach ($parts as $part)
+ {
+ $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
+
+ $line .= str_repeat(' ', $shortage);
+ $line .= $part;
+ }
+ }
+
$indent = 0;
while (isset($line[$indent]) and $line[$indent] === ' ')
@@ -142,9 +189,9 @@ class Parsedown
# ~
- if (isset($CurrentBlock['incomplete']))
+ if (isset($CurrentBlock['continuable']))
{
- $Block = $this->{'addTo'.$CurrentBlock['type']}($Line, $CurrentBlock);
+ $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);
if (isset($Block))
{
@@ -154,12 +201,10 @@ class Parsedown
}
else
{
- if (method_exists($this, 'complete'.$CurrentBlock['type']))
+ if ($this->isBlockCompletable($CurrentBlock['type']))
{
- $CurrentBlock = $this->{'complete'.$CurrentBlock['type']}($CurrentBlock);
+ $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
}
-
- unset($CurrentBlock['incomplete']);
}
}
@@ -167,21 +212,6 @@ class Parsedown
$marker = $text[0];
- if (isset($this->DefinitionTypes[$marker]))
- {
- foreach ($this->DefinitionTypes[$marker] as $definitionType)
- {
- $Definition = $this->{'identify'.$definitionType}($Line, $CurrentBlock);
-
- if (isset($Definition))
- {
- $this->Definitions[$definitionType][$Definition['id']] = $Definition['data'];
-
- continue 2;
- }
- }
- }
-
# ~
$blockTypes = $this->unmarkedBlockTypes;
@@ -199,7 +229,7 @@ class Parsedown
foreach ($blockTypes as $blockType)
{
- $Block = $this->{'identify'.$blockType}($Line, $CurrentBlock);
+ $Block = $this->{'block'.$blockType}($Line, $CurrentBlock);
if (isset($Block))
{
@@ -207,14 +237,14 @@ class Parsedown
if ( ! isset($Block['identified']))
{
- $Elements []= $CurrentBlock['element'];
+ $Blocks []= $CurrentBlock;
$Block['identified'] = true;
}
- if (method_exists($this, 'addTo'.$blockType))
+ if ($this->isBlockContinuable($blockType))
{
- $Block['incomplete'] = true;
+ $Block['continuable'] = true;
}
$CurrentBlock = $Block;
@@ -231,9 +261,9 @@ class Parsedown
}
else
{
- $Elements []= $CurrentBlock['element'];
+ $Blocks []= $CurrentBlock;
- $CurrentBlock = $this->buildParagraph($Line);
+ $CurrentBlock = $this->paragraph($Line);
$CurrentBlock['identified'] = true;
}
@@ -241,59 +271,59 @@ class Parsedown
# ~
- if (isset($CurrentBlock['incomplete']) and method_exists($this, 'complete'.$CurrentBlock['type']))
+ if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type']))
{
- $CurrentBlock = $this->{'complete'.$CurrentBlock['type']}($CurrentBlock);
+ $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
}
# ~
- $Elements []= $CurrentBlock['element'];
+ $Blocks []= $CurrentBlock;
- unset($Elements[0]);
+ unset($Blocks[0]);
# ~
- $markup = $this->elements($Elements);
+ $markup = '';
+
+ foreach ($Blocks as $Block)
+ {
+ if (isset($Block['hidden']))
+ {
+ continue;
+ }
+
+ $markup .= "\n";
+ $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
+ }
+
+ $markup .= "\n";
# ~
return $markup;
}
- #
- # Atx
-
- protected function identifyAtx($Line)
+ protected function isBlockContinuable($Type)
{
- if (isset($Line['text'][1]))
- {
- $level = 1;
+ return method_exists($this, 'block'.$Type.'Continue');
+ }
- while (isset($Line['text'][$level]) and $Line['text'][$level] === '#')
- {
- $level ++;
- }
-
- $text = trim($Line['text'], '# ');
-
- $Block = array(
- 'element' => array(
- 'name' => 'h'.$level,
- 'text' => $text,
- 'handler' => 'line',
- ),
- );
-
- return $Block;
- }
+ protected function isBlockCompletable($Type)
+ {
+ return method_exists($this, 'block'.$Type.'Complete');
}
#
# Code
- protected function identifyCodeBlock($Line)
+ protected function blockCode($Line, $Block = null)
{
+ if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted']))
+ {
+ return;
+ }
+
if ($Line['indent'] >= 4)
{
$text = substr($Line['body'], 4);
@@ -313,7 +343,7 @@ class Parsedown
}
}
- protected function addToCodeBlock($Line, $Block)
+ protected function blockCodeContinue($Line, $Block)
{
if ($Line['indent'] >= 4)
{
@@ -334,12 +364,10 @@ class Parsedown
}
}
- protected function completeCodeBlock($Block)
+ protected function blockCodeComplete($Block)
{
$text = $Block['element']['text']['text'];
- $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
-
$Block['element']['text']['text'] = $text;
return $Block;
@@ -348,12 +376,17 @@ class Parsedown
#
# Comment
- protected function identifyComment($Line)
+ protected function blockComment($Line)
{
+ if ($this->markupEscaped or $this->safeMode)
+ {
+ return;
+ }
+
if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!')
{
$Block = array(
- 'element' => $Line['body'],
+ 'markup' => $Line['body'],
);
if (preg_match('/-->$/', $Line['text']))
@@ -365,14 +398,14 @@ class Parsedown
}
}
- protected function addToComment($Line, array $Block)
+ protected function blockCommentContinue($Line, array $Block)
{
if (isset($Block['closed']))
{
return;
}
- $Block['element'] .= "\n" . $Line['body'];
+ $Block['markup'] .= "\n" . $Line['body'];
if (preg_match('/-->$/', $Line['text']))
{
@@ -385,18 +418,32 @@ class Parsedown
#
# Fenced Code
- protected function identifyFencedCode($Line)
+ protected function blockFencedCode($Line)
{
- if (preg_match('/^(['.$Line['text'][0].']{3,})[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches))
+ if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([^`]+)?[ ]*$/', $Line['text'], $matches))
{
$Element = array(
'name' => 'code',
'text' => '',
);
- if (isset($matches[2]))
+ if (isset($matches[1]))
{
- $class = 'language-'.$matches[2];
+ /**
+ * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
+ * Every HTML element may have a class attribute specified.
+ * The attribute, if specified, must have a value that is a set
+ * of space-separated tokens representing the various classes
+ * that the element belongs to.
+ * [...]
+ * The space characters, for the purposes of this specification,
+ * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab),
+ * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and
+ * U+000D CARRIAGE RETURN (CR).
+ */
+ $language = substr($matches[1], 0, strcspn($matches[1], " \t\n\f\r"));
+
+ $class = 'language-'.$language;
$Element['attributes'] = array(
'class' => $class,
@@ -416,7 +463,7 @@ class Parsedown
}
}
- protected function addToFencedCode($Line, $Block)
+ protected function blockFencedCodeContinue($Line, $Block)
{
if (isset($Block['complete']))
{
@@ -439,26 +486,57 @@ class Parsedown
return $Block;
}
- $Block['element']['text']['text'] .= "\n".$Line['body'];;
+ $Block['element']['text']['text'] .= "\n".$Line['body'];
return $Block;
}
- protected function completeFencedCode($Block)
+ protected function blockFencedCodeComplete($Block)
{
$text = $Block['element']['text']['text'];
- $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
-
$Block['element']['text']['text'] = $text;
return $Block;
}
+ #
+ # Header
+
+ protected function blockHeader($Line)
+ {
+ if (isset($Line['text'][1]))
+ {
+ $level = 1;
+
+ while (isset($Line['text'][$level]) and $Line['text'][$level] === '#')
+ {
+ $level ++;
+ }
+
+ if ($level > 6)
+ {
+ return;
+ }
+
+ $text = trim($Line['text'], '# ');
+
+ $Block = array(
+ 'element' => array(
+ 'name' => 'h' . min(6, $level),
+ 'text' => $text,
+ 'handler' => 'line',
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
#
# List
- protected function identifyList($Line)
+ protected function blockList($Line)
{
list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');
@@ -473,6 +551,16 @@ class Parsedown
),
);
+ if($name === 'ol')
+ {
+ $listStart = stristr($matches[0], '.', true);
+
+ if($listStart !== '1')
+ {
+ $Block['element']['attributes'] = array('start' => $listStart);
+ }
+ }
+
$Block['li'] = array(
'name' => 'li',
'handler' => 'li',
@@ -487,24 +575,28 @@ class Parsedown
}
}
- protected function addToList($Line, array $Block)
+ protected function blockListContinue($Line, array $Block)
{
- if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'[ ]+(.*)/', $Line['text'], $matches))
+ if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches))
{
if (isset($Block['interrupted']))
{
$Block['li']['text'] []= '';
+ $Block['loose'] = true;
+
unset($Block['interrupted']);
}
unset($Block['li']);
+ $text = isset($matches[1]) ? $matches[1] : '';
+
$Block['li'] = array(
'name' => 'li',
'handler' => 'li',
'text' => array(
- $matches[1],
+ $text,
),
);
@@ -513,6 +605,11 @@ class Parsedown
return $Block;
}
+ if ($Line['text'][0] === '[' and $this->blockReference($Line))
+ {
+ return $Block;
+ }
+
if ( ! isset($Block['interrupted']))
{
$text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
@@ -536,10 +633,26 @@ class Parsedown
}
}
+ protected function blockListComplete(array $Block)
+ {
+ if (isset($Block['loose']))
+ {
+ foreach ($Block['element']['text'] as &$li)
+ {
+ if (end($li['text']) !== '')
+ {
+ $li['text'] []= '';
+ }
+ }
+ }
+
+ return $Block;
+ }
+
#
# Quote
- protected function identifyQuote($Line)
+ protected function blockQuote($Line)
{
if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
{
@@ -555,7 +668,7 @@ class Parsedown
}
}
- protected function addToQuote($Line, array $Block)
+ protected function blockQuoteContinue($Line, array $Block)
{
if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
{
@@ -582,9 +695,9 @@ class Parsedown
#
# Rule
- protected function identifyRule($Line)
+ protected function blockRule($Line)
{
- if (preg_match('/^(['.$Line['text'][0].'])([ ]{0,2}\1){2,}[ ]*$/', $Line['text']))
+ if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text']))
{
$Block = array(
'element' => array(
@@ -599,7 +712,7 @@ class Parsedown
#
# Setext
- protected function identifySetext($Line, array $Block = null)
+ protected function blockSetextHeader($Line, array $Block = null)
{
if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
{
@@ -617,46 +730,71 @@ class Parsedown
#
# Markup
- protected function identifyMarkup($Line)
+ protected function blockMarkup($Line)
{
- if (preg_match('/^<(\w[\w\d]*)(?:[ ][^>\/]*)?(\/?)[ ]*>/', $Line['text'], $matches))
+ if ($this->markupEscaped or $this->safeMode)
{
- if (in_array($matches[1], $this->textLevelElements))
+ return;
+ }
+
+ if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
+ {
+ $element = strtolower($matches[1]);
+
+ if (in_array($element, $this->textLevelElements))
{
return;
}
$Block = array(
- 'element' => $Line['body'],
+ 'name' => $matches[1],
+ 'depth' => 0,
+ 'markup' => $Line['text'],
);
- if ($matches[2] or $matches[1] === 'hr' or preg_match('/<\/'.$matches[1].'>[ ]*$/', $Line['text']))
+ $length = strlen($matches[0]);
+
+ $remainder = substr($Line['text'], $length);
+
+ if (trim($remainder) === '')
{
- $Block['closed'] = true;
+ if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
+ {
+ $Block['closed'] = true;
+
+ $Block['void'] = true;
+ }
}
else
{
- $Block['depth'] = 0;
- $Block['name'] = $matches[1];
+ 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 addToMarkup($Line, array $Block)
+ protected function blockMarkupContinue($Line, array $Block)
{
if (isset($Block['closed']))
{
return;
}
- if (preg_match('/<'.$Block['name'].'([ ][^\/]+)?>/', $Line['text'])) # opening tag
+ if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
{
$Block['depth'] ++;
}
- if (stripos($Line['text'], ''.$Block['name'].'>') !== false) # closing tag
+ if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
{
if ($Block['depth'] > 0)
{
@@ -668,15 +806,51 @@ class Parsedown
}
}
- $Block['element'] .= "\n".$Line['body'];
+ if (isset($Block['interrupted']))
+ {
+ $Block['markup'] .= "\n";
+
+ unset($Block['interrupted']);
+ }
+
+ $Block['markup'] .= "\n".$Line['body'];
return $Block;
}
+ #
+ # Reference
+
+ protected function blockReference($Line)
+ {
+ if (preg_match('/^\[(.+?)\]:[ ]*(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches))
+ {
+ $id = strtolower($matches[1]);
+
+ $Data = array(
+ 'url' => $matches[2],
+ 'title' => null,
+ );
+
+ if (isset($matches[3]))
+ {
+ $Data['title'] = $matches[3];
+ }
+
+ $this->DefinitionData['Reference'][$id] = $Data;
+
+ $Block = array(
+ 'hidden' => true,
+ );
+
+ return $Block;
+ }
+ }
+
#
# Table
- protected function identifyTable($Line, array $Block = null)
+ protected function blockTable($Line, array $Block = null)
{
if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
{
@@ -710,7 +884,7 @@ class Parsedown
$alignment = 'left';
}
- if (substr($dividerCell, -1) === ':')
+ if (substr($dividerCell, - 1) === ':')
{
$alignment = $alignment === 'left' ? 'center' : 'right';
}
@@ -744,7 +918,7 @@ class Parsedown
$alignment = $alignments[$index];
$HeaderElement['attributes'] = array(
- 'align' => $alignment,
+ 'style' => 'text-align: '.$alignment.';',
);
}
@@ -783,8 +957,13 @@ class Parsedown
}
}
- protected function addToTable($Line, array $Block)
+ protected function blockTableContinue($Line, array $Block)
{
+ if (isset($Block['interrupted']))
+ {
+ return;
+ }
+
if ($Line['text'][0] === '|' or strpos($Line['text'], '|'))
{
$Elements = array();
@@ -794,9 +973,9 @@ class Parsedown
$row = trim($row);
$row = trim($row, '|');
- $cells = explode('|', $row);
+ preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
- foreach ($cells as $index => $cell)
+ foreach ($matches[0] as $index => $cell)
{
$cell = trim($cell);
@@ -809,7 +988,7 @@ class Parsedown
if (isset($Block['alignments'][$index]))
{
$Element['attributes'] = array(
- 'align' => $Block['alignments'][$index],
+ 'style' => 'text-align: '.$Block['alignments'][$index].';',
);
}
@@ -828,35 +1007,11 @@ class Parsedown
}
}
- #
- # Definitions
- #
-
- protected function identifyReference($Line)
- {
- if (preg_match('/^\[(.+?)\]:[ ]*(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches))
- {
- $Definition = array(
- 'id' => strtolower($matches[1]),
- 'data' => array(
- 'url' => $matches[2],
- ),
- );
-
- if (isset($matches[3]))
- {
- $Definition['data']['title'] = $matches[3];
- }
-
- return $Definition;
- }
- }
-
#
# ~
#
- protected function buildParagraph($Line)
+ protected function paragraph($Line)
{
$Block = array(
'element' => array(
@@ -870,159 +1025,108 @@ class Parsedown
}
#
- # ~
+ # Inline Elements
#
- protected function element(array $Element)
- {
- $markup = '<'.$Element['name'];
-
- if (isset($Element['attributes']))
- {
- foreach ($Element['attributes'] as $name => $value)
- {
- $markup .= ' '.$name.'="'.$value.'"';
- }
- }
-
- if (isset($Element['text']))
- {
- $markup .= '>';
-
- if (isset($Element['handler']))
- {
- $markup .= $this->{$Element['handler']}($Element['text']);
- }
- else
- {
- $markup .= $Element['text'];
- }
-
- $markup .= ''.$Element['name'].'>';
- }
- else
- {
- $markup .= ' />';
- }
-
- return $markup;
- }
-
- protected function elements(array $Elements)
- {
- $markup = '';
-
- foreach ($Elements as $Element)
- {
- if ($Element === null)
- {
- continue;
- }
-
- $markup .= "\n";
-
- if (is_string($Element)) # because of Markup
- {
- $markup .= $Element;
-
- continue;
- }
-
- $markup .= $this->element($Element);
- }
-
- $markup .= "\n";
-
- return $markup;
- }
-
- #
- # Spans
- #
-
- protected $SpanTypes = array(
- '!' => array('Link'), # ?
- '&' => array('Ampersand'),
+ protected $InlineTypes = array(
+ '"' => array('SpecialCharacter'),
+ '!' => array('Image'),
+ '&' => array('SpecialCharacter'),
'*' => array('Emphasis'),
- '/' => array('Url'),
- '<' => array('UrlTag', 'EmailTag', 'Tag', 'LessThan'),
+ ':' => array('Url'),
+ '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'),
+ '>' => array('SpecialCharacter'),
'[' => array('Link'),
'_' => array('Emphasis'),
- '`' => array('InlineCode'),
+ '`' => array('Code'),
'~' => array('Strikethrough'),
'\\' => array('EscapeSequence'),
);
# ~
- protected $spanMarkerList = '*_!&[`~\\';
+ protected $inlineMarkerList = '!"*_&[:<>`~\\';
#
# ~
#
- public function line($text)
+ public function line($text, $nonNestables=array())
{
$markup = '';
- $remainder = $text;
+ # $excerpt is based on the first occurrence of a marker
- $markerPosition = 0;
-
- while ($excerpt = strpbrk($remainder, $this->spanMarkerList))
+ while ($excerpt = strpbrk($text, $this->inlineMarkerList))
{
$marker = $excerpt[0];
- $markerPosition += strpos($remainder, $marker);
+ $markerPosition = strpos($text, $marker);
$Excerpt = array('text' => $excerpt, 'context' => $text);
- foreach ($this->SpanTypes[$marker] as $spanType)
+ foreach ($this->InlineTypes[$marker] as $inlineType)
{
- $handler = 'identify'.$spanType;
+ # check to see if the current inline type is nestable in the current context
- $Span = $this->$handler($Excerpt);
-
- if ( ! isset($Span))
+ if ( ! empty($nonNestables) and in_array($inlineType, $nonNestables))
{
continue;
}
- # The identified span can be ahead of the marker.
+ $Inline = $this->{'inline'.$inlineType}($Excerpt);
- if (isset($Span['position']) and $Span['position'] > $markerPosition)
+ if ( ! isset($Inline))
{
continue;
}
- # Spans that start at the position of their marker don't have to set a position.
+ # makes sure that the inline belongs to "our" marker
- if ( ! isset($Span['position']))
+ if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
{
- $Span['position'] = $markerPosition;
+ continue;
}
- $plainText = substr($text, 0, $Span['position']);
+ # sets a default inline position
- $markup .= $this->readPlainText($plainText);
+ if ( ! isset($Inline['position']))
+ {
+ $Inline['position'] = $markerPosition;
+ }
- $markup .= isset($Span['markup']) ? $Span['markup'] : $this->element($Span['element']);
+ # cause the new element to 'inherit' our non nestables
- $text = substr($text, $Span['position'] + $Span['extent']);
+ foreach ($nonNestables as $non_nestable)
+ {
+ $Inline['element']['nonNestables'][] = $non_nestable;
+ }
- $remainder = $text;
+ # the text that comes before the inline
+ $unmarkedText = substr($text, 0, $Inline['position']);
- $markerPosition = 0;
+ # compile the unmarked text
+ $markup .= $this->unmarkedText($unmarkedText);
+
+ # compile the inline
+ $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
+
+ # remove the examined text
+ $text = substr($text, $Inline['position'] + $Inline['extent']);
continue 2;
}
- $remainder = substr($excerpt, 1);
+ # the marker does not belong to an inline
- $markerPosition ++;
+ $unmarkedText = substr($text, 0, $markerPosition + 1);
+
+ $markup .= $this->unmarkedText($unmarkedText);
+
+ $text = substr($text, $markerPosition + 1);
}
- $markup .= $this->readPlainText($text);
+ $markup .= $this->unmarkedText($text);
return $markup;
}
@@ -1031,136 +1135,14 @@ class Parsedown
# ~
#
- protected function identifyUrl($Excerpt)
- {
- if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '/')
- {
- return;
- }
-
- if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE))
- {
- $url = str_replace(array('&', '<'), array('&', '<'), $matches[0][0]);
-
- return array(
- 'extent' => strlen($matches[0][0]),
- 'position' => $matches[0][1],
- 'element' => array(
- 'name' => 'a',
- 'text' => $url,
- 'attributes' => array(
- 'href' => $url,
- ),
- ),
- );
- }
- }
-
- protected function identifyAmpersand($Excerpt)
- {
- if ( ! preg_match('/^?\w+;/', $Excerpt['text']))
- {
- return array(
- 'markup' => '&',
- 'extent' => 1,
- );
- }
- }
-
- protected function identifyStrikethrough($Excerpt)
- {
- if ( ! isset($Excerpt['text'][1]))
- {
- return;
- }
-
- if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
- {
- return array(
- 'extent' => strlen($matches[0]),
- 'element' => array(
- 'name' => 'del',
- 'text' => $matches[1],
- 'handler' => 'line',
- ),
- );
- }
- }
-
- protected function identifyEscapeSequence($Excerpt)
- {
- if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
- {
- return array(
- 'markup' => $Excerpt['text'][1],
- 'extent' => 2,
- );
- }
- }
-
- protected function identifyLessThan()
- {
- return array(
- 'markup' => '<',
- 'extent' => 1,
- );
- }
-
- protected function identifyUrlTag($Excerpt)
- {
- if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(https?:[\/]{2}[^\s]+?)>/i', $Excerpt['text'], $matches))
- {
- $url = str_replace(array('&', '<'), array('&', '<'), $matches[1]);
-
- return array(
- 'extent' => strlen($matches[0]),
- 'element' => array(
- 'name' => 'a',
- 'text' => $url,
- 'attributes' => array(
- 'href' => $url,
- ),
- ),
- );
- }
- }
-
- protected function identifyEmailTag($Excerpt)
- {
- if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\S+?@\S+?)>/', $Excerpt['text'], $matches))
- {
- return array(
- 'extent' => strlen($matches[0]),
- 'element' => array(
- 'name' => 'a',
- 'text' => $matches[1],
- 'attributes' => array(
- 'href' => 'mailto:'.$matches[1],
- ),
- ),
- );
- }
- }
-
- protected function identifyTag($Excerpt)
- {
- if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<\/?\w.*?>/', $Excerpt['text'], $matches))
- {
- return array(
- 'markup' => $matches[0],
- 'extent' => strlen($matches[0]),
- );
- }
- }
-
- protected function identifyInlineCode($Excerpt)
+ protected function inlineCode($Excerpt)
{
$marker = $Excerpt['text'][0];
- if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(? strlen($matches[0]),
@@ -1172,99 +1154,31 @@ class Parsedown
}
}
- protected function identifyLink($Excerpt)
+ protected function inlineEmailTag($Excerpt)
{
- $extent = $Excerpt['text'][0] === '!' ? 1 : 0;
-
- if (strpos($Excerpt['text'], ']') and preg_match('/\[((?:[^][]|(?R))*)\]/', $Excerpt['text'], $matches))
+ if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches))
{
- $Link = array('text' => $matches[1], 'label' => strtolower($matches[1]));
+ $url = $matches[1];
- $extent += strlen($matches[0]);
-
- $substring = substr($Excerpt['text'], $extent);
-
- if (preg_match('/^\s*\[([^][]+)\]/', $substring, $matches))
+ if ( ! isset($matches[2]))
{
- $Link['label'] = strtolower($matches[1]);
-
- if (isset($this->Definitions['Reference'][$Link['label']]))
- {
- $Link += $this->Definitions['Reference'][$Link['label']];
-
- $extent += strlen($matches[0]);
- }
- else
- {
- return;
- }
+ $url = 'mailto:' . $url;
}
- elseif (isset($this->Definitions['Reference'][$Link['label']]))
- {
- $Link += $this->Definitions['Reference'][$Link['label']];
- if (preg_match('/^[ ]*\[\]/', $substring, $matches))
- {
- $extent += strlen($matches[0]);
- }
- }
- elseif (preg_match('/^\([ ]*(.*?)(?:[ ]+[\'"](.+?)[\'"])?[ ]*\)/', $substring, $matches))
- {
- $Link['url'] = $matches[1];
-
- if (isset($matches[2]))
- {
- $Link['title'] = $matches[2];
- }
-
- $extent += strlen($matches[0]);
- }
- else
- {
- return;
- }
- }
- else
- {
- return;
- }
-
- $url = str_replace(array('&', '<'), array('&', '<'), $Link['url']);
-
- if ($Excerpt['text'][0] === '!')
- {
- $Element = array(
- 'name' => 'img',
- 'attributes' => array(
- 'alt' => $Link['text'],
- 'src' => $url,
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $matches[1],
+ 'attributes' => array(
+ 'href' => $url,
+ ),
),
);
}
- else
- {
- $Element = array(
- 'name' => 'a',
- 'handler' => 'line',
- 'text' => $Link['text'],
- 'attributes' => array(
- 'href' => $url,
- ),
- );
- }
-
- if (isset($Link['title']))
- {
- $Element['attributes']['title'] = $Link['title'];
- }
-
- return array(
- 'extent' => $extent,
- 'element' => $Element,
- );
}
- protected function identifyEmphasis($Excerpt)
+ protected function inlineEmphasis($Excerpt)
{
if ( ! isset($Excerpt['text'][1]))
{
@@ -1296,22 +1210,348 @@ class Parsedown
);
}
- #
+ protected function inlineEscapeSequence($Excerpt)
+ {
+ if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
+ {
+ return array(
+ 'markup' => $Excerpt['text'][1],
+ 'extent' => 2,
+ );
+ }
+ }
+
+ protected function inlineImage($Excerpt)
+ {
+ if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
+ {
+ return;
+ }
+
+ $Excerpt['text']= substr($Excerpt['text'], 1);
+
+ $Link = $this->inlineLink($Excerpt);
+
+ if ($Link === null)
+ {
+ return;
+ }
+
+ $Inline = array(
+ 'extent' => $Link['extent'] + 1,
+ 'element' => array(
+ 'name' => 'img',
+ 'attributes' => array(
+ 'src' => $Link['element']['attributes']['href'],
+ 'alt' => $Link['element']['text'],
+ ),
+ ),
+ );
+
+ $Inline['element']['attributes'] += $Link['element']['attributes'];
+
+ unset($Inline['element']['attributes']['href']);
+
+ return $Inline;
+ }
+
+ protected function inlineLink($Excerpt)
+ {
+ $Element = array(
+ 'name' => 'a',
+ 'handler' => 'line',
+ 'nonNestables' => array('Url', 'Link'),
+ 'text' => null,
+ 'attributes' => array(
+ 'href' => null,
+ 'title' => null,
+ ),
+ );
+
+ $extent = 0;
+
+ $remainder = $Excerpt['text'];
+
+ if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches))
+ {
+ $Element['text'] = $matches[1];
+
+ $extent += strlen($matches[0]);
+
+ $remainder = substr($remainder, $extent);
+ }
+ else
+ {
+ return;
+ }
+
+ if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches))
+ {
+ $Element['attributes']['href'] = $matches[1];
+
+ if (isset($matches[2]))
+ {
+ $Element['attributes']['title'] = substr($matches[2], 1, - 1);
+ }
+
+ $extent += strlen($matches[0]);
+ }
+ else
+ {
+ if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
+ {
+ $definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
+ $definition = strtolower($definition);
+
+ $extent += strlen($matches[0]);
+ }
+ else
+ {
+ $definition = strtolower($Element['text']);
+ }
+
+ if ( ! isset($this->DefinitionData['Reference'][$definition]))
+ {
+ return;
+ }
+
+ $Definition = $this->DefinitionData['Reference'][$definition];
+
+ $Element['attributes']['href'] = $Definition['url'];
+ $Element['attributes']['title'] = $Definition['title'];
+ }
+
+ return array(
+ 'extent' => $extent,
+ 'element' => $Element,
+ );
+ }
+
+ protected function inlineMarkup($Excerpt)
+ {
+ if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false)
+ {
+ return;
+ }
+
+ if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'markup' => $matches[0],
+ 'extent' => strlen($matches[0]),
+ );
+ }
+
+ if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'markup' => $matches[0],
+ 'extent' => strlen($matches[0]),
+ );
+ }
+
+ if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'markup' => $matches[0],
+ 'extent' => strlen($matches[0]),
+ );
+ }
+ }
+
+ protected function inlineSpecialCharacter($Excerpt)
+ {
+ if ($Excerpt['text'][0] === '&' and ! preg_match('/^?\w+;/', $Excerpt['text']))
+ {
+ return array(
+ 'markup' => '&',
+ 'extent' => 1,
+ );
+ }
+
+ $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
+
+ if (isset($SpecialCharacter[$Excerpt['text'][0]]))
+ {
+ return array(
+ 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';',
+ 'extent' => 1,
+ );
+ }
+ }
+
+ protected function inlineStrikethrough($Excerpt)
+ {
+ if ( ! isset($Excerpt['text'][1]))
+ {
+ return;
+ }
+
+ if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'del',
+ 'text' => $matches[1],
+ 'handler' => 'line',
+ ),
+ );
+ }
+ }
+
+ protected function inlineUrl($Excerpt)
+ {
+ if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
+ {
+ return;
+ }
+
+ if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE))
+ {
+ $url = $matches[0][0];
+
+ $Inline = array(
+ 'extent' => strlen($matches[0][0]),
+ 'position' => $matches[0][1],
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $url,
+ 'attributes' => array(
+ 'href' => $url,
+ ),
+ ),
+ );
+
+ return $Inline;
+ }
+ }
+
+ protected function inlineUrlTag($Excerpt)
+ {
+ if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches))
+ {
+ $url = $matches[1];
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $url,
+ 'attributes' => array(
+ 'href' => $url,
+ ),
+ ),
+ );
+ }
+ }
+
# ~
- protected function readPlainText($text)
+ protected function unmarkedText($text)
{
- $breakMarker = $this->breaksEnabled ? "\n" : " \n";
-
- $text = str_replace($breakMarker, "
\n", $text);
+ if ($this->breaksEnabled)
+ {
+ $text = preg_replace('/[ ]*\n/', "
\n", $text);
+ }
+ else
+ {
+ $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "
\n", $text);
+ $text = str_replace(" \n", "\n", $text);
+ }
return $text;
}
#
- # ~
+ # Handlers
#
+ protected function element(array $Element)
+ {
+ if ($this->safeMode)
+ {
+ $Element = $this->sanitiseElement($Element);
+ }
+
+ $markup = '<'.$Element['name'];
+
+ if (isset($Element['attributes']))
+ {
+ foreach ($Element['attributes'] as $name => $value)
+ {
+ if ($value === null)
+ {
+ continue;
+ }
+
+ $markup .= ' '.$name.'="'.self::escape($value).'"';
+ }
+ }
+
+ $permitRawHtml = false;
+
+ if (isset($Element['text']))
+ {
+ $text = $Element['text'];
+ }
+ // very strongly consider an alternative if you're writing an
+ // extension
+ elseif (isset($Element['rawHtml']))
+ {
+ $text = $Element['rawHtml'];
+ $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode'];
+ $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode;
+ }
+
+ if (isset($text))
+ {
+ $markup .= '>';
+
+ if (!isset($Element['nonNestables']))
+ {
+ $Element['nonNestables'] = array();
+ }
+
+ if (isset($Element['handler']))
+ {
+ $markup .= $this->{$Element['handler']}($text, $Element['nonNestables']);
+ }
+ elseif (!$permitRawHtml)
+ {
+ $markup .= self::escape($text, true);
+ }
+ else
+ {
+ $markup .= $text;
+ }
+
+ $markup .= ''.$Element['name'].'>';
+ }
+ else
+ {
+ $markup .= ' />';
+ }
+
+ return $markup;
+ }
+
+ protected function elements(array $Elements)
+ {
+ $markup = '';
+
+ foreach ($Elements as $Element)
+ {
+ $markup .= "\n" . $this->element($Element);
+ }
+
+ $markup .= "\n";
+
+ return $markup;
+ }
+
+ # ~
+
protected function li($lines)
{
$markup = $this->lines($lines);
@@ -1332,9 +1572,87 @@ class Parsedown
}
#
- # Multiton
+ # Deprecated Methods
#
+ function parse($text)
+ {
+ $markup = $this->text($text);
+
+ return $markup;
+ }
+
+ protected function sanitiseElement(array $Element)
+ {
+ static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';
+ static $safeUrlNameToAtt = array(
+ 'a' => 'href',
+ 'img' => 'src',
+ );
+
+ if (isset($safeUrlNameToAtt[$Element['name']]))
+ {
+ $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]);
+ }
+
+ if ( ! empty($Element['attributes']))
+ {
+ foreach ($Element['attributes'] as $att => $val)
+ {
+ # filter out badly parsed attribute
+ if ( ! preg_match($goodAttribute, $att))
+ {
+ unset($Element['attributes'][$att]);
+ }
+ # dump onevent attribute
+ elseif (self::striAtStart($att, 'on'))
+ {
+ unset($Element['attributes'][$att]);
+ }
+ }
+ }
+
+ return $Element;
+ }
+
+ protected function filterUnsafeUrlInAttribute(array $Element, $attribute)
+ {
+ foreach ($this->safeLinksWhitelist as $scheme)
+ {
+ if (self::striAtStart($Element['attributes'][$attribute], $scheme))
+ {
+ return $Element;
+ }
+ }
+
+ $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]);
+
+ return $Element;
+ }
+
+ #
+ # Static Methods
+ #
+
+ protected static function escape($text, $allowQuotes = false)
+ {
+ return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8');
+ }
+
+ protected static function striAtStart($string, $needle)
+ {
+ $len = strlen($needle);
+
+ if ($len > strlen($string))
+ {
+ return false;
+ }
+ else
+ {
+ return strtolower(substr($string, 0, $len)) === strtolower($needle);
+ }
+ }
+
static function instance($name = 'default')
{
if (isset(self::$instances[$name]))
@@ -1342,7 +1660,7 @@ class Parsedown
return self::$instances[$name];
}
- $instance = new self();
+ $instance = new static();
self::$instances[$name] = $instance;
@@ -1351,41 +1669,33 @@ class Parsedown
private static $instances = array();
- #
- # Deprecated Methods
- #
-
- /**
- * @deprecated in favor of "text"
- */
- function parse($text)
- {
- $markup = $this->text($text);
-
- return $markup;
- }
-
#
# Fields
#
- protected $Definitions;
+ protected $DefinitionData;
#
- # Read-only
+ # Read-Only
protected $specialCharacters = array(
- '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!',
+ '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
);
protected $StrongRegex = array(
- '*' => '/^[*]{2}((?:[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
- '_' => '/^__((?:[^_]|_[^_]*_)+?)__(?!_)/us',
+ '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
+ '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
);
protected $EmRegex = array(
- '*' => '/^[*]((?:[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
- '_' => '/^_((?:[^_]|__[^_]*__)+?)_(?!_)\b/us',
+ '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
+ '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
+ );
+
+ protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
+
+ protected $voidElements = array(
+ 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
);
protected $textLevelElements = array(
@@ -1393,10 +1703,10 @@ class Parsedown
'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
'i', 'rp', 'del', 'code', 'strike', 'marquee',
'q', 'rt', 'ins', 'font', 'strong',
- 's', 'tt', 'sub', 'mark',
- 'u', 'xm', 'sup', 'nobr',
- 'var', 'ruby',
- 'wbr', 'span',
- 'time',
+ 's', 'tt', 'kbd', 'mark',
+ 'u', 'xm', 'sub', 'nobr',
+ 'sup', 'ruby',
+ 'var', 'span',
+ 'wbr', 'time',
);
-}
+}
\ No newline at end of file