page-markdown 0.1.4 → 0.1.5
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/CHANGELOG.md +186 -124
- package/README.md +105 -13
- package/dist/types/clean.d.ts +23 -0
- package/dist/types/convert.d.ts +19 -0
- package/dist/types/extract.d.ts +37 -0
- package/dist/types/facts.d.ts +14 -0
- package/dist/types/hidden.d.ts +14 -0
- package/dist/types/index.d.ts +98 -0
- package/dist/types/metadata.d.ts +18 -0
- package/dist/types/refine.d.ts +31 -0
- package/dist/types/sanitize.d.ts +26 -0
- package/dist/types/standardize.d.ts +15 -0
- package/dist/types/types.d.ts +154 -0
- package/package.json +17 -5
- package/src/clean.js +21 -9
- package/src/convert.js +78 -26
- package/src/extract.js +76 -70
- package/src/facts.js +99 -96
- package/src/hidden.js +46 -45
- package/src/index.js +21 -5
- package/src/metadata.js +79 -78
- package/src/refine.js +109 -104
- package/src/sanitize.js +196 -193
- package/src/standardize.js +41 -15
- package/src/types.js +118 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,124 +1,186 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 0.1.
|
|
4
|
-
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
- **
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
- **
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.5 — 2026-11-04
|
|
4
|
+
|
|
5
|
+
Content-fidelity fixes, each with a new fixture run in Node and in the browser:
|
|
6
|
+
|
|
7
|
+
- **Code blocks keep their lines.** Lines marked up with `<br>` or block
|
|
8
|
+
elements (Docusaurus and similar highlighters) came out joined on one line.
|
|
9
|
+
- **Copy buttons stay out of code.** A `<button>` inside `<pre>` leaked its
|
|
10
|
+
label into the code and lost the language. Whitespace around the `<code>`
|
|
11
|
+
no longer loses the fence.
|
|
12
|
+
- **Text beside a heading is kept.** A `<div>` holding a heading and bare text
|
|
13
|
+
was replaced by the heading, deleting the text.
|
|
14
|
+
- **Row spans keep columns aligned.** Later cells shifted left. A spanned cell
|
|
15
|
+
now repeats in each row of a GFM table, and block-rendered tables label cells
|
|
16
|
+
by their true column.
|
|
17
|
+
- **Large tables no longer freeze the page.** Table checks ran once per cell,
|
|
18
|
+
so time grew with the square of the size: 500 rows took about 8 seconds in
|
|
19
|
+
Chrome.
|
|
20
|
+
- **Modes leave more code alone.** Fences in list items and blockquotes, and
|
|
21
|
+
inline code, were edited by `no_links`, `text_only` and blank-line cleanup.
|
|
22
|
+
- **A lead paragraph is no longer doubled** when it starts with a link or holds
|
|
23
|
+
an entity such as `<`.
|
|
24
|
+
- **Script links are caught however they are written.** `javascript:` with an
|
|
25
|
+
embedded newline, tab or control character got through, as did `vbscript:`
|
|
26
|
+
and `data:`. These links keep only their text. Images with such a source are
|
|
27
|
+
dropped.
|
|
28
|
+
- **`<base href>` is respected** for links and images.
|
|
29
|
+
- **A linked image with empty alt is kept.**
|
|
30
|
+
- **An ad marker on `<body>` no longer empties the page.** Sites put page-wide ad
|
|
31
|
+
settings there (`data-ad-unit` on heise.de), and the ad filter removed the whole
|
|
32
|
+
body. `<html>` and `<body>` are never treated as ads; ads inside them still are.
|
|
33
|
+
- `stats.markdownChars` now matches the returned Markdown.
|
|
34
|
+
- The browser bundle grows to about 67.2 KB minified; the size ceiling is now
|
|
35
|
+
68 KB.
|
|
36
|
+
|
|
37
|
+
- **TypeScript types.** Declarations for `pageToMarkdown`, its options, the full
|
|
38
|
+
result and every stage export are generated from the JSDoc in `src` into
|
|
39
|
+
`dist/types`, through a `types` condition in `exports`. The result is
|
|
40
|
+
documented in a README table. `npm run typecheck` is part of `npm run ci`: it
|
|
41
|
+
checks `src` against its JSDoc, checks a consumer file with `tsc`, and checks
|
|
42
|
+
the declarations against real output from every fixture, in both directions.
|
|
43
|
+
Only comments change in the existing modules; the bundle is the same size.
|
|
44
|
+
- **Bench.** `npm run bench` adds a Readability + Turndown baseline, word recall
|
|
45
|
+
and precision, and 11 held-out pages whose probes were committed before any
|
|
46
|
+
engine ran on them. The headline changes from 32/32 against 11/32 to 84/100
|
|
47
|
+
against 74/100 and 61/100 on the held-out pages; see `bench/RESULTS.md` for the
|
|
48
|
+
failures. `bench/fetch-pages.mjs` now decodes by declared charset and keeps the
|
|
49
|
+
last good copy when a fetch fails. `npm run bench:write` updates
|
|
50
|
+
`bench/RESULTS.md`; `npm run bench` only prints.
|
|
51
|
+
- **Licence summary.** The README now says in plain words who can use the package
|
|
52
|
+
free and who needs a commercial licence, and how to ask for one. The licence
|
|
53
|
+
itself is unchanged.
|
|
54
|
+
- **Browser run.** `npm run test:browser` runs the golden fixtures in
|
|
55
|
+
headless Chrome or Edge, with the browser's own DOM, and checks what Node
|
|
56
|
+
can't show:
|
|
57
|
+
- the live document is left unmodified;
|
|
58
|
+
- real open shadow roots are captured, nested roots and slotted children
|
|
59
|
+
included;
|
|
60
|
+
- closed shadow roots stay out.
|
|
61
|
+
|
|
62
|
+
It is part of `npm run ci` and needs no new dependencies. The package's output
|
|
63
|
+
in Chrome and Edge matches its Node output.
|
|
64
|
+
|
|
65
|
+
## 0.1.4 — 2026-11-02
|
|
66
|
+
|
|
67
|
+
- **text-sanctifier 1.3.8** (was 1.3.3). `removeHiddenText` now also removes
|
|
68
|
+
runs of reserved tag-plane code points (U+E0080–U+E00FF, U+E01F0–U+E0FFF),
|
|
69
|
+
which 1.3.3 did not find, and reports them as `hidden_reserved_tag_plane`.
|
|
70
|
+
The browser bundle grows to about 65.6 KB minified.
|
|
71
|
+
- A test checks that every run kind text-sanctifier can report has an
|
|
72
|
+
omission note, so a new kind fails `npm run ci` instead of reaching users
|
|
73
|
+
with no note.
|
|
74
|
+
- A test fails when any source, test, fixture or doc file holds a raw
|
|
75
|
+
invisible or control character. A test regex in 0.1.3 held four, written
|
|
76
|
+
by an editing tool that expanded `\u` escapes; they never shipped, since
|
|
77
|
+
tests are not in the package.
|
|
78
|
+
|
|
79
|
+
## 0.1.3 — 2026-10-30
|
|
80
|
+
|
|
81
|
+
First release on npm. `npm run ci` (contract suite and size check) is the
|
|
82
|
+
local gate before a commit; there is no cloud CI. The size ceiling is 66 KB
|
|
83
|
+
minified.
|
|
84
|
+
|
|
85
|
+
Content-fidelity fixes:
|
|
86
|
+
|
|
87
|
+
- **Title heading kept beside shell comments.** A `# comment` line in fenced
|
|
88
|
+
code no longer counts as the article's top-level heading, which had
|
|
89
|
+
suppressed the `# <headline>` line.
|
|
90
|
+
- **Every pipe in a table cell is escaped.** `a || b` escaped only its first
|
|
91
|
+
pipe, which added a column and broke the table.
|
|
92
|
+
- **Images beside a heading survive.** A heading wrapper that also holds an
|
|
93
|
+
image (or picture, video, audio) is no longer unwrapped to the bare heading.
|
|
94
|
+
- **`no_links` keeps linked images intact.** `[](href)` became
|
|
95
|
+
``, an image pointing at the link target; the image is now kept
|
|
96
|
+
as it was.
|
|
97
|
+
- **Code in tables stays fenced.** A GFM cell is one line, so a data table
|
|
98
|
+
holding `<pre>` code (Wikipedia's syntax examples) was flattened to inline
|
|
99
|
+
code, losing its line breaks and indentation. Such a table is now rendered
|
|
100
|
+
as blocks: each cell under a bold label from its column header (or its row
|
|
101
|
+
header, for infoboxes), with code as verbatim fences. Tables without code
|
|
102
|
+
are still GFM grids.
|
|
103
|
+
- Code language from MediaWiki's `mw-highlight-lang-*` class.
|
|
104
|
+
- At most one blank line beside a fenced block.
|
|
105
|
+
- Five new regression fixtures, one per fix.
|
|
106
|
+
|
|
107
|
+
Hidden text (opt-in):
|
|
108
|
+
|
|
109
|
+
- **`removeHiddenText: true`** removes runs of tag characters, variation
|
|
110
|
+
selectors and invisible operators, which people can't see but models read
|
|
111
|
+
(hidden prompt instructions). It applies to the Markdown, fenced code
|
|
112
|
+
included, and to `fullPageMarkdown` and `metadata`. The England, Scotland
|
|
113
|
+
and Wales flags and a lone emoji or text presentation selector are kept.
|
|
114
|
+
`omissions` gets `hidden_tag`, `hidden_variation_selector` and
|
|
115
|
+
`hidden_invisible_operator` entries with `count` (runs) and `chars` (code
|
|
116
|
+
points). `removeHiddenText` is exported. Default output is unchanged.
|
|
117
|
+
- New runtime dependency: text-sanctifier 1.3.3 (first-party, no
|
|
118
|
+
dependencies), for its `text-sanctifier/hidden` run finder. About 1.5 KB
|
|
119
|
+
minified in the browser bundle.
|
|
120
|
+
- New contract fixture `hidden-text.html`, written with character references
|
|
121
|
+
so it holds no raw hidden characters.
|
|
122
|
+
|
|
123
|
+
## 0.1.2 — 2026-10-23
|
|
124
|
+
|
|
125
|
+
- **Page facts (opt-in).** `pageFacts: true` appends a `## Page facts` list
|
|
126
|
+
of short label/value pairs that sit outside the extracted article: a
|
|
127
|
+
package sidebar's version, license and downloads, spec and price rows,
|
|
128
|
+
infobox rows. Readability drops these asides, which suits reading but
|
|
129
|
+
loses the answer to "which version?". Pairs come from a heading followed by
|
|
130
|
+
a short value, `<dt>`/`<dd>`, and two-cell table rows. Values already in
|
|
131
|
+
the article, sentences, link lists, lone non-data links and anything in
|
|
132
|
+
navigation, header, footer or forms are skipped. `extraction.pageFacts`
|
|
133
|
+
counts them; `collectPageFacts` is exported. Default output is unchanged.
|
|
134
|
+
- New regression fixture: a package page whose version lives only in the
|
|
135
|
+
sidebar.
|
|
136
|
+
|
|
137
|
+
## 0.1.1 — 2026-10-22
|
|
138
|
+
|
|
139
|
+
Fixes from the first in-browser comparison (MDN, Wikipedia, the Python docs,
|
|
140
|
+
GitHub):
|
|
141
|
+
|
|
142
|
+
- **Tables never come out as raw HTML.** Own GFM table rules replace the
|
|
143
|
+
plugin's:
|
|
144
|
+
- a header row is used even when the table has none;
|
|
145
|
+
- colspans are padded with empty cells;
|
|
146
|
+
- cell content is flattened to one line (code, lists, line breaks);
|
|
147
|
+
- pipes are escaped;
|
|
148
|
+
- layout and nested tables are rendered as blocks.
|
|
149
|
+
- **Dropped lead paragraph restored.** When the extractor leaves out a
|
|
150
|
+
paragraph that matches the meta description (MDN's heading-less first
|
|
151
|
+
section), it is put back first. `extraction.leadRecovered` reports it, and
|
|
152
|
+
`recoverLead: false` turns it off.
|
|
153
|
+
- **Clean title heading.** The page's own H1 (permalink sign removed) when
|
|
154
|
+
the title contains it, else `og:title` or the title, without a site-name
|
|
155
|
+
segment at either end. It is exposed as `metadata.headline`.
|
|
156
|
+
- **Smaller bundle.** Strikethrough and task-list rules are now built in, and
|
|
157
|
+
`@joplin/turndown-plugin-gfm` is no longer a dependency. The browser bundle
|
|
158
|
+
is 60.2 KB minified, down from 61.7 KB.
|
|
159
|
+
|
|
160
|
+
## 0.1.0 — 2026-10-22
|
|
161
|
+
|
|
162
|
+
First release.
|
|
163
|
+
|
|
164
|
+
- `pageToMarkdown(document, options)`: sanitize → standardize → extract →
|
|
165
|
+
convert → clean, returning Markdown, metadata, extraction details,
|
|
166
|
+
omissions, warnings and stats.
|
|
167
|
+
- **Sanitize.** Builds an inert clone and inlines open shadow DOM within node,
|
|
168
|
+
character and depth limits, with an omission notice past them. Removes
|
|
169
|
+
scripts, styles, `noscript`, meta and link tags, and ads, plus any
|
|
170
|
+
`extraStripSelectors`.
|
|
171
|
+
- **Standardize.**
|
|
172
|
+
- Turns `<pre>` into fenced code, with the language from
|
|
173
|
+
`language-`/`lang-`/`highlight-` classes or `data-lang`.
|
|
174
|
+
- Unwraps heading wrappers and removes edit and permalink widgets.
|
|
175
|
+
- Removes empty and `javascript:` links.
|
|
176
|
+
- Makes links and images absolute and resolves lazy images; drops inline
|
|
177
|
+
`data:` images.
|
|
178
|
+
- **Extract.** Mozilla Readability on a copy of the clone, falling back to the
|
|
179
|
+
full page when it finds nothing or too little. Optionally restores the title
|
|
180
|
+
heading.
|
|
181
|
+
- **Convert.** Turndown with GitHub-flavoured tables and strikethrough.
|
|
182
|
+
- **Clean.** Fence-safe. Keeps short lines and `<br>` hard breaks, removes only
|
|
183
|
+
verified single-line JSON documents, and offers `full`, `no_links` and
|
|
184
|
+
`text_only` modes.
|
|
185
|
+
- **Metadata.** Meta tags, JSON-LD (`@graph` included), canonical URL and
|
|
186
|
+
language.
|
package/README.md
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
Faithful page-to-Markdown for humans and agents. Give it a web page's DOM and
|
|
4
4
|
it returns clean Markdown plus metadata. It is written for browsers, for
|
|
5
5
|
example an extension's content script, and runs in Node with a DOM library.
|
|
6
|
-
|
|
7
|
-
was built for WATT3D's
|
|
6
|
+
It is tested in Node and in headless Chrome and Edge, but not yet in Firefox or
|
|
7
|
+
Safari (see [Known limits](#known-limits-v01)). It was built for WATT3D's
|
|
8
|
+
browser extensions.
|
|
8
9
|
|
|
9
10
|
```
|
|
10
11
|
live Document
|
|
@@ -13,7 +14,8 @@ live Document
|
|
|
13
14
|
javascript: links removed; absolute links and images; lazy images resolved
|
|
14
15
|
→ extract Mozilla Readability on a copy; full-page fallback when it finds too little;
|
|
15
16
|
a dropped lead paragraph restored; a clean title heading
|
|
16
|
-
→ convert Turndown + GitHub-flavoured tables, strikethrough and task lists; never
|
|
17
|
+
→ convert Turndown + GitHub-flavoured tables, strikethrough and task lists; tables never
|
|
18
|
+
come out as raw HTML
|
|
17
19
|
→ clean fence-safe; keeps short lines and <br> hard breaks; never cuts text
|
|
18
20
|
→ hidden text opt-in (removeHiddenText): tag-character text, variation-selector runs and
|
|
19
21
|
invisible operators removed, fenced code included; counted in omissions
|
|
@@ -28,6 +30,9 @@ The live page is only read, never modified.
|
|
|
28
30
|
npm install page-markdown
|
|
29
31
|
```
|
|
30
32
|
|
|
33
|
+
Free for personal, non-commercial academic and fully open-source use. Other
|
|
34
|
+
commercial use needs a licence: see [Licence](#licence).
|
|
35
|
+
|
|
31
36
|
## Usage
|
|
32
37
|
|
|
33
38
|
```js
|
|
@@ -65,6 +70,38 @@ The stages are also exported for advanced use: `sanitizeToInertDocument`,
|
|
|
65
70
|
`standardizeDocument`, `extractContent`, `htmlToMarkdown`, `cleanMarkdown`,
|
|
66
71
|
`applyMode`, `collectMetadata`, `collectPageFacts` and `removeHiddenText`.
|
|
67
72
|
|
|
73
|
+
### Result
|
|
74
|
+
|
|
75
|
+
`pageToMarkdown` returns:
|
|
76
|
+
|
|
77
|
+
| Field | Type | Meaning |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `version` | `string` | The package version that made this result |
|
|
80
|
+
| `markdown` | `string` | The Markdown |
|
|
81
|
+
| `fullPageMarkdown` | `string?` | The whole page as Markdown. Present only with `includeFullPage` |
|
|
82
|
+
| `metadata.url`, `canonicalUrl` | `string` | The page URL; the canonical link, else the page URL |
|
|
83
|
+
| `metadata.documentTitle`, `title` | `string` | The `<title>`; og:title or twitter:title, else the JSON-LD headline, else the `<title>` |
|
|
84
|
+
| `metadata.description`, `author`, `published`, `modified`, `siteName`, `lang` | `string` | From meta tags and JSON-LD, `""` when the page doesn't state them. `siteName` falls back to the hostname |
|
|
85
|
+
| `metadata.headline` | `string?` | The heading chosen for the capture |
|
|
86
|
+
| `metadata.extractedTitle`, `byline` | `string?` | What the extractor reported |
|
|
87
|
+
| `extraction.method` | `string` | `"readability"`, `"full_page"`, or the name a custom extractor gave |
|
|
88
|
+
| `extraction.fallbackReason` | `string?` | Why extraction fell back to the full page: `extractor_found_nothing`, `extracted_text_below_minimum` or `extractor_error: …` |
|
|
89
|
+
| `extraction.leadRecovered` | `true?` | A dropped lead paragraph was restored |
|
|
90
|
+
| `extraction.pageFacts` | `number?` | How many facts `pageFacts` appended |
|
|
91
|
+
| `omissions[]` | `{ kind, count, note, chars? }` | What was left out or removed. `kind` is `shadow_content`, `json_line` or a `hidden_*` kind; only `hidden_*` entries have `chars` |
|
|
92
|
+
| `warnings[]` | `string` | Currently the full-page fallback, in words |
|
|
93
|
+
| `stats.mode` | `string` | The output mode used |
|
|
94
|
+
| `stats.extractedHtmlChars`, `convertedChars`, `markdownChars` | `number` | Sizes at each step. `markdownChars` is measured before `removeHiddenText` and the shadow omission notice |
|
|
95
|
+
| `stats.standardization` | `object` | Counts of what standardizing changed: `lazyImagesResolved`, `inlineImagesDropped`, `scriptLinksUnwrapped`, `emptyLinksRemoved`, `preWrapped`, `headingWidgetsRemoved`, `headingsUnwrapped` |
|
|
96
|
+
|
|
97
|
+
A `?` marks a field that is absent, not empty, when it doesn't apply.
|
|
98
|
+
|
|
99
|
+
### TypeScript
|
|
100
|
+
|
|
101
|
+
Types ship with the package, generated from the JSDoc in `src`: `pageToMarkdown`,
|
|
102
|
+
the stages, and `PageMarkdownOptions`, `PageMarkdownResult` and the types they use.
|
|
103
|
+
They bring in TypeScript's `dom` lib, so a Node project needs no `lib` change.
|
|
104
|
+
|
|
68
105
|
### Recipes
|
|
69
106
|
|
|
70
107
|
```js
|
|
@@ -87,7 +124,7 @@ const { markdown: safe, omissions } = pageToMarkdown(document, { removeHiddenTex
|
|
|
87
124
|
## Quality
|
|
88
125
|
|
|
89
126
|
`npm test` runs the contract suite. It covers:
|
|
90
|
-
- golden outputs for
|
|
127
|
+
- golden outputs for 34 fixtures;
|
|
91
128
|
- regressions for known converter bugs, including dropped lead paragraphs,
|
|
92
129
|
raw HTML tables, flattened code in tables, site-suffixed titles and
|
|
93
130
|
unescaped table pipes;
|
|
@@ -95,21 +132,60 @@ const { markdown: safe, omissions } = pageToMarkdown(document, { removeHiddenTex
|
|
|
95
132
|
- the shadow DOM limits and their omission notice;
|
|
96
133
|
- leaving the live page unmodified.
|
|
97
134
|
|
|
98
|
-
`npm run
|
|
99
|
-
|
|
100
|
-
`
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
135
|
+
`npm run typecheck` checks `src` against its JSDoc with `tsc` and generates the
|
|
136
|
+
declarations in `dist/types`. It then checks those from a consumer's side
|
|
137
|
+
(`test/types/consumer.ts`) and against real output: it runs the pipeline over the
|
|
138
|
+
fixtures and fails when a result or the export list no longer matches the
|
|
139
|
+
declarations, in either direction.
|
|
140
|
+
|
|
141
|
+
`npm run test:browser` runs the same golden fixtures in headless Chrome (or
|
|
142
|
+
Edge), parsed by the browser's own DOM, with no extra dependencies. It also
|
|
143
|
+
checks what Node can't show:
|
|
144
|
+
- a live document with layout is left unmodified;
|
|
145
|
+
- real open shadow roots are captured, including nested roots and slotted
|
|
146
|
+
children in rendered order;
|
|
147
|
+
- closed shadow roots stay out of the capture.
|
|
148
|
+
|
|
149
|
+
Set `PAGE_MARKDOWN_BROWSER` to choose the browser. The built page is kept at
|
|
150
|
+
`.browser-test/index.html` for opening by hand.
|
|
151
|
+
|
|
152
|
+
`npm run bench` scores the package against two baselines: Turndown over the page
|
|
153
|
+
body, and Readability then Turndown. It runs 15 fixture checks and 117 probes on
|
|
154
|
+
16 real pages, which `node bench/fetch-pages.mjs` fetches first, and measures word
|
|
155
|
+
recall against a hand-picked reference for each page. `npm run bench` only prints;
|
|
156
|
+
`npm run bench:write` also updates [bench/RESULTS.md](bench/RESULTS.md). Currently:
|
|
157
|
+
|
|
158
|
+
| Tier | page-markdown | Readability + Turndown | Turndown only |
|
|
159
|
+
|---|---|---|---|
|
|
160
|
+
| Fixtures, written with the package | 15/15 | 7/15 | 3/15 |
|
|
161
|
+
| Five pages the fixes were tuned on | 17/17 | 9/17 | 8/17 |
|
|
162
|
+
| **11 held-out pages** | **84/100** | **74/100** | **61/100** |
|
|
163
|
+
| Held-out word recall / precision | 86.3% / 95.5% | 85.4% / 95.6% | 98.2% / 72.1% |
|
|
164
|
+
|
|
165
|
+
The held-out row is the one to weigh. On those pages the package leads plain
|
|
166
|
+
Readability + Turndown on probes, and the two are level on word recall and
|
|
167
|
+
precision. The pages are no longer fully unseen: the package scored 75/100 before
|
|
168
|
+
two fixes that came from their failures, so read 84 as a little generous. What it
|
|
169
|
+
still fails there:
|
|
170
|
+
|
|
171
|
+
- **Most H2s dropped** on Docusaurus and Starlight, by Readability; Starlight also
|
|
172
|
+
keeps "Section titled" anchor text.
|
|
173
|
+
- **Non-default tab panels dropped** (Starlight).
|
|
174
|
+
- **Comments and product grids lost**, by Readability: 36% word recall on a
|
|
175
|
+
Lobsters thread, 50% on an IKEA category page.
|
|
176
|
+
- **Promo boxes kept** inside a BBC Arabic article.
|
|
177
|
+
|
|
178
|
+
Browser bundle: about 67 KB minified, 24 KB gzip (`npm run size`, ceiling 68 KB
|
|
104
179
|
minified). Readability is most of it.
|
|
105
180
|
|
|
106
|
-
`npm run ci` runs the contract suite and the size check, and must exit 0
|
|
181
|
+
`npm run ci` runs the contract suite, the type check, the browser run and the size check, and must exit 0
|
|
107
182
|
before a commit. It runs locally; the repo deliberately has no cloud CI.
|
|
108
183
|
|
|
109
184
|
## Known limits (v0.1)
|
|
110
185
|
|
|
111
|
-
- **
|
|
112
|
-
|
|
186
|
+
- **Chromium only so far.** Tested in Node (linkedom) and in headless Chrome
|
|
187
|
+
and Edge, but not in Firefox or Safari, and not yet inside a real extension's
|
|
188
|
+
content script.
|
|
113
189
|
- **Some legacy markup loses paragraph breaks.** Readability can drop runs of
|
|
114
190
|
`<br>` between inline elements in old HTML. The text survives; a paragraph
|
|
115
191
|
break may not.
|
|
@@ -120,6 +196,9 @@ before a commit. It runs locally; the repo deliberately has no cloud CI.
|
|
|
120
196
|
legitimate in some scripts: ideographic variation selectors in CJK names and
|
|
121
197
|
the Mongolian free variation selectors.
|
|
122
198
|
- **Iframes and closed shadow roots are not captured.**
|
|
199
|
+
- **Text that spells out HTML stays literal.** `<img onerror=…>` in page
|
|
200
|
+
text comes out as `<img onerror=…>`. Sanitize the Markdown before rendering it
|
|
201
|
+
as HTML.
|
|
123
202
|
|
|
124
203
|
## Licence
|
|
125
204
|
|
|
@@ -127,6 +206,19 @@ page-markdown is under the WATT3D Source License: AGPL-3.0 with the Additional
|
|
|
127
206
|
Terms in [ADDITIONAL_TERMS.md](ADDITIONAL_TERMS.md) (see [LICENSE](LICENSE)).
|
|
128
207
|
[robots.txt](robots.txt) states the AI-crawler policy.
|
|
129
208
|
|
|
209
|
+
In short (the licence files govern, and this is not legal advice):
|
|
210
|
+
|
|
211
|
+
| Use | Terms |
|
|
212
|
+
|---|---|
|
|
213
|
+
| Personal, non-commercial | Free |
|
|
214
|
+
| Non-commercial academic research | Free |
|
|
215
|
+
| Commercial use, including inside a for-profit company | Only if everything you own or operate is open source under a licence at least as protective as AGPL-3.0; otherwise you need a commercial licence |
|
|
216
|
+
| AI training, fine-tuning or a RAG corpus built from this software | Counts as commercial use, and the resulting model must be released with open weights |
|
|
217
|
+
|
|
218
|
+
AGPL-3.0 applies throughout, including its source-sharing duties for networked
|
|
219
|
+
services. For a commercial licence, write to
|
|
220
|
+
[WATT3D@protonmail.com](mailto:WATT3D@protonmail.com).
|
|
221
|
+
|
|
130
222
|
It bundles text-sanctifier's run finder (same licence) and Mozilla Readability
|
|
131
223
|
(Apache-2.0) and Turndown (MIT), whose licences are in
|
|
132
224
|
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). Those notices must accompany
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies `transform` to the prose between fenced code blocks only.
|
|
3
|
+
* A shorter embedded fence does not close a block; an unterminated fence
|
|
4
|
+
* keeps the rest verbatim.
|
|
5
|
+
* @param {string} markdown
|
|
6
|
+
* @param {(prose: string) => string} transform
|
|
7
|
+
*/
|
|
8
|
+
export function mapOutsideFences(markdown: string, transform: (prose: string) => string): string;
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} markdown
|
|
11
|
+
* @returns {{ markdown: string, removedJsonLines: number }}
|
|
12
|
+
*/
|
|
13
|
+
export function cleanMarkdown(markdown: string): {
|
|
14
|
+
markdown: string;
|
|
15
|
+
removedJsonLines: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Output modes. "no_links" keeps link text. "text_only" also drops headings,
|
|
19
|
+
* list markers, emphasis and images. Both leave fenced code verbatim.
|
|
20
|
+
* @param {string} markdown
|
|
21
|
+
* @param {import("./types.js").OutputMode} [mode]
|
|
22
|
+
*/
|
|
23
|
+
export function applyMode(markdown: string, mode?: import("./types.js").OutputMode): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A configured Turndown instance (cached).
|
|
3
|
+
* @returns {import("./types.js").TurndownInstance}
|
|
4
|
+
*/
|
|
5
|
+
export function createTurndown(): import("./types.js").TurndownInstance;
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} html
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export function htmlToMarkdown(html: string): string;
|
|
11
|
+
export const TURNDOWN_OPTIONS: Readonly<{
|
|
12
|
+
headingStyle: "atx";
|
|
13
|
+
bulletListMarker: "-";
|
|
14
|
+
codeBlockStyle: "fenced";
|
|
15
|
+
emDelimiter: "_";
|
|
16
|
+
strongDelimiter: "**";
|
|
17
|
+
linkStyle: "inlined";
|
|
18
|
+
hr: "---";
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {{ html: string, method: "readability"|"full_page"|(string & {}), fallbackReason?: string, title?: string,
|
|
3
|
+
* byline?: string, published?: string, siteName?: string, lang?: string, excerpt?: string }} Extraction
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Receives a copy of the standardized document. Returning nothing, empty HTML
|
|
7
|
+
* or too little text falls back to the full page; so does throwing.
|
|
8
|
+
* @typedef {(doc: Document) => Extraction|null|undefined} Extractor
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* @param {Document} doc standardized inert document (not mutated)
|
|
12
|
+
* @param {{ extract?: "readable"|"full"|Extractor,
|
|
13
|
+
* minExtractChars?: number, readability?: import("./types.js").ReadabilityOptions }} [options]
|
|
14
|
+
* @returns {Extraction}
|
|
15
|
+
*/
|
|
16
|
+
export function extractContent(doc: Document, options?: {
|
|
17
|
+
extract?: "readable" | "full" | Extractor;
|
|
18
|
+
minExtractChars?: number;
|
|
19
|
+
readability?: import("./types.js").ReadabilityOptions;
|
|
20
|
+
}): Extraction;
|
|
21
|
+
export const DEFAULT_MIN_EXTRACT_CHARS: 100;
|
|
22
|
+
export type Extraction = {
|
|
23
|
+
html: string;
|
|
24
|
+
method: "readability" | "full_page" | (string & {});
|
|
25
|
+
fallbackReason?: string;
|
|
26
|
+
title?: string;
|
|
27
|
+
byline?: string;
|
|
28
|
+
published?: string;
|
|
29
|
+
siteName?: string;
|
|
30
|
+
lang?: string;
|
|
31
|
+
excerpt?: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Receives a copy of the standardized document. Returning nothing, empty HTML
|
|
35
|
+
* or too little text falls back to the full page; so does throwing.
|
|
36
|
+
*/
|
|
37
|
+
export type Extractor = (doc: Document) => Extraction | null | undefined;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Document} doc standardized clone
|
|
3
|
+
* @param {string} articleHtml the extracted article
|
|
4
|
+
* @returns {Array<{ label: string, value: string }>} facts absent from the article
|
|
5
|
+
*/
|
|
6
|
+
export function collectPageFacts(doc: Document, articleHtml: string): Array<{
|
|
7
|
+
label: string;
|
|
8
|
+
value: string;
|
|
9
|
+
}>;
|
|
10
|
+
/**
|
|
11
|
+
* "## Page facts" list, or "" when there are none.
|
|
12
|
+
* @param {readonly import("./types.js").PageFact[]} facts
|
|
13
|
+
*/
|
|
14
|
+
export function pageFactsMarkdown(facts: readonly import("./types.js").PageFact[]): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string} text
|
|
3
|
+
* @returns {{ text: string, runs: import("./types.js").HiddenRun[] }}
|
|
4
|
+
*/
|
|
5
|
+
export function removeHiddenText(text: string): {
|
|
6
|
+
text: string;
|
|
7
|
+
runs: import("./types.js").HiddenRun[];
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* One omission per kind of run: `count` runs, `chars` code points.
|
|
11
|
+
* @param {ReadonlyArray<Pick<import("./types.js").HiddenRun, "kind"|"count">>} runs
|
|
12
|
+
* @returns {import("./types.js").HiddenTextOmission[]}
|
|
13
|
+
*/
|
|
14
|
+
export function hiddenTextOmissions(runs: ReadonlyArray<Pick<import("./types.js").HiddenRun, "kind" | "count">>): import("./types.js").HiddenTextOmission[];
|