update inbox list

This commit is contained in:
manhlab
2021-04-07 19:25:18 -04:00
parent fda7245f7c
commit 436de2efd6
8576 changed files with 1013325 additions and 3 deletions

View File

@@ -0,0 +1,222 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Element;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
use League\CommonMark\Node\Node;
/**
* Block-level element
*
* @method parent() ?AbstractBlock
*/
abstract class AbstractBlock extends Node
{
/**
* Used for storage of arbitrary data.
*
* @var array<string, mixed>
*/
public $data = [];
/**
* @var bool
*/
protected $open = true;
/**
* @var bool
*/
protected $lastLineBlank = false;
/**
* @var int
*/
protected $startLine;
/**
* @var int
*/
protected $endLine;
protected function setParent(Node $node = null)
{
if ($node && !$node instanceof self) {
throw new \InvalidArgumentException('Parent of block must also be block (can not be inline)');
}
parent::setParent($node);
}
public function isContainer(): bool
{
return true;
}
/**
* @return bool
*/
public function hasChildren(): bool
{
return $this->firstChild !== null;
}
/**
* Returns true if this block can contain the given block as a child node
*
* @param AbstractBlock $block
*
* @return bool
*/
abstract public function canContain(AbstractBlock $block): bool;
/**
* Whether this is a code block
*
* Code blocks are extra-greedy - they'll try to consume all subsequent
* lines of content without calling matchesNextLine() each time.
*
* @return bool
*/
abstract public function isCode(): bool;
/**
* @param Cursor $cursor
*
* @return bool
*/
abstract public function matchesNextLine(Cursor $cursor): bool;
/**
* @param int $startLine
*
* @return $this
*/
public function setStartLine(int $startLine)
{
$this->startLine = $startLine;
if (empty($this->endLine)) {
$this->endLine = $startLine;
}
return $this;
}
/**
* @return int
*/
public function getStartLine(): int
{
return $this->startLine;
}
/**
* @param int $endLine
*
* @return $this
*/
public function setEndLine(int $endLine)
{
$this->endLine = $endLine;
return $this;
}
/**
* @return int
*/
public function getEndLine(): int
{
return $this->endLine;
}
/**
* Whether the block ends with a blank line
*
* @return bool
*/
public function endsWithBlankLine(): bool
{
return $this->lastLineBlank;
}
/**
* @param bool $blank
*
* @return void
*/
public function setLastLineBlank(bool $blank)
{
$this->lastLineBlank = $blank;
}
/**
* Determines whether the last line should be marked as blank
*
* @param Cursor $cursor
* @param int $currentLineNumber
*
* @return bool
*/
public function shouldLastLineBeBlank(Cursor $cursor, int $currentLineNumber): bool
{
return $cursor->isBlank();
}
/**
* Whether the block is open for modifications
*
* @return bool
*/
public function isOpen(): bool
{
return $this->open;
}
/**
* Finalize the block; mark it closed for modification
*
* @param ContextInterface $context
* @param int $endLineNumber
*
* @return void
*/
public function finalize(ContextInterface $context, int $endLineNumber)
{
if (!$this->open) {
return;
}
$this->open = false;
$this->endLine = $endLineNumber;
// This should almost always be true
if ($context->getTip() !== null) {
$context->setTip($context->getTip()->parent());
}
}
/**
* @param string $key
* @param mixed $default
*
* @return mixed
*/
public function getData(string $key, $default = null)
{
return \array_key_exists($key, $this->data) ? $this->data[$key] : $default;
}
}

View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Element;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
use League\CommonMark\Util\ArrayCollection;
/**
* @method children() AbstractInline[]
*/
abstract class AbstractStringContainerBlock extends AbstractBlock implements StringContainerInterface
{
/**
* @var ArrayCollection<int, string>
*/
protected $strings;
/**
* @var string
*/
protected $finalStringContents = '';
/**
* Constructor
*/
public function __construct()
{
$this->strings = new ArrayCollection();
}
public function addLine(string $line)
{
$this->strings[] = $line;
}
abstract public function handleRemainingContents(ContextInterface $context, Cursor $cursor);
public function getStringContent(): string
{
return $this->finalStringContents;
}
}

View File

@@ -0,0 +1,51 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Element;
use League\CommonMark\Cursor;
/**
* @method children() AbstractBlock[]
*/
class BlockQuote extends AbstractBlock
{
public function canContain(AbstractBlock $block): bool
{
return true;
}
public function isCode(): bool
{
return false;
}
public function matchesNextLine(Cursor $cursor): bool
{
if (!$cursor->isIndented() && $cursor->getNextNonSpaceCharacter() === '>') {
$cursor->advanceToNextNonSpaceOrTab();
$cursor->advanceBy(1);
$cursor->advanceBySpaceOrTab();
return true;
}
return false;
}
public function shouldLastLineBeBlank(Cursor $cursor, int $currentLineNumber): bool
{
return false;
}
}

View File

@@ -0,0 +1,58 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Element;
use League\CommonMark\Cursor;
use League\CommonMark\Reference\ReferenceMap;
use League\CommonMark\Reference\ReferenceMapInterface;
/**
* @method children() AbstractBlock[]
*/
class Document extends AbstractBlock
{
/** @var ReferenceMapInterface */
protected $referenceMap;
public function __construct(?ReferenceMapInterface $referenceMap = null)
{
$this->setStartLine(1);
$this->referenceMap = $referenceMap ?? new ReferenceMap();
}
/**
* @return ReferenceMapInterface
*/
public function getReferenceMap(): ReferenceMapInterface
{
return $this->referenceMap;
}
public function canContain(AbstractBlock $block): bool
{
return true;
}
public function isCode(): bool
{
return false;
}
public function matchesNextLine(Cursor $cursor): bool
{
return true;
}
}

View File

@@ -0,0 +1,201 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Element;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
use League\CommonMark\Util\RegexHelper;
class FencedCode extends AbstractStringContainerBlock
{
/**
* @var string
*/
protected $info;
/**
* @var int
*/
protected $length;
/**
* @var string
*/
protected $char;
/**
* @var int
*/
protected $offset;
/**
* @param int $length
* @param string $char
* @param int $offset
*/
public function __construct(int $length, string $char, int $offset)
{
parent::__construct();
$this->length = $length;
$this->char = $char;
$this->offset = $offset;
}
/**
* @return string
*/
public function getInfo(): string
{
return $this->info;
}
/**
* @return string[]
*/
public function getInfoWords(): array
{
return \preg_split('/\s+/', $this->info) ?: [];
}
/**
* @return string
*/
public function getChar(): string
{
return $this->char;
}
/**
* @param string $char
*
* @return $this
*/
public function setChar(string $char): self
{
$this->char = $char;
return $this;
}
/**
* @return int
*/
public function getLength(): int
{
return $this->length;
}
/**
* @param int $length
*
* @return $this
*/
public function setLength(int $length): self
{
$this->length = $length;
return $this;
}
/**
* @return int
*/
public function getOffset(): int
{
return $this->offset;
}
/**
* @param int $offset
*
* @return $this
*/
public function setOffset(int $offset): self
{
$this->offset = $offset;
return $this;
}
public function canContain(AbstractBlock $block): bool
{
return false;
}
public function isCode(): bool
{
return true;
}
public function matchesNextLine(Cursor $cursor): bool
{
if ($this->length === -1) {
if ($cursor->isBlank()) {
$this->lastLineBlank = true;
}
return false;
}
// Skip optional spaces of fence offset
$cursor->match('/^ {0,' . $this->offset . '}/');
return true;
}
public function finalize(ContextInterface $context, int $endLineNumber)
{
parent::finalize($context, $endLineNumber);
// first line becomes info string
$firstLine = $this->strings->first();
if ($firstLine === false) {
$firstLine = '';
}
$this->info = RegexHelper::unescape(\trim($firstLine));
if ($this->strings->count() === 1) {
$this->finalStringContents = '';
} else {
$this->finalStringContents = \implode("\n", $this->strings->slice(1)) . "\n";
}
}
public function handleRemainingContents(ContextInterface $context, Cursor $cursor)
{
/** @var self $container */
$container = $context->getContainer();
// check for closing code fence
if ($cursor->getIndent() <= 3 && $cursor->getNextNonSpaceCharacter() === $container->getChar()) {
$match = RegexHelper::matchAll('/^(?:`{3,}|~{3,})(?= *$)/', $cursor->getLine(), $cursor->getNextNonSpacePosition());
if ($match !== null && \strlen($match[0]) >= $container->getLength()) {
// don't add closing fence to container; instead, close it:
$this->setLength(-1); // -1 means we've passed closer
return;
}
}
$container->addLine($cursor->getRemainder());
}
public function shouldLastLineBeBlank(Cursor $cursor, int $currentLineNumber): bool
{
return false;
}
}

View File

@@ -0,0 +1,80 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Element;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
class Heading extends AbstractStringContainerBlock implements InlineContainerInterface
{
/**
* @var int
*/
protected $level;
/**
* @param int $level
* @param string|string[] $contents
*/
public function __construct(int $level, $contents)
{
parent::__construct();
$this->level = $level;
if (!\is_array($contents)) {
$contents = [$contents];
}
foreach ($contents as $line) {
$this->addLine($line);
}
}
/**
* @return int
*/
public function getLevel(): int
{
return $this->level;
}
public function finalize(ContextInterface $context, int $endLineNumber)
{
parent::finalize($context, $endLineNumber);
$this->finalStringContents = \implode("\n", $this->strings->toArray());
}
public function canContain(AbstractBlock $block): bool
{
return false;
}
public function isCode(): bool
{
return false;
}
public function matchesNextLine(Cursor $cursor): bool
{
return false;
}
public function handleRemainingContents(ContextInterface $context, Cursor $cursor)
{
// nothing to do; contents were already added via the constructor.
}
}

View File

@@ -0,0 +1,104 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Element;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
use League\CommonMark\Util\RegexHelper;
class HtmlBlock extends AbstractStringContainerBlock
{
// Any changes to these constants should be reflected in .phpstorm.meta.php
const TYPE_1_CODE_CONTAINER = 1;
const TYPE_2_COMMENT = 2;
const TYPE_3 = 3;
const TYPE_4 = 4;
const TYPE_5_CDATA = 5;
const TYPE_6_BLOCK_ELEMENT = 6;
const TYPE_7_MISC_ELEMENT = 7;
/**
* @var int
*/
protected $type;
/**
* @param int $type
*/
public function __construct(int $type)
{
parent::__construct();
$this->type = $type;
}
/**
* @return int
*/
public function getType(): int
{
return $this->type;
}
/**
* @param int $type
*
* @return void
*/
public function setType(int $type)
{
$this->type = $type;
}
public function canContain(AbstractBlock $block): bool
{
return false;
}
public function isCode(): bool
{
return true;
}
public function matchesNextLine(Cursor $cursor): bool
{
if ($cursor->isBlank() && ($this->type === self::TYPE_6_BLOCK_ELEMENT || $this->type === self::TYPE_7_MISC_ELEMENT)) {
return false;
}
return true;
}
public function finalize(ContextInterface $context, int $endLineNumber)
{
parent::finalize($context, $endLineNumber);
$this->finalStringContents = \implode("\n", $this->strings->toArray());
}
public function handleRemainingContents(ContextInterface $context, Cursor $cursor)
{
/** @var self $tip */
$tip = $context->getTip();
$tip->addLine($cursor->getRemainder());
// Check for end condition
if ($this->type >= self::TYPE_1_CODE_CONTAINER && $this->type <= self::TYPE_5_CDATA) {
if ($cursor->match(RegexHelper::getHtmlBlockCloseRegex($this->type)) !== null) {
$this->finalize($context, $context->getLineNumber());
}
}
}
}

View File

@@ -0,0 +1,72 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Element;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
class IndentedCode extends AbstractStringContainerBlock
{
public function canContain(AbstractBlock $block): bool
{
return false;
}
public function isCode(): bool
{
return true;
}
public function matchesNextLine(Cursor $cursor): bool
{
if ($cursor->isIndented()) {
$cursor->advanceBy(Cursor::INDENT_LEVEL, true);
} elseif ($cursor->isBlank()) {
$cursor->advanceToNextNonSpaceOrTab();
} else {
return false;
}
return true;
}
public function finalize(ContextInterface $context, int $endLineNumber)
{
parent::finalize($context, $endLineNumber);
$reversed = \array_reverse($this->strings->toArray(), true);
foreach ($reversed as $index => $line) {
if ($line === '' || $line === "\n" || \preg_match('/^(\n *)$/', $line)) {
unset($reversed[$index]);
} else {
break;
}
}
$fixed = \array_reverse($reversed);
$tmp = \implode("\n", $fixed);
if (\substr($tmp, -1) !== "\n") {
$tmp .= "\n";
}
$this->finalStringContents = $tmp;
}
public function handleRemainingContents(ContextInterface $context, Cursor $cursor)
{
/** @var self $tip */
$tip = $context->getTip();
$tip->addLine($cursor->getRemainder());
}
}

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Element;
interface InlineContainerInterface extends StringContainerInterface
{
public function getStringContent(): string;
}

View File

@@ -0,0 +1,123 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Element;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
/**
* @method children() AbstractBlock[]
*/
class ListBlock extends AbstractBlock
{
const TYPE_BULLET = 'bullet';
const TYPE_ORDERED = 'ordered';
/**
* @deprecated This constant is deprecated in league/commonmark 1.4 and will be removed in 2.0; use TYPE_BULLET instead
*/
const TYPE_UNORDERED = self::TYPE_BULLET;
/**
* @var bool
*/
protected $tight = false;
/**
* @var ListData
*/
protected $listData;
public function __construct(ListData $listData)
{
$this->listData = $listData;
}
/**
* @return ListData
*/
public function getListData(): ListData
{
return $this->listData;
}
public function endsWithBlankLine(): bool
{
if ($this->lastLineBlank) {
return true;
}
if ($this->hasChildren()) {
return $this->lastChild() instanceof AbstractBlock && $this->lastChild()->endsWithBlankLine();
}
return false;
}
public function canContain(AbstractBlock $block): bool
{
return $block instanceof ListItem;
}
public function isCode(): bool
{
return false;
}
public function matchesNextLine(Cursor $cursor): bool
{
return true;
}
public function finalize(ContextInterface $context, int $endLineNumber)
{
parent::finalize($context, $endLineNumber);
$this->tight = true; // tight by default
foreach ($this->children() as $item) {
if (!($item instanceof AbstractBlock)) {
continue;
}
// check for non-final list item ending with blank line:
if ($item->endsWithBlankLine() && $item !== $this->lastChild()) {
$this->tight = false;
break;
}
// Recurse into children of list item, to see if there are
// spaces between any of them:
foreach ($item->children() as $subItem) {
if ($subItem instanceof AbstractBlock && $subItem->endsWithBlankLine() && ($item !== $this->lastChild() || $subItem !== $item->lastChild())) {
$this->tight = false;
break;
}
}
}
}
public function isTight(): bool
{
return $this->tight;
}
public function setTight(bool $tight): self
{
$this->tight = $tight;
return $this;
}
}

View File

@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Element;
class ListData
{
/**
* @var int|null
*/
public $start;
/**
* @var int
*/
public $padding = 0;
/**
* @var string
*/
public $type;
/**
* @var string|null
*/
public $delimiter;
/**
* @var string|null
*/
public $bulletChar;
/**
* @var int
*/
public $markerOffset;
/**
* @param ListData $data
*
* @return bool
*/
public function equals(ListData $data)
{
return $this->type === $data->type &&
$this->delimiter === $data->delimiter &&
$this->bulletChar === $data->bulletChar;
}
}

View File

@@ -0,0 +1,73 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Element;
use League\CommonMark\Cursor;
/**
* @method children() AbstractBlock[]
*/
class ListItem extends AbstractBlock
{
/**
* @var ListData
*/
protected $listData;
public function __construct(ListData $listData)
{
$this->listData = $listData;
}
/**
* @return ListData
*/
public function getListData(): ListData
{
return $this->listData;
}
public function canContain(AbstractBlock $block): bool
{
return true;
}
public function isCode(): bool
{
return false;
}
public function matchesNextLine(Cursor $cursor): bool
{
if ($cursor->isBlank()) {
if ($this->firstChild === null) {
return false;
}
$cursor->advanceToNextNonSpaceOrTab();
} elseif ($cursor->getIndent() >= $this->listData->markerOffset + $this->listData->padding) {
$cursor->advanceBy($this->listData->markerOffset + $this->listData->padding, true);
} else {
return false;
}
return true;
}
public function shouldLastLineBeBlank(Cursor $cursor, int $currentLineNumber): bool
{
return $cursor->isBlank() && $this->startLine < $currentLineNumber;
}
}

View File

@@ -0,0 +1,98 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Element;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
class Paragraph extends AbstractStringContainerBlock implements InlineContainerInterface
{
public function canContain(AbstractBlock $block): bool
{
return false;
}
public function isCode(): bool
{
return false;
}
public function matchesNextLine(Cursor $cursor): bool
{
if ($cursor->isBlank()) {
$this->lastLineBlank = true;
return false;
}
return true;
}
public function finalize(ContextInterface $context, int $endLineNumber)
{
parent::finalize($context, $endLineNumber);
$this->finalStringContents = \preg_replace('/^ */m', '', \implode("\n", $this->getStrings()));
// Short-circuit
if ($this->finalStringContents === '' || $this->finalStringContents[0] !== '[') {
return;
}
$cursor = new Cursor($this->finalStringContents);
$referenceFound = $this->parseReferences($context, $cursor);
$this->finalStringContents = $cursor->getRemainder();
if ($referenceFound && $cursor->isAtEnd()) {
$this->detach();
}
}
/**
* @param ContextInterface $context
* @param Cursor $cursor
*
* @return bool
*/
protected function parseReferences(ContextInterface $context, Cursor $cursor)
{
$referenceFound = false;
while ($cursor->getCharacter() === '[' && $context->getReferenceParser()->parse($cursor)) {
$this->finalStringContents = $cursor->getRemainder();
$referenceFound = true;
}
return $referenceFound;
}
public function handleRemainingContents(ContextInterface $context, Cursor $cursor)
{
$cursor->advanceToNextNonSpaceOrTab();
/** @var self $tip */
$tip = $context->getTip();
$tip->addLine($cursor->getRemainder());
}
/**
* @return string[]
*/
public function getStrings(): array
{
return $this->strings->toArray();
}
}

View File

@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Element;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
/**
* Interface for a block which can contain line(s) of strings
*/
interface StringContainerInterface
{
/**
* @param string $line
*
* @return void
*/
public function addLine(string $line);
/**
* @return string
*/
public function getStringContent(): string;
/**
* @param ContextInterface $context
* @param Cursor $cursor
*
* @return void
*/
public function handleRemainingContents(ContextInterface $context, Cursor $cursor);
}

View File

@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Element;
use League\CommonMark\Cursor;
class ThematicBreak extends AbstractBlock
{
public function canContain(AbstractBlock $block): bool
{
return false;
}
public function isCode(): bool
{
return false;
}
public function matchesNextLine(Cursor $cursor): bool
{
return false;
}
}

View File

@@ -0,0 +1,51 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Parser;
use League\CommonMark\Block\Element\Heading;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
use League\CommonMark\Util\RegexHelper;
final class ATXHeadingParser implements BlockParserInterface
{
public function parse(ContextInterface $context, Cursor $cursor): bool
{
if ($cursor->isIndented()) {
return false;
}
$match = RegexHelper::matchAll('/^#{1,6}(?:[ \t]+|$)/', $cursor->getLine(), $cursor->getNextNonSpacePosition());
if (!$match) {
return false;
}
$cursor->advanceToNextNonSpaceOrTab();
$cursor->advanceBy(\strlen($match[0]));
$level = \strlen(\trim($match[0]));
$str = $cursor->getRemainder();
/** @var string $str */
$str = \preg_replace('/^[ \t]*#+[ \t]*$/', '', $str);
/** @var string $str */
$str = \preg_replace('/[ \t]+#+[ \t]*$/', '', $str);
$context->addBlock(new Heading($level, $str));
$context->setBlocksParsed(true);
return true;
}
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Parser;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
interface BlockParserInterface
{
/**
* @param ContextInterface $context
* @param Cursor $cursor
*
* @return bool
*/
public function parse(ContextInterface $context, Cursor $cursor): bool;
}

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Parser;
use League\CommonMark\Block\Element\BlockQuote;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
final class BlockQuoteParser implements BlockParserInterface
{
public function parse(ContextInterface $context, Cursor $cursor): bool
{
if ($cursor->isIndented()) {
return false;
}
if ($cursor->getNextNonSpaceCharacter() !== '>') {
return false;
}
$cursor->advanceToNextNonSpaceOrTab();
$cursor->advanceBy(1);
$cursor->advanceBySpaceOrTab();
$context->addBlock(new BlockQuote());
return true;
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Parser;
use League\CommonMark\Block\Element\FencedCode;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
final class FencedCodeParser implements BlockParserInterface
{
public function parse(ContextInterface $context, Cursor $cursor): bool
{
if ($cursor->isIndented()) {
return false;
}
$c = $cursor->getCharacter();
if ($c !== ' ' && $c !== "\t" && $c !== '`' && $c !== '~') {
return false;
}
$indent = $cursor->getIndent();
$fence = $cursor->match('/^[ \t]*(?:`{3,}(?!.*`)|^~{3,})/');
if ($fence === null) {
return false;
}
// fenced code block
$fence = \ltrim($fence, " \t");
$fenceLength = \strlen($fence);
$context->addBlock(new FencedCode($fenceLength, $fence[0], $indent));
return true;
}
}

View File

@@ -0,0 +1,59 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Parser;
use League\CommonMark\Block\Element\HtmlBlock;
use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
use League\CommonMark\Util\RegexHelper;
final class HtmlBlockParser implements BlockParserInterface
{
public function parse(ContextInterface $context, Cursor $cursor): bool
{
if ($cursor->isIndented()) {
return false;
}
if ($cursor->getNextNonSpaceCharacter() !== '<') {
return false;
}
$savedState = $cursor->saveState();
$cursor->advanceToNextNonSpaceOrTab();
$line = $cursor->getRemainder();
for ($blockType = 1; $blockType <= 7; $blockType++) {
$match = RegexHelper::matchAt(
RegexHelper::getHtmlBlockOpenRegex($blockType),
$line
);
if ($match !== null && ($blockType < 7 || !($context->getContainer() instanceof Paragraph))) {
$cursor->restoreState($savedState);
$context->addBlock(new HtmlBlock($blockType));
$context->setBlocksParsed(true);
return true;
}
}
$cursor->restoreState($savedState);
return false;
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Parser;
use League\CommonMark\Block\Element\IndentedCode;
use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
final class IndentedCodeParser implements BlockParserInterface
{
public function parse(ContextInterface $context, Cursor $cursor): bool
{
if (!$cursor->isIndented()) {
return false;
}
if ($context->getTip() instanceof Paragraph) {
return false;
}
if ($cursor->isBlank()) {
return false;
}
$cursor->advanceBy(Cursor::INDENT_LEVEL, true);
$context->addBlock(new IndentedCode());
return true;
}
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Parser;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
final class LazyParagraphParser implements BlockParserInterface
{
public function parse(ContextInterface $context, Cursor $cursor): bool
{
if (!$cursor->isIndented()) {
return false;
}
$context->setBlocksParsed(true);
return true;
}
}

View File

@@ -0,0 +1,147 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Parser;
use League\CommonMark\Block\Element\ListBlock;
use League\CommonMark\Block\Element\ListData;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
use League\CommonMark\Util\ConfigurationAwareInterface;
use League\CommonMark\Util\ConfigurationInterface;
use League\CommonMark\Util\RegexHelper;
final class ListParser implements BlockParserInterface, ConfigurationAwareInterface
{
/** @var ConfigurationInterface|null */
private $config;
/** @var string|null */
private $listMarkerRegex;
public function setConfiguration(ConfigurationInterface $configuration)
{
$this->config = $configuration;
}
public function parse(ContextInterface $context, Cursor $cursor): bool
{
if ($cursor->isIndented() && !($context->getContainer() instanceof ListBlock)) {
return false;
}
$indent = $cursor->getIndent();
if ($indent >= 4) {
return false;
}
$tmpCursor = clone $cursor;
$tmpCursor->advanceToNextNonSpaceOrTab();
$rest = $tmpCursor->getRemainder();
if (\preg_match($this->listMarkerRegex ?? $this->generateListMarkerRegex(), $rest) === 1) {
$data = new ListData();
$data->markerOffset = $indent;
$data->type = ListBlock::TYPE_BULLET;
$data->delimiter = null;
$data->bulletChar = $rest[0];
$markerLength = 1;
} elseif (($matches = RegexHelper::matchAll('/^(\d{1,9})([.)])/', $rest)) && (!($context->getContainer() instanceof Paragraph) || $matches[1] === '1')) {
$data = new ListData();
$data->markerOffset = $indent;
$data->type = ListBlock::TYPE_ORDERED;
$data->start = (int) $matches[1];
$data->delimiter = $matches[2];
$data->bulletChar = null;
$markerLength = \strlen($matches[0]);
} else {
return false;
}
// Make sure we have spaces after
$nextChar = $tmpCursor->peek($markerLength);
if (!($nextChar === null || $nextChar === "\t" || $nextChar === ' ')) {
return false;
}
// If it interrupts paragraph, make sure first line isn't blank
$container = $context->getContainer();
if ($container instanceof Paragraph && !RegexHelper::matchAt(RegexHelper::REGEX_NON_SPACE, $rest, $markerLength)) {
return false;
}
// We've got a match! Advance offset and calculate padding
$cursor->advanceToNextNonSpaceOrTab(); // to start of marker
$cursor->advanceBy($markerLength, true); // to end of marker
$data->padding = $this->calculateListMarkerPadding($cursor, $markerLength);
// add the list if needed
if (!($container instanceof ListBlock) || !$data->equals($container->getListData())) {
$context->addBlock(new ListBlock($data));
}
// add the list item
$context->addBlock(new ListItem($data));
return true;
}
/**
* @param Cursor $cursor
* @param int $markerLength
*
* @return int
*/
private function calculateListMarkerPadding(Cursor $cursor, int $markerLength): int
{
$start = $cursor->saveState();
$spacesStartCol = $cursor->getColumn();
while ($cursor->getColumn() - $spacesStartCol < 5) {
if (!$cursor->advanceBySpaceOrTab()) {
break;
}
}
$blankItem = $cursor->peek() === null;
$spacesAfterMarker = $cursor->getColumn() - $spacesStartCol;
if ($spacesAfterMarker >= 5 || $spacesAfterMarker < 1 || $blankItem) {
$cursor->restoreState($start);
$cursor->advanceBySpaceOrTab();
return $markerLength + 1;
}
return $markerLength + $spacesAfterMarker;
}
private function generateListMarkerRegex(): string
{
// No configuration given - use the defaults
if ($this->config === null) {
return $this->listMarkerRegex = '/^[*+-]/';
}
$markers = $this->config->get('unordered_list_markers', ['*', '+', '-']);
if (!\is_array($markers)) {
throw new \RuntimeException('Invalid configuration option "unordered_list_markers": value must be an array of strings');
}
return $this->listMarkerRegex = '/^[' . \preg_quote(\implode('', $markers), '/') . ']/';
}
}

View File

@@ -0,0 +1,81 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Parser;
use League\CommonMark\Block\Element\Heading;
use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
use League\CommonMark\Reference\ReferenceParser;
use League\CommonMark\Util\RegexHelper;
final class SetExtHeadingParser implements BlockParserInterface
{
public function parse(ContextInterface $context, Cursor $cursor): bool
{
if ($cursor->isIndented()) {
return false;
}
if (!($context->getContainer() instanceof Paragraph)) {
return false;
}
$match = RegexHelper::matchAll('/^(?:=+|-+)[ \t]*$/', $cursor->getLine(), $cursor->getNextNonSpacePosition());
if ($match === null) {
return false;
}
$level = $match[0][0] === '=' ? 1 : 2;
$strings = $context->getContainer()->getStrings();
$strings = $this->resolveReferenceLinkDefinitions($strings, $context->getReferenceParser());
if (empty($strings)) {
return false;
}
$context->replaceContainerBlock(new Heading($level, $strings));
return true;
}
/**
* Resolve reference link definition
*
* @see https://github.com/commonmark/commonmark.js/commit/993bbe335931af847460effa99b2411eb643577d
*
* @param string[] $strings
* @param ReferenceParser $referenceParser
*
* @return string[]
*/
private function resolveReferenceLinkDefinitions(array $strings, ReferenceParser $referenceParser): array
{
foreach ($strings as &$string) {
$cursor = new Cursor($string);
while ($cursor->getCharacter() === '[' && $referenceParser->parse($cursor)) {
$string = $cursor->getRemainder();
}
if ($string !== '') {
break;
}
}
return \array_filter($strings, function ($s) {
return $s !== '';
});
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Parser;
use League\CommonMark\Block\Element\ThematicBreak;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
use League\CommonMark\Util\RegexHelper;
final class ThematicBreakParser implements BlockParserInterface
{
public function parse(ContextInterface $context, Cursor $cursor): bool
{
if ($cursor->isIndented()) {
return false;
}
$match = RegexHelper::matchAt(RegexHelper::REGEX_THEMATIC_BREAK, $cursor->getLine(), $cursor->getNextNonSpacePosition());
if ($match === null) {
return false;
}
// Advance to the end of the string, consuming the entire line (of the thematic break)
$cursor->advanceToEnd();
$context->addBlock(new ThematicBreak());
$context->setBlocksParsed(true);
return true;
}
}

View File

@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Renderer;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\BlockQuote;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
final class BlockQuoteRenderer implements BlockRendererInterface
{
/**
* @param BlockQuote $block
* @param ElementRendererInterface $htmlRenderer
* @param bool $inTightList
*
* @return HtmlElement
*/
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
if (!($block instanceof BlockQuote)) {
throw new \InvalidArgumentException('Incompatible block type: ' . \get_class($block));
}
$attrs = $block->getData('attributes', []);
$filling = $htmlRenderer->renderBlocks($block->children());
if ($filling === '') {
return new HtmlElement('blockquote', $attrs, $htmlRenderer->getOption('inner_separator', "\n"));
}
return new HtmlElement(
'blockquote',
$attrs,
$htmlRenderer->getOption('inner_separator', "\n") . $filling . $htmlRenderer->getOption('inner_separator', "\n")
);
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Renderer;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
interface BlockRendererInterface
{
/**
* @param AbstractBlock $block
* @param ElementRendererInterface $htmlRenderer
* @param bool $inTightList
*
* @return HtmlElement|string|null
*/
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false);
}

View File

@@ -0,0 +1,40 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Renderer;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\ElementRendererInterface;
final class DocumentRenderer implements BlockRendererInterface
{
/**
* @param Document $block
* @param ElementRendererInterface $htmlRenderer
* @param bool $inTightList
*
* @return string
*/
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
if (!($block instanceof Document)) {
throw new \InvalidArgumentException('Incompatible block type: ' . \get_class($block));
}
$wholeDoc = $htmlRenderer->renderBlocks($block->children());
return $wholeDoc === '' ? '' : $wholeDoc . "\n";
}
}

View File

@@ -0,0 +1,52 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Renderer;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\FencedCode;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
use League\CommonMark\Util\Xml;
final class FencedCodeRenderer implements BlockRendererInterface
{
/**
* @param FencedCode $block
* @param ElementRendererInterface $htmlRenderer
* @param bool $inTightList
*
* @return HtmlElement
*/
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
if (!($block instanceof FencedCode)) {
throw new \InvalidArgumentException('Incompatible block type: ' . \get_class($block));
}
$attrs = $block->getData('attributes', []);
$infoWords = $block->getInfoWords();
if (\count($infoWords) !== 0 && \strlen($infoWords[0]) !== 0) {
$attrs['class'] = isset($attrs['class']) ? $attrs['class'] . ' ' : '';
$attrs['class'] .= 'language-' . $infoWords[0];
}
return new HtmlElement(
'pre',
[],
new HtmlElement('code', $attrs, Xml::escape($block->getStringContent()))
);
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Renderer;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\Heading;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
final class HeadingRenderer implements BlockRendererInterface
{
/**
* @param Heading $block
* @param ElementRendererInterface $htmlRenderer
* @param bool $inTightList
*
* @return HtmlElement
*/
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
if (!($block instanceof Heading)) {
throw new \InvalidArgumentException('Incompatible block type: ' . \get_class($block));
}
$tag = 'h' . $block->getLevel();
$attrs = $block->getData('attributes', []);
return new HtmlElement($tag, $attrs, $htmlRenderer->renderInlines($block->children()));
}
}

View File

@@ -0,0 +1,59 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Renderer;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\HtmlBlock;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\EnvironmentInterface;
use League\CommonMark\Util\ConfigurationAwareInterface;
use League\CommonMark\Util\ConfigurationInterface;
final class HtmlBlockRenderer implements BlockRendererInterface, ConfigurationAwareInterface
{
/**
* @var ConfigurationInterface
*/
protected $config;
/**
* @param HtmlBlock $block
* @param ElementRendererInterface $htmlRenderer
* @param bool $inTightList
*
* @return string
*/
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
if (!($block instanceof HtmlBlock)) {
throw new \InvalidArgumentException('Incompatible block type: ' . \get_class($block));
}
if ($this->config->get('html_input') === EnvironmentInterface::HTML_INPUT_STRIP) {
return '';
}
if ($this->config->get('html_input') === EnvironmentInterface::HTML_INPUT_ESCAPE) {
return \htmlspecialchars($block->getStringContent(), \ENT_NOQUOTES);
}
return $block->getStringContent();
}
public function setConfiguration(ConfigurationInterface $configuration)
{
$this->config = $configuration;
}
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Renderer;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\IndentedCode;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
use League\CommonMark\Util\Xml;
final class IndentedCodeRenderer implements BlockRendererInterface
{
/**
* @param IndentedCode $block
* @param ElementRendererInterface $htmlRenderer
* @param bool $inTightList
*
* @return HtmlElement
*/
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
if (!($block instanceof IndentedCode)) {
throw new \InvalidArgumentException('Incompatible block type: ' . \get_class($block));
}
$attrs = $block->getData('attributes', []);
return new HtmlElement(
'pre',
[],
new HtmlElement('code', $attrs, Xml::escape($block->getStringContent()))
);
}
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Renderer;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\ListBlock;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
final class ListBlockRenderer implements BlockRendererInterface
{
/**
* @param ListBlock $block
* @param ElementRendererInterface $htmlRenderer
* @param bool $inTightList
*
* @return HtmlElement
*/
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
if (!($block instanceof ListBlock)) {
throw new \InvalidArgumentException('Incompatible block type: ' . \get_class($block));
}
$listData = $block->getListData();
$tag = $listData->type === ListBlock::TYPE_BULLET ? 'ul' : 'ol';
$attrs = $block->getData('attributes', []);
if ($listData->start !== null && $listData->start !== 1) {
$attrs['start'] = (string) $listData->start;
}
return new HtmlElement(
$tag,
$attrs,
$htmlRenderer->getOption('inner_separator', "\n") . $htmlRenderer->renderBlocks(
$block->children(),
$block->isTight()
) . $htmlRenderer->getOption('inner_separator', "\n")
);
}
}

View File

@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Renderer;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\Extension\TaskList\TaskListItemMarker;
use League\CommonMark\HtmlElement;
final class ListItemRenderer implements BlockRendererInterface
{
/**
* @param ListItem $block
* @param ElementRendererInterface $htmlRenderer
* @param bool $inTightList
*
* @return string
*/
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
if (!($block instanceof ListItem)) {
throw new \InvalidArgumentException('Incompatible block type: ' . \get_class($block));
}
$contents = $htmlRenderer->renderBlocks($block->children(), $inTightList);
if (\substr($contents, 0, 1) === '<' && !$this->startsTaskListItem($block)) {
$contents = "\n" . $contents;
}
if (\substr($contents, -1, 1) === '>') {
$contents .= "\n";
}
$attrs = $block->getData('attributes', []);
$li = new HtmlElement('li', $attrs, $contents);
return $li;
}
private function startsTaskListItem(ListItem $block): bool
{
$firstChild = $block->firstChild();
return $firstChild instanceof Paragraph && $firstChild->firstChild() instanceof TaskListItemMarker;
}
}

View File

@@ -0,0 +1,45 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Renderer;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
final class ParagraphRenderer implements BlockRendererInterface
{
/**
* @param Paragraph $block
* @param ElementRendererInterface $htmlRenderer
* @param bool $inTightList
*
* @return HtmlElement|string
*/
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
if (!($block instanceof Paragraph)) {
throw new \InvalidArgumentException('Incompatible block type: ' . \get_class($block));
}
if ($inTightList) {
return $htmlRenderer->renderInlines($block->children());
}
$attrs = $block->getData('attributes', []);
return new HtmlElement('p', $attrs, $htmlRenderer->renderInlines($block->children()));
}
}

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Block\Renderer;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\ThematicBreak;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
final class ThematicBreakRenderer implements BlockRendererInterface
{
/**
* @param ThematicBreak $block
* @param ElementRendererInterface $htmlRenderer
* @param bool $inTightList
*
* @return HtmlElement
*/
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
if (!($block instanceof ThematicBreak)) {
throw new \InvalidArgumentException('Incompatible block type: ' . \get_class($block));
}
$attrs = $block->getData('attributes', []);
return new HtmlElement('hr', $attrs, '', true);
}
}

View File

@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark;
/**
* Converts CommonMark-compatible Markdown to HTML.
*/
class CommonMarkConverter extends Converter
{
/**
* The currently-installed version.
*
* This might be a typical `x.y.z` version, or `x.y-dev`.
*/
public const VERSION = '1.4.3';
/** @var EnvironmentInterface */
protected $environment;
/**
* Create a new commonmark converter instance.
*
* @param array<string, mixed> $config
* @param EnvironmentInterface|null $environment
*/
public function __construct(array $config = [], EnvironmentInterface $environment = null)
{
if ($environment === null) {
$environment = Environment::createCommonMarkEnvironment();
}
if ($environment instanceof ConfigurableEnvironmentInterface) {
$environment->mergeConfig($config);
}
$this->environment = $environment;
parent::__construct(new DocParser($environment), new HtmlRenderer($environment));
}
public function getEnvironment(): EnvironmentInterface
{
return $this->environment;
}
}

View File

@@ -0,0 +1,108 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark;
use League\CommonMark\Block\Parser\BlockParserInterface;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Inline\Parser\InlineParserInterface;
use League\CommonMark\Inline\Renderer\InlineRendererInterface;
/**
* Interface for an Environment which can be configured with config settings, parsers, processors, and renderers
*/
interface ConfigurableEnvironmentInterface extends EnvironmentInterface
{
/**
* @param array<string, mixed> $config
*
* @return void
*/
public function mergeConfig(array $config = []);
/**
* @param array<string, mixed> $config
*
* @return void
*/
public function setConfig(array $config = []);
/**
* Registers the given extension with the Environment
*
* @param ExtensionInterface $extension
*
* @return ConfigurableEnvironmentInterface
*/
public function addExtension(ExtensionInterface $extension): ConfigurableEnvironmentInterface;
/**
* Registers the given block parser with the Environment
*
* @param BlockParserInterface $parser Block parser instance
* @param int $priority Priority (a higher number will be executed earlier)
*
* @return self
*/
public function addBlockParser(BlockParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface;
/**
* Registers the given inline parser with the Environment
*
* @param InlineParserInterface $parser Inline parser instance
* @param int $priority Priority (a higher number will be executed earlier)
*
* @return self
*/
public function addInlineParser(InlineParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface;
/**
* Registers the given delimiter processor with the Environment
*
* @param DelimiterProcessorInterface $processor Delimiter processors instance
*
* @return ConfigurableEnvironmentInterface
*/
public function addDelimiterProcessor(DelimiterProcessorInterface $processor): ConfigurableEnvironmentInterface;
/**
* @param string $blockClass The fully-qualified block element class name the renderer below should handle
* @param BlockRendererInterface $blockRenderer The renderer responsible for rendering the type of element given above
* @param int $priority Priority (a higher number will be executed earlier)
*
* @return self
*/
public function addBlockRenderer($blockClass, BlockRendererInterface $blockRenderer, int $priority = 0): ConfigurableEnvironmentInterface;
/**
* Registers the given inline renderer with the Environment
*
* @param string $inlineClass The fully-qualified inline element class name the renderer below should handle
* @param InlineRendererInterface $renderer The renderer responsible for rendering the type of element given above
* @param int $priority Priority (a higher number will be executed earlier)
*
* @return self
*/
public function addInlineRenderer(string $inlineClass, InlineRendererInterface $renderer, int $priority = 0): ConfigurableEnvironmentInterface;
/**
* Registers the given event listener
*
* @param string $eventClass Fully-qualified class name of the event this listener should respond to
* @param callable $listener Listener to be executed
* @param int $priority Priority (a higher number will be executed earlier)
*
* @return self
*/
public function addEventListener(string $eventClass, callable $listener, int $priority = 0): ConfigurableEnvironmentInterface;
}

200
vendor/league/commonmark/src/Context.php vendored Normal file
View File

@@ -0,0 +1,200 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\Reference\ReferenceParser;
/**
* Maintains the current state of the Markdown parser engine
*/
class Context implements ContextInterface
{
/**
* @var EnvironmentInterface
*/
protected $environment;
/**
* @var Document
*/
protected $doc;
/**
* @var AbstractBlock|null
*/
protected $tip;
/**
* @var AbstractBlock
*/
protected $container;
/**
* @var int
*/
protected $lineNumber;
/**
* @var string
*/
protected $line;
/**
* @var UnmatchedBlockCloser
*/
protected $blockCloser;
/**
* @var bool
*/
protected $blocksParsed = false;
/**
* @var ReferenceParser
*/
protected $referenceParser;
public function __construct(Document $document, EnvironmentInterface $environment)
{
$this->doc = $document;
$this->tip = $this->doc;
$this->container = $this->doc;
$this->environment = $environment;
$this->referenceParser = new ReferenceParser($document->getReferenceMap());
$this->blockCloser = new UnmatchedBlockCloser($this);
}
/**
* @param string $line
*
* @return void
*/
public function setNextLine(string $line)
{
++$this->lineNumber;
$this->line = $line;
}
public function getDocument(): Document
{
return $this->doc;
}
public function getTip(): ?AbstractBlock
{
return $this->tip;
}
/**
* @param AbstractBlock|null $block
*
* @return $this
*/
public function setTip(?AbstractBlock $block)
{
$this->tip = $block;
return $this;
}
public function getLineNumber(): int
{
return $this->lineNumber;
}
public function getLine(): string
{
return $this->line;
}
public function getBlockCloser(): UnmatchedBlockCloser
{
return $this->blockCloser;
}
public function getContainer(): AbstractBlock
{
return $this->container;
}
/**
* @param AbstractBlock $container
*
* @return $this
*/
public function setContainer(AbstractBlock $container)
{
$this->container = $container;
return $this;
}
public function addBlock(AbstractBlock $block)
{
$this->blockCloser->closeUnmatchedBlocks();
$block->setStartLine($this->lineNumber);
while ($this->tip !== null && !$this->tip->canContain($block)) {
$this->tip->finalize($this, $this->lineNumber);
}
// This should always be true
if ($this->tip !== null) {
$this->tip->appendChild($block);
}
$this->tip = $block;
$this->container = $block;
}
public function replaceContainerBlock(AbstractBlock $replacement)
{
$this->blockCloser->closeUnmatchedBlocks();
$this->container->replaceWith($replacement);
if ($this->tip === $this->container) {
$this->tip = $replacement;
}
$this->container = $replacement;
}
public function getBlocksParsed(): bool
{
return $this->blocksParsed;
}
/**
* @param bool $bool
*
* @return $this
*/
public function setBlocksParsed(bool $bool)
{
$this->blocksParsed = $bool;
return $this;
}
public function getReferenceParser(): ReferenceParser
{
return $this->referenceParser;
}
}

View File

@@ -0,0 +1,99 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\Reference\ReferenceParser;
interface ContextInterface
{
/**
* @return Document
*/
public function getDocument(): Document;
/**
* @return AbstractBlock|null
*/
public function getTip(): ?AbstractBlock;
/**
* @param AbstractBlock|null $block
*
* @return void
*/
public function setTip(?AbstractBlock $block);
/**
* @return int
*/
public function getLineNumber(): int;
/**
* @return string
*/
public function getLine(): string;
/**
* Finalize and close any unmatched blocks
*
* @return UnmatchedBlockCloser
*/
public function getBlockCloser(): UnmatchedBlockCloser;
/**
* @return AbstractBlock
*/
public function getContainer(): AbstractBlock;
/**
* @param AbstractBlock $container
*
* @return void
*/
public function setContainer(AbstractBlock $container);
/**
* @param AbstractBlock $block
*
* @return void
*/
public function addBlock(AbstractBlock $block);
/**
* @param AbstractBlock $replacement
*
* @return void
*/
public function replaceContainerBlock(AbstractBlock $replacement);
/**
* @return bool
*/
public function getBlocksParsed(): bool;
/**
* @param bool $bool
*
* @return $this
*/
public function setBlocksParsed(bool $bool);
/**
* @return ReferenceParser
*/
public function getReferenceParser(): ReferenceParser;
}

View File

@@ -0,0 +1,84 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark;
/**
* Converts CommonMark-compatible Markdown to HTML.
*
* @deprecated This class is deprecated since league/commonmark 1.4, use CommonMarkConverter instead.
*/
class Converter implements ConverterInterface
{
/**
* The document parser instance.
*
* @var DocParserInterface
*/
protected $docParser;
/**
* The html renderer instance.
*
* @var ElementRendererInterface
*/
protected $htmlRenderer;
/**
* Create a new commonmark converter instance.
*
* @param DocParserInterface $docParser
* @param ElementRendererInterface $htmlRenderer
*/
public function __construct(DocParserInterface $docParser, ElementRendererInterface $htmlRenderer)
{
if (!($this instanceof CommonMarkConverter)) {
@trigger_error(sprintf('The %s class is deprecated since league/commonmark 1.4, use %s instead.', self::class, CommonMarkConverter::class), E_USER_DEPRECATED);
}
$this->docParser = $docParser;
$this->htmlRenderer = $htmlRenderer;
}
/**
* Converts CommonMark to HTML.
*
* @param string $commonMark
*
* @throws \RuntimeException
*
* @return string
*
* @api
*/
public function convertToHtml(string $commonMark): string
{
$documentAST = $this->docParser->parse($commonMark);
return $this->htmlRenderer->renderBlock($documentAST);
}
/**
* Converts CommonMark to HTML.
*
* @see Converter::convertToHtml
*
* @param string $commonMark
*
* @throws \RuntimeException
*
* @return string
*/
public function __invoke(string $commonMark): string
{
return $this->convertToHtml($commonMark);
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark;
/**
* Interface for a service which converts CommonMark to HTML.
*
* @deprecated ConverterInterface is deprecated since league/commonmark 1.4, use MarkdownConverterInterface instead
*/
interface ConverterInterface extends MarkdownConverterInterface
{
}

496
vendor/league/commonmark/src/Cursor.php vendored Normal file
View File

@@ -0,0 +1,496 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark;
class Cursor
{
public const INDENT_LEVEL = 4;
/**
* @var string
*/
private $line;
/**
* @var int
*/
private $length;
/**
* @var int
*
* It's possible for this to be 1 char past the end, meaning we've parsed all chars and have
* reached the end. In this state, any character-returning method MUST return null.
*/
private $currentPosition = 0;
/**
* @var int
*/
private $column = 0;
/**
* @var int
*/
private $indent = 0;
/**
* @var int
*/
private $previousPosition = 0;
/**
* @var int|null
*/
private $nextNonSpaceCache;
/**
* @var bool
*/
private $partiallyConsumedTab = false;
/**
* @var bool
*/
private $lineContainsTabs;
/**
* @var bool
*/
private $isMultibyte;
/**
* @var array<int, string>
*/
private $charCache = [];
/**
* @param string $line The line being parsed (ASCII or UTF-8)
*/
public function __construct(string $line)
{
$this->line = $line;
$this->length = \mb_strlen($line, 'UTF-8') ?: 0;
$this->isMultibyte = $this->length !== \strlen($line);
$this->lineContainsTabs = false !== \strpos($line, "\t");
}
/**
* Returns the position of the next character which is not a space (or tab)
*
* @return int
*/
public function getNextNonSpacePosition(): int
{
if ($this->nextNonSpaceCache !== null) {
return $this->nextNonSpaceCache;
}
$i = $this->currentPosition;
$cols = $this->column;
while (($c = $this->getCharacter($i)) !== null) {
if ($c === ' ') {
$i++;
$cols++;
} elseif ($c === "\t") {
$i++;
$cols += (4 - ($cols % 4));
} else {
break;
}
}
$nextNonSpace = ($c === null) ? $this->length : $i;
$this->indent = $cols - $this->column;
return $this->nextNonSpaceCache = $nextNonSpace;
}
/**
* Returns the next character which isn't a space (or tab)
*
* @return string
*/
public function getNextNonSpaceCharacter(): ?string
{
return $this->getCharacter($this->getNextNonSpacePosition());
}
/**
* Calculates the current indent (number of spaces after current position)
*
* @return int
*/
public function getIndent(): int
{
if ($this->nextNonSpaceCache === null) {
$this->getNextNonSpacePosition();
}
return $this->indent;
}
/**
* Whether the cursor is indented to INDENT_LEVEL
*
* @return bool
*/
public function isIndented(): bool
{
return $this->getIndent() >= self::INDENT_LEVEL;
}
/**
* @param int|null $index
*
* @return string|null
*/
public function getCharacter(?int $index = null): ?string
{
if ($index === null) {
$index = $this->currentPosition;
}
// Index out-of-bounds, or we're at the end
if ($index < 0 || $index >= $this->length) {
return null;
}
if ($this->isMultibyte) {
if (isset($this->charCache[$index])) {
return $this->charCache[$index];
}
return $this->charCache[$index] = \mb_substr($this->line, $index, 1, 'UTF-8');
}
return $this->line[$index];
}
/**
* Returns the next character (or null, if none) without advancing forwards
*
* @param int $offset
*
* @return string|null
*/
public function peek(int $offset = 1): ?string
{
return $this->getCharacter($this->currentPosition + $offset);
}
/**
* Whether the remainder is blank
*
* @return bool
*/
public function isBlank(): bool
{
return $this->nextNonSpaceCache === $this->length || $this->getNextNonSpacePosition() === $this->length;
}
/**
* Move the cursor forwards
*
* @return void
*/
public function advance()
{
$this->advanceBy(1);
}
/**
* Move the cursor forwards
*
* @param int $characters Number of characters to advance by
* @param bool $advanceByColumns Whether to advance by columns instead of spaces
*
* @return void
*/
public function advanceBy(int $characters, bool $advanceByColumns = false)
{
if ($characters === 0) {
$this->previousPosition = $this->currentPosition;
return;
}
$this->previousPosition = $this->currentPosition;
$this->nextNonSpaceCache = null;
// Optimization to avoid tab handling logic if we have no tabs
if (!$this->lineContainsTabs || false === \strpos(
$nextFewChars = $this->isMultibyte ?
\mb_substr($this->line, $this->currentPosition, $characters, 'UTF-8') :
\substr($this->line, $this->currentPosition, $characters),
"\t"
)) {
$length = \min($characters, $this->length - $this->currentPosition);
$this->partiallyConsumedTab = false;
$this->currentPosition += $length;
$this->column += $length;
return;
}
if ($characters === 1 && !empty($nextFewChars)) {
$asArray = [$nextFewChars];
} elseif ($this->isMultibyte) {
/** @var string[] $asArray */
$asArray = \preg_split('//u', $nextFewChars, -1, \PREG_SPLIT_NO_EMPTY);
} else {
$asArray = \str_split($nextFewChars);
}
foreach ($asArray as $relPos => $c) {
if ($c === "\t") {
$charsToTab = 4 - ($this->column % 4);
if ($advanceByColumns) {
$this->partiallyConsumedTab = $charsToTab > $characters;
$charsToAdvance = $charsToTab > $characters ? $characters : $charsToTab;
$this->column += $charsToAdvance;
$this->currentPosition += $this->partiallyConsumedTab ? 0 : 1;
$characters -= $charsToAdvance;
} else {
$this->partiallyConsumedTab = false;
$this->column += $charsToTab;
$this->currentPosition++;
$characters--;
}
} else {
$this->partiallyConsumedTab = false;
$this->currentPosition++;
$this->column++;
$characters--;
}
if ($characters <= 0) {
break;
}
}
}
/**
* Advances the cursor by a single space or tab, if present
*
* @return bool
*/
public function advanceBySpaceOrTab(): bool
{
$character = $this->getCharacter();
if ($character === ' ' || $character === "\t") {
$this->advanceBy(1, true);
return true;
}
return false;
}
/**
* Parse zero or more space/tab characters
*
* @return int Number of positions moved
*/
public function advanceToNextNonSpaceOrTab(): int
{
$newPosition = $this->getNextNonSpacePosition();
$this->advanceBy($newPosition - $this->currentPosition);
$this->partiallyConsumedTab = false;
return $this->currentPosition - $this->previousPosition;
}
/**
* Parse zero or more space characters, including at most one newline.
*
* Tab characters are not parsed with this function.
*
* @return int Number of positions moved
*/
public function advanceToNextNonSpaceOrNewline(): int
{
$remainder = $this->getRemainder();
// Optimization: Avoid the regex if we know there are no spaces or newlines
if (empty($remainder) || ($remainder[0] !== ' ' && $remainder[0] !== "\n")) {
$this->previousPosition = $this->currentPosition;
return 0;
}
$matches = [];
\preg_match('/^ *(?:\n *)?/', $remainder, $matches, \PREG_OFFSET_CAPTURE);
// [0][0] contains the matched text
// [0][1] contains the index of that match
$increment = $matches[0][1] + \strlen($matches[0][0]);
$this->advanceBy($increment);
return $this->currentPosition - $this->previousPosition;
}
/**
* Move the position to the very end of the line
*
* @return int The number of characters moved
*/
public function advanceToEnd(): int
{
$this->previousPosition = $this->currentPosition;
$this->nextNonSpaceCache = null;
$this->currentPosition = $this->length;
return $this->currentPosition - $this->previousPosition;
}
public function getRemainder(): string
{
if ($this->currentPosition >= $this->length) {
return '';
}
$prefix = '';
$position = $this->currentPosition;
if ($this->partiallyConsumedTab) {
$position++;
$charsToTab = 4 - ($this->column % 4);
$prefix = \str_repeat(' ', $charsToTab);
}
$subString = $this->isMultibyte ?
\mb_substr($this->line, $position, null, 'UTF-8') :
\substr($this->line, $position);
return $prefix . $subString;
}
public function getLine(): string
{
return $this->line;
}
public function isAtEnd(): bool
{
return $this->currentPosition >= $this->length;
}
/**
* Try to match a regular expression
*
* Returns the matching text and advances to the end of that match
*
* @param string $regex
*
* @return string|null
*/
public function match(string $regex): ?string
{
$subject = $this->getRemainder();
if (!\preg_match($regex, $subject, $matches, \PREG_OFFSET_CAPTURE)) {
return null;
}
// $matches[0][0] contains the matched text
// $matches[0][1] contains the index of that match
if ($this->isMultibyte) {
// PREG_OFFSET_CAPTURE always returns the byte offset, not the char offset, which is annoying
$offset = \mb_strlen(\substr($subject, 0, $matches[0][1]), 'UTF-8');
$matchLength = \mb_strlen($matches[0][0], 'UTF-8');
} else {
$offset = $matches[0][1];
$matchLength = \strlen($matches[0][0]);
}
// [0][0] contains the matched text
// [0][1] contains the index of that match
$this->advanceBy($offset + $matchLength);
return $matches[0][0];
}
/**
* Encapsulates the current state of this cursor in case you need to rollback later.
*
* WARNING: Do not parse or use the return value for ANYTHING except for
* passing it back into restoreState(), as the number of values and their
* contents may change in any future release without warning.
*
* @return array<mixed>
*/
public function saveState()
{
return [
$this->currentPosition,
$this->previousPosition,
$this->nextNonSpaceCache,
$this->indent,
$this->column,
$this->partiallyConsumedTab,
];
}
/**
* Restore the cursor to a previous state.
*
* Pass in the value previously obtained by calling saveState().
*
* @param array<mixed> $state
*
* @return void
*/
public function restoreState($state)
{
list(
$this->currentPosition,
$this->previousPosition,
$this->nextNonSpaceCache,
$this->indent,
$this->column,
$this->partiallyConsumedTab,
) = $state;
}
public function getPosition(): int
{
return $this->currentPosition;
}
public function getPreviousText(): string
{
return \mb_substr($this->line, $this->previousPosition, $this->currentPosition - $this->previousPosition, 'UTF-8');
}
public function getSubstring(int $start, ?int $length = null): string
{
if ($this->isMultibyte) {
return \mb_substr($this->line, $start, $length, 'UTF-8');
} elseif ($length !== null) {
return \substr($this->line, $start, $length);
}
return \substr($this->line, $start);
}
public function getColumn(): int
{
return $this->column;
}
}

View File

@@ -0,0 +1,152 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Delimiter;
use League\CommonMark\Inline\Element\AbstractStringContainer;
final class Delimiter implements DelimiterInterface
{
/** @var string */
private $char;
/** @var int */
private $length;
/** @var int */
private $originalLength;
/** @var AbstractStringContainer */
private $inlineNode;
/** @var DelimiterInterface|null */
private $previous;
/** @var DelimiterInterface|null */
private $next;
/** @var bool */
private $canOpen;
/** @var bool */
private $canClose;
/** @var bool */
private $active;
/** @var int|null */
private $index;
/**
* @param string $char
* @param int $numDelims
* @param AbstractStringContainer $node
* @param bool $canOpen
* @param bool $canClose
* @param int|null $index
*/
public function __construct(string $char, int $numDelims, AbstractStringContainer $node, bool $canOpen, bool $canClose, ?int $index = null)
{
$this->char = $char;
$this->length = $numDelims;
$this->originalLength = $numDelims;
$this->inlineNode = $node;
$this->canOpen = $canOpen;
$this->canClose = $canClose;
$this->active = true;
$this->index = $index;
}
public function canClose(): bool
{
return $this->canClose;
}
/**
* @param bool $canClose
*
* @return void
*/
public function setCanClose(bool $canClose)
{
$this->canClose = $canClose;
}
public function canOpen(): bool
{
return $this->canOpen;
}
public function isActive(): bool
{
return $this->active;
}
public function setActive(bool $active)
{
$this->active = $active;
}
public function getChar(): string
{
return $this->char;
}
public function getIndex(): ?int
{
return $this->index;
}
public function getNext(): ?DelimiterInterface
{
return $this->next;
}
public function setNext(?DelimiterInterface $next)
{
$this->next = $next;
}
public function getLength(): int
{
return $this->length;
}
public function setLength(int $length)
{
$this->length = $length;
}
public function getOriginalLength(): int
{
return $this->originalLength;
}
public function getInlineNode(): AbstractStringContainer
{
return $this->inlineNode;
}
public function getPrevious(): ?DelimiterInterface
{
return $this->previous;
}
public function setPrevious(?DelimiterInterface $previous): DelimiterInterface
{
$this->previous = $previous;
return $this;
}
}

View File

@@ -0,0 +1,71 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Delimiter;
use League\CommonMark\Inline\Element\AbstractStringContainer;
interface DelimiterInterface
{
public function canClose(): bool;
public function canOpen(): bool;
public function isActive(): bool;
/**
* @param bool $active
*
* @return void
*/
public function setActive(bool $active);
/**
* @return string
*/
public function getChar(): string;
public function getIndex(): ?int;
public function getNext(): ?DelimiterInterface;
/**
* @param DelimiterInterface|null $next
*
* @return void
*/
public function setNext(?DelimiterInterface $next);
public function getLength(): int;
/**
* @param int $length
*
* @return void
*/
public function setLength(int $length);
public function getOriginalLength(): int;
public function getInlineNode(): AbstractStringContainer;
public function getPrevious(): ?DelimiterInterface;
/**
* @param DelimiterInterface|null $previous
*
* @return mixed|void
*/
public function setPrevious(?DelimiterInterface $previous);
}

View File

@@ -0,0 +1,234 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java)
* - (c) Atlassian Pty Ltd
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Delimiter;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
use League\CommonMark\Inline\AdjacentTextMerger;
final class DelimiterStack
{
/**
* @var DelimiterInterface|null
*/
private $top;
/**
* @param DelimiterInterface $newDelimiter
*
* @return void
*/
public function push(DelimiterInterface $newDelimiter)
{
$newDelimiter->setPrevious($this->top);
if ($this->top !== null) {
$this->top->setNext($newDelimiter);
}
$this->top = $newDelimiter;
}
private function findEarliest(DelimiterInterface $stackBottom = null): ?DelimiterInterface
{
$delimiter = $this->top;
while ($delimiter !== null && $delimiter->getPrevious() !== $stackBottom) {
$delimiter = $delimiter->getPrevious();
}
return $delimiter;
}
/**
* @param DelimiterInterface $delimiter
*
* @return void
*/
public function removeDelimiter(DelimiterInterface $delimiter)
{
if ($delimiter->getPrevious() !== null) {
$delimiter->getPrevious()->setNext($delimiter->getNext());
}
if ($delimiter->getNext() === null) {
// top of stack
$this->top = $delimiter->getPrevious();
} else {
$delimiter->getNext()->setPrevious($delimiter->getPrevious());
}
}
private function removeDelimiterAndNode(DelimiterInterface $delimiter): void
{
$delimiter->getInlineNode()->detach();
$this->removeDelimiter($delimiter);
}
private function removeDelimitersBetween(DelimiterInterface $opener, DelimiterInterface $closer): void
{
$delimiter = $closer->getPrevious();
while ($delimiter !== null && $delimiter !== $opener) {
$previous = $delimiter->getPrevious();
$this->removeDelimiter($delimiter);
$delimiter = $previous;
}
}
/**
* @param DelimiterInterface|null $stackBottom
*
* @return void
*/
public function removeAll(DelimiterInterface $stackBottom = null)
{
while ($this->top && $this->top !== $stackBottom) {
$this->removeDelimiter($this->top);
}
}
/**
* @param string $character
*
* @return void
*/
public function removeEarlierMatches(string $character)
{
$opener = $this->top;
while ($opener !== null) {
if ($opener->getChar() === $character) {
$opener->setActive(false);
}
$opener = $opener->getPrevious();
}
}
/**
* @param string|string[] $characters
*
* @return DelimiterInterface|null
*/
public function searchByCharacter($characters): ?DelimiterInterface
{
if (!\is_array($characters)) {
$characters = [$characters];
}
$opener = $this->top;
while ($opener !== null) {
if (\in_array($opener->getChar(), $characters)) {
break;
}
$opener = $opener->getPrevious();
}
return $opener;
}
/**
* @param DelimiterInterface|null $stackBottom
* @param DelimiterProcessorCollection $processors
*
* @return void
*/
public function processDelimiters(?DelimiterInterface $stackBottom, DelimiterProcessorCollection $processors)
{
$openersBottom = [];
// Find first closer above stackBottom
$closer = $this->findEarliest($stackBottom);
// Move forward, looking for closers, and handling each
while ($closer !== null) {
$delimiterChar = $closer->getChar();
$delimiterProcessor = $processors->getDelimiterProcessor($delimiterChar);
if (!$closer->canClose() || $delimiterProcessor === null) {
$closer = $closer->getNext();
continue;
}
$openingDelimiterChar = $delimiterProcessor->getOpeningCharacter();
$useDelims = 0;
$openerFound = false;
$potentialOpenerFound = false;
$opener = $closer->getPrevious();
while ($opener !== null && $opener !== $stackBottom && $opener !== ($openersBottom[$delimiterChar] ?? null)) {
if ($opener->canOpen() && $opener->getChar() === $openingDelimiterChar) {
$potentialOpenerFound = true;
$useDelims = $delimiterProcessor->getDelimiterUse($opener, $closer);
if ($useDelims > 0) {
$openerFound = true;
break;
}
}
$opener = $opener->getPrevious();
}
if (!$openerFound) {
if (!$potentialOpenerFound) {
// Only do this when we didn't even have a potential
// opener (one that matches the character and can open).
// If an opener was rejected because of the number of
// delimiters (e.g. because of the "multiple of 3"
// Set lower bound for future searches for openersrule),
// we want to consider it next time because the number
// of delimiters can change as we continue processing.
$openersBottom[$delimiterChar] = $closer->getPrevious();
if (!$closer->canOpen()) {
// We can remove a closer that can't be an opener,
// once we've seen there's no matching opener.
$this->removeDelimiter($closer);
}
}
$closer = $closer->getNext();
continue;
}
$openerNode = $opener->getInlineNode();
$closerNode = $closer->getInlineNode();
// Remove number of used delimiters from stack and inline nodes.
$opener->setLength($opener->getLength() - $useDelims);
$closer->setLength($closer->getLength() - $useDelims);
$openerNode->setContent(\substr($openerNode->getContent(), 0, -$useDelims));
$closerNode->setContent(\substr($closerNode->getContent(), 0, -$useDelims));
$this->removeDelimitersBetween($opener, $closer);
// The delimiter processor can re-parent the nodes between opener and closer,
// so make sure they're contiguous already. Exclusive because we want to keep opener/closer themselves.
AdjacentTextMerger::mergeTextNodesBetweenExclusive($openerNode, $closerNode);
$delimiterProcessor->process($openerNode, $closerNode, $useDelims);
// No delimiter characters left to process, so we can remove delimiter and the now empty node.
if ($opener->getLength() === 0) {
$this->removeDelimiterAndNode($opener);
}
if ($closer->getLength() === 0) {
$next = $closer->getNext();
$this->removeDelimiterAndNode($closer);
$closer = $next;
}
}
// Remove all delimiters
$this->removeAll($stackBottom);
}
}

View File

@@ -0,0 +1,73 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java)
* - (c) Atlassian Pty Ltd
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Delimiter\Processor;
final class DelimiterProcessorCollection implements DelimiterProcessorCollectionInterface
{
/** @var array<string,DelimiterProcessorInterface>|DelimiterProcessorInterface[] */
private $processorsByChar = [];
public function add(DelimiterProcessorInterface $processor)
{
$opening = $processor->getOpeningCharacter();
$closing = $processor->getClosingCharacter();
if ($opening === $closing) {
$old = $this->processorsByChar[$opening] ?? null;
if ($old !== null && $old->getOpeningCharacter() === $old->getClosingCharacter()) {
$this->addStaggeredDelimiterProcessorForChar($opening, $old, $processor);
} else {
$this->addDelimiterProcessorForChar($opening, $processor);
}
} else {
$this->addDelimiterProcessorForChar($opening, $processor);
$this->addDelimiterProcessorForChar($closing, $processor);
}
}
public function getDelimiterProcessor(string $char): ?DelimiterProcessorInterface
{
return $this->processorsByChar[$char] ?? null;
}
public function getDelimiterCharacters(): array
{
return \array_keys($this->processorsByChar);
}
private function addDelimiterProcessorForChar(string $delimiterChar, DelimiterProcessorInterface $processor): void
{
if (isset($this->processorsByChar[$delimiterChar])) {
throw new \InvalidArgumentException(\sprintf('Delim processor for character "%s" already exists', $processor->getOpeningCharacter()));
}
$this->processorsByChar[$delimiterChar] = $processor;
}
private function addStaggeredDelimiterProcessorForChar(string $opening, DelimiterProcessorInterface $old, DelimiterProcessorInterface $new): void
{
if ($old instanceof StaggeredDelimiterProcessor) {
$s = $old;
} else {
$s = new StaggeredDelimiterProcessor($opening, $old);
}
$s->add($new);
$this->processorsByChar[$opening] = $s;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java)
* - (c) Atlassian Pty Ltd
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Delimiter\Processor;
interface DelimiterProcessorCollectionInterface
{
/**
* Add the given delim processor to the collection
*
* @param DelimiterProcessorInterface $processor The delim processor to add
*
* @throws \InvalidArgumentException Exception will be thrown if attempting to add multiple processors for the same character
*
* @return void
*/
public function add(DelimiterProcessorInterface $processor);
/**
* Returns the delim processor which handles the given character if one exists
*
* @param string $char
*
* @return DelimiterProcessorInterface|null
*/
public function getDelimiterProcessor(string $char): ?DelimiterProcessorInterface;
/**
* Returns an array of delimiter characters who have associated processors
*
* @return string[]
*/
public function getDelimiterCharacters(): array;
}

View File

@@ -0,0 +1,86 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java)
* - (c) Atlassian Pty Ltd
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Delimiter\Processor;
use League\CommonMark\Delimiter\DelimiterInterface;
use League\CommonMark\Inline\Element\AbstractStringContainer;
/**
* Interface for a delimiter processor
*/
interface DelimiterProcessorInterface
{
/**
* Returns the character that marks the beginning of a delimited node.
*
* This must not clash with any other processors being added to the environment.
*
* @return string
*/
public function getOpeningCharacter(): string;
/**
* Returns the character that marks the ending of a delimited node.
*
* This must not clash with any other processors being added to the environment.
*
* Note that for a symmetric delimiter such as "*", this is the same as the opening.
*
* @return string
*/
public function getClosingCharacter(): string;
/**
* Minimum number of delimiter characters that are needed to active this.
*
* Must be at least 1.
*
* @return int
*/
public function getMinLength(): int;
/**
* Determine how many (if any) of the delimiter characters should be used.
*
* This allows implementations to decide how many characters to be used
* based on the properties of the delimiter runs. An implementation can also
* return 0 when it doesn't want to allow this particular combination of
* delimiter runs.
*
* @param DelimiterInterface $opener The opening delimiter run
* @param DelimiterInterface $closer The closing delimiter run
*
* @return int
*/
public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int;
/**
* Process the matched delimiters, e.g. by wrapping the nodes between opener
* and closer in a new node, or appending a new node after the opener.
*
* Note that removal of the delimiter from the delimiter nodes and detaching
* them is done by the caller.
*
* @param AbstractStringContainer $opener The node that contained the opening delimiter
* @param AbstractStringContainer $closer The node that contained the closing delimiter
* @param int $delimiterUse The number of delimiters that were used
*
* @return void
*/
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse);
}

View File

@@ -0,0 +1,105 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java)
* - (c) Atlassian Pty Ltd
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Delimiter\Processor;
use League\CommonMark\Delimiter\DelimiterInterface;
use League\CommonMark\Inline\Element\AbstractStringContainer;
use League\CommonMark\Inline\Element\Emphasis;
use League\CommonMark\Inline\Element\Strong;
use League\CommonMark\Util\ConfigurationAwareInterface;
use League\CommonMark\Util\ConfigurationInterface;
final class EmphasisDelimiterProcessor implements DelimiterProcessorInterface, ConfigurationAwareInterface
{
/** @var string */
private $char;
/** @var ConfigurationInterface|null */
private $config;
/**
* @param string $char The emphasis character to use (typically '*' or '_')
*/
public function __construct(string $char)
{
$this->char = $char;
}
public function getOpeningCharacter(): string
{
return $this->char;
}
public function getClosingCharacter(): string
{
return $this->char;
}
public function getMinLength(): int
{
return 1;
}
public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int
{
// "Multiple of 3" rule for internal delimiter runs
if (($opener->canClose() || $closer->canOpen()) && $closer->getOriginalLength() % 3 !== 0 && ($opener->getOriginalLength() + $closer->getOriginalLength()) % 3 === 0) {
return 0;
}
// Calculate actual number of delimiters used from this closer
if ($opener->getLength() >= 2 && $closer->getLength() >= 2) {
if ($this->config && $this->config->get('enable_strong', true)) {
return 2;
}
return 0;
}
if ($this->config && $this->config->get('enable_em', true)) {
return 1;
}
return 0;
}
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse)
{
if ($delimiterUse === 1) {
$emphasis = new Emphasis();
} elseif ($delimiterUse === 2) {
$emphasis = new Strong();
} else {
return;
}
$next = $opener->next();
while ($next !== null && $next !== $closer) {
$tmp = $next->next();
$emphasis->appendChild($next);
$next = $tmp;
}
$opener->insertAfter($emphasis);
}
public function setConfiguration(ConfigurationInterface $configuration)
{
$this->config = $configuration;
}
}

View File

@@ -0,0 +1,106 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java)
* - (c) Atlassian Pty Ltd
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Delimiter\Processor;
use League\CommonMark\Delimiter\DelimiterInterface;
use League\CommonMark\Inline\Element\AbstractStringContainer;
/**
* An implementation of DelimiterProcessorInterface that dispatches all calls to two or more other DelimiterProcessors
* depending on the length of the delimiter run. All child DelimiterProcessors must have different minimum
* lengths. A given delimiter run is dispatched to the child with the largest acceptable minimum length. If no
* child is applicable, the one with the largest minimum length is chosen.
*
* @internal
*/
final class StaggeredDelimiterProcessor implements DelimiterProcessorInterface
{
/** @var string */
private $delimiterChar;
/** @var int */
private $minLength = 0;
/** @var array<int, DelimiterProcessorInterface>|DelimiterProcessorInterface[] */
private $processors = []; // keyed by minLength in reverse order
public function __construct(string $char, DelimiterProcessorInterface $processor)
{
$this->delimiterChar = $char;
$this->add($processor);
}
public function getOpeningCharacter(): string
{
return $this->delimiterChar;
}
public function getClosingCharacter(): string
{
return $this->delimiterChar;
}
public function getMinLength(): int
{
return $this->minLength;
}
/**
* Adds the given processor to this staggered delimiter processor
*
* @param DelimiterProcessorInterface $processor
*
* @return void
*/
public function add(DelimiterProcessorInterface $processor)
{
$len = $processor->getMinLength();
if (isset($this->processors[$len])) {
throw new \InvalidArgumentException(\sprintf('Cannot add two delimiter processors for char "%s" and minimum length %d', $this->delimiterChar, $len));
}
$this->processors[$len] = $processor;
\krsort($this->processors);
$this->minLength = \min($this->minLength, $len);
}
public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int
{
return $this->findProcessor($opener->getLength())->getDelimiterUse($opener, $closer);
}
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse)
{
$this->findProcessor($delimiterUse)->process($opener, $closer, $delimiterUse);
}
private function findProcessor(int $len): DelimiterProcessorInterface
{
// Find the "longest" processor which can handle this length
foreach ($this->processors as $processor) {
if ($processor->getMinLength() <= $len) {
return $processor;
}
}
// Just use the first one in our list
/** @var DelimiterProcessorInterface $first */
$first = \reset($this->processors);
return $first;
}
}

View File

@@ -0,0 +1,229 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\AbstractStringContainerBlock;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\Block\Element\StringContainerInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Event\DocumentPreParsedEvent;
use League\CommonMark\Input\MarkdownInput;
final class DocParser implements DocParserInterface
{
/**
* @var EnvironmentInterface
*/
private $environment;
/**
* @var InlineParserEngine
*/
private $inlineParserEngine;
/**
* @var int|float
*/
private $maxNestingLevel;
/**
* @param EnvironmentInterface $environment
*/
public function __construct(EnvironmentInterface $environment)
{
$this->environment = $environment;
$this->inlineParserEngine = new InlineParserEngine($environment);
$this->maxNestingLevel = $environment->getConfig('max_nesting_level', \INF);
}
/**
* @param string $input
*
* @throws \RuntimeException
*
* @return Document
*/
public function parse(string $input): Document
{
$document = new Document();
$preParsedEvent = new DocumentPreParsedEvent($document, new MarkdownInput($input));
$this->environment->dispatch($preParsedEvent);
$markdown = $preParsedEvent->getMarkdown();
$context = new Context($document, $this->environment);
foreach ($markdown->getLines() as $line) {
$context->setNextLine($line);
$this->incorporateLine($context);
}
$lineCount = $markdown->getLineCount();
while ($tip = $context->getTip()) {
$tip->finalize($context, $lineCount);
}
$this->processInlines($context);
$this->environment->dispatch(new DocumentParsedEvent($document));
return $document;
}
private function incorporateLine(ContextInterface $context): void
{
$context->getBlockCloser()->resetTip();
$context->setBlocksParsed(false);
$cursor = new Cursor($context->getLine());
$this->resetContainer($context, $cursor);
$context->getBlockCloser()->setLastMatchedContainer($context->getContainer());
$this->parseBlocks($context, $cursor);
// What remains at the offset is a text line. Add the text to the appropriate container.
// First check for a lazy paragraph continuation:
if ($this->handleLazyParagraphContinuation($context, $cursor)) {
return;
}
// not a lazy continuation
// finalize any blocks not matched
$context->getBlockCloser()->closeUnmatchedBlocks();
// Determine whether the last line is blank, updating parents as needed
$this->setAndPropagateLastLineBlank($context, $cursor);
// Handle any remaining cursor contents
if ($context->getContainer() instanceof StringContainerInterface) {
$context->getContainer()->handleRemainingContents($context, $cursor);
} elseif (!$cursor->isBlank()) {
// Create paragraph container for line
$p = new Paragraph();
$context->addBlock($p);
$cursor->advanceToNextNonSpaceOrTab();
$p->addLine($cursor->getRemainder());
}
}
private function processInlines(ContextInterface $context): void
{
$walker = $context->getDocument()->walker();
while ($event = $walker->next()) {
if (!$event->isEntering()) {
continue;
}
$node = $event->getNode();
if ($node instanceof AbstractStringContainerBlock) {
$this->inlineParserEngine->parse($node, $context->getDocument()->getReferenceMap());
}
}
}
/**
* Sets the container to the last open child (or its parent)
*
* @param ContextInterface $context
* @param Cursor $cursor
*/
private function resetContainer(ContextInterface $context, Cursor $cursor): void
{
$container = $context->getDocument();
while ($lastChild = $container->lastChild()) {
if (!($lastChild instanceof AbstractBlock)) {
break;
}
if (!$lastChild->isOpen()) {
break;
}
$container = $lastChild;
if (!$container->matchesNextLine($cursor)) {
$container = $container->parent(); // back up to the last matching block
break;
}
}
$context->setContainer($container);
}
/**
* Parse blocks
*
* @param ContextInterface $context
* @param Cursor $cursor
*/
private function parseBlocks(ContextInterface $context, Cursor $cursor): void
{
while (!$context->getContainer()->isCode() && !$context->getBlocksParsed()) {
$parsed = false;
foreach ($this->environment->getBlockParsers() as $parser) {
if ($parser->parse($context, $cursor)) {
$parsed = true;
break;
}
}
if (!$parsed || $context->getContainer() instanceof StringContainerInterface || (($tip = $context->getTip()) && $tip->getDepth() >= $this->maxNestingLevel)) {
$context->setBlocksParsed(true);
break;
}
}
}
private function handleLazyParagraphContinuation(ContextInterface $context, Cursor $cursor): bool
{
$tip = $context->getTip();
if ($tip instanceof Paragraph &&
!$context->getBlockCloser()->areAllClosed() &&
!$cursor->isBlank() &&
\count($tip->getStrings()) > 0) {
// lazy paragraph continuation
$tip->addLine($cursor->getRemainder());
return true;
}
return false;
}
private function setAndPropagateLastLineBlank(ContextInterface $context, Cursor $cursor): void
{
$container = $context->getContainer();
if ($cursor->isBlank() && $lastChild = $container->lastChild()) {
if ($lastChild instanceof AbstractBlock) {
$lastChild->setLastLineBlank(true);
}
}
$lastLineBlank = $container->shouldLastLineBeBlank($cursor, $context->getLineNumber());
// Propagate lastLineBlank up through parents:
while ($container instanceof AbstractBlock && $container->endsWithBlankLine() !== $lastLineBlank) {
$container->setLastLineBlank($lastLineBlank);
$container = $container->parent();
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark;
use League\CommonMark\Block\Element\Document;
interface DocParserInterface
{
/**
* @param string $input
*
* @throws \RuntimeException
*
* @return Document
*/
public function parse(string $input): Document;
}

View File

@@ -0,0 +1,64 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Inline\Element\AbstractInline;
/**
* Renders a parsed AST to a string representation
*/
interface ElementRendererInterface
{
/**
* @param string $option
* @param mixed $default
*
* @return mixed|null
*/
public function getOption(string $option, $default = null);
/**
* @param AbstractInline $inline
*
* @return string
*/
public function renderInline(AbstractInline $inline): string;
/**
* @param AbstractInline[] $inlines
*
* @return string
*/
public function renderInlines(iterable $inlines): string;
/**
* @param AbstractBlock $block
* @param bool $inTightList
*
* @throws \RuntimeException
*
* @return string
*/
public function renderBlock(AbstractBlock $block, bool $inTightList = false): string;
/**
* @param AbstractBlock[] $blocks
* @param bool $inTightList
*
* @return string
*/
public function renderBlocks(iterable $blocks, bool $inTightList = false): string;
}

View File

@@ -0,0 +1,424 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark;
use League\CommonMark\Block\Parser\BlockParserInterface;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
use League\CommonMark\Event\AbstractEvent;
use League\CommonMark\Extension\CommonMarkCoreExtension;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
use League\CommonMark\Inline\Parser\InlineParserInterface;
use League\CommonMark\Inline\Renderer\InlineRendererInterface;
use League\CommonMark\Util\Configuration;
use League\CommonMark\Util\ConfigurationAwareInterface;
use League\CommonMark\Util\PrioritizedList;
final class Environment implements ConfigurableEnvironmentInterface
{
/**
* @var ExtensionInterface[]
*/
private $extensions = [];
/**
* @var ExtensionInterface[]
*/
private $uninitializedExtensions = [];
/**
* @var bool
*/
private $extensionsInitialized = false;
/**
* @var PrioritizedList<BlockParserInterface>
*/
private $blockParsers;
/**
* @var PrioritizedList<InlineParserInterface>
*/
private $inlineParsers;
/**
* @var array<string, PrioritizedList<InlineParserInterface>>
*/
private $inlineParsersByCharacter = [];
/**
* @var DelimiterProcessorCollection
*/
private $delimiterProcessors;
/**
* @var array<string, PrioritizedList<BlockRendererInterface>>
*/
private $blockRenderersByClass = [];
/**
* @var array<string, PrioritizedList<InlineRendererInterface>>
*/
private $inlineRenderersByClass = [];
/**
* @var array<string, PrioritizedList<callable>>
*/
private $listeners = [];
/**
* @var Configuration
*/
private $config;
/**
* @var string
*/
private $inlineParserCharacterRegex;
/**
* @param array<string, mixed> $config
*/
public function __construct(array $config = [])
{
$this->config = new Configuration($config);
$this->blockParsers = new PrioritizedList();
$this->inlineParsers = new PrioritizedList();
$this->delimiterProcessors = new DelimiterProcessorCollection();
}
public function mergeConfig(array $config = [])
{
$this->assertUninitialized('Failed to modify configuration.');
$this->config->merge($config);
}
public function setConfig(array $config = [])
{
$this->assertUninitialized('Failed to modify configuration.');
$this->config->replace($config);
}
public function getConfig($key = null, $default = null)
{
return $this->config->get($key, $default);
}
public function addBlockParser(BlockParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
{
$this->assertUninitialized('Failed to add block parser.');
$this->blockParsers->add($parser, $priority);
$this->injectEnvironmentAndConfigurationIfNeeded($parser);
return $this;
}
public function addInlineParser(InlineParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
{
$this->assertUninitialized('Failed to add inline parser.');
$this->inlineParsers->add($parser, $priority);
$this->injectEnvironmentAndConfigurationIfNeeded($parser);
foreach ($parser->getCharacters() as $character) {
if (!isset($this->inlineParsersByCharacter[$character])) {
$this->inlineParsersByCharacter[$character] = new PrioritizedList();
}
$this->inlineParsersByCharacter[$character]->add($parser, $priority);
}
return $this;
}
public function addDelimiterProcessor(DelimiterProcessorInterface $processor): ConfigurableEnvironmentInterface
{
$this->assertUninitialized('Failed to add delimiter processor.');
$this->delimiterProcessors->add($processor);
$this->injectEnvironmentAndConfigurationIfNeeded($processor);
return $this;
}
public function addBlockRenderer($blockClass, BlockRendererInterface $blockRenderer, int $priority = 0): ConfigurableEnvironmentInterface
{
$this->assertUninitialized('Failed to add block renderer.');
if (!isset($this->blockRenderersByClass[$blockClass])) {
$this->blockRenderersByClass[$blockClass] = new PrioritizedList();
}
$this->blockRenderersByClass[$blockClass]->add($blockRenderer, $priority);
$this->injectEnvironmentAndConfigurationIfNeeded($blockRenderer);
return $this;
}
public function addInlineRenderer(string $inlineClass, InlineRendererInterface $renderer, int $priority = 0): ConfigurableEnvironmentInterface
{
$this->assertUninitialized('Failed to add inline renderer.');
if (!isset($this->inlineRenderersByClass[$inlineClass])) {
$this->inlineRenderersByClass[$inlineClass] = new PrioritizedList();
}
$this->inlineRenderersByClass[$inlineClass]->add($renderer, $priority);
$this->injectEnvironmentAndConfigurationIfNeeded($renderer);
return $this;
}
public function getBlockParsers(): iterable
{
if (!$this->extensionsInitialized) {
$this->initializeExtensions();
}
return $this->blockParsers->getIterator();
}
public function getInlineParsersForCharacter(string $character): iterable
{
if (!$this->extensionsInitialized) {
$this->initializeExtensions();
}
if (!isset($this->inlineParsersByCharacter[$character])) {
return [];
}
return $this->inlineParsersByCharacter[$character]->getIterator();
}
public function getDelimiterProcessors(): DelimiterProcessorCollection
{
if (!$this->extensionsInitialized) {
$this->initializeExtensions();
}
return $this->delimiterProcessors;
}
public function getBlockRenderersForClass(string $blockClass): iterable
{
if (!$this->extensionsInitialized) {
$this->initializeExtensions();
}
return $this->getRenderersByClass($this->blockRenderersByClass, $blockClass, BlockRendererInterface::class);
}
public function getInlineRenderersForClass(string $inlineClass): iterable
{
if (!$this->extensionsInitialized) {
$this->initializeExtensions();
}
return $this->getRenderersByClass($this->inlineRenderersByClass, $inlineClass, InlineRendererInterface::class);
}
/**
* Get all registered extensions
*
* @return ExtensionInterface[]
*/
public function getExtensions(): iterable
{
return $this->extensions;
}
/**
* Add a single extension
*
* @param ExtensionInterface $extension
*
* @return $this
*/
public function addExtension(ExtensionInterface $extension): ConfigurableEnvironmentInterface
{
$this->assertUninitialized('Failed to add extension.');
$this->extensions[] = $extension;
$this->uninitializedExtensions[] = $extension;
return $this;
}
private function initializeExtensions(): void
{
// Ask all extensions to register their components
while (!empty($this->uninitializedExtensions)) {
foreach ($this->uninitializedExtensions as $i => $extension) {
$extension->register($this);
unset($this->uninitializedExtensions[$i]);
}
}
$this->extensionsInitialized = true;
// Lastly, let's build a regex which matches non-inline characters
// This will enable a huge performance boost with inline parsing
$this->buildInlineParserCharacterRegex();
}
/**
* @param object $object
*/
private function injectEnvironmentAndConfigurationIfNeeded($object): void
{
if ($object instanceof EnvironmentAwareInterface) {
$object->setEnvironment($this);
}
if ($object instanceof ConfigurationAwareInterface) {
$object->setConfiguration($this->config);
}
}
public static function createCommonMarkEnvironment(): ConfigurableEnvironmentInterface
{
$environment = new static();
$environment->addExtension(new CommonMarkCoreExtension());
$environment->mergeConfig([
'renderer' => [
'block_separator' => "\n",
'inner_separator' => "\n",
'soft_break' => "\n",
],
'html_input' => self::HTML_INPUT_ALLOW,
'allow_unsafe_links' => true,
'max_nesting_level' => \INF,
]);
return $environment;
}
public static function createGFMEnvironment(): ConfigurableEnvironmentInterface
{
$environment = self::createCommonMarkEnvironment();
$environment->addExtension(new GithubFlavoredMarkdownExtension());
return $environment;
}
public function getInlineParserCharacterRegex(): string
{
return $this->inlineParserCharacterRegex;
}
public function addEventListener(string $eventClass, callable $listener, int $priority = 0): ConfigurableEnvironmentInterface
{
$this->assertUninitialized('Failed to add event listener.');
if (!isset($this->listeners[$eventClass])) {
$this->listeners[$eventClass] = new PrioritizedList();
}
$this->listeners[$eventClass]->add($listener, $priority);
if (\is_object($listener)) {
$this->injectEnvironmentAndConfigurationIfNeeded($listener);
} elseif (\is_array($listener) && \is_object($listener[0])) {
$this->injectEnvironmentAndConfigurationIfNeeded($listener[0]);
}
return $this;
}
public function dispatch(AbstractEvent $event): void
{
if (!$this->extensionsInitialized) {
$this->initializeExtensions();
}
$type = \get_class($event);
foreach ($this->listeners[$type] ?? [] as $listener) {
if ($event->isPropagationStopped()) {
return;
}
$listener($event);
}
}
private function buildInlineParserCharacterRegex(): void
{
$chars = \array_unique(\array_merge(
\array_keys($this->inlineParsersByCharacter),
$this->delimiterProcessors->getDelimiterCharacters()
));
if (empty($chars)) {
// If no special inline characters exist then parse the whole line
$this->inlineParserCharacterRegex = '/^.+$/u';
} else {
// Match any character which inline parsers are not interested in
$this->inlineParserCharacterRegex = '/^[^' . \preg_quote(\implode('', $chars), '/') . ']+/u';
}
}
/**
* @param string $message
*
* @throws \RuntimeException
*/
private function assertUninitialized(string $message): void
{
if ($this->extensionsInitialized) {
throw new \RuntimeException($message . ' Extensions have already been initialized.');
}
}
/**
* @param array<string, PrioritizedList> $list
* @param string $class
* @param string $type
*
* @return iterable
*
* @phpstan-template T
*
* @phpstan-param array<string, PrioritizedList<T>> $list
* @phpstan-param string $class
* @phpstan-param class-string<T> $type
*
* @phpstan-return iterable<T>
*/
private function getRenderersByClass(array &$list, string $class, string $type): iterable
{
// If renderers are defined for this specific class, return them immediately
if (isset($list[$class])) {
return $list[$class];
}
while ($parent = \get_parent_class($parent ?? $class)) {
if (!isset($list[$parent])) {
continue;
}
// "Cache" this result to avoid future loops
return $list[$class] = $list[$parent];
}
return [];
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark;
interface EnvironmentAwareInterface
{
/**
* @param EnvironmentInterface $environment
*
* @return void
*/
public function setEnvironment(EnvironmentInterface $environment);
}

View File

@@ -0,0 +1,83 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark;
use League\CommonMark\Block\Parser\BlockParserInterface;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
use League\CommonMark\Event\AbstractEvent;
use League\CommonMark\Inline\Parser\InlineParserInterface;
use League\CommonMark\Inline\Renderer\InlineRendererInterface;
interface EnvironmentInterface
{
const HTML_INPUT_STRIP = 'strip';
const HTML_INPUT_ALLOW = 'allow';
const HTML_INPUT_ESCAPE = 'escape';
/**
* @param string|null $key
* @param mixed $default
*
* @return mixed
*/
public function getConfig($key = null, $default = null);
/**
* @return iterable<BlockParserInterface>
*/
public function getBlockParsers(): iterable;
/**
* @param string $character
*
* @return iterable<InlineParserInterface>
*/
public function getInlineParsersForCharacter(string $character): iterable;
/**
* @return DelimiterProcessorCollection
*/
public function getDelimiterProcessors(): DelimiterProcessorCollection;
/**
* @param string $blockClass
*
* @return iterable<BlockRendererInterface>
*/
public function getBlockRenderersForClass(string $blockClass): iterable;
/**
* @param string $inlineClass
*
* @return iterable<InlineRendererInterface>
*/
public function getInlineRenderersForClass(string $inlineClass): iterable;
/**
* Regex which matches any character which doesn't indicate an inline element
*
* This allows us to parse multiple non-special characters at once
*
* @return string
*/
public function getInlineParserCharacterRegex(): string;
/**
* Dispatches the given event to listeners
*
* @param AbstractEvent $event
*
* @return void
*/
public function dispatch(AbstractEvent $event): void;
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the Symfony EventDispatcher "Event" contract
* - (c) 2018-2019 Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Event;
/**
* Base class for classes containing event data.
*
* This class contains no event data. It is used by events that do not pass
* state information to an event handler when an event is raised.
*
* You can call the method stopPropagation() to abort the execution of
* further listeners in your event listener.
*/
abstract class AbstractEvent
{
/** @var bool */
private $propagationStopped = false;
/**
* Returns whether further event listeners should be triggered.
*/
final public function isPropagationStopped(): bool
{
return $this->propagationStopped;
}
/**
* Stops the propagation of the event to further event listeners.
*
* If multiple event listeners are connected to the same event, no
* further event listener will be triggered once any trigger calls
* stopPropagation().
*/
final public function stopPropagation(): void
{
$this->propagationStopped = true;
}
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Event;
use League\CommonMark\Block\Element\Document;
/**
* Event dispatched when the document has been fully parsed
*/
final class DocumentParsedEvent extends AbstractEvent
{
/** @var Document */
private $document;
public function __construct(Document $document)
{
$this->document = $document;
}
public function getDocument(): Document
{
return $this->document;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Event;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\Input\MarkdownInputInterface;
/**
* Event dispatched when the document is about to be parsed
*/
final class DocumentPreParsedEvent extends AbstractEvent
{
/** @var Document */
private $document;
/** @var MarkdownInputInterface */
private $markdown;
public function __construct(Document $document, MarkdownInputInterface $markdown)
{
$this->document = $document;
$this->markdown = $markdown;
}
public function getDocument(): Document
{
return $this->document;
}
public function getMarkdown(): MarkdownInputInterface
{
return $this->markdown;
}
public function replaceMarkdown(MarkdownInputInterface $markdownInput): void
{
$this->markdown = $markdownInput;
}
}

View File

@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Exception;
final class InvalidOptionException extends \RuntimeException
{
}

View File

@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Exception;
final class UnexpectedEncodingException extends \RuntimeException
{
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Autolink;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\ExtensionInterface;
final class AutolinkExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment->addEventListener(DocumentParsedEvent::class, new EmailAutolinkProcessor());
$environment->addEventListener(DocumentParsedEvent::class, new UrlAutolinkProcessor());
}
}

View File

@@ -0,0 +1,78 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Autolink;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Inline\Element\Link;
use League\CommonMark\Inline\Element\Text;
final class EmailAutolinkProcessor
{
const REGEX = '/([A-Za-z0-9.\-_+]+@[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_.]+)/';
/**
* @param DocumentParsedEvent $e
*
* @return void
*/
public function __invoke(DocumentParsedEvent $e)
{
$walker = $e->getDocument()->walker();
while ($event = $walker->next()) {
$node = $event->getNode();
if ($node instanceof Text && !($node->parent() instanceof Link)) {
self::processAutolinks($node);
}
}
}
private static function processAutolinks(Text $node): void
{
$contents = \preg_split(self::REGEX, $node->getContent(), -1, PREG_SPLIT_DELIM_CAPTURE);
if ($contents === false || \count($contents) === 1) {
return;
}
$leftovers = '';
foreach ($contents as $i => $content) {
if ($i % 2 === 0) {
$text = $leftovers . $content;
if ($text !== '') {
$node->insertBefore(new Text($leftovers . $content));
}
$leftovers = '';
continue;
}
// Does the URL end with punctuation that should be stripped?
if (\substr($content, -1) === '.') {
// Add the punctuation later
$content = \substr($content, 0, -1);
$leftovers = '.';
}
// The last character cannot be - or _
if (\in_array(\substr($content, -1), ['-', '_'])) {
$node->insertBefore(new Text($content . $leftovers));
$leftovers = '';
continue;
}
$node->insertBefore(new Link('mailto:' . $content, $content));
}
$node->detach();
}
}

View File

@@ -0,0 +1,90 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Autolink;
use League\CommonMark\Inline\Element\Link;
use League\CommonMark\Inline\Parser\InlineParserInterface;
use League\CommonMark\InlineParserContext;
final class InlineMentionParser implements InlineParserInterface
{
/** @var string */
private $linkPattern;
/** @var string */
private $handleRegex;
/**
* @param string $linkPattern
* @param string $handleRegex
*/
public function __construct($linkPattern, $handleRegex = '/^[A-Za-z0-9_]+(?!\w)/')
{
$this->linkPattern = $linkPattern;
$this->handleRegex = $handleRegex;
}
public function getCharacters(): array
{
return ['@'];
}
public function parse(InlineParserContext $inlineContext): bool
{
$cursor = $inlineContext->getCursor();
// The @ symbol must not have any other characters immediately prior
$previousChar = $cursor->peek(-1);
if ($previousChar !== null && $previousChar !== ' ') {
// peek() doesn't modify the cursor, so no need to restore state first
return false;
}
// Save the cursor state in case we need to rewind and bail
$previousState = $cursor->saveState();
// Advance past the @ symbol to keep parsing simpler
$cursor->advance();
// Parse the handle
$handle = $cursor->match($this->handleRegex);
if (empty($handle)) {
// Regex failed to match; this isn't a valid Twitter handle
$cursor->restoreState($previousState);
return false;
}
$url = \sprintf($this->linkPattern, $handle);
$inlineContext->getContainer()->appendChild(new Link($url, '@' . $handle));
return true;
}
/**
* @return InlineMentionParser
*/
public static function createTwitterHandleParser()
{
return new self('https://twitter.com/%s', '/^[A-Za-z0-9_]{1,15}(?!\w)/');
}
/**
* @return InlineMentionParser
*/
public static function createGithubHandleParser()
{
// RegEx adapted from https://github.com/shinnn/github-username-regex/blob/master/index.js
return new self('https://www.github.com/%s', '/^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/');
}
}

View File

@@ -0,0 +1,153 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Autolink;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Inline\Element\Link;
use League\CommonMark\Inline\Element\Text;
final class UrlAutolinkProcessor
{
// RegEx adapted from https://github.com/symfony/symfony/blob/4.2/src/Symfony/Component/Validator/Constraints/UrlValidator.php
const REGEX = '~
(?<=^|[ \\t\\n\\x0b\\x0c\\x0d*_\\~\\(]) # Can only come at the beginning of a line, after whitespace, or certain delimiting characters
(
# Must start with a supported scheme + auth, or "www"
(?:
(?:%s):// # protocol
(?:([\.\pL\pN-]+:)?([\.\pL\pN-]+)@)? # basic auth
|www\.)
(?:
(?:[\pL\pN\pS\-\.])+(?:\.?(?:[\pL\pN]|xn\-\-[\pL\pN-]+)+\.?) # a domain name
| # or
\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} # an IP address
| # or
\[
(?:(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-f]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,1}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,2}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,3}(?:(?:[0-9a-f]{1,4})))?::(?:(?:[0-9a-f]{1,4})):)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,4}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,5}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,6}(?:(?:[0-9a-f]{1,4})))?::))))
\] # an IPv6 address
)
(?::[0-9]+)? # a port (optional)
(?:/ (?:[\pL\pN\-._\~!$&\'()*+,;=:@]|%%[0-9A-Fa-f]{2})* )* # a path
(?:\? (?:[\pL\pN\-._\~!$&\'()*+,;=:@/?]|%%[0-9A-Fa-f]{2})* )? # a query (optional)
(?:\# (?:[\pL\pN\-._\~!$&\'()*+,;=:@/?]|%%[0-9A-Fa-f]{2})* )? # a fragment (optional)
)~ixu';
/** @var string */
private $finalRegex;
/**
* @param array<int, string> $allowedProtocols
*/
public function __construct(array $allowedProtocols = ['http', 'https', 'ftp'])
{
$this->finalRegex = \sprintf(self::REGEX, \implode('|', $allowedProtocols));
}
/**
* @param DocumentParsedEvent $e
*
* @return void
*/
public function __invoke(DocumentParsedEvent $e)
{
$walker = $e->getDocument()->walker();
while ($event = $walker->next()) {
$node = $event->getNode();
if ($node instanceof Text && !($node->parent() instanceof Link)) {
self::processAutolinks($node, $this->finalRegex);
}
}
}
private static function processAutolinks(Text $node, string $regex): void
{
$contents = \preg_split($regex, $node->getContent(), -1, PREG_SPLIT_DELIM_CAPTURE);
if ($contents === false || \count($contents) === 1) {
return;
}
$leftovers = '';
foreach ($contents as $i => $content) {
// Even-indexed elements are things before/after the URLs
if ($i % 2 === 0) {
// Insert any left-over characters here as well
$text = $leftovers . $content;
if ($text !== '') {
$node->insertBefore(new Text($leftovers . $content));
}
$leftovers = '';
continue;
}
$leftovers = '';
// Does the URL end with punctuation that should be stripped?
if (\preg_match('/(.+)([?!.,:*_~]+)$/', $content, $matches)) {
// Add the punctuation later
$content = $matches[1];
$leftovers = $matches[2];
}
// Does the URL end with something that looks like an entity reference?
if (\preg_match('/(.+)(&[A-Za-z0-9]+;)$/', $content, $matches)) {
$content = $matches[1];
$leftovers = $matches[2] . $leftovers;
}
// Does the URL need its closing paren chopped off?
if (\substr($content, -1) === ')' && self::hasMoreCloserParensThanOpeners($content)) {
$content = \substr($content, 0, -1);
$leftovers = ')' . $leftovers;
}
self::addLink($node, $content);
}
$node->detach();
}
private static function addLink(Text $node, string $url): void
{
// Auto-prefix 'http://' onto 'www' URLs
if (\substr($url, 0, 4) === 'www.') {
$node->insertBefore(new Link('http://' . $url, $url));
return;
}
$node->insertBefore(new Link($url, $url));
}
/**
* @param string $content
*
* @return bool
*/
private static function hasMoreCloserParensThanOpeners(string $content): bool
{
// Scan the entire autolink for the total number of parentheses.
// If there is a greater number of closing parentheses than opening ones,
// we dont consider the last character part of the autolink, in order to
// facilitate including an autolink inside a parenthesis.
\preg_match_all('/[()]/', $content, $matches);
$charCount = ['(' => 0, ')' => 0];
foreach ($matches[0] as $char) {
$charCount[$char]++;
}
return $charCount[')'] > $charCount['('];
}
}

View File

@@ -0,0 +1,80 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension;
use League\CommonMark\Block\Element as BlockElement;
use League\CommonMark\Block\Parser as BlockParser;
use League\CommonMark\Block\Renderer as BlockRenderer;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Delimiter\Processor\EmphasisDelimiterProcessor;
use League\CommonMark\Inline\Element as InlineElement;
use League\CommonMark\Inline\Parser as InlineParser;
use League\CommonMark\Inline\Renderer as InlineRenderer;
final class CommonMarkCoreExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment
->addBlockParser(new BlockParser\BlockQuoteParser(), 70)
->addBlockParser(new BlockParser\ATXHeadingParser(), 60)
->addBlockParser(new BlockParser\FencedCodeParser(), 50)
->addBlockParser(new BlockParser\HtmlBlockParser(), 40)
->addBlockParser(new BlockParser\SetExtHeadingParser(), 30)
->addBlockParser(new BlockParser\ThematicBreakParser(), 20)
->addBlockParser(new BlockParser\ListParser(), 10)
->addBlockParser(new BlockParser\IndentedCodeParser(), -100)
->addBlockParser(new BlockParser\LazyParagraphParser(), -200)
->addInlineParser(new InlineParser\NewlineParser(), 200)
->addInlineParser(new InlineParser\BacktickParser(), 150)
->addInlineParser(new InlineParser\EscapableParser(), 80)
->addInlineParser(new InlineParser\EntityParser(), 70)
->addInlineParser(new InlineParser\AutolinkParser(), 50)
->addInlineParser(new InlineParser\HtmlInlineParser(), 40)
->addInlineParser(new InlineParser\CloseBracketParser(), 30)
->addInlineParser(new InlineParser\OpenBracketParser(), 20)
->addInlineParser(new InlineParser\BangParser(), 10)
->addBlockRenderer(BlockElement\BlockQuote::class, new BlockRenderer\BlockQuoteRenderer(), 0)
->addBlockRenderer(BlockElement\Document::class, new BlockRenderer\DocumentRenderer(), 0)
->addBlockRenderer(BlockElement\FencedCode::class, new BlockRenderer\FencedCodeRenderer(), 0)
->addBlockRenderer(BlockElement\Heading::class, new BlockRenderer\HeadingRenderer(), 0)
->addBlockRenderer(BlockElement\HtmlBlock::class, new BlockRenderer\HtmlBlockRenderer(), 0)
->addBlockRenderer(BlockElement\IndentedCode::class, new BlockRenderer\IndentedCodeRenderer(), 0)
->addBlockRenderer(BlockElement\ListBlock::class, new BlockRenderer\ListBlockRenderer(), 0)
->addBlockRenderer(BlockElement\ListItem::class, new BlockRenderer\ListItemRenderer(), 0)
->addBlockRenderer(BlockElement\Paragraph::class, new BlockRenderer\ParagraphRenderer(), 0)
->addBlockRenderer(BlockElement\ThematicBreak::class, new BlockRenderer\ThematicBreakRenderer(), 0)
->addInlineRenderer(InlineElement\Code::class, new InlineRenderer\CodeRenderer(), 0)
->addInlineRenderer(InlineElement\Emphasis::class, new InlineRenderer\EmphasisRenderer(), 0)
->addInlineRenderer(InlineElement\HtmlInline::class, new InlineRenderer\HtmlInlineRenderer(), 0)
->addInlineRenderer(InlineElement\Image::class, new InlineRenderer\ImageRenderer(), 0)
->addInlineRenderer(InlineElement\Link::class, new InlineRenderer\LinkRenderer(), 0)
->addInlineRenderer(InlineElement\Newline::class, new InlineRenderer\NewlineRenderer(), 0)
->addInlineRenderer(InlineElement\Strong::class, new InlineRenderer\StrongRenderer(), 0)
->addInlineRenderer(InlineElement\Text::class, new InlineRenderer\TextRenderer(), 0)
;
if ($environment->getConfig('use_asterisk', true)) {
$environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('*'));
}
if ($environment->getConfig('use_underscore', true)) {
$environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('_'));
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\DisallowedRawHtml;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\Util\ConfigurationAwareInterface;
use League\CommonMark\Util\ConfigurationInterface;
final class DisallowedRawHtmlBlockRenderer implements BlockRendererInterface, ConfigurationAwareInterface
{
/** @var BlockRendererInterface */
private $htmlBlockRenderer;
public function __construct(BlockRendererInterface $htmlBlockRenderer)
{
$this->htmlBlockRenderer = $htmlBlockRenderer;
}
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
$rendered = $this->htmlBlockRenderer->render($block, $htmlRenderer, $inTightList);
if ($rendered === '') {
return '';
}
// Match these types of tags: <title> </title> <title x="sdf"> <title/> <title />
return preg_replace('/<(\/?(?:title|textarea|style|xmp|iframe|noembed|noframes|script|plaintext)[ \/>])/i', '&lt;$1', $rendered);
}
public function setConfiguration(ConfigurationInterface $configuration)
{
if ($this->htmlBlockRenderer instanceof ConfigurationAwareInterface) {
$this->htmlBlockRenderer->setConfiguration($configuration);
}
}
}

View File

@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\DisallowedRawHtml;
use League\CommonMark\Block\Element\HtmlBlock;
use League\CommonMark\Block\Renderer\HtmlBlockRenderer;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Inline\Element\HtmlInline;
use League\CommonMark\Inline\Renderer\HtmlInlineRenderer;
final class DisallowedRawHtmlExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment->addBlockRenderer(HtmlBlock::class, new DisallowedRawHtmlBlockRenderer(new HtmlBlockRenderer()), 50);
$environment->addInlineRenderer(HtmlInline::class, new DisallowedRawHtmlInlineRenderer(new HtmlInlineRenderer()), 50);
}
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\DisallowedRawHtml;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\Inline\Element\AbstractInline;
use League\CommonMark\Inline\Renderer\InlineRendererInterface;
use League\CommonMark\Util\ConfigurationAwareInterface;
use League\CommonMark\Util\ConfigurationInterface;
final class DisallowedRawHtmlInlineRenderer implements InlineRendererInterface, ConfigurationAwareInterface
{
/** @var InlineRendererInterface */
private $htmlInlineRenderer;
public function __construct(InlineRendererInterface $htmlBlockRenderer)
{
$this->htmlInlineRenderer = $htmlBlockRenderer;
}
public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer)
{
$rendered = $this->htmlInlineRenderer->render($inline, $htmlRenderer);
if ($rendered === '') {
return '';
}
// Match these types of tags: <title> </title> <title x="sdf"> <title/> <title />
return preg_replace('/<(\/?(?:title|textarea|style|xmp|iframe|noembed|noframes|script|plaintext)[ \/>])/i', '&lt;$1', $rendered);
}
public function setConfiguration(ConfigurationInterface $configuration)
{
if ($this->htmlInlineRenderer instanceof ConfigurationAwareInterface) {
$this->htmlInlineRenderer->setConfiguration($configuration);
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension;
use League\CommonMark\ConfigurableEnvironmentInterface;
interface ExtensionInterface
{
/**
* @param ConfigurableEnvironmentInterface $environment
*
* @return void
*/
public function register(ConfigurableEnvironmentInterface $environment);
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\ExternalLink;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\ExtensionInterface;
final class ExternalLinkExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment->addEventListener(DocumentParsedEvent::class, new ExternalLinkProcessor($environment));
}
}

View File

@@ -0,0 +1,99 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\ExternalLink;
use League\CommonMark\EnvironmentInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Inline\Element\Link;
final class ExternalLinkProcessor
{
/** @var EnvironmentInterface */
private $environment;
public function __construct(EnvironmentInterface $environment)
{
$this->environment = $environment;
}
/**
* @param DocumentParsedEvent $e
*
* @return void
*/
public function __invoke(DocumentParsedEvent $e)
{
$internalHosts = $this->environment->getConfig('external_link/internal_hosts', []);
$openInNewWindow = $this->environment->getConfig('external_link/open_in_new_window', false);
$classes = $this->environment->getConfig('external_link/html_class', '');
$walker = $e->getDocument()->walker();
while ($event = $walker->next()) {
if ($event->isEntering() && $event->getNode() instanceof Link) {
/** @var Link $link */
$link = $event->getNode();
$host = parse_url($link->getUrl(), PHP_URL_HOST);
if (empty($host)) {
// Something is terribly wrong with this URL
continue;
}
if (self::hostMatches($host, $internalHosts)) {
$link->data['external'] = false;
continue;
}
// Host does not match our list
$this->markLinkAsExternal($link, $openInNewWindow, $classes);
}
}
}
private function markLinkAsExternal(Link $link, bool $openInNewWindow, string $classes): void
{
$link->data['external'] = true;
$link->data['attributes'] = $link->getData('attributes', []);
$link->data['attributes']['rel'] = 'noopener noreferrer';
if ($openInNewWindow) {
$link->data['attributes']['target'] = '_blank';
}
if (!empty($classes)) {
$link->data['attributes']['class'] = trim(($link->data['attributes']['class'] ?? '') . ' ' . $classes);
}
}
/**
* @param string $host
* @param mixed $compareTo
*
* @return bool
*
* @internal This method is only public so we can easily test it. DO NOT USE THIS OUTSIDE OF THIS EXTENSION!
*/
public static function hostMatches(string $host, $compareTo)
{
foreach ((array) $compareTo as $c) {
if (strpos($c, '/') === 0) {
if (preg_match($c, $host)) {
return true;
}
} elseif ($c === $host) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Extension\Autolink\AutolinkExtension;
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
use League\CommonMark\Extension\Strikethrough\StrikethroughExtension;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\TaskList\TaskListExtension;
final class GithubFlavoredMarkdownExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment->addExtension(new AutolinkExtension());
$environment->addExtension(new DisallowedRawHtmlExtension());
$environment->addExtension(new StrikethroughExtension());
$environment->addExtension(new TableExtension());
$environment->addExtension(new TaskListExtension());
}
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\HeadingPermalink;
use League\CommonMark\Inline\Element\AbstractInline;
/**
* Represents an anchor link within a heading
*/
final class HeadingPermalink extends AbstractInline
{
/** @var string */
private $slug;
public function __construct(string $slug)
{
$this->slug = $slug;
}
public function getSlug(): string
{
return $this->slug;
}
}

View File

@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\HeadingPermalink;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\ExtensionInterface;
/**
* Extension which automatically anchor links to heading elements
*/
final class HeadingPermalinkExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment->addEventListener(DocumentParsedEvent::class, new HeadingPermalinkProcessor(), -100);
$environment->addInlineRenderer(HeadingPermalink::class, new HeadingPermalinkRenderer());
}
}

View File

@@ -0,0 +1,94 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\HeadingPermalink;
use League\CommonMark\Block\Element\Heading;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\HeadingPermalink\Slug\DefaultSlugGenerator;
use League\CommonMark\Extension\HeadingPermalink\Slug\SlugGeneratorInterface;
use League\CommonMark\Inline\Element\Code;
use League\CommonMark\Inline\Element\Text;
use League\CommonMark\Node\Node;
use League\CommonMark\Util\ConfigurationAwareInterface;
use League\CommonMark\Util\ConfigurationInterface;
/**
* Searches the Document for Heading elements and adds HeadingPermalinks to each one
*/
final class HeadingPermalinkProcessor implements ConfigurationAwareInterface
{
const INSERT_BEFORE = 'before';
const INSERT_AFTER = 'after';
/** @var SlugGeneratorInterface */
private $slugGenerator;
/** @var ConfigurationInterface */
private $config;
public function __construct(SlugGeneratorInterface $slugGenerator = null)
{
$this->slugGenerator = $slugGenerator ?? new DefaultSlugGenerator();
}
public function setConfiguration(ConfigurationInterface $configuration)
{
$this->config = $configuration;
}
public function __invoke(DocumentParsedEvent $e): void
{
$walker = $e->getDocument()->walker();
while ($event = $walker->next()) {
$node = $event->getNode();
if ($node instanceof Heading && $event->isEntering()) {
$this->addHeadingLink($node);
}
}
}
private function addHeadingLink(Heading $heading): void
{
$text = $this->getChildText($heading);
$slug = $this->slugGenerator->createSlug($text);
$headingLinkAnchor = new HeadingPermalink($slug);
switch ($this->config->get('heading_permalink/insert', 'before')) {
case self::INSERT_BEFORE:
$heading->prependChild($headingLinkAnchor);
return;
case self::INSERT_AFTER:
$heading->appendChild($headingLinkAnchor);
return;
default:
throw new \RuntimeException("Invalid configuration value for heading_permalink/insert; expected 'before' or 'after'");
}
}
private function getChildText(Node $node): string
{
$text = '';
$walker = $node->walker();
while ($event = $walker->next()) {
if ($event->isEntering() && (($child = $event->getNode()) instanceof Text || $child instanceof Code)) {
$text .= $child->getContent();
}
}
return $text;
}
}

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\HeadingPermalink;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
use League\CommonMark\Inline\Element\AbstractInline;
use League\CommonMark\Inline\Renderer\InlineRendererInterface;
use League\CommonMark\Util\ConfigurationAwareInterface;
use League\CommonMark\Util\ConfigurationInterface;
/**
* Renders the HeadingPermalink elements
*/
final class HeadingPermalinkRenderer implements InlineRendererInterface, ConfigurationAwareInterface
{
const DEFAULT_INNER_CONTENTS = '<svg class="heading-permalink-icon" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>';
/** @var ConfigurationInterface */
private $config;
public function setConfiguration(ConfigurationInterface $configuration)
{
$this->config = $configuration;
}
public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer)
{
if (!$inline instanceof HeadingPermalink) {
throw new \InvalidArgumentException('Incompatible inline type: ' . \get_class($inline));
}
$slug = $inline->getSlug();
$idPrefix = (string) $this->config->get('heading_permalink/id_prefix', 'user-content');
if ($idPrefix !== '') {
$idPrefix .= '-';
}
$attrs = [
'id' => $idPrefix . $slug,
'href' => '#' . $slug,
'name' => $slug,
'class' => $this->config->get('heading_permalink/html_class', 'heading-permalink'),
'aria-hidden' => 'true',
'title' => $this->config->get('heading_permalink/title', 'Permalink'),
];
$innerContents = $this->config->get('heading_permalink/inner_contents', self::DEFAULT_INNER_CONTENTS);
return new HtmlElement('a', $attrs, $innerContents, false);
}
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\HeadingPermalink\Slug;
/**
* Creates URL-friendly strings
*/
final class DefaultSlugGenerator implements SlugGeneratorInterface
{
public function createSlug(string $input): string
{
// Trim whitespace
$slug = \trim($input);
// Convert to lowercase
$slug = \mb_strtolower($slug);
// Try replacing whitespace with a dash
$slug = \preg_replace('/\s+/u', '-', $slug) ?? $slug;
// Try removing characters other than letters, numbers, and marks.
$slug = \preg_replace('/[^\p{L}\p{Nd}\p{Nl}\p{M}-]+/u', '', $slug) ?? $slug;
return $slug;
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\HeadingPermalink\Slug;
interface SlugGeneratorInterface
{
/**
* Create a URL-friendly slug based on the given input string
*
* @param string $input
*
* @return string
*/
public function createSlug(string $input): string;
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\InlinesOnly;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\Block\Element\InlineContainerInterface;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\Inline\Element\AbstractInline;
/**
* Simply renders child elements as-is, adding newlines as needed.
*/
final class ChildRenderer implements BlockRendererInterface
{
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
$out = '';
if ($block instanceof InlineContainerInterface) {
/** @var iterable<AbstractInline> $children */
$children = $block->children();
$out .= $htmlRenderer->renderInlines($children);
} else {
/** @var iterable<AbstractBlock> $children */
$children = $block->children();
$out .= $htmlRenderer->renderBlocks($children);
}
if (!($block instanceof Document)) {
$out .= "\n";
}
return $out;
}
}

View File

@@ -0,0 +1,63 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\InlinesOnly;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\Block\Parser as BlockParser;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Delimiter\Processor\EmphasisDelimiterProcessor;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Inline\Element as InlineElement;
use League\CommonMark\Inline\Parser as InlineParser;
use League\CommonMark\Inline\Renderer as InlineRenderer;
final class InlinesOnlyExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$childRenderer = new ChildRenderer();
$environment
->addBlockParser(new BlockParser\LazyParagraphParser(), -200)
->addInlineParser(new InlineParser\NewlineParser(), 200)
->addInlineParser(new InlineParser\BacktickParser(), 150)
->addInlineParser(new InlineParser\EscapableParser(), 80)
->addInlineParser(new InlineParser\EntityParser(), 70)
->addInlineParser(new InlineParser\AutolinkParser(), 50)
->addInlineParser(new InlineParser\HtmlInlineParser(), 40)
->addInlineParser(new InlineParser\CloseBracketParser(), 30)
->addInlineParser(new InlineParser\OpenBracketParser(), 20)
->addInlineParser(new InlineParser\BangParser(), 10)
->addBlockRenderer(Document::class, $childRenderer, 0)
->addBlockRenderer(Paragraph::class, $childRenderer, 0)
->addInlineRenderer(InlineElement\Code::class, new InlineRenderer\CodeRenderer(), 0)
->addInlineRenderer(InlineElement\Emphasis::class, new InlineRenderer\EmphasisRenderer(), 0)
->addInlineRenderer(InlineElement\HtmlInline::class, new InlineRenderer\HtmlInlineRenderer(), 0)
->addInlineRenderer(InlineElement\Image::class, new InlineRenderer\ImageRenderer(), 0)
->addInlineRenderer(InlineElement\Link::class, new InlineRenderer\LinkRenderer(), 0)
->addInlineRenderer(InlineElement\Newline::class, new InlineRenderer\NewlineRenderer(), 0)
->addInlineRenderer(InlineElement\Strong::class, new InlineRenderer\StrongRenderer(), 0)
->addInlineRenderer(InlineElement\Text::class, new InlineRenderer\TextRenderer(), 0)
;
if ($environment->getConfig('use_asterisk', true)) {
$environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('*'));
}
if ($environment->getConfig('use_underscore', true)) {
$environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('_'));
}
}
}

View File

@@ -0,0 +1,70 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\SmartPunct;
use League\CommonMark\Inline\Element\Text;
use League\CommonMark\Inline\Parser\InlineParserInterface;
use League\CommonMark\InlineParserContext;
final class PunctuationParser implements InlineParserInterface
{
/**
* @return string[]
*/
public function getCharacters(): array
{
return ['-', '.'];
}
public function parse(InlineParserContext $inlineContext): bool
{
$cursor = $inlineContext->getCursor();
$ch = $cursor->getCharacter();
// Ellipses
if ($ch === '.' && $matched = $cursor->match('/^\\.( ?\\.)\\1/')) {
$inlineContext->getContainer()->appendChild(new Text('…'));
return true;
}
// Em/En-dashes
elseif ($ch === '-' && $matched = $cursor->match('/^(?<!-)(-{2,})/')) {
$count = strlen($matched);
$en_dash = '';
$en_count = 0;
$em_dash = '—';
$em_count = 0;
if ($count % 3 === 0) { // If divisible by 3, use all em dashes
$em_count = $count / 3;
} elseif ($count % 2 === 0) { // If divisible by 2, use all en dashes
$en_count = $count / 2;
} elseif ($count % 3 === 2) { // If 2 extra dashes, use en dash for last 2; em dashes for rest
$em_count = ($count - 2) / 3;
$en_count = 1;
} else { // Use en dashes for last 4 hyphens; em dashes for rest
$em_count = ($count - 4) / 3;
$en_count = 2;
}
$inlineContext->getContainer()->appendChild(new Text(
str_repeat($em_dash, $em_count) . str_repeat($en_dash, $en_count)
));
return true;
}
return false;
}
}

View File

@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\SmartPunct;
use League\CommonMark\Inline\Element\AbstractStringContainer;
final class Quote extends AbstractStringContainer
{
public const DOUBLE_QUOTE = '"';
public const DOUBLE_QUOTE_OPENER = '“';
public const DOUBLE_QUOTE_CLOSER = '”';
public const SINGLE_QUOTE = "'";
public const SINGLE_QUOTE_OPENER = '';
public const SINGLE_QUOTE_CLOSER = '';
}

View File

@@ -0,0 +1,104 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\SmartPunct;
use League\CommonMark\Delimiter\Delimiter;
use League\CommonMark\Inline\Parser\InlineParserInterface;
use League\CommonMark\InlineParserContext;
use League\CommonMark\Util\RegexHelper;
final class QuoteParser implements InlineParserInterface
{
public const DOUBLE_QUOTES = [Quote::DOUBLE_QUOTE, Quote::DOUBLE_QUOTE_OPENER, Quote::DOUBLE_QUOTE_CLOSER];
public const SINGLE_QUOTES = [Quote::SINGLE_QUOTE, Quote::SINGLE_QUOTE_OPENER, Quote::SINGLE_QUOTE_CLOSER];
/**
* @return string[]
*/
public function getCharacters(): array
{
return array_merge(self::DOUBLE_QUOTES, self::SINGLE_QUOTES);
}
/**
* Normalizes any quote characters found and manually adds them to the delimiter stack
*/
public function parse(InlineParserContext $inlineContext): bool
{
$cursor = $inlineContext->getCursor();
$normalizedCharacter = $this->getNormalizedQuoteCharacter($cursor->getCharacter());
$charBefore = $cursor->peek(-1);
if ($charBefore === null) {
$charBefore = "\n";
}
$cursor->advance();
$charAfter = $cursor->getCharacter();
if ($charAfter === null) {
$charAfter = "\n";
}
[$leftFlanking, $rightFlanking] = $this->determineFlanking($charBefore, $charAfter);
$canOpen = $leftFlanking && !$rightFlanking;
$canClose = $rightFlanking;
$node = new Quote($normalizedCharacter, ['delim' => true]);
$inlineContext->getContainer()->appendChild($node);
// Add entry to stack to this opener
$inlineContext->getDelimiterStack()->push(new Delimiter($normalizedCharacter, 1, $node, $canOpen, $canClose));
return true;
}
private function getNormalizedQuoteCharacter(string $character): string
{
if (in_array($character, self::DOUBLE_QUOTES)) {
return Quote::DOUBLE_QUOTE;
} elseif (in_array($character, self::SINGLE_QUOTES)) {
return Quote::SINGLE_QUOTE;
}
return $character;
}
/**
* @param string $charBefore
* @param string $charAfter
*
* @return bool[]
*/
private function determineFlanking(string $charBefore, string $charAfter)
{
$afterIsWhitespace = preg_match('/\pZ|\s/u', $charAfter);
$afterIsPunctuation = preg_match(RegexHelper::REGEX_PUNCTUATION, $charAfter);
$beforeIsWhitespace = preg_match('/\pZ|\s/u', $charBefore);
$beforeIsPunctuation = preg_match(RegexHelper::REGEX_PUNCTUATION, $charBefore);
$leftFlanking = !$afterIsWhitespace &&
!($afterIsPunctuation &&
!$beforeIsWhitespace &&
!$beforeIsPunctuation);
$rightFlanking = !$beforeIsWhitespace &&
!($beforeIsPunctuation &&
!$afterIsWhitespace &&
!$afterIsPunctuation);
return [$leftFlanking, $rightFlanking];
}
}

View File

@@ -0,0 +1,90 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\SmartPunct;
use League\CommonMark\Delimiter\DelimiterInterface;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
use League\CommonMark\Inline\Element\AbstractStringContainer;
final class QuoteProcessor implements DelimiterProcessorInterface
{
/** @var string */
private $normalizedCharacter;
/** @var string */
private $openerCharacter;
/** @var string */
private $closerCharacter;
private function __construct(string $char, string $opener, string $closer)
{
$this->normalizedCharacter = $char;
$this->openerCharacter = $opener;
$this->closerCharacter = $closer;
}
public function getOpeningCharacter(): string
{
return $this->normalizedCharacter;
}
public function getClosingCharacter(): string
{
return $this->normalizedCharacter;
}
public function getMinLength(): int
{
return 1;
}
public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int
{
return 1;
}
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse)
{
$opener->insertAfter(new Quote($this->openerCharacter));
$closer->insertBefore(new Quote($this->closerCharacter));
}
/**
* Create a double-quote processor
*
* @param string $opener
* @param string $closer
*
* @return QuoteProcessor
*/
public static function createDoubleQuoteProcessor(string $opener = Quote::DOUBLE_QUOTE_OPENER, string $closer = Quote::DOUBLE_QUOTE_CLOSER): self
{
return new self(Quote::DOUBLE_QUOTE, $opener, $closer);
}
/**
* Create a single-quote processor
*
* @param string $opener
* @param string $closer
*
* @return QuoteProcessor
*/
public static function createSingleQuoteProcessor(string $opener = Quote::SINGLE_QUOTE_OPENER, string $closer = Quote::SINGLE_QUOTE_CLOSER): self
{
return new self(Quote::SINGLE_QUOTE, $opener, $closer);
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\SmartPunct;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
use League\CommonMark\Inline\Element\AbstractInline;
use League\CommonMark\Inline\Renderer\InlineRendererInterface;
final class QuoteRenderer implements InlineRendererInterface
{
/**
* @param Quote $inline
* @param ElementRendererInterface $htmlRenderer
*
* @return HtmlElement|string|null
*/
public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer)
{
if (!$inline instanceof Quote) {
throw new \InvalidArgumentException(sprintf('Expected an instance of "%s", got "%s" instead', Quote::class, get_class($inline)));
}
// Handles unpaired quotes which remain after processing delimiters
if ($inline->getContent() === Quote::SINGLE_QUOTE) {
// Render as an apostrophe
return Quote::SINGLE_QUOTE_CLOSER;
} elseif ($inline->getContent() === Quote::DOUBLE_QUOTE) {
// Render as an opening quote
return Quote::DOUBLE_QUOTE_OPENER;
}
return $inline->getContent();
}
}

View File

@@ -0,0 +1,49 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\SmartPunct;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\Block\Renderer as CoreBlockRenderer;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Inline\Element\Text;
use League\CommonMark\Inline\Renderer as CoreInlineRenderer;
final class SmartPunctExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment
->addInlineParser(new QuoteParser(), 10)
->addInlineParser(new PunctuationParser(), 0)
->addDelimiterProcessor(QuoteProcessor::createDoubleQuoteProcessor(
$environment->getConfig('smartpunct/double_quote_opener', Quote::DOUBLE_QUOTE_OPENER),
$environment->getConfig('smartpunct/double_quote_closer', Quote::DOUBLE_QUOTE_CLOSER)
))
->addDelimiterProcessor(QuoteProcessor::createSingleQuoteProcessor(
$environment->getConfig('smartpunct/single_quote_opener', Quote::SINGLE_QUOTE_OPENER),
$environment->getConfig('smartpunct/single_quote_closer', Quote::SINGLE_QUOTE_CLOSER)
))
->addBlockRenderer(Document::class, new CoreBlockRenderer\DocumentRenderer(), 0)
->addBlockRenderer(Paragraph::class, new CoreBlockRenderer\ParagraphRenderer(), 0)
->addInlineRenderer(Quote::class, new QuoteRenderer(), 100)
->addInlineRenderer(Text::class, new CoreInlineRenderer\TextRenderer(), 0)
;
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com> and uAfrica.com (http://uafrica.com)
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Strikethrough;
use League\CommonMark\Inline\Element\AbstractInline;
final class Strikethrough extends AbstractInline
{
public function isContainer(): bool
{
return true;
}
}

View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com> and uAfrica.com (http://uafrica.com)
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Strikethrough;
use League\CommonMark\Delimiter\DelimiterInterface;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
use League\CommonMark\Inline\Element\AbstractStringContainer;
final class StrikethroughDelimiterProcessor implements DelimiterProcessorInterface
{
public function getOpeningCharacter(): string
{
return '~';
}
public function getClosingCharacter(): string
{
return '~';
}
public function getMinLength(): int
{
return 2;
}
public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int
{
$min = \min($opener->getLength(), $closer->getLength());
return $min >= 2 ? $min : 0;
}
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse)
{
$strikethrough = new Strikethrough();
$tmp = $opener->next();
while ($tmp !== null && $tmp !== $closer) {
$next = $tmp->next();
$strikethrough->appendChild($tmp);
$tmp = $next;
}
$opener->insertAfter($strikethrough);
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com> and uAfrica.com (http://uafrica.com)
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Strikethrough;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Extension\ExtensionInterface;
final class StrikethroughExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment->addDelimiterProcessor(new StrikethroughDelimiterProcessor());
$environment->addInlineRenderer(Strikethrough::class, new StrikethroughRenderer());
}
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com> and uAfrica.com (http://uafrica.com)
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Strikethrough;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
use League\CommonMark\Inline\Element\AbstractInline;
use League\CommonMark\Inline\Renderer\InlineRendererInterface;
final class StrikethroughRenderer implements InlineRendererInterface
{
public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer)
{
if (!($inline instanceof Strikethrough)) {
throw new \InvalidArgumentException('Incompatible inline type: ' . get_class($inline));
}
return new HtmlElement('del', $inline->getData('attributes', []), $htmlRenderer->renderInlines($inline->children()));
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/*
* This is part of the league/commonmark package.
*
* (c) Martin Hasoň <martin.hason@gmail.com>
* (c) Webuni s.r.o. <info@webuni.cz>
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Table;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\AbstractStringContainerBlock;
use League\CommonMark\Block\Element\InlineContainerInterface;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
final class Table extends AbstractStringContainerBlock implements InlineContainerInterface
{
/** @var TableSection */
private $head;
/** @var TableSection */
private $body;
/** @var \Closure */
private $parser;
public function __construct(\Closure $parser)
{
parent::__construct();
$this->appendChild($this->head = new TableSection(TableSection::TYPE_HEAD));
$this->appendChild($this->body = new TableSection(TableSection::TYPE_BODY));
$this->parser = $parser;
}
public function canContain(AbstractBlock $block): bool
{
return $block instanceof TableSection;
}
public function isCode(): bool
{
return false;
}
public function getHead(): TableSection
{
return $this->head;
}
public function getBody(): TableSection
{
return $this->body;
}
public function matchesNextLine(Cursor $cursor): bool
{
return call_user_func($this->parser, $cursor, $this);
}
public function handleRemainingContents(ContextInterface $context, Cursor $cursor): void
{
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/*
* This is part of the league/commonmark package.
*
* (c) Martin Hasoň <martin.hason@gmail.com>
* (c) Webuni s.r.o. <info@webuni.cz>
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Table;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\AbstractStringContainerBlock;
use League\CommonMark\Block\Element\InlineContainerInterface;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
final class TableCell extends AbstractStringContainerBlock implements InlineContainerInterface
{
const TYPE_HEAD = 'th';
const TYPE_BODY = 'td';
const ALIGN_LEFT = 'left';
const ALIGN_RIGHT = 'right';
const ALIGN_CENTER = 'center';
/** @var string */
public $type = self::TYPE_BODY;
/** @var string|null */
public $align;
public function __construct(string $string = '', string $type = self::TYPE_BODY, string $align = null)
{
parent::__construct();
$this->finalStringContents = $string;
$this->addLine($string);
$this->type = $type;
$this->align = $align;
}
public function canContain(AbstractBlock $block): bool
{
return false;
}
public function isCode(): bool
{
return false;
}
public function matchesNextLine(Cursor $cursor): bool
{
return false;
}
public function handleRemainingContents(ContextInterface $context, Cursor $cursor): void
{
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* This is part of the league/commonmark package.
*
* (c) Martin Hasoň <martin.hason@gmail.com>
* (c) Webuni s.r.o. <info@webuni.cz>
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Table;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
final class TableCellRenderer implements BlockRendererInterface
{
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
if (!$block instanceof TableCell) {
throw new \InvalidArgumentException('Incompatible block type: ' . get_class($block));
}
$attrs = $block->getData('attributes', []);
if ($block->align !== null) {
$attrs['align'] = $block->align;
}
return new HtmlElement($block->type, $attrs, $htmlRenderer->renderInlines($block->children()));
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*
* This is part of the league/commonmark package.
*
* (c) Martin Hasoň <martin.hason@gmail.com>
* (c) Webuni s.r.o. <info@webuni.cz>
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Table;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Extension\ExtensionInterface;
final class TableExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment): void
{
$environment
->addBlockParser(new TableParser())
->addBlockRenderer(Table::class, new TableRenderer())
->addBlockRenderer(TableSection::class, new TableSectionRenderer())
->addBlockRenderer(TableRow::class, new TableRowRenderer())
->addBlockRenderer(TableCell::class, new TableCellRenderer())
;
}
}

View File

@@ -0,0 +1,283 @@
<?php
declare(strict_types=1);
/*
* This is part of the league/commonmark package.
*
* (c) Martin Hasoň <martin.hason@gmail.com>
* (c) Webuni s.r.o. <info@webuni.cz>
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Table;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\Block\Parser\BlockParserInterface;
use League\CommonMark\Context;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
use League\CommonMark\EnvironmentAwareInterface;
use League\CommonMark\EnvironmentInterface;
final class TableParser implements BlockParserInterface, EnvironmentAwareInterface
{
/**
* @var EnvironmentInterface
*/
private $environment;
public function parse(ContextInterface $context, Cursor $cursor): bool
{
$container = $context->getContainer();
if (!$container instanceof Paragraph) {
return false;
}
$lines = $container->getStrings();
if (count($lines) !== 1) {
return false;
}
if (\strpos($lines[0], '|') === false) {
return false;
}
$oldState = $cursor->saveState();
$cursor->advanceToNextNonSpaceOrTab();
$columns = $this->parseColumns($cursor);
if (empty($columns)) {
$cursor->restoreState($oldState);
return false;
}
$head = $this->parseRow(trim((string) array_pop($lines)), $columns, TableCell::TYPE_HEAD);
if (null === $head) {
$cursor->restoreState($oldState);
return false;
}
$table = new Table(function (Cursor $cursor, Table $table) use ($columns): bool {
// The next line cannot be a new block start
// This is a bit inefficient, but it's the only feasible way to check
// given the current v1 API.
if (self::isANewBlock($this->environment, $cursor->getLine())) {
return false;
}
$row = $this->parseRow(\trim($cursor->getLine()), $columns);
if (null === $row) {
return false;
}
$table->getBody()->appendChild($row);
return true;
});
$table->getHead()->appendChild($head);
if (count($lines) >= 1) {
$paragraph = new Paragraph();
foreach ($lines as $line) {
$paragraph->addLine($line);
}
$context->replaceContainerBlock($paragraph);
$context->addBlock($table);
} else {
$context->replaceContainerBlock($table);
}
return true;
}
/**
* @param string $line
* @param array<int, string> $columns
* @param string $type
*
* @return TableRow|null
*/
private function parseRow(string $line, array $columns, string $type = TableCell::TYPE_BODY): ?TableRow
{
$cells = $this->split(new Cursor(\trim($line)));
if (empty($cells)) {
return null;
}
// The header row must match the delimiter row in the number of cells
if ($type === TableCell::TYPE_HEAD && \count($cells) !== \count($columns)) {
return null;
}
$i = 0;
$row = new TableRow();
foreach ($cells as $i => $cell) {
if (!array_key_exists($i, $columns)) {
return $row;
}
$row->appendChild(new TableCell(trim($cell), $type, $columns[$i]));
}
for ($j = count($columns) - 1; $j > $i; --$j) {
$row->appendChild(new TableCell('', $type, null));
}
return $row;
}
/**
* @param Cursor $cursor
*
* @return array<int, string>
*/
private function split(Cursor $cursor): array
{
if ($cursor->getCharacter() === '|') {
$cursor->advanceBy(1);
}
$cells = [];
$sb = '';
while (!$cursor->isAtEnd()) {
switch ($c = $cursor->getCharacter()) {
case '\\':
if ($cursor->peek() === '|') {
// Pipe is special for table parsing. An escaped pipe doesn't result in a new cell, but is
// passed down to inline parsing as an unescaped pipe. Note that that applies even for the `\|`
// in an input like `\\|` - in other words, table parsing doesn't support escaping backslashes.
$sb .= '|';
$cursor->advanceBy(1);
} else {
// Preserve backslash before other characters or at end of line.
$sb .= '\\';
}
break;
case '|':
$cells[] = $sb;
$sb = '';
break;
default:
$sb .= $c;
}
$cursor->advanceBy(1);
}
if ($sb !== '') {
$cells[] = $sb;
}
return $cells;
}
/**
* @param Cursor $cursor
*
* @return array<int, string>
*/
private function parseColumns(Cursor $cursor): array
{
$columns = [];
$pipes = 0;
$valid = false;
while (!$cursor->isAtEnd()) {
switch ($c = $cursor->getCharacter()) {
case '|':
$cursor->advanceBy(1);
$pipes++;
if ($pipes > 1) {
// More than one adjacent pipe not allowed
return [];
}
// Need at least one pipe, even for a one-column table
$valid = true;
break;
case '-':
case ':':
if ($pipes === 0 && !empty($columns)) {
// Need a pipe after the first column (first column doesn't need to start with one)
return [];
}
$left = false;
$right = false;
if ($c === ':') {
$left = true;
$cursor->advanceBy(1);
}
if ($cursor->match('/^-+/') === null) {
// Need at least one dash
return [];
}
if ($cursor->getCharacter() === ':') {
$right = true;
$cursor->advanceBy(1);
}
$columns[] = $this->getAlignment($left, $right);
// Next, need another pipe
$pipes = 0;
break;
case ' ':
case "\t":
// White space is allowed between pipes and columns
$cursor->advanceToNextNonSpaceOrTab();
break;
default:
// Any other character is invalid
return [];
}
}
if (!$valid) {
return [];
}
return $columns;
}
private static function getAlignment(bool $left, bool $right): ?string
{
if ($left && $right) {
return TableCell::ALIGN_CENTER;
} elseif ($left) {
return TableCell::ALIGN_LEFT;
} elseif ($right) {
return TableCell::ALIGN_RIGHT;
}
return null;
}
public function setEnvironment(EnvironmentInterface $environment)
{
$this->environment = $environment;
}
private static function isANewBlock(EnvironmentInterface $environment, string $line): bool
{
$context = new Context(new Document(), $environment);
$context->setNextLine($line);
$cursor = new Cursor($line);
/** @var BlockParserInterface $parser */
foreach ($environment->getBlockParsers() as $parser) {
if ($parser->parse($context, $cursor)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* This is part of the league/commonmark package.
*
* (c) Martin Hasoň <martin.hason@gmail.com>
* (c) Webuni s.r.o. <info@webuni.cz>
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Table;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
final class TableRenderer implements BlockRendererInterface
{
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
if (!$block instanceof Table) {
throw new \InvalidArgumentException('Incompatible block type: ' . get_class($block));
}
$attrs = $block->getData('attributes', []);
$separator = $htmlRenderer->getOption('inner_separator', "\n");
$children = $htmlRenderer->renderBlocks($block->children());
return new HtmlElement('table', $attrs, $separator . \trim($children) . $separator);
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/*
* This is part of the league/commonmark package.
*
* (c) Martin Hasoň <martin.hason@gmail.com>
* (c) Webuni s.r.o. <info@webuni.cz>
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Table;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Cursor;
use League\CommonMark\Node\Node;
final class TableRow extends AbstractBlock
{
public function canContain(AbstractBlock $block): bool
{
return $block instanceof TableCell;
}
public function isCode(): bool
{
return false;
}
public function matchesNextLine(Cursor $cursor): bool
{
return false;
}
/**
* @return AbstractBlock[]
*/
public function children(): iterable
{
return array_filter((array) parent::children(), static function (Node $child): bool {
return $child instanceof AbstractBlock;
});
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* This is part of the league/commonmark package.
*
* (c) Martin Hasoň <martin.hason@gmail.com>
* (c) Webuni s.r.o. <info@webuni.cz>
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Table;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
final class TableRowRenderer implements BlockRendererInterface
{
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
if (!$block instanceof TableRow) {
throw new \InvalidArgumentException('Incompatible block type: ' . get_class($block));
}
$attrs = $block->getData('attributes', []);
$separator = $htmlRenderer->getOption('inner_separator', "\n");
return new HtmlElement('tr', $attrs, $separator . $htmlRenderer->renderBlocks($block->children()) . $separator);
}
}

Some files were not shown because too many files have changed in this diff Show More