1
0
mirror of https://github.com/erusev/parsedown.git synced 2023-08-10 21:13:06 +03:00

Compare commits

..

11 Commits

Author SHA1 Message Date
cb17b6477d Increment version for release 2019-12-30 22:54:17 +00:00
21f99b156a Merge pull request #745 from aidantwoods/dev-1.7.x/opt-in-rawHtml
Opt in raw html
2019-12-30 22:51:57 +00:00
791faca8af Test on 7.4 2019-12-30 22:47:43 +00:00
add8d18c80 Add rawHtml without using it (extensions may opt-in) 2019-12-30 22:37:17 +00:00
7073ac3ed1 Dev for 1.7.4 2019-12-30 22:37:17 +00:00
3d2b25b79d Add test to prevent regression 2019-04-10 21:49:32 +01:00
6d89393817 New release due to mislabeled previous tag 2019-03-17 18:48:37 +00:00
d60bcdc469 Bump version 2019-03-17 17:19:46 +00:00
c390a9e406 Merge pull request #700 from aidantwoods/fix/spaces-in-class-names-1.7.x
[1.7.x] Fix spaces in class names
2019-03-17 17:14:45 +00:00
0f1e9da8f4 Fix test platforms 2019-03-17 17:05:15 +00:00
bc003952fc [1.7.x] Fix spaces in class names 2019-03-17 16:49:45 +00:00
1705 changed files with 2300 additions and 12490 deletions

1
.gitattributes vendored
View File

@ -1,6 +1,5 @@
# Ignore all tests for archive
/test export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.travis.yml export-ignore
/phpunit.xml.dist export-ignore

View File

@ -1,98 +0,0 @@
name: Parsedown
on: [push, pull_request]
jobs:
units:
name: Unit Tests
strategy:
matrix:
os: [ubuntu-latest]
php: [8.1, 8.0, 7.4, 7.3, 7.2, 7.1]
runs-on: ${{ matrix.os }}
steps:
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer:v2
- uses: actions/checkout@v2
- name: Run Tests
run: |
composer remove infection/infection --no-update --dev
composer remove roave/infection-static-analysis-plugin --no-update --dev
composer update --prefer-dist --no-interaction --no-progress
composer test-units
mutations:
name: Mutation Tests
strategy:
matrix:
os: [ubuntu-latest]
php: [8.1]
runs-on: ${{ matrix.os }}
steps:
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer:v2
- uses: actions/checkout@v2
- name: Run Tests
run: |
composer update --prefer-dist --no-interaction --no-progress
vendor/bin/roave-infection-static-analysis-plugin --show-mutations --threads=4 --min-msi=80 --min-covered-msi=80
static-analysis:
name: Code Format and Static Analysis
strategy:
matrix:
os: [ubuntu-latest]
php: [8.1]
runs-on: ${{ matrix.os }}
steps:
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer:v2
- uses: actions/checkout@v2
- name: Run Tests
run: |
composer install --prefer-dist --no-interaction --no-progress
composer test-static -- --shepherd
composer test-formatting
composer test-dead-code
commonmark:
name: CommonMark
continue-on-error: true
strategy:
matrix:
os: [ubuntu-latest]
php: [8.1]
runs-on: ${{ matrix.os }}
steps:
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer:v2
- uses: actions/checkout@v2
- name: Install
run: composer install --prefer-dist --no-interaction --no-progress
- name: CommonMark Strict
continue-on-error: true
run: composer test-commonmark
- name: CommonMark Weak
continue-on-error: true
run: composer test-commonmark-weak

6
.gitignore vendored
View File

@ -1,6 +0,0 @@
composer.lock
vendor/
infection.log
tests/spec_cache.txt
.phpunit.result.cache
composer.lock

View File

@ -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 (new Config)
->setRules($rules)
->setFinder($finder)
->setUsingCache(false)
->setRiskyAllowed(true)
;

29
.travis.yml Normal file
View File

@ -0,0 +1,29 @@
language: php
dist: trusty
sudo: false
matrix:
include:
- php: 5.3
dist: precise
- php: 5.4
- php: 5.5
- php: 5.6
- php: 7.0
- php: 7.1
- php: 7.2
- php: 7.3
- php: 7.4
- php: nightly
fast_finish: true
allow_failures:
- php: nightly
install:
- composer install --prefer-dist --no-interaction --no-progress
script:
- vendor/bin/phpunit
- vendor/bin/phpunit test/CommonMarkTestWeak.php || true
- '[ -z "$TRAVIS_TAG" ] || [ "$TRAVIS_TAG" == "$(php -r "require(\"Parsedown.php\"); echo Parsedown::version;")" ]'

1712
Parsedown.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,61 +1,49 @@
<!-- ![Parsedown](https://i.imgur.com/yE8afYV.png) -->
> I also make [Caret](https://caret.io?ref=parsedown) - a Markdown editor for Mac and PC.
<p align="center"><img alt="Parsedown" src="https://i.imgur.com/fKVY6Kz.png" width="240" /></p>
## Parsedown
<h1>Parsedown</h1>
[![Build Status](https://img.shields.io/travis/erusev/parsedown/master.svg?style=flat-square)](https://travis-ci.org/erusev/parsedown)
<!--[![Total Downloads](http://img.shields.io/packagist/dt/erusev/parsedown.svg?style=flat-square)](https://packagist.org/packages/erusev/parsedown)-->
[![Build Status](https://travis-ci.org/erusev/parsedown.svg)](https://travis-ci.org/erusev/parsedown)
[![Total Downloads](https://poser.pugx.org/erusev/parsedown/d/total.svg)](https://packagist.org/packages/erusev/parsedown)
[![Version](https://poser.pugx.org/erusev/parsedown/v/stable.svg)](https://packagist.org/packages/erusev/parsedown)
[![License](https://poser.pugx.org/erusev/parsedown/license.svg)](https://packagist.org/packages/erusev/parsedown)
Better Markdown Parser in PHP
Better Markdown Parser in PHP - <a href="http://parsedown.org/demo">Demo</a>.
[Demo](http://parsedown.org/demo) |
[Benchmarks](http://parsedown.org/speed) |
[Tests](http://parsedown.org/tests/) |
[Documentation](https://github.com/erusev/parsedown/wiki/)
## Features
### Features
* One File
* No Dependencies
* [Super Fast](http://parsedown.org/speed)
* Super Fast
* Extensible
* [GitHub flavored](https://help.github.com/articles/github-flavored-markdown)
* Tested in 5.5 to 7.3
* Tested in 5.3 to 7.1 and in HHVM
* [Markdown Extra extension](https://github.com/erusev/parsedown-extra)
## Installation
### Installation
Install the [composer package]:
Include `Parsedown.php` or install [the composer package](https://packagist.org/packages/erusev/parsedown).
composer require erusev/parsedown
### Example
Or download the [latest release] and include `Parsedown.php`
[composer package]: https://packagist.org/packages/erusev/parsedown "The Parsedown package on packagist.org"
[latest release]: https://github.com/erusev/parsedown/releases/latest "The latest release of Parsedown"
## Example
```php
``` php
$Parsedown = new Parsedown();
echo $Parsedown->text('Hello _Parsedown_!'); # prints: <p>Hello <em>Parsedown</em>!</p>
```
You can also parse inline markdown only:
```php
echo $Parsedown->line('Hello _Parsedown_!'); # prints: Hello <em>Parsedown</em>!
```
More examples in [the wiki](https://github.com/erusev/parsedown/wiki/) and in [this video tutorial](http://youtu.be/wYZBY8DEikI).
## Security
### Security
Parsedown is capable of escaping user-input within the HTML that it generates. Additionally Parsedown will apply sanitisation to additional scripting vectors (such as scripting link destinations) that are introduced by the markdown syntax itself.
To tell Parsedown that it is processing untrusted user-input, use the following:
```php
$Parsedown->setSafeMode(true);
$parsedown = new Parsedown;
$parsedown->setSafeMode(true);
```
If instead, you wish to allow HTML within untrusted user-input, but still want output to be free from XSS it is recommended that you make use of a HTML sanitiser that allows HTML tags to be whitelisted, like [HTML Purifier](http://htmlpurifier.org/).
@ -66,19 +54,18 @@ In both cases you should strongly consider employing defence-in-depth measures,
Safe mode does not necessarily yield safe results when using extensions to Parsedown. Extensions should be evaluated on their own to determine their specific safety against XSS.
## Escaping HTML
### Escaping HTML
> ⚠️  **WARNING:** This method isn't safe from XSS!
If you wish to escape HTML **in trusted input**, you can use the following:
```php
$Parsedown->setMarkupEscaped(true);
$parsedown = new Parsedown;
$parsedown->setMarkupEscaped(true);
```
Beware that this still allows users to insert unsafe scripting vectors, such as links like `[xss](javascript:alert%281%29)`.
## Questions
### Questions
**How does Parsedown work?**
@ -92,7 +79,7 @@ It passes most of the CommonMark tests. Most of the tests that don't pass deal w
**Who uses it?**
[Laravel Framework](https://laravel.com/), [Bolt CMS](http://bolt.cm/), [Grav CMS](http://getgrav.org/), [Herbie CMS](http://www.getherbie.org/), [Kirby CMS](http://getkirby.com/), [October CMS](http://octobercms.com/), [Pico CMS](http://picocms.org), [Statamic CMS](http://www.statamic.com/), [phpDocumentor](http://www.phpdoc.org/), [RaspberryPi.org](http://www.raspberrypi.org/), [Symfony Demo](https://github.com/symfony/demo) and [more](https://packagist.org/packages/erusev/parsedown/dependents).
[Laravel Framework](https://laravel.com/), [Bolt CMS](http://bolt.cm/), [Grav CMS](http://getgrav.org/), [Herbie CMS](http://www.getherbie.org/), [Kirby CMS](http://getkirby.com/), [October CMS](http://octobercms.com/), [Pico CMS](http://picocms.org), [Statamic CMS](http://www.statamic.com/), [phpDocumentor](http://www.phpdoc.org/), [RaspberryPi.org](http://www.raspberrypi.org/), [Symfony demo](https://github.com/symfony/symfony-demo) and [more](https://packagist.org/packages/erusev/parsedown/dependents).
**How can I help?**

View File

@ -13,36 +13,21 @@
}
],
"require": {
"php": "^7.1||^8.0",
"php": ">=5.3.0",
"ext-mbstring": "*"
},
"require-dev": {
"phpunit/phpunit": "^9.3.11||^8.5.21||^7.5.20",
"vimeo/psalm": "^4.10.0",
"friendsofphp/php-cs-fixer": "^3.0.0",
"infection/infection": "^0.25.0",
"roave/infection-static-analysis-plugin": "^1.10.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/"
}
}
}

View File

@ -1,222 +0,0 @@
# Implementing "Extensions" in v2.0
Parsedown v1.x allowed extensability through class extensions, where an developer
could extend the core Parsedown class, and access or override any of the `protected`
level methods and variables.
Whilst this approach allows huge breadth to the type of functionality that can
be added by an extension, it has some downsides too:
* ### Composability: extensions cannot be combined easily
An extension must extend another extension for two extensions to work together.
This limits the usefulness of small extensions, because they cannot be combined with another small or popular extension.
If an extension author wishes the extension to be compatible with another extension, they can only pick one.
* ### API stability
Because extensions have access to functions and variables at the `protected` API layer, it is hard to determine impacts of
internal changes. Yet, without being able to make a certain amount of internal change it is impractical to fix bugs or develop
new features. In the `1.x` branch, `1.8` was never released outside of a "beta" version for this reason: changes in the
`protected` API layer would break extensions.
In order to address these concerns, "extensions" in Parsedown v2.0 will become more like "plugins", and with that comes a lot of
flexability.
ParsedownExtra is a popular extension for Parsedown, and this has been completely re-implemented for 2.0. In order to use
ParsedownExtra with Parsedown, a user simply needs to write the following:
```php
$Parsedown = new Parsedown(new ParsedownExtra);
$actualMarkup = $Parsedown->toHtml($markdown);
```
Here, ParsedownExtra is *composed* with Parsedown, but does not extend it.
A key feature of *composability* is the ability to compose *multiple* extensions together, for example another
extension, say, `ParsedownMath` could be composed with `ParsedownExtra` in a user-defined order.
This time using the `::from` method, rather than the convinence constructor provided by `ParsedownExtra`.
```php
$Parsedown = new Parsedown(ParsedownExtra::from(ParsedownMath::from(new State)));
```
```php
$Parsedown = new Parsedown(ParsedownMath::from(ParsedownExtra::from(new State)));
```
In the above, the first object that we initialise the chain of composed extensions is the `State` object. This `State`
object is passed from `ParsedownExtra` to `ParsedownMath`, and then finally, to `Parsedown`. At each stage new
information is added to the `State`: adding or removing parsing instructions, and to enabling or disabling features.
The `State` object both contains instructions for how to parse a document (e.g. new blocks and inlines), as well as
information used throughout parsing (such as link reference definitions, or recursion depth). By writing `new State`,
we create a `State` object that is setup with Parsedown's default behaviours, and by passing that object through
different extensions (using the `::from` method), these extensions are free to alter, add to, or remove from that
default behaviour.
## Introduction to the `State` Object
Key to Parsedown's new composability for extensions is the `State` object.
This name is a little obtuse, but is importantly accurate.
A `State` object incorporates `Block`s, `Inline`s, some additional render steps, and any custom configuration options that
the user might want to set. This can **fully** control how a document is parsed and rendered.
In the above code, `ParsedownExtra` and `ParsedownMath` would both be implementing the `StateBearer` interface, which
essentially means "this class holds onto a particular Parsedown State". A `StateBearer` should be constructable from
an existing `State` via `::from(StateBearer $StateBearer)`, and reveals the `State` it is holding onto via `->state(): State`.
Implementing the `StateBearer` interface is **strongly encouraged** if implementing an extension, but not necessarily required.
In the end, you can modify Parsedown's behaviour by producing an appropriate `State` object (which itself is trivially a
`StateBearer`).
In general, extensions are encouraged to go further still, and split each self-contained piece of functionality out into its own
`StateBearer`. This will allow your users to cherry-pick specific pieces of functionality and combine it with other
functionality from different authors as they like. For example, a feature of ParsedownExtra is the ability to define and expand
"abbreviations". This feature is self-contained, and does not depend on other features (e.g. "footnotes").
A user could import *only* the abbreviations feature from ParsedownExtra by using the following:
```php
use Erusev\Parsedown\State;
use Erusev\Parsedown\Parsedown;
use Erusev\ParsedownExtra\Features\Abbreviations;
$State = Abbreviations::from(new State);
$Parsedown = new Parsedown($State);
$actualMarkup = $Parsedown->toHtml($markdown);
```
This allows a user to have fine-grained control over which features they import, and will allow them much more control over
combining features from multiple sources. E.g. a user may not like the way ParsedownExtra has implemented the "footnotes" feature,
and so may wish to utilise an implementation from another source. By implementing each feature as its own `StateBearer`, we give
users the freedom to compose features in a way that works for them.
## Anatomy of the `State` Object
The `State` object, generically, consists of a set of `Configurable`s. The word "set" is important here: only one instance of each
`Configurable` may exist in a `State`. If you need to store related data in a `Configurable`, your `Configurable` needs to handle
this containerisation itself.
`State` has a special property: all `Configurable`s "exist" in any `State` object when retrieving that `Configurable` with `->get`.
This means that retrieval cannot fail when using this method, though does mean that all `Configurable`s need to be "default constructable" (i.e. can be constructed into a "default" state). All `Configurable`s must therefore implement the static method
`initial`, which must return an instance of the given `Configurable`. No initial data will be provided, but the `Configurable` **must** arrive at some sane default instance.
`Configurable`s must also be immutable, unless they declare themeslves otherwise by implementing the `MutableConfigurable` interface.
### Blocks
One of the "core" `Configurable`s in Parsedown is `BlockTypes`. This contains a mapping of "markers" (a character that Parsedown
looks for, before handing off to the block-specific parser), and a list of `Block`s that can begin parsing from a specific marker.
Also contained, is a list of "unmarked" blocks, which Parsedown will hand off to prior to trying any marked blocks. Within marked
blocks there is also a precedence order, where the first block type to successfully parse in this list will be the one chosen.
The default value given by `BlockTypes::initial()` consists of Parsedown's default blocks. The following is a snapshot of this list:
```php
const DEFAULT_BLOCK_TYPES = [
'#' => [Header::class],
'*' => [Rule::class, TList::class],
'+' => [TList::class],
'-' => [SetextHeader::class, Table::class, Rule::class, TList::class],
...
```
This means that if a `-` marker is found, Parsedown will first try to parse a `SetextHeader`, then try to parse a `Table`, and
so on...
A new block can be added to this list in several ways. ParsedownExtra, for example, adds a new `Abbreviation` block as follows:
```php
$BlockTypes = $State->get(BlockTypes::class)
->addingMarkedLowPrecedence('*', [Abbreviation::class])
;
$State = $State->setting($BlockTypes);
```
This first retrieves the current value of the `BlockTypes` configurable, adds `Abbreviation` with low precedence (i.e. the
back of the list) to the `*` marker, and then updates the `$State` object by using the `->setting` method.
### Immutability
Note that the `->setting` method must be used to create a new instance of the `State` object because `BlockTypes` is immutable,
the same will be true of most configurables. This approach is preferred because mutations to `State` are localised by default: i.e.
only affect copies of `$State` which we provide to other methods, but does not affect copies of `$State` which were provided to our
code by a parent caller.
Localised mutability allows for more sensible reasoning by default, for example (this time talking about `Inline`s), the `Link` inline
can enforce that no inline `Url`s are parsed (which would cause double links in output when parsing something like:
`[https://example.com](https://example.com)`). This can be done by updating the copy of `$State` which is passed down to lower level
parsers to simply no longer include parsing of `Url`s:
```php
$State = $State->setting(
$State->get(InlineTypes::class)->removing([Url::class])
);
```
If `InlineTypes` were mutable, this change would not only affect decendent parsing, but would also affect all parsing which occured after our link was parsed (i.e. would stop URL parsing from that point on in the document).
Another use case for this is implementing a recursion limiter (which *is* implemented as a configurable). After a user-specifiable
max-depth is exceeded: further parsing will halt. The implementaion for this is extremely simple, only because of immutability.
### Mutability
The preference toward immutability by default is not an assertion that "mutability is bad", rather that "unexpected mutability
is bad". By opting-in to mutability, we can treat mutability with the care it deserves.
While immutabiltiy can do a lot to simplify reasoning in the majority of cases, there are some cirumstances where mutability is
required to implement a specific feature. An exmaple of this is found in ParsedownExtra's "abbreviations" feature, which implements
the following:
```php
final class AbbreviationBook implements MutableConfigurable
{
/** @var array<string, string> */
private $book;
/**
* @param array<string, string> $book
*/
public function __construct(array $book = [])
{
$this->book = $book;
}
/** @return self */
public static function initial()
{
return new self;
}
public function mutatingSet(string $abbreviation, string $definition): void
{
$this->book[$abbreviation] = $definition;
}
public function lookup(string $abbreviation): ?string
{
return $this->book[$abbreviation] ?? null;
}
/** @return array<string, string> */
public function all()
{
return $this->book;
}
/** @return self */
public function isolatedCopy(): self
{
return new self($this->book);
}
}
```
Under the hood, `AbbreviationBook` is nothing more than a string-to-string mapping between an abbreviation, and its definition.
The powerful feature here is that when an abbreviation is identified during parsing, that definition can be updated immediately
everywhere, without needing to worry about the current parsing depth, or organise an alternate method to sharing this data. Footnotes
also make use of this with a `FootnoteBook`, with slightly more complexity in what is stored (so that inline references can be
individually numbered).

View File

@ -1,18 +0,0 @@
{
"timeout": 10,
"source": {
"directories": [
"src"
]
},
"logs": {
"text": "infection.log"
},
"mutators": {
"@default": true,
"@cast": false,
"This": false,
"FunctionCall": false,
"NewObject": false
}
}

View File

@ -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>

View File

@ -1,39 +0,0 @@
<?xml version="1.0"?>
<psalm
errorLevel="1"
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" /></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>
<InternalMethod>
<errorLevel type="suppress">
<directory name="tests" />
</errorLevel>
</InternalMethod>
</issueHandlers>
</psalm>

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -1,13 +0,0 @@
<?php
namespace Erusev\Parsedown;
use Erusev\Parsedown\AST\StateRenderable;
interface Component
{
/**
* @return StateRenderable
*/
public function stateRenderable();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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
);
}

View File

@ -1,121 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
use Erusev\Parsedown\AST\Handler;
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->precedingEmptyLines() > 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->precedingEmptyLines() > 0)) {
$indentOffset = $Context->line()->indentOffset() + $Context->line()->indent();
$Lines = $this->Lines->appendingTextLines($Context->line()->text(), $indentOffset);
return new self($Lines);
}
return null;
}
/**
* @return array{Block[], 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);
}
);
}
}

View File

@ -1,149 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
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->precedingEmptyLinesText();
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())]
)]);
}
}

View File

@ -1,116 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
use Erusev\Parsedown\AST\Handler;
use Erusev\Parsedown\Components\Block;
use Erusev\Parsedown\Configurables\HeaderSlug;
use Erusev\Parsedown\Configurables\SlugRegister;
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) {
$HeaderSlug = $State->get(HeaderSlug::class);
$Register = $State->get(SlugRegister::class);
$attributes = (
$HeaderSlug->isEnabled()
? ['id' => $HeaderSlug->transform($Register, $this->text())]
: []
);
return new Element(
'h' . \strval($this->level()),
$attributes,
$State->applyTo(Parsedown::line($this->text(), $State))
);
}
);
}
}

View File

@ -1,93 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
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->precedingEmptyLines() > 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->precedingEmptyLines() > 0) {
foreach (\explode("\n", $Context->precedingEmptyLinesText()) 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())])]
);
}
}

View File

@ -1,256 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
use Erusev\Parsedown\AST\Handler;
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
{
private const REGEX_HTML_ATTRIBUTE = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+';
private const BLOCK_ELEMENTS = [
'address' => true,
'article' => true,
'aside' => true,
'base' => true,
'basefont' => true,
'blockquote' => true,
'body' => true,
'caption' => true,
'center' => true,
'col' => true,
'colgroup' => true,
'dd' => true,
'details' => true,
'dialog' => true,
'dir' => true,
'div' => true,
'dl' => true,
'dt' => true,
'fieldset' => true,
'figcaption' => true,
'figure' => true,
'footer' => true,
'form' => true,
'frame' => true,
'frameset' => true,
'h1' => true,
'h2' => true,
'h3' => true,
'h4' => true,
'h5' => true,
'h6' => true,
'head' => true,
'header' => true,
'hr' => true,
'html' => true,
'iframe' => true,
'legend' => true,
'li' => true,
'link' => true,
'main' => true,
'menu' => true,
'menuitem' => true,
'nav' => true,
'noframes' => true,
'ol' => true,
'optgroup' => true,
'option' => true,
'p' => true,
'param' => true,
'section' => true,
'source' => true,
'summary' => true,
'table' => true,
'tbody' => true,
'td' => true,
'tfoot' => true,
'th' => true,
'thead' => true,
'title' => true,
'tr' => true,
'track' => true,
'ul' => true,
];
private const SIMPLE_CONTAINS_END_CONDITIONS = [
2 => '-->',
3 => '?>',
4 => '>',
5 => ']]>',
];
private const SPECIAL_HTML_BLOCK_TAGS = [
'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++)(.*+)$/', $text, $matches)) {
$isClosing = ($matches[1] === '/');
$element = \strtolower($matches[2]);
$tail = $matches[3];
if (\array_key_exists($element, self::BLOCK_ELEMENTS)
&& \preg_match('/^(?:\s|$|>|\/)/', $tail)
) {
return new self($rawLine, 6);
}
if (
! $isClosing && \preg_match(
'/^(?:[ ]*+'.self::REGEX_HTML_ATTRIBUTE.')*(?:[ ]*+)[\/]?+[>](.*+)$/',
$tail,
$matches
) || $isClosing && \preg_match(
'/^(?:[ ]*+)[\/]?+[>](.*+)$/',
$tail,
$matches
)
) {
$tail = $matches[1];
if (! \array_key_exists($element, self::SPECIAL_HTML_BLOCK_TAGS)
&& ! (isset($Block) && $Block instanceof Paragraph && $Context->precedingEmptyLines() < 1)
&& \preg_match('/^\s*+$/', $tail)
) {
return new self($rawLine, 7);
}
}
}
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->precedingEmptyLines() > 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->precedingEmptyLines() + 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::SIMPLE_CONTAINS_END_CONDITIONS[$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());
}
}
);
}
}

View File

@ -1,76 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
use Erusev\Parsedown\AST\Handler;
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->precedingEmptyLines() > 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))
);
}
);
}
}

View File

@ -1,66 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
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->get(DefinitionBook::class)->mutatingSet($id, $Data);
return new self($State);
}
return null;
}
/** @return State */
public function latestState()
{
return $this->State;
}
/**
* @return Invisible
*/
public function stateRenderable()
{
return new Invisible;
}
}

View File

@ -1,54 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
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', []);
}
}

View File

@ -1,108 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
use Erusev\Parsedown\AST\Handler;
use Erusev\Parsedown\Components\AcquisitioningBlock;
use Erusev\Parsedown\Components\Block;
use Erusev\Parsedown\Configurables\HeaderSlug;
use Erusev\Parsedown\Configurables\SlugRegister;
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->precedingEmptyLines() > 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) {
$HeaderSlug = $State->get(HeaderSlug::class);
$Register = $State->get(SlugRegister::class);
$attributes = (
$HeaderSlug->isEnabled()
? ['id' => $HeaderSlug->transform($Register, $this->text())]
: []
);
return new Element(
'h' . \strval($this->level()),
$attributes,
$State->applyTo(Parsedown::line($this->text(), $State))
);
}
);
}
}

View File

@ -1,346 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
use Erusev\Parsedown\AST\Handler;
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->precedingEmptyLines() > 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->precedingEmptyLines() > 0 && \end($this->Lis)->isEmpty()) {
return null;
}
$newlines = \str_repeat("\n", $Context->precedingEmptyLines());
$requiredIndent = $this->indent + \strlen($this->marker) + $this->afterMarkerSpaces;
$isLoose = $this->isLoose;
$indent = $Context->line()->indent();
$Lis = $this->Lis;
if ($this->type === 'ol') {
$regex = '/^([0-9]++'.$this->markerTypeRegex.')([\t ]++.*|$)/';
} else {
$regex = '/^('.$this->markerTypeRegex.')([\t ]++.*|$)/';
}
if ($Context->line()->indent() < $requiredIndent
&& \preg_match($regex, $Context->line()->text(), $matches)
) {
if ($Context->precedingEmptyLines() > 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->precedingEmptyLines() > 0) {
$Lis[\count($Lis) -1] = $Lis[\count($Lis) -1]->appendingBlankLines($Context->precedingEmptyLines());
$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->precedingEmptyLines() > 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{Block[], State}[]
*/
public function items(State $State)
{
return \array_map(
/** @return array{Block[], 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{Block[], 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)
)
);
}
);
}
}

View File

@ -1,273 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
use Erusev\Parsedown\AST\Handler;
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 list<_Alignment|null> */
private $alignments;
/** @var list<string> */
private $headerCells;
/** @var list<list<string>> */
private $rows;
/**
* @param list<_Alignment|null> $alignments
* @param list<string> $headerCells
* @param list<list<string>> $rows
*/
private function __construct($alignments, $headerCells, $rows)
{
$this->alignments = $alignments;
$this->headerCells = $headerCells;
$this->rows = $rows;
}
/**
* @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, []);
}
/**
* @param Context $Context
* @param State $State
* @return self|null
*/
public function advance(Context $Context, State $State)
{
if ($Context->precedingEmptyLines() > 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])
) {
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 list<_Alignment|null>|null
*/
private static function parseAlignments($dividerRow)
{
$dividerRow = \trim($dividerRow);
$dividerRow = \trim($dividerRow, '|');
$dividerCells = \explode('|', $dividerRow);
/** @var list<_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 list<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 list<Inline[]>[] */
public function rows(State $State)
{
return \array_map(
/**
* @param list<string> $cells
* @return list<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 list<_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)
))
]);
}
);
}
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -1,82 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
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());
}
}

View File

@ -1,83 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
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());
}
}

View File

@ -1,99 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\AST\Handler;
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;
private const STRONG_REGEX = [
'*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s',
'_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us',
];
private const 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());
}
}

View File

@ -1,65 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
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());
}
}

View File

@ -1,87 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
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");
}
}

View File

@ -1,120 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\AST\Handler;
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>
*/
public function stateRenderable()
{
return new Handler(
/** @return Element */
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());
}
}

View File

@ -1,154 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\AST\Handler;
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>
*/
public function stateRenderable()
{
return new Handler(
/** @return Element */
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());
}
}

View File

@ -1,83 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\AST\Handler;
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());
}
}

View File

@ -1,57 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
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());
}
}

View File

@ -1,97 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\AST\Handler;
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");
}
}

View File

@ -1,64 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
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().';');
}
}

View File

@ -1,76 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\AST\Handler;
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());
}
}

View File

@ -1,105 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
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;
private const URI = 'https?+:[^\s[:cntrl:]<>]*';
private const NO_TRAILING_PUNCT = '(?<![?!.,:*_~])';
/** @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)
{
// this needs some work to follow spec
if (
\preg_match(
'/'.self::URI.self::NO_TRAILING_PUNCT.'/iu',
$Excerpt->context(),
$matches,
\PREG_OFFSET_CAPTURE
)
) {
/** @var array{0: array{string, int}} $matches */
$url = $matches[0][0];
$position = \intval($matches[0][1]);
if (\preg_match('/[)]++$/', $url, $matches)) {
$trailingParens = \strlen($matches[0]);
$openingParens = \substr_count($url, '(');
$closingParens = \substr_count($url, ')');
if ($closingParens > $openingParens) {
$url = \substr($url, 0, -\min($trailingParens, $closingParens - $openingParens));
}
}
return new self($url, $position);
}
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());
}
}

View File

@ -1,63 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
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);
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
trait WidthTrait
{
/** @var int */
private $width;
/** @return int */
public function width()
{
return $this->width;
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace Erusev\Parsedown\Components;
use Erusev\Parsedown\State;
interface StateUpdatingBlock extends Block
{
/** @return State */
public function latestState();
}

View File

@ -1,9 +0,0 @@
<?php
namespace Erusev\Parsedown;
interface Configurable
{
/** @return static */
public static function initial();
}

View File

@ -1,231 +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
{
private const DEFAULT_BLOCK_TYPES = [
'#' => [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],
];
private const DEFAULT_UNMARKED_BLOCK_TYPES = [
IndentedCode::class,
];
/** @var array<array-key, list<class-string<Block>>> */
private $blockTypes;
/** @var list<class-string<Block>> */
private $unmarkedBlockTypes;
/**
* @param array<array-key, list<class-string<Block>>> $blockTypes
* @param list<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::DEFAULT_BLOCK_TYPES,
self::DEFAULT_UNMARKED_BLOCK_TYPES
);
}
/**
* @param string $marker
* @param list<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 list<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 class-string<Block> $searchBlockType
* @param class-string<Block> $replacementBlockType
*/
public function replacing($searchBlockType, $replacementBlockType): self
{
$replacer = self::makeReplacer($searchBlockType, $replacementBlockType);
return new self(
\array_map($replacer, $this->blockTypes),
$replacer($this->unmarkedBlockTypes)
);
}
/**
* @param class-string<Block> $searchBlockType
* @param class-string<Block> $replacementBlockType
* @return \Closure(list<class-string<Block>>):list<class-string<Block>>
*/
private static function makeReplacer($searchBlockType, $replacementBlockType)
{
/**
* @param list<class-string<Block>> $blockTypes
* @return list<class-string<Block>>
*/
return function ($blockTypes) use ($searchBlockType, $replacementBlockType) {
return \array_map(
/**
* @param class-string<Block> $blockType
* @return class-string<Block>
*/
function ($blockType) use ($searchBlockType, $replacementBlockType) {
return $blockType === $searchBlockType ? $replacementBlockType : $blockType;
},
$blockTypes
);
};
}
/**
* @param string $marker
* @param list<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 list<class-string<Block>> $newUnmarkedBlockTypes
* @return self
*/
public function settingUnmarked(array $newUnmarkedBlockTypes)
{
return new self($this->blockTypes, $newUnmarkedBlockTypes);
}
/**
* @param list<class-string<Block>> $newBlockTypes
* @return self
*/
public function addingUnmarkedHighPrecedence(array $newBlockTypes)
{
return $this->settingUnmarked(
\array_merge($newBlockTypes, $this->unmarkedBlockTypes)
);
}
/**
* @param list<class-string<Block>> $newBlockTypes
* @return self
*/
public function addingUnmarkedLowPrecedence(array $newBlockTypes)
{
return $this->settingUnmarked(
\array_merge($this->unmarkedBlockTypes, $newBlockTypes)
);
}
/**
* @param list<class-string<Block>> $removeBlockTypes
* @return self
*/
public function removing(array $removeBlockTypes)
{
return new self(
\array_map(
/**
* @param list<class-string<Block>> $blockTypes
* @return list<class-string<Block>>
*/
function ($blockTypes) use ($removeBlockTypes) {
return \array_values(\array_diff($blockTypes, $removeBlockTypes));
},
$this->blockTypes
),
\array_values(\array_diff($this->unmarkedBlockTypes, $removeBlockTypes))
);
}
/**
* @param string $marker
* @return list<class-string<Block>>
*/
public function markedBy($marker)
{
if (isset($this->blockTypes[$marker])) {
return $this->blockTypes[$marker];
}
return [];
}
/**
* @return list<class-string<Block>>
*/
public function unmarked()
{
return $this->unmarkedBlockTypes;
}
}

View File

@ -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);
}
}

View File

@ -1,10 +0,0 @@
<?php
namespace Erusev\Parsedown\Configurables;
use Erusev\Parsedown\Configurable;
final class Breaks implements Configurable
{
use BooleanConfigurable;
}

View File

@ -1,56 +0,0 @@
<?php
namespace Erusev\Parsedown\Configurables;
use Erusev\Parsedown\MutableConfigurable;
/**
* @psalm-type _Data=array{url: string, title: string|null}
*/
final class DefinitionBook implements MutableConfigurable
{
/** @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
*/
public function mutatingSet($id, array $data): void
{
$this->book[$id] = $data;
}
/**
* @param string $id
* @return _Data|null
*/
public function lookup($id)
{
if (isset($this->book[$id])) {
return $this->book[$id];
}
return null;
}
/** @return static */
public function isolatedCopy(): self
{
return new self($this->book);
}
}

View File

@ -1,100 +0,0 @@
<?php
namespace Erusev\Parsedown\Configurables;
use Erusev\Parsedown\Configurable;
final class HeaderSlug implements Configurable
{
/** @var bool */
private $enabled = false;
/** @var \Closure(string):string */
private $slugCallback;
/** @var \Closure(string,int):string */
private $duplicationCallback;
/**
* @param bool $enabled
* @param (\Closure(string):string)|null $slugCallback
* @param (\Closure(string, int):string)|null $duplicationCallback
*/
public function __construct(
$enabled,
$slugCallback = null,
$duplicationCallback = null
) {
$this->enabled = $enabled;
if (! isset($slugCallback)) {
$this->slugCallback = function (string $text): string {
$slug = \mb_strtolower($text);
$slug = \str_replace(' ', '-', $slug);
$slug = \preg_replace('/[^\p{L}\p{Nd}\p{Nl}\p{M}-]+/u', '', $slug);
$slug = \trim($slug, '-');
return $slug;
};
} else {
$this->slugCallback = $slugCallback;
}
if (! isset($duplicationCallback)) {
$this->duplicationCallback = function (string $slug, int $duplicateNumber): string {
return $slug . '-' . \strval($duplicateNumber-1);
};
} else {
$this->duplicationCallback = $duplicationCallback;
}
}
/** @return bool */
public function isEnabled()
{
return $this->enabled;
}
public function transform(SlugRegister $SlugRegister, string $text): string
{
$slug = ($this->slugCallback)($text);
if ($SlugRegister->slugCount($slug) > 0) {
$newSlug = ($this->duplicationCallback)($slug, $SlugRegister->mutatingIncrement($slug));
while ($SlugRegister->slugCount($newSlug) > 0) {
$newSlug = ($this->duplicationCallback)($slug, $SlugRegister->mutatingIncrement($slug));
}
return $newSlug;
}
$SlugRegister->mutatingIncrement($slug);
return $slug;
}
/** @param \Closure(string):string $slugCallback */
public static function withCallback($slugCallback): self
{
return new self(true, $slugCallback);
}
/** @param \Closure(string,int):string $duplicationCallback */
public static function withDuplicationCallback($duplicationCallback): self
{
return new self(true, null, $duplicationCallback);
}
/** @return self */
public static function enabled()
{
return new self(true);
}
/** @return self */
public static function initial()
{
return new self(false);
}
}

View File

@ -1,168 +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
{
private const DEFAULT_INLINE_TYPES = [
'!' => [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, list<class-string<Inline>>> */
private $inlineTypes;
/** @var string */
private $inlineMarkers;
/**
* @param array<array-key, list<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::DEFAULT_INLINE_TYPES);
}
/**
* @param string $marker
* @param list<class-string<Inline>> $newInlineTypes
* @return self
*/
public function setting($marker, array $newInlineTypes)
{
$inlineTypes = $this->inlineTypes;
$inlineTypes[$marker] = $newInlineTypes;
return new self($inlineTypes);
}
/**
* @param class-string<Inline> $searchInlineType
* @param class-string<Inline> $replacementInlineType
*/
public function replacing($searchInlineType, $replacementInlineType): self
{
return new self(
\array_map(
/**
* @param list<class-string<Inline>> $inlineTypes
* @return list<class-string<Inline>>
*/
function ($inlineTypes) use ($searchInlineType, $replacementInlineType) {
return \array_map(
/**
* @param class-string<Inline> $inlineType
* @return class-string<Inline>
*/
function ($inlineType) use ($searchInlineType, $replacementInlineType) {
return $inlineType === $searchInlineType ? $replacementInlineType : $inlineType;
},
$inlineTypes
);
},
$this->inlineTypes
)
);
}
/**
* @param string $marker
* @param list<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 list<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 list<class-string<Inline>> $removeInlineTypes
* @return self
*/
public function removing(array $removeInlineTypes)
{
return new self(\array_map(
/**
* @param list<class-string<Inline>> $inlineTypes
* @return list<class-string<Inline>>
*/
function ($inlineTypes) use ($removeInlineTypes) {
return \array_values(\array_diff($inlineTypes, $removeInlineTypes));
},
$this->inlineTypes
));
}
/**
* @param string $marker
* @return list<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;
}
}

View File

@ -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 incremented()
{
return new self($this->maxDepth, $this->currentDepth + 1);
}
/** @return bool */
public function isDepthExceeded()
{
return ($this->maxDepth < $this->currentDepth);
}
}

View File

@ -1,42 +0,0 @@
<?php
namespace Erusev\Parsedown\Configurables;
use Erusev\Parsedown\Configurable;
use Erusev\Parsedown\Html\Renderable;
use Erusev\Parsedown\State;
final class RenderStack implements Configurable
{
/** @var list<\Closure(Renderable[],State):Renderable[]> */
private $stack;
/**
* @param list<\Closure(Renderable[],State):Renderable[]> $stack
*/
private function __construct($stack = [])
{
$this->stack = $stack;
}
/** @return self */
public static function initial()
{
return new self;
}
/**
* @param \Closure(Renderable[],State):Renderable[] $RenderMap
* @return self
*/
public function push(\Closure $RenderMap): self
{
return new self(\array_merge($this->stack, [$RenderMap]));
}
/** @return list<\Closure(Renderable[],State):Renderable[]> */
public function getStack(): array
{
return $this->stack;
}
}

View File

@ -1,10 +0,0 @@
<?php
namespace Erusev\Parsedown\Configurables;
use Erusev\Parsedown\Configurable;
final class SafeMode implements Configurable
{
use BooleanConfigurable;
}

View File

@ -1,45 +0,0 @@
<?php
namespace Erusev\Parsedown\Configurables;
use Erusev\Parsedown\MutableConfigurable;
final class SlugRegister implements MutableConfigurable
{
/** @var array<string, int> */
private $register;
/**
* @param array<string, int> $register
*/
public function __construct(array $register = [])
{
$this->register = $register;
}
/** @return self */
public static function initial()
{
return new self;
}
public function mutatingIncrement(string $slug): int
{
if (! isset($this->register[$slug])) {
$this->register[$slug] = 0;
}
return ++$this->register[$slug];
}
public function slugCount(string $slug): int
{
return $this->register[$slug] ?? 0;
}
/** @return static */
public function isolatedCopy(): self
{
return new self($this->register);
}
}

View File

@ -1,10 +0,0 @@
<?php
namespace Erusev\Parsedown\Configurables;
use Erusev\Parsedown\Configurable;
final class StrictMode implements Configurable
{
use BooleanConfigurable;
}

View File

@ -1,11 +0,0 @@
<?php
namespace Erusev\Parsedown\Html;
use Erusev\Parsedown\AST\StateRenderable;
interface Renderable extends StateRenderable
{
/** @return string */
public function getHtml();
}

View File

@ -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;
}
}

View File

@ -1,84 +0,0 @@
<?php
namespace Erusev\Parsedown\Html\Renderables;
use Erusev\Parsedown\Html\Renderable;
use Erusev\Parsedown\Html\TransformableRenderable;
final class Container implements TransformableRenderable
{
use CanonicalStateRenderable;
/** @var Renderable[] */
private $Contents;
/**
* @param Renderable[] $Contents
*/
public function __construct($Contents = [])
{
$this->Contents = $Contents;
}
/**
* @return Renderable[]
*/
public function contents()
{
return $this->Contents;
}
public function adding(Renderable $Renderable): Container
{
return new Container(\array_merge($this->Contents, [$Renderable]));
}
/** @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();
},
''
);
}
/**
* @param \Closure(string):TransformableRenderable $Transform
* @return TransformableRenderable
*/
public function transformingContent(\Closure $Transform): TransformableRenderable
{
return new Container(\array_map(
function (Renderable $R) use ($Transform): Renderable {
if (! $R instanceof TransformableRenderable) {
return $R;
}
return $R->transformingContent($Transform);
},
$this->Contents
));
}
public function replacingAll(string $search, TransformableRenderable $Replacement): TransformableRenderable
{
return new Container(\array_map(
function (Renderable $R) use ($search, $Replacement): Renderable {
if (! $R instanceof TransformableRenderable) {
return $R;
}
return $R->replacingAll($search, $Replacement);
},
$this->Contents
));
}
}

View File

@ -1,235 +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;
use Erusev\Parsedown\Html\TransformableRenderable;
final class Element implements TransformableRenderable
{
use CanonicalStateRenderable;
const 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;
}
/**
* @param \Closure(string):TransformableRenderable $Transform
* @return TransformableRenderable
*/
public function transformingContent(\Closure $Transform): TransformableRenderable
{
if (! isset($this->Contents)) {
return $this;
}
return new self($this->name, $this->attributes, \array_map(
function (Renderable $R) use ($Transform): Renderable {
if (! $R instanceof TransformableRenderable) {
return $R;
}
return $R->transformingContent($Transform);
},
$this->Contents
));
}
public function replacingAll(string $search, TransformableRenderable $Replacement): TransformableRenderable
{
if (! isset($this->Contents)) {
return $this;
}
return new self($this->name, $this->attributes, \array_map(
function (Renderable $R) use ($search, $Replacement): Renderable {
if (! $R instanceof TransformableRenderable) {
return $R;
}
return $R->replacingAll($search, $Replacement);
},
$this->Contents
));
}
}

View File

@ -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 '';
}
}

View File

@ -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;
}
}

View File

@ -1,89 +0,0 @@
<?php
namespace Erusev\Parsedown\Html\Renderables;
use Erusev\Parsedown\Html\Sanitisation\Escaper;
use Erusev\Parsedown\Html\TransformableRenderable;
final class Text implements TransformableRenderable
{
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);
}
/**
* @param \Closure(string):TransformableRenderable $Transform
* @return TransformableRenderable
*/
public function transformingContent(\Closure $Transform): TransformableRenderable
{
return $Transform($this->text);
}
public function replacingAll(string $search, TransformableRenderable $Replacement): TransformableRenderable
{
$searchLen = \strlen($search);
if ($searchLen < 1) {
return $this;
}
$result = \preg_match_all(
'/\b'.\preg_quote($search, '/').'\b/',
$this->text,
$matches,
\PREG_OFFSET_CAPTURE
);
if (empty($result)) {
return $this;
}
$lastEndPos = 0;
$Container = new Container;
foreach ($matches[0] as $match) {
$pos = $match[1];
$endPos = $pos + $searchLen;
if ($pos !== $lastEndPos) {
$Container = $Container->adding(
new Text(\substr($this->text, $lastEndPos, $pos - $lastEndPos))
);
}
$Container = $Container->adding($Replacement);
$lastEndPos = $endPos;
}
if (\strlen($this->text) !== $lastEndPos) {
$Container = $Container->adding(
new Text(\substr($this->text, $lastEndPos))
);
}
return $Container;
}
}

View File

@ -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);
}
}

View File

@ -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'
);
}
}

View File

@ -1,58 +0,0 @@
<?php
namespace Erusev\Parsedown\Html\Sanitisation;
final class UrlSanitiser
{
private const 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);
}
}

View File

@ -1,34 +0,0 @@
<?php
namespace Erusev\Parsedown\Html;
interface TransformableRenderable extends Renderable
{
/**
* Takes a closure $Transform which will provide a transformation of
* a "contained text" into Renderables.
*
* In order for TransformableRenderable to make sense, a Renderable must
* have:
* 1. Some concept of "contained text". $Transform can be applied
* piece-wise if your container contains logically disjoint sections
* of text.
* 2. A generic mechanism for containing other Renderables, or replacing
* the current renderable with a container.
*
* It is acceptable to only partially transform "contained text".
*
* @param \Closure(string):TransformableRenderable $Transform
* @return TransformableRenderable
*/
public function transformingContent(\Closure $Transform): TransformableRenderable;
/**
* Similar to transformingContent, but replace the string $search in text content
* with the renderable $Replacement and return the result.
*
* @param string $search
* @return TransformableRenderable
*/
public function replacingAll(string $search, TransformableRenderable $Replacement): TransformableRenderable;
}

View File

@ -1,44 +0,0 @@
<?php
namespace Erusev\Parsedown;
/**
* Beware that the values of MutableConfigurables are NOT stable. Values SHOULD
* be accessed as close to use as possible. Parsing operations sharing the same
* State SHOULD NOT be triggered between where values are read and where they
* need to be relied upon.
*/
interface MutableConfigurable extends Configurable
{
/**
* Objects contained in State can generally be regarded as immutable,
* however, when mutability is *required* then isolatedCopy (this method)
* MUST be implemented to take a reliable copy of the contained state,
* which MUST be fully seperable from the current instance. This is
* sometimes referred to as a "deep copy".
*
* The following assumption is made when you implement
* MutableConfigurable:
*
* A shared, (more or less) globally writable, instantaniously updating
* (at all parsing levels), single copy of a Configurable is intentional
* and desired.
*
* As such, Parsedown will use the isolatedCopy method to ensure state
* isolation between successive parsing calls (which are considered to be
* isolated documents).
*
* You MUST NOT depend on the method `initial` being called when a clean
* parsing state is desired, this will not reliably occur; implement
* isolatedCopy properly to allow Parsedown to manage this.
*
* Failing to implement this method properly can result in unintended
* side-effects. If possible, you should design your Configurable to be
* immutable, which allows a single copy to be shared safely, and mutations
* localised to a heirarchy for which the order of operations is easy to
* reason about.
*
* @return static
*/
public function isolatedCopy();
}

View File

@ -1,297 +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\Configurables\RenderStack;
use Erusev\Parsedown\Html\Renderable;
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)
{
$State = ($StateBearer ?? new State)->state();
$this->State = $State->isolatedCopy();
}
/**
* @param string $markdown
* @return string
*/
public function toHtml($markdown)
{
list($StateRenderables, $State) = self::lines(
Lines::fromTextLines($markdown, 0),
$this->State->isolatedCopy()
);
$Renderables = \array_reduce(
\array_reverse($State->get(RenderStack::class)->getStack()),
/**
* @param Renderable[] $Renderables
* @param \Closure(Renderable[],State):Renderable[] $RenderMap
* @return Renderable[]
*/
function (array $Renderables, \Closure $RenderMap) use ($State): array {
return $RenderMap($Renderables, $State);
},
$State->applyTo($StateRenderables)
);
$html = self::render($Renderables);
return $html;
}
/**
* @return array{StateRenderable[], 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{Block[], State}
*/
public static function blocks(Lines $Lines, State $State)
{
$RecursionLimiter = $State->get(RecursionLimiter::class)->incremented();
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)->incremented();
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"
);
}
}

View File

@ -1,48 +0,0 @@
<?php
namespace Erusev\Parsedown\Parsing;
final class Context
{
/** @var Line */
private $Line;
/** @var int|null */
private $precedingEmptyLines;
/** @var string */
private $precedingEmptyLinesText;
/**
* @param Line $Line
* @param string $precedingEmptyLinesText
*/
public function __construct($Line, $precedingEmptyLinesText)
{
$this->Line = $Line;
$this->precedingEmptyLinesText = $precedingEmptyLinesText;
$this->precedingEmptyLines = null;
}
/** @return Line */
public function line()
{
return $this->Line;
}
/** @return int */
public function precedingEmptyLines()
{
if (! isset($this->precedingEmptyLines)) {
$this->precedingEmptyLines = \substr_count($this->precedingEmptyLinesText, "\n");
}
return $this->precedingEmptyLines;
}
/** @return string */
public function precedingEmptyLinesText()
{
return $this->precedingEmptyLinesText;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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->precedingEmptyLines() > 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 = [];
$sequentialEmptyLines = '';
foreach (\explode("\n", $text) as $line) {
if (\chop($line) === '') {
$sequentialEmptyLines .= $line . "\n";
continue;
}
$Contexts[] = new Context(
new Line($line, $indentOffset),
$sequentialEmptyLines
);
$sequentialEmptyLines = '';
}
return new self($Contexts, $sequentialEmptyLines);
}
/** @return bool */
public function isEmpty()
{
return ! $this->containsBlankLines && \count($this->Contexts) === 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]->precedingEmptyLinesText() . $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->precedingEmptyLinesText() . $Lines->trailingBlankLinesText
);
if ($Context->precedingEmptyLines() > 0) {
$Lines->containsBlankLines = true;
}
$Lines->trailingBlankLines = 0;
$Lines->trailingBlankLinesText = '';
$Lines->Contexts[] = $Context;
return $Lines;
}
}

View File

@ -1,125 +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<Configurable> */
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 $className
* @param class-string<T> $className
* @return T
*/
public function get($className)
{
if (
! isset($this->state[$className])
&& \is_subclass_of($className, MutableConfigurable::class, true)
) {
if (! isset(self::$initialCache[$className])) {
/** @var T */
self::$initialCache[$className] = $className::initial();
}
/**
* @var T
* @psalm-suppress PossiblyUndefinedMethod
*/
$this->state[$className] = self::$initialCache[$className]->isolatedCopy();
}
/** @var T */
return (
$this->state[$className]
?? self::$initialCache[$className]
?? self::$initialCache[$className] = $className::initial()
);
}
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
);
}
public function state(): State
{
return $this;
}
/** @return self */
public static function from(StateBearer $StateBearer)
{
return $StateBearer->state();
}
public function isolatedCopy(): self
{
return new self(\array_map(
function ($C) {
return $C instanceof MutableConfigurable ? $C->isolatedCopy() : $C;
},
$this->state
));
}
}

View File

@ -1,10 +0,0 @@
<?php
namespace Erusev\Parsedown;
interface StateBearer
{
public function state(): State;
/** @return static */
public static function from(StateBearer $StateBearer);
}

View 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;
}
}

View File

@ -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,61 +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();
$textLevelElements = \array_map(
function ($e) { return \preg_quote($e, '/'); },
$textLevelElements
);
$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->toHtml($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
);

198
test/ParsedownTest.php Normal file
View File

@ -0,0 +1,198 @@
<?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, $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');
$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">&lt;p&gt;foobar&lt;/p&gt;</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>&lt;div&gt;<em>content</em>&lt;/div&gt;</p>
<p>sparse:</p>
<p>&lt;div&gt;
&lt;div class=&quot;inner&quot;&gt;
<em>content</em>
&lt;/div&gt;
&lt;/div&gt;</p>
<p>paragraph</p>
<p>&lt;style type=&quot;text/css&quot;&gt;
p {
color: red;
}
&lt;/style&gt;</p>
<p>comment</p>
<p>&lt;!-- html comment --&gt;</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);
}
}

39
test/SampleExtensions.php Normal file
View File

@ -0,0 +1,39 @@
<?php
class UnsafeExtension extends Parsedown
{
protected function blockFencedCodeComplete($Block)
{
$text = $Block['element']['text']['text'];
unset($Block['element']['text']['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']['text']['rawHtml'] = "<p>$text</p>";
return $Block;
}
}
class TrustDelegatedExtension extends Parsedown
{
protected function blockFencedCodeComplete($Block)
{
$text = $Block['element']['text']['text'];
unset($Block['element']['text']['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']['text']['rawHtml'] = "<p>$text</p>";
$Block['element']['text']['allowRawHtmlInSafeMode'] = true;
return $Block;
}
}

9
test/TestParsedown.php Normal file
View File

@ -0,0 +1,9 @@
<?php
class TestParsedown extends Parsedown
{
public function getTextLevelElements()
{
return $this->textLevelElements;
}
}

View File

@ -6,8 +6,4 @@
<h6>h6</h6>
<p>####### not a heading</p>
<h1>closed h1</h1>
<h1></h1>
<h2></h2>
<h1># of levels</h1>
<h1># of levels #</h1>
<h1>heading</h1>
<p>#</p>

View File

@ -14,12 +14,4 @@
# closed h1 #
#
##
# # of levels
# # of levels # #
#heading
#

View File

@ -0,0 +1,8 @@
<pre><code>&lt;?php
$message = 'Hello World!';
echo $message;</code></pre>
<hr />
<pre><code>&gt; not a quote
- not a list item
[not a reference]: http://foo.com</code></pre>

10
test/data/code_block.md Normal file
View File

@ -0,0 +1,10 @@
<?php
$message = 'Hello World!';
echo $message;
---
> not a quote
- not a list item
[not a reference]: http://foo.com

View File

@ -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>

View File

@ -8,6 +8,4 @@ single backtick in a code span: `` ` ``
backtick-delimited string in a code span: `` `foo` ``
`sth `` sth`
`foo `
`sth `` sth`

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