@mintlify/scraping 4.0.1161 → 4.0.1163

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mintlify/scraping",
3
- "version": "4.0.1161",
3
+ "version": "4.0.1163",
4
4
  "description": "Scrape documentation frameworks to Mintlify docs",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -43,7 +43,7 @@
43
43
  "format:check": "oxfmt --check"
44
44
  },
45
45
  "dependencies": {
46
- "@mintlify/common": "1.0.1288",
46
+ "@mintlify/common": "1.0.1290",
47
47
  "fast-xml-parser": "5.7.3",
48
48
  "fs-extra": "11.1.1",
49
49
  "graphql": "16.11.0",
@@ -62,9 +62,9 @@
62
62
  "zod": "3.24.0"
63
63
  },
64
64
  "devDependencies": {
65
- "@mintlify/models": "0.0.376",
65
+ "@mintlify/models": "0.0.377",
66
66
  "@mintlify/ts-config": "2.0.2",
67
- "@mintlify/validation": "0.1.937",
67
+ "@mintlify/validation": "0.1.939",
68
68
  "@tsconfig/recommended": "1.0.2",
69
69
  "@types/hast": "3.0.4",
70
70
  "@types/mdast": "4.0.4",
@@ -87,5 +87,5 @@
87
87
  "optional": true
88
88
  }
89
89
  },
90
- "gitHead": "06a4673787feb66d1471add9eca97ffec1fda176"
90
+ "gitHead": "0873aa5354dd9084254ffa50e997d84966aa1d65"
91
91
  }
@@ -1,10 +1,23 @@
1
1
  import type { Root as HastRoot, Element } from 'hast';
2
- import type { Root as MdastRoot } from 'mdast';
2
+ import type {
3
+ Definition,
4
+ Heading,
5
+ Html,
6
+ Image,
7
+ ImageReference,
8
+ Link,
9
+ LinkReference,
10
+ Paragraph,
11
+ PhrasingContent,
12
+ Root as MdastRoot,
13
+ Text,
14
+ } from 'mdast';
3
15
  import remarkGfm from 'remark-gfm';
4
16
  import remarkMdx from 'remark-mdx';
5
17
  import remarkParse from 'remark-parse';
6
18
  import remarkStringify from 'remark-stringify';
7
19
  import { unified, type Plugin } from 'unified';
20
+ import type { Node, Parent } from 'unist';
8
21
  import { visit } from 'unist-util-visit';
9
22
 
10
23
  import { convertHeaderLinksToText } from '../components/link.js';
@@ -80,17 +93,191 @@ export function htmlToMdx(html: string, rewriteLink?: RewriteLink): string {
80
93
  return hastToMdx(hast, rewriteLink);
81
94
  }
82
95
 
83
- export function markdownToMdx(markdown: string): string {
84
- const mdastTree = unified().use(remarkParse).use(remarkGfm).parse(markdown) as MdastRoot;
85
- visit(mdastTree, 'html', (node: { type: string; value: string }) => {
86
- node.type = 'text';
87
- node.value = node.value.replace(/<[^>]*>/g, '');
96
+ // Raw HTML is dropped and its text kept. An HTML block becomes ordinary
97
+ // paragraphs (one per blank-line-separated run, since <pre> and comments may
98
+ // span blank lines), so its lines are subject to the same ESM escaping as any
99
+ // paragraph and keep their block boundaries; inline HTML becomes text within
100
+ // its paragraph, merged with its neighbours so a paragraph that opens with a
101
+ // tag still starts with its first word.
102
+ export function parseMarkdown(markdown: string, minHeadingDepth?: Heading['depth']): MdastRoot {
103
+ // CommonMark treats \r\n and a bare \r as line endings; normalising them up
104
+ // front means every later split and check only has to know about \n.
105
+ const normalized = markdown.replace(/\r\n?/g, '\n');
106
+ const mdastTree = unified().use(remarkParse).use(remarkGfm).parse(normalized) as MdastRoot;
107
+ visit(mdastTree, 'html', (node: Html, index, parent) => {
108
+ const value = stripTags(node.value);
109
+ if (
110
+ !parent ||
111
+ index === undefined ||
112
+ parent.type === 'paragraph' ||
113
+ parent.type === 'heading'
114
+ ) {
115
+ Object.assign(node, { type: 'text', value });
116
+ return;
117
+ }
118
+ const paragraphs: Paragraph[] = value
119
+ .split(/\n[ \t]*\n/)
120
+ .map((chunk) => chunk.trim())
121
+ .filter(Boolean)
122
+ .map(
123
+ (chunk): Paragraph => ({
124
+ type: 'paragraph',
125
+ children: [{ type: 'text', value: chunk }],
126
+ data: { htmlBlock: true } as Paragraph['data'],
127
+ })
128
+ );
129
+ parent.children.splice(index, 1, ...paragraphs);
130
+ return index + paragraphs.length;
88
131
  });
89
- return stringifyMdast(mdastTree);
132
+ visit(mdastTree, 'paragraph', (node: Paragraph) => {
133
+ node.children = mergeText(node.children);
134
+ });
135
+ if (minHeadingDepth) {
136
+ visit(mdastTree, 'heading', (node: Heading) => {
137
+ node.depth = node.depth < minHeadingDepth ? minHeadingDepth : node.depth;
138
+ });
139
+ }
140
+ return mdastTree;
141
+ }
142
+
143
+ // Removes every `<...>` span in one pass: from the first unmatched `<` to the
144
+ // next `>`, the same spans as /<[^>]*>/g, without rescanning to the end of the
145
+ // text for each `<` that never closes.
146
+ function stripTags(value: string): string {
147
+ let out = '';
148
+ let last = 0;
149
+ let open = -1;
150
+ for (let i = 0; i < value.length; i += 1) {
151
+ if (value[i] === '<' && open === -1) open = i;
152
+ else if (value[i] === '>' && open !== -1) {
153
+ out += value.slice(last, open);
154
+ last = i + 1;
155
+ open = -1;
156
+ }
157
+ }
158
+ return out + value.slice(last);
159
+ }
160
+
161
+ // Paragraphs made from an HTML block, such as a centred <h1> title or a badge
162
+ // row, which a README description should skip.
163
+ export function isHtmlParagraph(node: Paragraph): boolean {
164
+ return (node.data as { htmlBlock?: boolean } | undefined)?.htmlBlock === true;
165
+ }
166
+
167
+ // Joins adjacent text nodes and drops empty ones; the whitespace a removed tag
168
+ // leaves at either end of the paragraph is not content either.
169
+ function mergeText(children: PhrasingContent[]): PhrasingContent[] {
170
+ const merged: PhrasingContent[] = [];
171
+ for (const child of children) {
172
+ const previous = merged[merged.length - 1];
173
+ if (child.type === 'text' && previous?.type === 'text') {
174
+ previous.value += child.value;
175
+ } else if (child.type !== 'text' || child.value !== '') {
176
+ merged.push(child);
177
+ }
178
+ }
179
+ const first = merged[0];
180
+ if (first?.type === 'text') first.value = first.value.trimStart();
181
+ const last = merged[merged.length - 1];
182
+ if (last?.type === 'text') last.value = last.value.trimEnd();
183
+ return merged.filter((child) => child.type !== 'text' || child.value !== '');
184
+ }
185
+
186
+ // The text a reader sees for a node: text and code verbatim, an image's alt,
187
+ // a space for a hard line break, nothing for raw HTML.
188
+ export function plainText(node: Node): string {
189
+ if (node.type === 'text' || node.type === 'inlineCode') return (node as Text).value;
190
+ if (node.type === 'break') return ' ';
191
+ if (node.type === 'image' || node.type === 'imageReference') {
192
+ return (node as Image).alt ?? '';
193
+ }
194
+ if ('children' in node) {
195
+ return (node as Parent).children.map(plainText).join('');
196
+ }
197
+ return '';
198
+ }
199
+
200
+ // A paragraph with words of its own, as opposed to one made only of badges,
201
+ // images, or links, which is how most READMEs open.
202
+ export function isProseParagraph(node: Paragraph): boolean {
203
+ return node.children.some(
204
+ (child) =>
205
+ (child.type === 'text' && child.value.trim() !== '') ||
206
+ child.type === 'inlineCode' ||
207
+ ((child.type === 'emphasis' || child.type === 'strong' || child.type === 'delete') &&
208
+ plainText(child).trim() !== '')
209
+ );
210
+ }
211
+
212
+ // Rewrites every link, image, and definition URL, or drops the node when the
213
+ // callback returns undefined: a link becomes its text, an image its alt, and a
214
+ // definition disappears along with the references that pointed at it.
215
+ export function rewriteMdastLinks(
216
+ tree: MdastRoot,
217
+ rewriteLink: (node: Link | Image | Definition) => string | undefined
218
+ ): void {
219
+ const dropped = new Set<string>();
220
+ visit(tree, ['link', 'image', 'definition'], (node, index, parent) => {
221
+ const target = node as Link | Image | Definition;
222
+ const rewritten = rewriteLink(target);
223
+ if (rewritten !== undefined) {
224
+ target.url = rewritten;
225
+ return;
226
+ }
227
+ if (target.type === 'definition') dropped.add(target.identifier);
228
+ if (parent && index !== undefined) {
229
+ const replacement =
230
+ target.type === 'link'
231
+ ? target.children
232
+ : target.type === 'image'
233
+ ? [{ type: 'text' as const, value: target.alt ?? '' }]
234
+ : [];
235
+ parent.children.splice(index, 1, ...replacement);
236
+ return index + replacement.length;
237
+ }
238
+ });
239
+ if (dropped.size === 0) return;
240
+ visit(tree, ['linkReference', 'imageReference'], (node, index, parent) => {
241
+ const reference = node as LinkReference | ImageReference;
242
+ if (!dropped.has(reference.identifier) || !parent || index === undefined) return;
243
+ const replacement: PhrasingContent[] =
244
+ reference.type === 'linkReference'
245
+ ? reference.children
246
+ : [{ type: 'text', value: reference.alt ?? '' }];
247
+ (parent as Parent).children.splice(index, 1, ...replacement);
248
+ return index + replacement.length;
249
+ });
250
+ }
251
+
252
+ export function markdownToMdx(markdown: string, minHeadingDepth?: Heading['depth']): string {
253
+ return stringifyMdast(parseMarkdown(markdown, minHeadingDepth));
90
254
  }
91
255
 
92
- function stringifyMdast(tree: MdastRoot): string {
93
- const result = unified().use(remarkMdx).use(remarkGfm).use(remarkStringify).stringify(tree);
256
+ // MDX reads a paragraph that starts with `import` or `export` as ESM. A
257
+ // character reference for the first letter keeps the text while making it
258
+ // impossible for Markdown text to become JavaScript.
259
+ function escapeEsmParagraphs(tree: MdastRoot): void {
260
+ visit(tree, 'paragraph', (node: Paragraph) => {
261
+ const first = node.children[0];
262
+ if (first?.type !== 'text') return;
263
+ const match = /^(import|export)(?=\s|$)/.exec(first.value);
264
+ if (!match) return;
265
+ first.value = first.value.slice(1);
266
+ node.children.unshift({ type: 'html', value: `&#${match[0].charCodeAt(0)};` });
267
+ });
268
+ }
269
+
270
+ // Serialises a copy, so a cached tree can be stringified more than once and
271
+ // read for its plain text afterwards without carrying the ESM escapes. Text is
272
+ // merged again first: a dropped link or image leaves its text beside the next
273
+ // node, and `[imp](javascript:x)ort` must be seen as one word, `import`.
274
+ export function stringifyMdast(tree: MdastRoot): string {
275
+ const copy = structuredClone(tree);
276
+ visit(copy, 'paragraph', (node: Paragraph) => {
277
+ node.children = mergeText(node.children);
278
+ });
279
+ escapeEsmParagraphs(copy);
280
+ const result = unified().use(remarkMdx).use(remarkGfm).use(remarkStringify).stringify(copy);
94
281
  return String(result)
95
282
  .replace(/\n{3,}/g, '\n\n')
96
283
  .trim();