├── .github
└── workflows
│ ├── php.yml
│ └── validator1.yml
├── .gitignore
├── README.md
├── composer.json
├── src
├── Libs
│ └── Markdown
│ │ └── ParseMarkdown.php
└── Markdown.php
└── tests
├── CompilationTest.php
├── MarkupTest.php
├── files
├── footer.md
├── heading.md
├── hello-2.md
└── hello.md
└── pages
├── backup
└── homepage.html
├── compiledmarkdown.html
├── hello-2.html
├── hello-3.html
└── homepage.html
/.github/workflows/php.yml:
--------------------------------------------------------------------------------
1 | name: PHP Composer
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 |
20 | - name: Update Composer
21 | run: composer update
22 |
23 | - name: Validate composer.json and composer.lock
24 | run: composer validate --strict
25 |
26 | - name: Cache Composer packages
27 | id: composer-cache
28 | uses: actions/cache@v3
29 | with:
30 | path: vendor
31 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
32 | restore-keys: |
33 | ${{ runner.os }}-php-
34 |
35 | - name: Install dependencies
36 | run: composer install --prefer-dist --no-progress
37 |
38 | - name: Dump Autoload
39 | run: composer dump-autoload -o
40 |
41 | - name: Run Test Suite
42 | run: php ./vendor/bin/phpunit tests
43 |
44 |
45 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit"
46 | # Docs: https://getcomposer.org/doc/articles/scripts.md
47 |
48 | # - name: Run test suite
49 | # run: composer run-script test
50 |
--------------------------------------------------------------------------------
/.github/workflows/validator1.yml:
--------------------------------------------------------------------------------
1 | name: PHP Composer
2 |
3 | on:
4 | push:
5 | branches: [ "v0.2.4" ]
6 | pull_request:
7 | branches: [ "v0.2.4" ]
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 |
20 | - name: Validate composer.json and composer.lock
21 | run: composer validate --strict
22 |
23 | - name: Cache Composer packages
24 | id: composer-cache
25 | uses: actions/cache@v3
26 | with:
27 | path: vendor
28 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
29 | restore-keys: |
30 | ${{ runner.os }}-php-
31 |
32 | - name: Install dependencies
33 | run: composer install --prefer-dist --no-progress
34 |
35 | - name: Dump Autoload
36 | run: composer dump-autoload -o
37 |
38 | - name: Run Test Suite
39 | run: php ./vendor/bin/phpunit tests
40 |
41 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit"
42 | # Docs: https://getcomposer.org/doc/articles/scripts.md
43 |
44 | # - name: Run test suite
45 | # run: composer run-script test
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /composer.lock
2 | /vendor
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Markdown Parser for PHP
9 |
10 |
11 | A fast, simple, and straightforward Markdown to HTML converter for PHP.
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ## 🚀 Installation
32 |
33 | ```sh
34 | composer require fastvolt/markdown
35 | ```
36 |
37 |
38 |
39 | ## 📦 Basic Usage
40 |
41 | ```php
42 | use FastVolt\Helper\Markdown;
43 |
44 | $text = "## Hello, World";
45 |
46 | // initialize markdown object
47 | $markdown = new Markdown(); // or Markdown::new()
48 |
49 | // set markdown content
50 | $markdown->setContent($text);
51 |
52 | // compile as raw HTML
53 | echo $markdown->toHtml();
54 | ```
55 |
56 | #### Output:
57 |
58 | ```html
59 | Hello, World
60 | ```
61 |
62 |
63 |
64 |
65 | ## 📄 Convert Markdown File to Raw HTML
66 |
67 | > ***sample.md:***
68 |
69 | ```md
70 | #### Heading 4
71 | ### Heading 3
72 | ## Heading 2
73 | # Heading 1
74 |
75 | - List 1
76 | - List 2
77 |
78 | > THIS IS A BLOCKQUOTE
79 |
80 | [A LINK](https://github.com/fastvolt)
81 | ```
82 |
83 |
84 | > ***index.php:***
85 |
86 | ```php
87 | $markdown = Markdown::new();
88 |
89 | // set markdown file to parse
90 | $markdown->setFile('./sample.md');
91 |
92 | // compile as raw HTML
93 | echo $markdown->toHtml();
94 | ```
95 |
96 | > ***Output:***
97 |
98 | ```html
99 | Heading 4
100 | Heading 3
101 | Heading 2
102 | Heading 1
103 |
104 | - List 1
105 | - List 2
106 |
107 | THIS IS A BLOCKQUOTE
108 | A LINK
109 | ```
110 |
111 |
112 |
113 | ## 📝 Convert Markdown File to An HTML File
114 |
115 | > ***blogPost.md:***
116 |
117 | ```md
118 | Here is a Markdown File Waiting To Be Compiled To an HTML File
119 | ```
120 |
121 | > ***index.php:***
122 |
123 | ```php
124 |
125 | $markdown = Markdown::new()
126 | // set markdown file
127 | ->setFile(__DIR__ . '/blogPost.md')
128 | // set compilation directory
129 | ->setCompileDir(__DIR__ . '/pages/')
130 | // compile as an html file 'newHTMLFile.html'
131 | ->toHtmlFile(filename: 'newHTMLFile');
132 |
133 | if ($markdown) {
134 | echo "Compiled to ./pages/newHTMLFile.html";
135 | }
136 |
137 | ```
138 |
139 |
140 |
141 | ## 🔒 Sanitizing HTML Output (XSS Protection)
142 |
143 | You can sanitize input HTML and prevent cross-site scripting (XSS) attack using the `sanitize` flag:
144 |
145 | ```php
146 | $markdown = Markdown::new(
147 | sanitize: true
148 | );
149 |
150 | $markdown->setContent('Hello World
');
151 |
152 | echo $markdown->toHtml();
153 | ```
154 |
155 | > ***Output:***
156 |
157 | ```html
158 | <h1>Hello World</h1>
159 | ```
160 |
161 |
162 |
163 | ## ⚙️ Advanced Use Case
164 |
165 | ### Inline Markdown
166 | ```php
167 | $markdown = Markdown::new();
168 |
169 | $markdown->setInlineContent('_My name is **vincent**, the co-author of this blog_');
170 |
171 | echo $markdown->toHtml();
172 | ```
173 |
174 | > ***Output:***
175 |
176 | ```html
177 | My name is vincent, the co-author of this blog
178 | ```
179 |
180 | > ***NOTE:*** Some markdown symbols are not supported with this method
181 |
182 |
183 |
184 | ### Example #1
185 | Combine multiple markdown files, contents and compile them in multiple directories:
186 |
187 | > ***Header.md***
188 | ```md
189 | # Blog Title
190 | ### Here is the Blog Sub-title
191 | ```
192 |
193 | > ***Footer.md***
194 | ```md
195 | ### Thanks for Visiting My BlogPage
196 | ```
197 |
198 | > ***index.php***
199 |
200 | ```php
201 | $markdown = Markdown::new(sanitize: true)
202 | // include header file's markdown contents
203 | ->setFile('./Header.md')
204 | // body contents
205 | ->setInlineContent('_My name is **vincent**, the co-author of this blog_')
206 | ->setContent('Kindly follow me on my GitHub page via: [@vincent](https://github.com/oladoyinbov).')
207 | ->setContent('Here are the lists of my projects:')
208 | ->setContent('
209 | - Dragon CMS
210 | - Fastvolt Framework.
211 | + Fastvolt Router
212 | + Markdown Parser.
213 | ')
214 | // include footer file's markdown contents
215 | ->setFile('./Footer.md')
216 | // set compilation directory
217 | ->setCompileDir('./pages/')
218 | // set another compilation directory to backup the result
219 | ->setCompileDir('./backup/pages/')
220 | // compile and store as 'homepage.html'
221 | ->toHtmlFile(file_name: 'homepage');
222 |
223 | if ($markdown) {
224 | echo "Compile Successful";
225 | }
226 | ```
227 |
228 | > ***Output:*** `pages/homepage.html`, `backup/pages/homepage.html`
229 |
230 | ```html
231 | Blog Title
232 | Here is the Blog Sub-title
233 | My name is vincent, the co-author of this blog
234 | Kindly follow me on my github page via: @vincent.
235 | Here are the lists of my projects:
236 |
237 | - Dragon CMS
238 | - Fastvolt Framework.
239 |
240 | - Fastvolt Router
241 | - Markdown Parser.
242 |
243 |
244 |
245 | Thanks for Visiting My BlogPage
246 | ```
247 |
248 |
249 |
250 | ## Supported Formatting Symbols
251 |
252 | | Markdown Syntax | Description | Example Syntax | Rendered Output |
253 | |-----------------------------|-----------------------------|-------------------------------------------|----------------------------------------|
254 | | `#` to `######` | Headings (H1–H6) | `## Heading 2` | Heading 2
|
255 | | `**text**` or `__text__` | Bold | `**bold**` | bold |
256 | | `*text*` or `_text_` | Italic | `*italic*` | italic |
257 | | `~~text~~` | Strikethrough | `~~strike~~` | strike |
258 | | `` `code` `` | Inline code | `` `echo` `` | echo
|
259 | | ```
code block
```
| Code block | ```` ```php\n echo "Hi"; \n``` ```` | `...
` |
260 | | `-`, `+`, or `*` | Unordered list | `- Item 1`
`* Item 2` | `` |
261 | | `1.` `2.` | Ordered list | `1. Item`
`2. Item` | `- Item
` |
262 | | `[text](url)` | Hyperlink | `[GitHub](https://github.com)` | GitHub |
263 | | `> blockquote` | Blockquote | `> This is a quote` | This is a quote
|
264 | | `---`, `***`, `___` | Horizontal Rule | `---` | `
` |
265 | | `` | Image | `` | `
` |
266 | | `\` | Escape special character | `\*not italic\*` | *not italic* (as text) |
267 |
268 |
269 |
270 | ## ✅ Requirements
271 |
272 | PHP 8.1 or newer.
273 |
274 |
275 |
276 | ## ℹ️ Notes
277 |
278 | > This library is an extended and simplified version of the excellent [Parsedown](https://github.com/erusev/parsedown/) by Erusev.
279 |
280 |
281 |
282 | ## 📄 License
283 |
284 | This project is open-source and licensed under the MIT License by @fastvolt.
285 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fastvolt/markdown",
3 | "description": "A Fast, Simple and Straight-forward Markdown to HTML Converter for PHP.",
4 | "keywords": [
5 | "markdown",
6 | "markdown-to-html",
7 | "markdown-parser",
8 | "markdown-library"
9 | ],
10 | "type": "library",
11 | "license": "MIT",
12 | "autoload": {
13 | "psr-4": {
14 | "FastVolt\\Helper\\": "src/"
15 | }
16 | },
17 | "authors": [
18 | {
19 | "name": "Oladoyinbo Vincent",
20 | "email": "oladoyinboadverts@gmail.com"
21 | }
22 | ],
23 | "require": {
24 | "php": "^8.1",
25 | "ext-dom": "*",
26 | "ext-json": "*",
27 | "ext-libxml": "*",
28 | "ext-mbstring": "*",
29 | "ext-tokenizer": "*",
30 | "ext-xml": "*",
31 | "ext-xmlwriter": "*"
32 | },
33 | "require-dev": {
34 | "phpunit/phpunit": "^10.4",
35 | "friendsofphp/php-cs-fixer": "^3.38"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Libs/Markdown/ParseMarkdown.php:
--------------------------------------------------------------------------------
1 | `~\\';
20 | protected $urlsLinked = true;
21 |
22 | protected array $specialCharacters = [
23 | '\\',
24 | '`',
25 | '*',
26 | '_',
27 | '{',
28 | '}',
29 | '[',
30 | ']',
31 | '(',
32 | ')',
33 | '>',
34 | '#',
35 | '+',
36 | '-',
37 | '.',
38 | '!',
39 | '|',
40 | ];
41 |
42 | protected array $StrongRegex = [
43 | '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
44 | '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
45 | ];
46 |
47 | protected array $EmRegex = [
48 | '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
49 | '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
50 | ];
51 |
52 | protected string $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
53 |
54 | protected array $voidmarkdownElements = [
55 | 'area',
56 | 'base',
57 | 'br',
58 | 'col',
59 | 'command',
60 | 'embed',
61 | 'hr',
62 | 'img',
63 | 'input',
64 | 'link',
65 | 'meta',
66 | 'param',
67 | 'source',
68 | ];
69 |
70 | protected array $textLevelmarkdownElements = [
71 | 'a',
72 | 'br',
73 | 'bdo',
74 | 'abbr',
75 | 'blink',
76 | 'nextid',
77 | 'acronym',
78 | 'basefont',
79 | 'b',
80 | 'em',
81 | 'big',
82 | 'cite',
83 | 'small',
84 | 'spacer',
85 | 'listing',
86 | 'i',
87 | 'rp',
88 | 'del',
89 | 'code',
90 | 'strike',
91 | 'marquee',
92 | 'q',
93 | 'rt',
94 | 'ins',
95 | 'font',
96 | 'strong',
97 | 's',
98 | 'tt',
99 | 'kbd',
100 | 'mark',
101 | 'u',
102 | 'xm',
103 | 'sub',
104 | 'nobr',
105 | 'sup',
106 | 'ruby',
107 | 'var',
108 | 'span',
109 | 'wbr',
110 | 'time',
111 | ];
112 |
113 | protected array $safeLinksWhitelist = [
114 | 'http://',
115 | 'https://',
116 | 'ftp://',
117 | 'ftps://',
118 | 'mailto:',
119 | 'data:image/png;base64,',
120 | 'data:image/gif;base64,',
121 | 'data:image/jpeg;base64,',
122 | 'irc:',
123 | 'ircs:',
124 | 'git:',
125 | 'ssh:',
126 | 'news:',
127 | 'steam:',
128 | ];
129 |
130 | protected array $markdownBlockTypes = [
131 | '#' => ['Header'],
132 | '*' => ['Rule', 'List'],
133 | '+' => ['List'],
134 | '-' => ['SetextHeader', 'Table', 'Rule', 'List'],
135 | '0' => ['List'],
136 | '1' => ['List'],
137 | '2' => ['List'],
138 | '3' => ['List'],
139 | '4' => ['List'],
140 | '5' => ['List'],
141 | '6' => ['List'],
142 | '7' => ['List'],
143 | '8' => ['List'],
144 | '9' => ['List'],
145 | ':' => ['Table'],
146 | '<' => ['Comment', 'Markup'],
147 | '=' => ['SetextHeader'],
148 | '>' => ['Quote'],
149 | '[' => ['Reference'],
150 | '_' => ['Rule'],
151 | '`' => ['FencedCode'],
152 | '|' => ['Table'],
153 | '~' => ['FencedCode'],
154 | ];
155 |
156 | protected array $unmarkedmarkdownBlockTypes = [
157 | 'Code',
158 | ];
159 |
160 | protected array $InlineTypes = [
161 | '"' => ['SpecialCharacter'],
162 | '!' => ['Image'],
163 | '&' => ['SpecialCharacter'],
164 | '*' => ['Emphasis'],
165 | ':' => ['Url'],
166 | '<' => ['UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'],
167 | '>' => ['SpecialCharacter'],
168 | '[' => ['Link'],
169 | '_' => ['Emphasis'],
170 | '`' => ['Code'],
171 | '~' => ['Strikethrough'],
172 | '\\' => ['EscapeSequence'],
173 | ];
174 |
175 | public function __construct(
176 | protected bool $sanitize = true
177 | ) {
178 | $this->setBreaksEnabled = true;
179 | $this->DefinitionData = null;
180 | $this->safeMode = $sanitize;
181 |
182 | if (true === $sanitize) {
183 | $this->strictMode = true;
184 | $this->setUrlsLinked = false;
185 | $this->setMarkupEscaped = false;
186 | }
187 | }
188 |
189 | /**
190 | * Set Markup Contents
191 | *
192 | * @param string $text
193 | *
194 | * @return string
195 | */
196 | public function markdown_text(string $text): string
197 | {
198 | # make sure no definitions are set
199 | $this->DefinitionData = [];
200 |
201 | # standardize line breaks
202 | $text = str_replace(["\r\n", "\r"], "\n", $text);
203 |
204 | # remove surrounding line breaks
205 | $text = trim($text, "\n");
206 |
207 | # split text into lines
208 | $lines = explode("\n", $text);
209 |
210 | # iterate through lines to identify blocks
211 | $markup = $this->lines($lines);
212 |
213 | # trim line breaks
214 | $markup = trim($markup, "\n");
215 |
216 | return $markup;
217 | }
218 |
219 | # Setters
220 | private function setBreaksEnabled(bool $breaksEnabled): self
221 | {
222 | $this->breaksEnabled = $breaksEnabled;
223 |
224 | return $this;
225 | }
226 |
227 | private function setMarkupEscaped(bool $markupEscaped): self
228 | {
229 | $this->markupEscaped = $markupEscaped;
230 |
231 | return $this;
232 | }
233 |
234 | private function setUrlsLinked(bool $urlsLinked): self
235 | {
236 | $this->urlsLinked = $urlsLinked;
237 |
238 | return $this;
239 | }
240 |
241 | private function setSafeMode(bool $safeMode)
242 | {
243 | $this->safeMode = (bool) $safeMode;
244 |
245 | return $this;
246 | }
247 |
248 | protected function lines(array $lines): ?string
249 | {
250 | $CurrentmarkdownBlock = null;
251 |
252 | foreach ($lines as $line) {
253 |
254 | if (chop($line) === '') {
255 |
256 | if (isset($CurrentmarkdownBlock)) {
257 | $CurrentmarkdownBlock['interrupted'] = true;
258 | }
259 |
260 | continue;
261 | }
262 |
263 | if (strpos($line, "\t") !== false) {
264 |
265 | $parts = explode("\t", $line);
266 |
267 | $line = $parts[0];
268 |
269 | unset($parts[0]);
270 |
271 | foreach ($parts as $part) {
272 | $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
273 |
274 | $line .= str_repeat(' ', $shortage);
275 | $line .= $part;
276 | }
277 | }
278 |
279 | $indent = 0;
280 |
281 | while (isset($line[$indent]) and $line[$indent] === ' ') {
282 | $indent++;
283 | }
284 |
285 | $text = $indent > 0 ? substr($line, $indent) : $line;
286 |
287 | $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
288 |
289 | if (isset($CurrentmarkdownBlock['continuable'])) {
290 |
291 | $markdownBlock = $this->{'block' . $CurrentmarkdownBlock['type'] . 'Continue'}($Line, $CurrentmarkdownBlock);
292 |
293 | if (isset($markdownBlock)) {
294 |
295 | $CurrentmarkdownBlock = $markdownBlock;
296 |
297 | continue;
298 |
299 | } else {
300 |
301 | if ($this->ismarkdownBlockCompletable($CurrentmarkdownBlock['type'])) {
302 | $CurrentmarkdownBlock = $this->{'block' . $CurrentmarkdownBlock['type'] . 'Complete'}($CurrentmarkdownBlock);
303 | }
304 | }
305 | }
306 |
307 | $marker = $text[0];
308 |
309 | $blockTypes = $this->unmarkedmarkdownBlockTypes;
310 |
311 | if (isset($this->markdownBlockTypes[$marker])) {
312 | foreach ($this->markdownBlockTypes[$marker] as $blockType) {
313 | $blockTypes[] = $blockType;
314 | }
315 | }
316 |
317 | foreach ($blockTypes as $blockType) {
318 | $markdownBlock = $this->{'block' . $blockType}($Line, $CurrentmarkdownBlock);
319 |
320 | if (isset($markdownBlock)) {
321 | $markdownBlock['type'] = $blockType;
322 |
323 | if (!isset($markdownBlock['identified'])) {
324 | $markdownBlocks[] = $CurrentmarkdownBlock;
325 |
326 | $markdownBlock['identified'] = true;
327 | }
328 |
329 | if ($this->ismarkdownBlockContinuable($blockType)) {
330 | $markdownBlock['continuable'] = true;
331 | }
332 |
333 | $CurrentmarkdownBlock = $markdownBlock;
334 |
335 | continue 2;
336 | }
337 | }
338 |
339 | if (isset($CurrentmarkdownBlock) and !isset($CurrentmarkdownBlock['type']) and !isset($CurrentmarkdownBlock['interrupted'])) {
340 |
341 | $CurrentmarkdownBlock['element']['text'] .= "\n" . $text;
342 |
343 | } else {
344 | $markdownBlocks[] = $CurrentmarkdownBlock;
345 |
346 | $CurrentmarkdownBlock = $this->paragraph($Line);
347 |
348 | $CurrentmarkdownBlock['identified'] = true;
349 | }
350 | }
351 |
352 | if (isset($CurrentmarkdownBlock['continuable']) and $this->ismarkdownBlockCompletable($CurrentmarkdownBlock['type'])) {
353 | $CurrentmarkdownBlock = $this->{'block' . $CurrentmarkdownBlock['type'] . 'Complete'}($CurrentmarkdownBlock);
354 | }
355 |
356 | $markdownBlocks[] = $CurrentmarkdownBlock;
357 |
358 | unset($markdownBlocks[0]);
359 |
360 | $markup = '';
361 |
362 | foreach ($markdownBlocks as $markdownBlock) {
363 | if (isset($markdownBlock['hidden'])) {
364 | continue;
365 | }
366 |
367 | $markup .= "\n";
368 | $markup .= isset($markdownBlock['markup']) ? $markdownBlock['markup'] : $this->element($markdownBlock['element']);
369 | }
370 |
371 | $markup .= "\n";
372 |
373 | return $markup;
374 | }
375 |
376 | protected function ismarkdownBlockContinuable(string $Type): bool
377 | {
378 | return method_exists($this, 'block' . $Type . 'Continue');
379 | }
380 |
381 | protected function ismarkdownBlockCompletable(string $Type): bool
382 | {
383 | return method_exists($this, 'block' . $Type . 'Complete');
384 | }
385 |
386 | protected function blockCode(array $Line, ?array $markdownBlock = null): ?array
387 | {
388 | if (isset($markdownBlock) and !isset($markdownBlock['type']) and !isset($markdownBlock['interrupted'])) {
389 | return null;
390 | }
391 |
392 | if ($Line['indent'] >= 4) {
393 |
394 | $text = substr($Line['body'], 4);
395 |
396 | $markdownBlock = [
397 | 'element' => [
398 | 'name' => 'pre',
399 | 'handler' => 'element',
400 | 'text' => [
401 | 'name' => 'code',
402 | 'text' => $text,
403 | ],
404 | ],
405 | ];
406 |
407 | return $markdownBlock;
408 | }
409 |
410 | return null;
411 | }
412 |
413 | protected function blockCodeContinue(array $Line, array $markdownBlock)
414 | {
415 | if ($Line['indent'] >= 4) {
416 |
417 | if (isset($markdownBlock['interrupted'])) {
418 | $markdownBlock['element']['text']['text'] .= "\n";
419 |
420 | unset($markdownBlock['interrupted']);
421 | }
422 |
423 | $markdownBlock['element']['text']['text'] .= "\n";
424 |
425 | $text = substr($Line['body'], 4);
426 |
427 | $markdownBlock['element']['text']['text'] .= $text;
428 |
429 | return $markdownBlock;
430 | }
431 | }
432 |
433 | protected function blockCodeComplete(array $markdownBlock)
434 | {
435 | $text = $markdownBlock['element']['text']['text'];
436 |
437 | $markdownBlock['element']['text']['text'] = $text;
438 |
439 | return $markdownBlock;
440 | }
441 |
442 | /**
443 | * markdownBlock Comment
444 | *
445 | * @param array $Line
446 | *
447 | * @return mixed
448 | */
449 | protected function blockComment(array $Line)
450 | {
451 | if ($this->markupEscaped or $this->safeMode) {
452 | return null;
453 | }
454 |
455 | if (
456 | isset($Line['text'][3]) &&
457 | $Line['text'][3] === '-' &&
458 | $Line['text'][2] === '-' &&
459 | $Line['text'][1] === '!'
460 | ) {
461 |
462 | $markdownBlock = [
463 | 'markup' => $Line['body'],
464 | ];
465 |
466 | if (preg_match('/-->$/', $Line['text'])) {
467 | $markdownBlock['closed'] = true;
468 | }
469 |
470 | return $markdownBlock;
471 | }
472 | }
473 |
474 | protected function blockCommentContinue(array $Line, array $markdownBlock): ?array
475 | {
476 | if (isset($markdownBlock['closed'])) {
477 | return null;
478 | }
479 |
480 | $markdownBlock['markup'] .= "\n" . $Line['body'];
481 |
482 | if (preg_match('/-->$/', $Line['text'])) {
483 | $markdownBlock['closed'] = true;
484 | }
485 |
486 | return $markdownBlock;
487 | }
488 |
489 |
490 | /**
491 | * Fenced Codes
492 | *
493 | * @param array $Line
494 | *
495 | * @return mixed
496 | */
497 | protected function blockFencedCode(array $Line): ?array
498 | {
499 | if (preg_match('/^[' . $Line['text'][0] . ']{3,}[ ]*([^`]+)?[ ]*$/', $Line['text'], $matches)) {
500 |
501 | $markdownElement = [
502 | 'name' => 'code',
503 | 'text' => '',
504 | ];
505 |
506 | if (isset($matches[1])) {
507 | /**
508 | * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
509 | * Every HTML element may have a class attribute specified.
510 | * The attribute, if specified, must have a value that is a set
511 | * of space-separated tokens representing the various classes
512 | * that the element belongs to.
513 | * [...]
514 | * The space characters, for the purposes of this specification,
515 | * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab),
516 | * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and
517 | * U+000D CARRIAGE RETURN (CR).
518 | */
519 | $language = substr($matches[1], 0, strcspn($matches[1], " \t\n\f\r"));
520 |
521 | $class = 'language-' . $language;
522 |
523 | $markdownElement['attributes'] = [
524 | 'class' => $class,
525 | ];
526 | }
527 |
528 | $markdownBlock = [
529 | 'char' => $Line['text'][0],
530 | 'element' => [
531 | 'name' => 'pre',
532 | 'handler' => 'element',
533 | 'text' => $markdownElement,
534 | ],
535 | ];
536 |
537 | return $markdownBlock;
538 | }
539 |
540 | return null;
541 | }
542 |
543 | protected function blockFencedCodeContinue(array $Line, array $markdownBlock): ?array
544 | {
545 | if (isset($markdownBlock['complete'])) {
546 | return null;
547 | }
548 |
549 | if (isset($markdownBlock['interrupted'])) {
550 | $markdownBlock['element']['text']['text'] .= "\n";
551 |
552 | unset($markdownBlock['interrupted']);
553 | }
554 |
555 | if (preg_match('/^' . $markdownBlock['char'] . '{3,}[ ]*$/', $Line['text'])) {
556 | $markdownBlock['element']['text']['text'] = substr($markdownBlock['element']['text']['text'], 1);
557 |
558 | $markdownBlock['complete'] = true;
559 |
560 | return $markdownBlock;
561 | }
562 |
563 | $markdownBlock['element']['text']['text'] .= "\n" . $Line['body'];
564 |
565 | return $markdownBlock;
566 | }
567 |
568 | protected function blockFencedCodeComplete(array $markdownBlock): ?array
569 | {
570 | $text = $markdownBlock['element']['text']['text'];
571 |
572 | $markdownBlock['element']['text']['text'] = $text;
573 |
574 | return $markdownBlock;
575 | }
576 |
577 | /**
578 | * markdownBlock Header
579 | *
580 | * @param array $Line
581 | *
582 | * @return ?array
583 | */
584 | protected function blockHeader($Line): ?array
585 | {
586 | if (isset($Line['text'][1])) {
587 | $level = 1;
588 |
589 | while (isset($Line['text'][$level]) && $Line['text'][$level] === '#') {
590 | $level++;
591 | }
592 |
593 | if ($level > 6) {
594 | return null;
595 | }
596 |
597 | $text = trim($Line['text'], '# ');
598 |
599 | $markdownBlock = array(
600 | 'element' => array(
601 | 'name' => 'h' . min(6, $level),
602 | 'text' => $text,
603 | 'handler' => 'line',
604 | ),
605 | );
606 |
607 | return $markdownBlock;
608 | }
609 |
610 | return null;
611 | }
612 |
613 | protected function blockList(array $Line)
614 | {
615 | [$name, $pattern] = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');
616 |
617 | if (preg_match('/^(' . $pattern . '[ ]+)(.*)/', $Line['text'], $matches)) {
618 | $markdownBlock = array(
619 | 'indent' => $Line['indent'],
620 | 'pattern' => $pattern,
621 | 'element' => array(
622 | 'name' => $name,
623 | 'handler' => 'elements',
624 | ),
625 | );
626 |
627 | if ($name === 'ol') {
628 | $listStart = stristr($matches[0], '.', true);
629 |
630 | if ($listStart !== '1') {
631 | $markdownBlock['element']['attributes'] = array('start' => $listStart);
632 | }
633 | }
634 |
635 | $markdownBlock['li'] = array(
636 | 'name' => 'li',
637 | 'handler' => 'li',
638 | 'text' => array(
639 | $matches[2],
640 | ),
641 | );
642 |
643 | $markdownBlock['element']['text'][] = &$markdownBlock['li'];
644 |
645 | return $markdownBlock;
646 | }
647 | }
648 |
649 | protected function blockListContinue(array $Line, array $markdownBlock)
650 | {
651 | if ($markdownBlock['indent'] === $Line['indent'] and preg_match('/^' . $markdownBlock['pattern'] . '(?:[ ]+(.*)|$)/', $Line['text'], $matches)) {
652 | if (isset($markdownBlock['interrupted'])) {
653 | $markdownBlock['li']['text'][] = '';
654 |
655 | $markdownBlock['loose'] = true;
656 |
657 | unset($markdownBlock['interrupted']);
658 | }
659 |
660 | unset($markdownBlock['li']);
661 |
662 | $text = isset($matches[1]) ? $matches[1] : '';
663 |
664 | $markdownBlock['li'] = array(
665 | 'name' => 'li',
666 | 'handler' => 'li',
667 | 'text' => array(
668 | $text,
669 | ),
670 | );
671 |
672 | $markdownBlock['element']['text'][] = &$markdownBlock['li'];
673 |
674 | return $markdownBlock;
675 | }
676 |
677 | if ($Line['text'][0] === '[' and $this->blockReference($Line)) {
678 | return $markdownBlock;
679 | }
680 |
681 | if (!isset($markdownBlock['interrupted'])) {
682 | $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
683 |
684 | $markdownBlock['li']['text'][] = $text;
685 |
686 | return $markdownBlock;
687 | }
688 |
689 | if ($Line['indent'] > 0) {
690 | $markdownBlock['li']['text'][] = '';
691 |
692 | $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
693 |
694 | $markdownBlock['li']['text'][] = $text;
695 |
696 | unset($markdownBlock['interrupted']);
697 |
698 | return $markdownBlock;
699 | }
700 |
701 | return null;
702 | }
703 |
704 | protected function blockListComplete(array $markdownBlock)
705 | {
706 | if (isset($markdownBlock['loose'])) {
707 | foreach ($markdownBlock['element']['text'] as &$li) {
708 | if (end($li['text']) !== '') {
709 | $li['text'][] = '';
710 | }
711 | }
712 | }
713 |
714 | return $markdownBlock;
715 | }
716 |
717 | protected function blockQuote(array $Line): ?array
718 | {
719 | if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) {
720 |
721 | $markdownBlock = array(
722 | 'element' => array(
723 | 'name' => 'blockquote',
724 | 'handler' => 'lines',
725 | 'text' => (array) $matches[1],
726 | ),
727 | );
728 |
729 | return $markdownBlock;
730 | }
731 |
732 | return null;
733 | }
734 |
735 | protected function blockQuoteContinue(array $Line, array $markdownBlock): ?array
736 | {
737 | if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) {
738 |
739 | if (isset($markdownBlock['interrupted'])) {
740 |
741 | $markdownBlock['element']['text'][] = '';
742 |
743 | unset($markdownBlock['interrupted']);
744 | }
745 |
746 | $markdownBlock['element']['text'][] = $matches[1];
747 |
748 | return $markdownBlock;
749 | }
750 |
751 | if (!isset($markdownBlock['interrupted'])) {
752 |
753 | $markdownBlock['element']['text'][] = $Line['text'];
754 |
755 | return $markdownBlock;
756 | }
757 |
758 | return null;
759 | }
760 |
761 | protected function blockRule(array $Line): ?array
762 | {
763 | if (preg_match('/^([' . $Line['text'][0] . '])([ ]*\1){2,}[ ]*$/', $Line['text'])) {
764 |
765 | $markdownBlock = array(
766 | 'element' => array(
767 | 'name' => 'hr'
768 | ),
769 | );
770 |
771 | return $markdownBlock;
772 | }
773 |
774 | return null;
775 | }
776 |
777 | protected function blockSetextHeader(array $Line, ?array $markdownBlock = []): ?array
778 | {
779 | if (!isset($markdownBlock) or isset($markdownBlock['type']) or isset($markdownBlock['interrupted'])) {
780 | return null;
781 | }
782 |
783 | if (chop($Line['text'], $Line['text'][0]) === '') {
784 |
785 | $markdownBlock['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
786 |
787 | return $markdownBlock;
788 | }
789 |
790 | return null;
791 | }
792 |
793 | protected function blockMarkup(array $Line): ?array
794 | {
795 | if ($this->markupEscaped or $this->safeMode) {
796 | return null;
797 | }
798 |
799 | if (preg_match('/^<(\w[\w-]*)(?:[ ]*' . $this->regexHtmlAttribute . ')*[ ]*(\/)?>/', $Line['text'], $matches)) {
800 |
801 | $element = strtolower($matches[1]);
802 |
803 | if (in_array($element, $this->textLevelmarkdownElements)) {
804 | return null;
805 | }
806 |
807 | $markdownBlock = array(
808 | 'name' => $matches[1],
809 | 'depth' => 0,
810 | 'markup' => $Line['text'],
811 | );
812 |
813 | $length = strlen($matches[0]);
814 |
815 | $remainder = substr($Line['text'], $length);
816 |
817 | if (trim($remainder) === '') {
818 |
819 | if (isset($matches[2]) or in_array($matches[1], $this->voidmarkdownElements)) {
820 | $markdownBlock['closed'] = true;
821 |
822 | $markdownBlock['void'] = true;
823 | }
824 |
825 | } else {
826 | if (isset($matches[2]) or in_array($matches[1], $this->voidmarkdownElements)) {
827 | return null;
828 | }
829 |
830 | if (preg_match('/<\/' . $matches[1] . '>[ ]*$/i', $remainder)) {
831 | $markdownBlock['closed'] = true;
832 | }
833 | }
834 |
835 | return $markdownBlock;
836 | }
837 |
838 | return null;
839 | }
840 |
841 | protected function blockMarkupContinue(array $Line, array $markdownBlock): ?array
842 | {
843 | if (isset($markdownBlock['closed'])) {
844 | return null;
845 | }
846 |
847 | if (preg_match('/^<' . $markdownBlock['name'] . '(?:[ ]*' . $this->regexHtmlAttribute . ')*[ ]*>/i', $Line['text'])) # open
848 | {
849 | $markdownBlock['depth']++;
850 | }
851 |
852 | if (preg_match('/(.*?)<\/' . $markdownBlock['name'] . '>[ ]*$/i', $Line['text'], $matches)) # close
853 | {
854 | if ($markdownBlock['depth'] > 0) {
855 | $markdownBlock['depth']--;
856 | } else {
857 | $markdownBlock['closed'] = true;
858 | }
859 | }
860 |
861 | if (isset($markdownBlock['interrupted'])) {
862 | $markdownBlock['markup'] .= "\n";
863 |
864 | unset($markdownBlock['interrupted']);
865 | }
866 |
867 | $markdownBlock['markup'] .= "\n" . $Line['body'];
868 |
869 | return $markdownBlock;
870 | }
871 |
872 | protected function blockReference(array $Line): ?array
873 | {
874 | if (preg_match('/^\[(.+?)\]:[ ]*(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches)) {
875 |
876 | $id = strtolower($matches[1]);
877 |
878 | $Data = array(
879 | 'url' => $matches[2],
880 | 'title' => null,
881 | );
882 |
883 | if (isset($matches[3])) {
884 | $Data['title'] = $matches[3];
885 | }
886 |
887 | $this->DefinitionData['Reference'][$id] = $Data;
888 |
889 | $markdownBlock = array(
890 | 'hidden' => true,
891 | );
892 |
893 | return $markdownBlock;
894 | }
895 |
896 | return null;
897 | }
898 |
899 | protected function blockTable($Line, ?array $markdownBlock = [])
900 | {
901 | if (!isset($markdownBlock) || isset($markdownBlock['type']) || isset($markdownBlock['interrupted'])) {
902 | return null;
903 | }
904 |
905 | if (strpos($markdownBlock['element']['text'], '|') !== false || chop($Line['text'], ' -:|') === '') {
906 | $alignments = [];
907 |
908 | $divider = $Line['text'];
909 |
910 | $divider = trim($divider);
911 | $divider = trim($divider, '|');
912 |
913 | $dividerCells = explode('|', $divider);
914 |
915 | foreach ($dividerCells as $dividerCell) {
916 | $dividerCell = trim($dividerCell);
917 |
918 | if ($dividerCell === '') {
919 | continue;
920 | }
921 |
922 | $alignment = null;
923 |
924 | if ($dividerCell[0] === ':') {
925 | $alignment = 'left';
926 | }
927 |
928 | if (substr($dividerCell, -1) === ':') {
929 | $alignment = $alignment === 'left' ? 'center' : 'right';
930 | }
931 |
932 | $alignments[] = $alignment;
933 | }
934 |
935 |
936 |
937 | $HeadermarkdownElements = [];
938 | $header = $markdownBlock['element']['text'];
939 | $header = trim($header);
940 | $header = trim($header, '|');
941 | $headerCells = explode('|', $header);
942 |
943 | foreach ($headerCells as $index => $headerCell) {
944 | $headerCell = trim($headerCell);
945 | $HeadermarkdownElement = [
946 | 'name' => 'th',
947 | 'text' => $headerCell,
948 | 'handler' => 'line',
949 | ];
950 |
951 | if (isset($alignments[$index])) {
952 | $alignment = $alignments[$index];
953 | $HeadermarkdownElement['attributes'] = [
954 | 'style' => 'text-align: ' . $alignment . ';',
955 | ];
956 | }
957 |
958 | $HeadermarkdownElements[] = $HeadermarkdownElement;
959 | }
960 |
961 | $markdownBlock = [
962 | 'alignments' => $alignments,
963 | 'identified' => true,
964 | 'element' => [
965 | 'name' => 'table',
966 | 'handler' => 'elements',
967 | ],
968 | ];
969 |
970 | $markdownBlock['element']['text'][] = [
971 | 'name' => 'thead',
972 | 'handler' => 'elements',
973 | ];
974 |
975 | $markdownBlock['element']['text'][] = array(
976 | 'name' => 'tbody',
977 | 'handler' => 'elements',
978 | 'text' => [],
979 | );
980 |
981 | $markdownBlock['element']['text'][0]['text'][] = array(
982 | 'name' => 'tr',
983 | 'handler' => 'elements',
984 | 'text' => $HeadermarkdownElements,
985 | );
986 |
987 | return $markdownBlock;
988 | }
989 |
990 | return null;
991 | }
992 |
993 | protected function blockTableContinue($Line, $markdownBlock)
994 | {
995 | if (isset($markdownBlock['interrupted'])) {
996 | return null;
997 | }
998 |
999 | if ($Line['text'][0] === '|' or strpos($Line['text'], '|')) {
1000 | $markdownElements = [];
1001 |
1002 | $row = $Line['text'];
1003 |
1004 | $row = trim($row);
1005 | $row = trim($row, '|');
1006 |
1007 | preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
1008 |
1009 | foreach ($matches[0] as $index => $cell) {
1010 | $cell = trim($cell);
1011 |
1012 | $markdownElement = array(
1013 | 'name' => 'td',
1014 | 'handler' => 'line',
1015 | 'text' => $cell,
1016 | );
1017 |
1018 | if (isset($markdownBlock['alignments'][$index])) {
1019 | $markdownElement['attributes'] = [
1020 | 'style' => 'text-align: ' . $markdownBlock['alignments'][$index] . ';',
1021 | ];
1022 | }
1023 |
1024 | $markdownElements[] = $markdownElement;
1025 | }
1026 |
1027 | $markdownElement = array(
1028 | 'name' => 'tr',
1029 | 'handler' => 'elements',
1030 | 'text' => $markdownElements,
1031 | );
1032 |
1033 | $markdownBlock['element']['text'][1]['text'][] = $markdownElement;
1034 |
1035 | return $markdownBlock;
1036 | }
1037 | }
1038 |
1039 | protected function paragraph($Line)
1040 | {
1041 | $markdownBlock = array(
1042 | 'element' => array(
1043 | 'name' => 'p',
1044 | 'text' => $Line['text'],
1045 | 'handler' => 'line',
1046 | ),
1047 | );
1048 |
1049 | return $markdownBlock;
1050 | }
1051 |
1052 | /**
1053 | * Inline markdownElement
1054 | *
1055 | * @param string $text
1056 | * @param array $nonNestables
1057 | *
1058 | */
1059 | public function line(string $text, array $nonNestables = [])
1060 | {
1061 | $markup = '';
1062 |
1063 | # $excerpt is based on the first occurrence of a marker
1064 |
1065 | while ($excerpt = strpbrk($text, $this->inlineMarkerList)) {
1066 |
1067 | $marker = $excerpt[0];
1068 |
1069 | $markerPosition = strpos($text, $marker);
1070 |
1071 | $Excerpt = array('text' => $excerpt, 'context' => $text);
1072 |
1073 | foreach ($this->InlineTypes[$marker] as $inlineType) {
1074 | # check to see if the current inline type is nestable in the current context
1075 |
1076 | if (!empty($nonNestables) && in_array($inlineType, $nonNestables)) {
1077 | continue;
1078 | }
1079 |
1080 | $Inline = $this->{'inline' . $inlineType}($Excerpt);
1081 |
1082 | if (!isset($Inline)) {
1083 | continue;
1084 | }
1085 |
1086 | # makes sure that the inline belongs to "our" marker
1087 |
1088 | if (isset($Inline['position']) and $Inline['position'] > $markerPosition) {
1089 | continue;
1090 | }
1091 |
1092 | # sets a default inline position
1093 |
1094 | if (!isset($Inline['position'])) {
1095 | $Inline['position'] = $markerPosition;
1096 | }
1097 |
1098 | # cause the new element to 'inherit' our non nestables
1099 | foreach ($nonNestables as $non_nestable) {
1100 | $Inline['element']['nonNestables'][] = $non_nestable;
1101 | }
1102 |
1103 | # the text that comes before the inline
1104 | $unmarkedText = substr($text, 0, $Inline['position']);
1105 |
1106 | # compile the unmarked text
1107 | $markup .= $this->unmarkedText($unmarkedText);
1108 |
1109 | # compile the inline
1110 | $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
1111 |
1112 | # remove the examined text
1113 | $text = substr($text, $Inline['position'] + $Inline['extent']);
1114 |
1115 | continue 2;
1116 | }
1117 |
1118 | # the marker does not belong to an inline
1119 |
1120 | $unmarkedText = substr($text, 0, $markerPosition + 1);
1121 |
1122 | $markup .= $this->unmarkedText($unmarkedText);
1123 |
1124 | $text = substr($text, $markerPosition + 1);
1125 | }
1126 |
1127 | $markup .= $this->unmarkedText($text);
1128 |
1129 | return $markup;
1130 | }
1131 |
1132 | protected function inlineCode($Excerpt)
1133 | {
1134 | $marker = $Excerpt['text'][0];
1135 |
1136 | if (preg_match('/^(' . $marker . '+)[ ]*(.+?)[ ]*(? strlen($matches[0]),
1142 | 'element' => array(
1143 | 'name' => 'code',
1144 | 'text' => $text,
1145 | ),
1146 | );
1147 | }
1148 | }
1149 |
1150 | protected function inlineEmailTag($Excerpt)
1151 | {
1152 | if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches)) {
1153 | $url = $matches[1];
1154 |
1155 | if (!isset($matches[2])) {
1156 | $url = 'mailto:' . $url;
1157 | }
1158 |
1159 | return array(
1160 | 'extent' => strlen($matches[0]),
1161 | 'element' => array(
1162 | 'name' => 'a',
1163 | 'text' => $matches[1],
1164 | 'attributes' => array(
1165 | 'href' => $url,
1166 | ),
1167 | ),
1168 | );
1169 | }
1170 | }
1171 |
1172 | protected function inlineEmphasis(array $Excerpt): ?array
1173 | {
1174 | if (!isset($Excerpt['text'][1])) {
1175 | return null;
1176 | }
1177 |
1178 | $marker = $Excerpt['text'][0];
1179 |
1180 | if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) {
1181 | $emphasis = 'strong';
1182 | } elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) {
1183 | $emphasis = 'i';
1184 | } else {
1185 | return null;
1186 | }
1187 |
1188 | return array(
1189 | 'extent' => strlen($matches[0]),
1190 | 'element' => array(
1191 | 'name' => $emphasis,
1192 | 'handler' => 'line',
1193 | 'text' => $matches[1],
1194 | ),
1195 | );
1196 | }
1197 |
1198 | protected function inlineEscapeSequence(array $Excerpt)
1199 | {
1200 | if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) {
1201 | return array(
1202 | 'markup' => $Excerpt['text'][1],
1203 | 'extent' => 2,
1204 | );
1205 | }
1206 | }
1207 |
1208 | protected function inlineImage(array $Excerpt)
1209 | {
1210 | if (!isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') {
1211 | return null;
1212 | }
1213 |
1214 | $Excerpt['text'] = substr($Excerpt['text'], 1);
1215 |
1216 | $Link = $this->inlineLink($Excerpt);
1217 |
1218 | if ($Link === null) {
1219 | return null;
1220 | }
1221 |
1222 | $Inline = array(
1223 | 'extent' => $Link['extent'] + 1,
1224 | 'element' => array(
1225 | 'name' => 'img',
1226 | 'attributes' => array(
1227 | 'src' => $Link['element']['attributes']['href'],
1228 | 'alt' => $Link['element']['text'],
1229 | ),
1230 | ),
1231 | );
1232 |
1233 | $Inline['element']['attributes'] += $Link['element']['attributes'];
1234 |
1235 | unset($Inline['element']['attributes']['href']);
1236 |
1237 | return $Inline;
1238 | }
1239 |
1240 | protected function inlineLink(array $Excerpt)
1241 | {
1242 | $markdownElement = array(
1243 | 'name' => 'a',
1244 | 'handler' => 'line',
1245 | 'nonNestables' => array('Url', 'Link'),
1246 | 'text' => null,
1247 | 'attributes' => array(
1248 | 'href' => null,
1249 | 'title' => null,
1250 | ),
1251 | );
1252 |
1253 | $extent = 0;
1254 |
1255 | $remainder = $Excerpt['text'];
1256 |
1257 | if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) {
1258 | $markdownElement['text'] = $matches[1];
1259 |
1260 | $extent += strlen($matches[0]);
1261 |
1262 | $remainder = substr($remainder, $extent);
1263 | } else {
1264 | return null;
1265 | }
1266 |
1267 | if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches)) {
1268 | $markdownElement['attributes']['href'] = $matches[1];
1269 |
1270 | if (isset($matches[2])) {
1271 | $markdownElement['attributes']['title'] = substr($matches[2], 1, -1);
1272 | }
1273 |
1274 | $extent += strlen($matches[0]);
1275 | } else {
1276 | if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) {
1277 | $definition = strlen($matches[1]) ? $matches[1] : $markdownElement['text'];
1278 | $definition = strtolower($definition);
1279 |
1280 | $extent += strlen($matches[0]);
1281 | } else {
1282 | $definition = strtolower((string) $markdownElement['text']);
1283 | }
1284 |
1285 | if (!isset($this->DefinitionData['Reference'][$definition])) {
1286 | return null;
1287 | }
1288 |
1289 | $Definition = $this->DefinitionData['Reference'][$definition];
1290 |
1291 | $markdownElement['attributes']['href'] = $Definition['url'];
1292 | $markdownElement['attributes']['title'] = $Definition['title'];
1293 | }
1294 |
1295 | return array(
1296 | 'extent' => $extent,
1297 | 'element' => $markdownElement,
1298 | );
1299 | }
1300 |
1301 | protected function inlineMarkup($Excerpt)
1302 | {
1303 | if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) {
1304 | return null;
1305 | }
1306 |
1307 | if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches)) {
1308 | return array(
1309 | 'markup' => $matches[0],
1310 | 'extent' => strlen($matches[0]),
1311 | );
1312 | }
1313 |
1314 | if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) {
1315 | return array(
1316 | 'markup' => $matches[0],
1317 | 'extent' => strlen($matches[0]),
1318 | );
1319 | }
1320 |
1321 | if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*(?:[ ]*' . $this->regexHtmlAttribute . ')*[ ]*\/?>/s', $Excerpt['text'], $matches)) {
1322 | return array(
1323 | 'markup' => $matches[0],
1324 | 'extent' => strlen($matches[0]),
1325 | );
1326 | }
1327 | }
1328 |
1329 | protected function inlineSpecialCharacter($Excerpt)
1330 | {
1331 | if ($Excerpt['text'][0] === '&' and !preg_match('/^?\w+;/', $Excerpt['text'])) {
1332 | return array(
1333 | 'markup' => '&',
1334 | 'extent' => 1,
1335 | );
1336 | }
1337 |
1338 | $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
1339 |
1340 | if (isset($SpecialCharacter[$Excerpt['text'][0]])) {
1341 | return array(
1342 | 'markup' => '&' . $SpecialCharacter[$Excerpt['text'][0]] . ';',
1343 | 'extent' => 1,
1344 | );
1345 | }
1346 | }
1347 |
1348 | protected function inlineStrikethrough($Excerpt)
1349 | {
1350 | if (!isset($Excerpt['text'][1])) {
1351 | return null;
1352 | }
1353 |
1354 | if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) {
1355 | return array(
1356 | 'extent' => strlen($matches[0]),
1357 | 'element' => array(
1358 | 'name' => 'del',
1359 | 'text' => $matches[1],
1360 | 'handler' => 'line',
1361 | ),
1362 | );
1363 | }
1364 | }
1365 |
1366 | protected function inlineUrl($Excerpt): array|null
1367 | {
1368 | if ($this->urlsLinked !== true or !isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') {
1369 | return null;
1370 | }
1371 |
1372 | if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) {
1373 | $url = $matches[0][0];
1374 |
1375 | $Inline = array(
1376 | 'extent' => strlen($matches[0][0]),
1377 | 'position' => $matches[0][1],
1378 | 'element' => array(
1379 | 'name' => 'a',
1380 | 'text' => $url,
1381 | 'attributes' => array(
1382 | 'href' => $url,
1383 | ),
1384 | ),
1385 | );
1386 |
1387 | return $Inline;
1388 | }
1389 |
1390 | return null;
1391 | }
1392 |
1393 | protected function inlineUrlTag($Excerpt): array|null
1394 | {
1395 | if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) {
1396 | $url = $matches[1];
1397 |
1398 | return array(
1399 | 'extent' => strlen($matches[0]),
1400 | 'element' => array(
1401 | 'name' => 'a',
1402 | 'text' => $url,
1403 | 'attributes' => array(
1404 | 'href' => $url,
1405 | ),
1406 | ),
1407 | );
1408 | }
1409 |
1410 | return null;
1411 | }
1412 |
1413 | protected function unmarkedText($text): array|string|null
1414 | {
1415 | if ($this->breaksEnabled) {
1416 |
1417 | $text = preg_replace('/[ ]*\n/', "
\n", $text);
1418 |
1419 | } else {
1420 | $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "
\n", $text);
1421 | $text = str_replace(" \n", "\n", $text);
1422 | }
1423 |
1424 | return $text;
1425 | }
1426 |
1427 | protected function element(array $markdownElement)
1428 | {
1429 | if ($this->safeMode) {
1430 | $markdownElement = $this->sanitisemarkdownElement($markdownElement);
1431 | }
1432 |
1433 | $markup = '<' . $markdownElement['name'];
1434 |
1435 | if (isset($markdownElement['attributes'])) {
1436 | foreach ($markdownElement['attributes'] as $name => $value) {
1437 | if ($value === null) {
1438 | continue;
1439 | }
1440 |
1441 | $markup .= ' ' . $name . '="' . self::escape($value) . '"';
1442 | }
1443 | }
1444 |
1445 | $permitRawHtml = false;
1446 |
1447 | if (isset($markdownElement['text'])) {
1448 | $text = $markdownElement['text'];
1449 | }
1450 | // very strongly consider an alternative if you're writing an
1451 | // extension
1452 | elseif (isset($markdownElement['rawHtml'])) {
1453 | $text = $markdownElement['rawHtml'];
1454 | $allowRawHtmlInSafeMode = isset($markdownElement['allowRawHtmlInSafeMode']) && $markdownElement['allowRawHtmlInSafeMode'];
1455 | $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode;
1456 | }
1457 |
1458 | if (isset($text)) {
1459 | $markup .= '>';
1460 |
1461 | if (!isset($markdownElement['nonNestables'])) {
1462 | $markdownElement['nonNestables'] = [];
1463 | }
1464 |
1465 | if (isset($markdownElement['handler'])) {
1466 | $markup .= $this->{$markdownElement['handler']}($text, $markdownElement['nonNestables']);
1467 | } elseif (!$permitRawHtml) {
1468 | $markup .= self::escape($text, true);
1469 | } else {
1470 | $markup .= $text;
1471 | }
1472 |
1473 | $markup .= '' . $markdownElement['name'] . '>';
1474 | } else {
1475 | $markup .= ' />';
1476 | }
1477 |
1478 | return $markup;
1479 | }
1480 |
1481 | protected function elements(array $markdownElements): string
1482 | {
1483 | $markup = '';
1484 |
1485 | foreach ($markdownElements as $markdownElement) {
1486 | $markup .= "\n" . $this->element($markdownElement);
1487 | }
1488 |
1489 | $markup .= "\n";
1490 |
1491 | return $markup;
1492 | }
1493 |
1494 | protected function li($lines): ?string
1495 | {
1496 | $markup = $this->lines($lines);
1497 |
1498 | $trimmedMarkup = trim($markup);
1499 |
1500 | if (!in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '') {
1501 | $markup = $trimmedMarkup;
1502 | $markup = substr($markup, 3);
1503 |
1504 | $position = strpos($markup, "
");
1505 |
1506 | $markup = substr_replace($markup, '', $position, 4);
1507 | }
1508 |
1509 | return $markup;
1510 | }
1511 |
1512 | /**
1513 | * Replace occurrences $regexp with $Elements in $text. Return an array of
1514 | * elements representing the replacement.
1515 | */
1516 | protected static function pregReplaceElements(string $regexp, array $Elements, string $text): array
1517 | {
1518 | $newElements = [];
1519 |
1520 | while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) {
1521 | $offset = $matches[0][1];
1522 | $before = substr($text, 0, $offset);
1523 | $after = substr($text, $offset + strlen($matches[0][0]));
1524 |
1525 | $newElements[] = ['text' => $before];
1526 |
1527 | foreach ($Elements as $Element) {
1528 | $newElements[] = $Element;
1529 | }
1530 |
1531 | $text = $after;
1532 | }
1533 |
1534 | $newElements[] = ['text' => $text];
1535 |
1536 | return $newElements;
1537 | }
1538 |
1539 | /**
1540 | * Inline Text
1541 | *
1542 | * @param string $text
1543 | *
1544 | * @return array
1545 | */
1546 | protected function inlineText(string $text): array
1547 | {
1548 | $Inline = [
1549 | 'extent' => strlen($text),
1550 | 'element' => array(),
1551 | ];
1552 |
1553 | $Inline['element']['elements'] = self::pregReplaceElements(
1554 | $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/',
1555 | [
1556 | array('name' => 'br'),
1557 | array('text' => "\n"),
1558 | ],
1559 | $text
1560 | );
1561 |
1562 | return $Inline;
1563 | }
1564 |
1565 | protected function handleElementRecursive(array $Element): mixed
1566 | {
1567 | return $this->elementApplyRecursive(array($this, 'handle'), $Element);
1568 | }
1569 |
1570 | protected function handleElementsRecursive(array $Elements): array
1571 | {
1572 | return $this->elementsApplyRecursive(array($this, 'handle'), $Elements);
1573 | }
1574 |
1575 | protected function elementApplyRecursive($closure, array $Element): mixed
1576 | {
1577 | $Element = call_user_func($closure, $Element);
1578 |
1579 | if (isset($Element['elements'])) {
1580 | $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']);
1581 | } elseif (isset($Element['element'])) {
1582 | $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']);
1583 | }
1584 |
1585 | return $Element;
1586 | }
1587 |
1588 | protected function elementsApplyRecursive($closure, array $Elements): array
1589 | {
1590 | foreach ($Elements as &$Element) {
1591 | $Element = $this->elementApplyRecursive($closure, $Element);
1592 | }
1593 |
1594 | return $Elements;
1595 | }
1596 |
1597 | protected function elementsApplyRecursiveDepthFirst($closure, array $Elements): array
1598 | {
1599 | foreach ($Elements as &$Element) {
1600 | $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element);
1601 | }
1602 |
1603 | return $Elements;
1604 | }
1605 |
1606 | protected function elementApplyRecursiveDepthFirst($closure, array $Element)
1607 | {
1608 | if (isset($Element['elements'])) {
1609 | $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']);
1610 | } elseif (isset($Element['element'])) {
1611 | $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']);
1612 | }
1613 |
1614 | $Element = call_user_func($closure, $Element);
1615 |
1616 | return $Element;
1617 | }
1618 |
1619 | protected function sanitisemarkdownElement(array $markdownElement): array
1620 | {
1621 | static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';
1622 | static $safeUrlNameToAtt = [
1623 | 'a' => 'href',
1624 | 'img' => 'src',
1625 | ];
1626 |
1627 | if (isset($safeUrlNameToAtt[$markdownElement['name']])) {
1628 | $markdownElement = $this->filterUnsafeUrlInAttribute($markdownElement, $safeUrlNameToAtt[$markdownElement['name']]);
1629 | }
1630 |
1631 | if (!empty($markdownElement['attributes'])) {
1632 |
1633 | foreach ($markdownElement['attributes'] as $att => $val) {
1634 | # filter out badly parsed attribute
1635 | if (!preg_match($goodAttribute, $att)) {
1636 | unset($markdownElement['attributes'][$att]);
1637 | }
1638 | # dump onevent attribute
1639 | elseif (self::striAtStart($att, 'on')) {
1640 | unset($markdownElement['attributes'][$att]);
1641 | }
1642 | }
1643 | }
1644 |
1645 | return $markdownElement;
1646 | }
1647 |
1648 | protected function filterUnsafeUrlInAttribute(array $markdownElement, $attribute): array
1649 | {
1650 | foreach ($this->safeLinksWhitelist as $scheme) {
1651 |
1652 | if (self::striAtStart($markdownElement['attributes'][$attribute], $scheme)) {
1653 | return $markdownElement;
1654 | }
1655 | }
1656 |
1657 | $markdownElement['attributes'][$attribute] = str_replace(':', '%3A', $markdownElement['attributes'][$attribute]);
1658 |
1659 | return $markdownElement;
1660 | }
1661 |
1662 | protected function textElements($text): array
1663 | {
1664 | # make sure no definitions are set
1665 | $this->DefinitionData = [];
1666 |
1667 | # standardize line breaks
1668 | $text = str_replace(array("\r\n", "\r"), "\n", $text);
1669 |
1670 | # remove surrounding line breaks
1671 | $text = trim($text, "\n");
1672 |
1673 | # split text into lines
1674 | $lines = explode("\n", $text);
1675 |
1676 | # iterate through lines to identify blocks
1677 | return $this->linesElements($lines);
1678 | }
1679 |
1680 | protected $unmarkedBlockTypes;
1681 | protected $BlockTypes;
1682 |
1683 | protected function linesElements(array $lines): array
1684 | {
1685 | $Elements = [];
1686 | $CurrentBlock = null;
1687 |
1688 | foreach ($lines as $line) {
1689 | if (chop($line) === '') {
1690 | if (isset($CurrentBlock)) {
1691 | $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted'])
1692 | ? $CurrentBlock['interrupted'] + 1
1693 | : 1
1694 | );
1695 | }
1696 | continue;
1697 | }
1698 |
1699 | while (($beforeTab = strstr($line, "\t", true)) !== false) {
1700 | $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4;
1701 |
1702 | $line = $beforeTab
1703 | . str_repeat(' ', $shortage)
1704 | . substr($line, strlen($beforeTab) + 1)
1705 | ;
1706 | }
1707 |
1708 | $indent = strspn($line, ' ');
1709 | $text = $indent > 0 ? substr($line, $indent) : $line;
1710 | $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
1711 |
1712 | if (isset($CurrentBlock['continuable'])) {
1713 | $methodName = 'block' . $CurrentBlock['type'] . 'Continue';
1714 | $Block = $this->$methodName($Line, $CurrentBlock);
1715 |
1716 | if (isset($Block)) {
1717 | $CurrentBlock = $Block;
1718 |
1719 | continue;
1720 | } else {
1721 | $methodName = 'block' . $CurrentBlock['type'] . 'Complete';
1722 | $CurrentBlock = $this->$methodName($CurrentBlock);
1723 | }
1724 | }
1725 |
1726 | $marker = $text[0];
1727 | $blockTypes = $this->unmarkedBlockTypes;
1728 |
1729 | if (isset($this->BlockTypes[$marker])) {
1730 | foreach ($this->BlockTypes[$marker] as $blockType) {
1731 | $blockTypes[] = $blockType;
1732 | }
1733 | }
1734 |
1735 | foreach ($blockTypes as $blockType) {
1736 | $Block = $this->{"block$blockType"}($Line, $CurrentBlock);
1737 |
1738 | if (isset($Block)) {
1739 | $Block['type'] = $blockType;
1740 |
1741 | if (!isset($Block['identified'])) {
1742 | if (isset($CurrentBlock)) {
1743 | $Elements[] = $this->extractElement($CurrentBlock);
1744 | }
1745 |
1746 | $Block['identified'] = true;
1747 | }
1748 |
1749 | if ($this->isBlockContinuable($blockType)) {
1750 | $Block['continuable'] = true;
1751 | }
1752 |
1753 | $CurrentBlock = $Block;
1754 |
1755 | continue 2;
1756 | }
1757 | }
1758 |
1759 | if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph') {
1760 | $Block = $this->paragraphContinue($Line, $CurrentBlock);
1761 | }
1762 |
1763 | if (isset($Block)) {
1764 | $CurrentBlock = $Block;
1765 | } else {
1766 | if (isset($CurrentBlock)) {
1767 | $Elements[] = $this->extractElement($CurrentBlock);
1768 | }
1769 |
1770 | $CurrentBlock = $this->paragraph($Line);
1771 |
1772 | $CurrentBlock['identified'] = true;
1773 | }
1774 | }
1775 |
1776 | if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) {
1777 | $methodName = 'block' . $CurrentBlock['type'] . 'Complete';
1778 | $CurrentBlock = $this->$methodName($CurrentBlock);
1779 | }
1780 |
1781 | if (isset($CurrentBlock)) {
1782 | $Elements[] = $this->extractElement($CurrentBlock);
1783 | }
1784 |
1785 | return $Elements;
1786 | }
1787 |
1788 | protected function isBlockContinuable($Type): bool
1789 | {
1790 | return method_exists($this, "block{$Type}Continue");
1791 | }
1792 |
1793 | protected function isBlockCompletable($Type): bool
1794 | {
1795 | return method_exists($this, "block{$Type}Complete");
1796 | }
1797 |
1798 | protected function extractElement(array $Component)
1799 | {
1800 | if ( ! isset($Component['element'])) {
1801 | if (isset($Component['markup'])) {
1802 | $Component['element'] = ['rawHtml' => $Component['markup']];
1803 | } elseif (isset($Component['hidden'])) {
1804 | $Component['element'] = [];
1805 | }
1806 | }
1807 |
1808 | return $Component['element'];
1809 | }
1810 |
1811 | protected function paragraphContinue($Line, array $Block): array
1812 | {
1813 | if (isset($Block['interrupted'])) return [];
1814 |
1815 | $Block['element']['handler']['argument'] .= "\n".$Line['text'];
1816 |
1817 | return $Block;
1818 | }
1819 |
1820 | protected static function escape($text, $allowQuotes = false): string
1821 | {
1822 | return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8');
1823 | }
1824 |
1825 | protected static function striAtStart($string, $needle)
1826 | {
1827 | $len = strlen($needle);
1828 |
1829 | if ($len > strlen($string)) {
1830 | return false;
1831 | } else {
1832 | return strtolower(substr($string, 0, $len)) === strtolower($needle);
1833 | }
1834 | }
1835 |
1836 | protected static function instance($name = 'default')
1837 | {
1838 | if (isset(self::$instances[$name])) {
1839 | return self::$instances[$name];
1840 | }
1841 |
1842 | $instance = new static();
1843 | self::$instances[$name] = $instance;
1844 |
1845 | return $instance;
1846 | }
1847 | }
--------------------------------------------------------------------------------
/src/Markdown.php:
--------------------------------------------------------------------------------
1 | contents[]['multi-line'] = $content;
41 | return $this;
42 | }
43 |
44 | /**
45 | * Set Inline Markdown Contents
46 | *
47 | * @param string $content markdown contents
48 | *
49 | * @return self
50 | */
51 | public function setInlineContent(string $content): static
52 | {
53 | $this->contents[]['inline'] = $content;
54 | return $this;
55 | }
56 |
57 | /**
58 | * Set Markdown File
59 | *
60 | * @param string $file_name: Set File to read markdown content from e.g './markdowns/index.md'
61 | *
62 | * @return self
63 | */
64 | public function setFile(string $file_name): static
65 | {
66 | $this->contents[]['file'] = $file_name;
67 | return $this;
68 | }
69 |
70 | /**
71 | * Set directory where compiled markdown files will be stored in html format
72 | *
73 | * @param string $directory directory where your compiled html files will be stored
74 | */
75 | public function setCompileDir(string $directory = './markdowns/'): static
76 | {
77 | try {
78 | $compilationDir = !str_ends_with($directory, '/')
79 | ? "$directory/"
80 | : $directory;
81 |
82 | $this->compileDir[] = $compilationDir;
83 |
84 | if (!is_dir($compilationDir)) {
85 | if (mkdir($compilationDir, 0777)) {
86 | return $this;
87 | }
88 | }
89 | return $this;
90 | } catch (\Exception|\TypeError|\Throwable $e) {
91 | throw $e;
92 | }
93 | }
94 |
95 | /**
96 | * Read File Contents
97 | *
98 | * @param string $filename Input file name
99 | *
100 | * @return string|\Exception|null
101 | */
102 | private function read_file(string $filename): string|\Exception|null
103 | {
104 | if (!file_exists($filename)) {
105 | $filename = !str_starts_with($filename, '/')
106 | ? "/{$filename}"
107 | : $filename;
108 |
109 | if (!file_exists($filename)) {
110 | throw new \Exception("File Name or Directory ($filename) Does Not Exist!");
111 | }
112 | }
113 |
114 | return file_get_contents($filename);
115 | }
116 |
117 | /**
118 | * Single Lined Markdown Converter
119 | *
120 | * @return ?string
121 | */
122 | private function compileSingleLinedMarkdown(string $markdown): ?string
123 | {
124 | $instance = new ParseMarkdown(
125 | $this->sanitize
126 | );
127 |
128 | return $instance->line($markdown);
129 | }
130 |
131 | /**
132 | * Multi-Lined Markdown Converter
133 | *
134 | * @return ?string
135 | */
136 | private function compileMultiLinedMarkdown(string $markdown): ?string
137 | {
138 | $instance = new ParseMarkdown(
139 | $this->sanitize
140 | );
141 |
142 | return $instance->markdown_text($markdown);
143 | }
144 |
145 | /**
146 | * Check if File Name is Valid
147 | */
148 | private function validateFileName(string $name): \InvalidArgumentException|bool
149 | {
150 | $validateType = preg_match('/(^\s+)/', $name);
151 |
152 | # check if file name is valid and acceptable
153 | if ($validateType) {
154 | throw new \InvalidArgumentException('File Name Must Be A Valid String!');
155 | }
156 |
157 | return true;
158 | }
159 |
160 | /**
161 | * Add html extension to file name
162 | *
163 | * @param string $file_name replace default output filename
164 | *
165 | * @return ?string
166 | */
167 | private function addHtmlExtension(string $file_name): ?string
168 | {
169 | return !str_ends_with($file_name, '.html')
170 | ? "{$file_name}.html"
171 | : $file_name;
172 | }
173 |
174 | /**
175 | * Compile Markdown to Raw HTML Output
176 | *
177 | * @throws \LogicException
178 | */
179 | public function toHtml(): ?string
180 | {
181 | if (!isset($this->contents) || count($this->contents) == 0) {
182 | throw new \LogicException(
183 | message: 'Set a Markdown Content or File Before Conversion!'
184 | );
185 | }
186 |
187 | // store all compiled html contents here
188 | $html_contents = [];
189 |
190 | foreach ($this->contents as $key => $single_content) {
191 | $html_contents[] = match (array_key_first($single_content)) {
192 | 'inline' => $this->compileSingleLinedMarkdown($single_content['inline']),
193 | 'file' => $this->compileMultiLinedMarkdown($this->read_file($single_content['file'])),
194 | default => $this->compileMultiLinedMarkdown($single_content['multi-line'])
195 | };
196 | };
197 |
198 | return implode("\n\r", $html_contents);
199 | }
200 |
201 |
202 | /**
203 | * Compile Markdown Contents to Html File
204 | *
205 | * @param string $file_name: rename compiled html file
206 | *
207 | * @return bool|\LogicException
208 | */
209 | public function toHtmlFile(string $file_name = 'compiledmarkdown.html'): \LogicException|bool
210 | {
211 | // validate file name
212 | $this->validateFileName($file_name);
213 |
214 | // check if compilation directories are set
215 | if (!isset($this->compileDir) || count($this->compileDir) == 0) {
216 | throw new \LogicException('Ensure To Set A Storage Directory For Your Compiled HTML File!');
217 | }
218 |
219 | $html_contents = [];
220 |
221 | if (isset($this->contents) && count($this->contents) > 0) {
222 | foreach ($this->contents as $key => $single_content) {
223 | $html_contents[] = match (array_key_first($single_content)) {
224 | 'inline' => $this->compileSingleLinedMarkdown($single_content['inline']),
225 | 'file' => $this->compileMultiLinedMarkdown($this->read_file($single_content['file'])),
226 | default => $this->compileMultiLinedMarkdown($single_content['multi-line'])
227 | };
228 | };
229 |
230 | # add extension to filename
231 | $file_name = $this->addHtmlExtension($file_name);
232 |
233 | // Compile The Markdown Contents to Single pr Multiple Directories
234 | return $this->saveCompiledMarkdownFiles(
235 | compileDirs: $this->compileDir,
236 | file_name: $file_name,
237 | contents: $html_contents
238 | );
239 | }
240 |
241 | throw new \LogicException('Set A Markdown File or Content to Compile!');
242 | }
243 |
244 | private function saveCompiledMarkdownFiles(array $compileDirs, string $file_name, array $contents): bool
245 | {
246 | if (count($compileDirs) > 0) {
247 | foreach ($compileDirs as $single_directory) {
248 | if (!is_dir($single_directory)) {
249 | throw new \RuntimeException("Failed To Locate ('{$single_directory}') Directory!");
250 | }
251 |
252 | # write md to html file
253 | if ($create_file = fopen("{$single_directory}{$file_name}", 'w+')) {
254 | fwrite($create_file, implode("\n\r", $contents));
255 | fclose($create_file);
256 | continue;
257 | }
258 | }
259 | return true;
260 | }
261 | return false;
262 | }
263 | }
264 |
--------------------------------------------------------------------------------
/tests/CompilationTest.php:
--------------------------------------------------------------------------------
1 | setFile(file_name: __DIR__ . '/files/hello.md')
21 | ->toHtml();
22 |
23 | $this->assertSame(expected: 'hello 1
', actual: $markdown);
24 | }
25 |
26 |
27 | /**
28 | * Test Markdown to Html Conversion
29 | *
30 | * @return void
31 | */
32 | public function testMdtoHtml(): void
33 | {
34 | $markdown = Markdown::new()
35 | ->setInlineContent('_This is an inline markdown content_')
36 | ->toHtml(); // hello 1
37 |
38 | $this->assertSame(expected: 'This is an inline markdown content', actual: $markdown);
39 | }
40 |
41 |
42 | /**
43 | * Test Markdown File to Html File Conversion
44 | *
45 | * @return void
46 | */
47 | public function testMdFiletoHtmlFile(): void
48 | {
49 | $markdown = Markdown::new()
50 | ->setFile(file_name: __DIR__ . '/files/hello-2.md')
51 | ->setCompileDir(directory: './pages/')
52 | ->toHtmlFile(file_name: 'hello-2.html'); // hello 2
53 |
54 | $this->assertIsBool(actual: $markdown);
55 | $this->assertTrue(condition: $markdown);
56 | }
57 |
58 |
59 | /**
60 | * Test Markdown Content to Html File Conversion
61 | *
62 | * @return void
63 | */
64 | public function testMdContentToHtmlFile(): void
65 | {
66 | $markdown = Markdown::new()
67 | ->setContent(content: '### hello 3')
68 | ->setCompileDir(directory: './pages/')
69 | ->toHtmlFile(file_name: 'hello-3.html'); // hello 3
70 |
71 | $this->assertIsBool(actual: $markdown);
72 |
73 | $this->assertTrue(condition: $markdown);
74 | }
75 |
76 |
77 | /**
78 | * Test Markdown Compilation
79 | *
80 | * @return void
81 | */
82 | public function testMarkdownCompilation(): void
83 | {
84 | $markdown = Markdown::new()
85 | ->setContent('# Title')
86 | ->setContent('# Sub-Title')
87 | ->setInlineContent('_first word with_')
88 | ->setInlineContent('[A LINK](https://github.com/fastvolt)')
89 | ->toHtml();
90 |
91 | $this->assertIsString($markdown);
92 | }
93 |
94 | /**
95 | * Test Markdown Compilation
96 | *
97 | * @return void
98 | */
99 | public function testMarkdownAdvancedCompilation(): void
100 | {
101 | $markdown = Markdown::new(sanitize: true)
102 | ->setFile(__DIR__ . '/files/heading.md')
103 | ->setInlineContent('_My name is **vincent**, the co-author of this blog_')
104 | ->setContent('Kindly follow me on my github page via: [@vincent](https://github.com/oladoyinbov).')
105 | ->setContent('Here are the lists of my projects:')
106 | ->setContent('
107 | - Dragon CMS
108 | - Fastvolt Framework.
109 | + Fastvolt Router
110 | + Markdown Parser.
111 | ')
112 | ->setFile(__DIR__ . '/files/footer.md');
113 |
114 |
115 | // set compilation directory
116 | $markdown->setCompileDir('./pages/');
117 |
118 | // set second compilation directory (OPTIONAL)
119 | $markdown->setCompileDir('./pages/backup/');
120 |
121 | // Compile The Markdown with File Name 'homepage'
122 | $result = $markdown->toHtmlFile(file_name: 'homepage');
123 |
124 | $this->assertIsBool($result);
125 |
126 | $this->assertTrue($result === true);
127 | }
128 | }
--------------------------------------------------------------------------------
/tests/MarkupTest.php:
--------------------------------------------------------------------------------
1 |
13 | *
14 | * @return void
15 | */
16 | public function testHeading1(): void
17 | {
18 | $markdown = Markdown::new()
19 | ->setContent('# hello world')
20 | ->toHtml(); // hello 1
21 |
22 | $this->assertSame('hello world
', $markdown);
23 | }
24 |
25 | /**
26 | * Heading Test 2:
27 | *
28 | * @return void
29 | */
30 | public function testHeading2(): void
31 | {
32 | $markdown = Markdown::new()
33 | ->setContent('## hello world')
34 | ->toHtml(); // hello 1
35 |
36 | $this->assertSame('hello world
', $markdown);
37 | }
38 |
39 | /**
40 | * Heading Test 3:
41 | *
42 | * @return void
43 | */
44 | public function testHeading3(): void
45 | {
46 | $markdown = Markdown::new()
47 | ->setContent('### hello world')
48 | ->toHtml(); // hello 1
49 |
50 | $this->assertSame('hello world
', $markdown);
51 | }
52 |
53 | /**
54 | * Heading Test 4:
55 | *
56 | * @return void
57 | */
58 | public function testHeading4(): void
59 | {
60 | $markdown = Markdown::new()
61 | ->setContent('#### hello world')
62 | ->toHtml(); // hello 1
63 |
64 | $this->assertSame('hello world
', $markdown);
65 | }
66 |
67 | /**
68 | * Test Italic 1:
69 | *
70 | * @return void
71 | */
72 | public function testItalic(): void
73 | {
74 | $markdown = Markdown::new()
75 | ->setInlineContent('*hello world*')
76 | ->toHtml(); // hello 1
77 |
78 | $this->assertSame('hello world', $markdown);
79 | }
80 |
81 | /**
82 | * Test Link 1:
83 | *
84 | * @return void
85 | */
86 | public function testHyperLink(): void
87 | {
88 | $markdown = Markdown::new()
89 | ->setInlineContent('[A LINK](https://github.com/fastvolt)')
90 | ->toHtml(); // hello 1
91 |
92 | $this->assertSame('A LINK', $markdown);
93 | }
94 |
95 |
96 | /**
97 | * Test Inline Markdown Compilation
98 | *
99 | * @return void
100 | */
101 | public function testInlineMarkdownCompilation(): void
102 | {
103 | $markdown = Markdown::new()
104 | ->setInlineContent('**hello world with _emphasis_**')
105 | ->toHtml();
106 |
107 | $this->assertSame('hello world with emphasis', $markdown);
108 | }
109 |
110 | /**
111 | * Test Multi-lined Markdown Compilation
112 | *
113 | * @return void
114 | */
115 | public function testMultiLinedMarkdownCompilation(): void
116 | {
117 | $markdown = Markdown::new(false)
118 | ->setInlineContent('> first world')
119 | ->setInlineContent('__second word__')
120 | ->toHtml();
121 |
122 | $this->assertNotNull($markdown);
123 | $this->assertIsString($markdown);
124 | }
125 |
126 | /**
127 | * Test Markdown Compilation With Sanitization Off
128 | *
129 | * @return void
130 | */
131 | public function testMarkdownSanitization(): void
132 | {
133 | $markdown = Markdown::new(sanitize: true)
134 | ->setInlineContent('
first word
')
135 | ->toHtml();
136 |
137 | $this->assertSame('<p>first word</p>', $markdown);
138 | }
139 | }
--------------------------------------------------------------------------------
/tests/files/footer.md:
--------------------------------------------------------------------------------
1 | ### Thanks for Visiting My BlogPage
--------------------------------------------------------------------------------
/tests/files/heading.md:
--------------------------------------------------------------------------------
1 | # Blog Title
2 | ### Here is the Blog Sub-title
--------------------------------------------------------------------------------
/tests/files/hello-2.md:
--------------------------------------------------------------------------------
1 | ## hello 99
--------------------------------------------------------------------------------
/tests/files/hello.md:
--------------------------------------------------------------------------------
1 | # hello 1
--------------------------------------------------------------------------------
/tests/pages/backup/homepage.html:
--------------------------------------------------------------------------------
1 | Blog Title
2 | Here is the Blog Sub-title
3 |
My name is vincent, the co-author of this blog
4 |
Kindly follow me on my github page via: @vincent.
5 |
Here are the lists of my projects:
6 |
7 | - Dragon CMS
8 | - Fastvolt Framework.
9 |
10 | - Fastvolt Router
11 | - Markdown Parser.
12 |
13 |
14 |
Thanks for Visiting My BlogPage
--------------------------------------------------------------------------------
/tests/pages/compiledmarkdown.html:
--------------------------------------------------------------------------------
1 | hello
--------------------------------------------------------------------------------
/tests/pages/hello-2.html:
--------------------------------------------------------------------------------
1 | hello 2
--------------------------------------------------------------------------------
/tests/pages/hello-3.html:
--------------------------------------------------------------------------------
1 | hello 3
--------------------------------------------------------------------------------
/tests/pages/homepage.html:
--------------------------------------------------------------------------------
1 | Blog Title
2 | Here is the Blog Sub-title
3 |
My name is vincent, the co-author of this blog
4 |
Kindly follow me on my github page via: @vincent.
5 |
Here are the lists of my projects:
6 |
7 | - Dragon CMS
8 | - Fastvolt Framework.
9 |
10 | - Fastvolt Router
11 | - Markdown Parser.
12 |
13 |
14 |
Thanks for Visiting My BlogPage
--------------------------------------------------------------------------------