page-markdown 0.1.5 → 0.3.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/ADDITIONAL_TERMS.md +122 -122
- package/CHANGELOG.md +353 -186
- package/LICENSE +694 -694
- package/README.md +113 -41
- package/THIRD_PARTY_NOTICES.md +264 -264
- package/dist/types/concealed.d.ts +59 -0
- package/dist/types/extract.d.ts +10 -4
- package/dist/types/facts.d.ts +1 -0
- package/dist/types/index.d.ts +20 -6
- package/dist/types/sanitize.d.ts +22 -3
- package/dist/types/standardize.d.ts +15 -1
- package/dist/types/types.d.ts +51 -2
- package/package.json +69 -68
- package/robots.txt +79 -79
- package/src/clean.js +1 -1
- package/src/concealed.js +418 -0
- package/src/convert.js +95 -13
- package/src/extract.js +178 -8
- package/src/facts.js +6 -3
- package/src/index.js +219 -178
- package/src/refine.js +9 -3
- package/src/sanitize.js +106 -57
- package/src/standardize.js +167 -3
- package/src/types.js +23 -2
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
|
-
It is tested in Node
|
|
7
|
-
|
|
6
|
+
It is tested in Node, in headless Chrome and Edge, and in a real extension
|
|
7
|
+
(HELIOS, in Brave), but not yet in Firefox or Safari (see
|
|
8
|
+
[Known limits](#known-limits-v02)). It was built for WATT3D's
|
|
8
9
|
browser extensions.
|
|
9
10
|
|
|
10
11
|
```
|
|
@@ -13,6 +14,7 @@ live Document
|
|
|
13
14
|
→ standardize <pre> → fenced code with language; heading wrappers unwrapped; empty and
|
|
14
15
|
javascript: links removed; absolute links and images; lazy images resolved
|
|
15
16
|
→ extract Mozilla Readability on a copy; full-page fallback when it finds too little;
|
|
17
|
+
main-content fallback when it keeps only a small share of the page;
|
|
16
18
|
a dropped lead paragraph restored; a clean title heading
|
|
17
19
|
→ convert Turndown + GitHub-flavoured tables, strikethrough and task lists; tables never
|
|
18
20
|
come out as raw HTML
|
|
@@ -42,7 +44,7 @@ import { pageToMarkdown } from "page-markdown";
|
|
|
42
44
|
const result = pageToMarkdown(document);
|
|
43
45
|
result.markdown; // "# Title\n\n…"
|
|
44
46
|
result.metadata; // { url, canonicalUrl, title, author, published, siteName, lang, … }
|
|
45
|
-
result.extraction.method; // "readability" | "full_page"
|
|
47
|
+
result.extraction.method; // "readability" | "main_content" | "full_page" | custom method
|
|
46
48
|
|
|
47
49
|
// In Node, with any DOM implementation (linkedom shown)
|
|
48
50
|
import { parseHTML } from "linkedom";
|
|
@@ -55,15 +57,16 @@ pageToMarkdown(doc, { url: "https://example.com/article" });
|
|
|
55
57
|
| Option | Default | Meaning |
|
|
56
58
|
|---|---|---|
|
|
57
59
|
| `url` | the document's URL | Base for absolute links and images |
|
|
58
|
-
| `extract` | `"readable"` | `"readable"` (Readability
|
|
59
|
-
| `minExtractChars` | `100` | Below this,
|
|
60
|
+
| `extract` | `"readable"` | `"readable"` (Readability with main-content or full-page fallback), `"full"`, or a function `(docCopy) => { html, method }` |
|
|
61
|
+
| `minExtractChars` | `100` | Below this, Readability falls back to a sufficiently long `<main>` or `[role="main"]`, else the full page; custom extractors fall back to the full page |
|
|
62
|
+
| `minExtractShare` | `0.4` | When Readability keeps less than this share of the page's main content (and misses at least 500 characters), the main content is used instead: forum threads and product grids. `0` turns it off |
|
|
60
63
|
| `includeFullPage` | `false` | Also return `fullPageMarkdown` |
|
|
61
|
-
| `includeTitle` | `true` |
|
|
64
|
+
| `includeTitle` | `true` | Add `# <headline>` to Readability or main-content output when no H1 remains; skipped in `text_only` mode |
|
|
62
65
|
| `recoverLead` | `true` | Restore a lead paragraph the extractor dropped, when it matches the meta description |
|
|
63
66
|
| `pageFacts` | `false` | Append `## Page facts`: short label/value pairs outside the article (a sidebar's version or license, spec rows, infobox rows) that the extractor dropped. `extraction.pageFacts` counts them |
|
|
64
67
|
| `removeHiddenText` | `false` | Remove text hidden from people but read by models: runs of tag characters, variation selectors, invisible operators and reserved tag-plane code points, found by [text-sanctifier](https://github.com/iWhatty/text-sanctifier)'s `findHiddenRuns`. Applies to `markdown` (fenced code included), `fullPageMarkdown` and `metadata`. The England, Scotland and Wales flags and a lone emoji or text presentation selector are kept. `omissions` gets one entry per kind (`hidden_tag`, `hidden_variation_selector`, `hidden_invisible_operator`, `hidden_reserved_tag_plane`) with `count` runs and `chars` code points removed from `markdown` |
|
|
65
68
|
| `mode` | `"full"` | `"no_links"` keeps link text; `"text_only"` also drops structure. Fenced code is never changed |
|
|
66
|
-
| `sanitize` | see `DEFAULT_SANITIZE_OPTIONS` | `stripAds`, `extraStripSelectors` (e.g. `["[data-app-overlay]"]`), shadow limits |
|
|
69
|
+
| `sanitize` | see `DEFAULT_SANITIZE_OPTIONS` | `stripAds`, `extraStripSelectors` (e.g. `["[data-app-overlay]"]`), shadow limits, which apply per host in the page's own tree and don't charge slotted page content |
|
|
67
70
|
| `readability` | `{}` | Passed to Readability |
|
|
68
71
|
|
|
69
72
|
The stages are also exported for advanced use: `sanitizeToInertDocument`,
|
|
@@ -84,18 +87,65 @@ The stages are also exported for advanced use: `sanitizeToInertDocument`,
|
|
|
84
87
|
| `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
88
|
| `metadata.headline` | `string?` | The heading chosen for the capture |
|
|
86
89
|
| `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
|
|
90
|
+
| `extraction.method` | `string` | `"readability"`, `"main_content"`, `"full_page"`, or the name a custom extractor gave |
|
|
91
|
+
| `extraction.fallbackReason` | `string?` | Why extraction fell back: `extracted_small_share_of_page`, `extracted_site_menu` or `extracted_few_sections_of_page` use the main content; `extractor_found_nothing`, `extracted_text_below_minimum` or `extractor_error: ...` use a sufficiently long main-content region for Readability, else the full page. Custom extractors fall back to the full page |
|
|
89
92
|
| `extraction.leadRecovered` | `true?` | A dropped lead paragraph was restored |
|
|
90
93
|
| `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` |
|
|
94
|
+
| `omissions[]` | `{ kind, count, note, chars? }` | What was left out or removed. `kind` is `shadow_content`, `json_line`, `inactive_tabs`, `comments` or a `hidden_*` kind; only `hidden_*` entries have `chars`. All but `hidden_*` also add a closing line to `markdown`, as a fallback does, so a caller that passes on only the Markdown still says so |
|
|
95
|
+
| `warnings[]` | `string` | Read these before trusting a capture: a fallback, or shadow content cut off at the capture limit, in which case the main content may be missing |
|
|
93
96
|
| `stats.mode` | `string` | The output mode used |
|
|
94
|
-
| `stats.extractedHtmlChars`, `convertedChars`, `markdownChars` | `number` | Sizes at each step. `markdownChars` is
|
|
95
|
-
| `stats.
|
|
97
|
+
| `stats.extractedHtmlChars`, `convertedChars`, `markdownChars` | `number` | Sizes at each step. `markdownChars` is the returned Markdown's length |
|
|
98
|
+
| `stats.pageTextChars` | `number` | Whitespace-normalized text length of the standardized page at capture time |
|
|
99
|
+
| `stats.standardization` | `object` | Counts of what standardizing changed: `lazyImagesResolved`, `inlineImagesDropped`, `scriptLinksUnwrapped`, `invalidLinksUnwrapped`, `emptyLinksRemoved`, `preWrapped`, `headingWidgetsRemoved`, `headingsUnwrapped`; `omittedTabs` lists labels of inactive panels left out |
|
|
96
100
|
|
|
97
101
|
A `?` marks a field that is absent, not empty, when it doesn't apply.
|
|
98
102
|
|
|
103
|
+
### Concealed text (opt-in)
|
|
104
|
+
|
|
105
|
+
Detection is off by default. Enable it for a live document:
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
const result = pageToMarkdown(document, {
|
|
109
|
+
detectConcealedText: true,
|
|
110
|
+
includeFullPage: true,
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Supported computed styles separate concealed DOM text from the article and
|
|
115
|
+
return it in `concealedText` as `{ reason, chars, text }` blocks. Text is capped
|
|
116
|
+
at 2,048 UTF-16 units per block, 16,384 total and 100 entries;
|
|
117
|
+
`concealedTextTruncated` reports capping. Omission counts and character totals
|
|
118
|
+
are uncapped. A `Concealed text omitted:` notice appears in both `markdown`
|
|
119
|
+
and `fullPageMarkdown` when text is captured.
|
|
120
|
+
|
|
121
|
+
The seven reasons are `display_none`, `visibility_hidden`, `opacity_zero`,
|
|
122
|
+
`font_size_zero`, `clipped`, `transparent_text` and `same_color`. Supported
|
|
123
|
+
clips are zero-area legacy rectangles on absolute/fixed elements,
|
|
124
|
+
`inset(50%)` and zero-radius circles. Colour checks compare computed sRGB
|
|
125
|
+
rgb()/rgba() values for an element's own text against its own or nearest
|
|
126
|
+
declared opaque background.
|
|
127
|
+
|
|
128
|
+
`concealmentCheck` is `disabled` by default (no computed-style calls),
|
|
129
|
+
`static-only` when enabled without a computed-style API, `computed` after a
|
|
130
|
+
successful check, or `incomplete` if the capability probe or a consulted
|
|
131
|
+
property fails. Detection applies to live documents only; enabled static
|
|
132
|
+
input retains its previous output. A top-level `detectConcealedText` option
|
|
133
|
+
takes precedence over `sanitize.detectConcealedText`, then the default false.
|
|
134
|
+
|
|
135
|
+
Limits include white text with no declared opaque background, non-zero alpha,
|
|
136
|
+
near-colour matches, and unparsed OKLCH and Display P3 colours. Intervening
|
|
137
|
+
images or translucent backgrounds and uncertain text painting effects prevent
|
|
138
|
+
colour equality detection. Existing short inactive tab, collapsed answer and
|
|
139
|
+
code wrapper exceptions remain; independent concealment reasons still apply.
|
|
140
|
+
Off-screen or covered text, attributes, metadata and custom-extractor output
|
|
141
|
+
are outside this check.
|
|
142
|
+
|
|
143
|
+
The scroll-reveal rule is **provisional**: zero-opacity content with an
|
|
144
|
+
opacity-applicable transition or named animation of nonzero duration stays
|
|
145
|
+
in place with a warning. A declaration does not prove it will appear, and a
|
|
146
|
+
page can use one to evade detection. Real-site validation is still required.
|
|
147
|
+
Chrome is the only browser tested; Firefox and Safari are untested.
|
|
148
|
+
|
|
99
149
|
### TypeScript
|
|
100
150
|
|
|
101
151
|
Types ship with the package, generated from the JSDoc in `src`: `pageToMarkdown`,
|
|
@@ -124,7 +174,7 @@ const { markdown: safe, omissions } = pageToMarkdown(document, { removeHiddenTex
|
|
|
124
174
|
## Quality
|
|
125
175
|
|
|
126
176
|
`npm test` runs the contract suite. It covers:
|
|
127
|
-
- golden outputs for
|
|
177
|
+
- golden outputs for 51 fixtures;
|
|
128
178
|
- regressions for known converter bugs, including dropped lead paragraphs,
|
|
129
179
|
raw HTML tables, flattened code in tables, site-suffixed titles and
|
|
130
180
|
unescaped table pipes;
|
|
@@ -146,8 +196,11 @@ checks what Node can't show:
|
|
|
146
196
|
children in rendered order;
|
|
147
197
|
- closed shadow roots stay out of the capture.
|
|
148
198
|
|
|
149
|
-
|
|
150
|
-
|
|
199
|
+
It launches a browser, so it runs only when a person sets
|
|
200
|
+
`PAGE_MARKDOWN_ALLOW_BROWSER_LAUNCH=1` in their own shell; agents must not set it.
|
|
201
|
+
Without the flag it builds `.browser-test/index.html`, which you can open in a
|
|
202
|
+
browser that is already running, and exits with an error. Set
|
|
203
|
+
`PAGE_MARKDOWN_BROWSER` to choose the browser.
|
|
151
204
|
|
|
152
205
|
`npm run bench` scores the package against two baselines: Turndown over the page
|
|
153
206
|
body, and Readability then Turndown. It runs 15 fixture checks and 117 probes on
|
|
@@ -159,38 +212,57 @@ recall against a hand-picked reference for each page. `npm run bench` only print
|
|
|
159
212
|
|---|---|---|---|
|
|
160
213
|
| Fixtures, written with the package | 15/15 | 7/15 | 3/15 |
|
|
161
214
|
| Five pages the fixes were tuned on | 17/17 | 9/17 | 8/17 |
|
|
162
|
-
| **11 held
|
|
163
|
-
|
|
|
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.
|
|
215
|
+
| **11 pages, once held out** | **98/100** | **74/100** | **61/100** |
|
|
216
|
+
| Their word recall / precision | 97.6% / 89.1% | 85.4% / 95.6% | 98.2% / 72.1% |
|
|
177
217
|
|
|
178
|
-
|
|
179
|
-
|
|
218
|
+
Don't read 98 as a score on unseen pages. These probes were written before any
|
|
219
|
+
engine ran and the package first scored 75/100, but most of the gain since comes
|
|
220
|
+
from fixes for these same pages' failures. New pages are the real test.
|
|
180
221
|
|
|
181
|
-
|
|
182
|
-
|
|
222
|
+
It gains recall (97.6% against 85.4%) by keeping more, and pays in precision
|
|
223
|
+
(89.1% against 95.6%). The cost is mostly on the IKEA category page: 29.7% precision,
|
|
224
|
+
because the main-content fallback keeps the page's banners and filters along with
|
|
225
|
+
its products. What it still fails on these pages:
|
|
183
226
|
|
|
184
|
-
|
|
227
|
+
- **Promo boxes kept** inside a BBC Arabic article.
|
|
228
|
+
|
|
229
|
+
Browser bundle: about 86.0 KB minified, 31.2 KB gzip (`npm run size`, ceiling 86 KB
|
|
230
|
+
minified). Readability is most of it.
|
|
185
231
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
232
|
+
`npm run ci` runs the contract suite, the type check and the size check, and must
|
|
233
|
+
exit 0 before a commit. It launches no browser, so run the browser check yourself
|
|
234
|
+
before a release when the DOM handling has changed. It runs locally; the repo
|
|
235
|
+
deliberately has no cloud CI.
|
|
236
|
+
|
|
237
|
+
## Known limits (v0.2)
|
|
238
|
+
|
|
239
|
+
- **Chromium only so far.** Tested in Node (linkedom), in headless Chrome and
|
|
240
|
+
Edge, and on live pages in a real extension (HELIOS, in Brave), but not
|
|
241
|
+
in Firefox or Safari.
|
|
242
|
+
- **Extraction can still be wrong without saying so.** The main-content fallback
|
|
243
|
+
and its warning catch a small share of the page, a kept site menu and lost
|
|
244
|
+
sections, not every wrong choice. If Readability picks the wrong part of a page
|
|
245
|
+
and that part is large, nothing flags it. No closing line doesn't mean nothing
|
|
246
|
+
was lost. A short post among long replies can be dropped: a Mastodon status lost
|
|
247
|
+
the post itself, and its cause isn't known yet.
|
|
248
|
+
- **Answers hidden on an ordinary article can be lost.** Collapsed FAQ answers are
|
|
249
|
+
kept by the main-content fallback only; Readability removes hidden text.
|
|
250
|
+
- **Bylines and dates can be missing from the Markdown.** Readability often leaves
|
|
251
|
+
them out of the article. Look in `metadata.author` and `metadata.published`.
|
|
189
252
|
- **Some legacy markup loses paragraph breaks.** Readability can drop runs of
|
|
190
253
|
`<br>` between inline elements in old HTML. The text survives; a paragraph
|
|
191
254
|
break may not.
|
|
192
|
-
- **
|
|
193
|
-
|
|
255
|
+
- **A capture is a snapshot.** It reads the DOM as it is at call time, and it
|
|
256
|
+
can't tell whether the page has finished rendering. A page read too early looks
|
|
257
|
+
like a normal, short result. To wait, re-capture until `stats.pageTextChars`
|
|
258
|
+
settles.
|
|
259
|
+
- **Captures hold whatever is in the DOM,** including text that other extensions
|
|
260
|
+
add. Use `sanitize.extraStripSelectors` for what you know about.
|
|
261
|
+
- **Hidden content stays where only CSS hides it.** The pipeline reads the static
|
|
262
|
+
DOM. The `hidden` attribute and inline `display: none` are understood; a tab
|
|
263
|
+
panel hidden by a stylesheet alone is kept unless its group names the selected
|
|
264
|
+
one. Tabs marked with neither `role` nor `aria-role` and hidden with
|
|
265
|
+
`class="hidden"` can be lost, with no closing line.
|
|
194
266
|
- **`removeHiddenText` covers hidden runs only.** Zero-width spaces, bidi
|
|
195
267
|
controls and look-alike spaces pass through. It also removes runs that are
|
|
196
268
|
legitimate in some scripts: ideographic variation selectors in CJK names and
|