bookish-press 0.4.45 → 0.4.47

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 (533) hide show
  1. package/.env.example +8 -0
  2. package/.firebaserc +8 -0
  3. package/.prettierignore +8 -0
  4. package/.prettierrc.json +5 -0
  5. package/_update-notifier-last-checked +0 -0
  6. package/firebase.json +46 -0
  7. package/firestore.indexes.json +4 -0
  8. package/firestore.rules +59 -0
  9. package/functions/.eslintrc.js +28 -0
  10. package/functions/package-lock.json +12595 -0
  11. package/functions/package.json +43 -0
  12. package/functions/src/index.ts +345 -0
  13. package/functions/tsconfig.dev.json +3 -0
  14. package/functions/tsconfig.json +15 -0
  15. package/package.json +53 -270
  16. package/scripts/claims.cjs +99 -0
  17. package/src/app.d.ts +9 -0
  18. package/src/app.html +24 -0
  19. package/src/lib/components/Auth.svelte +65 -0
  20. package/{components/Authentication.d.ts → src/lib/components/Authentication.ts} +2 -0
  21. package/{components → src/lib/components}/BookPreview.svelte +27 -22
  22. package/{components → src/lib/components}/External.svelte +2 -1
  23. package/src/lib/components/Link.svelte +58 -0
  24. package/{components → src/lib/components}/app/BookList.svelte +6 -3
  25. package/{components → src/lib/components}/app/Button.svelte +5 -4
  26. package/{components → src/lib/components}/app/Feedback.svelte +11 -7
  27. package/{components → src/lib/components}/app/Head.svelte +2 -1
  28. package/{components → src/lib/components}/app/Large.svelte +0 -0
  29. package/{components → src/lib/components}/app/Lead.svelte +0 -0
  30. package/{components → src/lib/components}/app/Link.svelte +4 -3
  31. package/{components → src/lib/components}/app/Options.svelte +5 -4
  32. package/{components → src/lib/components}/app/Paragraph.svelte +2 -1
  33. package/{components → src/lib/components}/app/Section.svelte +0 -0
  34. package/{components → src/lib/components}/app/Table.svelte +0 -0
  35. package/{components → src/lib/components}/app/TextInput.svelte +12 -9
  36. package/{components → src/lib/components}/chapter/Atom.svelte +18 -12
  37. package/src/lib/components/chapter/Block.svelte +42 -0
  38. package/{components → src/lib/components}/chapter/Callout.svelte +6 -3
  39. package/{components → src/lib/components}/chapter/CaptionedCode.svelte +21 -13
  40. package/{components → src/lib/components}/chapter/ChapterBody.svelte +13 -9
  41. package/{components → src/lib/components}/chapter/Citations.svelte +34 -33
  42. package/{components → src/lib/components}/chapter/Code.svelte +63 -58
  43. package/{components → src/lib/components}/chapter/Comment.svelte +30 -18
  44. package/{components → src/lib/components}/chapter/Definition.svelte +23 -17
  45. package/{components → src/lib/components}/chapter/Embed.svelte +97 -77
  46. package/src/lib/components/chapter/ErrorMessage.svelte +12 -0
  47. package/{components → src/lib/components}/chapter/ExtendedPrism.js +0 -0
  48. package/{components → src/lib/components}/chapter/Figure.svelte +34 -26
  49. package/{components → src/lib/components}/chapter/Footnote.svelte +32 -20
  50. package/{components → src/lib/components}/chapter/Format.svelte +39 -34
  51. package/{components → src/lib/components}/chapter/InlineCode.svelte +9 -5
  52. package/{components → src/lib/components}/chapter/Label.svelte +23 -14
  53. package/{components → src/lib/components}/chapter/Link.svelte +27 -21
  54. package/{components → src/lib/components}/chapter/List.svelte +5 -3
  55. package/{components → src/lib/components}/chapter/Marginal.svelte +54 -41
  56. package/{components → src/lib/components}/chapter/Paragraph.svelte +25 -20
  57. package/{components → src/lib/components}/chapter/Positioned.svelte +4 -1
  58. package/{components → src/lib/components}/chapter/Problem.svelte +0 -0
  59. package/src/lib/components/chapter/Python.svelte +144 -0
  60. package/{components → src/lib/components}/chapter/Quote.svelte +9 -5
  61. package/{components → src/lib/components}/chapter/Rule.svelte +4 -1
  62. package/{components → src/lib/components}/chapter/Table.svelte +11 -7
  63. package/src/lib/components/chapter/Text.svelte +109 -0
  64. package/src/lib/components/editor/BookishEditor.svelte +1101 -0
  65. package/src/lib/components/editor/CalloutEditor.svelte +14 -0
  66. package/{components → src/lib/components}/editor/CaptionedCodeEditor.svelte +10 -6
  67. package/src/lib/components/editor/CaretContext.ts +55 -0
  68. package/{components/editor/CaretState.d.ts → src/lib/components/editor/CaretState.ts} +6 -7
  69. package/{components/editor/CaretUtilities.d.ts → src/lib/components/editor/CaretUtilities.ts} +1 -0
  70. package/{components → src/lib/components}/editor/CaretView.svelte +25 -20
  71. package/src/lib/components/editor/CitationsEditor.svelte +64 -0
  72. package/{components/editor/Clipboard.d.ts → src/lib/components/editor/Clipboard.ts} +2 -1
  73. package/{components/editor/Command.d.ts → src/lib/components/editor/Command.ts} +8 -2
  74. package/{components/editor/Commands.js → src/lib/components/editor/Commands.ts} +730 -315
  75. package/src/lib/components/editor/ConfirmButton.svelte +51 -0
  76. package/src/lib/components/editor/DefinitionEditor.svelte +44 -0
  77. package/src/lib/components/editor/EmbedEditor.svelte +152 -0
  78. package/{components → src/lib/components}/editor/Icon.svelte +2 -1
  79. package/{components → src/lib/components}/editor/ImageChooser.svelte +24 -19
  80. package/src/lib/components/editor/InlineCodeEditor.svelte +16 -0
  81. package/{components → src/lib/components}/editor/LabelEditor.svelte +14 -9
  82. package/{components → src/lib/components}/editor/LanguageEditor.svelte +8 -5
  83. package/src/lib/components/editor/LinkEditor.svelte +110 -0
  84. package/{components → src/lib/components}/editor/Note.svelte +0 -0
  85. package/{components → src/lib/components}/editor/Permissions.svelte +43 -36
  86. package/src/lib/components/editor/PositionEditor.svelte +21 -0
  87. package/src/lib/components/editor/QuoteEditor.svelte +14 -0
  88. package/{components → src/lib/components}/editor/Switch.svelte +14 -11
  89. package/src/lib/components/editor/TableEditor.svelte +14 -0
  90. package/{components → src/lib/components}/editor/TextEditor.svelte +101 -89
  91. package/src/lib/components/editor/Toggle.svelte +47 -0
  92. package/src/lib/components/editor/Toolbar.svelte +297 -0
  93. package/{components → src/lib/components}/editor/ToolbarGroup.svelte +5 -3
  94. package/{components → src/lib/components}/editor/ToolbarSpacer.svelte +0 -0
  95. package/src/lib/components/editor/URLEditor.svelte +15 -0
  96. package/{components/editor/UndoState.d.ts → src/lib/components/editor/UndoState.ts} +3 -2
  97. package/{components → src/lib/components}/editor/icons/annotate.svg +0 -0
  98. package/{components → src/lib/components}/editor/icons/block.svg +0 -0
  99. package/{components → src/lib/components}/editor/icons/bold.svg +0 -0
  100. package/{components → src/lib/components}/editor/icons/bullets.svg +0 -0
  101. package/{components → src/lib/components}/editor/icons/callout.svg +0 -0
  102. package/{components → src/lib/components}/editor/icons/citation.svg +0 -0
  103. package/{components → src/lib/components}/editor/icons/clear.svg +0 -0
  104. package/{components → src/lib/components}/editor/icons/clipboard.svg +0 -0
  105. package/{components → src/lib/components}/editor/icons/code.svg +0 -0
  106. package/{components → src/lib/components}/editor/icons/columnafter.svg +0 -0
  107. package/{components → src/lib/components}/editor/icons/columnbefore.svg +0 -0
  108. package/{components → src/lib/components}/editor/icons/comment.svg +0 -0
  109. package/{components → src/lib/components}/editor/icons/copy.svg +0 -0
  110. package/{components → src/lib/components}/editor/icons/cut.svg +0 -0
  111. package/{components → src/lib/components}/editor/icons/define.svg +0 -0
  112. package/{components → src/lib/components}/editor/icons/deletecolumn.svg +0 -0
  113. package/{components → src/lib/components}/editor/icons/deleterow.svg +0 -0
  114. package/{components → src/lib/components}/editor/icons/footnote.svg +0 -0
  115. package/{components → src/lib/components}/editor/icons/header1.svg +0 -0
  116. package/{components → src/lib/components}/editor/icons/header2.svg +0 -0
  117. package/{components → src/lib/components}/editor/icons/header3.svg +0 -0
  118. package/{components → src/lib/components}/editor/icons/home.svg +0 -0
  119. package/{components → src/lib/components}/editor/icons/indent.svg +0 -0
  120. package/{components → src/lib/components}/editor/icons/italic.svg +0 -0
  121. package/{components → src/lib/components}/editor/icons/label.svg +0 -0
  122. package/{components → src/lib/components}/editor/icons/left.svg +0 -0
  123. package/{components → src/lib/components}/editor/icons/level.svg +0 -0
  124. package/{components → src/lib/components}/editor/icons/link.svg +0 -0
  125. package/{components → src/lib/components}/editor/icons/list.svg +0 -0
  126. package/{components → src/lib/components}/editor/icons/media.svg +0 -0
  127. package/{components → src/lib/components}/editor/icons/middle.svg +0 -0
  128. package/{components → src/lib/components}/editor/icons/numbers.svg +0 -0
  129. package/{components → src/lib/components}/editor/icons/paragraph.svg +0 -0
  130. package/{components → src/lib/components}/editor/icons/paste.svg +0 -0
  131. package/{components → src/lib/components}/editor/icons/quote.svg +0 -0
  132. package/{components → src/lib/components}/editor/icons/redo.svg +0 -0
  133. package/{components → src/lib/components}/editor/icons/right.svg +0 -0
  134. package/{components → src/lib/components}/editor/icons/rowabove.svg +0 -0
  135. package/{components → src/lib/components}/editor/icons/rowbelow.svg +0 -0
  136. package/{components → src/lib/components}/editor/icons/rule.svg +0 -0
  137. package/{components → src/lib/components}/editor/icons/sub.svg +0 -0
  138. package/{components → src/lib/components}/editor/icons/super.svg +0 -0
  139. package/{components → src/lib/components}/editor/icons/table.svg +0 -0
  140. package/{components → src/lib/components}/editor/icons/text.svg +0 -0
  141. package/{components → src/lib/components}/editor/icons/time.svg +0 -0
  142. package/{components → src/lib/components}/editor/icons/undo.svg +0 -0
  143. package/{components → src/lib/components}/editor/icons/unindent.svg +0 -0
  144. package/{components → src/lib/components}/editor/icons/unlink.svg +0 -0
  145. package/{components → src/lib/components}/page/Acknowledgements.svelte +19 -10
  146. package/src/lib/components/page/Authors.svelte +92 -0
  147. package/src/lib/components/page/BookLoader.svelte +77 -0
  148. package/src/lib/components/page/BulkReferenceEditor.svelte +86 -0
  149. package/src/lib/components/page/Chapter.svelte +449 -0
  150. package/src/lib/components/page/ChapterContext.ts +13 -0
  151. package/{components → src/lib/components}/page/ChapterNumber.svelte +0 -0
  152. package/{components → src/lib/components}/page/ChapterTitle.svelte +4 -2
  153. package/src/lib/components/page/Contexts.ts +176 -0
  154. package/{components → src/lib/components}/page/DefinitionView.svelte +52 -37
  155. package/{components → src/lib/components}/page/Description.svelte +15 -7
  156. package/src/lib/components/page/Edition.svelte +189 -0
  157. package/src/lib/components/page/EditionLoader.svelte +123 -0
  158. package/{components → src/lib/components}/page/Editions.svelte +22 -18
  159. package/{components → src/lib/components}/page/Glossary.svelte +33 -28
  160. package/{components → src/lib/components}/page/Header.svelte +59 -52
  161. package/{components → src/lib/components}/page/Index.svelte +48 -41
  162. package/{components → src/lib/components}/page/Instructions.svelte +2 -1
  163. package/{components → src/lib/components}/page/License.svelte +17 -9
  164. package/{components → src/lib/components}/page/Loading.svelte +0 -0
  165. package/{components → src/lib/components}/page/Media.svelte +44 -27
  166. package/{components → src/lib/components}/page/MediaPreview.svelte +3 -2
  167. package/{components → src/lib/components}/page/Muted.svelte +0 -0
  168. package/{components → src/lib/components}/page/Outline.svelte +140 -110
  169. package/src/lib/components/page/Page.svelte +94 -0
  170. package/{components → src/lib/components}/page/PageHeader.svelte +2 -1
  171. package/{components → src/lib/components}/page/PageParagraph.svelte +0 -0
  172. package/src/lib/components/page/PossibleChapter.svelte +16 -0
  173. package/src/lib/components/page/PossibleReference.svelte +14 -0
  174. package/{components → src/lib/components}/page/Print.svelte +5 -3
  175. package/{components → src/lib/components}/page/Publish.svelte +59 -52
  176. package/{components → src/lib/components}/page/Reference.svelte +39 -30
  177. package/{components → src/lib/components}/page/References.svelte +18 -14
  178. package/{components → src/lib/components}/page/Rows.svelte +0 -0
  179. package/src/lib/components/page/Search.svelte +153 -0
  180. package/{components → src/lib/components}/page/Status.svelte +5 -3
  181. package/{components → src/lib/components}/page/SubdomainEditor.svelte +16 -13
  182. package/{components → src/lib/components}/page/TableOfContents.svelte +79 -68
  183. package/src/lib/components/page/TableOfContentsImage.svelte +22 -0
  184. package/{components → src/lib/components}/page/TableOfContentsRow.svelte +34 -24
  185. package/{components → src/lib/components}/page/ThemeEditor.svelte +44 -33
  186. package/{components → src/lib/components}/page/ThemeEditorPreview.svelte +21 -12
  187. package/src/lib/components/page/ThemeSetEditor.svelte +21 -0
  188. package/{components → src/lib/components}/page/ThemeVariableEditor.svelte +18 -14
  189. package/{components → src/lib/components}/page/Title.svelte +0 -0
  190. package/{components → src/lib/components}/page/Unknown.svelte +10 -8
  191. package/{components/page/margins.js → src/lib/components/page/margins.ts} +79 -46
  192. package/{index.js → src/lib/index.js} +0 -0
  193. package/src/lib/models/CRUD.ts +469 -0
  194. package/{models/Firebase.js → src/lib/models/Firebase.ts} +26 -16
  195. package/src/lib/models/book/Book.ts +283 -0
  196. package/src/lib/models/book/BookMedia.ts +182 -0
  197. package/src/lib/models/book/BookSaveStatus.ts +8 -0
  198. package/src/lib/models/book/Chapter.ts +462 -0
  199. package/{models/book/ChapterID.js → src/lib/models/book/ChapterID.ts} +1 -0
  200. package/src/lib/models/book/Definition.ts +2 -0
  201. package/src/lib/models/book/Edition.ts +1216 -0
  202. package/src/lib/models/book/Reference.ts +171 -0
  203. package/{models/book/Schema.js → src/lib/models/book/Schema.ts} +1 -0
  204. package/{models/book/Theme.js → src/lib/models/book/Theme.ts} +111 -6
  205. package/{models/book/ThemeColors.d.ts → src/lib/models/book/ThemeColors.ts} +6 -0
  206. package/{models/book/Words.js → src/lib/models/book/Words.ts} +1 -0
  207. package/{models/chapter/AtomNode.js → src/lib/models/chapter/AtomNode.ts} +30 -10
  208. package/{models/chapter/BlockNode.js → src/lib/models/chapter/BlockNode.ts} +80 -47
  209. package/src/lib/models/chapter/BlocksNode.test.ts +867 -0
  210. package/{models/chapter/BlocksNode.js → src/lib/models/chapter/BlocksNode.ts} +571 -359
  211. package/{models/chapter/Bookkeeping.d.ts → src/lib/models/chapter/Bookkeeping.ts} +0 -0
  212. package/src/lib/models/chapter/CalloutNode.ts +79 -0
  213. package/{models/chapter/Caret.js → src/lib/models/chapter/Caret.ts} +50 -26
  214. package/src/lib/models/chapter/ChapterNode.ts +169 -0
  215. package/{models/chapter/CitationsNode.js → src/lib/models/chapter/CitationsNode.ts} +17 -10
  216. package/{models/chapter/CodeNode.js → src/lib/models/chapter/CodeNode.ts} +125 -62
  217. package/src/lib/models/chapter/CommentNode.ts +61 -0
  218. package/{models/chapter/DefinitionNode.js → src/lib/models/chapter/DefinitionNode.ts} +14 -8
  219. package/src/lib/models/chapter/Edit.ts +6 -0
  220. package/src/lib/models/chapter/EmbedNode.ts +257 -0
  221. package/{models/chapter/ErrorNode.js → src/lib/models/chapter/ErrorNode.ts} +15 -8
  222. package/src/lib/models/chapter/FootnoteNode.ts +59 -0
  223. package/src/lib/models/chapter/FormatNode.test.ts +137 -0
  224. package/{models/chapter/FormatNode.js → src/lib/models/chapter/FormatNode.ts} +443 -262
  225. package/{models/chapter/InlineCodeNode.js → src/lib/models/chapter/InlineCodeNode.ts} +17 -9
  226. package/{models/chapter/LabelNode.js → src/lib/models/chapter/LabelNode.ts} +26 -10
  227. package/{models/chapter/LinkNode.js → src/lib/models/chapter/LinkNode.ts} +12 -7
  228. package/{models/chapter/ListNode.test.js → src/lib/models/chapter/ListNode.test.ts} +57 -20
  229. package/{models/chapter/ListNode.js → src/lib/models/chapter/ListNode.ts} +277 -200
  230. package/src/lib/models/chapter/MetadataNode.ts +44 -0
  231. package/{models → src/lib/models}/chapter/Node.test.js +0 -0
  232. package/{models/chapter/Node.js → src/lib/models/chapter/Node.ts} +155 -71
  233. package/src/lib/models/chapter/ParagraphNode.test.ts +54 -0
  234. package/{models/chapter/ParagraphNode.js → src/lib/models/chapter/ParagraphNode.ts} +95 -58
  235. package/{models/chapter/Parser.test.js → src/lib/models/chapter/Parser.test.ts} +7 -0
  236. package/{models/chapter/Parser.js → src/lib/models/chapter/Parser.ts} +369 -184
  237. package/{models/chapter/Position.d.ts → src/lib/models/chapter/Position.ts} +0 -0
  238. package/src/lib/models/chapter/QuoteNode.ts +135 -0
  239. package/{models/chapter/RootNode.d.ts → src/lib/models/chapter/RootNode.ts} +2 -0
  240. package/{models/chapter/RuleNode.js → src/lib/models/chapter/RuleNode.ts} +9 -6
  241. package/{models/chapter/TableNode.test.js → src/lib/models/chapter/TableNode.test.ts} +27 -10
  242. package/{models/chapter/TableNode.js → src/lib/models/chapter/TableNode.ts} +141 -82
  243. package/{models/chapter/TextNode.test.js → src/lib/models/chapter/TextNode.test.ts} +2 -0
  244. package/{models/chapter/TextNode.js → src/lib/models/chapter/TextNode.ts} +117 -65
  245. package/{util/getSubdomain.js → src/lib/util/getSubdomain.ts} +14 -8
  246. package/{util/isMobile.js → src/lib/util/isMobile.ts} +1 -0
  247. package/{util/mineReference.js → src/lib/util/mineReference.ts} +45 -25
  248. package/{util/scrollToEyeLevel.js → src/lib/util/scrollToEyeLevel.ts} +4 -2
  249. package/src/params/edition.ts +3 -0
  250. package/src/routes/+error.svelte +12 -0
  251. package/src/routes/+layout.svelte +241 -0
  252. package/src/routes/+page.svelte +43 -0
  253. package/src/routes/Footer.svelte +0 -0
  254. package/src/routes/Header.svelte +112 -0
  255. package/src/routes/[bookid]/+layout.svelte +5 -0
  256. package/src/routes/[bookid]/[[editionid=edition]]/+error.svelte +14 -0
  257. package/src/routes/[bookid]/[[editionid=edition]]/+layout.svelte +7 -0
  258. package/src/routes/[bookid]/[[editionid=edition]]/+page.svelte +5 -0
  259. package/src/routes/[bookid]/[[editionid=edition]]/[chapterid]/+page.svelte +5 -0
  260. package/src/routes/[bookid]/[[editionid=edition]]/glossary/+page.svelte +5 -0
  261. package/src/routes/[bookid]/[[editionid=edition]]/index/[[letter]]/+page.svelte +5 -0
  262. package/src/routes/[bookid]/[[editionid=edition]]/media/+page.svelte +5 -0
  263. package/src/routes/[bookid]/[[editionid=edition]]/print/+page.svelte +5 -0
  264. package/src/routes/[bookid]/[[editionid=edition]]/references/+page.svelte +5 -0
  265. package/src/routes/[bookid]/[[editionid=edition]]/search/+page.svelte +5 -0
  266. package/src/routes/[bookid]/editions/+page.svelte +20 -0
  267. package/src/routes/about/+page.svelte +102 -0
  268. package/src/routes/confirm/+page.svelte +62 -0
  269. package/src/routes/email/+page.svelte +99 -0
  270. package/src/routes/login/+page.svelte +66 -0
  271. package/src/routes/read/+page.svelte +38 -0
  272. package/src/routes/write/+error.svelte +10 -0
  273. package/src/routes/write/+layout.svelte +11 -0
  274. package/src/routes/write/+page.svelte +114 -0
  275. package/src/routes/write/[bookid]/+layout.svelte +5 -0
  276. package/src/routes/write/[bookid]/[[editionid=edition]]/+layout.svelte +7 -0
  277. package/src/routes/write/[bookid]/[[editionid=edition]]/+page.svelte +5 -0
  278. package/src/routes/write/[bookid]/[[editionid=edition]]/[chapterid]/+page.svelte +5 -0
  279. package/src/routes/write/[bookid]/[[editionid=edition]]/glossary/+page.svelte +5 -0
  280. package/src/routes/write/[bookid]/[[editionid=edition]]/index/[[letter]]/+page.svelte +5 -0
  281. package/src/routes/write/[bookid]/[[editionid=edition]]/media/+page.svelte +5 -0
  282. package/src/routes/write/[bookid]/[[editionid=edition]]/print/+page.svelte +5 -0
  283. package/src/routes/write/[bookid]/[[editionid=edition]]/references/+page.svelte +5 -0
  284. package/src/routes/write/[bookid]/[[editionid=edition]]/search/+page.svelte +5 -0
  285. package/src/routes/write/[bookid]/[[editionid=edition]]/theme/+page.svelte +5 -0
  286. package/src/routes/write/[bookid]/[[editionid=edition]]/unknown/+page.svelte +5 -0
  287. package/src/routes/write/[bookid]/editions/+page.svelte +20 -0
  288. package/src/vite-env.d.ts +1 -0
  289. package/static/icons/favicon.png +0 -0
  290. package/static/icons/icon.png +0 -0
  291. package/static/icons/ios-icon.png +0 -0
  292. package/storage.rules +9 -0
  293. package/svelte.config.js +19 -0
  294. package/tsconfig.json +18 -0
  295. package/tsconfig.node.json +9 -0
  296. package/vite.config.ts +8 -0
  297. package/vite.reader.config.ts +32 -0
  298. package/components/Auth.svelte +0 -55
  299. package/components/Auth.svelte.d.ts +0 -16
  300. package/components/Authentication.js +0 -1
  301. package/components/BookPreview.svelte.d.ts +0 -18
  302. package/components/External.svelte.d.ts +0 -18
  303. package/components/Link.svelte +0 -51
  304. package/components/Link.svelte.d.ts +0 -18
  305. package/components/app/BookList.svelte.d.ts +0 -18
  306. package/components/app/Button.svelte.d.ts +0 -22
  307. package/components/app/Feedback.svelte.d.ts +0 -18
  308. package/components/app/Head.svelte.d.ts +0 -18
  309. package/components/app/Large.svelte.d.ts +0 -27
  310. package/components/app/Lead.svelte.d.ts +0 -27
  311. package/components/app/Link.svelte.d.ts +0 -20
  312. package/components/app/Options.svelte.d.ts +0 -19
  313. package/components/app/Paragraph.svelte.d.ts +0 -18
  314. package/components/app/Section.svelte.d.ts +0 -27
  315. package/components/app/Table.svelte.d.ts +0 -27
  316. package/components/app/TextInput.svelte.d.ts +0 -22
  317. package/components/chapter/Atom.svelte.d.ts +0 -19
  318. package/components/chapter/Block.svelte +0 -37
  319. package/components/chapter/Block.svelte.d.ts +0 -17
  320. package/components/chapter/Callout.svelte.d.ts +0 -17
  321. package/components/chapter/CaptionedCode.svelte.d.ts +0 -17
  322. package/components/chapter/ChapterBody.svelte.d.ts +0 -19
  323. package/components/chapter/Citations.svelte.d.ts +0 -17
  324. package/components/chapter/Code.svelte.d.ts +0 -22
  325. package/components/chapter/Comment.svelte.d.ts +0 -17
  326. package/components/chapter/Definition.svelte.d.ts +0 -17
  327. package/components/chapter/Embed.svelte.d.ts +0 -20
  328. package/components/chapter/ErrorMessage.svelte +0 -9
  329. package/components/chapter/ErrorMessage.svelte.d.ts +0 -17
  330. package/components/chapter/ExtendedPrism.d.ts +0 -2
  331. package/components/chapter/Figure.svelte.d.ts +0 -24
  332. package/components/chapter/Footnote.svelte.d.ts +0 -19
  333. package/components/chapter/Format.svelte.d.ts +0 -19
  334. package/components/chapter/InlineCode.svelte.d.ts +0 -17
  335. package/components/chapter/Label.svelte.d.ts +0 -17
  336. package/components/chapter/Link.svelte.d.ts +0 -17
  337. package/components/chapter/List.svelte.d.ts +0 -17
  338. package/components/chapter/Marginal.svelte.d.ts +0 -22
  339. package/components/chapter/Paragraph.svelte.d.ts +0 -18
  340. package/components/chapter/Positioned.svelte.d.ts +0 -19
  341. package/components/chapter/Problem.svelte.d.ts +0 -27
  342. package/components/chapter/Python.svelte +0 -128
  343. package/components/chapter/Python.svelte.d.ts +0 -18
  344. package/components/chapter/Quote.svelte.d.ts +0 -17
  345. package/components/chapter/Rule.svelte.d.ts +0 -17
  346. package/components/chapter/Table.svelte.d.ts +0 -17
  347. package/components/chapter/Text.svelte +0 -87
  348. package/components/chapter/Text.svelte.d.ts +0 -18
  349. package/components/editor/BookishEditor.svelte +0 -884
  350. package/components/editor/BookishEditor.svelte.d.ts +0 -25
  351. package/components/editor/CalloutEditor.svelte +0 -10
  352. package/components/editor/CalloutEditor.svelte.d.ts +0 -17
  353. package/components/editor/CaptionedCodeEditor.svelte.d.ts +0 -17
  354. package/components/editor/CaretContext.d.ts +0 -50
  355. package/components/editor/CaretContext.js +0 -1
  356. package/components/editor/CaretState.js +0 -1
  357. package/components/editor/CaretUtilities.js +0 -1
  358. package/components/editor/CaretView.svelte.d.ts +0 -25
  359. package/components/editor/CitationsEditor.svelte +0 -53
  360. package/components/editor/CitationsEditor.svelte.d.ts +0 -17
  361. package/components/editor/Clipboard.js +0 -1
  362. package/components/editor/Command.js +0 -1
  363. package/components/editor/Commands.d.ts +0 -3
  364. package/components/editor/ConfirmButton.svelte +0 -49
  365. package/components/editor/ConfirmButton.svelte.d.ts +0 -19
  366. package/components/editor/DefinitionEditor.svelte +0 -36
  367. package/components/editor/DefinitionEditor.svelte.d.ts +0 -17
  368. package/components/editor/EmbedEditor.svelte +0 -137
  369. package/components/editor/EmbedEditor.svelte.d.ts +0 -17
  370. package/components/editor/Icon.svelte.d.ts +0 -16
  371. package/components/editor/ImageChooser.svelte.d.ts +0 -18
  372. package/components/editor/InlineCodeEditor.svelte +0 -12
  373. package/components/editor/InlineCodeEditor.svelte.d.ts +0 -17
  374. package/components/editor/LabelEditor.svelte.d.ts +0 -17
  375. package/components/editor/LanguageEditor.svelte.d.ts +0 -17
  376. package/components/editor/LinkEditor.svelte +0 -100
  377. package/components/editor/LinkEditor.svelte.d.ts +0 -17
  378. package/components/editor/Note.svelte.d.ts +0 -27
  379. package/components/editor/Permissions.svelte.d.ts +0 -20
  380. package/components/editor/PositionEditor.svelte +0 -17
  381. package/components/editor/PositionEditor.svelte.d.ts +0 -18
  382. package/components/editor/QuoteEditor.svelte +0 -10
  383. package/components/editor/QuoteEditor.svelte.d.ts +0 -17
  384. package/components/editor/Switch.svelte.d.ts +0 -20
  385. package/components/editor/TableEditor.svelte +0 -10
  386. package/components/editor/TableEditor.svelte.d.ts +0 -17
  387. package/components/editor/TextEditor.svelte.d.ts +0 -24
  388. package/components/editor/Toggle.svelte +0 -41
  389. package/components/editor/Toggle.svelte.d.ts +0 -19
  390. package/components/editor/Toolbar.svelte +0 -249
  391. package/components/editor/Toolbar.svelte.d.ts +0 -17
  392. package/components/editor/ToolbarGroup.svelte.d.ts +0 -18
  393. package/components/editor/ToolbarSpacer.svelte.d.ts +0 -23
  394. package/components/editor/URLEditor.svelte +0 -13
  395. package/components/editor/URLEditor.svelte.d.ts +0 -18
  396. package/components/editor/UndoState.js +0 -1
  397. package/components/page/Acknowledgements.svelte.d.ts +0 -14
  398. package/components/page/Authors.svelte +0 -85
  399. package/components/page/Authors.svelte.d.ts +0 -21
  400. package/components/page/BookLoader.svelte +0 -59
  401. package/components/page/BookLoader.svelte.d.ts +0 -16
  402. package/components/page/BulkReferenceEditor.svelte +0 -63
  403. package/components/page/BulkReferenceEditor.svelte.d.ts +0 -14
  404. package/components/page/Chapter.svelte +0 -392
  405. package/components/page/Chapter.svelte.d.ts +0 -18
  406. package/components/page/ChapterContext.d.ts +0 -10
  407. package/components/page/ChapterContext.js +0 -1
  408. package/components/page/ChapterNumber.svelte.d.ts +0 -27
  409. package/components/page/ChapterTitle.svelte.d.ts +0 -18
  410. package/components/page/Contexts.d.ts +0 -47
  411. package/components/page/Contexts.js +0 -133
  412. package/components/page/DefinitionView.svelte.d.ts +0 -18
  413. package/components/page/Description.svelte.d.ts +0 -14
  414. package/components/page/Edition.svelte +0 -161
  415. package/components/page/Edition.svelte.d.ts +0 -20
  416. package/components/page/EditionLoader.svelte +0 -99
  417. package/components/page/EditionLoader.svelte.d.ts +0 -18
  418. package/components/page/Editions.svelte.d.ts +0 -14
  419. package/components/page/Glossary.svelte.d.ts +0 -14
  420. package/components/page/Header.svelte.d.ts +0 -29
  421. package/components/page/Index.svelte.d.ts +0 -14
  422. package/components/page/Instructions.svelte.d.ts +0 -18
  423. package/components/page/License.svelte.d.ts +0 -14
  424. package/components/page/Loading.svelte.d.ts +0 -23
  425. package/components/page/Media.svelte.d.ts +0 -14
  426. package/components/page/MediaPreview.svelte.d.ts +0 -19
  427. package/components/page/Muted.svelte.d.ts +0 -27
  428. package/components/page/Outline.svelte.d.ts +0 -19
  429. package/components/page/Page.svelte +0 -84
  430. package/components/page/Page.svelte.d.ts +0 -19
  431. package/components/page/PageHeader.svelte.d.ts +0 -18
  432. package/components/page/PageParagraph.svelte.d.ts +0 -27
  433. package/components/page/PossibleChapter.svelte +0 -14
  434. package/components/page/PossibleChapter.svelte.d.ts +0 -14
  435. package/components/page/PossibleReference.svelte +0 -11
  436. package/components/page/PossibleReference.svelte.d.ts +0 -18
  437. package/components/page/Print.svelte.d.ts +0 -14
  438. package/components/page/Publish.svelte.d.ts +0 -14
  439. package/components/page/Reference.svelte.d.ts +0 -17
  440. package/components/page/References.svelte.d.ts +0 -14
  441. package/components/page/Rows.svelte.d.ts +0 -27
  442. package/components/page/Search.svelte +0 -133
  443. package/components/page/Search.svelte.d.ts +0 -14
  444. package/components/page/Status.svelte.d.ts +0 -14
  445. package/components/page/SubdomainEditor.svelte.d.ts +0 -14
  446. package/components/page/TableOfContents.svelte.d.ts +0 -14
  447. package/components/page/TableOfContentsImage.svelte +0 -18
  448. package/components/page/TableOfContentsImage.svelte.d.ts +0 -16
  449. package/components/page/TableOfContentsRow.svelte.d.ts +0 -24
  450. package/components/page/ThemeEditor.svelte.d.ts +0 -14
  451. package/components/page/ThemeEditorPreview.svelte.d.ts +0 -17
  452. package/components/page/ThemeSetEditor.svelte +0 -19
  453. package/components/page/ThemeSetEditor.svelte.d.ts +0 -18
  454. package/components/page/ThemeVariableEditor.svelte.d.ts +0 -18
  455. package/components/page/Title.svelte.d.ts +0 -27
  456. package/components/page/Unknown.svelte.d.ts +0 -16
  457. package/components/page/margins.d.ts +0 -2
  458. package/index.d.ts +0 -15
  459. package/models/CRUD.d.ts +0 -27
  460. package/models/CRUD.js +0 -262
  461. package/models/Firebase.d.ts +0 -5
  462. package/models/book/Book.d.ts +0 -61
  463. package/models/book/Book.js +0 -135
  464. package/models/book/BookMedia.d.ts +0 -23
  465. package/models/book/BookMedia.js +0 -108
  466. package/models/book/BookSaveStatus.d.ts +0 -7
  467. package/models/book/BookSaveStatus.js +0 -8
  468. package/models/book/Chapter.d.ts +0 -81
  469. package/models/book/Chapter.js +0 -212
  470. package/models/book/ChapterID.d.ts +0 -10
  471. package/models/book/Definition.d.ts +0 -6
  472. package/models/book/Definition.js +0 -1
  473. package/models/book/Edition.d.ts +0 -139
  474. package/models/book/Edition.js +0 -620
  475. package/models/book/Reference.d.ts +0 -24
  476. package/models/book/Reference.js +0 -78
  477. package/models/book/Schema.d.ts +0 -174
  478. package/models/book/Theme.d.ts +0 -113
  479. package/models/book/ThemeColors.js +0 -1
  480. package/models/book/Words.d.ts +0 -2
  481. package/models/chapter/AtomNode.d.ts +0 -18
  482. package/models/chapter/BlockNode.d.ts +0 -20
  483. package/models/chapter/BlocksNode.d.ts +0 -50
  484. package/models/chapter/BlocksNode.test.d.ts +0 -1
  485. package/models/chapter/BlocksNode.test.js +0 -568
  486. package/models/chapter/Bookkeeping.js +0 -1
  487. package/models/chapter/CalloutNode.d.ts +0 -17
  488. package/models/chapter/CalloutNode.js +0 -58
  489. package/models/chapter/Caret.d.ts +0 -23
  490. package/models/chapter/ChapterNode.d.ts +0 -34
  491. package/models/chapter/ChapterNode.js +0 -117
  492. package/models/chapter/CitationsNode.d.ts +0 -16
  493. package/models/chapter/CodeNode.d.ts +0 -35
  494. package/models/chapter/CommentNode.d.ts +0 -18
  495. package/models/chapter/CommentNode.js +0 -48
  496. package/models/chapter/DefinitionNode.d.ts +0 -12
  497. package/models/chapter/Edit.d.ts +0 -7
  498. package/models/chapter/Edit.js +0 -1
  499. package/models/chapter/EmbedNode.d.ts +0 -45
  500. package/models/chapter/EmbedNode.js +0 -168
  501. package/models/chapter/ErrorNode.d.ts +0 -17
  502. package/models/chapter/FootnoteNode.d.ts +0 -18
  503. package/models/chapter/FootnoteNode.js +0 -47
  504. package/models/chapter/FormatNode.d.ts +0 -60
  505. package/models/chapter/FormatNode.test.d.ts +0 -1
  506. package/models/chapter/FormatNode.test.js +0 -96
  507. package/models/chapter/InlineCodeNode.d.ts +0 -12
  508. package/models/chapter/LabelNode.d.ts +0 -17
  509. package/models/chapter/LinkNode.d.ts +0 -12
  510. package/models/chapter/ListNode.d.ts +0 -51
  511. package/models/chapter/ListNode.test.d.ts +0 -1
  512. package/models/chapter/MetadataNode.d.ts +0 -17
  513. package/models/chapter/MetadataNode.js +0 -32
  514. package/models/chapter/Node.d.ts +0 -38
  515. package/models/chapter/Node.test.d.ts +0 -1
  516. package/models/chapter/ParagraphNode.d.ts +0 -33
  517. package/models/chapter/ParagraphNode.test.d.ts +0 -1
  518. package/models/chapter/ParagraphNode.test.js +0 -30
  519. package/models/chapter/Parser.d.ts +0 -75
  520. package/models/chapter/Parser.test.d.ts +0 -1
  521. package/models/chapter/Position.js +0 -1
  522. package/models/chapter/QuoteNode.d.ts +0 -25
  523. package/models/chapter/QuoteNode.js +0 -99
  524. package/models/chapter/RootNode.js +0 -1
  525. package/models/chapter/RuleNode.d.ts +0 -15
  526. package/models/chapter/TableNode.d.ts +0 -37
  527. package/models/chapter/TableNode.test.d.ts +0 -1
  528. package/models/chapter/TextNode.d.ts +0 -36
  529. package/models/chapter/TextNode.test.d.ts +0 -1
  530. package/util/getSubdomain.d.ts +0 -3
  531. package/util/isMobile.d.ts +0 -2
  532. package/util/mineReference.d.ts +0 -4
  533. package/util/scrollToEyeLevel.d.ts +0 -2
@@ -0,0 +1,1101 @@
1
+ <script lang="ts">
2
+ import BookishNode from '$lib/models/chapter/Node';
3
+ import ChapterNode from '$lib/models/chapter/ChapterNode';
4
+ import Parser from '$lib/models/chapter/Parser';
5
+ import LinkNode from '$lib/models/chapter/LinkNode';
6
+ import ParagraphNode from '$lib/models/chapter/ParagraphNode';
7
+ import TextNode from '$lib/models/chapter/TextNode';
8
+ import AtomNode from '$lib/models/chapter/AtomNode';
9
+ import FormatNode from '$lib/models/chapter/FormatNode';
10
+ import BlocksNode from '$lib/models/chapter/BlocksNode';
11
+ import ListNode from '$lib/models/chapter/ListNode';
12
+ import TableNode from '$lib/models/chapter/TableNode';
13
+ import MetadataNode from '$lib/models/chapter/MetadataNode';
14
+ import type RootNode from '$lib/models/chapter/RootNode';
15
+ import type Edit from '$lib/models/chapter/Edit';
16
+ import CodeNode from '$lib/models/chapter/CodeNode';
17
+ import BlockNode from '$lib/models/chapter/BlockNode';
18
+ import EmbedNode from '$lib/models/chapter/EmbedNode';
19
+ import ErrorNode from '$lib/models/chapter/ErrorNode';
20
+
21
+ import type Caret from '$lib/models/chapter/Caret';
22
+ import type { CaretRange } from '$lib/models/chapter/Caret';
23
+ import {
24
+ caretRangeToIndexRange,
25
+ indexRangeToCaretRange,
26
+ } from '$lib/models/chapter/Caret';
27
+ import type WhatsAroundTheCaret from './CaretContext';
28
+
29
+ import type Command from './Command';
30
+ import type Clipboard from './Clipboard';
31
+ import type UndoState from './UndoState';
32
+ import type CaretUtilities from './CaretUtilities';
33
+
34
+ import commands from './Commands';
35
+
36
+ import { afterUpdate, onMount, setContext } from 'svelte';
37
+ import type { PasteContent } from './CaretContext';
38
+ import { getCaret } from '../page/Contexts';
39
+ import CaretView from './CaretView.svelte';
40
+
41
+ const IDLE_TIME = 500;
42
+
43
+ export let text: string;
44
+ export let parser: (text: string) => RootNode;
45
+ export let save: (node: RootNode) => Promise<void> | void;
46
+ export let chapter: boolean;
47
+ export let autofocus: boolean = false;
48
+ export let component: ConstructorOfATypedSvelteComponent;
49
+ export let placeholder: string;
50
+ export let leasee: string | boolean;
51
+ export let lease: (lock: boolean) => Promise<boolean> | boolean;
52
+
53
+ let element: HTMLDivElement | null = null;
54
+
55
+ let caretRange: CaretRange | undefined = undefined;
56
+ let caretCoordinate: { x: number; y: number; height: number } | undefined =
57
+ undefined;
58
+ let lastInputTime = 0;
59
+ let keyboardIdle = true;
60
+ let editorFocused = false;
61
+ let undoStack: UndoState[] = [];
62
+ let undoPosition = -1;
63
+ let clipboard: Clipboard | undefined = undefined;
64
+ let ignoredInput = false;
65
+
66
+ let activeEditor = getCaret();
67
+
68
+ let editedNode: RootNode;
69
+ $: {
70
+ // Note: when the text changes externally, the caret will be lost. We either have
71
+ // to implement fancy operational transform revision control for collaborative editing
72
+ // or a basic chapter locking strategy to avoid data loss. I went with locking for maintenance simplicity,
73
+ // but it would be nice to implement operational transform in the future.
74
+ if (editedNode === undefined || editedNode.toBookdown() !== text) {
75
+ editedNode = parser(text);
76
+ }
77
+ }
78
+
79
+ $: locked = typeof leasee === 'string';
80
+
81
+ onMount(() => {
82
+ const caret = editedNode.getFirstCaret();
83
+ if (caret) caretRange = { start: caret, end: caret };
84
+
85
+ // Focus the editor on load.
86
+ if (element && autofocus === true) {
87
+ element.focus();
88
+ }
89
+
90
+ // Listen to selection changes
91
+ document.addEventListener('selectionchange', handleSelectionChange);
92
+
93
+ // Stop listening to selection changes
94
+ return () => {
95
+ document.removeEventListener(
96
+ 'selectionchange',
97
+ handleSelectionChange
98
+ );
99
+ };
100
+ });
101
+
102
+ onMount(() => {
103
+ // When input happens, set a timer and then see if idle time has elapsed. If it has, track time since last keystroke to control caret blinking behavior.
104
+ const keystrokeTimer = setInterval(() => {
105
+ // Remember that we're idle so we can render things accordingly.
106
+ const isIdle = Date.now() - lastInputTime > IDLE_TIME;
107
+ if (isIdle) {
108
+ keyboardIdle = true;
109
+ ignoredInput = false;
110
+ }
111
+ }, IDLE_TIME);
112
+ return () => {
113
+ // Clear the timer if we're unmounting or re-rendering.
114
+ clearTimeout(keystrokeTimer);
115
+ };
116
+ });
117
+
118
+ // When the selection changes and is rendered, set the browser's selection to correspond. This helps with two things:
119
+ // 1) we can rely on the browser to render selections rather than rendering it ourselves.
120
+ // 2) We can measure the caret position, so we can render our own fancy one rather than relying on inconsistent cross browser behavior.
121
+ afterUpdate(() => {
122
+ if (!editorFocused) return;
123
+
124
+ // Measure the location of the selection using the browser's selection.
125
+ let docSelection = document.getSelection();
126
+ let newCaretPosition = undefined;
127
+ if (docSelection) {
128
+ if (caretRange) {
129
+ // Browsers don't render backwards ranges, so we sort this before measuring.
130
+ const sortedRange = caretRange;
131
+ let startNode = document.querySelector(
132
+ `[data-nodeid='${sortedRange.start.node.nodeID}`
133
+ );
134
+ let endNode = document.querySelector(
135
+ `[data-nodeid='${sortedRange.end.node.nodeID}`
136
+ );
137
+
138
+ // If we found both selected nodes and we have a reference to the DOM, update the
139
+ // browser's selection if necessary and measure it's position so we can draw our own caret.
140
+ if (startNode && endNode && element) {
141
+ // If the current selection isn't different from the new one, then we don't do any of this.
142
+ // This prevents an infinite loop when, for example, the user clicks, the browser selection changes,
143
+ // and we update the caret range, which updates the browser selection, ad infinitum.
144
+ let rangeChanged = true;
145
+ let currentRange = undefined;
146
+ if (docSelection.rangeCount > 0) {
147
+ currentRange = docSelection.getRangeAt(0);
148
+ // If our caret range is non-empty TextNodes at a particular index and equal to the browser's caret range, then nothing's changed, so we shouldn't update the selection.
149
+ const startIsEqual =
150
+ startNode.childNodes.length > 0 &&
151
+ currentRange.startContainer ===
152
+ startNode.childNodes[0] &&
153
+ currentRange.startOffset ===
154
+ sortedRange.start.index;
155
+ const endIsEqual =
156
+ endNode.childNodes.length > 0 &&
157
+ currentRange.endContainer ===
158
+ endNode.childNodes[0] &&
159
+ currentRange.endOffset === sortedRange.end.index;
160
+ if (startIsEqual && endIsEqual) rangeChanged = false;
161
+ }
162
+
163
+ // We always aim for the DOM text nodes inside of bookish-text, but when the text nodes are the empty string, React doesn't
164
+ // render a DOM text node, and browsers often render an empty bounding box in this case. To measure the caret position,
165
+ // we insert a zero-width space in the empty span, then remove it after measurement.
166
+ // We only do this to the end node if it's different from the start node.
167
+ // We leave them in there so that the "rangeChanged" guard below can track them.
168
+ const emptyStart = startNode.childNodes.length === 0;
169
+ const emptyEnd =
170
+ startNode !== endNode &&
171
+ endNode.childNodes.length === 0;
172
+ if (emptyStart)
173
+ startNode.appendChild(
174
+ document.createTextNode('\ufeff')
175
+ );
176
+ if (emptyEnd)
177
+ endNode.appendChild(document.createTextNode('\ufeff'));
178
+
179
+ // If we're just after a new line, set the selection to the character after.
180
+ const afterNewLine =
181
+ sortedRange.start.node instanceof TextNode &&
182
+ sortedRange.start.index > 0 &&
183
+ sortedRange.start.node
184
+ .getText()
185
+ .charAt(sortedRange.start.index - 1) === '\n';
186
+
187
+ // If the range has changed, set the selection so we can measure it's location.
188
+ // We guard this to avoid triggering unnecessary selection change events that don't actually modify the selection.
189
+ if (rangeChanged) {
190
+ try {
191
+ docSelection.setBaseAndExtent(
192
+ startNode.childNodes[0],
193
+ sortedRange.start.index,
194
+ endNode.childNodes[0],
195
+ sortedRange.end.index
196
+ );
197
+ } catch (e) {
198
+ console.error(e);
199
+ console.error(
200
+ `Error setting caret range, trying to set to ${startNode.childNodes[0]}:${sortedRange.start.index} - ${endNode.childNodes[0]}:${sortedRange.end.index}`
201
+ );
202
+ }
203
+ currentRange = docSelection.getRangeAt(0);
204
+ }
205
+
206
+ // Measure and remember caret position
207
+ if (currentRange) {
208
+ const rangeRect = currentRange.getBoundingClientRect();
209
+ const textRect = startNode.getBoundingClientRect();
210
+ const lineHeightString = window
211
+ .getComputedStyle(startNode)
212
+ .getPropertyValue('line-height');
213
+ const lineHeight = lineHeightString.endsWith('px')
214
+ ? parseInt(
215
+ lineHeightString.substring(
216
+ 0,
217
+ lineHeightString.length - 2
218
+ )
219
+ )
220
+ : undefined;
221
+
222
+ // Is the caret in relatively positioned ancestors above the editor? Undo their offsets.
223
+ let caretAncestor: HTMLElement | null =
224
+ startNode.closest('.bookish-editor');
225
+ let relativeX = 0;
226
+ let relativeY = 0;
227
+ while (caretAncestor != null) {
228
+ if (
229
+ window.getComputedStyle(caretAncestor)
230
+ .position === 'relative'
231
+ ) {
232
+ relativeX += caretAncestor.offsetLeft;
233
+ relativeY += caretAncestor.offsetTop;
234
+ }
235
+ caretAncestor = caretAncestor.parentElement;
236
+ }
237
+
238
+ // If we're after a new line, calculate the correct position, since selections don't actually render to the next line.
239
+ const left = afterNewLine
240
+ ? textRect.left
241
+ : rangeRect.left;
242
+ const top =
243
+ afterNewLine && lineHeight
244
+ ? rangeRect.top + lineHeight
245
+ : rangeRect.top;
246
+ const position = {
247
+ x: left + window.scrollX - relativeX,
248
+ y: top + window.scrollY - relativeY,
249
+ height: rangeRect.height,
250
+ };
251
+ newCaretPosition = position;
252
+ }
253
+ }
254
+ }
255
+ // If the range is empty, remove the browser's selection too.
256
+ else if (editorFocused) document.getSelection()?.empty();
257
+ }
258
+
259
+ // Save the new caret position so we render it.
260
+ caretCoordinate = newCaretPosition;
261
+ });
262
+
263
+ function rangeToCaret(domNode: Node, rangeIndex: number) {
264
+ // If it's a text node, find the closest TextNode parent
265
+ if (domNode.nodeType === Node.TEXT_NODE) {
266
+ let parent = domNode.parentNode;
267
+ // Keep looking up until we find the TextNode span with the node ID.
268
+ while (
269
+ parent &&
270
+ !(
271
+ parent instanceof HTMLElement &&
272
+ parent.classList.contains('bookish-text')
273
+ )
274
+ )
275
+ parent = parent.parentNode;
276
+ if (parent && parent.dataset.nodeid) {
277
+ const node = editedNode.getNode(
278
+ parseInt(parent.dataset.nodeid)
279
+ );
280
+ if (node instanceof TextNode)
281
+ // Account for the zero-width spaces that we insert in order to make selections possible on empty text nodes.
282
+ return {
283
+ node: node,
284
+ index: Math.min(rangeIndex, node.getLength()),
285
+ };
286
+ }
287
+ }
288
+ // If it's an element, see if it has a nodeID and handle it accordingly.
289
+ else if (
290
+ domNode.nodeType === Node.ELEMENT_NODE &&
291
+ domNode instanceof HTMLElement &&
292
+ domNode.dataset.nodeid
293
+ ) {
294
+ const node = editedNode.getNode(parseInt(domNode.dataset.nodeid));
295
+ // These assume that triple clicks on paragraphs in the browser choose text nodes for the selection start
296
+ // and spans or paragraph nodes for the end.
297
+ if (node instanceof ParagraphNode) {
298
+ const first = node.getFirstTextNode();
299
+ return { node: first, index: 0 };
300
+ } else if (node instanceof TextNode) {
301
+ return { node: node, index: 0 };
302
+ }
303
+ }
304
+
305
+ // Return nothing by default.
306
+ return undefined;
307
+ }
308
+
309
+ function handleSelectionChange() {
310
+ // Make sure we're showing the caret.
311
+ lastInputTime = Date.now();
312
+ keyboardIdle = false;
313
+
314
+ // Map the browser selection to our caret range model.
315
+ const selection = document.getSelection();
316
+
317
+ if (selection && selection.anchorNode && selection.focusNode) {
318
+ const start = rangeToCaret(
319
+ selection.anchorNode,
320
+ selection.anchorOffset
321
+ );
322
+ const end = rangeToCaret(
323
+ selection.focusNode,
324
+ selection.focusOffset
325
+ );
326
+ // If we found to text nodes, set the selection
327
+ if (start && end) {
328
+ caretRange = { start: start, end: end };
329
+ }
330
+ }
331
+ }
332
+
333
+ function getCaretCoordinate(
334
+ caret: Caret
335
+ ): { top: number; left: number; height: number } | undefined {
336
+ if (!(caret.node instanceof TextNode || caret.node instanceof AtomNode))
337
+ return undefined;
338
+ const domNode = document.querySelector(
339
+ `[data-nodeid='${caret.node.nodeID}`
340
+ );
341
+ // If we didn't find the node or there's no text node inside it, then there's no caret position.
342
+ // This happens temporarily before the useEffect above has a chance to insert zero-width non-breaking space.
343
+ if (domNode === null || domNode.childNodes.length === 0)
344
+ return undefined;
345
+ const range = document.createRange();
346
+ range.setStart(
347
+ domNode.childNodes.length === 0 ? domNode : domNode.childNodes[0],
348
+ domNode.childNodes.length === 0 ? 0 : caret.index
349
+ );
350
+ const characterRect = range.getBoundingClientRect();
351
+ if (characterRect.left > 0)
352
+ return {
353
+ top: characterRect.top,
354
+ left: characterRect.left,
355
+ height: characterRect.height,
356
+ };
357
+ const elementRect = domNode.getBoundingClientRect();
358
+ return {
359
+ top: elementRect.top,
360
+ left: elementRect.left,
361
+ height: elementRect.height,
362
+ };
363
+ }
364
+
365
+ function getCaretOnLine(caret: Caret, below: boolean): Caret {
366
+ if (caret.node instanceof TextNode || caret.node instanceof AtomNode) {
367
+ // Find the position of the current start node.
368
+ const startCoordinate = getCaretCoordinate(caret);
369
+ let candidate = editedNode.getAdjacentCaret(caret, below);
370
+ if (candidate === undefined) return caret;
371
+ let previousCandidate = undefined;
372
+ let previousCoordinate = undefined;
373
+ if (startCoordinate) {
374
+ while (
375
+ candidate.node instanceof TextNode ||
376
+ candidate.node instanceof AtomNode
377
+ ) {
378
+ const candidateCoordinate = getCaretCoordinate(candidate);
379
+ // Did we make it to the next line and the character before/after? If we're going down,
380
+ // we're looking for a top that's below the bottom. If we're going up, we're looking for a bottom
381
+ // that's below the top.
382
+ if (candidateCoordinate) {
383
+ const startTop = startCoordinate.top;
384
+ const candidateTop = candidateCoordinate.top;
385
+ const closerCandidate =
386
+ previousCoordinate &&
387
+ previousCandidate &&
388
+ Math.abs(
389
+ startCoordinate.left - previousCoordinate.left
390
+ ) <
391
+ Math.abs(
392
+ startCoordinate.left -
393
+ candidateCoordinate.left
394
+ )
395
+ ? previousCandidate
396
+ : candidate;
397
+ if (below) {
398
+ // If this candidate is on the line below the start line, then return the last candidate on the line below the start line.
399
+ if (
400
+ previousCandidate &&
401
+ previousCoordinate &&
402
+ previousCoordinate.top >
403
+ startTop + startCoordinate.height &&
404
+ candidateTop >
405
+ previousCoordinate.top +
406
+ previousCoordinate.height
407
+ )
408
+ return previousCandidate;
409
+ // If this is just below the start position, stop and return the closer of the current and previous candidates.
410
+ else if (
411
+ candidateTop >
412
+ startTop + startCoordinate.height &&
413
+ candidateCoordinate.left >= startCoordinate.left
414
+ )
415
+ return closerCandidate;
416
+ } else {
417
+ // If this candidate is on the live above the line above the start line, then return the last candidate on the line above the start line.
418
+ if (
419
+ previousCandidate &&
420
+ previousCoordinate &&
421
+ previousCoordinate.top +
422
+ previousCoordinate.height <
423
+ startTop &&
424
+ candidateTop + candidateCoordinate.height <
425
+ previousCoordinate.top
426
+ )
427
+ return previousCandidate;
428
+ // If this is just above the start position, stop and return the closer of the current and previous candidates.
429
+ if (
430
+ candidateTop + candidateCoordinate.height <
431
+ startTop &&
432
+ candidateCoordinate.left <= startCoordinate.left
433
+ )
434
+ return closerCandidate;
435
+ }
436
+ }
437
+
438
+ // Get the next candidate to consider.
439
+ const nextCandidate: Caret | undefined =
440
+ editedNode.getAdjacentCaret(candidate, below);
441
+ if (nextCandidate === undefined) break;
442
+
443
+ // If the caret didn't move, we stop searching, something is wrong.
444
+ if (
445
+ nextCandidate.node === candidate.node &&
446
+ nextCandidate.index === candidate.index
447
+ ) {
448
+ break;
449
+ }
450
+
451
+ // Otherwise, we advance to the next candidate.
452
+ previousCandidate = candidate;
453
+ previousCoordinate = candidateCoordinate;
454
+ candidate = nextCandidate;
455
+
456
+ if (candidate === undefined) return caret;
457
+ }
458
+ return candidate;
459
+ }
460
+ }
461
+
462
+ // If we didn't find one, just return what we were given.
463
+ return caret;
464
+ }
465
+
466
+ function atParagraphStart(): boolean {
467
+ if (
468
+ caretRange === undefined ||
469
+ caretRange.start.node !== caretRange.end.node ||
470
+ caretRange.start.index !== caretRange.end.index ||
471
+ !(caretRange.start.node instanceof TextNode)
472
+ )
473
+ return false;
474
+
475
+ const firstCaret = caretRange.start.node
476
+ .getFormatRoot(editedNode)
477
+ ?.getFirstCaret();
478
+ return (
479
+ firstCaret !== undefined &&
480
+ firstCaret.node === caretRange.start.node &&
481
+ firstCaret.index === caretRange.start.index
482
+ );
483
+ }
484
+
485
+ function undo(): Edit {
486
+ // Grab the next state
487
+ let undoState = undoStack[undoPosition + 1];
488
+
489
+ // Restore the content of the chapter.
490
+ let node = parser(undoState.bookdown);
491
+
492
+ if (node === undefined || node instanceof ErrorNode) return;
493
+
494
+ // Restore the view
495
+ updateRoot(node);
496
+
497
+ // Save the undo.
498
+ save(node);
499
+
500
+ // Move the undo state down a position.
501
+ if (undoPosition < undoStack.length) undoPosition = undoPosition + 1;
502
+
503
+ // Return the original caret.
504
+ const range = indexRangeToCaretRange(node, undoState.range);
505
+ if (range === undefined) return;
506
+ return { root: node, range: range };
507
+ }
508
+
509
+ function redo(): Edit {
510
+ // Grab the next state
511
+ let undoState = undoStack[undoPosition - 1];
512
+
513
+ // Restore the content of the chapter.
514
+ let node = parser(undoState.bookdown);
515
+
516
+ if (node === undefined) return;
517
+
518
+ updateRoot(node);
519
+
520
+ // Move the undo state down a position.
521
+ if (undoPosition > 0) undoPosition = undoPosition - 1;
522
+
523
+ // Return the original caret.
524
+ const range = indexRangeToCaretRange(node, undoState.range);
525
+ if (range === undefined) return;
526
+ return { root: node, range: range };
527
+ }
528
+
529
+ function getCaretContext(): WhatsAroundTheCaret | undefined {
530
+ if (caretRange === undefined) return undefined;
531
+
532
+ // Determine whether the range contains a list
533
+ const nodes = editedNode.getNodes();
534
+ let inside = false;
535
+ let includesList = false;
536
+ nodes.forEach((n) => {
537
+ if (caretRange === undefined) return;
538
+ if (n === caretRange.start.node) inside = true;
539
+ if (
540
+ inside &&
541
+ n.getClosestParentMatching(
542
+ editedNode,
543
+ (p) => p instanceof ListNode
544
+ ) !== undefined
545
+ )
546
+ includesList = true;
547
+ if (n === caretRange.end.node) inside = false;
548
+ });
549
+
550
+ const parents = editedNode
551
+ .getParentsOf(caretRange.start.node)
552
+ ?.reverse() as BookishNode[];
553
+
554
+ return {
555
+ // We make a new range so that setCaretRange always causes a re-render
556
+ chapter: chapter,
557
+ range: { start: caretRange.start, end: caretRange.end },
558
+ start: caretRange.start,
559
+ end: caretRange.end,
560
+ isSelection:
561
+ caretRange.start.node !== caretRange.end.node ||
562
+ caretRange.start.index !== caretRange.end.index,
563
+ root: editedNode,
564
+ blocks: parents?.find((n) => n instanceof BlocksNode) as BlocksNode,
565
+ paragraph: parents?.find(
566
+ (n) => n instanceof ParagraphNode
567
+ ) as ParagraphNode,
568
+ block: parents?.find((n) => n instanceof BlockNode) as BlockNode,
569
+ code: parents?.find((n) => n instanceof CodeNode) as CodeNode,
570
+ list: parents?.find((n) => n instanceof ListNode) as ListNode,
571
+ atom: parents?.find((n) => n instanceof AtomNode) as AtomNode<any>,
572
+ meta: parents?.find(
573
+ (n) => n instanceof MetadataNode
574
+ ) as MetadataNode<FormatNode>,
575
+ includesList: includesList,
576
+ table: parents?.find((n) => n instanceof TableNode) as TableNode,
577
+ format:
578
+ caretRange.end.node instanceof TextNode ||
579
+ caretRange.end.node instanceof AtomNode
580
+ ? caretRange.end.node.getFormatRoot(editedNode)
581
+ : undefined,
582
+ startIsText: caretRange.start.node instanceof TextNode,
583
+ endIsText: caretRange.end.node instanceof TextNode,
584
+ startIsTextOrAtom:
585
+ caretRange.start.node instanceof TextNode ||
586
+ caretRange.start.node instanceof AtomNode ||
587
+ caretRange.start.node instanceof EmbedNode,
588
+ endIsTextOrAtom:
589
+ caretRange.end.node instanceof TextNode ||
590
+ caretRange.end.node instanceof AtomNode ||
591
+ caretRange.start.node instanceof EmbedNode,
592
+ atParagraphStart: atParagraphStart(),
593
+ undoStack: undoStack,
594
+ undoPosition: undoPosition,
595
+ undo: undo,
596
+ redo: redo,
597
+ clipboard: clipboard,
598
+ handleCopy: handleCopy,
599
+ handlePaste: handlePaste,
600
+ };
601
+ }
602
+
603
+ function getUtilities(): CaretUtilities {
604
+ return {
605
+ getCaretOnLine: getCaretOnLine,
606
+ };
607
+ }
608
+
609
+ function handlePress(event: KeyboardEvent) {
610
+ handleKey(event, true);
611
+ }
612
+
613
+ function handleKey(event: KeyboardEvent, press: boolean = false) {
614
+ // Only handle keystrokes when this is focused.
615
+ // Otherwise, we let any focusable elements in this editor handle them.
616
+ if (!editorFocused) return;
617
+
618
+ // Remember the time of this keystroke.
619
+ lastInputTime = Date.now();
620
+ keyboardIdle = false;
621
+
622
+ if (caretRange === undefined) return;
623
+
624
+ // Build some context
625
+ const context = getCaretContext();
626
+
627
+ if (context === undefined) return;
628
+
629
+ // Loop through the commands to see if there's a match.
630
+ const unmatched = commands.every((command) => {
631
+ // If the keystroke and caret position matches the command signature, execute the command and update the caret range.
632
+ if (
633
+ (command.press === undefined || command.press === press) &&
634
+ (command.shift === undefined ||
635
+ command.shift === event.shiftKey) &&
636
+ (command.alt === undefined || command.alt === event.altKey) &&
637
+ (command.control === undefined ||
638
+ command.control === (event.ctrlKey || event.metaKey)) &&
639
+ (command.key === undefined ||
640
+ (Array.isArray(command.key) &&
641
+ command.key.includes(event.key)) ||
642
+ (command.key instanceof Function &&
643
+ command.key(event.key)) ||
644
+ command.key === event.key) &&
645
+ (command.code === undefined || command.code === event.code) &&
646
+ (command.active === true ||
647
+ (command.active instanceof Function &&
648
+ command.active.call(undefined, context, event.key)))
649
+ ) {
650
+ // Execute the command
651
+ const handled = executeCommand(command, event.key);
652
+
653
+ if (handled) {
654
+ event.preventDefault();
655
+ event.stopPropagation();
656
+ }
657
+
658
+ // Stop searching for a matching command.
659
+ return handled;
660
+ }
661
+ // Continue searching for a matching command.
662
+ return true;
663
+ });
664
+
665
+ if (unmatched) {
666
+ // Toolbar navigation
667
+ if (event.key === 'Escape') {
668
+ // If we've selected a FootnoteNode or Comment, navigate to the footnote or comment text to edit it.
669
+ if (
670
+ caretRange.start.node instanceof AtomNode &&
671
+ caretRange.start.node.getMeta() instanceof FormatNode
672
+ ) {
673
+ const firstCaret = caretRange.start.node
674
+ .getMeta()
675
+ .getFirstCaret();
676
+ caretRange = { start: firstCaret, end: firstCaret };
677
+ event.preventDefault();
678
+ event.stopPropagation();
679
+ return true;
680
+ }
681
+ // If we're inside an atom node, select the atom itself.
682
+ else if (caretRange.start.node.isInside(editedNode, AtomNode)) {
683
+ const atom = caretRange.start.node.getClosestParentOfType<
684
+ AtomNode<any>
685
+ >(editedNode, AtomNode);
686
+ if (atom) {
687
+ const atomCaret = { node: atom, index: 0 };
688
+ caretRange = { start: atomCaret, end: atomCaret };
689
+ event.preventDefault();
690
+ event.stopPropagation();
691
+ return true;
692
+ }
693
+ }
694
+ // Otherwise, if we have a reference to this editor's DOM element, find the nearest focusable element in it's toolbar and focus on that.
695
+ const controls = [
696
+ document.querySelector(
697
+ '.bookish-editor-toolbar [tabindex="0"]'
698
+ ),
699
+ document.querySelector('.bookish-editor-toolbar input'),
700
+ document.querySelector('.bookish-editor-toolbar select'),
701
+ document.querySelector('.bookish-editor-toolbar button'),
702
+ ];
703
+ const match = controls.find(
704
+ (control) => control && control instanceof HTMLElement
705
+ );
706
+ if (match && match instanceof HTMLElement) {
707
+ match.focus();
708
+ event.preventDefault();
709
+ event.stopPropagation();
710
+ return true;
711
+ }
712
+ }
713
+ }
714
+ }
715
+
716
+ function executeCommand(command: Command, key: string): boolean {
717
+ // Assume we process it, then flip it if we don't.
718
+ ignoredInput = false;
719
+
720
+ const context = getCaretContext();
721
+ if (context && caretRange) {
722
+ const results = command.handler.call(
723
+ undefined,
724
+ context,
725
+ getUtilities(),
726
+ key
727
+ );
728
+
729
+ // If the command invoked produced a new range
730
+ if (results !== undefined) {
731
+ const { root, range } = results;
732
+ const newRange = { start: range.start, end: range.end };
733
+
734
+ if (root === editedNode && command.mutates)
735
+ console.error(
736
+ `Warning: immutability violation on ${command.description}`
737
+ );
738
+
739
+ // Set the range to force a rerender, assuming something in the document changed.
740
+ caretRange = newRange;
741
+
742
+ // Save the copy in the undo stack if this isn't a navigation or selection state.
743
+ if (
744
+ command.category !== 'navigation' &&
745
+ command.category !== 'selection' &&
746
+ command.category !== 'history'
747
+ ) {
748
+ saveEdit(root as RootNode, newRange, command);
749
+ }
750
+ return true;
751
+ } else {
752
+ ignoredInput = true;
753
+ return false;
754
+ }
755
+ }
756
+ return false;
757
+ }
758
+
759
+ async function saveEdit(
760
+ newRoot: RootNode,
761
+ newRange: CaretRange,
762
+ command?: Command
763
+ ) {
764
+ // Don't save edits if locked.
765
+ if (locked) return;
766
+
767
+ // Call the save callback.
768
+ save(newRoot);
769
+
770
+ // Initialize a new history if we don't have one.
771
+ let newStack: UndoState[] = undoStack.length > 0 ? undoStack : [];
772
+
773
+ // If there's nothing on the stack, save the initial state before we add something to the stack.
774
+ if (undoStack.length === 0 && caretRange !== undefined) {
775
+ const currentRange = caretRangeToIndexRange(editedNode, caretRange);
776
+ if (currentRange !== undefined)
777
+ newStack = [
778
+ {
779
+ bookdown: editedNode.toBookdown(),
780
+ command: undefined,
781
+ range: currentRange,
782
+ },
783
+ ];
784
+ }
785
+
786
+ // Change the chapter's AST.
787
+ updateRoot(newRoot);
788
+
789
+ // Set the new undo stack, pre-pending the new command to the front.
790
+ const newTextRange = caretRangeToIndexRange(newRoot, newRange);
791
+ if (newTextRange !== undefined)
792
+ undoStack = [
793
+ {
794
+ bookdown: newRoot.toBookdown(),
795
+ command: command,
796
+ range: newTextRange,
797
+ // If the undo position is beyond the front, clear everything before it, because we're changing history.
798
+ },
799
+ ...(undoPosition > 0 ? newStack.slice(undoPosition) : newStack),
800
+ ];
801
+
802
+ // Set the undo position to the last index.
803
+ undoPosition = 0;
804
+
805
+ // Focus the editor on the new caret location, unless the toolbar is focused.
806
+ if (!toolbarIsFocused()) element?.focus();
807
+
808
+ // Update the caret
809
+ claimActiveEditor();
810
+ }
811
+
812
+ function updateRoot(newRoot: RootNode) {
813
+ editedNode = newRoot;
814
+ text = editedNode.toBookdown();
815
+ }
816
+
817
+ function editNode(previous: BookishNode, edited: BookishNode) {
818
+ if (caretRange) {
819
+ const newRoot = editedNode.withNodeReplaced(previous, edited);
820
+ if (newRoot === undefined) return;
821
+ saveEdit(newRoot, { start: caretRange.start, end: caretRange.end });
822
+
823
+ // Update the range if the current range contains the previous node. This will generally
824
+ // be true any time a selected atom node is edited.
825
+ if (
826
+ caretRange.start.node === previous &&
827
+ (edited instanceof AtomNode || edited instanceof TextNode)
828
+ )
829
+ caretRange = {
830
+ start: { node: edited, index: 0 },
831
+ end: { node: edited, index: 0 },
832
+ };
833
+ }
834
+ }
835
+
836
+ function handleMouseDown() {
837
+ if (!element) return;
838
+
839
+ // Grab focus.
840
+ element.focus();
841
+
842
+ // Update the caret range to ensure it's positioned correctly, since it depends on relative positioned parents.
843
+ if (caretRange)
844
+ caretRange = { start: caretRange.start, end: caretRange.end };
845
+
846
+ // If we've selected a non-TextNode, release it, so the browser is free to select a text node.
847
+ if (
848
+ caretRange &&
849
+ !(
850
+ caretRange.start.node instanceof TextNode ||
851
+ caretRange.start.node instanceof AtomNode ||
852
+ caretRange.start.node instanceof EmbedNode
853
+ )
854
+ ) {
855
+ caretRange = undefined;
856
+ }
857
+ }
858
+
859
+ function isFocused() {
860
+ return (
861
+ document.activeElement === element ||
862
+ (element !== null && element?.contains(document.activeElement)) ||
863
+ toolbarIsFocused()
864
+ );
865
+ }
866
+
867
+ function handleFocus() {
868
+ editorFocused = isFocused();
869
+ lease(true);
870
+ }
871
+
872
+ function handleBlur() {
873
+ editorFocused = isFocused();
874
+ lease(false);
875
+ }
876
+
877
+ function handleCopy(node: BookishNode) {
878
+ // Set the editor's clipboard.
879
+ clipboard = node;
880
+
881
+ // Set the OS clipboard.
882
+ if (navigator.clipboard) {
883
+ navigator.clipboard.write([
884
+ new ClipboardItem({
885
+ 'text/plain': new Blob([node.toText()], {
886
+ type: 'text/plain',
887
+ }),
888
+ 'text/html': new Blob([node.toHTML()], {
889
+ type: 'text/html',
890
+ }),
891
+ }),
892
+ ]);
893
+ } else
894
+ window.alert(
895
+ "Your browser doesn't support copying to the clipboard :("
896
+ );
897
+ }
898
+
899
+ function handlePaste(
900
+ context: WhatsAroundTheCaret,
901
+ text: PasteContent | BookishNode,
902
+ save: boolean
903
+ ) {
904
+ if (text instanceof BookishNode)
905
+ return handlePasteNode(context, text, save);
906
+
907
+ const plain =
908
+ text.plain === undefined ? undefined : new TextNode(text.plain);
909
+
910
+ if (plain) {
911
+ const edit = handlePasteNode(context, plain, save);
912
+ if (edit) return edit;
913
+ }
914
+ return undefined;
915
+ }
916
+
917
+ function handlePasteNode(
918
+ context: WhatsAroundTheCaret,
919
+ node: BookishNode,
920
+ save: boolean
921
+ ) {
922
+ // Track revisions in these two variables.
923
+ let newRoot = context.root;
924
+ let newCaret = context.start;
925
+
926
+ // Is there any text to remove? Remove it first.
927
+ if (context.isSelection) {
928
+ const edit = newRoot.withRangeFormatted(context.range, undefined);
929
+ if (edit === undefined || !(edit.root instanceof ChapterNode))
930
+ return;
931
+ newRoot = edit.root;
932
+ newCaret = edit.range.start;
933
+ }
934
+
935
+ // Find the parents of the caret node, from nearest to furthest
936
+ const parents = context.root.getParentsOf(newCaret.node);
937
+ if (parents === undefined) return;
938
+
939
+ // Pop parents, nearest to farthest, searching for one that handles an insertion.
940
+ let edit = undefined;
941
+ let parent = undefined;
942
+ while (parents.length > 0) {
943
+ parent = parents.pop();
944
+ if (parent === undefined) break;
945
+ // It's critical that we insert a copy and not the node in case people copy multiple times. A node can only appear once in a tree.
946
+ edit = parent.withNodeInserted(newCaret, node.copy());
947
+ if (edit !== undefined) break;
948
+ }
949
+
950
+ // Insert the node at the caret.
951
+ if (parent !== undefined && edit !== undefined) {
952
+ const newChapter = context.root.withNodeReplaced(parent, edit.root);
953
+ if (newChapter === undefined) return;
954
+
955
+ if (save) {
956
+ // Set the range to force a rerender, assuming something in the document changed.
957
+ caretRange = edit.range;
958
+
959
+ // Save the copy in the undo stack if this isn't a navigation or selection state.
960
+ saveEdit(newChapter, edit.range);
961
+ } else return { root: newChapter, range: edit.range };
962
+ }
963
+ }
964
+
965
+ function toolbarIsFocused() {
966
+ const toolbar = document.querySelector('.bookish-editor-toolbar');
967
+ return toolbar !== null && toolbar.contains(document.activeElement);
968
+ }
969
+
970
+ $: isAtom = caretRange && caretRange.start.node instanceof AtomNode;
971
+ $: inAtom =
972
+ caretRange && caretRange.start.node.isInside(editedNode, AtomNode);
973
+ $: isSelection =
974
+ caretRange !== undefined &&
975
+ (caretRange.start.node !== caretRange.end.node ||
976
+ caretRange.start.index !== caretRange.end.index);
977
+ $: isItalic =
978
+ caretRange !== undefined &&
979
+ isSelection === false &&
980
+ caretRange.start.node instanceof TextNode &&
981
+ caretRange.start.node.isItalic(editedNode);
982
+ $: isBold =
983
+ caretRange !== undefined &&
984
+ isSelection === false &&
985
+ caretRange.start.node instanceof TextNode &&
986
+ caretRange.start.node.isBold(editedNode);
987
+ $: isLink =
988
+ caretRange !== undefined &&
989
+ isSelection === false &&
990
+ caretRange.start.node.getClosestParentMatching(
991
+ editedNode,
992
+ (p) => p instanceof LinkNode
993
+ ) !== undefined;
994
+
995
+ // When the caret context or editor focus changes, update the active editor.
996
+ $: {
997
+ if ((caretRange || editorFocused) && isFocused()) claimActiveEditor();
998
+ else activeEditor.set(undefined);
999
+ }
1000
+
1001
+ function claimActiveEditor() {
1002
+ activeEditor.set({
1003
+ range: caretRange,
1004
+ coordinate: caretCoordinate,
1005
+ setCaret: (range: CaretRange | undefined) => {
1006
+ caretRange = range;
1007
+ element?.focus();
1008
+ },
1009
+ edit: editNode,
1010
+ executor: executeCommand,
1011
+ context: getCaretContext(),
1012
+ root: editedNode,
1013
+ focused: editorFocused,
1014
+ });
1015
+ }
1016
+
1017
+ // Pass the function above to all children so they can claim the active editor for non-focus reasons.
1018
+ // (Drag and drop, for example.)
1019
+ setContext('claimeditor', claimActiveEditor);
1020
+ </script>
1021
+
1022
+ <div
1023
+ class={`bookish-editor ${inAtom ? 'bookish-editor-atom-focused' : ''}`}
1024
+ class:locked
1025
+ aria-disabled={locked}
1026
+ bind:this={element}
1027
+ on:keydown={handleKey}
1028
+ on:keypress={handlePress}
1029
+ on:mousedown|stopPropagation={handleMouseDown}
1030
+ on:focus={handleFocus}
1031
+ on:blur={handleBlur}
1032
+ tabIndex="0"
1033
+ >
1034
+ <!-- A view of the root -->
1035
+ <svelte:component
1036
+ this={component}
1037
+ node={editedNode}
1038
+ {placeholder}
1039
+ editable={true}
1040
+ />
1041
+ <!-- The caret. We render our own since this view isn't contentEditable and we can't show a caret.
1042
+ Customize the rendering based on the formatting applied to the text node. -->
1043
+ {#if caretCoordinate && caretRange && !isAtom && isSelection === false && editorFocused}
1044
+ <CaretView
1045
+ left={caretCoordinate.x}
1046
+ top={caretCoordinate.y}
1047
+ height={caretCoordinate.height}
1048
+ blink={editorFocused && keyboardIdle}
1049
+ ignored={ignoredInput}
1050
+ linked={isLink}
1051
+ bold={isBold}
1052
+ italic={isItalic}
1053
+ disabled={!editorFocused || locked}
1054
+ {locked}
1055
+ />
1056
+ {/if}
1057
+ </div>
1058
+
1059
+ <style>
1060
+ .bookish-editor {
1061
+ min-height: 1em;
1062
+ }
1063
+
1064
+ .locked :global(.bookish-text) {
1065
+ color: var(--app-muted-color);
1066
+ }
1067
+
1068
+ .bookish-editor:focus {
1069
+ outline: var(--app-chrome-border-size) solid
1070
+ var(--app-interactive-color);
1071
+ outline-offset: var(--app-chrome-padding);
1072
+ border-radius: var(--app-chrome-roundedness);
1073
+ }
1074
+
1075
+ .bookish-editor.bookish-editor-atom-focused:focus {
1076
+ outline: none;
1077
+ }
1078
+
1079
+ .bookish-editor-inline-editor {
1080
+ display: inline-block;
1081
+ position: relative;
1082
+ }
1083
+
1084
+ .bookish-editor-inline-form {
1085
+ position: absolute;
1086
+ left: 0;
1087
+ top: 0;
1088
+ min-width: 7em;
1089
+ width: auto;
1090
+ z-index: 2;
1091
+ }
1092
+
1093
+ .bookish-link-active {
1094
+ font-weight: bold;
1095
+ }
1096
+
1097
+ .bookish-editor :global(hr) {
1098
+ margin-top: 1em;
1099
+ margin-bottom: 1em;
1100
+ }
1101
+ </style>