phpOMS/Utils/Parser/Markdown/Markdown.php
2023-09-29 02:22:20 +00:00

4206 lines
141 KiB
PHP
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Jingga
*
* PHP Version 8.1
*
* @package phpOMS\Utils\Parser\Markdown
* @license Original license Emanuil Rusev, erusev.com (MIT)
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
namespace phpOMS\Utils\Parser\Markdown;
use phpOMS\Uri\UriFactory;
/**
* Markdown parser class.
*
* @package phpOMS\Utils\Parser\Markdown
* @license Original & extra license Emanuil Rusev, erusev.com (MIT)
* @license Extended license Benjamin Hoegh (MIT)
* @link https://jingga.app
* @see https://github.com/erusev/parsedown
* @see https://github.com/erusev/parsedown-extra
* @see https://github.com/BenjaminHoegh/ParsedownExtended
* @since 1.0.0
*/
class Markdown
{
# ~
public const version = '1.8.0-beta-7';
private array $options = [];
# ~
private string $idToc = '';
public function __construct(array $params = [])
{
$this->options = $params;
$this->options['toc'] = $this->options['toc'] ?? false;
// Marks
$state = $this->options['mark'] ?? true;
if ($state !== false) {
$this->InlineTypes['='][] = 'mark';
$this->inlineMarkerList .= '=';
}
// Keystrokes
$state = $this->options['keystrokes'] ?? true;
if ($state !== false) {
$this->InlineTypes['['][] = 'Keystrokes';
$this->inlineMarkerList .= '[';
}
// Inline Math
$state = $this->options['math'] ?? false;
if ($state !== false) {
$this->InlineTypes['\\'][] = 'Math';
$this->inlineMarkerList .= '\\';
$this->InlineTypes['$'][] = 'Math';
$this->inlineMarkerList .= '$';
}
// Superscript
$state = $this->options['sup'] ?? false;
if ($state !== false) {
$this->InlineTypes['^'][] = 'Superscript';
$this->inlineMarkerList .= '^';
}
// Subscript
$state = $this->options['sub'] ?? false;
if ($state !== false) {
$this->InlineTypes['~'][] = 'Subscript';
}
// Emojis
$state = $this->options['emojis'] ?? true;
if ($state !== false) {
$this->InlineTypes[':'][] = 'Emojis';
$this->inlineMarkerList .= ':';
}
// Typographer
$state = $this->options['typographer'] ?? false;
if ($state !== false) {
$this->InlineTypes['('][] = 'Typographer';
$this->inlineMarkerList .= '(';
$this->InlineTypes['.'][] = 'Typographer';
$this->inlineMarkerList .= '.';
$this->InlineTypes['+'][] = 'Typographer';
$this->inlineMarkerList .= '+';
$this->InlineTypes['!'][] = 'Typographer';
$this->inlineMarkerList .= '!';
$this->InlineTypes['?'][] = 'Typographer';
$this->inlineMarkerList .= '?';
}
// Smartypants
$state = $this->options['smarty'] ?? false;
if ($state !== false) {
$this->InlineTypes['<'][] = 'Smartypants';
$this->inlineMarkerList .= '<';
$this->InlineTypes['>'][] = 'Smartypants';
$this->inlineMarkerList .= '>';
$this->InlineTypes['-'][] = 'Smartypants';
$this->inlineMarkerList .= '-';
$this->InlineTypes['.'][] = 'Smartypants';
$this->inlineMarkerList .= '.';
$this->InlineTypes["'"][] = 'Smartypants';
$this->inlineMarkerList .= "'";
$this->InlineTypes['"'][] = 'Smartypants';
$this->inlineMarkerList .= '"';
$this->InlineTypes['`'][] = 'Smartypants';
$this->inlineMarkerList .= '`';
}
/*
* Blocks
* ------------------------------------------------------------------------
*/
// Block Math
$state = $this->options['math'] ?? false;
if ($state !== false) {
$this->BlockTypes['\\'][] = 'Math';
$this->BlockTypes['$'][] = 'Math';
}
// Task
$state = $this->options['lists']['tasks'] ?? true;
if ($state !== false) {
$this->BlockTypes['['][] = 'Checkbox';
}
}
public function textParent($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;
}
/**
* Parses the given markdown string to an HTML string but it leaves the ToC
* tag as is. It's an alias of the parent method "\DynamicParent::text()".
*/
public function body($text) : string
{
$text = $this->encodeTagToHash($text); // Escapes ToC tag temporary
$html = $this->textParent($text); // Parses the markdown text
return $this->decodeTagFromHash($html); // Unescape the ToC tag
}
/**
* Parses markdown string to HTML and also the "[toc]" tag as well.
* It overrides the parent method: \Parsedown::text().
*/
public function text($text)
{
// Parses the markdown text except the ToC tag. This also searches
// the list of contents and available to get from "contentsList()"
// method.
$html = $this->body($text);
if (isset($this->options['toc']) && $this->options['toc'] == false) {
return $html;
}
$tagOrigin = $this->getTagToC();
if (\strpos($text, $tagOrigin) === false) {
return $html;
}
$tocData = $this->contentsList();
$tocId = $this->getIdAttributeToC();
$needle = '<p>'.$tagOrigin.'</p>';
$replace = "<div id=\"{$tocId}\">{$tocData}</div>";
return \str_replace($needle, $replace, $html);
}
/**
* Returns the parsed ToC.
*
* @param string $typeReturn Type of the return format. "html" or "json".
*
* @return string HTML/JSON string of ToC
*/
public function contentsList($typeReturn = 'html')
{
if (\strtolower($typeReturn) === 'html') {
$result = '';
if (!empty($this->contentsListString)) {
// Parses the ToC list in markdown to HTML
$result = $this->body($this->contentsListString);
}
return $result;
}
if (\strtolower($typeReturn) === 'json') {
return \json_encode($this->contentsListArray);
}
// Forces to return ToC as "html"
\error_log(
'Unknown return type given while parsing ToC.'
.' At: '.__FUNCTION__.'() '
.' in Line:'.__LINE__.' (Using default type)'
);
return $this->contentsList('html');
}
/**
* ------------------------------------------------------------------------
* Inline
* ------------------------------------------------------------------------.
*/
// inlineCode
protected function inlineCode($Excerpt)
{
$codeSnippets = $this->options['code']['inline'] ?? true;
$codeMain = $this->options['code'] ?? true;
if ($codeSnippets !== true || $codeMain !== true) {
return;
}
$marker = $Excerpt['text'][0];
if (\preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(?<!['.$marker.'])\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
{
$text = $matches[2];
$text = \preg_replace('/[ ]*+\n/', ' ', $text);
return [
'extent' => \strlen($matches[0]),
'element' => [
'name' => 'code',
'text' => $text,
],
];
}
}
protected function inlineEmailTag($Excerpt)
{
$mainState = $this->options['links'] ?? true;
$state = $this->options['links']['email_links'] ?? true;
if (!$mainState || !$state) {
return;
}
$hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?';
$commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@'
. $hostnameLabel . '(?:\.' . $hostnameLabel . ')*';
if (\strpos($Excerpt['text'], '>') !== false
&& \preg_match("/^<((mailto:)?{$commonMarkEmail})>/i", $Excerpt['text'], $matches)
){
$url = $matches[1];
if (!isset($matches[2]))
{
$url = "mailto:{$url}";
}
return [
'extent' => \strlen($matches[0]),
'element' => [
'name' => 'a',
'text' => $matches[1],
'attributes' => [
'href' => $url,
],
],
];
}
}
protected function inlineEmphasis($Excerpt)
{
$state = $this->options['emphasis'] ?? true;
if (!$state) {
return;
}
if (!isset($Excerpt['text'][1]))
{
return;
}
$marker = $Excerpt['text'][0];
if ($Excerpt['text'][1] === $marker && isset($this->StrongRegex[$marker]) && \preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
{
$emphasis = 'strong';
}
elseif ($Excerpt['text'][1] === $marker && isset($this->UnderlineRegex[$marker]) && \preg_match($this->UnderlineRegex[$marker], $Excerpt['text'], $matches))
{
$emphasis = 'u';
}
elseif (\preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
{
$emphasis = 'em';
}
else
{
return;
}
return [
'extent' => \strlen($matches[0]),
'element' => [
'name' => $emphasis,
'handler' => [
'function' => 'lineElements',
'argument' => $matches[1],
'destination' => 'elements',
],
],
];
}
protected function inlineImage($Excerpt)
{
$state = $this->options['images'] ?? true;
if (!$state) {
return;
}
if (!isset($Excerpt['text'][1]) || $Excerpt['text'][1] !== '[')
{
return;
}
$Excerpt['text']= \substr($Excerpt['text'], 1);
$Link = $this->inlineLink($Excerpt);
if ($Link === null)
{
return;
}
$Inline = [
'extent' => $Link['extent'] + 1,
'element' => [
'name' => 'img',
'attributes' => [
'src' => $Link['element']['attributes']['href'],
'alt' => $Link['element']['handler']['argument'],
],
'autobreak' => true,
],
];
$Inline['element']['attributes'] += $Link['element']['attributes'];
unset($Inline['element']['attributes']['href']);
return $Inline;
}
protected function inlineLink($Excerpt)
{
$state = $this->options['links'] ?? true;
if (!$state) {
return;
}
$Link = $this->inlineLinkParent($Excerpt);
$remainder = $Link !== null ? \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;
}
protected function inlineMarkup($Excerpt)
{
$state = $this->options['markup'] ?? true;
if (!$state) {
return;
}
if ($this->markupEscaped || $this->safeMode || \strpos($Excerpt['text'], '>') === false)
{
return;
}
if ($Excerpt['text'][1] === '/' && \preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches))
{
return [
'element' => ['rawHtml' => $matches[0]],
'extent' => \strlen($matches[0]),
];
}
if ($Excerpt['text'][1] === '!' && \preg_match('/^<!---?[^>-](?:-?+[^-])*-->/s', $Excerpt['text'], $matches))
{
return [
'element' => ['rawHtml' => $matches[0]],
'extent' => \strlen($matches[0]),
];
}
if ($Excerpt['text'][1] !== ' ' && \preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches))
{
return [
'element' => ['rawHtml' => $matches[0]],
'extent' => \strlen($matches[0]),
];
}
}
protected function inlineStrikethrough($Excerpt)
{
$state = $this->options['strikethroughs'] ?? true;
if (!$state) {
return;
}
if (!isset($Excerpt['text'][1]))
{
return;
}
if ($Excerpt['text'][1] === '~' && \preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
{
return [
'extent' => \strlen($matches[0]),
'element' => [
'name' => 'del',
'handler' => [
'function' => 'lineElements',
'argument' => $matches[1],
'destination' => 'elements',
],
],
];
}
}
protected function inlineUrl($Excerpt)
{
$state = $this->options['links'] ?? true;
if (!$state) {
return;
}
if ($this->urlsLinked !== true || !isset($Excerpt['text'][2]) || $Excerpt['text'][2] !== '/')
{
return;
}
if (\strpos($Excerpt['context'], 'http') !== false
&& \preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, \PREG_OFFSET_CAPTURE)
) {
$url = $matches[0][0];
return [
'extent' => \strlen($matches[0][0]),
'position' => $matches[0][1],
'element' => [
'name' => 'a',
'text' => $url,
'attributes' => [
'href' => $url,
],
],
];
}
}
protected function inlineUrlTag($Excerpt)
{
$state = $this->options['links'] ?? true;
if (!$state) {
return;
}
if (\strpos($Excerpt['text'], '>') !== false && \preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches))
{
$url = $matches[1];
return [
'extent' => \strlen($matches[0]),
'element' => [
'name' => 'a',
'text' => $url,
'attributes' => [
'href' => $url,
],
],
];
}
}
protected function inlineEmojis($excerpt)
{
$emojiMap = [
':smile:' => '😄', ':laughing:' => '😆', ':blush:' => '😊', ':smiley:' => '😃',
':relaxed:' => '☺️', ':smirk:' => '😏', ':heart_eyes:' => '😍', ':kissing_heart:' => '😘',
':kissing_closed_eyes:' => '😚', ':flushed:' => '😳', ':relieved:' => '😌', ':satisfied:' => '😆',
':grin:' => '😁', ':wink:' => '😉', ':stuck_out_tongue_winking_eye:' => '😜', ':stuck_out_tongue_closed_eyes:' => '😝',
':grinning:' => '😀', ':kissing:' => '😗', ':kissing_smiling_eyes:' => '😙', ':stuck_out_tongue:' => '😛',
':sleeping:' => '😴', ':worried:' => '😟', ':frowning:' => '😦', ':anguished:' => '😧',
':open_mouth:' => '😮', ':grimacing:' => '😬', ':confused:' => '😕', ':hushed:' => '😯',
':expressionless:' => '😑', ':unamused:' => '😒', ':sweat_smile:' => '😅', ':sweat:' => '😓',
':disappointed_relieved:' => '😥', ':weary:' => '😩', ':pensive:' => '😔', ':disappointed:' => '😞',
':confounded:' => '😖', ':fearful:' => '😨', ':cold_sweat:' => '😰', ':persevere:' => '😣',
':cry:' => '😢', ':sob:' => '😭', ':joy:' => '😂', ':astonished:' => '😲',
':scream:' => '😱', ':tired_face:' => '😫', ':angry:' => '😠', ':rage:' => '😡',
':triumph:' => '😤', ':sleepy:' => '😪', ':yum:' => '😋', ':mask:' => '😷',
':sunglasses:' => '😎', ':dizzy_face:' => '😵', ':imp:' => '👿', ':smiling_imp:' => '😈',
':neutral_face:' => '😐', ':no_mouth:' => '😶', ':innocent:' => '😇', ':alien:' => '👽',
':yellow_heart:' => '💛', ':blue_heart:' => '💙', ':purple_heart:' => '💜', ':heart:' => '❤️',
':green_heart:' => '💚', ':broken_heart:' => '💔', ':heartbeat:' => '💓', ':heartpulse:' => '💗',
':two_hearts:' => '💕', ':revolving_hearts:' => '💞', ':cupid:' => '💘', ':sparkling_heart:' => '💖',
':sparkles:' => '✨', ':star:' => '⭐️', ':star2:' => '🌟', ':dizzy:' => '💫',
':boom:' => '💥', ':collision:' => '💥', ':anger:' => '💢', ':exclamation:' => '❗️',
':question:' => '❓', ':grey_exclamation:' => '❕', ':grey_question:' => '❔', ':zzz:' => '💤',
':dash:' => '💨', ':sweat_drops:' => '💦', ':notes:' => '🎶', ':musical_note:' => '🎵',
':fire:' => '🔥', ':hankey:' => '💩', ':poop:' => '💩', ':shit:' => '💩',
':+1:' => '👍', ':thumbsup:' => '👍', ':-1:' => '👎', ':thumbsdown:' => '👎',
':ok_hand:' => '👌', ':punch:' => '👊', ':facepunch:' => '👊', ':fist:' => '✊',
':v:' => '✌️', ':wave:' => '👋', ':hand:' => '✋', ':raised_hand:' => '✋',
':open_hands:' => '👐', ':point_up:' => '☝️', ':point_down:' => '👇', ':point_left:' => '👈',
':point_right:' => '👉', ':raised_hands:' => '🙌', ':pray:' => '🙏', ':point_up_2:' => '👆',
':clap:' => '👏', ':muscle:' => '💪', ':metal:' => '🤘', ':fu:' => '🖕',
':walking:' => '🚶', ':runner:' => '🏃', ':running:' => '🏃', ':couple:' => '👫',
':family:' => '👪', ':two_men_holding_hands:' => '👬', ':two_women_holding_hands:' => '👭', ':dancer:' => '💃',
':dancers:' => '👯', ':ok_woman:' => '🙆', ':no_good:' => '🙅', ':information_desk_person:' => '💁',
':raising_hand:' => '🙋', ':bride_with_veil:' => '👰', ':person_with_pouting_face:' => '🙎', ':person_frowning:' => '🙍',
':bow:' => '🙇', ':couple_with_heart:' => '💑', ':massage:' => '💆', ':haircut:' => '💇',
':nail_care:' => '💅', ':boy:' => '👦', ':girl:' => '👧', ':woman:' => '👩',
':man:' => '👨', ':baby:' => '👶', ':older_woman:' => '👵', ':older_man:' => '👴',
':person_with_blond_hair:' => '👱', ':man_with_gua_pi_mao:' => '👲', ':man_with_turban:' => '👳', ':construction_worker:' => '👷',
':cop:' => '👮', ':angel:' => '👼', ':princess:' => '👸', ':smiley_cat:' => '😺',
':smile_cat:' => '😸', ':heart_eyes_cat:' => '😻', ':kissing_cat:' => '😽', ':smirk_cat:' => '😼',
':scream_cat:' => '🙀', ':crying_cat_face:' => '😿', ':joy_cat:' => '😹', ':pouting_cat:' => '😾',
':japanese_ogre:' => '👹', ':japanese_goblin:' => '👺', ':see_no_evil:' => '🙈', ':hear_no_evil:' => '🙉',
':speak_no_evil:' => '🙊', ':guardsman:' => '💂', ':skull:' => '💀', ':feet:' => '🐾',
':lips:' => '👄', ':kiss:' => '💋', ':droplet:' => '💧', ':ear:' => '👂',
':eyes:' => '👀', ':nose:' => '👃', ':tongue:' => '👅', ':love_letter:' => '💌',
':bust_in_silhouette:' => '👤', ':busts_in_silhouette:' => '👥', ':speech_balloon:' => '💬', ':thought_balloon:' => '💭',
':sunny:' => '☀️', ':umbrella:' => '☔️', ':cloud:' => '☁️', ':snowflake:' => '❄️',
':snowman:' => '⛄️', ':zap:' => '⚡️', ':cyclone:' => '🌀', ':foggy:' => '🌁',
':ocean:' => '🌊', ':cat:' => '🐱', ':dog:' => '🐶', ':mouse:' => '🐭',
':hamster:' => '🐹', ':rabbit:' => '🐰', ':wolf:' => '🐺', ':frog:' => '🐸',
':tiger:' => '🐯', ':koala:' => '🐨', ':bear:' => '🐻', ':pig:' => '🐷',
':pig_nose:' => '🐽', ':cow:' => '🐮', ':boar:' => '🐗', ':monkey_face:' => '🐵',
':monkey:' => '🐒', ':horse:' => '🐴', ':racehorse:' => '🐎', ':camel:' => '🐫',
':sheep:' => '🐑', ':elephant:' => '🐘', ':panda_face:' => '🐼', ':snake:' => '🐍',
':bird:' => '🐦', ':baby_chick:' => '🐤', ':hatched_chick:' => '🐥', ':hatching_chick:' => '🐣',
':chicken:' => '🐔', ':penguin:' => '🐧', ':turtle:' => '🐢', ':bug:' => '🐛',
':honeybee:' => '🐝', ':ant:' => '🐜', ':beetle:' => '🐞', ':snail:' => '🐌',
':octopus:' => '🐙', ':tropical_fish:' => '🐠', ':fish:' => '🐟', ':whale:' => '🐳',
':whale2:' => '🐋', ':dolphin:' => '🐬', ':cow2:' => '🐄', ':ram:' => '🐏',
':rat:' => '🐀', ':water_buffalo:' => '🐃', ':tiger2:' => '🐅', ':rabbit2:' => '🐇',
':dragon:' => '🐉', ':goat:' => '🐐', ':rooster:' => '🐓', ':dog2:' => '🐕',
':pig2:' => '🐖', ':mouse2:' => '🐁', ':ox:' => '🐂', ':dragon_face:' => '🐲',
':blowfish:' => '🐡', ':crocodile:' => '🐊', ':dromedary_camel:' => '🐪', ':leopard:' => '🐆',
':cat2:' => '🐈', ':poodle:' => '🐩', ':crab' => '🦀', ':paw_prints:' => '🐾', ':bouquet:' => '💐',
':cherry_blossom:' => '🌸', ':tulip:' => '🌷', ':four_leaf_clover:' => '🍀', ':rose:' => '🌹',
':sunflower:' => '🌻', ':hibiscus:' => '🌺', ':maple_leaf:' => '🍁', ':leaves:' => '🍃',
':fallen_leaf:' => '🍂', ':herb:' => '🌿', ':mushroom:' => '🍄', ':cactus:' => '🌵',
':palm_tree:' => '🌴', ':evergreen_tree:' => '🌲', ':deciduous_tree:' => '🌳', ':chestnut:' => '🌰',
':seedling:' => '🌱', ':blossom:' => '🌼', ':ear_of_rice:' => '🌾', ':shell:' => '🐚',
':globe_with_meridians:' => '🌐', ':sun_with_face:' => '🌞', ':full_moon_with_face:' => '🌝', ':new_moon_with_face:' => '🌚',
':new_moon:' => '🌑', ':waxing_crescent_moon:' => '🌒', ':first_quarter_moon:' => '🌓', ':waxing_gibbous_moon:' => '🌔',
':full_moon:' => '🌕', ':waning_gibbous_moon:' => '🌖', ':last_quarter_moon:' => '🌗', ':waning_crescent_moon:' => '🌘',
':last_quarter_moon_with_face:' => '🌜', ':first_quarter_moon_with_face:' => '🌛', ':moon:' => '🌔', ':earth_africa:' => '🌍',
':earth_americas:' => '🌎', ':earth_asia:' => '🌏', ':volcano:' => '🌋', ':milky_way:' => '🌌',
':partly_sunny:' => '⛅️', ':bamboo:' => '🎍', ':gift_heart:' => '💝', ':dolls:' => '🎎',
':school_satchel:' => '🎒', ':mortar_board:' => '🎓', ':flags:' => '🎏', ':fireworks:' => '🎆',
':sparkler:' => '🎇', ':wind_chime:' => '🎐', ':rice_scene:' => '🎑', ':jack_o_lantern:' => '🎃',
':ghost:' => '👻', ':santa:' => '🎅', ':christmas_tree:' => '🎄', ':gift:' => '🎁',
':bell:' => '🔔', ':no_bell:' => '🔕', ':tanabata_tree:' => '🎋', ':tada:' => '🎉',
':confetti_ball:' => '🎊', ':balloon:' => '🎈', ':crystal_ball:' => '🔮', ':cd:' => '💿',
':dvd:' => '📀', ':floppy_disk:' => '💾', ':camera:' => '📷', ':video_camera:' => '📹',
':movie_camera:' => '🎥', ':computer:' => '💻', ':tv:' => '📺', ':iphone:' => '📱',
':phone:' => '☎️', ':telephone:' => '☎️', ':telephone_receiver:' => '📞', ':pager:' => '📟',
':fax:' => '📠', ':minidisc:' => '💽', ':vhs:' => '📼', ':sound:' => '🔉',
':speaker:' => '🔈', ':mute:' => '🔇', ':loudspeaker:' => '📢', ':mega:' => '📣',
':hourglass:' => '⌛️', ':hourglass_flowing_sand:' => '⏳', ':alarm_clock:' => '⏰', ':watch:' => '⌚️',
':radio:' => '📻', ':satellite:' => '📡', ':loop:' => '➿', ':mag:' => '🔍',
':mag_right:' => '🔎', ':unlock:' => '🔓', ':lock:' => '🔒', ':lock_with_ink_pen:' => '🔏',
':closed_lock_with_key:' => '🔐', ':key:' => '🔑', ':bulb:' => '💡', ':flashlight:' => '🔦',
':high_brightness:' => '🔆', ':low_brightness:' => '🔅', ':electric_plug:' => '🔌', ':battery:' => '🔋',
':calling:' => '📲', ':email:' => '✉️', ':mailbox:' => '📫', ':postbox:' => '📮',
':bath:' => '🛀', ':bathtub:' => '🛁', ':shower:' => '🚿', ':toilet:' => '🚽',
':wrench:' => '🔧', ':nut_and_bolt:' => '🔩', ':hammer:' => '🔨', ':seat:' => '💺',
':moneybag:' => '💰', ':yen:' => '💴', ':dollar:' => '💵', ':pound:' => '💷',
':euro:' => '💶', ':credit_card:' => '💳', ':money_with_wings:' => '💸', ':e-mail:' => '📧',
':inbox_tray:' => '📥', ':outbox_tray:' => '📤', ':envelope:' => '✉️', ':incoming_envelope:' => '📨',
':postal_horn:' => '📯', ':mailbox_closed:' => '📪', ':mailbox_with_mail:' => '📬', ':mailbox_with_no_mail:' => '📭',
':door:' => '🚪', ':smoking:' => '🚬', ':bomb:' => '💣', ':gun:' => '🔫',
':hocho:' => '🔪', ':pill:' => '💊', ':syringe:' => '💉', ':page_facing_up:' => '📄',
':page_with_curl:' => '📃', ':bookmark_tabs:' => '📑', ':bar_chart:' => '📊', ':chart_with_upwards_trend:' => '📈',
':chart_with_downwards_trend:' => '📉', ':scroll:' => '📜', ':clipboard:' => '📋', ':calendar:' => '📆',
':date:' => '📅', ':card_index:' => '📇', ':file_folder:' => '📁', ':open_file_folder:' => '📂',
':scissors:' => '✂️', ':pushpin:' => '📌', ':paperclip:' => '📎', ':black_nib:' => '✒️',
':pencil2:' => '✏️', ':straight_ruler:' => '📏', ':triangular_ruler:' => '📐', ':closed_book:' => '📕',
':green_book:' => '📗', ':blue_book:' => '📘', ':orange_book:' => '📙', ':notebook:' => '📓',
':notebook_with_decorative_cover:' => '📔', ':ledger:' => '📒', ':books:' => '📚', ':bookmark:' => '🔖',
':name_badge:' => '📛', ':microscope:' => '🔬', ':telescope:' => '🔭', ':newspaper:' => '📰',
':football:' => '🏈', ':basketball:' => '🏀', ':soccer:' => '⚽️', ':baseball:' => '⚾️',
':tennis:' => '🎾', ':8ball:' => '🎱', ':rugby_football:' => '🏉', ':bowling:' => '🎳',
':golf:' => '⛳️', ':mountain_bicyclist:' => '🚵', ':bicyclist:' => '🚴', ':horse_racing:' => '🏇',
':snowboarder:' => '🏂', ':swimmer:' => '🏊', ':surfer:' => '🏄', ':ski:' => '🎿',
':spades:' => '♠️', ':hearts:' => '♥️', ':clubs:' => '♣️', ':diamonds:' => '♦️',
':gem:' => '💎', ':ring:' => '💍', ':trophy:' => '🏆', ':musical_score:' => '🎼',
':musical_keyboard:' => '🎹', ':violin:' => '🎻', ':space_invader:' => '👾', ':video_game:' => '🎮',
':black_joker:' => '🃏', ':flower_playing_cards:' => '🎴', ':game_die:' => '🎲', ':dart:' => '🎯',
':mahjong:' => '🀄️', ':clapper:' => '🎬', ':memo:' => '📝', ':pencil:' => '📝',
':book:' => '📖', ':art:' => '🎨', ':microphone:' => '🎤', ':headphones:' => '🎧',
':trumpet:' => '🎺', ':saxophone:' => '🎷', ':guitar:' => '🎸', ':shoe:' => '👞',
':sandal:' => '👡', ':high_heel:' => '👠', ':lipstick:' => '💄', ':boot:' => '👢',
':shirt:' => '👕', ':tshirt:' => '👕', ':necktie:' => '👔', ':womans_clothes:' => '👚',
':dress:' => '👗', ':running_shirt_with_sash:' => '🎽', ':jeans:' => '👖', ':kimono:' => '👘',
':bikini:' => '👙', ':ribbon:' => '🎀', ':tophat:' => '🎩', ':crown:' => '👑',
':womans_hat:' => '👒', ':mans_shoe:' => '👞', ':closed_umbrella:' => '🌂', ':briefcase:' => '💼',
':handbag:' => '👜', ':pouch:' => '👝', ':purse:' => '👛', ':eyeglasses:' => '👓',
':fishing_pole_and_fish:' => '🎣', ':coffee:' => '☕️', ':tea:' => '🍵', ':sake:' => '🍶',
':baby_bottle:' => '🍼', ':beer:' => '🍺', ':beers:' => '🍻', ':cocktail:' => '🍸',
':tropical_drink:' => '🍹', ':wine_glass:' => '🍷', ':fork_and_knife:' => '🍴', ':pizza:' => '🍕',
':hamburger:' => '🍔', ':fries:' => '🍟', ':poultry_leg:' => '🍗', ':meat_on_bone:' => '🍖',
':spaghetti:' => '🍝', ':curry:' => '🍛', ':fried_shrimp:' => '🍤', ':bento:' => '🍱',
':sushi:' => '🍣', ':fish_cake:' => '🍥', ':rice_ball:' => '🍙', ':rice_cracker:' => '🍘',
':rice:' => '🍚', ':ramen:' => '🍜', ':stew:' => '🍲', ':oden:' => '🍢',
':dango:' => '🍡', ':egg:' => '🥚', ':bread:' => '🍞', ':doughnut:' => '🍩',
':custard:' => '🍮', ':icecream:' => '🍦', ':ice_cream:' => '🍨', ':shaved_ice:' => '🍧',
':birthday:' => '🎂', ':cake:' => '🍰', ':cookie:' => '🍪', ':chocolate_bar:' => '🍫',
':candy:' => '🍬', ':lollipop:' => '🍭', ':honey_pot:' => '🍯', ':apple:' => '🍎',
':green_apple:' => '🍏', ':tangerine:' => '🍊', ':lemon:' => '🍋', ':cherries:' => '🍒',
':grapes:' => '🍇', ':watermelon:' => '🍉', ':strawberry:' => '🍓', ':peach:' => '🍑',
':melon:' => '🍈', ':banana:' => '🍌', ':pear:' => '🍐', ':pineapple:' => '🍍',
':sweet_potato:' => '🍠', ':eggplant:' => '🍆', ':tomato:' => '🍅', ':corn:' => '🌽',
':house:' => '🏠', ':house_with_garden:' => '🏡', ':school:' => '🏫', ':office:' => '🏢',
':post_office:' => '🏣', ':hospital:' => '🏥', ':bank:' => '🏦', ':convenience_store:' => '🏪',
':love_hotel:' => '🏩', ':hotel:' => '🏨', ':wedding:' => '💒', ':church:' => '⛪️',
':department_store:' => '🏬', ':european_post_office:' => '🏤', ':city_sunrise:' => '🌇', ':city_sunset:' => '🌆',
':japanese_castle:' => '🏯', ':european_castle:' => '🏰', ':tent:' => '⛺️', ':factory:' => '🏭',
':tokyo_tower:' => '🗼', ':japan:' => '🗾', ':mount_fuji:' => '🗻', ':sunrise_over_mountains:' => '🌄',
':sunrise:' => '🌅', ':stars:' => '🌠', ':statue_of_liberty:' => '🗽', ':bridge_at_night:' => '🌉',
':carousel_horse:' => '🎠', ':rainbow:' => '🌈', ':ferris_wheel:' => '🎡', ':fountain:' => '⛲️',
':roller_coaster:' => '🎢', ':ship:' => '🚢', ':speedboat:' => '🚤', ':boat:' => '⛵️',
':sailboat:' => '⛵️', ':rowboat:' => '🚣', ':anchor:' => '⚓️', ':rocket:' => '🚀',
':airplane:' => '✈️', ':helicopter:' => '🚁', ':steam_locomotive:' => '🚂', ':tram:' => '🚊',
':mountain_railway:' => '🚞', ':bike:' => '🚲', ':aerial_tramway:' => '🚡', ':suspension_railway:' => '🚟',
':mountain_cableway:' => '🚠', ':tractor:' => '🚜', ':blue_car:' => '🚙', ':oncoming_automobile:' => '🚘',
':car:' => '🚗', ':red_car:' => '🚗', ':taxi:' => '🚕', ':oncoming_taxi:' => '🚖',
':articulated_lorry:' => '🚛', ':bus:' => '🚌', ':oncoming_bus:' => '🚍', ':rotating_light:' => '🚨',
':police_car:' => '🚓', ':oncoming_police_car:' => '🚔', ':fire_engine:' => '🚒', ':ambulance:' => '🚑',
':minibus:' => '🚐', ':truck:' => '🚚', ':train:' => '🚋', ':station:' => '🚉',
':train2:' => '🚆', ':bullettrain_front:' => '🚅', ':bullettrain_side:' => '🚄', ':light_rail:' => '🚈',
':monorail:' => '🚝', ':railway_car:' => '🚃', ':trolleybus:' => '🚎', ':ticket:' => '🎫',
':fuelpump:' => '⛽️', ':vertical_traffic_light:' => '🚦', ':traffic_light:' => '🚥', ':warning:' => '⚠️',
':construction:' => '🚧', ':beginner:' => '🔰', ':atm:' => '🏧', ':slot_machine:' => '🎰',
':busstop:' => '🚏', ':barber:' => '💈', ':hotsprings:' => '♨️', ':checkered_flag:' => '🏁',
':crossed_flags:' => '🎌', ':izakaya_lantern:' => '🏮', ':moyai:' => '🗿', ':circus_tent:' => '🎪',
':performing_arts:' => '🎭', ':round_pushpin:' => '📍', ':triangular_flag_on_post:' => '🚩', ':jp:' => '🇯🇵',
':kr:' => '🇰🇷', ':cn:' => '🇨🇳', ':us:' => '🇺🇸', ':fr:' => '🇫🇷',
':es:' => '🇪🇸', ':it:' => '🇮🇹', ':ru:' => '🇷🇺', ':gb:' => '🇬🇧',
':uk:' => '🇬🇧', ':de:' => '🇩🇪', ':one:' => '1⃣', ':two:' => '2⃣',
':three:' => '3⃣', ':four:' => '4⃣', ':five:' => '5⃣', ':six:' => '6⃣',
':seven:' => '7⃣', ':eight:' => '8⃣', ':nine:' => '9⃣', ':keycap_ten:' => '🔟',
':1234:' => '🔢', ':zero:' => '0⃣', ':hash:' => '#️⃣', ':symbols:' => '🔣',
':arrow_backward:' => '◀️', ':arrow_down:' => '⬇️', ':arrow_forward:' => '▶️', ':arrow_left:' => '⬅️',
':capital_abcd:' => '🔠', ':abcd:' => '🔡', ':abc:' => '🔤', ':arrow_lower_left:' => '↙️',
':arrow_lower_right:' => '↘️', ':arrow_right:' => '➡️', ':arrow_up:' => '⬆️', ':arrow_upper_left:' => '↖️',
':arrow_upper_right:' => '↗️', ':arrow_double_down:' => '⏬', ':arrow_double_up:' => '⏫', ':arrow_down_small:' => '🔽',
':arrow_heading_down:' => '⤵️', ':arrow_heading_up:' => '⤴️', ':leftwards_arrow_with_hook:' => '↩️', ':arrow_right_hook:' => '↪️',
':left_right_arrow:' => '↔️', ':arrow_up_down:' => '↕️', ':arrow_up_small:' => '🔼', ':arrows_clockwise:' => '🔃',
':arrows_counterclockwise:' => '🔄', ':rewind:' => '⏪', ':fast_forward:' => '⏩', ':information_source:' => '',
':ok:' => '🆗', ':twisted_rightwards_arrows:' => '🔀', ':repeat:' => '🔁', ':repeat_one:' => '🔂',
':new:' => '🆕', ':top:' => '🔝', ':up:' => '🆙', ':cool:' => '🆒',
':free:' => '🆓', ':ng:' => '🆖', ':cinema:' => '🎦', ':koko:' => '🈁',
':signal_strength:' => '📶', ':u5272:' => '🈹', ':u5408:' => '🈴', ':u55b6:' => '🈺',
':u6307:' => '🈯️', ':u6708:' => '🈷️', ':u6709:' => '🈶', ':u6e80:' => '🈵',
':u7121:' => '🈚️', ':u7533:' => '🈸', ':u7a7a:' => '🈳', ':u7981:' => '🈲',
':sa:' => '🈂️', ':restroom:' => '🚻', ':mens:' => '🚹', ':womens:' => '🚺',
':baby_symbol:' => '🚼', ':no_smoking:' => '🚭', ':parking:' => '🅿️', ':wheelchair:' => '♿️',
':metro:' => '🚇', ':baggage_claim:' => '🛄', ':accept:' => '🉑', ':wc:' => '🚾',
':potable_water:' => '🚰', ':put_litter_in_its_place:' => '🚮', ':secret:' => '㊙️', ':congratulations:' => '㊗️',
':m:' => 'Ⓜ️', ':passport_control:' => '🛂', ':left_luggage:' => '🛅', ':customs:' => '🛃',
':ideograph_advantage:' => '🉐', ':cl:' => '🆑', ':sos:' => '🆘', ':id:' => '🆔',
':no_entry_sign:' => '🚫', ':underage:' => '🔞', ':no_mobile_phones:' => '📵', ':do_not_litter:' => '🚯',
':non-potable_water:' => '🚱', ':no_bicycles:' => '🚳', ':no_pedestrians:' => '🚷', ':children_crossing:' => '🚸',
':no_entry:' => '⛔️', ':eight_spoked_asterisk:' => '✳️', ':eight_pointed_black_star:' => '✴️', ':heart_decoration:' => '💟',
':vs:' => '🆚', ':vibration_mode:' => '📳', ':mobile_phone_off:' => '📴', ':chart:' => '💹',
':currency_exchange:' => '💱', ':aries:' => '♈️', ':taurus:' => '♉️', ':gemini:' => '♊️',
':cancer:' => '♋️', ':leo:' => '♌️', ':virgo:' => '♍️', ':libra:' => '♎️',
':scorpius:' => '♏️', ':sagittarius:' => '♐️', ':capricorn:' => '♑️', ':aquarius:' => '♒️',
':pisces:' => '♓️', ':ophiuchus:' => '⛎', ':six_pointed_star:' => '🔯', ':negative_squared_cross_mark:' => '❎',
':a:' => '🅰️', ':b:' => '🅱️', ':ab:' => '🆎', ':o2:' => '🅾️',
':diamond_shape_with_a_dot_inside:' => '💠', ':recycle:' => '♻️', ':end:' => '🔚', ':on:' => '🔛',
':soon:' => '🔜', ':clock1:' => '🕐', ':clock130:' => '🕜', ':clock10:' => '🕙',
':clock1030:' => '🕥', ':clock11:' => '🕚', ':clock1130:' => '🕦', ':clock12:' => '🕛',
':clock1230:' => '🕧', ':clock2:' => '🕑', ':clock230:' => '🕝', ':clock3:' => '🕒',
':clock330:' => '🕞', ':clock4:' => '🕓', ':clock430:' => '🕟', ':clock5:' => '🕔',
':clock530:' => '🕠', ':clock6:' => '🕕', ':clock630:' => '🕡', ':clock7:' => '🕖',
':clock730:' => '🕢', ':clock8:' => '🕗', ':clock830:' => '🕣', ':clock9:' => '🕘',
':clock930:' => '🕤', ':heavy_dollar_sign:' => '💲', ':copyright:' => '©️', ':registered:' => '®️',
':tm:' => '™️', ':x:' => '❌', ':heavy_exclamation_mark:' => '❗️', ':bangbang:' => '‼️',
':interrobang:' => '⁉️', ':o:' => '⭕️', ':heavy_multiplication_x:' => '✖️', ':heavy_plus_sign:' => '',
':heavy_minus_sign:' => '', ':heavy_division_sign:' => '➗', ':white_flower:' => '💮', ':100:' => '💯',
':heavy_check_mark:' => '✔️', ':ballot_box_with_check:' => '☑️', ':radio_button:' => '🔘', ':link:' => '🔗',
':curly_loop:' => '➰', ':wavy_dash:' => '〰️', ':part_alternation_mark:' => '〽️', ':trident:' => '🔱',
':white_check_mark:' => '✅', ':black_square_button:' => '🔲', ':white_square_button:' => '🔳', ':black_circle:' => '⚫️',
':white_circle:' => '⚪️', ':red_circle:' => '🔴', ':large_blue_circle:' => '🔵', ':large_blue_diamond:' => '🔷',
':large_orange_diamond:' => '🔶', ':small_blue_diamond:' => '🔹', ':small_orange_diamond:' => '🔸', ':small_red_triangle:' => '🔺',
':small_red_triangle_down:' => '🔻', ':black_small_square:' => '▪️', ':black_medium_small_square:' => '◾', ':black_medium_square:' => '◼️',
':black_large_square:' => '⬛', ':white_small_square:' => '▫️', ':white_medium_small_square:' => '◽', ':white_medium_square:' => '◻️',
':white_large_square:' => '⬜',
];
if (\preg_match('/^(:)([^: ]*?)(:)/', $excerpt['text'], $matches)) {
return [
'extent' => \strlen($matches[0]),
'element' => [
'text' => \str_replace(\array_keys($emojiMap), $emojiMap, $matches[0]),
],
];
}
}
// Inline Marks
protected function inlineMark($excerpt)
{
if (\preg_match('/^(==)([^=]*?)(==)/', $excerpt['text'], $matches)) {
return [
'extent' => \strlen($matches[0]),
'element' => [
'name' => 'mark',
'text' => $matches[2],
],
];
}
}
// Inline Keystrokes
protected function inlineKeystrokes($excerpt)
{
if (\preg_match('/^(?<!\[)(?:\[\[([^\[\]]*|[\[\]])\]\])(?!\])/s', $excerpt['text'], $matches)) {
return [
'extent' => \strlen($matches[0]),
'element' => [
'name' => 'kbd',
'text' => $matches[1],
],
];
}
}
// Inline Superscript
protected function inlineSuperscript($excerpt)
{
if (\preg_match('/(?:\^(?!\^)([^\^ ]*)\^(?!\^))/', $excerpt['text'], $matches)) {
return [
'extent' => \strlen($matches[0]),
'element' => [
'name' => 'sup',
'text' => $matches[1],
'function' => 'lineElements',
],
];
}
}
// Inline Subscript
protected function inlineSubscript($excerpt)
{
if (\preg_match('/(?:~(?!~)([^~ ]*)~(?!~))/', $excerpt['text'], $matches)) {
return [
'extent' => \strlen($matches[0]),
'element' => [
'name' => 'sub',
'text' => $matches[1],
'function' => 'lineElements',
],
];
}
}
// Inline typographer
protected function inlineTypographer($excerpt)
{
$substitutions = [
'/\(c\)/i' => '&copy;',
'/\(r\)/i' => '&reg;',
'/\(tm\)/i' => '&trade;',
'/\(p\)/i' => '&para;',
'/\+-/i' => '&plusmn;',
'/\.{4,}|\.{2}/i' => '...',
'/\!\.{3,}/i' => '!..',
'/\?\.{3,}/i' => '?..',
];
if (\preg_match('/\+-|\(p\)|\(tm\)|\(r\)|\(c\)|\.{2,}|\!\.{3,}|\?\.{3,}/i', $excerpt['text'], $matches)) {
return [
'extent' => \strlen($matches[0]),
'element' => [
'rawHtml' => \preg_replace(\array_keys($substitutions), \array_values($substitutions), $matches[0]),
],
];
}
}
// Inline Smartypants
protected function inlineSmartypants($excerpt)
{
// Substitutions
$backtickDoublequoteOpen = $this->options['smarty']['substitutions']['left-double-quote'] ?? '&ldquo;';
$backtickDoublequoteClose = $this->options['smarty']['substitutions']['right-double-quote'] ?? '&rdquo;';
$smartDoublequoteOpen = $this->options['smarty']['substitutions']['left-double-quote'] ?? '&ldquo;';
$smartDoublequoteClose = $this->options['smarty']['substitutions']['right-double-quote'] ?? '&rdquo;';
$smartSinglequoteOpen = $this->options['smarty']['substitutions']['left-single-quote'] ?? '&lsquo;';
$smartSinglequoteClose = $this->options['smarty']['substitutions']['right-single-quote'] ?? '&rsquo;';
$leftAngleQuote = $this->options['smarty']['substitutions']['left-angle-quote'] ?? '&laquo;';
$rightAngleQuote = $this->options['smarty']['substitutions']['right-angle-quote'] ?? '&raquo;';
if (\preg_match('/(``)(?!\s)([^"\'`]{1,})(\'\')|(\")(?!\s)([^\"]{1,})(\")|(\')(?!\s)([^\']{1,})(\')|(<{2})(?!\s)([^<>]{1,})(>{2})|(\.{3})|(-{3})|(-{2})/i', $excerpt['text'], $matches)) {
$matches = \array_values(\array_filter($matches));
// Smart backticks
$smartBackticks = $this->options['smarty']['smart_backticks'] ?? false;
if ($smartBackticks && $matches[1] === '``') {
$length = \strlen(\trim($excerpt['before']));
if ($length > 0) {
return;
}
return [
'extent' => \strlen($matches[0]),
'element' => [
'text' => \html_entity_decode($backtickDoublequoteOpen).$matches[2].\html_entity_decode($backtickDoublequoteClose),
],
];
}
// Smart quotes
$smartQuotes = $this->options['smarty']['smart_quotes'] ?? true;
if ($smartQuotes) {
if ($matches[1] === "'") {
$length = \strlen(\trim($excerpt['before']));
if ($length > 0) {
return;
}
return [
'extent' => \strlen($matches[0]),
'element' => [
'text' => \html_entity_decode($smartSinglequoteOpen).$matches[2].\html_entity_decode($smartSinglequoteClose),
],
];
}
if ($matches[1] === '"') {
$length = \strlen(\trim($excerpt['before']));
if ($length > 0) {
return;
}
return [
'extent' => \strlen($matches[0]),
'element' => [
'text' => \html_entity_decode($smartDoublequoteOpen).$matches[2].\html_entity_decode($smartDoublequoteClose),
],
];
}
}
// Smart angled quotes
$smartAngledQuotes = $this->options['smarty']['smart_angled_quotes'] ?? true;
if ($smartAngledQuotes && $matches[1] === '<<') {
$length = \strlen(\trim($excerpt['before']));
if ($length > 0) {
return;
}
return [
'extent' => \strlen($matches[0]),
'element' => [
'text' => \html_entity_decode($leftAngleQuote).$matches[2].\html_entity_decode($rightAngleQuote),
],
];
}
// Smart dashes
$smartDashes = $this->options['smarty']['smart_dashes'] ?? true;
if ($smartDashes) {
if ($matches[1] === '---') {
return [
'extent' => \strlen($matches[0]),
'element' => [
'rawHtml' => $this->options['smarty']['substitutions']['mdash'] ?? '&mdash;',
],
];
}
if ($matches[1] === '--') {
return [
'extent' => \strlen($matches[0]),
'element' => [
'rawHtml' => $this->options['smarty']['substitutions']['ndash'] ?? '&ndash;',
],
];
}
}
// Smart ellipses
$smartEllipses = $this->options['smarty']['smart_ellipses'] ?? true;
if ($smartEllipses && $matches[1] === '...') {
return [
'extent' => \strlen($matches[0]),
'element' => [
'rawHtml' => $this->options['smarty']['substitutions']['ellipses'] ?? '&hellip;',
],
];
}
}
}
// Inline Math
protected function inlineMath($excerpt)
{
$matchSingleDollar = $this->options['math']['single_dollar'] ?? false;
// Inline Matches
if ($matchSingleDollar) {
// Match single dollar - experimental
if (\preg_match('/^(?<!\\\\)((?<!\$)\$(?!\$)(.*?)(?<!\$)\$(?!\$)|(?<!\\\\\()\\\\\((.*?)(?<!\\\\\()\\\\\)(?!\\\\\)))/s', $excerpt['text'], $matches)) {
$mathMatch = $matches[0];
}
} elseif (\preg_match('/^(?<!\\\\\()\\\\\((.*?)(?<!\\\\\()\\\\\)(?!\\\\\))/s', $excerpt['text'], $matches)) {
$mathMatch = $matches[0];
}
if (isset($mathMatch)) {
return [
'extent' => \strlen($mathMatch),
'element' => [
'text' => $mathMatch,
],
];
}
}
protected function inlineEscapeSequence($excerpt)
{
$element = [
'element' => [
'rawHtml' => $excerpt['text'][1],
],
'extent' => 2,
];
$state = $this->options['math'] ?? false;
if ($state) {
if (isset($excerpt['text'][1]) && \in_array($excerpt['text'][1], $this->specialCharacters) && !\preg_match('/^(?<!\\\\)(?<!\\\\\()\\\\\((.{2,}?)(?<!\\\\\()\\\\\)(?!\\\\\))/s', $excerpt['text'])) {
return $element;
}
} elseif (isset($excerpt['text'][1]) && \in_array($excerpt['text'][1], $this->specialCharacters)) {
return $element;
}
}
/**
* ------------------------------------------------------------------------
* Blocks.
* ------------------------------------------------------------------------
*/
protected function blockFootnote($line, array $_ = null)
{
$state = $this->options['footnotes'] ?? true;
if ($state) {
return $this->blockFootnoteBase($line);
}
}
protected function blockDefinitionList($line, $block)
{
$state = $this->options['definition_lists'] ?? true;
if ($state) {
return $this->blockDefinitionListBase($line, $block);
}
}
protected function blockCode($line, $block = null)
{
$codeBlock = $this->options['code']['blocks'] ?? true;
$codeMain = $this->options['code'] ?? true;
if ($codeBlock === true && $codeMain === true) {
return $this->blockCodeBase($line, $block);
}
}
protected function blockComment($line, array $_ = null)
{
$state = $this->options['comments'] ?? true;
if ($state) {
return $this->blockCommentBase($line);
}
}
protected function blockHeader($line, array $_ = null)
{
$state = $this->options['headings'] ?? true;
if (!$state) {
return;
}
$block = $this->blockHeaderBase($line);
if (!empty($block)) {
// Get the text of the heading
if (isset($block['element']['handler']['argument'])) {
$text = $block['element']['handler']['argument'];
}
// Get the heading level. Levels are h1, h2, ..., h6
$level = $block['element']['name'];
$headersAllowed = $this->options['headings']['allowed'] ?? ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
if (!\in_array($level, $headersAllowed)) {
return;
}
// Checks if auto generated anchors is allowed
$autoAnchors = $this->options['headings']['auto_anchors'] ?? true;
if ($autoAnchors) {
// Get the anchor of the heading to link from the ToC list
$id = $block['element']['attributes']['id'] ?? $this->createAnchorID($text);
} else {
// Get the anchor of the heading to link from the ToC list
$id = $block['element']['attributes']['id'] ?? null;
}
// Set attributes to head tags
$block['element']['attributes']['id'] = $id;
$tocHeaders = $this->options['toc']['headings'] ?? ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
// Check if level are defined as a heading
if (\in_array($level, $tocHeaders)) {
// Add/stores the heading element info to the ToC list
$this->setContentsList([
'text' => $text,
'id' => $id,
'level' => $level,
]);
}
return $block;
}
}
protected function blockList($line, array $CurrentBlock = null)
{
$state = $this->options['lists'] ?? true;
if ($state) {
return $this->blockListBase($line, $CurrentBlock);
}
}
protected function blockQuote($line, array $_ = null)
{
$state = $this->options['qoutes'] ?? true;
if ($state) {
return $this->blockQuoteBase($line);
}
}
protected function blockRule($line, array $_ = null)
{
$state = $this->options['thematic_breaks'] ?? true;
if ($state) {
return $this->blockRuleBase($line);
}
}
protected function blockSetextHeader($line, $block = null)
{
$state = $this->options['headings'] ?? true;
if (!$state) {
return;
}
$block = $this->blockSetextHeaderBase($line, $block);
if (!empty($block)) {
// Get the text of the heading
if (isset($block['element']['handler']['argument'])) {
$text = $block['element']['handler']['argument'];
}
// Get the heading level. Levels are h1, h2, ..., h6
$level = $block['element']['name'];
$headersAllowed = $this->options['headings']['allowed'] ?? ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
if (!\in_array($level, $headersAllowed)) {
return;
}
// Checks if auto generated anchors is allowed
$autoAnchors = $this->options['headings']['auto_anchors'] ?? true;
if ($autoAnchors) {
// Get the anchor of the heading to link from the ToC list
$id = $block['element']['attributes']['id'] ?? $this->createAnchorID($text);
} else {
// Get the anchor of the heading to link from the ToC list
$id = $block['element']['attributes']['id'] ?? null;
}
// Set attributes to head tags
$block['element']['attributes']['id'] = $id;
$headersAllowed = $this->options['headings']['allowed'] ?? ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
// Check if level are defined as a heading
if (\in_array($level, $headersAllowed)) {
// Add/stores the heading element info to the ToC list
$this->setContentsList([
'text' => $text,
'id' => $id,
'level' => $level,
]);
}
return $block;
}
}
protected function blockMarkup($line, array $_ = null)
{
$state = $this->options['markup'] ?? true;
if ($state) {
return $this->blockMarkupBase($line);
}
}
protected function blockReference($line, array $_ = null)
{
$state = $this->options['references'] ?? true;
if ($state) {
return $this->blockReferenceBase($line);
}
}
protected function blockTable($line, $block = null)
{
$state = $this->options['tables'] ?? true;
if ($state) {
return $this->blockTableBase($line, $block);
}
}
protected function blockAbbreviation($line, array $_ = null)
{
$allowCustomAbbr = $this->options['abbreviations']['allow_custom_abbr'] ?? true;
$state = $this->options['abbreviations'] ?? true;
if ($state) {
if (isset($this->options['abbreviations']['predefine'])) {
foreach ($this->options['abbreviations']['predefine'] as $abbreviations => $description) {
$this->DefinitionData['Abbreviation'][$abbreviations] = $description;
}
}
if ($allowCustomAbbr == true) {
return $this->blockAbbreviationBase($line);
}
return;
}
}
// Block Math
protected function blockMath($line, array $_ = null)
{
$block = [
'element' => [
'text' => '',
],
];
if (\preg_match('/^(?<!\\\\)(\\\\\[)(?!.)$/', $line['text'])) {
$block['end'] = '\]';
return $block;
}
if (\preg_match('/^(?<!\\\\)(\$\$)(?!.)$/', $line['text'])) {
$block['end'] = '$$';
return $block;
}
}
// ~
protected function blockMathContinue($line, $block)
{
if (isset($block['complete'])) {
return;
}
if (isset($block['interrupted'])) {
$block['element']['text'] .= \str_repeat(
"\n",
$block['interrupted']
);
unset($block['interrupted']);
}
if (\preg_match('/^(?<!\\\\)(\\\\\])$/', $line['text']) && $block['end'] === '\]') {
$block['complete'] = true;
$block['math'] = true;
$block['element']['text'] =
'\\['.$block['element']['text'].'\\]';
return $block;
}
if (\preg_match('/^(?<!\\\\)(\$\$)$/', $line['text']) && $block['end'] === '$$') {
$block['complete'] = true;
$block['math'] = true;
$block['element']['text'] = '$$'.$block['element']['text'].'$$';
return $block;
}
$block['element']['text'] .= "\n".$line['body'];
// ~
return $block;
}
// ~
protected function blockMathComplete($block)
{
return $block;
}
// Block Fenced Code
protected function blockFencedCode($line, array $_ = null)
{
$codeBlock = $this->options['code']['blocks'] ?? true;
$codeMain = $this->options['code'] ?? true;
if ($codeBlock === false || $codeMain === false) {
return;
}
$block = $this->blockFencedCodeBase($line);
$marker = $line['text'][0];
$openerLength = \strspn($line['text'], $marker);
$language = \trim(
\preg_replace('/^`{3}([^\s]+)(.+)?/s', '$1', $line['text'])
);
$state = $this->options['diagrams'] ?? true;
if ($state) {
// Mermaid.js https://mermaidjs.github.io
if (\strtolower($language) == 'mermaid') {
$element = [
'text' => '',
];
return [
'char' => $marker,
'openerLength' => $openerLength,
'element' => [
'element' => $element,
'name' => 'div',
'attributes' => [
'class' => 'mermaid',
],
],
];
}
// Chart.js https://www.chartjs.org/
if (\strtolower($language) == 'chart') {
$element = [
'text' => '',
];
return [
'char' => $marker,
'openerLength' => $openerLength,
'element' => [
'element' => $element,
'name' => 'canvas',
'attributes' => [
'class' => 'chartjs',
],
],
];
}
}
return $block;
}
// Parsedown Tablespan from @KENNYSOFT
protected function blockTableComplete(array $block)
{
$state = $this->options['tables']['tablespan'] ?? false;
if ($state === false) {
return $block;
}
if (!isset($block)) {
return null;
}
$HeaderElements = &$block['element']['elements'][0]['elements'][0]['elements'];
for ($index = \count($HeaderElements) - 1; $index >= 0; --$index) {
$colspan = 1;
$HeaderElement = &$HeaderElements[$index];
while ($index && $HeaderElements[$index - 1]['handler']['argument'] === '>') {
++$colspan;
$PreviousHeaderElement = &$HeaderElements[--$index];
$PreviousHeaderElement['merged'] = true;
if (isset($PreviousHeaderElement['attributes'])) {
$HeaderElement['attributes'] = $PreviousHeaderElement['attributes'];
}
}
if ($colspan > 1) {
if (!isset($HeaderElement['attributes'])) {
$HeaderElement['attributes'] = [];
}
$HeaderElement['attributes']['colspan'] = $colspan;
}
}
for ($index = \count($HeaderElements) - 1; $index >= 0; --$index) {
if (isset($HeaderElements[$index]['merged'])) {
\array_splice($HeaderElements, $index, 1);
}
}
$rows = &$block['element']['elements'][1]['elements'];
foreach ($rows as $rowNo => &$row) {
$elements = &$row['elements'];
for ($index = \count($elements) - 1; $index >= 0; --$index) {
$colspan = 1;
$element = &$elements[$index];
while ($index && $elements[$index - 1]['handler']['argument'] === '>') {
++$colspan;
$PreviousElement = &$elements[--$index];
$PreviousElement['merged'] = true;
if (isset($PreviousElement['attributes'])) {
$element['attributes'] = $PreviousElement['attributes'];
}
}
if ($colspan > 1) {
if (!isset($element['attributes'])) {
$element['attributes'] = [];
}
$element['attributes']['colspan'] = $colspan;
}
}
}
foreach ($rows as $rowNo => &$row) {
$elements = &$row['elements'];
foreach ($elements as $index => &$element) {
$rowspan = 1;
if (isset($element['merged'])) {
continue;
}
while ($rowNo + $rowspan < \count($rows) && $index < \count($rows[$rowNo + $rowspan]['elements']) && $rows[$rowNo + $rowspan]['elements'][$index]['handler']['argument'] === '^' && (@$element['attributes']['colspan'] ?: null) === (@$rows[$rowNo + $rowspan]['elements'][$index]['attributes']['colspan'] ?: null)) {
$rows[$rowNo + $rowspan]['elements'][$index]['merged'] = true;
++$rowspan;
}
if ($rowspan > 1) {
if (!isset($element['attributes'])) {
$element['attributes'] = [];
}
$element['attributes']['rowspan'] = $rowspan;
}
}
}
foreach ($rows as $rowNo => &$row) {
$elements = &$row['elements'];
for ($index = \count($elements) - 1; $index >= 0; --$index) {
if (isset($elements[$index]['merged'])) {
\array_splice($elements, $index, 1);
}
}
}
return $block;
}
/*
* Checkbox
* -------------------------------------------------------------------------
*/
protected function blockCheckbox($line)
{
$text = \trim($line['text']);
$beginLine = \substr($text, 0, 4);
if ($beginLine === '[ ] ') {
return [
'handler' => 'checkboxUnchecked',
'text' => \substr(\trim($text), 4),
];
}
if ($beginLine === '[x] ') {
return [
'handler' => 'checkboxChecked',
'text' => \substr(\trim($text), 4),
];
}
}
protected function blockCheckboxContinue(array $block) : void
{
// This is here because Parsedown require it.
}
protected function blockCheckboxComplete(array $block)
{
$block['element'] = [
'rawHtml' => $this->{$block['handler']}($block['text']),
'allowRawHtmlInSafeMode' => true,
];
return $block;
}
protected function checkboxUnchecked($text) : string
{
if ($this->markupEscaped || $this->safeMode) {
$text = self::escape($text);
}
return '<input type="checkbox" disabled /> '.$this->format($text);
}
protected function checkboxChecked($text) : string
{
if ($this->markupEscaped || $this->safeMode) {
$text = self::escape($text);
}
return '<input type="checkbox" checked disabled /> '.$this->format($text);
}
/**
* ------------------------------------------------------------------------
* Helpers.
* ------------------------------------------------------------------------.
*/
/**
* Formats the checkbox label without double escaping.
*/
protected function format($text)
{
// backup settings
$markupEscaped = $this->markupEscaped;
$safeMode = $this->safeMode;
// disable rules to prevent double escaping.
$this->setMarkupEscaped(false);
$this->setSafeMode(false);
// format line
$text = $this->line($text);
// reset old values
$this->setMarkupEscaped($markupEscaped);
$this->setSafeMode($safeMode);
return $text;
}
protected function parseAttributeData($attributeString)
{
$state = $this->options['special_attributes'] ?? true;
if ($state) {
return $this->parseAttributeDataBase($attributeString);
}
return [];
}
/**
* Encodes the ToC tag to a hashed tag and replace.
*
* This is used to avoid parsing user defined ToC tag which includes "_" in
* their tag such as "[[_toc_]]". Unless it will be parsed as:
* "<p>[[<em>TOC</em>]]</p>"
*/
protected function encodeTagToHash($text)
{
$salt = $this->getSalt();
$tagOrigin = $this->getTagToC();
if (\strpos($text, $tagOrigin) === false) {
return $text;
}
$tagHashed = \hash('sha256', $salt.$tagOrigin);
return \str_replace($tagOrigin, $tagHashed, $text);
}
/**
* Decodes the hashed ToC tag to an original tag and replaces.
*
* This is used to avoid parsing user defined ToC tag which includes "_" in
* their tag such as "[[_toc_]]". Unless it will be parsed as:
* "<p>[[<em>TOC</em>]]</p>"
*/
protected function decodeTagFromHash($text)
{
$salt = $this->getSalt();
$tagOrigin = $this->getTagToC();
$tagHashed = \hash('sha256', $salt.$tagOrigin);
if (\strpos($text, $tagHashed) === false) {
return $text;
}
return \str_replace($tagHashed, $tagOrigin, $text);
}
/**
* Unique string to use as a salt value.
*/
protected function getSalt()
{
static $salt;
if (isset($salt)) {
return $salt;
}
$salt = \hash('md5', (string) \time());
return $salt;
}
/**
* Gets the markdown tag for ToC.
*/
protected function getTagToC()
{
return $this->options['toc']['set_toc_tag'] ?? '[toc]';
}
/**
* Gets the ID attribute of the ToC for HTML tags.
*/
protected function getIdAttributeToC()
{
if (!empty($this->idToc)) {
return $this->idToc;
}
return self::ID_ATTRIBUTE_DEFAULT;
}
/**
* Generates an anchor text that are link-able even if the heading is not in
* ASCII.
*/
protected function createAnchorID($str) : string
{
$optionUrlEncode = $this->options['toc']['urlencode'] ?? false;
if ($optionUrlEncode) {
// Check AnchorID is unique
$str = $this->incrementAnchorId($str);
return \urlencode($str);
}
$charMap = [
// Latin
'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'AA', 'Æ' => 'AE', 'Ç' => 'C',
'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I',
'Ð' => 'D', 'Ñ' => 'N', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', 'Ő' => 'O',
'Ø' => 'OE', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', 'Ű' => 'U', 'Ý' => 'Y', 'Þ' => 'TH',
'ß' => 'ss',
'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'aa', 'æ' => 'ae', 'ç' => 'c',
'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i',
'ð' => 'd', 'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ő' => 'o',
'ø' => 'oe', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'ű' => 'u', 'ý' => 'y', 'þ' => 'th',
'ÿ' => 'y',
// Latin symbols
'©' => '(c)', '®' => '(r)', '™' => '(tm)',
// Greek
'Α' => 'A', 'Β' => 'B', 'Γ' => 'G', 'Δ' => 'D', 'Ε' => 'E', 'Ζ' => 'Z', 'Η' => 'H', 'Θ' => '8',
'Ι' => 'I', 'Κ' => 'K', 'Λ' => 'L', 'Μ' => 'M', 'Ν' => 'N', 'Ξ' => '3', 'Ο' => 'O', 'Π' => 'P',
'Ρ' => 'R', 'Σ' => 'S', 'Τ' => 'T', 'Υ' => 'Y', 'Φ' => 'F', 'Χ' => 'X', 'Ψ' => 'PS', 'Ω' => 'W',
'Ά' => 'A', 'Έ' => 'E', 'Ί' => 'I', 'Ό' => 'O', 'Ύ' => 'Y', 'Ή' => 'H', 'Ώ' => 'W', 'Ϊ' => 'I',
'Ϋ' => 'Y',
'α' => 'a', 'β' => 'b', 'γ' => 'g', 'δ' => 'd', 'ε' => 'e', 'ζ' => 'z', 'η' => 'h', 'θ' => '8',
'ι' => 'i', 'κ' => 'k', 'λ' => 'l', 'μ' => 'm', 'ν' => 'n', 'ξ' => '3', 'ο' => 'o', 'π' => 'p',
'ρ' => 'r', 'σ' => 's', 'τ' => 't', 'υ' => 'y', 'φ' => 'f', 'χ' => 'x', 'ψ' => 'ps', 'ω' => 'w',
'ά' => 'a', 'έ' => 'e', 'ί' => 'i', 'ό' => 'o', 'ύ' => 'y', 'ή' => 'h', 'ώ' => 'w', 'ς' => 's',
'ϊ' => 'i', 'ΰ' => 'y', 'ϋ' => 'y', 'ΐ' => 'i',
// Turkish
'Ş' => 'S', 'İ' => 'I', 'Ğ' => 'G',
'ş' => 's', 'ı' => 'i', 'ğ' => 'g',
// Russian
'А' => 'A', 'Б' => 'B', 'В' => 'V', 'Г' => 'G', 'Д' => 'D', 'Е' => 'E', 'Ё' => 'Yo', 'Ж' => 'Zh',
'З' => 'Z', 'И' => 'I', 'Й' => 'J', 'К' => 'K', 'Л' => 'L', 'М' => 'M', 'Н' => 'N', 'О' => 'O',
'П' => 'P', 'Р' => 'R', 'С' => 'S', 'Т' => 'T', 'У' => 'U', 'Ф' => 'F', 'Х' => 'H', 'Ц' => 'C',
'Ч' => 'Ch', 'Ш' => 'Sh', 'Щ' => 'Sh', 'Ъ' => '', 'Ы' => 'Y', 'Ь' => '', 'Э' => 'E', 'Ю' => 'Yu',
'Я' => 'Ya',
'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e', 'ё' => 'yo', 'ж' => 'zh',
'з' => 'z', 'и' => 'i', 'й' => 'j', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n', 'о' => 'o',
'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'c',
'ч' => 'ch', 'ш' => 'sh', 'щ' => 'sh', 'ъ' => '', 'ы' => 'y', 'ь' => '', 'э' => 'e', 'ю' => 'yu',
'я' => 'ya',
// Ukrainian
'Є' => 'Ye', 'І' => 'I', 'Ї' => 'Yi', 'Ґ' => 'G',
'є' => 'ye', 'і' => 'i', 'ї' => 'yi', 'ґ' => 'g',
// Czech
'Č' => 'C', 'Ď' => 'D', 'Ě' => 'E', 'Ň' => 'N', 'Ř' => 'R', 'Š' => 'S', 'Ť' => 'T', 'Ů' => 'U',
'Ž' => 'Z',
'č' => 'c', 'ď' => 'd', 'ě' => 'e', 'ň' => 'n', 'ř' => 'r', 'š' => 's', 'ť' => 't', 'ů' => 'u',
'ž' => 'z',
// Polish
'Ą' => 'A', 'Ć' => 'C', 'Ę' => 'e', 'Ł' => 'L', 'Ń' => 'N', 'Ś' => 'S', 'Ź' => 'Z',
'Ż' => 'Z',
'ą' => 'a', 'ć' => 'c', 'ę' => 'e', 'ł' => 'l', 'ń' => 'n', 'ś' => 's', 'ź' => 'z',
'ż' => 'z',
// Latvian
'Ā' => 'A', 'Ē' => 'E', 'Ģ' => 'G', 'Ī' => 'i', 'Ķ' => 'k', 'Ļ' => 'L', 'Ņ' => 'N', 'Ū' => 'u',
'ā' => 'a', 'ē' => 'e', 'ģ' => 'g', 'ī' => 'i', 'ķ' => 'k', 'ļ' => 'l', 'ņ' => 'n', 'ū' => 'u',
];
// Transliterate characters to ASCII
$optionTransliterate = $this->options['toc']['transliterate'] ?? false;
if ($optionTransliterate) {
$str = \str_replace(\array_keys($charMap), $charMap, $str);
}
// Replace non-alphanumeric characters with our delimiter
$optionDelimiter = $this->options['toc']['delimiter'] ?? '-';
$str = \preg_replace('/[^\p{L}\p{Nd}]+/u', $optionDelimiter, $str);
// Remove duplicate delimiters
$str = \preg_replace('/('.\preg_quote($optionDelimiter, '/').'){2,}/', '$1', $str);
// Truncate slug to max. characters
$optionLimit = $this->options['toc']['limit'] ?? \mb_strlen($str, 'UTF-8');
$str = \mb_substr($str, 0, $optionLimit, 'UTF-8');
// Remove delimiter from ends
$str = \trim($str, $optionDelimiter);
$urlLowercase = $this->options['toc']['lowercase'] ?? true;
$str = $urlLowercase ? \mb_strtolower($str, 'UTF-8') : $str;
return $this->incrementAnchorId($str);
}
/**
* Get only the text from a markdown string.
* It parses to HTML once then trims the tags to get the text.
*/
protected function fetchText($text) : string
{
return \trim(\strip_tags($this->line($text)));
}
/**
* Set/stores the heading block to ToC list in a string and array format.
*/
protected function setContentsList(array $Content) : void
{
// Stores as an array
$this->setContentsListAsArray($Content);
// Stores as string in markdown list format.
$this->setContentsListAsString($Content);
}
/**
* Sets/stores the heading block info as an array.
*/
protected function setContentsListAsArray(array $Content) : void
{
$this->contentsListArray[] = $Content;
}
/**
* Sets/stores the heading block info as a list in markdown format.
*/
protected function setContentsListAsString(array $Content) : void
{
$text = $this->fetchText($Content['text']);
$id = $Content['id'];
$level = (int) \trim($Content['level'], 'h');
$link = "[{$text}](#{$id})";
if ($this->firstHeadLevel === 0) {
$this->firstHeadLevel = $level;
}
$cutIndent = $this->firstHeadLevel - 1;
$level = $cutIndent > $level ? 1 : $level - $cutIndent;
$indent = \str_repeat(' ', $level);
// Stores in markdown list format as below:
// - [Header1](#Header1)
// - [Header2-1](#Header2-1)
// - [Header3](#Header3)
// - [Header2-2](#Header2-2)
// ...
$this->contentsListString .= "{$indent}- {$link}".\PHP_EOL;
}
/**
* Collect and count anchors in use to prevent duplicated ids. Return string
* with incremental, numeric suffix. Also init optional blacklist of ids.
*/
protected function incrementAnchorId($str)
{
// add blacklist to list of used anchors
if (!$this->isBlacklistInitialized) {
$this->initBlacklist();
}
$this->anchorDuplicates[$str] = isset($this->anchorDuplicates[$str]) ? ++$this->anchorDuplicates[$str] : 0;
$newStr = $str;
if ($count = $this->anchorDuplicates[$str]) {
$newStr .= "-{$count}";
// increment until conversion doesn't produce new duplicates anymore
if (isset($this->anchorDuplicates[$newStr])) {
$newStr = $this->incrementAnchorId($str);
} else {
$this->anchorDuplicates[$newStr] = 0;
}
}
return $newStr;
}
/**
* Add blacklisted ids to anchor list.
*/
protected function initBlacklist() : void
{
if ($this->isBlacklistInitialized) {
return;
}
if (!empty($this->options['headings']['blacklist']) && \is_array($this->options['headings']['blacklist'])) {
foreach ($this->options['headings']['blacklist'] as $v) {
if (\is_string($v)) {
$this->anchorDuplicates[$v] = 0;
}
}
}
$this->isBlacklistInitialized = true;
}
protected function lineElements($text, $nonNestables = [])
{
$Elements = [];
$nonNestables = (
empty($nonNestables)
? []
: \array_combine($nonNestables, $nonNestables)
);
// $excerpt is based on the first occurrence of a marker
while ($excerpt = \strpbrk($text, $this->inlineMarkerList)) {
$marker = $excerpt[0];
$markerPosition = \strlen($text) - \strlen($excerpt);
// Get the first char before the marker
$beforeMarkerPosition = $markerPosition - 1;
$charBeforeMarker = $beforeMarkerPosition >= 0 ? $text[$markerPosition - 1] : '';
$Excerpt = ['text' => $excerpt, 'context' => $text, 'before' => $charBeforeMarker];
foreach ($this->InlineTypes[$marker] as $inlineType) {
// check to see if the current inline type is nestable in the current context
if (isset($nonNestables[$inlineType])) {
continue;
}
$Inline = $this->{"inline{$inlineType}"}($Excerpt);
if (!isset($Inline)) {
continue;
}
// makes sure that the inline belongs to "our" marker
if (isset($Inline['position']) && $Inline['position'] > $markerPosition) {
continue;
}
// sets a default inline position
if (!isset($Inline['position'])) {
$Inline['position'] = $markerPosition;
}
// cause the new element to 'inherit' our non nestables
$Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables'])
? \array_merge($Inline['element']['nonNestables'], $nonNestables)
: $nonNestables
;
// the text that comes before the inline
$unmarkedText = \substr($text, 0, $Inline['position']);
// compile the unmarked text
$InlineText = $this->inlineText($unmarkedText);
$Elements[] = $InlineText['element'];
// compile the inline
$Elements[] = $this->extractElement($Inline);
// remove the examined text
$text = \substr($text, $Inline['position'] + $Inline['extent']);
continue 2;
}
// the marker does not belong to an inline
$unmarkedText = \substr($text, 0, $markerPosition + 1);
$InlineText = $this->inlineText($unmarkedText);
$Elements[] = $InlineText['element'];
$text = \substr($text, $markerPosition + 1);
}
$InlineText = $this->inlineText($text);
$Elements[] = $InlineText['element'];
foreach ($Elements as &$Element) {
if (!isset($Element['autobreak'])) {
$Element['autobreak'] = false;
}
}
return $Elements;
}
private function pregReplaceAssoc(array $replace, $subject)
{
return \preg_replace(\array_keys($replace), \array_values($replace), $subject);
}
#
# Blocks
#
#
# Abbreviation
protected function blockAbbreviationBase($Line)
{
if (\preg_match('/^\*\[(.+?)\]:[ ]*(.+?)[ ]*$/', $Line['text'], $matches))
{
$this->DefinitionData['Abbreviation'][$matches[1]] = $matches[2];
return [
'hidden' => true,
];
}
}
#
# Footnote
protected function blockFootnoteBase($Line)
{
if (\preg_match('/^\[\^(.+?)\]:[ ]?(.*)$/', $Line['text'], $matches))
{
return [
'label' => $matches[1],
'text' => $matches[2],
'hidden' => true,
];
}
}
protected function blockFootnoteContinue($Line, $Block)
{
if ($Line['text'][0] === '[' && \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']] = [
'text' => $Block['text'],
'count' => null,
'number' => null,
];
return $Block;
}
#
# Definition List
protected function blockDefinitionListBase($Line, $Block)
{
if (!isset($Block) || $Block['type'] !== 'Paragraph')
{
return;
}
$Element = [
'name' => 'dl',
'elements' => [],
];
$terms = \explode("\n", $Block['element']['handler']['argument']);
foreach ($terms as $term)
{
$Element['elements'] []= [
'name' => 'dt',
'handler' => [
'function' => 'lineElements',
'argument' => $term,
'destination' => 'elements',
],
];
}
$Block['element'] = $Element;
return $this->addDdElement($Line, $Block);
}
protected function blockDefinitionListContinue($Line, array $Block)
{
if ($Line['text'][0] === ':')
{
return $this->addDdElement($Line, $Block);
}
else
{
if (isset($Block['interrupted']) && $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 blockHeaderBase($Line)
{
$Block = $this->blockHeaderParent($Line);
if ($Block !== null && \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 blockMarkupBase($Line)
{
if ($this->markupEscaped || $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 = [
'name' => $matches[1],
'depth' => 0,
'element' => [
'rawHtml' => $Line['text'],
'autobreak' => true,
],
];
$length = \strlen($matches[0]);
$remainder = \substr($Line['text'], $length);
if (\trim($remainder) === '')
{
if (isset($matches[2]) || \in_array($matches[1], $this->voidElements))
{
$Block['closed'] = true;
$Block['void'] = true;
}
}
else
{
if (isset($matches[2]) || \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 blockSetextHeaderBase($Line, array $Block = null)
{
$Block = $this->blockSetextHeaderParent($Line, $Block);
if ($Block !== null && \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 = [
'name' => 'sup',
'attributes' => ['id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name],
'element' => [
'name' => 'a',
'attributes' => ['href' => '#fn:'.$name, 'class' => 'footnote-ref'],
'text' => $this->DefinitionData['Footnote'][$name]['number'],
],
];
return [
'extent' => \strlen($matches[0]),
'element' => $Element,
];
}
}
private $footnoteCount = 0;
#
# ~
#
private $currentAbreviation;
private $currentMeaning;
protected function insertAbreviation(array $Element)
{
if (isset($Element['text']))
{
$Element['elements'] = self::pregReplaceElements(
'/\b'.\preg_quote($this->currentAbreviation, '/').'\b/',
[
[
'name' => 'abbr',
'attributes' => [
'title' => $this->currentMeaning,
],
'text' => $this->currentAbreviation,
],
],
$Element['text']
);
unset($Element['text']);
}
return $Element;
}
protected function inlineText($text)
{
$Inline = $this->inlineTextParent($text);
if (isset($this->DefinitionData['Abbreviation']))
{
foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning)
{
$this->currentAbreviation = $abbreviation;
$this->currentMeaning = $meaning;
$Inline['element'] = $this->elementApplyRecursiveDepthFirst(
[$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'] = [
'name' => 'dd',
'handler' => [
'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 = [
'name' => 'div',
'attributes' => ['class' => 'footnotes'],
'elements' => [
['name' => 'hr'],
[
'name' => 'ol',
'elements' => [],
],
],
];
\uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes');
foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData)
{
if (!isset($DefinitionData['number']))
{
continue;
}
$text = $DefinitionData['text'];
$textElements = $this->textElements($text);
$numbers = \range(1, $DefinitionData['count']);
$backLinkElements = [];
foreach ($numbers as $number)
{
$backLinkElements[] = ['text' => ' '];
$backLinkElements[] = [
'name' => 'a',
'attributes' => [
'href' => "#fnref{$number}:{$definitionId}",
'rev' => 'footnote',
'class' => 'footnote-backref',
],
'rawHtml' => '&#8617;',
'allowRawHtmlInSafeMode' => true,
'autobreak' => false,
];
}
unset($backLinkElements[0]);
$n = \count($textElements) - 1;
if ($textElements[$n]['name'] === 'p')
{
$backLinkElements = \array_merge(
[
[
'rawHtml' => '&#160;',
'allowRawHtmlInSafeMode' => true,
],
],
$backLinkElements
);
unset($textElements[$n]['name']);
$textElements[$n] = [
'name' => 'p',
'elements' => \array_merge(
[$textElements[$n]],
$backLinkElements
),
];
}
else
{
$textElements[] = [
'name' => 'p',
'elements' => $backLinkElements,
];
}
$Element['elements'][1]['elements'] []= [
'name' => 'li',
'attributes' => ['id' => 'fn:'.$definitionId],
'elements' => \array_merge(
$textElements
),
];
}
return $Element;
}
# ~
protected function parseAttributeDataBase($attributeString)
{
$Data = [];
$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 && ! \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);
return \str_replace('placeholder\x1A', $elementText, $markup);
}
# ~
protected function sortFootnotes($A, $B) # callback
{
return $A['number'] - $B['number'];
}
#
# Fields
#
protected $regexAttribute = '(?:[#.][-\w]+[ ]*)';
protected function textElements($text)
{
# make sure no definitions are set
$this->DefinitionData = [];
# standardize line breaks
$text = \str_replace(["\r\n", "\r"], "\n", $text);
# remove surrounding line breaks
$text = \trim($text, "\n");
# split text into lines
$lines = \explode("\n", $text);
# iterate through lines to identify blocks
return $this->linesElements($lines);
}
#
# Setters
#
public function setBreaksEnabled($breaksEnabled)
{
$this->breaksEnabled = $breaksEnabled;
return $this;
}
protected $breaksEnabled;
public function setMarkupEscaped($markupEscaped)
{
$this->markupEscaped = $markupEscaped;
return $this;
}
protected $markupEscaped;
public function setUrlsLinked($urlsLinked)
{
$this->urlsLinked = $urlsLinked;
return $this;
}
protected $urlsLinked = true;
public function setSafeMode($safeMode)
{
$this->safeMode = (bool) $safeMode;
return $this;
}
protected $safeMode;
public function setStrictMode($strictMode)
{
$this->strictMode = (bool) $strictMode;
return $this;
}
protected $strictMode;
protected $safeLinksWhitelist = [
'http://',
'https://',
'ftp://',
'ftps://',
'mailto:',
'tel:',
'data:image/png;base64,',
'data:image/gif;base64,',
'data:image/jpeg;base64,',
'irc:',
'ircs:',
'git:',
'ssh:',
'news:',
'steam:',
];
#
# Lines
#
protected $BlockTypes = [
'#' => ['Header'],
'*' => ['Rule', 'List', 'Abbreviation'],
'+' => ['List'],
'-' => ['SetextHeader', 'Table', 'Rule', 'List'],
'0' => ['List'],
'1' => ['List'],
'2' => ['List'],
'3' => ['List'],
'4' => ['List'],
'5' => ['List'],
'6' => ['List'],
'7' => ['List'],
'8' => ['List'],
'9' => ['List'],
':' => ['Table', 'DefinitionList'],
'<' => ['Comment', 'Markup'],
'=' => ['SetextHeader'],
'>' => ['Quote'],
'[' => ['Footnote', 'Reference'],
'_' => ['Rule'],
'`' => ['FencedCode'],
'|' => ['Table'],
'~' => ['FencedCode'],
];
# ~
protected $unmarkedBlockTypes = [
'Code',
];
#
# Blocks
#
protected function lines(array $lines)
{
return $this->elements($this->linesElements($lines));
}
protected function linesElements(array $lines)
{
$Elements = [];
$CurrentBlock = null;
foreach ($lines as $line)
{
if (\rtrim($line) === '')
{
if (isset($CurrentBlock))
{
$CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted'])
? $CurrentBlock['interrupted'] + 1 : 1
);
}
continue;
}
while (($beforeTab = \strstr($line, "\t", true)) !== false)
{
$shortage = 4 - \mb_strlen($beforeTab, 'utf-8') % 4;
$line = $beforeTab
. \str_repeat(' ', $shortage)
. \substr($line, \strlen($beforeTab) + 1)
;
}
$indent = \strspn($line, ' ');
$text = $indent > 0 ? \substr($line, $indent) : $line;
# ~
$Line = ['body' => $line, 'indent' => $indent, 'text' => $text];
# ~
if (isset($CurrentBlock['continuable']))
{
$methodName = 'block' . $CurrentBlock['type'] . 'Continue';
$Block = $this->{$methodName}($Line, $CurrentBlock);
if (isset($Block)) {
$CurrentBlock = $Block;
continue;
} elseif ($this->isBlockCompletable($CurrentBlock['type'])) {
$methodName = 'block' . $CurrentBlock['type'] . 'Complete';
$CurrentBlock = $this->{$methodName}($CurrentBlock);
}
}
# ~
$marker = $text[0];
# ~
$blockTypes = $this->unmarkedBlockTypes;
if (isset($this->BlockTypes[$marker]))
{
foreach ($this->BlockTypes[$marker] as $blockType)
{
$blockTypes []= $blockType;
}
}
#
# ~
foreach ($blockTypes as $blockType)
{
$Block = $this->{"block{$blockType}"}($Line, $CurrentBlock);
if (isset($Block))
{
$Block['type'] = $blockType;
if (!isset($Block['identified']))
{
if (isset($CurrentBlock))
{
$Elements[] = $this->extractElement($CurrentBlock);
}
$Block['identified'] = true;
}
if ($this->isBlockContinuable($blockType))
{
$Block['continuable'] = true;
}
$CurrentBlock = $Block;
continue 2;
}
}
# ~
if (isset($CurrentBlock) && $CurrentBlock['type'] === 'Paragraph')
{
$Block = $this->paragraphContinue($Line, $CurrentBlock);
}
if (isset($Block))
{
$CurrentBlock = $Block;
}
else
{
if (isset($CurrentBlock))
{
$Elements[] = $this->extractElement($CurrentBlock);
}
$CurrentBlock = $this->paragraph($Line);
$CurrentBlock['identified'] = true;
}
}
# ~
if (isset($CurrentBlock['continuable']) && $this->isBlockCompletable($CurrentBlock['type']))
{
$methodName = 'block' . $CurrentBlock['type'] . 'Complete';
$CurrentBlock = $this->{$methodName}($CurrentBlock);
}
# ~
if (isset($CurrentBlock))
{
$Elements[] = $this->extractElement($CurrentBlock);
}
# ~
return $Elements;
}
protected function extractElement(array $Component)
{
if (!isset($Component['element']))
{
if (isset($Component['markup']))
{
$Component['element'] = ['rawHtml' => $Component['markup']];
}
elseif (isset($Component['hidden']))
{
$Component['element'] = [];
}
}
return $Component['element'];
}
protected function isBlockContinuable($Type) : bool
{
return \method_exists($this, 'block' . $Type . 'Continue');
}
protected function isBlockCompletable($Type) : bool
{
return \method_exists($this, 'block' . $Type . 'Complete');
}
#
# Code
protected function blockCodeBase($Line, $Block = null)
{
if (isset($Block) && $Block['type'] === 'Paragraph' && !isset($Block['interrupted']))
{
return;
}
if ($Line['indent'] >= 4)
{
$text = \substr($Line['body'], 4);
return [
'element' => [
'name' => 'pre',
'element' => [
'name' => 'code',
'text' => $text,
],
],
];
}
}
protected function blockCodeContinue($Line, $Block)
{
if ($Line['indent'] >= 4)
{
if (isset($Block['interrupted']))
{
$Block['element']['element']['text'] .= \str_repeat("\n", $Block['interrupted']);
unset($Block['interrupted']);
}
$Block['element']['element']['text'] .= "\n";
$text = \substr($Line['body'], 4);
$Block['element']['element']['text'] .= $text;
return $Block;
}
}
protected function blockCodeComplete($Block)
{
return $Block;
}
#
# Comment
protected function blockCommentBase($Line)
{
if ($this->markupEscaped || $this->safeMode)
{
return;
}
if (\str_starts_with($Line['text'], '<!--'))
{
$Block = [
'element' => [
'rawHtml' => $Line['body'],
'autobreak' => true,
],
];
if (\strpos($Line['text'], '-->') !== false)
{
$Block['closed'] = true;
}
return $Block;
}
}
protected function blockCommentContinue($Line, array $Block)
{
if (isset($Block['closed']))
{
return;
}
$Block['element']['rawHtml'] .= "\n" . $Line['body'];
if (\strpos($Line['text'], '-->') !== false)
{
$Block['closed'] = true;
}
return $Block;
}
#
# Fenced Code
protected function blockFencedCodeBase($Line)
{
$marker = $Line['text'][0];
$openerLength = \strspn($Line['text'], $marker);
if ($openerLength < 3)
{
return;
}
$infostring = \trim(\substr($Line['text'], $openerLength), "\t ");
if (\strpos($infostring, '`') !== false)
{
return;
}
$Element = [
'name' => 'code',
'text' => '',
];
if ($infostring !== '')
{
/**
* 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($infostring, 0, \strcspn($infostring, " \t\n\f\r"));
$Element['attributes'] = ['class' => "language-{$language}"];
}
return [
'char' => $marker,
'openerLength' => $openerLength,
'element' => [
'name' => 'pre',
'element' => $Element,
],
];
}
protected function blockFencedCodeContinue($Line, $Block)
{
if (isset($Block['complete']))
{
return;
}
if (isset($Block['interrupted']))
{
$Block['element']['element']['text'] .= \str_repeat("\n", $Block['interrupted']);
unset($Block['interrupted']);
}
if (($len = \strspn($Line['text'], $Block['char'])) >= $Block['openerLength']
&& \rtrim(\substr($Line['text'], $len), ' ') === ''
) {
$Block['element']['element']['text'] = \substr($Block['element']['element']['text'], 1);
$Block['complete'] = true;
return $Block;
}
$Block['element']['element']['text'] .= "\n" . $Line['body'];
return $Block;
}
protected function blockFencedCodeComplete($Block)
{
return $Block;
}
#
# Header
protected function blockHeaderParent($Line)
{
$level = \strspn($Line['text'], '#');
if ($level > 6)
{
return;
}
$text = \trim($Line['text'], '#');
if ($this->strictMode && isset($text[0]) && $text[0] !== ' ')
{
return;
}
$text = \trim($text, ' ');
return [
'element' => [
'name' => 'h' . $level,
'handler' => [
'function' => 'lineElements',
'argument' => $text,
'destination' => 'elements',
],
],
];
}
#
# List
protected function blockListBase($Line, array $CurrentBlock = null)
{
list($name, $pattern) = $Line['text'][0] <= '-' ? ['ul', '[*+-]'] : ['ol', '[0-9]{1,9}+[.\)]'];
if (\preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches))
{
$contentIndent = \strlen($matches[2]);
if ($contentIndent >= 5)
{
--$contentIndent;
$matches[1] = \substr($matches[1], 0, -$contentIndent);
$matches[3] = \str_repeat(' ', $contentIndent) . $matches[3];
}
elseif ($contentIndent === 0)
{
$matches[1] .= ' ';
}
$markerWithoutWhitespace = \strstr($matches[1], ' ', true);
$Block = [
'indent' => $Line['indent'],
'pattern' => $pattern,
'data' => [
'type' => $name,
'marker' => $matches[1],
'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : \substr($markerWithoutWhitespace, -1)),
],
'element' => [
'name' => $name,
'elements' => [],
],
];
$Block['data']['markerTypeRegex'] = \preg_quote($Block['data']['markerType'], '/');
if ($name === 'ol')
{
$listStart = \ltrim(\strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0';
if ($listStart !== '1')
{
if (
isset($CurrentBlock)
&& $CurrentBlock['type'] === 'Paragraph'
&& !isset($CurrentBlock['interrupted'])
) {
return;
}
$Block['element']['attributes'] = ['start' => $listStart];
}
}
$Block['li'] = [
'name' => 'li',
'handler' => [
'function' => 'li',
'argument' => empty($matches[3]) ? [] : [$matches[3]],
'destination' => 'elements',
],
];
$Block['element']['elements'] []= & $Block['li'];
return $Block;
}
}
protected function blockListContinue($Line, array $Block)
{
if (isset($Block['interrupted']) && empty($Block['li']['handler']['argument']))
{
return null;
}
$requiredIndent = ($Block['indent'] + \strlen($Block['data']['marker']));
if ($Line['indent'] < $requiredIndent
&& (
(
$Block['data']['type'] === 'ol'
&& \preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches)
) || (
$Block['data']['type'] === 'ul'
&& \preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches)
)
)
) {
if (isset($Block['interrupted']))
{
$Block['li']['handler']['argument'] []= '';
$Block['loose'] = true;
unset($Block['interrupted']);
}
unset($Block['li']);
$text = isset($matches[1]) ? $matches[1] : '';
$Block['indent'] = $Line['indent'];
$Block['li'] = [
'name' => 'li',
'handler' => [
'function' => 'li',
'argument' => [$text],
'destination' => 'elements',
],
];
$Block['element']['elements'] []= & $Block['li'];
return $Block;
}
elseif ($Line['indent'] < $requiredIndent && $this->blockList($Line))
{
return null;
}
if ($Line['text'][0] === '[' && $this->blockReference($Line))
{
return $Block;
}
if ($Line['indent'] >= $requiredIndent)
{
if (isset($Block['interrupted']))
{
$Block['li']['handler']['argument'] []= '';
$Block['loose'] = true;
unset($Block['interrupted']);
}
$text = \substr($Line['body'], $requiredIndent);
$Block['li']['handler']['argument'] []= $text;
return $Block;
}
if (!isset($Block['interrupted']))
{
$text = \preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']);
$Block['li']['handler']['argument'] []= $text;
return $Block;
}
}
protected function blockListComplete(array $Block)
{
if (isset($Block['loose']))
{
foreach ($Block['element']['elements'] as &$li)
{
if (\end($li['handler']['argument']) !== '')
{
$li['handler']['argument'] []= '';
}
}
}
return $Block;
}
#
# Quote
protected function blockQuoteBase($Line)
{
if (\preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches))
{
return [
'element' => [
'name' => 'blockquote',
'handler' => [
'function' => 'linesElements',
'argument' => (array) $matches[1],
'destination' => 'elements',
],
],
];
}
}
protected function blockQuoteContinue($Line, array $Block)
{
if (isset($Block['interrupted']))
{
return;
}
if ($Line['text'][0] === '>' && \preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches))
{
$Block['element']['handler']['argument'] []= $matches[1];
return $Block;
}
if (!isset($Block['interrupted']))
{
$Block['element']['handler']['argument'] []= $Line['text'];
return $Block;
}
}
#
# Rule
protected function blockRuleBase($Line)
{
$marker = $Line['text'][0];
if (\substr_count($Line['text'], $marker) >= 3 && \rtrim($Line['text'], " {$marker}") === '')
{
return [
'element' => [
'name' => 'hr',
],
];
}
}
#
# Setext
protected function blockSetextHeaderParent($Line, array $Block = null)
{
if (!isset($Block) || $Block['type'] !== 'Paragraph' || isset($Block['interrupted']))
{
return;
}
if ($Line['indent'] < 4 && \rtrim(\rtrim($Line['text'], ' '), $Line['text'][0]) === '')
{
$Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
return $Block;
}
}
#
# Reference
protected function blockReferenceBase($Line)
{
if (\strpos($Line['text'], ']') !== false
&& \preg_match('/^\[(.+?)\]:[ ]*+<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches)
) {
$id = \strtolower($matches[1]);
$Data = [
'url' => UriFactory::build($matches[2]),
'title' => isset($matches[3]) ? $matches[3] : null,
];
$this->DefinitionData['Reference'][$id] = $Data;
return [
'element' => [],
];
}
}
#
# Table
protected function blockTableBase($Line, array $Block = null)
{
if (!isset($Block) || $Block['type'] !== 'Paragraph' || isset($Block['interrupted']))
{
return;
}
if (
\strpos($Block['element']['handler']['argument'], '|') === false
&& \strpos($Line['text'], '|') === false
&& \strpos($Line['text'], ':') === false
|| \strpos($Block['element']['handler']['argument'], "\n") !== false
) {
return;
}
if (\rtrim($Line['text'], ' -:|') !== '')
{
return;
}
$alignments = [];
$divider = $Line['text'];
$divider = \trim($divider);
$divider = \trim($divider, '|');
$dividerCells = \explode('|', $divider);
foreach ($dividerCells as $dividerCell)
{
$dividerCell = \trim($dividerCell);
if ($dividerCell === '')
{
return;
}
$alignment = null;
if ($dividerCell[0] === ':')
{
$alignment = 'left';
}
if (\substr($dividerCell, - 1) === ':')
{
$alignment = $alignment === 'left' ? 'center' : 'right';
}
$alignments []= $alignment;
}
# ~
$HeaderElements = [];
$header = $Block['element']['handler']['argument'];
$header = \trim($header);
$header = \trim($header, '|');
$headerCells = \explode('|', $header);
if (\count($headerCells) !== \count($alignments))
{
return;
}
foreach ($headerCells as $index => $headerCell)
{
$headerCell = \trim($headerCell);
$HeaderElement = [
'name' => 'th',
'handler' => [
'function' => 'lineElements',
'argument' => $headerCell,
'destination' => 'elements',
],
];
if (isset($alignments[$index]))
{
$alignment = $alignments[$index];
$HeaderElement['attributes'] = [
'style' => "text-align: {$alignment};",
];
}
$HeaderElements []= $HeaderElement;
}
# ~
$Block = [
'alignments' => $alignments,
'identified' => true,
'element' => [
'name' => 'table',
'elements' => [],
],
];
$Block['element']['elements'] []= [
'name' => 'thead',
];
$Block['element']['elements'] []= [
'name' => 'tbody',
'elements' => [],
];
$Block['element']['elements'][0]['elements'] []= [
'name' => 'tr',
'elements' => $HeaderElements,
];
return $Block;
}
protected function blockTableContinue($Line, array $Block)
{
if (isset($Block['interrupted']))
{
return;
}
if (\count($Block['alignments']) === 1 || $Line['text'][0] === '|' || \strpos($Line['text'], '|'))
{
$Elements = [];
$row = $Line['text'];
$row = \trim($row);
$row = \trim($row, '|');
\preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches);
$cells = \array_slice($matches[0], 0, \count($Block['alignments']));
foreach ($cells as $index => $cell)
{
$cell = \trim($cell);
$Element = [
'name' => 'td',
'handler' => [
'function' => 'lineElements',
'argument' => $cell,
'destination' => 'elements',
],
];
if (isset($Block['alignments'][$index]))
{
$Element['attributes'] = [
'style' => 'text-align: ' . $Block['alignments'][$index] . ';',
];
}
$Elements []= $Element;
}
$Element = [
'name' => 'tr',
'elements' => $Elements,
];
$Block['element']['elements'][1]['elements'] []= $Element;
return $Block;
}
}
#
# ~
#
protected function paragraph($Line)
{
return [
'type' => 'Paragraph',
'element' => [
'name' => 'p',
'handler' => [
'function' => 'lineElements',
'argument' => $Line['text'],
'destination' => 'elements',
],
],
];
}
protected function paragraphContinue($Line, array $Block)
{
if (isset($Block['interrupted']))
{
return;
}
$Block['element']['handler']['argument'] .= "\n".$Line['text'];
return $Block;
}
#
# Inline Elements
#
protected $InlineTypes = [
'!' => ['Image'],
'&' => ['SpecialCharacter'],
'*' => ['Emphasis'],
':' => ['Url'],
'<' => ['UrlTag', 'EmailTag', 'Markup'],
'[' => ['FootnoteMarker', 'Link'],
'_' => ['Emphasis'],
'`' => ['Code'],
'~' => ['Strikethrough'],
'\\' => ['EscapeSequence'],
];
# ~
protected $inlineMarkerList = '!*_&[:<`~\\';
#
# ~
#
public function line($text, $nonNestables = [])
{
return $this->elements($this->lineElements($text, $nonNestables));
}
/*
protected function lineElements($text, $nonNestables = array())
{
# standardize line breaks
$text = str_replace(array("\r\n", "\r"), "\n", $text);
$Elements = array();
$nonNestables = (empty($nonNestables)
? array()
: array_combine($nonNestables, $nonNestables)
);
# $excerpt is based on the first occurrence of a marker
while ($excerpt = strpbrk($text, $this->inlineMarkerList))
{
$marker = $excerpt[0];
$markerPosition = strlen($text) - strlen($excerpt);
$Excerpt = array('text' => $excerpt, 'context' => $text);
foreach ($this->InlineTypes[$marker] as $inlineType)
{
# check to see if the current inline type is nestable in the current context
if (isset($nonNestables[$inlineType]))
{
continue;
}
$Inline = $this->{"inline$inlineType"}($Excerpt);
if ( !isset($Inline))
{
continue;
}
# makes sure that the inline belongs to "our" marker
if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
{
continue;
}
# sets a default inline position
if ( !isset($Inline['position']))
{
$Inline['position'] = $markerPosition;
}
# cause the new element to 'inherit' our non nestables
$Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables'])
? array_merge($Inline['element']['nonNestables'], $nonNestables)
: $nonNestables
;
# the text that comes before the inline
$unmarkedText = substr($text, 0, $Inline['position']);
# compile the unmarked text
$InlineText = $this->inlineText($unmarkedText);
$Elements[] = $InlineText['element'];
# compile the inline
$Elements[] = $this->extractElement($Inline);
# remove the examined text
$text = substr($text, $Inline['position'] + $Inline['extent']);
continue 2;
}
# the marker does not belong to an inline
$unmarkedText = substr($text, 0, $markerPosition + 1);
$InlineText = $this->inlineText($unmarkedText);
$Elements[] = $InlineText['element'];
$text = substr($text, $markerPosition + 1);
}
$InlineText = $this->inlineText($text);
$Elements[] = $InlineText['element'];
foreach ($Elements as &$Element)
{
if ( !isset($Element['autobreak']))
{
$Element['autobreak'] = false;
}
}
return $Elements;
}
*/
#
# ~
#
protected function inlineTextParent($text)
{
$Inline = [
'extent' => \strlen($text),
'element' => [],
];
$Inline['element']['elements'] = self::pregReplaceElements(
$this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/',
[
['name' => 'br'],
['text' => "\n"],
],
$text
);
return $Inline;
}
protected function inlineLinkParent($Excerpt)
{
$Element = [
'name' => 'a',
'handler' => [
'function' => 'lineElements',
'argument' => null,
'destination' => 'elements',
],
'nonNestables' => ['Url', 'Link'],
'attributes' => [
'href' => null,
'title' => null,
],
];
$extent = 0;
$remainder = $Excerpt['text'];
if (\preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches))
{
$Element['handler']['argument'] = $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]) !== 0 ? $matches[1] : $Element['handler']['argument'];
$definition = \strtolower($definition);
$extent += \strlen($matches[0]);
}
else
{
$definition = \strtolower($Element['handler']['argument']);
}
if (!isset($this->DefinitionData['Reference'][$definition]))
{
return;
}
$Definition = $this->DefinitionData['Reference'][$definition];
$Element['attributes']['href'] = $Definition['url'];
$Element['attributes']['title'] = $Definition['title'];
}
return [
'extent' => $extent,
'element' => $Element,
];
}
protected function inlineSpecialCharacter($Excerpt)
{
if (\substr($Excerpt['text'], 1, 1) !== ' ' && \strpos($Excerpt['text'], ';') !== false
&& \preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches)
) {
return [
'element' => ['rawHtml' => '&' . $matches[1] . ';'],
'extent' => \strlen($matches[0]),
];
}
}
# ~
protected function unmarkedText($text)
{
$Inline = $this->inlineText($text);
return $this->element($Inline['element']);
}
#
# Handlers
#
protected function handle(array $Element)
{
if (isset($Element['handler']))
{
if (!isset($Element['nonNestables']))
{
$Element['nonNestables'] = [];
}
if (\is_string($Element['handler']))
{
$function = $Element['handler'];
$argument = $Element['text'];
unset($Element['text']);
$destination = 'rawHtml';
}
else
{
$function = $Element['handler']['function'];
$argument = $Element['handler']['argument'];
$destination = $Element['handler']['destination'];
}
$Element[$destination] = $this->{$function}($argument, $Element['nonNestables']);
if ($destination === 'handler')
{
$Element = $this->handle($Element);
}
unset($Element['handler']);
}
return $Element;
}
protected function handleElementRecursive(array $Element)
{
return $this->elementApplyRecursive([$this, 'handle'], $Element);
}
protected function handleElementsRecursive(array $Elements)
{
return $this->elementsApplyRecursive([$this, 'handle'], $Elements);
}
protected function elementApplyRecursive($closure, array $Element)
{
$Element = $closure($Element);
if (isset($Element['elements']))
{
$Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']);
}
elseif (isset($Element['element']))
{
$Element['element'] = $this->elementApplyRecursive($closure, $Element['element']);
}
return $Element;
}
protected function elementApplyRecursiveDepthFirst($closure, array $Element)
{
if (isset($Element['elements']))
{
$Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']);
}
elseif (isset($Element['element']))
{
$Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']);
}
return $closure($Element);
}
protected function elementsApplyRecursive($closure, array $Elements)
{
foreach ($Elements as &$Element)
{
$Element = $this->elementApplyRecursive($closure, $Element);
}
return $Elements;
}
protected function elementsApplyRecursiveDepthFirst($closure, array $Elements)
{
foreach ($Elements as &$Element)
{
$Element = $this->elementApplyRecursiveDepthFirst($closure, $Element);
}
return $Elements;
}
protected function element(array $Element)
{
if ($this->safeMode)
{
$Element = $this->sanitiseElement($Element);
}
# identity map if element has no handler
$Element = $this->handle($Element);
$hasName = isset($Element['name']);
$markup = '';
if ($hasName)
{
$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;
}
$hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']);
if ($hasContent)
{
$markup .= $hasName ? '>' : '';
if (isset($Element['elements']))
{
$markup .= $this->elements($Element['elements']);
}
elseif (isset($Element['element']))
{
$markup .= $this->element($Element['element']);
} elseif (!$permitRawHtml) {
$markup .= self::escape((string) $text, true);
}
else
{
$markup .= $text;
}
$markup .= $hasName ? '</' . $Element['name'] . '>' : '';
}
elseif ($hasName)
{
$markup .= ' />';
}
return $markup;
}
protected function elements(array $Elements) : string
{
$markup = '';
$autoBreak = true;
foreach ($Elements as $Element)
{
if (empty($Element))
{
continue;
}
$autoBreakNext = (isset($Element['autobreak'])
? $Element['autobreak'] : isset($Element['name'])
);
// (autobreak === false) covers both sides of an element
$autoBreak = $autoBreak ? $autoBreakNext : $autoBreak;
$markup .= ($autoBreak ? "\n" : '') . $this->element($Element);
$autoBreak = $autoBreakNext;
}
return $markup . ($autoBreak ? "\n" : '');
}
# ~
protected function li($lines)
{
$Elements = $this->linesElements($lines);
if (! \in_array('', $lines)
&& isset($Elements[0], $Elements[0]['name'])
&& $Elements[0]['name'] === 'p'
) {
unset($Elements[0]['name']);
}
return $Elements;
}
#
# AST Convenience
#
/**
* Replace occurrences $regexp with $Elements in $text. Return an array of
* elements representing the replacement.
*/
protected static function pregReplaceElements($regexp, $Elements, $text)
{
$newElements = [];
while (\preg_match($regexp, $text, $matches, \PREG_OFFSET_CAPTURE))
{
$offset = $matches[0][1];
$before = \substr($text, 0, $offset);
$after = \substr($text, $offset + \strlen($matches[0][0]));
$newElements[] = ['text' => $before];
foreach ($Elements as $Element)
{
$newElements[] = $Element;
}
$text = $after;
}
$newElements[] = ['text' => $text];
return $newElements;
}
#
# Deprecated Methods
#
public static function parse($text)
{
$parsedown = new self();
return $parsedown->text($text);
}
protected function sanitiseElement(array $Element)
{
static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';
static $safeUrlNameToAtt = [
'a' => 'href',
'img' => 'src',
];
if (!isset($Element['name']))
{
unset($Element['attributes']);
return $Element;
}
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(string $text, bool $allowQuotes = false) : string
{
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);
}
}
public static function instance($name = 'default')
{
if (isset(self::$instances[$name]))
{
return self::$instances[$name];
}
$instance = new static();
self::$instances[$name] = $instance;
return $instance;
}
private static $instances = [];
#
# Fields
#
protected $DefinitionData;
public const ID_ATTRIBUTE_DEFAULT = 'toc';
protected $tagToc = '[toc]';
protected $contentsListArray = [];
protected $contentsListString = '';
protected $firstHeadLevel = 0;
protected $isBlacklistInitialized = false;
protected $anchorDuplicates = [];
#
# Read-Only
protected $specialCharacters = [
'\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '?', '"', "'", '<',
];
protected $StrongRegex = [
'*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s',
];
protected $UnderlineRegex = [
'_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us',
];
protected $EmRegex = [
'*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
'_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
];
protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+';
protected $voidElements = [
'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
];
protected $textLevelElements = [
'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
'i', 'rp', 'del', 'code', 'strike', 'marquee',
'q', 'rt', 'ins', 'font', 'strong',
's', 'tt', 'kbd', 'mark',
'u', 'xm', 'sub', 'nobr',
'sup', 'ruby',
'var', 'span',
'wbr', 'time',
];
}