markdown-plaintext 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/dist/index.cjs +564 -0
- package/dist/index.d.cts +45 -0
- package/dist/index.d.mts +45 -0
- package/dist/index.mjs +561 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tom Ryan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# markdown-plaintext
|
|
2
|
+
|
|
3
|
+
Turn markdown into plain text without destroying the text. Zero dependencies.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import {toText} from 'markdown-plaintext';
|
|
7
|
+
|
|
8
|
+
toText('Use `__init__` to set up:\n\n```js\nconst a = **not bold**;\n```');
|
|
9
|
+
// 'Use __init__ to set up:\n\nconst a = **not bold**;'
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## The problem
|
|
13
|
+
|
|
14
|
+
Stripping markdown looks like a job for a few regular expressions, and that is how the popular light
|
|
15
|
+
package does it. It is also why that package turns `` `__init__` `` into `init`, strips the `**` out
|
|
16
|
+
of a fenced code block, leaves `&` undecoded, drops the target of `<https://example.com>`, and
|
|
17
|
+
carries an open ReDoS advisory.
|
|
18
|
+
|
|
19
|
+
Across 34 README and documentation files from projects like Node, React, TypeScript, axios, fastify
|
|
20
|
+
and webpack — documents nobody involved here wrote — measured against what a CommonMark parser says
|
|
21
|
+
the document contains:
|
|
22
|
+
|
|
23
|
+
| package | weekly downloads | documents that lost text | fragments lost |
|
|
24
|
+
| --- | --- | --- | --- |
|
|
25
|
+
| `remove-markdown` | 782k | 21 of 34 | 621 |
|
|
26
|
+
| `markdown-to-txt` | 107k | 4 of 34 | 335 |
|
|
27
|
+
| **`markdown-plaintext`** | — | **0 of 34** | **0** |
|
|
28
|
+
|
|
29
|
+
What gets lost is not decoration. It is `import { createRoot } from 'react-dom/client'`, it is
|
|
30
|
+
`<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js">`, it is the download URLs from
|
|
31
|
+
Node's README, it is contributors' email addresses. For search indexing, embeddings or previews,
|
|
32
|
+
that is the content.
|
|
33
|
+
|
|
34
|
+
## Measured
|
|
35
|
+
|
|
36
|
+
### Correctness
|
|
37
|
+
|
|
38
|
+
The oracle is remark, a CommonMark parser: it says which literal strings a document contains, and
|
|
39
|
+
anything a converter drops that remark kept is text the caller lost. The test suite generates 400
|
|
40
|
+
documents covering every construct and holds the package to losing none of them, and the same
|
|
41
|
+
comparison runs over real documentation.
|
|
42
|
+
|
|
43
|
+
### Speed
|
|
44
|
+
|
|
45
|
+
46 real documents, 0.95 MB of markdown:
|
|
46
|
+
|
|
47
|
+
| converter | ms per pass | MB/s |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| `remove-markdown` | 16.2 | 58.5 |
|
|
50
|
+
| **`markdown-plaintext`** | **46.5** | **20.3** |
|
|
51
|
+
| `markdown-to-txt` | 79.7 | 11.9 |
|
|
52
|
+
| `remark` + `mdast-util-to-string` | 726.1 | 1.3 |
|
|
53
|
+
|
|
54
|
+
`remove-markdown` is faster because it does less — it is running a list of replacements rather than
|
|
55
|
+
reading the document. Among converters that keep the text, this is the quickest, and it is 15x faster
|
|
56
|
+
than the fully correct route through a CommonMark toolchain.
|
|
57
|
+
|
|
58
|
+
### Adversarial input
|
|
59
|
+
|
|
60
|
+
`remove-markdown` has an open ReDoS advisory and an open issue from its own maintainer titled
|
|
61
|
+
"Investigate quadratic runtime for malformed delimiter-heavy inputs". Time to convert 16,000 repeats
|
|
62
|
+
of each shape:
|
|
63
|
+
|
|
64
|
+
| input | `remove-markdown` | `markdown-to-txt` | `markdown-plaintext` |
|
|
65
|
+
| --- | --- | --- | --- |
|
|
66
|
+
| `[` repeated | 187.9 ms | 102.6 ms | **0.5 ms** |
|
|
67
|
+
| `[a]` repeated | 378.0 ms | 16.3 ms | **2.3 ms** |
|
|
68
|
+
| `[a](b` repeated | 626.8 ms | 2,187.5 ms | **1.8 ms** |
|
|
69
|
+
|
|
70
|
+
Nothing here backtracks, and once a scan proves no bracket closes beyond a point, later brackets stop
|
|
71
|
+
looking. The test suite grows each of these shapes eightfold and asserts the time grows with it
|
|
72
|
+
rather than with its square.
|
|
73
|
+
|
|
74
|
+
## Install
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm install markdown-plaintext
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Node 18 or newer. No dependencies.
|
|
81
|
+
|
|
82
|
+
## Usage
|
|
83
|
+
|
|
84
|
+
### `toText(markdown, options?)`
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
import {toText} from 'markdown-plaintext';
|
|
88
|
+
|
|
89
|
+
toText('# Title\n\nSee [the docs](https://example.com).');
|
|
90
|
+
// 'Title\n\nSee the docs'
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
| option | default | meaning |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| `links` | `'text'` | `'url'` keeps the destination instead; `'both'` writes `label (destination)` |
|
|
96
|
+
| `images` | `'alt'` | `'drop'` removes images entirely |
|
|
97
|
+
| `lists` | `'text'` | `'markers'` keeps `-` and `1.` in front of each item |
|
|
98
|
+
| `tables` | `'rows'` | each row becomes tab-separated cells; `'drop'` removes tables |
|
|
99
|
+
| `html` | `'strip'` | tags go, their text stays, `script` and `style` contents go too; `'keep'` leaves HTML alone |
|
|
100
|
+
| `frontMatter` | `'drop'` | `'keep'` treats YAML or TOML front matter as content |
|
|
101
|
+
|
|
102
|
+
### `decodeEntities(text)`
|
|
103
|
+
|
|
104
|
+
The entity decoder used internally, exported because it is useful on its own. Handles `&`,
|
|
105
|
+
`é`, `😀` and the named entities in common use; leaves unknown names as written.
|
|
106
|
+
|
|
107
|
+
## What it does with each construct
|
|
108
|
+
|
|
109
|
+
| markdown | becomes |
|
|
110
|
+
| --- | --- |
|
|
111
|
+
| fenced and indented code | its contents, **exactly as written** |
|
|
112
|
+
| code spans | their contents, exactly as written |
|
|
113
|
+
| headings | the heading text |
|
|
114
|
+
| emphasis, strong, strikethrough | the text inside |
|
|
115
|
+
| links | the label (see `links`) |
|
|
116
|
+
| images | the alt text (see `images`) |
|
|
117
|
+
| autolinks `<https://x>`, `<a@b.c>` | the target, which is what they display |
|
|
118
|
+
| lists and task lists | one item per line, markers and checkboxes removed |
|
|
119
|
+
| blockquotes | the quoted content |
|
|
120
|
+
| tables | one row per line, cells separated by tabs |
|
|
121
|
+
| thematic breaks | nothing |
|
|
122
|
+
| HTML | tags removed, text kept; `script` and `style` contents dropped |
|
|
123
|
+
| entities and escapes | resolved |
|
|
124
|
+
| front matter | nothing |
|
|
125
|
+
| link and footnote definitions | nothing |
|
|
126
|
+
|
|
127
|
+
Blocks are separated by a blank line. Text is never reflowed.
|
|
128
|
+
|
|
129
|
+
## What it is not
|
|
130
|
+
|
|
131
|
+
A CommonMark parser. It is a text extractor, and the table above is the whole contract. It reads
|
|
132
|
+
block structure first — so a code fence is known to be a code fence before anything inside it is
|
|
133
|
+
touched — and removes inline markup only where it is markup. Where the two disagree on something the
|
|
134
|
+
table does not cover, CommonMark is right and this is a simplification.
|
|
135
|
+
|
|
136
|
+
In particular: it does not render, it produces no HTML, it does not preserve exact spacing between
|
|
137
|
+
blocks, and it does not attempt list renumbering or nested-list indentation.
|
|
138
|
+
|
|
139
|
+
## Replacing `remove-markdown`
|
|
140
|
+
|
|
141
|
+
One thing to check before swapping. If your pipeline strips code *after* converting, like this:
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
let content = removeMd(raw);
|
|
145
|
+
content = content.replace(/```[\s\S]*?```/g, ''); // no longer matches anything
|
|
146
|
+
content = content.replace(/`[^`]+`/g, '');
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
move the code-stripping *before* the conversion:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
const withoutCode = raw
|
|
153
|
+
.replace(/^ {0,3}(`{3,}|~{3,})[^\n]*\n[\s\S]*?^ {0,3}\1[^\n]*$/gm, '')
|
|
154
|
+
.replace(/`[^`\n]+`/g, '');
|
|
155
|
+
const content = toText(withoutCode);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Every converter removes fence markers, so a regex looking for ` ``` ` after conversion has nothing
|
|
159
|
+
to match. `remove-markdown` appears to get away with it only because it mangles the code into
|
|
160
|
+
something unrecognisable — and that mangled code still reaches your index. Because this package keeps
|
|
161
|
+
code intact, leaving the ordering alone means *all* of it reaches your index instead.
|
|
162
|
+
|
|
163
|
+
Measured on one real consumer's search-index pipeline over 18 documentation files, counting fragments
|
|
164
|
+
that still look like source code: 183 as shipped, 504 with the converter swapped and the ordering
|
|
165
|
+
left alone, 71 with the code stripped first.
|
|
166
|
+
|
|
167
|
+
## Alternatives
|
|
168
|
+
|
|
169
|
+
- [`remove-markdown`](https://www.npmjs.com/package/remove-markdown) — regex-based, zero
|
|
170
|
+
dependencies, much faster, and loses text in two thirds of the documents measured above.
|
|
171
|
+
- [`markdown-to-txt`](https://www.npmjs.com/package/markdown-to-txt) — wraps `marked`; more accurate
|
|
172
|
+
than the regex approach. Its highest-voted open issue is that it has been uninstallable without an
|
|
173
|
+
undeclared `lodash` since 2023.
|
|
174
|
+
- [`strip-markdown`](https://www.npmjs.com/package/strip-markdown) and
|
|
175
|
+
[`mdast-util-to-string`](https://www.npmjs.com/package/mdast-util-to-string) — the correct route,
|
|
176
|
+
through the unified toolchain. Use them if you already have an mdast tree, or if you need
|
|
177
|
+
CommonMark exactness more than you need one small dependency.
|
|
178
|
+
|
|
179
|
+
## Licence
|
|
180
|
+
|
|
181
|
+
MIT
|