@mintlify/cli 4.0.1743 → 4.0.1744
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/__test__/foreignSyntax.test.ts +73 -0
- package/__test__/validateCommand.test.ts +17 -20
- package/__test__/validateEngine.test.ts +17 -4
- package/__test__/validateRules.test.ts +659 -8
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/bin/validate/engine.js +6 -13
- package/bin/validate/files.js +26 -30
- package/bin/validate/mdx.js +38 -1
- package/bin/validate/rules/content/duplicateH1.js +57 -0
- package/bin/validate/rules/content/foreignSyntax.js +120 -0
- package/bin/validate/rules/content/katex.js +34 -0
- package/bin/validate/rules/content/unknownComponent.js +121 -0
- package/bin/validate/rules/index.js +13 -1
- package/bin/validate/site.js +11 -9
- package/package.json +10 -8
- package/src/validate/engine.ts +6 -12
- package/src/validate/files.ts +41 -31
- package/src/validate/mdx.ts +38 -0
- package/src/validate/rules/content/duplicateH1.ts +78 -0
- package/src/validate/rules/content/foreignSyntax.ts +144 -0
- package/src/validate/rules/content/katex.ts +33 -0
- package/src/validate/rules/content/unknownComponent.ts +130 -0
- package/src/validate/rules/index.ts +13 -1
- package/src/validate/site.ts +9 -8
- package/src/validate/types.ts +3 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getAST } from '@mintlify/common';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
|
-
import
|
|
3
|
+
import fsSync from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
|
|
@@ -84,6 +84,128 @@ describe('validate rules', () => {
|
|
|
84
84
|
expect(content).toContain('```html\n<br>\n```');
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
+
it('removes a first heading that matches the title, except in custom mode', async () => {
|
|
88
|
+
write(dir, {
|
|
89
|
+
'index.mdx': '---\ntitle: Hello World\n---\n\n# Hello World\n\nBody\n',
|
|
90
|
+
'custom.mdx': '---\ntitle: Hello\nmode: custom\n---\n\n# Hello\n',
|
|
91
|
+
'other.mdx': '---\ntitle: Hello\n---\n\n# Different\n',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const result = await check(dir, { fix: true });
|
|
95
|
+
|
|
96
|
+
expect(result.filesFixed).toEqual(['index.mdx']);
|
|
97
|
+
expect(read('index.mdx')).toBe('---\ntitle: Hello World\n---\n\nBody\n');
|
|
98
|
+
expect(read('custom.mdx')).toContain('# Hello');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('adds an import for a snippet and reports components it cannot find', async () => {
|
|
102
|
+
write(dir, {
|
|
103
|
+
'snippets/Widget.jsx': 'export const Widget = () => <div />;\n',
|
|
104
|
+
'index.mdx':
|
|
105
|
+
'---\ntitle: Home\n---\n\n<Widget />\n\n<Missing />\n\n<Card title="x">ok</Card>\n',
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const result = await check(dir, { fix: true });
|
|
109
|
+
|
|
110
|
+
expect(read('index.mdx')).toContain('import { Widget } from "/snippets/Widget.jsx";');
|
|
111
|
+
expect(result.issues.map(({ message }) => message)).toEqual([
|
|
112
|
+
expect.stringContaining('<Missing>'),
|
|
113
|
+
]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('adds an import for a tsx snippet', async () => {
|
|
117
|
+
write(dir, {
|
|
118
|
+
'snippets/Widget.tsx': 'export const Widget = ({ label }: { label: string }) => <div />;\n',
|
|
119
|
+
'index.mdx': '---\ntitle: Home\n---\n\n<Widget />\n',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const result = await check(dir, { rules: ['unknown-component'], fix: true });
|
|
123
|
+
|
|
124
|
+
expect(read('index.mdx')).toContain('import { Widget } from "/snippets/Widget.tsx";');
|
|
125
|
+
expect(result.issues).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it.each([
|
|
129
|
+
['export { Widget } from "/snippets/impl.jsx";'],
|
|
130
|
+
['import { Widget } from "/snippets/impl.jsx";\nexport { Widget };'],
|
|
131
|
+
])('imports a re-exported component by name: %s', async (reexport) => {
|
|
132
|
+
write(dir, {
|
|
133
|
+
'snippets/impl.jsx': 'export const Widget = () => <div />;\n',
|
|
134
|
+
'snippets/Widget.jsx': `${reexport}\nexport default function Other() { return null; }\n`,
|
|
135
|
+
'index.mdx': '---\ntitle: Home\n---\n\n<Widget />\n',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await check(dir, { rules: ['unknown-component'], fix: true });
|
|
139
|
+
|
|
140
|
+
expect(read('index.mdx')).toContain('import { Widget } from "/snippets/Widget.jsx";');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('adds every missing snippet import in one run', async () => {
|
|
144
|
+
const names = ['One', 'Two', 'Three', 'Four', 'Five'];
|
|
145
|
+
write(dir, {
|
|
146
|
+
...Object.fromEntries(
|
|
147
|
+
names.map((name) => [`snippets/${name}.jsx`, `export const ${name} = () => <div />;\n`])
|
|
148
|
+
),
|
|
149
|
+
'index.mdx': `---\ntitle: Home\n---\n\n${names.map((name) => `<${name} />`).join('\n\n')}\n`,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const result = await check(dir, { fix: true });
|
|
153
|
+
|
|
154
|
+
expect(result.issues).toEqual([]);
|
|
155
|
+
for (const name of names) {
|
|
156
|
+
expect(read('index.mdx')).toContain(`import { ${name} } from "/snippets/${name}.jsx";`);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('imports a component exported only as the default through a specifier', async () => {
|
|
161
|
+
write(dir, {
|
|
162
|
+
'snippets/Widget.jsx': 'const Widget = () => <div />;\nexport { Widget as default };\n',
|
|
163
|
+
'snippets/Other.jsx': 'const Hidden = () => <div />;\nexport { Hidden };\n',
|
|
164
|
+
'index.mdx': '---\ntitle: Home\n---\n\n<Widget />\n\n<Other />\n',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const result = await check(dir, { rules: ['unknown-component'], fix: true });
|
|
168
|
+
|
|
169
|
+
const content = read('index.mdx');
|
|
170
|
+
expect(content).toContain('import Widget from "/snippets/Widget.jsx";');
|
|
171
|
+
expect(content).not.toContain('Other.jsx');
|
|
172
|
+
expect(result.issues).toEqual([
|
|
173
|
+
expect.objectContaining({ message: expect.stringContaining('<Other>'), fixable: false }),
|
|
174
|
+
]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('treats names bound by destructuring exports as declared', async () => {
|
|
178
|
+
write(dir, {
|
|
179
|
+
'snippets/Widget.jsx': 'export const Widget = () => <div />;\n',
|
|
180
|
+
'snippets/Panel.jsx': 'export const Panel = () => <div />;\n',
|
|
181
|
+
'index.mdx':
|
|
182
|
+
'---\ntitle: Home\n---\n\nexport const { Widget, inner: { Panel = null }, ...Rest } = {};\nexport const [Gallery] = [];\n\n<Widget />\n\n<Panel />\n\n<Rest />\n\n<Gallery />\n',
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect((await check(dir, { rules: ['unknown-component'] })).issues).toEqual([]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('uses the default export when the name is only exported under an alias', async () => {
|
|
189
|
+
write(dir, {
|
|
190
|
+
'snippets/Widget.jsx':
|
|
191
|
+
'const Widget = () => <div />;\nexport { Widget as Renamed };\nexport default Widget;\n',
|
|
192
|
+
'index.mdx': '---\ntitle: Home\n---\n\n<Widget />\n',
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await check(dir, { fix: true });
|
|
196
|
+
|
|
197
|
+
expect(read('index.mdx')).toContain('import Widget from "/snippets/Widget.jsx";');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('treats a named default export as declared', async () => {
|
|
201
|
+
write(dir, {
|
|
202
|
+
'index.mdx':
|
|
203
|
+
'---\ntitle: Home\n---\n\nexport default function Hero() {\n return <div />;\n}\n\n<Hero />\n',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect((await check(dir, {})).issues).toEqual([]);
|
|
207
|
+
});
|
|
208
|
+
|
|
87
209
|
it('skips void tags inside block-quoted code fences', async () => {
|
|
88
210
|
write(dir, {
|
|
89
211
|
'index.mdx': '---\ntitle: Home\n---\n\n> ~~~html\n> <br>\n> ~~~\n\nline<br>\n',
|
|
@@ -99,6 +221,120 @@ describe('validate rules', () => {
|
|
|
99
221
|
expect(read('other.mdx')).toContain('after<br />');
|
|
100
222
|
});
|
|
101
223
|
|
|
224
|
+
it('keeps a duplicate heading that reference-style links point to', async () => {
|
|
225
|
+
write(dir, {
|
|
226
|
+
'index.mdx': '---\ntitle: Quickstart\n---\n\n# Quickstart\n\nBody\n',
|
|
227
|
+
'other.mdx': '---\ntitle: Other\n---\n\nSee [top][start].\n\n[start]: /index#quickstart\n',
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const result = await check(dir, { fix: true });
|
|
231
|
+
|
|
232
|
+
expect(read('index.mdx')).toContain('# Quickstart');
|
|
233
|
+
expect(result.issues.map(({ message }) => message)).toEqual([
|
|
234
|
+
expect.stringContaining('links point to #quickstart'),
|
|
235
|
+
]);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it.each([
|
|
239
|
+
["The User's Settings", '"The User\'s Settings"', "the-user's-settings", 'the-users-settings'],
|
|
240
|
+
["The User's Settings", '"The User\'s Settings"', 'the-user’s-settings', 'the-user’s-settings'],
|
|
241
|
+
[
|
|
242
|
+
"The User's Settings",
|
|
243
|
+
'"The User\'s Settings"',
|
|
244
|
+
'the-user%E2%80%99s-settings',
|
|
245
|
+
'the-user’s-settings',
|
|
246
|
+
],
|
|
247
|
+
['"Pending" Status', '\'"Pending" Status\'', 'pending-status', 'pending-status'],
|
|
248
|
+
['"Pending" Status', '\'"Pending" Status\'', '“pending”-status', '“pending”-status'],
|
|
249
|
+
])(
|
|
250
|
+
'keeps a duplicate heading with quotes that a link points to: %s, title %s, link #%s',
|
|
251
|
+
async (heading, title, link, anchor) => {
|
|
252
|
+
write(dir, {
|
|
253
|
+
'settings.mdx': `---\ntitle: ${title}\n---\n\n# ${heading}\n\nBody\n`,
|
|
254
|
+
'other.mdx': `---\ntitle: Other\n---\n\nSee [settings](/settings#${link}).\n`,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const result = await check(dir, { rules: ['duplicate-h1'], fix: true });
|
|
258
|
+
|
|
259
|
+
expect(read('settings.mdx')).toContain(`# ${heading}`);
|
|
260
|
+
expect(result.issues.map(({ message }) => message)).toEqual([
|
|
261
|
+
expect.stringContaining(`links point to #${anchor}`),
|
|
262
|
+
]);
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
it('reads the heading text the way the table of contents does', async () => {
|
|
267
|
+
write(dir, { 'index.mdx': '---\ntitle: Install\n---\n\n# Install {/* draft */}\n\nBody\n' });
|
|
268
|
+
|
|
269
|
+
const { issues } = await check(dir, { rules: ['duplicate-h1'] });
|
|
270
|
+
|
|
271
|
+
expect(issues).toEqual([expect.objectContaining({ rule: 'duplicate-h1', fixable: true })]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('removes a duplicate heading with quotes that no link points to', async () => {
|
|
275
|
+
write(dir, {
|
|
276
|
+
'settings.mdx': '---\ntitle: "The User\'s Settings"\n---\n\n# The User\'s Settings\n\nBody\n',
|
|
277
|
+
'other.mdx': '---\ntitle: Other\n---\n\nSee [settings](/settings#other-heading).\n',
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await check(dir, { rules: ['duplicate-h1'], fix: true });
|
|
281
|
+
|
|
282
|
+
expect(read('settings.mdx')).not.toContain("# The User's Settings");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('reports and imports components used inside expressions', async () => {
|
|
286
|
+
write(dir, {
|
|
287
|
+
'snippets/Widget.jsx': 'export const Widget = () => <div />;\n',
|
|
288
|
+
'index.mdx':
|
|
289
|
+
'---\ntitle: Home\n---\n\n{open && <Widget />}\n\n{items.map((Item) => <Item />)}\n\nText {flag ? <Missing /> : null} and {Math.max(1, 2)}.\n',
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const { issues } = await check(dir, { rules: ['unknown-component'] });
|
|
293
|
+
expect(
|
|
294
|
+
issues.map(({ line, column, message, fixable }) => [line, column, message, fixable])
|
|
295
|
+
).toEqual([
|
|
296
|
+
[5, 10, '<Widget> is not built in, imported or exported on this page', true],
|
|
297
|
+
[9, 14, '<Missing> is not built in, imported or exported on this page', false],
|
|
298
|
+
]);
|
|
299
|
+
|
|
300
|
+
await check(dir, { rules: ['unknown-component'], fix: true });
|
|
301
|
+
expect(read('index.mdx')).toContain('import { Widget } from "/snippets/Widget.jsx";');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('reports components used inside attribute values', async () => {
|
|
305
|
+
write(dir, {
|
|
306
|
+
'index.mdx':
|
|
307
|
+
'---\ntitle: Home\n---\n\n<Card title="a" icon={<Glyph />}>x</Card>\n\n<Card {...{ icon: <Dot /> }}>y</Card>\n',
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const { issues } = await check(dir, { rules: ['unknown-component'] });
|
|
311
|
+
|
|
312
|
+
expect(issues.map(({ line, column, message }) => [line, column, message])).toEqual([
|
|
313
|
+
[5, 23, '<Glyph> is not built in, imported or exported on this page'],
|
|
314
|
+
[7, 19, '<Dot> is not built in, imported or exported on this page'],
|
|
315
|
+
]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('does not flag imported or exported components', async () => {
|
|
319
|
+
write(dir, {
|
|
320
|
+
'index.mdx':
|
|
321
|
+
'---\ntitle: Home\n---\n\nimport Thing from "/snippets/Thing.jsx";\nimport { A as B } from "/snippets/b.jsx";\n\nexport const Local = () => <p />;\n\n<Thing /><B /><Local /><Color.Row />\n',
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
expect((await check(dir, {})).issues).toEqual([]);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('reports math KaTeX cannot render', async () => {
|
|
328
|
+
write(dir, {
|
|
329
|
+
'index.mdx': '---\ntitle: Home\n---\n\nGood $x^2$ bad $\\frac{1$\n',
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const { issues } = await check(dir, {});
|
|
333
|
+
|
|
334
|
+
expect(issues.map(({ rule }) => rule)).toEqual(['katex']);
|
|
335
|
+
expect(issues[0]?.fixable).toBe(false);
|
|
336
|
+
});
|
|
337
|
+
|
|
102
338
|
it('keeps an info-string fence line from closing an open fence', async () => {
|
|
103
339
|
write(dir, {
|
|
104
340
|
'index.mdx':
|
|
@@ -145,6 +381,174 @@ describe('validate rules', () => {
|
|
|
145
381
|
expect(content).toContain('`` a`<br>`b ``');
|
|
146
382
|
});
|
|
147
383
|
|
|
384
|
+
it('reports but does not remove a duplicate heading with a custom id', async () => {
|
|
385
|
+
write(dir, {
|
|
386
|
+
'index.mdx': '---\ntitle: Hello\n---\n\n# Hello {#intro}\n\nBody\n',
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const result = await check(dir, { fix: true });
|
|
390
|
+
|
|
391
|
+
expect(result.filesFixed).toEqual([]);
|
|
392
|
+
expect(result.issues).toEqual([
|
|
393
|
+
expect.objectContaining({ rule: 'duplicate-h1', fixable: false }),
|
|
394
|
+
]);
|
|
395
|
+
expect(read('index.mdx')).toContain('# Hello {#intro}');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('only treats a top-level heading as the first heading', async () => {
|
|
399
|
+
write(dir, {
|
|
400
|
+
'index.mdx': '---\ntitle: Hello\n---\n\n<Tab title="a">\n\n# Hello\n\n</Tab>\n',
|
|
401
|
+
'imports.mdx':
|
|
402
|
+
'---\ntitle: Hello\n---\n\nimport { A } from "/snippets/a.jsx";\n\n# Hello\n\n<A />\n',
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const result = await check(dir, { rules: ['duplicate-h1'], fix: true });
|
|
406
|
+
|
|
407
|
+
expect(result.filesFixed).toEqual(['imports.mdx']);
|
|
408
|
+
expect(read('index.mdx')).toContain('# Hello');
|
|
409
|
+
expect(read('imports.mdx')).not.toContain('# Hello');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('reads imports from the ESM tree, including side-effect imports', async () => {
|
|
413
|
+
write(dir, {
|
|
414
|
+
'index.mdx':
|
|
415
|
+
'---\ntitle: Home\n---\n\nimport "./styles.css";\nimport Thing from "/snippets/Thing.jsx";\nimport * as Lib from "/snippets/lib.js";\nexport { Other } from "/snippets/other.jsx";\nexport function Fn() { return null; }\n\n<Thing /><Lib.X /><Other /><Fn />\n',
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
expect((await check(dir, { rules: ['unknown-component'] })).issues).toEqual([]);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('leaves a blank line after an inserted import wherever it goes', async () => {
|
|
422
|
+
const widget = 'import { Widget } from "/snippets/Widget.jsx";';
|
|
423
|
+
write(dir, {
|
|
424
|
+
'snippets/Widget.jsx': 'export const Widget = () => <div />;\n',
|
|
425
|
+
'tight.mdx': '---\ntitle: Tight\n---\n<Widget />\n',
|
|
426
|
+
'top.mdx': '<Widget />\n',
|
|
427
|
+
'imports.mdx':
|
|
428
|
+
'---\ntitle: Imports\n---\nexport const A = () => null;\n\n<A />\n\n<Widget />\n',
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const result = await check(dir, { rules: ['unknown-component'], fix: true });
|
|
432
|
+
|
|
433
|
+
expect(result.issues).toEqual([]);
|
|
434
|
+
expect(read('tight.mdx')).toBe(`---\ntitle: Tight\n---\n\n${widget}\n\n<Widget />\n`);
|
|
435
|
+
expect(read('top.mdx')).toBe(`${widget}\n\n<Widget />\n`);
|
|
436
|
+
expect(read('imports.mdx')).toBe(
|
|
437
|
+
`---\ntitle: Imports\n---\n\n${widget}\n\nexport const A = () => null;\n\n<A />\n\n<Widget />\n`
|
|
438
|
+
);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('inserts snippet imports after TOML frontmatter', async () => {
|
|
442
|
+
write(dir, {
|
|
443
|
+
'snippets/Widget.jsx': 'export const Widget = () => <div />;\n',
|
|
444
|
+
'index.mdx': "+++\ntitle = 'Home'\n+++\n\n<Widget />\n",
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
await check(dir, { rules: ['unknown-component'], fix: true });
|
|
448
|
+
|
|
449
|
+
expect(read('index.mdx')).toBe(
|
|
450
|
+
'+++\ntitle = \'Home\'\n+++\n\nimport { Widget } from "/snippets/Widget.jsx";\n\n<Widget />\n'
|
|
451
|
+
);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('prefers a named import when the snippet exports the component by name', async () => {
|
|
455
|
+
write(dir, {
|
|
456
|
+
'snippets/Named.jsx':
|
|
457
|
+
'export const Named = () => <div />;\nexport default function Other() { return null; }\n',
|
|
458
|
+
'snippets/Listed.jsx':
|
|
459
|
+
'const Listed = () => <div />;\nexport { Listed };\nexport default {};\n',
|
|
460
|
+
'snippets/Default.jsx': 'export default function Default() { return null; }\n',
|
|
461
|
+
'index.mdx': '---\ntitle: Home\n---\n\n<Named />\n\n<Listed />\n\n<Default />\n',
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
await check(dir, { rules: ['unknown-component'], fix: true });
|
|
465
|
+
|
|
466
|
+
const content = read('index.mdx');
|
|
467
|
+
expect(content).toContain('import { Named } from "/snippets/Named.jsx";');
|
|
468
|
+
expect(content).toContain('import { Listed } from "/snippets/Listed.jsx";');
|
|
469
|
+
expect(content).toContain('import Default from "/snippets/Default.jsx";');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('skips price-like spans, which the site renders as text', async () => {
|
|
473
|
+
write(dir, {
|
|
474
|
+
'index.mdx': '---\ntitle: Home\n---\n\nPlans cost $5 & up, or $10 a seat.\n',
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
expect((await check(dir, {})).issues).toEqual([]);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('tags each issue with its rule severity', async () => {
|
|
481
|
+
write(dir, {
|
|
482
|
+
'void.mdx': '---\ntitle: Void\n---\n\nline<br>\n',
|
|
483
|
+
'index.mdx':
|
|
484
|
+
'---\ntitle: Home\n---\n\n# Home\n\n<Missing />\n\nbad $\\frac{1$ and $5 and $10\n',
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const { issues } = await check(dir, {
|
|
488
|
+
rules: ['void-tags', 'duplicate-h1', 'unknown-component', 'katex'],
|
|
489
|
+
});
|
|
490
|
+
const severities = Object.fromEntries(issues.map(({ rule, severity }) => [rule, severity]));
|
|
491
|
+
|
|
492
|
+
expect(severities).toEqual({
|
|
493
|
+
'void-tags': 'error',
|
|
494
|
+
'duplicate-h1': 'warning',
|
|
495
|
+
'unknown-component': 'warning',
|
|
496
|
+
katex: 'warning',
|
|
497
|
+
});
|
|
498
|
+
expect(hasErrors(issues)).toBe(true);
|
|
499
|
+
expect(hasErrors(issues.filter(({ severity }) => severity === 'warning'))).toBe(false);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('previews the fixes --fix applies without writing, accounting for interacting fixes', async () => {
|
|
503
|
+
const page = '---\ntitle: Hello\n---\n\n# Hello\n\nline<br>\n\n<Missing />\n\nbad $\\frac{1$\n';
|
|
504
|
+
write(dir, { 'index.mdx': page });
|
|
505
|
+
|
|
506
|
+
const plain = await check(dir, {});
|
|
507
|
+
expect(read('index.mdx')).toBe(page);
|
|
508
|
+
expect(plain.fixed).toEqual([]);
|
|
509
|
+
expect(plain.filesFixed).toEqual([]);
|
|
510
|
+
const preview = plain.issues.map(({ rule, line, fixable, fix }) => ({
|
|
511
|
+
rule,
|
|
512
|
+
line,
|
|
513
|
+
fixable,
|
|
514
|
+
fix: fix && { before: page.slice(fix.start, fix.end), after: fix.text },
|
|
515
|
+
}));
|
|
516
|
+
expect(preview).toEqual([
|
|
517
|
+
{
|
|
518
|
+
rule: 'duplicate-h1',
|
|
519
|
+
line: 5,
|
|
520
|
+
fixable: true,
|
|
521
|
+
fix: { before: '# Hello\n\n', after: '' },
|
|
522
|
+
},
|
|
523
|
+
{ rule: 'void-tags', line: 7, fixable: true, fix: { before: '<br>', after: '<br />' } },
|
|
524
|
+
{ rule: 'unknown-component', line: 9, fixable: false, fix: undefined },
|
|
525
|
+
{ rule: 'katex', line: 11, fixable: false, fix: undefined },
|
|
526
|
+
]);
|
|
527
|
+
|
|
528
|
+
const fixed = await check(dir, { fix: true });
|
|
529
|
+
expect(
|
|
530
|
+
fixed.fixed.map(({ rule, line, before, after }) => ({ rule, line, before, after }))
|
|
531
|
+
).toEqual(
|
|
532
|
+
preview.filter(({ fix }) => fix).map(({ rule, line, fix }) => ({ rule, line, ...fix }))
|
|
533
|
+
);
|
|
534
|
+
expect(fixed.issues.map(({ rule }) => rule)).toEqual(['unknown-component', 'katex']);
|
|
535
|
+
expect(read('index.mdx')).toBe(
|
|
536
|
+
'---\ntitle: Hello\n---\n\nline<br />\n\n<Missing />\n\nbad $\\frac{1$\n'
|
|
537
|
+
);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('accepts every component the build keeps or rewrites', async () => {
|
|
541
|
+
write(dir, {
|
|
542
|
+
'snippets/part.mdx': 'Part\n',
|
|
543
|
+
'index.mdx':
|
|
544
|
+
'---\ntitle: Home\n---\n\n<Snippet file="part.mdx" />\n\n<Example>x</Example>\n\n<Param name="a">x</Param>\n\n<ApiPlayground />\n\n<MDXContentController />\n\n<Color>\n <Color.Row title="a">\n <Color.Item name="b" value="#fff" />\n </Color.Row>\n</Color>\n\n<Tree>\n <Tree.Folder name="a">\n <Tree.File name="b" />\n </Tree.Folder>\n</Tree>\n\n<Color.Nope />\n',
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const { issues } = await check(dir, { rules: ['unknown-component'] });
|
|
548
|
+
|
|
549
|
+
expect(issues.map(({ message }) => message)).toEqual([expect.stringContaining('<Color.Nope>')]);
|
|
550
|
+
});
|
|
551
|
+
|
|
148
552
|
it('leaves escaped void tags alone', async () => {
|
|
149
553
|
write(dir, { 'index.mdx': '---\ntitle: Home\n---\n\nText \\<br> and \\<img src="/a.png">\n' });
|
|
150
554
|
|
|
@@ -181,6 +585,16 @@ describe('validate rules', () => {
|
|
|
181
585
|
expect(read('index.mdx')).toBe(page);
|
|
182
586
|
});
|
|
183
587
|
|
|
588
|
+
it('parses headings with a spaced custom id and keeps their fixes', async () => {
|
|
589
|
+
write(dir, {
|
|
590
|
+
'index.mdx': '---\ntitle: Hello\n---\n\n# Hello\n\n## Setup { #setup }\n\nBody\n',
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
const { issues } = await check(dir, {});
|
|
594
|
+
|
|
595
|
+
expect(issues).toEqual([expect.objectContaining({ rule: 'duplicate-h1', fixable: true })]);
|
|
596
|
+
});
|
|
597
|
+
|
|
184
598
|
it('accepts <MDX> fragments the build preprocesses', async () => {
|
|
185
599
|
write(dir, {
|
|
186
600
|
'index.mdx':
|
|
@@ -194,12 +608,12 @@ describe('validate rules', () => {
|
|
|
194
608
|
write(dir, {
|
|
195
609
|
'.mintignore': 'drafts/\n',
|
|
196
610
|
'openapi.yaml': 'openapi: [unclosed\n',
|
|
197
|
-
'drafts/wip.mdx': '---\ntitle: Wip\n---\n\
|
|
198
|
-
'SKILL.md': '
|
|
199
|
-
'index.mdx': '---\ntitle: Home\n---\n\
|
|
611
|
+
'drafts/wip.mdx': '---\ntitle: Wip\n---\n\n<Missing />\n',
|
|
612
|
+
'SKILL.md': '<Missing />\n',
|
|
613
|
+
'index.mdx': '---\ntitle: Home\n---\n\n<Missing />\n',
|
|
200
614
|
});
|
|
201
615
|
|
|
202
|
-
const result = await check(dir, { rules: ['
|
|
616
|
+
const result = await check(dir, { rules: ['unknown-component'] });
|
|
203
617
|
|
|
204
618
|
expect(result.filesChecked).toBe(1);
|
|
205
619
|
expect(result.issues.map(({ file }) => file)).toEqual(['index.mdx']);
|
|
@@ -269,6 +683,122 @@ describe('validate rules', () => {
|
|
|
269
683
|
expect(issues.map(({ line }) => line)).toEqual([7, 11]);
|
|
270
684
|
});
|
|
271
685
|
|
|
686
|
+
it('reports but does not remove a duplicate heading that other pages link to', async () => {
|
|
687
|
+
write(dir, {
|
|
688
|
+
'guides/start.mdx': '---\ntitle: Quickstart\n---\n\n# Quickstart\n\nBody\n',
|
|
689
|
+
'index.mdx': '---\ntitle: Home\n---\n\nSee [the start](/guides/start#quickstart).\n',
|
|
690
|
+
'guides/other.mdx': '---\ntitle: Other\n---\n\nUnrelated [link](./start#install).\n',
|
|
691
|
+
'plain.mdx': '---\ntitle: Plain\n---\n\n# Plain\n\nBody\n',
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
const result = await check(dir, { rules: ['duplicate-h1'], fix: true });
|
|
695
|
+
|
|
696
|
+
expect(result.filesFixed).toEqual(['plain.mdx']);
|
|
697
|
+
expect(read('guides/start.mdx')).toContain('# Quickstart');
|
|
698
|
+
expect(result.issues).toEqual([
|
|
699
|
+
expect.objectContaining({
|
|
700
|
+
file: 'guides/start.mdx',
|
|
701
|
+
fixable: false,
|
|
702
|
+
message: expect.stringContaining('#quickstart'),
|
|
703
|
+
}),
|
|
704
|
+
]);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it('finds relative and same-page links to the heading anchor', async () => {
|
|
708
|
+
write(dir, {
|
|
709
|
+
'guides/start.mdx':
|
|
710
|
+
'---\ntitle: Quickstart\n---\n\n# Quickstart\n\nBack to [top](#quickstart).\n',
|
|
711
|
+
'guides/next.mdx': '---\ntitle: Next\n---\n\n# Next\n\n',
|
|
712
|
+
'guides/after.mdx': '---\ntitle: After\n---\n\nRead <a href="next#next">next</a>.\n',
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
const { issues } = await check(dir, { rules: ['duplicate-h1'] });
|
|
716
|
+
|
|
717
|
+
expect(issues.map(({ file, fixable }) => [file, fixable])).toEqual([
|
|
718
|
+
['guides/next.mdx', false],
|
|
719
|
+
['guides/start.mdx', false],
|
|
720
|
+
]);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('ignores heading links inside code', async () => {
|
|
724
|
+
write(dir, {
|
|
725
|
+
'guides/start.mdx': '---\ntitle: Quickstart\n---\n\n# Quickstart\n\nBody\n',
|
|
726
|
+
'index.mdx':
|
|
727
|
+
'---\ntitle: Home\n---\n\n```md\n[start](/guides/start#quickstart)\n```\n\nUse `<a href="/guides/start#quickstart">`.\n',
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
const result = await check(dir, { rules: ['duplicate-h1'], fix: true });
|
|
731
|
+
|
|
732
|
+
expect(result.filesFixed).toEqual(['guides/start.mdx']);
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it('keeps a duplicate homepage heading that a root hash link points to', async () => {
|
|
736
|
+
write(dir, {
|
|
737
|
+
'index.mdx': '---\ntitle: Home\n---\n\n# Home\n\nBody\n',
|
|
738
|
+
'guides/start.mdx': '---\ntitle: Start\n---\n\nBack [home](/#home).\n',
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
const result = await check(dir, { rules: ['duplicate-h1'], fix: true });
|
|
742
|
+
|
|
743
|
+
expect(result.filesFixed).toEqual([]);
|
|
744
|
+
expect(read('index.mdx')).toContain('# Home');
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('finds heading links that carry a query string', async () => {
|
|
748
|
+
write(dir, {
|
|
749
|
+
'guides/start.mdx': '---\ntitle: Quickstart\n---\n\n# Quickstart\n\nBody\n',
|
|
750
|
+
'index.mdx': '---\ntitle: Home\n---\n\nSee [the start](/guides/start?tab=api#quickstart).\n',
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
const result = await check(dir, { rules: ['duplicate-h1'], fix: true });
|
|
754
|
+
|
|
755
|
+
expect(result.filesFixed).toEqual([]);
|
|
756
|
+
expect(read('guides/start.mdx')).toContain('# Quickstart');
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it('keeps a duplicate heading when links point to a later heading that would take its id', async () => {
|
|
760
|
+
write(dir, {
|
|
761
|
+
'index.mdx': '---\ntitle: Setup\n---\n\n# Setup\n\n## Setup\n\nBody\n',
|
|
762
|
+
'other.mdx': '---\ntitle: Other\n---\n\nSee [the details](/index#setup-2).\n',
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
const result = await check(dir, { rules: ['duplicate-h1'], fix: true });
|
|
766
|
+
|
|
767
|
+
expect(result.filesFixed).toEqual([]);
|
|
768
|
+
expect(read('index.mdx')).toContain('# Setup\n');
|
|
769
|
+
expect(result.issues).toEqual([
|
|
770
|
+
expect.objectContaining({ fixable: false, message: expect.stringContaining('#setup-2') }),
|
|
771
|
+
]);
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it('finds anchor links in string-literal expression hrefs', async () => {
|
|
775
|
+
write(dir, {
|
|
776
|
+
'double.mdx': '---\ntitle: Double\n---\n\n# Double\n',
|
|
777
|
+
'template.mdx': '---\ntitle: Template\n---\n\n# Template\n',
|
|
778
|
+
'dynamic.mdx': '---\ntitle: Dynamic\n---\n\n# Dynamic\n',
|
|
779
|
+
'links.mdx':
|
|
780
|
+
'---\ntitle: Links\n---\n\n<a href={"/double#double"}>a</a>\n\n<a href={`/template#template`}>b</a>\n\n<a href={`/dynamic#${slug}`}>c</a>\n',
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
const result = await check(dir, { rules: ['duplicate-h1'], fix: true });
|
|
784
|
+
|
|
785
|
+
expect(result.filesFixed).toEqual(['dynamic.mdx']);
|
|
786
|
+
expect(read('double.mdx')).toContain('# Double');
|
|
787
|
+
expect(read('template.mdx')).toContain('# Template');
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it('finds anchor links in hrefs with spaces around the equals sign', async () => {
|
|
791
|
+
write(dir, {
|
|
792
|
+
'guides/start.mdx': '---\ntitle: Quickstart\n---\n\n# Quickstart\n\nBody\n',
|
|
793
|
+
'index.mdx': '---\ntitle: Home\n---\n\n<a href = "/guides/start#quickstart">Start</a>\n',
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
const result = await check(dir, { rules: ['duplicate-h1'], fix: true });
|
|
797
|
+
|
|
798
|
+
expect(result.filesFixed).toEqual([]);
|
|
799
|
+
expect(read('guides/start.mdx')).toContain('# Quickstart');
|
|
800
|
+
});
|
|
801
|
+
|
|
272
802
|
it('keeps file order when linting many files concurrently', async () => {
|
|
273
803
|
const names = Array.from(
|
|
274
804
|
{ length: 40 },
|
|
@@ -319,10 +849,21 @@ describe('validate rules', () => {
|
|
|
319
849
|
);
|
|
320
850
|
});
|
|
321
851
|
|
|
322
|
-
it('
|
|
852
|
+
it('accepts pages the build parses, such as code inside <MDX>, without offering edits', async () => {
|
|
853
|
+
write(dir, {
|
|
854
|
+
'index.mdx': '---\ntitle: Home\n---\n\n<MDX>\nif (a < b) { run() }\n</MDX>\n\n<Missing />\n',
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
const result = await check(dir, {});
|
|
858
|
+
|
|
859
|
+
expect(result.issues.map(({ rule }) => rule)).toEqual(['unknown-component']);
|
|
860
|
+
expect(result.issues[0]?.fixable).toBe(false);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
it('lints snippets, skipping page-only rules', async () => {
|
|
323
864
|
write(dir, {
|
|
324
865
|
'index.mdx': '---\ntitle: Home\n---\n\nok\n',
|
|
325
|
-
'snippets/note.mdx': '---\ntitle: Same\n---\n\nline<br>\n',
|
|
866
|
+
'snippets/note.mdx': '---\ntitle: Same\n---\n\n# Same\n\nline<br>\n\n<FromPage />\n',
|
|
326
867
|
});
|
|
327
868
|
|
|
328
869
|
const result = await check(dir, {});
|
|
@@ -332,6 +873,60 @@ describe('validate rules', () => {
|
|
|
332
873
|
]);
|
|
333
874
|
});
|
|
334
875
|
|
|
876
|
+
it('orders findings on one line left to right across rules', async () => {
|
|
877
|
+
write(dir, {
|
|
878
|
+
'index.mdx': '---\ntitle: Home\n---\n\nSee <Missing /> and a line break here <br>\n',
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
const result = await check(dir, { rules: ['void-tags', 'unknown-component'] });
|
|
882
|
+
|
|
883
|
+
expect(result.issues.map(({ rule, line, column }) => [rule, line, column])).toEqual([
|
|
884
|
+
['unknown-component', 5, 5],
|
|
885
|
+
['void-tags', 5, 39],
|
|
886
|
+
]);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it('names leftover GitBook syntax even when the page does not parse', async () => {
|
|
890
|
+
write(dir, {
|
|
891
|
+
'index.mdx': '---\ntitle: Home\n---\n\n{% hint style="info" %}\nRead this\n{% endhint %}\n',
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
const result = await check(dir, {});
|
|
895
|
+
|
|
896
|
+
expect(result.issues.map(({ rule, line }) => [rule, line])).toEqual([
|
|
897
|
+
['foreign-syntax', 5],
|
|
898
|
+
['parse-error', 5],
|
|
899
|
+
]);
|
|
900
|
+
expect(result.issues[0]?.message).toContain('use <Info>');
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it('skips foreign syntax inside JSX attribute and JavaScript strings', async () => {
|
|
904
|
+
write(dir, {
|
|
905
|
+
'index.mdx':
|
|
906
|
+
'---\ntitle: Home\n---\n\n<Card title="{{ site.title }}" href="doc:intro" />\n\n{"{{ page.title }}"}\n\n{{ site.name }}\n\n[Intro](doc:intro)\n',
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
const { issues } = await check(dir, { rules: ['foreign-syntax'] });
|
|
910
|
+
const found = issues.filter(({ rule }) => rule === 'foreign-syntax');
|
|
911
|
+
|
|
912
|
+
expect(found.map(({ line, message }) => [line, message.split(' ')[0]])).toEqual([
|
|
913
|
+
[5, 'doc:intro'],
|
|
914
|
+
[9, '{{'],
|
|
915
|
+
[11, 'doc:intro'],
|
|
916
|
+
]);
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
it('reports foreign syntax the build accepts without failing lint', async () => {
|
|
920
|
+
write(dir, { 'index.mdx': '---\ntitle: Home\n---\n\n:::note\nRead this\n:::\n' });
|
|
921
|
+
|
|
922
|
+
const result = await check(dir, {});
|
|
923
|
+
|
|
924
|
+
expect(result.issues.map(({ rule, severity }) => [rule, severity])).toEqual([
|
|
925
|
+
['foreign-syntax', 'warning'],
|
|
926
|
+
]);
|
|
927
|
+
expect(hasErrors(result.issues)).toBe(false);
|
|
928
|
+
});
|
|
929
|
+
|
|
335
930
|
it('rejects --files that name no page or snippet of the site', async () => {
|
|
336
931
|
write(dir, { 'index.mdx': '---\ntitle: Home\n---\n\nok\n', 'README.md': '# Readme\n' });
|
|
337
932
|
|
|
@@ -355,7 +950,7 @@ describe('validate rules', () => {
|
|
|
355
950
|
'index.mdx': '---\ntitle: Home\n---\n\nline<br>\n',
|
|
356
951
|
'other.mdx': '---\ntitle: Other\n---\n\nline<br>\n',
|
|
357
952
|
});
|
|
358
|
-
const spy = vi.spyOn(
|
|
953
|
+
const spy = vi.spyOn(fsSync, 'readFileSync');
|
|
359
954
|
|
|
360
955
|
const result = await check(dir, { rules: ['void-tags'], files: ['other.mdx'] });
|
|
361
956
|
|
|
@@ -366,6 +961,62 @@ describe('validate rules', () => {
|
|
|
366
961
|
expect(result.issues.map(({ file }) => file)).toEqual(['other.mdx']);
|
|
367
962
|
});
|
|
368
963
|
|
|
964
|
+
it('does no anchor reads when duplicate-h1 is disabled', async () => {
|
|
965
|
+
write(dir, {
|
|
966
|
+
'index.mdx': '---\ntitle: Home\n---\n\n# Home\n',
|
|
967
|
+
'other.mdx': '---\ntitle: Other\n---\n\nSee [home](/index#home).\n',
|
|
968
|
+
});
|
|
969
|
+
const spy = vi.spyOn(fsSync, 'readFileSync');
|
|
970
|
+
|
|
971
|
+
const result = await checkSite(dir, selectRules({ disable: ['duplicate-h1'] }), {
|
|
972
|
+
selected: new Set(['index.mdx']),
|
|
973
|
+
localSchema: false,
|
|
974
|
+
fix: false,
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
const reads = spy.mock.calls.map(([file]) => String(file));
|
|
978
|
+
spy.mockRestore();
|
|
979
|
+
expect(reads.some((file) => file.endsWith('other.mdx'))).toBe(false);
|
|
980
|
+
expect(result.diagnostics.filter(({ rule }) => rule === 'duplicate-h1')).toEqual([]);
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
it('collects no anchor links when no page has a duplicate first heading', async () => {
|
|
984
|
+
write(dir, {
|
|
985
|
+
'index.mdx': '---\ntitle: Home\n---\n\n# Welcome\n',
|
|
986
|
+
'other.mdx': '---\ntitle: Other\n---\n\nSee [home](/index#welcome).\n',
|
|
987
|
+
});
|
|
988
|
+
const spy = vi.spyOn(fsSync, 'readFileSync');
|
|
989
|
+
|
|
990
|
+
const result = await check(dir, { rules: ['duplicate-h1'], files: ['index.mdx'] });
|
|
991
|
+
|
|
992
|
+
const reads = spy.mock.calls.map(([file]) => String(file));
|
|
993
|
+
spy.mockRestore();
|
|
994
|
+
expect(reads.some((file) => file.endsWith('other.mdx'))).toBe(false);
|
|
995
|
+
expect(result.issues).toEqual([]);
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
it('reads every file once when duplicate-h1 runs on a selection', async () => {
|
|
999
|
+
write(dir, {
|
|
1000
|
+
'index.mdx': '---\ntitle: Home\n---\n\n# Home\n',
|
|
1001
|
+
'other.mdx': '---\ntitle: Other\n---\n\nSee [home](/index#home).\n',
|
|
1002
|
+
});
|
|
1003
|
+
const spy = vi.spyOn(fsSync, 'readFileSync');
|
|
1004
|
+
|
|
1005
|
+
const result = await check(dir, { rules: ['duplicate-h1'], files: ['index.mdx'] });
|
|
1006
|
+
|
|
1007
|
+
const reads = spy.mock.calls.map(([file]) => String(file));
|
|
1008
|
+
spy.mockRestore();
|
|
1009
|
+
expect(reads.filter((file) => file.endsWith('other.mdx'))).toHaveLength(1);
|
|
1010
|
+
expect(reads.filter((file) => file.endsWith('index.mdx'))).toHaveLength(1);
|
|
1011
|
+
expect(result.issues).toEqual([
|
|
1012
|
+
expect.objectContaining({
|
|
1013
|
+
file: 'index.mdx',
|
|
1014
|
+
fixable: false,
|
|
1015
|
+
message: expect.stringContaining('#home'),
|
|
1016
|
+
}),
|
|
1017
|
+
]);
|
|
1018
|
+
});
|
|
1019
|
+
|
|
369
1020
|
it('skips void tags in block-quoted fences and inline code', async () => {
|
|
370
1021
|
write(dir, {
|
|
371
1022
|
'index.mdx': '---\ntitle: Home\n---\n\n> Quote\n>\n> ```html\n> <br>\n> ```\n\nSee `<hr>`.\n',
|