webforai 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +22 -0
  2. package/dist/bin.js +1895 -74
  3. package/dist/cjs/extract-mdast.js +58 -0
  4. package/dist/cjs/extractors/index.js +5 -0
  5. package/dist/cjs/extractors/lib/block-features.js +465 -0
  6. package/dist/cjs/extractors/lib/block-model.generated.js +45 -0
  7. package/dist/cjs/extractors/lib/block-model.js +72 -0
  8. package/dist/cjs/extractors/lib/blocks.js +212 -0
  9. package/dist/cjs/extractors/lib/code-tabs.js +191 -0
  10. package/dist/cjs/extractors/lib/constants.js +1 -1
  11. package/dist/cjs/extractors/lib/layout-tables.js +123 -0
  12. package/dist/cjs/extractors/lib/sanitize.js +144 -28
  13. package/dist/cjs/extractors/lib/takumi-signal.js +76 -0
  14. package/dist/cjs/extractors/lib/ui-chrome.js +7 -0
  15. package/dist/cjs/extractors/presets/auto.js +3 -3
  16. package/dist/cjs/extractors/presets/kiwame.js +231 -0
  17. package/dist/cjs/extractors/presets/takumi.js +34 -3
  18. package/dist/cjs/html-to-markdown.js +58 -4
  19. package/dist/cjs/html-to-mdast.js +2 -2
  20. package/dist/cjs/index.js +4 -0
  21. package/dist/cjs/mdast-handlers/custom-code-handler.js +16 -4
  22. package/dist/cjs/mdast-handlers/custom-div-handler.js +3 -3
  23. package/dist/cjs/mdast-handlers/custom-img-handler.js +10 -1
  24. package/dist/cjs/mdast-handlers/custom-table-handler.js +18 -1
  25. package/dist/cjs/mdast-handlers/table-cell-flatten.js +195 -0
  26. package/dist/cjs/mdast-handlers/twoslash.js +71 -0
  27. package/dist/cjs/metadata/index.js +4 -3
  28. package/dist/cjs/utils/hast-fast.js +1 -1
  29. package/dist/cjs/utils/parse-html.js +113 -0
  30. package/dist/cjs/utils/pre-text.js +104 -0
  31. package/dist/extract-mdast.js +58 -0
  32. package/dist/extractors/index.js +3 -0
  33. package/dist/extractors/lib/block-features.js +437 -0
  34. package/dist/extractors/lib/block-model.generated.js +21 -0
  35. package/dist/extractors/lib/block-model.js +48 -0
  36. package/dist/extractors/lib/blocks.js +185 -0
  37. package/dist/extractors/lib/code-tabs.js +168 -0
  38. package/dist/extractors/lib/constants.js +1 -1
  39. package/dist/extractors/lib/layout-tables.js +99 -0
  40. package/dist/extractors/lib/sanitize.js +142 -28
  41. package/dist/extractors/lib/takumi-signal.js +53 -0
  42. package/dist/extractors/lib/ui-chrome.js +5 -0
  43. package/dist/extractors/presets/auto.js +3 -3
  44. package/dist/extractors/presets/kiwame.js +206 -0
  45. package/dist/extractors/presets/takumi.js +34 -3
  46. package/dist/html-to-markdown.js +57 -4
  47. package/dist/html-to-mdast.js +2 -2
  48. package/dist/index.js +4 -0
  49. package/dist/mdast-handlers/custom-code-handler.js +15 -4
  50. package/dist/mdast-handlers/custom-div-handler.js +4 -4
  51. package/dist/mdast-handlers/custom-img-handler.js +10 -1
  52. package/dist/mdast-handlers/custom-table-handler.js +19 -2
  53. package/dist/mdast-handlers/table-cell-flatten.js +170 -0
  54. package/dist/mdast-handlers/twoslash.js +48 -0
  55. package/dist/metadata/index.js +4 -3
  56. package/dist/types/cli/commands/jobs/options.d.ts +2 -2
  57. package/dist/types/cli/commands/jobs/options.js.map +1 -1
  58. package/dist/types/cli/commands/webforai/options.js.map +1 -1
  59. package/dist/types/cli/commands/webforai/run.js.map +1 -1
  60. package/dist/types/cli/constants.d.ts +3 -1
  61. package/dist/types/cli/constants.js.map +1 -1
  62. package/dist/types/extract-mdast.js.map +1 -1
  63. package/dist/types/extractors/index.d.ts +1 -0
  64. package/dist/types/extractors/index.js.map +1 -1
  65. package/dist/types/extractors/lib/block-features.d.ts +43 -0
  66. package/dist/types/extractors/lib/block-features.js.map +1 -0
  67. package/dist/types/extractors/lib/block-model.d.ts +28 -0
  68. package/dist/types/extractors/lib/block-model.generated.d.ts +5 -0
  69. package/dist/types/extractors/lib/block-model.generated.js.map +1 -0
  70. package/dist/types/extractors/lib/block-model.js.map +1 -0
  71. package/dist/types/extractors/lib/blocks.d.ts +53 -0
  72. package/dist/types/extractors/lib/blocks.js.map +1 -0
  73. package/dist/types/extractors/lib/code-tabs.d.ts +30 -0
  74. package/dist/types/extractors/lib/code-tabs.js.map +1 -0
  75. package/dist/types/extractors/lib/constants.js.map +1 -1
  76. package/dist/types/extractors/lib/layout-tables.d.ts +17 -0
  77. package/dist/types/extractors/lib/layout-tables.js.map +1 -0
  78. package/dist/types/extractors/lib/sanitize.d.ts +33 -3
  79. package/dist/types/extractors/lib/sanitize.js.map +1 -1
  80. package/dist/types/extractors/lib/takumi-signal.d.ts +12 -0
  81. package/dist/types/extractors/lib/takumi-signal.js.map +1 -0
  82. package/dist/types/extractors/lib/ui-chrome.d.ts +8 -0
  83. package/dist/types/extractors/lib/ui-chrome.js.map +1 -1
  84. package/dist/types/extractors/presets/auto.d.ts +6 -1
  85. package/dist/types/extractors/presets/auto.js.map +1 -1
  86. package/dist/types/extractors/presets/kiwame.d.ts +31 -0
  87. package/dist/types/extractors/presets/kiwame.js.map +1 -0
  88. package/dist/types/extractors/presets/takumi.js.map +1 -1
  89. package/dist/types/html-to-markdown.d.ts +9 -0
  90. package/dist/types/html-to-markdown.js.map +1 -1
  91. package/dist/types/html-to-mdast.js.map +1 -1
  92. package/dist/types/index.d.ts +1 -1
  93. package/dist/types/index.js.map +1 -1
  94. package/dist/types/mdast-handlers/custom-code-handler.d.ts +7 -0
  95. package/dist/types/mdast-handlers/custom-code-handler.js.map +1 -1
  96. package/dist/types/mdast-handlers/custom-div-handler.js.map +1 -1
  97. package/dist/types/mdast-handlers/custom-img-handler.js.map +1 -1
  98. package/dist/types/mdast-handlers/custom-table-handler.js.map +1 -1
  99. package/dist/types/mdast-handlers/table-cell-flatten.d.ts +18 -0
  100. package/dist/types/mdast-handlers/table-cell-flatten.js.map +1 -0
  101. package/dist/types/mdast-handlers/twoslash.d.ts +14 -0
  102. package/dist/types/mdast-handlers/twoslash.js.map +1 -0
  103. package/dist/types/metadata/index.js.map +1 -1
  104. package/dist/types/utils/hast-fast.d.ts +1 -1
  105. package/dist/types/utils/hast-fast.js.map +1 -1
  106. package/dist/types/utils/parse-html.d.ts +33 -0
  107. package/dist/types/utils/parse-html.js.map +1 -0
  108. package/dist/types/utils/pre-text.d.ts +12 -0
  109. package/dist/types/utils/pre-text.js.map +1 -0
  110. package/dist/utils/hast-fast.js +1 -1
  111. package/dist/utils/parse-html.js +87 -0
  112. package/dist/utils/pre-text.js +81 -0
  113. package/package.json +3 -1
@@ -0,0 +1,206 @@
1
+ // src/extractors/presets/kiwame.ts
2
+ import { MetricsCollector, cloneHast, findElement, isElement, walk } from "../../utils/hast-fast.js";
3
+ import { FEATURE_COUNT, STACKED_FEATURE_COUNT, blockFeatures, stackedFeatures } from "../lib/block-features.js";
4
+ import { isUsableModel, scoreBlocks } from "../lib/block-model.js";
5
+ import { BLOCK_MODEL, BLOCK_STACK_MODEL } from "../lib/block-model.generated.js";
6
+ import { BLOCK_TAGS, segmentBlocks } from "../lib/blocks.js";
7
+ import { unwrapLayoutTables } from "../lib/layout-tables.js";
8
+ import { REFERENCE_HEADING, cleanContent, stripNonContent } from "../lib/sanitize.js";
9
+ import { takumiSelection } from "../lib/takumi-signal.js";
10
+ import { takumiExtractor } from "./takumi.js";
11
+ var DEFAULT_THRESHOLD = 0.47;
12
+ var findBody = (hast) => findElement(hast, (element) => element.tagName === "body") ?? hast;
13
+ var completeTables = (kept, blocks) => {
14
+ const rows = /* @__PURE__ */ new Set();
15
+ const tables = /* @__PURE__ */ new Set();
16
+ for (const block of blocks) {
17
+ if (!kept.has(block.owner)) {
18
+ continue;
19
+ }
20
+ const { row, table } = enclosingTable(block.frame);
21
+ if (row) {
22
+ rows.add(row);
23
+ }
24
+ if (table) {
25
+ tables.add(table);
26
+ }
27
+ }
28
+ for (const table of tables) {
29
+ const first = findElement(table, (element) => element.tagName === "tr");
30
+ if (first) {
31
+ rows.add(first);
32
+ }
33
+ }
34
+ for (const block of blocks) {
35
+ const { row } = enclosingTable(block.frame);
36
+ if (row && rows.has(row)) {
37
+ kept.add(block.owner);
38
+ }
39
+ }
40
+ };
41
+ var enclosingTable = (start) => {
42
+ let row;
43
+ for (let frame = start; frame; frame = frame.parent) {
44
+ const tag = frame.element.tagName;
45
+ if (tag === "tr" && !row) {
46
+ row = frame.element;
47
+ } else if (tag === "table") {
48
+ return { row, table: frame.element };
49
+ }
50
+ }
51
+ return { row };
52
+ };
53
+ var HEADING_LOOKAHEAD = 3;
54
+ var keepSectionHeadings = (kept, blocks) => {
55
+ let seenKept = false;
56
+ for (let index = 0; index < blocks.length; index++) {
57
+ const block = blocks[index];
58
+ if (kept.has(block.owner)) {
59
+ seenKept = true;
60
+ continue;
61
+ }
62
+ if (!(seenKept && /^h[1-6]$/.test(block.owner.tagName))) {
63
+ continue;
64
+ }
65
+ for (let next = index + 1; next < Math.min(blocks.length, index + 1 + HEADING_LOOKAHEAD); next++) {
66
+ if (/^h[1-6]$/.test(blocks[next].owner.tagName)) {
67
+ break;
68
+ }
69
+ if (kept.has(blocks[next].owner)) {
70
+ kept.add(block.owner);
71
+ break;
72
+ }
73
+ }
74
+ }
75
+ };
76
+ var CODE_IN_CONTENT_THRESHOLD = 0.2;
77
+ var CODE_CONTEXT = 6;
78
+ var keepCodeInContent = (kept, blocks, probabilities) => {
79
+ const keptIndex = blocks.map((block) => kept.has(block.owner));
80
+ const near = (from, step) => {
81
+ for (let distance = 1; distance <= CODE_CONTEXT; distance++) {
82
+ const index = from + step * distance;
83
+ if (index < 0 || index >= blocks.length) {
84
+ return false;
85
+ }
86
+ if (keptIndex[index]) {
87
+ return true;
88
+ }
89
+ }
90
+ return false;
91
+ };
92
+ for (const block of blocks) {
93
+ if (block.owner.tagName === "pre" && !keptIndex[block.index] && probabilities[block.index] >= CODE_IN_CONTENT_THRESHOLD && near(block.index, -1) && near(block.index, 1)) {
94
+ kept.add(block.owner);
95
+ }
96
+ }
97
+ };
98
+ var listOf = (block) => {
99
+ if (block.owner.tagName !== "li") {
100
+ return void 0;
101
+ }
102
+ for (let frame = block.frame.parent; frame; frame = frame.parent) {
103
+ const tag = frame.element.tagName;
104
+ if (tag === "ul" || tag === "ol") {
105
+ return frame.element;
106
+ }
107
+ }
108
+ return void 0;
109
+ };
110
+ var keepAuthoredLists = (kept, blocks) => {
111
+ let seenKept = false;
112
+ for (let index = 0; index < blocks.length - 1; index++) {
113
+ const block = blocks[index];
114
+ const introducesList = kept.has(block.owner) && /[::]\s*$/.test(block.text) || seenKept && /^h[1-6]$/.test(block.owner.tagName) && REFERENCE_HEADING.test(block.text.trim());
115
+ seenKept ||= kept.has(block.owner);
116
+ const list = listOf(blocks[index + 1]);
117
+ if (!(introducesList && list)) {
118
+ continue;
119
+ }
120
+ kept.add(block.owner);
121
+ for (let next = index + 1; next < blocks.length && listOf(blocks[next]) === list; next++) {
122
+ kept.add(blocks[next].owner);
123
+ }
124
+ }
125
+ };
126
+ var pruneToBlocks = (element, kept, ownerKept) => {
127
+ const isBlock = element.type === "element" && (BLOCK_TAGS.has(element.tagName) || element.tagName === "pre");
128
+ const selfKept = isBlock ? kept.has(element) : ownerKept;
129
+ if (element.type === "element" && element.tagName === "pre") {
130
+ return selfKept;
131
+ }
132
+ let keepChildren = false;
133
+ const children = [];
134
+ for (const child of element.children) {
135
+ if (child.type === "element") {
136
+ if (pruneToBlocks(child, kept, selfKept)) {
137
+ children.push(child);
138
+ keepChildren = true;
139
+ }
140
+ } else if (child.type === "text") {
141
+ if (selfKept) {
142
+ children.push(child);
143
+ }
144
+ } else {
145
+ children.push(child);
146
+ }
147
+ }
148
+ element.children = children;
149
+ return selfKept || keepChildren;
150
+ };
151
+ var createKiwameExtractor = (options = {}) => {
152
+ const { threshold = DEFAULT_THRESHOLD, model = BLOCK_MODEL, cleanup = true } = options;
153
+ const stackModel = options.stackModel === void 0 ? BLOCK_STACK_MODEL : options.stackModel;
154
+ const usable = isUsableModel(model);
155
+ const stackUsable = stackModel ? isUsableModel(stackModel, STACKED_FEATURE_COUNT) : false;
156
+ return (params) => {
157
+ if (!usable) {
158
+ return takumiExtractor(params);
159
+ }
160
+ const { hast, lang, owned } = params;
161
+ const body = owned ? findBody(hast) : cloneHast(findBody(hast));
162
+ stripNonContent(body);
163
+ const blocks = segmentBlocks(body);
164
+ if (blocks.length === 0 || !(isElement(body) || body.type === "root")) {
165
+ return body;
166
+ }
167
+ const collector = new MetricsCollector();
168
+ const takumiKept = takumiSelection(body, lang, params.url);
169
+ const features = blockFeatures({ root: body, blocks, collector, lang, takumiKept });
170
+ let probabilities = scoreBlocks(features, blocks.length, model, FEATURE_COUNT);
171
+ if (stackModel && stackUsable) {
172
+ const stacked = stackedFeatures(blocks, features, probabilities);
173
+ probabilities = scoreBlocks(stacked, blocks.length, stackModel, STACKED_FEATURE_COUNT);
174
+ }
175
+ const kept = /* @__PURE__ */ new Set();
176
+ for (const block of blocks) {
177
+ if (probabilities[block.index] >= threshold) {
178
+ kept.add(block.owner);
179
+ }
180
+ }
181
+ if (kept.size === 0) {
182
+ return takumiExtractor({ hast: body, lang, url: params.url, owned: true });
183
+ }
184
+ keepCodeInContent(kept, blocks, probabilities);
185
+ keepSectionHeadings(kept, blocks);
186
+ keepAuthoredLists(kept, blocks);
187
+ completeTables(kept, blocks);
188
+ pruneToBlocks(body, kept, false);
189
+ unwrapLayoutTables(body);
190
+ if (!cleanup) {
191
+ return body;
192
+ }
193
+ walk(body, (node) => {
194
+ if (isElement(node) && (node.tagName === "form" || node.tagName === "fieldset")) {
195
+ node.tagName = "div";
196
+ }
197
+ });
198
+ return cleanContent(body, new MetricsCollector());
199
+ };
200
+ };
201
+ var kiwameExtractor = createKiwameExtractor();
202
+ export {
203
+ DEFAULT_THRESHOLD,
204
+ createKiwameExtractor,
205
+ kiwameExtractor
206
+ };
@@ -12,6 +12,7 @@ import { cleanContent, findCleanupTargets, findUnlikelyElements, stripNonContent
12
12
  import { buildScoringContext, climbToArticleBoundary, collectArticleNodes, scoreCandidates } from "../lib/score.js";
13
13
  import { truncateTrailingBoilerplate } from "../lib/terminators.js";
14
14
  var MIN_RETAINED_FRACTION = 0.25;
15
+ var FURNITURE_LINK_DENSITY = 0.5;
15
16
  var SEMANTIC_SELECTORS = [
16
17
  (element) => stringProperty(element, "itemprop")?.includes("articleBody") ?? false,
17
18
  (element) => element.tagName === "article",
@@ -49,6 +50,31 @@ var collectMatching = (tree, matches, into) => {
49
50
  }
50
51
  }
51
52
  };
53
+ var containsTag = (element, tagName) => element.children.some((child) => isElement(child) && (child.tagName === tagName || containsTag(child, tagName)));
54
+ var removesEveryTitle = (root, doomed) => {
55
+ if (!doomed.some((element) => containsTag(element, "h1"))) {
56
+ return false;
57
+ }
58
+ const doomedSet = new Set(doomed);
59
+ let survivor = false;
60
+ const visit = (node) => {
61
+ if (survivor || !("children" in node)) {
62
+ return;
63
+ }
64
+ for (const child of node.children) {
65
+ if (!isElement(child) || doomedSet.has(child)) {
66
+ continue;
67
+ }
68
+ if (child.tagName === "h1") {
69
+ survivor = true;
70
+ return;
71
+ }
72
+ visit(child);
73
+ }
74
+ };
75
+ visit(root);
76
+ return !survivor;
77
+ };
52
78
  var asRootOf = (nodes) => ({ type: "root", children: nodes });
53
79
  var scoredSelection = (tree, collector, lang) => {
54
80
  const context = buildScoringContext(tree, collector, lang);
@@ -75,9 +101,14 @@ var takumiExtractor = (params) => {
75
101
  const shortcut = semanticShortcut(body, collector, minLength);
76
102
  const searchRoot = shortcut ?? body;
77
103
  const searchText = collector.textLength(searchRoot);
78
- const doomed = findUnlikelyElements(searchRoot);
79
- const doomedText = doomed.reduce((sum, element) => sum + collector.metrics(element).text, 0);
80
- if (searchText - doomedText > Math.max(minLength, searchText * MIN_RETAINED_FRACTION)) {
104
+ let doomed = findUnlikelyElements(searchRoot);
105
+ const textOf = (elements) => elements.reduce((sum, element) => sum + collector.metrics(element).text, 0);
106
+ let acceptable = searchText - textOf(doomed) > Math.max(minLength, searchText * MIN_RETAINED_FRACTION);
107
+ if (!acceptable) {
108
+ doomed = findUnlikelyElements(searchRoot, (element) => collector.linkDensity(element) < FURNITURE_LINK_DENSITY);
109
+ acceptable = doomed.length > 0 && searchText - textOf(doomed) >= minLength && !removesEveryTitle(searchRoot, doomed);
110
+ }
111
+ if (acceptable) {
81
112
  const doomedSet = new Set(doomed);
82
113
  pruneInPlace(searchRoot, (node) => !(isElement(node) && doomedSet.has(node)));
83
114
  collector = new MetricsCollector();
@@ -1,12 +1,12 @@
1
1
  // src/html-to-markdown.ts
2
- import { fromHtml } from "hast-util-from-html";
2
+ import { parseHtml } from "./utils/parse-html.js";
3
3
  import { htmlToMdast } from "./html-to-mdast.js";
4
4
  import { mdastToMarkdown } from "./mdast-to-markdown.js";
5
5
  import { extractMetadata, toFrontmatter } from "./metadata/index.js";
6
6
  import { getLangFromStr } from "./utils/hast-utils.js";
7
7
  var htmlToMarkdownWithMetadata = (htmlOrHast, options) => {
8
8
  const { baseUrl, frontmatter, title, formatting: toMarkdownOptions, ...toMdastOptions } = options || {};
9
- const hast = typeof htmlOrHast === "string" ? fromHtml(htmlOrHast, { fragment: true }) : htmlOrHast;
9
+ const hast = typeof htmlOrHast === "string" ? parseHtml(htmlOrHast, { fragment: true }) : htmlOrHast;
10
10
  const isOwned = typeof htmlOrHast === "string";
11
11
  const declaredLang = typeof htmlOrHast === "string" ? getLangFromStr(htmlOrHast) : void 0;
12
12
  const metadata = extractMetadata(hast);
@@ -20,10 +20,54 @@ var htmlToMarkdownWithMetadata = (htmlOrHast, options) => {
20
20
  url: toMdastOptions.url ?? metadata.canonicalUrl
21
21
  });
22
22
  const body = mdastToMarkdown(mdast, { baseUrl, ...toMarkdownOptions });
23
- const titled = title === false ? body : withTitle(body, metadata.title);
23
+ const pageUrl = toMdastOptions.url ?? metadata.canonicalUrl;
24
+ const titled = title === false ? body : withTitle(body, headingTitle(metadata.title, metadata.siteName, pageUrl));
24
25
  const markdown = frontmatter ? `${toFrontmatter(metadata)}${titled}` : titled;
25
26
  return { markdown, metadata };
26
27
  };
28
+ var TITLE_SEPARATOR = /\s+[-|–—·•:]\s+|\s*[||]\s*/;
29
+ var SITE_SECTION = /^(docs|documentation|blog|news|wiki|developers?|devblog|help|support|reference)$/;
30
+ var compact = (value) => value.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
31
+ var siteNames = (siteName, url) => {
32
+ const names = siteName ? siteName.split(TITLE_SEPARATOR).map(compact) : [];
33
+ try {
34
+ const labels = url ? new URL(url).hostname.split(".") : [];
35
+ const meaningful = labels.slice(0, -1).filter((label) => label !== "www");
36
+ names.push(...meaningful.map(compact), compact(meaningful.join("")), compact(labels.join("").replace(/^www/, "")));
37
+ } catch {
38
+ }
39
+ return names.filter((name) => name.length >= 3);
40
+ };
41
+ var headingTitle = (title, siteName, url) => {
42
+ if (!title) {
43
+ return title;
44
+ }
45
+ const segments = title.split(TITLE_SEPARATOR);
46
+ if (segments.length < 2) {
47
+ return title;
48
+ }
49
+ const names = siteNames(siteName, url);
50
+ if (names.length === 0) {
51
+ return title;
52
+ }
53
+ const isSite = (segment) => {
54
+ const value = compact(segment);
55
+ return value.length > 0 && names.some((name) => value === name || value.startsWith(name) && SITE_SECTION.test(value.slice(name.length)));
56
+ };
57
+ const last = segments[segments.length - 1];
58
+ if (isSite(last)) {
59
+ return stripSegment(title, last, "end");
60
+ }
61
+ if (siteName && compact(segments[0]) === compact(siteName)) {
62
+ return stripSegment(title, segments[0], "start");
63
+ }
64
+ return title;
65
+ };
66
+ var stripSegment = (title, segment, edge) => {
67
+ const rest = edge === "end" ? title.slice(0, title.lastIndexOf(segment)) : title.slice(segment.length);
68
+ const trimmed = rest.replace(edge === "end" ? /[\s\-|–—·•:|]+$/ : /^[\s\-|–—·•:|]+/, "");
69
+ return trimmed.length > 0 ? trimmed : title;
70
+ };
27
71
  var TITLE_LOOKAHEAD = 400;
28
72
  var withTitle = (body, title) => {
29
73
  if (!title) {
@@ -33,16 +77,25 @@ var withTitle = (body, title) => {
33
77
  if (/^#\s+\S/m.test(opening)) {
34
78
  return body;
35
79
  }
36
- if (normalizeForCompare(opening).includes(normalizeForCompare(title))) {
80
+ if (mentionsTitle(opening, title)) {
37
81
  return body;
38
82
  }
39
83
  return `# ${title}
40
84
 
41
85
  ${body}`;
42
86
  };
87
+ var MIN_EMBEDDED_TITLE_LENGTH = 24;
88
+ var mentionsTitle = (opening, title) => {
89
+ const wanted = normalizeForCompare(title);
90
+ if (wanted.length >= MIN_EMBEDDED_TITLE_LENGTH) {
91
+ return normalizeForCompare(opening).includes(wanted);
92
+ }
93
+ return opening.split("\n").some((line) => normalizeForCompare(line.replace(/[#*_`>]/g, "")) === wanted);
94
+ };
43
95
  var normalizeForCompare = (value) => value.replace(/\s+/g, " ").trim().toLowerCase();
44
96
  var htmlToMarkdown = (htmlOrHast, options) => htmlToMarkdownWithMetadata(htmlOrHast, options).markdown;
45
97
  export {
98
+ headingTitle,
46
99
  htmlToMarkdown,
47
100
  htmlToMarkdownWithMetadata
48
101
  };
@@ -1,6 +1,6 @@
1
1
  // src/html-to-mdast.ts
2
- import { fromHtml } from "hast-util-from-html";
3
2
  import { toMdast } from "hast-util-to-mdast";
3
+ import { parseHtml } from "./utils/parse-html.js";
4
4
  import { extractMdast } from "./extract-mdast.js";
5
5
  import { pipeExtractors } from "./extractors/index.js";
6
6
  import { customAHandler } from "./mdast-handlers/custom-a-handler.js";
@@ -16,7 +16,7 @@ import { cloneHast } from "./utils/hast-fast.js";
16
16
  import { getLangFromHast, getLangFromStr, getUrlFromHast } from "./utils/hast-utils.js";
17
17
  var htmlToMdast = (htmlOrHast, options) => {
18
18
  const { extractors, url: defaultUrl, lang: defaultLang } = options || {};
19
- const [lang, hast] = typeof htmlOrHast === "string" ? [defaultLang || getLangFromStr(htmlOrHast), fromHtml(htmlOrHast, { fragment: true })] : [defaultLang || getLangFromHast(htmlOrHast), htmlOrHast];
19
+ const [lang, hast] = typeof htmlOrHast === "string" ? [defaultLang || getLangFromStr(htmlOrHast), parseHtml(htmlOrHast, { fragment: true })] : [defaultLang || getLangFromHast(htmlOrHast), htmlOrHast];
20
20
  const isOwned = typeof htmlOrHast === "string" || (options?.owned ?? false);
21
21
  const url = defaultUrl || getUrlFromHast(hast);
22
22
  const extractedHast = pipeExtractors({ hast: isOwned ? hast : cloneHast(hast), lang, url, owned: true }, extractors);
package/dist/index.js CHANGED
@@ -11,6 +11,8 @@ import {
11
11
  autoExtractor,
12
12
  createAutoExtractor,
13
13
  takumiExtractor,
14
+ kiwameExtractor,
15
+ createKiwameExtractor,
14
16
  minimalFilter
15
17
  } from "./extractors/index.js";
16
18
  import {
@@ -42,6 +44,7 @@ export {
42
44
  autoExtractor,
43
45
  buildPageSignature,
44
46
  createAutoExtractor,
47
+ createKiwameExtractor,
45
48
  defineSelectorAdapter,
46
49
  detectClientShell,
47
50
  extractMetaRefresh,
@@ -49,6 +52,7 @@ export {
49
52
  htmlToMarkdown,
50
53
  htmlToMarkdownWithMetadata,
51
54
  htmlToMdast,
55
+ kiwameExtractor,
52
56
  mdastSplitter,
53
57
  mdastToMarkdown,
54
58
  minimalFilter,
@@ -1,8 +1,9 @@
1
1
  // src/mdast-handlers/custom-code-handler.ts
2
- import { toText } from "hast-util-to-text";
3
2
  import { trimTrailingLines } from "trim-trailing-lines";
4
3
  import { detectLanguage } from "../utils/detect-code-lang.js";
5
4
  import { classList, isElement } from "../utils/hast-fast.js";
5
+ import { preText } from "../utils/pre-text.js";
6
+ import { cleanTwoslash } from "./twoslash.js";
6
7
  var LANGUAGE_MATCH_REGEX = [/^language-(\S+)$/, /^highlight-source-(\S+)$/, /^CodeBlock--language-(\S+)$/];
7
8
  var LANGUAGE_ATTRIBUTES = ["dataLanguage", "dataLang"];
8
9
  var attributeLanguage = (element) => {
@@ -15,16 +16,26 @@ var attributeLanguage = (element) => {
15
16
  return void 0;
16
17
  };
17
18
  var codeLanguage = (...elements) => elements.flatMap(classList).map((className) => LANGUAGE_MATCH_REGEX.map((regex) => className.match(regex)).find(Boolean)?.[1]).find(Boolean) ?? elements.map(attributeLanguage).find(Boolean);
19
+ var codeMeta = (pre) => {
20
+ const title = pre.properties?.dataTitle;
21
+ if (typeof title !== "string") {
22
+ return null;
23
+ }
24
+ const clean = title.replace(/["\r\n]+/g, " ").trim();
25
+ return clean ? `title="${clean}"` : null;
26
+ };
18
27
  var customCodeHandler = (state, node) => {
28
+ const twoslashLang = cleanTwoslash(node);
19
29
  const code = node.children.find((child) => isElement(child) && child.tagName === "code");
20
- const codeValue = trimTrailingLines(toText(node));
21
- const classLang = codeLanguage(...isElement(code) ? [code, node] : [node]);
30
+ const codeValue = trimTrailingLines(preText(node));
31
+ const classLang = twoslashLang || codeLanguage(...isElement(code) ? [code, node] : [node]);
22
32
  const lang = classLang || detectLanguage(codeValue) || null;
23
- const result = { type: "code", lang, meta: null, value: codeValue };
33
+ const result = { type: "code", lang, meta: codeMeta(node), value: codeValue };
24
34
  state.patch(node, result);
25
35
  return result;
26
36
  };
27
37
  export {
28
38
  codeLanguage,
39
+ codeMeta,
29
40
  customCodeHandler
30
41
  };
@@ -2,11 +2,11 @@
2
2
  import { select } from "hast-util-select";
3
3
  import { defaultHandlers } from "hast-util-to-mdast";
4
4
  import { toString as hastToString } from "hast-util-to-string";
5
- import { toText } from "hast-util-to-text";
6
5
  import { trimTrailingLines } from "trim-trailing-lines";
7
6
  import { detectLanguage } from "../utils/detect-code-lang.js";
8
7
  import { classList, collectElements, isElement } from "../utils/hast-fast.js";
9
- import { codeLanguage } from "./custom-code-handler.js";
8
+ import { preText } from "../utils/pre-text.js";
9
+ import { codeLanguage, codeMeta } from "./custom-code-handler.js";
10
10
  var CODE_BLOCK_REGEX = /highlight-source|language-|codegroup|codeblock|code-block/i;
11
11
  var CODE_FILENAME_SELECTORS = "[class*='fileName'],[class*='fileName'],[class*='title'],[class*='Title']";
12
12
  var customDivHandler = (state, node) => {
@@ -17,13 +17,13 @@ var customDivHandler = (state, node) => {
17
17
  const codeBlocks = collectElements(node, (element) => element.tagName === "pre");
18
18
  const codeBlock = codeBlocks.length === 1 ? codeBlocks[0] : void 0;
19
19
  if (codeBlock) {
20
- const codeValue = trimTrailingLines(toText(codeBlock));
20
+ const codeValue = trimTrailingLines(preText(codeBlock));
21
21
  const filenameElement = select(CODE_FILENAME_SELECTORS, node);
22
22
  const fileLang = filenameElement ? hastToString(filenameElement).match(/\.(\w+)$/)?.[1] : null;
23
23
  const code = codeBlock.children.find((child) => isElement(child) && child.tagName === "code");
24
24
  const classLang = codeLanguage(...isElement(code) ? [code, codeBlock, node] : [codeBlock, node]);
25
25
  const lang = fileLang || classLang || detectLanguage(codeValue) || null;
26
- const result = { type: "code", lang, meta: null, value: codeValue };
26
+ const result = { type: "code", lang, meta: codeMeta(codeBlock), value: codeValue };
27
27
  state.patch(node, result);
28
28
  return result;
29
29
  }
@@ -12,11 +12,20 @@ var hasNoUsableSource = (node) => {
12
12
  }
13
13
  return false;
14
14
  };
15
+ var PLACEHOLDER_FILE = /^(?:[\w-]*[-_.])?placeholder[\w-]*\.(?:gif|png|svg|webp|jpe?g)$|^(?:spacer|blank|transparent|pixel)\.(?:gif|png)$/i;
16
+ var isUndescribedPlaceholder = (node) => {
17
+ if (stringProperty(node, "alt")?.trim() || stringProperty(node, "title")?.trim()) {
18
+ return false;
19
+ }
20
+ const src = stringProperty(node, "src") ?? "";
21
+ const file = src.split(/[?#]/)[0].split("/").pop() ?? "";
22
+ return PLACEHOLDER_FILE.test(file);
23
+ };
15
24
  var customImgHandler = (options) => (state, node) => {
16
25
  if (options?.hideImage) {
17
26
  return void 0;
18
27
  }
19
- if (hasNoUsableSource(node)) {
28
+ if (hasNoUsableSource(node) || isUndescribedPlaceholder(node)) {
20
29
  return void 0;
21
30
  }
22
31
  const image = defaultHandlers.image(state, node);
@@ -1,7 +1,8 @@
1
1
  // src/mdast-handlers/custom-table-handler.ts
2
2
  import { defaultHandlers } from "hast-util-to-mdast";
3
3
  import { toText } from "hast-util-to-text";
4
- import { isElement, numericProperty, walk } from "../utils/hast-fast.js";
4
+ import { findElement, isElement, numericProperty, walk } from "../utils/hast-fast.js";
5
+ import { flattenTableCells, tableHasCodeSamples, unfoldTable } from "./table-cell-flatten.js";
5
6
  var MAX_SPAN = 1e3;
6
7
  var clampSpans = (node) => {
7
8
  walk(node, (candidate) => {
@@ -16,6 +17,14 @@ var clampSpans = (node) => {
16
17
  }
17
18
  });
18
19
  };
20
+ var startsWithHeaderRow = (table) => {
21
+ const row = findElement(table, (element) => element.tagName === "tr");
22
+ if (!row) {
23
+ return false;
24
+ }
25
+ const cells = row.children.filter(isElement);
26
+ return cells.length > 0 && cells.every((cell) => cell.tagName === "th");
27
+ };
19
28
  var customTableHandler = (options) => (state, node) => {
20
29
  if (options?.asText) {
21
30
  const paragraph = { type: "paragraph", children: [{ type: "text", value: toText(node) }] };
@@ -23,7 +32,15 @@ var customTableHandler = (options) => (state, node) => {
23
32
  return paragraph;
24
33
  }
25
34
  clampSpans(node);
26
- return defaultHandlers.table(state, node);
35
+ const result = defaultHandlers.table(state, node);
36
+ if (!result || Array.isArray(result) || result.type !== "table") {
37
+ return result;
38
+ }
39
+ if (tableHasCodeSamples(result)) {
40
+ return unfoldTable(result, startsWithHeaderRow(node));
41
+ }
42
+ flattenTableCells(result);
43
+ return result;
27
44
  };
28
45
  export {
29
46
  customTableHandler