@mintlify/cli 4.0.1813 → 4.1.2

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 (274) hide show
  1. package/dist/assets/export-scripts/README.classic.md +15 -0
  2. package/dist/assets/export-scripts/README.md +14 -0
  3. package/dist/assets/export-scripts/serve.js +114 -0
  4. package/dist/assets/export-scripts/start-docs.bat +10 -0
  5. package/dist/assets/export-scripts/start-docs.sh +9 -0
  6. package/dist/assets/og/Inter-Bold.ttf +0 -0
  7. package/dist/assets/og/Inter-Medium.ttf +0 -0
  8. package/dist/assets/og/Inter-Regular.ttf +0 -0
  9. package/dist/assets/og/Inter-SemiBold.ttf +0 -0
  10. package/dist/assets/og/PaperMono-Regular.ttf +0 -0
  11. package/dist/assets/og/index_bg.wasm +0 -0
  12. package/dist/assets/openChrome.applescript +94 -0
  13. package/dist/assets/socket.io/socket.io.esm.min.js +7 -0
  14. package/dist/assets/socket.io/socket.io.esm.min.js.map +1 -0
  15. package/dist/assets/socket.io/socket.io.js +4907 -0
  16. package/dist/assets/socket.io/socket.io.js.map +1 -0
  17. package/dist/assets/socket.io/socket.io.min.js +7 -0
  18. package/dist/assets/socket.io/socket.io.min.js.map +1 -0
  19. package/dist/assets/socket.io/socket.io.msgpack.min.js +7 -0
  20. package/dist/assets/socket.io/socket.io.msgpack.min.js.map +1 -0
  21. package/dist/assets/xdg-open +1066 -0
  22. package/dist/assets/yargs-locales/be.json +46 -0
  23. package/dist/assets/yargs-locales/cs.json +51 -0
  24. package/dist/assets/yargs-locales/de.json +46 -0
  25. package/dist/assets/yargs-locales/en.json +55 -0
  26. package/dist/assets/yargs-locales/es.json +46 -0
  27. package/dist/assets/yargs-locales/fi.json +49 -0
  28. package/dist/assets/yargs-locales/fr.json +53 -0
  29. package/dist/assets/yargs-locales/hi.json +49 -0
  30. package/dist/assets/yargs-locales/hu.json +46 -0
  31. package/dist/assets/yargs-locales/id.json +50 -0
  32. package/dist/assets/yargs-locales/it.json +46 -0
  33. package/dist/assets/yargs-locales/ja.json +51 -0
  34. package/dist/assets/yargs-locales/ko.json +49 -0
  35. package/dist/assets/yargs-locales/nb.json +44 -0
  36. package/dist/assets/yargs-locales/nl.json +49 -0
  37. package/dist/assets/yargs-locales/nn.json +44 -0
  38. package/dist/assets/yargs-locales/pirate.json +13 -0
  39. package/dist/assets/yargs-locales/pl.json +49 -0
  40. package/dist/assets/yargs-locales/pt.json +45 -0
  41. package/dist/assets/yargs-locales/pt_BR.json +48 -0
  42. package/dist/assets/yargs-locales/ru.json +51 -0
  43. package/dist/assets/yargs-locales/th.json +46 -0
  44. package/dist/assets/yargs-locales/tr.json +48 -0
  45. package/dist/assets/yargs-locales/uk_UA.json +51 -0
  46. package/dist/assets/yargs-locales/uz.json +52 -0
  47. package/dist/assets/yargs-locales/zh_CN.json +48 -0
  48. package/dist/assets/yargs-locales/zh_TW.json +51 -0
  49. package/dist/converter-d8O3xkdU.js +102 -0
  50. package/dist/devtools-CGNZQKFV.js +24 -0
  51. package/dist/index.js +8 -0
  52. package/dist/rolldown-runtime-CEAe2Uac.js +7 -0
  53. package/dist/schemas-CBsmCNpK.js +372 -0
  54. package/dist/start.js +2310 -0
  55. package/package.json +38 -31
  56. package/.oxlintrc.json +0 -5
  57. package/.prettierignore +0 -2
  58. package/.turbo/turbo-build.log +0 -1
  59. package/CONTRIBUTING.md +0 -45
  60. package/__test__/agent-auth.test.ts +0 -114
  61. package/__test__/agent-claim.test.ts +0 -164
  62. package/__test__/agent-credentials.test.ts +0 -149
  63. package/__test__/agent-deployment.test.ts +0 -106
  64. package/__test__/agent-setup-config.test.ts +0 -324
  65. package/__test__/agent-setup.test.ts +0 -207
  66. package/__test__/agent.test.ts +0 -64
  67. package/__test__/analytics/client.test.ts +0 -180
  68. package/__test__/analytics/dates.test.ts +0 -12
  69. package/__test__/analytics/filters.test.ts +0 -116
  70. package/__test__/analytics/format.test.ts +0 -137
  71. package/__test__/authenticatedFetch.test.ts +0 -182
  72. package/__test__/brokenLinks.test.ts +0 -283
  73. package/__test__/checkPort.test.ts +0 -92
  74. package/__test__/deploy-github-actions.test.ts +0 -202
  75. package/__test__/deploy-github-setup.test.ts +0 -240
  76. package/__test__/deploy-instructions.test.ts +0 -155
  77. package/__test__/deploy.test.ts +0 -202
  78. package/__test__/feedback.test.ts +0 -34
  79. package/__test__/foreignSyntax.test.ts +0 -73
  80. package/__test__/format.test.ts +0 -72
  81. package/__test__/helpers.test.ts +0 -52
  82. package/__test__/init.test.ts +0 -119
  83. package/__test__/instructions.test.ts +0 -53
  84. package/__test__/keyring.test.ts +0 -82
  85. package/__test__/mdxAccessibility.test.ts +0 -81
  86. package/__test__/migrate.test.ts +0 -186
  87. package/__test__/openApiCheck.test.ts +0 -185
  88. package/__test__/pathValidation.test.ts +0 -25
  89. package/__test__/resolveFiles.test.ts +0 -70
  90. package/__test__/sessionId.test.ts +0 -46
  91. package/__test__/signup.test.ts +0 -36
  92. package/__test__/update.test.ts +0 -117
  93. package/__test__/utils.ts +0 -25
  94. package/__test__/validateCommand.test.ts +0 -647
  95. package/__test__/validateEngine.test.ts +0 -245
  96. package/__test__/validateRules.test.ts +0 -1275
  97. package/__test__/welcome.test.ts +0 -15
  98. package/__test__/workflows/client.test.ts +0 -126
  99. package/bin/accessibility.js +0 -111
  100. package/bin/accessibilityCheck.js +0 -82
  101. package/bin/addDomain.js +0 -130
  102. package/bin/agent-auth.js +0 -104
  103. package/bin/agent-claim.js +0 -97
  104. package/bin/agent-credentials.js +0 -124
  105. package/bin/agent-deployment.js +0 -96
  106. package/bin/agent-http.js +0 -45
  107. package/bin/agent-setup-config.js +0 -141
  108. package/bin/agent-setup.js +0 -146
  109. package/bin/agent.js +0 -42
  110. package/bin/analytics/client.js +0 -60
  111. package/bin/analytics/dates.js +0 -6
  112. package/bin/analytics/filters.js +0 -27
  113. package/bin/analytics/format.js +0 -13
  114. package/bin/analytics/index.js +0 -496
  115. package/bin/analytics/output.js +0 -75
  116. package/bin/analytics/types.js +0 -1
  117. package/bin/authenticatedFetch.js +0 -46
  118. package/bin/brokenLinks.js +0 -122
  119. package/bin/callbackServer.js +0 -65
  120. package/bin/cli.js +0 -813
  121. package/bin/comingSoon.js +0 -26
  122. package/bin/config.js +0 -78
  123. package/bin/constants.js +0 -16
  124. package/bin/deploy-base.js +0 -20
  125. package/bin/deploy-github-actions.js +0 -102
  126. package/bin/deploy-github-setup.js +0 -165
  127. package/bin/deploy-instructions.js +0 -71
  128. package/bin/deploy.js +0 -123
  129. package/bin/feedback.js +0 -36
  130. package/bin/format.js +0 -121
  131. package/bin/helpers.js +0 -238
  132. package/bin/idToken.js +0 -14
  133. package/bin/index.js +0 -102
  134. package/bin/init.js +0 -182
  135. package/bin/instructions.js +0 -101
  136. package/bin/keyring.js +0 -78
  137. package/bin/login.js +0 -123
  138. package/bin/logout.js +0 -18
  139. package/bin/mdxAccessibility.js +0 -113
  140. package/bin/mdxLinter.js +0 -55
  141. package/bin/middlewares/subdomainMiddleware.js +0 -28
  142. package/bin/middlewares/telemetryMiddleware.js +0 -61
  143. package/bin/migrate.js +0 -54
  144. package/bin/og.js +0 -145
  145. package/bin/openApiCheck.js +0 -99
  146. package/bin/resolveFiles.js +0 -114
  147. package/bin/score/client.js +0 -40
  148. package/bin/score/index.js +0 -211
  149. package/bin/score/types.js +0 -1
  150. package/bin/signup.js +0 -118
  151. package/bin/start.js +0 -36
  152. package/bin/status.js +0 -106
  153. package/bin/telemetry/client.js +0 -53
  154. package/bin/telemetry/distinctId.js +0 -68
  155. package/bin/telemetry/sessionId.js +0 -36
  156. package/bin/telemetry/track.js +0 -149
  157. package/bin/templates.js +0 -127
  158. package/bin/tokenRefresh.js +0 -50
  159. package/bin/tsconfig.build.tsbuildinfo +0 -1
  160. package/bin/update.js +0 -65
  161. package/bin/validate/command.js +0 -127
  162. package/bin/validate/engine.js +0 -154
  163. package/bin/validate/files.js +0 -163
  164. package/bin/validate/mdx.js +0 -238
  165. package/bin/validate/output.js +0 -44
  166. package/bin/validate/report.js +0 -11
  167. package/bin/validate/rules/build.js +0 -102
  168. package/bin/validate/rules/content/duplicateH1.js +0 -57
  169. package/bin/validate/rules/content/foreignSyntax.js +0 -120
  170. package/bin/validate/rules/content/katex.js +0 -34
  171. package/bin/validate/rules/content/missingSnippet.js +0 -34
  172. package/bin/validate/rules/content/undefinedVariable.js +0 -13
  173. package/bin/validate/rules/content/unknownComponent.js +0 -121
  174. package/bin/validate/rules/content/voidTags.js +0 -38
  175. package/bin/validate/rules/index.js +0 -21
  176. package/bin/validate/rules/openapi.js +0 -50
  177. package/bin/validate/rules/parseError.js +0 -7
  178. package/bin/validate/site.js +0 -57
  179. package/bin/validate/types.js +0 -1
  180. package/bin/vendor/converter.js +0 -30882
  181. package/bin/welcome.js +0 -108
  182. package/bin/workflows/client.js +0 -34
  183. package/bin/workflows/index.js +0 -269
  184. package/bin/workflows/types.js +0 -11
  185. package/scripts/bundle-converter.mjs +0 -26
  186. package/scripts/generate-logo.mjs +0 -79
  187. package/src/accessibility.ts +0 -180
  188. package/src/accessibilityCheck.tsx +0 -150
  189. package/src/addDomain.tsx +0 -206
  190. package/src/agent-auth.ts +0 -119
  191. package/src/agent-claim.ts +0 -124
  192. package/src/agent-credentials.ts +0 -104
  193. package/src/agent-deployment.ts +0 -116
  194. package/src/agent-http.ts +0 -34
  195. package/src/agent-setup-config.ts +0 -144
  196. package/src/agent-setup.ts +0 -165
  197. package/src/agent.ts +0 -56
  198. package/src/analytics/client.ts +0 -153
  199. package/src/analytics/dates.ts +0 -6
  200. package/src/analytics/filters.ts +0 -50
  201. package/src/analytics/format.ts +0 -13
  202. package/src/analytics/index.tsx +0 -599
  203. package/src/analytics/output.ts +0 -99
  204. package/src/analytics/types.ts +0 -145
  205. package/src/authenticatedFetch.ts +0 -42
  206. package/src/brokenLinks.tsx +0 -152
  207. package/src/callbackServer.ts +0 -64
  208. package/src/cli.tsx +0 -1105
  209. package/src/comingSoon.tsx +0 -29
  210. package/src/config.ts +0 -75
  211. package/src/constants.ts +0 -28
  212. package/src/deploy-base.ts +0 -18
  213. package/src/deploy-github-actions.ts +0 -139
  214. package/src/deploy-github-setup.ts +0 -189
  215. package/src/deploy-instructions.ts +0 -76
  216. package/src/deploy.ts +0 -165
  217. package/src/feedback.tsx +0 -31
  218. package/src/format.tsx +0 -82
  219. package/src/helpers.tsx +0 -279
  220. package/src/idToken.ts +0 -17
  221. package/src/index.ts +0 -109
  222. package/src/init.tsx +0 -237
  223. package/src/instructions.ts +0 -99
  224. package/src/keyring.ts +0 -65
  225. package/src/login.tsx +0 -166
  226. package/src/logout.tsx +0 -8
  227. package/src/mdxAccessibility.ts +0 -134
  228. package/src/mdxLinter.tsx +0 -88
  229. package/src/middlewares/subdomainMiddleware.ts +0 -20
  230. package/src/middlewares/telemetryMiddleware.ts +0 -55
  231. package/src/migrate.ts +0 -76
  232. package/src/og.tsx +0 -185
  233. package/src/openApiCheck.tsx +0 -125
  234. package/src/resolveFiles.ts +0 -101
  235. package/src/score/client.ts +0 -36
  236. package/src/score/index.tsx +0 -205
  237. package/src/score/types.ts +0 -15
  238. package/src/signup.tsx +0 -172
  239. package/src/start.ts +0 -27
  240. package/src/status.tsx +0 -129
  241. package/src/telemetry/client.ts +0 -46
  242. package/src/telemetry/distinctId.ts +0 -70
  243. package/src/telemetry/sessionId.ts +0 -40
  244. package/src/telemetry/track.ts +0 -163
  245. package/src/templates.tsx +0 -143
  246. package/src/tokenRefresh.ts +0 -53
  247. package/src/update.tsx +0 -89
  248. package/src/validate/command.tsx +0 -135
  249. package/src/validate/engine.ts +0 -210
  250. package/src/validate/files.ts +0 -249
  251. package/src/validate/mdx.ts +0 -290
  252. package/src/validate/output.tsx +0 -76
  253. package/src/validate/report.ts +0 -40
  254. package/src/validate/rules/build.ts +0 -146
  255. package/src/validate/rules/content/duplicateH1.ts +0 -78
  256. package/src/validate/rules/content/foreignSyntax.ts +0 -144
  257. package/src/validate/rules/content/katex.ts +0 -33
  258. package/src/validate/rules/content/missingSnippet.ts +0 -36
  259. package/src/validate/rules/content/undefinedVariable.ts +0 -20
  260. package/src/validate/rules/content/unknownComponent.ts +0 -130
  261. package/src/validate/rules/content/voidTags.ts +0 -43
  262. package/src/validate/rules/index.ts +0 -23
  263. package/src/validate/rules/openapi.ts +0 -59
  264. package/src/validate/rules/parseError.ts +0 -9
  265. package/src/validate/site.ts +0 -53
  266. package/src/validate/types.ts +0 -102
  267. package/src/vendor/converter.ts +0 -1
  268. package/src/welcome.ts +0 -154
  269. package/src/workflows/client.ts +0 -39
  270. package/src/workflows/index.tsx +0 -287
  271. package/src/workflows/types.ts +0 -73
  272. package/tsconfig.build.json +0 -4
  273. package/tsconfig.json +0 -10
  274. package/vitest.config.ts +0 -9
@@ -1,1275 +0,0 @@
1
- import { getAST } from '@mintlify/common';
2
- import fs from 'fs-extra';
3
- import fsSync from 'node:fs';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
-
7
- import { checkSite, selectRules, UsageError } from '../src/validate/engine.js';
8
- import { toParseError } from '../src/validate/files.js';
9
- import { applyEdits, maskNonMarkup, relocate, unmapEdit } from '../src/validate/mdx.js';
10
- import type { Diagnostic } from '../src/validate/types.js';
11
-
12
- vi.mock('@mintlify/prebuild', async (importOriginal) => ({
13
- ...(await importOriginal<typeof import('@mintlify/prebuild')>()),
14
- categorizeFilePaths: vi.fn(() => {
15
- throw new Error('lint must not categorize every file');
16
- }),
17
- }));
18
-
19
- const hasErrors = (diagnostics: Diagnostic[]) =>
20
- diagnostics.some(({ severity }) => severity === 'error');
21
-
22
- /** Runs the file rules (all defaults, or exactly `rules`) the way mint validate --no-build does. */
23
- const check = async (
24
- dir: string,
25
- { rules, fix = false, files }: { rules?: string[]; fix?: boolean; files?: string[] }
26
- ) => {
27
- const selected = selectRules({ disable: ['all'], enable: rules ?? ['content'] });
28
- const result = await checkSite(dir, selected, {
29
- selected: files && new Set(files),
30
- localSchema: false,
31
- fix,
32
- });
33
- const issues = result.diagnostics.map((diagnostic) => ({
34
- ...diagnostic,
35
- fixable: diagnostic.fix !== undefined,
36
- }));
37
- return {
38
- issues,
39
- fixed: result.fixed,
40
- filesFixed: [...new Set(result.fixed.map(({ file }) => file))].sort(),
41
- filesChecked: result.site.files.filter(({ file }) => !files || files.includes(file)).length,
42
- };
43
- };
44
-
45
- const write = (dir: string, files: Record<string, string>) => {
46
- fs.writeJSONSync(path.join(dir, 'docs.json'), {
47
- name: 'Test',
48
- navigation: { pages: ['index'] },
49
- });
50
- for (const [file, content] of Object.entries(files)) {
51
- fs.outputFileSync(path.join(dir, file), content);
52
- }
53
- };
54
-
55
- describe('validate rules', () => {
56
- let dir: string;
57
-
58
- beforeEach(() => {
59
- dir = fs.mkdtempSync(path.join(os.tmpdir(), 'lint-'));
60
- });
61
-
62
- afterEach(() => {
63
- fs.removeSync(dir);
64
- });
65
-
66
- const read = (file: string) => fs.readFileSync(path.join(dir, file), 'utf-8');
67
-
68
- it('self-closes lowercase void tags, skipping code and components', async () => {
69
- write(dir, {
70
- 'index.mdx':
71
- '---\ntitle: Home\n---\n\nline<br>\n\n<hr>\n\n<img src="/a.png" alt="a">\n\n<Link>x</Link>\n\n`<br>`\n\n```html\n<br>\n```\n',
72
- });
73
-
74
- const plain = await check(dir, {});
75
- expect(plain.issues.map(({ rule }) => rule)).toEqual(['void-tags', 'void-tags', 'void-tags']);
76
-
77
- const fixed = await check(dir, { fix: true });
78
- expect(fixed.issues).toEqual([]);
79
- const content = read('index.mdx');
80
- expect(content).toContain('line<br />');
81
- expect(content).toContain('<hr />');
82
- expect(content).toContain('<img src="/a.png" alt="a" />');
83
- expect(content).toContain('`<br>`');
84
- expect(content).toContain('```html\n<br>\n```');
85
- });
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
-
209
- it('skips void tags inside block-quoted code fences', async () => {
210
- write(dir, {
211
- 'index.mdx': '---\ntitle: Home\n---\n\n> ~~~html\n> <br>\n> ~~~\n\nline<br>\n',
212
- 'other.mdx': '---\ntitle: Other\n---\n\n> ```\n> <br>\n\nafter<br>\n',
213
- });
214
-
215
- const result = await check(dir, { fix: true });
216
-
217
- expect(result.fixed.map(({ before }) => before).sort()).toEqual(['<br>', '<br>']);
218
- expect(read('index.mdx')).toContain('> <br>\n');
219
- expect(read('index.mdx')).toContain('line<br />');
220
- expect(read('other.mdx')).toContain('> <br>\n');
221
- expect(read('other.mdx')).toContain('after<br />');
222
- });
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
-
338
- it('keeps an info-string fence line from closing an open fence', async () => {
339
- write(dir, {
340
- 'index.mdx':
341
- '---\ntitle: Home\n---\n\n```bash\n$ one\n\nStep 2\n\n```bash\n$ two <br>\n```\n\nafter<br>\n',
342
- });
343
-
344
- const result = await check(dir, { fix: true });
345
-
346
- expect(result.fixed.map(({ before }) => before)).toEqual(['<br>']);
347
- const content = read('index.mdx');
348
- expect(content).toContain('$ two <br>\n');
349
- expect(content).toContain('after<br />');
350
- });
351
-
352
- it('does not close a block-quoted fence with an info-string fence line', async () => {
353
- write(dir, {
354
- 'index.mdx':
355
- '---\ntitle: Home\n---\n\n> ```\n> <br>\n> ```bash\n> <hr>\n> ```\n\nafter<br>\n',
356
- });
357
-
358
- const result = await check(dir, { fix: true });
359
-
360
- expect(result.fixed.map(({ before }) => before)).toEqual(['<br>']);
361
- const content = read('index.mdx');
362
- expect(content).toContain('> <hr>\n');
363
- expect(content).toContain('after<br />');
364
- });
365
-
366
- it('skips custom elements, frontmatter, ESM and attribute strings for void tags', async () => {
367
- write(dir, {
368
- 'index.mdx':
369
- '---\ntitle: "a<br>b"\n---\n\nimport { A } from "/snippets/a.jsx";\nexport const html = "<br>";\n\n<img-comparison-slider>x</img-comparison-slider>\n\n<Card title="one<br>two">x</Card>\n\n<img src="/a.png" alt="<hr>">\n\n`` a`<br>`b ``\n',
370
- });
371
-
372
- const { issues } = await check(dir, { rules: ['void-tags'] });
373
- expect(issues.map(({ line }) => line)).toEqual([12]);
374
-
375
- await check(dir, { rules: ['void-tags'], fix: true });
376
- const content = read('index.mdx');
377
- expect(content).toContain('<img src="/a.png" alt="<hr>" />');
378
- expect(content).toContain('title: "a<br>b"');
379
- expect(content).toContain('export const html = "<br>";');
380
- expect(content).toContain('<Card title="one<br>two">');
381
- expect(content).toContain('`` a`<br>`b ``');
382
- });
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': 'error',
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
-
552
- it('reports {{variables}} docs.json does not define, outside code', async () => {
553
- write(dir, {
554
- 'index.mdx':
555
- "---\ntitle: Home\n---\n\nVersion {{version}} of {{product}}.\n\n`{{inline}}`\n\n```jinja\n{{ fenced }}\n```\n\nexport const color = 'red';\n\n<div style={{color}} data-x = {{ color }}>Hi</div>\n",
556
- 'snippets/note.mdx': 'Hi {{ missing }}\n',
557
- });
558
- fs.writeJSONSync(path.join(dir, 'docs.json'), {
559
- name: 'Test',
560
- navigation: { pages: ['index'] },
561
- variables: { product: 'Acme' },
562
- });
563
-
564
- const { issues } = await check(dir, { rules: ['undefined-variable'] });
565
-
566
- expect(
567
- issues.map(({ file, line, column, severity, message }) => [
568
- file,
569
- line,
570
- column,
571
- severity,
572
- message,
573
- ])
574
- ).toEqual([
575
- ['index.mdx', 5, 9, 'error', '{{version}} is not defined in the variables of docs.json'],
576
- [
577
- 'snippets/note.mdx',
578
- 1,
579
- 4,
580
- 'error',
581
- '{{missing}} is not defined in the variables of docs.json',
582
- ],
583
- ]);
584
- });
585
-
586
- it('reports legacy <Snippet file> tags the build cannot resolve', async () => {
587
- write(dir, {
588
- '_snippets/a.mdx': 'A\n',
589
- 'snippets/b.mdx': 'B\n',
590
- 'index.mdx':
591
- '---\ntitle: Home\n---\n\n<Snippet file="a.mdx" />\n\n<Snippet file="{{part}}" />\n\n<Snippet file="snippets/b.mdx" />\n\n<Snippet file="b.mdx" />\n\n<Snippet file="/a.mdx" />\n',
592
- });
593
- fs.writeJSONSync(path.join(dir, 'docs.json'), {
594
- name: 'Test',
595
- navigation: { pages: ['index'] },
596
- variables: { part: 'a.mdx' },
597
- });
598
-
599
- const { issues } = await check(dir, { rules: ['missing-snippet'] });
600
-
601
- expect(issues.map(({ line, severity, message }) => [line, severity, message])).toEqual([
602
- [11, 'error', '<Snippet file="b.mdx"> renders nothing: no snippet matches "b.mdx"'],
603
- [13, 'error', '<Snippet file="/a.mdx"> renders nothing: no snippet matches "/a.mdx"'],
604
- ]);
605
- });
606
-
607
- it('leaves escaped void tags alone', async () => {
608
- write(dir, { 'index.mdx': '---\ntitle: Home\n---\n\nText \\<br> and \\<img src="/a.png">\n' });
609
-
610
- expect((await check(dir, { rules: ['void-tags'] })).issues).toEqual([]);
611
- });
612
-
613
- it('substitutes docs.json variables in snippets before checking them', async () => {
614
- write(dir, {
615
- 'index.mdx': '---\ntitle: Home\n---\n\nok\n',
616
- 'snippets/note.mdx': 'Hello {{foo.bar}} line<br>\n',
617
- });
618
- fs.writeJSONSync(path.join(dir, 'docs.json'), {
619
- name: 'Test',
620
- navigation: { pages: ['index'] },
621
- variables: { 'foo.bar': 'a much longer value' },
622
- });
623
-
624
- const { issues } = await check(dir, {});
625
-
626
- expect(issues.map(({ rule, line, column }) => [rule, line, column])).toEqual([
627
- ['void-tags', 1, 23],
628
- ]);
629
-
630
- await check(dir, { fix: true });
631
- expect(read('snippets/note.mdx')).toBe('Hello {{foo.bar}} line<br />\n');
632
- });
633
-
634
- it('leaves void tags that already have a closing tag', async () => {
635
- const page = '---\ntitle: Home\n---\n\n<img src="/a.png" alt="a"></img>\n\nline<br></br>\n';
636
- write(dir, { 'index.mdx': page });
637
-
638
- expect((await check(dir, { rules: ['void-tags'] })).issues).toEqual([]);
639
- await check(dir, { rules: ['void-tags'], fix: true });
640
- expect(read('index.mdx')).toBe(page);
641
- });
642
-
643
- it('parses headings with a spaced custom id and keeps their fixes', async () => {
644
- write(dir, {
645
- 'index.mdx': '---\ntitle: Hello\n---\n\n# Hello\n\n## Setup { #setup }\n\nBody\n',
646
- });
647
-
648
- const { issues } = await check(dir, {});
649
-
650
- expect(issues).toEqual([expect.objectContaining({ rule: 'duplicate-h1', fixable: true })]);
651
- });
652
-
653
- it('accepts <MDX> fragments the build preprocesses', async () => {
654
- write(dir, {
655
- 'index.mdx':
656
- '---\ntitle: Home\n---\n\n{true ? <MDX>\n```bash\n<root>/{arch}/app.vpkg\n```\n</MDX> : null}\n',
657
- });
658
-
659
- expect((await check(dir, {})).issues).toEqual([]);
660
- });
661
-
662
- it('walks only Markdown files and respects .mintignore', async () => {
663
- write(dir, {
664
- '.mintignore': 'drafts/\n',
665
- 'openapi.yaml': 'openapi: [unclosed\n',
666
- 'drafts/wip.mdx': '---\ntitle: Wip\n---\n\n<Missing />\n',
667
- 'SKILL.md': '<Missing />\n',
668
- 'index.mdx': '---\ntitle: Home\n---\n\n<Missing />\n',
669
- });
670
-
671
- const result = await check(dir, { rules: ['unknown-component'] });
672
-
673
- expect(result.filesChecked).toBe(1);
674
- expect(result.issues.map(({ file }) => file)).toEqual(['index.mdx']);
675
- });
676
-
677
- it('does not mask void tags outside strings, but skips expression strings, multi-line exports and BOM frontmatter', async () => {
678
- write(dir, {
679
- 'index.mdx':
680
- '---\ntitle: "a<br>b"\n---\n\nexport const items = [\n "<br>",\n\n "<hr>",\n];\n\nText {"<br>"} and {cond && \'<img>\'}\n\nline<br>\n',
681
- });
682
-
683
- const { issues } = await check(dir, { rules: ['void-tags'] });
684
-
685
- expect(issues.map(({ line }) => line)).toEqual([13]);
686
- });
687
-
688
- it('ends an export block at a blank line once it parses, despite brackets in literals', async () => {
689
- write(dir, {
690
- 'index.mdx':
691
- '---\ntitle: Home\n---\n\nexport const css = `\n .a { color: red;\n`;\n\nfirst<br>\n\nexport const re = /[(]/;\n\nsecond<br>\n\nexport const items = [\n "<br>",\n\n "<hr>",\n];\n\nthird<br>\n',
692
- });
693
-
694
- const { issues } = await check(dir, { rules: ['void-tags'] });
695
-
696
- expect(issues.map(({ line }) => line)).toEqual([9, 13, 21]);
697
- });
698
-
699
- it('masks regex literals in expression value position but not division', async () => {
700
- write(dir, {
701
- 'index.mdx':
702
- "---\ntitle: Home\n---\n\n{items.map((s) => s.replace(/<br>/g, ''))}\n\n{total / width / 2} and<br>\n\n{value.replace(/\\/<hr>/, '')}\n\nline<br>\n",
703
- });
704
-
705
- const { issues } = await check(dir, { rules: ['void-tags'] });
706
- expect(issues.map(({ rule, line }) => [rule, line])).toEqual([
707
- ['void-tags', 7],
708
- ['void-tags', 11],
709
- ]);
710
-
711
- await check(dir, { rules: ['void-tags'], fix: true });
712
- const content = read('index.mdx');
713
- expect(content).toContain("s.replace(/<br>/g, '')");
714
- expect(content).toContain("replace(/\\/<hr>/, '')");
715
- expect(content).toContain('and<br />');
716
- expect(content).toContain('line<br />');
717
- });
718
-
719
- it('masks line comments inside expressions', async () => {
720
- write(dir, {
721
- 'index.mdx':
722
- '---\ntitle: Home\n---\n\n{\n// <br>\n}\n\n{(\n // <hr>\n value\n)}\n\n{total // <br>\n}\n\nline<br>\n',
723
- });
724
-
725
- const { issues } = await check(dir, { rules: ['void-tags'] });
726
-
727
- expect(issues.map(({ line }) => line)).toEqual([17]);
728
- });
729
-
730
- it('treats quotes in JSX text inside an expression as text, not strings', async () => {
731
- write(dir, {
732
- 'index.mdx':
733
- '---\ntitle: Home\n---\n\n{cond && <p>Don\'t {"<br>"} miss this</p>}\n\nline<br>\n\n{list.map((item) => <li key={item}>It\'s {item}</li>)}\n\nmore<hr>\n',
734
- });
735
-
736
- const { issues } = await check(dir, { rules: ['void-tags'] });
737
-
738
- expect(issues.map(({ line }) => line)).toEqual([7, 11]);
739
- });
740
-
741
- it('reports but does not remove a duplicate heading that other pages link to', async () => {
742
- write(dir, {
743
- 'guides/start.mdx': '---\ntitle: Quickstart\n---\n\n# Quickstart\n\nBody\n',
744
- 'index.mdx': '---\ntitle: Home\n---\n\nSee [the start](/guides/start#quickstart).\n',
745
- 'guides/other.mdx': '---\ntitle: Other\n---\n\nUnrelated [link](./start#install).\n',
746
- 'plain.mdx': '---\ntitle: Plain\n---\n\n# Plain\n\nBody\n',
747
- });
748
-
749
- const result = await check(dir, { rules: ['duplicate-h1'], fix: true });
750
-
751
- expect(result.filesFixed).toEqual(['plain.mdx']);
752
- expect(read('guides/start.mdx')).toContain('# Quickstart');
753
- expect(result.issues).toEqual([
754
- expect.objectContaining({
755
- file: 'guides/start.mdx',
756
- fixable: false,
757
- message: expect.stringContaining('#quickstart'),
758
- }),
759
- ]);
760
- });
761
-
762
- it('finds relative and same-page links to the heading anchor', async () => {
763
- write(dir, {
764
- 'guides/start.mdx':
765
- '---\ntitle: Quickstart\n---\n\n# Quickstart\n\nBack to [top](#quickstart).\n',
766
- 'guides/next.mdx': '---\ntitle: Next\n---\n\n# Next\n\n',
767
- 'guides/after.mdx': '---\ntitle: After\n---\n\nRead <a href="next#next">next</a>.\n',
768
- });
769
-
770
- const { issues } = await check(dir, { rules: ['duplicate-h1'] });
771
-
772
- expect(issues.map(({ file, fixable }) => [file, fixable])).toEqual([
773
- ['guides/next.mdx', false],
774
- ['guides/start.mdx', false],
775
- ]);
776
- });
777
-
778
- it('ignores heading links inside code', async () => {
779
- write(dir, {
780
- 'guides/start.mdx': '---\ntitle: Quickstart\n---\n\n# Quickstart\n\nBody\n',
781
- 'index.mdx':
782
- '---\ntitle: Home\n---\n\n```md\n[start](/guides/start#quickstart)\n```\n\nUse `<a href="/guides/start#quickstart">`.\n',
783
- });
784
-
785
- const result = await check(dir, { rules: ['duplicate-h1'], fix: true });
786
-
787
- expect(result.filesFixed).toEqual(['guides/start.mdx']);
788
- });
789
-
790
- it('keeps a duplicate homepage heading that a root hash link points to', async () => {
791
- write(dir, {
792
- 'index.mdx': '---\ntitle: Home\n---\n\n# Home\n\nBody\n',
793
- 'guides/start.mdx': '---\ntitle: Start\n---\n\nBack [home](/#home).\n',
794
- });
795
-
796
- const result = await check(dir, { rules: ['duplicate-h1'], fix: true });
797
-
798
- expect(result.filesFixed).toEqual([]);
799
- expect(read('index.mdx')).toContain('# Home');
800
- });
801
-
802
- it('finds heading links that carry a query string', async () => {
803
- write(dir, {
804
- 'guides/start.mdx': '---\ntitle: Quickstart\n---\n\n# Quickstart\n\nBody\n',
805
- 'index.mdx': '---\ntitle: Home\n---\n\nSee [the start](/guides/start?tab=api#quickstart).\n',
806
- });
807
-
808
- const result = await check(dir, { rules: ['duplicate-h1'], fix: true });
809
-
810
- expect(result.filesFixed).toEqual([]);
811
- expect(read('guides/start.mdx')).toContain('# Quickstart');
812
- });
813
-
814
- it('keeps a duplicate heading when links point to a later heading that would take its id', async () => {
815
- write(dir, {
816
- 'index.mdx': '---\ntitle: Setup\n---\n\n# Setup\n\n## Setup\n\nBody\n',
817
- 'other.mdx': '---\ntitle: Other\n---\n\nSee [the details](/index#setup-2).\n',
818
- });
819
-
820
- const result = await check(dir, { rules: ['duplicate-h1'], fix: true });
821
-
822
- expect(result.filesFixed).toEqual([]);
823
- expect(read('index.mdx')).toContain('# Setup\n');
824
- expect(result.issues).toEqual([
825
- expect.objectContaining({ fixable: false, message: expect.stringContaining('#setup-2') }),
826
- ]);
827
- });
828
-
829
- it('finds anchor links in string-literal expression hrefs', async () => {
830
- write(dir, {
831
- 'double.mdx': '---\ntitle: Double\n---\n\n# Double\n',
832
- 'template.mdx': '---\ntitle: Template\n---\n\n# Template\n',
833
- 'dynamic.mdx': '---\ntitle: Dynamic\n---\n\n# Dynamic\n',
834
- 'links.mdx':
835
- '---\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',
836
- });
837
-
838
- const result = await check(dir, { rules: ['duplicate-h1'], fix: true });
839
-
840
- expect(result.filesFixed).toEqual(['dynamic.mdx']);
841
- expect(read('double.mdx')).toContain('# Double');
842
- expect(read('template.mdx')).toContain('# Template');
843
- });
844
-
845
- it('finds anchor links in hrefs with spaces around the equals sign', async () => {
846
- write(dir, {
847
- 'guides/start.mdx': '---\ntitle: Quickstart\n---\n\n# Quickstart\n\nBody\n',
848
- 'index.mdx': '---\ntitle: Home\n---\n\n<a href = "/guides/start#quickstart">Start</a>\n',
849
- });
850
-
851
- const result = await check(dir, { rules: ['duplicate-h1'], fix: true });
852
-
853
- expect(result.filesFixed).toEqual([]);
854
- expect(read('guides/start.mdx')).toContain('# Quickstart');
855
- });
856
-
857
- it('keeps file order when linting many files concurrently', async () => {
858
- const names = Array.from(
859
- { length: 40 },
860
- (_, index) => `page-${String(index).padStart(2, '0')}.mdx`
861
- );
862
- write(dir, Object.fromEntries(names.map((name) => [name, '---\ntitle: T\n---\n\nline<br>\n'])));
863
-
864
- const plain = await check(dir, { rules: ['void-tags'] });
865
- expect(plain.issues.map(({ file }) => file)).toEqual(names);
866
-
867
- const fixed = await check(dir, { rules: ['void-tags'], fix: true });
868
- expect(fixed.fixed.map(({ file }) => file)).toEqual(names);
869
- });
870
-
871
- it('rejects unknown rule ids', async () => {
872
- write(dir, { 'index.mdx': '---\ntitle: Home\n---\n' });
873
-
874
- await expect(check(dir, { rules: ['nope'] })).rejects.toThrow('unknown rule or group "nope"');
875
- });
876
-
877
- it('reports every file that fails to parse in one run, with its position', async () => {
878
- write(dir, {
879
- 'index.mdx': '---\ntitle: Home\n---\n\nok\n',
880
- 'a.mdx': '---\ntitle: A\n---\n\n<Note>\nunclosed\n',
881
- 'b.mdx': '---\ntitle: B\n---\n\nText {broken\n',
882
- });
883
-
884
- const result = await check(dir, {});
885
- const parseErrors = result.issues.filter(({ rule }) => rule === 'parse-error');
886
-
887
- expect(parseErrors.map(({ file }) => file).sort()).toEqual(['a.mdx', 'b.mdx']);
888
- for (const issue of parseErrors) {
889
- expect(issue).toMatchObject({ severity: 'error', fixable: false });
890
- expect(issue.line).toBeGreaterThan(0);
891
- expect(issue.message).toContain('(other checks were skipped for this file)');
892
- }
893
- expect(hasErrors(result.issues)).toBe(true);
894
- });
895
-
896
- it('suggests escaping a < that cannot start a tag', async () => {
897
- write(dir, {
898
- 'index.mdx': '---\ntitle: Home\n---\n\nLatency is <5ms.\n',
899
- 'end.mdx': '---\ntitle: End\n---\n\nThe last one <\n',
900
- });
901
-
902
- const { issues } = await check(dir, {});
903
-
904
- expect(issues.map(({ file, line, column, message }) => [file, line, column, message])).toEqual([
905
- [
906
- 'end.mdx',
907
- 5,
908
- 15,
909
- "Unexpected end of file before name, expected a character that can start a name, such as a letter, `$`, or `_`; if this < isn't a tag, escape it as \\< (other checks were skipped for this file)",
910
- ],
911
- [
912
- 'index.mdx',
913
- 5,
914
- 13,
915
- "Unexpected character `5` (U+0035) before name, expected a character that can start a name, such as a letter, `$`, or `_`; if this < isn't a tag, escape it as \\< (other checks were skipped for this file)",
916
- ],
917
- ]);
918
- });
919
-
920
- it('does not suggest escaping < for other parse errors', async () => {
921
- write(dir, {
922
- 'index.mdx': '---\ntitle: Home\n---\n\nReturns a List<String>.\n',
923
- 'math.mdx': '---\ntitle: Math\n---\n\nTotal {1 <}\n',
924
- });
925
-
926
- const { issues } = await check(dir, {});
927
-
928
- expect(issues.map(({ rule }) => rule)).toEqual(['parse-error', 'parse-error']);
929
- for (const { message } of issues) expect(message).not.toContain('escape it');
930
- });
931
-
932
- it('still runs parse-error when --rules picks only other rules', async () => {
933
- write(dir, { 'index.mdx': '---\ntitle: Home\n---\n\n<Note>\nunclosed\n' });
934
-
935
- const result = await check(dir, { rules: ['void-tags'] });
936
-
937
- expect(result.issues.map(({ rule }) => rule)).toEqual(['parse-error']);
938
- await expect(check(dir, { rules: ['parse-error'] })).rejects.toThrow(
939
- "parse-error always runs and can't be disabled"
940
- );
941
- });
942
-
943
- it('accepts pages the build parses, such as code inside <MDX>, without offering edits', async () => {
944
- write(dir, {
945
- 'index.mdx': '---\ntitle: Home\n---\n\n<MDX>\nif (a < b) { run() }\n</MDX>\n\n<Missing />\n',
946
- });
947
-
948
- const result = await check(dir, {});
949
-
950
- expect(result.issues.map(({ rule }) => rule)).toEqual(['unknown-component']);
951
- expect(result.issues[0]?.fixable).toBe(false);
952
- });
953
-
954
- it('lints snippets, skipping page-only rules', async () => {
955
- write(dir, {
956
- 'index.mdx': '---\ntitle: Home\n---\n\nok\n',
957
- 'snippets/note.mdx': '---\ntitle: Same\n---\n\n# Same\n\nline<br>\n\n<FromPage />\n',
958
- });
959
-
960
- const result = await check(dir, {});
961
-
962
- expect(result.issues.map(({ file, rule }) => [file, rule])).toEqual([
963
- ['snippets/note.mdx', 'void-tags'],
964
- ]);
965
- });
966
-
967
- it('orders findings on one line left to right across rules', async () => {
968
- write(dir, {
969
- 'index.mdx': '---\ntitle: Home\n---\n\nSee <Missing /> and a line break here <br>\n',
970
- });
971
-
972
- const result = await check(dir, { rules: ['void-tags', 'unknown-component'] });
973
-
974
- expect(result.issues.map(({ rule, line, column }) => [rule, line, column])).toEqual([
975
- ['unknown-component', 5, 5],
976
- ['void-tags', 5, 39],
977
- ]);
978
- });
979
-
980
- it('names leftover GitBook syntax even when the page does not parse', async () => {
981
- write(dir, {
982
- 'index.mdx': '---\ntitle: Home\n---\n\n{% hint style="info" %}\nRead this\n{% endhint %}\n',
983
- });
984
-
985
- const result = await check(dir, {});
986
-
987
- expect(result.issues.map(({ rule, line }) => [rule, line])).toEqual([
988
- ['foreign-syntax', 5],
989
- ['parse-error', 5],
990
- ]);
991
- expect(result.issues[0]?.message).toContain('use <Info>');
992
- });
993
-
994
- it('skips foreign syntax inside JSX attribute and JavaScript strings', async () => {
995
- write(dir, {
996
- 'index.mdx':
997
- '---\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',
998
- });
999
-
1000
- const { issues } = await check(dir, { rules: ['foreign-syntax'] });
1001
- const found = issues.filter(({ rule }) => rule === 'foreign-syntax');
1002
-
1003
- expect(found.map(({ line, message }) => [line, message.split(' ')[0]])).toEqual([
1004
- [5, 'doc:intro'],
1005
- [9, '{{'],
1006
- [11, 'doc:intro'],
1007
- ]);
1008
- });
1009
-
1010
- it('reports foreign syntax the build accepts without failing lint', async () => {
1011
- write(dir, { 'index.mdx': '---\ntitle: Home\n---\n\n:::note\nRead this\n:::\n' });
1012
-
1013
- const result = await check(dir, {});
1014
-
1015
- expect(result.issues.map(({ rule, severity }) => [rule, severity])).toEqual([
1016
- ['foreign-syntax', 'warning'],
1017
- ]);
1018
- expect(hasErrors(result.issues)).toBe(false);
1019
- });
1020
-
1021
- it('rejects --files that name no page or snippet of the site', async () => {
1022
- write(dir, { 'index.mdx': '---\ntitle: Home\n---\n\nok\n', 'README.md': '# Readme\n' });
1023
-
1024
- await expect(check(dir, { files: ['README.md'] })).rejects.toThrow(UsageError);
1025
- });
1026
-
1027
- it('only lints the files it is given', async () => {
1028
- write(dir, {
1029
- 'index.mdx': '---\ntitle: Home\n---\n\nline<br>\n',
1030
- 'other.mdx': '---\ntitle: Other\n---\n\nline<br>\n',
1031
- });
1032
-
1033
- const result = await check(dir, { files: ['other.mdx'] });
1034
-
1035
- expect(result.filesChecked).toBe(1);
1036
- expect(result.issues.map(({ file }) => file)).toEqual(['other.mdx']);
1037
- });
1038
-
1039
- it('never reads the content of unselected files in the check pass', async () => {
1040
- write(dir, {
1041
- 'index.mdx': '---\ntitle: Home\n---\n\nline<br>\n',
1042
- 'other.mdx': '---\ntitle: Other\n---\n\nline<br>\n',
1043
- });
1044
- const spy = vi.spyOn(fsSync, 'readFileSync');
1045
-
1046
- const result = await check(dir, { rules: ['void-tags'], files: ['other.mdx'] });
1047
-
1048
- const reads = spy.mock.calls.map(([file]) => String(file));
1049
- spy.mockRestore();
1050
- expect(reads.some((file) => file.endsWith('other.mdx'))).toBe(true);
1051
- expect(reads.some((file) => file.endsWith('index.mdx'))).toBe(false);
1052
- expect(result.issues.map(({ file }) => file)).toEqual(['other.mdx']);
1053
- });
1054
-
1055
- it('does no anchor reads when duplicate-h1 is disabled', async () => {
1056
- write(dir, {
1057
- 'index.mdx': '---\ntitle: Home\n---\n\n# Home\n',
1058
- 'other.mdx': '---\ntitle: Other\n---\n\nSee [home](/index#home).\n',
1059
- });
1060
- const spy = vi.spyOn(fsSync, 'readFileSync');
1061
-
1062
- const result = await checkSite(dir, selectRules({ disable: ['duplicate-h1'] }), {
1063
- selected: new Set(['index.mdx']),
1064
- localSchema: false,
1065
- fix: false,
1066
- });
1067
-
1068
- const reads = spy.mock.calls.map(([file]) => String(file));
1069
- spy.mockRestore();
1070
- expect(reads.some((file) => file.endsWith('other.mdx'))).toBe(false);
1071
- expect(result.diagnostics.filter(({ rule }) => rule === 'duplicate-h1')).toEqual([]);
1072
- });
1073
-
1074
- it('collects no anchor links when no page has a duplicate first heading', async () => {
1075
- write(dir, {
1076
- 'index.mdx': '---\ntitle: Home\n---\n\n# Welcome\n',
1077
- 'other.mdx': '---\ntitle: Other\n---\n\nSee [home](/index#welcome).\n',
1078
- });
1079
- const spy = vi.spyOn(fsSync, 'readFileSync');
1080
-
1081
- const result = await check(dir, { rules: ['duplicate-h1'], files: ['index.mdx'] });
1082
-
1083
- const reads = spy.mock.calls.map(([file]) => String(file));
1084
- spy.mockRestore();
1085
- expect(reads.some((file) => file.endsWith('other.mdx'))).toBe(false);
1086
- expect(result.issues).toEqual([]);
1087
- });
1088
-
1089
- it('reads every file once when duplicate-h1 runs on a selection', async () => {
1090
- write(dir, {
1091
- 'index.mdx': '---\ntitle: Home\n---\n\n# Home\n',
1092
- 'other.mdx': '---\ntitle: Other\n---\n\nSee [home](/index#home).\n',
1093
- });
1094
- const spy = vi.spyOn(fsSync, 'readFileSync');
1095
-
1096
- const result = await check(dir, { rules: ['duplicate-h1'], files: ['index.mdx'] });
1097
-
1098
- const reads = spy.mock.calls.map(([file]) => String(file));
1099
- spy.mockRestore();
1100
- expect(reads.filter((file) => file.endsWith('other.mdx'))).toHaveLength(1);
1101
- expect(reads.filter((file) => file.endsWith('index.mdx'))).toHaveLength(1);
1102
- expect(result.issues).toEqual([
1103
- expect.objectContaining({
1104
- file: 'index.mdx',
1105
- fixable: false,
1106
- message: expect.stringContaining('#home'),
1107
- }),
1108
- ]);
1109
- });
1110
-
1111
- it('skips void tags in block-quoted fences and inline code', async () => {
1112
- write(dir, {
1113
- 'index.mdx': '---\ntitle: Home\n---\n\n> Quote\n>\n> ```html\n> <br>\n> ```\n\nSee `<hr>`.\n',
1114
- });
1115
-
1116
- expect((await check(dir, { rules: ['void-tags'] })).issues).toEqual([]);
1117
- });
1118
-
1119
- it('flags void tags outside code on a page that fails to parse', async () => {
1120
- write(dir, {
1121
- 'index.mdx':
1122
- '---\ntitle: Home\n---\n\n<Card title="a">\n\nLine<br>\n\n```html\n<br>\n```\n\nSee `<hr>`.\n\n</Card>\n',
1123
- });
1124
-
1125
- const { issues } = await check(dir, { rules: ['void-tags'] });
1126
-
1127
- expect(issues.map(({ line, message }) => [line, message])).toEqual([
1128
- [7, '<br> must be self-closed'],
1129
- ]);
1130
- });
1131
-
1132
- it('does not pair backticks across table cells', async () => {
1133
- write(dir, {
1134
- 'index.mdx': '---\ntitle: Home\n---\n\n| A | B |\n| - | - |\n| `open | <br> close` |\n',
1135
- });
1136
-
1137
- const { issues } = await check(dir, { rules: ['void-tags'] });
1138
-
1139
- expect(issues.map(({ line, column }) => [line, column])).toEqual([[7, 11]]);
1140
- });
1141
-
1142
- it('skips inline code inside table cells', async () => {
1143
- write(dir, {
1144
- 'index.mdx': '---\ntitle: Home\n---\n\n| A | B |\n| - | - |\n| `a | b` | `<br>` |\n',
1145
- });
1146
-
1147
- expect((await check(dir, { rules: ['void-tags'] })).issues).toEqual([]);
1148
- });
1149
-
1150
- it('does not treat an indented line inside JSX as code', async () => {
1151
- write(dir, { 'index.mdx': '---\ntitle: Home\n---\n\n<Card>\n line<br>\n</Card>\n' });
1152
-
1153
- const { issues } = await check(dir, { rules: ['void-tags'] });
1154
-
1155
- expect(issues.map(({ line, column }) => [line, column])).toEqual([[6, 9]]);
1156
- });
1157
-
1158
- it('does not open inline code at an escaped backtick', async () => {
1159
- write(dir, { 'index.mdx': '---\ntitle: Home\n---\n\nText \\`a<br>\\` and `b\\`<hr>\n' });
1160
-
1161
- const { issues } = await check(dir, { rules: ['void-tags'] });
1162
-
1163
- expect(issues.map(({ message }) => message)).toEqual([
1164
- '<br> must be self-closed',
1165
- '<hr> must be self-closed',
1166
- ]);
1167
- });
1168
-
1169
- it('flags HTML void elements but not components, custom elements or SVG image', async () => {
1170
- write(dir, {
1171
- 'index.mdx':
1172
- '---\ntitle: Home\n---\n\n<video>\n <source src="a.mp4">\n</video>\n\n<input type="checkbox">\n\n<Source src="b.mp4" />\n\n<source-list>x</source-list>\n\n<svg><image href="/d.png"><title>D</title></image></svg>\n',
1173
- });
1174
-
1175
- const { issues } = await check(dir, { rules: ['void-tags'] });
1176
- expect(issues.map(({ line, message }) => [line, message])).toEqual([
1177
- [6, '<source> must be self-closed'],
1178
- [9, '<input> must be self-closed'],
1179
- ]);
1180
-
1181
- await check(dir, { rules: ['void-tags'], fix: true });
1182
- const content = read('index.mdx');
1183
- expect(content).toContain('<source src="a.mp4" />');
1184
- expect(content).toContain('<input type="checkbox" />');
1185
- });
1186
- });
1187
-
1188
- describe('maskNonMarkup', () => {
1189
- it('masks a regex literal after typeof', () => {
1190
- expect(maskNonMarkup('{typeof /<br>/}')).toBe('{typeof / /}');
1191
- });
1192
-
1193
- it('masks template strings but keeps the code inside ${}', () => {
1194
- expect(maskNonMarkup('{`a\n\n${x + "<hr>"}\n\nb`}')).toBe('{` \n\n${x + " "}\n\n `}');
1195
- });
1196
-
1197
- it('keeps masking after an expression the tokenizer rejects', () => {
1198
- expect(maskNonMarkup('## A {#1-intro}\n\n{"<br>"}')).toBe('## A {#1-intro}\n\n{" "}');
1199
- });
1200
-
1201
- it('blanks an escaped tag and does not open an expression at an escaped brace', () => {
1202
- expect(maskNonMarkup('\\<br> \\{"<br>"} {"<br>"}')).toBe('\\ br> \\{"<br>"} {" "}');
1203
- });
1204
- });
1205
-
1206
- describe('toParseError', () => {
1207
- it('reads the position from the reason of an unclosed JSX tag', () => {
1208
- let error: unknown;
1209
- try {
1210
- getAST('---\ntitle: A\n---\n\n<Note>\nunclosed\n', 'a.mdx');
1211
- } catch (caught) {
1212
- error = caught;
1213
- }
1214
-
1215
- expect(toParseError(error)).toEqual({
1216
- line: 5,
1217
- column: 1,
1218
- message: 'Expected a closing tag for `<Note>`',
1219
- });
1220
- });
1221
- });
1222
-
1223
- describe('applyEdits', () => {
1224
- it('keeps a replacement that an insertion at its start precedes', () => {
1225
- expect(
1226
- applyEdits('a<br>b', [
1227
- { start: 1, end: 5, text: '<br />' },
1228
- { start: 1, end: 1, text: 'x' },
1229
- ])
1230
- ).toBe('ax<br />b');
1231
- });
1232
- });
1233
-
1234
- describe('mapping back through edits', () => {
1235
- const inserted = [
1236
- { content: 'ab', edits: [{ start: 1, end: 1, text: 'XY' }] },
1237
- { content: 'aXYb', edits: [] },
1238
- ];
1239
- const deleted = [
1240
- { content: 'aXYb', edits: [{ start: 1, end: 3, text: '' }] },
1241
- { content: 'ab', edits: [] },
1242
- ];
1243
-
1244
- it('snaps positions at or inside inserted text to the insertion point', () => {
1245
- expect(relocate(inserted, 1, { line: 1, column: 2 })).toEqual({ line: 1, column: 2 });
1246
- expect(relocate(inserted, 1, { line: 1, column: 3 })).toEqual({ line: 1, column: 2 });
1247
- expect(relocate(inserted, 1, { line: 1, column: 5 })).toEqual({ line: 1, column: 3 });
1248
- });
1249
-
1250
- it('places a position at a deletion after the deleted text', () => {
1251
- expect(relocate(deleted, 1, { line: 1, column: 2 })).toEqual({ line: 1, column: 4 });
1252
- });
1253
-
1254
- it('maps an edit at an insertion point to before the inserted text and rejects one inside it', () => {
1255
- expect(unmapEdit(inserted, 1, { start: 1, end: 1, text: 'Z' })).toEqual({
1256
- start: 1,
1257
- end: 1,
1258
- text: 'Z',
1259
- });
1260
- expect(unmapEdit(inserted, 1, { start: 2, end: 2, text: 'Z' })).toBeUndefined();
1261
- expect(unmapEdit(inserted, 1, { start: 3, end: 3, text: 'Z' })).toEqual({
1262
- start: 1,
1263
- end: 1,
1264
- text: 'Z',
1265
- });
1266
- });
1267
-
1268
- it('maps an edit at a deletion point to before the deleted text', () => {
1269
- expect(unmapEdit(deleted, 1, { start: 1, end: 1, text: 'Z' })).toEqual({
1270
- start: 1,
1271
- end: 1,
1272
- text: 'Z',
1273
- });
1274
- });
1275
- });