@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,39 @@
1
+ 'use client';
2
+
3
+ import { Skeleton } from '@eightyfourthousand/design-system';
4
+ import { useEffect, useState } from 'react';
5
+ import { Passage } from '@eightyfourthousand/data-access';
6
+ import { LabeledElement } from '../../../shared';
7
+
8
+ export const EndNoteLink = ({
9
+ uuid,
10
+ fetch,
11
+ }: {
12
+ uuid: string;
13
+ fetch?: (uuid: string) => Promise<Passage | undefined>;
14
+ }) => {
15
+ const [content, setContent] = useState<Passage>();
16
+
17
+ useEffect(() => {
18
+ if (!uuid || !fetch) {
19
+ return;
20
+ }
21
+
22
+ (async () => {
23
+ const res = await fetch?.(uuid);
24
+ setContent(res);
25
+ })();
26
+ }, [uuid, fetch]);
27
+
28
+ return content ? (
29
+ <div className="p-2">
30
+ <LabeledElement label={content.label} contentType="endnotes">
31
+ {content.content}
32
+ </LabeledElement>
33
+ </div>
34
+ ) : (
35
+ <Skeleton className="p-2 h-20 w-full" />
36
+ );
37
+ };
38
+
39
+ export default EndNoteLink;
@@ -0,0 +1,139 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@eightyfourthousand/design-system';
4
+ import { Editor } from '@tiptap/core';
5
+ import {
6
+ AlertCircleIcon,
7
+ AsteriskIcon,
8
+ Loader2Icon,
9
+ UnlinkIcon,
10
+ Trash2Icon,
11
+ } from 'lucide-react';
12
+ import { useCallback, useEffect, useState } from 'react';
13
+ import { useHoverCard } from '../../../shared/HoverCardProvider';
14
+ import { useNavigation } from '../../../shared/NavigationContext';
15
+ import { findEndnoteMarkByUuid, findPassageNode } from '../../util';
16
+ import { useEditorState } from '../../EditorProvider';
17
+ import { deleteEndnotePassageNode } from './endnote-utils';
18
+
19
+ const EDITOR_UPDATE_DELAY_MS = 100;
20
+
21
+ export const EndNoteLinkHoverContent = ({
22
+ uuid,
23
+ endNote,
24
+ editor,
25
+ }: {
26
+ uuid: string;
27
+ endNote: string;
28
+ editor: Editor;
29
+ anchor: HTMLElement;
30
+ }) => {
31
+ const [label, setLabel] = useState<string | undefined>();
32
+ const [labelState, setLabelState] = useState<
33
+ 'loading' | 'loaded' | 'error'
34
+ >('loading');
35
+ const { close, setIsEditing } = useHoverCard();
36
+ const { getEditor } = useEditorState();
37
+ const { fetchEndNote } = useNavigation();
38
+
39
+ useEffect(() => {
40
+ if (!endNote) {
41
+ setLabelState('error');
42
+ return;
43
+ }
44
+
45
+ // Try the local endnotes editor first — works for unpersisted passages too
46
+ const endnotesEditor = getEditor('endnotes');
47
+ if (endnotesEditor) {
48
+ const found = findPassageNode(endnotesEditor, endNote);
49
+ if (found?.node.attrs.label) {
50
+ setLabel(found.node.attrs.label);
51
+ setLabelState('loaded');
52
+ return;
53
+ }
54
+ }
55
+
56
+ // Fall back to network fetch
57
+ setLabelState('loading');
58
+ fetchEndNote(endNote).then((passage) => {
59
+ if (passage?.label) {
60
+ setLabel(passage.label);
61
+ setLabelState('loaded');
62
+ } else {
63
+ setLabelState('error');
64
+ }
65
+ });
66
+ }, [endNote, fetchEndNote, getEditor]);
67
+
68
+ const removeLink = useCallback(() => {
69
+ setIsEditing(false);
70
+ close();
71
+
72
+ setTimeout(() => {
73
+ const range = findEndnoteMarkByUuid({ editor, uuid });
74
+ if (!range) {
75
+ console.warn('EndNoteLink mark not found in the document.');
76
+ return;
77
+ }
78
+
79
+ const { from, to, mark } = range;
80
+ const { tr } = editor.state;
81
+ tr.removeMark(from, to, mark.type);
82
+ const notes = (mark.attrs.notes || []).filter(
83
+ (note: { uuid: string }) => uuid !== note.uuid,
84
+ );
85
+ if (notes.length > 0) {
86
+ tr.addMark(from, to, mark.type.create({ ...mark.attrs, notes }));
87
+ }
88
+ editor.view.dispatch(tr);
89
+ }, EDITOR_UPDATE_DELAY_MS);
90
+ }, [editor, uuid, close, setIsEditing]);
91
+
92
+ const deleteEndnoteAndLink = useCallback(() => {
93
+ setIsEditing(false);
94
+ close();
95
+
96
+ setTimeout(() => {
97
+ const endnotesEditor = getEditor('endnotes');
98
+ if (endnotesEditor) {
99
+ deleteEndnotePassageNode(endnotesEditor, endNote);
100
+ }
101
+ }, EDITOR_UPDATE_DELAY_MS);
102
+ }, [endNote, getEditor, close, setIsEditing]);
103
+
104
+ return (
105
+ <div className="flex justify-between gap-2 p-2 w-fit max-w-80">
106
+ <AsteriskIcon className="text-primary my-auto size-6 [&_svg]:size-4" />
107
+ {labelState === 'loading' && (
108
+ <Loader2Icon className="text-muted-foreground my-auto size-4 animate-spin" />
109
+ )}
110
+ {labelState === 'error' && (
111
+ <AlertCircleIcon className="text-destructive my-auto size-4" />
112
+ )}
113
+ {labelState === 'loaded' && (
114
+ <span className="truncate text-muted-foreground text-sm my-auto">
115
+ {label}
116
+ </span>
117
+ )}
118
+ <span className="flex-grow" />
119
+ <Button
120
+ variant="ghost"
121
+ size="icon"
122
+ className="size-6 [&_svg]:size-4"
123
+ title="Remove link"
124
+ onClick={removeLink}
125
+ >
126
+ <UnlinkIcon className="text-destructive my-auto" />
127
+ </Button>
128
+ <Button
129
+ variant="ghost"
130
+ size="icon"
131
+ className="size-6 [&_svg]:size-4"
132
+ title="Delete endnote"
133
+ onClick={deleteEndnoteAndLink}
134
+ >
135
+ <Trash2Icon className="text-destructive my-auto" />
136
+ </Button>
137
+ </div>
138
+ );
139
+ };
@@ -0,0 +1,236 @@
1
+ import { Mark, mergeAttributes } from '@tiptap/core';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import { cn } from '@eightyfourthousand/lib-utils';
4
+ import { createUpdateAttributes, registerEditorElement } from '../../util';
5
+
6
+ export interface EndNoteLinkOptions {
7
+ HTMLAttributes: Record<string, unknown>;
8
+ }
9
+
10
+ declare module '@tiptap/core' {
11
+ interface Commands<ReturnType> {
12
+ endNoteLink: {
13
+ setEndNoteLink: (endNote: string, label?: string) => ReturnType;
14
+ unsetEndNoteLink: () => ReturnType;
15
+ };
16
+ }
17
+ }
18
+
19
+ export const EndNoteLinkMark = Mark.create<EndNoteLinkOptions>({
20
+ name: 'endNoteLink',
21
+
22
+ addAttributes() {
23
+ return {
24
+ ...this.parent?.(),
25
+ notes: {
26
+ default: undefined,
27
+ parseHTML: (element) => element.getAttribute('notes'),
28
+ },
29
+ };
30
+ },
31
+
32
+ addOptions() {
33
+ return {
34
+ HTMLAttributes: {},
35
+ };
36
+ },
37
+
38
+ addMarkView() {
39
+ return (props) => {
40
+ const isEditable = props.editor.isEditable;
41
+ const className = isEditable
42
+ ? 'end-note-link select-text'
43
+ : 'end-note-link select-none';
44
+
45
+ const dom = document.createElement('span');
46
+ const contentDOM = document.createElement('span');
47
+ dom.appendChild(contentDOM);
48
+
49
+ // Define a visible default label when editable to indicate something
50
+ // _should_ be there.
51
+ const defaultLabel = isEditable ? '*' : '';
52
+ const notes: [
53
+ {
54
+ uuid: string;
55
+ location: string;
56
+ toh?: string;
57
+ endNote: string;
58
+ label?: string;
59
+ },
60
+ ] = props.mark.attrs.notes || [];
61
+ notes.sort((a, b) =>
62
+ (a.label || defaultLabel).localeCompare(b.label || defaultLabel),
63
+ );
64
+ notes.forEach(
65
+ (note: {
66
+ uuid: string;
67
+ location: string;
68
+ toh?: string;
69
+ endNote: string;
70
+ label?: string;
71
+ }) => {
72
+ const { uuid, location, toh, endNote, label } = note;
73
+ const isStart = location === 'start';
74
+
75
+ const endnoteDOM = document.createElement('sup');
76
+ const updateAttributes = createUpdateAttributes(endnoteDOM);
77
+ const attributes = mergeAttributes(this.options.HTMLAttributes, {
78
+ class: cn(className, toh, isStart ? 'me-0.75' : ''),
79
+ type: this.name,
80
+ endNote,
81
+ uuid,
82
+ });
83
+
84
+ updateAttributes(attributes);
85
+
86
+ // Register editor element for hover card detection in edit mode
87
+ if (isEditable) {
88
+ registerEditorElement(endnoteDOM, props.editor);
89
+ }
90
+
91
+ if (isStart) {
92
+ dom.insertBefore(endnoteDOM, dom.firstChild);
93
+ } else {
94
+ dom.appendChild(endnoteDOM);
95
+ }
96
+
97
+ const itemLabel = label?.split('.').pop() || defaultLabel;
98
+ endnoteDOM.textContent = itemLabel || defaultLabel;
99
+
100
+ endnoteDOM.addEventListener('click', () => {
101
+ if (!endNote) {
102
+ return;
103
+ }
104
+
105
+ const query = new URLSearchParams(window.location.search);
106
+ query.set('right', `open:endnotes:${endNote}`);
107
+ window.history.pushState({}, '', `?${query.toString()}`);
108
+ });
109
+ },
110
+ );
111
+
112
+ return {
113
+ dom,
114
+ contentDOM,
115
+ };
116
+ };
117
+ },
118
+
119
+ addCommands() {
120
+ return {
121
+ setEndNoteLink:
122
+ (endNote, label) =>
123
+ ({ state, dispatch }) => {
124
+ const { from, to, empty } = state.selection;
125
+ const markType = state.schema.marks[this.name];
126
+ if (!markType) return false;
127
+
128
+ const newNote = { uuid: uuidv4(), endNote, label };
129
+ const { tr } = state;
130
+
131
+ if (empty) {
132
+ // Cursor with no selection: append to stored/active marks
133
+ const existing = markType.isInSet(
134
+ state.storedMarks || state.doc.resolve(from).marks(),
135
+ );
136
+ const notes = existing
137
+ ? [...(existing.attrs.notes || []), newNote]
138
+ : [newNote];
139
+ tr.addStoredMark(markType.create({ notes }));
140
+ if (dispatch) dispatch(tr);
141
+ return true;
142
+ }
143
+
144
+ // Only the end of the selection matters — the endnote
145
+ // superscript renders there. Find the contiguous mark range
146
+ // that contains `to` within the same parent block.
147
+ const $to = state.doc.resolve(to);
148
+ const parent = $to.parent;
149
+ const parentStart = $to.start();
150
+
151
+ let markFrom = -1;
152
+ let markTo = -1;
153
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
154
+ let existingMark: any = null;
155
+ let found = false;
156
+
157
+ parent.forEach((child, offset) => {
158
+ if (found) return;
159
+
160
+ const childStart = parentStart + offset;
161
+ const childEnd = childStart + child.nodeSize;
162
+ const mark = markType.isInSet(child.marks);
163
+
164
+ if (mark && (markFrom === -1 || mark.eq(existingMark))) {
165
+ // Same mark (type + attrs) — extend the contiguous range
166
+ if (markFrom === -1) {
167
+ markFrom = childStart;
168
+ existingMark = mark;
169
+ }
170
+ markTo = childEnd;
171
+ } else if (markFrom !== -1) {
172
+ // End of a contiguous range (no mark, or different attrs)
173
+ if (to > markFrom && to <= markTo) {
174
+ found = true;
175
+ return;
176
+ }
177
+ if (mark) {
178
+ // Start a new range for a different endNoteLink mark
179
+ markFrom = childStart;
180
+ markTo = childEnd;
181
+ existingMark = mark;
182
+ } else {
183
+ markFrom = -1;
184
+ markTo = -1;
185
+ existingMark = null;
186
+ }
187
+ } else if (mark) {
188
+ markFrom = childStart;
189
+ markTo = childEnd;
190
+ existingMark = mark;
191
+ }
192
+ });
193
+
194
+ const toIsInMark =
195
+ existingMark && to > markFrom && to <= markTo;
196
+
197
+ if (!toIsInMark) {
198
+ // No existing mark at the end of the selection: create new
199
+ tr.addMark(from, to, markType.create({ notes: [newNote] }));
200
+ } else if (to === markTo) {
201
+ // `to` is exactly at the end of the mark — append the new
202
+ // note and keep the mark's original range unchanged.
203
+ const existingNotes = existingMark.attrs.notes || [];
204
+ tr.removeMark(markFrom, markTo, markType);
205
+ tr.addMark(
206
+ markFrom,
207
+ markTo,
208
+ markType.create({ notes: [...existingNotes, newNote] }),
209
+ );
210
+ } else {
211
+ // `to` is in the middle of the mark — split. Left side
212
+ // (up to `to`) gets only the new note, right side keeps
213
+ // the original mark unchanged.
214
+ tr.removeMark(markFrom, markTo, markType);
215
+ tr.addMark(
216
+ markFrom,
217
+ to,
218
+ markType.create({ notes: [newNote] }),
219
+ );
220
+ tr.addMark(to, markTo, existingMark);
221
+ }
222
+
223
+ if (dispatch) dispatch(tr);
224
+ return true;
225
+ },
226
+ unsetEndNoteLink:
227
+ () =>
228
+ ({ chain }) => {
229
+ return chain()
230
+ .unsetMark(this.name)
231
+ .resetAttributes(this.name, ['notes'])
232
+ .run();
233
+ },
234
+ };
235
+ },
236
+ });