@peaske7/readit 0.2.0 → 0.3.0-rc.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/.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 +152 -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 +890 -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 +233 -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/MermaidEnhancer.svelte +218 -0
- package/src/components/MermaidModal.svelte +67 -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 +141 -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 +103 -33
- package/src/lib/comment-storage.ts +25 -18
- package/src/lib/export.bench.ts +21 -0
- package/src/lib/export.ts +0 -1
- package/src/lib/fetch-or-throw.test.ts +59 -0
- package/src/lib/fetch-or-throw.ts +12 -0
- 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 +34 -0
- package/src/lib/i18n/ja.ts +34 -0
- package/src/lib/i18n/types.ts +33 -0
- package/src/lib/key-lock.test.ts +104 -0
- package/src/lib/key-lock.ts +23 -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 +178 -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 -95
- 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
package/bunfig.toml
ADDED
package/docs/perf-baseline.md
CHANGED
|
@@ -55,7 +55,62 @@ Build: production (`bun run build`)
|
|
|
55
55
|
|
|
56
56
|
> Run with: `bun run bench`
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
### Anchor Resolution
|
|
59
|
+
|
|
60
|
+
| Benchmark | ops/sec | Mean | p99 |
|
|
61
|
+
|-----------|---------|------|-----|
|
|
62
|
+
| findAnchor — exact, medium doc | 340,789 | 0.003ms | 0.006ms |
|
|
63
|
+
| findAnchor — exact, large doc | 348,679 | 0.003ms | 0.006ms |
|
|
64
|
+
| findAnchorNormalized — large doc | 10,414 | 0.096ms | 0.406ms |
|
|
65
|
+
| findAnchorFuzzy — mutated text, large doc | 163 | **6.13ms** | 6.41ms |
|
|
66
|
+
| findAnchorWithFallback — 1 comment | 101 | **9.86ms** | 10.27ms |
|
|
67
|
+
| findAnchorWithFallback — 10 comments | 39 | **25.66ms** | 26.10ms |
|
|
68
|
+
| findAnchorWithFallback — 50 comments | 3 | **313.30ms** | 640.60ms |
|
|
69
|
+
|
|
70
|
+
### Comment Storage
|
|
71
|
+
|
|
72
|
+
| Benchmark | ops/sec | Mean | p99 |
|
|
73
|
+
|-----------|---------|------|-----|
|
|
74
|
+
| parseCommentFile — 1 comment | 881,595 | 0.001ms | 0.003ms |
|
|
75
|
+
| parseCommentFile — 10 comments | 165,610 | 0.006ms | 0.009ms |
|
|
76
|
+
| parseCommentFile — 50 comments | 34,588 | 0.029ms | 0.045ms |
|
|
77
|
+
| serializeComments — 1 comment | 2,011,903 | 0.0005ms | 0.001ms |
|
|
78
|
+
| serializeComments — 10 comments | 286,303 | 0.004ms | 0.005ms |
|
|
79
|
+
| serializeComments — 50 comments | 59,027 | 0.017ms | 0.029ms |
|
|
80
|
+
| computeHash — 300 lines | 169,082 | 0.006ms | 0.009ms |
|
|
81
|
+
|
|
82
|
+
### Margin Layout
|
|
83
|
+
|
|
84
|
+
| Benchmark | ops/sec | Mean | p99 |
|
|
85
|
+
|-----------|---------|------|-----|
|
|
86
|
+
| well-spaced — 1 comment | 6,192,537 | 0.0002ms | 0.0002ms |
|
|
87
|
+
| well-spaced — 10 comments | 942,048 | 0.001ms | 0.002ms |
|
|
88
|
+
| well-spaced — 50 comments | 167,170 | 0.006ms | 0.009ms |
|
|
89
|
+
| clustered — 10 comments | 932,383 | 0.001ms | 0.002ms |
|
|
90
|
+
| clustered — 50 comments | 165,792 | 0.006ms | 0.009ms |
|
|
91
|
+
| with input zone — 50 comments | 143,787 | 0.007ms | 0.010ms |
|
|
92
|
+
|
|
93
|
+
### Export
|
|
94
|
+
|
|
95
|
+
| Benchmark | ops/sec | Mean |
|
|
96
|
+
|-----------|---------|------|
|
|
97
|
+
| formatComment — single | 17,270,891 | 0.00006ms |
|
|
98
|
+
| generatePrompt — 10 comments | 927,600 | 0.001ms |
|
|
99
|
+
| generatePrompt — 50 comments | 189,187 | 0.005ms |
|
|
100
|
+
|
|
101
|
+
### Highlight System (DOM Operations) — *the optimization target*
|
|
102
|
+
|
|
103
|
+
| Benchmark | ops/sec | Mean | p99 |
|
|
104
|
+
|-----------|---------|------|-----|
|
|
105
|
+
| findTextPosition — 1 comment | 61,802 | 0.016ms | 0.033ms |
|
|
106
|
+
| findTextPosition — 10 comments | 17,713 | 0.057ms | 0.116ms |
|
|
107
|
+
| findTextPosition — 50 comments | 4,555 | 0.220ms | 0.634ms |
|
|
108
|
+
| getDOMTextContent — medium doc | 40,004 | 0.025ms | 0.055ms |
|
|
109
|
+
| getDOMTextContent — large doc | 17,498 | 0.057ms | 0.222ms |
|
|
110
|
+
| collectTextNodes — medium doc | 41,437 | 0.024ms | 0.070ms |
|
|
111
|
+
| collectTextNodes — large doc | 21,645 | 0.046ms | 0.113ms |
|
|
112
|
+
| **applyBatch + clear — 10 highlights, medium** | **2,308** | **0.433ms** | **2.156ms** |
|
|
113
|
+
| **applyBatch + clear — 50 highlights, large** | **660** | **1.515ms** | **5.350ms** |
|
|
59
114
|
|
|
60
115
|
## Key Bottlenecks (from analysis)
|
|
61
116
|
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Go Server Rewrite Design Spec
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
Rewrite readit's server and CLI from Bun/TypeScript to Go. The Svelte 5 frontend stays unchanged. Go handles all heavy computation (markdown rendering, syntax highlighting, comment storage, file watching, SSE) while Svelte handles client-side interactivity (highlights, margin notes, comment CRUD UI).
|
|
6
|
+
|
|
7
|
+
## Motivation
|
|
8
|
+
|
|
9
|
+
The current Bun server's cold-start path is dominated by Shiki WASM initialization (80-200ms) and the JSDOM mermaid worker (2-5s). A compiled Go binary with native libraries eliminates both bottlenecks:
|
|
10
|
+
|
|
11
|
+
- Process startup: 30-50ms (Bun) → <1ms (Go binary)
|
|
12
|
+
- Syntax highlighting init: 80-200ms (Shiki WASM) → 0ms (chroma, compiled in)
|
|
13
|
+
- Markdown render (3000 lines): 5-20ms (markdown-it) → <1ms (goldmark)
|
|
14
|
+
- Single binary distribution, no node_modules runtime dependency
|
|
15
|
+
|
|
16
|
+
Target: 50-100x improvement on server-side TTFB.
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
readit/
|
|
22
|
+
├── go/
|
|
23
|
+
│ ├── cmd/readit/main.go # CLI entry point
|
|
24
|
+
│ ├── internal/server/
|
|
25
|
+
│ │ ├── server.go # Mux setup, static serving, dev proxy
|
|
26
|
+
│ │ ├── documents.go # Document routes + file state
|
|
27
|
+
│ │ ├── comments.go # Comment CRUD routes
|
|
28
|
+
│ │ ├── settings.go # Settings routes
|
|
29
|
+
│ │ ├── sse.go # SSE broker, heartbeat, shutdown timer
|
|
30
|
+
│ │ ├── markdown.go # goldmark + chroma rendering
|
|
31
|
+
│ │ ├── headings.go # AST-based heading extraction
|
|
32
|
+
│ │ ├── storage.go # .comments.md parse/serialize
|
|
33
|
+
│ │ ├── anchor.go # Anchor resolution + fuzzy matching
|
|
34
|
+
│ │ ├── watcher.go # fsnotify file watching + debounce
|
|
35
|
+
│ │ ├── template.go # HTML page template
|
|
36
|
+
│ │ ├── types.go # Shared types
|
|
37
|
+
│ │ └── embed.go # go:embed dist/ assets
|
|
38
|
+
│ ├── go.mod
|
|
39
|
+
│ └── go.sum
|
|
40
|
+
├── src/ # Svelte frontend (unchanged)
|
|
41
|
+
├── dist/ # Vite build output (Go embeds this)
|
|
42
|
+
├── Makefile
|
|
43
|
+
├── vite.config.ts
|
|
44
|
+
└── package.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Single Go package (`internal/server`) with flat files. `cmd/readit/main.go` calls `server.Start(opts)`. No nested packages, no interface indirection.
|
|
48
|
+
|
|
49
|
+
## Decisions
|
|
50
|
+
|
|
51
|
+
| Decision | Choice | Rationale |
|
|
52
|
+
|----------|--------|-----------|
|
|
53
|
+
| Language | Go | goldmark+chroma ecosystem, fast dev velocity, single binary |
|
|
54
|
+
| Repo structure | Monorepo (`go/` + `src/`) | Shared build pipeline, colocated frontend |
|
|
55
|
+
| Asset serving | `go:embed` + `--assets-dir` override | Single binary for production, filesystem for dev |
|
|
56
|
+
| Mermaid | Client-only with `<link rel="modulepreload">` | Eliminates JSDOM complexity, door open for server-side later |
|
|
57
|
+
| Dev workflow | `make dev` — Go manages Vite child process | Single command, Go proxies to Vite for HMR |
|
|
58
|
+
| Comment format | Keep `.comments.md` unchanged | Backward compatible, simple to parse in Go |
|
|
59
|
+
| File support | Markdown only | Tight scope for v1 |
|
|
60
|
+
| HTTP router | `net/http.ServeMux` (Go 1.22+) | Method+pattern routing, no framework dependency |
|
|
61
|
+
| CLI parsing | `flag` package | Simple subcommands, no cobra overhead |
|
|
62
|
+
|
|
63
|
+
## Server Core
|
|
64
|
+
|
|
65
|
+
The `Server` struct holds all shared state:
|
|
66
|
+
|
|
67
|
+
```go
|
|
68
|
+
type Server struct {
|
|
69
|
+
mux *http.ServeMux
|
|
70
|
+
files map[string]*FileState
|
|
71
|
+
fileOrder []string
|
|
72
|
+
sse *SSEBroker
|
|
73
|
+
watcher *Watcher
|
|
74
|
+
renderer *Renderer
|
|
75
|
+
settings Settings
|
|
76
|
+
workingDir string
|
|
77
|
+
clean bool
|
|
78
|
+
assetsFS fs.FS
|
|
79
|
+
template *template.Template
|
|
80
|
+
mu sync.RWMutex
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Routes registered as methods on `Server` — no handler interfaces, no middleware chain. Dev mode proxies non-API requests to Vite at `localhost:24678`.
|
|
85
|
+
|
|
86
|
+
## API Contract
|
|
87
|
+
|
|
88
|
+
The Go server implements the exact same API the Svelte frontend consumes. No changes to request/response shapes.
|
|
89
|
+
|
|
90
|
+
### Document Routes (`documents.go`)
|
|
91
|
+
|
|
92
|
+
| Method | Path | Purpose |
|
|
93
|
+
|--------|------|---------|
|
|
94
|
+
| GET | `/api/documents` | List open files |
|
|
95
|
+
| POST | `/api/documents` | Add file to session |
|
|
96
|
+
| GET | `/api/document?path=` | Get rendered HTML + headings |
|
|
97
|
+
|
|
98
|
+
### Comment Routes (`comments.go`)
|
|
99
|
+
|
|
100
|
+
| Method | Path | Purpose |
|
|
101
|
+
|--------|------|---------|
|
|
102
|
+
| GET | `/api/comments?path=` | List comments (with anchor resolution) |
|
|
103
|
+
| POST | `/api/comments?path=` | Create comment |
|
|
104
|
+
| PUT | `/api/comments/{id}?path=` | Update comment text |
|
|
105
|
+
| DELETE | `/api/comments/{id}?path=` | Delete comment |
|
|
106
|
+
| DELETE | `/api/comments?path=` | Delete all comments |
|
|
107
|
+
| PUT | `/api/comments/{id}/reanchor?path=` | Reanchor comment |
|
|
108
|
+
| GET | `/api/comments/raw?path=` | Raw .comments.md content |
|
|
109
|
+
|
|
110
|
+
### Settings Routes (`settings.go`)
|
|
111
|
+
|
|
112
|
+
| Method | Path | Purpose |
|
|
113
|
+
|--------|------|---------|
|
|
114
|
+
| GET | `/api/settings` | Read settings |
|
|
115
|
+
| PUT | `/api/settings` | Update font family |
|
|
116
|
+
|
|
117
|
+
### SSE Endpoints (`sse.go`)
|
|
118
|
+
|
|
119
|
+
| Method | Path | Purpose |
|
|
120
|
+
|--------|------|---------|
|
|
121
|
+
| GET | `/api/document/stream` | Document change events |
|
|
122
|
+
| GET | `/api/heartbeat` | Keep-alive, manages auto-shutdown |
|
|
123
|
+
|
|
124
|
+
### Other
|
|
125
|
+
|
|
126
|
+
| Method | Path | Purpose |
|
|
127
|
+
|--------|------|---------|
|
|
128
|
+
| GET | `/api/health` | Health check (`{"status":"ok"}`) |
|
|
129
|
+
| GET | `/` | SSR page with inline data |
|
|
130
|
+
| GET | `/assets/*` | Static assets (embedded or filesystem) |
|
|
131
|
+
|
|
132
|
+
### Inline Data Shape
|
|
133
|
+
|
|
134
|
+
The root page embeds JSON in `<script type="application/json" id="__readit">`:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"files": [{"path": "...", "fileName": "..."}],
|
|
139
|
+
"activeFile": "...",
|
|
140
|
+
"clean": false,
|
|
141
|
+
"workingDirectory": "...",
|
|
142
|
+
"documents": {
|
|
143
|
+
"/path/to/file.md": {
|
|
144
|
+
"html": "...",
|
|
145
|
+
"headings": [{"id": "...", "text": "...", "level": 1}],
|
|
146
|
+
"comments": [...]
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"settings": {"version": 1, "fontFamily": "serif"}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Markdown Pipeline (`markdown.go` + `headings.go`)
|
|
154
|
+
|
|
155
|
+
goldmark with extensions, configured once at startup:
|
|
156
|
+
|
|
157
|
+
- **GFM**: tables, strikethrough, autolinks, task lists
|
|
158
|
+
- **Chroma highlighting**: `onedark` style, CSS classes (not inline styles)
|
|
159
|
+
- **Auto heading IDs**: generated from heading text
|
|
160
|
+
- **Unsafe HTML**: raw HTML passthrough (matches current behavior)
|
|
161
|
+
|
|
162
|
+
Heading extraction walks the goldmark AST directly instead of regex.
|
|
163
|
+
|
|
164
|
+
Mermaid fenced code blocks pass through as `<pre><code class="language-mermaid">`. The Svelte frontend's `DocumentViewer.svelte` hydrates these client-side via lazy `import("mermaid")`. A `<link rel="modulepreload">` hint in the template accelerates the mermaid chunk download.
|
|
165
|
+
|
|
166
|
+
## Comment Storage (`storage.go` + `anchor.go`)
|
|
167
|
+
|
|
168
|
+
Parses and serializes the existing `.comments.md` format unchanged:
|
|
169
|
+
|
|
170
|
+
- Storage path: `~/.readit/comments/<mirrored-path>.comments.md`
|
|
171
|
+
- Format: YAML frontmatter (`source`, `hash`, `version`) + comment blocks separated by `---`
|
|
172
|
+
- Atomic writes: temp file + `os.Rename`
|
|
173
|
+
- Hash: SHA-256 of source content, truncated to 16 hex chars
|
|
174
|
+
|
|
175
|
+
Anchor resolution algorithm (direct port):
|
|
176
|
+
|
|
177
|
+
1. Exact match near `lineHint` position
|
|
178
|
+
2. Exact match anywhere in source
|
|
179
|
+
3. Normalized match (collapse whitespace)
|
|
180
|
+
4. Mark as `unresolved`
|
|
181
|
+
|
|
182
|
+
Two-key cache: comment file mtime + source content hash. Skip re-parsing when neither changed.
|
|
183
|
+
|
|
184
|
+
## SSE & File Watching (`sse.go` + `watcher.go`)
|
|
185
|
+
|
|
186
|
+
SSE broker manages two client sets:
|
|
187
|
+
|
|
188
|
+
- **Document stream clients**: receive `document-updated` and `document-added` events
|
|
189
|
+
- **Heartbeat clients**: keep-alive pings, manage auto-shutdown timer (1.5s grace after last client disconnects, production only)
|
|
190
|
+
|
|
191
|
+
File watcher uses `fsnotify` with 100ms debounce per file. On change: invalidate render cache → invalidate comment cache → broadcast SSE event.
|
|
192
|
+
|
|
193
|
+
## CLI (`cmd/readit/main.go`)
|
|
194
|
+
|
|
195
|
+
Subcommands:
|
|
196
|
+
|
|
197
|
+
- `readit <file.md> [flags]` — start server + open browser
|
|
198
|
+
- `readit list` — list files with comments (stdout)
|
|
199
|
+
- `readit show <file.md>` — print comments for file (stdout)
|
|
200
|
+
- `readit open <file.md>` — attach to running server or start new one
|
|
201
|
+
|
|
202
|
+
Flags:
|
|
203
|
+
|
|
204
|
+
- `--port` (default: random available)
|
|
205
|
+
- `--host` (default: `127.0.0.1`)
|
|
206
|
+
- `--no-open` (skip browser launch)
|
|
207
|
+
- `--clean` (clear existing comments)
|
|
208
|
+
- `--assets-dir` (override embedded assets)
|
|
209
|
+
- `--dev` (spawn Vite child process, proxy to it)
|
|
210
|
+
|
|
211
|
+
Server discovery: `~/.readit/server.json` with PID liveness check + HTTP health check. File lock (`server.lock`) prevents race conditions.
|
|
212
|
+
|
|
213
|
+
## Build & Dev Workflow
|
|
214
|
+
|
|
215
|
+
```makefile
|
|
216
|
+
dev: # Go spawns Vite child process, single command
|
|
217
|
+
build: # bun vite build → go build (embeds dist/)
|
|
218
|
+
test: # go test ./...
|
|
219
|
+
test-client: # bun run test
|
|
220
|
+
test-e2e: # playwright
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Dev mode: `make dev` → Go runs with `--dev`, spawns `bunx vite` on port 24678, proxies non-API requests. Ctrl+C kills both.
|
|
224
|
+
|
|
225
|
+
Production build: `make build` → Vite builds frontend into `dist/`, then `go build` embeds `dist/` into the binary.
|
|
226
|
+
|
|
227
|
+
## Types (`types.go`)
|
|
228
|
+
|
|
229
|
+
```go
|
|
230
|
+
type Comment struct {
|
|
231
|
+
ID string `json:"id"`
|
|
232
|
+
SelectedText string `json:"selectedText"`
|
|
233
|
+
Comment string `json:"comment"`
|
|
234
|
+
StartOffset int `json:"startOffset"`
|
|
235
|
+
EndOffset int `json:"endOffset"`
|
|
236
|
+
CreatedAt string `json:"createdAt"`
|
|
237
|
+
LineHint string `json:"lineHint,omitempty"`
|
|
238
|
+
AnchorConfidence string `json:"anchorConfidence,omitempty"`
|
|
239
|
+
AnchorPrefix string `json:"anchorPrefix,omitempty"`
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
type Heading struct {
|
|
243
|
+
ID string `json:"id"`
|
|
244
|
+
Text string `json:"text"`
|
|
245
|
+
Level int `json:"level"`
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
type FileState struct {
|
|
249
|
+
FilePath string
|
|
250
|
+
FileName string
|
|
251
|
+
Content []byte
|
|
252
|
+
RenderedHTML string
|
|
253
|
+
Headings []Heading
|
|
254
|
+
mu sync.Mutex
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
type Settings struct {
|
|
258
|
+
Version int `json:"version"`
|
|
259
|
+
FontFamily string `json:"fontFamily"`
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
JSON tags match the current API responses exactly.
|
|
264
|
+
|
|
265
|
+
## Go Dependencies
|
|
266
|
+
|
|
267
|
+
| Package | Purpose |
|
|
268
|
+
|---------|---------|
|
|
269
|
+
| `github.com/yuin/goldmark` | Markdown → HTML |
|
|
270
|
+
| `github.com/yuin/goldmark-highlighting/v2` | Chroma integration for goldmark |
|
|
271
|
+
| `github.com/alecthomas/chroma/v2` | Syntax highlighting (native Go) |
|
|
272
|
+
| `github.com/fsnotify/fsnotify` | Cross-platform file watching |
|
|
273
|
+
| `github.com/pkg/browser` | Cross-platform browser launch |
|
|
274
|
+
|
|
275
|
+
Five dependencies total. No HTTP framework, no CLI framework.
|
|
276
|
+
|
|
277
|
+
## Migration Path
|
|
278
|
+
|
|
279
|
+
1. Build Go server implementing the full API contract
|
|
280
|
+
2. Verify Svelte frontend works unchanged against Go server
|
|
281
|
+
3. Run existing E2E perf tests, compare against React/Svelte baselines
|
|
282
|
+
4. Remove `src/server.ts`, `src/cli.ts`, `src/lib/markdown-renderer.ts`, `src/lib/mermaid-worker.ts`, `src/lib/mermaid-renderer.ts`, `src/lib/comment-storage.ts`, `src/lib/anchor.ts`, related server-side code
|
|
283
|
+
5. Update `package.json` scripts to use Makefile
|
|
284
|
+
6. Remove server-side JS dependencies (`shiki`, `markdown-it`, `jsdom`, `mermaid`, `commander`)
|
package/e2e/comments.spec.ts
CHANGED
|
@@ -3,11 +3,7 @@ import * as os from "node:os";
|
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { expect, test } from "@playwright/test";
|
|
5
5
|
import { spawnCli } from "./utils/cli";
|
|
6
|
-
import {
|
|
7
|
-
addComment,
|
|
8
|
-
selectTextInArticle,
|
|
9
|
-
selectTextInIframe,
|
|
10
|
-
} from "./utils/selection";
|
|
6
|
+
import { addComment, selectTextInArticle } from "./utils/selection";
|
|
11
7
|
|
|
12
8
|
const FIXTURES_DIR = resolve(import.meta.dirname, "fixtures");
|
|
13
9
|
|
|
@@ -34,18 +30,13 @@ function cleanupCommentFile(sourcePath: string): void {
|
|
|
34
30
|
|
|
35
31
|
test.describe("Comment Creation", () => {
|
|
36
32
|
const sampleMdPath = resolve(FIXTURES_DIR, "sample.md");
|
|
37
|
-
const sampleHtmlPath = resolve(FIXTURES_DIR, "sample.html");
|
|
38
33
|
|
|
39
34
|
test.beforeEach(() => {
|
|
40
|
-
// Clean up any existing comment files before each test
|
|
41
35
|
cleanupCommentFile(sampleMdPath);
|
|
42
|
-
cleanupCommentFile(sampleHtmlPath);
|
|
43
36
|
});
|
|
44
37
|
|
|
45
38
|
test.afterEach(() => {
|
|
46
|
-
// Clean up after each test
|
|
47
39
|
cleanupCommentFile(sampleMdPath);
|
|
48
|
-
cleanupCommentFile(sampleHtmlPath);
|
|
49
40
|
});
|
|
50
41
|
|
|
51
42
|
test("adds comment to selected text in markdown document", async ({
|
|
@@ -57,7 +48,7 @@ test.describe("Comment Creation", () => {
|
|
|
57
48
|
await page.goto(url);
|
|
58
49
|
|
|
59
50
|
// Wait for document to load
|
|
60
|
-
const article = page.locator("article");
|
|
51
|
+
const article = page.locator("article#document-content");
|
|
61
52
|
await expect(article).toBeVisible();
|
|
62
53
|
|
|
63
54
|
// Select text in the article
|
|
@@ -68,55 +59,20 @@ test.describe("Comment Creation", () => {
|
|
|
68
59
|
const commentText = "This is my test comment";
|
|
69
60
|
await addComment(page, commentText);
|
|
70
61
|
|
|
71
|
-
// Verify: highlight exists
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
62
|
+
// Verify: highlight exists via CSS Custom Highlight API observability hook
|
|
63
|
+
await page.waitForFunction(
|
|
64
|
+
() => {
|
|
65
|
+
const h = (window as unknown as Record<string, unknown>)
|
|
66
|
+
.__readitHighlights as { commentIds: string[] } | undefined;
|
|
67
|
+
return h && h.commentIds.length > 0;
|
|
68
|
+
},
|
|
69
|
+
{ timeout: 10_000 },
|
|
70
|
+
);
|
|
75
71
|
|
|
76
|
-
// Verify
|
|
77
|
-
await expect(
|
|
78
|
-
} finally {
|
|
79
|
-
await cleanup();
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("adds comment to selected text in HTML document (iframe)", async ({
|
|
84
|
-
page,
|
|
85
|
-
}) => {
|
|
86
|
-
const { url, cleanup } = await spawnCli(sampleHtmlPath, { port: 4573 });
|
|
87
|
-
|
|
88
|
-
try {
|
|
89
|
-
await page.goto(url);
|
|
90
|
-
|
|
91
|
-
// Wait for iframe to load and its script to initialize
|
|
92
|
-
const iframe = page.frameLocator("iframe");
|
|
93
|
-
await expect(iframe.locator("body")).toBeVisible();
|
|
94
|
-
|
|
95
|
-
// Wait for iframe script to execute and send iframeReady
|
|
96
|
-
await page.waitForTimeout(500);
|
|
97
|
-
|
|
98
|
-
// Select text inside iframe - use test event which bypasses tree walking
|
|
99
|
-
// The test event directly sends offsets to the parent
|
|
100
|
-
const textToSelect = "testing text selection";
|
|
101
|
-
await selectTextInIframe(page, iframe, textToSelect);
|
|
102
|
-
|
|
103
|
-
// Verify pending highlight exists in iframe
|
|
104
|
-
const pendingMark = iframe.locator("mark[data-pending]");
|
|
105
|
-
await expect(pendingMark).toBeVisible({ timeout: 5000 });
|
|
72
|
+
// Verify the selected text is still visible in the article
|
|
73
|
+
await expect(article).toContainText(textToSelect);
|
|
106
74
|
|
|
107
|
-
//
|
|
108
|
-
const commentText = "Comment on HTML content";
|
|
109
|
-
await addComment(page, commentText);
|
|
110
|
-
|
|
111
|
-
// Wait for the comment to be saved via API and highlights to be applied
|
|
112
|
-
await page.waitForTimeout(500);
|
|
113
|
-
|
|
114
|
-
// Verify: highlight exists inside iframe with comment ID
|
|
115
|
-
const highlight = iframe.locator("mark[data-comment-id]").first();
|
|
116
|
-
await expect(highlight).toBeVisible();
|
|
117
|
-
await expect(highlight).toContainText(textToSelect);
|
|
118
|
-
|
|
119
|
-
// Verify: margin note shows the comment (in parent frame)
|
|
75
|
+
// Verify: margin note shows the comment
|
|
120
76
|
await expect(page.locator("body")).toContainText(commentText);
|
|
121
77
|
} finally {
|
|
122
78
|
await cleanup();
|
|
@@ -15,7 +15,7 @@ test.describe("Document Loading", () => {
|
|
|
15
15
|
await page.goto(url);
|
|
16
16
|
|
|
17
17
|
// Wait for document to load - use article scope to avoid header h1
|
|
18
|
-
const article = page.locator("article");
|
|
18
|
+
const article = page.locator("article#document-content");
|
|
19
19
|
await expect(article.locator("h1")).toContainText("Test Document");
|
|
20
20
|
|
|
21
21
|
// Verify paragraph content is rendered
|
|
@@ -29,26 +29,4 @@ test.describe("Document Loading", () => {
|
|
|
29
29
|
await cleanup();
|
|
30
30
|
}
|
|
31
31
|
});
|
|
32
|
-
|
|
33
|
-
test("loads HTML document in iframe", async ({ page }) => {
|
|
34
|
-
const { url, cleanup } = await spawnCli(
|
|
35
|
-
resolve(FIXTURES_DIR, "sample.html"),
|
|
36
|
-
{ port: 4571 },
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
await page.goto(url);
|
|
41
|
-
|
|
42
|
-
// Wait for iframe to exist
|
|
43
|
-
const iframe = page.frameLocator("iframe");
|
|
44
|
-
|
|
45
|
-
// Verify content inside iframe
|
|
46
|
-
await expect(iframe.locator("h1")).toContainText("Test Document");
|
|
47
|
-
await expect(iframe.locator("p").first()).toContainText(
|
|
48
|
-
"This is a paragraph for testing text selection",
|
|
49
|
-
);
|
|
50
|
-
} finally {
|
|
51
|
-
await cleanup();
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
32
|
});
|
package/e2e/export.spec.ts
CHANGED
|
@@ -19,7 +19,7 @@ test.describe("Comment Export", () => {
|
|
|
19
19
|
await page.goto(url);
|
|
20
20
|
|
|
21
21
|
// Wait for document to load
|
|
22
|
-
const article = page.locator("article");
|
|
22
|
+
const article = page.locator("article#document-content");
|
|
23
23
|
await expect(article).toBeVisible();
|
|
24
24
|
|
|
25
25
|
// Add a comment
|
|
@@ -48,9 +48,9 @@ test.describe("Comment Export", () => {
|
|
|
48
48
|
expect(clipboardContent).toContain(textToSelect);
|
|
49
49
|
expect(clipboardContent).toContain(commentText);
|
|
50
50
|
|
|
51
|
-
// Verify it follows the prompt format (selected text + comment
|
|
52
|
-
expect(clipboardContent).
|
|
53
|
-
expect(clipboardContent).toContain("
|
|
51
|
+
// Verify it follows the prompt format (quoted selected text + comment)
|
|
52
|
+
expect(clipboardContent).toMatch(/"testing text selection"/);
|
|
53
|
+
expect(clipboardContent).toContain("This is my review comment");
|
|
54
54
|
} finally {
|
|
55
55
|
await cleanup();
|
|
56
56
|
}
|
|
@@ -23,9 +23,9 @@ test(`add-comment: ${tier.name} (${tier.lines} lines, ${tier.comments} comments)
|
|
|
23
23
|
|
|
24
24
|
// Capture actual highlight count as baseline (may differ from tier.comments)
|
|
25
25
|
const baselineCount = await page.evaluate(() => {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
const h = (window as unknown as Record<string, unknown>)
|
|
27
|
+
.__readitHighlights as { commentIds: string[] } | undefined;
|
|
28
|
+
return h?.commentIds?.length ?? 0;
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
// Find a text that isn't already highlighted — use a line near the end
|
|
@@ -90,11 +90,9 @@ test(`add-comment: ${tier.name} (${tier.lines} lines, ${tier.comments} comments)
|
|
|
90
90
|
},
|
|
91
91
|
async () => {
|
|
92
92
|
await page.waitForFunction((expected) => {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
);
|
|
97
|
-
return ids.size > expected;
|
|
93
|
+
const h = (window as unknown as Record<string, unknown>)
|
|
94
|
+
.__readitHighlights as { commentIds: string[] } | undefined;
|
|
95
|
+
return (h?.commentIds?.length ?? 0) > expected;
|
|
98
96
|
}, baselineCount);
|
|
99
97
|
},
|
|
100
98
|
);
|
|
@@ -107,9 +105,9 @@ test(`add-comment: ${tier.name} (${tier.lines} lines, ${tier.comments} comments)
|
|
|
107
105
|
|
|
108
106
|
// Verify the highlight actually appeared
|
|
109
107
|
const finalCount = await page.evaluate(() => {
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
const h = (window as unknown as Record<string, unknown>)
|
|
109
|
+
.__readitHighlights as { commentIds: string[] } | undefined;
|
|
110
|
+
return h?.commentIds?.length ?? 0;
|
|
113
111
|
});
|
|
114
112
|
expect(finalCount).toBeGreaterThan(tier.comments);
|
|
115
113
|
} finally {
|
|
@@ -9,7 +9,6 @@ interface Comment {
|
|
|
9
9
|
id: string;
|
|
10
10
|
selectedText: string;
|
|
11
11
|
comment: string;
|
|
12
|
-
createdAt: string;
|
|
13
12
|
startOffset: number;
|
|
14
13
|
endOffset: number;
|
|
15
14
|
lineHint: string;
|
|
@@ -174,7 +173,6 @@ function makeComment(
|
|
|
174
173
|
id: `perf${String(index).padStart(4, "0")}`,
|
|
175
174
|
selectedText,
|
|
176
175
|
comment: `Review comment #${index + 1}: This section needs attention regarding clarity and accuracy.`,
|
|
177
|
-
createdAt: "2025-01-01T00:00:00.000Z",
|
|
178
176
|
startOffset,
|
|
179
177
|
endOffset,
|
|
180
178
|
lineHint,
|
|
@@ -199,9 +197,7 @@ function serializeComments(file: CommentFile): string {
|
|
|
199
197
|
|
|
200
198
|
for (const comment of file.comments) {
|
|
201
199
|
// Metadata
|
|
202
|
-
lines.push(
|
|
203
|
-
`<!-- c:${comment.id}|${comment.lineHint}|${comment.createdAt} -->`,
|
|
204
|
-
);
|
|
200
|
+
lines.push(`<!-- c:${comment.id}|${comment.lineHint} -->`);
|
|
205
201
|
|
|
206
202
|
// Selected text as blockquote
|
|
207
203
|
const quotedLines = comment.selectedText
|
|
Binary file
|