Skip to content

Commit 9406db4

Browse files
Fix normalizeString perf, add tests for FootnotesTest/ReferencesTest, improve README
Agent-Logs-Url: https://github.com/BenjaminHoegh/ParsedownExtended/sessions/8a9c81ad-69ac-4d8f-a5ac-ebf2d50b0a8d Co-authored-by: BenjaminHoegh <22152591+BenjaminHoegh@users.noreply.github.com>
1 parent 1be65e4 commit 9406db4

File tree

4 files changed

+229
-9
lines changed

4 files changed

+229
-9
lines changed

README.md

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,15 @@ Table of contents
3131
- [Introduction](#introduction)
3232
- [Features](#features)
3333
- [Getting started](#getting-started)
34+
- [Configuration](#configuration)
3435
- [Bugs and feature requests](#bugs-and-feature-requests)
3536
- [Contributing](#contributing)
3637
- [Community](#community)
3738
- [Copyright and license](#copyright-and-license)
3839

3940
## Introduction
4041

41-
ParsedownExtended is an extention for Parsedown, offering additional features and functionalities. It is designed to provide an easy-to-use Markdown parsing solution while extending the capabilities of the base Parsedown library.
42+
ParsedownExtended is an extension for Parsedown, offering additional features and functionalities. It is designed to provide an easy-to-use Markdown parsing solution while extending the capabilities of the base Parsedown library.
4243

4344
Stand alone versions of the extended features are also available as separate libraries:
4445
- [Parsedown Toc](https://github.com/BenjaminHoegh/ParsedownToc)
@@ -59,7 +60,7 @@ ParsedownExtended includes a variety of features to enhance your Markdown parsin
5960
- **Diagrams Syntax Support:** Recognizes diagram syntax for integration with libraries like mermaid.js and chart.js.
6061
- **LaTeX Syntax Support:** Detects LaTeX syntax, suitable for mathematical expressions, to be rendered with libraries like KaTeX.js.
6162
- **Predefined Abbreviations:** Define and use abbreviations easily.
62-
- **GFM Alerts:** Create alerts using GitHub Flavored Markdown Alert syntax there can be customized into your own language, or even create your own.
63+
- **GFM Alerts:** Create alerts using GitHub Flavored Markdown Alert syntax that can be customized into your own language, or even create your own.
6364
- **Customizable Options:** Extensive options for customizing each Markdown element.
6465
- **Additional Features:** ParsedownExtended continuously evolves, adding more features over time.
6566

@@ -103,6 +104,153 @@ echo $ParsedownExtended->text('Hello _Parsedown_!'); # prints: <p>Hello <em>Pars
103104
echo $ParsedownExtended->line('Hello _Parsedown_!'); # prints: Hello <em>Parsedown</em>!
104105
```
105106

107+
## Configuration
108+
109+
ParsedownExtended has an extensive configuration system. You can pass options to the constructor or change them at runtime using the `config()` API.
110+
111+
### Constructor overrides
112+
113+
```php
114+
$ParsedownExtended = new ParsedownExtended([
115+
'math' => ['enabled' => true],
116+
'smartypants' => ['enabled' => true],
117+
'emojis' => false,
118+
]);
119+
```
120+
121+
### Runtime configuration
122+
123+
Use dot-notation to target nested options:
124+
125+
```php
126+
$ParsedownExtended->config()->set('math.enabled', true);
127+
$ParsedownExtended->config()->set('headings.auto_anchors.lowercase', true);
128+
```
129+
130+
You can also chain `set()` calls:
131+
132+
```php
133+
$ParsedownExtended->config()
134+
->set('emphasis.superscript', true)
135+
->set('emphasis.subscript', true);
136+
```
137+
138+
### Feature examples
139+
140+
#### Math / LaTeX
141+
142+
Math is disabled by default and must be explicitly enabled:
143+
144+
```php
145+
$ParsedownExtended->config()->set('math', true);
146+
147+
// Inline: $E = mc^2$
148+
// Block:
149+
// $$
150+
// \sum_{i=1}^{n} i = \frac{n(n+1)}{2}
151+
// $$
152+
```
153+
154+
#### Emojis
155+
156+
```php
157+
// Enabled by default. Disable with:
158+
$ParsedownExtended->config()->set('emojis', false);
159+
160+
// Usage in markdown:
161+
// :thumbs_up: :smile: :heart:
162+
```
163+
164+
#### Smartypants
165+
166+
```php
167+
$ParsedownExtended->config()->set('smartypants', true);
168+
169+
// "Hello" becomes "Hello" (curly quotes)
170+
// -- becomes – (en-dash)
171+
// --- becomes — (em-dash)
172+
// ... becomes … (ellipsis)
173+
```
174+
175+
#### Heading auto-anchors
176+
177+
```php
178+
$ParsedownExtended->config()->set('headings.auto_anchors', true);
179+
$ParsedownExtended->config()->set('headings.auto_anchors.delimiter', '-');
180+
$ParsedownExtended->config()->set('headings.auto_anchors.lowercase', true);
181+
$ParsedownExtended->config()->set('headings.auto_anchors.transliterate', true);
182+
183+
// Custom anchor via markdown: # My Heading {#my-custom-id}
184+
185+
// Custom anchor generation via callback:
186+
$ParsedownExtended->setCreateAnchorIDCallback(function (string $text): string {
187+
return strtolower(preg_replace('/[^a-z0-9]+/i', '-', $text));
188+
});
189+
```
190+
191+
#### Table of Contents
192+
193+
```php
194+
// Place [TOC] anywhere in your markdown to insert the table of contents:
195+
$html = $ParsedownExtended->text("# Section 1\n\n[TOC]\n\n## Sub-section");
196+
197+
// Retrieve the TOC separately:
198+
$toc = $ParsedownExtended->contentsList(); // HTML string
199+
$json = $ParsedownExtended->contentsList('json'); // JSON string
200+
```
201+
202+
#### GFM Alerts
203+
204+
```php
205+
// Enabled by default. Customise types and CSS class:
206+
$ParsedownExtended->config()->set('alerts.types', ['note', 'tip', 'warning']);
207+
$ParsedownExtended->config()->set('alerts.class', 'markdown-alert');
208+
209+
// Usage in markdown:
210+
// > [!NOTE]
211+
// > This is a note.
212+
```
213+
214+
#### External links
215+
216+
```php
217+
$ParsedownExtended->config()->set('links.external_links.nofollow', true);
218+
$ParsedownExtended->config()->set('links.external_links.noopener', true);
219+
$ParsedownExtended->config()->set('links.external_links.noreferrer', true);
220+
$ParsedownExtended->config()->set('links.external_links.open_in_new_window', true);
221+
222+
// Mark specific hosts as internal so they are not treated as external:
223+
$ParsedownExtended->config()->set('links.external_links.internal_hosts', ['mysite.com']);
224+
```
225+
226+
#### Predefined abbreviations
227+
228+
```php
229+
$ParsedownExtended->config()->set('abbreviations.predefined', [
230+
'HTML' => 'HyperText Markup Language',
231+
'CSS' => 'Cascading Style Sheets',
232+
]);
233+
```
234+
235+
#### Superscript and subscript
236+
237+
Both are disabled by default:
238+
239+
```php
240+
$ParsedownExtended->config()->set('emphasis.superscript', true); // X^2^
241+
$ParsedownExtended->config()->set('emphasis.subscript', true); // H~2~O
242+
```
243+
244+
#### Diagrams
245+
246+
```php
247+
$ParsedownExtended->config()->set('diagrams', true);
248+
// Supports mermaid.js and chart.js fenced code blocks:
249+
// ```mermaid
250+
// graph TD; A-->B;
251+
// ```
252+
```
253+
106254
## Bugs and feature requests
107255

108256
Have a bug or a feature request? Please first read the [issue guidelines](https://github.com/BenjaminHoegh/ParsedownExtended/blob/main/.github/CONTRIBUTING.md#using-the-issue-tracker) and search for existing and closed issues. If your problem or idea is not addressed yet, [please open a new issue](https://github.com/BenjaminHoegh/ParsedownExtended/issues/new/choose).

src/ParsedownExtended.php

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2752,9 +2752,10 @@ protected function createAnchorID(string $text): ?string
27522752
/**
27532753
* Normalizes the given string to UTF-8 encoding.
27542754
*
2755-
* This function ensures that the given text is properly encoded to UTF-8, using
2756-
* `mb_convert_encoding` if available. If `mbstring` is not available, it returns
2757-
* the raw string as there is no equivalent alternative.
2755+
* This function ensures that the given text is properly encoded to UTF-8. It detects
2756+
* the source encoding from a small set of common encodings and converts only when
2757+
* necessary. If `mbstring` is not available, it returns the raw string as there is
2758+
* no equivalent alternative.
27582759
*
27592760
* @since 1.2.0
27602761
*
@@ -2768,11 +2769,16 @@ protected function normalizeString(string $text)
27682769
$mbstringLoaded = extension_loaded('mbstring');
27692770
}
27702771

2771-
if ($mbstringLoaded) {
2772-
return mb_convert_encoding($text, 'UTF-8', mb_list_encodings());
2773-
} else {
2774-
return $text; // Return raw text as there is no good alternative for mb_convert_encoding
2772+
if (!$mbstringLoaded) {
2773+
return $text;
2774+
}
2775+
2776+
$encoding = mb_detect_encoding($text, ['UTF-8', 'ISO-8859-1', 'Windows-1252'], true);
2777+
if ($encoding === false || $encoding === 'UTF-8') {
2778+
return $text;
27752779
}
2780+
2781+
return mb_convert_encoding($text, 'UTF-8', $encoding);
27762782
}
27772783

27782784
/**

tests/FootnotesTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,39 @@ protected function tearDown(): void
1717
{
1818
unset($this->parsedownExtended);
1919
}
20+
21+
public function testFootnotesEnabled()
22+
{
23+
$this->parsedownExtended->config()->set('footnotes', true);
24+
25+
$markdown = "Text with a footnote.[^1]\n\n[^1]: The footnote content.";
26+
$html = $this->parsedownExtended->text($markdown);
27+
28+
$this->assertStringContainsString('<sup', $html);
29+
$this->assertStringContainsString('The footnote content.', $html);
30+
}
31+
32+
public function testFootnotesDisabled()
33+
{
34+
$this->parsedownExtended->config()->set('footnotes', false);
35+
36+
$markdown = "Text with a footnote.[^1]\n\n[^1]: The footnote content.";
37+
$html = $this->parsedownExtended->text($markdown);
38+
39+
// When footnotes are disabled there should be no superscript footnote links
40+
$this->assertStringNotContainsString('<sup', $html);
41+
// The footnote definition is not processed as a footnote, so no back-reference link appears
42+
$this->assertStringNotContainsString('href="#fn:', $html);
43+
}
44+
45+
public function testMultipleFootnotes()
46+
{
47+
$this->parsedownExtended->config()->set('footnotes', true);
48+
49+
$markdown = "First[^1] and second[^2].\n\n[^1]: First footnote.\n\n[^2]: Second footnote.";
50+
$html = $this->parsedownExtended->text($markdown);
51+
52+
$this->assertStringContainsString('First footnote.', $html);
53+
$this->assertStringContainsString('Second footnote.', $html);
54+
}
2055
}

tests/ReferencesTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,35 @@ protected function tearDown(): void
1717
{
1818
unset($this->parsedownExtended);
1919
}
20+
21+
public function testReferencesEnabled()
22+
{
23+
$this->parsedownExtended->config()->set('references', true);
24+
25+
$markdown = "[link text][ref]\n\n[ref]: https://example.com";
26+
$html = $this->parsedownExtended->text($markdown);
27+
28+
$this->assertStringContainsString('<a href="https://example.com">link text</a>', $html);
29+
}
30+
31+
public function testReferencesDisabled()
32+
{
33+
$this->parsedownExtended->config()->set('references', false);
34+
35+
$markdown = "[link text][ref]\n\n[ref]: https://example.com";
36+
$html = $this->parsedownExtended->text($markdown);
37+
38+
$this->assertStringNotContainsString('<a href="https://example.com">link text</a>', $html);
39+
}
40+
41+
public function testReferenceWithTitle()
42+
{
43+
$this->parsedownExtended->config()->set('references', true);
44+
45+
$markdown = "[link text][ref]\n\n[ref]: https://example.com \"Example Title\"";
46+
$html = $this->parsedownExtended->text($markdown);
47+
48+
$this->assertStringContainsString('href="https://example.com"', $html);
49+
$this->assertStringContainsString('title="Example Title"', $html);
50+
}
2051
}

0 commit comments

Comments
 (0)