mirror of
https://github.com/erusev/parsedown.git
synced 2023-08-10 21:13:06 +03:00
Compare commits
6 Commits
test/sheph
...
master
Author | SHA1 | Date | |
---|---|---|---|
6598f3860c | |||
1e5080190c | |||
1610e4747c | |||
3159a9d3cd | |||
dbee8ab4f2 | |||
dba4125b59 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,2 @@
|
||||
composer.lock
|
||||
vendor/
|
||||
infection.log
|
||||
tests/spec_cache.txt
|
||||
|
37
.php_cs.dist
37
.php_cs.dist
@ -1,37 +0,0 @@
|
||||
<?php
|
||||
use PhpCsFixer\Config;
|
||||
use PhpCsFixer\Finder;
|
||||
$finder = Finder::create()
|
||||
->in(__DIR__ . '/src/')
|
||||
->in(__DIR__ . '/tests/')
|
||||
;
|
||||
$rules = [
|
||||
'@PSR2' => true,
|
||||
'array_syntax' => [
|
||||
'syntax' => 'short',
|
||||
],
|
||||
'braces' => [
|
||||
'allow_single_line_closure' => true,
|
||||
],
|
||||
'logical_operators' => true,
|
||||
'native_constant_invocation' => [
|
||||
'fix_built_in' => true,
|
||||
],
|
||||
'native_function_invocation' => [
|
||||
'include' => ['@all'],
|
||||
],
|
||||
'no_unused_imports' => true,
|
||||
'ordered_imports' => [
|
||||
'sort_algorithm' => 'alpha',
|
||||
],
|
||||
'single_blank_line_before_namespace' => true,
|
||||
'strict_comparison' => true,
|
||||
'strict_param' => true,
|
||||
'whitespace_after_comma_in_array' => true,
|
||||
];
|
||||
return Config::create()
|
||||
->setRules($rules)
|
||||
->setFinder($finder)
|
||||
->setUsingCache(false)
|
||||
->setRiskyAllowed(true)
|
||||
;
|
93
.travis.yml
93
.travis.yml
@ -1,73 +1,30 @@
|
||||
language: php
|
||||
|
||||
dist: trusty
|
||||
sudo: false
|
||||
|
||||
stages:
|
||||
- Code Format and Static Analysis
|
||||
- Units
|
||||
- Test CommonMark (weak)
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- env: ALLOW_FAILURE
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
|
||||
jobs:
|
||||
matrix:
|
||||
include:
|
||||
- stage: Code Format and Static Analysis
|
||||
php: 7.3
|
||||
install: composer install --prefer-dist --no-interaction --no-progress
|
||||
script:
|
||||
- '[ -z "$TRAVIS_TAG" ] || [ "$TRAVIS_TAG" == "$(php -r "require(\"vendor/autoload.php\"); echo Erusev\Parsedown\Parsedown::version;")" ]'
|
||||
- composer test-static -- --shepherd
|
||||
- composer test-formatting
|
||||
- composer test-dead-code
|
||||
- php: 5.3
|
||||
dist: precise
|
||||
- php: 5.4
|
||||
dist: trusty
|
||||
- php: 5.5
|
||||
dist: trusty
|
||||
- php: 5.6
|
||||
dist: xenial
|
||||
- php: 7.0
|
||||
dist: xenial
|
||||
- php: 7.1
|
||||
dist: bionic
|
||||
- php: 7.2
|
||||
dist: bionic
|
||||
- php: 7.3
|
||||
dist: bionic
|
||||
- php: 7.4
|
||||
dist: bionic
|
||||
|
||||
install:
|
||||
- composer install --prefer-dist --no-interaction --no-progress
|
||||
|
||||
- &UNIT_TEST
|
||||
stage: Units
|
||||
php: 5.5
|
||||
install:
|
||||
# remove packages with PHP requirements higher than 7.0 to prevent composer trying to resolve these, see: https://github.com/composer/composer/issues/6011
|
||||
- composer remove vimeo/psalm friendsofphp/php-cs-fixer infection/infection --dev --no-update --no-interaction
|
||||
- composer install --prefer-dist --no-interaction --no-progress
|
||||
script: composer test-units
|
||||
- <<: *UNIT_TEST
|
||||
php: 5.6
|
||||
- <<: *UNIT_TEST
|
||||
php: 7.0
|
||||
- &MUTATION_AND_UNIT_TEST
|
||||
<<: *UNIT_TEST
|
||||
php: 7.1
|
||||
install:
|
||||
- composer install --prefer-dist --no-interaction --no-progress
|
||||
script:
|
||||
- composer test-units
|
||||
- vendor/bin/infection --show-mutations --threads=4 --min-msi=90 --min-covered-msi=90
|
||||
- <<: *MUTATION_AND_UNIT_TEST
|
||||
php: 7.2
|
||||
- <<: *MUTATION_AND_UNIT_TEST
|
||||
php: 7.3
|
||||
- <<: *UNIT_TEST
|
||||
php: nightly
|
||||
env: ALLOW_FAILURE
|
||||
|
||||
|
||||
- &COMMONMARK_TEST
|
||||
stage: CommonMark
|
||||
name: Weak
|
||||
php: 7.3
|
||||
env: ALLOW_FAILURE
|
||||
install: composer install --prefer-dist --no-interaction --no-progress
|
||||
script:
|
||||
- composer test-commonmark-weak
|
||||
|
||||
- <<: *COMMONMARK_TEST
|
||||
name: Strict
|
||||
script:
|
||||
- composer test-commonmark
|
||||
script:
|
||||
- vendor/bin/phpunit
|
||||
- vendor/bin/phpunit test/CommonMarkTestWeak.php || true
|
||||
- '[ -z "$TRAVIS_TAG" ] || [ "$TRAVIS_TAG" == "$(php -r "require(\"Parsedown.php\"); echo Parsedown::version;")" ]'
|
||||
|
1994
Parsedown.php
Normal file
1994
Parsedown.php
Normal file
File diff suppressed because it is too large
Load Diff
10
README.md
10
README.md
@ -17,8 +17,8 @@ Better Markdown Parser in PHP - <a href="http://parsedown.org/demo">Demo</a>.
|
||||
* No Dependencies
|
||||
* [Super Fast](http://parsedown.org/speed)
|
||||
* Extensible
|
||||
* [GitHub flavored](https://help.github.com/articles/github-flavored-markdown)
|
||||
* Tested in 5.5 to 7.3
|
||||
* [GitHub flavored](https://github.github.com/gfm)
|
||||
* [Tested](http://parsedown.org/tests/) in 5.3 to 7.3
|
||||
* [Markdown Extra extension](https://github.com/erusev/parsedown-extra)
|
||||
|
||||
## Installation
|
||||
@ -68,7 +68,7 @@ Safe mode does not necessarily yield safe results when using extensions to Parse
|
||||
|
||||
## Escaping HTML
|
||||
|
||||
> ⚠️ **WARNING:** This method isn't safe from XSS!
|
||||
> **WARNING:** This method isn't safe from XSS!
|
||||
|
||||
If you wish to escape HTML **in trusted input**, you can use the following:
|
||||
|
||||
@ -97,3 +97,7 @@ It passes most of the CommonMark tests. Most of the tests that don't pass deal w
|
||||
**How can I help?**
|
||||
|
||||
Use it, star it, share it and if you feel generous, [donate](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=528P3NZQMP8N2).
|
||||
|
||||
**What else should I know?**
|
||||
|
||||
I also make [Nota](https://nota.md/) — a writing app designed for Markdown files :)
|
||||
|
@ -13,35 +13,21 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7||^5.5",
|
||||
"php": ">=5.3.0",
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7.4||^6.5.13||^5.7.27||^4.8.36",
|
||||
"vimeo/psalm": "^3.2.7",
|
||||
"friendsofphp/php-cs-fixer": "^2.13",
|
||||
"infection/infection": "^0.12.0"
|
||||
"phpunit/phpunit": "^4.8.35"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {"Erusev\\Parsedown\\": "src/"}
|
||||
"psr-0": {"Parsedown": ""}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {"Erusev\\Parsedown\\Tests\\": "tests/"}
|
||||
},
|
||||
"scripts": {
|
||||
"test": [
|
||||
"@test-static",
|
||||
"@test-formatting",
|
||||
"@test-dead-code",
|
||||
"@test-units"
|
||||
],
|
||||
"test-static": "vendor/bin/psalm",
|
||||
"test-dead-code": "vendor/bin/psalm --find-dead-code",
|
||||
"test-units": "vendor/bin/phpunit",
|
||||
"test-commonmark": "vendor/bin/phpunit tests/CommonMarkTestStrict.php",
|
||||
"test-commonmark-weak": "vendor/bin/phpunit tests/CommonMarkTestWeak.php",
|
||||
"test-formatting": "@composer fix -- --dry-run",
|
||||
|
||||
"fix": "vendor/bin/php-cs-fixer fix --verbose --show-progress=dots --diff"
|
||||
"psr-0": {
|
||||
"TestParsedown": "test/",
|
||||
"ParsedownTest": "test/",
|
||||
"CommonMarkTest": "test/",
|
||||
"CommonMarkTestWeak": "test/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
{
|
||||
"timeout": 10,
|
||||
"source": {
|
||||
"directories": [
|
||||
"src"
|
||||
]
|
||||
},
|
||||
"logs": {
|
||||
"text": "infection.log"
|
||||
},
|
||||
"mutators": {
|
||||
"@default": true,
|
||||
"@cast": false,
|
||||
"This": false,
|
||||
"FunctionCall": false,
|
||||
"NewObject": false
|
||||
}
|
||||
}
|
@ -1,15 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit bootstrap="vendor/autoload.php" colors="true">
|
||||
<testsuites>
|
||||
<testsuite name="ParsedownTests">
|
||||
<file>tests/ParsedownTest.php</file>
|
||||
<file>tests/CommonMarkTest.php</file>
|
||||
<directory suffix="Test.php">./tests/src</directory>
|
||||
<testsuite>
|
||||
<file>test/ParsedownTest.php</file>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<groups>
|
||||
<exclude>
|
||||
<group>update</group>
|
||||
</exclude>
|
||||
</groups>
|
||||
</phpunit>
|
||||
|
34
psalm.xml
34
psalm.xml
@ -1,34 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<psalm
|
||||
totallyTyped="true"
|
||||
strictBinaryOperands="true"
|
||||
checkForThrowsDocblock="true"
|
||||
>
|
||||
<projectFiles>
|
||||
<directory name="src" />
|
||||
<directory name="tests" />
|
||||
</projectFiles>
|
||||
|
||||
<issueHandlers>
|
||||
<PossiblyUnusedMethod>
|
||||
<errorLevel type="suppress">
|
||||
<directory name="tests" />
|
||||
</errorLevel>
|
||||
</PossiblyUnusedMethod>
|
||||
<PropertyNotSetInConstructor>
|
||||
<errorLevel type="suppress"><directory name="tests" /></errorLevel>
|
||||
</PropertyNotSetInConstructor>
|
||||
<UnusedClass>
|
||||
<errorLevel type="suppress"><directory name="tests/src" /></errorLevel>
|
||||
</UnusedClass>
|
||||
<UndefinedInterfaceMethod>
|
||||
<errorLevel type="suppress"><directory name="tests/src" /></errorLevel>
|
||||
</UndefinedInterfaceMethod>
|
||||
<PossiblyNullArrayAccess>
|
||||
<errorLevel type="suppress"><directory name="tests/src" /></errorLevel>
|
||||
</PossiblyNullArrayAccess>
|
||||
<PossiblyNullReference>
|
||||
<errorLevel type="suppress"><directory name="tests/src" /></errorLevel>
|
||||
</PossiblyNullReference>
|
||||
</issueHandlers>
|
||||
</psalm>
|
@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\AST;
|
||||
|
||||
use Erusev\Parsedown\Html\Renderable;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
/**
|
||||
* @template T as Renderable
|
||||
*/
|
||||
final class Handler implements StateRenderable
|
||||
{
|
||||
/** @var callable(State):T */
|
||||
private $closure;
|
||||
|
||||
/**
|
||||
* @param callable(State):T $closure
|
||||
*/
|
||||
public function __construct(callable $closure)
|
||||
{
|
||||
$this->closure = $closure;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param State $State
|
||||
* @return T&Renderable
|
||||
*/
|
||||
public function renderable(State $State)
|
||||
{
|
||||
$closure = $this->closure;
|
||||
|
||||
return $closure($State);
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\AST;
|
||||
|
||||
use Erusev\Parsedown\Html\Renderable;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
interface StateRenderable
|
||||
{
|
||||
/**
|
||||
* @param State $State
|
||||
* @return Renderable
|
||||
*/
|
||||
public function renderable(State $State);
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
|
||||
interface Component
|
||||
{
|
||||
/**
|
||||
* @return StateRenderable
|
||||
*/
|
||||
public function stateRenderable();
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components;
|
||||
|
||||
interface AcquisitioningBlock extends Block
|
||||
{
|
||||
/**
|
||||
* Return true if the block was built encompassing the previous block
|
||||
* $Block given to static::build, return false otherwise.
|
||||
* @return bool
|
||||
*/
|
||||
public function acquiredPrevious();
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components;
|
||||
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
|
||||
interface BacktrackingInline extends Inline
|
||||
{
|
||||
/**
|
||||
* Return an integer to declare that the inline should be treated as if it
|
||||
* started from that position in the excerpt given to static::build.
|
||||
* Return null to use the excerpt offset value.
|
||||
* @return int|null
|
||||
* */
|
||||
public function modifyStartPositionTo();
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components;
|
||||
|
||||
use Erusev\Parsedown\Component;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
interface Block extends Component
|
||||
{
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
);
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\ContinuableBlock;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\Parsing\Line;
|
||||
use Erusev\Parsedown\Parsing\Lines;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class BlockQuote implements ContinuableBlock
|
||||
{
|
||||
/** @var Lines */
|
||||
private $Lines;
|
||||
|
||||
/**
|
||||
* @param Lines $Lines
|
||||
*/
|
||||
private function __construct($Lines)
|
||||
{
|
||||
$this->Lines = $Lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
if (\preg_match('/^(>[ \t]?+)(.*+)/', $Context->line()->text(), $matches)) {
|
||||
$indentOffset = $Context->line()->indentOffset() + $Context->line()->indent() + \strlen($matches[1]);
|
||||
|
||||
$recoveredSpaces = 0;
|
||||
if (\strlen($matches[1]) === 2 && \substr($matches[1], 1, 1) === "\t") {
|
||||
$recoveredSpaces = Line::tabShortage(0, $indentOffset -1) -1;
|
||||
}
|
||||
|
||||
return new self(Lines::fromTextLines(
|
||||
\str_repeat(' ', $recoveredSpaces) . $matches[2],
|
||||
$indentOffset
|
||||
));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @return self|null
|
||||
*/
|
||||
public function advance(Context $Context, State $State)
|
||||
{
|
||||
if ($Context->previousEmptyLines() > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (\preg_match('/^(>[ \t]?+)(.*+)/', $Context->line()->text(), $matches)) {
|
||||
$indentOffset = $Context->line()->indentOffset() + $Context->line()->indent() + \strlen($matches[1]);
|
||||
|
||||
$recoveredSpaces = 0;
|
||||
if (\strlen($matches[1]) === 2 && \substr($matches[1], 1, 1) === "\t") {
|
||||
$recoveredSpaces = Line::tabShortage(0, $indentOffset -1) -1;
|
||||
}
|
||||
|
||||
$Lines = $this->Lines->appendingTextLines(
|
||||
\str_repeat(' ', $recoveredSpaces) . $matches[2],
|
||||
$indentOffset
|
||||
);
|
||||
|
||||
return new self($Lines);
|
||||
}
|
||||
|
||||
if (! $Context->previousEmptyLines() > 0) {
|
||||
$indentOffset = $Context->line()->indentOffset() + $Context->line()->indent();
|
||||
$Lines = $this->Lines->appendingTextLines($Context->line()->text(), $indentOffset);
|
||||
|
||||
return new self($Lines);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: Block[], 1: State}
|
||||
*/
|
||||
public function contents(State $State)
|
||||
{
|
||||
return Parsedown::blocks($this->Lines, $State);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element */
|
||||
function (State $State) {
|
||||
list($Blocks, $State) = $this->contents($State);
|
||||
|
||||
$StateRenderables = Parsedown::stateRenderablesFrom($Blocks);
|
||||
|
||||
$Renderables = $State->applyTo($StateRenderables);
|
||||
$Renderables[] = new Text("\n");
|
||||
|
||||
return new Element('blockquote', [], $Renderables);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -1,150 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\ContinuableBlock;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class FencedCode implements ContinuableBlock
|
||||
{
|
||||
/** @var string */
|
||||
private $code;
|
||||
|
||||
/** @var string */
|
||||
private $infostring;
|
||||
|
||||
/** @var string */
|
||||
private $marker;
|
||||
|
||||
/** @var int */
|
||||
private $openerLength;
|
||||
|
||||
/** @var bool */
|
||||
private $isComplete;
|
||||
|
||||
/**
|
||||
* @param string $code
|
||||
* @param string $infostring
|
||||
* @param string $marker
|
||||
* @param int $openerLength
|
||||
* @param bool $isComplete
|
||||
*/
|
||||
private function __construct($code, $infostring, $marker, $openerLength, $isComplete)
|
||||
{
|
||||
$this->code = $code;
|
||||
$this->infostring = $infostring;
|
||||
$this->marker = $marker;
|
||||
$this->openerLength = $openerLength;
|
||||
$this->isComplete = $isComplete;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
$marker = \substr($Context->line()->text(), 0, 1);
|
||||
|
||||
if ($marker !== '`' && $marker !== '~') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$openerLength = \strspn($Context->line()->text(), $marker);
|
||||
|
||||
if ($openerLength < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$infostring = \trim(\substr($Context->line()->text(), $openerLength), "\t ");
|
||||
|
||||
if (\strpos($infostring, '`') !== false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self('', $infostring, $marker, $openerLength, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @return self|null
|
||||
*/
|
||||
public function advance(Context $Context, State $State)
|
||||
{
|
||||
if ($this->isComplete) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$newCode = $this->code;
|
||||
|
||||
$newCode .= $Context->previousEmptyLinesText();
|
||||
|
||||
if (($len = \strspn($Context->line()->text(), $this->marker)) >= $this->openerLength
|
||||
&& \chop(\substr($Context->line()->text(), $len), ' ') === ''
|
||||
) {
|
||||
return new self($newCode, $this->infostring, $this->marker, $this->openerLength, true);
|
||||
}
|
||||
|
||||
$newCode .= $Context->line()->rawLine() . "\n";
|
||||
|
||||
return new self($newCode, $this->infostring, $this->marker, $this->openerLength, false);
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function infostring()
|
||||
{
|
||||
return $this->infostring;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function code()
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Element
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
/**
|
||||
* https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
|
||||
* Every HTML element may have a class attribute specified.
|
||||
* The attribute, if specified, must have a value that is a set
|
||||
* of space-separated tokens representing the various classes
|
||||
* that the element belongs to.
|
||||
* [...]
|
||||
* The space characters, for the purposes of this specification,
|
||||
* are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab),
|
||||
* U+000A LINE FEED (LF), U+000C FORM FEED (FF), and
|
||||
* U+000D CARRIAGE RETURN (CR).
|
||||
*/
|
||||
$infostring = \substr(
|
||||
$this->infostring(),
|
||||
0,
|
||||
\strcspn($this->infostring(), " \t\n\f\r")
|
||||
);
|
||||
|
||||
// only necessary pre-php7
|
||||
if ($infostring === false) {
|
||||
$infostring = '';
|
||||
}
|
||||
|
||||
return new Element('pre', [], [new Element(
|
||||
'code',
|
||||
$infostring !== '' ? ['class' => "language-{$infostring}"] : [],
|
||||
[new Text($this->code())]
|
||||
)]);
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Configurables\StrictMode;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Header implements Block
|
||||
{
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/** @var 1|2|3|4|5|6 */
|
||||
private $level;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param 1|2|3|4|5|6 $level
|
||||
*/
|
||||
private function __construct($text, $level)
|
||||
{
|
||||
$this->text = $text;
|
||||
$this->level = $level;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
if ($Context->line()->indent() > 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$level = \strspn($Context->line()->text(), '#');
|
||||
|
||||
if ($level > 6 || $level < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var 1|2|3|4|5|6 $level */
|
||||
|
||||
$text = \ltrim($Context->line()->text(), '#');
|
||||
|
||||
$firstChar = \substr($text, 0, 1);
|
||||
|
||||
if (
|
||||
$State->get(StrictMode::class)->isEnabled()
|
||||
&& \trim($firstChar, " \t") !== ''
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$text = \trim($text, " \t");
|
||||
|
||||
# remove closing sequence
|
||||
$removedClosing = \rtrim($text, '#');
|
||||
$lastChar = \substr($removedClosing, -1);
|
||||
|
||||
if (\trim($lastChar, " \t") === '') {
|
||||
$text = \rtrim($removedClosing, " \t");
|
||||
}
|
||||
|
||||
return new self($text, $level);
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function text()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/** @return 1|2|3|4|5|6 */
|
||||
public function level()
|
||||
{
|
||||
return $this->level;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element */
|
||||
function (State $State) {
|
||||
return new Element(
|
||||
'h' . \strval($this->level()),
|
||||
[],
|
||||
$State->applyTo(Parsedown::line($this->text(), $State))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\ContinuableBlock;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\Parsing\Line;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class IndentedCode implements ContinuableBlock
|
||||
{
|
||||
/** @var string */
|
||||
private $code;
|
||||
|
||||
/**
|
||||
* @param string $code
|
||||
*/
|
||||
private function __construct($code)
|
||||
{
|
||||
$this->code = $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
if (isset($Block) && $Block instanceof Paragraph && ! $Context->previousEmptyLines() > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($Context->line()->indent() < 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self($Context->line()->ltrimBodyUpto(4) . "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @return self|null
|
||||
*/
|
||||
public function advance(Context $Context, State $State)
|
||||
{
|
||||
if ($Context->line()->indent() < 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$newCode = $this->code;
|
||||
|
||||
$offset = $Context->line()->indentOffset();
|
||||
|
||||
if ($Context->previousEmptyLines() > 0) {
|
||||
foreach (\explode("\n", $Context->previousEmptyLinesText()) as $line) {
|
||||
$newCode .= (new Line($line, $offset))->ltrimBodyUpto(4) . "\n";
|
||||
}
|
||||
|
||||
$newCode = \substr($newCode, 0, -1);
|
||||
}
|
||||
|
||||
$newCode .= $Context->line()->ltrimBodyUpto(4) . "\n";
|
||||
|
||||
return new self($newCode);
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function code()
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Element
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Element(
|
||||
'pre',
|
||||
[],
|
||||
[new Element('code', [], [new Text($this->code())])]
|
||||
);
|
||||
}
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\ContinuableBlock;
|
||||
use Erusev\Parsedown\Configurables\SafeMode;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\RawHtml;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Markup implements ContinuableBlock
|
||||
{
|
||||
const REGEX_HTML_ATTRIBUTE = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+';
|
||||
|
||||
/** @var array{2: string, 3: string, 4: string, 5: string} */
|
||||
private static $simpleContainsEndConditions = [
|
||||
2 => '-->',
|
||||
3 => '?>',
|
||||
4 => '>',
|
||||
5 => ']]>'
|
||||
];
|
||||
|
||||
/** @var array<string, string> */
|
||||
private static $specialHtmlBlockTags = [
|
||||
'script' => true,
|
||||
'style' => true,
|
||||
'pre' => true,
|
||||
];
|
||||
|
||||
/** @var string */
|
||||
private $html;
|
||||
|
||||
/** @var 1|2|3|4|5|6|7 */
|
||||
private $type;
|
||||
|
||||
/** @var bool */
|
||||
private $closed;
|
||||
|
||||
/**
|
||||
* @param string $html
|
||||
* @param 1|2|3|4|5|6|7 $type
|
||||
* @param bool $closed
|
||||
*/
|
||||
private function __construct($html, $type, $closed = false)
|
||||
{
|
||||
$this->html = $html;
|
||||
$this->type = $type;
|
||||
$this->closed = $closed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
$text = $Context->line()->text();
|
||||
$rawLine = $Context->line()->rawLine();
|
||||
|
||||
if (\preg_match('/^<(?:script|pre|style)(?:\s++|>|$)/i', $text)) {
|
||||
return new self($rawLine, 1, self::closes12345TypeMarkup(1, $text));
|
||||
}
|
||||
|
||||
if (\substr($text, 0, 4) === '<!--') {
|
||||
return new self($rawLine, 2, self::closes12345TypeMarkup(2, $text));
|
||||
}
|
||||
|
||||
if (\substr($text, 0, 2) === '<?') {
|
||||
return new self($rawLine, 3, self::closes12345TypeMarkup(3, $text));
|
||||
}
|
||||
|
||||
if (\preg_match('/^<![A-Z]/', $text)) {
|
||||
return new self($rawLine, 4, self::closes12345TypeMarkup(4, $text));
|
||||
}
|
||||
|
||||
if (\substr($text, 0, 9) === '<![CDATA[') {
|
||||
return new self($rawLine, 5, self::closes12345TypeMarkup(5, $text));
|
||||
}
|
||||
|
||||
if (\preg_match('/^<[\/]?+(\w++)(?:[ ]*+'.self::REGEX_HTML_ATTRIBUTE.')*+[ ]*+(\/)?>/', $text, $matches)) {
|
||||
$element = \strtolower($matches[1]);
|
||||
|
||||
if (
|
||||
\array_key_exists($element, Element::$TEXT_LEVEL_ELEMENTS)
|
||||
|| \array_key_exists($element, self::$specialHtmlBlockTags)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self($rawLine, 6);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @return self|null
|
||||
*/
|
||||
public function advance(Context $Context, State $State)
|
||||
{
|
||||
$closed = $this->closed;
|
||||
$type = $this->type;
|
||||
|
||||
if ($closed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (($type === 6 || $type === 7) && $Context->previousEmptyLines() > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type === 1 || $type === 2 || $type === 3 || $type === 4 || $type === 5) {
|
||||
$closed = self::closes12345TypeMarkup($type, $Context->line()->text());
|
||||
}
|
||||
|
||||
$html = $this->html . \str_repeat("\n", $Context->previousEmptyLines() + 1);
|
||||
$html .= $Context->line()->rawLine();
|
||||
|
||||
return new self($html, $type, $closed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param 1|2|3|4|5 $type
|
||||
* @param string $text
|
||||
* @return bool
|
||||
*/
|
||||
private static function closes12345TypeMarkup($type, $text)
|
||||
{
|
||||
if ($type === 1) {
|
||||
if (\preg_match('/<\/(?:script|pre|style)>/i', $text)) {
|
||||
return true;
|
||||
}
|
||||
} elseif (\stripos($text, self::$simpleContainsEndConditions[$type]) !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function html()
|
||||
{
|
||||
return $this->html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element|RawHtml>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element|RawHtml */
|
||||
function (State $State) {
|
||||
if ($State->get(SafeMode::class)->isEnabled()) {
|
||||
return new Element('p', [], [new Text($this->html())]);
|
||||
} else {
|
||||
return new RawHtml($this->html());
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\ContinuableBlock;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Paragraph implements ContinuableBlock
|
||||
{
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
*/
|
||||
private function __construct($text)
|
||||
{
|
||||
$this->text = $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
return new self($Context->line()->text());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @return self|null
|
||||
*/
|
||||
public function advance(Context $Context, State $State)
|
||||
{
|
||||
if ($Context->previousEmptyLines() > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self($this->text . "\n" . $Context->line()->text());
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function text()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element */
|
||||
function (State $State) {
|
||||
return new Element(
|
||||
'p',
|
||||
[],
|
||||
$State->applyTo(Parsedown::line(\trim($this->text()), $State))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\StateUpdatingBlock;
|
||||
use Erusev\Parsedown\Configurables\DefinitionBook;
|
||||
use Erusev\Parsedown\Html\Renderables\Invisible;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Reference implements StateUpdatingBlock
|
||||
{
|
||||
/** @var State */
|
||||
private $State;
|
||||
|
||||
private function __construct(State $State)
|
||||
{
|
||||
$this->State = $State;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
if (\preg_match(
|
||||
'/^\[(.+?)\]:[ ]*+<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/',
|
||||
$Context->line()->text(),
|
||||
$matches
|
||||
)) {
|
||||
$id = \strtolower($matches[1]);
|
||||
|
||||
$Data = [
|
||||
'url' => $matches[2],
|
||||
'title' => isset($matches[3]) ? $matches[3] : null,
|
||||
];
|
||||
|
||||
$State = $State->setting(
|
||||
$State->get(DefinitionBook::class)->setting($id, $Data)
|
||||
);
|
||||
|
||||
return new self($State);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return State */
|
||||
public function latestState()
|
||||
{
|
||||
return $this->State;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Invisible
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Invisible;
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Rule implements Block
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
if ($Context->line()->indent() > 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$marker = \substr($Context->line()->text(), 0, 1);
|
||||
|
||||
if ($marker !== '*' && $marker !== '-' && $marker !== '_') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
\substr_count($Context->line()->text(), $marker) >= 3
|
||||
&& \chop($Context->line()->text(), " \t$marker") === ''
|
||||
) {
|
||||
return new self;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Element
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return Element::selfClosing('hr', []);
|
||||
}
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\AcquisitioningBlock;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class SetextHeader implements AcquisitioningBlock
|
||||
{
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/** @var 1|2 */
|
||||
private $level;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param 1|2 $level
|
||||
*/
|
||||
private function __construct($text, $level)
|
||||
{
|
||||
$this->text = $text;
|
||||
$this->level = $level;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
if (! isset($Block) || ! $Block instanceof Paragraph || $Context->previousEmptyLines() > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$marker = \substr($Context->line()->text(), 0, 1);
|
||||
|
||||
if ($marker !== '=' && $marker !== '-') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
$Context->line()->indent() < 4
|
||||
&& \chop(\chop($Context->line()->text(), " \t"), $marker) === ''
|
||||
) {
|
||||
$level = ($marker === '=' ? 1 : 2);
|
||||
|
||||
return new self(\trim($Block->text()), $level);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return bool */
|
||||
public function acquiredPrevious()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function text()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/** @return 1|2 */
|
||||
public function level()
|
||||
{
|
||||
return $this->level;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element */
|
||||
function (State $State) {
|
||||
return new Element(
|
||||
'h' . \strval($this->level()),
|
||||
[],
|
||||
$State->applyTo(Parsedown::line($this->text(), $State))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -1,347 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\ContinuableBlock;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\Parsing\Line;
|
||||
use Erusev\Parsedown\Parsing\Lines;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class TList implements ContinuableBlock
|
||||
{
|
||||
/** @var Lines[] */
|
||||
private $Lis;
|
||||
|
||||
/** @var int|null */
|
||||
private $listStart;
|
||||
|
||||
/** @var bool */
|
||||
private $isLoose;
|
||||
|
||||
/** @var int */
|
||||
private $indent;
|
||||
|
||||
/** @var 'ul'|'ol' */
|
||||
private $type;
|
||||
|
||||
/** @var string */
|
||||
private $marker;
|
||||
|
||||
/** @var int */
|
||||
private $afterMarkerSpaces;
|
||||
|
||||
/** @var string */
|
||||
private $markerType;
|
||||
|
||||
/** @var string */
|
||||
private $markerTypeRegex;
|
||||
|
||||
/**
|
||||
* @param Lines[] $Lis
|
||||
* @param int|null $listStart
|
||||
* @param bool $isLoose
|
||||
* @param int $indent
|
||||
* @param 'ul'|'ol' $type
|
||||
* @param string $marker
|
||||
* @param int $afterMarkerSpaces
|
||||
* @param string $markerType
|
||||
* @param string $markerTypeRegex
|
||||
*/
|
||||
private function __construct(
|
||||
$Lis,
|
||||
$listStart,
|
||||
$isLoose,
|
||||
$indent,
|
||||
$type,
|
||||
$marker,
|
||||
$afterMarkerSpaces,
|
||||
$markerType,
|
||||
$markerTypeRegex
|
||||
) {
|
||||
$this->Lis = $Lis;
|
||||
$this->listStart = $listStart;
|
||||
$this->isLoose = $isLoose;
|
||||
$this->indent = $indent;
|
||||
$this->type = $type;
|
||||
$this->marker = $marker;
|
||||
$this->afterMarkerSpaces = $afterMarkerSpaces;
|
||||
$this->markerType = $markerType;
|
||||
$this->markerTypeRegex = $markerTypeRegex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
list($type, $pattern) = (
|
||||
\substr($Context->line()->text(), 0, 1) <= '-'
|
||||
? ['ul', '[*+-]']
|
||||
: ['ol', '[0-9]{1,9}+[.\)]']
|
||||
);
|
||||
|
||||
if (\preg_match(
|
||||
'/^('.$pattern.')([\t ]++.*|$)/',
|
||||
$Context->line()->text(),
|
||||
$matches
|
||||
)) {
|
||||
$marker = $matches[1];
|
||||
|
||||
$preAfterMarkerSpacesIndentOffset = $Context->line()->indentOffset() + $Context->line()->indent() + \strlen($marker);
|
||||
|
||||
$LineWithMarkerIndent = new Line(isset($matches[2]) ? $matches[2] : '', $preAfterMarkerSpacesIndentOffset);
|
||||
$indentAfterMarker = $LineWithMarkerIndent->indent();
|
||||
|
||||
if ($indentAfterMarker > 4) {
|
||||
$perceivedIndent = $indentAfterMarker -1;
|
||||
$afterMarkerSpaces = 1;
|
||||
} else {
|
||||
$perceivedIndent = 0;
|
||||
$afterMarkerSpaces = $indentAfterMarker;
|
||||
}
|
||||
|
||||
$indentOffset = $preAfterMarkerSpacesIndentOffset + $afterMarkerSpaces;
|
||||
$text = \str_repeat(' ', $perceivedIndent) . $LineWithMarkerIndent->text();
|
||||
|
||||
$markerType = (
|
||||
$type === 'ul'
|
||||
? $marker
|
||||
: \substr($marker, -1)
|
||||
);
|
||||
|
||||
$markerTypeRegex = \preg_quote($markerType, '/');
|
||||
|
||||
/** @var int|null */
|
||||
$listStart = null;
|
||||
|
||||
if ($type === 'ol') {
|
||||
/** @psalm-suppress PossiblyFalseArgument */
|
||||
$listStart = \intval(\strstr($matches[1], $markerType, true) ?: '0');
|
||||
|
||||
if (
|
||||
$listStart !== 1
|
||||
&& isset($Block)
|
||||
&& $Block instanceof Paragraph
|
||||
&& ! $Context->previousEmptyLines() > 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return new self(
|
||||
[!empty($text) ? Lines::fromTextLines($text, $indentOffset) : Lines::none()],
|
||||
$listStart,
|
||||
false,
|
||||
$Context->line()->indent(),
|
||||
$type,
|
||||
$marker,
|
||||
$afterMarkerSpaces,
|
||||
$markerType,
|
||||
$markerTypeRegex
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @return self|null
|
||||
*/
|
||||
public function advance(Context $Context, State $State)
|
||||
{
|
||||
if ($Context->previousEmptyLines() > 0 && \end($this->Lis)->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$newlines = \str_repeat("\n", $Context->previousEmptyLines());
|
||||
|
||||
$requiredIndent = $this->indent + \strlen($this->marker) + $this->afterMarkerSpaces;
|
||||
$isLoose = $this->isLoose;
|
||||
$indent = $Context->line()->indent();
|
||||
|
||||
$Lis = $this->Lis;
|
||||
|
||||
if ($Context->line()->indent() < $requiredIndent
|
||||
&& ((
|
||||
$this->type === 'ol'
|
||||
&& \preg_match('/^([0-9]++'.$this->markerTypeRegex.')([\t ]++.*|$)/', $Context->line()->text(), $matches)
|
||||
) || (
|
||||
$this->type === 'ul'
|
||||
&& \preg_match('/^('.$this->markerTypeRegex.')([\t ]++.*|$)/', $Context->line()->text(), $matches)
|
||||
))
|
||||
) {
|
||||
if ($Context->previousEmptyLines() > 0) {
|
||||
$Lis[\count($Lis) -1] = $Lis[\count($Lis) -1]->appendingBlankLines(1);
|
||||
|
||||
$isLoose = true;
|
||||
}
|
||||
|
||||
$marker = $matches[1];
|
||||
|
||||
$preAfterMarkerSpacesIndentOffset = $Context->line()->indentOffset() + $Context->line()->indent() + \strlen($marker);
|
||||
|
||||
$LineWithMarkerIndent = new Line(isset($matches[2]) ? $matches[2] : '', $preAfterMarkerSpacesIndentOffset);
|
||||
$indentAfterMarker = $LineWithMarkerIndent->indent();
|
||||
|
||||
if ($indentAfterMarker > 4) {
|
||||
$perceivedIndent = $indentAfterMarker -1;
|
||||
$afterMarkerSpaces = 1;
|
||||
} else {
|
||||
$perceivedIndent = 0;
|
||||
$afterMarkerSpaces = $indentAfterMarker;
|
||||
}
|
||||
|
||||
$indentOffset = $preAfterMarkerSpacesIndentOffset + $afterMarkerSpaces;
|
||||
$text = \str_repeat(' ', $perceivedIndent) . $LineWithMarkerIndent->text();
|
||||
|
||||
$Lis[] = Lines::fromTextLines($newlines . $text, $indentOffset);
|
||||
|
||||
return new self(
|
||||
$Lis,
|
||||
$this->listStart,
|
||||
$isLoose,
|
||||
$indent,
|
||||
$this->type,
|
||||
$marker,
|
||||
$afterMarkerSpaces,
|
||||
$this->markerType,
|
||||
$this->markerTypeRegex
|
||||
);
|
||||
} elseif ($Context->line()->indent() < $requiredIndent && self::build($Context, $State) !== null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($Context->line()->indent() >= $requiredIndent) {
|
||||
if ($Context->previousEmptyLines() > 0) {
|
||||
$Lis[\count($Lis) -1] = $Lis[\count($Lis) -1]->appendingBlankLines($Context->previousEmptyLines());
|
||||
|
||||
$isLoose = true;
|
||||
}
|
||||
|
||||
$text = $Context->line()->ltrimBodyUpto($requiredIndent);
|
||||
$indentOffset = $Context->line()->indentOffset() + \min($requiredIndent, $Context->line()->indent());
|
||||
|
||||
$Lis[\count($Lis) -1] = $Lis[\count($Lis) -1]->appendingTextLines($text, $indentOffset);
|
||||
|
||||
return new self(
|
||||
$Lis,
|
||||
$this->listStart,
|
||||
$isLoose,
|
||||
$this->indent,
|
||||
$this->type,
|
||||
$this->marker,
|
||||
$this->afterMarkerSpaces,
|
||||
$this->markerType,
|
||||
$this->markerTypeRegex
|
||||
);
|
||||
}
|
||||
|
||||
if (! $Context->previousEmptyLines() > 0) {
|
||||
$text = $Context->line()->ltrimBodyUpto($requiredIndent);
|
||||
|
||||
$Lis[\count($Lis) -1] = $Lis[\count($Lis) -1]->appendingTextLines(
|
||||
$newlines . \str_repeat(' ', $Context->line()->indent()) . $text,
|
||||
$Context->line()->indentOffset() + \min($requiredIndent, $Context->line()->indent())
|
||||
);
|
||||
|
||||
return new self(
|
||||
$Lis,
|
||||
$this->listStart,
|
||||
$isLoose,
|
||||
$this->indent,
|
||||
$this->type,
|
||||
$this->marker,
|
||||
$this->afterMarkerSpaces,
|
||||
$this->markerType,
|
||||
$this->markerTypeRegex
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: Block[], 1: State}[]
|
||||
*/
|
||||
public function items(State $State)
|
||||
{
|
||||
return \array_map(
|
||||
/** @return array{0: Block[], 1: State} */
|
||||
function (Lines $Lines) use ($State) {
|
||||
return Parsedown::blocks($Lines, $State);
|
||||
},
|
||||
$this->Lis
|
||||
);
|
||||
}
|
||||
|
||||
/** @return 'ol'|'ul' */
|
||||
public function type()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/** @return int|null */
|
||||
public function listStart()
|
||||
{
|
||||
return $this->listStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element */
|
||||
function (State $State) {
|
||||
$listStart = $this->listStart();
|
||||
|
||||
return new Element(
|
||||
$this->type(),
|
||||
(
|
||||
isset($listStart) && $listStart !== 1
|
||||
? ['start' => \strval($listStart)]
|
||||
: []
|
||||
),
|
||||
\array_map(
|
||||
/**
|
||||
* @param array{0: Block[], 1: State} $Item
|
||||
* @return Element
|
||||
* */
|
||||
function ($Item) {
|
||||
list($Blocks, $State) = $Item;
|
||||
|
||||
$StateRenderables = Parsedown::stateRenderablesFrom($Blocks);
|
||||
$Renderables = $State->applyTo($StateRenderables);
|
||||
|
||||
if (! $this->isLoose
|
||||
&& isset($Renderables[0])
|
||||
&& $Renderables[0] instanceof Element
|
||||
&& $Renderables[0]->name() === 'p'
|
||||
) {
|
||||
$Contents = $Renderables[0]->contents();
|
||||
unset($Renderables[0]);
|
||||
$Renderables = \array_merge($Contents ?: [], $Renderables);
|
||||
}
|
||||
|
||||
return new Element('li', [], $Renderables);
|
||||
},
|
||||
$this->items($State)
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -1,277 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\AcquisitioningBlock;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\ContinuableBlock;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
/**
|
||||
* @psalm-type _Alignment='left'|'center'|'right'
|
||||
*/
|
||||
final class Table implements AcquisitioningBlock, ContinuableBlock
|
||||
{
|
||||
/** @var bool */
|
||||
private $acquired;
|
||||
|
||||
/** @var array<int, _Alignment|null> */
|
||||
private $alignments;
|
||||
|
||||
/** @var array<int, string> */
|
||||
private $headerCells;
|
||||
|
||||
/** @var array<int, array<int, string>> */
|
||||
private $rows;
|
||||
|
||||
/**
|
||||
* @param array<int, _Alignment|null> $alignments
|
||||
* @param array<int, string> $headerCells
|
||||
* @param array<int, array<int, string>> $rows
|
||||
* @param bool $acquired
|
||||
*/
|
||||
private function __construct($alignments, $headerCells, $rows, $acquired = false)
|
||||
{
|
||||
$this->alignments = $alignments;
|
||||
$this->headerCells = $headerCells;
|
||||
$this->rows = $rows;
|
||||
$this->acquired = $acquired;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
if (! isset($Block) || ! $Block instanceof Paragraph) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
\strpos($Block->text(), '|') === false
|
||||
&& \strpos($Context->line()->text(), '|') === false
|
||||
&& \strpos($Context->line()->text(), ':') === false
|
||||
|| \strpos($Block->text(), "\n") !== false
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (\chop($Context->line()->text(), ' -:|') !== '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
$alignments = self::parseAlignments($Context->line()->text());
|
||||
|
||||
if (! isset($alignments)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
# ~
|
||||
|
||||
$headerRow = \trim(\trim($Block->text()), '|');
|
||||
|
||||
$headerCells = \array_map('trim', \explode('|', $headerRow));
|
||||
|
||||
if (\count($headerCells) !== \count($alignments)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
# ~
|
||||
|
||||
return new self($alignments, $headerCells, [], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @return self|null
|
||||
*/
|
||||
public function advance(Context $Context, State $State)
|
||||
{
|
||||
if ($Context->previousEmptyLines() > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
\count($this->alignments) !== 1
|
||||
&& \strpos($Context->line()->text(), '|') === false
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = \trim(\trim($Context->line()->text()), '|');
|
||||
|
||||
if (
|
||||
! \preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches)
|
||||
|| ! isset($matches[0])
|
||||
|| ! \is_array($matches[0])
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cells = \array_map('trim', \array_slice($matches[0], 0, \count($this->alignments)));
|
||||
|
||||
return new self(
|
||||
$this->alignments,
|
||||
$this->headerCells,
|
||||
\array_merge($this->rows, [$cells])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dividerRow
|
||||
* @return array<int, _Alignment|null>|null
|
||||
*/
|
||||
private static function parseAlignments($dividerRow)
|
||||
{
|
||||
$dividerRow = \trim($dividerRow);
|
||||
$dividerRow = \trim($dividerRow, '|');
|
||||
|
||||
$dividerCells = \explode('|', $dividerRow);
|
||||
|
||||
/** @var array<int, _Alignment|null> */
|
||||
$alignments = [];
|
||||
|
||||
foreach ($dividerCells as $dividerCell) {
|
||||
$dividerCell = \trim($dividerCell);
|
||||
|
||||
if ($dividerCell === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var _Alignment|null */
|
||||
$alignment = null;
|
||||
|
||||
if (\substr($dividerCell, 0, 1) === ':') {
|
||||
$alignment = 'left';
|
||||
}
|
||||
|
||||
if (\substr($dividerCell, - 1) === ':') {
|
||||
$alignment = $alignment === 'left' ? 'center' : 'right';
|
||||
}
|
||||
|
||||
$alignments []= $alignment;
|
||||
}
|
||||
|
||||
return $alignments;
|
||||
}
|
||||
|
||||
/** @return bool */
|
||||
public function acquiredPrevious()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @return array<int, Inline[]> */
|
||||
public function headerRow(State $State)
|
||||
{
|
||||
return \array_map(
|
||||
/**
|
||||
* @param string $cell
|
||||
* @return Inline[]
|
||||
*/
|
||||
function ($cell) use ($State) {
|
||||
return Parsedown::inlines($cell, $State);
|
||||
},
|
||||
$this->headerCells
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<int, Inline[]>[] */
|
||||
public function rows(State $State)
|
||||
{
|
||||
return \array_map(
|
||||
/**
|
||||
* @param array<int, string> $cells
|
||||
* @return array<int, Inline[]>
|
||||
*/
|
||||
function ($cells) use ($State) {
|
||||
return \array_map(
|
||||
/**
|
||||
* @param string $cell
|
||||
* @return Inline[]
|
||||
*/
|
||||
function ($cell) use ($State) {
|
||||
return Parsedown::inlines($cell, $State);
|
||||
},
|
||||
$cells
|
||||
);
|
||||
},
|
||||
$this->rows
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<int, _Alignment|null> */
|
||||
public function alignments()
|
||||
{
|
||||
return $this->alignments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element */
|
||||
function (State $State) {
|
||||
return new Element('table', [], [
|
||||
new Element('thead', [], [new Element('tr', [], \array_map(
|
||||
/**
|
||||
* @param Inline[] $Cell
|
||||
* @param _Alignment|null $alignment
|
||||
* @return Element
|
||||
*/
|
||||
function ($Cell, $alignment) use ($State) {
|
||||
return new Element(
|
||||
'th',
|
||||
isset($alignment) ? ['style' => "text-align: $alignment;"] : [],
|
||||
$State->applyTo(Parsedown::stateRenderablesFrom($Cell))
|
||||
);
|
||||
},
|
||||
$this->headerRow($State),
|
||||
$this->alignments()
|
||||
))]),
|
||||
new Element('tbody', [], \array_map(
|
||||
/**
|
||||
* @param Inline[][] $Cells
|
||||
* @return Element
|
||||
*/
|
||||
function ($Cells) use ($State) {
|
||||
return new Element('tr', [], \array_map(
|
||||
/**
|
||||
* @param Inline[] $Cell
|
||||
* @param _Alignment|null $alignment
|
||||
* @return Element
|
||||
*/
|
||||
function ($Cell, $alignment) use ($State) {
|
||||
return new Element(
|
||||
'td',
|
||||
isset($alignment) ? ['style' => "text-align: $alignment;"] : [],
|
||||
$State->applyTo(Parsedown::stateRenderablesFrom($Cell))
|
||||
);
|
||||
},
|
||||
$Cells,
|
||||
\array_slice($this->alignments(), 0, \count($Cells))
|
||||
));
|
||||
},
|
||||
$this->rows($State)
|
||||
))
|
||||
]);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components;
|
||||
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
interface ContinuableBlock extends Block
|
||||
{
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public function advance(Context $Context, State $State);
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components;
|
||||
|
||||
use Erusev\Parsedown\Component;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
interface Inline extends Component
|
||||
{
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State);
|
||||
|
||||
/**
|
||||
* Number of characters consumed by the build action.
|
||||
* @return int
|
||||
* */
|
||||
public function width();
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext();
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Code implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param int $width
|
||||
*/
|
||||
private function __construct($text, $width)
|
||||
{
|
||||
$this->text = $text;
|
||||
$this->width = $width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
$marker = \substr($Excerpt->text(), 0, 1);
|
||||
|
||||
if ($marker !== '`') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (\preg_match(
|
||||
'/^(['.$marker.']++)(.*?)(?<!['.$marker.'])\1(?!'.$marker.')/s',
|
||||
$Excerpt->text(),
|
||||
$matches
|
||||
)) {
|
||||
$text = \str_replace("\n", ' ', $matches[2]);
|
||||
|
||||
$firstChar = \substr($text, 0, 1);
|
||||
$lastChar = \substr($text, -1);
|
||||
|
||||
if ($firstChar === ' ' && $lastChar === ' ') {
|
||||
$text = \substr(\substr($text, 1), 0, -1);
|
||||
}
|
||||
|
||||
return new self($text, \strlen($matches[0]));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function text()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Element
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Element('code', [], [new Text($this->text())]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->text());
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Email implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/** @var string */
|
||||
private $url;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param string $url
|
||||
* @param int $width
|
||||
*/
|
||||
private function __construct($text, $url, $width)
|
||||
{
|
||||
$this->text = $text;
|
||||
$this->url = $url;
|
||||
$this->width = $width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
$hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?';
|
||||
|
||||
$commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@'
|
||||
. $hostnameLabel . '(?:\.' . $hostnameLabel . ')*';
|
||||
|
||||
if (\preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt->text(), $matches)) {
|
||||
$url = $matches[1];
|
||||
|
||||
if (! isset($matches[2])) {
|
||||
$url = "mailto:$url";
|
||||
}
|
||||
|
||||
return new self($matches[1], $url, \strlen($matches[0]));
|
||||
}
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function text()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function url()
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Element
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Element('a', ['href' => $this->url()], [new Text($this->text())]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->text());
|
||||
}
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Emphasis implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/** @var 'em'|'strong' */
|
||||
private $type;
|
||||
|
||||
/** @var array{*: string, _: string} */
|
||||
private static $STRONG_REGEX = [
|
||||
'*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s',
|
||||
'_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us',
|
||||
];
|
||||
|
||||
/** @var array{*: string, _: string} */
|
||||
private static $EM_REGEX = [
|
||||
'*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
|
||||
'_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param 'em'|'strong' $type
|
||||
* @param int $width
|
||||
*/
|
||||
private function __construct($text, $type, $width)
|
||||
{
|
||||
$this->text = $text;
|
||||
$this->type = $type;
|
||||
$this->width = $width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
$marker = \substr($Excerpt->text(), 0, 1);
|
||||
|
||||
if ($marker !== '*' && $marker !== '_') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (\preg_match(self::$STRONG_REGEX[$marker], $Excerpt->text(), $matches)) {
|
||||
$emphasis = 'strong';
|
||||
} elseif (\preg_match(self::$EM_REGEX[$marker], $Excerpt->text(), $matches)) {
|
||||
$emphasis = 'em';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self($matches[1], $emphasis, \strlen($matches[0]));
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function text()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element */
|
||||
function (State $State) {
|
||||
return new Element(
|
||||
$this->type,
|
||||
[],
|
||||
$State->applyTo(Parsedown::line($this->text(), $State))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->text());
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class EscapeSequence implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
const SPECIALS = '!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~';
|
||||
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
*/
|
||||
private function __construct($text)
|
||||
{
|
||||
$this->text = $text;
|
||||
$this->width = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
$char = \substr($Excerpt->text(), 1, 1);
|
||||
|
||||
if ($char !== '' && \strpbrk($char, self::SPECIALS) !== false) {
|
||||
return new self($char);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function char()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Text($this->char());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->char());
|
||||
}
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\BacktrackingInline;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class HardBreak implements BacktrackingInline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var int */
|
||||
private $position;
|
||||
|
||||
/**
|
||||
* @param int $width
|
||||
* @param int $position
|
||||
*/
|
||||
private function __construct($width, $position)
|
||||
{
|
||||
$this->width = $width;
|
||||
$this->position = $position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
$marker = \substr($Excerpt->text(), 0, 1);
|
||||
|
||||
if ($marker !== "\n") {
|
||||
return null;
|
||||
}
|
||||
|
||||
$context = $Excerpt->context();
|
||||
$offset = $Excerpt->offset();
|
||||
|
||||
if (\substr($context, $offset -1, 1) === '\\') {
|
||||
$contentLen = $offset -1;
|
||||
|
||||
return new self($offset - $contentLen, $contentLen);
|
||||
}
|
||||
|
||||
if (\substr($context, $offset -2, 2) === ' ') {
|
||||
$trimTrailingWhitespace = \rtrim(\substr($context, 0, $offset));
|
||||
$contentLen = \strlen($trimTrailingWhitespace);
|
||||
|
||||
return new self($offset - $contentLen, $contentLen);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an integer to declare that the inline should be treated as if it
|
||||
* started from that position in the excerpt given to static::build.
|
||||
* Return null to use the excerpt offset value.
|
||||
* @return int|null
|
||||
* */
|
||||
public function modifyStartPositionTo()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Element
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return Element::selfClosing('br', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text("\n");
|
||||
}
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Configurables\SafeMode;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Html\Sanitisation\UrlSanitiser;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
/** @psalm-type _Metadata=array{href: string, title?: string} */
|
||||
final class Image implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var Link */
|
||||
private $Link;
|
||||
|
||||
/**
|
||||
* @param Link $Link
|
||||
*/
|
||||
private function __construct(Link $Link)
|
||||
{
|
||||
$this->Link = $Link;
|
||||
$this->width = $Link->width() + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
if (\substr($Excerpt->text(), 0, 1) !== '!') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$Excerpt = $Excerpt->addingToOffset(1);
|
||||
|
||||
$Link = Link::build($Excerpt, $State);
|
||||
|
||||
if (! isset($Link)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self($Link);
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function label()
|
||||
{
|
||||
return $this->Link->label();
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function url()
|
||||
{
|
||||
return $this->Link->url();
|
||||
}
|
||||
|
||||
/** @return string|null */
|
||||
public function title()
|
||||
{
|
||||
return $this->Link->title();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element|Text>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element|Text */
|
||||
function (State $State) {
|
||||
$attributes = [
|
||||
'src' => $this->url(),
|
||||
'alt' => \array_reduce(
|
||||
Parsedown::inlines($this->label(), $State),
|
||||
/**
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
function ($text, Inline $Inline) {
|
||||
return (
|
||||
$text
|
||||
. $Inline->bestPlaintext()->getStringBacking()
|
||||
);
|
||||
},
|
||||
''
|
||||
),
|
||||
];
|
||||
|
||||
$title = $this->title();
|
||||
|
||||
if (isset($title)) {
|
||||
$attributes['title'] = $title;
|
||||
}
|
||||
|
||||
if ($State->get(SafeMode::class)->isEnabled()) {
|
||||
$attributes['src'] = UrlSanitiser::filter($attributes['src']);
|
||||
}
|
||||
|
||||
return Element::selfClosing('img', $attributes);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->label());
|
||||
}
|
||||
}
|
@ -1,155 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Configurables\DefinitionBook;
|
||||
use Erusev\Parsedown\Configurables\InlineTypes;
|
||||
use Erusev\Parsedown\Configurables\SafeMode;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Html\Sanitisation\UrlSanitiser;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
/** @psalm-type _Metadata=array{href: string, title?: string} */
|
||||
final class Link implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var string */
|
||||
private $label;
|
||||
|
||||
/** @var string */
|
||||
private $url;
|
||||
|
||||
/** @var string|null */
|
||||
private $title;
|
||||
|
||||
/**
|
||||
* @param string $label
|
||||
* @param string $url
|
||||
* @param string|null $title
|
||||
* @param int $width
|
||||
*/
|
||||
private function __construct($label, $url, $title, $width)
|
||||
{
|
||||
$this->label = $label;
|
||||
$this->url = $url;
|
||||
$this->title = $title;
|
||||
$this->width = $width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
$remainder = $Excerpt->text();
|
||||
|
||||
if (! \preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$label = $matches[1];
|
||||
|
||||
$width = \strlen($matches[0]);
|
||||
|
||||
$remainder = \substr($remainder, $width);
|
||||
|
||||
if (\preg_match('/^[(]\s*+(?:((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+)?[)]/', $remainder, $matches)) {
|
||||
$url = isset($matches[1]) ? $matches[1] : '';
|
||||
$title = isset($matches[2]) ? \substr($matches[2], 1, - 1) : null;
|
||||
|
||||
$width += \strlen($matches[0]);
|
||||
|
||||
return new self($label, $url, $title, $width);
|
||||
} else {
|
||||
if (\preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) {
|
||||
$definition = \strlen($matches[1]) ? $matches[1] : $label;
|
||||
$definition = \strtolower($definition);
|
||||
|
||||
$width += \strlen($matches[0]);
|
||||
} else {
|
||||
$definition = \strtolower($label);
|
||||
}
|
||||
|
||||
$definition = \preg_replace('/\s++/', ' ', \trim($definition));
|
||||
|
||||
$data = $State->get(DefinitionBook::class)->lookup($definition);
|
||||
|
||||
if (! isset($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$url = $data['url'];
|
||||
$title = isset($data['title']) ? $data['title'] : null;
|
||||
|
||||
return new self($label, $url, $title, $width);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function label()
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function url()
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/** @return string|null */
|
||||
public function title()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element|Text>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element|Text */
|
||||
function (State $State) {
|
||||
$attributes = ['href' => $this->url()];
|
||||
|
||||
$title = $this->title();
|
||||
|
||||
if (isset($title)) {
|
||||
$attributes['title'] = $title;
|
||||
}
|
||||
|
||||
if ($State->get(SafeMode::class)->isEnabled()) {
|
||||
$attributes['href'] = UrlSanitiser::filter($attributes['href']);
|
||||
}
|
||||
|
||||
$State = $State->setting(
|
||||
$State->get(InlineTypes::class)->removing([Url::class])
|
||||
);
|
||||
|
||||
return new Element(
|
||||
'a',
|
||||
$attributes,
|
||||
$State->applyTo(Parsedown::line($this->label(), $State))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->label());
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Configurables\SafeMode;
|
||||
use Erusev\Parsedown\Html\Renderables\RawHtml;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Markup implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
const HTML_ATT_REGEX = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+';
|
||||
|
||||
/** @var string */
|
||||
private $html;
|
||||
|
||||
/**
|
||||
* @param string $html
|
||||
*/
|
||||
private function __construct($html)
|
||||
{
|
||||
$this->html = $html;
|
||||
$this->width = \strlen($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
$secondChar = \substr($Excerpt->text(), 1, 1);
|
||||
|
||||
if ($secondChar === '/' && \preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt->text(), $matches)) {
|
||||
return new self($matches[0]);
|
||||
}
|
||||
|
||||
if ($secondChar === '!' && \preg_match('/^<!---?[^>-](?:-?+[^-])*-->/s', $Excerpt->text(), $matches)) {
|
||||
return new self($matches[0]);
|
||||
}
|
||||
|
||||
if ($secondChar !== ' ' && \preg_match('/^<\w[\w-]*+(?:[ ]*+'.self::HTML_ATT_REGEX.')*+[ ]*+\/?>/s', $Excerpt->text(), $matches)) {
|
||||
return new self($matches[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function html()
|
||||
{
|
||||
return $this->html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Text|RawHtml>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Text|RawHtml */
|
||||
function (State $State) {
|
||||
if ($State->get(SafeMode::class)->isEnabled()) {
|
||||
return new Text($this->html());
|
||||
} else {
|
||||
return new RawHtml($this->html());
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->html());
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class PlainText implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
*/
|
||||
private function __construct($text)
|
||||
{
|
||||
$this->text = $text;
|
||||
$this->width = \strlen($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
return new self($Excerpt->text());
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function text()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Text($this->text());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->text());
|
||||
}
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\BacktrackingInline;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Configurables\Breaks;
|
||||
use Erusev\Parsedown\Html\Renderables\Container;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class SoftBreak implements BacktrackingInline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var int */
|
||||
private $position;
|
||||
|
||||
/**
|
||||
* @param int $width
|
||||
* @param int $position
|
||||
*/
|
||||
private function __construct($width, $position)
|
||||
{
|
||||
$this->width = $width;
|
||||
$this->position = $position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
$marker = \substr($Excerpt->text(), 0, 1);
|
||||
|
||||
if ($marker !== "\n") {
|
||||
return null;
|
||||
}
|
||||
|
||||
$context = $Excerpt->context();
|
||||
$offset = $Excerpt->offset();
|
||||
|
||||
$trimTrailingWhitespaceBefore = \rtrim(\substr($context, 0, $offset), ' ');
|
||||
$trimLeadingWhitespaceAfter = \ltrim(\substr($context, $offset + 1), ' ');
|
||||
$contentLenBefore = \strlen($trimTrailingWhitespaceBefore);
|
||||
$contentLenAfter = \strlen($trimLeadingWhitespaceAfter);
|
||||
|
||||
$originalLen = \strlen($context);
|
||||
$afterWidth = $originalLen - $offset - $contentLenAfter;
|
||||
|
||||
return new self($offset + $afterWidth - $contentLenBefore, $contentLenBefore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an integer to declare that the inline should be treated as if it
|
||||
* started from that position in the excerpt given to static::build.
|
||||
* Return null to use the excerpt offset value.
|
||||
* @return int|null
|
||||
* */
|
||||
public function modifyStartPositionTo()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Text|Container>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Text|Container */
|
||||
function (State $State) {
|
||||
if ($State->get(Breaks::class)->isEnabled()) {
|
||||
return new Container([
|
||||
Element::selfClosing('br', []),
|
||||
new Text("\n")
|
||||
]);
|
||||
}
|
||||
|
||||
return new Text("\n");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text("\n");
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\RawHtml;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class SpecialCharacter implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var string */
|
||||
private $charCodeHtml;
|
||||
|
||||
/**
|
||||
* @param string $charCodeHtml
|
||||
*/
|
||||
private function __construct($charCodeHtml)
|
||||
{
|
||||
$this->charCodeHtml = $charCodeHtml;
|
||||
$this->width = \strlen($charCodeHtml) + 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
if (\preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt->text(), $matches)) {
|
||||
return new self($matches[1]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function charCode()
|
||||
{
|
||||
return $this->charCodeHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RawHtml
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new RawHtml(
|
||||
'&' . (new Text($this->charCode()))->getHtml() . ';'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text('&'.$this->charCode().';');
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Strikethrough implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param int $width
|
||||
*/
|
||||
private function __construct($text, $width)
|
||||
{
|
||||
$this->text = $text;
|
||||
$this->width = $width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
$text = $Excerpt->text();
|
||||
|
||||
if (\preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $text, $matches)) {
|
||||
return new self($matches[1], \strlen($matches[0]));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function text()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element */
|
||||
function (State $State) {
|
||||
return new Element(
|
||||
'del',
|
||||
[],
|
||||
$State->applyTo(Parsedown::line($this->text(), $State))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->text());
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\BacktrackingInline;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Url implements BacktrackingInline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var string */
|
||||
private $url;
|
||||
|
||||
/** @var int */
|
||||
private $position;
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param int $position
|
||||
*/
|
||||
private function __construct($url, $position)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->width = \strlen($url);
|
||||
$this->position = $position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
if (\preg_match(
|
||||
'/(?<=^|\s|[*_~(])https?+:[\/]{2}[^\s<]+\b\/*+/ui',
|
||||
$Excerpt->context(),
|
||||
$matches,
|
||||
\PREG_OFFSET_CAPTURE
|
||||
)) {
|
||||
return new self($matches[0][0], \intval($matches[0][1]));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an integer to declare that the inline should be treated as if it
|
||||
* started from that position in the excerpt given to static::build.
|
||||
* Return null to use the excerpt offset value.
|
||||
* @return int|null
|
||||
* */
|
||||
public function modifyStartPositionTo()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function url()
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Element
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Element('a', ['href' => $this->url()], [new Text($this->url())]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->url());
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class UrlTag implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var string */
|
||||
private $url;
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param int $width
|
||||
*/
|
||||
private function __construct($url, $width)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->width = $width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
if (\preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt->text(), $matches)) {
|
||||
return new self($matches[1], \strlen($matches[0]));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function url()
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Element
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Element('a', ['href' => $this->url()], [new Text($this->url())]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->url);
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
trait WidthTrait
|
||||
{
|
||||
/** @var int */
|
||||
private $width;
|
||||
|
||||
/** @return int */
|
||||
public function width()
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components;
|
||||
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
interface StateUpdatingBlock extends Block
|
||||
{
|
||||
/** @return State */
|
||||
public function latestState();
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown;
|
||||
|
||||
interface Configurable
|
||||
{
|
||||
/** @return static */
|
||||
public static function initial();
|
||||
}
|
@ -1,194 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Configurables;
|
||||
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\Blocks\BlockQuote;
|
||||
use Erusev\Parsedown\Components\Blocks\FencedCode;
|
||||
use Erusev\Parsedown\Components\Blocks\Header;
|
||||
use Erusev\Parsedown\Components\Blocks\IndentedCode;
|
||||
use Erusev\Parsedown\Components\Blocks\Markup as BlockMarkup;
|
||||
use Erusev\Parsedown\Components\Blocks\Reference;
|
||||
use Erusev\Parsedown\Components\Blocks\Rule;
|
||||
use Erusev\Parsedown\Components\Blocks\SetextHeader;
|
||||
use Erusev\Parsedown\Components\Blocks\Table;
|
||||
use Erusev\Parsedown\Components\Blocks\TList;
|
||||
use Erusev\Parsedown\Configurable;
|
||||
|
||||
final class BlockTypes implements Configurable
|
||||
{
|
||||
/** @var array<array-key, array<int, class-string<Block>>> */
|
||||
private static $defaultBlockTypes = [
|
||||
'#' => [Header::class],
|
||||
'*' => [Rule::class, TList::class],
|
||||
'+' => [TList::class],
|
||||
'-' => [SetextHeader::class, Table::class, Rule::class, TList::class],
|
||||
'0' => [TList::class],
|
||||
'1' => [TList::class],
|
||||
'2' => [TList::class],
|
||||
'3' => [TList::class],
|
||||
'4' => [TList::class],
|
||||
'5' => [TList::class],
|
||||
'6' => [TList::class],
|
||||
'7' => [TList::class],
|
||||
'8' => [TList::class],
|
||||
'9' => [TList::class],
|
||||
':' => [Table::class],
|
||||
'<' => [BlockMarkup::class],
|
||||
'=' => [SetextHeader::class],
|
||||
'>' => [BlockQuote::class],
|
||||
'[' => [Reference::class],
|
||||
'_' => [Rule::class],
|
||||
'`' => [FencedCode::class],
|
||||
'|' => [Table::class],
|
||||
'~' => [FencedCode::class],
|
||||
];
|
||||
|
||||
/** @var array<int, class-string<Block>> */
|
||||
private static $defaultUnmarkedBlockTypes = [
|
||||
IndentedCode::class,
|
||||
];
|
||||
|
||||
/** @var array<array-key, array<int, class-string<Block>>> */
|
||||
private $blockTypes;
|
||||
|
||||
/** @var array<int, class-string<Block>> */
|
||||
private $unmarkedBlockTypes;
|
||||
|
||||
/**
|
||||
* @param array<array-key, array<int, class-string<Block>>> $blockTypes
|
||||
* @param array<int, class-string<Block>> $unmarkedBlockTypes
|
||||
*/
|
||||
public function __construct(array $blockTypes, array $unmarkedBlockTypes)
|
||||
{
|
||||
$this->blockTypes = $blockTypes;
|
||||
$this->unmarkedBlockTypes = $unmarkedBlockTypes;
|
||||
}
|
||||
|
||||
/** @return self */
|
||||
public static function initial()
|
||||
{
|
||||
return new self(
|
||||
self::$defaultBlockTypes,
|
||||
self::$defaultUnmarkedBlockTypes
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $marker
|
||||
* @param array<int, class-string<Block>> $newBlockTypes
|
||||
* @return self
|
||||
*/
|
||||
public function settingMarked($marker, array $newBlockTypes)
|
||||
{
|
||||
$blockTypes = $this->blockTypes;
|
||||
$blockTypes[$marker] = $newBlockTypes;
|
||||
|
||||
return new self($blockTypes, $this->unmarkedBlockTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $marker
|
||||
* @param array<int, class-string<Block>> $newBlockTypes
|
||||
* @return self
|
||||
*/
|
||||
public function addingMarkedHighPrecedence($marker, array $newBlockTypes)
|
||||
{
|
||||
return $this->settingMarked(
|
||||
$marker,
|
||||
\array_merge(
|
||||
$newBlockTypes,
|
||||
isset($this->blockTypes[$marker]) ? $this->blockTypes[$marker] : []
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $marker
|
||||
* @param array<int, class-string<Block>> $newBlockTypes
|
||||
* @return self
|
||||
*/
|
||||
public function addingMarkedLowPrecedence($marker, array $newBlockTypes)
|
||||
{
|
||||
return $this->settingMarked(
|
||||
$marker,
|
||||
\array_merge(
|
||||
isset($this->blockTypes[$marker]) ? $this->blockTypes[$marker] : [],
|
||||
$newBlockTypes
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, class-string<Block>> $newUnmarkedBlockTypes
|
||||
* @return self
|
||||
*/
|
||||
public function settingUnmarked(array $newUnmarkedBlockTypes)
|
||||
{
|
||||
return new self($this->blockTypes, $newUnmarkedBlockTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, class-string<Block>> $newBlockTypes
|
||||
* @return self
|
||||
*/
|
||||
public function addingUnmarkedHighPrecedence(array $newBlockTypes)
|
||||
{
|
||||
return $this->settingUnmarked(
|
||||
\array_merge($newBlockTypes, $this->unmarkedBlockTypes)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, class-string<Block>> $newBlockTypes
|
||||
* @return self
|
||||
*/
|
||||
public function addingUnmarkedLowPrecedence(array $newBlockTypes)
|
||||
{
|
||||
return $this->settingUnmarked(
|
||||
\array_merge($this->unmarkedBlockTypes, $newBlockTypes)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, class-string<Block>> $removeBlockTypes
|
||||
* @return self
|
||||
*/
|
||||
public function removing(array $removeBlockTypes)
|
||||
{
|
||||
return new self(
|
||||
\array_map(
|
||||
/**
|
||||
* @param array<int, class-string<Block>> $blockTypes
|
||||
* @return array<int, class-string<Block>>
|
||||
*/
|
||||
function ($blockTypes) use ($removeBlockTypes) {
|
||||
return \array_diff($blockTypes, $removeBlockTypes);
|
||||
},
|
||||
$this->blockTypes
|
||||
),
|
||||
\array_diff($this->unmarkedBlockTypes, $removeBlockTypes)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $marker
|
||||
* @return array<int, class-string<Block>>
|
||||
*/
|
||||
public function markedBy($marker)
|
||||
{
|
||||
if (isset($this->blockTypes[$marker])) {
|
||||
return $this->blockTypes[$marker];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, class-string<Block>>
|
||||
*/
|
||||
public function unmarked()
|
||||
{
|
||||
return $this->unmarkedBlockTypes;
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Configurables;
|
||||
|
||||
trait BooleanConfigurable
|
||||
{
|
||||
/** @var bool */
|
||||
private $enabled = false;
|
||||
|
||||
/**
|
||||
* @param bool $enabled
|
||||
*/
|
||||
public function __construct($enabled)
|
||||
{
|
||||
$this->enabled = $enabled;
|
||||
}
|
||||
|
||||
/** @return bool */
|
||||
public function isEnabled()
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
/** @return static */
|
||||
public static function enabled()
|
||||
{
|
||||
return new self(true);
|
||||
}
|
||||
|
||||
/** @return static */
|
||||
public static function initial()
|
||||
{
|
||||
return new self(false);
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Configurables;
|
||||
|
||||
use Erusev\Parsedown\Configurable;
|
||||
|
||||
final class Breaks implements Configurable
|
||||
{
|
||||
use BooleanConfigurable;
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Configurables;
|
||||
|
||||
use Erusev\Parsedown\Configurable;
|
||||
|
||||
/**
|
||||
* @psalm-type _Data=array{url: string, title: string|null}
|
||||
*/
|
||||
final class DefinitionBook implements Configurable
|
||||
{
|
||||
/** @var array<string, _Data> */
|
||||
private $book;
|
||||
|
||||
/**
|
||||
* @param array<string, _Data> $book
|
||||
*/
|
||||
public function __construct(array $book = [])
|
||||
{
|
||||
$this->book = $book;
|
||||
}
|
||||
|
||||
/** @return self */
|
||||
public static function initial()
|
||||
{
|
||||
return new self;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @param _Data $data
|
||||
* @return self
|
||||
*/
|
||||
public function setting($id, array $data)
|
||||
{
|
||||
$book = $this->book;
|
||||
$book[$id] = $data;
|
||||
|
||||
return new self($book);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @return _Data|null
|
||||
*/
|
||||
public function lookup($id)
|
||||
{
|
||||
if (isset($this->book[$id])) {
|
||||
return $this->book[$id];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,140 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Configurables;
|
||||
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Components\Inlines\Code;
|
||||
use Erusev\Parsedown\Components\Inlines\Email;
|
||||
use Erusev\Parsedown\Components\Inlines\Emphasis;
|
||||
use Erusev\Parsedown\Components\Inlines\EscapeSequence;
|
||||
use Erusev\Parsedown\Components\Inlines\HardBreak;
|
||||
use Erusev\Parsedown\Components\Inlines\Image;
|
||||
use Erusev\Parsedown\Components\Inlines\Link;
|
||||
use Erusev\Parsedown\Components\Inlines\Markup as InlineMarkup;
|
||||
use Erusev\Parsedown\Components\Inlines\SoftBreak;
|
||||
use Erusev\Parsedown\Components\Inlines\SpecialCharacter;
|
||||
use Erusev\Parsedown\Components\Inlines\Strikethrough;
|
||||
use Erusev\Parsedown\Components\Inlines\Url;
|
||||
use Erusev\Parsedown\Components\Inlines\UrlTag;
|
||||
use Erusev\Parsedown\Configurable;
|
||||
|
||||
final class InlineTypes implements Configurable
|
||||
{
|
||||
/** @var array<array-key, array<int, class-string<Inline>>> */
|
||||
private static $defaultInlineTypes = [
|
||||
'!' => [Image::class],
|
||||
'*' => [Emphasis::class],
|
||||
'_' => [Emphasis::class],
|
||||
'&' => [SpecialCharacter::class],
|
||||
'[' => [Link::class],
|
||||
':' => [Url::class],
|
||||
'<' => [UrlTag::class, Email::class, InlineMarkup::class],
|
||||
'`' => [Code::class],
|
||||
'~' => [Strikethrough::class],
|
||||
'\\' => [EscapeSequence::class],
|
||||
"\n" => [HardBreak::class, SoftBreak::class],
|
||||
];
|
||||
|
||||
/** @var array<array-key, array<int, class-string<Inline>>> */
|
||||
private $inlineTypes;
|
||||
|
||||
/** @var string */
|
||||
private $inlineMarkers;
|
||||
|
||||
/**
|
||||
* @param array<array-key, array<int, class-string<Inline>>> $inlineTypes
|
||||
*/
|
||||
public function __construct(array $inlineTypes)
|
||||
{
|
||||
$this->inlineTypes = $inlineTypes;
|
||||
$this->inlineMarkers = \implode('', \array_keys($inlineTypes));
|
||||
}
|
||||
|
||||
/** @return self */
|
||||
public static function initial()
|
||||
{
|
||||
return new self(self::$defaultInlineTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $marker
|
||||
* @param array<int, class-string<Inline>> $newInlineTypes
|
||||
* @return self
|
||||
*/
|
||||
public function setting($marker, array $newInlineTypes)
|
||||
{
|
||||
$inlineTypes = $this->inlineTypes;
|
||||
$inlineTypes[$marker] = $newInlineTypes;
|
||||
|
||||
return new self($inlineTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $marker
|
||||
* @param array<int, class-string<Inline>> $newInlineTypes
|
||||
* @return self
|
||||
*/
|
||||
public function addingHighPrecedence($marker, array $newInlineTypes)
|
||||
{
|
||||
return $this->setting(
|
||||
$marker,
|
||||
\array_merge(
|
||||
$newInlineTypes,
|
||||
isset($this->inlineTypes[$marker]) ? $this->inlineTypes[$marker] : []
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $marker
|
||||
* @param array<int, class-string<Inline>> $newInlineTypes
|
||||
* @return self
|
||||
*/
|
||||
public function addingLowPrecedence($marker, array $newInlineTypes)
|
||||
{
|
||||
return $this->setting(
|
||||
$marker,
|
||||
\array_merge(
|
||||
isset($this->inlineTypes[$marker]) ? $this->inlineTypes[$marker] : [],
|
||||
$newInlineTypes
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, class-string<Inline>> $removeInlineTypes
|
||||
* @return self
|
||||
*/
|
||||
public function removing(array $removeInlineTypes)
|
||||
{
|
||||
return new self(\array_map(
|
||||
/**
|
||||
* @param array<int, class-string<Inline>> $inlineTypes
|
||||
* @return array<int, class-string<Inline>>
|
||||
*/
|
||||
function ($inlineTypes) use ($removeInlineTypes) {
|
||||
return \array_diff($inlineTypes, $removeInlineTypes);
|
||||
},
|
||||
$this->inlineTypes
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $marker
|
||||
* @return array<int, class-string<Inline>>
|
||||
*/
|
||||
public function markedBy($marker)
|
||||
{
|
||||
if (isset($this->inlineTypes[$marker])) {
|
||||
return $this->inlineTypes[$marker];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function markers()
|
||||
{
|
||||
return $this->inlineMarkers;
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Configurables;
|
||||
|
||||
use Erusev\Parsedown\Configurable;
|
||||
|
||||
final class RecursionLimiter implements Configurable
|
||||
{
|
||||
/** @var int */
|
||||
private $maxDepth;
|
||||
|
||||
/** @var int */
|
||||
private $currentDepth;
|
||||
|
||||
/**
|
||||
* @param int $maxDepth
|
||||
* @param int $currentDepth
|
||||
*/
|
||||
private function __construct($maxDepth, $currentDepth)
|
||||
{
|
||||
$this->maxDepth = $maxDepth;
|
||||
$this->currentDepth = $currentDepth;
|
||||
}
|
||||
|
||||
/** @return self */
|
||||
public static function initial()
|
||||
{
|
||||
return self::maxDepth(256);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $maxDepth
|
||||
* @return self
|
||||
*/
|
||||
public static function maxDepth($maxDepth)
|
||||
{
|
||||
return new self($maxDepth, 0);
|
||||
}
|
||||
|
||||
/** @return self */
|
||||
public function increment()
|
||||
{
|
||||
return new self($this->maxDepth, $this->currentDepth + 1);
|
||||
}
|
||||
|
||||
/** @return bool */
|
||||
public function isDepthExceeded()
|
||||
{
|
||||
return ($this->maxDepth < $this->currentDepth);
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Configurables;
|
||||
|
||||
use Erusev\Parsedown\Configurable;
|
||||
|
||||
final class SafeMode implements Configurable
|
||||
{
|
||||
use BooleanConfigurable;
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Configurables;
|
||||
|
||||
use Erusev\Parsedown\Configurable;
|
||||
|
||||
final class StrictMode implements Configurable
|
||||
{
|
||||
use BooleanConfigurable;
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Html;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
|
||||
interface Renderable extends StateRenderable
|
||||
{
|
||||
/** @return string */
|
||||
public function getHtml();
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Html\Renderables;
|
||||
|
||||
use Erusev\Parsedown\Html\Renderable;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
trait CanonicalStateRenderable
|
||||
{
|
||||
/**
|
||||
* @return Renderable
|
||||
*/
|
||||
public function renderable(State $State)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Html\Renderables;
|
||||
|
||||
use Erusev\Parsedown\Html\Renderable;
|
||||
|
||||
final class Container implements Renderable
|
||||
{
|
||||
use CanonicalStateRenderable;
|
||||
|
||||
/** @var Renderable[] */
|
||||
private $Contents;
|
||||
|
||||
/**
|
||||
* @param Renderable[] $Contents
|
||||
*/
|
||||
public function __construct($Contents)
|
||||
{
|
||||
$this->Contents = $Contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Renderable[]
|
||||
*/
|
||||
public function contents()
|
||||
{
|
||||
return $this->Contents;
|
||||
}
|
||||
|
||||
|
||||
/** @return string */
|
||||
public function getHtml()
|
||||
{
|
||||
return \array_reduce(
|
||||
$this->Contents,
|
||||
/**
|
||||
* @param string $html
|
||||
* @param Renderable $Renderable
|
||||
* @return string
|
||||
*/
|
||||
function ($html, Renderable $Renderable) {
|
||||
return $html . $Renderable->getHtml();
|
||||
},
|
||||
''
|
||||
);
|
||||
}
|
||||
}
|
@ -1,195 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Html\Renderables;
|
||||
|
||||
use Erusev\Parsedown\Html\Renderable;
|
||||
use Erusev\Parsedown\Html\Sanitisation\CharacterFilter;
|
||||
use Erusev\Parsedown\Html\Sanitisation\Escaper;
|
||||
|
||||
final class Element implements Renderable
|
||||
{
|
||||
use CanonicalStateRenderable;
|
||||
|
||||
/** @var array<string, true> */
|
||||
public static $TEXT_LEVEL_ELEMENTS = [
|
||||
'a' => true,
|
||||
'b' => true,
|
||||
'i' => true,
|
||||
'q' => true,
|
||||
's' => true,
|
||||
'u' => true,
|
||||
|
||||
'br' => true,
|
||||
'em' => true,
|
||||
'rp' => true,
|
||||
'rt' => true,
|
||||
'tt' => true,
|
||||
'xm' => true,
|
||||
|
||||
'bdo' => true,
|
||||
'big' => true,
|
||||
'del' => true,
|
||||
'img' => true,
|
||||
'ins' => true,
|
||||
'kbd' => true,
|
||||
'sub' => true,
|
||||
'sup' => true,
|
||||
'var' => true,
|
||||
'wbr' => true,
|
||||
|
||||
'abbr' => true,
|
||||
'cite' => true,
|
||||
'code' => true,
|
||||
'font' => true,
|
||||
'mark' => true,
|
||||
'nobr' => true,
|
||||
'ruby' => true,
|
||||
'span' => true,
|
||||
'time' => true,
|
||||
|
||||
'blink' => true,
|
||||
'small' => true,
|
||||
|
||||
'nextid' => true,
|
||||
'spacer' => true,
|
||||
'strike' => true,
|
||||
'strong' => true,
|
||||
|
||||
'acronym' => true,
|
||||
'listing' => true,
|
||||
'marquee' => true,
|
||||
|
||||
'basefont' => true,
|
||||
];
|
||||
|
||||
/** @var string */
|
||||
private $name;
|
||||
|
||||
/** @var array<string, string>*/
|
||||
private $attributes;
|
||||
|
||||
/** @var Renderable[]|null */
|
||||
private $Contents;
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param array<string, string> $attributes
|
||||
* @param Renderable[]|null $Contents
|
||||
*/
|
||||
public function __construct($name, $attributes, $Contents)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->attributes = $attributes;
|
||||
$this->Contents = $Contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param array<string, string> $attributes
|
||||
* @return self
|
||||
*/
|
||||
public static function selfClosing($name, array $attributes)
|
||||
{
|
||||
return new self($name, $attributes, null);
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function name()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function attributes()
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Renderable[]|null
|
||||
*/
|
||||
public function contents()
|
||||
{
|
||||
return $this->Contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return self
|
||||
*/
|
||||
public function settingName($name)
|
||||
{
|
||||
return new self($name, $this->attributes, $this->Contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $attributes
|
||||
* @return self
|
||||
*/
|
||||
public function settingAttributes(array $attributes)
|
||||
{
|
||||
return new self($this->name, $attributes, $this->Contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Renderable[]|null $Contents
|
||||
* @return self
|
||||
*/
|
||||
public function settingContents($Contents)
|
||||
{
|
||||
return new self($this->name, $this->attributes, $Contents);
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function getHtml()
|
||||
{
|
||||
$elementName = CharacterFilter::htmlElementName($this->name);
|
||||
|
||||
$html = '<' . $elementName;
|
||||
|
||||
if (! empty($this->attributes)) {
|
||||
foreach ($this->attributes as $name => $value) {
|
||||
$html .= ' '
|
||||
. CharacterFilter::htmlAttributeName($name)
|
||||
. '="'
|
||||
. Escaper::htmlAttributeValue($value)
|
||||
. '"'
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->Contents !== null) {
|
||||
$html .= '>';
|
||||
|
||||
if (! empty($this->Contents)) {
|
||||
foreach ($this->Contents as $C) {
|
||||
if (
|
||||
$C instanceof Element
|
||||
&& ! \array_key_exists(\strtolower($C->name()), self::$TEXT_LEVEL_ELEMENTS)
|
||||
) {
|
||||
$html .= "\n";
|
||||
}
|
||||
|
||||
$html .= $C->getHtml();
|
||||
}
|
||||
|
||||
$Last = \end($this->Contents);
|
||||
|
||||
if (
|
||||
$Last instanceof Element
|
||||
&& ! \array_key_exists(\strtolower($Last->name()), self::$TEXT_LEVEL_ELEMENTS)
|
||||
) {
|
||||
$html .= "\n";
|
||||
}
|
||||
}
|
||||
|
||||
$html .= "</" . $elementName . ">";
|
||||
} else {
|
||||
$html .= ' />';
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Html\Renderables;
|
||||
|
||||
use Erusev\Parsedown\Html\Renderable;
|
||||
|
||||
final class Invisible implements Renderable
|
||||
{
|
||||
use CanonicalStateRenderable;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function getHtml()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Html\Renderables;
|
||||
|
||||
use Erusev\Parsedown\Html\Renderable;
|
||||
|
||||
final class RawHtml implements Renderable
|
||||
{
|
||||
use CanonicalStateRenderable;
|
||||
|
||||
/** @var string */
|
||||
private $html;
|
||||
|
||||
/**
|
||||
* @param string $html
|
||||
*/
|
||||
public function __construct($html = '')
|
||||
{
|
||||
$this->html = $html;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function getHtml()
|
||||
{
|
||||
return $this->html;
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Html\Renderables;
|
||||
|
||||
use Erusev\Parsedown\Html\Renderable;
|
||||
use Erusev\Parsedown\Html\Sanitisation\Escaper;
|
||||
|
||||
final class Text implements Renderable
|
||||
{
|
||||
use CanonicalStateRenderable;
|
||||
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
*/
|
||||
public function __construct($text = '')
|
||||
{
|
||||
$this->text = $text;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function getStringBacking()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function getHtml()
|
||||
{
|
||||
return Escaper::htmlElementValueEscapingDoubleQuotes($this->text);
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Html\Sanitisation;
|
||||
|
||||
final class CharacterFilter
|
||||
{
|
||||
/**
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
public static function htmlAttributeName($text)
|
||||
{
|
||||
/**
|
||||
* https://www.w3.org/TR/html/syntax.html#name
|
||||
*
|
||||
* Attribute names must consist of one or more characters other than
|
||||
* the space characters, U+0000 NULL, U+0022 QUOTATION MARK ("),
|
||||
* U+0027 APOSTROPHE ('), U+003E GREATER-THAN SIGN (>),
|
||||
* U+002F SOLIDUS (/), and U+003D EQUALS SIGN (=) characters,
|
||||
* the control characters, and any characters that are not defined by
|
||||
* Unicode.
|
||||
*/
|
||||
return \preg_replace(
|
||||
'/(?:[[:space:]\0"\'>\/=[:cntrl:]]|[^\pC\pL\pM\pN\pP\pS\pZ])++/iu',
|
||||
'',
|
||||
$text
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
public static function htmlElementName($text)
|
||||
{
|
||||
/**
|
||||
* https://www.w3.org/TR/html/syntax.html#tag-name
|
||||
*
|
||||
* HTML elements all have names that only use alphanumeric
|
||||
* ASCII characters.
|
||||
*/
|
||||
return \preg_replace('/[^[:alnum:]]/', '', $text);
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Html\Sanitisation;
|
||||
|
||||
final class Escaper
|
||||
{
|
||||
/**
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
public static function htmlAttributeValue($text)
|
||||
{
|
||||
return self::escape($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
public static function htmlElementValue($text)
|
||||
{
|
||||
return self::escape($text, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
public static function htmlElementValueEscapingDoubleQuotes($text)
|
||||
{
|
||||
return \htmlspecialchars($text, \ENT_COMPAT, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param bool $allowQuotes
|
||||
* @return string
|
||||
*/
|
||||
private static function escape($text, $allowQuotes = false)
|
||||
{
|
||||
return \htmlspecialchars(
|
||||
$text,
|
||||
$allowQuotes ? \ENT_NOQUOTES : \ENT_QUOTES,
|
||||
'UTF-8'
|
||||
);
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Html\Sanitisation;
|
||||
|
||||
final class UrlSanitiser
|
||||
{
|
||||
/** @var string[] */
|
||||
private static $COMMON_SCHEMES = [
|
||||
'http://',
|
||||
'https://',
|
||||
'ftp://',
|
||||
'ftps://',
|
||||
'mailto:',
|
||||
'tel:',
|
||||
'data:image/png;base64,',
|
||||
'data:image/gif;base64,',
|
||||
'data:image/jpeg;base64,',
|
||||
'irc:',
|
||||
'ircs:',
|
||||
'git:',
|
||||
'ssh:',
|
||||
'news:',
|
||||
'steam:',
|
||||
];
|
||||
|
||||
/**
|
||||
* Disable literal intepretation of unknown scheme in $url. Returns the
|
||||
* filtered version of $url.
|
||||
* @param string $url
|
||||
* @param string[]|null $permittedSchemes
|
||||
* @return string
|
||||
*/
|
||||
public static function filter($url, $permittedSchemes = null)
|
||||
{
|
||||
if (! isset($permittedSchemes)) {
|
||||
$permittedSchemes = self::$COMMON_SCHEMES;
|
||||
}
|
||||
|
||||
foreach ($permittedSchemes as $scheme) {
|
||||
if (self::striAtStart($url, $scheme)) {
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
return \str_replace(':', '%3A', $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* @param string $needle
|
||||
* @return bool
|
||||
*/
|
||||
private static function striAtStart($string, $needle)
|
||||
{
|
||||
$needleLen = \strlen($needle);
|
||||
|
||||
return \strtolower(\substr($string, 0, $needleLen)) === \strtolower($needle);
|
||||
}
|
||||
}
|
@ -1,286 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\AcquisitioningBlock;
|
||||
use Erusev\Parsedown\Components\BacktrackingInline;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\Blocks\Paragraph;
|
||||
use Erusev\Parsedown\Components\ContinuableBlock;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Components\Inlines\PlainText;
|
||||
use Erusev\Parsedown\Components\StateUpdatingBlock;
|
||||
use Erusev\Parsedown\Configurables\BlockTypes;
|
||||
use Erusev\Parsedown\Configurables\InlineTypes;
|
||||
use Erusev\Parsedown\Configurables\RecursionLimiter;
|
||||
use Erusev\Parsedown\Html\Renderable;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\Parsing\Line;
|
||||
use Erusev\Parsedown\Parsing\Lines;
|
||||
|
||||
final class Parsedown
|
||||
{
|
||||
const version = '2.0.0-dev';
|
||||
|
||||
/** @var State */
|
||||
private $State;
|
||||
|
||||
public function __construct(StateBearer $StateBearer = null)
|
||||
{
|
||||
$StateBearer = $StateBearer ?: new State;
|
||||
|
||||
$this->State = $StateBearer->state();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
public function text($text)
|
||||
{
|
||||
list($StateRenderables, $State) = self::lines(
|
||||
Lines::fromTextLines($text, 0),
|
||||
$this->State
|
||||
);
|
||||
|
||||
$Renderables = $State->applyTo($StateRenderables);
|
||||
|
||||
$html = self::render($Renderables);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: StateRenderable[], 1: State}
|
||||
*/
|
||||
public static function lines(Lines $Lines, State $State)
|
||||
{
|
||||
list($Blocks, $State) = self::blocks($Lines, $State);
|
||||
|
||||
return [self::stateRenderablesFrom($Blocks), $State];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Component[] $Components
|
||||
* @return StateRenderable[]
|
||||
*/
|
||||
public static function stateRenderablesFrom($Components)
|
||||
{
|
||||
return \array_map(
|
||||
/**
|
||||
* @param Component $Component
|
||||
* @return StateRenderable
|
||||
*/
|
||||
function ($Component) { return $Component->stateRenderable(); },
|
||||
$Components
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: Block[], 1: State}
|
||||
*/
|
||||
public static function blocks(Lines $Lines, State $State)
|
||||
{
|
||||
$RecursionLimiter = $State->get(RecursionLimiter::class)->increment();
|
||||
|
||||
if ($RecursionLimiter->isDepthExceeded()) {
|
||||
$State = $State->setting(new BlockTypes([], []));
|
||||
}
|
||||
|
||||
$State = $State->setting($RecursionLimiter);
|
||||
|
||||
/** @var Block[] */
|
||||
$Blocks = [];
|
||||
/** @var Block|null */
|
||||
$Block = null;
|
||||
/** @var Block|null */
|
||||
$CurrentBlock = null;
|
||||
|
||||
foreach ($Lines->contexts() as $Context) {
|
||||
$Line = $Context->line();
|
||||
|
||||
if (
|
||||
isset($CurrentBlock)
|
||||
&& $CurrentBlock instanceof ContinuableBlock
|
||||
&& ! $CurrentBlock instanceof Paragraph
|
||||
) {
|
||||
$Block = $CurrentBlock->advance($Context, $State);
|
||||
|
||||
if ($Block instanceof StateUpdatingBlock) {
|
||||
$State = $Block->latestState();
|
||||
}
|
||||
|
||||
if (isset($Block)) {
|
||||
$CurrentBlock = $Block;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$marker = \substr($Line->text(), 0, 1);
|
||||
|
||||
$potentialBlockTypes = \array_merge(
|
||||
$State->get(BlockTypes::class)->unmarked(),
|
||||
$State->get(BlockTypes::class)->markedBy($marker)
|
||||
);
|
||||
|
||||
foreach ($potentialBlockTypes as $blockType) {
|
||||
$Block = $blockType::build($Context, $State, $CurrentBlock);
|
||||
|
||||
if (isset($Block)) {
|
||||
if ($Block instanceof StateUpdatingBlock) {
|
||||
$State = $Block->latestState();
|
||||
}
|
||||
|
||||
if (isset($CurrentBlock)
|
||||
&& (
|
||||
! $Block instanceof AcquisitioningBlock
|
||||
|| ! $Block->acquiredPrevious()
|
||||
)
|
||||
) {
|
||||
$Blocks[] = $CurrentBlock;
|
||||
}
|
||||
|
||||
$CurrentBlock = $Block;
|
||||
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($CurrentBlock) && $CurrentBlock instanceof Paragraph) {
|
||||
$Block = $CurrentBlock->advance($Context, $State);
|
||||
}
|
||||
|
||||
if (isset($Block)) {
|
||||
$CurrentBlock = $Block;
|
||||
} else {
|
||||
if (isset($CurrentBlock)) {
|
||||
$Blocks[] = $CurrentBlock;
|
||||
}
|
||||
|
||||
$CurrentBlock = Paragraph::build($Context, $State);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($CurrentBlock)) {
|
||||
$Blocks[] = $CurrentBlock;
|
||||
}
|
||||
|
||||
return [$Blocks, $State];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @return StateRenderable[]
|
||||
*/
|
||||
public static function line($text, State $State)
|
||||
{
|
||||
return self::stateRenderablesFrom(self::inlines($text, $State));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @return Inline[]
|
||||
*/
|
||||
public static function inlines($text, State $State)
|
||||
{
|
||||
# standardize line breaks
|
||||
$text = \str_replace(["\r\n", "\r"], "\n", $text);
|
||||
|
||||
$RecursionLimiter = $State->get(RecursionLimiter::class)->increment();
|
||||
|
||||
if ($RecursionLimiter->isDepthExceeded()) {
|
||||
return [Plaintext::build(new Excerpt($text, 0), $State)];
|
||||
}
|
||||
|
||||
$State = $State->setting($RecursionLimiter);
|
||||
|
||||
/** @var Inline[] */
|
||||
$Inlines = [];
|
||||
|
||||
# $excerpt is based on the first occurrence of a marker
|
||||
|
||||
$InlineTypes = $State->get(InlineTypes::class);
|
||||
$markerMask = $InlineTypes->markers();
|
||||
|
||||
for (
|
||||
$Excerpt = (new Excerpt($text, 0))->pushingOffsetTo($markerMask);
|
||||
$Excerpt->text() !== '';
|
||||
$Excerpt = $Excerpt->pushingOffsetTo($markerMask)
|
||||
) {
|
||||
$marker = \substr($Excerpt->text(), 0, 1);
|
||||
|
||||
foreach ($InlineTypes->markedBy($marker) as $inlineType) {
|
||||
$Inline = $inlineType::build($Excerpt, $State);
|
||||
|
||||
if (! isset($Inline)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$markerPosition = $Excerpt->offset();
|
||||
|
||||
/** @var int|null */
|
||||
$startPosition = null;
|
||||
|
||||
if ($Inline instanceof BacktrackingInline) {
|
||||
$startPosition = $Inline->modifyStartPositionTo();
|
||||
}
|
||||
|
||||
if (! isset($startPosition)) {
|
||||
$startPosition = $markerPosition;
|
||||
}
|
||||
|
||||
$endPosition = $startPosition + $Inline->width();
|
||||
|
||||
if ($startPosition > $markerPosition
|
||||
|| $endPosition < $markerPosition
|
||||
|| $startPosition < 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$Inlines[] = Plaintext::build($Excerpt->choppingUpToOffset($startPosition), $State);
|
||||
|
||||
$Inlines[] = $Inline;
|
||||
|
||||
/** @psalm-suppress LoopInvalidation */
|
||||
$Excerpt = $Excerpt->choppingFromOffset($endPosition);
|
||||
|
||||
continue 2;
|
||||
}
|
||||
|
||||
/** @psalm-suppress LoopInvalidation */
|
||||
$Excerpt = $Excerpt->addingToOffset(1);
|
||||
}
|
||||
|
||||
$Inlines[] = Plaintext::build($Excerpt->choppingFromOffset(0), $State);
|
||||
|
||||
return $Inlines;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Renderable[] $Renderables
|
||||
* @return string
|
||||
*/
|
||||
public static function render(array $Renderables)
|
||||
{
|
||||
return \trim(
|
||||
\array_reduce(
|
||||
$Renderables,
|
||||
/**
|
||||
* @param string $html
|
||||
* @return string
|
||||
*/
|
||||
function ($html, Renderable $Renderable) {
|
||||
$newHtml = $Renderable->getHtml();
|
||||
|
||||
return $html . ($newHtml === '' ? '' : "\n") . $newHtml;
|
||||
},
|
||||
''
|
||||
),
|
||||
"\n"
|
||||
);
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Parsing;
|
||||
|
||||
final class Context
|
||||
{
|
||||
/** @var Line */
|
||||
private $Line;
|
||||
|
||||
/** @var int */
|
||||
private $previousEmptyLines;
|
||||
|
||||
/** @var string */
|
||||
private $previousEmptyLinesText;
|
||||
|
||||
/**
|
||||
* @param Line $Line
|
||||
* @param string $previousEmptyLinesText
|
||||
*/
|
||||
public function __construct($Line, $previousEmptyLinesText)
|
||||
{
|
||||
$this->Line = $Line;
|
||||
$this->previousEmptyLinesText = $previousEmptyLinesText;
|
||||
$this->previousEmptyLines = \substr_count($previousEmptyLinesText, "\n");
|
||||
}
|
||||
|
||||
/** @return Line */
|
||||
public function line()
|
||||
{
|
||||
return $this->Line;
|
||||
}
|
||||
|
||||
/** @return int */
|
||||
public function previousEmptyLines()
|
||||
{
|
||||
return $this->previousEmptyLines;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function previousEmptyLinesText()
|
||||
{
|
||||
return $this->previousEmptyLinesText;
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Parsing;
|
||||
|
||||
final class Excerpt
|
||||
{
|
||||
/** @var string */
|
||||
private $context;
|
||||
|
||||
/** @var int */
|
||||
private $offset;
|
||||
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @param string $context
|
||||
* @param int $offset
|
||||
*/
|
||||
public function __construct($context, $offset)
|
||||
{
|
||||
$this->context = $context;
|
||||
$this->offset = $offset;
|
||||
$this->text = \substr($context, $offset);
|
||||
|
||||
// only necessary pre-php7
|
||||
if ($this->text === false) {
|
||||
$this->text = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $mask
|
||||
* @return self
|
||||
*/
|
||||
public function pushingOffsetTo($mask)
|
||||
{
|
||||
return $this->addingToOffset(\strcspn($this->text, $mask));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $offset
|
||||
* @return self
|
||||
*/
|
||||
public function choppingFromOffset($offset)
|
||||
{
|
||||
return new self(\substr($this->context, $offset), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $offset
|
||||
* @return self
|
||||
*/
|
||||
public function choppingUpToOffset($offset)
|
||||
{
|
||||
return new self(\substr($this->context, 0, $offset), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $offsetIncrement
|
||||
* @return self
|
||||
*/
|
||||
public function addingToOffset($offsetIncrement)
|
||||
{
|
||||
return new self($this->context, $this->offset + $offsetIncrement);
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function context()
|
||||
{
|
||||
return $this->context;
|
||||
}
|
||||
|
||||
/** @return int */
|
||||
public function offset()
|
||||
{
|
||||
return $this->offset;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function text()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Parsing;
|
||||
|
||||
final class Line
|
||||
{
|
||||
const INDENT_STEP = 4;
|
||||
|
||||
/** @var int */
|
||||
private $indent;
|
||||
|
||||
/** @var int */
|
||||
private $indentOffset;
|
||||
|
||||
/** @var string */
|
||||
private $rawLine;
|
||||
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @param string $line
|
||||
* @param int $indentOffset
|
||||
*/
|
||||
public function __construct($line, $indentOffset = 0)
|
||||
{
|
||||
$this->rawLine = $line;
|
||||
$this->indentOffset = $indentOffset % self::INDENT_STEP;
|
||||
|
||||
$lineWithoutTabs = self::indentTabsToSpaces($line, $indentOffset);
|
||||
|
||||
$this->indent = \strspn($lineWithoutTabs, ' ');
|
||||
$this->text = \substr($lineWithoutTabs, $this->indent);
|
||||
}
|
||||
|
||||
/** @return int */
|
||||
public function indentOffset()
|
||||
{
|
||||
return $this->indentOffset;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function rawLine()
|
||||
{
|
||||
return $this->rawLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $fromPosition
|
||||
* @param int $indentOffset
|
||||
* @return int
|
||||
*/
|
||||
public static function tabShortage($fromPosition, $indentOffset)
|
||||
{
|
||||
return self::INDENT_STEP - ($fromPosition + $indentOffset) % self::INDENT_STEP;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param int $indentOffset
|
||||
* @return string
|
||||
*/
|
||||
private static function indentTabsToSpaces($text, $indentOffset = 0)
|
||||
{
|
||||
$rawIndentLen = \strspn($text, " \t");
|
||||
$indentString = \substr($text, 0, $rawIndentLen);
|
||||
$latterString = \substr($text, $rawIndentLen);
|
||||
|
||||
while (($beforeTab = \strstr($indentString, "\t", true)) !== false) {
|
||||
$shortage = self::tabShortage(\mb_strlen($beforeTab, 'UTF-8'), $indentOffset);
|
||||
|
||||
$indentString = $beforeTab
|
||||
. \str_repeat(' ', $shortage)
|
||||
. \substr($indentString, \strlen($beforeTab) + 1)
|
||||
;
|
||||
}
|
||||
|
||||
return $indentString . $latterString;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $pos
|
||||
* @return string
|
||||
*/
|
||||
public function ltrimBodyUpto($pos)
|
||||
{
|
||||
if ($pos <= 0) {
|
||||
return $this->rawLine;
|
||||
}
|
||||
|
||||
if ($pos >= $this->indent) {
|
||||
return \ltrim($this->rawLine, "\t ");
|
||||
}
|
||||
|
||||
$rawIndentLen = \strspn($this->rawLine, " \t");
|
||||
$rawIndentString = \substr($this->rawLine, 0, $rawIndentLen);
|
||||
|
||||
$effectiveIndent = 0;
|
||||
|
||||
foreach (\str_split($rawIndentString) as $n => $char) {
|
||||
if ($char === "\t") {
|
||||
$shortage = self::tabShortage($effectiveIndent, $this->indentOffset);
|
||||
|
||||
$effectiveIndent += $shortage;
|
||||
|
||||
if ($effectiveIndent >= $pos) {
|
||||
$overshoot = $effectiveIndent - $pos;
|
||||
|
||||
return \str_repeat(' ', $overshoot) . \substr($this->rawLine, $n + 1);
|
||||
}
|
||||
|
||||
continue;
|
||||
} else {
|
||||
$effectiveIndent += 1;
|
||||
|
||||
if ($effectiveIndent === $pos) {
|
||||
return \substr($this->rawLine, $n + 1);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return \ltrim($this->rawLine, "\t ");
|
||||
}
|
||||
|
||||
/** @return int */
|
||||
public function indent()
|
||||
{
|
||||
return $this->indent;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function text()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
}
|
@ -1,179 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Parsing;
|
||||
|
||||
final class Lines
|
||||
{
|
||||
/** @var Context[] */
|
||||
private $Contexts;
|
||||
|
||||
/** @var bool */
|
||||
private $containsBlankLines;
|
||||
|
||||
/** @var string */
|
||||
private $trailingBlankLinesText;
|
||||
|
||||
/** @var int */
|
||||
private $trailingBlankLines;
|
||||
|
||||
/**
|
||||
* @param Context[] $Contexts
|
||||
* @param string $trailingBlankLinesText
|
||||
*/
|
||||
private function __construct($Contexts, $trailingBlankLinesText)
|
||||
{
|
||||
$this->Contexts = $Contexts;
|
||||
$this->trailingBlankLinesText = $trailingBlankLinesText;
|
||||
$this->trailingBlankLines = \substr_count($trailingBlankLinesText, "\n");
|
||||
|
||||
$containsBlankLines = $this->trailingBlankLines > 0;
|
||||
|
||||
if (! $containsBlankLines) {
|
||||
foreach ($Contexts as $Context) {
|
||||
if ($Context->previousEmptyLines() > 0) {
|
||||
$containsBlankLines = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->containsBlankLines = $containsBlankLines;
|
||||
}
|
||||
|
||||
/** @return self */
|
||||
public static function none()
|
||||
{
|
||||
return new self([], '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param int $indentOffset
|
||||
* @return self
|
||||
*/
|
||||
public static function fromTextLines($text, $indentOffset)
|
||||
{
|
||||
# standardize line breaks
|
||||
$text = \str_replace(["\r\n", "\r"], "\n", $text);
|
||||
|
||||
$Contexts = [];
|
||||
$sequentialLines = '';
|
||||
|
||||
foreach (\explode("\n", $text) as $line) {
|
||||
if (\chop($line) === '') {
|
||||
$sequentialLines .= $line . "\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
$Contexts[] = new Context(
|
||||
new Line($line, $indentOffset),
|
||||
$sequentialLines
|
||||
);
|
||||
|
||||
$sequentialLines = '';
|
||||
}
|
||||
|
||||
return new self($Contexts, $sequentialLines);
|
||||
}
|
||||
|
||||
/** @return bool */
|
||||
public function isEmpty()
|
||||
{
|
||||
return \count($this->Contexts) === 0 && $this->trailingBlankLines === 0;
|
||||
}
|
||||
|
||||
/** @return Context[] */
|
||||
public function Contexts()
|
||||
{
|
||||
return $this->Contexts;
|
||||
}
|
||||
|
||||
/** @return bool */
|
||||
public function containsBlankLines()
|
||||
{
|
||||
return $this->containsBlankLines;
|
||||
}
|
||||
|
||||
/** @return int */
|
||||
public function trailingBlankLines()
|
||||
{
|
||||
return $this->trailingBlankLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $count
|
||||
* @return self
|
||||
*/
|
||||
public function appendingBlankLines($count = 1)
|
||||
{
|
||||
if ($count < 0) {
|
||||
$count = 0;
|
||||
}
|
||||
|
||||
$Lines = clone($this);
|
||||
$Lines->trailingBlankLinesText .= \str_repeat("\n", $count);
|
||||
$Lines->trailingBlankLines += $count;
|
||||
$Lines->containsBlankLines = $Lines->containsBlankLines || ($count > 0);
|
||||
|
||||
return $Lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param int $indentOffset
|
||||
* @return Lines
|
||||
*/
|
||||
public function appendingTextLines($text, $indentOffset)
|
||||
{
|
||||
$Lines = clone($this);
|
||||
|
||||
$NextLines = self::fromTextLines($text, $indentOffset);
|
||||
|
||||
if (\count($NextLines->Contexts) === 0) {
|
||||
$Lines->trailingBlankLines += $NextLines->trailingBlankLines;
|
||||
$Lines->trailingBlankLinesText .= $NextLines->trailingBlankLinesText;
|
||||
|
||||
$Lines->containsBlankLines = true;
|
||||
|
||||
return $Lines;
|
||||
}
|
||||
|
||||
$NextLines->Contexts[0] = new Context(
|
||||
$NextLines->Contexts[0]->line(),
|
||||
$NextLines->Contexts[0]->previousEmptyLinesText() . $Lines->trailingBlankLinesText
|
||||
);
|
||||
|
||||
$Lines->Contexts = \array_merge($Lines->Contexts, $NextLines->Contexts);
|
||||
|
||||
$Lines->trailingBlankLines = $NextLines->trailingBlankLines;
|
||||
$Lines->trailingBlankLinesText = $NextLines->trailingBlankLinesText;
|
||||
|
||||
$Lines->containsBlankLines = $Lines->containsBlankLines
|
||||
|| $NextLines->containsBlankLines
|
||||
;
|
||||
|
||||
return $Lines;
|
||||
}
|
||||
|
||||
/** @return Lines */
|
||||
public function appendingContext(Context $Context)
|
||||
{
|
||||
$Lines = clone($this);
|
||||
|
||||
$Context = new Context(
|
||||
$Context->line(),
|
||||
$Context->previousEmptyLinesText() . $Lines->trailingBlankLinesText
|
||||
);
|
||||
|
||||
if ($Context->previousEmptyLines() > 0) {
|
||||
$Lines->containsBlankLines = true;
|
||||
}
|
||||
|
||||
$Lines->trailingBlankLines = 0;
|
||||
$Lines->trailingBlankLinesText = '';
|
||||
|
||||
$Lines->Contexts[] = $Context;
|
||||
|
||||
return $Lines;
|
||||
}
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Html\Renderable;
|
||||
|
||||
final class State implements StateBearer
|
||||
{
|
||||
/**
|
||||
* @var array<class-string<Configurable>, Configurable>
|
||||
*/
|
||||
private $state;
|
||||
|
||||
/**
|
||||
* @var array<class-string<Configurable>, Configurable>
|
||||
*/
|
||||
private static $initialCache;
|
||||
|
||||
/**
|
||||
* @param Configurable[] $Configurables
|
||||
*/
|
||||
public function __construct(array $Configurables = [])
|
||||
{
|
||||
$this->state = \array_combine(
|
||||
\array_map(
|
||||
/** @return class-string */
|
||||
function (Configurable $C) { return \get_class($C); },
|
||||
$Configurables
|
||||
),
|
||||
$Configurables
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self
|
||||
*/
|
||||
public function setting(Configurable $C)
|
||||
{
|
||||
return new self([\get_class($C) => $C] + $this->state);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self
|
||||
*/
|
||||
public function mergingWith(State $State)
|
||||
{
|
||||
return new self($State->state + $this->state);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T as Configurable
|
||||
* @template-typeof T $configurableClass
|
||||
* @param class-string<Configurable> $configurableClass
|
||||
* @return T
|
||||
*/
|
||||
public function get($configurableClass)
|
||||
{
|
||||
if (isset($this->state[$configurableClass])) {
|
||||
return $this->state[$configurableClass];
|
||||
}
|
||||
|
||||
if (! isset(self::$initialCache[$configurableClass])) {
|
||||
self::$initialCache[$configurableClass] = $configurableClass::initial();
|
||||
}
|
||||
|
||||
return self::$initialCache[$configurableClass];
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->state = \array_map(
|
||||
/** @return Configurable */
|
||||
function (Configurable $C) { return clone($C); },
|
||||
$this->state
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StateRenderable[] $StateRenderables
|
||||
* @return Renderable[]
|
||||
*/
|
||||
public function applyTo(array $StateRenderables)
|
||||
{
|
||||
return \array_map(
|
||||
/** @return Renderable */
|
||||
function (StateRenderable $SR) { return $SR->renderable($this); },
|
||||
$StateRenderables
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return State
|
||||
*/
|
||||
public function state()
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown;
|
||||
|
||||
interface StateBearer
|
||||
{
|
||||
/**
|
||||
* @return State
|
||||
*/
|
||||
public function state();
|
||||
}
|
71
test/CommonMarkTestStrict.php
Normal file
71
test/CommonMarkTestStrict.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Test Parsedown against the CommonMark spec
|
||||
*
|
||||
* @link http://commonmark.org/ CommonMark
|
||||
*/
|
||||
class CommonMarkTestStrict extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
const SPEC_URL = 'https://raw.githubusercontent.com/jgm/CommonMark/master/spec.txt';
|
||||
|
||||
protected $parsedown;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
$this->parsedown = new TestParsedown();
|
||||
$this->parsedown->setUrlsLinked(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider data
|
||||
* @param $id
|
||||
* @param $section
|
||||
* @param $markdown
|
||||
* @param $expectedHtml
|
||||
*/
|
||||
public function testExample($id, $section, $markdown, $expectedHtml)
|
||||
{
|
||||
$actualHtml = $this->parsedown->text($markdown);
|
||||
$this->assertEquals($expectedHtml, $actualHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function data()
|
||||
{
|
||||
$spec = file_get_contents(self::SPEC_URL);
|
||||
if ($spec === false) {
|
||||
$this->fail('Unable to load CommonMark spec from ' . self::SPEC_URL);
|
||||
}
|
||||
|
||||
$spec = str_replace("\r\n", "\n", $spec);
|
||||
$spec = strstr($spec, '<!-- END TESTS -->', true);
|
||||
|
||||
$matches = array();
|
||||
preg_match_all('/^`{32} example\n((?s).*?)\n\.\n(?:|((?s).*?)\n)`{32}$|^#{1,6} *(.*?)$/m', $spec, $matches, PREG_SET_ORDER);
|
||||
|
||||
$data = array();
|
||||
$currentId = 0;
|
||||
$currentSection = '';
|
||||
foreach ($matches as $match) {
|
||||
if (isset($match[3])) {
|
||||
$currentSection = $match[3];
|
||||
} else {
|
||||
$currentId++;
|
||||
$markdown = str_replace('→', "\t", $match[1]);
|
||||
$expectedHtml = isset($match[2]) ? str_replace('→', "\t", $match[2]) : '';
|
||||
|
||||
$data[$currentId] = array(
|
||||
'id' => $currentId,
|
||||
'section' => $currentSection,
|
||||
'markdown' => $markdown,
|
||||
'expectedHtml' => $expectedHtml
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
@ -1,8 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Tests;
|
||||
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
require_once(__DIR__ . '/CommonMarkTestStrict.php');
|
||||
|
||||
/**
|
||||
* Test Parsedown against the CommonMark spec, but less aggressive
|
||||
@ -18,66 +15,45 @@ use Erusev\Parsedown\Html\Renderables\Element;
|
||||
*/
|
||||
class CommonMarkTestWeak extends CommonMarkTestStrict
|
||||
{
|
||||
/** @var string */
|
||||
protected $textLevelElementRegex;
|
||||
|
||||
/**
|
||||
* @param string|null $name
|
||||
* @param array $data
|
||||
* @param string $dataName
|
||||
*/
|
||||
public function __construct($name = null, array $data = [], $dataName = '')
|
||||
protected function setUp()
|
||||
{
|
||||
$textLevelElements = \array_keys(Element::$TEXT_LEVEL_ELEMENTS);
|
||||
parent::setUp();
|
||||
|
||||
\array_walk(
|
||||
$textLevelElements,
|
||||
/**
|
||||
* @param string &$element
|
||||
* @return void
|
||||
*/
|
||||
function (&$element) {
|
||||
$element = \preg_quote($element, '/');
|
||||
}
|
||||
);
|
||||
$this->textLevelElementRegex = '\b(?:' . \implode('|', $textLevelElements) . ')\b';
|
||||
|
||||
parent::__construct($name, $data, $dataName);
|
||||
$textLevelElements = $this->parsedown->getTextLevelElements();
|
||||
array_walk($textLevelElements, function (&$element) {
|
||||
$element = preg_quote($element, '/');
|
||||
});
|
||||
$this->textLevelElementRegex = '\b(?:' . implode('|', $textLevelElements) . ')\b';
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider data
|
||||
* @param int $_
|
||||
* @param string $__
|
||||
* @param string $markdown
|
||||
* @param string $expectedHtml
|
||||
* @return void
|
||||
* @throws \PHPUnit\Framework\AssertionFailedError
|
||||
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
|
||||
* @param $id
|
||||
* @param $section
|
||||
* @param $markdown
|
||||
* @param $expectedHtml
|
||||
*/
|
||||
public function testExample($_, $__, $markdown, $expectedHtml)
|
||||
public function testExample($id, $section, $markdown, $expectedHtml)
|
||||
{
|
||||
$expectedHtml = $this->cleanupHtml($expectedHtml);
|
||||
|
||||
$actualHtml = $this->Parsedown->text($markdown);
|
||||
$actualHtml = $this->parsedown->text($markdown);
|
||||
$actualHtml = $this->cleanupHtml($actualHtml);
|
||||
|
||||
$this->assertEquals($expectedHtml, $actualHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $markup
|
||||
* @return string
|
||||
*/
|
||||
protected function cleanupHtml($markup)
|
||||
{
|
||||
// invisible whitespaces at the beginning and end of block elements
|
||||
// however, whitespaces at the beginning of <pre> elements do matter
|
||||
$markup = \preg_replace(
|
||||
[
|
||||
$markup = preg_replace(
|
||||
array(
|
||||
'/(<(?!(?:' . $this->textLevelElementRegex . '|\bpre\b))\w+\b[^>]*>(?:<' . $this->textLevelElementRegex . '[^>]*>)*)\s+/s',
|
||||
'/\s+((?:<\/' . $this->textLevelElementRegex . '>)*<\/(?!' . $this->textLevelElementRegex . ')\w+\b>)/s'
|
||||
],
|
||||
),
|
||||
'$1',
|
||||
$markup
|
||||
);
|
199
test/ParsedownTest.php
Executable file
199
test/ParsedownTest.php
Executable file
@ -0,0 +1,199 @@
|
||||
<?php
|
||||
require 'SampleExtensions.php';
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ParsedownTest extends TestCase
|
||||
{
|
||||
final function __construct($name = null, array $data = array(), $dataName = '')
|
||||
{
|
||||
$this->dirs = $this->initDirs();
|
||||
$this->Parsedown = $this->initParsedown();
|
||||
|
||||
parent::__construct($name, $data, $dataName);
|
||||
}
|
||||
|
||||
private $dirs;
|
||||
protected $Parsedown;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function initDirs()
|
||||
{
|
||||
$dirs []= dirname(__FILE__).'/data/';
|
||||
|
||||
return $dirs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Parsedown
|
||||
*/
|
||||
protected function initParsedown()
|
||||
{
|
||||
$Parsedown = new TestParsedown();
|
||||
|
||||
return $Parsedown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider data
|
||||
* @param $test
|
||||
* @param $dir
|
||||
*/
|
||||
function test_($test, $dir)
|
||||
{
|
||||
$markdown = file_get_contents($dir . $test . '.md');
|
||||
|
||||
$expectedMarkup = file_get_contents($dir . $test . '.html');
|
||||
|
||||
$expectedMarkup = str_replace("\r\n", "\n", $expectedMarkup);
|
||||
$expectedMarkup = str_replace("\r", "\n", $expectedMarkup);
|
||||
|
||||
$this->Parsedown->setSafeMode(substr($test, 0, 3) === 'xss');
|
||||
$this->Parsedown->setStrictMode(substr($test, 0, 6) === 'strict');
|
||||
|
||||
$actualMarkup = $this->Parsedown->text($markdown);
|
||||
|
||||
$this->assertEquals($expectedMarkup, $actualMarkup);
|
||||
}
|
||||
|
||||
function testRawHtml()
|
||||
{
|
||||
$markdown = "```php\nfoobar\n```";
|
||||
$expectedMarkup = '<pre><code class="language-php"><p>foobar</p></code></pre>';
|
||||
$expectedSafeMarkup = '<pre><code class="language-php"><p>foobar</p></code></pre>';
|
||||
|
||||
$unsafeExtension = new UnsafeExtension;
|
||||
$actualMarkup = $unsafeExtension->text($markdown);
|
||||
|
||||
$this->assertEquals($expectedMarkup, $actualMarkup);
|
||||
|
||||
$unsafeExtension->setSafeMode(true);
|
||||
$actualSafeMarkup = $unsafeExtension->text($markdown);
|
||||
|
||||
$this->assertEquals($expectedSafeMarkup, $actualSafeMarkup);
|
||||
}
|
||||
|
||||
function testTrustDelegatedRawHtml()
|
||||
{
|
||||
$markdown = "```php\nfoobar\n```";
|
||||
$expectedMarkup = '<pre><code class="language-php"><p>foobar</p></code></pre>';
|
||||
$expectedSafeMarkup = $expectedMarkup;
|
||||
|
||||
$unsafeExtension = new TrustDelegatedExtension;
|
||||
$actualMarkup = $unsafeExtension->text($markdown);
|
||||
|
||||
$this->assertEquals($expectedMarkup, $actualMarkup);
|
||||
|
||||
$unsafeExtension->setSafeMode(true);
|
||||
$actualSafeMarkup = $unsafeExtension->text($markdown);
|
||||
|
||||
$this->assertEquals($expectedSafeMarkup, $actualSafeMarkup);
|
||||
}
|
||||
|
||||
function data()
|
||||
{
|
||||
$data = array();
|
||||
|
||||
foreach ($this->dirs as $dir)
|
||||
{
|
||||
$Folder = new DirectoryIterator($dir);
|
||||
|
||||
foreach ($Folder as $File)
|
||||
{
|
||||
/** @var $File DirectoryIterator */
|
||||
|
||||
if ( ! $File->isFile())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$filename = $File->getFilename();
|
||||
|
||||
$extension = pathinfo($filename, PATHINFO_EXTENSION);
|
||||
|
||||
if ($extension !== 'md')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$basename = $File->getBasename('.md');
|
||||
|
||||
if (file_exists($dir . $basename . '.html'))
|
||||
{
|
||||
$data []= array($basename, $dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function test_no_markup()
|
||||
{
|
||||
$markdownWithHtml = <<<MARKDOWN_WITH_MARKUP
|
||||
<div>_content_</div>
|
||||
|
||||
sparse:
|
||||
|
||||
<div>
|
||||
<div class="inner">
|
||||
_content_
|
||||
</div>
|
||||
</div>
|
||||
|
||||
paragraph
|
||||
|
||||
<style type="text/css">
|
||||
p {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
comment
|
||||
|
||||
<!-- html comment -->
|
||||
MARKDOWN_WITH_MARKUP;
|
||||
|
||||
$expectedHtml = <<<EXPECTED_HTML
|
||||
<p><div><em>content</em></div></p>
|
||||
<p>sparse:</p>
|
||||
<p><div>
|
||||
<div class="inner">
|
||||
<em>content</em>
|
||||
</div>
|
||||
</div></p>
|
||||
<p>paragraph</p>
|
||||
<p><style type="text/css">
|
||||
p {
|
||||
color: red;
|
||||
}
|
||||
</style></p>
|
||||
<p>comment</p>
|
||||
<p><!-- html comment --></p>
|
||||
EXPECTED_HTML;
|
||||
|
||||
$parsedownWithNoMarkup = new TestParsedown();
|
||||
$parsedownWithNoMarkup->setMarkupEscaped(true);
|
||||
$this->assertEquals($expectedHtml, $parsedownWithNoMarkup->text($markdownWithHtml));
|
||||
}
|
||||
|
||||
public function testLateStaticBinding()
|
||||
{
|
||||
$parsedown = Parsedown::instance();
|
||||
$this->assertInstanceOf('Parsedown', $parsedown);
|
||||
|
||||
// After instance is already called on Parsedown
|
||||
// subsequent calls with the same arguments return the same instance
|
||||
$sameParsedown = TestParsedown::instance();
|
||||
$this->assertInstanceOf('Parsedown', $sameParsedown);
|
||||
$this->assertSame($parsedown, $sameParsedown);
|
||||
|
||||
$testParsedown = TestParsedown::instance('test late static binding');
|
||||
$this->assertInstanceOf('TestParsedown', $testParsedown);
|
||||
|
||||
$sameInstanceAgain = TestParsedown::instance('test late static binding');
|
||||
$this->assertSame($testParsedown, $sameInstanceAgain);
|
||||
}
|
||||
}
|
40
test/SampleExtensions.php
Normal file
40
test/SampleExtensions.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
class UnsafeExtension extends Parsedown
|
||||
{
|
||||
protected function blockFencedCodeComplete($Block)
|
||||
{
|
||||
$text = $Block['element']['element']['text'];
|
||||
unset($Block['element']['element']['text']);
|
||||
|
||||
// WARNING: There is almost always a better way of doing things!
|
||||
//
|
||||
// This example is one of them, unsafe behaviour is NOT needed here.
|
||||
// Only use this if you trust the input and have no idea what
|
||||
// the output HTML will look like (e.g. using an external parser).
|
||||
$Block['element']['element']['rawHtml'] = "<p>$text</p>";
|
||||
|
||||
return $Block;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TrustDelegatedExtension extends Parsedown
|
||||
{
|
||||
protected function blockFencedCodeComplete($Block)
|
||||
{
|
||||
$text = $Block['element']['element']['text'];
|
||||
unset($Block['element']['element']['text']);
|
||||
|
||||
// WARNING: There is almost always a better way of doing things!
|
||||
//
|
||||
// This behaviour is NOT needed in the demonstrated case.
|
||||
// Only use this if you are sure that the result being added into
|
||||
// rawHtml is safe.
|
||||
// (e.g. using an external parser with escaping capabilities).
|
||||
$Block['element']['element']['rawHtml'] = "<p>$text</p>";
|
||||
$Block['element']['element']['allowRawHtmlInSafeMode'] = true;
|
||||
|
||||
return $Block;
|
||||
}
|
||||
}
|
9
test/TestParsedown.php
Normal file
9
test/TestParsedown.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
class TestParsedown extends Parsedown
|
||||
{
|
||||
public function getTextLevelElements()
|
||||
{
|
||||
return $this->textLevelElements;
|
||||
}
|
||||
}
|
13
test/data/code_block.html
Normal file
13
test/data/code_block.html
Normal file
@ -0,0 +1,13 @@
|
||||
<pre><code><?php
|
||||
|
||||
$message = 'Hello World!';
|
||||
echo $message;</code></pre>
|
||||
<hr />
|
||||
<pre><code>> not a quote
|
||||
- not a list item
|
||||
[not a reference]: http://foo.com</code></pre>
|
||||
<hr />
|
||||
<pre><code>foo
|
||||
|
||||
|
||||
bar</code></pre>
|
@ -14,26 +14,4 @@
|
||||
foo
|
||||
|
||||
|
||||
bar
|
||||
|
||||
---
|
||||
|
||||
- foo
|
||||
|
||||
bar
|
||||
|
||||
---
|
||||
|
||||
code
|
||||
more code
|
||||
not quite enough indentation
|
||||
|
||||
---
|
||||
|
||||
* foo
|
||||
* bar
|
||||
|
||||
---
|
||||
|
||||
* foo
|
||||
* bar
|
||||
bar
|
@ -3,5 +3,4 @@
|
||||
<p><code>and look at this one!</code></p>
|
||||
<p>single backtick in a code span: <code>`</code></p>
|
||||
<p>backtick-delimited string in a code span: <code>`foo`</code></p>
|
||||
<p><code>sth `` sth</code></p>
|
||||
<p><code>foo </code></p>
|
||||
<p><code>sth `` sth</code></p>
|
@ -8,6 +8,4 @@ single backtick in a code span: `` ` ``
|
||||
|
||||
backtick-delimited string in a code span: `` `foo` ``
|
||||
|
||||
`sth `` sth`
|
||||
|
||||
`foo `
|
||||
`sth `` sth`
|
@ -1,8 +1,6 @@
|
||||
<ul>
|
||||
<li>li
|
||||
<ul>
|
||||
<li>li
|
||||
<ul>
|
||||
<li>li<ul>
|
||||
<li>li<ul>
|
||||
<li>li</li>
|
||||
<li>li</li>
|
||||
</ul>
|
||||
@ -14,14 +12,10 @@
|
||||
</ul>
|
||||
<hr />
|
||||
<ul>
|
||||
<li>level 1
|
||||
<ul>
|
||||
<li>level 2
|
||||
<ul>
|
||||
<li>level 3
|
||||
<ul>
|
||||
<li>level 4
|
||||
<ul>
|
||||
<li>level 1<ul>
|
||||
<li>level 2<ul>
|
||||
<li>level 3<ul>
|
||||
<li>level 4<ul>
|
||||
<li>level 5</li>
|
||||
</ul>
|
||||
</li>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user