bookish-press 0.4.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 (309) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/components/Auth.svelte +52 -0
  4. package/components/Auth.svelte.d.ts +16 -0
  5. package/components/Authentication.d.ts +8 -0
  6. package/components/Authentication.js +1 -0
  7. package/components/BookPreview.svelte +87 -0
  8. package/components/BookPreview.svelte.d.ts +18 -0
  9. package/components/External.svelte +4 -0
  10. package/components/External.svelte.d.ts +18 -0
  11. package/components/Link.svelte +5 -0
  12. package/components/Link.svelte.d.ts +19 -0
  13. package/components/chapter/Atom.svelte +36 -0
  14. package/components/chapter/Atom.svelte.d.ts +19 -0
  15. package/components/chapter/Block.svelte +32 -0
  16. package/components/chapter/Block.svelte.d.ts +17 -0
  17. package/components/chapter/Callout.svelte +10 -0
  18. package/components/chapter/Callout.svelte.d.ts +17 -0
  19. package/components/chapter/CaptionedCode.svelte +34 -0
  20. package/components/chapter/CaptionedCode.svelte.d.ts +17 -0
  21. package/components/chapter/ChapterBody.svelte +23 -0
  22. package/components/chapter/ChapterBody.svelte.d.ts +18 -0
  23. package/components/chapter/Citations.svelte +66 -0
  24. package/components/chapter/Citations.svelte.d.ts +17 -0
  25. package/components/chapter/Code.svelte +35 -0
  26. package/components/chapter/Code.svelte.d.ts +22 -0
  27. package/components/chapter/Comment.svelte +46 -0
  28. package/components/chapter/Comment.svelte.d.ts +17 -0
  29. package/components/chapter/Definition.svelte +38 -0
  30. package/components/chapter/Definition.svelte.d.ts +20 -0
  31. package/components/chapter/Embed.svelte +122 -0
  32. package/components/chapter/Embed.svelte.d.ts +18 -0
  33. package/components/chapter/ErrorMessage.svelte +4 -0
  34. package/components/chapter/ErrorMessage.svelte.d.ts +17 -0
  35. package/components/chapter/Footnote.svelte +37 -0
  36. package/components/chapter/Footnote.svelte.d.ts +19 -0
  37. package/components/chapter/Format.svelte +57 -0
  38. package/components/chapter/Format.svelte.d.ts +18 -0
  39. package/components/chapter/InlineCode.svelte +12 -0
  40. package/components/chapter/InlineCode.svelte.d.ts +17 -0
  41. package/components/chapter/Label.svelte +20 -0
  42. package/components/chapter/Label.svelte.d.ts +17 -0
  43. package/components/chapter/Link.svelte +51 -0
  44. package/components/chapter/Link.svelte.d.ts +17 -0
  45. package/components/chapter/List.svelte +14 -0
  46. package/components/chapter/List.svelte.d.ts +17 -0
  47. package/components/chapter/Marginal.svelte +48 -0
  48. package/components/chapter/Marginal.svelte.d.ts +19 -0
  49. package/components/chapter/Paragraph.svelte +15 -0
  50. package/components/chapter/Paragraph.svelte.d.ts +18 -0
  51. package/components/chapter/Python.svelte +81 -0
  52. package/components/chapter/Python.svelte.d.ts +18 -0
  53. package/components/chapter/Quote.svelte +16 -0
  54. package/components/chapter/Quote.svelte.d.ts +17 -0
  55. package/components/chapter/Rule.svelte +4 -0
  56. package/components/chapter/Rule.svelte.d.ts +17 -0
  57. package/components/chapter/Table.svelte +29 -0
  58. package/components/chapter/Table.svelte.d.ts +17 -0
  59. package/components/chapter/Text.svelte +62 -0
  60. package/components/chapter/Text.svelte.d.ts +17 -0
  61. package/components/chapter/renderPosition.d.ts +3 -0
  62. package/components/chapter/renderPosition.js +4 -0
  63. package/components/editor/BookishEditor.svelte +755 -0
  64. package/components/editor/BookishEditor.svelte.d.ts +22 -0
  65. package/components/editor/CalloutEditor.svelte +10 -0
  66. package/components/editor/CalloutEditor.svelte.d.ts +17 -0
  67. package/components/editor/CaptionedCodeEditor.svelte +22 -0
  68. package/components/editor/CaptionedCodeEditor.svelte.d.ts +17 -0
  69. package/components/editor/CaretContext.d.ts +18 -0
  70. package/components/editor/CaretContext.js +1 -0
  71. package/components/editor/CaretState.d.ts +47 -0
  72. package/components/editor/CaretState.js +1 -0
  73. package/components/editor/CaretUtilities.d.ts +5 -0
  74. package/components/editor/CaretUtilities.js +1 -0
  75. package/components/editor/CitationsEditor.svelte +42 -0
  76. package/components/editor/CitationsEditor.svelte.d.ts +17 -0
  77. package/components/editor/Clipboard.d.ts +3 -0
  78. package/components/editor/Clipboard.js +1 -0
  79. package/components/editor/Command.d.ts +18 -0
  80. package/components/editor/Command.js +1 -0
  81. package/components/editor/Commands.d.ts +3 -0
  82. package/components/editor/Commands.js +951 -0
  83. package/components/editor/ConfirmButton.svelte +43 -0
  84. package/components/editor/ConfirmButton.svelte.d.ts +18 -0
  85. package/components/editor/DefinitionEditor.svelte +28 -0
  86. package/components/editor/DefinitionEditor.svelte.d.ts +17 -0
  87. package/components/editor/EmbedEditor.svelte +87 -0
  88. package/components/editor/EmbedEditor.svelte.d.ts +17 -0
  89. package/components/editor/ImageChooser.svelte +70 -0
  90. package/components/editor/ImageChooser.svelte.d.ts +18 -0
  91. package/components/editor/InlineCodeEditor.svelte +9 -0
  92. package/components/editor/InlineCodeEditor.svelte.d.ts +17 -0
  93. package/components/editor/LabelEditor.svelte +23 -0
  94. package/components/editor/LabelEditor.svelte.d.ts +17 -0
  95. package/components/editor/LanguageEditor.svelte +11 -0
  96. package/components/editor/LanguageEditor.svelte.d.ts +17 -0
  97. package/components/editor/LinkEditor.svelte +75 -0
  98. package/components/editor/LinkEditor.svelte.d.ts +17 -0
  99. package/components/editor/Note.svelte +9 -0
  100. package/components/editor/Note.svelte.d.ts +23 -0
  101. package/components/editor/PositionEditor.svelte +14 -0
  102. package/components/editor/PositionEditor.svelte.d.ts +18 -0
  103. package/components/editor/QuoteEditor.svelte +5 -0
  104. package/components/editor/QuoteEditor.svelte.d.ts +17 -0
  105. package/components/editor/Switch.svelte +56 -0
  106. package/components/editor/Switch.svelte.d.ts +20 -0
  107. package/components/editor/TextEditor.svelte +193 -0
  108. package/components/editor/TextEditor.svelte.d.ts +23 -0
  109. package/components/editor/Toggle.svelte +38 -0
  110. package/components/editor/Toggle.svelte.d.ts +19 -0
  111. package/components/editor/Toolbar.svelte +202 -0
  112. package/components/editor/Toolbar.svelte.d.ts +20 -0
  113. package/components/editor/ToolbarGroup.svelte +41 -0
  114. package/components/editor/ToolbarGroup.svelte.d.ts +19 -0
  115. package/components/editor/ToolbarIcon.svelte +22 -0
  116. package/components/editor/ToolbarIcon.svelte.d.ts +16 -0
  117. package/components/editor/ToolbarSpacer.svelte +1 -0
  118. package/components/editor/ToolbarSpacer.svelte.d.ts +19 -0
  119. package/components/editor/URLEditor.svelte +15 -0
  120. package/components/editor/URLEditor.svelte.d.ts +18 -0
  121. package/components/editor/UndoState.d.ts +8 -0
  122. package/components/editor/UndoState.js +1 -0
  123. package/components/page/Acknowledgements.svelte +29 -0
  124. package/components/page/Acknowledgements.svelte.d.ts +14 -0
  125. package/components/page/Alert.svelte +11 -0
  126. package/components/page/Alert.svelte.d.ts +23 -0
  127. package/components/page/Authors.svelte +62 -0
  128. package/components/page/Authors.svelte.d.ts +20 -0
  129. package/components/page/BulkReferenceEditor.svelte +32 -0
  130. package/components/page/BulkReferenceEditor.svelte.d.ts +14 -0
  131. package/components/page/Chapter.svelte +301 -0
  132. package/components/page/Chapter.svelte.d.ts +18 -0
  133. package/components/page/ChapterContext.d.ts +10 -0
  134. package/components/page/ChapterContext.js +1 -0
  135. package/components/page/Contexts.d.ts +27 -0
  136. package/components/page/Contexts.js +21 -0
  137. package/components/page/DefinitionView.svelte +129 -0
  138. package/components/page/DefinitionView.svelte.d.ts +18 -0
  139. package/components/page/Description.svelte +22 -0
  140. package/components/page/Description.svelte.d.ts +14 -0
  141. package/components/page/Edition.svelte +106 -0
  142. package/components/page/Edition.svelte.d.ts +21 -0
  143. package/components/page/Glossary.svelte +61 -0
  144. package/components/page/Glossary.svelte.d.ts +14 -0
  145. package/components/page/Header.svelte +105 -0
  146. package/components/page/Header.svelte.d.ts +27 -0
  147. package/components/page/Index.svelte +117 -0
  148. package/components/page/Index.svelte.d.ts +14 -0
  149. package/components/page/Instructions.svelte +30 -0
  150. package/components/page/Instructions.svelte.d.ts +16 -0
  151. package/components/page/License.svelte +29 -0
  152. package/components/page/License.svelte.d.ts +14 -0
  153. package/components/page/Loading.svelte +13 -0
  154. package/components/page/Loading.svelte.d.ts +19 -0
  155. package/components/page/Media.svelte +76 -0
  156. package/components/page/Media.svelte.d.ts +14 -0
  157. package/components/page/MediaPreview.svelte +11 -0
  158. package/components/page/MediaPreview.svelte.d.ts +19 -0
  159. package/components/page/Outline.svelte +173 -0
  160. package/components/page/Outline.svelte.d.ts +19 -0
  161. package/components/page/Page.svelte +53 -0
  162. package/components/page/Page.svelte.d.ts +19 -0
  163. package/components/page/PossibleChapter.svelte +14 -0
  164. package/components/page/PossibleChapter.svelte.d.ts +14 -0
  165. package/components/page/PossibleReference.svelte +16 -0
  166. package/components/page/PossibleReference.svelte.d.ts +19 -0
  167. package/components/page/Print.svelte +8 -0
  168. package/components/page/Print.svelte.d.ts +14 -0
  169. package/components/page/Reference.svelte +145 -0
  170. package/components/page/Reference.svelte.d.ts +17 -0
  171. package/components/page/References.svelte +58 -0
  172. package/components/page/References.svelte.d.ts +14 -0
  173. package/components/page/Revisions.svelte +124 -0
  174. package/components/page/Revisions.svelte.d.ts +14 -0
  175. package/components/page/Search.svelte +108 -0
  176. package/components/page/Search.svelte.d.ts +14 -0
  177. package/components/page/Status.svelte +49 -0
  178. package/components/page/Status.svelte.d.ts +19 -0
  179. package/components/page/SubdomainEditor.svelte +30 -0
  180. package/components/page/SubdomainEditor.svelte.d.ts +14 -0
  181. package/components/page/TableOfContents.svelte +234 -0
  182. package/components/page/TableOfContents.svelte.d.ts +14 -0
  183. package/components/page/TableOfContentsImage.svelte +15 -0
  184. package/components/page/TableOfContentsImage.svelte.d.ts +16 -0
  185. package/components/page/TableOfContentsRow.svelte +57 -0
  186. package/components/page/TableOfContentsRow.svelte.d.ts +24 -0
  187. package/components/page/ThemeEditor.svelte +52 -0
  188. package/components/page/ThemeEditor.svelte.d.ts +14 -0
  189. package/components/page/ThemeEditorPreview.svelte +37 -0
  190. package/components/page/ThemeEditorPreview.svelte.d.ts +17 -0
  191. package/components/page/ThemeSetEditor.svelte +17 -0
  192. package/components/page/ThemeSetEditor.svelte.d.ts +18 -0
  193. package/components/page/ThemeVariableEditor.svelte +28 -0
  194. package/components/page/ThemeVariableEditor.svelte.d.ts +18 -0
  195. package/components/page/Unknown.svelte +29 -0
  196. package/components/page/Unknown.svelte.d.ts +16 -0
  197. package/components/page/margins.d.ts +2 -0
  198. package/components/page/margins.js +132 -0
  199. package/models/BookLoader.d.ts +2 -0
  200. package/models/BookLoader.js +67 -0
  201. package/models/Firebase.d.ts +4 -0
  202. package/models/Firebase.js +26 -0
  203. package/models/Firestore.d.ts +22 -0
  204. package/models/Firestore.js +194 -0
  205. package/models/book/Book.d.ts +60 -0
  206. package/models/book/Book.js +135 -0
  207. package/models/book/BookMedia.d.ts +23 -0
  208. package/models/book/BookMedia.js +102 -0
  209. package/models/book/Chapter.d.ts +70 -0
  210. package/models/book/Chapter.js +221 -0
  211. package/models/book/ChapterID.d.ts +10 -0
  212. package/models/book/ChapterID.js +10 -0
  213. package/models/book/Definition.d.ts +6 -0
  214. package/models/book/Definition.js +1 -0
  215. package/models/book/Edition.d.ts +111 -0
  216. package/models/book/Edition.js +466 -0
  217. package/models/book/Theme.d.ts +106 -0
  218. package/models/book/Theme.js +76 -0
  219. package/models/book/ThemeColors.d.ts +15 -0
  220. package/models/book/ThemeColors.js +1 -0
  221. package/models/chapter/AtomNode.d.ts +18 -0
  222. package/models/chapter/AtomNode.js +31 -0
  223. package/models/chapter/BlockNode.d.ts +21 -0
  224. package/models/chapter/BlockNode.js +117 -0
  225. package/models/chapter/BlocksNode.d.ts +50 -0
  226. package/models/chapter/BlocksNode.js +709 -0
  227. package/models/chapter/BlocksNode.test.d.ts +1 -0
  228. package/models/chapter/BlocksNode.test.js +302 -0
  229. package/models/chapter/Bookkeeping.d.ts +4 -0
  230. package/models/chapter/Bookkeeping.js +1 -0
  231. package/models/chapter/CalloutNode.d.ts +17 -0
  232. package/models/chapter/CalloutNode.js +36 -0
  233. package/models/chapter/Caret.d.ts +21 -0
  234. package/models/chapter/Caret.js +69 -0
  235. package/models/chapter/ChapterNode.d.ts +34 -0
  236. package/models/chapter/ChapterNode.js +80 -0
  237. package/models/chapter/CitationsNode.d.ts +16 -0
  238. package/models/chapter/CitationsNode.js +17 -0
  239. package/models/chapter/CodeNode.d.ts +30 -0
  240. package/models/chapter/CodeNode.js +64 -0
  241. package/models/chapter/CommentNode.d.ts +18 -0
  242. package/models/chapter/CommentNode.js +32 -0
  243. package/models/chapter/DefinitionNode.d.ts +12 -0
  244. package/models/chapter/DefinitionNode.js +20 -0
  245. package/models/chapter/Edit.d.ts +7 -0
  246. package/models/chapter/Edit.js +1 -0
  247. package/models/chapter/EmbedNode.d.ts +40 -0
  248. package/models/chapter/EmbedNode.js +89 -0
  249. package/models/chapter/ErrorNode.d.ts +17 -0
  250. package/models/chapter/ErrorNode.js +21 -0
  251. package/models/chapter/FootnoteNode.d.ts +18 -0
  252. package/models/chapter/FootnoteNode.js +29 -0
  253. package/models/chapter/FormatNode.d.ts +60 -0
  254. package/models/chapter/FormatNode.js +614 -0
  255. package/models/chapter/FormatNode.test.d.ts +1 -0
  256. package/models/chapter/FormatNode.test.js +54 -0
  257. package/models/chapter/InlineCodeNode.d.ts +12 -0
  258. package/models/chapter/InlineCodeNode.js +18 -0
  259. package/models/chapter/LabelNode.d.ts +17 -0
  260. package/models/chapter/LabelNode.js +22 -0
  261. package/models/chapter/LinkNode.d.ts +12 -0
  262. package/models/chapter/LinkNode.js +16 -0
  263. package/models/chapter/ListNode.d.ts +51 -0
  264. package/models/chapter/ListNode.js +370 -0
  265. package/models/chapter/ListNode.test.d.ts +1 -0
  266. package/models/chapter/ListNode.test.js +49 -0
  267. package/models/chapter/MetadataNode.d.ts +17 -0
  268. package/models/chapter/MetadataNode.js +24 -0
  269. package/models/chapter/Node.d.ts +36 -0
  270. package/models/chapter/Node.js +173 -0
  271. package/models/chapter/Node.test.d.ts +1 -0
  272. package/models/chapter/Node.test.js +27 -0
  273. package/models/chapter/ParagraphNode.d.ts +33 -0
  274. package/models/chapter/ParagraphNode.js +93 -0
  275. package/models/chapter/ParagraphNode.test.d.ts +1 -0
  276. package/models/chapter/ParagraphNode.test.js +21 -0
  277. package/models/chapter/Parser.d.ts +75 -0
  278. package/models/chapter/Parser.js +824 -0
  279. package/models/chapter/Parser.test.d.ts +1 -0
  280. package/models/chapter/Parser.test.js +25 -0
  281. package/models/chapter/Position.d.ts +2 -0
  282. package/models/chapter/Position.js +1 -0
  283. package/models/chapter/QuoteNode.d.ts +25 -0
  284. package/models/chapter/QuoteNode.js +61 -0
  285. package/models/chapter/ReferenceNode.d.ts +29 -0
  286. package/models/chapter/ReferenceNode.js +48 -0
  287. package/models/chapter/RootNode.d.ts +5 -0
  288. package/models/chapter/RootNode.js +1 -0
  289. package/models/chapter/RuleNode.d.ts +15 -0
  290. package/models/chapter/RuleNode.js +16 -0
  291. package/models/chapter/TableNode.d.ts +35 -0
  292. package/models/chapter/TableNode.js +176 -0
  293. package/models/chapter/TableNode.test.d.ts +1 -0
  294. package/models/chapter/TableNode.test.js +31 -0
  295. package/models/chapter/TextNode.d.ts +36 -0
  296. package/models/chapter/TextNode.js +136 -0
  297. package/models/chapter/TextNode.test.d.ts +1 -0
  298. package/models/chapter/TextNode.test.js +6 -0
  299. package/models/loadEdition.d.ts +5 -0
  300. package/models/loadEdition.js +16 -0
  301. package/package.json +189 -0
  302. package/util/getSubdomain.d.ts +3 -0
  303. package/util/getSubdomain.js +22 -0
  304. package/util/isMobile.d.ts +2 -0
  305. package/util/isMobile.js +6 -0
  306. package/util/mineReference.d.ts +3 -0
  307. package/util/mineReference.js +62 -0
  308. package/util/scrollToEyeLevel.d.ts +2 -0
  309. package/util/scrollToEyeLevel.js +5 -0
@@ -0,0 +1,614 @@
1
+ import Node from "./Node";
2
+ import TextNode from "./TextNode";
3
+ import { caretRangeToIndexRange, caretToIndex, indexToCaret } from "./Caret";
4
+ import MetadataNode from "./MetadataNode";
5
+ import AtomNode from "./AtomNode";
6
+ export default class FormatNode extends Node {
7
+ #format;
8
+ #segments;
9
+ constructor(format, segments) {
10
+ super();
11
+ this.#format = format;
12
+ // Remove consecutive empty text nodes.
13
+ this.#segments = segments.filter((item, position, list) => {
14
+ if (position === 0 || !(item instanceof TextNode) || item.getLength() > 0)
15
+ return true;
16
+ const previous = list[position - 1];
17
+ return !(previous instanceof TextNode && previous.getLength() === 0);
18
+ });
19
+ }
20
+ getType() { return "formatted"; }
21
+ getFormat() { return this.#format; }
22
+ getSegments() { return this.#segments; }
23
+ isEmpty() { return this.#segments.length === 0; }
24
+ isEmptyText() { return this.#segments.length === 1 && this.#segments[0] instanceof TextNode && this.#segments[0].getText() === ""; }
25
+ getLength() { return this.#segments.length; }
26
+ getParentOf(node) {
27
+ return this.#segments.map(b => b === node ? this : b.getParentOf(node)).find(b => b !== undefined);
28
+ }
29
+ toText() {
30
+ return this.#segments.map(segment => segment.toText()).join(" ");
31
+ }
32
+ toHTML() {
33
+ const format = this.#format;
34
+ const segments = this.#segments.map(s => s.toHTML()).join("");
35
+ if (format === "*")
36
+ return `<strong>${segments}</strong>`;
37
+ else if (format === "_")
38
+ return `<em>${segments}</em>`;
39
+ else if (format === "^")
40
+ return `<sup>${segments}</sup>`;
41
+ else if (format === "v")
42
+ return `<sub>${segments}</sub>`;
43
+ else
44
+ return segments;
45
+ }
46
+ toBookdown() {
47
+ return (this.#format === "v" ? "^v" : this.#format) + this.#segments.map(s => s instanceof AtomNode ? s.toBookdown(this) : s.toBookdown()).join("") + (this.#format === "v" ? "^" : this.#format);
48
+ }
49
+ getChildren() { return this.#segments; }
50
+ getFirstTextNode() {
51
+ const text = this.getTextNodes();
52
+ return text[0];
53
+ }
54
+ getLastTextNode() {
55
+ const text = this.getTextNodes();
56
+ return text[text.length - 1];
57
+ }
58
+ copy() {
59
+ return new FormatNode(this.#format, this.#segments.map(s => s.copy()));
60
+ }
61
+ getTextNodes() {
62
+ return this.getNodes().filter(n => n instanceof TextNode);
63
+ }
64
+ // Get all text or atom nodes in this format, except for any inside an atom node,
65
+ // as this is used for navigation and editing, and they should be isolated from that.
66
+ getTextAndAtomNodes() {
67
+ return this.getNodes().filter(n => (n instanceof TextNode || n instanceof AtomNode));
68
+ }
69
+ getAdjacentTextOrAtom(node, next) {
70
+ // Find all of the text and atom nodes not in atom nodes in this node.
71
+ const text = this.getTextAndAtomNodes().filter(n => {
72
+ const atomAncestor = n.getClosestParentOfType(this, AtomNode);
73
+ return atomAncestor === undefined || (atomAncestor instanceof AtomNode && atomAncestor.getMeta() === this);
74
+ });
75
+ const index = text.indexOf(node);
76
+ return index === undefined ? undefined :
77
+ (next ?
78
+ (index >= 0 && index < text.length - 1 ? text[index + 1] : undefined) :
79
+ (index > 0 && index <= text.length ? text[index - 1] : undefined));
80
+ }
81
+ getNextTextOrAtom(node) {
82
+ return this.getAdjacentTextOrAtom(node, true);
83
+ }
84
+ getPreviousTextOrAtom(node) {
85
+ return this.getAdjacentTextOrAtom(node, false);
86
+ }
87
+ getAdjacentCaret(caret, next) {
88
+ if (!(caret.node instanceof TextNode) && !(caret.node instanceof AtomNode))
89
+ return;
90
+ // Is there an adjascent caret in the current text node? If so, return it.
91
+ if (caret.node instanceof TextNode) {
92
+ const adjacentText = next ? caret.node.getNextCaret(caret.index) : caret.node.getPreviousCaret(caret.index);
93
+ if (adjacentText !== undefined)
94
+ return adjacentText;
95
+ }
96
+ // If there's not, is there an adjacent caret in the sequence of text and atom nodes?
97
+ const adjacentNode = this.getAdjacentTextOrAtom(caret.node, next);
98
+ if (adjacentNode !== undefined) {
99
+ // Normally we skip over node boundaries because they look identical to each other.
100
+ // But some nodes are zero length, so we have to account for that.
101
+ return { node: adjacentNode, index: next ? (adjacentNode instanceof TextNode ? (adjacentNode.isEmpty() ? 0 : 1) : 0) :
102
+ (adjacentNode instanceof TextNode ? (adjacentNode.isEmpty() ? 0 : adjacentNode.getLength() - 1) : 0)
103
+ };
104
+ }
105
+ // Otherwise, there is no adjascent caret.
106
+ return undefined;
107
+ }
108
+ getFirstCaret() {
109
+ const text = this.getTextNodes();
110
+ if (text.length === 0)
111
+ return undefined;
112
+ return { node: this.getTextNodes()[0], index: 0 };
113
+ }
114
+ getLastCaret() {
115
+ const text = this.getTextNodes();
116
+ if (text.length === 0)
117
+ return undefined;
118
+ const last = text[text.length - 1];
119
+ return { node: last, index: last.getLength() };
120
+ }
121
+ getSelection() {
122
+ const first = this.getFirstTextNode();
123
+ const last = this.getLastTextNode();
124
+ return { start: { node: first, index: 0 }, end: { node: last, index: last.getLength() } };
125
+ }
126
+ getIndexOfTextNode(node) {
127
+ const text = this.getTextNodes();
128
+ return text.indexOf(node);
129
+ }
130
+ getSelectedText(range) {
131
+ if (!(range.start.node instanceof TextNode) || !(range.end.node instanceof TextNode))
132
+ return undefined;
133
+ const start = this.getIndexOfTextNode(range.start.node);
134
+ const end = this.getIndexOfTextNode(range.end.node);
135
+ if (start === undefined || end === undefined)
136
+ return undefined;
137
+ return this.getTextNodes().map((current, index) => {
138
+ return current === range.start.node ? (current === range.end.node ?
139
+ current.getText().substring(range.start.index, range.end.index) :
140
+ current.getText().substring(range.start.index)) :
141
+ index > start && index < end ? current.getText() :
142
+ current === range.end.node ? current.getText().substring(0, range.end.index) :
143
+ "";
144
+ }).join("");
145
+ }
146
+ withTextIfEmpty() { return this.isEmpty() ? this.withSegmentAppended(new TextNode()) : this; }
147
+ withSegmentPrepended(segment) {
148
+ return new FormatNode(this.#format, [segment, ...this.#segments]);
149
+ }
150
+ withSegmentAppended(segment) {
151
+ return new FormatNode(this.#format, [...this.#segments, segment]);
152
+ }
153
+ withSegmentReplaced(segment, replacement) {
154
+ const index = this.#segments.indexOf(segment);
155
+ if (index < 0)
156
+ return undefined;
157
+ const segments = this.#segments.slice();
158
+ segments[index] = replacement;
159
+ return new FormatNode(this.#format, segments);
160
+ }
161
+ withoutSegment(segment) {
162
+ const index = this.#segments.indexOf(segment);
163
+ if (index < 0)
164
+ return undefined;
165
+ const newSegments = this.#segments.slice();
166
+ newSegments.splice(index, 1);
167
+ return new FormatNode(this.#format, newSegments);
168
+ }
169
+ withChildReplaced(node, replacement) {
170
+ return replacement === undefined ?
171
+ this.withoutSegment(node) :
172
+ this.withSegmentReplaced(node, replacement);
173
+ }
174
+ // Creates two formatted nodes that split this node at the given caret location.
175
+ split(caret) {
176
+ // We can only copy if
177
+ // ... this has a parent.
178
+ // ... the given caret is in this formatted node.
179
+ // ... the caret is on a text node.
180
+ if (!(caret.node instanceof TextNode) || !this.contains(caret.node))
181
+ return undefined;
182
+ const firstCaret = this.getFirstCaret();
183
+ const lastCaret = this.getLastCaret();
184
+ if (firstCaret === undefined || lastCaret === undefined)
185
+ return undefined;
186
+ // Delete everything after in the first, everything before in the second.
187
+ const first = this.withoutRange({ start: caret, end: lastCaret });
188
+ const second = this.withoutRange({ start: firstCaret, end: caret });
189
+ if (first !== undefined && second !== undefined)
190
+ return [first, second];
191
+ }
192
+ withoutRange(range) {
193
+ return this.withFormat(range, undefined);
194
+ }
195
+ // This function takes the given range, and if it's within the bounds of this FormatNode,
196
+ // produces a new FormatNode that either formats the given range with the given format or deletes the range if the format is undefined.
197
+ // It's approach is to scan through every text, meta, and atom node build a new format based on the requested format.
198
+ withFormat(range, format) {
199
+ // This only transforms ranges that start and end with text/atom nodes in this format node.
200
+ if (!(range.start.node instanceof TextNode || range.start.node instanceof AtomNode) ||
201
+ !(range.end.node instanceof TextNode || range.end.node instanceof AtomNode) ||
202
+ !(this.contains(range.start.node) && this.contains(range.end.node)))
203
+ return;
204
+ const sortedRange = this.sortRange(range);
205
+ // Remember the text positions so we can return a new range in the new tree.
206
+ const selectionStartIndex = caretToIndex(this, sortedRange.start);
207
+ const selectionEndIndex = caretToIndex(this, sortedRange.end);
208
+ if (selectionStartIndex === undefined || selectionEndIndex === undefined)
209
+ return;
210
+ // Remember if the selection is zero width.
211
+ let zeroWidthSelection = selectionStartIndex === selectionEndIndex;
212
+ // Find all of the content in the node, except for format nodes and decendants of atom nodes that have format nodes,
213
+ // to construct a new format tree with the old content.
214
+ const everythingButFormats = this.getNodes().filter(n => !(n instanceof FormatNode));
215
+ // Check if all of the selected content has the requested format so we can toggle it if so.
216
+ let checkIndex = 0; // This tracks the current location in our scan.
217
+ let editingEmptyNode = false; // This remembers the special case of the caret being inside an empty text node.
218
+ let alreadyApplied = false; // We assume it's not applied until proven otherwise.
219
+ let nodeCount = 0;
220
+ let nodesWithFormat = 0;
221
+ // It's already applied if we're deleting (in which case this doesn't apply).
222
+ if (format === undefined)
223
+ alreadyApplied = true;
224
+ // Otherwise, formatting is already applied if all of the non-format nodes already have this format.
225
+ // We need to know this so we can toggle the format off.
226
+ else {
227
+ everythingButFormats.forEach(node => {
228
+ const parent = this.getParentOf(node);
229
+ // If it's text, and the current position is in range of the selection, does the position contain the requested format?
230
+ if (node instanceof TextNode && parent instanceof FormatNode) {
231
+ // If we're formatting an empty node we previously found, this node's formatting is irrelevant.
232
+ if (editingEmptyNode) { }
233
+ // If this node is empty but contains a zero-width selection, remember it, since this is the only node for which formats are relevant.
234
+ else if (node.getText().length === 0) {
235
+ if (checkIndex === selectionStartIndex && zeroWidthSelection) {
236
+ editingEmptyNode = true;
237
+ alreadyApplied = parent.getFormats(this).includes(format);
238
+ }
239
+ }
240
+ // If this node has more than zero characters, the formatting is all applied if the selection contains the entirety of this node's text
241
+ // and this node's parent contains the formatting.
242
+ else {
243
+ const parentIsFormatted = parent.getFormats(this).includes(format);
244
+ const selectionContainsNode = (selectionStartIndex > checkIndex && selectionStartIndex < checkIndex + node.getCaretPositionCount() - 1) || (selectionEndIndex > checkIndex && selectionEndIndex < checkIndex + node.getCaretPositionCount() - 1);
245
+ const nodeContainsSelection = selectionStartIndex >= checkIndex && selectionEndIndex <= checkIndex + node.getCaretPositionCount();
246
+ checkIndex += node.getCaretPositionCount();
247
+ if (nodeContainsSelection || selectionContainsNode) {
248
+ nodeCount++;
249
+ if (parentIsFormatted)
250
+ nodesWithFormat++;
251
+ }
252
+ }
253
+ }
254
+ // Treat all non-text as containing the requested format.
255
+ else
256
+ return true;
257
+ });
258
+ if (nodeCount > 0 && nodesWithFormat === nodeCount)
259
+ alreadyApplied = true;
260
+ }
261
+ // Reformat everything. The strategy is to step through each character, atom node, and metadata node in this format node
262
+ // and create a new series of formats that preserve existing formatting while applying new formatting to the selection.
263
+ let newFormats = [{ format: "", segments: [] }]; // A stack of formats we're constructing.
264
+ let textIndex = 0;
265
+ let currentText = ""; // The current text we've accumulated, inserted before each format change and at the end.
266
+ let formattingEmptyNode = false;
267
+ let emptyNode = undefined;
268
+ function saveText() {
269
+ // If there's some text, update the current format with the current text, then reset it.
270
+ if (currentText.length > 0) {
271
+ newFormats[0].segments.push(new TextNode(currentText));
272
+ currentText = "";
273
+ }
274
+ }
275
+ function inRange() {
276
+ return selectionStartIndex !== undefined && selectionEndIndex !== undefined && textIndex >= selectionStartIndex && textIndex < selectionEndIndex;
277
+ }
278
+ function getFormats() {
279
+ // We don't include non-formatted text in the list of formats applied.
280
+ return newFormats.map(f => f.format).filter(f => f !== "");
281
+ }
282
+ function finishFormat() {
283
+ const formatSpec = newFormats[0];
284
+ newFormats.shift();
285
+ // Keep the new format clean by only adding the format node if it's not empty, or if it's a zero width selection.
286
+ if (formatSpec.segments.length > 0 || (zeroWidthSelection && textIndex === selectionStartIndex))
287
+ newFormats[0].segments.push(new FormatNode(formatSpec.format, formatSpec.segments));
288
+ }
289
+ everythingButFormats.forEach(node => {
290
+ const parent = this.getParentOf(node);
291
+ if (node instanceof TextNode) {
292
+ // If this is a text node in an atom node we're not formatting the atom node's format, then just
293
+ // account for it's length, but do nothing else.
294
+ const atomAncestor = node.getClosestParentOfType(this, AtomNode);
295
+ if (atomAncestor instanceof AtomNode && atomAncestor.getMeta() !== this && atomAncestor.getMeta() instanceof FormatNode) {
296
+ textIndex += node.getCaretPositionCount();
297
+ }
298
+ else if (parent instanceof FormatNode) {
299
+ // Remember we found an empty node so that we don't insert an extra one later.
300
+ if (node.getText().length === 0) {
301
+ if (textIndex === selectionStartIndex && zeroWidthSelection) {
302
+ formattingEmptyNode = true;
303
+ textIndex++;
304
+ }
305
+ }
306
+ else {
307
+ // Process each character in the node, including the very last position.
308
+ for (let i = 0; i <= node.getText().length; i++) {
309
+ // Determine the desired format for this character. Start with the current formats
310
+ // and if we're in the selected range, adjust them accordingly based on the current formats
311
+ // and the requested format. (If this is a range deletion, then we just stick with the existing formats).
312
+ let formatter = node.getFormat(this);
313
+ let desiredFormats = formatter ? formatter.getFormats(this) : [];
314
+ if (format !== undefined &&
315
+ ((textIndex === selectionStartIndex && zeroWidthSelection) || inRange())) {
316
+ // Remove all formatting if we were asked to.
317
+ if (format === "")
318
+ desiredFormats = [];
319
+ // Apply the formatting if it's not already applied.
320
+ else if (!alreadyApplied && !desiredFormats.includes(format))
321
+ desiredFormats.push(format);
322
+ // Remove the formatting if it's already applied in the selection.
323
+ else if (alreadyApplied && desiredFormats.includes(format))
324
+ desiredFormats.splice(desiredFormats.indexOf(format), 1);
325
+ }
326
+ // Remove undesired formats.
327
+ getFormats().forEach(activeFormat => {
328
+ // If the current format is not in the desired list, close the text
329
+ // node and keep popping formats until we find the parent of the offending format.
330
+ if (!desiredFormats.includes(activeFormat)) {
331
+ // Save any text that we've accumulated.
332
+ saveText();
333
+ // Not sure this loop is necessary; we should always remove in the order we added.
334
+ while (newFormats[0].format !== activeFormat)
335
+ finishFormat();
336
+ // Do it one more time to close out the undesired format.
337
+ finishFormat();
338
+ // If we have unformatted something at a zero-width range, create empty format node, add a text node in it and remember it so
339
+ // we can place the caret in it.
340
+ if (!formattingEmptyNode && textIndex === selectionStartIndex && zeroWidthSelection) {
341
+ // Remember the node for later, so we can place the caret in it.
342
+ emptyNode = new TextNode();
343
+ // Append the empty node to the current format.
344
+ newFormats[0].segments.push(emptyNode);
345
+ // Push a new format onto the stack to start adding to.
346
+ newFormats.unshift({ format: activeFormat, segments: [] });
347
+ }
348
+ }
349
+ });
350
+ // Ensure the desired formats.
351
+ desiredFormats.forEach(desiredFormat => {
352
+ // If the current format does not yet include the desired format,
353
+ // close the text node and start a new format.
354
+ if (!getFormats().includes(desiredFormat)) {
355
+ // Save any text that we've accumulated in the curent format.
356
+ saveText();
357
+ // Add a new format onto the stack to start adding to.
358
+ newFormats.unshift({ format: desiredFormat, segments: [] });
359
+ // If we inserted an empty format node, add a text node in it and remember it so
360
+ // we can place the caret in it.
361
+ if (!formattingEmptyNode && textIndex === selectionStartIndex && zeroWidthSelection) {
362
+ emptyNode = new TextNode();
363
+ newFormats[0].segments.push(emptyNode);
364
+ finishFormat();
365
+ }
366
+ }
367
+ });
368
+ // If there's a format we're applying, or we're outside the deletion range, append the current character.
369
+ // We don't do this if we're checking the very last index of the node, since there is no character after it.
370
+ if (i < node.getLength()) {
371
+ if (format !== undefined || !inRange())
372
+ currentText += node.getText().charAt(i);
373
+ // Increment the text index for this character.
374
+ textIndex++;
375
+ }
376
+ }
377
+ }
378
+ }
379
+ // If the parent is a MetadataNode
380
+ else if (parent instanceof MetadataNode) {
381
+ // Save whatever text came before it.
382
+ saveText();
383
+ // Process each character in the node if we're formatting, and if we're deleting, skip anything in range of deletion.
384
+ for (let i = 0; i < node.getText().length; i++) {
385
+ if (format !== undefined || !inRange())
386
+ currentText += node.getText().charAt(i);
387
+ textIndex++;
388
+ }
389
+ // Duplicate the MetadataNode but with new text. Erase the MetadataNode if the text is empty
390
+ // by simply not adding one to this new format. Then, erase the text, since it was added in the metadata node.
391
+ if (currentText.length > 0) {
392
+ newFormats[0].segments.push(parent.withText(new TextNode(currentText)));
393
+ currentText = "";
394
+ }
395
+ }
396
+ }
397
+ // If it's an AtomNode, just add it unmodified, since we only format text.
398
+ // But if it's it's in range of the deletion, skip it, since we're deleting.
399
+ else if (node instanceof AtomNode) {
400
+ // Before adding the atom, create a text node and reset the accumulator.
401
+ saveText();
402
+ // Add the atom immediately after, unless it's in the deletion range.
403
+ // We include the start index since AtomNode's text index is on the left.
404
+ if (format !== undefined || textIndex < selectionStartIndex || textIndex > selectionEndIndex) {
405
+ // Does the atom node have a format? Format it!
406
+ let newAtom = node;
407
+ const atomMeta = node.getMeta();
408
+ if (atomMeta instanceof FormatNode) {
409
+ if (atomMeta.contains(range.start.node) && atomMeta.contains(range.end.node)) {
410
+ const newFormat = atomMeta.withFormat(range, format);
411
+ newAtom = node.withMeta(newFormat);
412
+ }
413
+ }
414
+ // Add the formatted atom if there is one.
415
+ newFormats[0].segments.push(newAtom !== undefined ? newAtom : node);
416
+ }
417
+ // Increment the text index; atoms count for one character.
418
+ textIndex++;
419
+ }
420
+ });
421
+ // Save any remaining text that we've accumulated.
422
+ saveText();
423
+ // Keep finishing formats until we get to the original we created.
424
+ while (newFormats.length > 1)
425
+ finishFormat();
426
+ // Create the final format.
427
+ let newFormat = new FormatNode(newFormats[0].format, newFormats[0].segments);
428
+ // If after all that, it's empty, make sure there's one empty text node to type in.
429
+ if (newFormat.getTextAndAtomNodes().length === 0)
430
+ newFormat = new FormatNode("", [new TextNode()]);
431
+ // If it starts or ends with an atom, insert an empty node so that text can be inserted before or after.
432
+ else {
433
+ if (newFormat.#segments[0] instanceof AtomNode)
434
+ newFormat = newFormat.withSegmentPrepended(new TextNode());
435
+ if (newFormat.#segments[newFormat.getLength() - 1] instanceof AtomNode)
436
+ newFormat = newFormat.withSegmentAppended(new TextNode());
437
+ }
438
+ return newFormat;
439
+ }
440
+ withRangeFormatted(range, format) {
441
+ // Convert the range to text indices.
442
+ const text = caretRangeToIndexRange(this, range);
443
+ if (text === undefined)
444
+ return;
445
+ const newFormat = this.withFormat(range, format);
446
+ if (newFormat === undefined)
447
+ return;
448
+ const newStart = indexToCaret(newFormat, text.start);
449
+ const newEnd = format === undefined ? newStart : indexToCaret(newFormat, text.end);
450
+ if (newStart === undefined || newEnd === undefined)
451
+ return;
452
+ return { root: newFormat, range: { start: newStart, end: newEnd } };
453
+ }
454
+ getFormats(root) {
455
+ let format = this;
456
+ const formats = [];
457
+ while (format instanceof FormatNode) {
458
+ if (format.#format !== "")
459
+ formats.push(format.#format);
460
+ format = format.getParent(root);
461
+ }
462
+ return formats;
463
+ }
464
+ withSegmentAt(segment, caret) {
465
+ if (!(caret.node instanceof TextNode))
466
+ return;
467
+ // Verify that the caret's node is a segment in this node.
468
+ const index = this.#segments.indexOf(caret.node);
469
+ if (index < 0)
470
+ return;
471
+ // Splice the text node.
472
+ const left = new TextNode(caret.node.getText().substring(0, caret.index));
473
+ const right = new TextNode(caret.node.getText().substring(caret.index));
474
+ // Insert the new left, right and middle
475
+ const newSegments = this.#segments.slice();
476
+ newSegments.splice(index, 1, left, segment, right);
477
+ return new FormatNode(this.#format, newSegments);
478
+ }
479
+ withSegmentAtSelection(range, nodeCreator) {
480
+ // If there's a selection, grab it's text and then remove the text and update the root and text being edited.
481
+ let selectedText = this.getSelectedText(range);
482
+ let newFormat = this;
483
+ const sortedRange = this.sortRange(range);
484
+ let caret = sortedRange.start;
485
+ if (sortedRange.start.node !== sortedRange.end.node || sortedRange.start.index !== sortedRange.end.index) {
486
+ // Try to remove the selected text. Bail on fail.
487
+ const textIndex = caretToIndex(this, sortedRange.start);
488
+ if (textIndex === undefined)
489
+ return;
490
+ newFormat = this.withoutRange(sortedRange);
491
+ if (newFormat === undefined)
492
+ return;
493
+ const newCaret = indexToCaret(newFormat, textIndex);
494
+ if (newCaret === undefined)
495
+ return;
496
+ caret = newCaret;
497
+ }
498
+ // Create and insert the into the formatted node.
499
+ const newNode = nodeCreator.call(undefined, selectedText ? selectedText : "");
500
+ const revisedFormat = newFormat.withSegmentAt(newNode, caret);
501
+ if (revisedFormat === undefined)
502
+ return;
503
+ const newCaret = newNode instanceof AtomNode ? newNode.getDefaultCaret() :
504
+ newNode instanceof FormatNode ? newNode.getFirstCaret() :
505
+ newNode instanceof MetadataNode ? { node: newNode.getText(), index: 0 } :
506
+ newNode instanceof TextNode ? { node: newNode, index: 0 } :
507
+ undefined;
508
+ if (newCaret === undefined)
509
+ return;
510
+ return { root: revisedFormat, range: { start: newCaret, end: newCaret } };
511
+ }
512
+ withoutContentBefore(caret) {
513
+ return this.withContentAt(caret, false);
514
+ }
515
+ withoutContentAfter(caret) {
516
+ return this.withContentAt(caret, true);
517
+ }
518
+ withContentAt(caret, before) {
519
+ let range = undefined;
520
+ if (before) {
521
+ const lastCaret = this.getLastCaret();
522
+ if (lastCaret === undefined)
523
+ return;
524
+ range = { start: caret, end: lastCaret };
525
+ }
526
+ else {
527
+ const firstCaret = this.getFirstCaret();
528
+ if (firstCaret === undefined)
529
+ return;
530
+ range = { start: firstCaret, end: caret };
531
+ }
532
+ return this.withoutRange(range);
533
+ }
534
+ withContentInRange(range) {
535
+ const containsStart = this.contains(range.start.node);
536
+ const containsEnd = this.contains(range.end.node);
537
+ if (!containsStart && !containsEnd)
538
+ return this.copy();
539
+ const startCaret = containsStart ? range.start : this.getFirstCaret();
540
+ const endCaret = containsEnd ? range.end : this.getLastCaret();
541
+ if (startCaret === undefined || endCaret === undefined)
542
+ return;
543
+ const startIndex = caretToIndex(this, startCaret);
544
+ const endIndex = caretToIndex(this, endCaret);
545
+ if (startIndex === undefined || endIndex === undefined)
546
+ return;
547
+ const withoutBefore = this.withoutContentBefore(startCaret);
548
+ if (withoutBefore === undefined)
549
+ return;
550
+ const revisedEndCaret = indexToCaret(withoutBefore, (endIndex - startIndex));
551
+ if (revisedEndCaret === undefined)
552
+ return;
553
+ return withoutBefore.withoutContentAfter(revisedEndCaret);
554
+ }
555
+ withSegmentsAppended(format) {
556
+ return new FormatNode(this.#format, [...this.#segments, ...format.getSegments()]);
557
+ }
558
+ withoutAdjacentContent(caret, next) {
559
+ return this.withoutCharacter(caret, next);
560
+ }
561
+ withoutCharacter(caret, next) {
562
+ // Confirm that this caret is in this format.
563
+ if (!this.contains(caret.node))
564
+ return;
565
+ // Is there an adjascent caret? Return nothing if not.
566
+ const adjacentCaret = this.getAdjacentCaret(caret, next);
567
+ if (adjacentCaret === undefined)
568
+ return;
569
+ // Handle each case differently
570
+ if (caret.node instanceof AtomNode) {
571
+ const newFormat = this.withNodeReplaced(caret.node, undefined);
572
+ if (newFormat === undefined)
573
+ return;
574
+ return { root: newFormat, range: { start: adjacentCaret, end: adjacentCaret } };
575
+ }
576
+ else if (adjacentCaret.node instanceof AtomNode) {
577
+ const newFormat = this.withNodeReplaced(adjacentCaret.node, undefined);
578
+ if (newFormat === undefined)
579
+ return;
580
+ return { root: newFormat, range: { start: caret, end: caret } };
581
+ }
582
+ else {
583
+ // If next, keep the caret in place, otherwise go to the adjacent caret position.
584
+ const textIndex = caretToIndex(this, next ? caret : adjacentCaret);
585
+ if (textIndex === undefined)
586
+ return;
587
+ // If there is one, try removing everything between this and the adjascent caret.
588
+ const newFormat = this.withoutRange({ start: caret, end: adjacentCaret });
589
+ if (newFormat === undefined)
590
+ return;
591
+ const newCaret = indexToCaret(newFormat, textIndex);
592
+ if (newCaret === undefined)
593
+ return;
594
+ return { root: newFormat, range: { start: newCaret, end: newCaret } };
595
+ }
596
+ }
597
+ withNodeInserted(caret, node) {
598
+ // The caret node has to be one of the nodes in this format to insert.
599
+ const index = this.#segments.indexOf(caret.node);
600
+ if (index < 0)
601
+ return;
602
+ // The inserted node has to be a segment type.
603
+ if (!(node instanceof FormatNode || node instanceof TextNode || node instanceof MetadataNode || node instanceof AtomNode))
604
+ return;
605
+ const newFormat = this.withSegmentAt(node, caret);
606
+ const newCaret = node instanceof AtomNode ? node.getDefaultCaret() :
607
+ node instanceof FormatNode ? node.getLastCaret() :
608
+ node instanceof MetadataNode ? { node: node.getText(), index: node.getText().getLength() } :
609
+ { node: node, index: node.getLength() };
610
+ if (newFormat === undefined || newCaret === undefined)
611
+ return;
612
+ return { root: newFormat, range: { start: newCaret, end: newCaret } };
613
+ }
614
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ import { test, expect } from 'vitest';
2
+ import FormatNode from "./FormatNode";
3
+ import TextNode from "./TextNode";
4
+ test("Bold first two characters", () => {
5
+ const text = new TextNode("Hello");
6
+ const format = new FormatNode("", [text]);
7
+ expect(format.withFormat({ start: { node: text, index: 0 }, end: { node: text, index: 2 } }, "*")?.toBookdown()).toBe("*He*llo");
8
+ });
9
+ test("Unbold word", () => {
10
+ const text = new TextNode("Hello");
11
+ const format = new FormatNode("", [new FormatNode("*", [text])]);
12
+ expect(format.withFormat({ start: { node: text, index: 0 }, end: { node: text, index: 5 } }, "")?.toBookdown()).toBe("Hello");
13
+ });
14
+ test("Bold single caret position", () => {
15
+ const text = new TextNode("Hello");
16
+ const format = new FormatNode("", [text]);
17
+ expect(format.withFormat({ start: { node: text, index: 2 }, end: { node: text, index: 2 } }, "*")?.toBookdown()).toBe("He**llo");
18
+ });
19
+ test("Delete first two characters", () => {
20
+ const text = new TextNode("Hello");
21
+ const format = new FormatNode("", [text]);
22
+ expect(format.withoutRange({ start: { node: text, index: 0 }, end: { node: text, index: 2 } })?.toBookdown()).toBe("llo");
23
+ });
24
+ test("Delete characters crossing format boundary", () => {
25
+ const text = new TextNode("He");
26
+ const bolded = new TextNode("ll");
27
+ const format = new FormatNode("", [text, new FormatNode("*", [bolded]), new TextNode("o")]);
28
+ expect(format.withoutRange({ start: { node: text, index: 1 }, end: { node: bolded, index: 1 } })?.toBookdown()).toBe("H*l*o");
29
+ });
30
+ test("Split", () => {
31
+ const text = new TextNode("This is my paragraph. It is two sentences.");
32
+ const indexAfterPeriod = text.getText().indexOf(".") + 1;
33
+ const caret = { node: text, index: indexAfterPeriod };
34
+ const format = new FormatNode("", [text]);
35
+ expect(format.split(caret)?.map(t => t.toBookdown()).join("THEN")).toBe("This is my paragraph.THEN It is two sentences.");
36
+ });
37
+ test("Backspace/delete", () => {
38
+ const first = new TextNode("Hello, my name is ");
39
+ const second = new TextNode("Amy");
40
+ const third = new TextNode(".");
41
+ const format = new FormatNode("", [first, new FormatNode("*", [second]), third]);
42
+ expect(format.withoutCharacter({ node: first, index: 5 }, false)?.root.toBookdown())
43
+ .toBe("Hell, my name is *Amy*.");
44
+ expect(format.withoutCharacter({ node: first, index: 5 }, true)?.root.toBookdown())
45
+ .toBe("Hello my name is *Amy*.");
46
+ expect(format.withoutCharacter({ node: second, index: 0 }, false)?.root.toBookdown())
47
+ .toBe("Hello, my name is*Amy*.");
48
+ expect(format.withoutCharacter({ node: second, index: 0 }, true)?.root.toBookdown())
49
+ .toBe("Hello, my name is *my*.");
50
+ expect(format.withoutCharacter({ node: first, index: 0 }, false)?.root.toBookdown())
51
+ .toBeUndefined();
52
+ expect(format.withoutCharacter({ node: third, index: 1 }, true)?.root.toBookdown())
53
+ .toBeUndefined();
54
+ });