-
Notifications
You must be signed in to change notification settings - Fork 35
Expand file tree
/
Copy pathauthored-links.test.ts
More file actions
241 lines (211 loc) Β· 6.63 KB
/
Copy pathauthored-links.test.ts
File metadata and controls
241 lines (211 loc) Β· 6.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
import assert from "node:assert/strict";
import { test } from "node:test";
import { normalizeAuthoredLinks } from "../src/_internal/authored-links.ts";
test("normalizes authored Markdown and static JSX links", () => {
const source = `[Root](/)
[Guide](/guide?view=all#top)
[Collision](/docs/guide)
[External](https://example.com)
[Protocol](//cdn.example.com/x)
[Relative](../guide)
[Anchor](#top)
[Guide ref][guide]
[guide]: /guide "Guide"
<a HREF="/native">Native</a>
<Card href='/card' />
<Card href={"/expression"} />
<Card href={("/parenthesized")} />
<Card href={\`/template\`} />
<Card href={"/" + "joined"} />
<Card href={/* fixed */ "/commented"} />
<Card href={true ? "/conditional" : "/other"} />
<Card href={(void 0, "/sequence")} />
<Card href={"\\u002fescaped"} />
<Card href={destination} />
<Card {...{ pattern: /}/ }} href="/after-spread" />
<Card before={{.../}/}} href={"/after-regex-spread"} />
\`[Code](/unchanged)\`
\`\`\`md
[Fence](/unchanged)
\`\`\`
`;
const transformed = normalizeAuthoredLinks(source, {
base: "/docs/",
sourceId: "guide.mdx",
});
assert.match(transformed, /\[Root\]\(\/docs\/\)/);
assert.match(transformed, /\[Guide\]\(\/docs\/guide\?view=all#top\)/);
assert.match(transformed, /\[Collision\]\(\/docs\/docs\/guide\)/);
assert.match(transformed, /\[guide\]: \/docs\/guide "Guide"/);
assert.match(transformed, /HREF="\/docs\/native"/);
assert.match(transformed, /href='\/docs\/card'/);
assert.match(transformed, /href=\{"\/docs\/expression"\}/);
assert.match(transformed, /href=\{\("\/docs\/parenthesized"\)\}/);
assert.match(transformed, /href=\{`\/docs\/template`\}/);
assert.match(transformed, /href=\{"\/docs\/" \+ "joined"\}/);
assert.match(transformed, /href=\{\/\* fixed \*\/ "\/docs\/commented"\}/);
assert.match(
transformed,
/href=\{true \? "\/docs\/conditional" : "\/other"\}/,
);
assert.match(transformed, /href=\{\(void 0, "\/docs\/sequence"\)\}/);
assert.match(transformed, /href=\{"\/docs\\u002fescaped"\}/);
assert.match(transformed, /href=\{destination\}/);
assert.match(transformed, /href="\/docs\/after-spread"/);
assert.match(transformed, /href=\{"\/docs\/after-regex-spread"\}/);
assert.match(transformed, /\[Code\]\(\/unchanged\)/);
assert.match(transformed, /\[Fence\]\(\/unchanged\)/);
});
test("preserves source at the root base", () => {
const source = "[Guide](/guide)";
assert.equal(normalizeAuthoredLinks(source, { base: "/" }), source);
});
test("fails closed at the root base", () => {
assert.throws(
() => normalizeAuthoredLinks("<Card href={", { base: "/" }),
/could not parse source/,
);
});
test("rejects canonical-path escapes", () => {
for (const destination of [
"/../admin",
"/.%2e/admin",
"/%252e%252e/admin",
"/%2525252e%2525252e/admin",
"/safe/%2f..%2fadmin",
]) {
for (const source of [
`[Escape](${destination})`,
`[escape]: ${destination}`,
`<Card href="${destination}" />`,
`<Card href={"${destination}"} />`,
]) {
assert.throws(
() => normalizeAuthoredLinks(source, { base: "/docs" }),
/destination escapes its canonical path/,
);
}
}
assert.throws(
() =>
normalizeAuthoredLinks(`<Card href={"\\u002f..\\u002fadmin"} />`, {
base: "/docs",
}),
/destination escapes its canonical path/,
);
});
test("maps Satteri code-point positions to UTF-16 offsets", () => {
const source = `ππ [Link](/link)\n\nπ <Card href="/card" />`;
assert.equal(
normalizeAuthoredLinks(source, { base: "/ζζ‘£" }),
`ππ [Link](/ζζ‘£/link)\n\nπ <Card href="/ζζ‘£/card" />`,
);
});
test("normalizes links nested in JSX fragments", () => {
assert.equal(
normalizeAuthoredLinks("<>[Guide](/guide)</>", { base: "/docs" }),
"<>[Guide](/docs/guide)</>",
);
});
test("normalizes after multiline JSX expressions without losing source offsets", () => {
const source = `<Card foo={
destination ?? { pattern: /}/, value: \`x\${nested}\` }
} href={
"/guide"
} />
<Card foo={\r\n\t destination\r\n} href={\r\n\t'/crlf'\r\n} />`;
assert.equal(
normalizeAuthoredLinks(source, { base: "/docs" }),
`<Card foo={
destination ?? { pattern: /}/, value: \`x\${nested}\` }
} href={
"/docs/guide"
} />
<Card foo={\r\n\t destination\r\n} href={\r\n\t'/docs/crlf'\r\n} />`,
);
});
test("distinguishes postfix arithmetic from regular expressions", () => {
const source = `<Card value={count++ / 2} href="/increment" />
<Card value={count-- / 2} href="/decrement" />`;
assert.equal(
normalizeAuthoredLinks(source, { base: "/docs" }),
`<Card value={count++ / 2} href="/docs/increment" />
<Card value={count-- / 2} href="/docs/decrement" />`,
);
});
test("fails closed with source location on malformed MDX", () => {
assert.throws(
() =>
normalizeAuthoredLinks("# Before\n\n<Card href={", {
base: "/docs",
sourceId: "broken.mdx",
}),
/broken\.mdx:3:1: could not parse source/,
);
});
test("rejects an invalid deployment base", () => {
for (const base of [
"docs",
"//evil.test",
'/docs" onClick={evil}',
"/../docs",
"/%2e%2e/docs",
"/%2E./docs",
"/docs/.%2e/escape",
"/%252e%252e/docs",
"/%25252fadmin",
]) {
assert.throws(
() => normalizeAuthoredLinks("[Guide](/guide)", { base }),
/base must be an absolute pathname/,
);
}
});
test("normalizes hrefs on JSX elements whose children contain JSX-like tokens", () => {
const source = `<Tabs syncKey="x" href="/guide">
\`\`\`js
{
\`\`\`
</Tabs>`;
assert.equal(
normalizeAuthoredLinks(source, { base: "/docs" }),
`<Tabs syncKey="x" href="/docs/guide">
\`\`\`js
{
\`\`\`
</Tabs>`,
);
});
test("skips JSX elements whose raw slice spans blockquote markers", () => {
const source = `> Example:
> <PackageManagers
> \ttype="create"
> \tpkg="vike@latest"
> />`;
assert.equal(normalizeAuthoredLinks(source, { base: "/docs" }), source);
});
test("skips blockquote-mangled JSX hrefs instead of failing", () => {
const source = `> <Card
> \thref="/card"
> />`;
assert.equal(normalizeAuthoredLinks(source, { base: "/docs" }), source);
});
test("leaves plain Markdown files that are not valid MDX untouched", () => {
const source = `<!-- Auto Generated Below -->
<a name="module_x"></a>
[Guide](/guide)`;
assert.equal(
normalizeAuthoredLinks(source, { base: "/docs", sourceId: "generated.md" }),
source,
);
});
test("still normalizes links in Markdown files that are valid MDX", () => {
const source = `[Guide](/guide)`;
assert.equal(
normalizeAuthoredLinks(source, {
base: "/docs",
sourceId: "generated.md",
}),
`[Guide](/docs/guide)`,
);
});