@peaske7/readit 0.2.0 → 0.2.1
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/.claude/CLAUDE.md +118 -76
- package/.claude/commands/review.md +1 -1
- package/.claude/roadmap.md +32 -9
- package/.claude/user-stories.md +100 -15
- package/AGENTS.md +30 -26
- package/Makefile +32 -0
- package/README.md +90 -2
- package/biome.json +18 -8
- package/bun.lock +426 -568
- package/bunfig.toml +2 -0
- package/docs/perf-baseline.md +56 -1
- package/docs/superpowers/specs/2026-03-27-go-server-rewrite-design.md +284 -0
- package/e2e/comments.spec.ts +14 -58
- package/e2e/document-load.spec.ts +1 -23
- package/e2e/export.spec.ts +4 -4
- package/e2e/perf/add-comment.spec.ts +9 -11
- package/e2e/perf/fixtures/generate.ts +1 -5
- package/e2e/perf/screenshot-final.png +0 -0
- package/e2e/perf/utils/metrics.ts +73 -9
- package/e2e/persistence-file.spec.ts +41 -26
- package/e2e/utils/selection.ts +17 -73
- package/go/cmd/readit/main.go +416 -0
- package/go/go.mod +20 -0
- package/go/go.sum +41 -0
- package/go/internal/server/anchor.go +302 -0
- package/go/internal/server/anchor_test.go +111 -0
- package/go/internal/server/comments.go +390 -0
- package/go/internal/server/documents.go +113 -0
- package/go/internal/server/embed.go +17 -0
- package/go/internal/server/headings.go +33 -0
- package/go/internal/server/headings_test.go +75 -0
- package/go/internal/server/htmltext.go +123 -0
- package/go/internal/server/markdown.go +157 -0
- package/go/internal/server/markdown_bench_test.go +42 -0
- package/go/internal/server/markdown_test.go +79 -0
- package/go/internal/server/server.go +453 -0
- package/go/internal/server/server_bench_test.go +122 -0
- package/go/internal/server/settings.go +110 -0
- package/go/internal/server/sse.go +140 -0
- package/go/internal/server/storage.go +275 -0
- package/go/internal/server/storage_test.go +118 -0
- package/go/internal/server/template.go +66 -0
- package/go/internal/server/types.go +101 -0
- package/go/internal/server/watcher.go +74 -0
- package/index.html +4 -14
- package/nvim-readit/lua/readit/health.lua +64 -0
- package/nvim-readit/lua/readit/init.lua +463 -0
- package/nvim-readit/plugin/readit.lua +19 -0
- package/package.json +20 -28
- package/shell/_readit +158 -0
- package/shell/readit.zsh +87 -0
- package/src/App.svelte +881 -0
- package/src/cli.ts +183 -21
- package/src/components/ActionsMenu.svelte +95 -0
- package/src/components/CommentBadge.svelte +67 -0
- package/src/components/CommentErrorBanner.svelte +33 -0
- package/src/components/CommentInput.svelte +75 -0
- package/src/components/CommentListItem.svelte +95 -0
- package/src/components/CommentManager.svelte +129 -0
- package/src/components/CommentNav.svelte +109 -0
- package/src/components/DocumentViewer.svelte +218 -0
- package/src/components/FloatingComment.svelte +107 -0
- package/src/components/Header.svelte +76 -0
- package/src/components/InlineEditor.svelte +72 -0
- package/src/components/MarginNote.svelte +167 -0
- package/src/components/MarginNotesContainer.svelte +33 -0
- package/src/components/RawModal.svelte +126 -0
- package/src/components/ReanchorConfirm.svelte +30 -0
- package/src/components/SettingsModal.svelte +220 -0
- package/src/components/ShortcutCapture.svelte +82 -0
- package/src/components/ShortcutList.svelte +145 -0
- package/src/components/TabBar.svelte +52 -0
- package/src/components/TableOfContents.svelte +125 -0
- package/src/components/ui/ActionLink.svelte +40 -0
- package/src/components/ui/{Button.tsx → Button.svelte} +19 -20
- package/src/components/ui/Dialog.svelte +97 -0
- package/src/components/ui/DropdownMenu.svelte +85 -0
- package/src/components/ui/DropdownMenuItem.svelte +38 -0
- package/src/components/ui/DropdownMenuSeparator.svelte +11 -0
- package/src/components/ui/{Text.tsx → Text.svelte} +18 -23
- package/src/env.d.ts +6 -0
- package/src/index.css +36 -166
- package/src/lib/__fixtures__/bench-data.ts +0 -13
- package/src/lib/anchor.bench.ts +1 -12
- package/src/lib/anchor.test.ts +0 -8
- package/src/lib/anchor.ts +0 -4
- package/src/lib/comment-storage.bench.ts +49 -0
- package/src/lib/comment-storage.test.ts +41 -33
- package/src/lib/comment-storage.ts +21 -18
- package/src/lib/export.bench.ts +21 -0
- package/src/lib/export.ts +0 -1
- package/src/{hooks/useHeadings.test.ts → lib/headings.test.ts} +10 -24
- package/src/{hooks/useHeadings.ts → lib/headings.ts} +11 -13
- package/src/lib/highlight/core.test.ts +0 -5
- package/src/lib/highlight/dom.ts +52 -216
- package/src/lib/highlight/highlight-registry.ts +221 -0
- package/src/lib/highlight/highlight.bench.ts +92 -0
- package/src/lib/highlight/highlighter.ts +112 -132
- package/src/lib/highlight/resolver.ts +5 -79
- package/src/lib/highlight/types.ts +0 -5
- package/src/lib/html-text.test.ts +162 -0
- package/src/lib/html-text.ts +161 -0
- package/src/lib/i18n/en.ts +26 -0
- package/src/lib/i18n/ja.ts +26 -0
- package/src/lib/i18n/types.ts +25 -0
- package/src/lib/margin-layout.bench.ts +61 -0
- package/src/lib/margin-layout.ts +0 -7
- package/src/lib/markdown-renderer.test.ts +154 -0
- package/src/lib/markdown-renderer.ts +177 -0
- package/src/lib/mermaid-config.ts +38 -0
- package/src/lib/mermaid-renderer.ts +162 -0
- package/src/lib/mermaid-worker.ts +60 -0
- package/src/lib/positions.ts +31 -24
- package/src/lib/shortcut-registry.ts +244 -0
- package/src/lib/utils.ts +0 -29
- package/src/main.ts +16 -0
- package/src/schema.ts +16 -5
- package/src/server.ts +355 -91
- package/src/stores/app.svelte.ts +231 -0
- package/src/stores/locale.svelte.ts +46 -0
- package/src/stores/settings.svelte.ts +90 -0
- package/src/stores/shortcuts.svelte.ts +104 -0
- package/src/stores/ui.svelte.ts +12 -0
- package/src/template.ts +104 -0
- package/src/test-setup.ts +47 -0
- package/svelte.config.js +5 -0
- package/tsconfig.json +2 -2
- package/vite.config.ts +23 -3
- package/vscode-readit/.mcp.json +7 -0
- package/vscode-readit/.vscodeignore +7 -0
- package/vscode-readit/bun.lock +78 -0
- package/vscode-readit/icon.svg +10 -0
- package/vscode-readit/package.json +110 -0
- package/vscode-readit/src/extension.ts +117 -0
- package/vscode-readit/src/server-manager.ts +272 -0
- package/vscode-readit/src/webview-provider.ts +204 -0
- package/vscode-readit/tsconfig.json +20 -0
- package/e2e/fixtures/sample.html +0 -13
- package/src/App.tsx +0 -368
- package/src/components/ActionsMenu.tsx +0 -91
- package/src/components/DocumentViewer/CodeBlock.tsx +0 -160
- package/src/components/DocumentViewer/DocumentViewer.tsx +0 -230
- package/src/components/DocumentViewer/MermaidDiagram.tsx +0 -136
- package/src/components/Header.tsx +0 -54
- package/src/components/InlineEditor.tsx +0 -74
- package/src/components/MarginNote.tsx +0 -185
- package/src/components/MarginNotes.tsx +0 -23
- package/src/components/RawModal.tsx +0 -144
- package/src/components/ReanchorConfirm.tsx +0 -36
- package/src/components/SettingsModal.tsx +0 -232
- package/src/components/TabBar.tsx +0 -60
- package/src/components/TableOfContents.tsx +0 -108
- package/src/components/comments/CommentBadge.tsx +0 -49
- package/src/components/comments/CommentInput.tsx +0 -86
- package/src/components/comments/CommentListItem.tsx +0 -90
- package/src/components/comments/CommentManager.tsx +0 -129
- package/src/components/comments/CommentNav.tsx +0 -109
- package/src/components/ui/ActionLink.tsx +0 -28
- package/src/components/ui/Dialog.tsx +0 -116
- package/src/components/ui/DropdownMenu.tsx +0 -158
- package/src/contexts/CommentContext.tsx +0 -198
- package/src/contexts/LocaleContext.tsx +0 -76
- package/src/contexts/PositionsContext.tsx +0 -16
- package/src/contexts/SettingsContext.tsx +0 -133
- package/src/hooks/useClickOutside.ts +0 -31
- package/src/hooks/useCommentNavigation.ts +0 -107
- package/src/hooks/useComments.ts +0 -311
- package/src/hooks/useDocument.ts +0 -157
- package/src/hooks/useScrollSpy.ts +0 -77
- package/src/hooks/useTextSelection.ts +0 -86
- package/src/lib/highlight/worker.ts +0 -45
- package/src/main.tsx +0 -13
- package/src/store.ts +0 -222
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import type * as os from "node:os";
|
|
2
1
|
import { describe, expect, it, vi } from "vitest";
|
|
3
2
|
import type { CommentFile } from "../schema";
|
|
4
3
|
import { COMMENT_FILE_LARGE } from "./__fixtures__/bench-data";
|
|
5
|
-
|
|
4
|
+
|
|
5
|
+
vi.mock("node:os", () => ({
|
|
6
|
+
homedir: () => "/home/testuser",
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const {
|
|
6
10
|
computeHash,
|
|
7
11
|
createComment,
|
|
8
12
|
getCommentPath,
|
|
@@ -11,16 +15,7 @@ import {
|
|
|
11
15
|
parseCommentFile,
|
|
12
16
|
serializeComments,
|
|
13
17
|
truncateSelection,
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Mock os.homedir for consistent test results
|
|
17
|
-
vi.mock("node:os", async () => {
|
|
18
|
-
const actual = await vi.importActual<typeof os>("node:os");
|
|
19
|
-
return {
|
|
20
|
-
...actual,
|
|
21
|
-
homedir: vi.fn(() => "/home/testuser"),
|
|
22
|
-
};
|
|
23
|
-
});
|
|
18
|
+
} = await import("./comment-storage");
|
|
24
19
|
|
|
25
20
|
describe("getCommentPath", () => {
|
|
26
21
|
it("handles absolute path", () => {
|
|
@@ -37,11 +32,6 @@ describe("getCommentPath", () => {
|
|
|
37
32
|
);
|
|
38
33
|
});
|
|
39
34
|
|
|
40
|
-
it("handles HTML files", () => {
|
|
41
|
-
const result = getCommentPath("/docs/api.html");
|
|
42
|
-
expect(result).toBe("/home/testuser/.readit/comments/docs/api.comments.md");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
35
|
it("handles root path", () => {
|
|
46
36
|
const result = getCommentPath("/file.md");
|
|
47
37
|
expect(result).toBe("/home/testuser/.readit/comments/file.comments.md");
|
|
@@ -103,7 +93,6 @@ describe("getLineNumber", () => {
|
|
|
103
93
|
});
|
|
104
94
|
|
|
105
95
|
it("handles many lines", () => {
|
|
106
|
-
// "line\n" repeated 100 times, each segment is 5 chars
|
|
107
96
|
const content = Array(100).fill("line").join("\n");
|
|
108
97
|
expect(getLineNumber(content, 0)).toBe(1); // Start of line 1
|
|
109
98
|
expect(getLineNumber(content, 250)).toBe(51); // After 50 newlines = line 51
|
|
@@ -169,7 +158,6 @@ This is my comment.
|
|
|
169
158
|
expect(result.comments[0].selectedText).toBe("selected text here");
|
|
170
159
|
expect(result.comments[0].comment).toBe("This is my comment.");
|
|
171
160
|
expect(result.comments[0].lineHint).toBe("L42");
|
|
172
|
-
expect(result.comments[0].createdAt).toBe("2024-12-24T10:30:00Z");
|
|
173
161
|
});
|
|
174
162
|
|
|
175
163
|
it("parses multiple comments", () => {
|
|
@@ -339,7 +327,6 @@ describe("serializeComments", () => {
|
|
|
339
327
|
id: "12345678",
|
|
340
328
|
selectedText: "selected text",
|
|
341
329
|
comment: "My comment",
|
|
342
|
-
createdAt: "2024-12-24T10:30:00Z",
|
|
343
330
|
lineHint: "L42",
|
|
344
331
|
startOffset: 100,
|
|
345
332
|
endOffset: 113,
|
|
@@ -347,7 +334,7 @@ describe("serializeComments", () => {
|
|
|
347
334
|
],
|
|
348
335
|
};
|
|
349
336
|
const result = serializeComments(file);
|
|
350
|
-
expect(result).toContain("<!-- c:12345678|L42
|
|
337
|
+
expect(result).toContain("<!-- c:12345678|L42 -->");
|
|
351
338
|
expect(result).toContain("> selected text");
|
|
352
339
|
expect(result).toContain("My comment");
|
|
353
340
|
});
|
|
@@ -362,7 +349,6 @@ describe("serializeComments", () => {
|
|
|
362
349
|
id: "12345678",
|
|
363
350
|
selectedText: "line one\nline two",
|
|
364
351
|
comment: "Comment",
|
|
365
|
-
createdAt: "2024-12-24T10:30:00Z",
|
|
366
352
|
lineHint: "L42-L43",
|
|
367
353
|
startOffset: 100,
|
|
368
354
|
endOffset: 120,
|
|
@@ -384,7 +370,6 @@ describe("serializeComments", () => {
|
|
|
384
370
|
id: "12345678",
|
|
385
371
|
selectedText: "truncated text...",
|
|
386
372
|
comment: "Comment",
|
|
387
|
-
createdAt: "2024-12-24T10:30:00Z",
|
|
388
373
|
lineHint: "L42",
|
|
389
374
|
startOffset: 100,
|
|
390
375
|
endOffset: 500,
|
|
@@ -408,7 +393,6 @@ describe("serializeComments", () => {
|
|
|
408
393
|
id: "12345678",
|
|
409
394
|
selectedText: "short text",
|
|
410
395
|
comment: "Comment",
|
|
411
|
-
createdAt: "2024-12-24T10:30:00Z",
|
|
412
396
|
lineHint: "L42",
|
|
413
397
|
startOffset: 100,
|
|
414
398
|
endOffset: 110,
|
|
@@ -429,7 +413,6 @@ describe("serializeComments", () => {
|
|
|
429
413
|
id: "12345678",
|
|
430
414
|
selectedText: "selected text",
|
|
431
415
|
comment: "My comment",
|
|
432
|
-
createdAt: "2024-12-24T10:30:00Z",
|
|
433
416
|
lineHint: "L42",
|
|
434
417
|
startOffset: 100,
|
|
435
418
|
endOffset: 113,
|
|
@@ -438,7 +421,6 @@ describe("serializeComments", () => {
|
|
|
438
421
|
id: "87654321",
|
|
439
422
|
selectedText: "another\nmultiline\nselection",
|
|
440
423
|
comment: "Another comment with\n\nmultiple paragraphs.",
|
|
441
|
-
createdAt: "2024-12-24T11:00:00Z",
|
|
442
424
|
lineHint: "L50-L52",
|
|
443
425
|
startOffset: 200,
|
|
444
426
|
endOffset: 230,
|
|
@@ -460,10 +442,42 @@ describe("serializeComments", () => {
|
|
|
460
442
|
original.comments[i].selectedText,
|
|
461
443
|
);
|
|
462
444
|
expect(parsed.comments[i].lineHint).toBe(original.comments[i].lineHint);
|
|
463
|
-
expect(parsed.comments[i].createdAt).toBe(original.comments[i].createdAt);
|
|
464
445
|
}
|
|
465
446
|
});
|
|
466
447
|
|
|
448
|
+
it("roundtrip: survives a markdown horizontal rule inside a comment body", () => {
|
|
449
|
+
const original: CommentFile = {
|
|
450
|
+
source: "/test.md",
|
|
451
|
+
hash: "abc123",
|
|
452
|
+
version: 1,
|
|
453
|
+
comments: [
|
|
454
|
+
{
|
|
455
|
+
id: "aaaaaaaa",
|
|
456
|
+
selectedText: "first selected",
|
|
457
|
+
comment: "before\n\n---\n\nafter",
|
|
458
|
+
lineHint: "L1",
|
|
459
|
+
startOffset: 0,
|
|
460
|
+
endOffset: 14,
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
id: "bbbbbbbb",
|
|
464
|
+
selectedText: "second selected",
|
|
465
|
+
comment: "second body",
|
|
466
|
+
lineHint: "L5",
|
|
467
|
+
startOffset: 50,
|
|
468
|
+
endOffset: 65,
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const serialized = serializeComments(original);
|
|
474
|
+
const parsed = parseCommentFile(serialized);
|
|
475
|
+
|
|
476
|
+
expect(parsed.comments).toHaveLength(2);
|
|
477
|
+
expect(parsed.comments[0].id).toBe("aaaaaaaa");
|
|
478
|
+
expect(parsed.comments[1].id).toBe("bbbbbbbb");
|
|
479
|
+
});
|
|
480
|
+
|
|
467
481
|
it("roundtrip: preserves anchorPrefix through serialize/parse", () => {
|
|
468
482
|
const original: CommentFile = {
|
|
469
483
|
source: "/test.md",
|
|
@@ -474,7 +488,6 @@ describe("serializeComments", () => {
|
|
|
474
488
|
id: "12345678",
|
|
475
489
|
selectedText: "truncated text\n...\nend of text",
|
|
476
490
|
comment: "Comment on long selection",
|
|
477
|
-
createdAt: "2024-12-24T10:30:00Z",
|
|
478
491
|
lineHint: "L42",
|
|
479
492
|
startOffset: 100,
|
|
480
493
|
endOffset: 2000,
|
|
@@ -514,11 +527,6 @@ describe("createComment", () => {
|
|
|
514
527
|
expect(comment.lineHint).toBe("L2");
|
|
515
528
|
});
|
|
516
529
|
|
|
517
|
-
it("generates ISO timestamp", () => {
|
|
518
|
-
const comment = createComment("text", "comment", 0, 4, "text");
|
|
519
|
-
expect(comment.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
|
520
|
-
});
|
|
521
|
-
|
|
522
530
|
it("truncates very long selections", () => {
|
|
523
531
|
const longText = "a".repeat(2000);
|
|
524
532
|
const comment = createComment(longText, "comment", 0, 2000, longText);
|
|
@@ -7,7 +7,7 @@ const FORMAT_VERSION = 1;
|
|
|
7
7
|
const HASH_LENGTH = 16;
|
|
8
8
|
const MAX_SELECTION_LENGTH = 1000;
|
|
9
9
|
const TRUNCATION_MARKER = "\n...\n";
|
|
10
|
-
const ANCHOR_PREFIX_LENGTH = 200;
|
|
10
|
+
const ANCHOR_PREFIX_LENGTH = 200;
|
|
11
11
|
|
|
12
12
|
export function truncateSelection(text: string): string {
|
|
13
13
|
if (text.length <= MAX_SELECTION_LENGTH) {
|
|
@@ -19,13 +19,8 @@ export function truncateSelection(text: string): string {
|
|
|
19
19
|
return text.slice(0, half) + TRUNCATION_MARKER + text.slice(-half);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* Compute the path where comments for a source file should be stored.
|
|
24
|
-
* Comments are stored in ~/.readit/comments/{absolute-path-structure}/{filename}.comments.md
|
|
25
|
-
*/
|
|
26
22
|
export function getCommentPath(sourcePath: string): string {
|
|
27
23
|
const absolute = path.resolve(sourcePath);
|
|
28
|
-
// Remove leading slash and drive letter (Windows)
|
|
29
24
|
const normalized = absolute.replace(/^\//, "").replace(/^[A-Z]:[\\/]/, "");
|
|
30
25
|
const ext = path.extname(normalized);
|
|
31
26
|
const withoutExt = normalized.slice(0, -ext.length || undefined);
|
|
@@ -94,9 +89,22 @@ export function parseCommentFile(content: string): CommentFile {
|
|
|
94
89
|
}
|
|
95
90
|
|
|
96
91
|
const bodyContent = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
|
|
97
|
-
const blocks = bodyContent.split(/\n---\n/).filter((block) => block.trim());
|
|
98
92
|
|
|
99
|
-
|
|
93
|
+
const markerRe = /<!--\s*c:[^|]+\|[^|>\s]+(?:\|[^>]*)?\s*-->/g;
|
|
94
|
+
const markerStarts: number[] = [];
|
|
95
|
+
for (const m of bodyContent.matchAll(markerRe)) {
|
|
96
|
+
if (m.index !== undefined) markerStarts.push(m.index);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < markerStarts.length; i++) {
|
|
100
|
+
const start = markerStarts[i];
|
|
101
|
+
const end =
|
|
102
|
+
i + 1 < markerStarts.length ? markerStarts[i + 1] : bodyContent.length;
|
|
103
|
+
const block = bodyContent
|
|
104
|
+
.slice(start, end)
|
|
105
|
+
.replace(/\n+---\s*$/, "")
|
|
106
|
+
.trim();
|
|
107
|
+
if (!block) continue;
|
|
100
108
|
const comment = parseCommentBlock(block);
|
|
101
109
|
if (comment) {
|
|
102
110
|
result.comments.push(comment);
|
|
@@ -107,15 +115,15 @@ export function parseCommentFile(content: string): CommentFile {
|
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
function parseCommentBlock(block: string): Comment | undefined {
|
|
110
|
-
|
|
111
|
-
|
|
118
|
+
const metadataMatch = block.match(
|
|
119
|
+
/<!--\s*c:([^|]+)\|([^|>\s]+)(?:\|[^>]*)?\s*-->/,
|
|
120
|
+
);
|
|
112
121
|
if (!metadataMatch) {
|
|
113
122
|
return undefined;
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
const [, id, lineHint
|
|
125
|
+
const [, id, lineHint] = metadataMatch;
|
|
117
126
|
|
|
118
|
-
// Extract anchor prefix if present: <!-- anchor:{prefix} -->
|
|
119
127
|
const anchorMatch = block.match(/<!--\s*anchor:(.+?)\s*-->/);
|
|
120
128
|
const anchorPrefix = anchorMatch ? anchorMatch[1] : undefined;
|
|
121
129
|
|
|
@@ -138,10 +146,8 @@ function parseCommentBlock(block: string): Comment | undefined {
|
|
|
138
146
|
id,
|
|
139
147
|
selectedText,
|
|
140
148
|
comment: commentBody,
|
|
141
|
-
createdAt: createdAt.trim(),
|
|
142
149
|
lineHint,
|
|
143
150
|
anchorPrefix,
|
|
144
|
-
// Offsets will be resolved by anchor matching when loading
|
|
145
151
|
startOffset: 0,
|
|
146
152
|
endOffset: 0,
|
|
147
153
|
};
|
|
@@ -171,7 +177,7 @@ function serializeComment(comment: Comment): string {
|
|
|
171
177
|
const lines: string[] = [];
|
|
172
178
|
|
|
173
179
|
const lineHint = comment.lineHint || "L0";
|
|
174
|
-
lines.push(`<!-- c:${comment.id}|${lineHint}
|
|
180
|
+
lines.push(`<!-- c:${comment.id}|${lineHint} -->`);
|
|
175
181
|
|
|
176
182
|
if (comment.anchorPrefix) {
|
|
177
183
|
lines.push(`<!-- anchor:${comment.anchorPrefix} -->`);
|
|
@@ -199,8 +205,6 @@ export function createComment(
|
|
|
199
205
|
): Comment {
|
|
200
206
|
const id = crypto.randomUUID().slice(0, 8);
|
|
201
207
|
const lineHint = getLineHint(sourceContent, startOffset, endOffset);
|
|
202
|
-
const now = new Date();
|
|
203
|
-
const createdAt = now.toISOString();
|
|
204
208
|
|
|
205
209
|
const needsTruncation = selectedText.length > MAX_SELECTION_LENGTH;
|
|
206
210
|
|
|
@@ -208,7 +212,6 @@ export function createComment(
|
|
|
208
212
|
id,
|
|
209
213
|
selectedText: truncateSelection(selectedText),
|
|
210
214
|
comment: commentText,
|
|
211
|
-
createdAt,
|
|
212
215
|
startOffset,
|
|
213
216
|
endOffset,
|
|
214
217
|
lineHint,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { bench, describe } from "vitest";
|
|
2
|
+
import { COMMENTS_10, COMMENTS_50 } from "./__fixtures__/bench-data";
|
|
3
|
+
import { formatComment, generatePrompt } from "./export";
|
|
4
|
+
|
|
5
|
+
describe("formatComment", () => {
|
|
6
|
+
const comment = COMMENTS_10[0];
|
|
7
|
+
|
|
8
|
+
bench("single comment", () => {
|
|
9
|
+
formatComment(comment);
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("generatePrompt", () => {
|
|
14
|
+
bench("10 comments", () => {
|
|
15
|
+
generatePrompt(COMMENTS_10, "benchmark.md");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
bench("50 comments", () => {
|
|
19
|
+
generatePrompt(COMMENTS_50, "benchmark.md");
|
|
20
|
+
});
|
|
21
|
+
});
|
package/src/lib/export.ts
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import { renderHook } from "@testing-library/react";
|
|
2
1
|
import { describe, expect, it } from "vitest";
|
|
3
|
-
import {
|
|
2
|
+
import { parseMarkdownHeadings } from "./headings";
|
|
4
3
|
|
|
5
|
-
describe("
|
|
4
|
+
describe("parseMarkdownHeadings", () => {
|
|
6
5
|
it("extracts basic headings", () => {
|
|
7
6
|
const content = `# Heading 1
|
|
8
7
|
## Heading 2
|
|
9
8
|
### Heading 3`;
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
expect(result.current).toEqual([
|
|
10
|
+
expect(parseMarkdownHeadings(content)).toEqual([
|
|
14
11
|
{ id: "heading-1", text: "Heading 1", level: 1 },
|
|
15
12
|
{ id: "heading-2", text: "Heading 2", level: 2 },
|
|
16
13
|
{ id: "heading-3", text: "Heading 3", level: 3 },
|
|
@@ -22,9 +19,7 @@ describe("useHeadings", () => {
|
|
|
22
19
|
## Section
|
|
23
20
|
## Section`;
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
expect(result.current).toEqual([
|
|
22
|
+
expect(parseMarkdownHeadings(content)).toEqual([
|
|
28
23
|
{ id: "section", text: "Section", level: 2 },
|
|
29
24
|
{ id: "section-1", text: "Section", level: 2 },
|
|
30
25
|
{ id: "section-2", text: "Section", level: 2 },
|
|
@@ -41,9 +36,7 @@ echo "hello"
|
|
|
41
36
|
|
|
42
37
|
## Another Real Heading`;
|
|
43
38
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
expect(result.current).toEqual([
|
|
39
|
+
expect(parseMarkdownHeadings(content)).toEqual([
|
|
47
40
|
{ id: "real-heading", text: "Real Heading", level: 1 },
|
|
48
41
|
{ id: "another-real-heading", text: "Another Real Heading", level: 2 },
|
|
49
42
|
]);
|
|
@@ -60,9 +53,7 @@ def foo():
|
|
|
60
53
|
|
|
61
54
|
## Another Real Heading`;
|
|
62
55
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
expect(result.current).toEqual([
|
|
56
|
+
expect(parseMarkdownHeadings(content)).toEqual([
|
|
66
57
|
{ id: "real-heading", text: "Real Heading", level: 1 },
|
|
67
58
|
{ id: "another-real-heading", text: "Another Real Heading", level: 2 },
|
|
68
59
|
]);
|
|
@@ -83,9 +74,7 @@ def foo():
|
|
|
83
74
|
|
|
84
75
|
## Results`;
|
|
85
76
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
expect(result.current).toEqual([
|
|
77
|
+
expect(parseMarkdownHeadings(content)).toEqual([
|
|
89
78
|
{ id: "introduction", text: "Introduction", level: 1 },
|
|
90
79
|
{ id: "methods", text: "Methods", level: 2 },
|
|
91
80
|
{ id: "results", text: "Results", level: 2 },
|
|
@@ -102,16 +91,13 @@ npx readit document.md --port 3000
|
|
|
102
91
|
|
|
103
92
|
## Usage`;
|
|
104
93
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
expect(result.current).toEqual([
|
|
94
|
+
expect(parseMarkdownHeadings(content)).toEqual([
|
|
108
95
|
{ id: "setup", text: "Setup", level: 1 },
|
|
109
96
|
{ id: "usage", text: "Usage", level: 2 },
|
|
110
97
|
]);
|
|
111
98
|
});
|
|
112
99
|
|
|
113
|
-
it("returns empty array for
|
|
114
|
-
|
|
115
|
-
expect(result.current).toEqual([]);
|
|
100
|
+
it("returns empty array for empty content", () => {
|
|
101
|
+
expect(parseMarkdownHeadings("")).toEqual([]);
|
|
116
102
|
});
|
|
117
103
|
});
|
|
@@ -1,21 +1,26 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
2
|
-
import { slugify } from "../lib/utils";
|
|
3
|
-
|
|
4
1
|
export interface Heading {
|
|
5
2
|
id: string;
|
|
6
3
|
text: string;
|
|
7
4
|
level: 1 | 2 | 3 | 4 | 5 | 6;
|
|
8
5
|
}
|
|
9
6
|
|
|
10
|
-
function
|
|
7
|
+
export function slugify(text: string): string {
|
|
8
|
+
return text
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.trim()
|
|
11
|
+
.replace(/[^\w\s-]/g, "")
|
|
12
|
+
.replace(/\s+/g, "-")
|
|
13
|
+
.replace(/-+/g, "-");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function stripCodeBlocks(content: string): string {
|
|
11
17
|
let result = content.replace(/^(`{3,}|~{3,}).*$[\s\S]*?^\1\s*$/gm, "");
|
|
12
|
-
// Only remove indented blocks preceded by a blank line (avoids removing list items)
|
|
13
18
|
result = result.replace(/(?:^|\n\n)((?:(?:[ ]{4}|\t).+\n?)+)/g, "\n\n");
|
|
14
19
|
|
|
15
20
|
return result;
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
function parseMarkdownHeadings(content: string): Heading[] {
|
|
23
|
+
export function parseMarkdownHeadings(content: string): Heading[] {
|
|
19
24
|
const headings: Heading[] = [];
|
|
20
25
|
const seenIds = new Map<string, number>();
|
|
21
26
|
const contentWithoutCode = stripCodeBlocks(content);
|
|
@@ -37,10 +42,3 @@ function parseMarkdownHeadings(content: string): Heading[] {
|
|
|
37
42
|
|
|
38
43
|
return headings;
|
|
39
44
|
}
|
|
40
|
-
|
|
41
|
-
export function useHeadings(content: string | null): Heading[] {
|
|
42
|
-
return useMemo(() => {
|
|
43
|
-
if (!content) return [];
|
|
44
|
-
return parseMarkdownHeadings(content);
|
|
45
|
-
}, [content]);
|
|
46
|
-
}
|
|
@@ -31,18 +31,14 @@ describe("findTextPosition", () => {
|
|
|
31
31
|
describe("multiple occurrences", () => {
|
|
32
32
|
it("finds closest occurrence to hint (before)", () => {
|
|
33
33
|
const text = "the cat and the dog and the bird";
|
|
34
|
-
// "the" occurs at: 0, 12, 24
|
|
35
34
|
|
|
36
|
-
// Hint at 10 should find "the" at 12 (closest)
|
|
37
35
|
const result = findTextPosition(text, "the", 10);
|
|
38
36
|
expect(result?.start).toBe(12);
|
|
39
37
|
});
|
|
40
38
|
|
|
41
39
|
it("finds closest occurrence to hint (after)", () => {
|
|
42
40
|
const text = "the cat and the dog and the bird";
|
|
43
|
-
// "the" occurs at: 0, 12, 24
|
|
44
41
|
|
|
45
|
-
// Hint at 30 should find "the" at 24 (closest)
|
|
46
42
|
const result = findTextPosition(text, "the", 30);
|
|
47
43
|
expect(result?.start).toBe(24);
|
|
48
44
|
});
|
|
@@ -61,7 +57,6 @@ describe("findTextPosition", () => {
|
|
|
61
57
|
|
|
62
58
|
it("handles exact match at hint position", () => {
|
|
63
59
|
const text = "abc abc abc";
|
|
64
|
-
// "abc" occurs at: 0, 4, 8
|
|
65
60
|
const result = findTextPosition(text, "abc", 4);
|
|
66
61
|
expect(result?.start).toBe(4);
|
|
67
62
|
});
|