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 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 `&amp;` 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 `&amp;`,
105
+ `&#233;`, `&#x1F600;` 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