@eightyfourthousand/lib-editing 2026.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/.babelrc +12 -0
  2. package/.eslintrc.json +18 -0
  3. package/README.md +7 -0
  4. package/jest.config.ts +10 -0
  5. package/package.json +35 -0
  6. package/postcss.config.mjs +9 -0
  7. package/project.json +29 -0
  8. package/src/fixtures/basic/json.ts +396 -0
  9. package/src/fixtures/toh251/json.ts +4913 -0
  10. package/src/fixtures/toh251/passages.ts +2814 -0
  11. package/src/fixtures/types.ts +8 -0
  12. package/src/index.ts +4 -0
  13. package/src/lib/block.ts +226 -0
  14. package/src/lib/components/editor/BlockEditor.tsx +38 -0
  15. package/src/lib/components/editor/EditorBackMatterPage.tsx +95 -0
  16. package/src/lib/components/editor/EditorBodyPage.tsx +87 -0
  17. package/src/lib/components/editor/EditorHeader.tsx +22 -0
  18. package/src/lib/components/editor/EditorLayout.tsx +62 -0
  19. package/src/lib/components/editor/EditorLeftPanelPage.tsx +27 -0
  20. package/src/lib/components/editor/EditorProvider.tsx +399 -0
  21. package/src/lib/components/editor/PaginationProvider.tsx +472 -0
  22. package/src/lib/components/editor/TitlesBuilder.tsx +40 -0
  23. package/src/lib/components/editor/TranslationBuilder.tsx +171 -0
  24. package/src/lib/components/editor/TranslationEditor.tsx +32 -0
  25. package/src/lib/components/editor/extensions/Abbreviation/Abbreviation.ts +133 -0
  26. package/src/lib/components/editor/extensions/Audio/Audio.ts +69 -0
  27. package/src/lib/components/editor/extensions/Bold.ts +43 -0
  28. package/src/lib/components/editor/extensions/Document.ts +8 -0
  29. package/src/lib/components/editor/extensions/DragHandle/DragHandle.ts +429 -0
  30. package/src/lib/components/editor/extensions/EndNoteLink/EndNoteLink.tsx +39 -0
  31. package/src/lib/components/editor/extensions/EndNoteLink/EndNoteLinkHoverContent.tsx +139 -0
  32. package/src/lib/components/editor/extensions/EndNoteLink/EndNoteLinkMark.ts +236 -0
  33. package/src/lib/components/editor/extensions/EndNoteLink/endnote-utils.ts +412 -0
  34. package/src/lib/components/editor/extensions/GlobalConfig.ts +52 -0
  35. package/src/lib/components/editor/extensions/GlossaryInstance/GlossaryInput.tsx +54 -0
  36. package/src/lib/components/editor/extensions/GlossaryInstance/GlossaryInstance.tsx +129 -0
  37. package/src/lib/components/editor/extensions/GlossaryInstance/GlossaryInstanceNode.ts +148 -0
  38. package/src/lib/components/editor/extensions/Heading/Heading.ts +71 -0
  39. package/src/lib/components/editor/extensions/HoverInputField.tsx +54 -0
  40. package/src/lib/components/editor/extensions/Image.ts +18 -0
  41. package/src/lib/components/editor/extensions/Indent.ts +103 -0
  42. package/src/lib/components/editor/extensions/InternalLink/InternalLink.ts +173 -0
  43. package/src/lib/components/editor/extensions/InternalLink/InternalLinkHoverContent.tsx +137 -0
  44. package/src/lib/components/editor/extensions/InternalLink/InternalLinkInput.tsx +71 -0
  45. package/src/lib/components/editor/extensions/InternalLink/index.ts +1 -0
  46. package/src/lib/components/editor/extensions/Italic.ts +50 -0
  47. package/src/lib/components/editor/extensions/LeadingSpace.ts +106 -0
  48. package/src/lib/components/editor/extensions/Line/LineNode.ts +41 -0
  49. package/src/lib/components/editor/extensions/LineGroup/LineGroupNode.ts +124 -0
  50. package/src/lib/components/editor/extensions/Link/Link.ts +65 -0
  51. package/src/lib/components/editor/extensions/Link/LinkHoverContent.tsx +124 -0
  52. package/src/lib/components/editor/extensions/Link/index.ts +1 -0
  53. package/src/lib/components/editor/extensions/List.ts +74 -0
  54. package/src/lib/components/editor/extensions/Mantra/Mantra.ts +88 -0
  55. package/src/lib/components/editor/extensions/Mention/Mention.ts +184 -0
  56. package/src/lib/components/editor/extensions/Mention/MentionHoverContent.tsx +158 -0
  57. package/src/lib/components/editor/extensions/NodeWrapper.tsx +57 -0
  58. package/src/lib/components/editor/extensions/Paragraph/Paragraph.ts +25 -0
  59. package/src/lib/components/editor/extensions/ParagraphIndent.ts +87 -0
  60. package/src/lib/components/editor/extensions/Passage/EditLabel.tsx +57 -0
  61. package/src/lib/components/editor/extensions/Passage/EditorOptions.tsx +29 -0
  62. package/src/lib/components/editor/extensions/Passage/Passage.tsx +238 -0
  63. package/src/lib/components/editor/extensions/Passage/PassageNode.ts +223 -0
  64. package/src/lib/components/editor/extensions/Passage/ReaderOptions.tsx +55 -0
  65. package/src/lib/components/editor/extensions/Passage/ShowAnnotations.tsx +92 -0
  66. package/src/lib/components/editor/extensions/Passage/index.ts +1 -0
  67. package/src/lib/components/editor/extensions/Passage/label.spec.ts +118 -0
  68. package/src/lib/components/editor/extensions/Passage/label.ts +39 -0
  69. package/src/lib/components/editor/extensions/Placeholder.ts +9 -0
  70. package/src/lib/components/editor/extensions/SlashCommand/SlashCommand.ts +65 -0
  71. package/src/lib/components/editor/extensions/SlashCommand/SuggestionList.tsx +109 -0
  72. package/src/lib/components/editor/extensions/SlashCommand/Suggestions.ts +185 -0
  73. package/src/lib/components/editor/extensions/SmallCaps.ts +110 -0
  74. package/src/lib/components/editor/extensions/StarterKit.ts +36 -0
  75. package/src/lib/components/editor/extensions/Subscript.ts +43 -0
  76. package/src/lib/components/editor/extensions/Superscript.ts +43 -0
  77. package/src/lib/components/editor/extensions/Table.ts +32 -0
  78. package/src/lib/components/editor/extensions/TextAlign.ts +5 -0
  79. package/src/lib/components/editor/extensions/TitleMetadata.ts +40 -0
  80. package/src/lib/components/editor/extensions/TitleNode.ts +133 -0
  81. package/src/lib/components/editor/extensions/TitlesNode.ts +102 -0
  82. package/src/lib/components/editor/extensions/Trailer.ts +57 -0
  83. package/src/lib/components/editor/extensions/TranslationDocument.ts +7 -0
  84. package/src/lib/components/editor/extensions/TranslationMetadata.ts +58 -0
  85. package/src/lib/components/editor/extensions/Underline.ts +43 -0
  86. package/src/lib/components/editor/hooks/index.ts +4 -0
  87. package/src/lib/components/editor/hooks/useBlockEditor.ts +53 -0
  88. package/src/lib/components/editor/hooks/useDefaultExtensions.ts +39 -0
  89. package/src/lib/components/editor/hooks/useDirtyStore.ts +33 -0
  90. package/src/lib/components/editor/hooks/useTranslationExtensions.ts +148 -0
  91. package/src/lib/components/editor/index.ts +10 -0
  92. package/src/lib/components/editor/menus/EmptyBubbleMenu.tsx +42 -0
  93. package/src/lib/components/editor/menus/MainBubbleMenu.tsx +51 -0
  94. package/src/lib/components/editor/menus/TranslationBubbleMenu.tsx +57 -0
  95. package/src/lib/components/editor/menus/index.ts +3 -0
  96. package/src/lib/components/editor/menus/selectors/EndNoteSelector.tsx +388 -0
  97. package/src/lib/components/editor/menus/selectors/GlossarySelector.tsx +63 -0
  98. package/src/lib/components/editor/menus/selectors/LinkSelector.tsx +68 -0
  99. package/src/lib/components/editor/menus/selectors/MantraSelector.tsx +119 -0
  100. package/src/lib/components/editor/menus/selectors/NodeSelector.tsx +144 -0
  101. package/src/lib/components/editor/menus/selectors/ParagraphButtons.tsx +68 -0
  102. package/src/lib/components/editor/menus/selectors/SelectorInputField.tsx +68 -0
  103. package/src/lib/components/editor/menus/selectors/TextAlignSelector.tsx +89 -0
  104. package/src/lib/components/editor/menus/selectors/TextButtons.tsx +89 -0
  105. package/src/lib/components/editor/menus/selectors/TranslationNodeSelector.tsx +143 -0
  106. package/src/lib/components/editor/menus/selectors/TranslationTextButtons.tsx +125 -0
  107. package/src/lib/components/editor/menus/selectors/index.ts +5 -0
  108. package/src/lib/components/editor/save-filter.spec.ts +94 -0
  109. package/src/lib/components/editor/save-filter.ts +27 -0
  110. package/src/lib/components/editor/util.ts +304 -0
  111. package/src/lib/components/index.ts +3 -0
  112. package/src/lib/components/reader/ReaderBackMatterPage.tsx +62 -0
  113. package/src/lib/components/reader/ReaderBackMatterPanel.tsx +53 -0
  114. package/src/lib/components/reader/ReaderBodyPage.tsx +46 -0
  115. package/src/lib/components/reader/ReaderBodyPanel.tsx +68 -0
  116. package/src/lib/components/reader/ReaderLayout.tsx +39 -0
  117. package/src/lib/components/reader/ReaderLeftPanel.tsx +8 -0
  118. package/src/lib/components/reader/ReaderLeftPanelPage.tsx +31 -0
  119. package/src/lib/components/reader/TranslationReader.tsx +28 -0
  120. package/src/lib/components/reader/index.ts +2 -0
  121. package/src/lib/components/reader/ssr.ts +3 -0
  122. package/src/lib/components/shared/AiSummarizerPage.tsx +12 -0
  123. package/src/lib/components/shared/BackMatterPanel.tsx +143 -0
  124. package/src/lib/components/shared/BodyPanel.tsx +214 -0
  125. package/src/lib/components/shared/HoverCardProvider.tsx +407 -0
  126. package/src/lib/components/shared/Imprint.tsx +24 -0
  127. package/src/lib/components/shared/LabeledElement.tsx +133 -0
  128. package/src/lib/components/shared/LeftPanel.tsx +65 -0
  129. package/src/lib/components/shared/NavigationContext.ts +64 -0
  130. package/src/lib/components/shared/NavigationProvider.tsx +368 -0
  131. package/src/lib/components/shared/OpenGraphImage.tsx +75 -0
  132. package/src/lib/components/shared/PassageSkeleton.tsx +10 -0
  133. package/src/lib/components/shared/RestrictionWarning.tsx +177 -0
  134. package/src/lib/components/shared/SourceReader.tsx +83 -0
  135. package/src/lib/components/shared/SuggestRevisionForm.tsx +99 -0
  136. package/src/lib/components/shared/TableOfContents.tsx +280 -0
  137. package/src/lib/components/shared/ThreeColumnRenderer.tsx +54 -0
  138. package/src/lib/components/shared/TranslationHeader.tsx +86 -0
  139. package/src/lib/components/shared/TranslationHoverCard.tsx +84 -0
  140. package/src/lib/components/shared/TranslationSkeleton.tsx +16 -0
  141. package/src/lib/components/shared/TranslationTable.tsx +155 -0
  142. package/src/lib/components/shared/bibliography/BibliographyBody.tsx +28 -0
  143. package/src/lib/components/shared/bibliography/BibliographyList.tsx +63 -0
  144. package/src/lib/components/shared/bibliography/index.ts +1 -0
  145. package/src/lib/components/shared/generate-metadata.ts +44 -0
  146. package/src/lib/components/shared/glossary/GlossaryInstanceBody.tsx +144 -0
  147. package/src/lib/components/shared/glossary/GlossaryPaginationProvider.tsx +317 -0
  148. package/src/lib/components/shared/glossary/GlossarySkeleton.tsx +19 -0
  149. package/src/lib/components/shared/glossary/GlossaryTermList.tsx +58 -0
  150. package/src/lib/components/shared/glossary/index.ts +3 -0
  151. package/src/lib/components/shared/hooks/useGlossaryInstanceListener.tsx +42 -0
  152. package/src/lib/components/shared/hooks/useScrollInTab.tsx +43 -0
  153. package/src/lib/components/shared/hooks/useScrollPositionRestore.ts +274 -0
  154. package/src/lib/components/shared/hooks/useTohToggle.tsx +52 -0
  155. package/src/lib/components/shared/index.ts +11 -0
  156. package/src/lib/components/shared/ssr.ts +2 -0
  157. package/src/lib/components/shared/titles/FramedCard.tsx +132 -0
  158. package/src/lib/components/shared/titles/LongTitle.tsx +20 -0
  159. package/src/lib/components/shared/titles/LongTitles.tsx +28 -0
  160. package/src/lib/components/shared/titles/Title.tsx +54 -0
  161. package/src/lib/components/shared/titles/TitleDetails.tsx +47 -0
  162. package/src/lib/components/shared/titles/TitleForm.tsx +37 -0
  163. package/src/lib/components/shared/titles/Titles.tsx +114 -0
  164. package/src/lib/components/shared/titles/TitlesCard.tsx +113 -0
  165. package/src/lib/components/shared/titles/index.ts +8 -0
  166. package/src/lib/components/shared/types.ts +79 -0
  167. package/src/lib/components/ssr.ts +2 -0
  168. package/src/lib/exporters/abbreviation.spec.ts +31 -0
  169. package/src/lib/exporters/abbreviation.ts +22 -0
  170. package/src/lib/exporters/annotation.ts +193 -0
  171. package/src/lib/exporters/audio.spec.ts +77 -0
  172. package/src/lib/exporters/audio.ts +27 -0
  173. package/src/lib/exporters/blockquote.spec.ts +48 -0
  174. package/src/lib/exporters/blockquote.ts +24 -0
  175. package/src/lib/exporters/code.spec.ts +93 -0
  176. package/src/lib/exporters/code.ts +26 -0
  177. package/src/lib/exporters/end-note-link.spec.ts +104 -0
  178. package/src/lib/exporters/end-note-link.ts +35 -0
  179. package/src/lib/exporters/export.ts +12 -0
  180. package/src/lib/exporters/glossary-instance.spec.ts +85 -0
  181. package/src/lib/exporters/glossary-instance.ts +31 -0
  182. package/src/lib/exporters/has-abbreviation.spec.ts +31 -0
  183. package/src/lib/exporters/has-abbreviation.ts +21 -0
  184. package/src/lib/exporters/heading.spec.ts +80 -0
  185. package/src/lib/exporters/heading.ts +28 -0
  186. package/src/lib/exporters/image.spec.ts +48 -0
  187. package/src/lib/exporters/image.ts +25 -0
  188. package/src/lib/exporters/indent.spec.ts +58 -0
  189. package/src/lib/exporters/indent.ts +18 -0
  190. package/src/lib/exporters/index.ts +1 -0
  191. package/src/lib/exporters/internal-link.spec.ts +90 -0
  192. package/src/lib/exporters/internal-link.ts +35 -0
  193. package/src/lib/exporters/italic.spec.ts +84 -0
  194. package/src/lib/exporters/italic.ts +55 -0
  195. package/src/lib/exporters/leading-space.spec.ts +28 -0
  196. package/src/lib/exporters/leading-space.ts +16 -0
  197. package/src/lib/exporters/line-group.spec.ts +48 -0
  198. package/src/lib/exporters/line-group.ts +24 -0
  199. package/src/lib/exporters/line.spec.ts +48 -0
  200. package/src/lib/exporters/line.ts +24 -0
  201. package/src/lib/exporters/link.spec.ts +123 -0
  202. package/src/lib/exporters/link.ts +67 -0
  203. package/src/lib/exporters/list-item.spec.ts +48 -0
  204. package/src/lib/exporters/list-item.ts +24 -0
  205. package/src/lib/exporters/list.spec.ts +82 -0
  206. package/src/lib/exporters/list.ts +31 -0
  207. package/src/lib/exporters/mantra.spec.ts +51 -0
  208. package/src/lib/exporters/mantra.ts +25 -0
  209. package/src/lib/exporters/mention.ts +41 -0
  210. package/src/lib/exporters/paragraph.spec.ts +173 -0
  211. package/src/lib/exporters/paragraph.ts +32 -0
  212. package/src/lib/exporters/quote.spec.ts +56 -0
  213. package/src/lib/exporters/quote.ts +25 -0
  214. package/src/lib/exporters/span.spec.ts +118 -0
  215. package/src/lib/exporters/span.ts +44 -0
  216. package/src/lib/exporters/table-body-data.spec.ts +48 -0
  217. package/src/lib/exporters/table-body-data.ts +24 -0
  218. package/src/lib/exporters/table-body-header.spec.ts +48 -0
  219. package/src/lib/exporters/table-body-header.ts +24 -0
  220. package/src/lib/exporters/table-body-row.spec.ts +48 -0
  221. package/src/lib/exporters/table-body-row.ts +24 -0
  222. package/src/lib/exporters/table.spec.ts +48 -0
  223. package/src/lib/exporters/table.ts +24 -0
  224. package/src/lib/exporters/trailer.spec.ts +48 -0
  225. package/src/lib/exporters/trailer.ts +24 -0
  226. package/src/lib/exporters/util.ts +62 -0
  227. package/src/lib/passage.ts +182 -0
  228. package/src/lib/titles.ts +80 -0
  229. package/src/lib/transformers/abbreviation.spec.ts +87 -0
  230. package/src/lib/transformers/abbreviation.ts +30 -0
  231. package/src/lib/transformers/annotate.ts +146 -0
  232. package/src/lib/transformers/audio.spec.ts +55 -0
  233. package/src/lib/transformers/audio.ts +29 -0
  234. package/src/lib/transformers/blockquote.spec.ts +48 -0
  235. package/src/lib/transformers/blockquote.ts +41 -0
  236. package/src/lib/transformers/code.spec.ts +52 -0
  237. package/src/lib/transformers/code.ts +22 -0
  238. package/src/lib/transformers/deprecated.ts +7 -0
  239. package/src/lib/transformers/end-note-link.spec.ts +56 -0
  240. package/src/lib/transformers/end-note-link.ts +76 -0
  241. package/src/lib/transformers/glossary-instance.spec.ts +55 -0
  242. package/src/lib/transformers/glossary-instance.ts +40 -0
  243. package/src/lib/transformers/has-abbreviation.spec.ts +50 -0
  244. package/src/lib/transformers/has-abbreviation.ts +30 -0
  245. package/src/lib/transformers/heading.spec.ts +62 -0
  246. package/src/lib/transformers/heading.ts +30 -0
  247. package/src/lib/transformers/image.spec.ts +51 -0
  248. package/src/lib/transformers/image.ts +28 -0
  249. package/src/lib/transformers/indent.spec.ts +53 -0
  250. package/src/lib/transformers/indent.ts +17 -0
  251. package/src/lib/transformers/index.ts +33 -0
  252. package/src/lib/transformers/inline-title.spec.ts +59 -0
  253. package/src/lib/transformers/inline-title.ts +34 -0
  254. package/src/lib/transformers/internal-link.spec.ts +67 -0
  255. package/src/lib/transformers/internal-link.ts +65 -0
  256. package/src/lib/transformers/italic.ts +22 -0
  257. package/src/lib/transformers/leading-space.spec.ts +55 -0
  258. package/src/lib/transformers/leading-space.ts +17 -0
  259. package/src/lib/transformers/line-group.spec.ts +48 -0
  260. package/src/lib/transformers/line-group.ts +37 -0
  261. package/src/lib/transformers/line.spec.ts +54 -0
  262. package/src/lib/transformers/line.ts +27 -0
  263. package/src/lib/transformers/link.spec.ts +61 -0
  264. package/src/lib/transformers/link.ts +27 -0
  265. package/src/lib/transformers/list-item.spec.ts +48 -0
  266. package/src/lib/transformers/list-item.ts +37 -0
  267. package/src/lib/transformers/list.spec.ts +58 -0
  268. package/src/lib/transformers/list.ts +42 -0
  269. package/src/lib/transformers/mantra.spec.ts +58 -0
  270. package/src/lib/transformers/mantra.ts +28 -0
  271. package/src/lib/transformers/mention.ts +70 -0
  272. package/src/lib/transformers/paragraph.spec.ts +46 -0
  273. package/src/lib/transformers/paragraph.ts +26 -0
  274. package/src/lib/transformers/quote.spec.ts +42 -0
  275. package/src/lib/transformers/quote.ts +3 -0
  276. package/src/lib/transformers/quoted.ts +3 -0
  277. package/src/lib/transformers/recurse.ts +76 -0
  278. package/src/lib/transformers/reference.ts +3 -0
  279. package/src/lib/transformers/span.spec.ts +68 -0
  280. package/src/lib/transformers/span.ts +78 -0
  281. package/src/lib/transformers/split-at.ts +58 -0
  282. package/src/lib/transformers/split-block.ts +110 -0
  283. package/src/lib/transformers/split-content.ts +67 -0
  284. package/src/lib/transformers/split-insert.ts +76 -0
  285. package/src/lib/transformers/split-marks.ts +42 -0
  286. package/src/lib/transformers/split-node.ts +138 -0
  287. package/src/lib/transformers/table-body-data.spec.ts +44 -0
  288. package/src/lib/transformers/table-body-data.ts +29 -0
  289. package/src/lib/transformers/table-body-header.spec.ts +44 -0
  290. package/src/lib/transformers/table-body-header.ts +29 -0
  291. package/src/lib/transformers/table-body-row.spec.ts +44 -0
  292. package/src/lib/transformers/table-body-row.ts +29 -0
  293. package/src/lib/transformers/table.spec.ts +47 -0
  294. package/src/lib/transformers/table.ts +29 -0
  295. package/src/lib/transformers/trailer.spec.ts +43 -0
  296. package/src/lib/transformers/trailer.ts +26 -0
  297. package/src/lib/transformers/transformer.ts +25 -0
  298. package/src/lib/transformers/unknown.ts +8 -0
  299. package/src/lib/transformers/util.ts +20 -0
  300. package/src/lib/types.ts +10 -0
  301. package/src/ssr.ts +1 -0
  302. package/tsconfig.json +20 -0
  303. package/tsconfig.lib.json +29 -0
  304. package/tsconfig.spec.json +22 -0
@@ -0,0 +1,412 @@
1
+ import { Editor } from '@tiptap/core';
2
+ import { incrementLabel } from '../Passage/label';
3
+ import { findPassageNode } from '../../util';
4
+
5
+ interface EndNoteLinkNote {
6
+ uuid: string;
7
+ endNote: string;
8
+ label?: string;
9
+ location?: string;
10
+ toh?: string;
11
+ }
12
+
13
+ interface MarkRange {
14
+ from: number;
15
+ to: number;
16
+ mark: ReturnType<Editor['state']['doc']['resolve']> extends never
17
+ ? never
18
+ : // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ any;
20
+ note: EndNoteLinkNote;
21
+ }
22
+
23
+ /**
24
+ * Scan main editor from start to `cursorPos`, collecting `endNoteLink` marks.
25
+ * Return the last one's `endNote` UUID and its position info.
26
+ */
27
+ export function findLastEndNoteLinkBefore(
28
+ editor: Editor,
29
+ cursorPos: number,
30
+ ): { endNote: string; from: number; to: number } | undefined {
31
+ const { doc } = editor.state;
32
+ let last: { endNote: string; from: number; to: number } | undefined;
33
+
34
+ doc.descendants((node, pos) => {
35
+ if (pos >= cursorPos) return false;
36
+
37
+ for (const mark of node.marks) {
38
+ if (mark.type.name === 'endNoteLink') {
39
+ const notes: EndNoteLinkNote[] = mark.attrs.notes || [];
40
+ for (const note of notes) {
41
+ if (note.endNote) {
42
+ last = { endNote: note.endNote, from: pos, to: pos + node.nodeSize };
43
+ }
44
+ }
45
+ }
46
+ }
47
+ return true;
48
+ });
49
+
50
+ return last;
51
+ }
52
+
53
+ /**
54
+ * Traverse the editor doc, return all mark ranges where
55
+ * notes[].endNote === endNotePassageUuid.
56
+ */
57
+ export function findAllEndnoteLinksForPassage(
58
+ editor: Editor,
59
+ endNotePassageUuid: string,
60
+ ): MarkRange[] {
61
+ const { doc } = editor.state;
62
+ const results: MarkRange[] = [];
63
+
64
+ doc.descendants((node, pos) => {
65
+ for (const mark of node.marks) {
66
+ if (mark.type.name === 'endNoteLink') {
67
+ const notes: EndNoteLinkNote[] = mark.attrs.notes || [];
68
+ for (const note of notes) {
69
+ if (note.endNote === endNotePassageUuid) {
70
+ results.push({
71
+ from: pos,
72
+ to: pos + node.nodeSize,
73
+ mark,
74
+ note,
75
+ });
76
+ }
77
+ }
78
+ }
79
+ }
80
+ return true;
81
+ });
82
+
83
+ return results;
84
+ }
85
+
86
+ /**
87
+ * Batch-remove all `endNoteLink` marks pointing to a given passage UUID
88
+ * using a single transaction.
89
+ */
90
+ export function removeAllEndnoteLinksForPassage(
91
+ editor: Editor,
92
+ endNotePassageUuid: string,
93
+ ): void {
94
+ const ranges = findAllEndnoteLinksForPassage(editor, endNotePassageUuid);
95
+ if (ranges.length === 0) return;
96
+
97
+ const { tr } = editor.state;
98
+
99
+ for (const { from, to, mark, note } of ranges) {
100
+ tr.removeMark(from, to, mark.type);
101
+ // If the mark has other notes besides the one we're removing, re-add it
102
+ const remainingNotes = (mark.attrs.notes || []).filter(
103
+ (n: EndNoteLinkNote) => n.uuid !== note.uuid,
104
+ );
105
+ if (remainingNotes.length > 0) {
106
+ tr.addMark(from, to, mark.type.create({ ...mark.attrs, notes: remainingNotes }));
107
+ }
108
+ }
109
+
110
+ editor.view.dispatch(tr);
111
+ }
112
+
113
+ // Re-export for convenience — canonical definition is in ../../util.ts
114
+ export { findPassageNode } from '../../util';
115
+
116
+ /**
117
+ * Get the last passage node in the endnotes editor.
118
+ * Returns { label, sort, uuid } or undefined if no passages exist.
119
+ */
120
+ export function getLastEndnoteInEditor(
121
+ editor: Editor,
122
+ ): { label: string; sort: number; uuid: string } | undefined {
123
+ const { doc } = editor.state;
124
+ let last: { label: string; sort: number; uuid: string; pos: number } | undefined;
125
+
126
+ doc.descendants((node, pos) => {
127
+ if (node.type.name === 'passage') {
128
+ last = {
129
+ label: node.attrs.label || '',
130
+ sort: node.attrs.sort ?? 0,
131
+ uuid: node.attrs.uuid,
132
+ pos,
133
+ };
134
+ }
135
+ return true;
136
+ });
137
+
138
+ return last
139
+ ? { label: last.label, sort: last.sort, uuid: last.uuid }
140
+ : undefined;
141
+ }
142
+
143
+ /**
144
+ * Get the first endnotes-type passage node in the endnotes editor.
145
+ * Skips endnotesHeader passages.
146
+ * Returns { label, sort, uuid } or undefined if no passages exist.
147
+ */
148
+ export function getFirstEndnoteInEditor(
149
+ editor: Editor,
150
+ ): { label: string; sort: number; uuid: string } | undefined {
151
+ const { doc } = editor.state;
152
+ let first: { label: string; sort: number; uuid: string } | undefined;
153
+
154
+ doc.descendants((node) => {
155
+ if (!first && node.type.name === 'passage' && node.attrs.type === 'endnotes') {
156
+ first = {
157
+ label: node.attrs.label || '',
158
+ sort: node.attrs.sort ?? 0,
159
+ uuid: node.attrs.uuid,
160
+ };
161
+ return false;
162
+ }
163
+ return true;
164
+ });
165
+
166
+ return first;
167
+ }
168
+
169
+ /**
170
+ * Insert a new empty endnote passage into the endnotes editor at the correct
171
+ * position. Use `afterPassageUuid` to insert after a passage, or
172
+ * `beforePassageUuid` to insert before one. Falls back to end of doc.
173
+ * Increments labels and sort values of all subsequent passages.
174
+ */
175
+ export function insertEndnotePassage(
176
+ editor: Editor,
177
+ {
178
+ label,
179
+ sort,
180
+ uuid,
181
+ afterPassageUuid,
182
+ beforePassageUuid,
183
+ }: {
184
+ label: string;
185
+ sort: number;
186
+ uuid: string;
187
+ afterPassageUuid?: string;
188
+ beforePassageUuid?: string;
189
+ },
190
+ ): void {
191
+ const { state } = editor;
192
+ const { tr, schema } = state;
193
+ const passageType = schema.nodes.passage;
194
+ const paragraphType = schema.nodes.paragraph;
195
+
196
+ if (!passageType || !paragraphType) {
197
+ console.warn('Required node types not found in schema');
198
+ return;
199
+ }
200
+
201
+ const newPassage = passageType.create(
202
+ { label, sort, uuid, type: 'endnotes' },
203
+ paragraphType.create(),
204
+ );
205
+
206
+ // Find insertion position
207
+ let insertPos = state.doc.content.size;
208
+ if (beforePassageUuid) {
209
+ state.doc.descendants((node, pos) => {
210
+ if (
211
+ node.type.name === 'passage' &&
212
+ node.attrs.uuid === beforePassageUuid
213
+ ) {
214
+ insertPos = pos;
215
+ return false;
216
+ }
217
+ return true;
218
+ });
219
+ } else if (afterPassageUuid) {
220
+ state.doc.descendants((node, pos) => {
221
+ if (
222
+ node.type.name === 'passage' &&
223
+ node.attrs.uuid === afterPassageUuid
224
+ ) {
225
+ insertPos = pos + node.nodeSize;
226
+ return false;
227
+ }
228
+ return true;
229
+ });
230
+ }
231
+
232
+ tr.insert(insertPos, newPassage);
233
+
234
+ // Increment labels and sorts of passages after the insertion point.
235
+ // After the insert, the new passage occupies [insertPos, insertPos + newPassage.nodeSize).
236
+ // Passages that were at insertPos in the original doc are now shifted by newPassage.nodeSize.
237
+ const afterNewPos = insertPos + newPassage.nodeSize;
238
+ const parts = label.split('.');
239
+ const prefix = parts.slice(0, -1).join('.');
240
+ const prefixWithDot = prefix ? prefix + '.' : '';
241
+ const depth = parts.length;
242
+ let expectedNext = incrementLabel(label);
243
+
244
+ tr.doc.descendants((child, childPos) => {
245
+ if (childPos < afterNewPos) return true;
246
+ if (child.type.name !== 'passage') return true;
247
+ const childLabel = child.attrs.label as string;
248
+ if (!childLabel) return true;
249
+ if (prefixWithDot && !childLabel.startsWith(prefixWithDot)) return true;
250
+ if (childLabel.split('.').length !== depth) return true;
251
+
252
+ if (child.attrs.type === 'endnotesHeader') {
253
+ // Only update sort, never change header labels
254
+ tr.setNodeMarkup(childPos, null, {
255
+ ...child.attrs,
256
+ sort: (child.attrs.sort ?? 0) + 1,
257
+ });
258
+ } else {
259
+ tr.setNodeMarkup(childPos, null, {
260
+ ...child.attrs,
261
+ label: expectedNext,
262
+ sort: (child.attrs.sort ?? 0) + 1,
263
+ });
264
+ expectedNext = incrementLabel(expectedNext);
265
+ }
266
+ return true;
267
+ });
268
+
269
+ editor.view.dispatch(tr);
270
+ }
271
+
272
+ /**
273
+ * Delete a passage node from the endnotes editor by UUID,
274
+ * then normalize labels of subsequent passages.
275
+ */
276
+ export function deleteEndnotePassageNode(
277
+ editor: Editor,
278
+ passageUuid: string,
279
+ ): void {
280
+ const found = findPassageNode(editor, passageUuid);
281
+ if (!found) return;
282
+
283
+ const { pos, node } = found;
284
+ const { tr } = editor.state;
285
+
286
+ // Capture the deleted passage's label before removing it — subsequent
287
+ // passages should be renumbered starting from this label.
288
+ const deletedLabel = node.attrs.label as string | undefined;
289
+
290
+ // Delete the passage node
291
+ tr.delete(pos, pos + node.nodeSize);
292
+
293
+ // Normalize labels after deletion: passages that were after the deleted
294
+ // node (now starting at `pos` in the updated doc) get renumbered.
295
+ if (deletedLabel) {
296
+ let expectedNext = deletedLabel;
297
+ const parts = deletedLabel.split('.');
298
+ const prefix = parts.slice(0, -1).join('.');
299
+ const prefixWithDot = prefix ? prefix + '.' : '';
300
+ const depth = parts.length;
301
+
302
+ tr.doc.descendants((child, childPos) => {
303
+ // Only process passages at or after the deletion point
304
+ if (childPos < pos) return true;
305
+ if (child.type.name !== 'passage') return true;
306
+ const childLabel = child.attrs.label as string;
307
+ if (!childLabel) return true;
308
+
309
+ // Only normalize passages with same prefix and depth
310
+ if (prefixWithDot && !childLabel.startsWith(prefixWithDot)) return true;
311
+ if (childLabel.split('.').length !== depth) return true;
312
+
313
+ if (child.attrs.type === 'endnotesHeader') {
314
+ // Never change header labels
315
+ return true;
316
+ }
317
+ if (childLabel !== expectedNext) {
318
+ tr.setNodeMarkup(childPos, null, {
319
+ ...child.attrs,
320
+ label: expectedNext,
321
+ });
322
+ }
323
+ expectedNext = incrementLabel(expectedNext);
324
+ return true;
325
+ });
326
+ }
327
+
328
+ editor.view.dispatch(tr);
329
+ }
330
+
331
+ /**
332
+ * Build a map of endnote passage UUID → label from the endnotes editor.
333
+ */
334
+ export function buildEndnoteLabelMap(
335
+ endnotesEditor: Editor,
336
+ ): Map<string, string> {
337
+ const map = new Map<string, string>();
338
+ endnotesEditor.state.doc.descendants((node) => {
339
+ if (node.type.name === 'passage' && node.attrs.uuid && node.attrs.label) {
340
+ map.set(node.attrs.uuid, node.attrs.label);
341
+ }
342
+ return true;
343
+ });
344
+ return map;
345
+ }
346
+
347
+ /**
348
+ * Update the `label` field inside `endNoteLink` marks in an editor to match
349
+ * the current labels in the endnotes editor. Call this after deleting/renumbering
350
+ * endnote passages so the superscript numbers in the UI stay in sync.
351
+ */
352
+ export function syncEndnoteLinkLabels(
353
+ editor: Editor,
354
+ labelMap: Map<string, string>,
355
+ ): void {
356
+ const { tr } = editor.state;
357
+ let changed = false;
358
+
359
+ editor.state.doc.descendants((node, pos) => {
360
+ for (const mark of node.marks) {
361
+ if (mark.type.name !== 'endNoteLink') continue;
362
+ const notes: EndNoteLinkNote[] = mark.attrs.notes || [];
363
+ let notesChanged = false;
364
+
365
+ const updatedNotes = notes.map((note) => {
366
+ const newLabel = labelMap.get(note.endNote);
367
+ if (newLabel !== undefined && newLabel !== note.label) {
368
+ notesChanged = true;
369
+ return { ...note, label: newLabel };
370
+ }
371
+ return note;
372
+ });
373
+
374
+ if (notesChanged) {
375
+ const from = pos;
376
+ const to = pos + node.nodeSize;
377
+ tr.removeMark(from, to, mark.type);
378
+ tr.addMark(
379
+ from,
380
+ to,
381
+ mark.type.create({ ...mark.attrs, notes: updatedNotes }),
382
+ );
383
+ changed = true;
384
+ }
385
+ }
386
+ return true;
387
+ });
388
+
389
+ if (changed) {
390
+ editor.view.dispatch(tr);
391
+ }
392
+ }
393
+
394
+ /** Editor keys that can contain endNoteLink marks. */
395
+ const ENDNOTE_LINK_EDITOR_KEYS = ['front', 'translation'] as const;
396
+
397
+ /**
398
+ * After deleting/renumbering endnote passages, sync the updated labels into
399
+ * endNoteLink marks across all editors that may contain them (front + translation).
400
+ */
401
+ export function syncEndnoteLinkLabelsAcrossEditors(
402
+ endnotesEditor: Editor,
403
+ getEditor: (key: string) => Editor | undefined,
404
+ ): void {
405
+ const labelMap = buildEndnoteLabelMap(endnotesEditor);
406
+ for (const key of ENDNOTE_LINK_EDITOR_KEYS) {
407
+ const ed = getEditor(key);
408
+ if (ed) {
409
+ syncEndnoteLinkLabels(ed, labelMap);
410
+ }
411
+ }
412
+ }
@@ -0,0 +1,52 @@
1
+ import { Extension } from '@tiptap/core';
2
+
3
+ declare module '@tiptap/core' {
4
+ interface Commands<ReturnType> {
5
+ globalConfig: {
6
+ /**
7
+ * Enable or disable debug mode
8
+ * @example editor.commands.setDebugMode(true)
9
+ */
10
+ setDebug: (enabled: boolean) => ReturnType;
11
+
12
+ /**
13
+ * Check if debug mode is enabled
14
+ * @example editor.isDebug()
15
+ */
16
+ isDebug: () => ReturnType;
17
+ };
18
+ }
19
+
20
+ interface Storage {
21
+ globalConfig: {
22
+ debug: boolean;
23
+ };
24
+ }
25
+ }
26
+
27
+ export const GlobalConfig = Extension.create({
28
+ name: 'globalConfig',
29
+
30
+ addStorage() {
31
+ return {
32
+ debug: false,
33
+ };
34
+ },
35
+
36
+ addCommands() {
37
+ return {
38
+ setDebug:
39
+ (enabled) =>
40
+ ({ editor }) => {
41
+ this.storage.debug = enabled;
42
+ // Force re-render if needed
43
+ editor.view.updateState(editor.state);
44
+ return true;
45
+ },
46
+
47
+ isDebug: () => () => {
48
+ return this.storage.debug;
49
+ },
50
+ };
51
+ },
52
+ });
@@ -0,0 +1,54 @@
1
+ import { Button, Input } from '@eightyfourthousand/design-system';
2
+ import { CheckIcon, Trash2Icon } from 'lucide-react';
3
+ import { useRef } from 'react';
4
+
5
+ export const GlossaryInput = ({
6
+ uuid,
7
+ onSubmit,
8
+ }: {
9
+ uuid: string;
10
+ onSubmit: (entity?: string) => void;
11
+ }) => {
12
+ const uuidRef = useRef<HTMLInputElement>(null);
13
+
14
+ const submit = () => {
15
+ onSubmit(uuidRef.current?.value);
16
+ };
17
+
18
+ return (
19
+ <div className="flex space-x-1 items-center">
20
+ <Input
21
+ ref={uuidRef}
22
+ placeholder="Glossary uuid..."
23
+ value={uuid}
24
+ onKeyDown={(e) => {
25
+ if (e.key === 'Enter') {
26
+ e.preventDefault();
27
+ submit();
28
+ }
29
+ }}
30
+ />
31
+ <Button
32
+ size="icon"
33
+ variant="ghost"
34
+ disabled={!!uuidRef.current}
35
+ onClick={submit}
36
+ >
37
+ <CheckIcon />
38
+ </Button>
39
+ <Button
40
+ variant="ghost"
41
+ size="icon"
42
+ type="button"
43
+ onClick={() => {
44
+ if (uuidRef.current) {
45
+ uuidRef.current.value = '';
46
+ }
47
+ onSubmit();
48
+ }}
49
+ >
50
+ <Trash2Icon className="text-destructive" />
51
+ </Button>
52
+ </div>
53
+ );
54
+ };
@@ -0,0 +1,129 @@
1
+ import { Button } from '@eightyfourthousand/design-system';
2
+ import { Editor } from '@tiptap/core';
3
+ import { BookOpenIcon, PencilIcon, Trash2Icon } from 'lucide-react';
4
+ import { useCallback, useState } from 'react';
5
+ import { GlossaryInput } from './GlossaryInput';
6
+ import { findMarkByUuid } from '../../util';
7
+ import { useHoverCard } from '../../../shared/HoverCardProvider';
8
+
9
+ const EDITOR_UPDATE_DELAY_MS = 100;
10
+
11
+ export const GlossaryInstance = ({
12
+ uuid,
13
+ glossary,
14
+ editor,
15
+ anchor,
16
+ }: {
17
+ uuid: string;
18
+ glossary: string;
19
+ editor: Editor;
20
+ anchor: HTMLElement;
21
+ }) => {
22
+ const [isEditing, setIsEditingLocal] = useState(false);
23
+ const { close, setIsEditing: setIsEditingContext } = useHoverCard();
24
+
25
+ const setIsEditing = useCallback(
26
+ (editing: boolean) => {
27
+ setIsEditingLocal(editing);
28
+ setIsEditingContext(editing);
29
+ },
30
+ [setIsEditingContext],
31
+ );
32
+
33
+ const deleteLink = useCallback(() => {
34
+ setIsEditing(false);
35
+ close();
36
+
37
+ setTimeout(() => {
38
+ const range = findMarkByUuid({
39
+ editor,
40
+ uuid,
41
+ markType: 'glossaryInstance',
42
+ });
43
+ if (!range) {
44
+ console.warn('GlossaryInstance mark not found in the document.');
45
+ return;
46
+ }
47
+
48
+ const { from, to, mark } = range;
49
+ const { tr } = editor.state;
50
+ tr.removeMark(from, to, mark.type);
51
+ editor.view.dispatch(tr);
52
+ }, EDITOR_UPDATE_DELAY_MS);
53
+ }, [editor, uuid, close, setIsEditing]);
54
+
55
+ const updateGlossary = useCallback(
56
+ (newGlossary: string) => {
57
+ setIsEditing(false);
58
+ close();
59
+
60
+ setTimeout(() => {
61
+ const range = findMarkByUuid({
62
+ editor,
63
+ uuid,
64
+ markType: 'glossaryInstance',
65
+ });
66
+ if (!range) {
67
+ console.warn('GlossaryInstance mark not found in the document.');
68
+ return;
69
+ }
70
+
71
+ const { from, to, mark } = range;
72
+ const { tr } = editor.state;
73
+ tr.removeMark(from, to, mark.type);
74
+ tr.addMark(
75
+ from,
76
+ to,
77
+ mark.type.create({ ...mark.attrs, glossary: newGlossary }),
78
+ );
79
+ editor.view.dispatch(tr);
80
+
81
+ // Update the DOM attribute directly for immediate feedback
82
+ anchor.setAttribute('glossary', newGlossary);
83
+ }, EDITOR_UPDATE_DELAY_MS);
84
+ },
85
+ [editor, uuid, anchor, close, setIsEditing],
86
+ );
87
+
88
+ return (
89
+ <div className="flex justify-between gap-2 p-2 w-fit max-w-80">
90
+ {isEditing ? (
91
+ <GlossaryInput
92
+ uuid={glossary}
93
+ onSubmit={(newGlossary) => {
94
+ if (newGlossary) {
95
+ updateGlossary(newGlossary);
96
+ } else {
97
+ deleteLink();
98
+ }
99
+ setIsEditing(false);
100
+ }}
101
+ />
102
+ ) : (
103
+ <>
104
+ <BookOpenIcon className="text-primary my-auto size-6 [&_svg]:size-4" />
105
+ <span className="truncate text-muted-foreground text-sm my-auto">
106
+ {glossary}
107
+ </span>
108
+ <span className="flex-grow" />
109
+ <Button
110
+ variant="ghost"
111
+ size="icon"
112
+ className="size-6 [&_svg]:size-4"
113
+ onClick={() => setIsEditing(true)}
114
+ >
115
+ <PencilIcon className="text-primary my-auto" />
116
+ </Button>
117
+ <Button
118
+ variant="ghost"
119
+ size="icon"
120
+ className="size-6 [&_svg]:size-4"
121
+ onClick={deleteLink}
122
+ >
123
+ <Trash2Icon className="text-destructive my-auto" />
124
+ </Button>
125
+ </>
126
+ )}
127
+ </div>
128
+ );
129
+ };