288 lines
7.9 KiB
PHP
288 lines
7.9 KiB
PHP
<?php
|
|
/**
|
|
* @copyright Copyright (c) 2014 Carsten Brandt
|
|
* @license https://github.com/cebe/markdown/blob/master/LICENSE
|
|
* @link https://github.com/cebe/markdown#readme
|
|
*/
|
|
|
|
namespace cebe\markdown\inline;
|
|
|
|
// work around https://github.com/facebook/hhvm/issues/1120
|
|
defined('ENT_HTML401') || define('ENT_HTML401', 0);
|
|
|
|
/**
|
|
* Addes links and images as well as url markers.
|
|
*
|
|
* This trait conflicts with the HtmlTrait. If both are used together,
|
|
* you have to define a resolution, by defining the HtmlTrait::parseInlineHtml
|
|
* as private so it is not used directly:
|
|
*
|
|
* ```php
|
|
* use block\HtmlTrait {
|
|
* parseInlineHtml as private parseInlineHtml;
|
|
* }
|
|
* ```
|
|
*
|
|
* If the method exists it is called internally by this trait.
|
|
*
|
|
* Also make sure to reset references on prepare():
|
|
*
|
|
* ```php
|
|
* protected function prepare()
|
|
* {
|
|
* // reset references
|
|
* $this->references = [];
|
|
* }
|
|
* ```
|
|
*/
|
|
trait LinkTrait
|
|
{
|
|
/**
|
|
* @var array a list of defined references in this document.
|
|
*/
|
|
protected $references = [];
|
|
|
|
/**
|
|
* Remove backslash from escaped characters
|
|
* @param $text
|
|
* @return string
|
|
*/
|
|
protected function replaceEscape($text)
|
|
{
|
|
$strtr = [];
|
|
foreach($this->escapeCharacters as $char) {
|
|
$strtr["\\$char"] = $char;
|
|
}
|
|
return strtr($text, $strtr);
|
|
}
|
|
|
|
/**
|
|
* Parses a link indicated by `[`.
|
|
* @marker [
|
|
*/
|
|
protected function parseLink($markdown)
|
|
{
|
|
if (!in_array('parseLink', array_slice($this->context, 1)) && ($parts = $this->parseLinkOrImage($markdown)) !== false) {
|
|
list($text, $url, $title, $offset, $key) = $parts;
|
|
return [
|
|
[
|
|
'link',
|
|
'text' => $this->parseInline($text),
|
|
'url' => $url,
|
|
'title' => $title,
|
|
'refkey' => $key,
|
|
'orig' => substr($markdown, 0, $offset),
|
|
],
|
|
$offset
|
|
];
|
|
} else {
|
|
// remove all starting [ markers to avoid next one to be parsed as link
|
|
$result = '[';
|
|
$i = 1;
|
|
while (isset($markdown[$i]) && $markdown[$i] === '[') {
|
|
$result .= '[';
|
|
$i++;
|
|
}
|
|
return [['text', $result], $i];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses an image indicated by `![`.
|
|
* @marker ![
|
|
*/
|
|
protected function parseImage($markdown)
|
|
{
|
|
if (($parts = $this->parseLinkOrImage(substr($markdown, 1))) !== false) {
|
|
list($text, $url, $title, $offset, $key) = $parts;
|
|
|
|
return [
|
|
[
|
|
'image',
|
|
'text' => $text,
|
|
'url' => $url,
|
|
'title' => $title,
|
|
'refkey' => $key,
|
|
'orig' => substr($markdown, 0, $offset + 1),
|
|
],
|
|
$offset + 1
|
|
];
|
|
} else {
|
|
// remove all starting [ markers to avoid next one to be parsed as link
|
|
$result = '!';
|
|
$i = 1;
|
|
while (isset($markdown[$i]) && $markdown[$i] === '[') {
|
|
$result .= '[';
|
|
$i++;
|
|
}
|
|
return [['text', $result], $i];
|
|
}
|
|
}
|
|
|
|
protected function parseLinkOrImage($markdown)
|
|
{
|
|
if (strpos($markdown, ']') !== false && preg_match('/\[((?>[^\]\[]+|(?R))*)\]/', $markdown, $textMatches)) { // TODO improve bracket regex
|
|
$text = $textMatches[1];
|
|
$offset = strlen($textMatches[0]);
|
|
$markdown = substr($markdown, $offset);
|
|
|
|
$pattern = <<<REGEXP
|
|
/(?(R) # in case of recursion match parentheses
|
|
\(((?>[^\s()]+)|(?R))*\)
|
|
| # else match a link with title
|
|
^\(\s*(((?>[^\s()]+)|(?R))*)(\s+"(.*?)")?\s*\)
|
|
)/x
|
|
REGEXP;
|
|
if (preg_match($pattern, $markdown, $refMatches)) {
|
|
// inline link
|
|
return [
|
|
$text,
|
|
isset($refMatches[2]) ? $this->replaceEscape($refMatches[2]) : '', // url
|
|
empty($refMatches[5]) ? null: $refMatches[5], // title
|
|
$offset + strlen($refMatches[0]), // offset
|
|
null, // reference key
|
|
];
|
|
} elseif (preg_match('/^([ \n]?\[(.*?)\])?/s', $markdown, $refMatches)) {
|
|
// reference style link
|
|
if (empty($refMatches[2])) {
|
|
$key = strtolower($text);
|
|
} else {
|
|
$key = strtolower($refMatches[2]);
|
|
}
|
|
return [
|
|
$text,
|
|
null, // url
|
|
null, // title
|
|
$offset + strlen($refMatches[0]), // offset
|
|
$key,
|
|
];
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Parses inline HTML.
|
|
* @marker <
|
|
*/
|
|
protected function parseLt($text)
|
|
{
|
|
if (strpos($text, '>') !== false) {
|
|
if (!in_array('parseLink', $this->context)) { // do not allow links in links
|
|
if (preg_match('/^<([^\s>]*?@[^\s]*?\.\w+?)>/', $text, $matches)) {
|
|
// email address
|
|
return [
|
|
['email', $this->replaceEscape($matches[1])],
|
|
strlen($matches[0])
|
|
];
|
|
} elseif (preg_match('/^<([a-z]{3,}:\/\/[^\s]+?)>/', $text, $matches)) {
|
|
// URL
|
|
return [
|
|
['url', $this->replaceEscape($matches[1])],
|
|
strlen($matches[0])
|
|
];
|
|
}
|
|
}
|
|
// try inline HTML if it was neither a URL nor email if HtmlTrait is included.
|
|
if (method_exists($this, 'parseInlineHtml')) {
|
|
return $this->parseInlineHtml($text);
|
|
}
|
|
}
|
|
return [['text', '<'], 1];
|
|
}
|
|
|
|
protected function renderEmail($block)
|
|
{
|
|
$email = htmlspecialchars($block[1], ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
return "<a href=\"mailto:$email\">$email</a>";
|
|
}
|
|
|
|
protected function renderUrl($block)
|
|
{
|
|
$url = htmlspecialchars($block[1], ENT_COMPAT | ENT_HTML401, 'UTF-8');
|
|
$decodedUrl = urldecode($block[1]);
|
|
$secureUrlText = preg_match('//u', $decodedUrl) ? $decodedUrl : $block[1];
|
|
$text = htmlspecialchars($secureUrlText, ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
return "<a href=\"$url\">$text</a>";
|
|
}
|
|
|
|
protected function lookupReference($key)
|
|
{
|
|
$normalizedKey = preg_replace('/\s+/', ' ', $key);
|
|
if (isset($this->references[$key]) || isset($this->references[$key = $normalizedKey])) {
|
|
return $this->references[$key];
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected function renderLink($block)
|
|
{
|
|
if (isset($block['refkey'])) {
|
|
if (($ref = $this->lookupReference($block['refkey'])) !== false) {
|
|
$block = array_merge($block, $ref);
|
|
} else {
|
|
if (strncmp($block['orig'], '[', 1) === 0) {
|
|
return '[' . $this->renderAbsy($this->parseInline(substr($block['orig'], 1)));
|
|
}
|
|
return $block['orig'];
|
|
}
|
|
}
|
|
return '<a href="' . htmlspecialchars($block['url'], ENT_COMPAT | ENT_HTML401, 'UTF-8') . '"'
|
|
. (empty($block['title']) ? '' : ' title="' . htmlspecialchars($block['title'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"')
|
|
. '>' . $this->renderAbsy($block['text']) . '</a>';
|
|
}
|
|
|
|
protected function renderImage($block)
|
|
{
|
|
if (isset($block['refkey'])) {
|
|
if (($ref = $this->lookupReference($block['refkey'])) !== false) {
|
|
$block = array_merge($block, $ref);
|
|
} else {
|
|
if (strncmp($block['orig'], '![', 2) === 0) {
|
|
return '![' . $this->renderAbsy($this->parseInline(substr($block['orig'], 2)));
|
|
}
|
|
return $block['orig'];
|
|
}
|
|
}
|
|
return '<img src="' . htmlspecialchars($block['url'], ENT_COMPAT | ENT_HTML401, 'UTF-8') . '"'
|
|
. ' alt="' . htmlspecialchars($block['text'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"'
|
|
. (empty($block['title']) ? '' : ' title="' . htmlspecialchars($block['title'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"')
|
|
. ($this->html5 ? '>' : ' />');
|
|
}
|
|
|
|
// references
|
|
|
|
protected function identifyReference($line)
|
|
{
|
|
return isset($line[0]) && ($line[0] === ' ' || $line[0] === '[') && preg_match('/^ {0,3}\[[^\[](.*?)\]:\s*([^\s]+?)(?:\s+[\'"](.+?)[\'"])?\s*$/', $line);
|
|
}
|
|
|
|
/**
|
|
* Consume link references
|
|
*/
|
|
protected function consumeReference($lines, $current)
|
|
{
|
|
while (isset($lines[$current]) && preg_match('/^ {0,3}\[(.+?)\]:\s*(.+?)(?:\s+[\(\'"](.+?)[\)\'"])?\s*$/', $lines[$current], $matches)) {
|
|
$label = strtolower($matches[1]);
|
|
|
|
$this->references[$label] = [
|
|
'url' => $this->replaceEscape($matches[2]),
|
|
];
|
|
if (isset($matches[3])) {
|
|
$this->references[$label]['title'] = $matches[3];
|
|
} else {
|
|
// title may be on the next line
|
|
if (isset($lines[$current + 1]) && preg_match('/^\s+[\(\'"](.+?)[\)\'"]\s*$/', $lines[$current + 1], $matches)) {
|
|
$this->references[$label]['title'] = $matches[1];
|
|
$current++;
|
|
}
|
|
}
|
|
$current++;
|
|
}
|
|
return [false, --$current];
|
|
}
|
|
|
|
abstract protected function parseInline($text);
|
|
abstract protected function renderAbsy($blocks);
|
|
}
|