mirror of
https://github.com/erusev/parsedown.git
synced 2023-08-10 21:13:06 +03:00
Compare commits
658 Commits
1.5.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 | |||
20ff8bbb57 | |||
bc21988fe5 | |||
e3c3e28554 | |||
f053740132 | |||
7a92a31739 | |||
6eca8796fb | |||
8876c0984e | |||
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 | |||
cbc4b3f612 | |||
0080ef218e | |||
3aef89b399 | |||
f4e0234af0 | |||
5c22531e4d | |||
3978e33fd0 | |||
a37797ef34 | |||
e3cd271f16 | |||
f0b7b61c16 | |||
ed41fcf3d6 | |||
1fa8fae301 | |||
543a6c4175 | |||
932bafe0f0 | |||
ac857809ab | |||
846274996a | |||
c145a75848 | |||
a81aedeb10 | |||
50952b3243 | |||
4d3600f273 | |||
d6d5f53ff4 | |||
f17aa0438a | |||
38f4027d5e | |||
2cee8d8a2d | |||
cceefafd55 | |||
1c58e9d8d5 | |||
2772b034c6 | |||
1a44cbd62c | |||
73dbe2fd17 | |||
33a23fbfb2 | |||
a2ed1592bd | |||
3d7a473aa9 | |||
228d5f4754 | |||
2cacfb8da4 | |||
d33e736fa3 | |||
3a46a31e09 | |||
e1bcc1c472 | |||
f671ae7364 | |||
b5951e08c6 | |||
490a8f35a4 | |||
94688f21cc | |||
693f2c4842 | |||
9545a295cf | |||
3d649081e5 | |||
32de2cedcc | |||
e7443a2bd8 | |||
10a7ff776c | |||
5ad15b87fa | |||
b166cab9a2 | |||
0f974bf34f | |||
3d7cdeec5f | |||
97953b193e | |||
c046a6b646 | |||
f1fefc257c | |||
6f23ec8203 | |||
23d4544986 | |||
5a4ff5d189 | |||
50ac4a06e8 | |||
003af26499 | |||
aa63058a88 | |||
15d56cdd27 | |||
93d77b0b47 | |||
c8072a1987 | |||
b008290917 | |||
9928c933d8 | |||
ddb3bd2107 | |||
e603c2378d | |||
3ebbd730b5 | |||
1f02626ed6 | |||
fa005fdb95 | |||
5f40cab3e7 | |||
0e89e3714b | |||
6b24125f06 | |||
a589bcac79 | |||
a9dfc97ddc | |||
28774a4359 | |||
b8b5711ee5 | |||
9579e5f5e5 | |||
7f7f6418a3 | |||
ee81967749 | |||
96e0810188 | |||
99bd1bd678 | |||
e7a6a06166 | |||
eca5bb8262 | |||
1312908056 | |||
76b7d7babd | |||
ba802c1c8d | |||
438874e9a8 | |||
8e26f45dee | |||
e2bb3eaaf8 | |||
0de61e7b3a | |||
5b72dceb26 | |||
95699c9ba6 | |||
790066e9a7 | |||
b9e5228e92 | |||
31c8856f53 | |||
d5823ad622 | |||
6736ba9a04 | |||
468d1e3da8 | |||
7aa1d97bba | |||
f768f9c63f | |||
aa83968534 | |||
85eadccc05 | |||
c94fa12d67 | |||
11e02d45fa | |||
ecd53f9add | |||
844b2f49ea | |||
b2ad712644 | |||
65116c3cb0 | |||
147003107a | |||
618b26056c | |||
b828fe7c8d | |||
6c9df528aa | |||
cb8cc57742 | |||
4b7d7cdef2 | |||
97e667ab30 |
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,10 +1,73 @@
|
||||
language: php
|
||||
|
||||
php:
|
||||
- 5.6
|
||||
- 5.5
|
||||
- 5.4
|
||||
- 5.3
|
||||
- 5.2
|
||||
- hhvm
|
||||
|
||||
dist: trusty
|
||||
sudo: false
|
||||
|
||||
stages:
|
||||
- Code Format and Static Analysis
|
||||
- Units
|
||||
- Test CommonMark (weak)
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- env: ALLOW_FAILURE
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
|
||||
jobs:
|
||||
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.
|
||||
|
1528
Parsedown.php
1528
Parsedown.php
File diff suppressed because it is too large
Load Diff
85
README.md
85
README.md
@ -1,37 +1,88 @@
|
||||
## Parsedown
|
||||
<!--  -->
|
||||
|
||||
Better Markdown Parser in PHP
|
||||
<p align="center"><img alt="Parsedown" src="https://i.imgur.com/fKVY6Kz.png" width="240" /></p>
|
||||
|
||||
[[ demo ]](http://parsedown.org/demo)
|
||||
<h1>Parsedown</h1>
|
||||
|
||||
### Features
|
||||
[](https://travis-ci.org/erusev/parsedown)
|
||||
[](https://packagist.org/packages/erusev/parsedown)
|
||||
[](https://packagist.org/packages/erusev/parsedown)
|
||||
[](https://packagist.org/packages/erusev/parsedown)
|
||||
|
||||
* [Fast](http://parsedown.org/speed)
|
||||
* [Consistent](http://parsedown.org/consistency)
|
||||
Better Markdown Parser in PHP - <a href="http://parsedown.org/demo">Demo</a>.
|
||||
|
||||
## Features
|
||||
|
||||
* One File
|
||||
* No Dependencies
|
||||
* [Super Fast](http://parsedown.org/speed)
|
||||
* Extensible
|
||||
* [GitHub flavored](https://help.github.com/articles/github-flavored-markdown)
|
||||
* [Tested](http://parsedown.org/tests/) in PHP 5.2, 5.3, 5.4, 5.5, 5.6 and [hhvm](http://www.hhvm.com/)
|
||||
* [Extensible](https://github.com/erusev/parsedown/wiki/Writing-Extensions)
|
||||
* 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>
|
||||
```
|
||||
|
||||
More examples in [the wiki](https://github.com/erusev/parsedown/wiki/Usage) and in [this video tutorial](http://youtu.be/wYZBY8DEikI).
|
||||
You can also parse inline markdown only:
|
||||
|
||||
### Questions
|
||||
```php
|
||||
echo $Parsedown->line('Hello _Parsedown_!'); # prints: Hello <em>Parsedown</em>!
|
||||
```
|
||||
|
||||
More examples in [the wiki](https://github.com/erusev/parsedown/wiki/) and in [this video tutorial](http://youtu.be/wYZBY8DEikI).
|
||||
|
||||
## Security
|
||||
|
||||
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?**
|
||||
|
||||
It tries to read Markdown like a human. First, it looks at the lines. It’s interested in how the lines start. This helps it recognise blocks. It knows, for example, that if a line start with a `-` then it perhaps belong to a list. Once it recognises the blocks, it continues to the content. As it reads, it watches out for special characters. This helps it recognise inline elements (or inlines).
|
||||
It tries to read Markdown like a human. First, it looks at the lines. It’s interested in how the lines start. This helps it recognise blocks. It knows, for example, that if a line starts with a `-` then perhaps it belongs to a list. Once it recognises the blocks, it continues to the content. As it reads, it watches out for special characters. This helps it recognise inline elements (or inlines).
|
||||
|
||||
We call this approach "line based". We believe that Parsedown is the first Markdown parser to use it. Since the release of Parsedown, other developers have used the same approach to develop other Markdown parsers in PHP and in other languages.
|
||||
|
||||
@ -41,8 +92,8 @@ 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/), [RaspberryPi.org](http://www.raspberrypi.org/) and [more](https://www.versioneye.com/php/erusev:parsedown/references).
|
||||
[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?**
|
||||
|
||||
Use it, star it, share it and if you feel generous, [donate some money](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=528P3NZQMP8N2).
|
||||
Use it, star it, share it and if you feel generous, [donate](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=528P3NZQMP8N2).
|
||||
|
@ -12,7 +12,36 @@
|
||||
"homepage": "http://erusev.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"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,139 +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));
|
||||
}
|
||||
}
|
@ -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 +0,0 @@
|
||||
<p><img src="/md.png" alt="alt" title="title" /></p>
|
@ -1 +0,0 @@
|
||||

|
@ -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>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user