@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,118 @@
1
+ import { incrementLabel, decrementLabel } from './label';
2
+
3
+ describe('incrementLabel', () => {
4
+ it('should increment the last component of a dotted label', () => {
5
+ expect(incrementLabel('1.5')).toBe('1.6');
6
+ });
7
+
8
+ it('should increment a single-component label', () => {
9
+ expect(incrementLabel('3')).toBe('4');
10
+ });
11
+
12
+ it('should increment a deeply nested label', () => {
13
+ expect(incrementLabel('1.2.3')).toBe('1.2.4');
14
+ });
15
+
16
+ it('should increment at a specific depth', () => {
17
+ expect(incrementLabel('1.5', 0)).toBe('2.5');
18
+ });
19
+
20
+ it('should increment the middle component of a three-part label', () => {
21
+ expect(incrementLabel('1.5.3', 1)).toBe('1.6.3');
22
+ });
23
+
24
+ it('should handle zero as the last component', () => {
25
+ expect(incrementLabel('1.0')).toBe('1.1');
26
+ });
27
+
28
+ it('should handle empty string', () => {
29
+ expect(incrementLabel('')).toBe('1');
30
+ });
31
+ });
32
+
33
+ describe('decrementLabel', () => {
34
+ it('should decrement the last component of a dotted label', () => {
35
+ expect(decrementLabel('1.5')).toBe('1.4');
36
+ });
37
+
38
+ it('should decrement a single-component label', () => {
39
+ expect(decrementLabel('3')).toBe('2');
40
+ });
41
+
42
+ it('should decrement a deeply nested label', () => {
43
+ expect(decrementLabel('1.2.3')).toBe('1.2.2');
44
+ });
45
+
46
+ it('should decrement at a specific depth', () => {
47
+ expect(decrementLabel('1.5', 0)).toBe('0.5');
48
+ });
49
+
50
+ it('should decrement the middle component of a three-part label', () => {
51
+ expect(decrementLabel('1.5.3', 1)).toBe('1.4.3');
52
+ });
53
+
54
+ it('should clamp at zero and not go negative', () => {
55
+ expect(decrementLabel('1.0')).toBe('1.0');
56
+ });
57
+
58
+ it('should clamp a single zero', () => {
59
+ expect(decrementLabel('0')).toBe('0');
60
+ });
61
+
62
+ it('should handle empty string', () => {
63
+ expect(decrementLabel('')).toBe('0');
64
+ });
65
+ });
66
+
67
+ describe('incrementLabel and decrementLabel are inverses', () => {
68
+ it('decrement after increment returns the original label', () => {
69
+ const original = '1.5';
70
+ expect(decrementLabel(incrementLabel(original))).toBe(original);
71
+ });
72
+
73
+ it('increment after decrement returns the original label', () => {
74
+ const original = '2.3';
75
+ expect(incrementLabel(decrementLabel(original))).toBe(original);
76
+ });
77
+
78
+ it('roundtrips at a specific depth', () => {
79
+ const original = '4.7.2';
80
+ expect(decrementLabel(incrementLabel(original, 1), 1)).toBe(original);
81
+ });
82
+ });
83
+
84
+ describe('normalizeLabelsAfter helpers', () => {
85
+ it('incrementLabel of empty string produces "1"', () => {
86
+ expect(incrementLabel('')).toBe('1');
87
+ });
88
+
89
+ it('repeated incrementLabel produces a contiguous sequence', () => {
90
+ let label = '1.2';
91
+ const sequence = [label];
92
+ for (let i = 0; i < 4; i++) {
93
+ label = incrementLabel(label);
94
+ sequence.push(label);
95
+ }
96
+ expect(sequence).toEqual(['1.2', '1.3', '1.4', '1.5', '1.6']);
97
+ });
98
+
99
+ it('repeated incrementLabel produces a contiguous sequence for single-component labels', () => {
100
+ let label = '3';
101
+ const sequence = [label];
102
+ for (let i = 0; i < 3; i++) {
103
+ label = incrementLabel(label);
104
+ sequence.push(label);
105
+ }
106
+ expect(sequence).toEqual(['3', '4', '5', '6']);
107
+ });
108
+
109
+ it('repeated incrementLabel produces a contiguous sequence for deeply nested labels', () => {
110
+ let label = '2.1.4';
111
+ const sequence = [label];
112
+ for (let i = 0; i < 3; i++) {
113
+ label = incrementLabel(label);
114
+ sequence.push(label);
115
+ }
116
+ expect(sequence).toEqual(['2.1.4', '2.1.5', '2.1.6', '2.1.7']);
117
+ });
118
+ });
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Increment the numeric component of a passage label at the given depth.
3
+ * If depth is -1 (default), increments the last component.
4
+ *
5
+ * Examples:
6
+ * incrementLabel("1.5") → "1.6"
7
+ * incrementLabel("3") → "4"
8
+ * incrementLabel("1.5", 0) → "2.5"
9
+ */
10
+ export const incrementLabel = (label: string, depth = -1) => {
11
+ const labelParts: (string | number)[] = ((label as string) || '').split('.');
12
+ const index = depth === -1 ? labelParts.length - 1 : depth;
13
+ const toIncrement = `${labelParts[index]}` || '0';
14
+ const newVal = Number.parseInt(toIncrement) + 1;
15
+ labelParts[index] = newVal;
16
+
17
+ return labelParts.join('.');
18
+ };
19
+
20
+ /**
21
+ * Decrement the numeric component of a passage label at the given depth.
22
+ * If depth is -1 (default), decrements the last component.
23
+ * Guards against going below 0.
24
+ *
25
+ * Examples:
26
+ * decrementLabel("1.5") → "1.4"
27
+ * decrementLabel("3") → "2"
28
+ * decrementLabel("1.0") → "1.0" (clamped at 0)
29
+ * decrementLabel("1.5", 0) → "0.5"
30
+ */
31
+ export const decrementLabel = (label: string, depth = -1) => {
32
+ const labelParts: (string | number)[] = ((label as string) || '').split('.');
33
+ const index = depth === -1 ? labelParts.length - 1 : depth;
34
+ const toDecrement = `${labelParts[index]}` || '0';
35
+ const newVal = Math.max(0, Number.parseInt(toDecrement) - 1);
36
+ labelParts[index] = newVal;
37
+
38
+ return labelParts.join('.');
39
+ };
@@ -0,0 +1,9 @@
1
+ import { cn } from '@eightyfourthousand/lib-utils';
2
+ import { Placeholder } from '@tiptap/extensions';
3
+
4
+ export default Placeholder.configure({
5
+ placeholder: 'Type / for commands...',
6
+ emptyEditorClass: cn('is-editor-empty text-gray-400'),
7
+ emptyNodeClass: cn('is-empty text-gray-400'),
8
+ includeChildren: true,
9
+ });
@@ -0,0 +1,65 @@
1
+ import { Editor, Extension, Range } from '@tiptap/core';
2
+ import { PluginKey } from '@tiptap/pm/state';
3
+ import Suggestion, { SuggestionOptions } from '@tiptap/suggestion';
4
+
5
+ type OnCommandSelect = (props: { editor: Editor; range: Range }) => void;
6
+
7
+ export interface SuggestionItem {
8
+ title: string;
9
+ description: string;
10
+ keywords: string[];
11
+ command: OnCommandSelect;
12
+ }
13
+
14
+ export interface SlashCommandNodeAttributes {
15
+ command: OnCommandSelect;
16
+ }
17
+
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ export interface SlashCommandOptions<Item extends SuggestionItem = any> {
20
+ /**
21
+ * The suggestion options.
22
+ *
23
+ * @default {}
24
+ * @example { char: '/', pluginKey: slashCommandPluginKey, command: ({editor, range, props}) => { ... } }
25
+ */
26
+ suggestion: Omit<
27
+ SuggestionOptions<Item, SlashCommandNodeAttributes>,
28
+ 'editor'
29
+ >;
30
+ }
31
+
32
+ /**
33
+ * The pluging key for the slash command extension.
34
+ * @default 'slashCommand'
35
+ */
36
+ export const slashCommandPluginKey = new PluginKey('slashCommand');
37
+
38
+ export const SlashCommand = Extension.create<SlashCommandOptions>({
39
+ name: 'slashCommand',
40
+
41
+ addOptions() {
42
+ return {
43
+ suggestion: {
44
+ char: '/',
45
+ pluginKey: slashCommandPluginKey,
46
+ command: ({ editor, range, props }) => {
47
+ props.command({ editor, range });
48
+ },
49
+ allow: () => {
50
+ // Add exceptions to when the suggestion should not be shown here.
51
+ return true;
52
+ },
53
+ },
54
+ };
55
+ },
56
+
57
+ addProseMirrorPlugins() {
58
+ return [
59
+ Suggestion({
60
+ editor: this.editor,
61
+ ...this.options.suggestion,
62
+ }),
63
+ ];
64
+ },
65
+ });
@@ -0,0 +1,109 @@
1
+ 'use client';
2
+
3
+ import { SlashCommandNodeAttributes, SuggestionItem } from './SlashCommand';
4
+ import { cn } from '@eightyfourthousand/lib-utils';
5
+ import { SuggestionKeyDownProps, SuggestionProps } from '@tiptap/suggestion';
6
+ import { LucideIcon } from 'lucide-react';
7
+ import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
8
+
9
+ export interface CommandSuggestionItem extends SuggestionItem {
10
+ icon: LucideIcon;
11
+ }
12
+
13
+ export interface SuggestionListHandle {
14
+ onKeyDown: (props: SuggestionKeyDownProps) => boolean;
15
+ }
16
+
17
+ export type SuggestionListProps = SuggestionProps<
18
+ CommandSuggestionItem,
19
+ SlashCommandNodeAttributes
20
+ >;
21
+
22
+ const SuggestionList = forwardRef<SuggestionListHandle, SuggestionListProps>(
23
+ (props, ref) => {
24
+ const [selectedIndex, setSelectedIndex] = useState(0);
25
+ const selectItem = (index: number) => {
26
+ const item = props.items[index];
27
+ if (item) {
28
+ props.command(item);
29
+ }
30
+ };
31
+
32
+ const incrementUp = () => {
33
+ setSelectedIndex(
34
+ (selectedIndex + props.items.length - 1) % props.items.length,
35
+ );
36
+ };
37
+
38
+ const incrementDown = () => {
39
+ setSelectedIndex((selectedIndex + 1) % props.items.length);
40
+ };
41
+
42
+ const onEnter = () => {
43
+ selectItem(selectedIndex);
44
+ };
45
+
46
+ useEffect(() => {
47
+ setSelectedIndex(0);
48
+ }, [props.items]);
49
+
50
+ useImperativeHandle(ref, () => ({
51
+ onKeyDown: ({ event }) => {
52
+ if (event.key === 'ArrowUp') {
53
+ incrementUp();
54
+ return true;
55
+ }
56
+ if (event.key === 'ArrowDown') {
57
+ incrementDown();
58
+ return true;
59
+ }
60
+ if (event.key === 'Enter') {
61
+ onEnter();
62
+ return true;
63
+ }
64
+ return false;
65
+ },
66
+ }));
67
+
68
+ return (
69
+ <div
70
+ className={cn(
71
+ 'z-50 bg-popover flex flex-col space-y-1 rounded-md border shadow-md transition-all p-1 max-h-[320px] w-72 overflow-y-auto',
72
+ )}
73
+ >
74
+ {props.items.length > 0 ? (
75
+ props.items.map((item, index) => (
76
+ <div
77
+ key={index}
78
+ className={cn(
79
+ 'flex space-x-2 hover:bg-muted p-1 rounded-md cursor-pointer text-foreground',
80
+ {
81
+ 'bg-muted': index === selectedIndex,
82
+ },
83
+ )}
84
+ onClick={() => {
85
+ props.command(item);
86
+ }}
87
+ >
88
+ <div className="size-10 flex items-center justify-center border bg-popover rounded-md">
89
+ <item.icon className="size-4" />
90
+ </div>
91
+ <div className="flex flex-col">
92
+ <p className="font-medium text-sm">{item.title}</p>
93
+ <span className="text-xs text-muted-foreground">
94
+ {item.description}
95
+ </span>
96
+ </div>
97
+ </div>
98
+ ))
99
+ ) : (
100
+ <div className="px-3 py-2 text-sm text-muted-foreground">
101
+ No results
102
+ </div>
103
+ )}
104
+ </div>
105
+ );
106
+ },
107
+ );
108
+
109
+ export default SuggestionList;
@@ -0,0 +1,185 @@
1
+ import {
2
+ Heading1Icon,
3
+ Heading2Icon,
4
+ Heading3Icon,
5
+ LetterTextIcon,
6
+ ListIcon,
7
+ ListOrderedIcon,
8
+ TextQuoteIcon,
9
+ } from 'lucide-react';
10
+ import tippy, { Instance } from 'tippy.js';
11
+ import { SlashCommandNodeAttributes } from './SlashCommand';
12
+ import SuggestionList, {
13
+ CommandSuggestionItem,
14
+ SuggestionListHandle,
15
+ SuggestionListProps,
16
+ } from './SuggestionList';
17
+ import { SuggestionOptions } from '@tiptap/suggestion';
18
+ import { ReactRenderer } from '@tiptap/react';
19
+
20
+ type SuggestionType = Omit<
21
+ SuggestionOptions<CommandSuggestionItem, SlashCommandNodeAttributes>,
22
+ 'editor'
23
+ >;
24
+
25
+ export const TextSuggestion: CommandSuggestionItem = {
26
+ title: 'Text',
27
+ description: 'Just start typing',
28
+ keywords: ['text', 'paragraph', 'p'],
29
+ icon: LetterTextIcon,
30
+ command: ({ editor }) => {
31
+ editor.chain().focus().toggleNode('paragraph', 'paragraph').run();
32
+ },
33
+ };
34
+
35
+ export const Heading1Suggestion: CommandSuggestionItem = {
36
+ title: 'Heading 1',
37
+ description: 'Big section heading.',
38
+ keywords: ['title', 'big', 'large', 'heading'],
39
+ icon: Heading1Icon,
40
+ command: ({ editor, range }) => {
41
+ editor
42
+ .chain()
43
+ .focus()
44
+ .deleteRange(range)
45
+ .setNode('heading', { level: 1 })
46
+ .run();
47
+ },
48
+ };
49
+
50
+ export const Heading2Suggestion: CommandSuggestionItem = {
51
+ title: 'Heading 2',
52
+ description: 'Medium section heading.',
53
+ keywords: ['subtitle', 'medium', 'heading'],
54
+ icon: Heading2Icon,
55
+ command: ({ editor, range }) => {
56
+ editor
57
+ .chain()
58
+ .focus()
59
+ .deleteRange(range)
60
+ .setNode('heading', { level: 2 })
61
+ .run();
62
+ },
63
+ };
64
+
65
+ export const Heading3Suggestion: CommandSuggestionItem = {
66
+ title: 'Heading 3',
67
+ description: 'Small section heading.',
68
+ keywords: ['subtitle', 'small', 'heading'],
69
+ icon: Heading3Icon,
70
+ command: ({ editor, range }) => {
71
+ editor
72
+ .chain()
73
+ .focus()
74
+ .deleteRange(range)
75
+ .setNode('heading', { level: 3 })
76
+ .run();
77
+ },
78
+ };
79
+
80
+ export const BulletListSuggestion: CommandSuggestionItem = {
81
+ title: 'Bullet List',
82
+ description: 'Create a simple bullet list.',
83
+ keywords: ['unordered', 'list', 'bullet'],
84
+ icon: ListIcon,
85
+ command: ({ editor, range }) => {
86
+ editor.chain().focus().deleteRange(range).toggleBulletList().run();
87
+ },
88
+ };
89
+
90
+ export const NumberListSuggestion: CommandSuggestionItem = {
91
+ title: 'Numbered List',
92
+ description: 'Create a list with numbering.',
93
+ keywords: ['ordered', 'list'],
94
+ icon: ListOrderedIcon,
95
+ command: ({ editor, range }) => {
96
+ editor.chain().focus().deleteRange(range).toggleOrderedList().run();
97
+ },
98
+ };
99
+
100
+ export const QuoteSuggestion: CommandSuggestionItem = {
101
+ title: 'Quote',
102
+ description: 'Capture a quote.',
103
+ keywords: ['blockquote'],
104
+ icon: TextQuoteIcon,
105
+ command: ({ editor, range }) =>
106
+ editor
107
+ .chain()
108
+ .focus()
109
+ .deleteRange(range)
110
+ .toggleNode('paragraph', 'paragraph')
111
+ .toggleBlockquote()
112
+ .run(),
113
+ };
114
+
115
+ const defaultSuggestions: CommandSuggestionItem[] = [
116
+ TextSuggestion,
117
+ Heading1Suggestion,
118
+ Heading2Suggestion,
119
+ Heading3Suggestion,
120
+ BulletListSuggestion,
121
+ NumberListSuggestion,
122
+ QuoteSuggestion,
123
+ ];
124
+
125
+ export const getSuggestion = (
126
+ suggestions: CommandSuggestionItem[] = defaultSuggestions,
127
+ ): SuggestionType => {
128
+ return {
129
+ items: ({ query }) => {
130
+ return suggestions.filter((item: CommandSuggestionItem) => {
131
+ return item.keywords.some((kwd) => kwd.startsWith(query.toLowerCase()));
132
+ });
133
+ },
134
+ render: () => {
135
+ let component: ReactRenderer<SuggestionListHandle, SuggestionListProps>;
136
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
137
+ let popup: Instance<any>[];
138
+ return {
139
+ onStart: (props) => {
140
+ component = new ReactRenderer(SuggestionList, {
141
+ props,
142
+ editor: props.editor,
143
+ });
144
+
145
+ const clientRect = props.clientRect?.();
146
+ const getReferenceClientRect = clientRect ? () => clientRect : null;
147
+ popup = tippy('body', {
148
+ getReferenceClientRect,
149
+ content: component.element,
150
+ showOnCreate: true,
151
+ interactive: true,
152
+ trigger: 'manual',
153
+ placement: 'bottom-start',
154
+ appendTo: () => document.body,
155
+ });
156
+ },
157
+
158
+ onUpdate(props) {
159
+ component.updateProps(props);
160
+
161
+ if (!props.clientRect) {
162
+ return;
163
+ }
164
+
165
+ popup[0].setProps({
166
+ getReferenceClientRect: props.clientRect,
167
+ });
168
+ },
169
+
170
+ onKeyDown(props) {
171
+ if (props.event.key === 'Escape') {
172
+ popup[0].hide();
173
+ return true;
174
+ }
175
+ return component.ref?.onKeyDown(props) ?? false;
176
+ },
177
+
178
+ onExit() {
179
+ component.destroy();
180
+ popup[0].destroy();
181
+ },
182
+ };
183
+ },
184
+ };
185
+ };
@@ -0,0 +1,110 @@
1
+ import { Mark, mergeAttributes } from '@tiptap/core';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+
4
+ export interface SmallCapsOptions {
5
+ /**
6
+ * HTML attributes to add to the small caps element.
7
+ * @default {}
8
+ * @example { class: 'foo' }
9
+ */
10
+ HTMLAttributes: Record<string, unknown>;
11
+ }
12
+
13
+ declare module '@tiptap/core' {
14
+ interface Commands<ReturnType> {
15
+ smallCaps: {
16
+ /**
17
+ * Set a small caps mark
18
+ * @example editor.commands.setSmallCaps()
19
+ */
20
+ setSmallCaps: () => ReturnType;
21
+ /**
22
+ * Toggle a small caps mark
23
+ * @example editor.commands.toggleSmallCaps()
24
+ */
25
+ toggleSmallCaps: () => ReturnType;
26
+ /**
27
+ * Unset a small caps mark
28
+ * @example editor.commands.unsetSmallCaps()
29
+ */
30
+ unsetSmallCaps: () => ReturnType;
31
+ };
32
+ }
33
+ }
34
+
35
+ /**
36
+ * This extension allows you to create small caps text.
37
+ */
38
+ export const SmallCaps = Mark.create<SmallCapsOptions>({
39
+ name: 'smallCaps',
40
+ addAttributes() {
41
+ return {
42
+ type: {
43
+ default: undefined,
44
+ parseHTML: (element) => element.getAttribute('data-type'),
45
+ renderHTML(attributes) {
46
+ return mergeAttributes(attributes, { 'data-type': attributes.type });
47
+ },
48
+ },
49
+ textStyle: {
50
+ default: undefined,
51
+ parseHTML: (element) => element.getAttribute('data-text-style'),
52
+ renderHTML(attributes) {
53
+ return mergeAttributes(attributes, {
54
+ 'data-text-style': attributes.textStyle,
55
+ });
56
+ },
57
+ },
58
+ };
59
+ },
60
+
61
+ addOptions() {
62
+ return {
63
+ HTMLAttributes: {},
64
+ };
65
+ },
66
+
67
+ parseHTML() {
68
+ return [
69
+ {
70
+ tag: 'sm[type="small-caps"]',
71
+ },
72
+ ];
73
+ },
74
+
75
+ renderHTML({ HTMLAttributes }) {
76
+ return [
77
+ 'sm',
78
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
79
+ class: 'uppercase',
80
+ }),
81
+ 0,
82
+ ];
83
+ },
84
+
85
+ addCommands() {
86
+ return {
87
+ setSmallCaps:
88
+ () =>
89
+ ({ commands }) => {
90
+ return commands.setMark(this.name, { uuid: uuidv4() });
91
+ },
92
+ toggleSmallCaps:
93
+ () =>
94
+ ({ commands }) => {
95
+ return commands.toggleMark(this.name, { uuid: uuidv4() });
96
+ },
97
+ unsetSmallCaps:
98
+ () =>
99
+ ({ commands }) => {
100
+ return commands.unsetMark(this.name);
101
+ },
102
+ };
103
+ },
104
+
105
+ addKeyboardShortcuts() {
106
+ return {
107
+ 'Mod-+': () => this.editor.commands.toggleSmallCaps(),
108
+ };
109
+ },
110
+ });
@@ -0,0 +1,36 @@
1
+ import {
2
+ StarterKitOptions,
3
+ StarterKit as TiptapStarterKit,
4
+ } from '@tiptap/starter-kit';
5
+ import { BLOCKQUOTE_STYLE, CODE_STYLE, OL_STYLE } from '@eightyfourthousand/design-system';
6
+
7
+ export const STARTER_KIT_CONFIG: Partial<StarterKitOptions> = {
8
+ document: false,
9
+ heading: false,
10
+ paragraph: false,
11
+ italic: false,
12
+ link: false,
13
+ underline: false,
14
+ bold: false,
15
+ bulletList: false,
16
+ listItem: false,
17
+ code: {
18
+ HTMLAttributes: {
19
+ class: CODE_STYLE,
20
+ },
21
+ },
22
+ orderedList: {
23
+ HTMLAttributes: {
24
+ class: OL_STYLE,
25
+ },
26
+ },
27
+ blockquote: {
28
+ HTMLAttributes: {
29
+ class: BLOCKQUOTE_STYLE,
30
+ },
31
+ },
32
+ };
33
+
34
+ export const StarterKit = TiptapStarterKit.configure(STARTER_KIT_CONFIG);
35
+
36
+ export default StarterKit;