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 CHANGED
@@ -1,124 +1,186 @@
1
- # Changelog
2
-
3
- ## 0.1.4 — 2026-11-02
4
-
5
- - **text-sanctifier 1.3.8** (was 1.3.3). `removeHiddenText` now also removes
6
- runs of reserved tag-plane code points (U+E0080–U+E00FF, U+E01F0–U+E0FFF),
7
- which 1.3.3 did not find, and reports them as `hidden_reserved_tag_plane`.
8
- The browser bundle grows to about 65.6 KB minified.
9
- - A test checks that every run kind text-sanctifier can report has an
10
- omission note, so a new kind fails `npm run ci` instead of reaching users
11
- with no note.
12
- - A test fails when any source, test, fixture or doc file holds a raw
13
- invisible or control character. A test regex in 0.1.3 held four, written
14
- by an editing tool that expanded `\u` escapes; they never shipped, since
15
- tests are not in the package.
16
-
17
- ## 0.1.3 — 2026-10-30
18
-
19
- First release on npm. `npm run ci` (contract suite and size check) is the
20
- local gate before a commit; there is no cloud CI. The size ceiling is 66 KB
21
- minified.
22
-
23
- Content-fidelity fixes:
24
-
25
- - **Title heading kept beside shell comments.** A `# comment` line in fenced
26
- code no longer counts as the article's top-level heading, which had
27
- suppressed the `# <headline>` line.
28
- - **Every pipe in a table cell is escaped.** `a || b` escaped only its first
29
- pipe, which added a column and broke the table.
30
- - **Images beside a heading survive.** A heading wrapper that also holds an
31
- image (or picture, video, audio) is no longer unwrapped to the bare heading.
32
- - **`no_links` keeps linked images intact.** `[![alt](img)](href)` became
33
- `![alt](href)`, an image pointing at the link target; the image is now kept
34
- as it was.
35
- - **Code in tables stays fenced.** A GFM cell is one line, so a data table
36
- holding `<pre>` code (Wikipedia's syntax examples) was flattened to inline
37
- code, losing its line breaks and indentation. Such a table is now rendered
38
- as blocks: each cell under a bold label from its column header (or its row
39
- header, for infoboxes), with code as verbatim fences. Tables without code
40
- are still GFM grids.
41
- - Code language from MediaWiki's `mw-highlight-lang-*` class.
42
- - At most one blank line beside a fenced block.
43
- - Five new regression fixtures, one per fix.
44
-
45
- Hidden text (opt-in):
46
-
47
- - **`removeHiddenText: true`** removes runs of tag characters, variation
48
- selectors and invisible operators, which people can't see but models read
49
- (hidden prompt instructions). It applies to the Markdown, fenced code
50
- included, and to `fullPageMarkdown` and `metadata`. The England, Scotland
51
- and Wales flags and a lone emoji or text presentation selector are kept.
52
- `omissions` gets `hidden_tag`, `hidden_variation_selector` and
53
- `hidden_invisible_operator` entries with `count` (runs) and `chars` (code
54
- points). `removeHiddenText` is exported. Default output is unchanged.
55
- - New runtime dependency: text-sanctifier 1.3.3 (first-party, no
56
- dependencies), for its `text-sanctifier/hidden` run finder. About 1.5 KB
57
- minified in the browser bundle.
58
- - New contract fixture `hidden-text.html`, written with character references
59
- so it holds no raw hidden characters.
60
-
61
- ## 0.1.2 — 2026-10-23
62
-
63
- - **Page facts (opt-in).** `pageFacts: true` appends a `## Page facts` list
64
- of short label/value pairs that sit outside the extracted article: a
65
- package sidebar's version, license and downloads, spec and price rows,
66
- infobox rows. Readability drops these asides, which suits reading but
67
- loses the answer to "which version?". Pairs come from a heading followed by
68
- a short value, `<dt>`/`<dd>`, and two-cell table rows. Values already in
69
- the article, sentences, link lists, lone non-data links and anything in
70
- navigation, header, footer or forms are skipped. `extraction.pageFacts`
71
- counts them; `collectPageFacts` is exported. Default output is unchanged.
72
- - New regression fixture: a package page whose version lives only in the
73
- sidebar.
74
-
75
- ## 0.1.1 — 2026-10-22
76
-
77
- Fixes from the first in-browser comparison (MDN, Wikipedia, the Python docs,
78
- GitHub):
79
-
80
- - **Tables never come out as raw HTML.** Own GFM table rules replace the
81
- plugin's:
82
- - a header row is used even when the table has none;
83
- - colspans are padded with empty cells;
84
- - cell content is flattened to one line (code, lists, line breaks);
85
- - pipes are escaped;
86
- - layout and nested tables are rendered as blocks.
87
- - **Dropped lead paragraph restored.** When the extractor leaves out a
88
- paragraph that matches the meta description (MDN's heading-less first
89
- section), it is put back first. `extraction.leadRecovered` reports it, and
90
- `recoverLead: false` turns it off.
91
- - **Clean title heading.** The page's own H1 (permalink sign removed) when
92
- the title contains it, else `og:title` or the title, without a site-name
93
- segment at either end. It is exposed as `metadata.headline`.
94
- - **Smaller bundle.** Strikethrough and task-list rules are now built in, and
95
- `@joplin/turndown-plugin-gfm` is no longer a dependency. The browser bundle
96
- is 60.2 KB minified, down from 61.7 KB.
97
-
98
- ## 0.1.0 — 2026-10-22
99
-
100
- First release.
101
-
102
- - `pageToMarkdown(document, options)`: sanitize → standardize → extract →
103
- convert → clean, returning Markdown, metadata, extraction details,
104
- omissions, warnings and stats.
105
- - **Sanitize.** Builds an inert clone and inlines open shadow DOM within node,
106
- character and depth limits, with an omission notice past them. Removes
107
- scripts, styles, `noscript`, meta and link tags, and ads, plus any
108
- `extraStripSelectors`.
109
- - **Standardize.**
110
- - Turns `<pre>` into fenced code, with the language from
111
- `language-`/`lang-`/`highlight-` classes or `data-lang`.
112
- - Unwraps heading wrappers and removes edit and permalink widgets.
113
- - Removes empty and `javascript:` links.
114
- - Makes links and images absolute and resolves lazy images; drops inline
115
- `data:` images.
116
- - **Extract.** Mozilla Readability on a copy of the clone, falling back to the
117
- full page when it finds nothing or too little. Optionally restores the title
118
- heading.
119
- - **Convert.** Turndown with GitHub-flavoured tables and strikethrough.
120
- - **Clean.** Fence-safe. Keeps short lines and `<br>` hard breaks, removes only
121
- verified single-line JSON documents, and offers `full`, `no_links` and
122
- `text_only` modes.
123
- - **Metadata.** Meta tags, JSON-LD (`@graph` included), canonical URL and
124
- language.
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 `&lt;`.
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.** `[![alt](img)](href)` became
95
+ `![alt](href)`, 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
- So far it is tested only in Node (see [Known limits](#known-limits-v01)). It
7
- was built for WATT3D's browser extensions.
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 raw HTML
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 26 fixtures;
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 bench` scores the package against a naive baseline (Turndown over the
99
- page body). It runs 15 fixture checks and 17 probes on five real pages, which
100
- `node bench/fetch-pages.mjs` fetches first. See
101
- [bench/RESULTS.md](bench/RESULTS.md): currently **32/32 against 11/32**.
102
-
103
- Browser bundle: about 65 KB minified, 23 KB gzip (`npm run size`, ceiling 66 KB
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
- - **Node only so far.** Tested in Node with linkedom, not yet in a browser. The
112
- open shadow DOM path is exercised only with a stand-in host in tests.
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.** `&lt;img onerror=…&gt;` 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[];