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.
- package/LICENSE +21 -0
- package/README.md +5 -0
- package/components/Auth.svelte +52 -0
- package/components/Auth.svelte.d.ts +16 -0
- package/components/Authentication.d.ts +8 -0
- package/components/Authentication.js +1 -0
- package/components/BookPreview.svelte +87 -0
- package/components/BookPreview.svelte.d.ts +18 -0
- package/components/External.svelte +4 -0
- package/components/External.svelte.d.ts +18 -0
- package/components/Link.svelte +5 -0
- package/components/Link.svelte.d.ts +19 -0
- package/components/chapter/Atom.svelte +36 -0
- package/components/chapter/Atom.svelte.d.ts +19 -0
- package/components/chapter/Block.svelte +32 -0
- package/components/chapter/Block.svelte.d.ts +17 -0
- package/components/chapter/Callout.svelte +10 -0
- package/components/chapter/Callout.svelte.d.ts +17 -0
- package/components/chapter/CaptionedCode.svelte +34 -0
- package/components/chapter/CaptionedCode.svelte.d.ts +17 -0
- package/components/chapter/ChapterBody.svelte +23 -0
- package/components/chapter/ChapterBody.svelte.d.ts +18 -0
- package/components/chapter/Citations.svelte +66 -0
- package/components/chapter/Citations.svelte.d.ts +17 -0
- package/components/chapter/Code.svelte +35 -0
- package/components/chapter/Code.svelte.d.ts +22 -0
- package/components/chapter/Comment.svelte +46 -0
- package/components/chapter/Comment.svelte.d.ts +17 -0
- package/components/chapter/Definition.svelte +38 -0
- package/components/chapter/Definition.svelte.d.ts +20 -0
- package/components/chapter/Embed.svelte +122 -0
- package/components/chapter/Embed.svelte.d.ts +18 -0
- package/components/chapter/ErrorMessage.svelte +4 -0
- package/components/chapter/ErrorMessage.svelte.d.ts +17 -0
- package/components/chapter/Footnote.svelte +37 -0
- package/components/chapter/Footnote.svelte.d.ts +19 -0
- package/components/chapter/Format.svelte +57 -0
- package/components/chapter/Format.svelte.d.ts +18 -0
- package/components/chapter/InlineCode.svelte +12 -0
- package/components/chapter/InlineCode.svelte.d.ts +17 -0
- package/components/chapter/Label.svelte +20 -0
- package/components/chapter/Label.svelte.d.ts +17 -0
- package/components/chapter/Link.svelte +51 -0
- package/components/chapter/Link.svelte.d.ts +17 -0
- package/components/chapter/List.svelte +14 -0
- package/components/chapter/List.svelte.d.ts +17 -0
- package/components/chapter/Marginal.svelte +48 -0
- package/components/chapter/Marginal.svelte.d.ts +19 -0
- package/components/chapter/Paragraph.svelte +15 -0
- package/components/chapter/Paragraph.svelte.d.ts +18 -0
- package/components/chapter/Python.svelte +81 -0
- package/components/chapter/Python.svelte.d.ts +18 -0
- package/components/chapter/Quote.svelte +16 -0
- package/components/chapter/Quote.svelte.d.ts +17 -0
- package/components/chapter/Rule.svelte +4 -0
- package/components/chapter/Rule.svelte.d.ts +17 -0
- package/components/chapter/Table.svelte +29 -0
- package/components/chapter/Table.svelte.d.ts +17 -0
- package/components/chapter/Text.svelte +62 -0
- package/components/chapter/Text.svelte.d.ts +17 -0
- package/components/chapter/renderPosition.d.ts +3 -0
- package/components/chapter/renderPosition.js +4 -0
- package/components/editor/BookishEditor.svelte +755 -0
- package/components/editor/BookishEditor.svelte.d.ts +22 -0
- package/components/editor/CalloutEditor.svelte +10 -0
- package/components/editor/CalloutEditor.svelte.d.ts +17 -0
- package/components/editor/CaptionedCodeEditor.svelte +22 -0
- package/components/editor/CaptionedCodeEditor.svelte.d.ts +17 -0
- package/components/editor/CaretContext.d.ts +18 -0
- package/components/editor/CaretContext.js +1 -0
- package/components/editor/CaretState.d.ts +47 -0
- package/components/editor/CaretState.js +1 -0
- package/components/editor/CaretUtilities.d.ts +5 -0
- package/components/editor/CaretUtilities.js +1 -0
- package/components/editor/CitationsEditor.svelte +42 -0
- package/components/editor/CitationsEditor.svelte.d.ts +17 -0
- package/components/editor/Clipboard.d.ts +3 -0
- package/components/editor/Clipboard.js +1 -0
- package/components/editor/Command.d.ts +18 -0
- package/components/editor/Command.js +1 -0
- package/components/editor/Commands.d.ts +3 -0
- package/components/editor/Commands.js +951 -0
- package/components/editor/ConfirmButton.svelte +43 -0
- package/components/editor/ConfirmButton.svelte.d.ts +18 -0
- package/components/editor/DefinitionEditor.svelte +28 -0
- package/components/editor/DefinitionEditor.svelte.d.ts +17 -0
- package/components/editor/EmbedEditor.svelte +87 -0
- package/components/editor/EmbedEditor.svelte.d.ts +17 -0
- package/components/editor/ImageChooser.svelte +70 -0
- package/components/editor/ImageChooser.svelte.d.ts +18 -0
- package/components/editor/InlineCodeEditor.svelte +9 -0
- package/components/editor/InlineCodeEditor.svelte.d.ts +17 -0
- package/components/editor/LabelEditor.svelte +23 -0
- package/components/editor/LabelEditor.svelte.d.ts +17 -0
- package/components/editor/LanguageEditor.svelte +11 -0
- package/components/editor/LanguageEditor.svelte.d.ts +17 -0
- package/components/editor/LinkEditor.svelte +75 -0
- package/components/editor/LinkEditor.svelte.d.ts +17 -0
- package/components/editor/Note.svelte +9 -0
- package/components/editor/Note.svelte.d.ts +23 -0
- package/components/editor/PositionEditor.svelte +14 -0
- package/components/editor/PositionEditor.svelte.d.ts +18 -0
- package/components/editor/QuoteEditor.svelte +5 -0
- package/components/editor/QuoteEditor.svelte.d.ts +17 -0
- package/components/editor/Switch.svelte +56 -0
- package/components/editor/Switch.svelte.d.ts +20 -0
- package/components/editor/TextEditor.svelte +193 -0
- package/components/editor/TextEditor.svelte.d.ts +23 -0
- package/components/editor/Toggle.svelte +38 -0
- package/components/editor/Toggle.svelte.d.ts +19 -0
- package/components/editor/Toolbar.svelte +202 -0
- package/components/editor/Toolbar.svelte.d.ts +20 -0
- package/components/editor/ToolbarGroup.svelte +41 -0
- package/components/editor/ToolbarGroup.svelte.d.ts +19 -0
- package/components/editor/ToolbarIcon.svelte +22 -0
- package/components/editor/ToolbarIcon.svelte.d.ts +16 -0
- package/components/editor/ToolbarSpacer.svelte +1 -0
- package/components/editor/ToolbarSpacer.svelte.d.ts +19 -0
- package/components/editor/URLEditor.svelte +15 -0
- package/components/editor/URLEditor.svelte.d.ts +18 -0
- package/components/editor/UndoState.d.ts +8 -0
- package/components/editor/UndoState.js +1 -0
- package/components/page/Acknowledgements.svelte +29 -0
- package/components/page/Acknowledgements.svelte.d.ts +14 -0
- package/components/page/Alert.svelte +11 -0
- package/components/page/Alert.svelte.d.ts +23 -0
- package/components/page/Authors.svelte +62 -0
- package/components/page/Authors.svelte.d.ts +20 -0
- package/components/page/BulkReferenceEditor.svelte +32 -0
- package/components/page/BulkReferenceEditor.svelte.d.ts +14 -0
- package/components/page/Chapter.svelte +301 -0
- package/components/page/Chapter.svelte.d.ts +18 -0
- package/components/page/ChapterContext.d.ts +10 -0
- package/components/page/ChapterContext.js +1 -0
- package/components/page/Contexts.d.ts +27 -0
- package/components/page/Contexts.js +21 -0
- package/components/page/DefinitionView.svelte +129 -0
- package/components/page/DefinitionView.svelte.d.ts +18 -0
- package/components/page/Description.svelte +22 -0
- package/components/page/Description.svelte.d.ts +14 -0
- package/components/page/Edition.svelte +106 -0
- package/components/page/Edition.svelte.d.ts +21 -0
- package/components/page/Glossary.svelte +61 -0
- package/components/page/Glossary.svelte.d.ts +14 -0
- package/components/page/Header.svelte +105 -0
- package/components/page/Header.svelte.d.ts +27 -0
- package/components/page/Index.svelte +117 -0
- package/components/page/Index.svelte.d.ts +14 -0
- package/components/page/Instructions.svelte +30 -0
- package/components/page/Instructions.svelte.d.ts +16 -0
- package/components/page/License.svelte +29 -0
- package/components/page/License.svelte.d.ts +14 -0
- package/components/page/Loading.svelte +13 -0
- package/components/page/Loading.svelte.d.ts +19 -0
- package/components/page/Media.svelte +76 -0
- package/components/page/Media.svelte.d.ts +14 -0
- package/components/page/MediaPreview.svelte +11 -0
- package/components/page/MediaPreview.svelte.d.ts +19 -0
- package/components/page/Outline.svelte +173 -0
- package/components/page/Outline.svelte.d.ts +19 -0
- package/components/page/Page.svelte +53 -0
- package/components/page/Page.svelte.d.ts +19 -0
- package/components/page/PossibleChapter.svelte +14 -0
- package/components/page/PossibleChapter.svelte.d.ts +14 -0
- package/components/page/PossibleReference.svelte +16 -0
- package/components/page/PossibleReference.svelte.d.ts +19 -0
- package/components/page/Print.svelte +8 -0
- package/components/page/Print.svelte.d.ts +14 -0
- package/components/page/Reference.svelte +145 -0
- package/components/page/Reference.svelte.d.ts +17 -0
- package/components/page/References.svelte +58 -0
- package/components/page/References.svelte.d.ts +14 -0
- package/components/page/Revisions.svelte +124 -0
- package/components/page/Revisions.svelte.d.ts +14 -0
- package/components/page/Search.svelte +108 -0
- package/components/page/Search.svelte.d.ts +14 -0
- package/components/page/Status.svelte +49 -0
- package/components/page/Status.svelte.d.ts +19 -0
- package/components/page/SubdomainEditor.svelte +30 -0
- package/components/page/SubdomainEditor.svelte.d.ts +14 -0
- package/components/page/TableOfContents.svelte +234 -0
- package/components/page/TableOfContents.svelte.d.ts +14 -0
- package/components/page/TableOfContentsImage.svelte +15 -0
- package/components/page/TableOfContentsImage.svelte.d.ts +16 -0
- package/components/page/TableOfContentsRow.svelte +57 -0
- package/components/page/TableOfContentsRow.svelte.d.ts +24 -0
- package/components/page/ThemeEditor.svelte +52 -0
- package/components/page/ThemeEditor.svelte.d.ts +14 -0
- package/components/page/ThemeEditorPreview.svelte +37 -0
- package/components/page/ThemeEditorPreview.svelte.d.ts +17 -0
- package/components/page/ThemeSetEditor.svelte +17 -0
- package/components/page/ThemeSetEditor.svelte.d.ts +18 -0
- package/components/page/ThemeVariableEditor.svelte +28 -0
- package/components/page/ThemeVariableEditor.svelte.d.ts +18 -0
- package/components/page/Unknown.svelte +29 -0
- package/components/page/Unknown.svelte.d.ts +16 -0
- package/components/page/margins.d.ts +2 -0
- package/components/page/margins.js +132 -0
- package/models/BookLoader.d.ts +2 -0
- package/models/BookLoader.js +67 -0
- package/models/Firebase.d.ts +4 -0
- package/models/Firebase.js +26 -0
- package/models/Firestore.d.ts +22 -0
- package/models/Firestore.js +194 -0
- package/models/book/Book.d.ts +60 -0
- package/models/book/Book.js +135 -0
- package/models/book/BookMedia.d.ts +23 -0
- package/models/book/BookMedia.js +102 -0
- package/models/book/Chapter.d.ts +70 -0
- package/models/book/Chapter.js +221 -0
- package/models/book/ChapterID.d.ts +10 -0
- package/models/book/ChapterID.js +10 -0
- package/models/book/Definition.d.ts +6 -0
- package/models/book/Definition.js +1 -0
- package/models/book/Edition.d.ts +111 -0
- package/models/book/Edition.js +466 -0
- package/models/book/Theme.d.ts +106 -0
- package/models/book/Theme.js +76 -0
- package/models/book/ThemeColors.d.ts +15 -0
- package/models/book/ThemeColors.js +1 -0
- package/models/chapter/AtomNode.d.ts +18 -0
- package/models/chapter/AtomNode.js +31 -0
- package/models/chapter/BlockNode.d.ts +21 -0
- package/models/chapter/BlockNode.js +117 -0
- package/models/chapter/BlocksNode.d.ts +50 -0
- package/models/chapter/BlocksNode.js +709 -0
- package/models/chapter/BlocksNode.test.d.ts +1 -0
- package/models/chapter/BlocksNode.test.js +302 -0
- package/models/chapter/Bookkeeping.d.ts +4 -0
- package/models/chapter/Bookkeeping.js +1 -0
- package/models/chapter/CalloutNode.d.ts +17 -0
- package/models/chapter/CalloutNode.js +36 -0
- package/models/chapter/Caret.d.ts +21 -0
- package/models/chapter/Caret.js +69 -0
- package/models/chapter/ChapterNode.d.ts +34 -0
- package/models/chapter/ChapterNode.js +80 -0
- package/models/chapter/CitationsNode.d.ts +16 -0
- package/models/chapter/CitationsNode.js +17 -0
- package/models/chapter/CodeNode.d.ts +30 -0
- package/models/chapter/CodeNode.js +64 -0
- package/models/chapter/CommentNode.d.ts +18 -0
- package/models/chapter/CommentNode.js +32 -0
- package/models/chapter/DefinitionNode.d.ts +12 -0
- package/models/chapter/DefinitionNode.js +20 -0
- package/models/chapter/Edit.d.ts +7 -0
- package/models/chapter/Edit.js +1 -0
- package/models/chapter/EmbedNode.d.ts +40 -0
- package/models/chapter/EmbedNode.js +89 -0
- package/models/chapter/ErrorNode.d.ts +17 -0
- package/models/chapter/ErrorNode.js +21 -0
- package/models/chapter/FootnoteNode.d.ts +18 -0
- package/models/chapter/FootnoteNode.js +29 -0
- package/models/chapter/FormatNode.d.ts +60 -0
- package/models/chapter/FormatNode.js +614 -0
- package/models/chapter/FormatNode.test.d.ts +1 -0
- package/models/chapter/FormatNode.test.js +54 -0
- package/models/chapter/InlineCodeNode.d.ts +12 -0
- package/models/chapter/InlineCodeNode.js +18 -0
- package/models/chapter/LabelNode.d.ts +17 -0
- package/models/chapter/LabelNode.js +22 -0
- package/models/chapter/LinkNode.d.ts +12 -0
- package/models/chapter/LinkNode.js +16 -0
- package/models/chapter/ListNode.d.ts +51 -0
- package/models/chapter/ListNode.js +370 -0
- package/models/chapter/ListNode.test.d.ts +1 -0
- package/models/chapter/ListNode.test.js +49 -0
- package/models/chapter/MetadataNode.d.ts +17 -0
- package/models/chapter/MetadataNode.js +24 -0
- package/models/chapter/Node.d.ts +36 -0
- package/models/chapter/Node.js +173 -0
- package/models/chapter/Node.test.d.ts +1 -0
- package/models/chapter/Node.test.js +27 -0
- package/models/chapter/ParagraphNode.d.ts +33 -0
- package/models/chapter/ParagraphNode.js +93 -0
- package/models/chapter/ParagraphNode.test.d.ts +1 -0
- package/models/chapter/ParagraphNode.test.js +21 -0
- package/models/chapter/Parser.d.ts +75 -0
- package/models/chapter/Parser.js +824 -0
- package/models/chapter/Parser.test.d.ts +1 -0
- package/models/chapter/Parser.test.js +25 -0
- package/models/chapter/Position.d.ts +2 -0
- package/models/chapter/Position.js +1 -0
- package/models/chapter/QuoteNode.d.ts +25 -0
- package/models/chapter/QuoteNode.js +61 -0
- package/models/chapter/ReferenceNode.d.ts +29 -0
- package/models/chapter/ReferenceNode.js +48 -0
- package/models/chapter/RootNode.d.ts +5 -0
- package/models/chapter/RootNode.js +1 -0
- package/models/chapter/RuleNode.d.ts +15 -0
- package/models/chapter/RuleNode.js +16 -0
- package/models/chapter/TableNode.d.ts +35 -0
- package/models/chapter/TableNode.js +176 -0
- package/models/chapter/TableNode.test.d.ts +1 -0
- package/models/chapter/TableNode.test.js +31 -0
- package/models/chapter/TextNode.d.ts +36 -0
- package/models/chapter/TextNode.js +136 -0
- package/models/chapter/TextNode.test.d.ts +1 -0
- package/models/chapter/TextNode.test.js +6 -0
- package/models/loadEdition.d.ts +5 -0
- package/models/loadEdition.js +16 -0
- package/package.json +189 -0
- package/util/getSubdomain.d.ts +3 -0
- package/util/getSubdomain.js +22 -0
- package/util/isMobile.d.ts +2 -0
- package/util/isMobile.js +6 -0
- package/util/mineReference.d.ts +3 -0
- package/util/mineReference.js +62 -0
- package/util/scrollToEyeLevel.d.ts +2 -0
- package/util/scrollToEyeLevel.js +5 -0
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import BlockNode from "./BlockNode";
|
|
2
|
+
import ListNode from "./ListNode";
|
|
3
|
+
import { caretToIndex, indexToCaret } from "./Caret";
|
|
4
|
+
import ParagraphNode from "./ParagraphNode";
|
|
5
|
+
import FormatNode from "./FormatNode";
|
|
6
|
+
import TextNode from "./TextNode";
|
|
7
|
+
import AtomNode from "./AtomNode";
|
|
8
|
+
import CodeNode from "./CodeNode";
|
|
9
|
+
export default class BlocksNode extends BlockNode {
|
|
10
|
+
#blocks;
|
|
11
|
+
constructor(elements) {
|
|
12
|
+
super();
|
|
13
|
+
// Always ensure there's an empty paragraph before and after non-paragraph nodes to allow for insertions.
|
|
14
|
+
const adjustedElements = [];
|
|
15
|
+
for (let i = 0; i < elements.length; i++) {
|
|
16
|
+
const block = elements[i];
|
|
17
|
+
const previousBlock = i === 0 ? undefined : elements[i - 1];
|
|
18
|
+
// If this block and the previous one are lists of the same kind, merge them.
|
|
19
|
+
if (block instanceof ListNode && previousBlock instanceof ListNode && block.isNumbered() === previousBlock.isNumbered()) {
|
|
20
|
+
adjustedElements[adjustedElements.length - 1] = previousBlock.withListAppended(block);
|
|
21
|
+
}
|
|
22
|
+
// If the block isn't a paragraph and the last one isn't either or this is the first one., put a paragraph between them to allow for insertion.
|
|
23
|
+
else {
|
|
24
|
+
if (!(block instanceof ParagraphNode) && (i === 0 || !(previousBlock instanceof ParagraphNode)))
|
|
25
|
+
adjustedElements.push(new ParagraphNode());
|
|
26
|
+
adjustedElements.push(block);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Check the last one.
|
|
30
|
+
if (!(adjustedElements[adjustedElements.length - 1] instanceof ParagraphNode))
|
|
31
|
+
adjustedElements.push(new ParagraphNode());
|
|
32
|
+
this.#blocks = adjustedElements;
|
|
33
|
+
}
|
|
34
|
+
getBlocks() { return this.#blocks; }
|
|
35
|
+
getFormats() { return this.getBlocks().reduce((prev, current) => prev.concat(current.getFormats()), []); }
|
|
36
|
+
getIndexOf(block) {
|
|
37
|
+
const index = this.#blocks.indexOf(block);
|
|
38
|
+
return index < 0 ? undefined : index;
|
|
39
|
+
}
|
|
40
|
+
getBlockBefore(anchor) {
|
|
41
|
+
const index = this.#blocks.indexOf(anchor);
|
|
42
|
+
if (index <= 0)
|
|
43
|
+
return undefined;
|
|
44
|
+
return this.#blocks[index - 1];
|
|
45
|
+
}
|
|
46
|
+
getBlockAfter(anchor) {
|
|
47
|
+
const index = this.#blocks.indexOf(anchor);
|
|
48
|
+
if (index < 0 || index > this.#blocks.length - 2)
|
|
49
|
+
return undefined;
|
|
50
|
+
return this.#blocks[index + 1];
|
|
51
|
+
}
|
|
52
|
+
getBlocksBetween(first, last) {
|
|
53
|
+
const firstIndex = this.getIndexOf(first);
|
|
54
|
+
const lastIndex = this.getIndexOf(last);
|
|
55
|
+
if (firstIndex !== undefined && lastIndex !== undefined) {
|
|
56
|
+
// Swap to be in order.
|
|
57
|
+
if (firstIndex > lastIndex) {
|
|
58
|
+
const temp = first;
|
|
59
|
+
first = last;
|
|
60
|
+
last = temp;
|
|
61
|
+
}
|
|
62
|
+
const blocks = [];
|
|
63
|
+
let inside = false;
|
|
64
|
+
for (let i = 0; i < this.#blocks.length; i++) {
|
|
65
|
+
let block = this.#blocks[i];
|
|
66
|
+
if (block === first)
|
|
67
|
+
inside = true;
|
|
68
|
+
if (inside)
|
|
69
|
+
blocks.push(block);
|
|
70
|
+
if (block === last)
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
return blocks;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
getBlocksInRange(range) {
|
|
78
|
+
// Find the start and end block the carets reside in. Bail on fail.
|
|
79
|
+
const startBlock = this.getBlockOfCaret(range.start);
|
|
80
|
+
const endBlock = range.end === range.start ? startBlock : this.getBlockOfCaret(range.end);
|
|
81
|
+
if (startBlock === undefined || endBlock === undefined)
|
|
82
|
+
return;
|
|
83
|
+
// Find all of the blocks between the start and end blocks.
|
|
84
|
+
// Start tracking when we hit the start node. Stop tracking when we hit the end node.
|
|
85
|
+
const blocksToEdit = [];
|
|
86
|
+
let insideSelection = false;
|
|
87
|
+
this.getNodes().filter(n => n instanceof BlockNode).forEach(node => {
|
|
88
|
+
if (node === startBlock)
|
|
89
|
+
insideSelection = true;
|
|
90
|
+
if (insideSelection && node instanceof BlockNode)
|
|
91
|
+
blocksToEdit.push(node);
|
|
92
|
+
if (insideSelection && node === endBlock)
|
|
93
|
+
insideSelection = false;
|
|
94
|
+
});
|
|
95
|
+
return blocksToEdit;
|
|
96
|
+
}
|
|
97
|
+
getBlockOfCaret(caret) {
|
|
98
|
+
return caret.node.getClosestParentOfType(this, BlockNode);
|
|
99
|
+
}
|
|
100
|
+
getParentOf(node) {
|
|
101
|
+
return this.#blocks.map(b => b === node ? this : b.getParentOf(node)).find(b => b !== undefined);
|
|
102
|
+
}
|
|
103
|
+
getTextNodes() {
|
|
104
|
+
return this.getNodes().filter(node => node instanceof TextNode);
|
|
105
|
+
}
|
|
106
|
+
getAtomNodes() {
|
|
107
|
+
return this.getNodes().filter(node => node instanceof AtomNode);
|
|
108
|
+
}
|
|
109
|
+
getTextAndAtomNodes() {
|
|
110
|
+
return this.getNodes().filter(node => node instanceof TextNode || node instanceof AtomNode);
|
|
111
|
+
}
|
|
112
|
+
getIndexOfTextNode(node) {
|
|
113
|
+
const text = this.getTextNodes();
|
|
114
|
+
return text.indexOf(node);
|
|
115
|
+
}
|
|
116
|
+
getFirstCaret() {
|
|
117
|
+
const text = this.getTextNodes();
|
|
118
|
+
return text.length === 0 ? undefined : { node: text[0], index: 0 };
|
|
119
|
+
}
|
|
120
|
+
getLastCaret() {
|
|
121
|
+
const text = this.getTextNodes();
|
|
122
|
+
return text.length === 0 ? undefined : { node: text[text.length - 1], index: text[text.length - 1].getLength() };
|
|
123
|
+
}
|
|
124
|
+
getAdjacentCaret(caret, next) {
|
|
125
|
+
if (!this.contains(caret.node))
|
|
126
|
+
return;
|
|
127
|
+
// Navigate from code node.
|
|
128
|
+
const code = caret.node.getFarthestParentMatching(this, n => n instanceof CodeNode);
|
|
129
|
+
if (code && caret.node === code.getCodeNode()) {
|
|
130
|
+
// Get the adjacent text node.
|
|
131
|
+
const newCaret = next ? caret.node.getNextCaret(caret.index) : caret.node.getPreviousCaret(caret.index);
|
|
132
|
+
if (newCaret !== undefined)
|
|
133
|
+
return newCaret;
|
|
134
|
+
// Otherwise, get the adjacent format's first/last caret.
|
|
135
|
+
if (next)
|
|
136
|
+
return code.getCaption().getFirstCaret();
|
|
137
|
+
// If there's no adjacent caret, find the adjacent root format.
|
|
138
|
+
const blocks = this.getNodes().filter(n => n instanceof BlockNode);
|
|
139
|
+
const codeIndex = blocks.indexOf(code);
|
|
140
|
+
if (codeIndex <= 0)
|
|
141
|
+
return;
|
|
142
|
+
const adjacentBlock = blocks[codeIndex - 1];
|
|
143
|
+
const adjacentFormats = adjacentBlock.getFormats();
|
|
144
|
+
const adjacentFormat = adjacentFormats[adjacentFormats.length - 1];
|
|
145
|
+
return adjacentFormat.getLastCaret();
|
|
146
|
+
}
|
|
147
|
+
const format = caret.node.getFarthestParentMatching(this, n => n instanceof FormatNode);
|
|
148
|
+
if (format === undefined)
|
|
149
|
+
return;
|
|
150
|
+
// Find the format that contains the caret and return the adjacent caret if there is one.
|
|
151
|
+
const adjacentCaretInFormat = format.getAdjacentCaret(caret, next);
|
|
152
|
+
if (adjacentCaretInFormat !== undefined)
|
|
153
|
+
return adjacentCaretInFormat;
|
|
154
|
+
// If there's no adjacent caret in the root format, find the adjacent format or code.
|
|
155
|
+
const formats = this.getNodes().filter(n => {
|
|
156
|
+
const parent = n.getParent(this);
|
|
157
|
+
return (n instanceof TextNode && parent instanceof CodeNode) || (n instanceof FormatNode && !(parent instanceof AtomNode) && !(parent instanceof FormatNode));
|
|
158
|
+
});
|
|
159
|
+
const index = formats.indexOf(format);
|
|
160
|
+
const adjacentFormat = index < 0 || formats.length === 0 ? undefined : formats[index + (next ? 1 : -1)];
|
|
161
|
+
if (adjacentFormat === undefined)
|
|
162
|
+
return;
|
|
163
|
+
return next ? adjacentFormat.getFirstCaret() : adjacentFormat.getLastCaret();
|
|
164
|
+
}
|
|
165
|
+
getSelectedFormatRange(format, range) {
|
|
166
|
+
// Find all of the formats.
|
|
167
|
+
const formats = this.getNodes().filter(n => n instanceof FormatNode && !(this.getParentOf(n) instanceof FormatNode));
|
|
168
|
+
// Find the given format index.
|
|
169
|
+
const formatIndex = formats.indexOf(format);
|
|
170
|
+
if (formatIndex < 0)
|
|
171
|
+
return undefined;
|
|
172
|
+
// Find the range start and end formats.
|
|
173
|
+
const startFormat = formats.find(f => f.contains(range.start.node));
|
|
174
|
+
const endFormat = formats.find(f => f.contains(range.end.node));
|
|
175
|
+
if (startFormat === undefined || endFormat === undefined)
|
|
176
|
+
return undefined;
|
|
177
|
+
const startIndex = formats.indexOf(startFormat);
|
|
178
|
+
const endIndex = formats.indexOf(endFormat);
|
|
179
|
+
// No range if out of bounds.
|
|
180
|
+
if (startIndex < 0 || endIndex < 0)
|
|
181
|
+
return undefined;
|
|
182
|
+
if (formatIndex < startIndex || formatIndex > endIndex)
|
|
183
|
+
return undefined;
|
|
184
|
+
// Choose the portion of the format that is selected.
|
|
185
|
+
const startCaret = formatIndex === startIndex ? range.start : format.getFirstCaret();
|
|
186
|
+
const endCaret = formatIndex === endIndex ? range.end : format.getLastCaret();
|
|
187
|
+
if (startCaret === undefined || endCaret === undefined)
|
|
188
|
+
return;
|
|
189
|
+
return { start: startCaret, end: endCaret };
|
|
190
|
+
}
|
|
191
|
+
withContentInRange(range) {
|
|
192
|
+
const containsStart = this.contains(range.start.node);
|
|
193
|
+
const containsEnd = this.contains(range.end.node);
|
|
194
|
+
if (!containsStart && !containsEnd)
|
|
195
|
+
return this.copy();
|
|
196
|
+
let foundStart = false;
|
|
197
|
+
let foundEnd = false;
|
|
198
|
+
const newBlocks = [];
|
|
199
|
+
for (let i = 0; i < this.#blocks.length; i++) {
|
|
200
|
+
if (this.#blocks[i].contains(range.start.node))
|
|
201
|
+
foundStart = true;
|
|
202
|
+
if (((containsStart && foundStart) || !containsStart) &&
|
|
203
|
+
((containsEnd && !foundEnd) || !containsEnd)) {
|
|
204
|
+
const newBlock = this.#blocks[i].withContentInRange(range);
|
|
205
|
+
if (newBlock === undefined)
|
|
206
|
+
return;
|
|
207
|
+
newBlocks.push(newBlock);
|
|
208
|
+
}
|
|
209
|
+
if (this.#blocks[i].contains(range.end.node))
|
|
210
|
+
foundEnd = true;
|
|
211
|
+
}
|
|
212
|
+
return this.create(newBlocks);
|
|
213
|
+
}
|
|
214
|
+
withBlockInserted(anchor, block, before) {
|
|
215
|
+
const index = this.#blocks.indexOf(anchor);
|
|
216
|
+
if (index < 0)
|
|
217
|
+
return;
|
|
218
|
+
const newBlocks = this.#blocks.slice();
|
|
219
|
+
newBlocks.splice(index + (before ? 0 : 1), 0, block);
|
|
220
|
+
return this.create(newBlocks);
|
|
221
|
+
}
|
|
222
|
+
withBlockAppended(block) {
|
|
223
|
+
let newBlocks = this.#blocks.slice();
|
|
224
|
+
newBlocks.push(block);
|
|
225
|
+
return this.create(newBlocks);
|
|
226
|
+
}
|
|
227
|
+
withoutBlock(block) {
|
|
228
|
+
return this.create(this.#blocks.filter(n => n !== block));
|
|
229
|
+
}
|
|
230
|
+
withBlockInsertedBefore(anchor, block) {
|
|
231
|
+
return this.withBlockInserted(anchor, block, true);
|
|
232
|
+
}
|
|
233
|
+
withBlockInsertedAfter(anchor, block) {
|
|
234
|
+
return this.withBlockInserted(anchor, block, false);
|
|
235
|
+
}
|
|
236
|
+
withSelectionSplit(range) {
|
|
237
|
+
let caret = range.start;
|
|
238
|
+
let blocks = this;
|
|
239
|
+
// If there's a selection, remove it before inserting.
|
|
240
|
+
if (range.start.node !== range.end.node || range.start.index !== range.end.index) {
|
|
241
|
+
const edit = blocks.withoutRange(range);
|
|
242
|
+
if (edit === undefined || !(edit.root instanceof BlocksNode))
|
|
243
|
+
return;
|
|
244
|
+
blocks = edit.root;
|
|
245
|
+
caret = edit.range.start;
|
|
246
|
+
}
|
|
247
|
+
// Find what paragraph the caret is in.
|
|
248
|
+
// There are some contexts with no paragraphs. Return the start range given.
|
|
249
|
+
const paragraph = caret.node.getClosestParentOfType(blocks, ParagraphNode);
|
|
250
|
+
if (paragraph === undefined)
|
|
251
|
+
return;
|
|
252
|
+
// Split the paragraph in two.
|
|
253
|
+
const split = paragraph.split(caret);
|
|
254
|
+
if (split === undefined)
|
|
255
|
+
return;
|
|
256
|
+
const [first, last] = split;
|
|
257
|
+
const newCaret = last.getFirstCaret();
|
|
258
|
+
if (newCaret === undefined)
|
|
259
|
+
return;
|
|
260
|
+
// Find the paragraph's blocks node.
|
|
261
|
+
const paragraphBlocks = paragraph.getParent(blocks);
|
|
262
|
+
if (!(paragraphBlocks instanceof BlocksNode))
|
|
263
|
+
return;
|
|
264
|
+
const newBlocks = paragraphBlocks
|
|
265
|
+
.withBlockInsertedBefore(paragraph, first)
|
|
266
|
+
?.withBlockInsertedAfter(paragraph, last)
|
|
267
|
+
?.withoutBlock(paragraph);
|
|
268
|
+
if (newBlocks === undefined)
|
|
269
|
+
return;
|
|
270
|
+
// Replace the paragraphs blocks in these blocks.
|
|
271
|
+
const newRoot = blocks.withNodeReplaced(paragraphBlocks, newBlocks);
|
|
272
|
+
if (newRoot === undefined)
|
|
273
|
+
return;
|
|
274
|
+
return { root: newRoot, range: { start: newCaret, end: newCaret } };
|
|
275
|
+
}
|
|
276
|
+
withoutRange(range) {
|
|
277
|
+
return this.withRangeFormatted(range, undefined);
|
|
278
|
+
}
|
|
279
|
+
withoutAtom(atom) {
|
|
280
|
+
// Find the format that contains the atom.
|
|
281
|
+
const format = atom.getFarthestParentMatching(this, p => p instanceof FormatNode);
|
|
282
|
+
if (format === undefined)
|
|
283
|
+
return;
|
|
284
|
+
// Find the text or atom node to the left and right of the atom to determine the new caret position.
|
|
285
|
+
const previous = format.getPreviousTextOrAtom(atom);
|
|
286
|
+
const next = format.getNextTextOrAtom(atom);
|
|
287
|
+
const newText = new TextNode();
|
|
288
|
+
// If the format is otherwise empty, replace the atom with an empty text node
|
|
289
|
+
const newFormat = previous === undefined && next == undefined ?
|
|
290
|
+
format.withSegmentReplaced(atom, newText) :
|
|
291
|
+
format.withoutSegment(atom);
|
|
292
|
+
// New caret is left, or right, or empty text.
|
|
293
|
+
const newCaret = { node: previous ? previous : next ? next : newText, index: previous && previous instanceof TextNode ? previous.getLength() : 0 };
|
|
294
|
+
// No format? Fail.
|
|
295
|
+
if (newFormat === undefined)
|
|
296
|
+
return;
|
|
297
|
+
// Create a new chapter with the revised format.
|
|
298
|
+
const newRoot = this.withNodeReplaced(format, newFormat);
|
|
299
|
+
if (newRoot === undefined)
|
|
300
|
+
return;
|
|
301
|
+
// Return the edited tree!
|
|
302
|
+
return { root: newRoot, range: { start: newCaret, end: newCaret } };
|
|
303
|
+
}
|
|
304
|
+
withRangeFormatted(range, format) {
|
|
305
|
+
// Only works on text nodes, atom nodes, and metadata nodes.
|
|
306
|
+
if (!(range.start.node instanceof TextNode || range.start.node instanceof AtomNode) ||
|
|
307
|
+
!(range.end.node instanceof TextNode || range.end.node instanceof AtomNode))
|
|
308
|
+
return;
|
|
309
|
+
// Have we selected a single atom node for removal? Remove it and place the caret in the adjacent word.
|
|
310
|
+
if (format === undefined && (range.start.node instanceof AtomNode && range.end.node instanceof AtomNode && range.start.node === range.end.node)) {
|
|
311
|
+
return this.withoutAtom(range.start.node);
|
|
312
|
+
}
|
|
313
|
+
// Preserve the original range, since there are a few cases where we adjust it.
|
|
314
|
+
let adjustedRange = range;
|
|
315
|
+
// If this a single point in format, and we're clearing formatting, adjust the selection to the whole format.
|
|
316
|
+
if (format === "" && range.start.node === range.end.node && range.start.index === range.end.index) {
|
|
317
|
+
const selection = range.start.node.getFormatRoot(this)?.getSelection();
|
|
318
|
+
if (selection === undefined)
|
|
319
|
+
return;
|
|
320
|
+
adjustedRange = selection;
|
|
321
|
+
}
|
|
322
|
+
// Sort the range if it's out of order, since the algorithm below assumes that it's ordered.
|
|
323
|
+
const sortedRange = this.sortRange(adjustedRange);
|
|
324
|
+
// Remember the start and end text index so we can map it back after editing.
|
|
325
|
+
const startTextIndex = caretToIndex(this, sortedRange.start);
|
|
326
|
+
const endTextIndex = caretToIndex(this, sortedRange.end);
|
|
327
|
+
if (startTextIndex === undefined || endTextIndex === undefined)
|
|
328
|
+
return;
|
|
329
|
+
// Ask all of the blocks to format themselves, replacing them as we do.
|
|
330
|
+
let newBlocks = this;
|
|
331
|
+
let formattedBlocks = [];
|
|
332
|
+
let inRange = false;
|
|
333
|
+
for (let i = 0; i < this.#blocks.length; i++) {
|
|
334
|
+
const block = this.#blocks[i];
|
|
335
|
+
const containsStart = block.contains(sortedRange.start.node);
|
|
336
|
+
const containsEnd = block.contains(sortedRange.end.node);
|
|
337
|
+
if (containsStart || containsEnd || inRange) {
|
|
338
|
+
inRange = true;
|
|
339
|
+
// Compute a proper range based on containment.
|
|
340
|
+
const blockStart = containsStart ? sortedRange.start : block.getFirstCaret();
|
|
341
|
+
const blockEnd = containsEnd ? sortedRange.end : block.getLastCaret();
|
|
342
|
+
if (blockStart === undefined || blockEnd === undefined)
|
|
343
|
+
return;
|
|
344
|
+
// If we're just going to delete it, then don't bother formatting it.
|
|
345
|
+
if (!containsStart && !containsEnd && format === undefined) {
|
|
346
|
+
newBlocks = newBlocks.withChildReplaced(block, undefined);
|
|
347
|
+
if (newBlocks === undefined)
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
// Otherwise, format it.
|
|
351
|
+
else {
|
|
352
|
+
const edit = block.withRangeFormatted({ start: blockStart, end: blockEnd }, format);
|
|
353
|
+
// If we failed to format it, remove it.
|
|
354
|
+
if (edit === undefined)
|
|
355
|
+
return;
|
|
356
|
+
// If we succeeded in formatting it, replace it, otherwise skip it.
|
|
357
|
+
else {
|
|
358
|
+
const newBlock = edit.root;
|
|
359
|
+
// Remember non-empty revised blocks so we can merge things after.
|
|
360
|
+
if (!newBlock.isEmpty())
|
|
361
|
+
formattedBlocks.push(newBlock);
|
|
362
|
+
// Replace the block, or delete it if it's empty.
|
|
363
|
+
newBlocks = newBlocks.withChildReplaced(block, newBlock.isEmpty() ? undefined : newBlock);
|
|
364
|
+
if (newBlocks === undefined)
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (block.contains(sortedRange.end.node))
|
|
370
|
+
inRange = false;
|
|
371
|
+
}
|
|
372
|
+
// If we're deleting and there are two non-empty paragraphs left, merge them.
|
|
373
|
+
if (format === undefined && formattedBlocks.length >= 2) {
|
|
374
|
+
const first = formattedBlocks[0];
|
|
375
|
+
const last = formattedBlocks[formattedBlocks.length - 1];
|
|
376
|
+
if (first instanceof ParagraphNode && last instanceof ParagraphNode) {
|
|
377
|
+
// Replace the first paragraph with the second merged.
|
|
378
|
+
newBlocks = newBlocks.withNodeReplaced(first, first.withContent(first.getFormat().withSegmentsAppended(last.getFormat())));
|
|
379
|
+
if (newBlocks === undefined)
|
|
380
|
+
return;
|
|
381
|
+
// Remove the last paragraph.
|
|
382
|
+
newBlocks = newBlocks.withNodeReplaced(last, undefined);
|
|
383
|
+
if (newBlocks === undefined)
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const startCaret = indexToCaret(newBlocks, startTextIndex);
|
|
388
|
+
// If we deleted, then the end caret should be the same as the start.
|
|
389
|
+
const endCaret = format === undefined ? startCaret : indexToCaret(newBlocks, endTextIndex);
|
|
390
|
+
if (startCaret === undefined || endCaret === undefined)
|
|
391
|
+
return;
|
|
392
|
+
// Return the new root and the start and end of the range.
|
|
393
|
+
return { root: newBlocks, range: { start: startCaret, end: endCaret } };
|
|
394
|
+
}
|
|
395
|
+
withSegmentAtSelection(range, nodeCreator) {
|
|
396
|
+
let blocks = this;
|
|
397
|
+
let caret = range.start;
|
|
398
|
+
// Only works at a text node.
|
|
399
|
+
if (!(caret.node instanceof TextNode))
|
|
400
|
+
return;
|
|
401
|
+
// Get the nearest FormatNode parent of the selected text. Bail on fail.
|
|
402
|
+
const formatted = caret.node.getClosestParentOfType(blocks, FormatNode);
|
|
403
|
+
if (formatted === undefined)
|
|
404
|
+
return;
|
|
405
|
+
// Update the format and replace it in the blocks.
|
|
406
|
+
const edit = formatted.withSegmentAtSelection(range, nodeCreator);
|
|
407
|
+
if (edit === undefined || !(edit.root instanceof FormatNode))
|
|
408
|
+
return;
|
|
409
|
+
const newBlocks = blocks.withNodeReplaced(formatted, edit.root);
|
|
410
|
+
if (newBlocks === undefined)
|
|
411
|
+
return;
|
|
412
|
+
return { root: newBlocks, range: edit.range };
|
|
413
|
+
}
|
|
414
|
+
withParagraphsAsLists(range, numbered) {
|
|
415
|
+
const sortedRange = this.sortRange(range);
|
|
416
|
+
const blocksInRange = this.getBlocksInRange(sortedRange);
|
|
417
|
+
if (blocksInRange === undefined)
|
|
418
|
+
return;
|
|
419
|
+
// Only format if every block selected is a paragraph node. Non-paragraphs can't be in lists.
|
|
420
|
+
if (!blocksInRange.every(b => b instanceof ParagraphNode))
|
|
421
|
+
return;
|
|
422
|
+
// Split the blocks into groups with shared parents.
|
|
423
|
+
const sequences = [[]];
|
|
424
|
+
let previous = undefined;
|
|
425
|
+
blocksInRange.forEach(block => {
|
|
426
|
+
if (previous === undefined || block.getParent(this) === previous.getParent(this))
|
|
427
|
+
sequences[sequences.length - 1].push(block);
|
|
428
|
+
else {
|
|
429
|
+
sequences.push([block]);
|
|
430
|
+
}
|
|
431
|
+
previous = block;
|
|
432
|
+
});
|
|
433
|
+
// For each sequence, create a list of all of the paragraphs in it.
|
|
434
|
+
let newBlocks = this;
|
|
435
|
+
for (let i = 0; i < sequences.length; i++) {
|
|
436
|
+
const sequence = sequences[i];
|
|
437
|
+
// Make the new list
|
|
438
|
+
const newList = new ListNode(sequence.map(p => p.getFormat()), numbered);
|
|
439
|
+
// Insert the new list
|
|
440
|
+
const blocksParent = sequence[0].getParent(newBlocks);
|
|
441
|
+
if (blocksParent === undefined)
|
|
442
|
+
return;
|
|
443
|
+
let newSequenceBlocks = blocksParent.withBlockInsertedBefore(sequence[0], newList);
|
|
444
|
+
// Remove all of the old paragraphs
|
|
445
|
+
for (let j = 0; j < sequence.length; j++) {
|
|
446
|
+
newSequenceBlocks = newSequenceBlocks?.withoutBlock(sequence[j]);
|
|
447
|
+
if (newSequenceBlocks === undefined)
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
// Replace the blocks parent with the new blocks.
|
|
451
|
+
const newRoot = newBlocks.withNodeReplaced(blocksParent, newSequenceBlocks);
|
|
452
|
+
if (newRoot === undefined || !(newRoot instanceof BlocksNode))
|
|
453
|
+
return;
|
|
454
|
+
newBlocks = newRoot;
|
|
455
|
+
}
|
|
456
|
+
return newBlocks;
|
|
457
|
+
}
|
|
458
|
+
withListsAsParagraphs(range) {
|
|
459
|
+
let newBlocks = this;
|
|
460
|
+
const sortedRange = this.sortRange(range);
|
|
461
|
+
const blocksInRange = this.getBlocksInRange(sortedRange);
|
|
462
|
+
if (blocksInRange === undefined)
|
|
463
|
+
return;
|
|
464
|
+
// Convert all of the lists into their root lists, since we only convert entire lists, not parts of lists.
|
|
465
|
+
const listsInRange = blocksInRange.filter(l => l instanceof ListNode);
|
|
466
|
+
const listRootsInRange = [...new Set(listsInRange.map(l => l.getParent(this) instanceof ListNode ? l.getFarthestParentMatching(this, m => m instanceof ListNode) : l).filter(n => n !== undefined))];
|
|
467
|
+
if (listRootsInRange.length === 0)
|
|
468
|
+
return undefined;
|
|
469
|
+
// For each list in the range, convert all items in range to paragraphs.
|
|
470
|
+
for (let i = 0; i < listRootsInRange.length; i++) {
|
|
471
|
+
const list = listRootsInRange[i];
|
|
472
|
+
const paragraphs = list.getNodes().filter(n => n instanceof FormatNode).map(f => new ParagraphNode(0, f));
|
|
473
|
+
// Insert in reverse order to keep them in order, since the list is our anchor.
|
|
474
|
+
for (let j = paragraphs.length - 1; j >= 0; j--) {
|
|
475
|
+
const paragraph = paragraphs[j];
|
|
476
|
+
const insertedBlocks = newBlocks.withBlockInsertedAfter(list, paragraph);
|
|
477
|
+
if (insertedBlocks === undefined)
|
|
478
|
+
return;
|
|
479
|
+
newBlocks = insertedBlocks;
|
|
480
|
+
}
|
|
481
|
+
const withoutList = newBlocks.withoutBlock(list);
|
|
482
|
+
if (withoutList === undefined)
|
|
483
|
+
return;
|
|
484
|
+
newBlocks = withoutList;
|
|
485
|
+
}
|
|
486
|
+
return newBlocks;
|
|
487
|
+
}
|
|
488
|
+
withListsIndented(range, indent) {
|
|
489
|
+
let newBlocks = this;
|
|
490
|
+
// Find all of the formats in list nodes in the range and indent them.
|
|
491
|
+
const ancestor = range.start.node.getCommonAncestor(newBlocks, range.end.node);
|
|
492
|
+
const nodes = ancestor?.getNodes();
|
|
493
|
+
const formats = nodes?.filter(n => n instanceof FormatNode && newBlocks.getParentOf(n) instanceof ListNode);
|
|
494
|
+
const startIndex = nodes?.indexOf(range.start.node);
|
|
495
|
+
const endIndex = nodes?.indexOf(range.end.node);
|
|
496
|
+
if (formats === undefined || startIndex === undefined || endIndex === undefined)
|
|
497
|
+
return;
|
|
498
|
+
// Loop through the formats in the range and indent or dedent them, constructing a new root.
|
|
499
|
+
const first = startIndex < endIndex ? range.start.node : range.end.node;
|
|
500
|
+
const last = startIndex < endIndex ? range.end.node : range.start.node;
|
|
501
|
+
let inside = false;
|
|
502
|
+
for (let i = 0; i < formats.length; i++) {
|
|
503
|
+
const format = formats[i];
|
|
504
|
+
if (first.hasAncestor(newBlocks, format))
|
|
505
|
+
inside = true;
|
|
506
|
+
// If we're in the selection and the format is in a list, restructure it's list to indent/unindent it.
|
|
507
|
+
if (inside) {
|
|
508
|
+
// Find the root list node of the format.
|
|
509
|
+
const list = format.getFarthestParentMatching(newBlocks, n => n instanceof ListNode);
|
|
510
|
+
if (list instanceof ListNode) {
|
|
511
|
+
const newList = indent ? list.withItemIndented(format) : list.withItemUnindented(format);
|
|
512
|
+
if (newList === undefined)
|
|
513
|
+
return;
|
|
514
|
+
const blocksWithIndent = newBlocks.withChildReplaced(list, newList);
|
|
515
|
+
if (blocksWithIndent === undefined)
|
|
516
|
+
return;
|
|
517
|
+
newBlocks = blocksWithIndent;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (last.hasAncestor(newBlocks, format))
|
|
521
|
+
inside = false;
|
|
522
|
+
}
|
|
523
|
+
// We shouldn't need to update the range because we haven't modified the text nodes, just their position.
|
|
524
|
+
return newBlocks;
|
|
525
|
+
}
|
|
526
|
+
// This accounts for adjascent lists that end up with the same style.
|
|
527
|
+
withListAsStyle(list, numbered) {
|
|
528
|
+
return this.withNodeReplaced(list, list.withStyle(numbered));
|
|
529
|
+
}
|
|
530
|
+
// Removes the character before or after the current caret
|
|
531
|
+
withoutAdjacentContent(caret, next) {
|
|
532
|
+
// Confirm that this caret is in this tree. Bail on fail.
|
|
533
|
+
if (!this.contains(caret.node))
|
|
534
|
+
return;
|
|
535
|
+
// Find the parents of the node.
|
|
536
|
+
const parents = this.getParentsOf(caret.node);
|
|
537
|
+
while (parents && parents.length > 0) {
|
|
538
|
+
const parent = parents.pop();
|
|
539
|
+
// Ask the format node it's in to edit it, and return if successful.
|
|
540
|
+
if (parent instanceof FormatNode) {
|
|
541
|
+
const editedFormat = parent.withoutCharacter(caret, next);
|
|
542
|
+
// If it failed, let a parent try. Otherwise, just update the format.
|
|
543
|
+
if (editedFormat !== undefined) {
|
|
544
|
+
const newBlocks = this.withNodeReplaced(parent, editedFormat.root);
|
|
545
|
+
if (newBlocks === undefined)
|
|
546
|
+
return;
|
|
547
|
+
return { root: newBlocks, range: editedFormat.range };
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// If it's a format in the list node, merge with the previous list item
|
|
551
|
+
else if (parent instanceof ListNode) {
|
|
552
|
+
const index = parent.getItemContaining(caret);
|
|
553
|
+
if (index === undefined)
|
|
554
|
+
break;
|
|
555
|
+
// Merge the item backwards.
|
|
556
|
+
if (parent.getLength() > 1) {
|
|
557
|
+
const edit = parent.withItemMergedBackwards(index);
|
|
558
|
+
if (edit === undefined)
|
|
559
|
+
return;
|
|
560
|
+
const [newList, newCaret] = edit;
|
|
561
|
+
const newBlocks = this.withNodeReplaced(parent, newList);
|
|
562
|
+
if (newBlocks === undefined)
|
|
563
|
+
return;
|
|
564
|
+
return { root: newBlocks, range: { start: newCaret, end: newCaret } };
|
|
565
|
+
}
|
|
566
|
+
// Convert the remaining list item into a paragraph.
|
|
567
|
+
else if (parents.length > 0 && parents[parents.length - 1] instanceof BlocksNode) {
|
|
568
|
+
const item = parent.getItem(0);
|
|
569
|
+
if (!(item instanceof FormatNode))
|
|
570
|
+
return;
|
|
571
|
+
const newParagraph = new ParagraphNode(0, item);
|
|
572
|
+
const newBlocks = this.withNodeReplaced(parent, newParagraph);
|
|
573
|
+
const newCaret = newParagraph.getFirstCaret();
|
|
574
|
+
if (newBlocks === undefined || newCaret === undefined)
|
|
575
|
+
return;
|
|
576
|
+
return { root: newBlocks, range: { start: newCaret, end: newCaret } };
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// If it's at the beginning of the paragraph, and there's a previous paragraph, merge with the previous paragraph.
|
|
580
|
+
else if (parent instanceof ParagraphNode) {
|
|
581
|
+
const firstCaret = next ? parent.getLastCaret() : parent.getFirstCaret();
|
|
582
|
+
if (firstCaret === undefined)
|
|
583
|
+
return;
|
|
584
|
+
const blocks = parents[parents.length - 1];
|
|
585
|
+
const adjacentBlock = next ? blocks.getBlockAfter(parent) : blocks.getBlockBefore(parent);
|
|
586
|
+
// Is this the first/last caret of the paragraph and there's an adjascent block? Try deleting something.
|
|
587
|
+
if (firstCaret.node === caret.node && firstCaret.index === caret.index && adjacentBlock !== undefined) {
|
|
588
|
+
// If the block adjacent this paragraph is a paragraph, merge the paragraph into the adjacent paragraph.
|
|
589
|
+
if (adjacentBlock instanceof ParagraphNode) {
|
|
590
|
+
const currentCaret = next ? parent.getLastCaret() : adjacentBlock.getLastCaret();
|
|
591
|
+
if (currentCaret === undefined)
|
|
592
|
+
return;
|
|
593
|
+
const textIndex = next ? caretToIndex(parent.getFormat(), currentCaret) : caretToIndex(adjacentBlock.getFormat(), currentCaret);
|
|
594
|
+
if (textIndex === undefined)
|
|
595
|
+
return;
|
|
596
|
+
const mergedParagraph = next ? adjacentBlock.withParagraphPrepended(parent) : adjacentBlock.withParagraphAppended(parent);
|
|
597
|
+
if (mergedParagraph === undefined)
|
|
598
|
+
return;
|
|
599
|
+
const newBlocks = this.withNodeReplaced(blocks, blocks.withChildReplaced(adjacentBlock, mergedParagraph)?.withoutBlock(parent));
|
|
600
|
+
if (newBlocks === undefined)
|
|
601
|
+
return;
|
|
602
|
+
const newCaret = indexToCaret(mergedParagraph.getFormat(), textIndex);
|
|
603
|
+
if (newCaret === undefined)
|
|
604
|
+
return;
|
|
605
|
+
return { root: newBlocks, range: { start: newCaret, end: newCaret } };
|
|
606
|
+
}
|
|
607
|
+
// If the block adjacent is a list node, merge the paragraph to the list's first/last item.
|
|
608
|
+
else if (adjacentBlock instanceof ListNode) {
|
|
609
|
+
// If we're going forwards, put the first list item into the paragraph.
|
|
610
|
+
if (next) {
|
|
611
|
+
const format = adjacentBlock.getFirstItem();
|
|
612
|
+
if (format !== undefined) {
|
|
613
|
+
const newParagraph = parent.withContent(parent.getFormat().withSegmentsAppended(format));
|
|
614
|
+
const newBlocks = blocks.withNodeReplaced(parent, newParagraph)?.withNodeReplaced(format, undefined);
|
|
615
|
+
if (newBlocks === undefined)
|
|
616
|
+
return;
|
|
617
|
+
return { root: newBlocks, range: { start: caret, end: caret } };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// If we're going backwards, get the last format of the list and insert the paragraph's content into it,
|
|
621
|
+
// then remove the paragraph from the blocks.
|
|
622
|
+
else {
|
|
623
|
+
const format = adjacentBlock.getLastItem();
|
|
624
|
+
if (format !== undefined) {
|
|
625
|
+
const newFormat = format.withSegmentAppended(parent.getFormat());
|
|
626
|
+
const newBlocks = blocks.withNodeReplaced(format, newFormat)?.withoutBlock(parent);
|
|
627
|
+
const newCaret = parent.getFormat().getFirstCaret();
|
|
628
|
+
if (newBlocks === undefined || newCaret === undefined)
|
|
629
|
+
return;
|
|
630
|
+
return { root: newBlocks, range: { start: newCaret, end: newCaret } };
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
// Otherwise, return a root without the previous block, deleting it.
|
|
635
|
+
else {
|
|
636
|
+
const newBlocks = this.withNodeReplaced(blocks, blocks.withoutBlock(adjacentBlock));
|
|
637
|
+
return newBlocks === undefined ? undefined : { root: newBlocks, range: { start: caret, end: caret } };
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
// No parent could handle it. Fail!
|
|
643
|
+
}
|
|
644
|
+
withNodeInserted(caret, node) {
|
|
645
|
+
// The caret's node must be in this tree.
|
|
646
|
+
if (!this.contains(caret.node))
|
|
647
|
+
return;
|
|
648
|
+
// Get the blocks to insert.
|
|
649
|
+
const blocks = node instanceof BlocksNode ? node.getBlocks() : node instanceof BlockNode ? [node] : [];
|
|
650
|
+
// If there are none, do nothing.
|
|
651
|
+
if (blocks.length === 0)
|
|
652
|
+
return;
|
|
653
|
+
// Find the block in this blocks node that contains the caret.
|
|
654
|
+
const anchor = this.#blocks.find(b => b.contains(caret.node));
|
|
655
|
+
if (anchor === undefined)
|
|
656
|
+
return;
|
|
657
|
+
let lastInsert = anchor;
|
|
658
|
+
let newBlocks = this;
|
|
659
|
+
while (blocks.length > 0) {
|
|
660
|
+
// Get the block to insert
|
|
661
|
+
let newBlock = blocks.shift();
|
|
662
|
+
if (newBlock === undefined)
|
|
663
|
+
return;
|
|
664
|
+
// If we're inserting after the anchor and the anchor is a paragraph and the block to insert is a paragraph, merge them.
|
|
665
|
+
if (lastInsert === anchor && anchor instanceof ParagraphNode && newBlock instanceof ParagraphNode) {
|
|
666
|
+
newBlock = anchor.withParagraphAppended(newBlock);
|
|
667
|
+
if (newBlock === undefined)
|
|
668
|
+
return;
|
|
669
|
+
newBlocks = newBlocks.withChildReplaced(anchor, newBlock);
|
|
670
|
+
}
|
|
671
|
+
// If we're inserting after the anchor and the anchor is a list and the block to insert is a list, merge them.
|
|
672
|
+
else if (lastInsert === anchor && anchor instanceof ListNode && newBlock instanceof ListNode) {
|
|
673
|
+
const edit = anchor.withNodeInserted(caret, newBlock);
|
|
674
|
+
if (edit === undefined || !(edit.root instanceof BlockNode))
|
|
675
|
+
return;
|
|
676
|
+
newBlock = edit.root;
|
|
677
|
+
newBlocks = newBlocks.withChildReplaced(anchor, newBlock);
|
|
678
|
+
}
|
|
679
|
+
// Otherwise just insert the block after the last insert, including if it was the anchor block.
|
|
680
|
+
else {
|
|
681
|
+
newBlocks = newBlocks.withBlockInsertedAfter(lastInsert, newBlock);
|
|
682
|
+
}
|
|
683
|
+
// Bail on fail.
|
|
684
|
+
if (newBlocks === undefined)
|
|
685
|
+
return;
|
|
686
|
+
// Remember what we inserted so we can insert after it.
|
|
687
|
+
lastInsert = newBlock;
|
|
688
|
+
}
|
|
689
|
+
// Set the caret to the last caret of the last insert, and if there isn't one, insert an empty paragraph so we can.
|
|
690
|
+
const formats = lastInsert.getFormats();
|
|
691
|
+
let newCaret = undefined;
|
|
692
|
+
if (formats.length === 0) {
|
|
693
|
+
const newParagraph = new ParagraphNode(0, new FormatNode("", [new TextNode()]));
|
|
694
|
+
newCaret = newParagraph.getFirstCaret();
|
|
695
|
+
newBlocks = newBlocks.withBlockInsertedAfter(lastInsert, newParagraph);
|
|
696
|
+
if (newCaret === undefined || newBlocks === undefined)
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
newCaret = formats[formats.length - 1].getLastCaret();
|
|
701
|
+
if (newCaret === undefined)
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
return { root: newBlocks, range: { start: newCaret, end: newCaret } };
|
|
705
|
+
}
|
|
706
|
+
toHTML() {
|
|
707
|
+
return this.#blocks.map(b => b.toHTML()).join("");
|
|
708
|
+
}
|
|
709
|
+
}
|