@larkiny/astro-github-loader 0.11.3 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -55
- package/dist/github.assets.d.ts +70 -0
- package/dist/github.assets.js +253 -0
- package/dist/github.auth.js +13 -9
- package/dist/github.cleanup.d.ts +3 -2
- package/dist/github.cleanup.js +30 -23
- package/dist/github.constants.d.ts +0 -16
- package/dist/github.constants.js +0 -16
- package/dist/github.content.d.ts +5 -131
- package/dist/github.content.js +152 -794
- package/dist/github.dryrun.d.ts +9 -5
- package/dist/github.dryrun.js +46 -25
- package/dist/github.link-transform.d.ts +2 -2
- package/dist/github.link-transform.js +65 -57
- package/dist/github.loader.js +30 -46
- package/dist/github.logger.d.ts +2 -2
- package/dist/github.logger.js +33 -24
- package/dist/github.paths.d.ts +76 -0
- package/dist/github.paths.js +190 -0
- package/dist/github.storage.d.ts +15 -0
- package/dist/github.storage.js +109 -0
- package/dist/github.types.d.ts +34 -4
- package/dist/index.d.ts +8 -6
- package/dist/index.js +3 -6
- package/dist/test-helpers.d.ts +130 -0
- package/dist/test-helpers.js +194 -0
- package/package.json +3 -1
- package/src/github.assets.spec.ts +717 -0
- package/src/github.assets.ts +365 -0
- package/src/github.auth.spec.ts +245 -0
- package/src/github.auth.ts +24 -10
- package/src/github.cleanup.spec.ts +380 -0
- package/src/github.cleanup.ts +91 -47
- package/src/github.constants.ts +0 -17
- package/src/github.content.spec.ts +305 -454
- package/src/github.content.ts +259 -957
- package/src/github.dryrun.spec.ts +586 -0
- package/src/github.dryrun.ts +105 -54
- package/src/github.link-transform.spec.ts +1345 -0
- package/src/github.link-transform.ts +174 -95
- package/src/github.loader.spec.ts +75 -50
- package/src/github.loader.ts +101 -76
- package/src/github.logger.spec.ts +795 -0
- package/src/github.logger.ts +77 -35
- package/src/github.paths.spec.ts +523 -0
- package/src/github.paths.ts +259 -0
- package/src/github.storage.spec.ts +367 -0
- package/src/github.storage.ts +127 -0
- package/src/github.types.ts +48 -9
- package/src/index.ts +43 -6
- package/src/test-helpers.ts +215 -0
|
@@ -0,0 +1,1345 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
globalLinkTransform,
|
|
4
|
+
generateAutoLinkMappings,
|
|
5
|
+
type ImportedFile,
|
|
6
|
+
type LinkHandler,
|
|
7
|
+
} from "./github.link-transform.js";
|
|
8
|
+
import type {
|
|
9
|
+
LinkMapping,
|
|
10
|
+
IncludePattern,
|
|
11
|
+
LinkTransformContext,
|
|
12
|
+
} from "./github.types.js";
|
|
13
|
+
import { createLogger } from "./github.logger.js";
|
|
14
|
+
|
|
15
|
+
describe("globalLinkTransform", () => {
|
|
16
|
+
const logger = createLogger("silent");
|
|
17
|
+
|
|
18
|
+
function createImportedFile(
|
|
19
|
+
sourcePath: string,
|
|
20
|
+
targetPath: string,
|
|
21
|
+
content: string,
|
|
22
|
+
id: string = sourcePath,
|
|
23
|
+
linkContext?: LinkTransformContext,
|
|
24
|
+
): ImportedFile {
|
|
25
|
+
return {
|
|
26
|
+
sourcePath,
|
|
27
|
+
targetPath,
|
|
28
|
+
content,
|
|
29
|
+
id,
|
|
30
|
+
linkContext,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("internal markdown link transformations", () => {
|
|
35
|
+
it("should transform relative markdown links between files", () => {
|
|
36
|
+
const files: ImportedFile[] = [
|
|
37
|
+
createImportedFile(
|
|
38
|
+
"docs/guide.md",
|
|
39
|
+
"src/content/docs/guide.md",
|
|
40
|
+
"[See intro](./intro.md)",
|
|
41
|
+
),
|
|
42
|
+
createImportedFile(
|
|
43
|
+
"docs/intro.md",
|
|
44
|
+
"src/content/docs/intro.md",
|
|
45
|
+
"# Intro",
|
|
46
|
+
),
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const result = globalLinkTransform(files, {
|
|
50
|
+
stripPrefixes: ["src/content/docs"],
|
|
51
|
+
logger,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(result[0].content).toBe("[See intro](/intro/)");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should handle links with ./ prefix", () => {
|
|
58
|
+
const files: ImportedFile[] = [
|
|
59
|
+
createImportedFile(
|
|
60
|
+
"docs/guide.md",
|
|
61
|
+
"src/content/docs/guide.md",
|
|
62
|
+
"[Link](./other.md)",
|
|
63
|
+
),
|
|
64
|
+
createImportedFile(
|
|
65
|
+
"docs/other.md",
|
|
66
|
+
"src/content/docs/other.md",
|
|
67
|
+
"# Other",
|
|
68
|
+
),
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const result = globalLinkTransform(files, {
|
|
72
|
+
stripPrefixes: ["src/content/docs"],
|
|
73
|
+
logger,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(result[0].content).toBe("[Link](/other/)");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should handle links with ../ prefix", () => {
|
|
80
|
+
const files: ImportedFile[] = [
|
|
81
|
+
createImportedFile(
|
|
82
|
+
"docs/api/client.md",
|
|
83
|
+
"src/content/docs/api/client.md",
|
|
84
|
+
"[Guide](../guide.md)",
|
|
85
|
+
),
|
|
86
|
+
createImportedFile(
|
|
87
|
+
"docs/guide.md",
|
|
88
|
+
"src/content/docs/guide.md",
|
|
89
|
+
"# Guide",
|
|
90
|
+
),
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const result = globalLinkTransform(files, {
|
|
94
|
+
stripPrefixes: ["src/content/docs"],
|
|
95
|
+
logger,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(result[0].content).toBe("[Guide](/guide/)");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should handle relative links without ./ prefix", () => {
|
|
102
|
+
const files: ImportedFile[] = [
|
|
103
|
+
createImportedFile(
|
|
104
|
+
"docs/guide.md",
|
|
105
|
+
"src/content/docs/guide.md",
|
|
106
|
+
"[See other](other.md)",
|
|
107
|
+
),
|
|
108
|
+
createImportedFile(
|
|
109
|
+
"docs/other.md",
|
|
110
|
+
"src/content/docs/other.md",
|
|
111
|
+
"# Other",
|
|
112
|
+
),
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const result = globalLinkTransform(files, {
|
|
116
|
+
stripPrefixes: ["src/content/docs"],
|
|
117
|
+
logger,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(result[0].content).toBe("[See other](/other/)");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should preserve anchors in transformed links", () => {
|
|
124
|
+
const files: ImportedFile[] = [
|
|
125
|
+
createImportedFile(
|
|
126
|
+
"docs/guide.md",
|
|
127
|
+
"src/content/docs/guide.md",
|
|
128
|
+
"[Section](./intro.md#setup)",
|
|
129
|
+
),
|
|
130
|
+
createImportedFile(
|
|
131
|
+
"docs/intro.md",
|
|
132
|
+
"src/content/docs/intro.md",
|
|
133
|
+
"# Intro",
|
|
134
|
+
),
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const result = globalLinkTransform(files, {
|
|
138
|
+
stripPrefixes: ["src/content/docs"],
|
|
139
|
+
logger,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(result[0].content).toBe("[Section](/intro/#setup)");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should handle nested directory structures", () => {
|
|
146
|
+
const files: ImportedFile[] = [
|
|
147
|
+
createImportedFile(
|
|
148
|
+
"docs/api/v1/endpoints.md",
|
|
149
|
+
"src/content/docs/api/v1/endpoints.md",
|
|
150
|
+
"[Auth](../auth/index.md)",
|
|
151
|
+
),
|
|
152
|
+
createImportedFile(
|
|
153
|
+
"docs/api/auth/index.md",
|
|
154
|
+
"src/content/docs/api/auth/index.md",
|
|
155
|
+
"# Auth",
|
|
156
|
+
),
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
const result = globalLinkTransform(files, {
|
|
160
|
+
stripPrefixes: ["src/content/docs"],
|
|
161
|
+
logger,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(result[0].content).toBe("[Auth](/api/auth/)");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should handle index.md files correctly", () => {
|
|
168
|
+
const files: ImportedFile[] = [
|
|
169
|
+
createImportedFile(
|
|
170
|
+
"docs/guide.md",
|
|
171
|
+
"src/content/docs/guide.md",
|
|
172
|
+
"[API](./api/index.md)",
|
|
173
|
+
),
|
|
174
|
+
createImportedFile(
|
|
175
|
+
"docs/api/index.md",
|
|
176
|
+
"src/content/docs/api/index.md",
|
|
177
|
+
"# API",
|
|
178
|
+
),
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
const result = globalLinkTransform(files, {
|
|
182
|
+
stripPrefixes: ["src/content/docs"],
|
|
183
|
+
logger,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(result[0].content).toBe("[API](/api/)");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("should strip .md extension from unresolved internal links", () => {
|
|
190
|
+
const files: ImportedFile[] = [
|
|
191
|
+
createImportedFile(
|
|
192
|
+
"docs/guide.md",
|
|
193
|
+
"src/content/docs/guide.md",
|
|
194
|
+
"[External doc](./missing.md)",
|
|
195
|
+
),
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
const result = globalLinkTransform(files, {
|
|
199
|
+
stripPrefixes: ["src/content/docs"],
|
|
200
|
+
logger,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(result[0].content).toBe("[External doc](docs/missing)");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("external link preservation", () => {
|
|
208
|
+
it("should preserve https:// links", () => {
|
|
209
|
+
const files: ImportedFile[] = [
|
|
210
|
+
createImportedFile(
|
|
211
|
+
"docs/guide.md",
|
|
212
|
+
"src/content/docs/guide.md",
|
|
213
|
+
"[GitHub](https://github.com)",
|
|
214
|
+
),
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
const result = globalLinkTransform(files, {
|
|
218
|
+
stripPrefixes: ["src/content/docs"],
|
|
219
|
+
logger,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(result[0].content).toBe("[GitHub](https://github.com)");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("should preserve http:// links", () => {
|
|
226
|
+
const files: ImportedFile[] = [
|
|
227
|
+
createImportedFile(
|
|
228
|
+
"docs/guide.md",
|
|
229
|
+
"src/content/docs/guide.md",
|
|
230
|
+
"[Site](http://example.com)",
|
|
231
|
+
),
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
const result = globalLinkTransform(files, {
|
|
235
|
+
stripPrefixes: ["src/content/docs"],
|
|
236
|
+
logger,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(result[0].content).toBe("[Site](http://example.com)");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("should preserve mailto: links", () => {
|
|
243
|
+
const files: ImportedFile[] = [
|
|
244
|
+
createImportedFile(
|
|
245
|
+
"docs/guide.md",
|
|
246
|
+
"src/content/docs/guide.md",
|
|
247
|
+
"[Email](mailto:test@example.com)",
|
|
248
|
+
),
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
const result = globalLinkTransform(files, {
|
|
252
|
+
stripPrefixes: ["src/content/docs"],
|
|
253
|
+
logger,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(result[0].content).toBe("[Email](mailto:test@example.com)");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("should preserve ftp:// links", () => {
|
|
260
|
+
const files: ImportedFile[] = [
|
|
261
|
+
createImportedFile(
|
|
262
|
+
"docs/guide.md",
|
|
263
|
+
"src/content/docs/guide.md",
|
|
264
|
+
"[FTP](ftp://ftp.example.com)",
|
|
265
|
+
),
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
const result = globalLinkTransform(files, {
|
|
269
|
+
stripPrefixes: ["src/content/docs"],
|
|
270
|
+
logger,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(result[0].content).toBe("[FTP](ftp://ftp.example.com)");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("should preserve data: URLs", () => {
|
|
277
|
+
const files: ImportedFile[] = [
|
|
278
|
+
createImportedFile(
|
|
279
|
+
"docs/guide.md",
|
|
280
|
+
"src/content/docs/guide.md",
|
|
281
|
+
"[Data](data:text/plain;base64,SGVsbG8=)",
|
|
282
|
+
),
|
|
283
|
+
];
|
|
284
|
+
|
|
285
|
+
const result = globalLinkTransform(files, {
|
|
286
|
+
stripPrefixes: ["src/content/docs"],
|
|
287
|
+
logger,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
expect(result[0].content).toBe("[Data](data:text/plain;base64,SGVsbG8=)");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("should preserve anchor-only links", () => {
|
|
294
|
+
const files: ImportedFile[] = [
|
|
295
|
+
createImportedFile(
|
|
296
|
+
"docs/guide.md",
|
|
297
|
+
"src/content/docs/guide.md",
|
|
298
|
+
"[Section](#heading)",
|
|
299
|
+
),
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
const result = globalLinkTransform(files, {
|
|
303
|
+
stripPrefixes: ["src/content/docs"],
|
|
304
|
+
logger,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
expect(result[0].content).toBe("[Section](#heading)");
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe("link mappings with string patterns", () => {
|
|
312
|
+
it("should apply string pattern replacements", () => {
|
|
313
|
+
const files: ImportedFile[] = [
|
|
314
|
+
createImportedFile(
|
|
315
|
+
"docs/guide.md",
|
|
316
|
+
"src/content/docs/guide.md",
|
|
317
|
+
"[API](api/client.md)",
|
|
318
|
+
),
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
const linkMappings: LinkMapping[] = [
|
|
322
|
+
{
|
|
323
|
+
pattern: "api/",
|
|
324
|
+
replacement: "reference/api/",
|
|
325
|
+
},
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
const result = globalLinkTransform(files, {
|
|
329
|
+
stripPrefixes: ["src/content/docs"],
|
|
330
|
+
linkMappings,
|
|
331
|
+
logger,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Link is normalized to docs/api/client.md, mapping is applied, then .md is stripped
|
|
335
|
+
expect(result[0].content).toBe("[API](docs/reference/api/client.md)");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("should apply multiple string pattern replacements in sequence", () => {
|
|
339
|
+
const files: ImportedFile[] = [
|
|
340
|
+
createImportedFile(
|
|
341
|
+
"docs/guide.md",
|
|
342
|
+
"src/content/docs/guide.md",
|
|
343
|
+
"[API](docs/api/client.md)",
|
|
344
|
+
),
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
const linkMappings: LinkMapping[] = [
|
|
348
|
+
{
|
|
349
|
+
pattern: "docs/",
|
|
350
|
+
replacement: "content/docs/",
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
pattern: "api/",
|
|
354
|
+
replacement: "reference/api/",
|
|
355
|
+
},
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
const result = globalLinkTransform(files, {
|
|
359
|
+
stripPrefixes: ["src/content/docs"],
|
|
360
|
+
linkMappings,
|
|
361
|
+
logger,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Both mappings are applied in sequence, then .md is stripped at the end
|
|
365
|
+
expect(result[0].content).toBe(
|
|
366
|
+
"[API](content/docs/docs/reference/api/client.md)",
|
|
367
|
+
);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("should handle string pattern with function replacement", () => {
|
|
371
|
+
const files: ImportedFile[] = [
|
|
372
|
+
createImportedFile(
|
|
373
|
+
"docs/guide.md",
|
|
374
|
+
"src/content/docs/guide.md",
|
|
375
|
+
"[API](api/client.md)",
|
|
376
|
+
),
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
const linkMappings: LinkMapping[] = [
|
|
380
|
+
{
|
|
381
|
+
pattern: "api/",
|
|
382
|
+
replacement: (match: string) => match.replace("api/", "reference/"),
|
|
383
|
+
},
|
|
384
|
+
];
|
|
385
|
+
|
|
386
|
+
const result = globalLinkTransform(files, {
|
|
387
|
+
stripPrefixes: ["src/content/docs"],
|
|
388
|
+
linkMappings,
|
|
389
|
+
logger,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Normalized to docs/api/client.md, mapping replaces api/ with reference/, .md remains
|
|
393
|
+
expect(result[0].content).toBe("[API](docs/reference/client.md)");
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
describe("link mappings with regex patterns", () => {
|
|
398
|
+
it("should apply regex pattern replacements", () => {
|
|
399
|
+
const files: ImportedFile[] = [
|
|
400
|
+
createImportedFile(
|
|
401
|
+
"docs/guide.md",
|
|
402
|
+
"src/content/docs/guide.md",
|
|
403
|
+
"[API](api/v1/client.md)",
|
|
404
|
+
),
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
const linkMappings: LinkMapping[] = [
|
|
408
|
+
{
|
|
409
|
+
pattern: /^docs\/api\/v\d+\//,
|
|
410
|
+
replacement: "docs/reference/api/",
|
|
411
|
+
},
|
|
412
|
+
];
|
|
413
|
+
|
|
414
|
+
const result = globalLinkTransform(files, {
|
|
415
|
+
stripPrefixes: ["src/content/docs"],
|
|
416
|
+
linkMappings,
|
|
417
|
+
logger,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Normalized to docs/api/v1/client.md, pattern matches, replacement applied
|
|
421
|
+
expect(result[0].content).toBe("[API](docs/reference/api/client.md)");
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("should handle regex pattern with capture groups", () => {
|
|
425
|
+
const files: ImportedFile[] = [
|
|
426
|
+
createImportedFile(
|
|
427
|
+
"docs/guide.md",
|
|
428
|
+
"src/content/docs/guide.md",
|
|
429
|
+
"[API](api/v2/client.md)",
|
|
430
|
+
),
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
const linkMappings: LinkMapping[] = [
|
|
434
|
+
{
|
|
435
|
+
pattern: /^docs\/api\/(v\d+)\//,
|
|
436
|
+
replacement: "docs/reference/$1/",
|
|
437
|
+
},
|
|
438
|
+
];
|
|
439
|
+
|
|
440
|
+
const result = globalLinkTransform(files, {
|
|
441
|
+
stripPrefixes: ["src/content/docs"],
|
|
442
|
+
linkMappings,
|
|
443
|
+
logger,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Normalized to docs/api/v2/client.md, capture group preserved in replacement
|
|
447
|
+
expect(result[0].content).toBe("[API](docs/reference/v2/client.md)");
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("should handle regex pattern with function replacement", () => {
|
|
451
|
+
const files: ImportedFile[] = [
|
|
452
|
+
createImportedFile(
|
|
453
|
+
"docs/guide.md",
|
|
454
|
+
"src/content/docs/guide.md",
|
|
455
|
+
"[API](api/client.md)",
|
|
456
|
+
),
|
|
457
|
+
];
|
|
458
|
+
|
|
459
|
+
const linkMappings: LinkMapping[] = [
|
|
460
|
+
{
|
|
461
|
+
pattern: /^docs\/api\/(.+)$/,
|
|
462
|
+
replacement: (match: string) => match.toUpperCase(),
|
|
463
|
+
},
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
const result = globalLinkTransform(files, {
|
|
467
|
+
stripPrefixes: ["src/content/docs"],
|
|
468
|
+
linkMappings,
|
|
469
|
+
logger,
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Normalized to docs/api/client.md, function transforms entire path to uppercase
|
|
473
|
+
expect(result[0].content).toBe("[API](DOCS/API/CLIENT.MD)");
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
describe("contextFilter on link mappings", () => {
|
|
478
|
+
it("should apply mapping when contextFilter returns true", () => {
|
|
479
|
+
const linkContext: LinkTransformContext = {
|
|
480
|
+
sourcePath: "docs/api/guide.md",
|
|
481
|
+
targetPath: "src/content/docs/api/guide.md",
|
|
482
|
+
basePath: "src/content/docs/api",
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const files: ImportedFile[] = [
|
|
486
|
+
createImportedFile(
|
|
487
|
+
"docs/api/guide.md",
|
|
488
|
+
"src/content/docs/api/guide.md",
|
|
489
|
+
"[Ref](../ref/client.md)",
|
|
490
|
+
"docs/api/guide",
|
|
491
|
+
linkContext,
|
|
492
|
+
),
|
|
493
|
+
];
|
|
494
|
+
|
|
495
|
+
const linkMappings: LinkMapping[] = [
|
|
496
|
+
{
|
|
497
|
+
pattern: "docs/ref/",
|
|
498
|
+
replacement: "docs/reference/",
|
|
499
|
+
contextFilter: (ctx) => ctx.basePath.includes("api"),
|
|
500
|
+
},
|
|
501
|
+
];
|
|
502
|
+
|
|
503
|
+
const result = globalLinkTransform(files, {
|
|
504
|
+
stripPrefixes: ["src/content/docs"],
|
|
505
|
+
linkMappings,
|
|
506
|
+
logger,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Normalized to docs/ref/client.md, mapping applied because contextFilter returns true
|
|
510
|
+
expect(result[0].content).toBe("[Ref](docs/reference/client.md)");
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("should skip mapping when contextFilter returns false", () => {
|
|
514
|
+
const linkContext: LinkTransformContext = {
|
|
515
|
+
sourcePath: "docs/guides/tutorial.md",
|
|
516
|
+
targetPath: "src/content/docs/guides/tutorial.md",
|
|
517
|
+
basePath: "src/content/docs/guides",
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const files: ImportedFile[] = [
|
|
521
|
+
createImportedFile(
|
|
522
|
+
"docs/guides/tutorial.md",
|
|
523
|
+
"src/content/docs/guides/tutorial.md",
|
|
524
|
+
"[Ref](../ref/client.md)",
|
|
525
|
+
"docs/guides/tutorial",
|
|
526
|
+
linkContext,
|
|
527
|
+
),
|
|
528
|
+
];
|
|
529
|
+
|
|
530
|
+
const linkMappings: LinkMapping[] = [
|
|
531
|
+
{
|
|
532
|
+
pattern: "ref/",
|
|
533
|
+
replacement: "reference/",
|
|
534
|
+
contextFilter: (ctx) => ctx.basePath.includes("api"),
|
|
535
|
+
},
|
|
536
|
+
];
|
|
537
|
+
|
|
538
|
+
const result = globalLinkTransform(files, {
|
|
539
|
+
stripPrefixes: ["src/content/docs"],
|
|
540
|
+
linkMappings,
|
|
541
|
+
logger,
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Mapping should not be applied since contextFilter returns false
|
|
545
|
+
expect(result[0].content).toBe("[Ref](docs/ref/client)");
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it("should apply mapping when no contextFilter is specified", () => {
|
|
549
|
+
const linkContext: LinkTransformContext = {
|
|
550
|
+
sourcePath: "docs/guide.md",
|
|
551
|
+
targetPath: "src/content/docs/guide.md",
|
|
552
|
+
basePath: "src/content/docs",
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const files: ImportedFile[] = [
|
|
556
|
+
createImportedFile(
|
|
557
|
+
"docs/guide.md",
|
|
558
|
+
"src/content/docs/guide.md",
|
|
559
|
+
"[API](api/client.md)",
|
|
560
|
+
"docs/guide",
|
|
561
|
+
linkContext,
|
|
562
|
+
),
|
|
563
|
+
];
|
|
564
|
+
|
|
565
|
+
const linkMappings: LinkMapping[] = [
|
|
566
|
+
{
|
|
567
|
+
pattern: "docs/api/",
|
|
568
|
+
replacement: "docs/reference/",
|
|
569
|
+
},
|
|
570
|
+
];
|
|
571
|
+
|
|
572
|
+
const result = globalLinkTransform(files, {
|
|
573
|
+
stripPrefixes: ["src/content/docs"],
|
|
574
|
+
linkMappings,
|
|
575
|
+
logger,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// Normalized to docs/api/client.md, mapping applied
|
|
579
|
+
expect(result[0].content).toBe("[API](docs/reference/client.md)");
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
describe("stripPrefixes configuration", () => {
|
|
584
|
+
it("should strip single prefix from URLs", () => {
|
|
585
|
+
const files: ImportedFile[] = [
|
|
586
|
+
createImportedFile(
|
|
587
|
+
"docs/guide.md",
|
|
588
|
+
"src/content/docs/guide.md",
|
|
589
|
+
"[Other](./other.md)",
|
|
590
|
+
),
|
|
591
|
+
createImportedFile(
|
|
592
|
+
"docs/other.md",
|
|
593
|
+
"src/content/docs/other.md",
|
|
594
|
+
"# Other",
|
|
595
|
+
),
|
|
596
|
+
];
|
|
597
|
+
|
|
598
|
+
const result = globalLinkTransform(files, {
|
|
599
|
+
stripPrefixes: ["src/content/docs"],
|
|
600
|
+
logger,
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
expect(result[0].content).toBe("[Other](/other/)");
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it("should strip multiple prefixes, using first match", () => {
|
|
607
|
+
const files: ImportedFile[] = [
|
|
608
|
+
createImportedFile(
|
|
609
|
+
"docs/guide.md",
|
|
610
|
+
"src/content/docs/main/guide.md",
|
|
611
|
+
"[Other](./other.md)",
|
|
612
|
+
),
|
|
613
|
+
createImportedFile(
|
|
614
|
+
"docs/other.md",
|
|
615
|
+
"src/content/docs/main/other.md",
|
|
616
|
+
"# Other",
|
|
617
|
+
),
|
|
618
|
+
];
|
|
619
|
+
|
|
620
|
+
const result = globalLinkTransform(files, {
|
|
621
|
+
stripPrefixes: ["src/content/docs/main", "src/content/docs"],
|
|
622
|
+
logger,
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
expect(result[0].content).toBe("[Other](/other/)");
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("should handle empty stripPrefixes array", () => {
|
|
629
|
+
const files: ImportedFile[] = [
|
|
630
|
+
createImportedFile(
|
|
631
|
+
"docs/guide.md",
|
|
632
|
+
"docs/guide.md",
|
|
633
|
+
"[Other](./other.md)",
|
|
634
|
+
),
|
|
635
|
+
createImportedFile("docs/other.md", "docs/other.md", "# Other"),
|
|
636
|
+
];
|
|
637
|
+
|
|
638
|
+
const result = globalLinkTransform(files, {
|
|
639
|
+
stripPrefixes: [],
|
|
640
|
+
logger,
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
expect(result[0].content).toBe("[Other](/docs/other/)");
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
describe("custom handlers", () => {
|
|
648
|
+
it("should apply custom handler when test returns true", () => {
|
|
649
|
+
const files: ImportedFile[] = [
|
|
650
|
+
createImportedFile(
|
|
651
|
+
"docs/guide.md",
|
|
652
|
+
"src/content/docs/guide.md",
|
|
653
|
+
"[Custom](unresolved.md)",
|
|
654
|
+
),
|
|
655
|
+
];
|
|
656
|
+
|
|
657
|
+
const customHandlers: LinkHandler[] = [
|
|
658
|
+
{
|
|
659
|
+
test: (link) => link.includes("unresolved"),
|
|
660
|
+
transform: (link) =>
|
|
661
|
+
link.replace("docs/unresolved", "/special/handled"),
|
|
662
|
+
},
|
|
663
|
+
];
|
|
664
|
+
|
|
665
|
+
const result = globalLinkTransform(files, {
|
|
666
|
+
stripPrefixes: ["src/content/docs"],
|
|
667
|
+
customHandlers,
|
|
668
|
+
logger,
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// Custom handlers receive the normalized path (docs/unresolved.md)
|
|
672
|
+
expect(result[0].content).toBe("[Custom](/special/handled.md)");
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it("should not apply custom handler when test returns false", () => {
|
|
676
|
+
const files: ImportedFile[] = [
|
|
677
|
+
createImportedFile(
|
|
678
|
+
"docs/guide.md",
|
|
679
|
+
"src/content/docs/guide.md",
|
|
680
|
+
"[Normal](./other.md)",
|
|
681
|
+
),
|
|
682
|
+
createImportedFile(
|
|
683
|
+
"docs/other.md",
|
|
684
|
+
"src/content/docs/other.md",
|
|
685
|
+
"# Other",
|
|
686
|
+
),
|
|
687
|
+
];
|
|
688
|
+
|
|
689
|
+
const customHandlers: LinkHandler[] = [
|
|
690
|
+
{
|
|
691
|
+
test: (link) => link.startsWith("custom://"),
|
|
692
|
+
transform: (link) => link.replace("custom://", "/special/"),
|
|
693
|
+
},
|
|
694
|
+
];
|
|
695
|
+
|
|
696
|
+
const result = globalLinkTransform(files, {
|
|
697
|
+
stripPrefixes: ["src/content/docs"],
|
|
698
|
+
customHandlers,
|
|
699
|
+
logger,
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
expect(result[0].content).toBe("[Normal](/other/)");
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it("should apply first matching custom handler", () => {
|
|
706
|
+
const files: ImportedFile[] = [
|
|
707
|
+
createImportedFile(
|
|
708
|
+
"docs/guide.md",
|
|
709
|
+
"src/content/docs/guide.md",
|
|
710
|
+
"[Link](special.md)",
|
|
711
|
+
),
|
|
712
|
+
];
|
|
713
|
+
|
|
714
|
+
const customHandlers: LinkHandler[] = [
|
|
715
|
+
{
|
|
716
|
+
test: (link) => link.includes("special"),
|
|
717
|
+
transform: (link) => link.replace("special", "first"),
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
test: (link) => link.includes("special"),
|
|
721
|
+
transform: (link) => link.replace("special", "second"),
|
|
722
|
+
},
|
|
723
|
+
];
|
|
724
|
+
|
|
725
|
+
const result = globalLinkTransform(files, {
|
|
726
|
+
stripPrefixes: ["src/content/docs"],
|
|
727
|
+
customHandlers,
|
|
728
|
+
logger,
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// First matching handler is applied
|
|
732
|
+
expect(result[0].content).toBe("[Link](docs/first.md)");
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
describe("complex scenarios", () => {
|
|
737
|
+
it("should handle multiple links in the same file", () => {
|
|
738
|
+
const files: ImportedFile[] = [
|
|
739
|
+
createImportedFile(
|
|
740
|
+
"docs/guide.md",
|
|
741
|
+
"src/content/docs/guide.md",
|
|
742
|
+
"[Intro](./intro.md) and [API](./api.md) and [External](https://example.com)",
|
|
743
|
+
),
|
|
744
|
+
createImportedFile(
|
|
745
|
+
"docs/intro.md",
|
|
746
|
+
"src/content/docs/intro.md",
|
|
747
|
+
"# Intro",
|
|
748
|
+
),
|
|
749
|
+
createImportedFile("docs/api.md", "src/content/docs/api.md", "# API"),
|
|
750
|
+
];
|
|
751
|
+
|
|
752
|
+
const result = globalLinkTransform(files, {
|
|
753
|
+
stripPrefixes: ["src/content/docs"],
|
|
754
|
+
logger,
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
expect(result[0].content).toBe(
|
|
758
|
+
"[Intro](/intro/) and [API](/api/) and [External](https://example.com)",
|
|
759
|
+
);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it("should handle links with complex anchors", () => {
|
|
763
|
+
const files: ImportedFile[] = [
|
|
764
|
+
createImportedFile(
|
|
765
|
+
"docs/guide.md",
|
|
766
|
+
"src/content/docs/guide.md",
|
|
767
|
+
"[Section](./intro.md#complex-section-name-123)",
|
|
768
|
+
),
|
|
769
|
+
createImportedFile(
|
|
770
|
+
"docs/intro.md",
|
|
771
|
+
"src/content/docs/intro.md",
|
|
772
|
+
"# Intro",
|
|
773
|
+
),
|
|
774
|
+
];
|
|
775
|
+
|
|
776
|
+
const result = globalLinkTransform(files, {
|
|
777
|
+
stripPrefixes: ["src/content/docs"],
|
|
778
|
+
logger,
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
expect(result[0].content).toBe(
|
|
782
|
+
"[Section](/intro/#complex-section-name-123)",
|
|
783
|
+
);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
it("should transform all files in the array", () => {
|
|
787
|
+
const files: ImportedFile[] = [
|
|
788
|
+
createImportedFile(
|
|
789
|
+
"docs/guide.md",
|
|
790
|
+
"src/content/docs/guide.md",
|
|
791
|
+
"[Intro](./intro.md)",
|
|
792
|
+
),
|
|
793
|
+
createImportedFile(
|
|
794
|
+
"docs/intro.md",
|
|
795
|
+
"src/content/docs/intro.md",
|
|
796
|
+
"[Guide](./guide.md)",
|
|
797
|
+
),
|
|
798
|
+
];
|
|
799
|
+
|
|
800
|
+
const result = globalLinkTransform(files, {
|
|
801
|
+
stripPrefixes: ["src/content/docs"],
|
|
802
|
+
logger,
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
expect(result[0].content).toBe("[Intro](/intro/)");
|
|
806
|
+
expect(result[1].content).toBe("[Guide](/guide/)");
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it("should preserve original file properties except content", () => {
|
|
810
|
+
const linkContext: LinkTransformContext = {
|
|
811
|
+
sourcePath: "docs/guide.md",
|
|
812
|
+
targetPath: "src/content/docs/guide.md",
|
|
813
|
+
basePath: "src/content/docs",
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
const files: ImportedFile[] = [
|
|
817
|
+
createImportedFile(
|
|
818
|
+
"docs/guide.md",
|
|
819
|
+
"src/content/docs/guide.md",
|
|
820
|
+
"[Intro](./intro.md)",
|
|
821
|
+
"custom-id",
|
|
822
|
+
linkContext,
|
|
823
|
+
),
|
|
824
|
+
];
|
|
825
|
+
|
|
826
|
+
const result = globalLinkTransform(files, {
|
|
827
|
+
stripPrefixes: ["src/content/docs"],
|
|
828
|
+
logger,
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
expect(result[0].sourcePath).toBe("docs/guide.md");
|
|
832
|
+
expect(result[0].targetPath).toBe("src/content/docs/guide.md");
|
|
833
|
+
expect(result[0].id).toBe("custom-id");
|
|
834
|
+
expect(result[0].linkContext).toEqual(linkContext);
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
describe("edge cases", () => {
|
|
839
|
+
it("should handle empty file content", () => {
|
|
840
|
+
const files: ImportedFile[] = [
|
|
841
|
+
createImportedFile("docs/empty.md", "src/content/docs/empty.md", ""),
|
|
842
|
+
];
|
|
843
|
+
|
|
844
|
+
const result = globalLinkTransform(files, {
|
|
845
|
+
stripPrefixes: ["src/content/docs"],
|
|
846
|
+
logger,
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
expect(result[0].content).toBe("");
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it("should handle content with no links", () => {
|
|
853
|
+
const files: ImportedFile[] = [
|
|
854
|
+
createImportedFile(
|
|
855
|
+
"docs/guide.md",
|
|
856
|
+
"src/content/docs/guide.md",
|
|
857
|
+
"# Guide\n\nThis is just text with no links.",
|
|
858
|
+
),
|
|
859
|
+
];
|
|
860
|
+
|
|
861
|
+
const result = globalLinkTransform(files, {
|
|
862
|
+
stripPrefixes: ["src/content/docs"],
|
|
863
|
+
logger,
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
expect(result[0].content).toBe(
|
|
867
|
+
"# Guide\n\nThis is just text with no links.",
|
|
868
|
+
);
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
it("should handle malformed markdown links", () => {
|
|
872
|
+
const files: ImportedFile[] = [
|
|
873
|
+
createImportedFile(
|
|
874
|
+
"docs/guide.md",
|
|
875
|
+
"src/content/docs/guide.md",
|
|
876
|
+
"[Incomplete link]()",
|
|
877
|
+
),
|
|
878
|
+
];
|
|
879
|
+
|
|
880
|
+
const result = globalLinkTransform(files, {
|
|
881
|
+
stripPrefixes: ["src/content/docs"],
|
|
882
|
+
logger,
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
expect(result[0].content).toBe("[Incomplete link]()");
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
it("should handle empty array of files", () => {
|
|
889
|
+
const files: ImportedFile[] = [];
|
|
890
|
+
|
|
891
|
+
const result = globalLinkTransform(files, {
|
|
892
|
+
stripPrefixes: ["src/content/docs"],
|
|
893
|
+
logger,
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
expect(result).toEqual([]);
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
it("should handle link to directory ending with /", () => {
|
|
900
|
+
const files: ImportedFile[] = [
|
|
901
|
+
createImportedFile(
|
|
902
|
+
"docs/guide.md",
|
|
903
|
+
"src/content/docs/guide.md",
|
|
904
|
+
"[API](./api/)",
|
|
905
|
+
),
|
|
906
|
+
createImportedFile(
|
|
907
|
+
"docs/api/index.md",
|
|
908
|
+
"src/content/docs/api/index.md",
|
|
909
|
+
"# API",
|
|
910
|
+
),
|
|
911
|
+
];
|
|
912
|
+
|
|
913
|
+
const result = globalLinkTransform(files, {
|
|
914
|
+
stripPrefixes: ["src/content/docs"],
|
|
915
|
+
logger,
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
expect(result[0].content).toBe("[API](/api/)");
|
|
919
|
+
});
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
describe("generateAutoLinkMappings", () => {
|
|
924
|
+
describe("basic functionality", () => {
|
|
925
|
+
it("should return empty array when no pathMappings configured", () => {
|
|
926
|
+
const includes: IncludePattern[] = [
|
|
927
|
+
{
|
|
928
|
+
pattern: "docs/**/*.md",
|
|
929
|
+
basePath: "src/content/docs",
|
|
930
|
+
},
|
|
931
|
+
];
|
|
932
|
+
|
|
933
|
+
const result = generateAutoLinkMappings(includes);
|
|
934
|
+
|
|
935
|
+
expect(result).toEqual([]);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
it("should return empty array when includes is empty", () => {
|
|
939
|
+
const includes: IncludePattern[] = [];
|
|
940
|
+
|
|
941
|
+
const result = generateAutoLinkMappings(includes);
|
|
942
|
+
|
|
943
|
+
expect(result).toEqual([]);
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
it("should generate link mapping for file mapping", () => {
|
|
947
|
+
const includes: IncludePattern[] = [
|
|
948
|
+
{
|
|
949
|
+
pattern: "docs/**/*.md",
|
|
950
|
+
basePath: "src/content/docs/api",
|
|
951
|
+
pathMappings: {
|
|
952
|
+
"docs/README.md": "overview.md",
|
|
953
|
+
},
|
|
954
|
+
},
|
|
955
|
+
];
|
|
956
|
+
|
|
957
|
+
const result = generateAutoLinkMappings(
|
|
958
|
+
includes,
|
|
959
|
+
["src/content/docs"],
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
expect(result).toHaveLength(1);
|
|
963
|
+
expect(result[0].pattern).toBeInstanceOf(RegExp);
|
|
964
|
+
expect(result[0].global).toBe(true);
|
|
965
|
+
|
|
966
|
+
// Test that the pattern matches the exact file
|
|
967
|
+
expect("docs/README.md").toMatch(result[0].pattern as RegExp);
|
|
968
|
+
expect("docs/README.mdx").not.toMatch(result[0].pattern as RegExp);
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
it("should generate link mapping for folder mapping with trailing slash", () => {
|
|
972
|
+
const includes: IncludePattern[] = [
|
|
973
|
+
{
|
|
974
|
+
pattern: "docs/**/*.md",
|
|
975
|
+
basePath: "src/content/docs/api",
|
|
976
|
+
pathMappings: {
|
|
977
|
+
"docs/capabilities/": "features/",
|
|
978
|
+
},
|
|
979
|
+
},
|
|
980
|
+
];
|
|
981
|
+
|
|
982
|
+
const result = generateAutoLinkMappings(
|
|
983
|
+
includes,
|
|
984
|
+
["src/content/docs"],
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
expect(result).toHaveLength(1);
|
|
988
|
+
expect(result[0].pattern).toBeInstanceOf(RegExp);
|
|
989
|
+
expect(result[0].global).toBe(true);
|
|
990
|
+
|
|
991
|
+
// Test that the pattern matches files in the folder
|
|
992
|
+
expect("docs/capabilities/feature1.md").toMatch(
|
|
993
|
+
result[0].pattern as RegExp,
|
|
994
|
+
);
|
|
995
|
+
expect("docs/capabilities/nested/feature2.md").toMatch(
|
|
996
|
+
result[0].pattern as RegExp,
|
|
997
|
+
);
|
|
998
|
+
expect("docs/other/file.md").not.toMatch(result[0].pattern as RegExp);
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
it("should generate multiple link mappings for multiple pathMappings", () => {
|
|
1002
|
+
const includes: IncludePattern[] = [
|
|
1003
|
+
{
|
|
1004
|
+
pattern: "docs/**/*.md",
|
|
1005
|
+
basePath: "src/content/docs/api",
|
|
1006
|
+
pathMappings: {
|
|
1007
|
+
"docs/README.md": "overview.md",
|
|
1008
|
+
"docs/capabilities/": "features/",
|
|
1009
|
+
"docs/api.md": "index.md",
|
|
1010
|
+
},
|
|
1011
|
+
},
|
|
1012
|
+
];
|
|
1013
|
+
|
|
1014
|
+
const result = generateAutoLinkMappings(
|
|
1015
|
+
includes,
|
|
1016
|
+
["src/content/docs"],
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
expect(result).toHaveLength(3);
|
|
1020
|
+
result.forEach((mapping) => {
|
|
1021
|
+
expect(mapping.global).toBe(true);
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
describe("enhanced path mapping with crossSectionPath", () => {
|
|
1027
|
+
it("should use explicit crossSectionPath when provided", () => {
|
|
1028
|
+
const includes: IncludePattern[] = [
|
|
1029
|
+
{
|
|
1030
|
+
pattern: "docs/api/**/*.md",
|
|
1031
|
+
basePath: "src/content/docs/reference/api",
|
|
1032
|
+
pathMappings: {
|
|
1033
|
+
"docs/api/": {
|
|
1034
|
+
target: "",
|
|
1035
|
+
crossSectionPath: "/custom/path/api",
|
|
1036
|
+
},
|
|
1037
|
+
},
|
|
1038
|
+
},
|
|
1039
|
+
];
|
|
1040
|
+
|
|
1041
|
+
const result = generateAutoLinkMappings(
|
|
1042
|
+
includes,
|
|
1043
|
+
["src/content/docs"],
|
|
1044
|
+
);
|
|
1045
|
+
|
|
1046
|
+
expect(result).toHaveLength(1);
|
|
1047
|
+
expect(result[0].global).toBe(true);
|
|
1048
|
+
|
|
1049
|
+
// Test the replacement function
|
|
1050
|
+
if (typeof result[0].replacement === "function") {
|
|
1051
|
+
const transformedPath = result[0].replacement(
|
|
1052
|
+
"docs/api/client.md",
|
|
1053
|
+
"",
|
|
1054
|
+
{} as LinkTransformContext,
|
|
1055
|
+
);
|
|
1056
|
+
expect(transformedPath).toContain("/custom/path/api/");
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
it("should infer crossSectionPath from basePath when not provided", () => {
|
|
1061
|
+
const includes: IncludePattern[] = [
|
|
1062
|
+
{
|
|
1063
|
+
pattern: "docs/api/**/*.md",
|
|
1064
|
+
basePath: "src/content/docs/reference/api",
|
|
1065
|
+
pathMappings: {
|
|
1066
|
+
"docs/api/": "",
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
];
|
|
1070
|
+
|
|
1071
|
+
const result = generateAutoLinkMappings(
|
|
1072
|
+
includes,
|
|
1073
|
+
["src/content/docs"],
|
|
1074
|
+
);
|
|
1075
|
+
|
|
1076
|
+
expect(result).toHaveLength(1);
|
|
1077
|
+
|
|
1078
|
+
// Test the replacement function - should infer /reference/api from basePath
|
|
1079
|
+
if (typeof result[0].replacement === "function") {
|
|
1080
|
+
const transformedPath = result[0].replacement(
|
|
1081
|
+
"docs/api/client.md",
|
|
1082
|
+
"",
|
|
1083
|
+
{} as LinkTransformContext,
|
|
1084
|
+
);
|
|
1085
|
+
expect(transformedPath).toContain("/reference/api/");
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
it("should handle enhanced object format with empty target", () => {
|
|
1090
|
+
const includes: IncludePattern[] = [
|
|
1091
|
+
{
|
|
1092
|
+
pattern: "docs/api/**/*.md",
|
|
1093
|
+
basePath: "src/content/docs/reference/api",
|
|
1094
|
+
pathMappings: {
|
|
1095
|
+
"docs/api/": {
|
|
1096
|
+
target: "",
|
|
1097
|
+
crossSectionPath: "/reference/api",
|
|
1098
|
+
},
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
];
|
|
1102
|
+
|
|
1103
|
+
const result = generateAutoLinkMappings(
|
|
1104
|
+
includes,
|
|
1105
|
+
["src/content/docs"],
|
|
1106
|
+
);
|
|
1107
|
+
|
|
1108
|
+
expect(result).toHaveLength(1);
|
|
1109
|
+
|
|
1110
|
+
if (typeof result[0].replacement === "function") {
|
|
1111
|
+
const transformedPath = result[0].replacement(
|
|
1112
|
+
"docs/api/endpoints.md",
|
|
1113
|
+
"",
|
|
1114
|
+
{} as LinkTransformContext,
|
|
1115
|
+
);
|
|
1116
|
+
// With empty target, should just prepend crossSectionPath to relative path
|
|
1117
|
+
expect(transformedPath).toBe("/reference/api/endpoints/");
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
it("should handle enhanced object format with non-empty target", () => {
|
|
1122
|
+
const includes: IncludePattern[] = [
|
|
1123
|
+
{
|
|
1124
|
+
pattern: "docs/api/**/*.md",
|
|
1125
|
+
basePath: "src/content/docs/reference",
|
|
1126
|
+
pathMappings: {
|
|
1127
|
+
"docs/api/": {
|
|
1128
|
+
target: "api/v2/",
|
|
1129
|
+
crossSectionPath: "/reference",
|
|
1130
|
+
},
|
|
1131
|
+
},
|
|
1132
|
+
},
|
|
1133
|
+
];
|
|
1134
|
+
|
|
1135
|
+
const result = generateAutoLinkMappings(
|
|
1136
|
+
includes,
|
|
1137
|
+
["src/content/docs"],
|
|
1138
|
+
);
|
|
1139
|
+
|
|
1140
|
+
expect(result).toHaveLength(1);
|
|
1141
|
+
|
|
1142
|
+
if (typeof result[0].replacement === "function") {
|
|
1143
|
+
const transformedPath = result[0].replacement(
|
|
1144
|
+
"docs/api/client.md",
|
|
1145
|
+
"",
|
|
1146
|
+
{} as LinkTransformContext,
|
|
1147
|
+
);
|
|
1148
|
+
expect(transformedPath).toBe("/reference/api/v2/client/");
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
it("should handle file mapping with enhanced object format", () => {
|
|
1153
|
+
const includes: IncludePattern[] = [
|
|
1154
|
+
{
|
|
1155
|
+
pattern: "docs/**/*.md",
|
|
1156
|
+
basePath: "src/content/docs/api",
|
|
1157
|
+
pathMappings: {
|
|
1158
|
+
"docs/README.md": {
|
|
1159
|
+
target: "overview.md",
|
|
1160
|
+
crossSectionPath: "/api",
|
|
1161
|
+
},
|
|
1162
|
+
},
|
|
1163
|
+
},
|
|
1164
|
+
];
|
|
1165
|
+
|
|
1166
|
+
const result = generateAutoLinkMappings(
|
|
1167
|
+
includes,
|
|
1168
|
+
["src/content/docs"],
|
|
1169
|
+
);
|
|
1170
|
+
|
|
1171
|
+
expect(result).toHaveLength(1);
|
|
1172
|
+
expect(result[0].global).toBe(true);
|
|
1173
|
+
|
|
1174
|
+
if (typeof result[0].replacement === "function") {
|
|
1175
|
+
const transformedPath = result[0].replacement(
|
|
1176
|
+
"docs/README.md",
|
|
1177
|
+
"",
|
|
1178
|
+
{} as LinkTransformContext,
|
|
1179
|
+
);
|
|
1180
|
+
expect(transformedPath).toBe("/api/overview/");
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
describe("stripPrefixes handling", () => {
|
|
1186
|
+
it("should apply stripPrefixes in the generated mappings", () => {
|
|
1187
|
+
const includes: IncludePattern[] = [
|
|
1188
|
+
{
|
|
1189
|
+
pattern: "docs/**/*.md",
|
|
1190
|
+
basePath: "src/content/docs/api",
|
|
1191
|
+
pathMappings: {
|
|
1192
|
+
"docs/": "",
|
|
1193
|
+
},
|
|
1194
|
+
},
|
|
1195
|
+
];
|
|
1196
|
+
|
|
1197
|
+
const result = generateAutoLinkMappings(
|
|
1198
|
+
includes,
|
|
1199
|
+
["src/content/docs"],
|
|
1200
|
+
);
|
|
1201
|
+
|
|
1202
|
+
expect(result).toHaveLength(1);
|
|
1203
|
+
|
|
1204
|
+
if (typeof result[0].replacement === "function") {
|
|
1205
|
+
const transformedPath = result[0].replacement(
|
|
1206
|
+
"docs/guide.md",
|
|
1207
|
+
"",
|
|
1208
|
+
{} as LinkTransformContext,
|
|
1209
|
+
);
|
|
1210
|
+
// Should strip src/content/docs prefix
|
|
1211
|
+
expect(transformedPath).toBe("/api/guide/");
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
it("should work with empty stripPrefixes array", () => {
|
|
1216
|
+
const includes: IncludePattern[] = [
|
|
1217
|
+
{
|
|
1218
|
+
pattern: "docs/**/*.md",
|
|
1219
|
+
basePath: "content/docs",
|
|
1220
|
+
pathMappings: {
|
|
1221
|
+
"docs/": "",
|
|
1222
|
+
},
|
|
1223
|
+
},
|
|
1224
|
+
];
|
|
1225
|
+
|
|
1226
|
+
const result = generateAutoLinkMappings(includes, []);
|
|
1227
|
+
|
|
1228
|
+
expect(result).toHaveLength(1);
|
|
1229
|
+
|
|
1230
|
+
if (typeof result[0].replacement === "function") {
|
|
1231
|
+
const transformedPath = result[0].replacement(
|
|
1232
|
+
"docs/guide.md",
|
|
1233
|
+
"",
|
|
1234
|
+
{} as LinkTransformContext,
|
|
1235
|
+
);
|
|
1236
|
+
// No prefix stripping, should include full path
|
|
1237
|
+
expect(transformedPath).toMatch(/^\/content\/docs/);
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
describe("multiple include patterns", () => {
|
|
1243
|
+
it("should generate mappings from multiple include patterns", () => {
|
|
1244
|
+
const includes: IncludePattern[] = [
|
|
1245
|
+
{
|
|
1246
|
+
pattern: "docs/api/**/*.md",
|
|
1247
|
+
basePath: "src/content/docs/reference/api",
|
|
1248
|
+
pathMappings: {
|
|
1249
|
+
"docs/api/": "api/",
|
|
1250
|
+
},
|
|
1251
|
+
},
|
|
1252
|
+
{
|
|
1253
|
+
pattern: "docs/guides/**/*.md",
|
|
1254
|
+
basePath: "src/content/docs/guides",
|
|
1255
|
+
pathMappings: {
|
|
1256
|
+
"docs/guides/": "",
|
|
1257
|
+
},
|
|
1258
|
+
},
|
|
1259
|
+
];
|
|
1260
|
+
|
|
1261
|
+
const result = generateAutoLinkMappings(
|
|
1262
|
+
includes,
|
|
1263
|
+
["src/content/docs"],
|
|
1264
|
+
);
|
|
1265
|
+
|
|
1266
|
+
expect(result).toHaveLength(2);
|
|
1267
|
+
result.forEach((mapping) => {
|
|
1268
|
+
expect(mapping.global).toBe(true);
|
|
1269
|
+
});
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
it("should handle mix of patterns with and without pathMappings", () => {
|
|
1273
|
+
const includes: IncludePattern[] = [
|
|
1274
|
+
{
|
|
1275
|
+
pattern: "docs/api/**/*.md",
|
|
1276
|
+
basePath: "src/content/docs/api",
|
|
1277
|
+
pathMappings: {
|
|
1278
|
+
"docs/api/": "",
|
|
1279
|
+
},
|
|
1280
|
+
},
|
|
1281
|
+
{
|
|
1282
|
+
pattern: "docs/guides/**/*.md",
|
|
1283
|
+
basePath: "src/content/docs/guides",
|
|
1284
|
+
// No pathMappings
|
|
1285
|
+
},
|
|
1286
|
+
];
|
|
1287
|
+
|
|
1288
|
+
const result = generateAutoLinkMappings(
|
|
1289
|
+
includes,
|
|
1290
|
+
["src/content/docs"],
|
|
1291
|
+
);
|
|
1292
|
+
|
|
1293
|
+
expect(result).toHaveLength(1);
|
|
1294
|
+
expect(result[0].global).toBe(true);
|
|
1295
|
+
});
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
describe("special characters in paths", () => {
|
|
1299
|
+
it("should escape regex special characters in source paths", () => {
|
|
1300
|
+
const includes: IncludePattern[] = [
|
|
1301
|
+
{
|
|
1302
|
+
pattern: "docs/**/*.md",
|
|
1303
|
+
basePath: "src/content/docs",
|
|
1304
|
+
pathMappings: {
|
|
1305
|
+
"docs/c++/": "cpp/",
|
|
1306
|
+
},
|
|
1307
|
+
},
|
|
1308
|
+
];
|
|
1309
|
+
|
|
1310
|
+
const result = generateAutoLinkMappings(
|
|
1311
|
+
includes,
|
|
1312
|
+
["src/content/docs"],
|
|
1313
|
+
);
|
|
1314
|
+
|
|
1315
|
+
expect(result).toHaveLength(1);
|
|
1316
|
+
|
|
1317
|
+
// The + should be escaped in the regex pattern
|
|
1318
|
+
expect("docs/c++/guide.md").toMatch(result[0].pattern as RegExp);
|
|
1319
|
+
expect("docs/cxx/guide.md").not.toMatch(result[0].pattern as RegExp);
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
it("should handle dots in path names", () => {
|
|
1323
|
+
const includes: IncludePattern[] = [
|
|
1324
|
+
{
|
|
1325
|
+
pattern: "docs/**/*.md",
|
|
1326
|
+
basePath: "src/content/docs",
|
|
1327
|
+
pathMappings: {
|
|
1328
|
+
"docs/v1.0/": "v1/",
|
|
1329
|
+
},
|
|
1330
|
+
},
|
|
1331
|
+
];
|
|
1332
|
+
|
|
1333
|
+
const result = generateAutoLinkMappings(
|
|
1334
|
+
includes,
|
|
1335
|
+
["src/content/docs"],
|
|
1336
|
+
);
|
|
1337
|
+
|
|
1338
|
+
expect(result).toHaveLength(1);
|
|
1339
|
+
|
|
1340
|
+
// The dot should be escaped in the regex pattern
|
|
1341
|
+
expect("docs/v1.0/api.md").toMatch(result[0].pattern as RegExp);
|
|
1342
|
+
expect("docs/v1x0/api.md").not.toMatch(result[0].pattern as RegExp);
|
|
1343
|
+
});
|
|
1344
|
+
});
|
|
1345
|
+
});
|