@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.
Files changed (322) hide show
  1. package/.agents/skills/remotion-best-practices/SKILL.md +61 -0
  2. package/.agents/skills/remotion-best-practices/rules/3d.md +86 -0
  3. package/.agents/skills/remotion-best-practices/rules/animations.md +27 -0
  4. package/.agents/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +178 -0
  5. package/.agents/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
  6. package/.agents/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
  7. package/.agents/skills/remotion-best-practices/rules/assets.md +78 -0
  8. package/.agents/skills/remotion-best-practices/rules/audio-visualization.md +198 -0
  9. package/.agents/skills/remotion-best-practices/rules/audio.md +169 -0
  10. package/.agents/skills/remotion-best-practices/rules/calculate-metadata.md +134 -0
  11. package/.agents/skills/remotion-best-practices/rules/can-decode.md +75 -0
  12. package/.agents/skills/remotion-best-practices/rules/charts.md +120 -0
  13. package/.agents/skills/remotion-best-practices/rules/compositions.md +154 -0
  14. package/.agents/skills/remotion-best-practices/rules/display-captions.md +184 -0
  15. package/.agents/skills/remotion-best-practices/rules/extract-frames.md +229 -0
  16. package/.agents/skills/remotion-best-practices/rules/ffmpeg.md +38 -0
  17. package/.agents/skills/remotion-best-practices/rules/fonts.md +152 -0
  18. package/.agents/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
  19. package/.agents/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
  20. package/.agents/skills/remotion-best-practices/rules/get-video-duration.md +60 -0
  21. package/.agents/skills/remotion-best-practices/rules/gifs.md +141 -0
  22. package/.agents/skills/remotion-best-practices/rules/images.md +134 -0
  23. package/.agents/skills/remotion-best-practices/rules/import-srt-captions.md +69 -0
  24. package/.agents/skills/remotion-best-practices/rules/light-leaks.md +73 -0
  25. package/.agents/skills/remotion-best-practices/rules/lottie.md +70 -0
  26. package/.agents/skills/remotion-best-practices/rules/maps.md +412 -0
  27. package/.agents/skills/remotion-best-practices/rules/measuring-dom-nodes.md +34 -0
  28. package/.agents/skills/remotion-best-practices/rules/measuring-text.md +140 -0
  29. package/.agents/skills/remotion-best-practices/rules/parameters.md +109 -0
  30. package/.agents/skills/remotion-best-practices/rules/sequencing.md +118 -0
  31. package/.agents/skills/remotion-best-practices/rules/sfx.md +26 -0
  32. package/.agents/skills/remotion-best-practices/rules/subtitles.md +36 -0
  33. package/.agents/skills/remotion-best-practices/rules/tailwind.md +11 -0
  34. package/.agents/skills/remotion-best-practices/rules/text-animations.md +20 -0
  35. package/.agents/skills/remotion-best-practices/rules/timing.md +179 -0
  36. package/.agents/skills/remotion-best-practices/rules/transcribe-captions.md +70 -0
  37. package/.agents/skills/remotion-best-practices/rules/transitions.md +197 -0
  38. package/.agents/skills/remotion-best-practices/rules/transparent-videos.md +106 -0
  39. package/.agents/skills/remotion-best-practices/rules/trimming.md +51 -0
  40. package/.agents/skills/remotion-best-practices/rules/videos.md +171 -0
  41. package/.agents/skills/remotion-best-practices/rules/voiceover.md +99 -0
  42. package/.agents/skills/simple/SKILL.md +52 -0
  43. package/.agents/skills/vercel-react-best-practices/AGENTS.md +3254 -0
  44. package/.agents/skills/vercel-react-best-practices/README.md +123 -0
  45. package/.agents/skills/vercel-react-best-practices/SKILL.md +141 -0
  46. package/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  47. package/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
  48. package/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
  49. package/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  50. package/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
  51. package/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
  52. package/.agents/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  53. package/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  54. package/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
  55. package/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  56. package/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  57. package/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  58. package/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  59. package/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  60. package/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
  61. package/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
  62. package/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  63. package/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
  64. package/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  65. package/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  66. package/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  67. package/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  68. package/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  69. package/.agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md +60 -0
  70. package/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  71. package/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  72. package/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  73. package/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  74. package/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  75. package/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  76. package/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  77. package/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  78. package/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  79. package/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  80. package/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  81. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  82. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  83. package/.agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md +85 -0
  84. package/.agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md +68 -0
  85. package/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  86. package/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  87. package/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  88. package/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  89. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  90. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  91. package/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  92. package/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  93. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  94. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  95. package/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  96. package/.agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md +82 -0
  97. package/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  98. package/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  99. package/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  100. package/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  101. package/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
  102. package/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  103. package/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
  104. package/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
  105. package/.agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md +142 -0
  106. package/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
  107. package/.agents/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  108. package/.claude/CLAUDE.md +142 -0
  109. package/.claude/commands/review.md +120 -0
  110. package/.claude/commands/sync-docs.md +71 -0
  111. package/.claude/roadmap.md +98 -0
  112. package/.claude/rules/style-guide.md +830 -0
  113. package/.claude/settings.json +18 -0
  114. package/.claude/user-stories.md +248 -0
  115. package/AGENTS.md +64 -0
  116. package/README.md +7 -7
  117. package/biome.json +69 -0
  118. package/bun.lock +1124 -0
  119. package/docs/design.md +563 -0
  120. package/docs/plans/2026-03-13-client-mode-design.md +86 -0
  121. package/docs/plans/2026-03-13-client-mode-plan.md +605 -0
  122. package/docs/plans/2026-03-13-keyboard-shortcuts-design.md +129 -0
  123. package/docs/plans/2026-03-13-keyboard-shortcuts-plan.md +1471 -0
  124. package/docs/plans/2026-03-13-multi-document-design.md +183 -0
  125. package/docs/plans/2026-03-13-performance-benchmarks-design.md +121 -0
  126. package/e2e/comments.spec.ts +125 -0
  127. package/e2e/document-load.spec.ts +54 -0
  128. package/e2e/export.spec.ts +58 -0
  129. package/e2e/fixtures/sample.html +13 -0
  130. package/e2e/fixtures/sample.md +7 -0
  131. package/e2e/persistence-file.spec.ts +342 -0
  132. package/e2e/utils/cli.ts +84 -0
  133. package/e2e/utils/selection.ts +135 -0
  134. package/{dist/index.html → index.html} +8 -2
  135. package/lefthook.yml +8 -0
  136. package/package.json +17 -39
  137. package/playwright.config.ts +22 -0
  138. package/skills-lock.json +20 -0
  139. package/src/App.tsx +396 -0
  140. package/src/cli/index.ts +467 -0
  141. package/src/components/ActionsMenu.tsx +110 -0
  142. package/src/components/DocumentViewer/CodeBlock.tsx +83 -0
  143. package/src/components/DocumentViewer/DocumentViewer.tsx +257 -0
  144. package/src/components/DocumentViewer/IframeContainer.tsx +251 -0
  145. package/src/components/DocumentViewer/MermaidDiagram.tsx +137 -0
  146. package/src/components/DocumentViewer/index.ts +1 -0
  147. package/src/components/FloatingTOC.tsx +59 -0
  148. package/src/components/Header.tsx +63 -0
  149. package/src/components/InlineEditor.tsx +72 -0
  150. package/src/components/MarginNote.tsx +198 -0
  151. package/src/components/MarginNotes.tsx +50 -0
  152. package/src/components/RawModal.tsx +141 -0
  153. package/src/components/ReanchorConfirm.tsx +33 -0
  154. package/src/components/SettingsModal.tsx +221 -0
  155. package/src/components/ShortcutCapture.tsx +45 -0
  156. package/src/components/ShortcutList.tsx +157 -0
  157. package/src/components/TabBar.tsx +60 -0
  158. package/src/components/TableOfContents.tsx +108 -0
  159. package/src/components/comments/CommentBadge.tsx +43 -0
  160. package/src/components/comments/CommentInput.tsx +119 -0
  161. package/src/components/comments/CommentListItem.tsx +82 -0
  162. package/src/components/comments/CommentManager.tsx +106 -0
  163. package/src/components/comments/CommentMinimap.tsx +62 -0
  164. package/src/components/comments/CommentNav.tsx +104 -0
  165. package/src/components/ui/ActionBar.tsx +16 -0
  166. package/src/components/ui/ActionLink.tsx +32 -0
  167. package/src/components/ui/Button.tsx +55 -0
  168. package/src/components/ui/Dialog.tsx +156 -0
  169. package/src/components/ui/DropdownMenu.tsx +114 -0
  170. package/src/components/ui/SeparatorDot.tsx +9 -0
  171. package/src/components/ui/Text.tsx +54 -0
  172. package/src/contexts/CommentContext.tsx +222 -0
  173. package/src/contexts/LayoutContext.tsx +76 -0
  174. package/src/hooks/useClickOutside.ts +35 -0
  175. package/src/hooks/useClipboard.ts +79 -0
  176. package/src/hooks/useCommentNavigation.ts +130 -0
  177. package/src/hooks/useComments.ts +323 -0
  178. package/src/hooks/useDocument.ts +141 -0
  179. package/src/hooks/useFontPreference.ts +76 -0
  180. package/src/hooks/useHeadings.test.ts +159 -0
  181. package/src/hooks/useHeadings.ts +129 -0
  182. package/src/hooks/useKeybindings.ts +120 -0
  183. package/src/hooks/useKeyboardShortcuts.ts +63 -0
  184. package/src/hooks/useLayoutMode.ts +44 -0
  185. package/src/hooks/useReanchorMode.ts +33 -0
  186. package/src/hooks/useScrollMetrics.ts +56 -0
  187. package/src/hooks/useScrollSpy.ts +81 -0
  188. package/src/hooks/useTextSelection.ts +123 -0
  189. package/src/hooks/useThemePreference.ts +66 -0
  190. package/src/index.css +823 -0
  191. package/src/lib/__fixtures__/bench-data.ts +167 -0
  192. package/src/lib/anchor.bench.ts +112 -0
  193. package/src/lib/anchor.test.ts +531 -0
  194. package/src/lib/anchor.ts +465 -0
  195. package/src/lib/comment-storage.bench.ts +63 -0
  196. package/src/lib/comment-storage.test.ts +624 -0
  197. package/src/lib/comment-storage.ts +263 -0
  198. package/src/lib/context.bench.ts +41 -0
  199. package/src/lib/context.test.ts +224 -0
  200. package/src/lib/context.ts +193 -0
  201. package/src/lib/export.bench.ts +35 -0
  202. package/src/lib/export.ts +43 -0
  203. package/src/lib/highlight/colors.ts +37 -0
  204. package/src/lib/highlight/core.test.ts +98 -0
  205. package/src/lib/highlight/core.ts +54 -0
  206. package/src/lib/highlight/dom.ts +342 -0
  207. package/src/lib/highlight/highlighter.ts +427 -0
  208. package/src/lib/highlight/index.ts +23 -0
  209. package/src/lib/highlight/script-builder.ts +485 -0
  210. package/src/lib/highlight/types.ts +57 -0
  211. package/src/lib/html-processor.test.tsx +170 -0
  212. package/src/lib/html-processor.tsx +95 -0
  213. package/src/lib/layout-constants.ts +12 -0
  214. package/src/lib/margin-layout.bench.ts +28 -0
  215. package/src/lib/margin-layout.ts +100 -0
  216. package/src/lib/scroll.test.ts +118 -0
  217. package/src/lib/scroll.ts +47 -0
  218. package/src/lib/shortcut-registry.test.ts +173 -0
  219. package/src/lib/shortcut-registry.ts +209 -0
  220. package/src/lib/utils.test.ts +110 -0
  221. package/src/lib/utils.ts +61 -0
  222. package/src/main.tsx +10 -0
  223. package/src/server/index.ts +883 -0
  224. package/src/store/index.test.ts +220 -0
  225. package/src/store/index.ts +234 -0
  226. package/src/test-setup.ts +1 -0
  227. package/src/types/index.ts +115 -0
  228. package/test.md +74 -0
  229. package/tsconfig.cli.json +12 -0
  230. package/tsconfig.json +20 -0
  231. package/vite.config.ts +19 -0
  232. package/vitest.config.ts +15 -0
  233. package/dist/assets/_basePickBy-hOr-yGsE.js +0 -1
  234. package/dist/assets/_baseUniq-b7bzdUSn.js +0 -1
  235. package/dist/assets/arc-D65wG9gm.js +0 -1
  236. package/dist/assets/architecture-PBZL5I3N-DBa6CAv_.js +0 -1
  237. package/dist/assets/architectureDiagram-2XIMDMQ5-Djwpsh98.js +0 -36
  238. package/dist/assets/array-DOVTz2Mq.js +0 -1
  239. package/dist/assets/blockDiagram-WCTKOSBZ-BdW5TTxj.js +0 -132
  240. package/dist/assets/c4Diagram-IC4MRINW-DTmkHEXu.js +0 -10
  241. package/dist/assets/channel-B3MUFipN.js +0 -1
  242. package/dist/assets/chunk-4BX2VUAB-DEqzsvDc.js +0 -1
  243. package/dist/assets/chunk-55IACEB6-BzVuSUV8.js +0 -1
  244. package/dist/assets/chunk-7E7YKBS2-CZ8IcA4c.js +0 -1
  245. package/dist/assets/chunk-7R4GIKGN-CWVVC8HX.js +0 -79
  246. package/dist/assets/chunk-C72U2L5F-B1Tso5TH.js +0 -1
  247. package/dist/assets/chunk-EGIJ26TM-Cx_7CFik.js +0 -1
  248. package/dist/assets/chunk-FMBD7UC4-Cfk_iGhv.js +0 -15
  249. package/dist/assets/chunk-GEFDOKGD-C_5hRbJt.js +0 -2
  250. package/dist/assets/chunk-GLR3WWYH-CkY7IyBj.js +0 -2
  251. package/dist/assets/chunk-HHEYEP7N-B0I4X5cr.js +0 -1
  252. package/dist/assets/chunk-JSJVCQXG-CAjwlVLg.js +0 -1
  253. package/dist/assets/chunk-KX2RTZJC-DWqnZZ02.js +0 -1
  254. package/dist/assets/chunk-KYZI473N-gjRVhJgJ.js +0 -53
  255. package/dist/assets/chunk-L3YUKLVL-D7C9GuxL.js +0 -1
  256. package/dist/assets/chunk-MX3YWQON-i-77iuVj.js +0 -1
  257. package/dist/assets/chunk-NQ4KR5QH-B22Pvemm.js +0 -220
  258. package/dist/assets/chunk-O4XLMI2P-ZQd5L6ZD.js +0 -7
  259. package/dist/assets/chunk-OZEHJAEY-BaPKTELw.js +0 -1
  260. package/dist/assets/chunk-PQ6SQG4A-DqE1eupT.js +0 -1
  261. package/dist/assets/chunk-PU5JKC2W-BTqWqedh.js +0 -70
  262. package/dist/assets/chunk-QZHKN3VN-Nm9TvMss.js +0 -1
  263. package/dist/assets/chunk-R5LLSJPH-DkiNs1dN.js +0 -1
  264. package/dist/assets/chunk-WL4C6EOR-CioD2fv2.js +0 -189
  265. package/dist/assets/chunk-XIRO2GV7-B4GGQONY.js +0 -1
  266. package/dist/assets/chunk-XPW4576I-C0IbbQos.js +0 -32
  267. package/dist/assets/chunk-XZSTWKYB-DMOqFWmT.js +0 -94
  268. package/dist/assets/chunk-YBOYWFTD-CoeQgeVY.js +0 -1
  269. package/dist/assets/classDiagram-VBA2DB6C-DV9ltQ7h.js +0 -1
  270. package/dist/assets/classDiagram-v2-RAHNMMFH-C6nD9wmM.js +0 -1
  271. package/dist/assets/clone-DuY6BQEm.js +0 -1
  272. package/dist/assets/cose-bilkent-S5V4N54A-B6FexK6p.js +0 -1
  273. package/dist/assets/cytoscape.esm-DoTFyJaN.js +0 -321
  274. package/dist/assets/dagre-CCcocoCU.js +0 -1
  275. package/dist/assets/dagre-KLK3FWXG-DIELowj9.js +0 -4
  276. package/dist/assets/defaultLocale-Ck2Xxk-C.js +0 -1
  277. package/dist/assets/diagram-E7M64L7V-D1mm0PoO.js +0 -24
  278. package/dist/assets/diagram-IFDJBPK2-7DVjly8y.js +0 -43
  279. package/dist/assets/diagram-P4PSJMXO-jO7pfyMb.js +0 -24
  280. package/dist/assets/dist-BywRdrPx.js +0 -1
  281. package/dist/assets/erDiagram-INFDFZHY-DSRxlRFy.js +0 -70
  282. package/dist/assets/flowDiagram-PKNHOUZH-CgKzzNdR.js +0 -162
  283. package/dist/assets/ganttDiagram-A5KZAMGK-CtsE7Y4E.js +0 -292
  284. package/dist/assets/gitGraph-HDMCJU4V-BU9uhwtz.js +0 -1
  285. package/dist/assets/gitGraphDiagram-K3NZZRJ6-DOU8RGdw.js +0 -65
  286. package/dist/assets/graphlib-WkJoBgka.js +0 -1
  287. package/dist/assets/index-CKVArt9D.js +0 -562
  288. package/dist/assets/index-DzRKJazf.css +0 -2
  289. package/dist/assets/info-3K5VOQVL-CPpvM-SG.js +0 -1
  290. package/dist/assets/infoDiagram-LFFYTUFH-VKLs5DsF.js +0 -2
  291. package/dist/assets/init-Bft5Ffpj.js +0 -1
  292. package/dist/assets/isArrayLikeObject-icl0H0jo.js +0 -1
  293. package/dist/assets/isEmpty-Du8sNmkE.js +0 -1
  294. package/dist/assets/ishikawaDiagram-PHBUUO56-CsWvEjux.js +0 -70
  295. package/dist/assets/journeyDiagram-4ABVD52K-BzJGTdIT.js +0 -139
  296. package/dist/assets/kanban-definition-K7BYSVSG-B_9ClJ1A.js +0 -89
  297. package/dist/assets/katex-BJrMXEjr.js +0 -261
  298. package/dist/assets/line-CC_tDGId.js +0 -1
  299. package/dist/assets/linear-Cts_d04Y.js +0 -1
  300. package/dist/assets/math-CNhlSIO3.js +0 -1
  301. package/dist/assets/mermaid-parser.core-Vb9KKv1R.js +0 -4
  302. package/dist/assets/mermaid.core-C_7xsp3d.js +0 -11
  303. package/dist/assets/mindmap-definition-YRQLILUH-BWmfy5wB.js +0 -68
  304. package/dist/assets/ordinal-DIg8h6NI.js +0 -1
  305. package/dist/assets/packet-RMMSAZCW-Q-WG6o3b.js +0 -1
  306. package/dist/assets/path-DfRbCp9y.js +0 -1
  307. package/dist/assets/pie-UPGHQEXC-Cwi2tLlt.js +0 -1
  308. package/dist/assets/pieDiagram-SKSYHLDU-Dyf3X_in.js +0 -30
  309. package/dist/assets/quadrantDiagram-337W2JSQ-B5_5m61Q.js +0 -7
  310. package/dist/assets/radar-KQ55EAFF-Dtw2VzxY.js +0 -1
  311. package/dist/assets/requirementDiagram-Z7DCOOCP-BSERBnlW.js +0 -73
  312. package/dist/assets/rough.esm-KjoEK0it.js +0 -1
  313. package/dist/assets/sankeyDiagram-WA2Y5GQK-CMcEY8Cz.js +0 -10
  314. package/dist/assets/sequenceDiagram-2WXFIKYE-D28qcXwC.js +0 -145
  315. package/dist/assets/src-C8kkzlHX.js +0 -1
  316. package/dist/assets/stateDiagram-RAJIS63D-7oVrCmRl.js +0 -1
  317. package/dist/assets/stateDiagram-v2-FVOUBMTO-DtFptQAd.js +0 -1
  318. package/dist/assets/timeline-definition-YZTLITO2-rbCfBEvG.js +0 -61
  319. package/dist/assets/treemap-KZPCXAKY-BlRvF0um.js +0 -1
  320. package/dist/assets/vennDiagram-LZ73GAT5-DBit3zWa.js +0 -34
  321. package/dist/assets/xychartDiagram-JWTSCODW-BVYXv51y.js +0 -7
  322. package/dist/index.js +0 -1040
package/dist/index.js DELETED
@@ -1,1040 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/cli/index.ts
4
- import { existsSync, readdirSync, readFileSync, statSync } from "fs";
5
- import * as fs2 from "fs/promises";
6
- import * as os3 from "os";
7
- import { join as join4, resolve as resolve3 } from "path";
8
- import { Command } from "commander";
9
- import open from "open";
10
-
11
- // src/lib/comment-storage.ts
12
- import * as crypto from "crypto";
13
- import * as os from "os";
14
- import * as path from "path";
15
- var FORMAT_VERSION = 1;
16
- var HASH_LENGTH = 16;
17
- var MAX_SELECTION_LENGTH = 1e3;
18
- var TRUNCATION_MARKER = "\n...\n";
19
- var ANCHOR_PREFIX_LENGTH = 200;
20
- function truncateSelection(text) {
21
- if (text.length <= MAX_SELECTION_LENGTH) {
22
- return text;
23
- }
24
- const half = Math.floor(
25
- (MAX_SELECTION_LENGTH - TRUNCATION_MARKER.length) / 2
26
- );
27
- return text.slice(0, half) + TRUNCATION_MARKER + text.slice(-half);
28
- }
29
- function getCommentPath(sourcePath) {
30
- const absolute = path.resolve(sourcePath);
31
- const normalized = absolute.replace(/^\//, "").replace(/^[A-Z]:[\\/]/, "");
32
- const ext = path.extname(normalized);
33
- const withoutExt = normalized.slice(0, -ext.length || void 0);
34
- return path.join(
35
- os.homedir(),
36
- ".readit",
37
- "comments",
38
- `${withoutExt}.comments.md`
39
- );
40
- }
41
- function computeHash(content) {
42
- return crypto.createHash("sha256").update(content).digest("hex").slice(0, HASH_LENGTH);
43
- }
44
- function getLineNumber(content, offset) {
45
- if (offset <= 0 || content.length === 0) return 1;
46
- const clampedOffset = Math.min(offset, content.length);
47
- return content.slice(0, clampedOffset).split("\n").length;
48
- }
49
- function getLineHint(content, startOffset, endOffset) {
50
- const startLine = getLineNumber(content, startOffset);
51
- const endLine = getLineNumber(content, endOffset);
52
- return startLine === endLine ? `L${startLine}` : `L${startLine}-${endLine}`;
53
- }
54
- function parseCommentFile(content) {
55
- const result = {
56
- source: "",
57
- hash: "",
58
- version: FORMAT_VERSION,
59
- comments: []
60
- };
61
- if (!content.trim()) {
62
- return result;
63
- }
64
- const frontMatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
65
- if (frontMatterMatch) {
66
- const frontMatter = frontMatterMatch[1];
67
- const sourceMatch = frontMatter.match(/^source:\s*(.+)$/m);
68
- const hashMatch = frontMatter.match(/^hash:\s*(.+)$/m);
69
- const versionMatch = frontMatter.match(/^version:\s*(\d+)$/m);
70
- if (sourceMatch) result.source = sourceMatch[1].trim();
71
- if (hashMatch) result.hash = hashMatch[1].trim();
72
- if (versionMatch) result.version = Number.parseInt(versionMatch[1], 10);
73
- if (result.version > FORMAT_VERSION) {
74
- throw new Error(
75
- `Comment file requires readit v${result.version} or higher. Current version supports format v${FORMAT_VERSION}.`
76
- );
77
- }
78
- }
79
- const bodyContent = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
80
- const blocks = bodyContent.split(/\n---\n/).filter((block) => block.trim());
81
- for (const block of blocks) {
82
- const comment = parseCommentBlock(block);
83
- if (comment) {
84
- result.comments.push(comment);
85
- }
86
- }
87
- return result;
88
- }
89
- function parseCommentBlock(block) {
90
- const metadataMatch = block.match(/<!--\s*c:([^|]+)\|([^|]+)\|([^>]+)\s*-->/);
91
- if (!metadataMatch) {
92
- return void 0;
93
- }
94
- const [, id, lineHint, createdAt] = metadataMatch;
95
- const anchorMatch = block.match(/<!--\s*anchor:(.+?)\s*-->/);
96
- const anchorPrefix = anchorMatch ? anchorMatch[1] : void 0;
97
- const blockquoteMatch = block.match(/^>\s*(.+(?:\n>\s*.+)*)$/m);
98
- if (!blockquoteMatch) {
99
- return void 0;
100
- }
101
- const selectedText = blockquoteMatch[1].split("\n").map((line) => line.replace(/^>\s*/, "")).join("\n");
102
- const afterBlockquote = block.slice(
103
- block.indexOf(blockquoteMatch[0]) + blockquoteMatch[0].length
104
- );
105
- const commentBody = afterBlockquote.trim();
106
- return {
107
- id,
108
- selectedText,
109
- comment: commentBody,
110
- createdAt: createdAt.trim(),
111
- lineHint,
112
- anchorPrefix,
113
- // Offsets will be resolved by anchor matching when loading
114
- startOffset: 0,
115
- endOffset: 0
116
- };
117
- }
118
- function serializeComments(file) {
119
- const lines = [];
120
- lines.push("---");
121
- lines.push(`source: ${file.source}`);
122
- lines.push(`hash: ${file.hash}`);
123
- lines.push(`version: ${file.version}`);
124
- lines.push("---");
125
- lines.push("");
126
- for (const comment of file.comments) {
127
- lines.push(serializeComment(comment));
128
- lines.push("");
129
- lines.push("---");
130
- lines.push("");
131
- }
132
- return lines.join("\n");
133
- }
134
- function serializeComment(comment) {
135
- const lines = [];
136
- const lineHint = comment.lineHint || "L0";
137
- lines.push(`<!-- c:${comment.id}|${lineHint}|${comment.createdAt} -->`);
138
- if (comment.anchorPrefix) {
139
- lines.push(`<!-- anchor:${comment.anchorPrefix} -->`);
140
- }
141
- const quotedLines = comment.selectedText.split("\n").map((line) => `> ${line}`);
142
- lines.push(...quotedLines);
143
- if (comment.comment) {
144
- lines.push("");
145
- lines.push(comment.comment);
146
- }
147
- return lines.join("\n");
148
- }
149
- function createComment(selectedText, commentText, startOffset, endOffset, sourceContent) {
150
- const id = crypto.randomUUID().slice(0, 8);
151
- const lineHint = getLineHint(sourceContent, startOffset, endOffset);
152
- const now = /* @__PURE__ */ new Date();
153
- const createdAt = now.toISOString();
154
- const needsTruncation = selectedText.length > MAX_SELECTION_LENGTH;
155
- return {
156
- id,
157
- selectedText: truncateSelection(selectedText),
158
- comment: commentText,
159
- createdAt,
160
- startOffset,
161
- endOffset,
162
- lineHint,
163
- // Store first N chars for anchor matching when text is truncated
164
- anchorPrefix: needsTruncation ? selectedText.slice(0, ANCHOR_PREFIX_LENGTH) : void 0
165
- };
166
- }
167
-
168
- // src/server/index.ts
169
- import { watch } from "fs";
170
- import * as fs from "fs/promises";
171
- import * as os2 from "os";
172
- import * as path2 from "path";
173
- import { basename, dirname, join as join3 } from "path";
174
- import { fileURLToPath } from "url";
175
- import express from "express";
176
-
177
- // src/types/index.ts
178
- var AnchorConfidences = {
179
- EXACT: "exact",
180
- NORMALIZED: "normalized",
181
- FUZZY: "fuzzy",
182
- UNRESOLVED: "unresolved"
183
- };
184
- var FontFamilies = {
185
- SERIF: "serif",
186
- SANS_SERIF: "sans-serif"
187
- };
188
-
189
- // src/lib/anchor.ts
190
- var DEFAULT_SEARCH_WINDOW = 500;
191
- var DEFAULT_FUZZY_THRESHOLD = 5;
192
- var MAX_FUZZY_TEXT_LENGTH = 200;
193
- var FUZZY_SEARCH_WINDOW = 2e3;
194
- function normalizeWhitespace(text) {
195
- return text.replace(/\s+/g, " ").trim();
196
- }
197
- function levenshteinDistance(a, b, maxDistance) {
198
- if (a.length > b.length) {
199
- [a, b] = [b, a];
200
- }
201
- const m = a.length;
202
- const n = b.length;
203
- if (m === 0) return n;
204
- if (n === 0) return m;
205
- if (maxDistance !== void 0 && Math.abs(m - n) > maxDistance) {
206
- return Number.POSITIVE_INFINITY;
207
- }
208
- let prevRow = new Array(m + 1);
209
- let currRow = new Array(m + 1);
210
- for (let i = 0; i <= m; i++) {
211
- prevRow[i] = i;
212
- }
213
- for (let j = 1; j <= n; j++) {
214
- currRow[0] = j;
215
- let rowMin = currRow[0];
216
- for (let i = 1; i <= m; i++) {
217
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
218
- currRow[i] = Math.min(
219
- prevRow[i] + 1,
220
- // deletion
221
- currRow[i - 1] + 1,
222
- // insertion
223
- prevRow[i - 1] + cost
224
- // substitution
225
- );
226
- rowMin = Math.min(rowMin, currRow[i]);
227
- }
228
- if (maxDistance !== void 0 && rowMin > maxDistance) {
229
- return Number.POSITIVE_INFINITY;
230
- }
231
- [prevRow, currRow] = [currRow, prevRow];
232
- }
233
- return prevRow[m];
234
- }
235
- function getLineOffset(content, lineNumber) {
236
- if (lineNumber <= 1) return 0;
237
- let currentLine = 1;
238
- for (let i = 0; i < content.length; i++) {
239
- if (content[i] === "\n") {
240
- currentLine++;
241
- if (currentLine === lineNumber) {
242
- return i + 1;
243
- }
244
- }
245
- }
246
- return content.length;
247
- }
248
- function parseLineHint(lineHint) {
249
- const match = lineHint.match(/^L(\d+)(?:-(\d+))?$/);
250
- if (!match) {
251
- return { start: 1, end: 1 };
252
- }
253
- const start = Number.parseInt(match[1], 10);
254
- const end = match[2] ? Number.parseInt(match[2], 10) : start;
255
- return { start, end };
256
- }
257
- function findAnchor({
258
- source,
259
- selectedText,
260
- lineHint,
261
- searchWindow = DEFAULT_SEARCH_WINDOW
262
- }) {
263
- if (!selectedText || !source) {
264
- return void 0;
265
- }
266
- const { start: hintLine } = parseLineHint(lineHint);
267
- const lineOffset = getLineOffset(source, hintLine);
268
- const windowStart = Math.max(0, lineOffset - searchWindow);
269
- const windowEnd = Math.min(source.length, lineOffset + searchWindow);
270
- const window = source.slice(windowStart, windowEnd);
271
- const localIndex = window.indexOf(selectedText);
272
- if (localIndex !== -1) {
273
- const start = windowStart + localIndex;
274
- const end = start + selectedText.length;
275
- return {
276
- start,
277
- end,
278
- line: getLineNumber(source, start),
279
- confidence: AnchorConfidences.EXACT
280
- };
281
- }
282
- const globalIndex = source.indexOf(selectedText);
283
- if (globalIndex !== -1) {
284
- return {
285
- start: globalIndex,
286
- end: globalIndex + selectedText.length,
287
- line: getLineNumber(source, globalIndex),
288
- confidence: AnchorConfidences.EXACT
289
- };
290
- }
291
- return void 0;
292
- }
293
- function buildNormalizedPositionMap(text) {
294
- const toOriginal = [];
295
- let normalized = "";
296
- let inWhitespace = false;
297
- for (let i = 0; i < text.length; i++) {
298
- const char = text[i];
299
- const isSpace = /\s/.test(char);
300
- if (isSpace) {
301
- if (!inWhitespace && normalized.length > 0) {
302
- normalized += " ";
303
- toOriginal.push(i);
304
- }
305
- inWhitespace = true;
306
- } else {
307
- normalized += char;
308
- toOriginal.push(i);
309
- inWhitespace = false;
310
- }
311
- }
312
- if (normalized.endsWith(" ")) {
313
- normalized = normalized.slice(0, -1);
314
- toOriginal.pop();
315
- }
316
- return { normalized, toOriginal };
317
- }
318
- function findAnchorNormalized({
319
- source,
320
- selectedText,
321
- lineHint,
322
- searchWindow = FUZZY_SEARCH_WINDOW
323
- }) {
324
- if (!selectedText || !source) {
325
- return void 0;
326
- }
327
- const normalizedText = normalizeWhitespace(selectedText);
328
- if (!normalizedText) {
329
- return void 0;
330
- }
331
- if (normalizedText === selectedText) {
332
- return void 0;
333
- }
334
- const { start: hintLine } = parseLineHint(lineHint);
335
- const lineOffset = getLineOffset(source, hintLine);
336
- const windowStart = Math.max(0, lineOffset - searchWindow);
337
- const windowEnd = Math.min(source.length, lineOffset + searchWindow);
338
- const window = source.slice(windowStart, windowEnd);
339
- const { normalized: normalizedWindow, toOriginal } = buildNormalizedPositionMap(window);
340
- const normalizedIndex = normalizedWindow.indexOf(normalizedText);
341
- if (normalizedIndex !== -1) {
342
- const originalStart = windowStart + toOriginal[normalizedIndex];
343
- const endNormIndex = normalizedIndex + normalizedText.length - 1;
344
- let originalEnd = windowStart + toOriginal[endNormIndex] + 1;
345
- while (originalEnd < source.length && /\s/.test(source[originalEnd])) {
346
- originalEnd++;
347
- }
348
- return {
349
- start: originalStart,
350
- end: originalEnd,
351
- line: getLineNumber(source, originalStart),
352
- confidence: AnchorConfidences.NORMALIZED
353
- };
354
- }
355
- const { normalized: fullNormalized, toOriginal: fullToOriginal } = buildNormalizedPositionMap(source);
356
- const globalIndex = fullNormalized.indexOf(normalizedText);
357
- if (globalIndex !== -1) {
358
- const originalStart = fullToOriginal[globalIndex];
359
- const endNormIndex = globalIndex + normalizedText.length - 1;
360
- let originalEnd = fullToOriginal[endNormIndex] + 1;
361
- while (originalEnd < source.length && /\s/.test(source[originalEnd])) {
362
- originalEnd++;
363
- }
364
- return {
365
- start: originalStart,
366
- end: originalEnd,
367
- line: getLineNumber(source, originalStart),
368
- confidence: AnchorConfidences.NORMALIZED
369
- };
370
- }
371
- return void 0;
372
- }
373
- function findAnchorFuzzy({
374
- source,
375
- selectedText,
376
- lineHint,
377
- threshold = DEFAULT_FUZZY_THRESHOLD
378
- }) {
379
- if (!selectedText || !source) {
380
- return void 0;
381
- }
382
- const textLen = selectedText.length;
383
- if (textLen > MAX_FUZZY_TEXT_LENGTH) {
384
- return void 0;
385
- }
386
- let bestMatch;
387
- let bestDistance = threshold + 1;
388
- let searchStart = 0;
389
- let searchEnd = source.length;
390
- if (lineHint) {
391
- const { start: hintLine } = parseLineHint(lineHint);
392
- const lineOffset = getLineOffset(source, hintLine);
393
- searchStart = Math.max(0, lineOffset - FUZZY_SEARCH_WINDOW);
394
- searchEnd = Math.min(source.length, lineOffset + FUZZY_SEARCH_WINDOW);
395
- }
396
- const minLen = Math.max(1, textLen - threshold);
397
- const maxLen = textLen + threshold;
398
- for (let len = minLen; len <= maxLen; len++) {
399
- for (let i = searchStart; i <= searchEnd - len; i++) {
400
- const candidate = source.slice(i, i + len);
401
- const distance = levenshteinDistance(
402
- selectedText,
403
- candidate,
404
- bestDistance - 1
405
- );
406
- if (distance < bestDistance) {
407
- bestDistance = distance;
408
- bestMatch = {
409
- start: i,
410
- end: i + len,
411
- line: getLineNumber(source, i),
412
- confidence: AnchorConfidences.FUZZY,
413
- distance
414
- };
415
- if (distance === 0) {
416
- return bestMatch;
417
- }
418
- }
419
- }
420
- }
421
- return bestMatch;
422
- }
423
- function findAnchorWithFallback({
424
- source,
425
- selectedText,
426
- lineHint,
427
- fuzzyThreshold
428
- }) {
429
- const exactMatch = findAnchor({ source, selectedText, lineHint });
430
- if (exactMatch) {
431
- return exactMatch;
432
- }
433
- const normalizedMatch = findAnchorNormalized({
434
- source,
435
- selectedText,
436
- lineHint
437
- });
438
- if (normalizedMatch) {
439
- return normalizedMatch;
440
- }
441
- return findAnchorFuzzy({
442
- source,
443
- selectedText,
444
- lineHint,
445
- threshold: fuzzyThreshold
446
- });
447
- }
448
-
449
- // src/server/index.ts
450
- var __dirname = dirname(fileURLToPath(import.meta.url));
451
- function isErrnoException(err) {
452
- return err instanceof Error && "code" in err;
453
- }
454
- async function readCommentsFromFile(filePath, sourceContent) {
455
- const commentPath = getCommentPath(filePath);
456
- try {
457
- const content = await fs.readFile(commentPath, "utf-8");
458
- const file = parseCommentFile(content);
459
- return file.comments.map((comment) => {
460
- const textForMatching = comment.anchorPrefix || comment.selectedText;
461
- const anchor = findAnchorWithFallback({
462
- source: sourceContent,
463
- selectedText: textForMatching,
464
- lineHint: comment.lineHint || "L1"
465
- });
466
- if (anchor) {
467
- return {
468
- ...comment,
469
- startOffset: anchor.start,
470
- endOffset: anchor.end,
471
- lineHint: `L${anchor.line}`,
472
- anchorConfidence: anchor.confidence
473
- };
474
- }
475
- return {
476
- ...comment,
477
- anchorConfidence: AnchorConfidences.UNRESOLVED
478
- };
479
- });
480
- } catch (err) {
481
- if (isErrnoException(err) && err.code === "ENOENT") {
482
- return [];
483
- }
484
- throw err;
485
- }
486
- }
487
- async function writeCommentsToFile(filePath, sourceContent, comments) {
488
- const commentPath = getCommentPath(filePath);
489
- const commentDir = dirname(commentPath);
490
- await fs.mkdir(commentDir, { recursive: true });
491
- const file = {
492
- source: filePath,
493
- hash: computeHash(sourceContent),
494
- version: 1,
495
- comments
496
- };
497
- const content = serializeComments(file);
498
- const tempPath = `${commentPath}.tmp`;
499
- await fs.writeFile(tempPath, content, "utf-8");
500
- await fs.rename(tempPath, commentPath);
501
- }
502
- async function deleteCommentFile(filePath) {
503
- const commentPath = getCommentPath(filePath);
504
- try {
505
- await fs.unlink(commentPath);
506
- } catch (err) {
507
- if (!isErrnoException(err) || err.code !== "ENOENT") {
508
- throw err;
509
- }
510
- }
511
- }
512
- function getSettingsPath(sourcePath) {
513
- const absolute = path2.resolve(sourcePath);
514
- const normalized = absolute.replace(/^\//, "").replace(/^[A-Z]:[\\/]/, "");
515
- return path2.join(
516
- os2.homedir(),
517
- ".readit",
518
- "settings",
519
- `${normalized}.settings.json`
520
- );
521
- }
522
- var DEFAULT_SETTINGS = {
523
- version: 1,
524
- fontFamily: FontFamilies.SERIF
525
- };
526
- async function readSettingsFromFile(filePath) {
527
- const settingsPath = getSettingsPath(filePath);
528
- try {
529
- const content = await fs.readFile(settingsPath, "utf-8");
530
- return JSON.parse(content);
531
- } catch (err) {
532
- if (isErrnoException(err) && err.code === "ENOENT") {
533
- return DEFAULT_SETTINGS;
534
- }
535
- throw err;
536
- }
537
- }
538
- async function writeSettingsToFile(filePath, settings) {
539
- const settingsPath = getSettingsPath(filePath);
540
- const settingsDir = dirname(settingsPath);
541
- await fs.mkdir(settingsDir, { recursive: true });
542
- const tempPath = `${settingsPath}.tmp`;
543
- await fs.writeFile(tempPath, JSON.stringify(settings, null, 2), "utf-8");
544
- await fs.rename(tempPath, settingsPath);
545
- }
546
- function isValidFontFamily(value) {
547
- return value === FontFamilies.SERIF || value === FontFamilies.SANS_SERIF;
548
- }
549
- async function handleGetComments(ctx, res) {
550
- try {
551
- const comments = await readCommentsFromFile(
552
- ctx.filePath,
553
- ctx.getCurrentContent()
554
- );
555
- res.json({ comments });
556
- } catch (err) {
557
- console.error("Failed to read comments:", err);
558
- res.status(500).json({ error: "Failed to read comments" });
559
- }
560
- }
561
- async function handleAddComment(ctx, req, res) {
562
- try {
563
- const {
564
- selectedText,
565
- comment: commentText,
566
- startOffset,
567
- endOffset
568
- } = req.body;
569
- if (!selectedText || typeof commentText !== "string" || startOffset === void 0 || endOffset === void 0) {
570
- res.status(400).json({ error: "Missing required fields" });
571
- return;
572
- }
573
- const currentContent = ctx.getCurrentContent();
574
- const newComment = createComment(
575
- selectedText,
576
- commentText,
577
- startOffset,
578
- endOffset,
579
- currentContent
580
- );
581
- const existingComments = await readCommentsFromFile(
582
- ctx.filePath,
583
- currentContent
584
- );
585
- const allComments = [...existingComments, newComment];
586
- await writeCommentsToFile(ctx.filePath, currentContent, allComments);
587
- res.status(201).json({ comment: newComment });
588
- } catch (err) {
589
- console.error("Failed to add comment:", err);
590
- res.status(500).json({ error: "Failed to add comment" });
591
- }
592
- }
593
- async function handleUpdateComment(ctx, req, res) {
594
- try {
595
- const { id } = req.params;
596
- const { comment: commentText } = req.body;
597
- if (typeof commentText !== "string") {
598
- res.status(400).json({ error: "Missing comment text" });
599
- return;
600
- }
601
- const currentContent = ctx.getCurrentContent();
602
- const existingComments = await readCommentsFromFile(
603
- ctx.filePath,
604
- currentContent
605
- );
606
- const commentIndex = existingComments.findIndex((c) => c.id === id);
607
- if (commentIndex === -1) {
608
- res.status(404).json({ error: "Comment not found" });
609
- return;
610
- }
611
- const updatedComments = existingComments.map(
612
- (c, i) => i === commentIndex ? { ...c, comment: commentText.trim() } : c
613
- );
614
- await writeCommentsToFile(ctx.filePath, currentContent, updatedComments);
615
- res.json({ comment: updatedComments[commentIndex] });
616
- } catch (err) {
617
- console.error("Failed to update comment:", err);
618
- res.status(500).json({ error: "Failed to update comment" });
619
- }
620
- }
621
- async function handleDeleteComment(ctx, req, res) {
622
- try {
623
- const { id } = req.params;
624
- const currentContent = ctx.getCurrentContent();
625
- const existingComments = await readCommentsFromFile(
626
- ctx.filePath,
627
- currentContent
628
- );
629
- const filteredComments = existingComments.filter((c) => c.id !== id);
630
- if (filteredComments.length === existingComments.length) {
631
- res.status(404).json({ error: "Comment not found" });
632
- return;
633
- }
634
- if (filteredComments.length === 0) {
635
- await deleteCommentFile(ctx.filePath);
636
- } else {
637
- await writeCommentsToFile(ctx.filePath, currentContent, filteredComments);
638
- }
639
- res.json({ success: true });
640
- } catch (err) {
641
- console.error("Failed to delete comment:", err);
642
- res.status(500).json({ error: "Failed to delete comment" });
643
- }
644
- }
645
- async function handleClearComments(ctx, res) {
646
- try {
647
- await deleteCommentFile(ctx.filePath);
648
- res.json({ success: true });
649
- } catch (err) {
650
- console.error("Failed to clear comments:", err);
651
- res.status(500).json({ error: "Failed to clear comments" });
652
- }
653
- }
654
- async function handleGetRawComments(ctx, res) {
655
- const commentPath = getCommentPath(ctx.filePath);
656
- try {
657
- const content = await fs.readFile(commentPath, "utf-8");
658
- res.json({ content, path: commentPath });
659
- } catch (err) {
660
- if (isErrnoException(err) && err.code === "ENOENT") {
661
- res.json({ content: null, path: commentPath });
662
- return;
663
- }
664
- console.error("Failed to read raw comments:", err);
665
- res.status(500).json({ error: "Failed to read raw comments" });
666
- }
667
- }
668
- async function handleReanchorComment(ctx, req, res) {
669
- try {
670
- const { id } = req.params;
671
- const { selectedText, startOffset, endOffset } = req.body;
672
- if (!selectedText || startOffset === void 0 || endOffset === void 0) {
673
- res.status(400).json({ error: "Missing required fields" });
674
- return;
675
- }
676
- const currentContent = ctx.getCurrentContent();
677
- const existingComments = await readCommentsFromFile(
678
- ctx.filePath,
679
- currentContent
680
- );
681
- const commentIndex = existingComments.findIndex((c) => c.id === id);
682
- if (commentIndex === -1) {
683
- res.status(404).json({ error: "Comment not found" });
684
- return;
685
- }
686
- const lineHint = getLineHint(currentContent, startOffset, endOffset);
687
- const truncatedText = truncateSelection(selectedText);
688
- const updatedComment = {
689
- ...existingComments[commentIndex],
690
- selectedText: truncatedText,
691
- startOffset,
692
- endOffset,
693
- lineHint,
694
- anchorConfidence: AnchorConfidences.EXACT,
695
- // Store anchor prefix for future matching if text was truncated
696
- anchorPrefix: selectedText.length > 1e3 ? selectedText.slice(0, 200) : void 0
697
- };
698
- const updatedComments = existingComments.map(
699
- (c, i) => i === commentIndex ? updatedComment : c
700
- );
701
- await writeCommentsToFile(ctx.filePath, currentContent, updatedComments);
702
- res.json({ comment: updatedComment });
703
- } catch (err) {
704
- console.error("Failed to re-anchor comment:", err);
705
- res.status(500).json({ error: "Failed to re-anchor comment" });
706
- }
707
- }
708
- function createApp(options) {
709
- const app = express();
710
- let currentContent = options.content;
711
- const documentStreamClients = /* @__PURE__ */ new Set();
712
- let debounceTimer = null;
713
- let watcher = null;
714
- try {
715
- watcher = watch(options.filePath, async (eventType) => {
716
- if (eventType !== "change") return;
717
- if (debounceTimer) clearTimeout(debounceTimer);
718
- debounceTimer = setTimeout(async () => {
719
- try {
720
- const newContent = await fs.readFile(options.filePath, "utf-8");
721
- if (newContent !== currentContent) {
722
- currentContent = newContent;
723
- console.log("File changed, notifying clients...");
724
- for (const client of documentStreamClients) {
725
- client.write("data: update\n\n");
726
- }
727
- }
728
- } catch (err) {
729
- console.error("Failed to read updated file:", err);
730
- }
731
- }, 100);
732
- });
733
- } catch (err) {
734
- console.warn("File watching not available:", err);
735
- }
736
- app.use(express.json());
737
- const isDev = process.env.NODE_ENV === "development";
738
- if (isDev) {
739
- app.get("/", (_req, res) => {
740
- res.redirect("http://localhost:5173");
741
- });
742
- } else {
743
- const distPath = __dirname;
744
- app.use(express.static(distPath));
745
- app.get("/{*path}", (req, res, next) => {
746
- if (req.path.startsWith("/api")) {
747
- return next();
748
- }
749
- res.sendFile(join3(distPath, "index.html"));
750
- });
751
- }
752
- app.get("/api/document", (_req, res) => {
753
- res.json({
754
- content: currentContent,
755
- type: options.type,
756
- filePath: options.filePath,
757
- fileName: basename(options.filePath),
758
- clean: options.clean || false
759
- });
760
- });
761
- app.get("/api/document/stream", (req, res) => {
762
- res.writeHead(200, {
763
- "Content-Type": "text/event-stream",
764
- "Cache-Control": "no-cache",
765
- Connection: "keep-alive"
766
- });
767
- res.write("data: connected\n\n");
768
- documentStreamClients.add(res);
769
- req.on("close", () => {
770
- documentStreamClients.delete(res);
771
- });
772
- });
773
- app.get("/api/health", (_req, res) => {
774
- res.json({ status: "ok" });
775
- });
776
- const ctx = {
777
- filePath: options.filePath,
778
- getCurrentContent: () => currentContent
779
- };
780
- app.get("/api/comments", (_req, res) => handleGetComments(ctx, res));
781
- app.get("/api/comments/raw", (_req, res) => handleGetRawComments(ctx, res));
782
- app.post("/api/comments", (req, res) => handleAddComment(ctx, req, res));
783
- app.put(
784
- "/api/comments/:id",
785
- (req, res) => handleUpdateComment(ctx, req, res)
786
- );
787
- app.put(
788
- "/api/comments/:id/reanchor",
789
- (req, res) => handleReanchorComment(ctx, req, res)
790
- );
791
- app.delete(
792
- "/api/comments/:id",
793
- (req, res) => handleDeleteComment(ctx, req, res)
794
- );
795
- app.delete("/api/comments", (_req, res) => handleClearComments(ctx, res));
796
- app.get("/api/settings", async (_req, res) => {
797
- try {
798
- const settings = await readSettingsFromFile(ctx.filePath);
799
- res.json(settings);
800
- } catch (err) {
801
- console.error("Failed to read settings:", err);
802
- res.status(500).json({ error: "Failed to read settings" });
803
- }
804
- });
805
- app.put("/api/settings", async (req, res) => {
806
- try {
807
- const { fontFamily } = req.body;
808
- if (!isValidFontFamily(fontFamily)) {
809
- res.status(400).json({ error: "Invalid font family" });
810
- return;
811
- }
812
- const settings = {
813
- version: 1,
814
- fontFamily
815
- };
816
- await writeSettingsToFile(ctx.filePath, settings);
817
- res.json(settings);
818
- } catch (err) {
819
- console.error("Failed to save settings:", err);
820
- res.status(500).json({ error: "Failed to save settings" });
821
- }
822
- });
823
- app.get("/api/heartbeat", (req, res) => {
824
- res.writeHead(200, {
825
- "Content-Type": "text/event-stream",
826
- "Cache-Control": "no-cache",
827
- Connection: "keep-alive"
828
- });
829
- res.write("data: connected\n\n");
830
- const interval = setInterval(() => {
831
- res.write("data: ping\n\n");
832
- }, 5e3);
833
- req.on("close", () => {
834
- clearInterval(interval);
835
- if (isDev) return;
836
- setTimeout(() => {
837
- console.log("\nBrowser disconnected, shutting down...");
838
- process.exit(0);
839
- }, 100);
840
- });
841
- });
842
- return { app, watcher };
843
- }
844
- function tryListenOnPort(app, port, host) {
845
- return new Promise((resolve4, reject) => {
846
- const server = app.listen(port, host);
847
- server.on("listening", () => {
848
- const displayHost = host === "0.0.0.0" ? "localhost" : host;
849
- resolve4({ port, url: `http://${displayHost}:${port}`, server });
850
- });
851
- server.on("error", reject);
852
- });
853
- }
854
- async function startServerWithFallback(app, preferredPort, host) {
855
- const MAX_PORT = 65535;
856
- for (let port = preferredPort; port <= MAX_PORT; port++) {
857
- try {
858
- return await tryListenOnPort(app, port, host);
859
- } catch (err) {
860
- if (err.code === "EADDRINUSE") {
861
- console.log(`Port ${port} is busy, trying ${port + 1}...`);
862
- continue;
863
- }
864
- throw err;
865
- }
866
- }
867
- throw new Error(`No available port found starting from ${preferredPort}`);
868
- }
869
- async function startServer(options) {
870
- const { app, watcher } = createApp(options);
871
- const result = await startServerWithFallback(app, options.port, options.host);
872
- result.server.on("close", () => {
873
- watcher?.close();
874
- });
875
- return result;
876
- }
877
-
878
- // src/cli/index.ts
879
- var program = new Command();
880
- function getFileType(filePath) {
881
- if (filePath.endsWith(".md") || filePath.endsWith(".markdown")) {
882
- return "markdown";
883
- }
884
- if (filePath.endsWith(".html") || filePath.endsWith(".htm")) {
885
- return "html";
886
- }
887
- return null;
888
- }
889
- function isPermissionError(err) {
890
- return err instanceof Error && "code" in err && err.code === "EACCES";
891
- }
892
- function findCommentFiles(dir) {
893
- const results = [];
894
- try {
895
- const entries = readdirSync(dir);
896
- for (const entry of entries) {
897
- const fullPath = join4(dir, entry);
898
- try {
899
- const stat = statSync(fullPath);
900
- if (stat.isDirectory()) {
901
- results.push(...findCommentFiles(fullPath));
902
- } else if (entry.endsWith(".comments.md")) {
903
- results.push(fullPath);
904
- }
905
- } catch (err) {
906
- if (isPermissionError(err)) {
907
- console.warn(`Warning: Permission denied: ${fullPath}`);
908
- }
909
- }
910
- }
911
- } catch (err) {
912
- if (isPermissionError(err)) {
913
- console.warn(`Warning: Permission denied: ${dir}`);
914
- }
915
- }
916
- return results;
917
- }
918
- program.name("readit").description("Review Markdown and HTML documents with inline comments").version("0.1.3");
919
- program.command("list").description("List all files with comments").action(async () => {
920
- const readitDir = join4(os3.homedir(), ".readit", "comments");
921
- if (!existsSync(readitDir)) {
922
- console.log("No comments found.");
923
- return;
924
- }
925
- const commentFiles = findCommentFiles(readitDir);
926
- if (commentFiles.length === 0) {
927
- console.log("No comments found.");
928
- return;
929
- }
930
- console.log(`
931
- Found ${commentFiles.length} file(s) with comments:
932
- `);
933
- for (const file of commentFiles) {
934
- try {
935
- const content = readFileSync(file, "utf-8");
936
- const parsed = parseCommentFile(content);
937
- const commentCount = parsed.comments.length;
938
- const sourcePath = parsed.source || "(unknown source)";
939
- console.log(` ${sourcePath}`);
940
- console.log(
941
- ` ${commentCount} comment${commentCount !== 1 ? "s" : ""}`
942
- );
943
- console.log();
944
- } catch {
945
- }
946
- }
947
- });
948
- program.command("show <file>").description("Show comments for a file").action(async (file) => {
949
- const filePath = resolve3(process.cwd(), file);
950
- const commentPath = getCommentPath(filePath);
951
- if (!existsSync(commentPath)) {
952
- console.log(`No comments found for: ${filePath}`);
953
- return;
954
- }
955
- try {
956
- const content = await fs2.readFile(commentPath, "utf-8");
957
- const parsed = parseCommentFile(content);
958
- if (parsed.comments.length === 0) {
959
- console.log(`No comments found for: ${filePath}`);
960
- return;
961
- }
962
- console.log(`
963
- Comments for: ${filePath}`);
964
- console.log(`${"\u2500".repeat(60)}
965
- `);
966
- for (let i = 0; i < parsed.comments.length; i++) {
967
- const comment = parsed.comments[i];
968
- console.log(`[${i + 1}] ${comment.lineHint || "L?"}`);
969
- console.log(
970
- `Selected: "${comment.selectedText.slice(0, 80)}${comment.selectedText.length > 80 ? "..." : ""}"`
971
- );
972
- console.log(`Comment: ${comment.comment}`);
973
- console.log(`Created: ${comment.createdAt}`);
974
- console.log();
975
- }
976
- } catch (err) {
977
- console.error(
978
- "error: failed to read comments:",
979
- err instanceof Error ? err.message : err
980
- );
981
- process.exit(1);
982
- }
983
- });
984
- program.argument("<file>", "Markdown or HTML file to review").option("-p, --port <number>", "Port to run server on", "4567").option("--host <address>", "Host address to bind to", "127.0.0.1").option("--no-open", "Don't automatically open browser").option("--clean", "Clear all existing comments on startup").action(
985
- async (file, options) => {
986
- const filePath = resolve3(process.cwd(), file);
987
- if (!existsSync(filePath)) {
988
- console.error(`error: file not found: ${filePath}`);
989
- process.exit(1);
990
- }
991
- const fileType = getFileType(file);
992
- if (!fileType) {
993
- console.error(
994
- "error: file must be a Markdown (.md, .markdown) or HTML (.html, .htm) file"
995
- );
996
- process.exit(1);
997
- }
998
- const content = readFileSync(filePath, "utf-8");
999
- const preferredPort = Number.parseInt(options.port, 10);
1000
- if (Number.isNaN(preferredPort) || preferredPort < 1 || preferredPort > 65535) {
1001
- console.error(`error: invalid port number: ${options.port}`);
1002
- process.exit(1);
1003
- }
1004
- try {
1005
- const { url, server } = await startServer({
1006
- content,
1007
- type: fileType,
1008
- filePath,
1009
- port: preferredPort,
1010
- host: options.host,
1011
- clean: options.clean
1012
- });
1013
- console.log(`
1014
- readit - Document Review Tool
1015
-
1016
- File: ${filePath}
1017
- Type: ${fileType}
1018
- URL: ${url}
1019
-
1020
- Server running. Close browser tab to stop.
1021
- Press Ctrl+C to force stop.
1022
- `);
1023
- if (options.open) {
1024
- open(url);
1025
- }
1026
- process.on("SIGINT", () => {
1027
- console.log("\n\nShutting down...");
1028
- server.close();
1029
- process.exit(0);
1030
- });
1031
- } catch (error) {
1032
- console.error(
1033
- "error: failed to start server:",
1034
- error instanceof Error ? error.message : error
1035
- );
1036
- process.exit(1);
1037
- }
1038
- }
1039
- );
1040
- program.parse();