@peaske7/readit 0.1.4 → 0.1.6
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/.agents/skills/remotion-best-practices/SKILL.md +61 -0
- package/.agents/skills/remotion-best-practices/rules/3d.md +86 -0
- package/.agents/skills/remotion-best-practices/rules/animations.md +27 -0
- package/.agents/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +178 -0
- package/.agents/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
- package/.agents/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
- package/.agents/skills/remotion-best-practices/rules/assets.md +78 -0
- package/.agents/skills/remotion-best-practices/rules/audio-visualization.md +198 -0
- package/.agents/skills/remotion-best-practices/rules/audio.md +169 -0
- package/.agents/skills/remotion-best-practices/rules/calculate-metadata.md +134 -0
- package/.agents/skills/remotion-best-practices/rules/can-decode.md +75 -0
- package/.agents/skills/remotion-best-practices/rules/charts.md +120 -0
- package/.agents/skills/remotion-best-practices/rules/compositions.md +154 -0
- package/.agents/skills/remotion-best-practices/rules/display-captions.md +184 -0
- package/.agents/skills/remotion-best-practices/rules/extract-frames.md +229 -0
- package/.agents/skills/remotion-best-practices/rules/ffmpeg.md +38 -0
- package/.agents/skills/remotion-best-practices/rules/fonts.md +152 -0
- package/.agents/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
- package/.agents/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
- package/.agents/skills/remotion-best-practices/rules/get-video-duration.md +60 -0
- package/.agents/skills/remotion-best-practices/rules/gifs.md +141 -0
- package/.agents/skills/remotion-best-practices/rules/images.md +134 -0
- package/.agents/skills/remotion-best-practices/rules/import-srt-captions.md +69 -0
- package/.agents/skills/remotion-best-practices/rules/light-leaks.md +73 -0
- package/.agents/skills/remotion-best-practices/rules/lottie.md +70 -0
- package/.agents/skills/remotion-best-practices/rules/maps.md +412 -0
- package/.agents/skills/remotion-best-practices/rules/measuring-dom-nodes.md +34 -0
- package/.agents/skills/remotion-best-practices/rules/measuring-text.md +140 -0
- package/.agents/skills/remotion-best-practices/rules/parameters.md +109 -0
- package/.agents/skills/remotion-best-practices/rules/sequencing.md +118 -0
- package/.agents/skills/remotion-best-practices/rules/sfx.md +26 -0
- package/.agents/skills/remotion-best-practices/rules/subtitles.md +36 -0
- package/.agents/skills/remotion-best-practices/rules/tailwind.md +11 -0
- package/.agents/skills/remotion-best-practices/rules/text-animations.md +20 -0
- package/.agents/skills/remotion-best-practices/rules/timing.md +179 -0
- package/.agents/skills/remotion-best-practices/rules/transcribe-captions.md +70 -0
- package/.agents/skills/remotion-best-practices/rules/transitions.md +197 -0
- package/.agents/skills/remotion-best-practices/rules/transparent-videos.md +106 -0
- package/.agents/skills/remotion-best-practices/rules/trimming.md +51 -0
- package/.agents/skills/remotion-best-practices/rules/videos.md +171 -0
- package/.agents/skills/remotion-best-practices/rules/voiceover.md +99 -0
- package/.agents/skills/simple/SKILL.md +52 -0
- package/.agents/skills/vercel-react-best-practices/AGENTS.md +3254 -0
- package/.agents/skills/vercel-react-best-practices/README.md +123 -0
- package/.agents/skills/vercel-react-best-practices/SKILL.md +141 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md +60 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md +85 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md +68 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md +82 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md +142 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/.claude/CLAUDE.md +142 -0
- package/.claude/commands/review.md +120 -0
- package/.claude/commands/sync-docs.md +71 -0
- package/.claude/roadmap.md +98 -0
- package/.claude/rules/style-guide.md +830 -0
- package/.claude/settings.json +18 -0
- package/.claude/user-stories.md +248 -0
- package/AGENTS.md +64 -0
- package/README.md +7 -7
- package/biome.json +69 -0
- package/bun.lock +1124 -0
- package/docs/design.md +563 -0
- package/docs/plans/2026-03-13-client-mode-design.md +86 -0
- package/docs/plans/2026-03-13-client-mode-plan.md +605 -0
- package/docs/plans/2026-03-13-keyboard-shortcuts-design.md +129 -0
- package/docs/plans/2026-03-13-keyboard-shortcuts-plan.md +1471 -0
- package/docs/plans/2026-03-13-multi-document-design.md +183 -0
- package/docs/plans/2026-03-13-performance-benchmarks-design.md +121 -0
- package/e2e/comments.spec.ts +125 -0
- package/e2e/document-load.spec.ts +54 -0
- package/e2e/export.spec.ts +58 -0
- package/e2e/fixtures/sample.html +13 -0
- package/e2e/fixtures/sample.md +7 -0
- package/e2e/persistence-file.spec.ts +342 -0
- package/e2e/utils/cli.ts +84 -0
- package/e2e/utils/selection.ts +135 -0
- package/{dist/index.html → index.html} +8 -2
- package/lefthook.yml +8 -0
- package/package.json +17 -39
- package/playwright.config.ts +22 -0
- package/skills-lock.json +20 -0
- package/src/App.tsx +396 -0
- package/src/cli/index.ts +467 -0
- package/src/components/ActionsMenu.tsx +110 -0
- package/src/components/DocumentViewer/CodeBlock.tsx +83 -0
- package/src/components/DocumentViewer/DocumentViewer.tsx +257 -0
- package/src/components/DocumentViewer/IframeContainer.tsx +251 -0
- package/src/components/DocumentViewer/MermaidDiagram.tsx +137 -0
- package/src/components/DocumentViewer/index.ts +1 -0
- package/src/components/FloatingTOC.tsx +59 -0
- package/src/components/Header.tsx +63 -0
- package/src/components/InlineEditor.tsx +72 -0
- package/src/components/MarginNote.tsx +198 -0
- package/src/components/MarginNotes.tsx +50 -0
- package/src/components/RawModal.tsx +141 -0
- package/src/components/ReanchorConfirm.tsx +33 -0
- package/src/components/SettingsModal.tsx +221 -0
- package/src/components/ShortcutCapture.tsx +45 -0
- package/src/components/ShortcutList.tsx +157 -0
- package/src/components/TabBar.tsx +60 -0
- package/src/components/TableOfContents.tsx +108 -0
- package/src/components/comments/CommentBadge.tsx +43 -0
- package/src/components/comments/CommentInput.tsx +119 -0
- package/src/components/comments/CommentListItem.tsx +82 -0
- package/src/components/comments/CommentManager.tsx +106 -0
- package/src/components/comments/CommentMinimap.tsx +62 -0
- package/src/components/comments/CommentNav.tsx +104 -0
- package/src/components/ui/ActionBar.tsx +16 -0
- package/src/components/ui/ActionLink.tsx +32 -0
- package/src/components/ui/Button.tsx +55 -0
- package/src/components/ui/Dialog.tsx +156 -0
- package/src/components/ui/DropdownMenu.tsx +114 -0
- package/src/components/ui/SeparatorDot.tsx +9 -0
- package/src/components/ui/Text.tsx +54 -0
- package/src/contexts/CommentContext.tsx +222 -0
- package/src/contexts/LayoutContext.tsx +76 -0
- package/src/hooks/useClickOutside.ts +35 -0
- package/src/hooks/useClipboard.ts +79 -0
- package/src/hooks/useCommentNavigation.ts +130 -0
- package/src/hooks/useComments.ts +323 -0
- package/src/hooks/useDocument.ts +141 -0
- package/src/hooks/useFontPreference.ts +76 -0
- package/src/hooks/useHeadings.test.ts +159 -0
- package/src/hooks/useHeadings.ts +129 -0
- package/src/hooks/useKeybindings.ts +120 -0
- package/src/hooks/useKeyboardShortcuts.ts +63 -0
- package/src/hooks/useLayoutMode.ts +44 -0
- package/src/hooks/useReanchorMode.ts +33 -0
- package/src/hooks/useScrollMetrics.ts +56 -0
- package/src/hooks/useScrollSpy.ts +81 -0
- package/src/hooks/useTextSelection.ts +123 -0
- package/src/hooks/useThemePreference.ts +66 -0
- package/src/index.css +823 -0
- package/src/lib/__fixtures__/bench-data.ts +167 -0
- package/src/lib/anchor.bench.ts +112 -0
- package/src/lib/anchor.test.ts +531 -0
- package/src/lib/anchor.ts +465 -0
- package/src/lib/comment-storage.bench.ts +63 -0
- package/src/lib/comment-storage.test.ts +624 -0
- package/src/lib/comment-storage.ts +263 -0
- package/src/lib/context.bench.ts +41 -0
- package/src/lib/context.test.ts +224 -0
- package/src/lib/context.ts +193 -0
- package/src/lib/export.bench.ts +35 -0
- package/src/lib/export.ts +43 -0
- package/src/lib/highlight/colors.ts +37 -0
- package/src/lib/highlight/core.test.ts +98 -0
- package/src/lib/highlight/core.ts +54 -0
- package/src/lib/highlight/dom.ts +342 -0
- package/src/lib/highlight/highlighter.ts +427 -0
- package/src/lib/highlight/index.ts +23 -0
- package/src/lib/highlight/script-builder.ts +485 -0
- package/src/lib/highlight/types.ts +57 -0
- package/src/lib/html-processor.test.tsx +170 -0
- package/src/lib/html-processor.tsx +95 -0
- package/src/lib/layout-constants.ts +12 -0
- package/src/lib/margin-layout.bench.ts +28 -0
- package/src/lib/margin-layout.ts +100 -0
- package/src/lib/scroll.test.ts +118 -0
- package/src/lib/scroll.ts +47 -0
- package/src/lib/shortcut-registry.test.ts +173 -0
- package/src/lib/shortcut-registry.ts +209 -0
- package/src/lib/utils.test.ts +110 -0
- package/src/lib/utils.ts +61 -0
- package/src/main.tsx +10 -0
- package/src/server/index.ts +883 -0
- package/src/store/index.test.ts +220 -0
- package/src/store/index.ts +234 -0
- package/src/test-setup.ts +1 -0
- package/src/types/index.ts +115 -0
- package/test.md +74 -0
- package/tsconfig.cli.json +12 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +19 -0
- package/vitest.config.ts +15 -0
- package/dist/assets/_basePickBy-hOr-yGsE.js +0 -1
- package/dist/assets/_baseUniq-b7bzdUSn.js +0 -1
- package/dist/assets/arc-D65wG9gm.js +0 -1
- package/dist/assets/architecture-PBZL5I3N-DBa6CAv_.js +0 -1
- package/dist/assets/architectureDiagram-2XIMDMQ5-Djwpsh98.js +0 -36
- package/dist/assets/array-DOVTz2Mq.js +0 -1
- package/dist/assets/blockDiagram-WCTKOSBZ-BdW5TTxj.js +0 -132
- package/dist/assets/c4Diagram-IC4MRINW-DTmkHEXu.js +0 -10
- package/dist/assets/channel-B3MUFipN.js +0 -1
- package/dist/assets/chunk-4BX2VUAB-DEqzsvDc.js +0 -1
- package/dist/assets/chunk-55IACEB6-BzVuSUV8.js +0 -1
- package/dist/assets/chunk-7E7YKBS2-CZ8IcA4c.js +0 -1
- package/dist/assets/chunk-7R4GIKGN-CWVVC8HX.js +0 -79
- package/dist/assets/chunk-C72U2L5F-B1Tso5TH.js +0 -1
- package/dist/assets/chunk-EGIJ26TM-Cx_7CFik.js +0 -1
- package/dist/assets/chunk-FMBD7UC4-Cfk_iGhv.js +0 -15
- package/dist/assets/chunk-GEFDOKGD-C_5hRbJt.js +0 -2
- package/dist/assets/chunk-GLR3WWYH-CkY7IyBj.js +0 -2
- package/dist/assets/chunk-HHEYEP7N-B0I4X5cr.js +0 -1
- package/dist/assets/chunk-JSJVCQXG-CAjwlVLg.js +0 -1
- package/dist/assets/chunk-KX2RTZJC-DWqnZZ02.js +0 -1
- package/dist/assets/chunk-KYZI473N-gjRVhJgJ.js +0 -53
- package/dist/assets/chunk-L3YUKLVL-D7C9GuxL.js +0 -1
- package/dist/assets/chunk-MX3YWQON-i-77iuVj.js +0 -1
- package/dist/assets/chunk-NQ4KR5QH-B22Pvemm.js +0 -220
- package/dist/assets/chunk-O4XLMI2P-ZQd5L6ZD.js +0 -7
- package/dist/assets/chunk-OZEHJAEY-BaPKTELw.js +0 -1
- package/dist/assets/chunk-PQ6SQG4A-DqE1eupT.js +0 -1
- package/dist/assets/chunk-PU5JKC2W-BTqWqedh.js +0 -70
- package/dist/assets/chunk-QZHKN3VN-Nm9TvMss.js +0 -1
- package/dist/assets/chunk-R5LLSJPH-DkiNs1dN.js +0 -1
- package/dist/assets/chunk-WL4C6EOR-CioD2fv2.js +0 -189
- package/dist/assets/chunk-XIRO2GV7-B4GGQONY.js +0 -1
- package/dist/assets/chunk-XPW4576I-C0IbbQos.js +0 -32
- package/dist/assets/chunk-XZSTWKYB-DMOqFWmT.js +0 -94
- package/dist/assets/chunk-YBOYWFTD-CoeQgeVY.js +0 -1
- package/dist/assets/classDiagram-VBA2DB6C-DV9ltQ7h.js +0 -1
- package/dist/assets/classDiagram-v2-RAHNMMFH-C6nD9wmM.js +0 -1
- package/dist/assets/clone-DuY6BQEm.js +0 -1
- package/dist/assets/cose-bilkent-S5V4N54A-B6FexK6p.js +0 -1
- package/dist/assets/cytoscape.esm-DoTFyJaN.js +0 -321
- package/dist/assets/dagre-CCcocoCU.js +0 -1
- package/dist/assets/dagre-KLK3FWXG-DIELowj9.js +0 -4
- package/dist/assets/defaultLocale-Ck2Xxk-C.js +0 -1
- package/dist/assets/diagram-E7M64L7V-D1mm0PoO.js +0 -24
- package/dist/assets/diagram-IFDJBPK2-7DVjly8y.js +0 -43
- package/dist/assets/diagram-P4PSJMXO-jO7pfyMb.js +0 -24
- package/dist/assets/dist-BywRdrPx.js +0 -1
- package/dist/assets/erDiagram-INFDFZHY-DSRxlRFy.js +0 -70
- package/dist/assets/flowDiagram-PKNHOUZH-CgKzzNdR.js +0 -162
- package/dist/assets/ganttDiagram-A5KZAMGK-CtsE7Y4E.js +0 -292
- package/dist/assets/gitGraph-HDMCJU4V-BU9uhwtz.js +0 -1
- package/dist/assets/gitGraphDiagram-K3NZZRJ6-DOU8RGdw.js +0 -65
- package/dist/assets/graphlib-WkJoBgka.js +0 -1
- package/dist/assets/index-CKVArt9D.js +0 -562
- package/dist/assets/index-DzRKJazf.css +0 -2
- package/dist/assets/info-3K5VOQVL-CPpvM-SG.js +0 -1
- package/dist/assets/infoDiagram-LFFYTUFH-VKLs5DsF.js +0 -2
- package/dist/assets/init-Bft5Ffpj.js +0 -1
- package/dist/assets/isArrayLikeObject-icl0H0jo.js +0 -1
- package/dist/assets/isEmpty-Du8sNmkE.js +0 -1
- package/dist/assets/ishikawaDiagram-PHBUUO56-CsWvEjux.js +0 -70
- package/dist/assets/journeyDiagram-4ABVD52K-BzJGTdIT.js +0 -139
- package/dist/assets/kanban-definition-K7BYSVSG-B_9ClJ1A.js +0 -89
- package/dist/assets/katex-BJrMXEjr.js +0 -261
- package/dist/assets/line-CC_tDGId.js +0 -1
- package/dist/assets/linear-Cts_d04Y.js +0 -1
- package/dist/assets/math-CNhlSIO3.js +0 -1
- package/dist/assets/mermaid-parser.core-Vb9KKv1R.js +0 -4
- package/dist/assets/mermaid.core-C_7xsp3d.js +0 -11
- package/dist/assets/mindmap-definition-YRQLILUH-BWmfy5wB.js +0 -68
- package/dist/assets/ordinal-DIg8h6NI.js +0 -1
- package/dist/assets/packet-RMMSAZCW-Q-WG6o3b.js +0 -1
- package/dist/assets/path-DfRbCp9y.js +0 -1
- package/dist/assets/pie-UPGHQEXC-Cwi2tLlt.js +0 -1
- package/dist/assets/pieDiagram-SKSYHLDU-Dyf3X_in.js +0 -30
- package/dist/assets/quadrantDiagram-337W2JSQ-B5_5m61Q.js +0 -7
- package/dist/assets/radar-KQ55EAFF-Dtw2VzxY.js +0 -1
- package/dist/assets/requirementDiagram-Z7DCOOCP-BSERBnlW.js +0 -73
- package/dist/assets/rough.esm-KjoEK0it.js +0 -1
- package/dist/assets/sankeyDiagram-WA2Y5GQK-CMcEY8Cz.js +0 -10
- package/dist/assets/sequenceDiagram-2WXFIKYE-D28qcXwC.js +0 -145
- package/dist/assets/src-C8kkzlHX.js +0 -1
- package/dist/assets/stateDiagram-RAJIS63D-7oVrCmRl.js +0 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-DtFptQAd.js +0 -1
- package/dist/assets/timeline-definition-YZTLITO2-rbCfBEvG.js +0 -61
- package/dist/assets/treemap-KZPCXAKY-BlRvF0um.js +0 -1
- package/dist/assets/vennDiagram-LZ73GAT5-DBit3zWa.js +0 -34
- package/dist/assets/xychartDiagram-JWTSCODW-BVYXv51y.js +0 -7
- package/dist/index.js +0 -1040
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { Comment, CommentFile } from "../types";
|
|
5
|
+
|
|
6
|
+
const FORMAT_VERSION = 1;
|
|
7
|
+
const HASH_LENGTH = 16;
|
|
8
|
+
const MAX_SELECTION_LENGTH = 1000;
|
|
9
|
+
const TRUNCATION_MARKER = "\n...\n";
|
|
10
|
+
const ANCHOR_PREFIX_LENGTH = 200; // chars stored for anchor matching when text is truncated
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Truncate very long selections to first ~500 + ... + last ~500 chars.
|
|
14
|
+
*/
|
|
15
|
+
export function truncateSelection(text: string): string {
|
|
16
|
+
if (text.length <= MAX_SELECTION_LENGTH) {
|
|
17
|
+
return text;
|
|
18
|
+
}
|
|
19
|
+
const half = Math.floor(
|
|
20
|
+
(MAX_SELECTION_LENGTH - TRUNCATION_MARKER.length) / 2,
|
|
21
|
+
);
|
|
22
|
+
return text.slice(0, half) + TRUNCATION_MARKER + text.slice(-half);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Compute the path where comments for a source file should be stored.
|
|
27
|
+
* Comments are stored in ~/.readit/comments/{absolute-path-structure}/{filename}.comments.md
|
|
28
|
+
*/
|
|
29
|
+
export function getCommentPath(sourcePath: string): string {
|
|
30
|
+
// Resolve to absolute path
|
|
31
|
+
const absolute = path.resolve(sourcePath);
|
|
32
|
+
|
|
33
|
+
// Remove leading slash and drive letter (Windows)
|
|
34
|
+
const normalized = absolute.replace(/^\//, "").replace(/^[A-Z]:[\\/]/, "");
|
|
35
|
+
|
|
36
|
+
// Get filename without extension, add .comments.md
|
|
37
|
+
const ext = path.extname(normalized);
|
|
38
|
+
const withoutExt = normalized.slice(0, -ext.length || undefined);
|
|
39
|
+
|
|
40
|
+
return path.join(
|
|
41
|
+
os.homedir(),
|
|
42
|
+
".readit",
|
|
43
|
+
"comments",
|
|
44
|
+
`${withoutExt}.comments.md`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compute SHA-256 hash of content, returning first 16 characters.
|
|
50
|
+
*/
|
|
51
|
+
export function computeHash(content: string): string {
|
|
52
|
+
return crypto
|
|
53
|
+
.createHash("sha256")
|
|
54
|
+
.update(content)
|
|
55
|
+
.digest("hex")
|
|
56
|
+
.slice(0, HASH_LENGTH);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get line number (1-indexed) for a character offset in content.
|
|
61
|
+
*/
|
|
62
|
+
export function getLineNumber(content: string, offset: number): number {
|
|
63
|
+
if (offset <= 0 || content.length === 0) return 1;
|
|
64
|
+
const clampedOffset = Math.min(offset, content.length);
|
|
65
|
+
return content.slice(0, clampedOffset).split("\n").length;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get line range string for a selection (e.g., "L42" or "L42-45").
|
|
70
|
+
*/
|
|
71
|
+
export function getLineHint(
|
|
72
|
+
content: string,
|
|
73
|
+
startOffset: number,
|
|
74
|
+
endOffset: number,
|
|
75
|
+
): string {
|
|
76
|
+
const startLine = getLineNumber(content, startOffset);
|
|
77
|
+
const endLine = getLineNumber(content, endOffset);
|
|
78
|
+
return startLine === endLine ? `L${startLine}` : `L${startLine}-${endLine}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parse a comment file's markdown content into a CommentFile structure.
|
|
83
|
+
*/
|
|
84
|
+
export function parseCommentFile(content: string): CommentFile {
|
|
85
|
+
const result: CommentFile = {
|
|
86
|
+
source: "",
|
|
87
|
+
hash: "",
|
|
88
|
+
version: FORMAT_VERSION,
|
|
89
|
+
comments: [],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (!content.trim()) {
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Parse YAML front matter
|
|
97
|
+
const frontMatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
98
|
+
if (frontMatterMatch) {
|
|
99
|
+
const frontMatter = frontMatterMatch[1];
|
|
100
|
+
const sourceMatch = frontMatter.match(/^source:\s*(.+)$/m);
|
|
101
|
+
const hashMatch = frontMatter.match(/^hash:\s*(.+)$/m);
|
|
102
|
+
const versionMatch = frontMatter.match(/^version:\s*(\d+)$/m);
|
|
103
|
+
|
|
104
|
+
if (sourceMatch) result.source = sourceMatch[1].trim();
|
|
105
|
+
if (hashMatch) result.hash = hashMatch[1].trim();
|
|
106
|
+
if (versionMatch) result.version = Number.parseInt(versionMatch[1], 10);
|
|
107
|
+
|
|
108
|
+
// Validate version compatibility
|
|
109
|
+
if (result.version > FORMAT_VERSION) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Comment file requires readit v${result.version} or higher. ` +
|
|
112
|
+
`Current version supports format v${FORMAT_VERSION}.`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Remove front matter and split by separator
|
|
118
|
+
const bodyContent = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
|
|
119
|
+
const blocks = bodyContent.split(/\n---\n/).filter((block) => block.trim());
|
|
120
|
+
|
|
121
|
+
for (const block of blocks) {
|
|
122
|
+
const comment = parseCommentBlock(block);
|
|
123
|
+
if (comment) {
|
|
124
|
+
result.comments.push(comment);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Parse a single comment block.
|
|
133
|
+
*/
|
|
134
|
+
function parseCommentBlock(block: string): Comment | undefined {
|
|
135
|
+
// Extract metadata from HTML comment: <!-- c:{id}|{lineHint}|{timestamp} -->
|
|
136
|
+
const metadataMatch = block.match(/<!--\s*c:([^|]+)\|([^|]+)\|([^>]+)\s*-->/);
|
|
137
|
+
if (!metadataMatch) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const [, id, lineHint, createdAt] = metadataMatch;
|
|
142
|
+
|
|
143
|
+
// Extract anchor prefix if present: <!-- anchor:{prefix} -->
|
|
144
|
+
const anchorMatch = block.match(/<!--\s*anchor:(.+?)\s*-->/);
|
|
145
|
+
const anchorPrefix = anchorMatch ? anchorMatch[1] : undefined;
|
|
146
|
+
|
|
147
|
+
// Extract selected text from blockquote
|
|
148
|
+
const blockquoteMatch = block.match(/^>\s*(.+(?:\n>\s*.+)*)$/m);
|
|
149
|
+
if (!blockquoteMatch) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Remove the "> " prefix from each line
|
|
154
|
+
const selectedText = blockquoteMatch[1]
|
|
155
|
+
.split("\n")
|
|
156
|
+
.map((line) => line.replace(/^>\s*/, ""))
|
|
157
|
+
.join("\n");
|
|
158
|
+
|
|
159
|
+
// Extract comment body (everything after blockquote)
|
|
160
|
+
const afterBlockquote = block.slice(
|
|
161
|
+
block.indexOf(blockquoteMatch[0]) + blockquoteMatch[0].length,
|
|
162
|
+
);
|
|
163
|
+
const commentBody = afterBlockquote.trim();
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
id,
|
|
167
|
+
selectedText,
|
|
168
|
+
comment: commentBody,
|
|
169
|
+
createdAt: createdAt.trim(),
|
|
170
|
+
lineHint,
|
|
171
|
+
anchorPrefix,
|
|
172
|
+
// Offsets will be resolved by anchor matching when loading
|
|
173
|
+
startOffset: 0,
|
|
174
|
+
endOffset: 0,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Serialize a CommentFile structure to markdown content.
|
|
180
|
+
*/
|
|
181
|
+
export function serializeComments(file: CommentFile): string {
|
|
182
|
+
const lines: string[] = [];
|
|
183
|
+
|
|
184
|
+
// YAML front matter
|
|
185
|
+
lines.push("---");
|
|
186
|
+
lines.push(`source: ${file.source}`);
|
|
187
|
+
lines.push(`hash: ${file.hash}`);
|
|
188
|
+
lines.push(`version: ${file.version}`);
|
|
189
|
+
lines.push("---");
|
|
190
|
+
lines.push("");
|
|
191
|
+
|
|
192
|
+
// Comments
|
|
193
|
+
for (const comment of file.comments) {
|
|
194
|
+
lines.push(serializeComment(comment));
|
|
195
|
+
lines.push("");
|
|
196
|
+
lines.push("---");
|
|
197
|
+
lines.push("");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return lines.join("\n");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Serialize a single comment to markdown block.
|
|
205
|
+
*/
|
|
206
|
+
function serializeComment(comment: Comment): string {
|
|
207
|
+
const lines: string[] = [];
|
|
208
|
+
|
|
209
|
+
// Metadata as HTML comment
|
|
210
|
+
const lineHint = comment.lineHint || "L0";
|
|
211
|
+
lines.push(`<!-- c:${comment.id}|${lineHint}|${comment.createdAt} -->`);
|
|
212
|
+
|
|
213
|
+
// Anchor prefix for long selections (used for anchor matching when text is truncated)
|
|
214
|
+
if (comment.anchorPrefix) {
|
|
215
|
+
lines.push(`<!-- anchor:${comment.anchorPrefix} -->`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Selected text as blockquote
|
|
219
|
+
const quotedLines = comment.selectedText
|
|
220
|
+
.split("\n")
|
|
221
|
+
.map((line) => `> ${line}`);
|
|
222
|
+
lines.push(...quotedLines);
|
|
223
|
+
|
|
224
|
+
// Comment body
|
|
225
|
+
if (comment.comment) {
|
|
226
|
+
lines.push("");
|
|
227
|
+
lines.push(comment.comment);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return lines.join("\n");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Create a new comment with a generated ID and current timestamp.
|
|
235
|
+
*/
|
|
236
|
+
export function createComment(
|
|
237
|
+
selectedText: string,
|
|
238
|
+
commentText: string,
|
|
239
|
+
startOffset: number,
|
|
240
|
+
endOffset: number,
|
|
241
|
+
sourceContent: string,
|
|
242
|
+
): Comment {
|
|
243
|
+
const id = crypto.randomUUID().slice(0, 8);
|
|
244
|
+
const lineHint = getLineHint(sourceContent, startOffset, endOffset);
|
|
245
|
+
const now = new Date();
|
|
246
|
+
const createdAt = now.toISOString();
|
|
247
|
+
|
|
248
|
+
const needsTruncation = selectedText.length > MAX_SELECTION_LENGTH;
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
id,
|
|
252
|
+
selectedText: truncateSelection(selectedText),
|
|
253
|
+
comment: commentText,
|
|
254
|
+
createdAt,
|
|
255
|
+
startOffset,
|
|
256
|
+
endOffset,
|
|
257
|
+
lineHint,
|
|
258
|
+
// Store first N chars for anchor matching when text is truncated
|
|
259
|
+
anchorPrefix: needsTruncation
|
|
260
|
+
? selectedText.slice(0, ANCHOR_PREFIX_LENGTH)
|
|
261
|
+
: undefined,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { bench, describe } from "vitest";
|
|
2
|
+
import { HTML_DOC, LARGE_DOC } from "./__fixtures__/bench-data";
|
|
3
|
+
import { extractContext, formatForLLM, stripHtmlTags } from "./context";
|
|
4
|
+
|
|
5
|
+
describe("stripHtmlTags", () => {
|
|
6
|
+
bench("200-line HTML document", () => {
|
|
7
|
+
stripHtmlTags(HTML_DOC);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
bench("short HTML string", () => {
|
|
11
|
+
stripHtmlTags(
|
|
12
|
+
"<p>Hello & <strong>world</strong> with <entities></p>",
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("extractContext", () => {
|
|
18
|
+
bench("single-line selection, markdown", () => {
|
|
19
|
+
extractContext({ content: LARGE_DOC, startOffset: 500, endOffset: 530 });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
bench("multi-line selection, markdown", () => {
|
|
23
|
+
extractContext({ content: LARGE_DOC, startOffset: 500, endOffset: 800 });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
bench("HTML content (triggers stripHtmlTags)", () => {
|
|
27
|
+
extractContext({ content: HTML_DOC, startOffset: 100, endOffset: 200 });
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("formatForLLM", () => {
|
|
32
|
+
const ctx = extractContext({
|
|
33
|
+
content: LARGE_DOC,
|
|
34
|
+
startOffset: 500,
|
|
35
|
+
endOffset: 530,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
bench("format with comment", () => {
|
|
39
|
+
formatForLLM({ context: ctx, fileName: "doc.md", comment: "Needs review" });
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { extractContext, formatForLLM, stripHtmlTags } from "./context";
|
|
3
|
+
|
|
4
|
+
describe("stripHtmlTags", () => {
|
|
5
|
+
it("removes simple HTML tags", () => {
|
|
6
|
+
expect(stripHtmlTags("<p>Hello</p>")).toBe("Hello");
|
|
7
|
+
expect(stripHtmlTags("<div><span>Nested</span></div>")).toBe("Nested");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("removes script and style content entirely", () => {
|
|
11
|
+
expect(stripHtmlTags('<script>alert("xss")</script>text')).toBe("text");
|
|
12
|
+
expect(stripHtmlTags("<style>.foo { color: red }</style>text")).toBe(
|
|
13
|
+
"text",
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("decodes common named entities", () => {
|
|
18
|
+
expect(stripHtmlTags("<tag>")).toBe("<tag>");
|
|
19
|
+
expect(stripHtmlTags("& "'")).toBe("& \"'");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("decodes numeric entities (decimal)", () => {
|
|
23
|
+
expect(stripHtmlTags("ABC")).toBe("ABC");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("decodes numeric entities (hex)", () => {
|
|
27
|
+
expect(stripHtmlTags("ABC")).toBe("ABC");
|
|
28
|
+
expect(stripHtmlTags("ABC")).toBe("ABC"); // case-insensitive
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("handles mixed content", () => {
|
|
32
|
+
const html = "<p>Hello & <strong>World</strong>!</p>";
|
|
33
|
+
expect(stripHtmlTags(html)).toBe("Hello & World!");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("preserves plain text", () => {
|
|
37
|
+
expect(stripHtmlTags("plain text")).toBe("plain text");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("extractContext", () => {
|
|
42
|
+
it("extracts single-line selection with markers", () => {
|
|
43
|
+
const content = "line1\nline2\nline3\nline4\nline5";
|
|
44
|
+
// "line2" starts at offset 6, ends at 11
|
|
45
|
+
const result = extractContext({ content, startOffset: 6, endOffset: 11 });
|
|
46
|
+
|
|
47
|
+
expect(result.startLine).toBe(2);
|
|
48
|
+
expect(result.endLine).toBe(2);
|
|
49
|
+
// Should have >>> and <<< markers around "line2"
|
|
50
|
+
expect(result.lines.some((l) => l.includes(">>> line2 <<<"))).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("includes context lines before and after", () => {
|
|
54
|
+
const content = "line1\nline2\nline3\nline4\nline5";
|
|
55
|
+
// Select "line3" at offset 12
|
|
56
|
+
const result = extractContext({
|
|
57
|
+
content,
|
|
58
|
+
startOffset: 12,
|
|
59
|
+
endOffset: 17,
|
|
60
|
+
contextLines: 2,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(result.startLine).toBe(3);
|
|
64
|
+
expect(result.endLine).toBe(3);
|
|
65
|
+
// Should include 2 lines before (line1, line2) and 2 after (line4, line5)
|
|
66
|
+
expect(result.lines.length).toBe(5);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("handles multi-line selection", () => {
|
|
70
|
+
const content = "line1\nline2\nline3\nline4\nline5";
|
|
71
|
+
// Select from "line2" to "line3" (offset 6 to 17)
|
|
72
|
+
const result = extractContext({ content, startOffset: 6, endOffset: 17 });
|
|
73
|
+
|
|
74
|
+
expect(result.startLine).toBe(2);
|
|
75
|
+
expect(result.endLine).toBe(3);
|
|
76
|
+
// Start line should have >>> marker
|
|
77
|
+
expect(result.lines.some((l) => l.includes(">>>"))).toBe(true);
|
|
78
|
+
// End line should have <<< marker
|
|
79
|
+
expect(result.lines.some((l) => l.includes("<<<"))).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("handles selection at start of document", () => {
|
|
83
|
+
const content = "line1\nline2\nline3";
|
|
84
|
+
// Select "line1" at start
|
|
85
|
+
const result = extractContext({ content, startOffset: 0, endOffset: 5 });
|
|
86
|
+
|
|
87
|
+
expect(result.startLine).toBe(1);
|
|
88
|
+
expect(result.endLine).toBe(1);
|
|
89
|
+
expect(result.lines[0]).toContain(">>> line1 <<<");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("handles selection at end of document", () => {
|
|
93
|
+
const content = "line1\nline2\nline3";
|
|
94
|
+
// Select "line3" (offset 12 to 17)
|
|
95
|
+
const result = extractContext({ content, startOffset: 12, endOffset: 17 });
|
|
96
|
+
|
|
97
|
+
expect(result.startLine).toBe(3);
|
|
98
|
+
expect(result.endLine).toBe(3);
|
|
99
|
+
expect(result.lines.some((l) => l.includes(">>> line3 <<<"))).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("truncates very long lines", () => {
|
|
103
|
+
const longLine = "a".repeat(250);
|
|
104
|
+
const content = `short\n${longLine}\nshort`;
|
|
105
|
+
// Select part of the long line
|
|
106
|
+
const result = extractContext({ content, startOffset: 6, endOffset: 256 });
|
|
107
|
+
|
|
108
|
+
// Long line should be truncated with ...
|
|
109
|
+
const longLineOutput = result.lines.find((l) => l.length > 100);
|
|
110
|
+
expect(longLineOutput?.endsWith("...")).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("handles HTML content by stripping tags", () => {
|
|
114
|
+
const html = "<p>paragraph</p>\n<div>div content</div>";
|
|
115
|
+
// After stripping: "paragraph\ndiv content"
|
|
116
|
+
// Select "paragraph" at offset 0-9
|
|
117
|
+
const result = extractContext({
|
|
118
|
+
content: html,
|
|
119
|
+
startOffset: 0,
|
|
120
|
+
endOffset: 9,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(result.lines.some((l) => l.includes(">>> paragraph <<<"))).toBe(
|
|
124
|
+
true,
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("normalizes CRLF to LF", () => {
|
|
129
|
+
const content = "line1\r\nline2\r\nline3";
|
|
130
|
+
// After normalization: "line1\nline2\nline3"
|
|
131
|
+
// Select "line2" at offset 6
|
|
132
|
+
const result = extractContext({ content, startOffset: 6, endOffset: 11 });
|
|
133
|
+
|
|
134
|
+
expect(result.startLine).toBe(2);
|
|
135
|
+
expect(result.lines.some((l) => l.includes(">>> line2 <<<"))).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("limits context lines to document bounds", () => {
|
|
139
|
+
const content = "only\ntwo\nlines";
|
|
140
|
+
// Select middle line with 5 context lines requested
|
|
141
|
+
const result = extractContext({
|
|
142
|
+
content,
|
|
143
|
+
startOffset: 5,
|
|
144
|
+
endOffset: 8,
|
|
145
|
+
contextLines: 5,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Should not go beyond document bounds
|
|
149
|
+
expect(result.lines.length).toBeLessThanOrEqual(3);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("truncates very long selections with ellipsis", () => {
|
|
153
|
+
// Create content with more than MAX_SELECTION_LINES (10) lines
|
|
154
|
+
const lines = Array.from({ length: 15 }, (_, i) => `line${i + 1}`);
|
|
155
|
+
const content = lines.join("\n");
|
|
156
|
+
// Select all content
|
|
157
|
+
const result = extractContext({
|
|
158
|
+
content,
|
|
159
|
+
startOffset: 0,
|
|
160
|
+
endOffset: content.length,
|
|
161
|
+
contextLines: 0,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Should include ... for truncated middle
|
|
165
|
+
expect(result.lines.some((l) => l === "...")).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("formatForLLM", () => {
|
|
170
|
+
it("formats context with header and line range", () => {
|
|
171
|
+
const context = {
|
|
172
|
+
lines: ["before", ">>> selected <<<", "after"],
|
|
173
|
+
startLine: 5,
|
|
174
|
+
endLine: 5,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const result = formatForLLM({ context, fileName: "test.md" });
|
|
178
|
+
|
|
179
|
+
expect(result).toContain("# From: test.md");
|
|
180
|
+
expect(result).toContain("Lines 5-5:");
|
|
181
|
+
expect(result).toContain("---");
|
|
182
|
+
expect(result).toContain(">>> selected <<<");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("includes optional comment", () => {
|
|
186
|
+
const context = {
|
|
187
|
+
lines: ["text"],
|
|
188
|
+
startLine: 1,
|
|
189
|
+
endLine: 1,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const result = formatForLLM({
|
|
193
|
+
context,
|
|
194
|
+
fileName: "test.md",
|
|
195
|
+
comment: "This needs review",
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(result).toContain("Comment: This needs review");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("omits comment section when not provided", () => {
|
|
202
|
+
const context = {
|
|
203
|
+
lines: ["text"],
|
|
204
|
+
startLine: 1,
|
|
205
|
+
endLine: 1,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const result = formatForLLM({ context, fileName: "test.md" });
|
|
209
|
+
|
|
210
|
+
expect(result).not.toContain("Comment:");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("formats multi-line range correctly", () => {
|
|
214
|
+
const context = {
|
|
215
|
+
lines: [">>> start", "middle", "end <<<"],
|
|
216
|
+
startLine: 10,
|
|
217
|
+
endLine: 12,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const result = formatForLLM({ context, fileName: "doc.html" });
|
|
221
|
+
|
|
222
|
+
expect(result).toContain("Lines 10-12:");
|
|
223
|
+
});
|
|
224
|
+
});
|