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.
@@ -3,10 +3,14 @@
3
3
  * @property {string} [url] page URL for absolute links (default: the document's URL)
4
4
  * @property {"full"|"no_links"|"text_only"} [mode="full"]
5
5
  * @property {"readable"|"full"|import("./extract.js").Extractor} [extract="readable"]
6
- * "readable": Readability with full-page fallback; "full": the whole body;
6
+ * "readable": Readability with main-content or full-page fallback; "full": the whole body;
7
7
  * or a custom extractor receiving a copy of the standardized document.
8
- * @property {number} [minExtractChars=100] below this, fall back to the full page
8
+ * @property {number} [minExtractChars=100] below this, fall back to the page's <main>
9
+ * if it marks one and it holds enough, else to the full page
10
+ * @property {number} [minExtractShare=0.4] when Readability keeps less than this
11
+ * share of the page's main content, use the main content instead; 0 turns it off
9
12
  * @property {boolean} [includeFullPage=false] also return the full-page Markdown
13
+ * @property {boolean} [detectConcealedText=false] opt-in live computed-style check, default false
10
14
  * @property {boolean} [includeTitle=true] start readable output with "# <headline>"
11
15
  * @property {boolean} [recoverLead=true] restore a dropped lead paragraph that matches the meta description
12
16
  * @property {boolean} [pageFacts=false] append "## Page facts": short label/value pairs
@@ -27,7 +31,7 @@
27
31
  * @returns {import("./types.js").PageMarkdownResult}
28
32
  */
29
33
  export function pageToMarkdown(input: Document | Element, options?: PageMarkdownOptions): import("./types.js").PageMarkdownResult;
30
- export const VERSION: "0.1.5";
34
+ export const VERSION: "0.3.0";
31
35
  export { standardizeDocument } from "./standardize.js";
32
36
  export { collectMetadata } from "./metadata.js";
33
37
  export * from "./types.js";
@@ -38,18 +42,28 @@ export type PageMarkdownOptions = {
38
42
  url?: string | undefined;
39
43
  mode?: "full" | "no_links" | "text_only" | undefined;
40
44
  /**
41
- * "readable": Readability with full-page fallback; "full": the whole body;
45
+ * "readable": Readability with main-content or full-page fallback; "full": the whole body;
42
46
  * or a custom extractor receiving a copy of the standardized document.
43
47
  */
44
48
  extract?: import("./extract.js").Extractor | "readable" | "full" | undefined;
45
49
  /**
46
- * below this, fall back to the full page
50
+ * below this, fall back to the page's <main>
51
+ * if it marks one and it holds enough, else to the full page
47
52
  */
48
53
  minExtractChars?: number | undefined;
54
+ /**
55
+ * when Readability keeps less than this
56
+ * share of the page's main content, use the main content instead; 0 turns it off
57
+ */
58
+ minExtractShare?: number | undefined;
49
59
  /**
50
60
  * also return the full-page Markdown
51
61
  */
52
62
  includeFullPage?: boolean | undefined;
63
+ /**
64
+ * opt-in live computed-style check, default false
65
+ */
66
+ detectConcealedText?: boolean | undefined;
53
67
  /**
54
68
  * start readable output with "# <headline>"
55
69
  */
@@ -90,7 +104,7 @@ export type PageMarkdownOptions = {
90
104
  } | undefined;
91
105
  };
92
106
  export { sanitizeToInertDocument, DEFAULT_SANITIZE_OPTIONS, isAdvertisementElement, createInertDocument } from "./sanitize.js";
93
- export { extractContent, DEFAULT_MIN_EXTRACT_CHARS } from "./extract.js";
107
+ export { extractContent, DEFAULT_MIN_EXTRACT_CHARS, DEFAULT_MIN_EXTRACT_SHARE } from "./extract.js";
94
108
  export { htmlToMarkdown, createTurndown, TURNDOWN_OPTIONS } from "./convert.js";
95
109
  export { cleanMarkdown, applyMode, mapOutsideFences } from "./clean.js";
96
110
  export { recoverLeadParagraph, chooseHeadline } from "./refine.js";
@@ -9,18 +9,37 @@ export function createInertDocument(doc: Document): Document;
9
9
  /**
10
10
  * Clones `root` (a Document or Element) into a new inert document.
11
11
  * Open shadow roots replace their host's light children (native slots supply
12
- * assigned children). Limits apply only to shadow content; a host over the
13
- * limit is emptied and counted as an omission.
12
+ * assigned children). Limits apply only to shadow content, per host in the
13
+ * page's own tree, so slotted page content is not charged: docs sites put a
14
+ * whole article in one host's slot. A host over the limit keeps what fits and
15
+ * is counted as an omission.
14
16
  *
15
17
  * @param {Document|Element} root
16
18
  * @param {import("./types.js").SanitizeOptions} [options]
17
- * @returns {{ document: Document, omissions: { shadowHosts: number } }}
18
19
  */
19
20
  export function sanitizeToInertDocument(root: Document | Element, options?: import("./types.js").SanitizeOptions): {
20
21
  document: Document;
21
22
  omissions: {
22
23
  shadowHosts: number;
23
24
  };
25
+ concealment: {
26
+ process: (el: any, copy: any, work: any, pairs: any) => void;
27
+ initial: (source: any) => any;
28
+ inherit: (work: any, child: any, clone: any) => any;
29
+ exempt: (el: any) => void;
30
+ lift: (inert: any) => void;
31
+ entries: import("./types.js").ConcealedText[];
32
+ summary: {
33
+ count: number;
34
+ chars: number;
35
+ truncated: boolean;
36
+ };
37
+ readonly counters: {
38
+ [k: string]: number;
39
+ };
40
+ readonly check: import("./types.js").ConcealmentCheck;
41
+ readonly pendingOpacity: number;
42
+ };
24
43
  };
25
44
  /** @type {Readonly<Required<import("./types.js").SanitizeOptions>>} */
26
45
  export const DEFAULT_SANITIZE_OPTIONS: Readonly<Required<import("./types.js").SanitizeOptions>>;
@@ -1,15 +1,29 @@
1
+ /**
2
+ * Tabs read as one document: inactive panels are shown, each under its tab's
3
+ * name, in place of a tab strip that points nowhere. A long inactive panel is
4
+ * left out, and named in stats.omittedTabs.
5
+ */
6
+ export function prepareConcealment(doc: any, concealment: any): {
7
+ panels: any[];
8
+ hasProvenance: any;
9
+ inactive: Map<any, boolean>;
10
+ panelLengths: Map<any, any>;
11
+ };
1
12
  /**
2
13
  * @param {Document} doc the inert clone (mutated in place)
3
- * @param {{ url?: string }} [options]
14
+ * @param {{ url?: string, concealment?: ReturnType<typeof import('./concealed.js').createConcealmentWalk> }} [options]
4
15
  */
5
16
  export function standardizeDocument(doc: Document, options?: {
6
17
  url?: string;
18
+ concealment?: ReturnType<typeof import("./concealed.js").createConcealmentWalk>;
7
19
  }): {
8
20
  lazyImagesResolved: number;
9
21
  inlineImagesDropped: number;
10
22
  scriptLinksUnwrapped: number;
23
+ invalidLinksUnwrapped: number;
11
24
  emptyLinksRemoved: number;
12
25
  preWrapped: number;
13
26
  headingWidgetsRemoved: number;
14
27
  headingsUnwrapped: number;
28
+ omittedTabs: string[];
15
29
  };
@@ -7,6 +7,10 @@ export type SanitizeOptions = {
7
7
  stripMeta?: boolean | undefined;
8
8
  stripLinkTags?: boolean | undefined;
9
9
  stripAds?: boolean | undefined;
10
+ /**
11
+ * opt-in computed-style check, default false
12
+ */
13
+ detectConcealedText?: boolean | undefined;
10
14
  /**
11
15
  * app-specific overlays or widgets to drop, e.g. "[data-app-overlay]"
12
16
  */
@@ -37,7 +41,7 @@ export type PageMarkdownMetadata = CollectedMetadata & ExtractedMetadata;
37
41
  export type PageMarkdownExtraction = {
38
42
  method: Extraction["method"];
39
43
  /**
40
- * why extraction fell back to the full page
44
+ * why extraction fell back to the full page or the main content
41
45
  */
42
46
  fallbackReason?: string | undefined;
43
47
  /**
@@ -97,7 +101,8 @@ export type HiddenTextOmission = {
97
101
  note: string;
98
102
  };
99
103
  /**
100
- * Check `kind` before reading `chars`: only hidden-text omissions have it.
104
+ * Check `kind` before reading `chars`: hidden Unicode omissions use code points;
105
+ * concealed_text uses UTF-16 units. Other kinds do not have it.
101
106
  */
102
107
  export type PageMarkdownOmission = {
103
108
  kind: "shadow_content";
@@ -107,6 +112,19 @@ export type PageMarkdownOmission = {
107
112
  kind: "json_line";
108
113
  count: number;
109
114
  note: string;
115
+ } | {
116
+ kind: "inactive_tabs";
117
+ count: number;
118
+ note: string;
119
+ } | {
120
+ kind: "comments";
121
+ count: number;
122
+ note: string;
123
+ } | {
124
+ kind: "concealed_text";
125
+ count: number;
126
+ chars: number;
127
+ note: string;
110
128
  } | HiddenTextOmission;
111
129
  export type StandardizationStats = ReturnType<typeof import("./standardize.js").standardizeDocument>;
112
130
  export type PageMarkdownStats = {
@@ -114,6 +132,11 @@ export type PageMarkdownStats = {
114
132
  extractedHtmlChars: number;
115
133
  convertedChars: number;
116
134
  markdownChars: number;
135
+ /**
136
+ * the page's text at capture time: a capture is a
137
+ * snapshot of the DOM, so a caller can re-capture until this number settles
138
+ */
139
+ pageTextChars: number;
117
140
  standardization: StandardizationStats;
118
141
  };
119
142
  export type PageMarkdownResult = {
@@ -121,6 +144,18 @@ export type PageMarkdownResult = {
121
144
  * the package version that made this result
122
145
  */
123
146
  version: string;
147
+ /**
148
+ * capped captured blocks, raw text in UTF-16 units
149
+ */
150
+ concealedText: ConcealedText[];
151
+ /**
152
+ * source computed-style capability
153
+ */
154
+ concealmentCheck: ConcealmentCheck;
155
+ /**
156
+ * true if any block, text or entry cap omitted report text
157
+ */
158
+ concealedTextTruncated: boolean;
124
159
  markdown: string;
125
160
  metadata: PageMarkdownMetadata;
126
161
  extraction: PageMarkdownExtraction;
@@ -136,6 +171,20 @@ export type PageFact = {
136
171
  label: string;
137
172
  value: string;
138
173
  };
174
+ /**
175
+ * The one-time probe establishes that the document's view can return every
176
+ * required field and exposed optional field. Afterwards, incomplete means a
177
+ * CONSULTED value was empty or threw; unread irrelevant values are not checked.
178
+ * Computed means no such failure; static-only means no computed-style API;
179
+ * disabled means detection was turned off.
180
+ */
181
+ export type ConcealmentCheck = "computed" | "static-only" | "disabled" | "incomplete";
182
+ export type ConcealmentReason = "display_none" | "visibility_hidden" | "opacity_zero" | "font_size_zero" | "clipped" | "transparent_text" | "same_color";
183
+ export type ConcealedText = {
184
+ reason: ConcealmentReason;
185
+ chars: number;
186
+ text: string;
187
+ };
139
188
  export type TurndownFilter = string | string[] | ((node: HTMLElement, options: Record<string, unknown>) => boolean);
140
189
  /**
141
190
  * Turndown's public methods; Turndown ships no types of its own.
package/package.json CHANGED
@@ -1,68 +1,69 @@
1
- {
2
- "name": "page-markdown",
3
- "version": "0.1.5",
4
- "description": "Faithful page-to-Markdown for humans and agents: sanitize, extract the readable article, convert with GFM, and clean without losing content.",
5
- "type": "module",
6
- "exports": {
7
- ".": {
8
- "types": "./dist/types/index.d.ts",
9
- "default": "./src/index.js"
10
- }
11
- },
12
- "types": "./dist/types/index.d.ts",
13
- "files": [
14
- "src",
15
- "dist/types",
16
- "LICENSE",
17
- "ADDITIONAL_TERMS.md",
18
- "THIRD_PARTY_NOTICES.md",
19
- "README.md",
20
- "CHANGELOG.md",
21
- "robots.txt"
22
- ],
23
- "sideEffects": false,
24
- "packageManager": "pnpm@10.33.0",
25
- "engines": {
26
- "node": ">=20"
27
- },
28
- "scripts": {
29
- "test": "node --test \"test/**/*.test.mjs\"",
30
- "build:types": "tsc -p tsconfig.json",
31
- "typecheck": "npm run build:types && node test/types/real-output.mjs && tsc -p test/types && tsc -p test/types/linkedom",
32
- "size": "node bench/size.mjs",
33
- "test:browser": "node test/browser/run.mjs",
34
- "ci": "npm test && npm run typecheck && npm run test:browser && npm run size",
35
- "prepack": "npm run build:types",
36
- "prepublishOnly": "npm run ci",
37
- "bench": "node bench/compare.mjs",
38
- "bench:write": "node bench/compare.mjs --write"
39
- },
40
- "keywords": [
41
- "markdown",
42
- "readability",
43
- "html-to-markdown",
44
- "reader-mode",
45
- "web-clipper",
46
- "browser-extension"
47
- ],
48
- "author": "WATT3D <WATT3D@protonmail.com>",
49
- "license": "SEE LICENSE IN LICENSE",
50
- "repository": {
51
- "type": "git",
52
- "url": "git+https://github.com/iWhatty/page-markdown.git"
53
- },
54
- "homepage": "https://github.com/iWhatty/page-markdown#readme",
55
- "bugs": {
56
- "url": "https://github.com/iWhatty/page-markdown/issues"
57
- },
58
- "dependencies": {
59
- "@mozilla/readability": "0.6.0",
60
- "text-sanctifier": "1.3.8",
61
- "turndown": "7.2.4"
62
- },
63
- "devDependencies": {
64
- "esbuild": "0.27.0",
65
- "linkedom": "0.18.12",
66
- "typescript": "5.9.3"
67
- }
68
- }
1
+ {
2
+ "name": "page-markdown",
3
+ "version": "0.3.0",
4
+ "description": "Faithful page-to-Markdown for humans and agents: sanitize, extract the readable article, convert with GFM, and clean without losing content.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/types/index.d.ts",
9
+ "default": "./src/index.js"
10
+ }
11
+ },
12
+ "types": "./dist/types/index.d.ts",
13
+ "files": [
14
+ "src",
15
+ "dist/types",
16
+ "LICENSE",
17
+ "ADDITIONAL_TERMS.md",
18
+ "THIRD_PARTY_NOTICES.md",
19
+ "README.md",
20
+ "CHANGELOG.md",
21
+ "robots.txt"
22
+ ],
23
+ "sideEffects": false,
24
+ "packageManager": "pnpm@10.33.0",
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "scripts": {
29
+ "test": "node --test \"test/**/*.test.mjs\"",
30
+ "build:types": "tsc -p tsconfig.json",
31
+ "typecheck": "npm run build:types && node test/types/real-output.mjs && tsc -p test/types && tsc -p test/types/linkedom",
32
+ "size": "node bench/size.mjs",
33
+ "test:browser": "node test/browser/run.mjs",
34
+ "report:build": "node test/browser/build-report.mjs",
35
+ "ci": "npm test && npm run typecheck && npm run size",
36
+ "prepack": "npm run build:types",
37
+ "prepublishOnly": "npm run ci",
38
+ "bench": "node bench/compare.mjs",
39
+ "bench:write": "node bench/compare.mjs --write"
40
+ },
41
+ "keywords": [
42
+ "markdown",
43
+ "readability",
44
+ "html-to-markdown",
45
+ "reader-mode",
46
+ "web-clipper",
47
+ "browser-extension"
48
+ ],
49
+ "author": "WATT3D <WATT3D@protonmail.com>",
50
+ "license": "SEE LICENSE IN LICENSE",
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/iWhatty/page-markdown.git"
54
+ },
55
+ "homepage": "https://github.com/iWhatty/page-markdown#readme",
56
+ "bugs": {
57
+ "url": "https://github.com/iWhatty/page-markdown/issues"
58
+ },
59
+ "dependencies": {
60
+ "@mozilla/readability": "0.6.0",
61
+ "text-sanctifier": "1.3.8",
62
+ "turndown": "7.2.4"
63
+ },
64
+ "devDependencies": {
65
+ "esbuild": "0.27.0",
66
+ "linkedom": "0.18.12",
67
+ "typescript": "5.9.3"
68
+ }
69
+ }
package/robots.txt CHANGED
@@ -1,79 +1,79 @@
1
- # WATT3D — AI scraping policy
2
- #
3
- # This file enumerates known AI / LLM training and inference crawlers
4
- # that the WATT3D AI Training Rider does not consent to. See
5
- # ./ADDITIONAL_TERMS.md for the legal scope and definitions.
6
- #
7
- # Honoring robots.txt is voluntary. The rider applies whether or not
8
- # a bot respects this file. The enumeration is non-exhaustive — bots
9
- # not listed here are still bound by the rider if they perform
10
- # Training Use as defined in ADDITIONAL_TERMS.md §1.
11
-
12
- User-agent: GPTBot
13
- Disallow: /
14
-
15
- User-agent: ChatGPT-User
16
- Disallow: /
17
-
18
- User-agent: OAI-SearchBot
19
- Disallow: /
20
-
21
- User-agent: anthropic-ai
22
- Disallow: /
23
-
24
- User-agent: ClaudeBot
25
- Disallow: /
26
-
27
- User-agent: Claude-Web
28
- Disallow: /
29
-
30
- User-agent: Claude-SearchBot
31
- Disallow: /
32
-
33
- User-agent: Google-Extended
34
- Disallow: /
35
-
36
- User-agent: Applebot-Extended
37
- Disallow: /
38
-
39
- User-agent: Meta-ExternalAgent
40
- Disallow: /
41
-
42
- User-agent: Meta-ExternalFetcher
43
- Disallow: /
44
-
45
- User-agent: FacebookBot
46
- Disallow: /
47
-
48
- User-agent: CCBot
49
- Disallow: /
50
-
51
- User-agent: PerplexityBot
52
- Disallow: /
53
-
54
- User-agent: Perplexity-User
55
- Disallow: /
56
-
57
- User-agent: cohere-ai
58
- Disallow: /
59
-
60
- User-agent: Bytespider
61
- Disallow: /
62
-
63
- User-agent: Amazonbot
64
- Disallow: /
65
-
66
- User-agent: Omgilibot
67
- Disallow: /
68
-
69
- User-agent: AI2Bot
70
- Disallow: /
71
-
72
- User-agent: Diffbot
73
- Disallow: /
74
-
75
- # Everyone else: full access. Search engines and human-driven crawlers
76
- # remain welcome.
77
-
78
- User-agent: *
79
- Allow: /
1
+ # WATT3D — AI scraping policy
2
+ #
3
+ # This file enumerates known AI / LLM training and inference crawlers
4
+ # that the WATT3D AI Training Rider does not consent to. See
5
+ # ./ADDITIONAL_TERMS.md for the legal scope and definitions.
6
+ #
7
+ # Honoring robots.txt is voluntary. The rider applies whether or not
8
+ # a bot respects this file. The enumeration is non-exhaustive — bots
9
+ # not listed here are still bound by the rider if they perform
10
+ # Training Use as defined in ADDITIONAL_TERMS.md §1.
11
+
12
+ User-agent: GPTBot
13
+ Disallow: /
14
+
15
+ User-agent: ChatGPT-User
16
+ Disallow: /
17
+
18
+ User-agent: OAI-SearchBot
19
+ Disallow: /
20
+
21
+ User-agent: anthropic-ai
22
+ Disallow: /
23
+
24
+ User-agent: ClaudeBot
25
+ Disallow: /
26
+
27
+ User-agent: Claude-Web
28
+ Disallow: /
29
+
30
+ User-agent: Claude-SearchBot
31
+ Disallow: /
32
+
33
+ User-agent: Google-Extended
34
+ Disallow: /
35
+
36
+ User-agent: Applebot-Extended
37
+ Disallow: /
38
+
39
+ User-agent: Meta-ExternalAgent
40
+ Disallow: /
41
+
42
+ User-agent: Meta-ExternalFetcher
43
+ Disallow: /
44
+
45
+ User-agent: FacebookBot
46
+ Disallow: /
47
+
48
+ User-agent: CCBot
49
+ Disallow: /
50
+
51
+ User-agent: PerplexityBot
52
+ Disallow: /
53
+
54
+ User-agent: Perplexity-User
55
+ Disallow: /
56
+
57
+ User-agent: cohere-ai
58
+ Disallow: /
59
+
60
+ User-agent: Bytespider
61
+ Disallow: /
62
+
63
+ User-agent: Amazonbot
64
+ Disallow: /
65
+
66
+ User-agent: Omgilibot
67
+ Disallow: /
68
+
69
+ User-agent: AI2Bot
70
+ Disallow: /
71
+
72
+ User-agent: Diffbot
73
+ Disallow: /
74
+
75
+ # Everyone else: full access. Search engines and human-driven crawlers
76
+ # remain welcome.
77
+
78
+ User-agent: *
79
+ Allow: /
package/src/clean.js CHANGED
@@ -103,7 +103,7 @@ const stripEmphasis = (md) => replaceOutsideCode(
103
103
  /(?<![\w*])([*_])(?!\s)(.+?)(?<!\s)\1(?![\w*])/,
104
104
  (all, mark, text) => text,
105
105
  );
106
- const stripImages = (md) => replaceOutsideCode(md, /!\[([^\]]*)\]\([^)]*\)/, (all, alt) => alt);
106
+ const stripImages = (md) => replaceOutsideCode(md, INLINE_LINK, (all, bang, text) => text);
107
107
 
108
108
  /**
109
109
  * Output modes. "no_links" keeps link text. "text_only" also drops headings,