*
* @package php-markdown
* @author Michel Fortin <michel.fortin@michelf.com>
- * @copyright 2004-2016 Michel Fortin <https://michelf.com/projects/php-markdown/>
+ * @copyright 2004-2018 Michel Fortin <https://michelf.com/projects/php-markdown/>
* @copyright (Original Markdown) 2004-2006 John Gruber <https://daringfireball.net/projects/markdown/>
*/
* Define the package version
* @var string
*/
- const MARKDOWNLIB_VERSION = "1.7.0";
+ const MARKDOWNLIB_VERSION = "1.8.0";
/**
* Simple function interface - Initialize the parser and return the result
* @var int
*/
public $tab_width = 4;
-
+
/**
* Change to `true` to disallow markup or entities.
* @var boolean
*/
public $no_markup = false;
public $no_entities = false;
-
+
/**
* Change to `true` to enable line breaks on \n without two trailling spaces
* @var callable
*/
public $header_id_func = null;
-
+
/**
* Optional function for converting code block content to HTML
* @var callable
* Class attribute to toggle "enhanced ordered list" behaviour
* setting this to true will allow ordered lists to start from the index
* number that is defined first.
- *
+ *
* For example:
* 2. List item two
* 3. List item three
public function __construct() {
$this->_initDetab();
$this->prepareItalicsAndBold();
-
- $this->nested_brackets_re =
+
+ $this->nested_brackets_re =
str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth).
str_repeat('\])*', $this->nested_brackets_depth);
-
- $this->nested_url_parenthesis_re =
+
+ $this->nested_url_parenthesis_re =
str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth).
str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth);
-
+
$this->escape_chars_re = '['.preg_quote($this->escape_chars).']';
-
+
// Sort document, block, and span gamut in ascendent priority order.
asort($this->document_gamut);
asort($this->block_gamut);
protected $urls = array();
protected $titles = array();
protected $html_hashes = array();
-
+
/**
* Status flag to avoid invalid nesting.
* @var boolean
*/
protected $in_anchor = false;
-
+
+ /**
+ * Status flag to avoid invalid nesting.
+ * @var boolean
+ */
+ protected $in_emphasis_processing = false;
+
/**
* Called before the transformation process starts to setup parser states.
* @return void
$this->titles = $this->predef_titles;
$this->html_hashes = array();
$this->in_anchor = false;
+ $this->in_emphasis_processing = false;
}
-
+
/**
* Called after the transformation process to clear any variable which may
* be taking up memory unnecessarly.
*/
public function transform($text) {
$this->setup();
-
+
# Remove UTF-8 BOM and marker character in input, if present.
$text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text);
foreach ($this->document_gamut as $method => $priority) {
$text = $this->$method($text);
}
-
+
$this->teardown();
return $text . "\n";
* @return string
*/
protected function stripLinkDefinitions($text) {
-
+
$less_than_tab = $this->tab_width - 1;
// Link defs are in the form: ^[id]: url "optional title"
* for is hard-coded:
*
* * List "a" is made of tags which can be both inline or block-level.
- * These will be treated block-level when the start tag is alone on
- * its line, otherwise they're not matched here and will be taken as
+ * These will be treated block-level when the start tag is alone on
+ * its line, otherwise they're not matched here and will be taken as
* inline later.
* * List "b" is made of tags which are always block-level;
*/
|
\'[^\']*\' # text inside single quotes (tolerate ">")
)*
- )?
+ )?
';
$content =
str_repeat('
str_repeat('
</\2\s*> # closing nested tag
)
- |
+ |
<(?!/\2\s*> # other tags with a different name
)
)*',
)
( # save in $1
- # Match from `\n<tag>` to `</tag>\n`, handling nested tags
+ # Match from `\n<tag>` to `</tag>\n`, handling nested tags
# in between.
-
+
[ ]{0,'.$less_than_tab.'}
<('.$block_tags_b_re.')# start tag = $2
'.$attr.'> # attributes followed by > and \n
</\3> # the matching end tag
[ ]* # trailing spaces/tabs
(?=\n+|\Z) # followed by a newline or end of document
-
- | # Special case just for <hr />. It was easier to make a special
+
+ | # Special case just for <hr />. It was easier to make a special
# case than to make the other regex more complicated.
-
+
[ ]{0,'.$less_than_tab.'}
<(hr) # start tag = $2
'.$attr.' # attributes
/?> # the matching end tag
[ ]*
(?=\n{2,}|\Z) # followed by a blank line or end of document
-
+
| # Special case for standalone HTML comments:
-
+
[ ]{0,'.$less_than_tab.'}
(?s:
<!-- .*? -->
)
[ ]*
(?=\n{2,}|\Z) # followed by a blank line or end of document
-
+
| # PHP and ASP-style processor instructions (<? and <%)
-
+
[ ]{0,'.$less_than_tab.'}
(?s:
<([?%]) # $2
)
[ ]*
(?=\n{2,}|\Z) # followed by a blank line or end of document
-
+
)
)}Sxmi',
array($this, '_hashHTMLBlocks_callback'),
$key = $this->hashBlock($text);
return "\n\n$key\n\n";
}
-
+
/**
- * Called whenever a tag must be hashed when a function insert an atomic
+ * Called whenever a tag must be hashed when a function insert an atomic
* element in the text stream. Passing $text to through this function gives
* a unique text-token which will be reverted back when calling unhash.
*
// Swap back any tag hash found in $text so we do not have to `unhash`
// multiple times at the end.
$text = $this->unhash($text);
-
+
// Then hash the block.
static $i = 0;
$key = "$boundary\x1A" . ++$i . $boundary;
/**
* Run block gamut tranformations.
*
- * We need to escape raw HTML in Markdown source before doing anything
- * else. This need to be done for each block, and not only at the
+ * We need to escape raw HTML in Markdown source before doing anything
+ * else. This need to be done for each block, and not only at the
* begining in the Markdown function since hashed blocks can be part of
- * list items and could have been indented. Indented blocks would have
+ * list items and could have been indented. Indented blocks would have
* been seen as a code block in a previous pass of hashHTMLBlocks.
*
* @param string $text
}
/**
- * Run block gamut tranformations, without hashing HTML blocks. This is
+ * Run block gamut tranformations, without hashing HTML blocks. This is
* useful when HTML blocks are known to be already hashed, like in the first
* whole-document pass.
*
* @return string
*/
protected function runBasicBlockGamut($text) {
-
+
foreach ($this->block_gamut as $method => $priority) {
$text = $this->$method($text);
}
-
+
// Finally form paragraph and restore hashed blocks.
$text = $this->formParagraphs($text);
[ ]* # Tailing spaces
$ # End of line.
}mx',
- "\n".$this->hashBlock("<hr$this->empty_element_suffix")."\n",
+ "\n".$this->hashBlock("<hr$this->empty_element_suffix")."\n",
$text
);
}
*/
protected function doHardBreaks($text) {
if ($this->hard_wrap) {
- return preg_replace_callback('/ *\n/',
+ return preg_replace_callback('/ *\n/',
array($this, '_doHardBreaks_callback'), $text);
} else {
- return preg_replace_callback('/ {2,}\n/',
+ return preg_replace_callback('/ {2,}\n/',
array($this, '_doHardBreaks_callback'), $text);
}
}
return $text;
}
$this->in_anchor = true;
-
+
// First, handle reference-style links: [link text] [id]
$text = preg_replace_callback('{
( # wrap whole match in $1
// for shortcut links like [this][] or [this].
$link_id = $link_text;
}
-
+
// lower-case and turn embedded newlines into spaces
$link_id = strtolower($link_id);
$link_id = preg_replace('{[ ]?\n}', ' ', $link_id);
if (isset($this->urls[$link_id])) {
$url = $this->urls[$link_id];
$url = $this->encodeURLAttribute($url);
-
+
$result = "<a href=\"$url\"";
if ( isset( $this->titles[$link_id] ) ) {
$title = $this->titles[$link_id];
$title = $this->encodeAttribute($title);
$result .= " title=\"$title\"";
}
-
+
$link_text = $this->runSpanGamut($link_text);
$result .= ">$link_text</a>";
$result = $this->hashPart($result);
$title = $this->encodeAttribute($title);
$result .= " title=\"$title\"";
}
-
+
$link_text = $this->runSpanGamut($link_text);
$result .= ">$link_text</a>";
\]
)
- }xs',
+ }xs',
array($this, '_doImages_reference_callback'), $text);
// Next, handle inline images: ![alt text](url "optional title")
* Setext-style headers:
* Header 1
* ========
- *
+ *
* Header 2
* --------
*/
if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1])) {
return $matches[0];
}
-
+
$level = $matches[2]{0} == '=' ? 1 : 2;
// ID attribute generation
* @param string $headerValue
* @return string
*/
- protected function _generateIdFromHeaderValue($headerValue) {
+ protected function _generateIdFromHeaderValue($headerValue) {
if (!is_callable($this->header_id_func)) {
return "";
}
)
)
'; // mx
-
+
// We use a different prefix before nested lists than top-level lists.
//See extended comment in _ProcessListItems().
-
+
if ($this->list_level) {
$text = preg_replace_callback('{
^
* without resorting to mind-reading. Perhaps the solution is to
* change the syntax rules such that sub-lists must start with a
* starting cardinal number; e.g. "1." or "a.".
- */
+ */
$this->list_level++;
// Trim trailing blank lines:
$marker_space = $matches[3];
$tailing_blank_line =& $matches[5];
- if ($leading_line || $tailing_blank_line ||
+ if ($leading_line || $tailing_blank_line ||
preg_match('/\n{2,}/', $item))
{
// Replace marker with the appropriate whitespace indentation
* @var array
*/
protected $em_strong_prepared_relist;
-
+
/**
* Prepare regular expressions for searching emphasis tokens in any
* context.
}
$token_relist[] = $em_re;
$token_relist[] = $strong_re;
-
+
// Construct master expression from list.
$token_re = '{(' . implode('|', $token_relist) . ')}';
$this->em_strong_prepared_relist["$em$strong"] = $token_re;
* @return string
*/
protected function doItalicsAndBold($text) {
+ if ($this->in_emphasis_processing) {
+ return $text; // avoid reentrency
+ }
+ $this->in_emphasis_processing = true;
+
$token_stack = array('');
$text_stack = array('');
$em = '';
$strong = '';
$tree_char_em = false;
-
+
while (1) {
// Get prepared regular expression for seraching emphasis tokens
// in current context.
$token_re = $this->em_strong_prepared_relist["$em$strong"];
-
- // Each loop iteration search for the next emphasis token.
+
+ // Each loop iteration search for the next emphasis token.
// Each token is then passed to handleSpanToken.
$parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
$text_stack[0] .= $parts[0];
$token =& $parts[1];
$text =& $parts[2];
-
+
if (empty($token)) {
// Reached end of text span: empty stack without emitting.
// any more emphasis.
}
break;
}
-
+
$token_len = strlen($token);
if ($tree_char_em) {
// Reached closing marker while inside a three-char emphasis.
$$tag = ''; // $$tag stands for $em or $strong
}
} else {
- // Reached opening three-char emphasis marker. Push on token
+ // Reached opening three-char emphasis marker. Push on token
// stack; will be handled by the special condition above.
$em = $token{0};
$strong = "$em$em";
if (strlen($token_stack[0]) == 1) {
$text_stack[1] .= array_shift($token_stack);
$text_stack[0] .= array_shift($text_stack);
+ $em = '';
}
// Closing strong marker:
array_shift($token_stack);
}
}
}
+ $this->in_emphasis_processing = false;
return $text_stack[0];
}
$bq = $this->runBlockGamut($bq); // recurse
$bq = preg_replace('/^/m', " ", $bq);
- // These leading spaces cause problem with <pre> content,
+ // These leading spaces cause problem with <pre> content,
// so we need to fix that:
- $bq = preg_replace_callback('{(\s*<pre>.+?</pre>)}sx',
+ $bq = preg_replace_callback('{(\s*<pre>.+?</pre>)}sx',
array($this, '_doBlockQuotes_callback2'), $bq);
return "\n" . $this->hashBlock("<blockquote>\n$bq\n</blockquote>") . "\n\n";
// // We can't call Markdown(), because that resets the hash;
// // that initialization code should be pulled into its own sub, though.
// $div_content = $this->hashHTMLBlocks($div_content);
-//
+//
// // Run document gamut methods on the content.
// foreach ($this->document_gamut as $method => $priority) {
// $div_content = $this->$method($div_content);
return $url;
}
-
+
/**
- * Smart processing for ampersands and angle brackets that need to
+ * Smart processing for ampersands and angle brackets that need to
* be encoded. Valid character entities are left alone unless the
* no-entities mode is set.
* @param string $text
} else {
// Ampersand-encoding based entirely on Nat Irons's Amputator
// MT plugin: <http://bumppo.net/projects/amputator/>
- $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/',
+ $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/',
'&', $text);
}
// Encode remaining <'s
*/
protected function parseSpan($str) {
$output = '';
-
+
$span_re = '{
(
\\\\'.$this->escape_chars_re.'
}xs';
while (1) {
- // Each loop iteration seach for either the next tag, the next
- // openning code span marker, or the next escaped character.
+ // Each loop iteration seach for either the next tag, the next
+ // openning code span marker, or the next escaped character.
// Each token is then passed to handleSpanToken.
$parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE);
-
+
// Create token from text preceding tag.
if ($parts[0] != "") {
$output .= $parts[0];
}
-
+
// Check if we reach the end.
if (isset($parts[1])) {
$output .= $this->handleSpanToken($parts[1], $parts[2]);
break;
}
}
-
+
return $output;
}
return $this->hashPart("&#". ord($token{1}). ";");
case "`":
// Search for end marker in remaining text.
- if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm',
+ if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm',
$str, $matches))
{
$str = $matches[2];
* For each line we separate the line in blocks delemited by tab characters.
* Then we reconstruct every line by adding the appropriate number of space
* between each blocks.
- *
+ *
* @param string $text
* @return string
*/
- protected function detab($text) {
+ protected function detab($text) {
$text = preg_replace_callback('/^.*\t.*$/m',
array($this, '_detab_callback'), $text);
protected function _detab_callback($matches) {
$line = $matches[0];
$strlen = $this->utf8_strlen; // strlen function for UTF-8.
-
+
// Split in blocks.
$blocks = explode("\t", $line);
// Add each blocks to the line.
unset($blocks[0]); // Do not add first block twice.
foreach ($blocks as $block) {
// Calculate amount of space, insert spaces, insert block.
- $amount = $this->tab_width -
+ $amount = $this->tab_width -
$strlen($line, 'UTF-8') % $this->tab_width;
$line .= str_repeat(" ", $amount) . $block;
}
/**
* Check for the availability of the function in the `utf8_strlen` property
- * (initially `mb_strlen`). If the function is not available, create a
+ * (initially `mb_strlen`). If the function is not available, create a
* function that will loosely count the number of UTF-8 characters with a
* regular expression.
* @return void
*/
protected function _initDetab() {
-
+
if (function_exists($this->utf8_strlen)) {
return;
}
- $this->utf8_strlen = create_function('$text', 'return preg_match_all(
- "/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/",
- $text, $m);');
+ $this->utf8_strlen = function($text) {
+ return preg_match_all('/[\x00-\xBF]|[\xC0-\xFF][\x80-\xBF]*/', $text, $m);
+ };
}
/**
* @return string
*/
protected function unhash($text) {
- return preg_replace_callback('/(.)\x1A[0-9]+\1/',
+ return preg_replace_callback('/(.)\x1A[0-9]+\1/',
array($this, '_unhash_callback'), $text);
}