@fragno-dev/cli 0.1.16 → 0.1.18
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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +24 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +58 -25
- package/dist/cli.js.map +1 -1
- package/package.json +12 -4
- package/src/commands/corpus.test.ts +1129 -0
- package/src/commands/corpus.ts +93 -42
|
@@ -0,0 +1,1129 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import type { Subject } from "@fragno-dev/corpus";
|
|
3
|
+
import { getSubjects, getSubject } from "@fragno-dev/corpus";
|
|
4
|
+
import {
|
|
5
|
+
addLineNumbers,
|
|
6
|
+
filterByLineRange,
|
|
7
|
+
buildSubjectsMarkdown,
|
|
8
|
+
extractHeadingsAndBlocks,
|
|
9
|
+
} from "./corpus.js";
|
|
10
|
+
|
|
11
|
+
describe("corpus package integration", () => {
|
|
12
|
+
it("should be able to find and load subjects directory from published package", () => {
|
|
13
|
+
// This test verifies that the subjects directory is correctly included in the
|
|
14
|
+
// published package and can be found when the code runs from dist/
|
|
15
|
+
const subjects = getSubjects();
|
|
16
|
+
|
|
17
|
+
// Should find at least one subject
|
|
18
|
+
expect(subjects.length).toBeGreaterThan(0);
|
|
19
|
+
|
|
20
|
+
// Each subject should have an id and title
|
|
21
|
+
for (const subject of subjects) {
|
|
22
|
+
expect(subject.id).toBeDefined();
|
|
23
|
+
expect(subject.id.length).toBeGreaterThan(0);
|
|
24
|
+
expect(subject.title).toBeDefined();
|
|
25
|
+
expect(subject.title.length).toBeGreaterThan(0);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should be able to load a specific subject file", () => {
|
|
30
|
+
// Verify we can actually load a subject's content (not just list them)
|
|
31
|
+
const subjects = getSubject("defining-routes");
|
|
32
|
+
|
|
33
|
+
expect(subjects).toHaveLength(1);
|
|
34
|
+
const subject = subjects[0];
|
|
35
|
+
|
|
36
|
+
expect(subject.id).toBe("defining-routes");
|
|
37
|
+
expect(subject.title).toBeDefined();
|
|
38
|
+
expect(subject.title.length).toBeGreaterThan(0);
|
|
39
|
+
|
|
40
|
+
// Subject should have sections (the actual markdown content)
|
|
41
|
+
expect(subject.sections.length).toBeGreaterThan(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should handle loading multiple subjects at once", () => {
|
|
45
|
+
const subjects = getSubject("defining-routes", "client-state-management");
|
|
46
|
+
|
|
47
|
+
expect(subjects).toHaveLength(2);
|
|
48
|
+
|
|
49
|
+
const ids = subjects.map((s) => s.id);
|
|
50
|
+
expect(ids).toContain("defining-routes");
|
|
51
|
+
expect(ids).toContain("client-state-management");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("addLineNumbers", () => {
|
|
56
|
+
it("should add line numbers with proper padding", () => {
|
|
57
|
+
const content = "line 1\nline 2\nline 3";
|
|
58
|
+
const result = addLineNumbers(content);
|
|
59
|
+
|
|
60
|
+
expect(result).toBe("1│ line 1\n2│ line 2\n3│ line 3");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should pad line numbers correctly for multi-digit lines", () => {
|
|
64
|
+
const content = Array.from({ length: 100 }, (_, i) => `line ${i + 1}`).join("\n");
|
|
65
|
+
const result = addLineNumbers(content);
|
|
66
|
+
|
|
67
|
+
const lines = result.split("\n");
|
|
68
|
+
// First line should have padding
|
|
69
|
+
expect(lines[0]).toBe(" 1│ line 1");
|
|
70
|
+
// Line 10 should have less padding
|
|
71
|
+
expect(lines[9]).toBe(" 10│ line 10");
|
|
72
|
+
// Line 100 should have no padding
|
|
73
|
+
expect(lines[99]).toBe("100│ line 100");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should start from custom line number", () => {
|
|
77
|
+
const content = "line 1\nline 2\nline 3";
|
|
78
|
+
const result = addLineNumbers(content, 50);
|
|
79
|
+
|
|
80
|
+
expect(result).toBe("50│ line 1\n51│ line 2\n52│ line 3");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should handle empty lines", () => {
|
|
84
|
+
const content = "line 1\n\nline 3";
|
|
85
|
+
const result = addLineNumbers(content);
|
|
86
|
+
|
|
87
|
+
expect(result).toBe("1│ line 1\n2│ \n3│ line 3");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should handle single line", () => {
|
|
91
|
+
const content = "single line";
|
|
92
|
+
const result = addLineNumbers(content);
|
|
93
|
+
|
|
94
|
+
expect(result).toBe("1│ single line");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should handle empty content", () => {
|
|
98
|
+
const content = "";
|
|
99
|
+
const result = addLineNumbers(content);
|
|
100
|
+
|
|
101
|
+
expect(result).toBe("1│ ");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should pad correctly when starting from high numbers", () => {
|
|
105
|
+
const content = "line 1\nline 2";
|
|
106
|
+
const result = addLineNumbers(content, 998);
|
|
107
|
+
|
|
108
|
+
expect(result).toBe("998│ line 1\n999│ line 2");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("filterByLineRange", () => {
|
|
113
|
+
it("should filter lines within range", () => {
|
|
114
|
+
const content = "line 1\nline 2\nline 3\nline 4\nline 5";
|
|
115
|
+
const result = filterByLineRange(content, 2, 4);
|
|
116
|
+
|
|
117
|
+
expect(result).toBe("line 2\nline 3\nline 4");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should handle start line 1", () => {
|
|
121
|
+
const content = "line 1\nline 2\nline 3";
|
|
122
|
+
const result = filterByLineRange(content, 1, 2);
|
|
123
|
+
|
|
124
|
+
expect(result).toBe("line 1\nline 2");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should handle range extending to end", () => {
|
|
128
|
+
const content = "line 1\nline 2\nline 3";
|
|
129
|
+
const result = filterByLineRange(content, 2, 5);
|
|
130
|
+
|
|
131
|
+
expect(result).toBe("line 2\nline 3");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should handle range starting beyond content length", () => {
|
|
135
|
+
const content = "line 1\nline 2\nline 3";
|
|
136
|
+
const result = filterByLineRange(content, 10, 20);
|
|
137
|
+
|
|
138
|
+
expect(result).toBe("");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should handle negative start line (clamps to 0)", () => {
|
|
142
|
+
const content = "line 1\nline 2\nline 3";
|
|
143
|
+
const result = filterByLineRange(content, -5, 2);
|
|
144
|
+
|
|
145
|
+
expect(result).toBe("line 1\nline 2");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should return single line when start equals end", () => {
|
|
149
|
+
const content = "line 1\nline 2\nline 3";
|
|
150
|
+
const result = filterByLineRange(content, 2, 2);
|
|
151
|
+
|
|
152
|
+
expect(result).toBe("line 2");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should handle entire content", () => {
|
|
156
|
+
const content = "line 1\nline 2\nline 3";
|
|
157
|
+
const result = filterByLineRange(content, 1, 3);
|
|
158
|
+
|
|
159
|
+
expect(result).toBe("line 1\nline 2\nline 3");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("buildSubjectsMarkdown", () => {
|
|
164
|
+
it("should build markdown for subject with title only", () => {
|
|
165
|
+
const subjects: Subject[] = [
|
|
166
|
+
{
|
|
167
|
+
id: "test-subject",
|
|
168
|
+
title: "Test Subject",
|
|
169
|
+
description: "",
|
|
170
|
+
imports: "",
|
|
171
|
+
prelude: [],
|
|
172
|
+
testInit: [],
|
|
173
|
+
examples: [],
|
|
174
|
+
sections: [],
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
const result = buildSubjectsMarkdown(subjects);
|
|
179
|
+
expect(result).toBe("# Test Subject\n\n");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("should include description when present", () => {
|
|
183
|
+
const subjects: Subject[] = [
|
|
184
|
+
{
|
|
185
|
+
id: "test-subject",
|
|
186
|
+
title: "Test Subject",
|
|
187
|
+
description: "This is a test description.",
|
|
188
|
+
imports: "",
|
|
189
|
+
prelude: [],
|
|
190
|
+
testInit: [],
|
|
191
|
+
examples: [],
|
|
192
|
+
sections: [],
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
const result = buildSubjectsMarkdown(subjects);
|
|
197
|
+
expect(result).toContain("# Test Subject");
|
|
198
|
+
expect(result).toContain("This is a test description.");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("should include imports block when present", () => {
|
|
202
|
+
const subjects: Subject[] = [
|
|
203
|
+
{
|
|
204
|
+
id: "test-subject",
|
|
205
|
+
title: "Test Subject",
|
|
206
|
+
description: "",
|
|
207
|
+
imports: 'import { x } from "y";',
|
|
208
|
+
prelude: [],
|
|
209
|
+
testInit: [],
|
|
210
|
+
examples: [],
|
|
211
|
+
sections: [],
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
const result = buildSubjectsMarkdown(subjects);
|
|
216
|
+
expect(result).toContain("### Imports");
|
|
217
|
+
expect(result).toContain("```typescript");
|
|
218
|
+
expect(result).toContain('import { x } from "y";');
|
|
219
|
+
expect(result).toContain("```");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("should include prelude blocks when present", () => {
|
|
223
|
+
const subjects: Subject[] = [
|
|
224
|
+
{
|
|
225
|
+
id: "test-subject",
|
|
226
|
+
title: "Test Subject",
|
|
227
|
+
description: "",
|
|
228
|
+
imports: "",
|
|
229
|
+
prelude: [
|
|
230
|
+
{ code: "const schema = {};", id: "schema" },
|
|
231
|
+
{ code: "const config = {};", id: "config" },
|
|
232
|
+
],
|
|
233
|
+
testInit: [],
|
|
234
|
+
examples: [],
|
|
235
|
+
sections: [],
|
|
236
|
+
},
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
const result = buildSubjectsMarkdown(subjects);
|
|
240
|
+
expect(result).toContain("### Prelude");
|
|
241
|
+
expect(result).toContain("const schema = {};");
|
|
242
|
+
expect(result).toContain("const config = {};");
|
|
243
|
+
// Should have multiple code blocks
|
|
244
|
+
expect(result.split("```typescript").length).toBe(3); // One opening for each block
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should include sections with content", () => {
|
|
248
|
+
const subjects: Subject[] = [
|
|
249
|
+
{
|
|
250
|
+
id: "test-subject",
|
|
251
|
+
title: "Test Subject",
|
|
252
|
+
description: "",
|
|
253
|
+
imports: "",
|
|
254
|
+
prelude: [],
|
|
255
|
+
testInit: [],
|
|
256
|
+
examples: [],
|
|
257
|
+
sections: [
|
|
258
|
+
{
|
|
259
|
+
heading: "Basic Usage",
|
|
260
|
+
content: "This is how you use it.\n\n```typescript\nconst x = 1;\n```",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
heading: "Advanced Usage",
|
|
264
|
+
content: "This is advanced usage.",
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
const result = buildSubjectsMarkdown(subjects);
|
|
271
|
+
expect(result).toContain("## Basic Usage");
|
|
272
|
+
expect(result).toContain("This is how you use it.");
|
|
273
|
+
expect(result).toContain("## Advanced Usage");
|
|
274
|
+
expect(result).toContain("This is advanced usage.");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("should build markdown for multiple subjects", () => {
|
|
278
|
+
const subjects: Subject[] = [
|
|
279
|
+
{
|
|
280
|
+
id: "subject-1",
|
|
281
|
+
title: "Subject 1",
|
|
282
|
+
description: "First subject",
|
|
283
|
+
imports: "",
|
|
284
|
+
prelude: [],
|
|
285
|
+
testInit: [],
|
|
286
|
+
examples: [],
|
|
287
|
+
sections: [],
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: "subject-2",
|
|
291
|
+
title: "Subject 2",
|
|
292
|
+
description: "Second subject",
|
|
293
|
+
imports: "",
|
|
294
|
+
prelude: [],
|
|
295
|
+
testInit: [],
|
|
296
|
+
examples: [],
|
|
297
|
+
sections: [],
|
|
298
|
+
},
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
const result = buildSubjectsMarkdown(subjects);
|
|
302
|
+
expect(result).toContain("# Subject 1");
|
|
303
|
+
expect(result).toContain("First subject");
|
|
304
|
+
expect(result).toContain("# Subject 2");
|
|
305
|
+
expect(result).toContain("Second subject");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("should build complete markdown with all components", () => {
|
|
309
|
+
const subjects: Subject[] = [
|
|
310
|
+
{
|
|
311
|
+
id: "complete-subject",
|
|
312
|
+
title: "Complete Subject",
|
|
313
|
+
description: "A complete example",
|
|
314
|
+
imports: 'import { defineRoute } from "@fragno-dev/core";',
|
|
315
|
+
prelude: [{ code: "const schema = {};", id: "schema" }],
|
|
316
|
+
testInit: [],
|
|
317
|
+
examples: [
|
|
318
|
+
{
|
|
319
|
+
code: "const example = 1;",
|
|
320
|
+
explanation: "Example explanation",
|
|
321
|
+
id: "example-1",
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
sections: [
|
|
325
|
+
{
|
|
326
|
+
heading: "Usage",
|
|
327
|
+
content: "Usage information here",
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
},
|
|
331
|
+
];
|
|
332
|
+
|
|
333
|
+
const result = buildSubjectsMarkdown(subjects);
|
|
334
|
+
expect(result).toContain("# Complete Subject");
|
|
335
|
+
expect(result).toContain("A complete example");
|
|
336
|
+
expect(result).toContain("### Imports");
|
|
337
|
+
expect(result).toContain('import { defineRoute } from "@fragno-dev/core";');
|
|
338
|
+
expect(result).toContain("### Prelude");
|
|
339
|
+
expect(result).toContain("const schema = {};");
|
|
340
|
+
expect(result).toContain("## Usage");
|
|
341
|
+
expect(result).toContain("Usage information here");
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe("buildSubjectsMarkdown - code blocks", () => {
|
|
346
|
+
it("should properly format code blocks in sections", () => {
|
|
347
|
+
const subjects: Subject[] = [
|
|
348
|
+
{
|
|
349
|
+
id: "test-subject",
|
|
350
|
+
title: "Test Subject",
|
|
351
|
+
description: "",
|
|
352
|
+
imports: "",
|
|
353
|
+
prelude: [],
|
|
354
|
+
testInit: [],
|
|
355
|
+
examples: [
|
|
356
|
+
{
|
|
357
|
+
code: "const x = 1;\nconst y = 2;",
|
|
358
|
+
explanation: "Example with multiline code",
|
|
359
|
+
id: "example-1",
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
sections: [
|
|
363
|
+
{
|
|
364
|
+
heading: "Examples",
|
|
365
|
+
content:
|
|
366
|
+
"Here's an example:\n\n```typescript\nconst x = 1;\nconst y = 2;\n```\n\nThat's the example.",
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
},
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
const result = buildSubjectsMarkdown(subjects);
|
|
373
|
+
expect(result).toContain("## Examples");
|
|
374
|
+
expect(result).toContain("```typescript");
|
|
375
|
+
expect(result).toContain("const x = 1;");
|
|
376
|
+
expect(result).toContain("const y = 2;");
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("should handle sections with special markdown characters", () => {
|
|
380
|
+
const subjects: Subject[] = [
|
|
381
|
+
{
|
|
382
|
+
id: "test-subject",
|
|
383
|
+
title: "Test Subject",
|
|
384
|
+
description: "",
|
|
385
|
+
imports: "",
|
|
386
|
+
prelude: [],
|
|
387
|
+
testInit: [],
|
|
388
|
+
examples: [],
|
|
389
|
+
sections: [
|
|
390
|
+
{
|
|
391
|
+
heading: "Special Characters",
|
|
392
|
+
content: "Text with **bold** and *italic* and `code` inline.",
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
},
|
|
396
|
+
];
|
|
397
|
+
|
|
398
|
+
const result = buildSubjectsMarkdown(subjects);
|
|
399
|
+
expect(result).toContain("**bold**");
|
|
400
|
+
expect(result).toContain("*italic*");
|
|
401
|
+
expect(result).toContain("`code`");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("should handle nested lists in sections", () => {
|
|
405
|
+
const subjects: Subject[] = [
|
|
406
|
+
{
|
|
407
|
+
id: "test-subject",
|
|
408
|
+
title: "Test Subject",
|
|
409
|
+
description: "",
|
|
410
|
+
imports: "",
|
|
411
|
+
prelude: [],
|
|
412
|
+
testInit: [],
|
|
413
|
+
examples: [],
|
|
414
|
+
sections: [
|
|
415
|
+
{
|
|
416
|
+
heading: "Lists",
|
|
417
|
+
content: "- Item 1\n- Item 2\n - Nested item\n- Item 3",
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
},
|
|
421
|
+
];
|
|
422
|
+
|
|
423
|
+
const result = buildSubjectsMarkdown(subjects);
|
|
424
|
+
expect(result).toContain("- Item 1");
|
|
425
|
+
expect(result).toContain("- Item 2");
|
|
426
|
+
expect(result).toContain(" - Nested item");
|
|
427
|
+
expect(result).toContain("- Item 3");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("should handle code blocks with different languages", () => {
|
|
431
|
+
const subjects: Subject[] = [
|
|
432
|
+
{
|
|
433
|
+
id: "test-subject",
|
|
434
|
+
title: "Test Subject",
|
|
435
|
+
description: "",
|
|
436
|
+
imports: "",
|
|
437
|
+
prelude: [],
|
|
438
|
+
testInit: [],
|
|
439
|
+
examples: [],
|
|
440
|
+
sections: [
|
|
441
|
+
{
|
|
442
|
+
heading: "Multi-language Examples",
|
|
443
|
+
content: "TypeScript:\n\n```typescript\nconst x = 1;\n```\n\nJSON:\n\n```json\n{}\n```",
|
|
444
|
+
},
|
|
445
|
+
],
|
|
446
|
+
},
|
|
447
|
+
];
|
|
448
|
+
|
|
449
|
+
const result = buildSubjectsMarkdown(subjects);
|
|
450
|
+
expect(result).toContain("```typescript");
|
|
451
|
+
expect(result).toContain("```json");
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("should preserve code block indentation", () => {
|
|
455
|
+
const subjects: Subject[] = [
|
|
456
|
+
{
|
|
457
|
+
id: "test-subject",
|
|
458
|
+
title: "Test Subject",
|
|
459
|
+
description: "",
|
|
460
|
+
imports: "",
|
|
461
|
+
prelude: [
|
|
462
|
+
{
|
|
463
|
+
code: "function test() {\n const x = 1;\n return x;\n}",
|
|
464
|
+
id: "test-fn",
|
|
465
|
+
},
|
|
466
|
+
],
|
|
467
|
+
testInit: [],
|
|
468
|
+
examples: [],
|
|
469
|
+
sections: [],
|
|
470
|
+
},
|
|
471
|
+
];
|
|
472
|
+
|
|
473
|
+
const result = buildSubjectsMarkdown(subjects);
|
|
474
|
+
expect(result).toContain(" const x = 1;");
|
|
475
|
+
expect(result).toContain(" return x;");
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
describe("addLineNumbers - edge cases", () => {
|
|
480
|
+
it("should handle very long lines without breaking", () => {
|
|
481
|
+
const longLine = "a".repeat(1000);
|
|
482
|
+
const content = `short line\n${longLine}\nshort line`;
|
|
483
|
+
const result = addLineNumbers(content);
|
|
484
|
+
|
|
485
|
+
const lines = result.split("\n");
|
|
486
|
+
expect(lines).toHaveLength(3);
|
|
487
|
+
expect(lines[1]).toContain(longLine);
|
|
488
|
+
expect(lines[1]).toMatch(/^2│/);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("should handle lines with tabs", () => {
|
|
492
|
+
const content = "line 1\n\tindented line\n\t\tdoubly indented";
|
|
493
|
+
const result = addLineNumbers(content);
|
|
494
|
+
|
|
495
|
+
expect(result).toContain("1│ line 1");
|
|
496
|
+
expect(result).toContain("2│ \tindented line");
|
|
497
|
+
expect(result).toContain("3│ \t\tdoubly indented");
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("should handle unicode characters", () => {
|
|
501
|
+
const content = "Hello 世界\nمرحبا\n🎉";
|
|
502
|
+
const result = addLineNumbers(content);
|
|
503
|
+
|
|
504
|
+
expect(result).toContain("1│ Hello 世界");
|
|
505
|
+
expect(result).toContain("2│ مرحبا");
|
|
506
|
+
expect(result).toContain("3│ 🎉");
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("should handle content with only newlines", () => {
|
|
510
|
+
const content = "\n\n\n";
|
|
511
|
+
const result = addLineNumbers(content);
|
|
512
|
+
|
|
513
|
+
const lines = result.split("\n");
|
|
514
|
+
expect(lines).toHaveLength(4);
|
|
515
|
+
expect(lines[0]).toBe("1│ ");
|
|
516
|
+
expect(lines[1]).toBe("2│ ");
|
|
517
|
+
expect(lines[2]).toBe("3│ ");
|
|
518
|
+
expect(lines[3]).toBe("4│ ");
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("should pad consistently for very large line numbers", () => {
|
|
522
|
+
const content = Array.from({ length: 1000 }, (_, i) => `line ${i + 1}`).join("\n");
|
|
523
|
+
const result = addLineNumbers(content);
|
|
524
|
+
|
|
525
|
+
const lines = result.split("\n");
|
|
526
|
+
// All lines should start with 4 digits + │
|
|
527
|
+
for (const line of lines) {
|
|
528
|
+
expect(line).toMatch(/^\s{0,3}\d{1,4}│/);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// First line should have 3 spaces of padding
|
|
532
|
+
expect(lines[0]).toMatch(/^\s{3}1│/);
|
|
533
|
+
// Line 1000 should have no padding
|
|
534
|
+
expect(lines[999]).toMatch(/^1000│/);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
describe("filterByLineRange - edge cases", () => {
|
|
539
|
+
it("should handle Windows-style line endings", () => {
|
|
540
|
+
const content = "line 1\r\nline 2\r\nline 3";
|
|
541
|
+
const result = filterByLineRange(content, 2, 2);
|
|
542
|
+
|
|
543
|
+
// The split will include the \r
|
|
544
|
+
expect(result).toContain("line 2");
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("should handle empty lines at boundaries", () => {
|
|
548
|
+
const content = "\nline 2\n\nline 4\n";
|
|
549
|
+
const result = filterByLineRange(content, 2, 4);
|
|
550
|
+
|
|
551
|
+
expect(result).toBe("line 2\n\nline 4");
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it("should handle single character per line", () => {
|
|
555
|
+
const content = "a\nb\nc\nd";
|
|
556
|
+
const result = filterByLineRange(content, 2, 3);
|
|
557
|
+
|
|
558
|
+
expect(result).toBe("b\nc");
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it("should preserve whitespace-only lines", () => {
|
|
562
|
+
const content = "line 1\n \nline 3";
|
|
563
|
+
const result = filterByLineRange(content, 1, 3);
|
|
564
|
+
|
|
565
|
+
expect(result).toBe("line 1\n \nline 3");
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
describe("extractHeadingsAndBlocks", () => {
|
|
570
|
+
it("should extract title with line number", () => {
|
|
571
|
+
const subjects: Subject[] = [
|
|
572
|
+
{
|
|
573
|
+
id: "test-subject",
|
|
574
|
+
title: "Test Subject",
|
|
575
|
+
description: "",
|
|
576
|
+
imports: "",
|
|
577
|
+
prelude: [],
|
|
578
|
+
testInit: [],
|
|
579
|
+
examples: [],
|
|
580
|
+
sections: [],
|
|
581
|
+
},
|
|
582
|
+
];
|
|
583
|
+
|
|
584
|
+
const result = extractHeadingsAndBlocks(subjects);
|
|
585
|
+
expect(result).toContain("1│ # Test Subject");
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it("should show instruction header", () => {
|
|
589
|
+
const subjects: Subject[] = [
|
|
590
|
+
{
|
|
591
|
+
id: "test-subject",
|
|
592
|
+
title: "Test Subject",
|
|
593
|
+
description: "",
|
|
594
|
+
imports: "",
|
|
595
|
+
prelude: [],
|
|
596
|
+
testInit: [],
|
|
597
|
+
examples: [],
|
|
598
|
+
sections: [],
|
|
599
|
+
},
|
|
600
|
+
];
|
|
601
|
+
|
|
602
|
+
const result = extractHeadingsAndBlocks(subjects);
|
|
603
|
+
expect(result).toContain("Use --start N --end N flags to show specific line ranges");
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it("should include description with line numbers", () => {
|
|
607
|
+
const subjects: Subject[] = [
|
|
608
|
+
{
|
|
609
|
+
id: "test-subject",
|
|
610
|
+
title: "Test Subject",
|
|
611
|
+
description: "This is a description.",
|
|
612
|
+
imports: "",
|
|
613
|
+
prelude: [],
|
|
614
|
+
testInit: [],
|
|
615
|
+
examples: [],
|
|
616
|
+
sections: [],
|
|
617
|
+
},
|
|
618
|
+
];
|
|
619
|
+
|
|
620
|
+
const result = extractHeadingsAndBlocks(subjects);
|
|
621
|
+
expect(result).toContain("This is a description.");
|
|
622
|
+
// Should have line numbers
|
|
623
|
+
expect(result).toMatch(/\d+│ This is a description\./);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it("should show imports heading with line number", () => {
|
|
627
|
+
const subjects: Subject[] = [
|
|
628
|
+
{
|
|
629
|
+
id: "test-subject",
|
|
630
|
+
title: "Test Subject",
|
|
631
|
+
description: "",
|
|
632
|
+
imports: 'import { x } from "y";',
|
|
633
|
+
prelude: [],
|
|
634
|
+
testInit: [],
|
|
635
|
+
examples: [],
|
|
636
|
+
sections: [],
|
|
637
|
+
},
|
|
638
|
+
];
|
|
639
|
+
|
|
640
|
+
const result = extractHeadingsAndBlocks(subjects);
|
|
641
|
+
expect(result).toContain("### Imports");
|
|
642
|
+
expect(result).toMatch(/\d+│ ### Imports/);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it("should list prelude blocks with IDs and line ranges", () => {
|
|
646
|
+
const subjects: Subject[] = [
|
|
647
|
+
{
|
|
648
|
+
id: "test-subject",
|
|
649
|
+
title: "Test Subject",
|
|
650
|
+
description: "",
|
|
651
|
+
imports: "",
|
|
652
|
+
prelude: [
|
|
653
|
+
{ code: "const schema = {};", id: "schema" },
|
|
654
|
+
{ code: "const config = {\n key: 'value'\n};", id: "config" },
|
|
655
|
+
],
|
|
656
|
+
testInit: [],
|
|
657
|
+
examples: [],
|
|
658
|
+
sections: [],
|
|
659
|
+
},
|
|
660
|
+
];
|
|
661
|
+
|
|
662
|
+
const result = extractHeadingsAndBlocks(subjects);
|
|
663
|
+
expect(result).toContain("### Prelude");
|
|
664
|
+
expect(result).toContain("- id: `schema`");
|
|
665
|
+
expect(result).toContain("- id: `config`");
|
|
666
|
+
// Should show line ranges
|
|
667
|
+
expect(result).toMatch(/- id: `schema`, L\d+-\d+/);
|
|
668
|
+
expect(result).toMatch(/- id: `config`, L\d+-\d+/);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it("should extract section headings with line numbers", () => {
|
|
672
|
+
const subjects: Subject[] = [
|
|
673
|
+
{
|
|
674
|
+
id: "test-subject",
|
|
675
|
+
title: "Test Subject",
|
|
676
|
+
description: "",
|
|
677
|
+
imports: "",
|
|
678
|
+
prelude: [],
|
|
679
|
+
testInit: [],
|
|
680
|
+
examples: [],
|
|
681
|
+
sections: [
|
|
682
|
+
{
|
|
683
|
+
heading: "Basic Usage",
|
|
684
|
+
content: "Content here",
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
heading: "Advanced Usage",
|
|
688
|
+
content: "More content",
|
|
689
|
+
},
|
|
690
|
+
],
|
|
691
|
+
},
|
|
692
|
+
];
|
|
693
|
+
|
|
694
|
+
const result = extractHeadingsAndBlocks(subjects);
|
|
695
|
+
expect(result).toMatch(/\d+│ ## Basic Usage/);
|
|
696
|
+
expect(result).toMatch(/\d+│ ## Advanced Usage/);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it("should handle multiline descriptions", () => {
|
|
700
|
+
const subjects: Subject[] = [
|
|
701
|
+
{
|
|
702
|
+
id: "test-subject",
|
|
703
|
+
title: "Test Subject",
|
|
704
|
+
description: "Line 1 of description\nLine 2 of description\nLine 3 of description",
|
|
705
|
+
imports: "",
|
|
706
|
+
prelude: [],
|
|
707
|
+
testInit: [],
|
|
708
|
+
examples: [],
|
|
709
|
+
sections: [],
|
|
710
|
+
},
|
|
711
|
+
];
|
|
712
|
+
|
|
713
|
+
const result = extractHeadingsAndBlocks(subjects);
|
|
714
|
+
expect(result).toContain("Line 1 of description");
|
|
715
|
+
expect(result).toContain("Line 2 of description");
|
|
716
|
+
expect(result).toContain("Line 3 of description");
|
|
717
|
+
// Each line should have a line number
|
|
718
|
+
const lines = result.split("\n").filter((line) => line.includes("of description"));
|
|
719
|
+
expect(lines).toHaveLength(3);
|
|
720
|
+
for (const line of lines) {
|
|
721
|
+
expect(line).toMatch(/\d+│/);
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it("should show empty lines with line numbers", () => {
|
|
726
|
+
const subjects: Subject[] = [
|
|
727
|
+
{
|
|
728
|
+
id: "test-subject",
|
|
729
|
+
title: "Test Subject",
|
|
730
|
+
description: "",
|
|
731
|
+
imports: "",
|
|
732
|
+
prelude: [],
|
|
733
|
+
testInit: [],
|
|
734
|
+
examples: [],
|
|
735
|
+
sections: [],
|
|
736
|
+
},
|
|
737
|
+
];
|
|
738
|
+
|
|
739
|
+
const result = extractHeadingsAndBlocks(subjects);
|
|
740
|
+
// There should be an empty line after title with line number
|
|
741
|
+
expect(result).toMatch(/2│\s*$/m);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it("should handle subjects without prelude or imports", () => {
|
|
745
|
+
const subjects: Subject[] = [
|
|
746
|
+
{
|
|
747
|
+
id: "test-subject",
|
|
748
|
+
title: "Test Subject",
|
|
749
|
+
description: "Simple description",
|
|
750
|
+
imports: "",
|
|
751
|
+
prelude: [],
|
|
752
|
+
testInit: [],
|
|
753
|
+
examples: [],
|
|
754
|
+
sections: [
|
|
755
|
+
{
|
|
756
|
+
heading: "Section 1",
|
|
757
|
+
content: "Content",
|
|
758
|
+
},
|
|
759
|
+
],
|
|
760
|
+
},
|
|
761
|
+
];
|
|
762
|
+
|
|
763
|
+
const result = extractHeadingsAndBlocks(subjects);
|
|
764
|
+
expect(result).toContain("# Test Subject");
|
|
765
|
+
expect(result).toContain("Simple description");
|
|
766
|
+
expect(result).toContain("## Section 1");
|
|
767
|
+
expect(result).not.toContain("### Imports");
|
|
768
|
+
expect(result).not.toContain("### Prelude");
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it("should handle multiple subjects", () => {
|
|
772
|
+
const subjects: Subject[] = [
|
|
773
|
+
{
|
|
774
|
+
id: "subject-1",
|
|
775
|
+
title: "Subject 1",
|
|
776
|
+
description: "",
|
|
777
|
+
imports: "",
|
|
778
|
+
prelude: [],
|
|
779
|
+
testInit: [],
|
|
780
|
+
examples: [],
|
|
781
|
+
sections: [],
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
id: "subject-2",
|
|
785
|
+
title: "Subject 2",
|
|
786
|
+
description: "",
|
|
787
|
+
imports: "",
|
|
788
|
+
prelude: [],
|
|
789
|
+
testInit: [],
|
|
790
|
+
examples: [],
|
|
791
|
+
sections: [],
|
|
792
|
+
},
|
|
793
|
+
];
|
|
794
|
+
|
|
795
|
+
const result = extractHeadingsAndBlocks(subjects);
|
|
796
|
+
expect(result).toContain("# Subject 1");
|
|
797
|
+
expect(result).toContain("# Subject 2");
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
it("should pad line numbers consistently", () => {
|
|
801
|
+
const subjects: Subject[] = [
|
|
802
|
+
{
|
|
803
|
+
id: "test-subject",
|
|
804
|
+
title: "Test Subject",
|
|
805
|
+
description: "",
|
|
806
|
+
imports: "",
|
|
807
|
+
prelude: [],
|
|
808
|
+
testInit: [],
|
|
809
|
+
examples: [],
|
|
810
|
+
sections: Array.from({ length: 20 }, (_, i) => ({
|
|
811
|
+
heading: `Section ${i + 1}`,
|
|
812
|
+
content: "Some content",
|
|
813
|
+
})),
|
|
814
|
+
},
|
|
815
|
+
];
|
|
816
|
+
|
|
817
|
+
const result = extractHeadingsAndBlocks(subjects);
|
|
818
|
+
const lines = result.split("\n");
|
|
819
|
+
// Find lines with content (not just the header)
|
|
820
|
+
const numberedLines = lines.filter((line) => /^\s*\d+│/.test(line));
|
|
821
|
+
|
|
822
|
+
// Check that all line numbers have consistent padding
|
|
823
|
+
const lineNumbers = numberedLines.map((line) => {
|
|
824
|
+
const match = line.match(/^(\s*\d+)│/);
|
|
825
|
+
return match ? match[1] : "";
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
// All line number parts should have the same length
|
|
829
|
+
const lengths = lineNumbers.map((num) => num.length);
|
|
830
|
+
const maxLength = Math.max(...lengths);
|
|
831
|
+
for (const length of lengths) {
|
|
832
|
+
expect(length).toBe(maxLength);
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
it("should handle prelude blocks without IDs", () => {
|
|
837
|
+
const subjects: Subject[] = [
|
|
838
|
+
{
|
|
839
|
+
id: "test-subject",
|
|
840
|
+
title: "Test Subject",
|
|
841
|
+
description: "",
|
|
842
|
+
imports: "",
|
|
843
|
+
prelude: [
|
|
844
|
+
{ code: "const x = 1;", id: undefined },
|
|
845
|
+
{ code: "const y = 2;", id: "with-id" },
|
|
846
|
+
],
|
|
847
|
+
testInit: [],
|
|
848
|
+
examples: [],
|
|
849
|
+
sections: [],
|
|
850
|
+
},
|
|
851
|
+
];
|
|
852
|
+
|
|
853
|
+
const result = extractHeadingsAndBlocks(subjects);
|
|
854
|
+
expect(result).toContain("- id: `(no-id)`");
|
|
855
|
+
expect(result).toContain("- id: `with-id`");
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it("should calculate correct line ranges for imports code", () => {
|
|
859
|
+
const subjects: Subject[] = [
|
|
860
|
+
{
|
|
861
|
+
id: "test-subject",
|
|
862
|
+
title: "Test Subject",
|
|
863
|
+
description: "",
|
|
864
|
+
imports: 'import { a } from "a";\nimport { b } from "b";',
|
|
865
|
+
prelude: [],
|
|
866
|
+
testInit: [],
|
|
867
|
+
examples: [],
|
|
868
|
+
sections: [],
|
|
869
|
+
},
|
|
870
|
+
];
|
|
871
|
+
|
|
872
|
+
const result = extractHeadingsAndBlocks(subjects);
|
|
873
|
+
// Should show imports section
|
|
874
|
+
expect(result).toContain("### Imports");
|
|
875
|
+
// Should show the code fence line
|
|
876
|
+
expect(result).toContain("```typescript");
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
describe("integration - line numbers with filtering", () => {
|
|
881
|
+
it("should correctly filter and then add line numbers", () => {
|
|
882
|
+
const content = "line 1\nline 2\nline 3\nline 4\nline 5";
|
|
883
|
+
|
|
884
|
+
// Filter to lines 2-4
|
|
885
|
+
const filtered = filterByLineRange(content, 2, 4);
|
|
886
|
+
expect(filtered).toBe("line 2\nline 3\nline 4");
|
|
887
|
+
|
|
888
|
+
// Add line numbers starting from 2
|
|
889
|
+
const numbered = addLineNumbers(filtered, 2);
|
|
890
|
+
expect(numbered).toBe("2│ line 2\n3│ line 3\n4│ line 4");
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
it("should handle filtering single line then adding line numbers", () => {
|
|
894
|
+
const content = "line 1\nline 2\nline 3";
|
|
895
|
+
|
|
896
|
+
const filtered = filterByLineRange(content, 2, 2);
|
|
897
|
+
const numbered = addLineNumbers(filtered, 2);
|
|
898
|
+
|
|
899
|
+
expect(numbered).toBe("2│ line 2");
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
it("should preserve content integrity through filter and number pipeline", () => {
|
|
903
|
+
const content = Array.from({ length: 50 }, (_, i) => `content line ${i + 1}`).join("\n");
|
|
904
|
+
|
|
905
|
+
// Filter middle section
|
|
906
|
+
const filtered = filterByLineRange(content, 20, 30);
|
|
907
|
+
const lines = filtered.split("\n");
|
|
908
|
+
expect(lines).toHaveLength(11); // 20 through 30 inclusive
|
|
909
|
+
|
|
910
|
+
// Add line numbers
|
|
911
|
+
const numbered = addLineNumbers(filtered, 20);
|
|
912
|
+
const numberedLines = numbered.split("\n");
|
|
913
|
+
|
|
914
|
+
// Verify first and last lines
|
|
915
|
+
expect(numberedLines[0]).toContain("20│ content line 20");
|
|
916
|
+
expect(numberedLines[10]).toContain("30│ content line 30");
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
describe("integration - markdown building with special content", () => {
|
|
921
|
+
it("should handle subjects with all possible components", () => {
|
|
922
|
+
const subjects: Subject[] = [
|
|
923
|
+
{
|
|
924
|
+
id: "full-subject",
|
|
925
|
+
title: "Complete Test Subject",
|
|
926
|
+
description: "First line\nSecond line\nThird line",
|
|
927
|
+
imports: 'import { x } from "x";\nimport { y } from "y";',
|
|
928
|
+
prelude: [
|
|
929
|
+
{ code: "const schema = {};", id: "schema" },
|
|
930
|
+
{ code: "const config = {\n key: 'value'\n};", id: "config" },
|
|
931
|
+
],
|
|
932
|
+
testInit: [{ code: "setup();" }],
|
|
933
|
+
examples: [
|
|
934
|
+
{
|
|
935
|
+
code: "const example1 = 1;",
|
|
936
|
+
explanation: "First example",
|
|
937
|
+
id: "ex1",
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
code: "const example2 = 2;",
|
|
941
|
+
explanation: "Second example",
|
|
942
|
+
id: "ex2",
|
|
943
|
+
},
|
|
944
|
+
],
|
|
945
|
+
sections: [
|
|
946
|
+
{
|
|
947
|
+
heading: "Getting Started",
|
|
948
|
+
content: "Introduction paragraph.\n\nMore details here.",
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
heading: "Advanced Usage",
|
|
952
|
+
content: "Advanced content with code:\n\n```typescript\nadvanced code;\n```",
|
|
953
|
+
},
|
|
954
|
+
],
|
|
955
|
+
},
|
|
956
|
+
];
|
|
957
|
+
|
|
958
|
+
const markdown = buildSubjectsMarkdown(subjects);
|
|
959
|
+
|
|
960
|
+
// Verify all components are present
|
|
961
|
+
expect(markdown).toContain("# Complete Test Subject");
|
|
962
|
+
expect(markdown).toContain("First line\nSecond line\nThird line");
|
|
963
|
+
expect(markdown).toContain("### Imports");
|
|
964
|
+
expect(markdown).toContain('import { x } from "x";');
|
|
965
|
+
expect(markdown).toContain("### Prelude");
|
|
966
|
+
expect(markdown).toContain("const schema = {};");
|
|
967
|
+
expect(markdown).toContain("const config");
|
|
968
|
+
expect(markdown).toContain("## Getting Started");
|
|
969
|
+
expect(markdown).toContain("## Advanced Usage");
|
|
970
|
+
|
|
971
|
+
// Verify structure - title should come before sections
|
|
972
|
+
const titleIndex = markdown.indexOf("# Complete Test Subject");
|
|
973
|
+
const sectionIndex = markdown.indexOf("## Getting Started");
|
|
974
|
+
expect(titleIndex).toBeLessThan(sectionIndex);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
it("should build consistent markdown for empty subjects", () => {
|
|
978
|
+
const subjects: Subject[] = [
|
|
979
|
+
{
|
|
980
|
+
id: "empty",
|
|
981
|
+
title: "Empty Subject",
|
|
982
|
+
description: "",
|
|
983
|
+
imports: "",
|
|
984
|
+
prelude: [],
|
|
985
|
+
testInit: [],
|
|
986
|
+
examples: [],
|
|
987
|
+
sections: [],
|
|
988
|
+
},
|
|
989
|
+
];
|
|
990
|
+
|
|
991
|
+
const markdown = buildSubjectsMarkdown(subjects);
|
|
992
|
+
expect(markdown).toBe("# Empty Subject\n\n");
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
it("should properly separate multiple subjects in markdown", () => {
|
|
996
|
+
const subjects: Subject[] = [
|
|
997
|
+
{
|
|
998
|
+
id: "first",
|
|
999
|
+
title: "First Subject",
|
|
1000
|
+
description: "First description",
|
|
1001
|
+
imports: "",
|
|
1002
|
+
prelude: [],
|
|
1003
|
+
testInit: [],
|
|
1004
|
+
examples: [],
|
|
1005
|
+
sections: [],
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
id: "second",
|
|
1009
|
+
title: "Second Subject",
|
|
1010
|
+
description: "Second description",
|
|
1011
|
+
imports: "",
|
|
1012
|
+
prelude: [],
|
|
1013
|
+
testInit: [],
|
|
1014
|
+
examples: [],
|
|
1015
|
+
sections: [],
|
|
1016
|
+
},
|
|
1017
|
+
];
|
|
1018
|
+
|
|
1019
|
+
const markdown = buildSubjectsMarkdown(subjects);
|
|
1020
|
+
|
|
1021
|
+
// Both subjects should be present
|
|
1022
|
+
expect(markdown).toContain("# First Subject");
|
|
1023
|
+
expect(markdown).toContain("# Second Subject");
|
|
1024
|
+
|
|
1025
|
+
// They should be in order
|
|
1026
|
+
const firstIndex = markdown.indexOf("# First Subject");
|
|
1027
|
+
const secondIndex = markdown.indexOf("# Second Subject");
|
|
1028
|
+
expect(firstIndex).toBeLessThan(secondIndex);
|
|
1029
|
+
});
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
describe("edge cases - line number display width", () => {
|
|
1033
|
+
it("should handle transition from 9 to 10 lines correctly", () => {
|
|
1034
|
+
const content = Array.from({ length: 10 }, (_, i) => `line ${i + 1}`).join("\n");
|
|
1035
|
+
const result = addLineNumbers(content);
|
|
1036
|
+
|
|
1037
|
+
const lines = result.split("\n");
|
|
1038
|
+
// Lines 1-9 should have 1 space padding
|
|
1039
|
+
expect(lines[0]).toMatch(/^ 1│/);
|
|
1040
|
+
expect(lines[8]).toMatch(/^ 9│/);
|
|
1041
|
+
// Line 10 should have no padding
|
|
1042
|
+
expect(lines[9]).toMatch(/^10│/);
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
it("should handle transition from 99 to 100 lines correctly", () => {
|
|
1046
|
+
const content = Array.from({ length: 100 }, (_, i) => `line ${i + 1}`).join("\n");
|
|
1047
|
+
const result = addLineNumbers(content);
|
|
1048
|
+
|
|
1049
|
+
const lines = result.split("\n");
|
|
1050
|
+
// Lines 1-9 should have 2 spaces
|
|
1051
|
+
expect(lines[0]).toMatch(/^ {2}1│/);
|
|
1052
|
+
// Lines 10-99 should have 1 space
|
|
1053
|
+
expect(lines[9]).toMatch(/^ 10│/);
|
|
1054
|
+
expect(lines[98]).toMatch(/^ 99│/);
|
|
1055
|
+
// Line 100 should have no padding
|
|
1056
|
+
expect(lines[99]).toMatch(/^100│/);
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
it("should handle transition from 999 to 1000 lines correctly", () => {
|
|
1060
|
+
const content = Array.from({ length: 1000 }, (_, i) => `line ${i + 1}`).join("\n");
|
|
1061
|
+
const result = addLineNumbers(content);
|
|
1062
|
+
|
|
1063
|
+
const lines = result.split("\n");
|
|
1064
|
+
// Line 1 should have 3 spaces
|
|
1065
|
+
expect(lines[0]).toMatch(/^ {3}1│/);
|
|
1066
|
+
// Line 999 should have no padding
|
|
1067
|
+
expect(lines[998]).toMatch(/^ 999│/);
|
|
1068
|
+
// Line 1000 should have no padding
|
|
1069
|
+
expect(lines[999]).toMatch(/^1000│/);
|
|
1070
|
+
});
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
describe("extractHeadingsAndBlocks - complex scenarios", () => {
|
|
1074
|
+
it("should handle examples that match sections", () => {
|
|
1075
|
+
const subjects: Subject[] = [
|
|
1076
|
+
{
|
|
1077
|
+
id: "test",
|
|
1078
|
+
title: "Test",
|
|
1079
|
+
description: "",
|
|
1080
|
+
imports: "",
|
|
1081
|
+
prelude: [],
|
|
1082
|
+
testInit: [],
|
|
1083
|
+
examples: [
|
|
1084
|
+
{
|
|
1085
|
+
code: "const user = { name: 'John' };",
|
|
1086
|
+
explanation: "Create a user",
|
|
1087
|
+
id: "create-user",
|
|
1088
|
+
},
|
|
1089
|
+
],
|
|
1090
|
+
sections: [
|
|
1091
|
+
{
|
|
1092
|
+
heading: "User Management",
|
|
1093
|
+
content:
|
|
1094
|
+
"Here's how to create a user:\n\n```typescript\nconst user = { name: 'John' };\n```",
|
|
1095
|
+
},
|
|
1096
|
+
],
|
|
1097
|
+
},
|
|
1098
|
+
];
|
|
1099
|
+
|
|
1100
|
+
const result = extractHeadingsAndBlocks(subjects);
|
|
1101
|
+
expect(result).toContain("## User Management");
|
|
1102
|
+
expect(result).toContain("- id: `create-user`");
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
it("should handle sections with no matching examples", () => {
|
|
1106
|
+
const subjects: Subject[] = [
|
|
1107
|
+
{
|
|
1108
|
+
id: "test",
|
|
1109
|
+
title: "Test",
|
|
1110
|
+
description: "",
|
|
1111
|
+
imports: "",
|
|
1112
|
+
prelude: [],
|
|
1113
|
+
testInit: [],
|
|
1114
|
+
examples: [],
|
|
1115
|
+
sections: [
|
|
1116
|
+
{
|
|
1117
|
+
heading: "Overview",
|
|
1118
|
+
content: "This is just text, no code examples.",
|
|
1119
|
+
},
|
|
1120
|
+
],
|
|
1121
|
+
},
|
|
1122
|
+
];
|
|
1123
|
+
|
|
1124
|
+
const result = extractHeadingsAndBlocks(subjects);
|
|
1125
|
+
expect(result).toContain("## Overview");
|
|
1126
|
+
// Should not have any example IDs listed
|
|
1127
|
+
expect(result).not.toContain("- id:");
|
|
1128
|
+
});
|
|
1129
|
+
});
|