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

Compare commits

..

529 Commits

Author SHA1 Message Date
32278afaf3 Fix title 2022-05-21 22:40:24 +01:00
b3f0ae4a7e Merge pull request #821 from erusev/documentation-v2.0
Documentation 2.0
2022-05-21 22:37:26 +01:00
7c7d581601 Error level 1 instead of totallyTyped
Seems this is now the equivalent and sanctioned way to do this.
2022-05-21 22:31:09 +01:00
5cc84366cb Test on 8.1 2022-05-21 22:31:00 +01:00
6be43664fd Add additional explaination for the State object
Adds a small non-technical introduction to help with intuition, after
the object appears for the first time.
2022-05-21 22:26:39 +01:00
bf6cfe8af0 Show importing of Parsedown and State in example 2021-12-08 20:38:35 +00:00
6fdcfaa14a Add initial documentation
This isn't yet complete, but is a decent start
2021-12-08 20:38:35 +00:00
79effc4ae2 Ignore invalid docblock in tests 2021-12-08 20:35:46 +00:00
f77cb47528 Remove unnecessary cast 2021-12-08 20:32:09 +00:00
95c9cce7de Update php-cs-fixer 2021-10-16 00:45:18 +01:00
9749ef7a8e Slightly nicer to allow construction from StateBearer 2021-10-15 23:23:14 +01:00
638079a3da Slightly nicer method name
Normally this name might be too vague for an interface, but implementing
this interface is a fairly special use case. We can expect dedicated
types here (so method name unlikely to be an issue).
2021-10-15 23:20:20 +01:00
421a2393d0 StateBearer must be constructable from State 2021-10-15 23:18:08 +01:00
4af22ec41a Allow substitution of blocks and inlines 2021-10-15 19:25:47 +01:00
4553515e8e Relax mutation tolerance 2021-10-13 22:12:06 +01:00
8ce88c1b36 Off by 1 2021-10-13 19:55:23 +01:00
b73fe2e9ec Apply correct state 2021-10-13 19:52:27 +01:00
7b0a9cda04 Transformations must return transformable renderables 2021-10-13 19:46:53 +01:00
05a47e7083 Complete docblock 2021-10-13 19:35:21 +01:00
e9dec33dc6 Replacing all function to TransformableRenderable 2021-10-13 19:26:07 +01:00
a9f41548d3 adding method for Container 2021-10-13 19:02:52 +01:00
71d9263664 Pass State to render stack closure 2021-10-13 18:50:04 +01:00
8fc49f6b0a Add TransformableRenderable capability 2021-10-12 21:58:24 +01:00
a48c451d6c Restrict autolinks to https 2021-10-12 18:21:17 +01:00
2ea4bece92 Return statebearer instead of state 2021-10-12 18:12:00 +01:00
efaf20d005 Minor refactor to use type hints 2021-10-12 18:05:58 +01:00
0f27f2e842 Add initState to tests 2021-10-12 18:04:36 +01:00
c773305bc1 Add RenderStack
This is a special configurable that can be used to alter the
Renderable[] AST before output.
2021-10-12 00:06:51 +01:00
7c2681be19 Remove travis 2021-10-11 23:12:06 +01:00
9d5af12971 Run both commonmark tests 2021-10-11 20:49:58 +01:00
58bb815870 Dev flag 2021-10-11 20:48:31 +01:00
de06199f4e Support old phpunit, remove lock file 2021-10-11 20:47:21 +01:00
f396d49a4c Use static analysis plugin for infection 2021-10-11 20:43:25 +01:00
7f11869f65 Remove infection for unit tests 2021-10-11 20:33:25 +01:00
3472b9bd3f GitHub Actions 2021-10-11 20:26:42 +01:00
7d1b9ca562 Better autolinks
This doesn't follow gfm spec yet, work lifted from my code over
at
8505e2737e/src/Parsers/GitHubFlavor/Inlines/AutoLink.php

Fixes #505
Fixes #717
2021-10-11 20:09:57 +01:00
cc5614bc5c Test 8.0 in travis 2021-10-11 19:25:58 +01:00
ca16d7573d Fix psalm errors 2021-10-11 19:23:44 +01:00
98aab22002 Update dependencies 2021-10-11 19:22:23 +01:00
9f6112e70a Ignore phpunit result cache 2021-10-11 19:21:21 +01:00
0c5e8c152e Merge pull request #767 from aidantwoods/enhancement/header-slug
Create ID's for Header elements so they can be referenced in anchor tags
2020-05-10 14:41:15 +01:00
8764512c23 Add SlugRegister so IDs are not duplicated 2020-05-10 14:32:01 +01:00
4e99e29d28 Trim leading and trailing hyphens from slug 2020-05-10 14:32:00 +01:00
d8bf07535c Strip superscripts and divisions from eventual slug
As suggested by @Ayesh

Co-authored-by: Ayesh Karunaratne <ayesh@ayesh.me>
2020-05-10 14:32:00 +01:00
e332b4710a Add HeaderSlug configurable
Adds HeaderSlug configurable, with the option for the slug function
to be customised.

Co-authored-by: netniV <netniv@hotmail.com>
2020-05-10 14:31:55 +01:00
74df602863 Merge pull request #768 from aidantwoods/feature/mutable-configurable
Add mutable configurable
2020-05-10 14:30:15 +01:00
c835535176 Implement DefinitionBook as MutableConfigurable
This is a slightly more correct implementation, but perhaps not worth
the headache.
2020-05-10 14:19:05 +01:00
0ef406e6d2 Add MutableConfigurable
Hopefully I do not regret this...
2020-05-10 14:18:51 +01:00
8e8d1dac21 Resolve dependencies when running unit tests 2020-05-05 22:20:11 +01:00
bc018e1d00 Remove 'matrix' key 2020-05-05 22:19:56 +01:00
dbb929fff7 Commit lock file so static analysis results are stable 2020-05-05 22:19:14 +01:00
836c028aa0 Add flag to get type-coverage 2020-01-26 13:09:19 +00:00
bded7a6ff3 PHP 7.4 is now released 2020-01-19 18:14:37 +00:00
2235e36a2c Use verb imperative to indicate method is non-mutating 2020-01-19 18:06:22 +00:00
a6c17f449e previousEmptyLines -> precedingEmptyLines 2020-01-19 17:15:02 +00:00
7610eacbf9 Fix uppercase typo 2020-01-19 15:36:53 +00:00
525b72349b Expensive operation last 2020-01-19 15:34:04 +00:00
39b8b04d33 Clarify sequential lines being tracked are empty 2020-01-19 15:32:51 +00:00
8d09320009 Compute on read optimisation for previousEmptyLines 2020-01-19 15:31:33 +00:00
a72455c78a Use ->toHtml over ->text 2020-01-19 15:26:48 +00:00
a2ea704a43 Use list type over int array 2020-01-19 15:23:58 +00:00
13932bca9a Simplify match logic 2020-01-19 15:09:44 +00:00
0a6408043f Only run units on 7.4snapshot 2019-09-09 23:46:04 +01:00
01319b7572 Add 7.4 snapshot 2019-09-09 23:37:31 +01:00
0a1e4bd802 Formatting fix 2019-09-09 23:33:06 +01:00
bfdd0f29bc More specific annotation 2019-09-09 23:27:36 +01:00
5db3687892 $matches is guarenteed to contain data for the full pattern 2019-09-09 23:26:21 +01:00
bde621e4c4 Drop master from psalm requirement 2019-09-09 23:15:11 +01:00
f75e7bd970 Prefer master until next release 2019-07-31 23:25:25 +01:00
18ee4ffe04 Bump min psalm version 2019-07-25 19:18:26 +02:00
54f7719a08 Force return type to T
It's currently not a planned feature to allow Psalm to understand
the type relationship of class-name => class-instance as the array
here is constructed.
Ref: https://github.com/vimeo/psalm/issues/1969
2019-07-25 19:16:28 +02:00
f51dd8878a Drop use of undefined reference 2019-07-25 19:07:08 +02:00
211a7eb5aa Tables always acquire previous 2019-07-25 16:18:00 +02:00
37f306c3a8 Utilise constant arrays over static vars 2019-07-25 00:45:53 +02:00
ea55a9ffb0 Integer keys aren't necessary 2019-07-25 00:33:15 +02:00
b9bc0b7d37 Update expect-to-pass cache 2019-07-25 00:27:05 +02:00
34902bc80c Account for type 7 HTML blocks
These differ from type 6 blocks in a few ways (other than the HTML
element names given in the spec).

1. Type 7 blocks cannot interrupt paragraphs
2. Type 7 block must contain ONLY whitespace on the rest of the
   line, whereas type 6 blocks need only be followed by whitespace
   after the tag name, or have the opening tag be complete.
2019-07-25 00:22:09 +02:00
f4fb5bd943 Add type 7 HTML block tests 2019-07-25 00:20:59 +02:00
614c76be6f Drop pre 7.1
Drop pre 5.6 for constant arrays
Drop pre 7.1 for private constants
2019-07-25 00:18:29 +02:00
91b8bda46e Update test cache 2019-06-16 21:36:11 +01:00
5a12d4245f Allow unused class in tests 2019-06-16 21:35:13 +01:00
298b319d96 Fix malformed return type 2019-06-16 21:33:55 +01:00
7b1389b48b More specific return type 2019-06-16 21:32:37 +01:00
efcccb3256 Update commonmark cache 2019-04-10 07:02:52 +01:00
9eb6a02334 Limit recursion depth by configurable
Fixes https://github.com/erusev/parsedown/issues/681
2019-04-07 17:34:40 +01:00
b9b75dbcea Update commonmark cache 2019-04-07 16:38:46 +01:00
35d24d0b56 PHP < 7 patch 2019-04-07 16:38:46 +01:00
3f74fe8347 Port fix for https://github.com/erusev/parsedown/issues/699 into new
codebase
2019-04-07 16:38:46 +01:00
710a6ad250 Remove format error 2019-04-07 16:38:46 +01:00
9b9c9d83d2 Update CommonMark expect to pass 2019-04-07 16:38:46 +01:00
d32f5de2fe According to GFM spec these shouldn't infact be recognised 2019-04-07 16:38:45 +01:00
99525fdd76 Match GFM spec for extended autolinks
There is a small list of characters which may precede an autolink
2019-04-07 16:38:45 +01:00
de1e0b9361 Update README to match Travis breadth 2019-04-07 16:38:45 +01:00
c86757b6ae Use caret operator for PHP version
As suggested by @glensc
2019-04-07 16:38:20 +01:00
33522f0aa4 Normalise link reference lookups
Ref: https://github.com/erusev/parsedown/pull/104
2019-04-07 16:38:19 +01:00
7f6127f3f8 Ensure we cover all mutations in tests 2019-04-07 16:38:19 +01:00
4adbd0b8a7 Backtracking capable inlines better expressed by interface 2019-04-07 16:38:19 +01:00
4501a094db Remove copy-pasto 2019-04-07 16:38:19 +01:00
4a215f33d4 Trim in renderer 2019-04-07 16:38:19 +01:00
3ccd64a9a1 Expand public API of Components
Ref: https://github.com/erusev/parsedown/issues/694
2019-04-07 16:38:19 +01:00
3c0b528d54 Constructor shouldn't be part of public API 2019-04-07 16:38:18 +01:00
f83ee87902 Make this pattern a bit more reusable 2019-04-07 16:38:18 +01:00
747abe7600 Test indented code boundary when list advances 2019-04-07 16:38:18 +01:00
a396fccace No contexts implies everything being appended is a blank line
(of which where is at least one).
2019-04-07 16:38:18 +01:00
93e68056a8 Further improve tests 2019-04-07 16:38:18 +01:00
4fb6ac31a5 Improve tests 2019-04-07 16:38:18 +01:00
69f6754c4d These classes of mutations would be caught be static analysis
Infection doesn't support running Psalm, so removing
the mutations is next best solution.
2019-04-07 16:38:18 +01:00
d8d483bd6a Add some component level tests 2019-04-07 16:38:17 +01:00
c0792947a6 Remove unused methods 2019-04-07 16:38:17 +01:00
658129d847 Suppress instead of writing super verbose tests 2019-04-07 16:38:17 +01:00
65450f47cd Simplify 2019-04-07 16:38:17 +01:00
fc23ca5ef5 Remove more redundant checks 2019-04-07 16:38:17 +01:00
015e476f3e Remove unused import 2019-04-07 16:38:17 +01:00
289b641a42 Remove inaccurate comment 2019-04-07 16:38:17 +01:00
147a87a4f3 These are self-explainitory 2019-04-07 16:38:16 +01:00
b90efc69ec Ensure marker is properly contained in the Inline 2019-04-07 16:38:16 +01:00
dbe37bcb0e Type check tests 2019-04-07 16:38:16 +01:00
30613b2430 Ensure $startPosition is positive 2019-04-07 16:38:16 +01:00
cef5b16ae0 Update excludes 2019-04-07 16:38:16 +01:00
5ecfc42728 Early exit if found 2019-04-07 16:38:16 +01:00
3bb24c20a6 Assert marker is correct for hard and soft breaks 2019-04-07 16:38:16 +01:00
369aea5d8d Collect State from continuable state updating block on advance 2019-04-07 16:38:15 +01:00
2b79d599fb Require State to build and advance blocks 2019-04-07 16:38:15 +01:00
8fd3c77109 Tighten requirements 2019-04-07 16:38:15 +01:00
df703dcb0e Missed one 2019-04-07 16:38:15 +01:00
36fac49ed8 Remove redundant checks
These don't appear to have a measurable positive impact on performance.
2019-04-07 16:38:15 +01:00
41fb6b0d43 Move url sanitisation out of Element class 2019-04-07 16:38:15 +01:00
a681cf631c Acquisition capable blocks as an interface 2019-04-07 16:38:15 +01:00
6ac6b7f7f7 Test blockquote whitespace handling 2019-04-07 16:38:14 +01:00
3c6578dd4b Remove deleted stage 2019-04-07 16:38:14 +01:00
c2973100e0 Fix whitespace trimming for soft and hard breaks 2019-04-07 16:38:14 +01:00
0626a83289 Test trimming doesn't occur when asymmetric 2019-04-07 16:38:14 +01:00
2efae741bb Simplify expression 2019-04-07 16:38:14 +01:00
93650fb9b5 PHP 5.5 compat 2019-04-07 16:38:14 +01:00
9bf91d7183 Cache spec locally for 5 minutes 2019-04-07 16:38:13 +01:00
f95c3bb154 --show-mutations over cat infection.log 2019-04-07 16:38:13 +01:00
660c2e43a3 Paragraph would end itself and new one must start for header row to
exist
2019-04-07 16:38:13 +01:00
d9792bb12c Ensure markers are checked when beginning blocks 2019-04-07 16:38:13 +01:00
08c40afc16 Test against HTML block endings 2019-04-07 16:38:13 +01:00
14f8ff52e1 Test continuation of indented code blocks 2019-04-07 16:38:13 +01:00
c310625b93 Length not necessary 2019-04-07 16:38:13 +01:00
811991b27d Run mutation tests after unit tests on supported platforms 2019-04-07 16:38:12 +01:00
d29f900374 Nightly doesn't have xdebug 2019-04-07 16:38:12 +01:00
efe324c08b Add mutation testing 2019-04-07 16:38:12 +01:00
54f2c4eb4c Fix recovered spaces calculation 2019-04-07 16:38:12 +01:00
117912c373 Substr over indexing string 2019-04-07 16:38:12 +01:00
63a97a926b Remove leftover hackyness 2019-04-07 16:38:12 +01:00
cb211a88a8 PHP < 7 compat 2019-04-07 16:38:12 +01:00
c49d40027f Add StateBearer which can carry state 2019-04-07 16:38:11 +01:00
4dee1e9a55 Add convenience instance-based initialisers 2019-04-07 16:38:11 +01:00
dbc0efeec0 Require integer-keyed lists so that array_merge result is predictable 2019-04-07 16:38:11 +01:00
fe1355ef9e Test strict and weak in Travis 2019-04-07 16:38:11 +01:00
f2f7433dcf Switch to CommonMarkStrict tests 2019-04-07 16:38:11 +01:00
a2bca78f7e Fix whitespace errors 2019-04-07 16:38:11 +01:00
42d21a2413 Remove cached items no that longer exist in spec.txt 2019-04-07 16:38:10 +01:00
f47ba7aa34 Track whitespace left on blank lines to match CommonMark
Test changes copy pasted to match CommonMark reference parser
2019-04-07 16:38:10 +01:00
49dd8b113d Make sure closing sequence is removed correctly 2019-04-07 16:38:10 +01:00
30763a0f38 HTML tags should have a name 2019-04-07 16:38:10 +01:00
3dd1326ded Trim paragraph contents 2019-04-07 16:38:10 +01:00
0f55cd5b26 Permit empty links 2019-04-07 16:38:10 +01:00
5ada761532 Cache new passing tests 2019-04-07 16:38:10 +01:00
4fa89c1a80 Fix regex compilation 2019-04-07 16:38:09 +01:00
7b72eb6454 As best I can tell, these were passed accidently before 2019-04-07 16:38:09 +01:00
745db11d2f Since SafeMode concerns output, spacing should still be parsed like markup 2019-04-07 16:38:09 +01:00
82d20d8ffe Markup like CommonMark 2019-04-07 16:38:09 +01:00
7fd6e0bb31 Backslash escape like CommonMark 2019-04-07 16:38:09 +01:00
eab734b457 Match CommonMark's rendering a bit better 2019-04-07 16:38:08 +01:00
2e0ad27c5e CommonMark escapes double-quotes 2019-04-07 16:38:08 +01:00
d6c97ee111 Make escaping slightly less aggressive 2019-04-07 16:38:08 +01:00
62615f4fc5 Allow empty code spans 2019-04-07 16:38:08 +01:00
50e135cd4e Update expect-to-pass CommonMark spec examples for f4e0234 2019-04-07 16:38:08 +01:00
0514997103 Add initial test/commonmark/ folder 2019-04-07 16:38:08 +01:00
4c0734d935 Sync phpunit data set and CommonMark spec example numbers 2019-04-07 16:38:07 +01:00
734b4fc3d7 Test Parsedown against cached expect-to-pass CommonMark spec examples
This test suite runs tests the same way as `test/CommonMarkTestWeak.php`, but uses a cached set of CommonMark spec examples in `test/commonmark/`. It is executed along with Parsedown's default test suite and runs various CommonMark spec examples, which are expected to pass. If they don't pass, the Parsedown build fails. The intention of this test suite is to make sure, that previously passed CommonMark spec examples don't fail due to unwanted side-effects of code changes.

You can re-create the `test/commonmark/` directory by executing the PHPUnit group `update`. The test suite will then run `test/CommonMarkTestWeak.php` and create files with the Markdown source and the resulting HTML markup of all passed tests. The command to execute looks like the following:

    $ phpunit --group update
2019-04-07 16:38:07 +01:00
4563ee592d Don't assume marker type is correct 2019-04-07 16:38:07 +01:00
cbe7b25b21 No markup can be achieved by removing the respective parsing Components 2019-04-07 16:38:07 +01:00
f0da746c7b Remove reminder comment
urlsLinked(false) is replaced by customising to remove
the inline from InlineTypes configurable when initialising
Parsedown
2019-04-07 16:38:07 +01:00
aab56cf8cc Fix strange formatting 2019-04-07 16:38:07 +01:00
48c0c34470 Caching for initial configurable values removes need to seed state 2019-04-07 16:38:07 +01:00
bc3c1544c5 Don't special case invisible
If something has no html, it doesn't need to have a newline
2019-04-07 16:38:06 +01:00
d6f526d80f Return state after block parse instead of mutating the instance copy 2019-04-07 16:38:06 +01:00
b728f254b7 Ensure Url parsing is removed all the way down (not just edge) 2019-04-07 16:38:06 +01:00
ebde35cf0d Add some style cleanup rules already followed 2019-04-07 16:38:06 +01:00
d733c262c2 Cleanup logical operators 2019-04-07 16:38:06 +01:00
19e21f2d1b Remove test extensions 2019-04-07 16:38:06 +01:00
11da347aa1 We don't need to pass a Parsedown instance down
Since Parsedown is instancible from State, we only
need to carry that down.
2019-04-07 16:38:05 +01:00
b89bd0e3c2 Add breaks configurable 2019-04-07 16:38:05 +01:00
fce09a702a Put reused code in a trait so boolean configurables are easy to make 2019-04-07 16:38:05 +01:00
8fe93f30ac Add easy way to remove Components from InlineTyes and BlockTypes 2019-04-07 16:38:05 +01:00
9f9ef78662 This should be slightly faster
Merge would honour changes, and removals
are equivalent to changing to default value.
2019-04-07 16:38:05 +01:00
57632f38fb More meaningful method name 2019-04-07 16:38:05 +01:00
5e7fb61879 More keyword fixes for pre-PHP7 2019-04-07 16:38:05 +01:00
2618509cc6 Now the class is a bit shorter we can remove these makeshift dividers 2019-04-07 16:38:04 +01:00
fce4633ff9 Inlines and Blocks as Configurables 2019-04-07 16:38:04 +01:00
eb90905d27 Default value is intrinsic to a configurable, we can just always
retrieve that.
2019-04-07 16:38:04 +01:00
5a50930cb0 Allow inlines to backtrack into characters that were suspected as
inlines but ruled out
This is required for backslash escaped Hard breaks to work:
Parsedown first checks to see if these are escape sequences,
however when they are ruled out they should not be assumed to be
plaintext since a later inline (Hardbreak) may backtrack into these
unconsumed characters.
2019-04-07 16:38:04 +01:00
1fd2e14b72 Add hard and soft breaks 2019-04-07 16:38:04 +01:00
714ae50211 Text variable isn't very useful inside the loop 2019-04-07 16:38:04 +01:00
14b3761687 Produce Blocks before converting to StateRenderables
(As we do with Inlines)
2019-04-07 16:38:04 +01:00
00821bd072 Don't remove right #'s too early (before dealing with whitesapce) 2019-04-07 16:38:03 +01:00
b8cdc6e9a5 Remove closing # sequence from header 2019-04-07 16:38:03 +01:00
9d97b8eb6a We should be using strict mode in commonmark benchmark 2019-04-07 16:38:03 +01:00
67231cbae1 Tabs are allowed after header delimiter 2019-04-07 16:38:03 +01:00
81a2050608 Headers can't start with more than three spaces 2019-04-07 16:38:03 +01:00
3d41f270c2 Better name for testing safe mode and strict mode state
Add a nice named constructor
2019-04-07 16:38:03 +01:00
bb424e606f Improve indent handling by lists 2019-04-07 16:38:03 +01:00
51c3d9d445 Make it clearer this is one backslash character 2019-04-07 16:38:02 +01:00
74bba0b2fa rtrim non hard breaking lines 2019-04-07 16:38:02 +01:00
fc37ad11ed Tabs shouldn't break thematic breaks 2019-04-07 16:38:02 +01:00
4e9a0113c3 Thematic breaks can't have an indent of more than 3 2019-04-07 16:38:02 +01:00
96d8a1f18c Add dead code test to composer test script 2019-04-07 16:38:02 +01:00
82c981657d Require Inlines to provide a best plaintext rendering
This allows markdown to be parsed "inside" the alt
attribute of an image, and then the best plaintext
can be used as the rest.
This improves CommonMark compliance.
2019-04-07 16:38:02 +01:00
576a2c4519 Generalise line parsing to return Inlines before applying state 2019-04-07 16:38:01 +01:00
083ad582c7 State management
Decouple state from final rendering
Restore initial state after parsing
2019-04-07 16:38:01 +01:00
c9388cb5c2 Make use of line name instead of lineElements 2019-04-07 16:38:01 +01:00
bb8a16ad81 Remove useless line method
This will be achievable by directly restricting enabled Components
2019-04-07 16:38:01 +01:00
2cfd05a00e Test for dead code in travis 2019-04-07 16:38:01 +01:00
7f526c07a0 Indent offset is encompassed in Lines object 2019-04-07 16:38:01 +01:00
366600034c This isn't used and is derivable from the indent 2019-04-07 16:38:01 +01:00
6add0ea877 Remove useless variables 2019-04-07 16:38:00 +01:00
dac6b01d1a Remove meaningless interrupt check
Interrupted implies previousEmptyLines > 0 in incoming Context
2019-04-07 16:38:00 +01:00
6f5780abfd Improve Link API 2019-04-07 16:38:00 +01:00
2757274854 Constant arrays to static vars for PHP 5.5 2019-04-07 16:38:00 +01:00
37895448ba Fix PHP 5.6 bug 2019-04-07 16:38:00 +01:00
6f1bc7db14 Fix pre PHP7 closure-as-property closure syntax 2019-04-07 16:38:00 +01:00
e4ed4da626 More keyword related fixes for pre PHP 7 2019-04-07 16:38:00 +01:00
351a68a14c Remove some return type hints that I missed 2019-04-07 16:37:59 +01:00
a9aa7e7aae Test on PHP 5.5 and 5.6
Issue on pre PHP 7 may be to do with method name and not call syntax as
suspected
2019-04-07 16:37:59 +01:00
f8003dcded Remove debug lines 2019-04-07 16:37:59 +01:00
ca008872ba Use latest PHP on non-unit test build staged 2019-04-07 16:37:59 +01:00
7188f49a71 7.3 was released, so make failure blocking 2019-04-07 16:37:59 +01:00
b3608829e5 Calling static functions on metatype class string is >=PHP7 only
🤷‍♂️
2019-04-07 16:37:59 +01:00
f420fad41f Remove ignore statement for core class :) 2019-04-07 16:37:59 +01:00
f58845c480 Bump Psalm version for new static analysis features 2019-04-07 16:37:58 +01:00
04816a9944 Adjust tests for new API
Remove tests that test old core and extension features
Comment out test for no markup independent of safe mode
2019-04-07 16:37:58 +01:00
e2c9b2fa2b Remap text-level elements retreival 2019-04-07 16:37:58 +01:00
799ced66fa Use Parsedown directly
Remove UrlsLinked adjustment--we'll have a better way
of doing that later
2019-04-07 16:37:58 +01:00
e6e24a8d0d Rewrite to use new internals 2019-04-07 16:37:58 +01:00
36cfb21908 Remove no longer needed parts of core class 2019-04-07 16:37:58 +01:00
dbdbda52a8 Make Parsedown non-extendable and remove comment header 2019-04-07 16:37:58 +01:00
114eb0bc5b Add required imports 2019-04-07 16:37:57 +01:00
a286033f52 Implement UrlTag 2019-04-07 16:37:57 +01:00
53bb9a6467 Implement Url 2019-04-07 16:37:57 +01:00
db1d0a4999 Implement Strikethrough 2019-04-07 16:37:57 +01:00
f256352f53 Implement SpecialCharacter 2019-04-07 16:37:57 +01:00
778eacd081 Implement Markup 2019-04-07 16:37:57 +01:00
5e8905c455 Implement Image 2019-04-07 16:37:57 +01:00
dad0088adb Implement Link 2019-04-07 16:37:56 +01:00
79a38a1ebb Implement EscapeSequence 2019-04-07 16:37:56 +01:00
164a39f3e9 Implement Emphasis 2019-04-07 16:37:56 +01:00
f2a3a2fb08 Implement Email 2019-04-07 16:37:56 +01:00
497045d25b Implement Code 2019-04-07 16:37:56 +01:00
760945008b Implement plaintext 2019-04-07 16:37:56 +01:00
25cf5a1729 Add some traits for common Inline implementations 2019-04-07 16:37:55 +01:00
18e239fba1 Implement Paragraph 2019-04-07 16:37:55 +01:00
b53971e656 Implement Table 2019-04-07 16:37:55 +01:00
0c730e0dc5 Implement Reference 2019-04-07 16:37:55 +01:00
565c8dd3cc Implement IndentedCode 2019-04-07 16:37:55 +01:00
ee094cb397 Implement Markup 2019-04-07 16:37:55 +01:00
edc004f503 Implement SetextHeader 2019-04-07 16:37:55 +01:00
af97e99b39 Implement Rule 2019-04-07 16:37:54 +01:00
a95bc60c30 Implement List 2019-04-07 16:37:54 +01:00
07c2566042 Implement Header 2019-04-07 16:37:54 +01:00
57c6350184 Implement FencedCode 2019-04-07 16:37:54 +01:00
194c916c6a Implement Comment 2019-04-07 16:37:54 +01:00
c50deda690 Implement BlockQuote 2019-04-07 16:37:54 +01:00
3094329950 Add traits to provide common block implementations 2019-04-07 16:37:54 +01:00
74a855946d Add some more exotic type specialisations of block 2019-04-07 16:37:53 +01:00
c17868cac8 Basic block and inline definitions 2019-04-07 16:37:53 +01:00
5a00cb7f07 Define a basic component 2019-04-07 16:37:53 +01:00
db657952d1 Add DefinitionBook configurable to replace definition data 2019-04-07 16:37:53 +01:00
c55dbb0d3f Add safe mode and strict mode configurables 2019-04-07 16:37:53 +01:00
072f91df47 Add some useful renderables 2019-04-07 16:37:53 +01:00
c852b487b4 Expose text string backing 2019-04-07 16:37:53 +01:00
00835c5101 Adjustments to rendering spacing 2019-04-07 16:37:52 +01:00
a971e5aa54 Put element related sanitisation and data in a centralised location 2019-04-07 16:37:52 +01:00
23cfbd153c Add canonical state renderable to provide default implementation for
renderables to be trivially state renderable
2019-04-07 16:37:52 +01:00
23560bfa33 Add handler so closures can implement state renderable via the wrapper 2019-04-07 16:37:52 +01:00
1f06b47e6c Add configurable and state 2019-04-07 16:37:52 +01:00
7746c9df06 Add state renderable -- all renderables are state renderables 2019-04-07 16:37:52 +01:00
dcc5ea0c9b Add Excerpt class 2019-04-07 16:37:52 +01:00
7ef8b30043 Capitalisation 2019-04-07 16:37:51 +01:00
deaf0682b5 Make tab shortage function public 2019-04-07 16:37:51 +01:00
3a0db641aa Test format before units 2019-04-07 16:37:51 +01:00
215953334e Update psalm version for bugfixes 2019-04-07 16:37:51 +01:00
1541859e0e PHP < 7 compat for Html renderables 2019-04-07 16:37:51 +01:00
0f6c0fa84d PHP < 7 compat
Don't use token name for function name

Remove return typehint

Remove parameter typehints
2019-04-07 16:37:51 +01:00
0f36000dc9 Add typed Context and Lines objects 2019-04-07 16:37:50 +01:00
57b86b3fc4 Use psalm for static analysis
Don't check the main Parsedown file just yet
2019-04-07 16:37:50 +01:00
f6a845fa52 Use typed Line object 2019-04-07 16:37:50 +01:00
db04e1575f Commonmark stage should be allowed to fail in travis 2019-04-07 16:37:50 +01:00
6d03fa0d3a Rephrasing 2019-04-07 16:37:50 +01:00
49829c2019 Remove HHVM for now 2019-04-07 16:37:50 +01:00
c419295466 Remove unsupported dependencies when running unit tests (PHP < 7 compat) 2019-04-07 16:37:50 +01:00
23b07fa185 Cache composer cache in travis 2019-04-07 16:37:49 +01:00
5795a6f0a9 Add travis build stages 2019-04-07 16:37:49 +01:00
a42848da57 CommonMark weak test in composer 2019-04-07 16:37:49 +01:00
82a528711f Fix commonmark test 2019-04-07 16:37:49 +01:00
8c091b8e63 Testing composer shortcuts 2019-04-07 16:37:49 +01:00
a636bf7bfa Update phpunit version 2019-04-07 16:37:49 +01:00
8512e65a18 Standardise formatting 2019-04-07 16:37:49 +01:00
267256cbb8 Fix typos 2019-04-07 16:37:48 +01:00
f8aa618f3d Default construct to empty text 2019-04-07 16:37:48 +01:00
f85f6cbd40 Add setting constructors for each property 2019-04-07 16:37:48 +01:00
822cf15ac9 Add helper constructor with variadic 2019-04-07 16:37:48 +01:00
9046f066df Html/Component -> Html/Renderable 2019-04-07 16:37:48 +01:00
7690b98f61 Correct namespace 2019-04-07 16:37:48 +01:00
04581d0915 Basic HTML constructs 2019-04-07 16:37:47 +01:00
5ab8839d04 PHP 5.3 and 5.4 class name support 2019-04-07 16:37:47 +01:00
c429c47fee Remove strict_types for PHP 5.3 and 5.4 compat 2019-04-07 16:37:47 +01:00
88ab68fd0b Refactor into namespaces for PSR-4 2019-04-07 16:37:47 +01:00
bfaa76d370 Reflect travis breadth in README
Closes https://github.com/erusev/parsedown/issues/692
2019-04-07 16:36:22 +01:00
3825db53a2 Merge branch '1.8.x-beta' 2019-04-06 17:58:29 +01:00
fe7a50eceb New release due to mislabeled previous tag 2019-03-17 18:47:21 +00:00
bce642f2d7 7.3 was released 2019-03-17 18:38:54 +00:00
7d4c06cb52 Bump version 2019-03-17 17:19:07 +00:00
f7b66e6b20 Merge pull request #701 from aidantwoods/fix/spaces-in-class-names-1.8.x-beta
[1.8.x-beta] Fix spaces in class names
2019-03-17 17:10:10 +00:00
811bc32726 Fix test platforms 2019-03-17 17:04:25 +00:00
8fd5464c46 [1.8.x-beta] Fix spaces in class names 2019-03-17 17:01:52 +00:00
21c8c792de Merge pull request #698 from cybernet/patch-1
Symfony Demo link update
2019-03-14 18:49:20 +00:00
6ca29539e1 Symfony Demo link update 2019-03-12 20:49:21 +00:00
a503c1a69b composer/composer#7990 2019-03-02 12:08:48 +02:00
819c68899d Simplify installation instructions 2018-12-28 13:17:22 +02:00
4c2d79fc6a More consistent code blocks in readme 2018-12-28 12:50:30 +02:00
48a2fb26fe Add badges to readme 2018-12-28 12:47:13 +02:00
33b79d2446 More logo padding and no underline 2018-12-28 02:21:55 +02:00
d6d2d96459 Add repo name as h1 2018-12-28 02:19:44 +02:00
d5b6ab5198 Readme logo to be centered 2018-12-28 02:14:25 +02:00
15e8439c7f Back to smaller padding in readme logo 2018-12-28 02:04:16 +02:00
ff6148f9b9 Improve readme badges 2018-12-28 01:56:45 +02:00
dfd8657bc5 h1 around logo in readme 2018-12-28 01:28:59 +02:00
ee64646765 More padding for logo in readme 2018-12-28 01:21:51 +02:00
c956090b55 Update readme logo 2018-12-28 01:07:49 +02:00
0be26550f3 Update readme logo 2018-12-28 00:55:48 +02:00
8e26a65a6f More consistent letter case in readme 2018-12-28 00:41:14 +02:00
2a24a8583b More consistent formatting in readme 2018-12-28 00:36:04 +02:00
1d55344e92 Simpler readme header 2018-12-27 23:59:37 +02:00
5dc8d1cc39 Simpler readme examples 2018-12-27 23:51:07 +02:00
33cf0f0b99 Centered header in readme 2018-12-27 23:46:53 +02:00
dc1ff7d6c2 Remove horizontal rule from readme 2018-12-27 22:50:39 +02:00
0f0987571d Bigger headings in readme 2018-12-27 22:32:54 +02:00
18eaa649b5 Add logo to readme 2018-12-27 22:23:17 +02:00
e124572b60 Merge pull request #675 from andreybolonin/patch-1
add php 7.3
2018-11-16 07:45:32 +00:00
1686a34469 add php 7.3 to allow_failures 2018-11-16 09:34:17 +02:00
2bd7113c55 Merge pull request #676 from aidantwoods/fix/uninitialized-string-offset
Fix access to potentially uninitialised offset
2018-11-06 21:49:46 +00:00
29fce0ec37 Fix access to potentially uninitialised offset 2018-11-06 21:10:23 +00:00
b0bbc275d4 add php 7.3 2018-10-31 15:48:28 +02:00
72f9ca92ae Merge pull request #671 from aidantwoods/fix/line-ending-standardisation
Ensure line-breaks get standardised when using Parsedown via `line` method
2018-10-16 18:51:05 +01:00
89c3fa05d9 Ensure line-breaks get standardised when using via line method
As noted in https://github.com/erusev/parsedown/pull/624 there are
occasions where line break standardisation is assumed (e.g. where
`inlineCode` replaces line breaks with a space).

Closes #624
2018-10-16 18:41:42 +01:00
69163d6e88 Merge pull request #670 from tillkruss/patch-1
Add "tel:" to whitelist
2018-10-14 21:48:40 +01:00
3a0c964291 Add "tel:" to whitelist 2018-10-02 16:38:21 -07:00
1829106e60 Merge pull request #668 from itshoro/block-heading-min
remove redundant header level cap
2018-09-19 21:28:02 +01:00
464f5f9329 removed min function as it is redundant in the context 2018-09-19 17:36:40 +02:00
c26a2ee4bf Bump beta version 2018-06-11 19:15:32 +01:00
ba3b60d6e4 Merge pull request #641 from aidantwoods/fix/api-stability-complete-function-removal
Restore existence of protected API methods
2018-06-08 14:38:42 +01:00
0b1e6b8c86 Restore existence of protected API methods 2018-06-07 19:47:09 +01:00
1f69f7e697 Bump version 2018-05-08 22:46:15 +01:00
c83af0a7d5 Merge pull request #628 from aidantwoods/fix/revert-rawHtml-breaks-insertion
Preserve plain-text in AST to avoid blinding extensions to it
2018-05-08 22:41:44 +01:00
4686daf8c2 Preserve plain-text in AST to avoid blinding extensions to it 2018-05-08 22:32:57 +01:00
c9e7183cfa Merge pull request #627 from aidantwoods/fix/hidden-blocks
Intepret special "hidden" key as an empty element
2018-05-08 22:07:51 +01:00
9eed1104e7 Intepret special "hidden" key as an empty element 2018-05-08 21:54:30 +01:00
fd95703da5 Version bump 2018-05-07 14:26:12 +01:00
8d172a2994 Merge pull request #614 from aidantwoods/enhancement/performance-tweaks
General and performance tweaks
2018-05-07 11:18:21 +01:00
dfab7240a4 Merge pull request #621 from paukenba/master
Tilde characters may be escaped
2018-04-24 17:39:09 +01:00
113c6d2b21 Tilde characters may be escaped 2018-04-23 15:09:30 +02:00
a9764ec90f Remove complex string interpolation expressions 2018-04-14 15:27:06 +01:00
0a842fb5b1 Merge pull request #615 from aidantwoods/fix/old-handler-compat
Compatability fixes
2018-04-12 22:29:29 +01:00
7f4318dbdb PHP 5.3 == 💩 2018-04-12 22:22:53 +01:00
3e70819a20 Readability improvements, thanks @PhrozenByte 2018-04-12 22:16:25 +01:00
2bf7ca41a0 Add compat for extensions using old markup key. 2018-04-12 21:25:50 +01:00
b75fd409ff Must unset text key so that our destination is preferred as content 2018-04-12 21:10:09 +01:00
88a3f31dd7 Rewrite as one statement 2018-04-12 19:33:01 +01:00
726d4ef44a Sanity checks before starting regex engine 2018-04-09 18:09:45 +01:00
450a74fedf More expensive statement last 2018-04-09 18:09:45 +01:00
7e15d99d90 Remove regex from block rule 2018-04-09 18:09:44 +01:00
d2dd736e1b Remove regex from fenced code block
Also remove unused function
2018-04-09 18:09:44 +01:00
e74a5bd7ed In theory PHP stores the length of strings, so looking this up should be quick 2018-04-09 18:09:44 +01:00
b53aa74a72 Use standard library function 2018-04-09 18:09:44 +01:00
3ea08140b6 Remove use of array 2018-04-09 18:09:44 +01:00
c45e41950f Use standard library over while loop 2018-04-09 18:09:44 +01:00
2faba6fef5 Remove unneeded complete function 2018-04-09 18:09:44 +01:00
b42add3762 Make some regexes possesive 2018-04-09 18:09:43 +01:00
107223d3a0 Avoid recomputation 2018-04-09 18:09:43 +01:00
d4f1ac465c String interpolation is slightly faster than concat 2018-04-09 18:09:43 +01:00
d6e306d620 Optimise commonly used regexes to fail fast 2018-04-09 18:09:04 +01:00
dc5cf8770b The AST has high complexity here (and so traversal is hard anyway)
We gain quite a bit of a speed boost by working with text here
since this is a very common function
2018-04-09 18:09:04 +01:00
70f5c02d47 Use non-nestable values as keys for O(1) lookup 2018-04-09 18:09:04 +01:00
90ad738933 General readability 2018-04-09 18:09:04 +01:00
f2327023c1 No need to unset if not set 2018-04-09 18:09:04 +01:00
6f13f97674 Use mutating loop instead of array_map 2018-04-09 18:08:58 +01:00
8091e5586a Merge pull request #612 from aidantwoods/fix/table-columns
Table header should not be allowed to contain new lines
2018-04-09 16:53:07 +01:00
cb33daf0e6 Assert table header does not contain new lines 2018-04-09 16:38:03 +01:00
c440c91af5 Add failing test case 2018-04-09 16:32:36 +01:00
3514881e14 Merge pull request #611 from aidantwoods/enhancement/paragraph-block-semantics
Paragraph block semantics
2018-04-09 16:30:33 +01:00
043c55e4c6 Give paragraph block semantics for overloading 2018-04-09 15:12:17 +01:00
e4cd13350b Remove setLiteralBreaks 2018-04-09 15:11:45 +01:00
ae8067e862 Swap undefined type for type === 'Paragraph' for ease of reading
The way in which we use this assumes that it is a paragraph, for example
appending text into the handler argument — so there is no loss of
generality here, we're simply being explicit.
2018-04-09 14:48:48 +01:00
5353ebb524 Avoid needing two arrays
We only need to collect elements, we can discard finished blocks
2018-04-09 14:48:39 +01:00
39df7d4f8e Swap 'hidden' blocks for empty elements 2018-04-09 14:46:24 +01:00
50f15add44 Merge pull request #610 from aidantwoods/fix/lost-line-breaks
Fix lost line breaks
2018-04-09 14:19:38 +01:00
3f5b0ee781 Count number of interrupts 2018-04-09 14:13:10 +01:00
9a021b2130 Add failing test cases 2018-04-09 14:11:49 +01:00
43d25a74fe Fix function name 2018-04-08 18:40:50 +01:00
1d68e5506c Merge pull request #608 from aidantwoods/fix/recursion
Add seperate depth-first function instead of replacing recursive method
2018-04-08 18:02:17 +01:00
86940be224 Use mutating loop instead of creating new array 2018-04-08 17:49:36 +01:00
cdaf86b039 Add seperate depth-first function instead of replacing recursive method 2018-04-08 17:39:24 +01:00
1d65fb858a Restore file permission to that of 1.7.1 2018-04-08 14:30:23 +01:00
600db7e4de Bump version const to 1.8.0-beta-1 2018-04-08 02:46:30 +01:00
1be2a01de8 Merge pull request #607 from aidantwoods/update/readme-install-instructions
Update readme install instructions
2018-04-07 16:59:49 +01:00
f50ba3d803 Merge pull request #606 from erusev/revert-605-fix/this-php5.3-compat
Replace fix in "Explicitly capture $this for PHP 5.3"
2018-04-06 21:00:52 +01:00
387ef63888 Replace array reduce with foreach loop for PHP 5.3 compat 2018-04-06 20:55:27 +01:00
68be90348c Revert "Explicitly capture $this for PHP 5.3" 2018-04-06 20:50:34 +01:00
48b9f71bdc Merge pull request #605 from aidantwoods/fix/this-php5.3-compat
Explicitly capture $this for PHP 5.3
2018-04-06 20:44:32 +01:00
0039cd00f8 Explicitly capture $this for PHP 5.3 2018-04-06 20:40:25 +01:00
c6b717cc35 Merge pull request #604 from aidantwoods/fix/autobreak
Ensure autobreak false is honoured over empty name
2018-04-06 20:00:32 +01:00
8f3f61883d Merge pull request #603 from aidantwoods/fix/recursion
Apply depth first
2018-04-06 19:57:55 +01:00
4c9ea94d0c Apply depth first to avoid risk of segfault if closure creates subelements 2018-04-06 19:52:25 +01:00
32e69de014 Ensure autobreak false is honoured over empty name 2018-04-06 19:51:27 +01:00
201299ddc2 Merge pull request #602 from aidantwoods/enhancement/text-elements
Split some of `text` into `textElements`
2018-04-06 19:50:24 +01:00
557db7c179 Split some of text into textElements
`process` is no longer needed
2018-04-06 18:11:27 +01:00
0c0ed38290 Don't encourage downloading from master 2018-04-06 16:13:29 +01:00
798bda682e Update README.md
updated installation instructions. fixes #242
2018-04-06 16:09:36 +01:00
9b7b7348b4 Merge pull request #598 from aidantwoods/enhancement/set-literal-breaks
Add literalBreaks support
2018-04-06 15:06:45 +01:00
96581dbe16 Merge pull request #600 from aidantwoods/fix/code-block-closer
Fix fenced code block closer length rules
2018-04-05 18:46:49 +01:00
06b810cd4a Fix fenced code block closer to match CommonMark rules 2018-04-05 16:55:14 +01:00
38ea813b0e Add failing test case 2018-04-05 16:54:35 +01:00
24e48e91c8 Add literalBreaks support
Line breaks will be converted to <br />
2018-04-05 01:01:52 +01:00
e33f1a48c8 Merge pull request #596 from aidantwoods/enhancement/test-on-php72
Test on PHP 7.2
2018-04-02 19:56:02 +01:00
1c8f6bc253 Merge pull request #594 from aidantwoods/enhancement/ast-recursion-helper
Add recursive helper for AST
2018-04-02 19:53:58 +01:00
ed3e967fb6 Update README 2018-04-02 19:52:26 +01:00
3b3d13489b Test on PHP 7.2 2018-04-02 19:51:07 +01:00
498c88c4eb Merge pull request #589 from NathanBaulch/hashtag
Support #hashtag per CommonMark and GFM specs
2018-04-02 17:25:28 +01:00
772c919b05 Fix bug where empty atx headings would not be recognised (CommonMark)
Fixes #595
2018-04-02 17:18:01 +01:00
cf6d23de55 Rename hashtags enabled to strict mode
We can use this to seperate any intentional spec deviations from
spec behaviour so users can pick between compatability and spec
implementations
2018-04-02 17:18:01 +01:00
d0279cdd3b Enable #hashtag support via setting 2018-04-02 17:18:01 +01:00
8a90586218 Support #hashtag per CommonMark and GFM specs 2018-04-02 17:18:00 +01:00
390fa0da1b This is probably faster than duplicating the closure 2018-04-01 17:55:32 +01:00
9026b1abdb Add recursive helper for AST, use this for implementation of calling handler
recursively
2018-04-01 17:10:49 +01:00
68736f8800 Merge pull request #593 from aidantwoods/enhancement/process
Decouple manipulating final AST from `text` method
2018-04-01 16:37:44 +01:00
535110c57e Add process method so extensions may process final AST without
copying implementation of `text`
2018-03-31 23:23:12 +01:00
ce073c9baa Merge pull request #576 from aidantwoods/enhancement/moar-ast
Produce AST prior to render
2018-03-31 23:11:45 +01:00
e4d6c8f911 Add support for recursive handlers 2018-03-31 22:01:38 +01:00
cbe2e74d52 Merge pull request #592 from aidantwoods/fix/html-comment
Fix HTML comment endings
2018-03-30 19:28:40 +01:00
aa90dd481a Match CommonMark spec on HTML comments:
Start condition: line begins with the string `<!--`.
End condition: line contains the string `-->`.
2018-03-30 19:22:50 +01:00
20e592359f Add failing test case 2018-03-30 19:22:13 +01:00
9f1f5de387 Add recursive handle methods to generate entire AST for traversal 2018-03-28 20:59:56 +01:00
40b9da7837 Merge pull request #587 from aidantwoods/fix/ol-interrupt
Fix ordered list interrupt
2018-03-28 15:43:44 +01:00
a9c21447ce Only interrupt paragraph if starting with 1 2018-03-28 15:38:11 +01:00
a3e02c1d0e Add failing test case 2018-03-28 15:37:47 +01:00
40e797031e Old handler compatability layer 2018-03-28 03:42:38 +01:00
448b72a149 Merge pull request #586 from aidantwoods/fix/blockquote-adjacent
Fix merging of adjacent blockquotes
2018-03-28 03:31:36 +01:00
92e426e0e8 Fix merging of adjacent blockquotes 2018-03-28 03:27:09 +01:00
07216480db Change test to comply with CommonMark 2018-03-28 03:26:45 +01:00
caea783006 Add failing test case 2018-03-28 03:24:01 +01:00
d849d64611 Merge pull request #584 from aidantwoods/fix/tables
Permit 1 column tables with less delimiters
2018-03-27 23:18:41 +01:00
00e51ee424 Permit 1 column tables with less delimiters 2018-03-27 23:12:51 +01:00
0550c3eaf9 Merge pull request #583 from aidantwoods/fix/atx-headings
Fix trimming of internal #'s in ATX headers
2018-03-27 22:10:39 +01:00
790aed42ab Fix trimming of internal #'s 2018-03-27 22:04:11 +01:00
1c52cb6b5e Add failing test cases 2018-03-27 22:01:32 +01:00
ae13290221 Merge pull request #574 from aidantwoods/fix/remove-legacy-escaping
Remove legacy escaping
2018-03-27 13:18:30 +01:00
e16162e288 Merge pull request #582 from aidantwoods/fix/formatting
Formatting adjustments
2018-03-27 12:12:01 +01:00
244ea0aaa6 Remove some whitespace 2018-03-27 12:11:00 +01:00
2f291e0b2f Merge pull request #439 from aidantwoods/patch-4
Improve CommonMark mixed-marker list compliance
2018-03-27 11:31:34 +01:00
d2a73f9179 Trim whitespace 2018-03-27 11:23:04 +01:00
f594d4c18b Add more tests for CommonMark compliance 2018-03-27 11:20:04 +01:00
21cdd8a0b3 Merge branch 'master' into patch-4 2018-03-27 11:13:06 +01:00
a52d386250 Merge pull request #579 from aidantwoods/enhancement/gitignore
Add .gitignore for repo specific paths
2018-03-26 19:06:10 +01:00
dd9f4036ee Add .gitignore to export ignore in .gitattribtutes 2018-03-26 18:47:33 +01:00
e7fbbf537b Add repo specific paths to .gitignore 2018-03-26 18:45:34 +01:00
cac63f6fcb Merge pull request #578 from aidantwoods/fix/setext-heading-spaces
Fix setext heading space handling
2018-03-25 23:08:31 +01:00
f71bec00f4 Fix space handling in setext headings 2018-03-25 22:50:42 +01:00
913e04782f Add failing test cases to be fixed 2018-03-25 22:50:16 +01:00
1fa6b038af PHP 5.3 compat 2018-03-25 20:00:31 +01:00
e59fbd736d Remove 'markup' key exception for outputting via AST 2018-03-25 20:00:31 +01:00
8c14c5c239 Use rawHtml to provide conditional escaping for markup 2018-03-25 20:00:30 +01:00
0205a4cbe6 Use rawHtml to provide conditional escaping on special chars 2018-03-25 19:59:11 +01:00
011465bca6 Use rawHtml to provide conditional escaping for specialChars 2018-03-25 19:59:11 +01:00
adcba80502 Implement unmarked text via AST 2018-03-25 19:59:11 +01:00
65d7bc5013 Special casing for elements with no name 2018-03-25 19:59:11 +01:00
1a47e74be1 Quotes are permitted in escaped body 2018-03-25 19:59:05 +01:00
56cc41803a Merge pull request #514 from Daniel-KM/fix/consistency_follow
Added tests for consistency when a markdown follows a markup without blank line
2018-03-25 19:53:21 +01:00
d86d839677 Merge branch 'master' into fix/consistency_follow 2018-03-25 19:37:04 +01:00
d5ded2b935 Decouple handler argument from structure keys 2018-03-21 16:02:57 +00:00
098f24d189 Seperate handler delegation from AST
This also splits 'text' into 'text', 'elements', and
'element' to hopefully better communicate structure
2018-03-21 02:32:01 +00:00
eb55e426b9 Initial refactor to use AST 2018-03-21 02:18:34 +00:00
ced6187ca5 Merge pull request #575 from cebe/patch-3
Added inline example to README
2018-03-20 16:33:53 +00:00
972648ff64 Added inline example to README
see https://github.com/erusev/parsedown/issues/562
2018-03-20 16:56:40 +01:00
77dc0a090a Merge pull request #569 from aidantwoods/feature/unsafe-html
Add unsafeHtml option for extensions to use on trusted input
2018-03-18 21:58:48 +00:00
88dc949890 Refactor based on suggestion by @PhrozenByte 2018-03-18 20:17:12 +00:00
624a08b7eb Update commment 2018-03-15 19:55:33 +00:00
3fc54bc966 Allow extension to "vouch" for raw HTML they produce
Rename "unsafeHtml" to "rawHtml"
2018-03-15 19:46:03 +00:00
ef7ed7b66c Still grab the text if safe mode enabled, but output it escaped 2018-03-15 11:09:55 +00:00
e4c5be026d Further attempt to dissuade this feature's use 2018-03-15 11:00:03 +00:00
e6444bb57e Add unsafeHtml option for extensions to use on trusted input 2018-03-15 10:48:38 +00:00
a3265e7c6f Merge pull request #511 from aidantwoods/feature/null-name-element
Allow element to have no name
2018-03-15 09:41:16 +00:00
aac00ac742 Merge pull request #566 from aidantwoods/fix/email-autolink
Email autolink shouldn't be started by HTML tags
2018-03-10 00:06:59 +00:00
6830c3339f Readability
Thanks @PhrozenByte for the suggestion :)
2018-03-09 17:38:41 +00:00
19f1bb9353 Disable backtracking where the regex doesn't need it 2018-03-09 17:06:14 +00:00
721b885dd3 Fix #565 by validating email as defined in commonmark spec 2018-03-09 17:05:42 +00:00
f70d96479a Add test case for email surrounded by tags 2018-03-09 16:48:32 +00:00
72d30d33bc allow element to have no name 2018-03-01 01:17:32 +00:00
c05ef0c12a Merge branch 'aidantwoods-htmlblocks' into fix/consistency_follow 2017-06-23 00:00:00 +02:00
47e4163a68 Merge branch 'htmlblocks' of https://github.com/aidantwoods/parsedown into aidantwoods-htmlblocks 2017-06-23 00:00:00 +02:00
c05bff047a correct test to match CommonMark specified input for output 2017-06-22 00:03:12 +01:00
6a4afac0d0 remove ability for htmlblock to allow paragraph after if it closes on the same line 2017-06-22 00:02:03 +01:00
129f807e32 Inverted checks of consistency for markdown following markups. 2017-06-22 00:00:00 +02:00
be963a6531 Added tests for consistency when a markdown follows a markup without blank line. 2017-06-19 00:00:00 +02:00
1d0af35f10 update test to result generated by CommonMark reference parser 2017-03-29 18:26:07 +01:00
d7956e3ade blockmarkup ends on interrupt by newline (CommonMark compliance) 2017-03-29 18:25:56 +01:00
67e454e300 Merge pull request #2 from PhrozenByte/aidantwoods/patch-4
Use the list marker width to determine whether a list item is continued
2016-10-14 08:29:11 +01:00
a3836b1853 Handle subsequent list items which aren't indented sufficiently
Subsequent list items which aren't indented sufficiently are treated as part of the original list, see CommonMark spec example [#256](http://spec.commonmark.org/0.26/#example-256).
2016-10-13 20:44:02 +02:00
a9e1163c85 Fix code formatting 2016-10-13 19:52:38 +02:00
7b1529fff0 Use the list marker width to determine whether a list item is continued
This basically represents [list item parsing](http://spec.commonmark.org/0.26/#list-items), rule 1 of the CommonMark specs.
2016-10-13 19:51:32 +02:00
1d61f90bf9 Support list items starting with indented code 2016-10-13 19:47:06 +02:00
4b3b7df710 Support list items starting with a blank line
According to the CommonMark specs ([list items](http://spec.commonmark.org/0.26/#list-items), rule 3), list items starting with a blank line basically behave like as if the \n doesn't exist. Also see example [#241](http://spec.commonmark.org/0.26/#example-241).
2016-10-13 19:46:29 +02:00
30ff5c6e75 Remove unused $placeholder variable 2016-10-13 19:31:35 +02:00
bdf537e9d5 Fix ordered list start argument
See CommonMark spec examples [#226](http://spec.commonmark.org/0.26/#example-226) to #229
2016-10-13 19:30:50 +02:00
81025cd468 Revert "Break less previously passed CommonMarkWeak tests"
This reverts commit 2db3199510.
2016-10-13 19:25:43 +02:00
e691034861 Revert "Prevent failure with data set 77 in CommonMarkWeak"
This reverts commit 0a43799da4.
2016-10-13 19:25:37 +02:00
eb853da92a Revert "Prevent breaking remaining previously compliant CommonMarkWeak tests"
This reverts commit 6973302ca8.
2016-10-13 19:25:30 +02:00
6973302ca8 Prevent breaking remaining previously compliant CommonMarkWeak tests 2016-10-13 15:55:13 +01:00
0a43799da4 Prevent failure with data set 77 in CommonMarkWeak 2016-10-13 14:29:52 +01:00
2db3199510 Break less previously passed CommonMarkWeak tests 2016-10-12 18:10:44 +01:00
8965c7864f More appropriate tests for these changes 2016-10-11 20:55:59 +01:00
d26b33c20f Add ) as an ordered list marker
Also added marker check to ordered list case when deciding to continue the current list
2016-10-11 19:18:43 +01:00
d9679141fa Update test to comply with CommonMark 2016-10-11 13:50:47 +01:00
0bd61a73ed Check that the current line is a list before starting a new one 2016-10-11 13:48:38 +01:00
06c4344a71 Contextual limits on indentation stripping 2016-10-11 13:38:47 +01:00
c4d4a6800d (beginning to) improve commonmark compliance:lists
These changes aren't fit for merge, nor do they work correctly (yet)
2016-10-11 12:05:33 +01:00
1704 changed files with 12495 additions and 2203 deletions

1
.gitattributes vendored
View File

@ -1,5 +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

98
.github/workflows/ci.yml vendored Normal file
View File

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

6
.gitignore vendored Normal file
View File

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

37
.php-cs-fixer.dist.php Normal file
View 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 (new Config)
->setRules($rules)
->setFinder($finder)
->setUsingCache(false)
->setRiskyAllowed(true)
;

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -13,21 +13,36 @@
}
],
"require": {
"php": ">=5.3.0",
"php": "^7.1||^8.0",
"ext-mbstring": "*"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35"
"phpunit/phpunit": "^9.3.11||^8.5.21||^7.5.20",
"vimeo/psalm": "^4.10.0",
"friendsofphp/php-cs-fixer": "^3.0.0",
"infection/infection": "^0.25.0",
"roave/infection-static-analysis-plugin": "^1.10.0"
},
"autoload": {
"psr-0": {"Parsedown": ""}
"psr-4": {"Erusev\\Parsedown\\": "src/"}
},
"autoload-dev": {
"psr-0": {
"TestParsedown": "test/",
"ParsedownTest": "test/",
"CommonMarkTest": "test/",
"CommonMarkTestWeak": "test/"
}
"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"
}
}

View File

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

18
infection.json.dist Normal file
View 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
}
}

View File

@ -1,8 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>
<groups>
<exclude>
<group>update</group>
</exclude>
</groups>
</phpunit>

39
psalm.xml Normal file
View File

@ -0,0 +1,39 @@
<?xml version="1.0"?>
<psalm
errorLevel="1"
strictBinaryOperands="true"
checkForThrowsDocblock="true"
>
<projectFiles>
<directory name="src" />
<directory name="tests" />
</projectFiles>
<issueHandlers>
<PossiblyUnusedMethod>
<errorLevel type="suppress">
<directory name="tests" />
</errorLevel>
</PossiblyUnusedMethod>
<PropertyNotSetInConstructor>
<errorLevel type="suppress"><directory name="tests" /></errorLevel>
</PropertyNotSetInConstructor>
<UnusedClass>
<errorLevel type="suppress"><directory name="tests" /></errorLevel>
</UnusedClass>
<UndefinedInterfaceMethod>
<errorLevel type="suppress"><directory name="tests/src" /></errorLevel>
</UndefinedInterfaceMethod>
<PossiblyNullArrayAccess>
<errorLevel type="suppress"><directory name="tests/src" /></errorLevel>
</PossiblyNullArrayAccess>
<PossiblyNullReference>
<errorLevel type="suppress"><directory name="tests/src" /></errorLevel>
</PossiblyNullReference>
<InternalMethod>
<errorLevel type="suppress">
<directory name="tests" />
</errorLevel>
</InternalMethod>
</issueHandlers>
</psalm>

34
src/AST/Handler.php Normal file
View 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);
}
}

View 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
View File

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

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

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

View File

@ -0,0 +1,121 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
use Erusev\Parsedown\AST\Handler;
use Erusev\Parsedown\Components\Block;
use Erusev\Parsedown\Components\ContinuableBlock;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Html\Renderables\Text;
use Erusev\Parsedown\Parsedown;
use Erusev\Parsedown\Parsing\Context;
use Erusev\Parsedown\Parsing\Line;
use Erusev\Parsedown\Parsing\Lines;
use Erusev\Parsedown\State;
final class BlockQuote implements ContinuableBlock
{
/** @var Lines */
private $Lines;
/**
* @param Lines $Lines
*/
private function __construct($Lines)
{
$this->Lines = $Lines;
}
/**
* @param Context $Context
* @param State $State
* @param Block|null $Block
* @return static|null
*/
public static function build(
Context $Context,
State $State,
Block $Block = null
) {
if (\preg_match('/^(>[ \t]?+)(.*+)/', $Context->line()->text(), $matches)) {
$indentOffset = $Context->line()->indentOffset() + $Context->line()->indent() + \strlen($matches[1]);
$recoveredSpaces = 0;
if (\strlen($matches[1]) === 2 && \substr($matches[1], 1, 1) === "\t") {
$recoveredSpaces = Line::tabShortage(0, $indentOffset -1) -1;
}
return new self(Lines::fromTextLines(
\str_repeat(' ', $recoveredSpaces) . $matches[2],
$indentOffset
));
}
return null;
}
/**
* @param Context $Context
* @param State $State
* @return self|null
*/
public function advance(Context $Context, State $State)
{
if ($Context->precedingEmptyLines() > 0) {
return null;
}
if (\preg_match('/^(>[ \t]?+)(.*+)/', $Context->line()->text(), $matches)) {
$indentOffset = $Context->line()->indentOffset() + $Context->line()->indent() + \strlen($matches[1]);
$recoveredSpaces = 0;
if (\strlen($matches[1]) === 2 && \substr($matches[1], 1, 1) === "\t") {
$recoveredSpaces = Line::tabShortage(0, $indentOffset -1) -1;
}
$Lines = $this->Lines->appendingTextLines(
\str_repeat(' ', $recoveredSpaces) . $matches[2],
$indentOffset
);
return new self($Lines);
}
if (!($Context->precedingEmptyLines() > 0)) {
$indentOffset = $Context->line()->indentOffset() + $Context->line()->indent();
$Lines = $this->Lines->appendingTextLines($Context->line()->text(), $indentOffset);
return new self($Lines);
}
return null;
}
/**
* @return array{Block[], State}
*/
public function contents(State $State)
{
return Parsedown::blocks($this->Lines, $State);
}
/**
* @return Handler<Element>
*/
public function stateRenderable()
{
return new Handler(
/** @return Element */
function (State $State) {
list($Blocks, $State) = $this->contents($State);
$StateRenderables = Parsedown::stateRenderablesFrom($Blocks);
$Renderables = $State->applyTo($StateRenderables);
$Renderables[] = new Text("\n");
return new Element('blockquote', [], $Renderables);
}
);
}
}

View File

@ -0,0 +1,149 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
use Erusev\Parsedown\Components\Block;
use Erusev\Parsedown\Components\ContinuableBlock;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Html\Renderables\Text;
use Erusev\Parsedown\Parsing\Context;
use Erusev\Parsedown\State;
final class FencedCode implements ContinuableBlock
{
/** @var string */
private $code;
/** @var string */
private $infostring;
/** @var string */
private $marker;
/** @var int */
private $openerLength;
/** @var bool */
private $isComplete;
/**
* @param string $code
* @param string $infostring
* @param string $marker
* @param int $openerLength
* @param bool $isComplete
*/
private function __construct($code, $infostring, $marker, $openerLength, $isComplete)
{
$this->code = $code;
$this->infostring = $infostring;
$this->marker = $marker;
$this->openerLength = $openerLength;
$this->isComplete = $isComplete;
}
/**
* @param Context $Context
* @param State $State
* @param Block|null $Block
* @return static|null
*/
public static function build(
Context $Context,
State $State,
Block $Block = null
) {
$marker = \substr($Context->line()->text(), 0, 1);
if ($marker !== '`' && $marker !== '~') {
return null;
}
$openerLength = \strspn($Context->line()->text(), $marker);
if ($openerLength < 3) {
return null;
}
$infostring = \trim(\substr($Context->line()->text(), $openerLength), "\t ");
if (\strpos($infostring, '`') !== false) {
return null;
}
return new self('', $infostring, $marker, $openerLength, false);
}
/**
* @param Context $Context
* @param State $State
* @return self|null
*/
public function advance(Context $Context, State $State)
{
if ($this->isComplete) {
return null;
}
$newCode = $this->code;
$newCode .= $Context->precedingEmptyLinesText();
if (($len = \strspn($Context->line()->text(), $this->marker)) >= $this->openerLength
&& \chop(\substr($Context->line()->text(), $len), ' ') === ''
) {
return new self($newCode, $this->infostring, $this->marker, $this->openerLength, true);
}
$newCode .= $Context->line()->rawLine() . "\n";
return new self($newCode, $this->infostring, $this->marker, $this->openerLength, false);
}
/** @return string */
public function infostring()
{
return $this->infostring;
}
/** @return string */
public function code()
{
return $this->code;
}
/**
* @return Element
*/
public function stateRenderable()
{
/**
* https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
* Every HTML element may have a class attribute specified.
* The attribute, if specified, must have a value that is a set
* of space-separated tokens representing the various classes
* that the element belongs to.
* [...]
* The space characters, for the purposes of this specification,
* are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab),
* U+000A LINE FEED (LF), U+000C FORM FEED (FF), and
* U+000D CARRIAGE RETURN (CR).
*/
$infostring = \substr(
$this->infostring(),
0,
\strcspn($this->infostring(), " \t\n\f\r")
);
// only necessary pre-php7
if ($infostring === false) {
$infostring = '';
}
return new Element('pre', [], [new Element(
'code',
$infostring !== '' ? ['class' => "language-{$infostring}"] : [],
[new Text($this->code())]
)]);
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
use Erusev\Parsedown\AST\Handler;
use Erusev\Parsedown\Components\Block;
use Erusev\Parsedown\Configurables\HeaderSlug;
use Erusev\Parsedown\Configurables\SlugRegister;
use Erusev\Parsedown\Configurables\StrictMode;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Parsedown;
use Erusev\Parsedown\Parsing\Context;
use Erusev\Parsedown\State;
final class Header implements Block
{
/** @var string */
private $text;
/** @var 1|2|3|4|5|6 */
private $level;
/**
* @param string $text
* @param 1|2|3|4|5|6 $level
*/
private function __construct($text, $level)
{
$this->text = $text;
$this->level = $level;
}
/**
* @param Context $Context
* @param State $State
* @param Block|null $Block
* @return static|null
*/
public static function build(
Context $Context,
State $State,
Block $Block = null
) {
if ($Context->line()->indent() > 3) {
return null;
}
$level = \strspn($Context->line()->text(), '#');
if ($level > 6 || $level < 1) {
return null;
}
/** @var 1|2|3|4|5|6 $level */
$text = \ltrim($Context->line()->text(), '#');
$firstChar = \substr($text, 0, 1);
if (
$State->get(StrictMode::class)->isEnabled()
&& \trim($firstChar, " \t") !== ''
) {
return null;
}
$text = \trim($text, " \t");
# remove closing sequence
$removedClosing = \rtrim($text, '#');
$lastChar = \substr($removedClosing, -1);
if (\trim($lastChar, " \t") === '') {
$text = \rtrim($removedClosing, " \t");
}
return new self($text, $level);
}
/** @return string */
public function text()
{
return $this->text;
}
/** @return 1|2|3|4|5|6 */
public function level()
{
return $this->level;
}
/**
* @return Handler<Element>
*/
public function stateRenderable()
{
return new Handler(
/** @return Element */
function (State $State) {
$HeaderSlug = $State->get(HeaderSlug::class);
$Register = $State->get(SlugRegister::class);
$attributes = (
$HeaderSlug->isEnabled()
? ['id' => $HeaderSlug->transform($Register, $this->text())]
: []
);
return new Element(
'h' . \strval($this->level()),
$attributes,
$State->applyTo(Parsedown::line($this->text(), $State))
);
}
);
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
use Erusev\Parsedown\Components\Block;
use Erusev\Parsedown\Components\ContinuableBlock;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Html\Renderables\Text;
use Erusev\Parsedown\Parsing\Context;
use Erusev\Parsedown\Parsing\Line;
use Erusev\Parsedown\State;
final class IndentedCode implements ContinuableBlock
{
/** @var string */
private $code;
/**
* @param string $code
*/
private function __construct($code)
{
$this->code = $code;
}
/**
* @param Context $Context
* @param State $State
* @param Block|null $Block
* @return static|null
*/
public static function build(
Context $Context,
State $State,
Block $Block = null
) {
if (isset($Block) && $Block instanceof Paragraph && ! ($Context->precedingEmptyLines() > 0)) {
return null;
}
if ($Context->line()->indent() < 4) {
return null;
}
return new self($Context->line()->ltrimBodyUpto(4) . "\n");
}
/**
* @param Context $Context
* @param State $State
* @return self|null
*/
public function advance(Context $Context, State $State)
{
if ($Context->line()->indent() < 4) {
return null;
}
$newCode = $this->code;
$offset = $Context->line()->indentOffset();
if ($Context->precedingEmptyLines() > 0) {
foreach (\explode("\n", $Context->precedingEmptyLinesText()) as $line) {
$newCode .= (new Line($line, $offset))->ltrimBodyUpto(4) . "\n";
}
$newCode = \substr($newCode, 0, -1);
}
$newCode .= $Context->line()->ltrimBodyUpto(4) . "\n";
return new self($newCode);
}
/** @return string */
public function code()
{
return $this->code;
}
/**
* @return Element
*/
public function stateRenderable()
{
return new Element(
'pre',
[],
[new Element('code', [], [new Text($this->code())])]
);
}
}

View File

@ -0,0 +1,256 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
use Erusev\Parsedown\AST\Handler;
use Erusev\Parsedown\Components\Block;
use Erusev\Parsedown\Components\ContinuableBlock;
use Erusev\Parsedown\Configurables\SafeMode;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Html\Renderables\RawHtml;
use Erusev\Parsedown\Html\Renderables\Text;
use Erusev\Parsedown\Parsing\Context;
use Erusev\Parsedown\State;
final class Markup implements ContinuableBlock
{
private const REGEX_HTML_ATTRIBUTE = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+';
private const BLOCK_ELEMENTS = [
'address' => true,
'article' => true,
'aside' => true,
'base' => true,
'basefont' => true,
'blockquote' => true,
'body' => true,
'caption' => true,
'center' => true,
'col' => true,
'colgroup' => true,
'dd' => true,
'details' => true,
'dialog' => true,
'dir' => true,
'div' => true,
'dl' => true,
'dt' => true,
'fieldset' => true,
'figcaption' => true,
'figure' => true,
'footer' => true,
'form' => true,
'frame' => true,
'frameset' => true,
'h1' => true,
'h2' => true,
'h3' => true,
'h4' => true,
'h5' => true,
'h6' => true,
'head' => true,
'header' => true,
'hr' => true,
'html' => true,
'iframe' => true,
'legend' => true,
'li' => true,
'link' => true,
'main' => true,
'menu' => true,
'menuitem' => true,
'nav' => true,
'noframes' => true,
'ol' => true,
'optgroup' => true,
'option' => true,
'p' => true,
'param' => true,
'section' => true,
'source' => true,
'summary' => true,
'table' => true,
'tbody' => true,
'td' => true,
'tfoot' => true,
'th' => true,
'thead' => true,
'title' => true,
'tr' => true,
'track' => true,
'ul' => true,
];
private const SIMPLE_CONTAINS_END_CONDITIONS = [
2 => '-->',
3 => '?>',
4 => '>',
5 => ']]>',
];
private const SPECIAL_HTML_BLOCK_TAGS = [
'script' => true,
'style' => true,
'pre' => true,
];
/** @var string */
private $html;
/** @var 1|2|3|4|5|6|7 */
private $type;
/** @var bool */
private $closed;
/**
* @param string $html
* @param 1|2|3|4|5|6|7 $type
* @param bool $closed
*/
private function __construct($html, $type, $closed = false)
{
$this->html = $html;
$this->type = $type;
$this->closed = $closed;
}
/**
* @param Context $Context
* @param State $State
* @param Block|null $Block
* @return static|null
*/
public static function build(
Context $Context,
State $State,
Block $Block = null
) {
$text = $Context->line()->text();
$rawLine = $Context->line()->rawLine();
if (\preg_match('/^<(?:script|pre|style)(?:\s++|>|$)/i', $text)) {
return new self($rawLine, 1, self::closes12345TypeMarkup(1, $text));
}
if (\substr($text, 0, 4) === '<!--') {
return new self($rawLine, 2, self::closes12345TypeMarkup(2, $text));
}
if (\substr($text, 0, 2) === '<?') {
return new self($rawLine, 3, self::closes12345TypeMarkup(3, $text));
}
if (\preg_match('/^<![A-Z]/', $text)) {
return new self($rawLine, 4, self::closes12345TypeMarkup(4, $text));
}
if (\substr($text, 0, 9) === '<![CDATA[') {
return new self($rawLine, 5, self::closes12345TypeMarkup(5, $text));
}
if (\preg_match('/^<([\/]?+)(\w++)(.*+)$/', $text, $matches)) {
$isClosing = ($matches[1] === '/');
$element = \strtolower($matches[2]);
$tail = $matches[3];
if (\array_key_exists($element, self::BLOCK_ELEMENTS)
&& \preg_match('/^(?:\s|$|>|\/)/', $tail)
) {
return new self($rawLine, 6);
}
if (
! $isClosing && \preg_match(
'/^(?:[ ]*+'.self::REGEX_HTML_ATTRIBUTE.')*(?:[ ]*+)[\/]?+[>](.*+)$/',
$tail,
$matches
) || $isClosing && \preg_match(
'/^(?:[ ]*+)[\/]?+[>](.*+)$/',
$tail,
$matches
)
) {
$tail = $matches[1];
if (! \array_key_exists($element, self::SPECIAL_HTML_BLOCK_TAGS)
&& ! (isset($Block) && $Block instanceof Paragraph && $Context->precedingEmptyLines() < 1)
&& \preg_match('/^\s*+$/', $tail)
) {
return new self($rawLine, 7);
}
}
}
return null;
}
/**
* @param Context $Context
* @param State $State
* @return self|null
*/
public function advance(Context $Context, State $State)
{
$closed = $this->closed;
$type = $this->type;
if ($closed) {
return null;
}
if (($type === 6 || $type === 7) && $Context->precedingEmptyLines() > 0) {
return null;
}
if ($type === 1 || $type === 2 || $type === 3 || $type === 4 || $type === 5) {
$closed = self::closes12345TypeMarkup($type, $Context->line()->text());
}
$html = $this->html . \str_repeat("\n", $Context->precedingEmptyLines() + 1);
$html .= $Context->line()->rawLine();
return new self($html, $type, $closed);
}
/**
* @param 1|2|3|4|5 $type
* @param string $text
* @return bool
*/
private static function closes12345TypeMarkup($type, $text)
{
if ($type === 1) {
if (\preg_match('/<\/(?:script|pre|style)>/i', $text)) {
return true;
}
} elseif (\stripos($text, self::SIMPLE_CONTAINS_END_CONDITIONS[$type]) !== false) {
return true;
}
return false;
}
/** @return string */
public function html()
{
return $this->html;
}
/**
* @return Handler<Element|RawHtml>
*/
public function stateRenderable()
{
return new Handler(
/** @return Element|RawHtml */
function (State $State) {
if ($State->get(SafeMode::class)->isEnabled()) {
return new Element('p', [], [new Text($this->html())]);
} else {
return new RawHtml($this->html());
}
}
);
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
use Erusev\Parsedown\AST\Handler;
use Erusev\Parsedown\Components\Block;
use Erusev\Parsedown\Components\ContinuableBlock;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Parsedown;
use Erusev\Parsedown\Parsing\Context;
use Erusev\Parsedown\State;
final class Paragraph implements ContinuableBlock
{
/** @var string */
private $text;
/**
* @param string $text
*/
private function __construct($text)
{
$this->text = $text;
}
/**
* @param Context $Context
* @param State $State
* @param Block|null $Block
* @return static
*/
public static function build(
Context $Context,
State $State,
Block $Block = null
) {
return new self($Context->line()->text());
}
/**
* @param Context $Context
* @param State $State
* @return self|null
*/
public function advance(Context $Context, State $State)
{
if ($Context->precedingEmptyLines() > 0) {
return null;
}
return new self($this->text . "\n" . $Context->line()->text());
}
/** @return string */
public function text()
{
return $this->text;
}
/**
* @return Handler<Element>
*/
public function stateRenderable()
{
return new Handler(
/** @return Element */
function (State $State) {
return new Element(
'p',
[],
$State->applyTo(Parsedown::line(\trim($this->text()), $State))
);
}
);
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
use Erusev\Parsedown\Components\Block;
use Erusev\Parsedown\Components\StateUpdatingBlock;
use Erusev\Parsedown\Configurables\DefinitionBook;
use Erusev\Parsedown\Html\Renderables\Invisible;
use Erusev\Parsedown\Parsing\Context;
use Erusev\Parsedown\State;
final class Reference implements StateUpdatingBlock
{
/** @var State */
private $State;
private function __construct(State $State)
{
$this->State = $State;
}
/**
* @param Context $Context
* @param State $State
* @param Block|null $Block
* @return static|null
*/
public static function build(
Context $Context,
State $State,
Block $Block = null
) {
if (\preg_match(
'/^\[(.+?)\]:[ ]*+<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/',
$Context->line()->text(),
$matches
)) {
$id = \strtolower($matches[1]);
$Data = [
'url' => $matches[2],
'title' => isset($matches[3]) ? $matches[3] : null,
];
$State->get(DefinitionBook::class)->mutatingSet($id, $Data);
return new self($State);
}
return null;
}
/** @return State */
public function latestState()
{
return $this->State;
}
/**
* @return Invisible
*/
public function stateRenderable()
{
return new Invisible;
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
use Erusev\Parsedown\Components\Block;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Parsing\Context;
use Erusev\Parsedown\State;
final class Rule implements Block
{
private function __construct()
{
}
/**
* @param Context $Context
* @param State $State
* @param Block|null $Block
* @return static|null
*/
public static function build(
Context $Context,
State $State,
Block $Block = null
) {
if ($Context->line()->indent() > 3) {
return null;
}
$marker = \substr($Context->line()->text(), 0, 1);
if ($marker !== '*' && $marker !== '-' && $marker !== '_') {
return null;
}
if (
\substr_count($Context->line()->text(), $marker) >= 3
&& \chop($Context->line()->text(), " \t$marker") === ''
) {
return new self;
}
return null;
}
/**
* @return Element
*/
public function stateRenderable()
{
return Element::selfClosing('hr', []);
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
use Erusev\Parsedown\AST\Handler;
use Erusev\Parsedown\Components\AcquisitioningBlock;
use Erusev\Parsedown\Components\Block;
use Erusev\Parsedown\Configurables\HeaderSlug;
use Erusev\Parsedown\Configurables\SlugRegister;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Parsedown;
use Erusev\Parsedown\Parsing\Context;
use Erusev\Parsedown\State;
final class SetextHeader implements AcquisitioningBlock
{
/** @var string */
private $text;
/** @var 1|2 */
private $level;
/**
* @param string $text
* @param 1|2 $level
*/
private function __construct($text, $level)
{
$this->text = $text;
$this->level = $level;
}
/**
* @param Context $Context
* @param State $State
* @param Block|null $Block
* @return static|null
*/
public static function build(
Context $Context,
State $State,
Block $Block = null
) {
if (! isset($Block) || ! $Block instanceof Paragraph || $Context->precedingEmptyLines() > 0) {
return null;
}
$marker = \substr($Context->line()->text(), 0, 1);
if ($marker !== '=' && $marker !== '-') {
return null;
}
if (
$Context->line()->indent() < 4
&& \chop(\chop($Context->line()->text(), " \t"), $marker) === ''
) {
$level = ($marker === '=' ? 1 : 2);
return new self(\trim($Block->text()), $level);
}
return null;
}
/** @return bool */
public function acquiredPrevious()
{
return true;
}
/** @return string */
public function text()
{
return $this->text;
}
/** @return 1|2 */
public function level()
{
return $this->level;
}
/**
* @return Handler<Element>
*/
public function stateRenderable()
{
return new Handler(
/** @return Element */
function (State $State) {
$HeaderSlug = $State->get(HeaderSlug::class);
$Register = $State->get(SlugRegister::class);
$attributes = (
$HeaderSlug->isEnabled()
? ['id' => $HeaderSlug->transform($Register, $this->text())]
: []
);
return new Element(
'h' . \strval($this->level()),
$attributes,
$State->applyTo(Parsedown::line($this->text(), $State))
);
}
);
}
}

View File

@ -0,0 +1,346 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
use Erusev\Parsedown\AST\Handler;
use Erusev\Parsedown\Components\Block;
use Erusev\Parsedown\Components\ContinuableBlock;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Parsedown;
use Erusev\Parsedown\Parsing\Context;
use Erusev\Parsedown\Parsing\Line;
use Erusev\Parsedown\Parsing\Lines;
use Erusev\Parsedown\State;
final class TList implements ContinuableBlock
{
/** @var Lines[] */
private $Lis;
/** @var int|null */
private $listStart;
/** @var bool */
private $isLoose;
/** @var int */
private $indent;
/** @var 'ul'|'ol' */
private $type;
/** @var string */
private $marker;
/** @var int */
private $afterMarkerSpaces;
/** @var string */
private $markerType;
/** @var string */
private $markerTypeRegex;
/**
* @param Lines[] $Lis
* @param int|null $listStart
* @param bool $isLoose
* @param int $indent
* @param 'ul'|'ol' $type
* @param string $marker
* @param int $afterMarkerSpaces
* @param string $markerType
* @param string $markerTypeRegex
*/
private function __construct(
$Lis,
$listStart,
$isLoose,
$indent,
$type,
$marker,
$afterMarkerSpaces,
$markerType,
$markerTypeRegex
) {
$this->Lis = $Lis;
$this->listStart = $listStart;
$this->isLoose = $isLoose;
$this->indent = $indent;
$this->type = $type;
$this->marker = $marker;
$this->afterMarkerSpaces = $afterMarkerSpaces;
$this->markerType = $markerType;
$this->markerTypeRegex = $markerTypeRegex;
}
/**
* @param Context $Context
* @param State $State
* @param Block|null $Block
* @return static|null
*/
public static function build(
Context $Context,
State $State,
Block $Block = null
) {
list($type, $pattern) = (
\substr($Context->line()->text(), 0, 1) <= '-'
? ['ul', '[*+-]']
: ['ol', '[0-9]{1,9}+[.\)]']
);
if (\preg_match(
'/^('.$pattern.')([\t ]++.*|$)/',
$Context->line()->text(),
$matches
)) {
$marker = $matches[1];
$preAfterMarkerSpacesIndentOffset = $Context->line()->indentOffset() + $Context->line()->indent() + \strlen($marker);
$LineWithMarkerIndent = new Line(isset($matches[2]) ? $matches[2] : '', $preAfterMarkerSpacesIndentOffset);
$indentAfterMarker = $LineWithMarkerIndent->indent();
if ($indentAfterMarker > 4) {
$perceivedIndent = $indentAfterMarker -1;
$afterMarkerSpaces = 1;
} else {
$perceivedIndent = 0;
$afterMarkerSpaces = $indentAfterMarker;
}
$indentOffset = $preAfterMarkerSpacesIndentOffset + $afterMarkerSpaces;
$text = \str_repeat(' ', $perceivedIndent) . $LineWithMarkerIndent->text();
$markerType = (
$type === 'ul'
? $marker
: \substr($marker, -1)
);
$markerTypeRegex = \preg_quote($markerType, '/');
/** @var int|null */
$listStart = null;
if ($type === 'ol') {
/** @psalm-suppress PossiblyFalseArgument */
$listStart = \intval(\strstr($matches[1], $markerType, true) ?: '0');
if (
$listStart !== 1
&& isset($Block)
&& $Block instanceof Paragraph
&& ! ($Context->precedingEmptyLines() > 0)
) {
return null;
}
}
return new self(
[!empty($text) ? Lines::fromTextLines($text, $indentOffset) : Lines::none()],
$listStart,
false,
$Context->line()->indent(),
$type,
$marker,
$afterMarkerSpaces,
$markerType,
$markerTypeRegex
);
}
}
/**
* @param Context $Context
* @param State $State
* @return self|null
*/
public function advance(Context $Context, State $State)
{
if ($Context->precedingEmptyLines() > 0 && \end($this->Lis)->isEmpty()) {
return null;
}
$newlines = \str_repeat("\n", $Context->precedingEmptyLines());
$requiredIndent = $this->indent + \strlen($this->marker) + $this->afterMarkerSpaces;
$isLoose = $this->isLoose;
$indent = $Context->line()->indent();
$Lis = $this->Lis;
if ($this->type === 'ol') {
$regex = '/^([0-9]++'.$this->markerTypeRegex.')([\t ]++.*|$)/';
} else {
$regex = '/^('.$this->markerTypeRegex.')([\t ]++.*|$)/';
}
if ($Context->line()->indent() < $requiredIndent
&& \preg_match($regex, $Context->line()->text(), $matches)
) {
if ($Context->precedingEmptyLines() > 0) {
$Lis[\count($Lis) -1] = $Lis[\count($Lis) -1]->appendingBlankLines(1);
$isLoose = true;
}
$marker = $matches[1];
$preAfterMarkerSpacesIndentOffset = $Context->line()->indentOffset() + $Context->line()->indent() + \strlen($marker);
$LineWithMarkerIndent = new Line(isset($matches[2]) ? $matches[2] : '', $preAfterMarkerSpacesIndentOffset);
$indentAfterMarker = $LineWithMarkerIndent->indent();
if ($indentAfterMarker > 4) {
$perceivedIndent = $indentAfterMarker -1;
$afterMarkerSpaces = 1;
} else {
$perceivedIndent = 0;
$afterMarkerSpaces = $indentAfterMarker;
}
$indentOffset = $preAfterMarkerSpacesIndentOffset + $afterMarkerSpaces;
$text = \str_repeat(' ', $perceivedIndent) . $LineWithMarkerIndent->text();
$Lis[] = Lines::fromTextLines($newlines . $text, $indentOffset);
return new self(
$Lis,
$this->listStart,
$isLoose,
$indent,
$this->type,
$marker,
$afterMarkerSpaces,
$this->markerType,
$this->markerTypeRegex
);
} elseif ($Context->line()->indent() < $requiredIndent && self::build($Context, $State) !== null) {
return null;
}
if ($Context->line()->indent() >= $requiredIndent) {
if ($Context->precedingEmptyLines() > 0) {
$Lis[\count($Lis) -1] = $Lis[\count($Lis) -1]->appendingBlankLines($Context->precedingEmptyLines());
$isLoose = true;
}
$text = $Context->line()->ltrimBodyUpto($requiredIndent);
$indentOffset = $Context->line()->indentOffset() + \min($requiredIndent, $Context->line()->indent());
$Lis[\count($Lis) -1] = $Lis[\count($Lis) -1]->appendingTextLines($text, $indentOffset);
return new self(
$Lis,
$this->listStart,
$isLoose,
$this->indent,
$this->type,
$this->marker,
$this->afterMarkerSpaces,
$this->markerType,
$this->markerTypeRegex
);
}
if (! ($Context->precedingEmptyLines() > 0)) {
$text = $Context->line()->ltrimBodyUpto($requiredIndent);
$Lis[\count($Lis) -1] = $Lis[\count($Lis) -1]->appendingTextLines(
$newlines . \str_repeat(' ', $Context->line()->indent()) . $text,
$Context->line()->indentOffset() + \min($requiredIndent, $Context->line()->indent())
);
return new self(
$Lis,
$this->listStart,
$isLoose,
$this->indent,
$this->type,
$this->marker,
$this->afterMarkerSpaces,
$this->markerType,
$this->markerTypeRegex
);
}
return null;
}
/**
* @return array{Block[], State}[]
*/
public function items(State $State)
{
return \array_map(
/** @return array{Block[], State} */
function (Lines $Lines) use ($State) {
return Parsedown::blocks($Lines, $State);
},
$this->Lis
);
}
/** @return 'ol'|'ul' */
public function type()
{
return $this->type;
}
/** @return int|null */
public function listStart()
{
return $this->listStart;
}
/**
* @return Handler<Element>
*/
public function stateRenderable()
{
return new Handler(
/** @return Element */
function (State $State) {
$listStart = $this->listStart();
return new Element(
$this->type(),
(
isset($listStart) && $listStart !== 1
? ['start' => \strval($listStart)]
: []
),
\array_map(
/**
* @param array{Block[], State} $Item
* @return Element
* */
function ($Item) {
list($Blocks, $State) = $Item;
$StateRenderables = Parsedown::stateRenderablesFrom($Blocks);
$Renderables = $State->applyTo($StateRenderables);
if (! $this->isLoose
&& isset($Renderables[0])
&& $Renderables[0] instanceof Element
&& $Renderables[0]->name() === 'p'
) {
$Contents = $Renderables[0]->contents();
unset($Renderables[0]);
$Renderables = \array_merge($Contents ?: [], $Renderables);
}
return new Element('li', [], $Renderables);
},
$this->items($State)
)
);
}
);
}
}

View File

@ -0,0 +1,273 @@
<?php
namespace Erusev\Parsedown\Components\Blocks;
use Erusev\Parsedown\AST\Handler;
use Erusev\Parsedown\Components\AcquisitioningBlock;
use Erusev\Parsedown\Components\Block;
use Erusev\Parsedown\Components\ContinuableBlock;
use Erusev\Parsedown\Components\Inline;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Parsedown;
use Erusev\Parsedown\Parsing\Context;
use Erusev\Parsedown\State;
/**
* @psalm-type _Alignment='left'|'center'|'right'
*/
final class Table implements AcquisitioningBlock, ContinuableBlock
{
/** @var list<_Alignment|null> */
private $alignments;
/** @var list<string> */
private $headerCells;
/** @var list<list<string>> */
private $rows;
/**
* @param list<_Alignment|null> $alignments
* @param list<string> $headerCells
* @param list<list<string>> $rows
*/
private function __construct($alignments, $headerCells, $rows)
{
$this->alignments = $alignments;
$this->headerCells = $headerCells;
$this->rows = $rows;
}
/**
* @param Context $Context
* @param State $State
* @param Block|null $Block
* @return static|null
*/
public static function build(
Context $Context,
State $State,
Block $Block = null
) {
if (! isset($Block) || ! $Block instanceof Paragraph) {
return null;
}
if (
\strpos($Block->text(), '|') === false
&& \strpos($Context->line()->text(), '|') === false
&& \strpos($Context->line()->text(), ':') === false
|| \strpos($Block->text(), "\n") !== false
) {
return null;
}
if (\chop($Context->line()->text(), ' -:|') !== '') {
return null;
}
$alignments = self::parseAlignments($Context->line()->text());
if (! isset($alignments)) {
return null;
}
# ~
$headerRow = \trim(\trim($Block->text()), '|');
$headerCells = \array_map('trim', \explode('|', $headerRow));
if (\count($headerCells) !== \count($alignments)) {
return null;
}
# ~
return new self($alignments, $headerCells, []);
}
/**
* @param Context $Context
* @param State $State
* @return self|null
*/
public function advance(Context $Context, State $State)
{
if ($Context->precedingEmptyLines() > 0) {
return null;
}
if (
\count($this->alignments) !== 1
&& \strpos($Context->line()->text(), '|') === false
) {
return null;
}
$row = \trim(\trim($Context->line()->text()), '|');
if (
! \preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches)
|| ! isset($matches[0])
) {
return null;
}
$cells = \array_map(
'trim',
\array_slice($matches[0], 0, \count($this->alignments))
);
return new self(
$this->alignments,
$this->headerCells,
\array_merge($this->rows, [$cells])
);
}
/**
* @param string $dividerRow
* @return list<_Alignment|null>|null
*/
private static function parseAlignments($dividerRow)
{
$dividerRow = \trim($dividerRow);
$dividerRow = \trim($dividerRow, '|');
$dividerCells = \explode('|', $dividerRow);
/** @var list<_Alignment|null> */
$alignments = [];
foreach ($dividerCells as $dividerCell) {
$dividerCell = \trim($dividerCell);
if ($dividerCell === '') {
return null;
}
/** @var _Alignment|null */
$alignment = null;
if (\substr($dividerCell, 0, 1) === ':') {
$alignment = 'left';
}
if (\substr($dividerCell, - 1) === ':') {
$alignment = $alignment === 'left' ? 'center' : 'right';
}
$alignments []= $alignment;
}
return $alignments;
}
/** @return bool */
public function acquiredPrevious()
{
return true;
}
/** @return list<Inline[]> */
public function headerRow(State $State)
{
return \array_map(
/**
* @param string $cell
* @return Inline[]
*/
function ($cell) use ($State) {
return Parsedown::inlines($cell, $State);
},
$this->headerCells
);
}
/** @return list<Inline[]>[] */
public function rows(State $State)
{
return \array_map(
/**
* @param list<string> $cells
* @return list<Inline[]>
*/
function ($cells) use ($State) {
return \array_map(
/**
* @param string $cell
* @return Inline[]
*/
function ($cell) use ($State) {
return Parsedown::inlines($cell, $State);
},
$cells
);
},
$this->rows
);
}
/** @return list<_Alignment|null> */
public function alignments()
{
return $this->alignments;
}
/**
* @return Handler<Element>
*/
public function stateRenderable()
{
return new Handler(
/** @return Element */
function (State $State) {
return new Element('table', [], [
new Element('thead', [], [new Element('tr', [], \array_map(
/**
* @param Inline[] $Cell
* @param _Alignment|null $alignment
* @return Element
*/
function ($Cell, $alignment) use ($State) {
return new Element(
'th',
isset($alignment) ? ['style' => "text-align: $alignment;"] : [],
$State->applyTo(Parsedown::stateRenderablesFrom($Cell))
);
},
$this->headerRow($State),
$this->alignments()
))]),
new Element('tbody', [], \array_map(
/**
* @param Inline[][] $Cells
* @return Element
*/
function ($Cells) use ($State) {
return new Element('tr', [], \array_map(
/**
* @param Inline[] $Cell
* @param _Alignment|null $alignment
* @return Element
*/
function ($Cell, $alignment) use ($State) {
return new Element(
'td',
isset($alignment) ? ['style' => "text-align: $alignment;"] : [],
$State->applyTo(Parsedown::stateRenderablesFrom($Cell))
);
},
$Cells,
\array_slice($this->alignments(), 0, \count($Cells))
));
},
$this->rows($State)
))
]);
}
);
}
}

View File

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

View File

@ -0,0 +1,82 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\Components\Inline;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Html\Renderables\Text;
use Erusev\Parsedown\Parsing\Excerpt;
use Erusev\Parsedown\State;
final class Code implements Inline
{
use WidthTrait;
/** @var string */
private $text;
/**
* @param string $text
* @param int $width
*/
private function __construct($text, $width)
{
$this->text = $text;
$this->width = $width;
}
/**
* @param Excerpt $Excerpt
* @param State $State
* @return static|null
*/
public static function build(Excerpt $Excerpt, State $State)
{
$marker = \substr($Excerpt->text(), 0, 1);
if ($marker !== '`') {
return null;
}
if (\preg_match(
'/^(['.$marker.']++)(.*?)(?<!['.$marker.'])\1(?!'.$marker.')/s',
$Excerpt->text(),
$matches
)) {
$text = \str_replace("\n", ' ', $matches[2]);
$firstChar = \substr($text, 0, 1);
$lastChar = \substr($text, -1);
if ($firstChar === ' ' && $lastChar === ' ') {
$text = \substr(\substr($text, 1), 0, -1);
}
return new self($text, \strlen($matches[0]));
}
return null;
}
/** @return string */
public function text()
{
return $this->text;
}
/**
* @return Element
*/
public function stateRenderable()
{
return new Element('code', [], [new Text($this->text())]);
}
/**
* @return Text
*/
public function bestPlaintext()
{
return new Text($this->text());
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\Components\Inline;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Html\Renderables\Text;
use Erusev\Parsedown\Parsing\Excerpt;
use Erusev\Parsedown\State;
final class Email implements Inline
{
use WidthTrait;
/** @var string */
private $text;
/** @var string */
private $url;
/**
* @param string $text
* @param string $url
* @param int $width
*/
private function __construct($text, $url, $width)
{
$this->text = $text;
$this->url = $url;
$this->width = $width;
}
/**
* @param Excerpt $Excerpt
* @param State $State
* @return static|null
*/
public static function build(Excerpt $Excerpt, State $State)
{
$hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?';
$commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@'
. $hostnameLabel . '(?:\.' . $hostnameLabel . ')*';
if (\preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt->text(), $matches)) {
$url = $matches[1];
if (! isset($matches[2])) {
$url = "mailto:$url";
}
return new self($matches[1], $url, \strlen($matches[0]));
}
}
/** @return string */
public function text()
{
return $this->text;
}
/** @return string */
public function url()
{
return $this->url;
}
/**
* @return Element
*/
public function stateRenderable()
{
return new Element('a', ['href' => $this->url()], [new Text($this->text())]);
}
/**
* @return Text
*/
public function bestPlaintext()
{
return new Text($this->text());
}
}

View File

@ -0,0 +1,99 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\AST\Handler;
use Erusev\Parsedown\Components\Inline;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Html\Renderables\Text;
use Erusev\Parsedown\Parsedown;
use Erusev\Parsedown\Parsing\Excerpt;
use Erusev\Parsedown\State;
final class Emphasis implements Inline
{
use WidthTrait;
/** @var string */
private $text;
/** @var 'em'|'strong' */
private $type;
private const STRONG_REGEX = [
'*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s',
'_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us',
];
private const EM_REGEX = [
'*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
'_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
];
/**
* @param string $text
* @param 'em'|'strong' $type
* @param int $width
*/
private function __construct($text, $type, $width)
{
$this->text = $text;
$this->type = $type;
$this->width = $width;
}
/**
* @param Excerpt $Excerpt
* @param State $State
* @return static|null
*/
public static function build(Excerpt $Excerpt, State $State)
{
$marker = \substr($Excerpt->text(), 0, 1);
if ($marker !== '*' && $marker !== '_') {
return null;
}
if (\preg_match(self::STRONG_REGEX[$marker], $Excerpt->text(), $matches)) {
$emphasis = 'strong';
} elseif (\preg_match(self::EM_REGEX[$marker], $Excerpt->text(), $matches)) {
$emphasis = 'em';
} else {
return null;
}
return new self($matches[1], $emphasis, \strlen($matches[0]));
}
/** @return string */
public function text()
{
return $this->text;
}
/**
* @return Handler<Element>
*/
public function stateRenderable()
{
return new Handler(
/** @return Element */
function (State $State) {
return new Element(
$this->type,
[],
$State->applyTo(Parsedown::line($this->text(), $State))
);
}
);
}
/**
* @return Text
*/
public function bestPlaintext()
{
return new Text($this->text());
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\Components\Inline;
use Erusev\Parsedown\Html\Renderables\Text;
use Erusev\Parsedown\Parsing\Excerpt;
use Erusev\Parsedown\State;
final class EscapeSequence implements Inline
{
use WidthTrait;
const SPECIALS = '!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~';
/** @var string */
private $text;
/**
* @param string $text
*/
private function __construct($text)
{
$this->text = $text;
$this->width = 2;
}
/**
* @param Excerpt $Excerpt
* @param State $State
* @return static|null
*/
public static function build(Excerpt $Excerpt, State $State)
{
$char = \substr($Excerpt->text(), 1, 1);
if ($char !== '' && \strpbrk($char, self::SPECIALS) !== false) {
return new self($char);
}
return null;
}
/** @return string */
public function char()
{
return $this->text;
}
/**
* @return Text
*/
public function stateRenderable()
{
return new Text($this->char());
}
/**
* @return Text
*/
public function bestPlaintext()
{
return new Text($this->char());
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\Components\BacktrackingInline;
use Erusev\Parsedown\Components\Inline;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Html\Renderables\Text;
use Erusev\Parsedown\Parsing\Excerpt;
use Erusev\Parsedown\State;
final class HardBreak implements BacktrackingInline
{
use WidthTrait;
/** @var int */
private $position;
/**
* @param int $width
* @param int $position
*/
private function __construct($width, $position)
{
$this->width = $width;
$this->position = $position;
}
/**
* @param Excerpt $Excerpt
* @param State $State
* @return static|null
*/
public static function build(Excerpt $Excerpt, State $State)
{
$marker = \substr($Excerpt->text(), 0, 1);
if ($marker !== "\n") {
return null;
}
$context = $Excerpt->context();
$offset = $Excerpt->offset();
if (\substr($context, $offset -1, 1) === '\\') {
$contentLen = $offset -1;
return new self($offset - $contentLen, $contentLen);
}
if (\substr($context, $offset -2, 2) === ' ') {
$trimTrailingWhitespace = \rtrim(\substr($context, 0, $offset));
$contentLen = \strlen($trimTrailingWhitespace);
return new self($offset - $contentLen, $contentLen);
}
return null;
}
/**
* Return an integer to declare that the inline should be treated as if it
* started from that position in the excerpt given to static::build.
* Return null to use the excerpt offset value.
* @return int|null
* */
public function modifyStartPositionTo()
{
return $this->position;
}
/**
* @return Element
*/
public function stateRenderable()
{
return Element::selfClosing('br', []);
}
/**
* @return Text
*/
public function bestPlaintext()
{
return new Text("\n");
}
}

View File

@ -0,0 +1,120 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\AST\Handler;
use Erusev\Parsedown\Components\Inline;
use Erusev\Parsedown\Configurables\SafeMode;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Html\Renderables\Text;
use Erusev\Parsedown\Html\Sanitisation\UrlSanitiser;
use Erusev\Parsedown\Parsedown;
use Erusev\Parsedown\Parsing\Excerpt;
use Erusev\Parsedown\State;
/** @psalm-type _Metadata=array{href: string, title?: string} */
final class Image implements Inline
{
use WidthTrait;
/** @var Link */
private $Link;
/**
* @param Link $Link
*/
private function __construct(Link $Link)
{
$this->Link = $Link;
$this->width = $Link->width() + 1;
}
/**
* @param Excerpt $Excerpt
* @param State $State
* @return static|null
*/
public static function build(Excerpt $Excerpt, State $State)
{
if (\substr($Excerpt->text(), 0, 1) !== '!') {
return null;
}
$Excerpt = $Excerpt->addingToOffset(1);
$Link = Link::build($Excerpt, $State);
if (! isset($Link)) {
return null;
}
return new self($Link);
}
/** @return string */
public function label()
{
return $this->Link->label();
}
/** @return string */
public function url()
{
return $this->Link->url();
}
/** @return string|null */
public function title()
{
return $this->Link->title();
}
/**
* @return Handler<Element>
*/
public function stateRenderable()
{
return new Handler(
/** @return Element */
function (State $State) {
$attributes = [
'src' => $this->url(),
'alt' => \array_reduce(
Parsedown::inlines($this->label(), $State),
/**
* @param string $text
* @return string
*/
function ($text, Inline $Inline) {
return (
$text
. $Inline->bestPlaintext()->getStringBacking()
);
},
''
),
];
$title = $this->title();
if (isset($title)) {
$attributes['title'] = $title;
}
if ($State->get(SafeMode::class)->isEnabled()) {
$attributes['src'] = UrlSanitiser::filter($attributes['src']);
}
return Element::selfClosing('img', $attributes);
}
);
}
/**
* @return Text
*/
public function bestPlaintext()
{
return new Text($this->label());
}
}

View File

@ -0,0 +1,154 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\AST\Handler;
use Erusev\Parsedown\Components\Inline;
use Erusev\Parsedown\Configurables\DefinitionBook;
use Erusev\Parsedown\Configurables\InlineTypes;
use Erusev\Parsedown\Configurables\SafeMode;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Html\Renderables\Text;
use Erusev\Parsedown\Html\Sanitisation\UrlSanitiser;
use Erusev\Parsedown\Parsedown;
use Erusev\Parsedown\Parsing\Excerpt;
use Erusev\Parsedown\State;
/** @psalm-type _Metadata=array{href: string, title?: string} */
final class Link implements Inline
{
use WidthTrait;
/** @var string */
private $label;
/** @var string */
private $url;
/** @var string|null */
private $title;
/**
* @param string $label
* @param string $url
* @param string|null $title
* @param int $width
*/
private function __construct($label, $url, $title, $width)
{
$this->label = $label;
$this->url = $url;
$this->title = $title;
$this->width = $width;
}
/**
* @param Excerpt $Excerpt
* @param State $State
* @return static|null
*/
public static function build(Excerpt $Excerpt, State $State)
{
$remainder = $Excerpt->text();
if (! \preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) {
return null;
}
$label = $matches[1];
$width = \strlen($matches[0]);
$remainder = \substr($remainder, $width);
if (\preg_match('/^[(]\s*+(?:((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+)?[)]/', $remainder, $matches)) {
$url = isset($matches[1]) ? $matches[1] : '';
$title = isset($matches[2]) ? \substr($matches[2], 1, - 1) : null;
$width += \strlen($matches[0]);
return new self($label, $url, $title, $width);
} else {
if (\preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) {
$definition = \strlen($matches[1]) ? $matches[1] : $label;
$definition = \strtolower($definition);
$width += \strlen($matches[0]);
} else {
$definition = \strtolower($label);
}
$definition = \preg_replace('/\s++/', ' ', \trim($definition));
$data = $State->get(DefinitionBook::class)->lookup($definition);
if (! isset($data)) {
return null;
}
$url = $data['url'];
$title = isset($data['title']) ? $data['title'] : null;
return new self($label, $url, $title, $width);
}
}
/** @return string */
public function label()
{
return $this->label;
}
/** @return string */
public function url()
{
return $this->url;
}
/** @return string|null */
public function title()
{
return $this->title;
}
/**
* @return Handler<Element>
*/
public function stateRenderable()
{
return new Handler(
/** @return Element */
function (State $State) {
$attributes = ['href' => $this->url()];
$title = $this->title();
if (isset($title)) {
$attributes['title'] = $title;
}
if ($State->get(SafeMode::class)->isEnabled()) {
$attributes['href'] = UrlSanitiser::filter($attributes['href']);
}
$State = $State->setting(
$State->get(InlineTypes::class)->removing([Url::class])
);
return new Element(
'a',
$attributes,
$State->applyTo(Parsedown::line($this->label(), $State))
);
}
);
}
/**
* @return Text
*/
public function bestPlaintext()
{
return new Text($this->label());
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\AST\Handler;
use Erusev\Parsedown\Components\Inline;
use Erusev\Parsedown\Configurables\SafeMode;
use Erusev\Parsedown\Html\Renderables\RawHtml;
use Erusev\Parsedown\Html\Renderables\Text;
use Erusev\Parsedown\Parsing\Excerpt;
use Erusev\Parsedown\State;
final class Markup implements Inline
{
use WidthTrait;
const HTML_ATT_REGEX = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+';
/** @var string */
private $html;
/**
* @param string $html
*/
private function __construct($html)
{
$this->html = $html;
$this->width = \strlen($html);
}
/**
* @param Excerpt $Excerpt
* @param State $State
* @return static|null
*/
public static function build(Excerpt $Excerpt, State $State)
{
$secondChar = \substr($Excerpt->text(), 1, 1);
if ($secondChar === '/' && \preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt->text(), $matches)) {
return new self($matches[0]);
}
if ($secondChar === '!' && \preg_match('/^<!---?[^>-](?:-?+[^-])*-->/s', $Excerpt->text(), $matches)) {
return new self($matches[0]);
}
if ($secondChar !== ' ' && \preg_match('/^<\w[\w-]*+(?:[ ]*+'.self::HTML_ATT_REGEX.')*+[ ]*+\/?>/s', $Excerpt->text(), $matches)) {
return new self($matches[0]);
}
}
/** @return string */
public function html()
{
return $this->html;
}
/**
* @return Handler<Text|RawHtml>
*/
public function stateRenderable()
{
return new Handler(
/** @return Text|RawHtml */
function (State $State) {
if ($State->get(SafeMode::class)->isEnabled()) {
return new Text($this->html());
} else {
return new RawHtml($this->html());
}
}
);
}
/**
* @return Text
*/
public function bestPlaintext()
{
return new Text($this->html());
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\Components\Inline;
use Erusev\Parsedown\Html\Renderables\Text;
use Erusev\Parsedown\Parsing\Excerpt;
use Erusev\Parsedown\State;
final class PlainText implements Inline
{
use WidthTrait;
/** @var string */
private $text;
/**
* @param string $text
*/
private function __construct($text)
{
$this->text = $text;
$this->width = \strlen($text);
}
/**
* @param Excerpt $Excerpt
* @param State $State
* @return static
*/
public static function build(Excerpt $Excerpt, State $State)
{
return new self($Excerpt->text());
}
/** @return string */
public function text()
{
return $this->text;
}
/**
* @return Text
*/
public function stateRenderable()
{
return new Text($this->text());
}
/**
* @return Text
*/
public function bestPlaintext()
{
return new Text($this->text());
}
}

View File

@ -0,0 +1,97 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\AST\Handler;
use Erusev\Parsedown\Components\BacktrackingInline;
use Erusev\Parsedown\Components\Inline;
use Erusev\Parsedown\Configurables\Breaks;
use Erusev\Parsedown\Html\Renderables\Container;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Html\Renderables\Text;
use Erusev\Parsedown\Parsing\Excerpt;
use Erusev\Parsedown\State;
final class SoftBreak implements BacktrackingInline
{
use WidthTrait;
/** @var int */
private $position;
/**
* @param int $width
* @param int $position
*/
private function __construct($width, $position)
{
$this->width = $width;
$this->position = $position;
}
/**
* @param Excerpt $Excerpt
* @param State $State
* @return static|null
*/
public static function build(Excerpt $Excerpt, State $State)
{
$marker = \substr($Excerpt->text(), 0, 1);
if ($marker !== "\n") {
return null;
}
$context = $Excerpt->context();
$offset = $Excerpt->offset();
$trimTrailingWhitespaceBefore = \rtrim(\substr($context, 0, $offset), ' ');
$trimLeadingWhitespaceAfter = \ltrim(\substr($context, $offset + 1), ' ');
$contentLenBefore = \strlen($trimTrailingWhitespaceBefore);
$contentLenAfter = \strlen($trimLeadingWhitespaceAfter);
$originalLen = \strlen($context);
$afterWidth = $originalLen - $offset - $contentLenAfter;
return new self($offset + $afterWidth - $contentLenBefore, $contentLenBefore);
}
/**
* Return an integer to declare that the inline should be treated as if it
* started from that position in the excerpt given to static::build.
* Return null to use the excerpt offset value.
* @return int|null
* */
public function modifyStartPositionTo()
{
return $this->position;
}
/**
* @return Handler<Text|Container>
*/
public function stateRenderable()
{
return new Handler(
/** @return Text|Container */
function (State $State) {
if ($State->get(Breaks::class)->isEnabled()) {
return new Container([
Element::selfClosing('br', []),
new Text("\n")
]);
}
return new Text("\n");
}
);
}
/**
* @return Text
*/
public function bestPlaintext()
{
return new Text("\n");
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\Components\Inline;
use Erusev\Parsedown\Html\Renderables\RawHtml;
use Erusev\Parsedown\Html\Renderables\Text;
use Erusev\Parsedown\Parsing\Excerpt;
use Erusev\Parsedown\State;
final class SpecialCharacter implements Inline
{
use WidthTrait;
/** @var string */
private $charCodeHtml;
/**
* @param string $charCodeHtml
*/
private function __construct($charCodeHtml)
{
$this->charCodeHtml = $charCodeHtml;
$this->width = \strlen($charCodeHtml) + 2;
}
/**
* @param Excerpt $Excerpt
* @param State $State
* @return static|null
*/
public static function build(Excerpt $Excerpt, State $State)
{
if (\preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt->text(), $matches)) {
return new self($matches[1]);
}
return null;
}
/** @return string */
public function charCode()
{
return $this->charCodeHtml;
}
/**
* @return RawHtml
*/
public function stateRenderable()
{
return new RawHtml(
'&' . (new Text($this->charCode()))->getHtml() . ';'
);
}
/**
* @return Text
*/
public function bestPlaintext()
{
return new Text('&'.$this->charCode().';');
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\AST\Handler;
use Erusev\Parsedown\Components\Inline;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Html\Renderables\Text;
use Erusev\Parsedown\Parsedown;
use Erusev\Parsedown\Parsing\Excerpt;
use Erusev\Parsedown\State;
final class Strikethrough implements Inline
{
use WidthTrait;
/** @var string */
private $text;
/**
* @param string $text
* @param int $width
*/
private function __construct($text, $width)
{
$this->text = $text;
$this->width = $width;
}
/**
* @param Excerpt $Excerpt
* @param State $State
* @return static|null
*/
public static function build(Excerpt $Excerpt, State $State)
{
$text = $Excerpt->text();
if (\preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $text, $matches)) {
return new self($matches[1], \strlen($matches[0]));
}
return null;
}
/** @return string */
public function text()
{
return $this->text;
}
/**
* @return Handler<Element>
*/
public function stateRenderable()
{
return new Handler(
/** @return Element */
function (State $State) {
return new Element(
'del',
[],
$State->applyTo(Parsedown::line($this->text(), $State))
);
}
);
}
/**
* @return Text
*/
public function bestPlaintext()
{
return new Text($this->text());
}
}

View File

@ -0,0 +1,105 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\Components\BacktrackingInline;
use Erusev\Parsedown\Components\Inline;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Html\Renderables\Text;
use Erusev\Parsedown\Parsing\Excerpt;
use Erusev\Parsedown\State;
final class Url implements BacktrackingInline
{
use WidthTrait;
private const URI = 'https?+:[^\s[:cntrl:]<>]*';
private const NO_TRAILING_PUNCT = '(?<![?!.,:*_~])';
/** @var string */
private $url;
/** @var int */
private $position;
/**
* @param string $url
* @param int $position
*/
private function __construct($url, $position)
{
$this->url = $url;
$this->width = \strlen($url);
$this->position = $position;
}
/**
* @param Excerpt $Excerpt
* @param State $State
* @return static|null
*/
public static function build(Excerpt $Excerpt, State $State)
{
// this needs some work to follow spec
if (
\preg_match(
'/'.self::URI.self::NO_TRAILING_PUNCT.'/iu',
$Excerpt->context(),
$matches,
\PREG_OFFSET_CAPTURE
)
) {
/** @var array{0: array{string, int}} $matches */
$url = $matches[0][0];
$position = \intval($matches[0][1]);
if (\preg_match('/[)]++$/', $url, $matches)) {
$trailingParens = \strlen($matches[0]);
$openingParens = \substr_count($url, '(');
$closingParens = \substr_count($url, ')');
if ($closingParens > $openingParens) {
$url = \substr($url, 0, -\min($trailingParens, $closingParens - $openingParens));
}
}
return new self($url, $position);
}
return null;
}
/**
* Return an integer to declare that the inline should be treated as if it
* started from that position in the excerpt given to static::build.
* Return null to use the excerpt offset value.
* @return int|null
* */
public function modifyStartPositionTo()
{
return $this->position;
}
/** @return string */
public function url()
{
return $this->url;
}
/**
* @return Element
*/
public function stateRenderable()
{
return new Element('a', ['href' => $this->url()], [new Text($this->url())]);
}
/**
* @return Text
*/
public function bestPlaintext()
{
return new Text($this->url());
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace Erusev\Parsedown\Components\Inlines;
use Erusev\Parsedown\Components\Inline;
use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Html\Renderables\Text;
use Erusev\Parsedown\Parsing\Excerpt;
use Erusev\Parsedown\State;
final class UrlTag implements Inline
{
use WidthTrait;
/** @var string */
private $url;
/**
* @param string $url
* @param int $width
*/
private function __construct($url, $width)
{
$this->url = $url;
$this->width = $width;
}
/**
* @param Excerpt $Excerpt
* @param State $State
* @return static|null
*/
public static function build(Excerpt $Excerpt, State $State)
{
if (\preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt->text(), $matches)) {
return new self($matches[1], \strlen($matches[0]));
}
return null;
}
/** @return string */
public function url()
{
return $this->url;
}
/**
* @return Element
*/
public function stateRenderable()
{
return new Element('a', ['href' => $this->url()], [new Text($this->url())]);
}
/**
* @return Text
*/
public function bestPlaintext()
{
return new Text($this->url);
}
}

View File

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

View 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
View File

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

View File

@ -0,0 +1,231 @@
<?php
namespace Erusev\Parsedown\Configurables;
use Erusev\Parsedown\Components\Block;
use Erusev\Parsedown\Components\Blocks\BlockQuote;
use Erusev\Parsedown\Components\Blocks\FencedCode;
use Erusev\Parsedown\Components\Blocks\Header;
use Erusev\Parsedown\Components\Blocks\IndentedCode;
use Erusev\Parsedown\Components\Blocks\Markup as BlockMarkup;
use Erusev\Parsedown\Components\Blocks\Reference;
use Erusev\Parsedown\Components\Blocks\Rule;
use Erusev\Parsedown\Components\Blocks\SetextHeader;
use Erusev\Parsedown\Components\Blocks\Table;
use Erusev\Parsedown\Components\Blocks\TList;
use Erusev\Parsedown\Configurable;
final class BlockTypes implements Configurable
{
private const DEFAULT_BLOCK_TYPES = [
'#' => [Header::class],
'*' => [Rule::class, TList::class],
'+' => [TList::class],
'-' => [SetextHeader::class, Table::class, Rule::class, TList::class],
'0' => [TList::class],
'1' => [TList::class],
'2' => [TList::class],
'3' => [TList::class],
'4' => [TList::class],
'5' => [TList::class],
'6' => [TList::class],
'7' => [TList::class],
'8' => [TList::class],
'9' => [TList::class],
':' => [Table::class],
'<' => [BlockMarkup::class],
'=' => [SetextHeader::class],
'>' => [BlockQuote::class],
'[' => [Reference::class],
'_' => [Rule::class],
'`' => [FencedCode::class],
'|' => [Table::class],
'~' => [FencedCode::class],
];
private const DEFAULT_UNMARKED_BLOCK_TYPES = [
IndentedCode::class,
];
/** @var array<array-key, list<class-string<Block>>> */
private $blockTypes;
/** @var list<class-string<Block>> */
private $unmarkedBlockTypes;
/**
* @param array<array-key, list<class-string<Block>>> $blockTypes
* @param list<class-string<Block>> $unmarkedBlockTypes
*/
public function __construct(array $blockTypes, array $unmarkedBlockTypes)
{
$this->blockTypes = $blockTypes;
$this->unmarkedBlockTypes = $unmarkedBlockTypes;
}
/** @return self */
public static function initial()
{
return new self(
self::DEFAULT_BLOCK_TYPES,
self::DEFAULT_UNMARKED_BLOCK_TYPES
);
}
/**
* @param string $marker
* @param list<class-string<Block>> $newBlockTypes
* @return self
*/
public function settingMarked($marker, array $newBlockTypes)
{
$blockTypes = $this->blockTypes;
$blockTypes[$marker] = $newBlockTypes;
return new self($blockTypes, $this->unmarkedBlockTypes);
}
/**
* @param string $marker
* @param list<class-string<Block>> $newBlockTypes
* @return self
*/
public function addingMarkedHighPrecedence($marker, array $newBlockTypes)
{
return $this->settingMarked(
$marker,
\array_merge(
$newBlockTypes,
isset($this->blockTypes[$marker]) ? $this->blockTypes[$marker] : []
)
);
}
/**
* @param class-string<Block> $searchBlockType
* @param class-string<Block> $replacementBlockType
*/
public function replacing($searchBlockType, $replacementBlockType): self
{
$replacer = self::makeReplacer($searchBlockType, $replacementBlockType);
return new self(
\array_map($replacer, $this->blockTypes),
$replacer($this->unmarkedBlockTypes)
);
}
/**
* @param class-string<Block> $searchBlockType
* @param class-string<Block> $replacementBlockType
* @return \Closure(list<class-string<Block>>):list<class-string<Block>>
*/
private static function makeReplacer($searchBlockType, $replacementBlockType)
{
/**
* @param list<class-string<Block>> $blockTypes
* @return list<class-string<Block>>
*/
return function ($blockTypes) use ($searchBlockType, $replacementBlockType) {
return \array_map(
/**
* @param class-string<Block> $blockType
* @return class-string<Block>
*/
function ($blockType) use ($searchBlockType, $replacementBlockType) {
return $blockType === $searchBlockType ? $replacementBlockType : $blockType;
},
$blockTypes
);
};
}
/**
* @param string $marker
* @param list<class-string<Block>> $newBlockTypes
* @return self
*/
public function addingMarkedLowPrecedence($marker, array $newBlockTypes)
{
return $this->settingMarked(
$marker,
\array_merge(
isset($this->blockTypes[$marker]) ? $this->blockTypes[$marker] : [],
$newBlockTypes
)
);
}
/**
* @param list<class-string<Block>> $newUnmarkedBlockTypes
* @return self
*/
public function settingUnmarked(array $newUnmarkedBlockTypes)
{
return new self($this->blockTypes, $newUnmarkedBlockTypes);
}
/**
* @param list<class-string<Block>> $newBlockTypes
* @return self
*/
public function addingUnmarkedHighPrecedence(array $newBlockTypes)
{
return $this->settingUnmarked(
\array_merge($newBlockTypes, $this->unmarkedBlockTypes)
);
}
/**
* @param list<class-string<Block>> $newBlockTypes
* @return self
*/
public function addingUnmarkedLowPrecedence(array $newBlockTypes)
{
return $this->settingUnmarked(
\array_merge($this->unmarkedBlockTypes, $newBlockTypes)
);
}
/**
* @param list<class-string<Block>> $removeBlockTypes
* @return self
*/
public function removing(array $removeBlockTypes)
{
return new self(
\array_map(
/**
* @param list<class-string<Block>> $blockTypes
* @return list<class-string<Block>>
*/
function ($blockTypes) use ($removeBlockTypes) {
return \array_values(\array_diff($blockTypes, $removeBlockTypes));
},
$this->blockTypes
),
\array_values(\array_diff($this->unmarkedBlockTypes, $removeBlockTypes))
);
}
/**
* @param string $marker
* @return list<class-string<Block>>
*/
public function markedBy($marker)
{
if (isset($this->blockTypes[$marker])) {
return $this->blockTypes[$marker];
}
return [];
}
/**
* @return list<class-string<Block>>
*/
public function unmarked()
{
return $this->unmarkedBlockTypes;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,168 @@
<?php
namespace Erusev\Parsedown\Configurables;
use Erusev\Parsedown\Components\Inline;
use Erusev\Parsedown\Components\Inlines\Code;
use Erusev\Parsedown\Components\Inlines\Email;
use Erusev\Parsedown\Components\Inlines\Emphasis;
use Erusev\Parsedown\Components\Inlines\EscapeSequence;
use Erusev\Parsedown\Components\Inlines\HardBreak;
use Erusev\Parsedown\Components\Inlines\Image;
use Erusev\Parsedown\Components\Inlines\Link;
use Erusev\Parsedown\Components\Inlines\Markup as InlineMarkup;
use Erusev\Parsedown\Components\Inlines\SoftBreak;
use Erusev\Parsedown\Components\Inlines\SpecialCharacter;
use Erusev\Parsedown\Components\Inlines\Strikethrough;
use Erusev\Parsedown\Components\Inlines\Url;
use Erusev\Parsedown\Components\Inlines\UrlTag;
use Erusev\Parsedown\Configurable;
final class InlineTypes implements Configurable
{
private const DEFAULT_INLINE_TYPES = [
'!' => [Image::class],
'*' => [Emphasis::class],
'_' => [Emphasis::class],
'&' => [SpecialCharacter::class],
'[' => [Link::class],
':' => [Url::class],
'<' => [UrlTag::class, Email::class, InlineMarkup::class],
'`' => [Code::class],
'~' => [Strikethrough::class],
'\\' => [EscapeSequence::class],
"\n" => [HardBreak::class, SoftBreak::class],
];
/** @var array<array-key, list<class-string<Inline>>> */
private $inlineTypes;
/** @var string */
private $inlineMarkers;
/**
* @param array<array-key, list<class-string<Inline>>> $inlineTypes
*/
public function __construct(array $inlineTypes)
{
$this->inlineTypes = $inlineTypes;
$this->inlineMarkers = \implode('', \array_keys($inlineTypes));
}
/** @return self */
public static function initial()
{
return new self(self::DEFAULT_INLINE_TYPES);
}
/**
* @param string $marker
* @param list<class-string<Inline>> $newInlineTypes
* @return self
*/
public function setting($marker, array $newInlineTypes)
{
$inlineTypes = $this->inlineTypes;
$inlineTypes[$marker] = $newInlineTypes;
return new self($inlineTypes);
}
/**
* @param class-string<Inline> $searchInlineType
* @param class-string<Inline> $replacementInlineType
*/
public function replacing($searchInlineType, $replacementInlineType): self
{
return new self(
\array_map(
/**
* @param list<class-string<Inline>> $inlineTypes
* @return list<class-string<Inline>>
*/
function ($inlineTypes) use ($searchInlineType, $replacementInlineType) {
return \array_map(
/**
* @param class-string<Inline> $inlineType
* @return class-string<Inline>
*/
function ($inlineType) use ($searchInlineType, $replacementInlineType) {
return $inlineType === $searchInlineType ? $replacementInlineType : $inlineType;
},
$inlineTypes
);
},
$this->inlineTypes
)
);
}
/**
* @param string $marker
* @param list<class-string<Inline>> $newInlineTypes
* @return self
*/
public function addingHighPrecedence($marker, array $newInlineTypes)
{
return $this->setting(
$marker,
\array_merge(
$newInlineTypes,
isset($this->inlineTypes[$marker]) ? $this->inlineTypes[$marker] : []
)
);
}
/**
* @param string $marker
* @param list<class-string<Inline>> $newInlineTypes
* @return self
*/
public function addingLowPrecedence($marker, array $newInlineTypes)
{
return $this->setting(
$marker,
\array_merge(
isset($this->inlineTypes[$marker]) ? $this->inlineTypes[$marker] : [],
$newInlineTypes
)
);
}
/**
* @param list<class-string<Inline>> $removeInlineTypes
* @return self
*/
public function removing(array $removeInlineTypes)
{
return new self(\array_map(
/**
* @param list<class-string<Inline>> $inlineTypes
* @return list<class-string<Inline>>
*/
function ($inlineTypes) use ($removeInlineTypes) {
return \array_values(\array_diff($inlineTypes, $removeInlineTypes));
},
$this->inlineTypes
));
}
/**
* @param string $marker
* @return list<class-string<Inline>>
*/
public function markedBy($marker)
{
if (isset($this->inlineTypes[$marker])) {
return $this->inlineTypes[$marker];
}
return [];
}
/** @return string */
public function markers()
{
return $this->inlineMarkers;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

@ -0,0 +1,235 @@
<?php
namespace Erusev\Parsedown\Html\Renderables;
use Erusev\Parsedown\Html\Renderable;
use Erusev\Parsedown\Html\Sanitisation\CharacterFilter;
use Erusev\Parsedown\Html\Sanitisation\Escaper;
use Erusev\Parsedown\Html\TransformableRenderable;
final class Element implements TransformableRenderable
{
use CanonicalStateRenderable;
const TEXT_LEVEL_ELEMENTS = [
'a' => true,
'b' => true,
'i' => true,
'q' => true,
's' => true,
'u' => true,
'br' => true,
'em' => true,
'rp' => true,
'rt' => true,
'tt' => true,
'xm' => true,
'bdo' => true,
'big' => true,
'del' => true,
'img' => true,
'ins' => true,
'kbd' => true,
'sub' => true,
'sup' => true,
'var' => true,
'wbr' => true,
'abbr' => true,
'cite' => true,
'code' => true,
'font' => true,
'mark' => true,
'nobr' => true,
'ruby' => true,
'span' => true,
'time' => true,
'blink' => true,
'small' => true,
'nextid' => true,
'spacer' => true,
'strike' => true,
'strong' => true,
'acronym' => true,
'listing' => true,
'marquee' => true,
'basefont' => true,
];
/** @var string */
private $name;
/** @var array<string, string>*/
private $attributes;
/** @var Renderable[]|null */
private $Contents;
/**
* @param string $name
* @param array<string, string> $attributes
* @param Renderable[]|null $Contents
*/
public function __construct($name, $attributes, $Contents)
{
$this->name = $name;
$this->attributes = $attributes;
$this->Contents = $Contents;
}
/**
* @param string $name
* @param array<string, string> $attributes
* @return self
*/
public static function selfClosing($name, array $attributes)
{
return new self($name, $attributes, null);
}
/** @return string */
public function name()
{
return $this->name;
}
/**
* @return array<string, string>
*/
public function attributes()
{
return $this->attributes;
}
/**
* @return Renderable[]|null
*/
public function contents()
{
return $this->Contents;
}
/**
* @param string $name
* @return self
*/
public function settingName($name)
{
return new self($name, $this->attributes, $this->Contents);
}
/**
* @param array<string, string> $attributes
* @return self
*/
public function settingAttributes(array $attributes)
{
return new self($this->name, $attributes, $this->Contents);
}
/**
* @param Renderable[]|null $Contents
* @return self
*/
public function settingContents($Contents)
{
return new self($this->name, $this->attributes, $Contents);
}
/** @return string */
public function getHtml()
{
$elementName = CharacterFilter::htmlElementName($this->name);
$html = '<' . $elementName;
if (! empty($this->attributes)) {
foreach ($this->attributes as $name => $value) {
$html .= ' '
. CharacterFilter::htmlAttributeName($name)
. '="'
. Escaper::htmlAttributeValue($value)
. '"'
;
}
}
if ($this->Contents !== null) {
$html .= '>';
if (! empty($this->Contents)) {
foreach ($this->Contents as $C) {
if (
$C instanceof Element
&& ! \array_key_exists(\strtolower($C->name()), self::TEXT_LEVEL_ELEMENTS)
) {
$html .= "\n";
}
$html .= $C->getHtml();
}
$Last = \end($this->Contents);
if (
$Last instanceof Element
&& ! \array_key_exists(\strtolower($Last->name()), self::TEXT_LEVEL_ELEMENTS)
) {
$html .= "\n";
}
}
$html .= "</" . $elementName . ">";
} else {
$html .= ' />';
}
return $html;
}
/**
* @param \Closure(string):TransformableRenderable $Transform
* @return TransformableRenderable
*/
public function transformingContent(\Closure $Transform): TransformableRenderable
{
if (! isset($this->Contents)) {
return $this;
}
return new self($this->name, $this->attributes, \array_map(
function (Renderable $R) use ($Transform): Renderable {
if (! $R instanceof TransformableRenderable) {
return $R;
}
return $R->transformingContent($Transform);
},
$this->Contents
));
}
public function replacingAll(string $search, TransformableRenderable $Replacement): TransformableRenderable
{
if (! isset($this->Contents)) {
return $this;
}
return new self($this->name, $this->attributes, \array_map(
function (Renderable $R) use ($search, $Replacement): Renderable {
if (! $R instanceof TransformableRenderable) {
return $R;
}
return $R->replacingAll($search, $Replacement);
},
$this->Contents
));
}
}

View File

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

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

View File

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

View File

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

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

View File

@ -0,0 +1,58 @@
<?php
namespace Erusev\Parsedown\Html\Sanitisation;
final class UrlSanitiser
{
private const COMMON_SCHEMES = [
'http://',
'https://',
'ftp://',
'ftps://',
'mailto:',
'tel:',
'data:image/png;base64,',
'data:image/gif;base64,',
'data:image/jpeg;base64,',
'irc:',
'ircs:',
'git:',
'ssh:',
'news:',
'steam:',
];
/**
* Disable literal intepretation of unknown scheme in $url. Returns the
* filtered version of $url.
* @param string $url
* @param string[]|null $permittedSchemes
* @return string
*/
public static function filter($url, $permittedSchemes = null)
{
if (! isset($permittedSchemes)) {
$permittedSchemes = self::COMMON_SCHEMES;
}
foreach ($permittedSchemes as $scheme) {
if (self::striAtStart($url, $scheme)) {
return $url;
}
}
return \str_replace(':', '%3A', $url);
}
/**
* @param string $string
* @param string $needle
* @return bool
*/
private static function striAtStart($string, $needle)
{
$needleLen = \strlen($needle);
return \strtolower(\substr($string, 0, $needleLen)) === \strtolower($needle);
}
}

View File

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

View File

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

297
src/Parsedown.php Normal file
View File

@ -0,0 +1,297 @@
<?php
namespace Erusev\Parsedown;
use Erusev\Parsedown\AST\StateRenderable;
use Erusev\Parsedown\Components\AcquisitioningBlock;
use Erusev\Parsedown\Components\BacktrackingInline;
use Erusev\Parsedown\Components\Block;
use Erusev\Parsedown\Components\Blocks\Paragraph;
use Erusev\Parsedown\Components\ContinuableBlock;
use Erusev\Parsedown\Components\Inline;
use Erusev\Parsedown\Components\Inlines\PlainText;
use Erusev\Parsedown\Components\StateUpdatingBlock;
use Erusev\Parsedown\Configurables\BlockTypes;
use Erusev\Parsedown\Configurables\InlineTypes;
use Erusev\Parsedown\Configurables\RecursionLimiter;
use Erusev\Parsedown\Configurables\RenderStack;
use Erusev\Parsedown\Html\Renderable;
use Erusev\Parsedown\Parsing\Excerpt;
use Erusev\Parsedown\Parsing\Line;
use Erusev\Parsedown\Parsing\Lines;
final class Parsedown
{
const version = '2.0.0-dev';
/** @var State */
private $State;
public function __construct(StateBearer $StateBearer = null)
{
$State = ($StateBearer ?? new State)->state();
$this->State = $State->isolatedCopy();
}
/**
* @param string $markdown
* @return string
*/
public function toHtml($markdown)
{
list($StateRenderables, $State) = self::lines(
Lines::fromTextLines($markdown, 0),
$this->State->isolatedCopy()
);
$Renderables = \array_reduce(
\array_reverse($State->get(RenderStack::class)->getStack()),
/**
* @param Renderable[] $Renderables
* @param \Closure(Renderable[],State):Renderable[] $RenderMap
* @return Renderable[]
*/
function (array $Renderables, \Closure $RenderMap) use ($State): array {
return $RenderMap($Renderables, $State);
},
$State->applyTo($StateRenderables)
);
$html = self::render($Renderables);
return $html;
}
/**
* @return array{StateRenderable[], State}
*/
public static function lines(Lines $Lines, State $State)
{
list($Blocks, $State) = self::blocks($Lines, $State);
return [self::stateRenderablesFrom($Blocks), $State];
}
/**
* @param Component[] $Components
* @return StateRenderable[]
*/
public static function stateRenderablesFrom($Components)
{
return \array_map(
/**
* @param Component $Component
* @return StateRenderable
*/
function ($Component) { return $Component->stateRenderable(); },
$Components
);
}
/**
* @return array{Block[], State}
*/
public static function blocks(Lines $Lines, State $State)
{
$RecursionLimiter = $State->get(RecursionLimiter::class)->incremented();
if ($RecursionLimiter->isDepthExceeded()) {
$State = $State->setting(new BlockTypes([], []));
}
$State = $State->setting($RecursionLimiter);
/** @var Block[] */
$Blocks = [];
/** @var Block|null */
$Block = null;
/** @var Block|null */
$CurrentBlock = null;
foreach ($Lines->contexts() as $Context) {
$Line = $Context->line();
if (
isset($CurrentBlock)
&& $CurrentBlock instanceof ContinuableBlock
&& ! $CurrentBlock instanceof Paragraph
) {
$Block = $CurrentBlock->advance($Context, $State);
if ($Block instanceof StateUpdatingBlock) {
$State = $Block->latestState();
}
if (isset($Block)) {
$CurrentBlock = $Block;
continue;
}
}
$marker = \substr($Line->text(), 0, 1);
$potentialBlockTypes = \array_merge(
$State->get(BlockTypes::class)->unmarked(),
$State->get(BlockTypes::class)->markedBy($marker)
);
foreach ($potentialBlockTypes as $blockType) {
$Block = $blockType::build($Context, $State, $CurrentBlock);
if (isset($Block)) {
if ($Block instanceof StateUpdatingBlock) {
$State = $Block->latestState();
}
if (isset($CurrentBlock)
&& (
! $Block instanceof AcquisitioningBlock
|| ! $Block->acquiredPrevious()
)
) {
$Blocks[] = $CurrentBlock;
}
$CurrentBlock = $Block;
continue 2;
}
}
if (isset($CurrentBlock) && $CurrentBlock instanceof Paragraph) {
$Block = $CurrentBlock->advance($Context, $State);
}
if (isset($Block)) {
$CurrentBlock = $Block;
} else {
if (isset($CurrentBlock)) {
$Blocks[] = $CurrentBlock;
}
$CurrentBlock = Paragraph::build($Context, $State);
}
}
if (isset($CurrentBlock)) {
$Blocks[] = $CurrentBlock;
}
return [$Blocks, $State];
}
/**
* @param string $text
* @return StateRenderable[]
*/
public static function line($text, State $State)
{
return self::stateRenderablesFrom(self::inlines($text, $State));
}
/**
* @param string $text
* @return Inline[]
*/
public static function inlines($text, State $State)
{
# standardize line breaks
$text = \str_replace(["\r\n", "\r"], "\n", $text);
$RecursionLimiter = $State->get(RecursionLimiter::class)->incremented();
if ($RecursionLimiter->isDepthExceeded()) {
return [Plaintext::build(new Excerpt($text, 0), $State)];
}
$State = $State->setting($RecursionLimiter);
/** @var Inline[] */
$Inlines = [];
# $excerpt is based on the first occurrence of a marker
$InlineTypes = $State->get(InlineTypes::class);
$markerMask = $InlineTypes->markers();
for (
$Excerpt = (new Excerpt($text, 0))->pushingOffsetTo($markerMask);
$Excerpt->text() !== '';
$Excerpt = $Excerpt->pushingOffsetTo($markerMask)
) {
$marker = \substr($Excerpt->text(), 0, 1);
foreach ($InlineTypes->markedBy($marker) as $inlineType) {
$Inline = $inlineType::build($Excerpt, $State);
if (! isset($Inline)) {
continue;
}
$markerPosition = $Excerpt->offset();
/** @var int|null */
$startPosition = null;
if ($Inline instanceof BacktrackingInline) {
$startPosition = $Inline->modifyStartPositionTo();
}
if (! isset($startPosition)) {
$startPosition = $markerPosition;
}
$endPosition = $startPosition + $Inline->width();
if ($startPosition > $markerPosition
|| $endPosition < $markerPosition
|| $startPosition < 0
) {
continue;
}
$Inlines[] = Plaintext::build($Excerpt->choppingUpToOffset($startPosition), $State);
$Inlines[] = $Inline;
/** @psalm-suppress LoopInvalidation */
$Excerpt = $Excerpt->choppingFromOffset($endPosition);
continue 2;
}
/** @psalm-suppress LoopInvalidation */
$Excerpt = $Excerpt->addingToOffset(1);
}
$Inlines[] = Plaintext::build($Excerpt->choppingFromOffset(0), $State);
return $Inlines;
}
/**
* @param Renderable[] $Renderables
* @return string
*/
public static function render(array $Renderables)
{
return \trim(
\array_reduce(
$Renderables,
/**
* @param string $html
* @return string
*/
function ($html, Renderable $Renderable) {
$newHtml = $Renderable->getHtml();
return $html . ($newHtml === '' ? '' : "\n") . $newHtml;
},
''
),
"\n"
);
}
}

48
src/Parsing/Context.php Normal file
View File

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

85
src/Parsing/Excerpt.php Normal file
View 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
View 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
View 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->precedingEmptyLines() > 0) {
$containsBlankLines = true;
break;
}
}
}
$this->containsBlankLines = $containsBlankLines;
}
/** @return self */
public static function none()
{
return new self([], '');
}
/**
* @param string $text
* @param int $indentOffset
* @return self
*/
public static function fromTextLines($text, $indentOffset)
{
# standardize line breaks
$text = \str_replace(["\r\n", "\r"], "\n", $text);
$Contexts = [];
$sequentialEmptyLines = '';
foreach (\explode("\n", $text) as $line) {
if (\chop($line) === '') {
$sequentialEmptyLines .= $line . "\n";
continue;
}
$Contexts[] = new Context(
new Line($line, $indentOffset),
$sequentialEmptyLines
);
$sequentialEmptyLines = '';
}
return new self($Contexts, $sequentialEmptyLines);
}
/** @return bool */
public function isEmpty()
{
return ! $this->containsBlankLines && \count($this->Contexts) === 0;
}
/** @return Context[] */
public function contexts()
{
return $this->Contexts;
}
/** @return bool */
public function containsBlankLines()
{
return $this->containsBlankLines;
}
/** @return int */
public function trailingBlankLines()
{
return $this->trailingBlankLines;
}
/**
* @param int $count
* @return self
*/
public function appendingBlankLines($count = 1)
{
if ($count < 0) {
$count = 0;
}
$Lines = clone($this);
$Lines->trailingBlankLinesText .= \str_repeat("\n", $count);
$Lines->trailingBlankLines += $count;
$Lines->containsBlankLines = $Lines->containsBlankLines || ($count > 0);
return $Lines;
}
/**
* @param string $text
* @param int $indentOffset
* @return Lines
*/
public function appendingTextLines($text, $indentOffset)
{
$Lines = clone($this);
$NextLines = self::fromTextLines($text, $indentOffset);
if (\count($NextLines->Contexts) === 0) {
$Lines->trailingBlankLines += $NextLines->trailingBlankLines;
$Lines->trailingBlankLinesText .= $NextLines->trailingBlankLinesText;
$Lines->containsBlankLines = true;
return $Lines;
}
$NextLines->Contexts[0] = new Context(
$NextLines->Contexts[0]->line(),
$NextLines->Contexts[0]->precedingEmptyLinesText() . $Lines->trailingBlankLinesText
);
$Lines->Contexts = \array_merge($Lines->Contexts, $NextLines->Contexts);
$Lines->trailingBlankLines = $NextLines->trailingBlankLines;
$Lines->trailingBlankLinesText = $NextLines->trailingBlankLinesText;
$Lines->containsBlankLines = $Lines->containsBlankLines
|| $NextLines->containsBlankLines
;
return $Lines;
}
/** @return Lines */
public function appendingContext(Context $Context)
{
$Lines = clone($this);
$Context = new Context(
$Context->line(),
$Context->precedingEmptyLinesText() . $Lines->trailingBlankLinesText
);
if ($Context->precedingEmptyLines() > 0) {
$Lines->containsBlankLines = true;
}
$Lines->trailingBlankLines = 0;
$Lines->trailingBlankLinesText = '';
$Lines->Contexts[] = $Context;
return $Lines;
}
}

125
src/State.php Normal file
View File

@ -0,0 +1,125 @@
<?php
namespace Erusev\Parsedown;
use Erusev\Parsedown\AST\StateRenderable;
use Erusev\Parsedown\Html\Renderable;
final class State implements StateBearer
{
/**
* @var array<class-string<Configurable>, Configurable>
*/
private $state;
/**
* @var array<class-string<Configurable>, Configurable>
*/
private static $initialCache = [];
/**
* @param Configurable[] $Configurables
*/
public function __construct(array $Configurables = [])
{
$this->state = \array_combine(
\array_map(
/** @return class-string<Configurable> */
function (Configurable $C) { return \get_class($C); },
$Configurables
),
$Configurables
);
}
/**
* @return self
*/
public function setting(Configurable $C)
{
return new self([\get_class($C) => $C] + $this->state);
}
/**
* @return self
*/
public function mergingWith(State $State)
{
return new self($State->state + $this->state);
}
/**
* @template T as Configurable
* @template-typeof T $className
* @param class-string<T> $className
* @return T
*/
public function get($className)
{
if (
! isset($this->state[$className])
&& \is_subclass_of($className, MutableConfigurable::class, true)
) {
if (! isset(self::$initialCache[$className])) {
/** @var T */
self::$initialCache[$className] = $className::initial();
}
/**
* @var T
* @psalm-suppress PossiblyUndefinedMethod
*/
$this->state[$className] = self::$initialCache[$className]->isolatedCopy();
}
/** @var T */
return (
$this->state[$className]
?? self::$initialCache[$className]
?? self::$initialCache[$className] = $className::initial()
);
}
public function __clone()
{
$this->state = \array_map(
/** @return Configurable */
function (Configurable $C) { return clone($C); },
$this->state
);
}
/**
* @param StateRenderable[] $StateRenderables
* @return Renderable[]
*/
public function applyTo(array $StateRenderables)
{
return \array_map(
/** @return Renderable */
function (StateRenderable $SR) { return $SR->renderable($this); },
$StateRenderables
);
}
public function state(): State
{
return $this;
}
/** @return self */
public static function from(StateBearer $StateBearer)
{
return $StateBearer->state();
}
public function isolatedCopy(): self
{
return new self(\array_map(
function ($C) {
return $C instanceof MutableConfigurable ? $C->isolatedCopy() : $C;
},
$this->state
));
}
}

10
src/StateBearer.php Normal file
View File

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

View File

@ -1,71 +0,0 @@
<?php
/**
* Test Parsedown against the CommonMark spec
*
* @link http://commonmark.org/ CommonMark
*/
class CommonMarkTestStrict extends PHPUnit_Framework_TestCase
{
const SPEC_URL = 'https://raw.githubusercontent.com/jgm/CommonMark/master/spec.txt';
protected $parsedown;
protected function setUp()
{
$this->parsedown = new TestParsedown();
$this->parsedown->setUrlsLinked(false);
}
/**
* @dataProvider data
* @param $id
* @param $section
* @param $markdown
* @param $expectedHtml
*/
public function testExample($id, $section, $markdown, $expectedHtml)
{
$actualHtml = $this->parsedown->text($markdown);
$this->assertEquals($expectedHtml, $actualHtml);
}
/**
* @return array
*/
public function data()
{
$spec = file_get_contents(self::SPEC_URL);
if ($spec === false) {
$this->fail('Unable to load CommonMark spec from ' . self::SPEC_URL);
}
$spec = str_replace("\r\n", "\n", $spec);
$spec = strstr($spec, '<!-- END TESTS -->', true);
$matches = array();
preg_match_all('/^`{32} example\n((?s).*?)\n\.\n(?:|((?s).*?)\n)`{32}$|^#{1,6} *(.*?)$/m', $spec, $matches, PREG_SET_ORDER);
$data = array();
$currentId = 0;
$currentSection = '';
foreach ($matches as $match) {
if (isset($match[3])) {
$currentSection = $match[3];
} else {
$currentId++;
$markdown = str_replace('→', "\t", $match[1]);
$expectedHtml = isset($match[2]) ? str_replace('→', "\t", $match[2]) : '';
$data[$currentId] = array(
'id' => $currentId,
'section' => $currentSection,
'markdown' => $markdown,
'expectedHtml' => $expectedHtml
);
}
}
return $data;
}
}

View File

@ -1,162 +0,0 @@
<?php
use PHPUnit\Framework\TestCase;
class ParsedownTest extends TestCase
{
final function __construct($name = null, array $data = array(), $dataName = '')
{
$this->dirs = $this->initDirs();
$this->Parsedown = $this->initParsedown();
parent::__construct($name, $data, $dataName);
}
private $dirs, $Parsedown;
/**
* @return array
*/
protected function initDirs()
{
$dirs []= dirname(__FILE__).'/data/';
return $dirs;
}
/**
* @return Parsedown
*/
protected function initParsedown()
{
$Parsedown = new TestParsedown();
return $Parsedown;
}
/**
* @dataProvider data
* @param $test
* @param $dir
*/
function test_($test, $dir)
{
$markdown = file_get_contents($dir . $test . '.md');
$expectedMarkup = file_get_contents($dir . $test . '.html');
$expectedMarkup = str_replace("\r\n", "\n", $expectedMarkup);
$expectedMarkup = str_replace("\r", "\n", $expectedMarkup);
$this->Parsedown->setSafeMode(substr($test, 0, 3) === 'xss');
$actualMarkup = $this->Parsedown->text($markdown);
$this->assertEquals($expectedMarkup, $actualMarkup);
}
function data()
{
$data = array();
foreach ($this->dirs as $dir)
{
$Folder = new DirectoryIterator($dir);
foreach ($Folder as $File)
{
/** @var $File DirectoryIterator */
if ( ! $File->isFile())
{
continue;
}
$filename = $File->getFilename();
$extension = pathinfo($filename, PATHINFO_EXTENSION);
if ($extension !== 'md')
{
continue;
}
$basename = $File->getBasename('.md');
if (file_exists($dir . $basename . '.html'))
{
$data []= array($basename, $dir);
}
}
}
return $data;
}
public function test_no_markup()
{
$markdownWithHtml = <<<MARKDOWN_WITH_MARKUP
<div>_content_</div>
sparse:
<div>
<div class="inner">
_content_
</div>
</div>
paragraph
<style type="text/css">
p {
color: red;
}
</style>
comment
<!-- html comment -->
MARKDOWN_WITH_MARKUP;
$expectedHtml = <<<EXPECTED_HTML
<p>&lt;div&gt;<em>content</em>&lt;/div&gt;</p>
<p>sparse:</p>
<p>&lt;div&gt;
&lt;div class=&quot;inner&quot;&gt;
<em>content</em>
&lt;/div&gt;
&lt;/div&gt;</p>
<p>paragraph</p>
<p>&lt;style type=&quot;text/css&quot;&gt;
p {
color: red;
}
&lt;/style&gt;</p>
<p>comment</p>
<p>&lt;!-- html comment --&gt;</p>
EXPECTED_HTML;
$parsedownWithNoMarkup = new TestParsedown();
$parsedownWithNoMarkup->setMarkupEscaped(true);
$this->assertEquals($expectedHtml, $parsedownWithNoMarkup->text($markdownWithHtml));
}
public function testLateStaticBinding()
{
$parsedown = Parsedown::instance();
$this->assertInstanceOf('Parsedown', $parsedown);
// After instance is already called on Parsedown
// subsequent calls with the same arguments return the same instance
$sameParsedown = TestParsedown::instance();
$this->assertInstanceOf('Parsedown', $sameParsedown);
$this->assertSame($parsedown, $sameParsedown);
$testParsedown = TestParsedown::instance('test late static binding');
$this->assertInstanceOf('TestParsedown', $testParsedown);
$sameInstanceAgain = TestParsedown::instance('test late static binding');
$this->assertSame($testParsedown, $sameInstanceAgain);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +0,0 @@
- li
- li
- li
- li
- li
- li

View File

@ -1 +0,0 @@
<p>my email is <a href="mailto:me@example.com">me@example.com</a></p>

View File

@ -1 +0,0 @@
my email is <me@example.com>

View File

@ -1,11 +0,0 @@
<pre><code>&lt;?php
$message = 'fenced code block';
echo $message;</code></pre>
<pre><code>tilde</code></pre>
<pre><code class="language-php">echo 'language identifier';</code></pre>
<pre><code class="language-c#">echo 'language identifier with non words';</code></pre>
<pre><code class="language-html+php">&lt;?php
echo "Hello World";
?&gt;
&lt;a href="http://auraphp.com" &gt;Aura Project&lt;/a&gt;</code></pre>

View File

@ -1,5 +0,0 @@
<!-- single line -->
<p>paragraph</p>
<!--
multiline -->
<p>paragraph</p>

View File

@ -1,2 +0,0 @@
<p>line<br />
line</p>

View File

@ -1,2 +0,0 @@
line
line

View File

@ -1,11 +0,0 @@
1. one
2. two
repeating numbers:
1. one
1. two
large numbers:
123. one

View File

@ -1,11 +0,0 @@
<blockquote>
<p>quote</p>
</blockquote>
<p>indented:</p>
<blockquote>
<p>quote</p>
</blockquote>
<p>no space after <code>&gt;</code>:</p>
<blockquote>
<p>quote</p>
</blockquote>

View File

@ -1,7 +0,0 @@
> quote
indented:
> quote
no space after `>`:
>quote

View File

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

View File

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

View File

@ -1,8 +0,0 @@
<div>
line 1
line 2
line 3
line 4
</div>

View File

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

View File

@ -1,8 +0,0 @@
- li
- li
mixed markers:
* li
+ li
- li

View File

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