mirror of
https://github.com/erusev/parsedown.git
synced 2023-08-10 21:13:06 +03:00
Compare commits
551 Commits
1.6.1
...
test/sheph
Author | SHA1 | Date | |
---|---|---|---|
488ecc0377 | |||
efcccb3256 | |||
9eb6a02334 | |||
b9b75dbcea | |||
35d24d0b56 | |||
3f74fe8347 | |||
710a6ad250 | |||
9b9c9d83d2 | |||
d32f5de2fe | |||
99525fdd76 | |||
de1e0b9361 | |||
c86757b6ae | |||
33522f0aa4 | |||
7f6127f3f8 | |||
4adbd0b8a7 | |||
4501a094db | |||
4a215f33d4 | |||
3ccd64a9a1 | |||
3c0b528d54 | |||
f83ee87902 | |||
747abe7600 | |||
a396fccace | |||
93e68056a8 | |||
4fb6ac31a5 | |||
69f6754c4d | |||
d8d483bd6a | |||
c0792947a6 | |||
658129d847 | |||
65450f47cd | |||
fc23ca5ef5 | |||
015e476f3e | |||
289b641a42 | |||
147a87a4f3 | |||
b90efc69ec | |||
dbe37bcb0e | |||
30613b2430 | |||
cef5b16ae0 | |||
5ecfc42728 | |||
3bb24c20a6 | |||
369aea5d8d | |||
2b79d599fb | |||
8fd3c77109 | |||
df703dcb0e | |||
36fac49ed8 | |||
41fb6b0d43 | |||
a681cf631c | |||
6ac6b7f7f7 | |||
3c6578dd4b | |||
c2973100e0 | |||
0626a83289 | |||
2efae741bb | |||
93650fb9b5 | |||
9bf91d7183 | |||
f95c3bb154 | |||
660c2e43a3 | |||
d9792bb12c | |||
08c40afc16 | |||
14f8ff52e1 | |||
c310625b93 | |||
811991b27d | |||
d29f900374 | |||
efe324c08b | |||
54f2c4eb4c | |||
117912c373 | |||
63a97a926b | |||
cb211a88a8 | |||
c49d40027f | |||
4dee1e9a55 | |||
dbc0efeec0 | |||
fe1355ef9e | |||
f2f7433dcf | |||
a2bca78f7e | |||
42d21a2413 | |||
f47ba7aa34 | |||
49dd8b113d | |||
30763a0f38 | |||
3dd1326ded | |||
0f55cd5b26 | |||
5ada761532 | |||
4fa89c1a80 | |||
7b72eb6454 | |||
745db11d2f | |||
82d20d8ffe | |||
7fd6e0bb31 | |||
eab734b457 | |||
2e0ad27c5e | |||
d6c97ee111 | |||
62615f4fc5 | |||
50e135cd4e | |||
0514997103 | |||
4c0734d935 | |||
734b4fc3d7 | |||
4563ee592d | |||
cbe7b25b21 | |||
f0da746c7b | |||
aab56cf8cc | |||
48c0c34470 | |||
bc3c1544c5 | |||
d6f526d80f | |||
b728f254b7 | |||
ebde35cf0d | |||
d733c262c2 | |||
19e21f2d1b | |||
11da347aa1 | |||
b89bd0e3c2 | |||
fce09a702a | |||
8fe93f30ac | |||
9f9ef78662 | |||
57632f38fb | |||
5e7fb61879 | |||
2618509cc6 | |||
fce4633ff9 | |||
eb90905d27 | |||
5a50930cb0 | |||
1fd2e14b72 | |||
714ae50211 | |||
14b3761687 | |||
00821bd072 | |||
b8cdc6e9a5 | |||
9d97b8eb6a | |||
67231cbae1 | |||
81a2050608 | |||
3d41f270c2 | |||
bb424e606f | |||
51c3d9d445 | |||
74bba0b2fa | |||
fc37ad11ed | |||
4e9a0113c3 | |||
96d8a1f18c | |||
82c981657d | |||
576a2c4519 | |||
083ad582c7 | |||
c9388cb5c2 | |||
bb8a16ad81 | |||
2cfd05a00e | |||
7f526c07a0 | |||
366600034c | |||
6add0ea877 | |||
dac6b01d1a | |||
6f5780abfd | |||
2757274854 | |||
37895448ba | |||
6f1bc7db14 | |||
e4ed4da626 | |||
351a68a14c | |||
a9aa7e7aae | |||
f8003dcded | |||
ca008872ba | |||
7188f49a71 | |||
b3608829e5 | |||
f420fad41f | |||
f58845c480 | |||
04816a9944 | |||
e2c9b2fa2b | |||
799ced66fa | |||
e6e24a8d0d | |||
36cfb21908 | |||
dbdbda52a8 | |||
114eb0bc5b | |||
a286033f52 | |||
53bb9a6467 | |||
db1d0a4999 | |||
f256352f53 | |||
778eacd081 | |||
5e8905c455 | |||
dad0088adb | |||
79a38a1ebb | |||
164a39f3e9 | |||
f2a3a2fb08 | |||
497045d25b | |||
760945008b | |||
25cf5a1729 | |||
18e239fba1 | |||
b53971e656 | |||
0c730e0dc5 | |||
565c8dd3cc | |||
ee094cb397 | |||
edc004f503 | |||
af97e99b39 | |||
a95bc60c30 | |||
07c2566042 | |||
57c6350184 | |||
194c916c6a | |||
c50deda690 | |||
3094329950 | |||
74a855946d | |||
c17868cac8 | |||
5a00cb7f07 | |||
db657952d1 | |||
c55dbb0d3f | |||
072f91df47 | |||
c852b487b4 | |||
00835c5101 | |||
a971e5aa54 | |||
23cfbd153c | |||
23560bfa33 | |||
1f06b47e6c | |||
7746c9df06 | |||
dcc5ea0c9b | |||
7ef8b30043 | |||
deaf0682b5 | |||
3a0db641aa | |||
215953334e | |||
1541859e0e | |||
0f6c0fa84d | |||
0f36000dc9 | |||
57b86b3fc4 | |||
f6a845fa52 | |||
db04e1575f | |||
6d03fa0d3a | |||
49829c2019 | |||
c419295466 | |||
23b07fa185 | |||
5795a6f0a9 | |||
a42848da57 | |||
82a528711f | |||
8c091b8e63 | |||
a636bf7bfa | |||
8512e65a18 | |||
267256cbb8 | |||
f8aa618f3d | |||
f85f6cbd40 | |||
822cf15ac9 | |||
9046f066df | |||
7690b98f61 | |||
04581d0915 | |||
5ab8839d04 | |||
c429c47fee | |||
88ab68fd0b | |||
bfaa76d370 | |||
3825db53a2 | |||
fe7a50eceb | |||
bce642f2d7 | |||
7d4c06cb52 | |||
f7b66e6b20 | |||
811bc32726 | |||
8fd5464c46 | |||
21c8c792de | |||
6ca29539e1 | |||
a503c1a69b | |||
819c68899d | |||
4c2d79fc6a | |||
48a2fb26fe | |||
33b79d2446 | |||
d6d2d96459 | |||
d5b6ab5198 | |||
15e8439c7f | |||
ff6148f9b9 | |||
dfd8657bc5 | |||
ee64646765 | |||
c956090b55 | |||
0be26550f3 | |||
8e26a65a6f | |||
2a24a8583b | |||
1d55344e92 | |||
5dc8d1cc39 | |||
33cf0f0b99 | |||
dc1ff7d6c2 | |||
0f0987571d | |||
18eaa649b5 | |||
e124572b60 | |||
1686a34469 | |||
2bd7113c55 | |||
29fce0ec37 | |||
b0bbc275d4 | |||
72f9ca92ae | |||
89c3fa05d9 | |||
69163d6e88 | |||
3a0c964291 | |||
1829106e60 | |||
464f5f9329 | |||
c26a2ee4bf | |||
ba3b60d6e4 | |||
0b1e6b8c86 | |||
1f69f7e697 | |||
c83af0a7d5 | |||
4686daf8c2 | |||
c9e7183cfa | |||
9eed1104e7 | |||
fd95703da5 | |||
8d172a2994 | |||
dfab7240a4 | |||
113c6d2b21 | |||
a9764ec90f | |||
0a842fb5b1 | |||
7f4318dbdb | |||
3e70819a20 | |||
2bf7ca41a0 | |||
b75fd409ff | |||
88a3f31dd7 | |||
726d4ef44a | |||
450a74fedf | |||
7e15d99d90 | |||
d2dd736e1b | |||
e74a5bd7ed | |||
b53aa74a72 | |||
3ea08140b6 | |||
c45e41950f | |||
2faba6fef5 | |||
b42add3762 | |||
107223d3a0 | |||
d4f1ac465c | |||
d6e306d620 | |||
dc5cf8770b | |||
70f5c02d47 | |||
90ad738933 | |||
f2327023c1 | |||
6f13f97674 | |||
8091e5586a | |||
cb33daf0e6 | |||
c440c91af5 | |||
3514881e14 | |||
043c55e4c6 | |||
e4cd13350b | |||
ae8067e862 | |||
5353ebb524 | |||
39df7d4f8e | |||
50f15add44 | |||
3f5b0ee781 | |||
9a021b2130 | |||
43d25a74fe | |||
1d68e5506c | |||
86940be224 | |||
cdaf86b039 | |||
1d65fb858a | |||
600db7e4de | |||
1be2a01de8 | |||
f50ba3d803 | |||
387ef63888 | |||
68be90348c | |||
48b9f71bdc | |||
0039cd00f8 | |||
c6b717cc35 | |||
8f3f61883d | |||
4c9ea94d0c | |||
32e69de014 | |||
201299ddc2 | |||
557db7c179 | |||
0c0ed38290 | |||
798bda682e | |||
9b7b7348b4 | |||
96581dbe16 | |||
06b810cd4a | |||
38ea813b0e | |||
24e48e91c8 | |||
e33f1a48c8 | |||
1c8f6bc253 | |||
ed3e967fb6 | |||
3b3d13489b | |||
498c88c4eb | |||
772c919b05 | |||
cf6d23de55 | |||
d0279cdd3b | |||
8a90586218 | |||
390fa0da1b | |||
9026b1abdb | |||
68736f8800 | |||
535110c57e | |||
ce073c9baa | |||
e4d6c8f911 | |||
cbe2e74d52 | |||
aa90dd481a | |||
20e592359f | |||
9f1f5de387 | |||
40b9da7837 | |||
a9c21447ce | |||
a3e02c1d0e | |||
40e797031e | |||
448b72a149 | |||
92e426e0e8 | |||
07216480db | |||
caea783006 | |||
d849d64611 | |||
00e51ee424 | |||
0550c3eaf9 | |||
790aed42ab | |||
1c52cb6b5e | |||
ae13290221 | |||
e16162e288 | |||
244ea0aaa6 | |||
2f291e0b2f | |||
d2a73f9179 | |||
f594d4c18b | |||
21cdd8a0b3 | |||
a52d386250 | |||
dd9f4036ee | |||
e7fbbf537b | |||
cac63f6fcb | |||
f71bec00f4 | |||
913e04782f | |||
1fa6b038af | |||
e59fbd736d | |||
8c14c5c239 | |||
0205a4cbe6 | |||
011465bca6 | |||
adcba80502 | |||
65d7bc5013 | |||
1a47e74be1 | |||
56cc41803a | |||
d86d839677 | |||
d5ded2b935 | |||
098f24d189 | |||
eb55e426b9 | |||
ced6187ca5 | |||
972648ff64 | |||
77dc0a090a | |||
88dc949890 | |||
624a08b7eb | |||
3fc54bc966 | |||
ef7ed7b66c | |||
e4c5be026d | |||
e6444bb57e | |||
a3265e7c6f | |||
aac00ac742 | |||
6830c3339f | |||
19f1bb9353 | |||
721b885dd3 | |||
f70d96479a | |||
92e9c27ba0 | |||
9857334186 | |||
ae7e8e5067 | |||
253822057a | |||
a18bf495ed | |||
e5bf9560d7 | |||
33b51eaefa | |||
d686a50292 | |||
f3068df45a | |||
9b1f54b9d3 | |||
90439ef882 | |||
72d30d33bc | |||
97dd037e6f | |||
fa89f0d743 | |||
d638fd8a25 | |||
cc53d5ae29 | |||
45f40696f6 | |||
e8f3d4efc0 | |||
096e164756 | |||
e2f3961f80 | |||
e941dcc3f0 | |||
c192001a7e | |||
48a053fe29 | |||
5057e505d8 | |||
ad62bf5a6f | |||
6678d59be4 | |||
c999a4b61b | |||
e938ab4ffe | |||
e69374af0d | |||
722b776684 | |||
7fd92a8fbd | |||
0e1043a8d6 | |||
1196ed9512 | |||
1244122b84 | |||
d98d60aaf3 | |||
296ebf0e60 | |||
a60ba300b1 | |||
089789dfff | |||
03e1a6ac02 | |||
fbe3fe878f | |||
09827f542c | |||
70ef6f5521 | |||
691e36b1f2 | |||
af6affdc2c | |||
9cf41f27ab | |||
16aadff2ed | |||
07c937583d | |||
4404201175 | |||
c05ef0c12a | |||
47e4163a68 | |||
c05bff047a | |||
6a4afac0d0 | |||
129f807e32 | |||
be963a6531 | |||
728952b90a | |||
c82af01bd6 | |||
67c3efbea0 | |||
593ffd45a3 | |||
bbb7687f31 | |||
b1e5aebaf6 | |||
c63b690a79 | |||
226f636360 | |||
2e4afde68d | |||
dc30cb441c | |||
f76b10aaab | |||
054ba3c487 | |||
4bae1c9834 | |||
aee3963e6b | |||
4dc98b635d | |||
e4bb12329e | |||
6d0156d707 | |||
29ad172261 | |||
131ba75851 | |||
924b26e16c | |||
af04ac92e2 | |||
6bb66db00f | |||
b3d45c4bb9 | |||
1d4296f34d | |||
bf5105cb1a | |||
1140613fc7 | |||
1d0af35f10 | |||
d7956e3ade | |||
4367f89a74 | |||
1bf24f7334 | |||
0a09d5ad45 | |||
3fc442b078 | |||
bd0e31a7dd | |||
dfaf03639a | |||
7081afe8cb | |||
4b6493999a | |||
0172d779d7 | |||
cc5b38ca39 | |||
48351504de | |||
67e454e300 | |||
ae0211a84c | |||
a9f696f7bb | |||
a3836b1853 | |||
a9e1163c85 | |||
7b1529fff0 | |||
1d61f90bf9 | |||
4b3b7df710 | |||
30ff5c6e75 | |||
bdf537e9d5 | |||
81025cd468 | |||
e691034861 | |||
eb853da92a | |||
6973302ca8 | |||
0a43799da4 | |||
2db3199510 | |||
2423644d72 | |||
8965c7864f | |||
d26b33c20f | |||
d9679141fa | |||
0bd61a73ed | |||
06c4344a71 | |||
c4d4a6800d | |||
be671e72a3 | |||
f0587d41a9 | |||
3aef89b399 | |||
543a6c4175 | |||
a81aedeb10 | |||
50952b3243 | |||
4d3600f273 | |||
d6d5f53ff4 | |||
1a44cbd62c | |||
73dbe2fd17 | |||
33a23fbfb2 | |||
228d5f4754 | |||
2cacfb8da4 | |||
d33e736fa3 | |||
3a46a31e09 | |||
e1bcc1c472 | |||
b5951e08c6 |
6
.gitattributes
vendored
Normal file
6
.gitattributes
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
# Ignore all tests for archive
|
||||
/test export-ignore
|
||||
/.gitattributes export-ignore
|
||||
/.gitignore export-ignore
|
||||
/.travis.yml export-ignore
|
||||
/phpunit.xml.dist export-ignore
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
composer.lock
|
||||
vendor/
|
||||
infection.log
|
||||
tests/spec_cache.txt
|
37
.php_cs.dist
Normal file
37
.php_cs.dist
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
use PhpCsFixer\Config;
|
||||
use PhpCsFixer\Finder;
|
||||
$finder = Finder::create()
|
||||
->in(__DIR__ . '/src/')
|
||||
->in(__DIR__ . '/tests/')
|
||||
;
|
||||
$rules = [
|
||||
'@PSR2' => true,
|
||||
'array_syntax' => [
|
||||
'syntax' => 'short',
|
||||
],
|
||||
'braces' => [
|
||||
'allow_single_line_closure' => true,
|
||||
],
|
||||
'logical_operators' => true,
|
||||
'native_constant_invocation' => [
|
||||
'fix_built_in' => true,
|
||||
],
|
||||
'native_function_invocation' => [
|
||||
'include' => ['@all'],
|
||||
],
|
||||
'no_unused_imports' => true,
|
||||
'ordered_imports' => [
|
||||
'sort_algorithm' => 'alpha',
|
||||
],
|
||||
'single_blank_line_before_namespace' => true,
|
||||
'strict_comparison' => true,
|
||||
'strict_param' => true,
|
||||
'whitespace_after_comma_in_array' => true,
|
||||
];
|
||||
return Config::create()
|
||||
->setRules($rules)
|
||||
->setFinder($finder)
|
||||
->setUsingCache(false)
|
||||
->setRiskyAllowed(true)
|
||||
;
|
79
.travis.yml
79
.travis.yml
@ -1,16 +1,73 @@
|
||||
language: php
|
||||
|
||||
php:
|
||||
- 7.1
|
||||
- 7.0
|
||||
- 5.6
|
||||
- 5.5
|
||||
- 5.4
|
||||
- 5.3
|
||||
- hhvm
|
||||
- hhvm-nightly
|
||||
dist: trusty
|
||||
sudo: false
|
||||
|
||||
matrix:
|
||||
stages:
|
||||
- Code Format and Static Analysis
|
||||
- Units
|
||||
- Test CommonMark (weak)
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- php: hhvm-nightly
|
||||
- env: ALLOW_FAILURE
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: Code Format and Static Analysis
|
||||
php: 7.3
|
||||
install: composer install --prefer-dist --no-interaction --no-progress
|
||||
script:
|
||||
- '[ -z "$TRAVIS_TAG" ] || [ "$TRAVIS_TAG" == "$(php -r "require(\"vendor/autoload.php\"); echo Erusev\Parsedown\Parsedown::version;")" ]'
|
||||
- composer test-static -- --shepherd
|
||||
- composer test-formatting
|
||||
- composer test-dead-code
|
||||
|
||||
|
||||
- &UNIT_TEST
|
||||
stage: Units
|
||||
php: 5.5
|
||||
install:
|
||||
# remove packages with PHP requirements higher than 7.0 to prevent composer trying to resolve these, see: https://github.com/composer/composer/issues/6011
|
||||
- composer remove vimeo/psalm friendsofphp/php-cs-fixer infection/infection --dev --no-update --no-interaction
|
||||
- composer install --prefer-dist --no-interaction --no-progress
|
||||
script: composer test-units
|
||||
- <<: *UNIT_TEST
|
||||
php: 5.6
|
||||
- <<: *UNIT_TEST
|
||||
php: 7.0
|
||||
- &MUTATION_AND_UNIT_TEST
|
||||
<<: *UNIT_TEST
|
||||
php: 7.1
|
||||
install:
|
||||
- composer install --prefer-dist --no-interaction --no-progress
|
||||
script:
|
||||
- composer test-units
|
||||
- vendor/bin/infection --show-mutations --threads=4 --min-msi=90 --min-covered-msi=90
|
||||
- <<: *MUTATION_AND_UNIT_TEST
|
||||
php: 7.2
|
||||
- <<: *MUTATION_AND_UNIT_TEST
|
||||
php: 7.3
|
||||
- <<: *UNIT_TEST
|
||||
php: nightly
|
||||
env: ALLOW_FAILURE
|
||||
|
||||
|
||||
- &COMMONMARK_TEST
|
||||
stage: CommonMark
|
||||
name: Weak
|
||||
php: 7.3
|
||||
env: ALLOW_FAILURE
|
||||
install: composer install --prefer-dist --no-interaction --no-progress
|
||||
script:
|
||||
- composer test-commonmark-weak
|
||||
|
||||
- <<: *COMMONMARK_TEST
|
||||
name: Strict
|
||||
script:
|
||||
- composer test-commonmark
|
||||
|
@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Emanuil Rusev, erusev.com
|
||||
Copyright (c) 2013-2018 Emanuil Rusev, erusev.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
@ -17,4 +17,4 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
1548
Parsedown.php
1548
Parsedown.php
File diff suppressed because it is too large
Load Diff
79
README.md
79
README.md
@ -1,41 +1,84 @@
|
||||
> You might also like [Caret](http://caret.io?ref=parsedown) - our Markdown editor for Mac / Windows / Linux.
|
||||
<!--  -->
|
||||
|
||||
## Parsedown
|
||||
<p align="center"><img alt="Parsedown" src="https://i.imgur.com/fKVY6Kz.png" width="240" /></p>
|
||||
|
||||
[](https://travis-ci.org/erusev/parsedown)
|
||||
<!--[](https://packagist.org/packages/erusev/parsedown)-->
|
||||
<h1>Parsedown</h1>
|
||||
|
||||
Better Markdown Parser in PHP
|
||||
[](https://travis-ci.org/erusev/parsedown)
|
||||
[](https://packagist.org/packages/erusev/parsedown)
|
||||
[](https://packagist.org/packages/erusev/parsedown)
|
||||
[](https://packagist.org/packages/erusev/parsedown)
|
||||
|
||||
[Demo](http://parsedown.org/demo) |
|
||||
[Benchmarks](http://parsedown.org/speed) |
|
||||
[Tests](http://parsedown.org/tests/) |
|
||||
[Documentation](https://github.com/erusev/parsedown/wiki/)
|
||||
Better Markdown Parser in PHP - <a href="http://parsedown.org/demo">Demo</a>.
|
||||
|
||||
### Features
|
||||
## Features
|
||||
|
||||
* One File
|
||||
* Super Fast
|
||||
* No Dependencies
|
||||
* [Super Fast](http://parsedown.org/speed)
|
||||
* Extensible
|
||||
* [GitHub flavored](https://help.github.com/articles/github-flavored-markdown)
|
||||
* Tested in 5.3 to 7.1 and in HHVM
|
||||
* Tested in 5.5 to 7.3
|
||||
* [Markdown Extra extension](https://github.com/erusev/parsedown-extra)
|
||||
|
||||
### Installation
|
||||
## Installation
|
||||
|
||||
Include `Parsedown.php` or install [the composer package](https://packagist.org/packages/erusev/parsedown).
|
||||
Install the [composer package]:
|
||||
|
||||
### Example
|
||||
composer require erusev/parsedown
|
||||
|
||||
``` php
|
||||
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
|
||||
$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).
|
||||
|
||||
### Questions
|
||||
## 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);
|
||||
```
|
||||
|
||||
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/).
|
||||
|
||||
In both cases you should strongly consider employing defence-in-depth measures, like [deploying a Content-Security-Policy](https://scotthelme.co.uk/content-security-policy-an-introduction/) (a browser security feature) so that your page is likely to be safe even if an attacker finds a vulnerability in one of the first lines of defence above.
|
||||
|
||||
#### Security of Parsedown Extensions
|
||||
|
||||
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
|
||||
|
||||
> ⚠️ **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);
|
||||
```
|
||||
|
||||
Beware that this still allows users to insert unsafe scripting vectors, such as links like `[xss](javascript:alert%281%29)`.
|
||||
|
||||
## Questions
|
||||
|
||||
**How does Parsedown work?**
|
||||
|
||||
@ -49,7 +92,7 @@ It passes most of the CommonMark tests. Most of the tests that don't pass deal w
|
||||
|
||||
**Who uses it?**
|
||||
|
||||
[phpDocumentor](http://www.phpdoc.org/), [October CMS](http://octobercms.com/), [Bolt CMS](http://bolt.cm/), [Kirby CMS](http://getkirby.com/), [Grav CMS](http://getgrav.org/), [Statamic CMS](http://www.statamic.com/), [Herbie CMS](http://www.getherbie.org/), [RaspberryPi.org](http://www.raspberrypi.org/) 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/demo) and [more](https://packagist.org/packages/erusev/parsedown/dependents).
|
||||
|
||||
**How can I help?**
|
||||
|
||||
|
@ -13,9 +13,35 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
"php": "^7||^5.5",
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7.4||^6.5.13||^5.7.27||^4.8.36",
|
||||
"vimeo/psalm": "^3.2.7",
|
||||
"friendsofphp/php-cs-fixer": "^2.13",
|
||||
"infection/infection": "^0.12.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": {"Parsedown": ""}
|
||||
"psr-4": {"Erusev\\Parsedown\\": "src/"}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
18
infection.json.dist
Normal file
18
infection.json.dist
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"timeout": 10,
|
||||
"source": {
|
||||
"directories": [
|
||||
"src"
|
||||
]
|
||||
},
|
||||
"logs": {
|
||||
"text": "infection.log"
|
||||
},
|
||||
"mutators": {
|
||||
"@default": true,
|
||||
"@cast": false,
|
||||
"This": false,
|
||||
"FunctionCall": false,
|
||||
"NewObject": false
|
||||
}
|
||||
}
|
@ -1,8 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit bootstrap="test/bootstrap.php" colors="true">
|
||||
<phpunit bootstrap="vendor/autoload.php" colors="true">
|
||||
<testsuites>
|
||||
<testsuite>
|
||||
<file>test/ParsedownTest.php</file>
|
||||
<testsuite name="ParsedownTests">
|
||||
<file>tests/ParsedownTest.php</file>
|
||||
<file>tests/CommonMarkTest.php</file>
|
||||
<directory suffix="Test.php">./tests/src</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
<groups>
|
||||
<exclude>
|
||||
<group>update</group>
|
||||
</exclude>
|
||||
</groups>
|
||||
</phpunit>
|
||||
|
34
psalm.xml
Normal file
34
psalm.xml
Normal file
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0"?>
|
||||
<psalm
|
||||
totallyTyped="true"
|
||||
strictBinaryOperands="true"
|
||||
checkForThrowsDocblock="true"
|
||||
>
|
||||
<projectFiles>
|
||||
<directory name="src" />
|
||||
<directory name="tests" />
|
||||
</projectFiles>
|
||||
|
||||
<issueHandlers>
|
||||
<PossiblyUnusedMethod>
|
||||
<errorLevel type="suppress">
|
||||
<directory name="tests" />
|
||||
</errorLevel>
|
||||
</PossiblyUnusedMethod>
|
||||
<PropertyNotSetInConstructor>
|
||||
<errorLevel type="suppress"><directory name="tests" /></errorLevel>
|
||||
</PropertyNotSetInConstructor>
|
||||
<UnusedClass>
|
||||
<errorLevel type="suppress"><directory name="tests/src" /></errorLevel>
|
||||
</UnusedClass>
|
||||
<UndefinedInterfaceMethod>
|
||||
<errorLevel type="suppress"><directory name="tests/src" /></errorLevel>
|
||||
</UndefinedInterfaceMethod>
|
||||
<PossiblyNullArrayAccess>
|
||||
<errorLevel type="suppress"><directory name="tests/src" /></errorLevel>
|
||||
</PossiblyNullArrayAccess>
|
||||
<PossiblyNullReference>
|
||||
<errorLevel type="suppress"><directory name="tests/src" /></errorLevel>
|
||||
</PossiblyNullReference>
|
||||
</issueHandlers>
|
||||
</psalm>
|
34
src/AST/Handler.php
Normal file
34
src/AST/Handler.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
15
src/AST/StateRenderable.php
Normal file
15
src/AST/StateRenderable.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?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);
|
||||
}
|
13
src/Component.php
Normal file
13
src/Component.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
|
||||
interface Component
|
||||
{
|
||||
/**
|
||||
* @return StateRenderable
|
||||
*/
|
||||
public function stateRenderable();
|
||||
}
|
13
src/Components/AcquisitioningBlock.php
Normal file
13
src/Components/AcquisitioningBlock.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?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();
|
||||
}
|
16
src/Components/BacktrackingInline.php
Normal file
16
src/Components/BacktrackingInline.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?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();
|
||||
}
|
22
src/Components/Block.php
Normal file
22
src/Components/Block.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?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
|
||||
);
|
||||
}
|
122
src/Components/Blocks/BlockQuote.php
Normal file
122
src/Components/Blocks/BlockQuote.php
Normal file
@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\ContinuableBlock;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\Parsing\Line;
|
||||
use Erusev\Parsedown\Parsing\Lines;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class BlockQuote implements ContinuableBlock
|
||||
{
|
||||
/** @var Lines */
|
||||
private $Lines;
|
||||
|
||||
/**
|
||||
* @param Lines $Lines
|
||||
*/
|
||||
private function __construct($Lines)
|
||||
{
|
||||
$this->Lines = $Lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
if (\preg_match('/^(>[ \t]?+)(.*+)/', $Context->line()->text(), $matches)) {
|
||||
$indentOffset = $Context->line()->indentOffset() + $Context->line()->indent() + \strlen($matches[1]);
|
||||
|
||||
$recoveredSpaces = 0;
|
||||
if (\strlen($matches[1]) === 2 && \substr($matches[1], 1, 1) === "\t") {
|
||||
$recoveredSpaces = Line::tabShortage(0, $indentOffset -1) -1;
|
||||
}
|
||||
|
||||
return new self(Lines::fromTextLines(
|
||||
\str_repeat(' ', $recoveredSpaces) . $matches[2],
|
||||
$indentOffset
|
||||
));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @return self|null
|
||||
*/
|
||||
public function advance(Context $Context, State $State)
|
||||
{
|
||||
if ($Context->previousEmptyLines() > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (\preg_match('/^(>[ \t]?+)(.*+)/', $Context->line()->text(), $matches)) {
|
||||
$indentOffset = $Context->line()->indentOffset() + $Context->line()->indent() + \strlen($matches[1]);
|
||||
|
||||
$recoveredSpaces = 0;
|
||||
if (\strlen($matches[1]) === 2 && \substr($matches[1], 1, 1) === "\t") {
|
||||
$recoveredSpaces = Line::tabShortage(0, $indentOffset -1) -1;
|
||||
}
|
||||
|
||||
$Lines = $this->Lines->appendingTextLines(
|
||||
\str_repeat(' ', $recoveredSpaces) . $matches[2],
|
||||
$indentOffset
|
||||
);
|
||||
|
||||
return new self($Lines);
|
||||
}
|
||||
|
||||
if (! $Context->previousEmptyLines() > 0) {
|
||||
$indentOffset = $Context->line()->indentOffset() + $Context->line()->indent();
|
||||
$Lines = $this->Lines->appendingTextLines($Context->line()->text(), $indentOffset);
|
||||
|
||||
return new self($Lines);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: Block[], 1: State}
|
||||
*/
|
||||
public function contents(State $State)
|
||||
{
|
||||
return Parsedown::blocks($this->Lines, $State);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element */
|
||||
function (State $State) {
|
||||
list($Blocks, $State) = $this->contents($State);
|
||||
|
||||
$StateRenderables = Parsedown::stateRenderablesFrom($Blocks);
|
||||
|
||||
$Renderables = $State->applyTo($StateRenderables);
|
||||
$Renderables[] = new Text("\n");
|
||||
|
||||
return new Element('blockquote', [], $Renderables);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
150
src/Components/Blocks/FencedCode.php
Normal file
150
src/Components/Blocks/FencedCode.php
Normal file
@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\ContinuableBlock;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class FencedCode implements ContinuableBlock
|
||||
{
|
||||
/** @var string */
|
||||
private $code;
|
||||
|
||||
/** @var string */
|
||||
private $infostring;
|
||||
|
||||
/** @var string */
|
||||
private $marker;
|
||||
|
||||
/** @var int */
|
||||
private $openerLength;
|
||||
|
||||
/** @var bool */
|
||||
private $isComplete;
|
||||
|
||||
/**
|
||||
* @param string $code
|
||||
* @param string $infostring
|
||||
* @param string $marker
|
||||
* @param int $openerLength
|
||||
* @param bool $isComplete
|
||||
*/
|
||||
private function __construct($code, $infostring, $marker, $openerLength, $isComplete)
|
||||
{
|
||||
$this->code = $code;
|
||||
$this->infostring = $infostring;
|
||||
$this->marker = $marker;
|
||||
$this->openerLength = $openerLength;
|
||||
$this->isComplete = $isComplete;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
$marker = \substr($Context->line()->text(), 0, 1);
|
||||
|
||||
if ($marker !== '`' && $marker !== '~') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$openerLength = \strspn($Context->line()->text(), $marker);
|
||||
|
||||
if ($openerLength < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$infostring = \trim(\substr($Context->line()->text(), $openerLength), "\t ");
|
||||
|
||||
if (\strpos($infostring, '`') !== false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self('', $infostring, $marker, $openerLength, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @return self|null
|
||||
*/
|
||||
public function advance(Context $Context, State $State)
|
||||
{
|
||||
if ($this->isComplete) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$newCode = $this->code;
|
||||
|
||||
$newCode .= $Context->previousEmptyLinesText();
|
||||
|
||||
if (($len = \strspn($Context->line()->text(), $this->marker)) >= $this->openerLength
|
||||
&& \chop(\substr($Context->line()->text(), $len), ' ') === ''
|
||||
) {
|
||||
return new self($newCode, $this->infostring, $this->marker, $this->openerLength, true);
|
||||
}
|
||||
|
||||
$newCode .= $Context->line()->rawLine() . "\n";
|
||||
|
||||
return new self($newCode, $this->infostring, $this->marker, $this->openerLength, false);
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function infostring()
|
||||
{
|
||||
return $this->infostring;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function code()
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Element
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
/**
|
||||
* https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
|
||||
* Every HTML element may have a class attribute specified.
|
||||
* The attribute, if specified, must have a value that is a set
|
||||
* of space-separated tokens representing the various classes
|
||||
* that the element belongs to.
|
||||
* [...]
|
||||
* The space characters, for the purposes of this specification,
|
||||
* are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab),
|
||||
* U+000A LINE FEED (LF), U+000C FORM FEED (FF), and
|
||||
* U+000D CARRIAGE RETURN (CR).
|
||||
*/
|
||||
$infostring = \substr(
|
||||
$this->infostring(),
|
||||
0,
|
||||
\strcspn($this->infostring(), " \t\n\f\r")
|
||||
);
|
||||
|
||||
// only necessary pre-php7
|
||||
if ($infostring === false) {
|
||||
$infostring = '';
|
||||
}
|
||||
|
||||
return new Element('pre', [], [new Element(
|
||||
'code',
|
||||
$infostring !== '' ? ['class' => "language-{$infostring}"] : [],
|
||||
[new Text($this->code())]
|
||||
)]);
|
||||
}
|
||||
}
|
107
src/Components/Blocks/Header.php
Normal file
107
src/Components/Blocks/Header.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Configurables\StrictMode;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Header implements Block
|
||||
{
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/** @var 1|2|3|4|5|6 */
|
||||
private $level;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param 1|2|3|4|5|6 $level
|
||||
*/
|
||||
private function __construct($text, $level)
|
||||
{
|
||||
$this->text = $text;
|
||||
$this->level = $level;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
if ($Context->line()->indent() > 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$level = \strspn($Context->line()->text(), '#');
|
||||
|
||||
if ($level > 6 || $level < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var 1|2|3|4|5|6 $level */
|
||||
|
||||
$text = \ltrim($Context->line()->text(), '#');
|
||||
|
||||
$firstChar = \substr($text, 0, 1);
|
||||
|
||||
if (
|
||||
$State->get(StrictMode::class)->isEnabled()
|
||||
&& \trim($firstChar, " \t") !== ''
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$text = \trim($text, " \t");
|
||||
|
||||
# remove closing sequence
|
||||
$removedClosing = \rtrim($text, '#');
|
||||
$lastChar = \substr($removedClosing, -1);
|
||||
|
||||
if (\trim($lastChar, " \t") === '') {
|
||||
$text = \rtrim($removedClosing, " \t");
|
||||
}
|
||||
|
||||
return new self($text, $level);
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function text()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/** @return 1|2|3|4|5|6 */
|
||||
public function level()
|
||||
{
|
||||
return $this->level;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element */
|
||||
function (State $State) {
|
||||
return new Element(
|
||||
'h' . \strval($this->level()),
|
||||
[],
|
||||
$State->applyTo(Parsedown::line($this->text(), $State))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
94
src/Components/Blocks/IndentedCode.php
Normal file
94
src/Components/Blocks/IndentedCode.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\ContinuableBlock;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\Parsing\Line;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class IndentedCode implements ContinuableBlock
|
||||
{
|
||||
/** @var string */
|
||||
private $code;
|
||||
|
||||
/**
|
||||
* @param string $code
|
||||
*/
|
||||
private function __construct($code)
|
||||
{
|
||||
$this->code = $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
if (isset($Block) && $Block instanceof Paragraph && ! $Context->previousEmptyLines() > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($Context->line()->indent() < 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self($Context->line()->ltrimBodyUpto(4) . "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @return self|null
|
||||
*/
|
||||
public function advance(Context $Context, State $State)
|
||||
{
|
||||
if ($Context->line()->indent() < 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$newCode = $this->code;
|
||||
|
||||
$offset = $Context->line()->indentOffset();
|
||||
|
||||
if ($Context->previousEmptyLines() > 0) {
|
||||
foreach (\explode("\n", $Context->previousEmptyLinesText()) as $line) {
|
||||
$newCode .= (new Line($line, $offset))->ltrimBodyUpto(4) . "\n";
|
||||
}
|
||||
|
||||
$newCode = \substr($newCode, 0, -1);
|
||||
}
|
||||
|
||||
$newCode .= $Context->line()->ltrimBodyUpto(4) . "\n";
|
||||
|
||||
return new self($newCode);
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function code()
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Element
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Element(
|
||||
'pre',
|
||||
[],
|
||||
[new Element('code', [], [new Text($this->code())])]
|
||||
);
|
||||
}
|
||||
}
|
174
src/Components/Blocks/Markup.php
Normal file
174
src/Components/Blocks/Markup.php
Normal file
@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\ContinuableBlock;
|
||||
use Erusev\Parsedown\Configurables\SafeMode;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\RawHtml;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Markup implements ContinuableBlock
|
||||
{
|
||||
const REGEX_HTML_ATTRIBUTE = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+';
|
||||
|
||||
/** @var array{2: string, 3: string, 4: string, 5: string} */
|
||||
private static $simpleContainsEndConditions = [
|
||||
2 => '-->',
|
||||
3 => '?>',
|
||||
4 => '>',
|
||||
5 => ']]>'
|
||||
];
|
||||
|
||||
/** @var array<string, string> */
|
||||
private static $specialHtmlBlockTags = [
|
||||
'script' => true,
|
||||
'style' => true,
|
||||
'pre' => true,
|
||||
];
|
||||
|
||||
/** @var string */
|
||||
private $html;
|
||||
|
||||
/** @var 1|2|3|4|5|6|7 */
|
||||
private $type;
|
||||
|
||||
/** @var bool */
|
||||
private $closed;
|
||||
|
||||
/**
|
||||
* @param string $html
|
||||
* @param 1|2|3|4|5|6|7 $type
|
||||
* @param bool $closed
|
||||
*/
|
||||
private function __construct($html, $type, $closed = false)
|
||||
{
|
||||
$this->html = $html;
|
||||
$this->type = $type;
|
||||
$this->closed = $closed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
$text = $Context->line()->text();
|
||||
$rawLine = $Context->line()->rawLine();
|
||||
|
||||
if (\preg_match('/^<(?:script|pre|style)(?:\s++|>|$)/i', $text)) {
|
||||
return new self($rawLine, 1, self::closes12345TypeMarkup(1, $text));
|
||||
}
|
||||
|
||||
if (\substr($text, 0, 4) === '<!--') {
|
||||
return new self($rawLine, 2, self::closes12345TypeMarkup(2, $text));
|
||||
}
|
||||
|
||||
if (\substr($text, 0, 2) === '<?') {
|
||||
return new self($rawLine, 3, self::closes12345TypeMarkup(3, $text));
|
||||
}
|
||||
|
||||
if (\preg_match('/^<![A-Z]/', $text)) {
|
||||
return new self($rawLine, 4, self::closes12345TypeMarkup(4, $text));
|
||||
}
|
||||
|
||||
if (\substr($text, 0, 9) === '<![CDATA[') {
|
||||
return new self($rawLine, 5, self::closes12345TypeMarkup(5, $text));
|
||||
}
|
||||
|
||||
if (\preg_match('/^<[\/]?+(\w++)(?:[ ]*+'.self::REGEX_HTML_ATTRIBUTE.')*+[ ]*+(\/)?>/', $text, $matches)) {
|
||||
$element = \strtolower($matches[1]);
|
||||
|
||||
if (
|
||||
\array_key_exists($element, Element::$TEXT_LEVEL_ELEMENTS)
|
||||
|| \array_key_exists($element, self::$specialHtmlBlockTags)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self($rawLine, 6);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @return self|null
|
||||
*/
|
||||
public function advance(Context $Context, State $State)
|
||||
{
|
||||
$closed = $this->closed;
|
||||
$type = $this->type;
|
||||
|
||||
if ($closed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (($type === 6 || $type === 7) && $Context->previousEmptyLines() > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type === 1 || $type === 2 || $type === 3 || $type === 4 || $type === 5) {
|
||||
$closed = self::closes12345TypeMarkup($type, $Context->line()->text());
|
||||
}
|
||||
|
||||
$html = $this->html . \str_repeat("\n", $Context->previousEmptyLines() + 1);
|
||||
$html .= $Context->line()->rawLine();
|
||||
|
||||
return new self($html, $type, $closed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param 1|2|3|4|5 $type
|
||||
* @param string $text
|
||||
* @return bool
|
||||
*/
|
||||
private static function closes12345TypeMarkup($type, $text)
|
||||
{
|
||||
if ($type === 1) {
|
||||
if (\preg_match('/<\/(?:script|pre|style)>/i', $text)) {
|
||||
return true;
|
||||
}
|
||||
} elseif (\stripos($text, self::$simpleContainsEndConditions[$type]) !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function html()
|
||||
{
|
||||
return $this->html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element|RawHtml>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element|RawHtml */
|
||||
function (State $State) {
|
||||
if ($State->get(SafeMode::class)->isEnabled()) {
|
||||
return new Element('p', [], [new Text($this->html())]);
|
||||
} else {
|
||||
return new RawHtml($this->html());
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
77
src/Components/Blocks/Paragraph.php
Normal file
77
src/Components/Blocks/Paragraph.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\ContinuableBlock;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Paragraph implements ContinuableBlock
|
||||
{
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
*/
|
||||
private function __construct($text)
|
||||
{
|
||||
$this->text = $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
return new self($Context->line()->text());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @return self|null
|
||||
*/
|
||||
public function advance(Context $Context, State $State)
|
||||
{
|
||||
if ($Context->previousEmptyLines() > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self($this->text . "\n" . $Context->line()->text());
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function text()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element */
|
||||
function (State $State) {
|
||||
return new Element(
|
||||
'p',
|
||||
[],
|
||||
$State->applyTo(Parsedown::line(\trim($this->text()), $State))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
69
src/Components/Blocks/Reference.php
Normal file
69
src/Components/Blocks/Reference.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\StateUpdatingBlock;
|
||||
use Erusev\Parsedown\Configurables\DefinitionBook;
|
||||
use Erusev\Parsedown\Html\Renderables\Invisible;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Reference implements StateUpdatingBlock
|
||||
{
|
||||
/** @var State */
|
||||
private $State;
|
||||
|
||||
private function __construct(State $State)
|
||||
{
|
||||
$this->State = $State;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
if (\preg_match(
|
||||
'/^\[(.+?)\]:[ ]*+<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/',
|
||||
$Context->line()->text(),
|
||||
$matches
|
||||
)) {
|
||||
$id = \strtolower($matches[1]);
|
||||
|
||||
$Data = [
|
||||
'url' => $matches[2],
|
||||
'title' => isset($matches[3]) ? $matches[3] : null,
|
||||
];
|
||||
|
||||
$State = $State->setting(
|
||||
$State->get(DefinitionBook::class)->setting($id, $Data)
|
||||
);
|
||||
|
||||
return new self($State);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return State */
|
||||
public function latestState()
|
||||
{
|
||||
return $this->State;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Invisible
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Invisible;
|
||||
}
|
||||
}
|
55
src/Components/Blocks/Rule.php
Normal file
55
src/Components/Blocks/Rule.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Rule implements Block
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
if ($Context->line()->indent() > 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$marker = \substr($Context->line()->text(), 0, 1);
|
||||
|
||||
if ($marker !== '*' && $marker !== '-' && $marker !== '_') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
\substr_count($Context->line()->text(), $marker) >= 3
|
||||
&& \chop($Context->line()->text(), " \t$marker") === ''
|
||||
) {
|
||||
return new self;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Element
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return Element::selfClosing('hr', []);
|
||||
}
|
||||
}
|
99
src/Components/Blocks/SetextHeader.php
Normal file
99
src/Components/Blocks/SetextHeader.php
Normal file
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\AcquisitioningBlock;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class SetextHeader implements AcquisitioningBlock
|
||||
{
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/** @var 1|2 */
|
||||
private $level;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param 1|2 $level
|
||||
*/
|
||||
private function __construct($text, $level)
|
||||
{
|
||||
$this->text = $text;
|
||||
$this->level = $level;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
if (! isset($Block) || ! $Block instanceof Paragraph || $Context->previousEmptyLines() > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$marker = \substr($Context->line()->text(), 0, 1);
|
||||
|
||||
if ($marker !== '=' && $marker !== '-') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
$Context->line()->indent() < 4
|
||||
&& \chop(\chop($Context->line()->text(), " \t"), $marker) === ''
|
||||
) {
|
||||
$level = ($marker === '=' ? 1 : 2);
|
||||
|
||||
return new self(\trim($Block->text()), $level);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return bool */
|
||||
public function acquiredPrevious()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function text()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/** @return 1|2 */
|
||||
public function level()
|
||||
{
|
||||
return $this->level;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element */
|
||||
function (State $State) {
|
||||
return new Element(
|
||||
'h' . \strval($this->level()),
|
||||
[],
|
||||
$State->applyTo(Parsedown::line($this->text(), $State))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
347
src/Components/Blocks/TList.php
Normal file
347
src/Components/Blocks/TList.php
Normal file
@ -0,0 +1,347 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\ContinuableBlock;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\Parsing\Line;
|
||||
use Erusev\Parsedown\Parsing\Lines;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class TList implements ContinuableBlock
|
||||
{
|
||||
/** @var Lines[] */
|
||||
private $Lis;
|
||||
|
||||
/** @var int|null */
|
||||
private $listStart;
|
||||
|
||||
/** @var bool */
|
||||
private $isLoose;
|
||||
|
||||
/** @var int */
|
||||
private $indent;
|
||||
|
||||
/** @var 'ul'|'ol' */
|
||||
private $type;
|
||||
|
||||
/** @var string */
|
||||
private $marker;
|
||||
|
||||
/** @var int */
|
||||
private $afterMarkerSpaces;
|
||||
|
||||
/** @var string */
|
||||
private $markerType;
|
||||
|
||||
/** @var string */
|
||||
private $markerTypeRegex;
|
||||
|
||||
/**
|
||||
* @param Lines[] $Lis
|
||||
* @param int|null $listStart
|
||||
* @param bool $isLoose
|
||||
* @param int $indent
|
||||
* @param 'ul'|'ol' $type
|
||||
* @param string $marker
|
||||
* @param int $afterMarkerSpaces
|
||||
* @param string $markerType
|
||||
* @param string $markerTypeRegex
|
||||
*/
|
||||
private function __construct(
|
||||
$Lis,
|
||||
$listStart,
|
||||
$isLoose,
|
||||
$indent,
|
||||
$type,
|
||||
$marker,
|
||||
$afterMarkerSpaces,
|
||||
$markerType,
|
||||
$markerTypeRegex
|
||||
) {
|
||||
$this->Lis = $Lis;
|
||||
$this->listStart = $listStart;
|
||||
$this->isLoose = $isLoose;
|
||||
$this->indent = $indent;
|
||||
$this->type = $type;
|
||||
$this->marker = $marker;
|
||||
$this->afterMarkerSpaces = $afterMarkerSpaces;
|
||||
$this->markerType = $markerType;
|
||||
$this->markerTypeRegex = $markerTypeRegex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
list($type, $pattern) = (
|
||||
\substr($Context->line()->text(), 0, 1) <= '-'
|
||||
? ['ul', '[*+-]']
|
||||
: ['ol', '[0-9]{1,9}+[.\)]']
|
||||
);
|
||||
|
||||
if (\preg_match(
|
||||
'/^('.$pattern.')([\t ]++.*|$)/',
|
||||
$Context->line()->text(),
|
||||
$matches
|
||||
)) {
|
||||
$marker = $matches[1];
|
||||
|
||||
$preAfterMarkerSpacesIndentOffset = $Context->line()->indentOffset() + $Context->line()->indent() + \strlen($marker);
|
||||
|
||||
$LineWithMarkerIndent = new Line(isset($matches[2]) ? $matches[2] : '', $preAfterMarkerSpacesIndentOffset);
|
||||
$indentAfterMarker = $LineWithMarkerIndent->indent();
|
||||
|
||||
if ($indentAfterMarker > 4) {
|
||||
$perceivedIndent = $indentAfterMarker -1;
|
||||
$afterMarkerSpaces = 1;
|
||||
} else {
|
||||
$perceivedIndent = 0;
|
||||
$afterMarkerSpaces = $indentAfterMarker;
|
||||
}
|
||||
|
||||
$indentOffset = $preAfterMarkerSpacesIndentOffset + $afterMarkerSpaces;
|
||||
$text = \str_repeat(' ', $perceivedIndent) . $LineWithMarkerIndent->text();
|
||||
|
||||
$markerType = (
|
||||
$type === 'ul'
|
||||
? $marker
|
||||
: \substr($marker, -1)
|
||||
);
|
||||
|
||||
$markerTypeRegex = \preg_quote($markerType, '/');
|
||||
|
||||
/** @var int|null */
|
||||
$listStart = null;
|
||||
|
||||
if ($type === 'ol') {
|
||||
/** @psalm-suppress PossiblyFalseArgument */
|
||||
$listStart = \intval(\strstr($matches[1], $markerType, true) ?: '0');
|
||||
|
||||
if (
|
||||
$listStart !== 1
|
||||
&& isset($Block)
|
||||
&& $Block instanceof Paragraph
|
||||
&& ! $Context->previousEmptyLines() > 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return new self(
|
||||
[!empty($text) ? Lines::fromTextLines($text, $indentOffset) : Lines::none()],
|
||||
$listStart,
|
||||
false,
|
||||
$Context->line()->indent(),
|
||||
$type,
|
||||
$marker,
|
||||
$afterMarkerSpaces,
|
||||
$markerType,
|
||||
$markerTypeRegex
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @return self|null
|
||||
*/
|
||||
public function advance(Context $Context, State $State)
|
||||
{
|
||||
if ($Context->previousEmptyLines() > 0 && \end($this->Lis)->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$newlines = \str_repeat("\n", $Context->previousEmptyLines());
|
||||
|
||||
$requiredIndent = $this->indent + \strlen($this->marker) + $this->afterMarkerSpaces;
|
||||
$isLoose = $this->isLoose;
|
||||
$indent = $Context->line()->indent();
|
||||
|
||||
$Lis = $this->Lis;
|
||||
|
||||
if ($Context->line()->indent() < $requiredIndent
|
||||
&& ((
|
||||
$this->type === 'ol'
|
||||
&& \preg_match('/^([0-9]++'.$this->markerTypeRegex.')([\t ]++.*|$)/', $Context->line()->text(), $matches)
|
||||
) || (
|
||||
$this->type === 'ul'
|
||||
&& \preg_match('/^('.$this->markerTypeRegex.')([\t ]++.*|$)/', $Context->line()->text(), $matches)
|
||||
))
|
||||
) {
|
||||
if ($Context->previousEmptyLines() > 0) {
|
||||
$Lis[\count($Lis) -1] = $Lis[\count($Lis) -1]->appendingBlankLines(1);
|
||||
|
||||
$isLoose = true;
|
||||
}
|
||||
|
||||
$marker = $matches[1];
|
||||
|
||||
$preAfterMarkerSpacesIndentOffset = $Context->line()->indentOffset() + $Context->line()->indent() + \strlen($marker);
|
||||
|
||||
$LineWithMarkerIndent = new Line(isset($matches[2]) ? $matches[2] : '', $preAfterMarkerSpacesIndentOffset);
|
||||
$indentAfterMarker = $LineWithMarkerIndent->indent();
|
||||
|
||||
if ($indentAfterMarker > 4) {
|
||||
$perceivedIndent = $indentAfterMarker -1;
|
||||
$afterMarkerSpaces = 1;
|
||||
} else {
|
||||
$perceivedIndent = 0;
|
||||
$afterMarkerSpaces = $indentAfterMarker;
|
||||
}
|
||||
|
||||
$indentOffset = $preAfterMarkerSpacesIndentOffset + $afterMarkerSpaces;
|
||||
$text = \str_repeat(' ', $perceivedIndent) . $LineWithMarkerIndent->text();
|
||||
|
||||
$Lis[] = Lines::fromTextLines($newlines . $text, $indentOffset);
|
||||
|
||||
return new self(
|
||||
$Lis,
|
||||
$this->listStart,
|
||||
$isLoose,
|
||||
$indent,
|
||||
$this->type,
|
||||
$marker,
|
||||
$afterMarkerSpaces,
|
||||
$this->markerType,
|
||||
$this->markerTypeRegex
|
||||
);
|
||||
} elseif ($Context->line()->indent() < $requiredIndent && self::build($Context, $State) !== null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($Context->line()->indent() >= $requiredIndent) {
|
||||
if ($Context->previousEmptyLines() > 0) {
|
||||
$Lis[\count($Lis) -1] = $Lis[\count($Lis) -1]->appendingBlankLines($Context->previousEmptyLines());
|
||||
|
||||
$isLoose = true;
|
||||
}
|
||||
|
||||
$text = $Context->line()->ltrimBodyUpto($requiredIndent);
|
||||
$indentOffset = $Context->line()->indentOffset() + \min($requiredIndent, $Context->line()->indent());
|
||||
|
||||
$Lis[\count($Lis) -1] = $Lis[\count($Lis) -1]->appendingTextLines($text, $indentOffset);
|
||||
|
||||
return new self(
|
||||
$Lis,
|
||||
$this->listStart,
|
||||
$isLoose,
|
||||
$this->indent,
|
||||
$this->type,
|
||||
$this->marker,
|
||||
$this->afterMarkerSpaces,
|
||||
$this->markerType,
|
||||
$this->markerTypeRegex
|
||||
);
|
||||
}
|
||||
|
||||
if (! $Context->previousEmptyLines() > 0) {
|
||||
$text = $Context->line()->ltrimBodyUpto($requiredIndent);
|
||||
|
||||
$Lis[\count($Lis) -1] = $Lis[\count($Lis) -1]->appendingTextLines(
|
||||
$newlines . \str_repeat(' ', $Context->line()->indent()) . $text,
|
||||
$Context->line()->indentOffset() + \min($requiredIndent, $Context->line()->indent())
|
||||
);
|
||||
|
||||
return new self(
|
||||
$Lis,
|
||||
$this->listStart,
|
||||
$isLoose,
|
||||
$this->indent,
|
||||
$this->type,
|
||||
$this->marker,
|
||||
$this->afterMarkerSpaces,
|
||||
$this->markerType,
|
||||
$this->markerTypeRegex
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: Block[], 1: State}[]
|
||||
*/
|
||||
public function items(State $State)
|
||||
{
|
||||
return \array_map(
|
||||
/** @return array{0: Block[], 1: State} */
|
||||
function (Lines $Lines) use ($State) {
|
||||
return Parsedown::blocks($Lines, $State);
|
||||
},
|
||||
$this->Lis
|
||||
);
|
||||
}
|
||||
|
||||
/** @return 'ol'|'ul' */
|
||||
public function type()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/** @return int|null */
|
||||
public function listStart()
|
||||
{
|
||||
return $this->listStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element */
|
||||
function (State $State) {
|
||||
$listStart = $this->listStart();
|
||||
|
||||
return new Element(
|
||||
$this->type(),
|
||||
(
|
||||
isset($listStart) && $listStart !== 1
|
||||
? ['start' => \strval($listStart)]
|
||||
: []
|
||||
),
|
||||
\array_map(
|
||||
/**
|
||||
* @param array{0: Block[], 1: State} $Item
|
||||
* @return Element
|
||||
* */
|
||||
function ($Item) {
|
||||
list($Blocks, $State) = $Item;
|
||||
|
||||
$StateRenderables = Parsedown::stateRenderablesFrom($Blocks);
|
||||
$Renderables = $State->applyTo($StateRenderables);
|
||||
|
||||
if (! $this->isLoose
|
||||
&& isset($Renderables[0])
|
||||
&& $Renderables[0] instanceof Element
|
||||
&& $Renderables[0]->name() === 'p'
|
||||
) {
|
||||
$Contents = $Renderables[0]->contents();
|
||||
unset($Renderables[0]);
|
||||
$Renderables = \array_merge($Contents ?: [], $Renderables);
|
||||
}
|
||||
|
||||
return new Element('li', [], $Renderables);
|
||||
},
|
||||
$this->items($State)
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
277
src/Components/Blocks/Table.php
Normal file
277
src/Components/Blocks/Table.php
Normal file
@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Blocks;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\AcquisitioningBlock;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\ContinuableBlock;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Context;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
/**
|
||||
* @psalm-type _Alignment='left'|'center'|'right'
|
||||
*/
|
||||
final class Table implements AcquisitioningBlock, ContinuableBlock
|
||||
{
|
||||
/** @var bool */
|
||||
private $acquired;
|
||||
|
||||
/** @var array<int, _Alignment|null> */
|
||||
private $alignments;
|
||||
|
||||
/** @var array<int, string> */
|
||||
private $headerCells;
|
||||
|
||||
/** @var array<int, array<int, string>> */
|
||||
private $rows;
|
||||
|
||||
/**
|
||||
* @param array<int, _Alignment|null> $alignments
|
||||
* @param array<int, string> $headerCells
|
||||
* @param array<int, array<int, string>> $rows
|
||||
* @param bool $acquired
|
||||
*/
|
||||
private function __construct($alignments, $headerCells, $rows, $acquired = false)
|
||||
{
|
||||
$this->alignments = $alignments;
|
||||
$this->headerCells = $headerCells;
|
||||
$this->rows = $rows;
|
||||
$this->acquired = $acquired;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @param Block|null $Block
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(
|
||||
Context $Context,
|
||||
State $State,
|
||||
Block $Block = null
|
||||
) {
|
||||
if (! isset($Block) || ! $Block instanceof Paragraph) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
\strpos($Block->text(), '|') === false
|
||||
&& \strpos($Context->line()->text(), '|') === false
|
||||
&& \strpos($Context->line()->text(), ':') === false
|
||||
|| \strpos($Block->text(), "\n") !== false
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (\chop($Context->line()->text(), ' -:|') !== '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
$alignments = self::parseAlignments($Context->line()->text());
|
||||
|
||||
if (! isset($alignments)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
# ~
|
||||
|
||||
$headerRow = \trim(\trim($Block->text()), '|');
|
||||
|
||||
$headerCells = \array_map('trim', \explode('|', $headerRow));
|
||||
|
||||
if (\count($headerCells) !== \count($alignments)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
# ~
|
||||
|
||||
return new self($alignments, $headerCells, [], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $Context
|
||||
* @param State $State
|
||||
* @return self|null
|
||||
*/
|
||||
public function advance(Context $Context, State $State)
|
||||
{
|
||||
if ($Context->previousEmptyLines() > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
\count($this->alignments) !== 1
|
||||
&& \strpos($Context->line()->text(), '|') === false
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = \trim(\trim($Context->line()->text()), '|');
|
||||
|
||||
if (
|
||||
! \preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches)
|
||||
|| ! isset($matches[0])
|
||||
|| ! \is_array($matches[0])
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cells = \array_map('trim', \array_slice($matches[0], 0, \count($this->alignments)));
|
||||
|
||||
return new self(
|
||||
$this->alignments,
|
||||
$this->headerCells,
|
||||
\array_merge($this->rows, [$cells])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dividerRow
|
||||
* @return array<int, _Alignment|null>|null
|
||||
*/
|
||||
private static function parseAlignments($dividerRow)
|
||||
{
|
||||
$dividerRow = \trim($dividerRow);
|
||||
$dividerRow = \trim($dividerRow, '|');
|
||||
|
||||
$dividerCells = \explode('|', $dividerRow);
|
||||
|
||||
/** @var array<int, _Alignment|null> */
|
||||
$alignments = [];
|
||||
|
||||
foreach ($dividerCells as $dividerCell) {
|
||||
$dividerCell = \trim($dividerCell);
|
||||
|
||||
if ($dividerCell === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var _Alignment|null */
|
||||
$alignment = null;
|
||||
|
||||
if (\substr($dividerCell, 0, 1) === ':') {
|
||||
$alignment = 'left';
|
||||
}
|
||||
|
||||
if (\substr($dividerCell, - 1) === ':') {
|
||||
$alignment = $alignment === 'left' ? 'center' : 'right';
|
||||
}
|
||||
|
||||
$alignments []= $alignment;
|
||||
}
|
||||
|
||||
return $alignments;
|
||||
}
|
||||
|
||||
/** @return bool */
|
||||
public function acquiredPrevious()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @return array<int, Inline[]> */
|
||||
public function headerRow(State $State)
|
||||
{
|
||||
return \array_map(
|
||||
/**
|
||||
* @param string $cell
|
||||
* @return Inline[]
|
||||
*/
|
||||
function ($cell) use ($State) {
|
||||
return Parsedown::inlines($cell, $State);
|
||||
},
|
||||
$this->headerCells
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<int, Inline[]>[] */
|
||||
public function rows(State $State)
|
||||
{
|
||||
return \array_map(
|
||||
/**
|
||||
* @param array<int, string> $cells
|
||||
* @return array<int, Inline[]>
|
||||
*/
|
||||
function ($cells) use ($State) {
|
||||
return \array_map(
|
||||
/**
|
||||
* @param string $cell
|
||||
* @return Inline[]
|
||||
*/
|
||||
function ($cell) use ($State) {
|
||||
return Parsedown::inlines($cell, $State);
|
||||
},
|
||||
$cells
|
||||
);
|
||||
},
|
||||
$this->rows
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<int, _Alignment|null> */
|
||||
public function alignments()
|
||||
{
|
||||
return $this->alignments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element */
|
||||
function (State $State) {
|
||||
return new Element('table', [], [
|
||||
new Element('thead', [], [new Element('tr', [], \array_map(
|
||||
/**
|
||||
* @param Inline[] $Cell
|
||||
* @param _Alignment|null $alignment
|
||||
* @return Element
|
||||
*/
|
||||
function ($Cell, $alignment) use ($State) {
|
||||
return new Element(
|
||||
'th',
|
||||
isset($alignment) ? ['style' => "text-align: $alignment;"] : [],
|
||||
$State->applyTo(Parsedown::stateRenderablesFrom($Cell))
|
||||
);
|
||||
},
|
||||
$this->headerRow($State),
|
||||
$this->alignments()
|
||||
))]),
|
||||
new Element('tbody', [], \array_map(
|
||||
/**
|
||||
* @param Inline[][] $Cells
|
||||
* @return Element
|
||||
*/
|
||||
function ($Cells) use ($State) {
|
||||
return new Element('tr', [], \array_map(
|
||||
/**
|
||||
* @param Inline[] $Cell
|
||||
* @param _Alignment|null $alignment
|
||||
* @return Element
|
||||
*/
|
||||
function ($Cell, $alignment) use ($State) {
|
||||
return new Element(
|
||||
'td',
|
||||
isset($alignment) ? ['style' => "text-align: $alignment;"] : [],
|
||||
$State->applyTo(Parsedown::stateRenderablesFrom($Cell))
|
||||
);
|
||||
},
|
||||
$Cells,
|
||||
\array_slice($this->alignments(), 0, \count($Cells))
|
||||
));
|
||||
},
|
||||
$this->rows($State)
|
||||
))
|
||||
]);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
16
src/Components/ContinuableBlock.php
Normal file
16
src/Components/ContinuableBlock.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?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);
|
||||
}
|
29
src/Components/Inline.php
Normal file
29
src/Components/Inline.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?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();
|
||||
}
|
83
src/Components/Inlines/Code.php
Normal file
83
src/Components/Inlines/Code.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Code implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param int $width
|
||||
*/
|
||||
private function __construct($text, $width)
|
||||
{
|
||||
$this->text = $text;
|
||||
$this->width = $width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
$marker = \substr($Excerpt->text(), 0, 1);
|
||||
|
||||
if ($marker !== '`') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (\preg_match(
|
||||
'/^(['.$marker.']++)(.*?)(?<!['.$marker.'])\1(?!'.$marker.')/s',
|
||||
$Excerpt->text(),
|
||||
$matches
|
||||
)) {
|
||||
$text = \str_replace("\n", ' ', $matches[2]);
|
||||
|
||||
$firstChar = \substr($text, 0, 1);
|
||||
$lastChar = \substr($text, -1);
|
||||
|
||||
if ($firstChar === ' ' && $lastChar === ' ') {
|
||||
$text = \substr(\substr($text, 1), 0, -1);
|
||||
}
|
||||
|
||||
return new self($text, \strlen($matches[0]));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function text()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Element
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Element('code', [], [new Text($this->text())]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->text());
|
||||
}
|
||||
}
|
84
src/Components/Inlines/Email.php
Normal file
84
src/Components/Inlines/Email.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Email implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/** @var string */
|
||||
private $url;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param string $url
|
||||
* @param int $width
|
||||
*/
|
||||
private function __construct($text, $url, $width)
|
||||
{
|
||||
$this->text = $text;
|
||||
$this->url = $url;
|
||||
$this->width = $width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
$hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?';
|
||||
|
||||
$commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@'
|
||||
. $hostnameLabel . '(?:\.' . $hostnameLabel . ')*';
|
||||
|
||||
if (\preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt->text(), $matches)) {
|
||||
$url = $matches[1];
|
||||
|
||||
if (! isset($matches[2])) {
|
||||
$url = "mailto:$url";
|
||||
}
|
||||
|
||||
return new self($matches[1], $url, \strlen($matches[0]));
|
||||
}
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function text()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function url()
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Element
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Element('a', ['href' => $this->url()], [new Text($this->text())]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->text());
|
||||
}
|
||||
}
|
102
src/Components/Inlines/Emphasis.php
Normal file
102
src/Components/Inlines/Emphasis.php
Normal file
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Emphasis implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/** @var 'em'|'strong' */
|
||||
private $type;
|
||||
|
||||
/** @var array{*: string, _: string} */
|
||||
private static $STRONG_REGEX = [
|
||||
'*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s',
|
||||
'_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us',
|
||||
];
|
||||
|
||||
/** @var array{*: string, _: string} */
|
||||
private static $EM_REGEX = [
|
||||
'*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
|
||||
'_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param 'em'|'strong' $type
|
||||
* @param int $width
|
||||
*/
|
||||
private function __construct($text, $type, $width)
|
||||
{
|
||||
$this->text = $text;
|
||||
$this->type = $type;
|
||||
$this->width = $width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
$marker = \substr($Excerpt->text(), 0, 1);
|
||||
|
||||
if ($marker !== '*' && $marker !== '_') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (\preg_match(self::$STRONG_REGEX[$marker], $Excerpt->text(), $matches)) {
|
||||
$emphasis = 'strong';
|
||||
} elseif (\preg_match(self::$EM_REGEX[$marker], $Excerpt->text(), $matches)) {
|
||||
$emphasis = 'em';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self($matches[1], $emphasis, \strlen($matches[0]));
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function text()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element */
|
||||
function (State $State) {
|
||||
return new Element(
|
||||
$this->type,
|
||||
[],
|
||||
$State->applyTo(Parsedown::line($this->text(), $State))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->text());
|
||||
}
|
||||
}
|
66
src/Components/Inlines/EscapeSequence.php
Normal file
66
src/Components/Inlines/EscapeSequence.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class EscapeSequence implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
const SPECIALS = '!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~';
|
||||
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
*/
|
||||
private function __construct($text)
|
||||
{
|
||||
$this->text = $text;
|
||||
$this->width = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
$char = \substr($Excerpt->text(), 1, 1);
|
||||
|
||||
if ($char !== '' && \strpbrk($char, self::SPECIALS) !== false) {
|
||||
return new self($char);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function char()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Text($this->char());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->char());
|
||||
}
|
||||
}
|
88
src/Components/Inlines/HardBreak.php
Normal file
88
src/Components/Inlines/HardBreak.php
Normal file
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\BacktrackingInline;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class HardBreak implements BacktrackingInline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var int */
|
||||
private $position;
|
||||
|
||||
/**
|
||||
* @param int $width
|
||||
* @param int $position
|
||||
*/
|
||||
private function __construct($width, $position)
|
||||
{
|
||||
$this->width = $width;
|
||||
$this->position = $position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
$marker = \substr($Excerpt->text(), 0, 1);
|
||||
|
||||
if ($marker !== "\n") {
|
||||
return null;
|
||||
}
|
||||
|
||||
$context = $Excerpt->context();
|
||||
$offset = $Excerpt->offset();
|
||||
|
||||
if (\substr($context, $offset -1, 1) === '\\') {
|
||||
$contentLen = $offset -1;
|
||||
|
||||
return new self($offset - $contentLen, $contentLen);
|
||||
}
|
||||
|
||||
if (\substr($context, $offset -2, 2) === ' ') {
|
||||
$trimTrailingWhitespace = \rtrim(\substr($context, 0, $offset));
|
||||
$contentLen = \strlen($trimTrailingWhitespace);
|
||||
|
||||
return new self($offset - $contentLen, $contentLen);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an integer to declare that the inline should be treated as if it
|
||||
* started from that position in the excerpt given to static::build.
|
||||
* Return null to use the excerpt offset value.
|
||||
* @return int|null
|
||||
* */
|
||||
public function modifyStartPositionTo()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Element
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return Element::selfClosing('br', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text("\n");
|
||||
}
|
||||
}
|
121
src/Components/Inlines/Image.php
Normal file
121
src/Components/Inlines/Image.php
Normal file
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Configurables\SafeMode;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Html\Sanitisation\UrlSanitiser;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
/** @psalm-type _Metadata=array{href: string, title?: string} */
|
||||
final class Image implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var Link */
|
||||
private $Link;
|
||||
|
||||
/**
|
||||
* @param Link $Link
|
||||
*/
|
||||
private function __construct(Link $Link)
|
||||
{
|
||||
$this->Link = $Link;
|
||||
$this->width = $Link->width() + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
if (\substr($Excerpt->text(), 0, 1) !== '!') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$Excerpt = $Excerpt->addingToOffset(1);
|
||||
|
||||
$Link = Link::build($Excerpt, $State);
|
||||
|
||||
if (! isset($Link)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self($Link);
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function label()
|
||||
{
|
||||
return $this->Link->label();
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function url()
|
||||
{
|
||||
return $this->Link->url();
|
||||
}
|
||||
|
||||
/** @return string|null */
|
||||
public function title()
|
||||
{
|
||||
return $this->Link->title();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element|Text>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element|Text */
|
||||
function (State $State) {
|
||||
$attributes = [
|
||||
'src' => $this->url(),
|
||||
'alt' => \array_reduce(
|
||||
Parsedown::inlines($this->label(), $State),
|
||||
/**
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
function ($text, Inline $Inline) {
|
||||
return (
|
||||
$text
|
||||
. $Inline->bestPlaintext()->getStringBacking()
|
||||
);
|
||||
},
|
||||
''
|
||||
),
|
||||
];
|
||||
|
||||
$title = $this->title();
|
||||
|
||||
if (isset($title)) {
|
||||
$attributes['title'] = $title;
|
||||
}
|
||||
|
||||
if ($State->get(SafeMode::class)->isEnabled()) {
|
||||
$attributes['src'] = UrlSanitiser::filter($attributes['src']);
|
||||
}
|
||||
|
||||
return Element::selfClosing('img', $attributes);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->label());
|
||||
}
|
||||
}
|
155
src/Components/Inlines/Link.php
Normal file
155
src/Components/Inlines/Link.php
Normal file
@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Configurables\DefinitionBook;
|
||||
use Erusev\Parsedown\Configurables\InlineTypes;
|
||||
use Erusev\Parsedown\Configurables\SafeMode;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Html\Sanitisation\UrlSanitiser;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
/** @psalm-type _Metadata=array{href: string, title?: string} */
|
||||
final class Link implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var string */
|
||||
private $label;
|
||||
|
||||
/** @var string */
|
||||
private $url;
|
||||
|
||||
/** @var string|null */
|
||||
private $title;
|
||||
|
||||
/**
|
||||
* @param string $label
|
||||
* @param string $url
|
||||
* @param string|null $title
|
||||
* @param int $width
|
||||
*/
|
||||
private function __construct($label, $url, $title, $width)
|
||||
{
|
||||
$this->label = $label;
|
||||
$this->url = $url;
|
||||
$this->title = $title;
|
||||
$this->width = $width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
$remainder = $Excerpt->text();
|
||||
|
||||
if (! \preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$label = $matches[1];
|
||||
|
||||
$width = \strlen($matches[0]);
|
||||
|
||||
$remainder = \substr($remainder, $width);
|
||||
|
||||
if (\preg_match('/^[(]\s*+(?:((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+)?[)]/', $remainder, $matches)) {
|
||||
$url = isset($matches[1]) ? $matches[1] : '';
|
||||
$title = isset($matches[2]) ? \substr($matches[2], 1, - 1) : null;
|
||||
|
||||
$width += \strlen($matches[0]);
|
||||
|
||||
return new self($label, $url, $title, $width);
|
||||
} else {
|
||||
if (\preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) {
|
||||
$definition = \strlen($matches[1]) ? $matches[1] : $label;
|
||||
$definition = \strtolower($definition);
|
||||
|
||||
$width += \strlen($matches[0]);
|
||||
} else {
|
||||
$definition = \strtolower($label);
|
||||
}
|
||||
|
||||
$definition = \preg_replace('/\s++/', ' ', \trim($definition));
|
||||
|
||||
$data = $State->get(DefinitionBook::class)->lookup($definition);
|
||||
|
||||
if (! isset($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$url = $data['url'];
|
||||
$title = isset($data['title']) ? $data['title'] : null;
|
||||
|
||||
return new self($label, $url, $title, $width);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function label()
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function url()
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/** @return string|null */
|
||||
public function title()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element|Text>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element|Text */
|
||||
function (State $State) {
|
||||
$attributes = ['href' => $this->url()];
|
||||
|
||||
$title = $this->title();
|
||||
|
||||
if (isset($title)) {
|
||||
$attributes['title'] = $title;
|
||||
}
|
||||
|
||||
if ($State->get(SafeMode::class)->isEnabled()) {
|
||||
$attributes['href'] = UrlSanitiser::filter($attributes['href']);
|
||||
}
|
||||
|
||||
$State = $State->setting(
|
||||
$State->get(InlineTypes::class)->removing([Url::class])
|
||||
);
|
||||
|
||||
return new Element(
|
||||
'a',
|
||||
$attributes,
|
||||
$State->applyTo(Parsedown::line($this->label(), $State))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->label());
|
||||
}
|
||||
}
|
84
src/Components/Inlines/Markup.php
Normal file
84
src/Components/Inlines/Markup.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Configurables\SafeMode;
|
||||
use Erusev\Parsedown\Html\Renderables\RawHtml;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Markup implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
const HTML_ATT_REGEX = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+';
|
||||
|
||||
/** @var string */
|
||||
private $html;
|
||||
|
||||
/**
|
||||
* @param string $html
|
||||
*/
|
||||
private function __construct($html)
|
||||
{
|
||||
$this->html = $html;
|
||||
$this->width = \strlen($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
$secondChar = \substr($Excerpt->text(), 1, 1);
|
||||
|
||||
if ($secondChar === '/' && \preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt->text(), $matches)) {
|
||||
return new self($matches[0]);
|
||||
}
|
||||
|
||||
if ($secondChar === '!' && \preg_match('/^<!---?[^>-](?:-?+[^-])*-->/s', $Excerpt->text(), $matches)) {
|
||||
return new self($matches[0]);
|
||||
}
|
||||
|
||||
if ($secondChar !== ' ' && \preg_match('/^<\w[\w-]*+(?:[ ]*+'.self::HTML_ATT_REGEX.')*+[ ]*+\/?>/s', $Excerpt->text(), $matches)) {
|
||||
return new self($matches[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function html()
|
||||
{
|
||||
return $this->html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Text|RawHtml>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Text|RawHtml */
|
||||
function (State $State) {
|
||||
if ($State->get(SafeMode::class)->isEnabled()) {
|
||||
return new Text($this->html());
|
||||
} else {
|
||||
return new RawHtml($this->html());
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->html());
|
||||
}
|
||||
}
|
58
src/Components/Inlines/PlainText.php
Normal file
58
src/Components/Inlines/PlainText.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class PlainText implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
*/
|
||||
private function __construct($text)
|
||||
{
|
||||
$this->text = $text;
|
||||
$this->width = \strlen($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
return new self($Excerpt->text());
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function text()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Text($this->text());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->text());
|
||||
}
|
||||
}
|
98
src/Components/Inlines/SoftBreak.php
Normal file
98
src/Components/Inlines/SoftBreak.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\BacktrackingInline;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Configurables\Breaks;
|
||||
use Erusev\Parsedown\Html\Renderables\Container;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class SoftBreak implements BacktrackingInline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var int */
|
||||
private $position;
|
||||
|
||||
/**
|
||||
* @param int $width
|
||||
* @param int $position
|
||||
*/
|
||||
private function __construct($width, $position)
|
||||
{
|
||||
$this->width = $width;
|
||||
$this->position = $position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
$marker = \substr($Excerpt->text(), 0, 1);
|
||||
|
||||
if ($marker !== "\n") {
|
||||
return null;
|
||||
}
|
||||
|
||||
$context = $Excerpt->context();
|
||||
$offset = $Excerpt->offset();
|
||||
|
||||
$trimTrailingWhitespaceBefore = \rtrim(\substr($context, 0, $offset), ' ');
|
||||
$trimLeadingWhitespaceAfter = \ltrim(\substr($context, $offset + 1), ' ');
|
||||
$contentLenBefore = \strlen($trimTrailingWhitespaceBefore);
|
||||
$contentLenAfter = \strlen($trimLeadingWhitespaceAfter);
|
||||
|
||||
$originalLen = \strlen($context);
|
||||
$afterWidth = $originalLen - $offset - $contentLenAfter;
|
||||
|
||||
return new self($offset + $afterWidth - $contentLenBefore, $contentLenBefore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an integer to declare that the inline should be treated as if it
|
||||
* started from that position in the excerpt given to static::build.
|
||||
* Return null to use the excerpt offset value.
|
||||
* @return int|null
|
||||
* */
|
||||
public function modifyStartPositionTo()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Text|Container>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Text|Container */
|
||||
function (State $State) {
|
||||
if ($State->get(Breaks::class)->isEnabled()) {
|
||||
return new Container([
|
||||
Element::selfClosing('br', []),
|
||||
new Text("\n")
|
||||
]);
|
||||
}
|
||||
|
||||
return new Text("\n");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text("\n");
|
||||
}
|
||||
}
|
65
src/Components/Inlines/SpecialCharacter.php
Normal file
65
src/Components/Inlines/SpecialCharacter.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\RawHtml;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class SpecialCharacter implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var string */
|
||||
private $charCodeHtml;
|
||||
|
||||
/**
|
||||
* @param string $charCodeHtml
|
||||
*/
|
||||
private function __construct($charCodeHtml)
|
||||
{
|
||||
$this->charCodeHtml = $charCodeHtml;
|
||||
$this->width = \strlen($charCodeHtml) + 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
if (\preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt->text(), $matches)) {
|
||||
return new self($matches[1]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function charCode()
|
||||
{
|
||||
return $this->charCodeHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RawHtml
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new RawHtml(
|
||||
'&' . (new Text($this->charCode()))->getHtml() . ';'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text('&'.$this->charCode().';');
|
||||
}
|
||||
}
|
77
src/Components/Inlines/Strikethrough.php
Normal file
77
src/Components/Inlines/Strikethrough.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\Handler;
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsedown;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Strikethrough implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param int $width
|
||||
*/
|
||||
private function __construct($text, $width)
|
||||
{
|
||||
$this->text = $text;
|
||||
$this->width = $width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
$text = $Excerpt->text();
|
||||
|
||||
if (\preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $text, $matches)) {
|
||||
return new self($matches[1], \strlen($matches[0]));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function text()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Handler<Element>
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Handler(
|
||||
/** @return Element */
|
||||
function (State $State) {
|
||||
return new Element(
|
||||
'del',
|
||||
[],
|
||||
$State->applyTo(Parsedown::line($this->text(), $State))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->text());
|
||||
}
|
||||
}
|
85
src/Components/Inlines/Url.php
Normal file
85
src/Components/Inlines/Url.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\BacktrackingInline;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class Url implements BacktrackingInline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var string */
|
||||
private $url;
|
||||
|
||||
/** @var int */
|
||||
private $position;
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param int $position
|
||||
*/
|
||||
private function __construct($url, $position)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->width = \strlen($url);
|
||||
$this->position = $position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
if (\preg_match(
|
||||
'/(?<=^|\s|[*_~(])https?+:[\/]{2}[^\s<]+\b\/*+/ui',
|
||||
$Excerpt->context(),
|
||||
$matches,
|
||||
\PREG_OFFSET_CAPTURE
|
||||
)) {
|
||||
return new self($matches[0][0], \intval($matches[0][1]));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an integer to declare that the inline should be treated as if it
|
||||
* started from that position in the excerpt given to static::build.
|
||||
* Return null to use the excerpt offset value.
|
||||
* @return int|null
|
||||
* */
|
||||
public function modifyStartPositionTo()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function url()
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Element
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Element('a', ['href' => $this->url()], [new Text($this->url())]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->url());
|
||||
}
|
||||
}
|
64
src/Components/Inlines/UrlTag.php
Normal file
64
src/Components/Inlines/UrlTag.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Html\Renderables\Element;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
final class UrlTag implements Inline
|
||||
{
|
||||
use WidthTrait;
|
||||
|
||||
/** @var string */
|
||||
private $url;
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param int $width
|
||||
*/
|
||||
private function __construct($url, $width)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->width = $width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Excerpt $Excerpt
|
||||
* @param State $State
|
||||
* @return static|null
|
||||
*/
|
||||
public static function build(Excerpt $Excerpt, State $State)
|
||||
{
|
||||
if (\preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt->text(), $matches)) {
|
||||
return new self($matches[1], \strlen($matches[0]));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function url()
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Element
|
||||
*/
|
||||
public function stateRenderable()
|
||||
{
|
||||
return new Element('a', ['href' => $this->url()], [new Text($this->url())]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Text
|
||||
*/
|
||||
public function bestPlaintext()
|
||||
{
|
||||
return new Text($this->url);
|
||||
}
|
||||
}
|
15
src/Components/Inlines/WidthTrait.php
Normal file
15
src/Components/Inlines/WidthTrait.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components\Inlines;
|
||||
|
||||
trait WidthTrait
|
||||
{
|
||||
/** @var int */
|
||||
private $width;
|
||||
|
||||
/** @return int */
|
||||
public function width()
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
}
|
11
src/Components/StateUpdatingBlock.php
Normal file
11
src/Components/StateUpdatingBlock.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Components;
|
||||
|
||||
use Erusev\Parsedown\State;
|
||||
|
||||
interface StateUpdatingBlock extends Block
|
||||
{
|
||||
/** @return State */
|
||||
public function latestState();
|
||||
}
|
9
src/Configurable.php
Normal file
9
src/Configurable.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown;
|
||||
|
||||
interface Configurable
|
||||
{
|
||||
/** @return static */
|
||||
public static function initial();
|
||||
}
|
194
src/Configurables/BlockTypes.php
Normal file
194
src/Configurables/BlockTypes.php
Normal file
@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Configurables;
|
||||
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\Blocks\BlockQuote;
|
||||
use Erusev\Parsedown\Components\Blocks\FencedCode;
|
||||
use Erusev\Parsedown\Components\Blocks\Header;
|
||||
use Erusev\Parsedown\Components\Blocks\IndentedCode;
|
||||
use Erusev\Parsedown\Components\Blocks\Markup as BlockMarkup;
|
||||
use Erusev\Parsedown\Components\Blocks\Reference;
|
||||
use Erusev\Parsedown\Components\Blocks\Rule;
|
||||
use Erusev\Parsedown\Components\Blocks\SetextHeader;
|
||||
use Erusev\Parsedown\Components\Blocks\Table;
|
||||
use Erusev\Parsedown\Components\Blocks\TList;
|
||||
use Erusev\Parsedown\Configurable;
|
||||
|
||||
final class BlockTypes implements Configurable
|
||||
{
|
||||
/** @var array<array-key, array<int, class-string<Block>>> */
|
||||
private static $defaultBlockTypes = [
|
||||
'#' => [Header::class],
|
||||
'*' => [Rule::class, TList::class],
|
||||
'+' => [TList::class],
|
||||
'-' => [SetextHeader::class, Table::class, Rule::class, TList::class],
|
||||
'0' => [TList::class],
|
||||
'1' => [TList::class],
|
||||
'2' => [TList::class],
|
||||
'3' => [TList::class],
|
||||
'4' => [TList::class],
|
||||
'5' => [TList::class],
|
||||
'6' => [TList::class],
|
||||
'7' => [TList::class],
|
||||
'8' => [TList::class],
|
||||
'9' => [TList::class],
|
||||
':' => [Table::class],
|
||||
'<' => [BlockMarkup::class],
|
||||
'=' => [SetextHeader::class],
|
||||
'>' => [BlockQuote::class],
|
||||
'[' => [Reference::class],
|
||||
'_' => [Rule::class],
|
||||
'`' => [FencedCode::class],
|
||||
'|' => [Table::class],
|
||||
'~' => [FencedCode::class],
|
||||
];
|
||||
|
||||
/** @var array<int, class-string<Block>> */
|
||||
private static $defaultUnmarkedBlockTypes = [
|
||||
IndentedCode::class,
|
||||
];
|
||||
|
||||
/** @var array<array-key, array<int, class-string<Block>>> */
|
||||
private $blockTypes;
|
||||
|
||||
/** @var array<int, class-string<Block>> */
|
||||
private $unmarkedBlockTypes;
|
||||
|
||||
/**
|
||||
* @param array<array-key, array<int, class-string<Block>>> $blockTypes
|
||||
* @param array<int, class-string<Block>> $unmarkedBlockTypes
|
||||
*/
|
||||
public function __construct(array $blockTypes, array $unmarkedBlockTypes)
|
||||
{
|
||||
$this->blockTypes = $blockTypes;
|
||||
$this->unmarkedBlockTypes = $unmarkedBlockTypes;
|
||||
}
|
||||
|
||||
/** @return self */
|
||||
public static function initial()
|
||||
{
|
||||
return new self(
|
||||
self::$defaultBlockTypes,
|
||||
self::$defaultUnmarkedBlockTypes
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $marker
|
||||
* @param array<int, class-string<Block>> $newBlockTypes
|
||||
* @return self
|
||||
*/
|
||||
public function settingMarked($marker, array $newBlockTypes)
|
||||
{
|
||||
$blockTypes = $this->blockTypes;
|
||||
$blockTypes[$marker] = $newBlockTypes;
|
||||
|
||||
return new self($blockTypes, $this->unmarkedBlockTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $marker
|
||||
* @param array<int, class-string<Block>> $newBlockTypes
|
||||
* @return self
|
||||
*/
|
||||
public function addingMarkedHighPrecedence($marker, array $newBlockTypes)
|
||||
{
|
||||
return $this->settingMarked(
|
||||
$marker,
|
||||
\array_merge(
|
||||
$newBlockTypes,
|
||||
isset($this->blockTypes[$marker]) ? $this->blockTypes[$marker] : []
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $marker
|
||||
* @param array<int, class-string<Block>> $newBlockTypes
|
||||
* @return self
|
||||
*/
|
||||
public function addingMarkedLowPrecedence($marker, array $newBlockTypes)
|
||||
{
|
||||
return $this->settingMarked(
|
||||
$marker,
|
||||
\array_merge(
|
||||
isset($this->blockTypes[$marker]) ? $this->blockTypes[$marker] : [],
|
||||
$newBlockTypes
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, class-string<Block>> $newUnmarkedBlockTypes
|
||||
* @return self
|
||||
*/
|
||||
public function settingUnmarked(array $newUnmarkedBlockTypes)
|
||||
{
|
||||
return new self($this->blockTypes, $newUnmarkedBlockTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, class-string<Block>> $newBlockTypes
|
||||
* @return self
|
||||
*/
|
||||
public function addingUnmarkedHighPrecedence(array $newBlockTypes)
|
||||
{
|
||||
return $this->settingUnmarked(
|
||||
\array_merge($newBlockTypes, $this->unmarkedBlockTypes)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, class-string<Block>> $newBlockTypes
|
||||
* @return self
|
||||
*/
|
||||
public function addingUnmarkedLowPrecedence(array $newBlockTypes)
|
||||
{
|
||||
return $this->settingUnmarked(
|
||||
\array_merge($this->unmarkedBlockTypes, $newBlockTypes)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, class-string<Block>> $removeBlockTypes
|
||||
* @return self
|
||||
*/
|
||||
public function removing(array $removeBlockTypes)
|
||||
{
|
||||
return new self(
|
||||
\array_map(
|
||||
/**
|
||||
* @param array<int, class-string<Block>> $blockTypes
|
||||
* @return array<int, class-string<Block>>
|
||||
*/
|
||||
function ($blockTypes) use ($removeBlockTypes) {
|
||||
return \array_diff($blockTypes, $removeBlockTypes);
|
||||
},
|
||||
$this->blockTypes
|
||||
),
|
||||
\array_diff($this->unmarkedBlockTypes, $removeBlockTypes)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $marker
|
||||
* @return array<int, class-string<Block>>
|
||||
*/
|
||||
public function markedBy($marker)
|
||||
{
|
||||
if (isset($this->blockTypes[$marker])) {
|
||||
return $this->blockTypes[$marker];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, class-string<Block>>
|
||||
*/
|
||||
public function unmarked()
|
||||
{
|
||||
return $this->unmarkedBlockTypes;
|
||||
}
|
||||
}
|
35
src/Configurables/BooleanConfigurable.php
Normal file
35
src/Configurables/BooleanConfigurable.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
10
src/Configurables/Breaks.php
Normal file
10
src/Configurables/Breaks.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Configurables;
|
||||
|
||||
use Erusev\Parsedown\Configurable;
|
||||
|
||||
final class Breaks implements Configurable
|
||||
{
|
||||
use BooleanConfigurable;
|
||||
}
|
54
src/Configurables/DefinitionBook.php
Normal file
54
src/Configurables/DefinitionBook.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Configurables;
|
||||
|
||||
use Erusev\Parsedown\Configurable;
|
||||
|
||||
/**
|
||||
* @psalm-type _Data=array{url: string, title: string|null}
|
||||
*/
|
||||
final class DefinitionBook implements Configurable
|
||||
{
|
||||
/** @var array<string, _Data> */
|
||||
private $book;
|
||||
|
||||
/**
|
||||
* @param array<string, _Data> $book
|
||||
*/
|
||||
public function __construct(array $book = [])
|
||||
{
|
||||
$this->book = $book;
|
||||
}
|
||||
|
||||
/** @return self */
|
||||
public static function initial()
|
||||
{
|
||||
return new self;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @param _Data $data
|
||||
* @return self
|
||||
*/
|
||||
public function setting($id, array $data)
|
||||
{
|
||||
$book = $this->book;
|
||||
$book[$id] = $data;
|
||||
|
||||
return new self($book);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @return _Data|null
|
||||
*/
|
||||
public function lookup($id)
|
||||
{
|
||||
if (isset($this->book[$id])) {
|
||||
return $this->book[$id];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
140
src/Configurables/InlineTypes.php
Normal file
140
src/Configurables/InlineTypes.php
Normal file
@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Configurables;
|
||||
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Components\Inlines\Code;
|
||||
use Erusev\Parsedown\Components\Inlines\Email;
|
||||
use Erusev\Parsedown\Components\Inlines\Emphasis;
|
||||
use Erusev\Parsedown\Components\Inlines\EscapeSequence;
|
||||
use Erusev\Parsedown\Components\Inlines\HardBreak;
|
||||
use Erusev\Parsedown\Components\Inlines\Image;
|
||||
use Erusev\Parsedown\Components\Inlines\Link;
|
||||
use Erusev\Parsedown\Components\Inlines\Markup as InlineMarkup;
|
||||
use Erusev\Parsedown\Components\Inlines\SoftBreak;
|
||||
use Erusev\Parsedown\Components\Inlines\SpecialCharacter;
|
||||
use Erusev\Parsedown\Components\Inlines\Strikethrough;
|
||||
use Erusev\Parsedown\Components\Inlines\Url;
|
||||
use Erusev\Parsedown\Components\Inlines\UrlTag;
|
||||
use Erusev\Parsedown\Configurable;
|
||||
|
||||
final class InlineTypes implements Configurable
|
||||
{
|
||||
/** @var array<array-key, array<int, class-string<Inline>>> */
|
||||
private static $defaultInlineTypes = [
|
||||
'!' => [Image::class],
|
||||
'*' => [Emphasis::class],
|
||||
'_' => [Emphasis::class],
|
||||
'&' => [SpecialCharacter::class],
|
||||
'[' => [Link::class],
|
||||
':' => [Url::class],
|
||||
'<' => [UrlTag::class, Email::class, InlineMarkup::class],
|
||||
'`' => [Code::class],
|
||||
'~' => [Strikethrough::class],
|
||||
'\\' => [EscapeSequence::class],
|
||||
"\n" => [HardBreak::class, SoftBreak::class],
|
||||
];
|
||||
|
||||
/** @var array<array-key, array<int, class-string<Inline>>> */
|
||||
private $inlineTypes;
|
||||
|
||||
/** @var string */
|
||||
private $inlineMarkers;
|
||||
|
||||
/**
|
||||
* @param array<array-key, array<int, class-string<Inline>>> $inlineTypes
|
||||
*/
|
||||
public function __construct(array $inlineTypes)
|
||||
{
|
||||
$this->inlineTypes = $inlineTypes;
|
||||
$this->inlineMarkers = \implode('', \array_keys($inlineTypes));
|
||||
}
|
||||
|
||||
/** @return self */
|
||||
public static function initial()
|
||||
{
|
||||
return new self(self::$defaultInlineTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $marker
|
||||
* @param array<int, class-string<Inline>> $newInlineTypes
|
||||
* @return self
|
||||
*/
|
||||
public function setting($marker, array $newInlineTypes)
|
||||
{
|
||||
$inlineTypes = $this->inlineTypes;
|
||||
$inlineTypes[$marker] = $newInlineTypes;
|
||||
|
||||
return new self($inlineTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $marker
|
||||
* @param array<int, class-string<Inline>> $newInlineTypes
|
||||
* @return self
|
||||
*/
|
||||
public function addingHighPrecedence($marker, array $newInlineTypes)
|
||||
{
|
||||
return $this->setting(
|
||||
$marker,
|
||||
\array_merge(
|
||||
$newInlineTypes,
|
||||
isset($this->inlineTypes[$marker]) ? $this->inlineTypes[$marker] : []
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $marker
|
||||
* @param array<int, class-string<Inline>> $newInlineTypes
|
||||
* @return self
|
||||
*/
|
||||
public function addingLowPrecedence($marker, array $newInlineTypes)
|
||||
{
|
||||
return $this->setting(
|
||||
$marker,
|
||||
\array_merge(
|
||||
isset($this->inlineTypes[$marker]) ? $this->inlineTypes[$marker] : [],
|
||||
$newInlineTypes
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, class-string<Inline>> $removeInlineTypes
|
||||
* @return self
|
||||
*/
|
||||
public function removing(array $removeInlineTypes)
|
||||
{
|
||||
return new self(\array_map(
|
||||
/**
|
||||
* @param array<int, class-string<Inline>> $inlineTypes
|
||||
* @return array<int, class-string<Inline>>
|
||||
*/
|
||||
function ($inlineTypes) use ($removeInlineTypes) {
|
||||
return \array_diff($inlineTypes, $removeInlineTypes);
|
||||
},
|
||||
$this->inlineTypes
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $marker
|
||||
* @return array<int, class-string<Inline>>
|
||||
*/
|
||||
public function markedBy($marker)
|
||||
{
|
||||
if (isset($this->inlineTypes[$marker])) {
|
||||
return $this->inlineTypes[$marker];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function markers()
|
||||
{
|
||||
return $this->inlineMarkers;
|
||||
}
|
||||
}
|
51
src/Configurables/RecursionLimiter.php
Normal file
51
src/Configurables/RecursionLimiter.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Configurables;
|
||||
|
||||
use Erusev\Parsedown\Configurable;
|
||||
|
||||
final class RecursionLimiter implements Configurable
|
||||
{
|
||||
/** @var int */
|
||||
private $maxDepth;
|
||||
|
||||
/** @var int */
|
||||
private $currentDepth;
|
||||
|
||||
/**
|
||||
* @param int $maxDepth
|
||||
* @param int $currentDepth
|
||||
*/
|
||||
private function __construct($maxDepth, $currentDepth)
|
||||
{
|
||||
$this->maxDepth = $maxDepth;
|
||||
$this->currentDepth = $currentDepth;
|
||||
}
|
||||
|
||||
/** @return self */
|
||||
public static function initial()
|
||||
{
|
||||
return self::maxDepth(256);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $maxDepth
|
||||
* @return self
|
||||
*/
|
||||
public static function maxDepth($maxDepth)
|
||||
{
|
||||
return new self($maxDepth, 0);
|
||||
}
|
||||
|
||||
/** @return self */
|
||||
public function increment()
|
||||
{
|
||||
return new self($this->maxDepth, $this->currentDepth + 1);
|
||||
}
|
||||
|
||||
/** @return bool */
|
||||
public function isDepthExceeded()
|
||||
{
|
||||
return ($this->maxDepth < $this->currentDepth);
|
||||
}
|
||||
}
|
10
src/Configurables/SafeMode.php
Normal file
10
src/Configurables/SafeMode.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Configurables;
|
||||
|
||||
use Erusev\Parsedown\Configurable;
|
||||
|
||||
final class SafeMode implements Configurable
|
||||
{
|
||||
use BooleanConfigurable;
|
||||
}
|
10
src/Configurables/StrictMode.php
Normal file
10
src/Configurables/StrictMode.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Configurables;
|
||||
|
||||
use Erusev\Parsedown\Configurable;
|
||||
|
||||
final class StrictMode implements Configurable
|
||||
{
|
||||
use BooleanConfigurable;
|
||||
}
|
11
src/Html/Renderable.php
Normal file
11
src/Html/Renderable.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Html;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
|
||||
interface Renderable extends StateRenderable
|
||||
{
|
||||
/** @return string */
|
||||
public function getHtml();
|
||||
}
|
17
src/Html/Renderables/CanonicalStateRenderable.php
Normal file
17
src/Html/Renderables/CanonicalStateRenderable.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
47
src/Html/Renderables/Container.php
Normal file
47
src/Html/Renderables/Container.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Html\Renderables;
|
||||
|
||||
use Erusev\Parsedown\Html\Renderable;
|
||||
|
||||
final class Container implements Renderable
|
||||
{
|
||||
use CanonicalStateRenderable;
|
||||
|
||||
/** @var Renderable[] */
|
||||
private $Contents;
|
||||
|
||||
/**
|
||||
* @param Renderable[] $Contents
|
||||
*/
|
||||
public function __construct($Contents)
|
||||
{
|
||||
$this->Contents = $Contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Renderable[]
|
||||
*/
|
||||
public function contents()
|
||||
{
|
||||
return $this->Contents;
|
||||
}
|
||||
|
||||
|
||||
/** @return string */
|
||||
public function getHtml()
|
||||
{
|
||||
return \array_reduce(
|
||||
$this->Contents,
|
||||
/**
|
||||
* @param string $html
|
||||
* @param Renderable $Renderable
|
||||
* @return string
|
||||
*/
|
||||
function ($html, Renderable $Renderable) {
|
||||
return $html . $Renderable->getHtml();
|
||||
},
|
||||
''
|
||||
);
|
||||
}
|
||||
}
|
195
src/Html/Renderables/Element.php
Normal file
195
src/Html/Renderables/Element.php
Normal file
@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Html\Renderables;
|
||||
|
||||
use Erusev\Parsedown\Html\Renderable;
|
||||
use Erusev\Parsedown\Html\Sanitisation\CharacterFilter;
|
||||
use Erusev\Parsedown\Html\Sanitisation\Escaper;
|
||||
|
||||
final class Element implements Renderable
|
||||
{
|
||||
use CanonicalStateRenderable;
|
||||
|
||||
/** @var array<string, true> */
|
||||
public static $TEXT_LEVEL_ELEMENTS = [
|
||||
'a' => true,
|
||||
'b' => true,
|
||||
'i' => true,
|
||||
'q' => true,
|
||||
's' => true,
|
||||
'u' => true,
|
||||
|
||||
'br' => true,
|
||||
'em' => true,
|
||||
'rp' => true,
|
||||
'rt' => true,
|
||||
'tt' => true,
|
||||
'xm' => true,
|
||||
|
||||
'bdo' => true,
|
||||
'big' => true,
|
||||
'del' => true,
|
||||
'img' => true,
|
||||
'ins' => true,
|
||||
'kbd' => true,
|
||||
'sub' => true,
|
||||
'sup' => true,
|
||||
'var' => true,
|
||||
'wbr' => true,
|
||||
|
||||
'abbr' => true,
|
||||
'cite' => true,
|
||||
'code' => true,
|
||||
'font' => true,
|
||||
'mark' => true,
|
||||
'nobr' => true,
|
||||
'ruby' => true,
|
||||
'span' => true,
|
||||
'time' => true,
|
||||
|
||||
'blink' => true,
|
||||
'small' => true,
|
||||
|
||||
'nextid' => true,
|
||||
'spacer' => true,
|
||||
'strike' => true,
|
||||
'strong' => true,
|
||||
|
||||
'acronym' => true,
|
||||
'listing' => true,
|
||||
'marquee' => true,
|
||||
|
||||
'basefont' => true,
|
||||
];
|
||||
|
||||
/** @var string */
|
||||
private $name;
|
||||
|
||||
/** @var array<string, string>*/
|
||||
private $attributes;
|
||||
|
||||
/** @var Renderable[]|null */
|
||||
private $Contents;
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param array<string, string> $attributes
|
||||
* @param Renderable[]|null $Contents
|
||||
*/
|
||||
public function __construct($name, $attributes, $Contents)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->attributes = $attributes;
|
||||
$this->Contents = $Contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param array<string, string> $attributes
|
||||
* @return self
|
||||
*/
|
||||
public static function selfClosing($name, array $attributes)
|
||||
{
|
||||
return new self($name, $attributes, null);
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function name()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function attributes()
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Renderable[]|null
|
||||
*/
|
||||
public function contents()
|
||||
{
|
||||
return $this->Contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return self
|
||||
*/
|
||||
public function settingName($name)
|
||||
{
|
||||
return new self($name, $this->attributes, $this->Contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $attributes
|
||||
* @return self
|
||||
*/
|
||||
public function settingAttributes(array $attributes)
|
||||
{
|
||||
return new self($this->name, $attributes, $this->Contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Renderable[]|null $Contents
|
||||
* @return self
|
||||
*/
|
||||
public function settingContents($Contents)
|
||||
{
|
||||
return new self($this->name, $this->attributes, $Contents);
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function getHtml()
|
||||
{
|
||||
$elementName = CharacterFilter::htmlElementName($this->name);
|
||||
|
||||
$html = '<' . $elementName;
|
||||
|
||||
if (! empty($this->attributes)) {
|
||||
foreach ($this->attributes as $name => $value) {
|
||||
$html .= ' '
|
||||
. CharacterFilter::htmlAttributeName($name)
|
||||
. '="'
|
||||
. Escaper::htmlAttributeValue($value)
|
||||
. '"'
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->Contents !== null) {
|
||||
$html .= '>';
|
||||
|
||||
if (! empty($this->Contents)) {
|
||||
foreach ($this->Contents as $C) {
|
||||
if (
|
||||
$C instanceof Element
|
||||
&& ! \array_key_exists(\strtolower($C->name()), self::$TEXT_LEVEL_ELEMENTS)
|
||||
) {
|
||||
$html .= "\n";
|
||||
}
|
||||
|
||||
$html .= $C->getHtml();
|
||||
}
|
||||
|
||||
$Last = \end($this->Contents);
|
||||
|
||||
if (
|
||||
$Last instanceof Element
|
||||
&& ! \array_key_exists(\strtolower($Last->name()), self::$TEXT_LEVEL_ELEMENTS)
|
||||
) {
|
||||
$html .= "\n";
|
||||
}
|
||||
}
|
||||
|
||||
$html .= "</" . $elementName . ">";
|
||||
} else {
|
||||
$html .= ' />';
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
20
src/Html/Renderables/Invisible.php
Normal file
20
src/Html/Renderables/Invisible.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?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 '';
|
||||
}
|
||||
}
|
27
src/Html/Renderables/RawHtml.php
Normal file
27
src/Html/Renderables/RawHtml.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
34
src/Html/Renderables/Text.php
Normal file
34
src/Html/Renderables/Text.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Html\Renderables;
|
||||
|
||||
use Erusev\Parsedown\Html\Renderable;
|
||||
use Erusev\Parsedown\Html\Sanitisation\Escaper;
|
||||
|
||||
final class Text implements Renderable
|
||||
{
|
||||
use CanonicalStateRenderable;
|
||||
|
||||
/** @var string */
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
*/
|
||||
public function __construct($text = '')
|
||||
{
|
||||
$this->text = $text;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function getStringBacking()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function getHtml()
|
||||
{
|
||||
return Escaper::htmlElementValueEscapingDoubleQuotes($this->text);
|
||||
}
|
||||
}
|
44
src/Html/Sanitisation/CharacterFilter.php
Normal file
44
src/Html/Sanitisation/CharacterFilter.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
47
src/Html/Sanitisation/Escaper.php
Normal file
47
src/Html/Sanitisation/Escaper.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?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'
|
||||
);
|
||||
}
|
||||
}
|
59
src/Html/Sanitisation/UrlSanitiser.php
Normal file
59
src/Html/Sanitisation/UrlSanitiser.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Html\Sanitisation;
|
||||
|
||||
final class UrlSanitiser
|
||||
{
|
||||
/** @var string[] */
|
||||
private static $COMMON_SCHEMES = [
|
||||
'http://',
|
||||
'https://',
|
||||
'ftp://',
|
||||
'ftps://',
|
||||
'mailto:',
|
||||
'tel:',
|
||||
'data:image/png;base64,',
|
||||
'data:image/gif;base64,',
|
||||
'data:image/jpeg;base64,',
|
||||
'irc:',
|
||||
'ircs:',
|
||||
'git:',
|
||||
'ssh:',
|
||||
'news:',
|
||||
'steam:',
|
||||
];
|
||||
|
||||
/**
|
||||
* Disable literal intepretation of unknown scheme in $url. Returns the
|
||||
* filtered version of $url.
|
||||
* @param string $url
|
||||
* @param string[]|null $permittedSchemes
|
||||
* @return string
|
||||
*/
|
||||
public static function filter($url, $permittedSchemes = null)
|
||||
{
|
||||
if (! isset($permittedSchemes)) {
|
||||
$permittedSchemes = self::$COMMON_SCHEMES;
|
||||
}
|
||||
|
||||
foreach ($permittedSchemes as $scheme) {
|
||||
if (self::striAtStart($url, $scheme)) {
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
return \str_replace(':', '%3A', $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* @param string $needle
|
||||
* @return bool
|
||||
*/
|
||||
private static function striAtStart($string, $needle)
|
||||
{
|
||||
$needleLen = \strlen($needle);
|
||||
|
||||
return \strtolower(\substr($string, 0, $needleLen)) === \strtolower($needle);
|
||||
}
|
||||
}
|
286
src/Parsedown.php
Normal file
286
src/Parsedown.php
Normal file
@ -0,0 +1,286 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Components\AcquisitioningBlock;
|
||||
use Erusev\Parsedown\Components\BacktrackingInline;
|
||||
use Erusev\Parsedown\Components\Block;
|
||||
use Erusev\Parsedown\Components\Blocks\Paragraph;
|
||||
use Erusev\Parsedown\Components\ContinuableBlock;
|
||||
use Erusev\Parsedown\Components\Inline;
|
||||
use Erusev\Parsedown\Components\Inlines\PlainText;
|
||||
use Erusev\Parsedown\Components\StateUpdatingBlock;
|
||||
use Erusev\Parsedown\Configurables\BlockTypes;
|
||||
use Erusev\Parsedown\Configurables\InlineTypes;
|
||||
use Erusev\Parsedown\Configurables\RecursionLimiter;
|
||||
use Erusev\Parsedown\Html\Renderable;
|
||||
use Erusev\Parsedown\Html\Renderables\Text;
|
||||
use Erusev\Parsedown\Parsing\Excerpt;
|
||||
use Erusev\Parsedown\Parsing\Line;
|
||||
use Erusev\Parsedown\Parsing\Lines;
|
||||
|
||||
final class Parsedown
|
||||
{
|
||||
const version = '2.0.0-dev';
|
||||
|
||||
/** @var State */
|
||||
private $State;
|
||||
|
||||
public function __construct(StateBearer $StateBearer = null)
|
||||
{
|
||||
$StateBearer = $StateBearer ?: new State;
|
||||
|
||||
$this->State = $StateBearer->state();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
public function text($text)
|
||||
{
|
||||
list($StateRenderables, $State) = self::lines(
|
||||
Lines::fromTextLines($text, 0),
|
||||
$this->State
|
||||
);
|
||||
|
||||
$Renderables = $State->applyTo($StateRenderables);
|
||||
|
||||
$html = self::render($Renderables);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: StateRenderable[], 1: State}
|
||||
*/
|
||||
public static function lines(Lines $Lines, State $State)
|
||||
{
|
||||
list($Blocks, $State) = self::blocks($Lines, $State);
|
||||
|
||||
return [self::stateRenderablesFrom($Blocks), $State];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Component[] $Components
|
||||
* @return StateRenderable[]
|
||||
*/
|
||||
public static function stateRenderablesFrom($Components)
|
||||
{
|
||||
return \array_map(
|
||||
/**
|
||||
* @param Component $Component
|
||||
* @return StateRenderable
|
||||
*/
|
||||
function ($Component) { return $Component->stateRenderable(); },
|
||||
$Components
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: Block[], 1: State}
|
||||
*/
|
||||
public static function blocks(Lines $Lines, State $State)
|
||||
{
|
||||
$RecursionLimiter = $State->get(RecursionLimiter::class)->increment();
|
||||
|
||||
if ($RecursionLimiter->isDepthExceeded()) {
|
||||
$State = $State->setting(new BlockTypes([], []));
|
||||
}
|
||||
|
||||
$State = $State->setting($RecursionLimiter);
|
||||
|
||||
/** @var Block[] */
|
||||
$Blocks = [];
|
||||
/** @var Block|null */
|
||||
$Block = null;
|
||||
/** @var Block|null */
|
||||
$CurrentBlock = null;
|
||||
|
||||
foreach ($Lines->contexts() as $Context) {
|
||||
$Line = $Context->line();
|
||||
|
||||
if (
|
||||
isset($CurrentBlock)
|
||||
&& $CurrentBlock instanceof ContinuableBlock
|
||||
&& ! $CurrentBlock instanceof Paragraph
|
||||
) {
|
||||
$Block = $CurrentBlock->advance($Context, $State);
|
||||
|
||||
if ($Block instanceof StateUpdatingBlock) {
|
||||
$State = $Block->latestState();
|
||||
}
|
||||
|
||||
if (isset($Block)) {
|
||||
$CurrentBlock = $Block;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$marker = \substr($Line->text(), 0, 1);
|
||||
|
||||
$potentialBlockTypes = \array_merge(
|
||||
$State->get(BlockTypes::class)->unmarked(),
|
||||
$State->get(BlockTypes::class)->markedBy($marker)
|
||||
);
|
||||
|
||||
foreach ($potentialBlockTypes as $blockType) {
|
||||
$Block = $blockType::build($Context, $State, $CurrentBlock);
|
||||
|
||||
if (isset($Block)) {
|
||||
if ($Block instanceof StateUpdatingBlock) {
|
||||
$State = $Block->latestState();
|
||||
}
|
||||
|
||||
if (isset($CurrentBlock)
|
||||
&& (
|
||||
! $Block instanceof AcquisitioningBlock
|
||||
|| ! $Block->acquiredPrevious()
|
||||
)
|
||||
) {
|
||||
$Blocks[] = $CurrentBlock;
|
||||
}
|
||||
|
||||
$CurrentBlock = $Block;
|
||||
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($CurrentBlock) && $CurrentBlock instanceof Paragraph) {
|
||||
$Block = $CurrentBlock->advance($Context, $State);
|
||||
}
|
||||
|
||||
if (isset($Block)) {
|
||||
$CurrentBlock = $Block;
|
||||
} else {
|
||||
if (isset($CurrentBlock)) {
|
||||
$Blocks[] = $CurrentBlock;
|
||||
}
|
||||
|
||||
$CurrentBlock = Paragraph::build($Context, $State);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($CurrentBlock)) {
|
||||
$Blocks[] = $CurrentBlock;
|
||||
}
|
||||
|
||||
return [$Blocks, $State];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @return StateRenderable[]
|
||||
*/
|
||||
public static function line($text, State $State)
|
||||
{
|
||||
return self::stateRenderablesFrom(self::inlines($text, $State));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @return Inline[]
|
||||
*/
|
||||
public static function inlines($text, State $State)
|
||||
{
|
||||
# standardize line breaks
|
||||
$text = \str_replace(["\r\n", "\r"], "\n", $text);
|
||||
|
||||
$RecursionLimiter = $State->get(RecursionLimiter::class)->increment();
|
||||
|
||||
if ($RecursionLimiter->isDepthExceeded()) {
|
||||
return [Plaintext::build(new Excerpt($text, 0), $State)];
|
||||
}
|
||||
|
||||
$State = $State->setting($RecursionLimiter);
|
||||
|
||||
/** @var Inline[] */
|
||||
$Inlines = [];
|
||||
|
||||
# $excerpt is based on the first occurrence of a marker
|
||||
|
||||
$InlineTypes = $State->get(InlineTypes::class);
|
||||
$markerMask = $InlineTypes->markers();
|
||||
|
||||
for (
|
||||
$Excerpt = (new Excerpt($text, 0))->pushingOffsetTo($markerMask);
|
||||
$Excerpt->text() !== '';
|
||||
$Excerpt = $Excerpt->pushingOffsetTo($markerMask)
|
||||
) {
|
||||
$marker = \substr($Excerpt->text(), 0, 1);
|
||||
|
||||
foreach ($InlineTypes->markedBy($marker) as $inlineType) {
|
||||
$Inline = $inlineType::build($Excerpt, $State);
|
||||
|
||||
if (! isset($Inline)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$markerPosition = $Excerpt->offset();
|
||||
|
||||
/** @var int|null */
|
||||
$startPosition = null;
|
||||
|
||||
if ($Inline instanceof BacktrackingInline) {
|
||||
$startPosition = $Inline->modifyStartPositionTo();
|
||||
}
|
||||
|
||||
if (! isset($startPosition)) {
|
||||
$startPosition = $markerPosition;
|
||||
}
|
||||
|
||||
$endPosition = $startPosition + $Inline->width();
|
||||
|
||||
if ($startPosition > $markerPosition
|
||||
|| $endPosition < $markerPosition
|
||||
|| $startPosition < 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$Inlines[] = Plaintext::build($Excerpt->choppingUpToOffset($startPosition), $State);
|
||||
|
||||
$Inlines[] = $Inline;
|
||||
|
||||
/** @psalm-suppress LoopInvalidation */
|
||||
$Excerpt = $Excerpt->choppingFromOffset($endPosition);
|
||||
|
||||
continue 2;
|
||||
}
|
||||
|
||||
/** @psalm-suppress LoopInvalidation */
|
||||
$Excerpt = $Excerpt->addingToOffset(1);
|
||||
}
|
||||
|
||||
$Inlines[] = Plaintext::build($Excerpt->choppingFromOffset(0), $State);
|
||||
|
||||
return $Inlines;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Renderable[] $Renderables
|
||||
* @return string
|
||||
*/
|
||||
public static function render(array $Renderables)
|
||||
{
|
||||
return \trim(
|
||||
\array_reduce(
|
||||
$Renderables,
|
||||
/**
|
||||
* @param string $html
|
||||
* @return string
|
||||
*/
|
||||
function ($html, Renderable $Renderable) {
|
||||
$newHtml = $Renderable->getHtml();
|
||||
|
||||
return $html . ($newHtml === '' ? '' : "\n") . $newHtml;
|
||||
},
|
||||
''
|
||||
),
|
||||
"\n"
|
||||
);
|
||||
}
|
||||
}
|
44
src/Parsing/Context.php
Normal file
44
src/Parsing/Context.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Parsing;
|
||||
|
||||
final class Context
|
||||
{
|
||||
/** @var Line */
|
||||
private $Line;
|
||||
|
||||
/** @var int */
|
||||
private $previousEmptyLines;
|
||||
|
||||
/** @var string */
|
||||
private $previousEmptyLinesText;
|
||||
|
||||
/**
|
||||
* @param Line $Line
|
||||
* @param string $previousEmptyLinesText
|
||||
*/
|
||||
public function __construct($Line, $previousEmptyLinesText)
|
||||
{
|
||||
$this->Line = $Line;
|
||||
$this->previousEmptyLinesText = $previousEmptyLinesText;
|
||||
$this->previousEmptyLines = \substr_count($previousEmptyLinesText, "\n");
|
||||
}
|
||||
|
||||
/** @return Line */
|
||||
public function line()
|
||||
{
|
||||
return $this->Line;
|
||||
}
|
||||
|
||||
/** @return int */
|
||||
public function previousEmptyLines()
|
||||
{
|
||||
return $this->previousEmptyLines;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function previousEmptyLinesText()
|
||||
{
|
||||
return $this->previousEmptyLinesText;
|
||||
}
|
||||
}
|
85
src/Parsing/Excerpt.php
Normal file
85
src/Parsing/Excerpt.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
138
src/Parsing/Line.php
Normal file
138
src/Parsing/Line.php
Normal file
@ -0,0 +1,138 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
179
src/Parsing/Lines.php
Normal file
179
src/Parsing/Lines.php
Normal file
@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown\Parsing;
|
||||
|
||||
final class Lines
|
||||
{
|
||||
/** @var Context[] */
|
||||
private $Contexts;
|
||||
|
||||
/** @var bool */
|
||||
private $containsBlankLines;
|
||||
|
||||
/** @var string */
|
||||
private $trailingBlankLinesText;
|
||||
|
||||
/** @var int */
|
||||
private $trailingBlankLines;
|
||||
|
||||
/**
|
||||
* @param Context[] $Contexts
|
||||
* @param string $trailingBlankLinesText
|
||||
*/
|
||||
private function __construct($Contexts, $trailingBlankLinesText)
|
||||
{
|
||||
$this->Contexts = $Contexts;
|
||||
$this->trailingBlankLinesText = $trailingBlankLinesText;
|
||||
$this->trailingBlankLines = \substr_count($trailingBlankLinesText, "\n");
|
||||
|
||||
$containsBlankLines = $this->trailingBlankLines > 0;
|
||||
|
||||
if (! $containsBlankLines) {
|
||||
foreach ($Contexts as $Context) {
|
||||
if ($Context->previousEmptyLines() > 0) {
|
||||
$containsBlankLines = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->containsBlankLines = $containsBlankLines;
|
||||
}
|
||||
|
||||
/** @return self */
|
||||
public static function none()
|
||||
{
|
||||
return new self([], '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param int $indentOffset
|
||||
* @return self
|
||||
*/
|
||||
public static function fromTextLines($text, $indentOffset)
|
||||
{
|
||||
# standardize line breaks
|
||||
$text = \str_replace(["\r\n", "\r"], "\n", $text);
|
||||
|
||||
$Contexts = [];
|
||||
$sequentialLines = '';
|
||||
|
||||
foreach (\explode("\n", $text) as $line) {
|
||||
if (\chop($line) === '') {
|
||||
$sequentialLines .= $line . "\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
$Contexts[] = new Context(
|
||||
new Line($line, $indentOffset),
|
||||
$sequentialLines
|
||||
);
|
||||
|
||||
$sequentialLines = '';
|
||||
}
|
||||
|
||||
return new self($Contexts, $sequentialLines);
|
||||
}
|
||||
|
||||
/** @return bool */
|
||||
public function isEmpty()
|
||||
{
|
||||
return \count($this->Contexts) === 0 && $this->trailingBlankLines === 0;
|
||||
}
|
||||
|
||||
/** @return Context[] */
|
||||
public function Contexts()
|
||||
{
|
||||
return $this->Contexts;
|
||||
}
|
||||
|
||||
/** @return bool */
|
||||
public function containsBlankLines()
|
||||
{
|
||||
return $this->containsBlankLines;
|
||||
}
|
||||
|
||||
/** @return int */
|
||||
public function trailingBlankLines()
|
||||
{
|
||||
return $this->trailingBlankLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $count
|
||||
* @return self
|
||||
*/
|
||||
public function appendingBlankLines($count = 1)
|
||||
{
|
||||
if ($count < 0) {
|
||||
$count = 0;
|
||||
}
|
||||
|
||||
$Lines = clone($this);
|
||||
$Lines->trailingBlankLinesText .= \str_repeat("\n", $count);
|
||||
$Lines->trailingBlankLines += $count;
|
||||
$Lines->containsBlankLines = $Lines->containsBlankLines || ($count > 0);
|
||||
|
||||
return $Lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param int $indentOffset
|
||||
* @return Lines
|
||||
*/
|
||||
public function appendingTextLines($text, $indentOffset)
|
||||
{
|
||||
$Lines = clone($this);
|
||||
|
||||
$NextLines = self::fromTextLines($text, $indentOffset);
|
||||
|
||||
if (\count($NextLines->Contexts) === 0) {
|
||||
$Lines->trailingBlankLines += $NextLines->trailingBlankLines;
|
||||
$Lines->trailingBlankLinesText .= $NextLines->trailingBlankLinesText;
|
||||
|
||||
$Lines->containsBlankLines = true;
|
||||
|
||||
return $Lines;
|
||||
}
|
||||
|
||||
$NextLines->Contexts[0] = new Context(
|
||||
$NextLines->Contexts[0]->line(),
|
||||
$NextLines->Contexts[0]->previousEmptyLinesText() . $Lines->trailingBlankLinesText
|
||||
);
|
||||
|
||||
$Lines->Contexts = \array_merge($Lines->Contexts, $NextLines->Contexts);
|
||||
|
||||
$Lines->trailingBlankLines = $NextLines->trailingBlankLines;
|
||||
$Lines->trailingBlankLinesText = $NextLines->trailingBlankLinesText;
|
||||
|
||||
$Lines->containsBlankLines = $Lines->containsBlankLines
|
||||
|| $NextLines->containsBlankLines
|
||||
;
|
||||
|
||||
return $Lines;
|
||||
}
|
||||
|
||||
/** @return Lines */
|
||||
public function appendingContext(Context $Context)
|
||||
{
|
||||
$Lines = clone($this);
|
||||
|
||||
$Context = new Context(
|
||||
$Context->line(),
|
||||
$Context->previousEmptyLinesText() . $Lines->trailingBlankLinesText
|
||||
);
|
||||
|
||||
if ($Context->previousEmptyLines() > 0) {
|
||||
$Lines->containsBlankLines = true;
|
||||
}
|
||||
|
||||
$Lines->trailingBlankLines = 0;
|
||||
$Lines->trailingBlankLinesText = '';
|
||||
|
||||
$Lines->Contexts[] = $Context;
|
||||
|
||||
return $Lines;
|
||||
}
|
||||
}
|
99
src/State.php
Normal file
99
src/State.php
Normal file
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown;
|
||||
|
||||
use Erusev\Parsedown\AST\StateRenderable;
|
||||
use Erusev\Parsedown\Html\Renderable;
|
||||
|
||||
final class State implements StateBearer
|
||||
{
|
||||
/**
|
||||
* @var array<class-string<Configurable>, Configurable>
|
||||
*/
|
||||
private $state;
|
||||
|
||||
/**
|
||||
* @var array<class-string<Configurable>, Configurable>
|
||||
*/
|
||||
private static $initialCache;
|
||||
|
||||
/**
|
||||
* @param Configurable[] $Configurables
|
||||
*/
|
||||
public function __construct(array $Configurables = [])
|
||||
{
|
||||
$this->state = \array_combine(
|
||||
\array_map(
|
||||
/** @return class-string */
|
||||
function (Configurable $C) { return \get_class($C); },
|
||||
$Configurables
|
||||
),
|
||||
$Configurables
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self
|
||||
*/
|
||||
public function setting(Configurable $C)
|
||||
{
|
||||
return new self([\get_class($C) => $C] + $this->state);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self
|
||||
*/
|
||||
public function mergingWith(State $State)
|
||||
{
|
||||
return new self($State->state + $this->state);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T as Configurable
|
||||
* @template-typeof T $configurableClass
|
||||
* @param class-string<Configurable> $configurableClass
|
||||
* @return T
|
||||
*/
|
||||
public function get($configurableClass)
|
||||
{
|
||||
if (isset($this->state[$configurableClass])) {
|
||||
return $this->state[$configurableClass];
|
||||
}
|
||||
|
||||
if (! isset(self::$initialCache[$configurableClass])) {
|
||||
self::$initialCache[$configurableClass] = $configurableClass::initial();
|
||||
}
|
||||
|
||||
return self::$initialCache[$configurableClass];
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->state = \array_map(
|
||||
/** @return Configurable */
|
||||
function (Configurable $C) { return clone($C); },
|
||||
$this->state
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StateRenderable[] $StateRenderables
|
||||
* @return Renderable[]
|
||||
*/
|
||||
public function applyTo(array $StateRenderables)
|
||||
{
|
||||
return \array_map(
|
||||
/** @return Renderable */
|
||||
function (StateRenderable $SR) { return $SR->renderable($this); },
|
||||
$StateRenderables
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return State
|
||||
*/
|
||||
public function state()
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
11
src/StateBearer.php
Normal file
11
src/StateBearer.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Erusev\Parsedown;
|
||||
|
||||
interface StateBearer
|
||||
{
|
||||
/**
|
||||
* @return State
|
||||
*/
|
||||
public function state();
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Test Parsedown against the CommonMark spec.
|
||||
*
|
||||
* Some code based on the original JavaScript test runner by jgm.
|
||||
*
|
||||
* @link http://commonmark.org/ CommonMark
|
||||
* @link http://git.io/8WtRvQ JavaScript test runner
|
||||
*/
|
||||
class CommonMarkTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
const SPEC_URL = 'https://raw.githubusercontent.com/jgm/stmd/master/spec.txt';
|
||||
|
||||
/**
|
||||
* @dataProvider data
|
||||
* @param $section
|
||||
* @param $markdown
|
||||
* @param $expectedHtml
|
||||
*/
|
||||
function test_($section, $markdown, $expectedHtml)
|
||||
{
|
||||
$Parsedown = new Parsedown();
|
||||
$Parsedown->setUrlsLinked(false);
|
||||
|
||||
$actualHtml = $Parsedown->text($markdown);
|
||||
$actualHtml = $this->normalizeMarkup($actualHtml);
|
||||
|
||||
$this->assertEquals($expectedHtml, $actualHtml);
|
||||
}
|
||||
|
||||
function data()
|
||||
{
|
||||
$spec = file_get_contents(self::SPEC_URL);
|
||||
$spec = strstr($spec, '<!-- END TESTS -->', true);
|
||||
|
||||
$tests = array();
|
||||
$currentSection = '';
|
||||
|
||||
preg_replace_callback(
|
||||
'/^\.\n([\s\S]*?)^\.\n([\s\S]*?)^\.$|^#{1,6} *(.*)$/m',
|
||||
function($matches) use ( & $tests, & $currentSection, & $testCount) {
|
||||
if (isset($matches[3]) and $matches[3]) {
|
||||
$currentSection = $matches[3];
|
||||
} else {
|
||||
$testCount++;
|
||||
$markdown = $matches[1];
|
||||
$markdown = preg_replace('/→/', "\t", $markdown);
|
||||
$expectedHtml = $matches[2];
|
||||
$expectedHtml = $this->normalizeMarkup($expectedHtml);
|
||||
$tests []= array(
|
||||
$currentSection, # section
|
||||
$markdown, # markdown
|
||||
$expectedHtml, # html
|
||||
);
|
||||
}
|
||||
},
|
||||
$spec
|
||||
);
|
||||
|
||||
return $tests;
|
||||
}
|
||||
|
||||
private function normalizeMarkup($markup)
|
||||
{
|
||||
$markup = preg_replace("/\n+/", "\n", $markup);
|
||||
$markup = preg_replace('/^\s+/m', '', $markup);
|
||||
$markup = preg_replace('/^((?:<[\w]+>)+)\n/m', '$1', $markup);
|
||||
$markup = preg_replace('/\n((?:<\/[\w]+>)+)$/m', '$1', $markup);
|
||||
$markup = trim($markup);
|
||||
|
||||
return $markup;
|
||||
}
|
||||
}
|
@ -1,159 +0,0 @@
|
||||
<?php
|
||||
|
||||
class ParsedownTest extends PHPUnit_Framework_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 Parsedown();
|
||||
|
||||
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);
|
||||
|
||||
$actualMarkup = $this->Parsedown->text($markdown);
|
||||
|
||||
$this->assertEquals($expectedMarkup, $actualMarkup);
|
||||
}
|
||||
|
||||
function data()
|
||||
{
|
||||
$data = array();
|
||||
|
||||
foreach ($this->dirs as $dir)
|
||||
{
|
||||
$Folder = new DirectoryIterator($dir);
|
||||
|
||||
foreach ($Folder as $File)
|
||||
{
|
||||
/** @var $File DirectoryIterator */
|
||||
|
||||
if ( ! $File->isFile())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$filename = $File->getFilename();
|
||||
|
||||
$extension = pathinfo($filename, PATHINFO_EXTENSION);
|
||||
|
||||
if ($extension !== 'md')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$basename = $File->getBasename('.md');
|
||||
|
||||
if (file_exists($dir . $basename . '.html'))
|
||||
{
|
||||
$data []= array($basename, $dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function test_no_markup()
|
||||
{
|
||||
$markdownWithHtml = <<<MARKDOWN_WITH_MARKUP
|
||||
<div>_content_</div>
|
||||
|
||||
sparse:
|
||||
|
||||
<div>
|
||||
<div class="inner">
|
||||
_content_
|
||||
</div>
|
||||
</div>
|
||||
|
||||
paragraph
|
||||
|
||||
<style type="text/css">
|
||||
p {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
comment
|
||||
|
||||
<!-- html comment -->
|
||||
MARKDOWN_WITH_MARKUP;
|
||||
|
||||
$expectedHtml = <<<EXPECTED_HTML
|
||||
<p><div><em>content</em></div></p>
|
||||
<p>sparse:</p>
|
||||
<p><div>
|
||||
<div class="inner">
|
||||
<em>content</em>
|
||||
</div>
|
||||
</div></p>
|
||||
<p>paragraph</p>
|
||||
<p><style type="text/css">
|
||||
p {
|
||||
color: red;
|
||||
}
|
||||
</style></p>
|
||||
<p>comment</p>
|
||||
<p><!-- html comment --></p>
|
||||
EXPECTED_HTML;
|
||||
$parsedownWithNoMarkup = new Parsedown();
|
||||
$parsedownWithNoMarkup->setMarkupEscaped(true);
|
||||
$this->assertEquals($expectedHtml, $parsedownWithNoMarkup->text($markdownWithHtml));
|
||||
}
|
||||
|
||||
public function testLateStaticBinding()
|
||||
{
|
||||
include __DIR__ . '/TestParsedown.php';
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
class TestParsedown extends Parsedown
|
||||
{
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
<?php
|
||||
|
||||
include 'Parsedown.php';
|
@ -1,8 +0,0 @@
|
||||
<pre><code><?php
|
||||
|
||||
$message = 'Hello World!';
|
||||
echo $message;</code></pre>
|
||||
<hr />
|
||||
<pre><code>> not a quote
|
||||
- not a list item
|
||||
[not a reference]: http://foo.com</code></pre>
|
@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
$message = 'Hello World!';
|
||||
echo $message;
|
||||
|
||||
---
|
||||
|
||||
> not a quote
|
||||
- not a list item
|
||||
[not a reference]: http://foo.com
|
@ -1,12 +0,0 @@
|
||||
<ul>
|
||||
<li>li
|
||||
<ul>
|
||||
<li>li
|
||||
<ul>
|
||||
<li>li</li>
|
||||
<li>li</li>
|
||||
</ul></li>
|
||||
<li>li</li>
|
||||
</ul></li>
|
||||
<li>li</li>
|
||||
</ul>
|
@ -1,6 +0,0 @@
|
||||
- li
|
||||
- li
|
||||
- li
|
||||
- li
|
||||
- li
|
||||
- li
|
@ -1 +0,0 @@
|
||||
<p>my email is <a href="mailto:me@example.com">me@example.com</a></p>
|
@ -1 +0,0 @@
|
||||
my email is <me@example.com>
|
@ -1,6 +0,0 @@
|
||||
<pre><code><?php
|
||||
|
||||
$message = 'fenced code block';
|
||||
echo $message;</code></pre>
|
||||
<pre><code>tilde</code></pre>
|
||||
<pre><code class="language-php">echo 'language identifier';</code></pre>
|
@ -1,14 +0,0 @@
|
||||
```
|
||||
<?php
|
||||
|
||||
$message = 'fenced code block';
|
||||
echo $message;
|
||||
```
|
||||
|
||||
~~~
|
||||
tilde
|
||||
~~~
|
||||
|
||||
```php
|
||||
echo 'language identifier';
|
||||
```
|
@ -1,5 +0,0 @@
|
||||
<!-- single line -->
|
||||
<p>paragraph</p>
|
||||
<!--
|
||||
multiline -->
|
||||
<p>paragraph</p>
|
@ -1,6 +0,0 @@
|
||||
<p><a href="http://example.com">link</a></p>
|
||||
<p><a href="/url-(parentheses)">link</a> with parentheses in URL </p>
|
||||
<p>(<a href="/index.php">link</a>) in parentheses</p>
|
||||
<p><a href="http://example.com"><code>link</code></a></p>
|
||||
<p><a href="http://example.com"><img src="http://parsedown.org/md.png" alt="MD Logo" /></a></p>
|
||||
<p><a href="http://example.com"><img src="http://parsedown.org/md.png" alt="MD Logo" /> and text</a></p>
|
@ -1,11 +0,0 @@
|
||||
[link](http://example.com)
|
||||
|
||||
[link](/url-(parentheses)) with parentheses in URL
|
||||
|
||||
([link](/index.php)) in parentheses
|
||||
|
||||
[`link`](http://example.com)
|
||||
|
||||
[](http://example.com)
|
||||
|
||||
[ and text](http://example.com)
|
@ -1,2 +0,0 @@
|
||||
<p>line<br />
|
||||
line</p>
|
@ -1,2 +0,0 @@
|
||||
line
|
||||
line
|
@ -1,11 +0,0 @@
|
||||
1. one
|
||||
2. two
|
||||
|
||||
repeating numbers:
|
||||
|
||||
1. one
|
||||
1. two
|
||||
|
||||
large numbers:
|
||||
|
||||
123. one
|
@ -1,11 +0,0 @@
|
||||
<blockquote>
|
||||
<p>quote</p>
|
||||
</blockquote>
|
||||
<p>indented:</p>
|
||||
<blockquote>
|
||||
<p>quote</p>
|
||||
</blockquote>
|
||||
<p>no space after <code>></code>:</p>
|
||||
<blockquote>
|
||||
<p>quote</p>
|
||||
</blockquote>
|
@ -1,7 +0,0 @@
|
||||
> quote
|
||||
|
||||
indented:
|
||||
> quote
|
||||
|
||||
no space after `>`:
|
||||
>quote
|
@ -1,37 +0,0 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>header 1</th>
|
||||
<th>header 2</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>cell 1.1</td>
|
||||
<td>cell 1.2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>cell 2.1</td>
|
||||
<td>cell 2.2</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align: left;">header 1</th>
|
||||
<th>header 2</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align: left;">cell 1.1</td>
|
||||
<td>cell 1.2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: left;">cell 2.1</td>
|
||||
<td>cell 2.2</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
@ -1,11 +0,0 @@
|
||||
header 1 | header 2
|
||||
-------- | --------
|
||||
cell 1.1 | cell 1.2
|
||||
cell 2.1 | cell 2.2
|
||||
|
||||
---
|
||||
|
||||
header 1 | header 2
|
||||
:------- | --------
|
||||
cell 1.1 | cell 1.2
|
||||
cell 2.1 | cell 2.2
|
@ -1,7 +0,0 @@
|
||||
<ul>
|
||||
<li>
|
||||
<p>li</p>
|
||||
</li>
|
||||
<li>li</li>
|
||||
<li>li</li>
|
||||
</ul>
|
@ -1,8 +0,0 @@
|
||||
<div>
|
||||
line 1
|
||||
|
||||
line 2
|
||||
line 3
|
||||
|
||||
line 4
|
||||
</div>
|
@ -1,10 +0,0 @@
|
||||
<ul>
|
||||
<li>li</li>
|
||||
<li>li</li>
|
||||
</ul>
|
||||
<p>mixed markers:</p>
|
||||
<ul>
|
||||
<li>li</li>
|
||||
<li>li</li>
|
||||
<li>li</li>
|
||||
</ul>
|
@ -1,8 +0,0 @@
|
||||
- li
|
||||
- li
|
||||
|
||||
mixed markers:
|
||||
|
||||
* li
|
||||
+ li
|
||||
- li
|
@ -1,3 +0,0 @@
|
||||
<p>an autolink <a href="http://example.com">http://example.com</a></p>
|
||||
<p>inside of brackets [<a href="http://example.com">http://example.com</a>], inside of braces {<a href="http://example.com">http://example.com</a>}, inside of parentheses (<a href="http://example.com">http://example.com</a>)</p>
|
||||
<p>trailing slash <a href="http://example.com/">http://example.com/</a> and <a href="http://example.com/path/">http://example.com/path/</a></p>
|
@ -1,5 +0,0 @@
|
||||
an autolink http://example.com
|
||||
|
||||
inside of brackets [http://example.com], inside of braces {http://example.com}, inside of parentheses (http://example.com)
|
||||
|
||||
trailing slash http://example.com/ and http://example.com/path/
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user