@miliastry/quasar 1.0.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 +119 -0
- package/README.md +45 -0
- package/dist/Visuals/lyne.css +1027 -0
- package/dist/Visuals/osu.css +257 -0
- package/dist/index.d.mts +4563 -0
- package/dist/index.d.ts +4563 -0
- package/dist/index.js +11291 -0
- package/dist/index.mjs +11197 -0
- package/package.json +51 -0
- package/src/Analysis/Contracts/AnalysisReport.ts +26 -0
- package/src/Analysis/Contracts/Contribution.ts +76 -0
- package/src/Analysis/Contracts/Pass.ts +76 -0
- package/src/Analysis/Contracts/PipelineContext.ts +48 -0
- package/src/Analysis/Passes/Analysis/GradientAnalyzer.ts +292 -0
- package/src/Analysis/Passes/Analysis/MergeableColorAnalyzer.ts +112 -0
- package/src/Analysis/Passes/Analysis/RainbowAnalyzer.ts +233 -0
- package/src/Analysis/Passes/Analysis/WaveAnalyzer.ts +211 -0
- package/src/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.ts +135 -0
- package/src/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.ts +84 -0
- package/src/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.ts +99 -0
- package/src/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.ts +119 -0
- package/src/Analysis/Passes/Decision/DefaultDecision.ts +139 -0
- package/src/Analysis/Passes/Decision/__tests__/DefaultDecision.test.ts +179 -0
- package/src/Analysis/Passes/Transform/CollapseGradientTransform.ts +176 -0
- package/src/Analysis/Passes/Transform/MergeColorsTransform.ts +126 -0
- package/src/Analysis/Passes/Transform/RainbowCollapseTransform.ts +83 -0
- package/src/Analysis/Passes/Transform/WaveCollapseTransform.ts +88 -0
- package/src/Analysis/Passes/Utility/CharacterCountAnalyzer.ts +45 -0
- package/src/Analysis/Pipeline/Pipeline.ts +133 -0
- package/src/Analysis/Pipeline/PipelineBuilder.ts +55 -0
- package/src/Analysis/Pipeline/PipelineStage.ts +19 -0
- package/src/Analysis/Utils/color-utils.ts +132 -0
- package/src/Analysis/__tests__/Integration.test.ts +162 -0
- package/src/Analysis/__tests__/Pipeline.test.ts +133 -0
- package/src/Analysis/index.ts +52 -0
- package/src/BBCode/BBCodeDocumentModel.ts +175 -0
- package/src/BBCode/BBCodeToGreenNode.ts +755 -0
- package/src/BBCode/Parser.ts +384 -0
- package/src/BBCode/index.ts +12 -0
- package/src/Collab/positions.ts +91 -0
- package/src/Commands/Command.ts +44 -0
- package/src/Commands/CommandRegistry.ts +78 -0
- package/src/Commands/DeleteNode.ts +20 -0
- package/src/Commands/InsertText.ts +21 -0
- package/src/Commands/SplitMerge.ts +28 -0
- package/src/Commands/WrapInTag.ts +21 -0
- package/src/Commands/index.ts +6 -0
- package/src/Diff/TreeDiffer.ts +264 -0
- package/src/Diff/__tests__/TreeDiffer.test.ts +65 -0
- package/src/Diff/index.ts +2 -0
- package/src/Events/EventBus.ts +160 -0
- package/src/Events/index.ts +2 -0
- package/src/Formatter/Formatter.ts +54 -0
- package/src/Formatter/index.ts +2 -0
- package/src/HTML/HTMLDocumentModel.ts +35 -0
- package/src/HTML/HTMLToGreenNode.ts +290 -0
- package/src/Incremental/ChangeTracker.ts +105 -0
- package/src/Incremental/IncrementalParser.ts +591 -0
- package/src/Incremental/__tests__/IncrementalParser.test.ts +164 -0
- package/src/Incremental/index.ts +4 -0
- package/src/Lexer/BBCodeLexer.ts +382 -0
- package/src/Lexer/Lexer.ts +181 -0
- package/src/Lexer/index.ts +10 -0
- package/src/Linter/Linter.ts +193 -0
- package/src/Linter/index.ts +2 -0
- package/src/Markdown/MarkdownAST.ts +112 -0
- package/src/Markdown/MarkdownDocumentModel.ts +55 -0
- package/src/Markdown/MarkdownLexer.ts +203 -0
- package/src/Markdown/MarkdownParser.ts +455 -0
- package/src/Markdown/MarkdownToGreenNode.ts +153 -0
- package/src/Model/DocumentModel.ts +694 -0
- package/src/Model/NodeFactory.ts +117 -0
- package/src/Model/TagRegistry.ts +495 -0
- package/src/Model/index.ts +5 -0
- package/src/Plugins/PluginAPI.ts +119 -0
- package/src/Plugins/PluginRegistry.ts +132 -0
- package/src/Plugins/index.ts +3 -0
- package/src/Queries/QueryEngine.ts +152 -0
- package/src/Queries/index.ts +1 -0
- package/src/RenderPipeline/RenderPipeline.ts +125 -0
- package/src/RenderPipeline/RenderTree.ts +134 -0
- package/src/RenderPipeline/index.ts +4 -0
- package/src/Semantic/SemanticAnalyzer.ts +506 -0
- package/src/Semantic/index.ts +2 -0
- package/src/Symbols/SymbolTable.ts +124 -0
- package/src/Symbols/index.ts +1 -0
- package/src/Syntax/GreenNode.ts +324 -0
- package/src/Syntax/GreenNodePool.ts +269 -0
- package/src/Syntax/NodeMatcher.ts +370 -0
- package/src/Syntax/RedNode.ts +569 -0
- package/src/Syntax/RedNodeStore.ts +184 -0
- package/src/Syntax/TreeBuilder.ts +214 -0
- package/src/Syntax/__tests__/GreenNode.test.ts +33 -0
- package/src/Syntax/__tests__/RedNode.test.ts +81 -0
- package/src/Syntax/__tests__/RedNodeStore.test.ts +104 -0
- package/src/Syntax/greenEdit.ts +110 -0
- package/src/Syntax/hash.ts +30 -0
- package/src/Syntax/index.ts +12 -0
- package/src/Syntax/partition.ts +161 -0
- package/src/Syntax/preserveNodeIds.ts +201 -0
- package/src/Tests/ASTOptimizerIdempotence.test.ts +77 -0
- package/src/Tests/BlockPatcher.test.ts +437 -0
- package/src/Tests/BlockPatcherWindowed.test.ts +364 -0
- package/src/Tests/BoxDrawer.test.ts +217 -0
- package/src/Tests/BoxRichTitle.test.ts +105 -0
- package/src/Tests/Chars500kBenchmark.test.ts +151 -0
- package/src/Tests/Chars500kEdits.test.ts +321 -0
- package/src/Tests/CollabPositions.test.ts +146 -0
- package/src/Tests/CompilerPathProfiling.test.ts +186 -0
- package/src/Tests/DOMMorpher.test.ts +142 -0
- package/src/Tests/DomPatchPerf.test.ts +60 -0
- package/src/Tests/EffectSegments.snapshot.json +616 -0
- package/src/Tests/EffectSegments.test.ts +68 -0
- package/src/Tests/FindNodeAtOffset.test.ts +65 -0
- package/src/Tests/Fuzzer.test.ts +166 -0
- package/src/Tests/GreenNodePool.test.ts +153 -0
- package/src/Tests/Lexer.test.ts +238 -0
- package/src/Tests/LyneMode.test.ts +187 -0
- package/src/Tests/ModelCoherence.test.ts +180 -0
- package/src/Tests/Partition.test.ts +238 -0
- package/src/Tests/PluginTags.test.ts +150 -0
- package/src/Tests/ProblematicSection.test.ts +46 -0
- package/src/Tests/ProblematicSectionHTML.test.ts +58 -0
- package/src/Tests/RedReuse.test.ts +134 -0
- package/src/Tests/ReproDelete20k.test.ts +62 -0
- package/src/Tests/SemanticValidators.test.ts +136 -0
- package/src/Tests/StableNodeIds.test.ts +210 -0
- package/src/Tests/StudioColorBloat.test.ts +25 -0
- package/src/Tests/StudioDebugText.test.ts +27 -0
- package/src/Tests/StudioTrailingChar.test.ts +25 -0
- package/src/Tests/StudioValidText.test.ts +25 -0
- package/src/Tests/UrlImgBug.test.ts +23 -0
- package/src/Tests/VisualBuilderFidelity.test.ts +105 -0
- package/src/Tests/referenceDocument.ts +119 -0
- package/src/Transactions/Transaction.ts +176 -0
- package/src/Transactions/UndoManager.ts +111 -0
- package/src/Transactions/index.ts +3 -0
- package/src/Transformers/ASTOptimizer.ts +315 -0
- package/src/Transformers/GradientTransformer.ts +143 -0
- package/src/Transformers/GrowTransformer.ts +115 -0
- package/src/Transformers/RainbowTransformer.ts +121 -0
- package/src/Transformers/SineWaveTransformer.ts +130 -0
- package/src/Transformers/Transformer.ts +22 -0
- package/src/Types/core.ts +270 -0
- package/src/Types/diagnostics.ts +156 -0
- package/src/Types/index.ts +23 -0
- package/src/Types/operations.ts +180 -0
- package/src/Types/queries.ts +121 -0
- package/src/Types/symbols.ts +69 -0
- package/src/Types/tokens.ts +186 -0
- package/src/Utils/BBCodeGenerator.ts +126 -0
- package/src/Utils/ColorMath.ts +276 -0
- package/src/Utils/color.ts +112 -0
- package/src/Utils/dom-to-svg.test.ts +86 -0
- package/src/Utils/dom-to-svg.ts +615 -0
- package/src/Utils/treeTransformers.ts +717 -0
- package/src/Visitors/BBBlocksExporter.ts +69 -0
- package/src/Visitors/BBCodeExporter.ts +318 -0
- package/src/Visitors/BlockPatcher.ts +963 -0
- package/src/Visitors/DOMMorpher.ts +134 -0
- package/src/Visitors/HTMLRenderer.ts +1077 -0
- package/src/Visitors/JSONExporter.ts +66 -0
- package/src/Visitors/MarkdownExporter.ts +99 -0
- package/src/Visitors/SVGRenderer.ts +35 -0
- package/src/Visitors/TiptapExporter.ts +145 -0
- package/src/Visitors/Visitor.ts +48 -0
- package/src/Visitors/index.ts +9 -0
- package/src/Visuals/BoxDrawer.ts +175 -0
- package/src/Visuals/index.ts +42 -0
- package/src/Visuals/lyne.css +1027 -0
- package/src/Visuals/osu.css +257 -0
- package/src/index.ts +197 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { GreenNode, greenNode, greenLeaf } from '../Syntax/GreenNode';
|
|
2
|
+
import { RedNode } from '../Syntax/RedNode';
|
|
3
|
+
import type { NodeKind } from '../Types/core';
|
|
4
|
+
import { isBlockKind } from '../BBCode/BBCodeToGreenNode';
|
|
5
|
+
|
|
6
|
+
function domToGreenTree(root: HTMLElement): GreenNode {
|
|
7
|
+
let currentOffset = 0;
|
|
8
|
+
|
|
9
|
+
function convert(node: globalThis.Node): GreenNode {
|
|
10
|
+
const start = currentOffset;
|
|
11
|
+
const children: GreenNode[] = [];
|
|
12
|
+
let kind: string = 'document';
|
|
13
|
+
let text = '';
|
|
14
|
+
|
|
15
|
+
if (node.nodeType === 3) { // Text Node
|
|
16
|
+
text = node.nodeValue || '';
|
|
17
|
+
|
|
18
|
+
// Convert pure whitespace text nodes into spacing/empty_line directly
|
|
19
|
+
if (text.trim() === '') {
|
|
20
|
+
const parentTag = node.parentNode ? node.parentNode.nodeName.toLowerCase() : '';
|
|
21
|
+
if (['ul', 'ol', 'table', 'tbody', 'tr'].includes(parentTag)) {
|
|
22
|
+
return [] as any; // Ignore structural whitespace
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const newlines = (text.match(/\n/g) || []).length;
|
|
26
|
+
if (newlines >= 2) {
|
|
27
|
+
kind = 'empty_line';
|
|
28
|
+
text = '\n\n';
|
|
29
|
+
currentOffset += 2;
|
|
30
|
+
} else if (newlines === 1) {
|
|
31
|
+
kind = 'spacing';
|
|
32
|
+
text = '\n';
|
|
33
|
+
currentOffset += 1;
|
|
34
|
+
} else {
|
|
35
|
+
kind = 'text'; // Fallback for 0 newlines
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
kind = 'text';
|
|
39
|
+
currentOffset += text.length;
|
|
40
|
+
}
|
|
41
|
+
} else if (node.nodeType === 1) { // Element Node
|
|
42
|
+
const el = node as HTMLElement;
|
|
43
|
+
const tag = el.tagName.toLowerCase();
|
|
44
|
+
|
|
45
|
+
switch (tag) {
|
|
46
|
+
case 'b':
|
|
47
|
+
case 'strong': kind = 'bold'; break;
|
|
48
|
+
case 'i':
|
|
49
|
+
case 'em': kind = 'italic'; break;
|
|
50
|
+
case 'u': kind = 'underline'; break;
|
|
51
|
+
case 's':
|
|
52
|
+
case 'strike':
|
|
53
|
+
case 'del': kind = 'strikethrough'; break;
|
|
54
|
+
case 'a':
|
|
55
|
+
kind = 'url';
|
|
56
|
+
text = `=${el.getAttribute('href') || ''}`;
|
|
57
|
+
break;
|
|
58
|
+
case 'img':
|
|
59
|
+
kind = 'image';
|
|
60
|
+
text = el.getAttribute('src') || '';
|
|
61
|
+
break;
|
|
62
|
+
case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
|
|
63
|
+
kind = 'heading';
|
|
64
|
+
const level = parseInt(tag.charAt(1));
|
|
65
|
+
text = `=${level}`;
|
|
66
|
+
break;
|
|
67
|
+
case 'br':
|
|
68
|
+
kind = 'spacing';
|
|
69
|
+
text = '\n';
|
|
70
|
+
currentOffset += 1;
|
|
71
|
+
break;
|
|
72
|
+
case 'div':
|
|
73
|
+
if (el.classList.contains('notice')) kind = 'notice';
|
|
74
|
+
else if (el.classList.contains('bbcode-sp')) kind = 'spacing';
|
|
75
|
+
else if (el.classList.contains('bbcode-para')) kind = 'empty_line';
|
|
76
|
+
else if (el.classList.contains('bbcode-imagemap') || el.classList.contains('imagemap-container')) {
|
|
77
|
+
kind = 'imagemap';
|
|
78
|
+
const img = el.querySelector('img');
|
|
79
|
+
if (img) children.push(greenLeaf('text', img.getAttribute('src') || ''));
|
|
80
|
+
children.push(greenLeaf('spacing', ''));
|
|
81
|
+
|
|
82
|
+
const areas = el.querySelectorAll('.bbcode-imap-area, .imagemap-area');
|
|
83
|
+
areas.forEach((a, idx) => {
|
|
84
|
+
const href = a.getAttribute('href') || '';
|
|
85
|
+
const title = a.getAttribute('title') || '';
|
|
86
|
+
const style = (a as HTMLElement).style;
|
|
87
|
+
const x = parseFloat(style.left || '0');
|
|
88
|
+
const y = parseFloat(style.top || '0');
|
|
89
|
+
const w = parseFloat(style.width || '0');
|
|
90
|
+
const h = parseFloat(style.height || '0');
|
|
91
|
+
|
|
92
|
+
const line = `${x} ${y} ${w} ${h} ${href} ${title}`.trim();
|
|
93
|
+
children.push(greenLeaf('text', line));
|
|
94
|
+
if (idx < areas.length - 1) {
|
|
95
|
+
children.push(greenLeaf('spacing', ''));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
else if (el.style.textAlign === 'center') kind = 'center';
|
|
100
|
+
else if (el.style.textAlign === 'right') kind = 'right';
|
|
101
|
+
else if (el.style.textAlign === 'left') kind = 'left';
|
|
102
|
+
else kind = 'group';
|
|
103
|
+
break;
|
|
104
|
+
case 'span':
|
|
105
|
+
if (el.classList.contains('spoiler')) kind = 'spoiler';
|
|
106
|
+
else if (el.classList.contains('aesthetic')) kind = 'aesthetic';
|
|
107
|
+
else if (el.style.color) { kind = 'color'; text = `=${el.style.color}`; }
|
|
108
|
+
else if (el.style.fontSize) { kind = 'font_size'; text = `=${el.style.fontSize.replace('%', '')}`; }
|
|
109
|
+
// If it's just a text wrapper generated by HTMLRenderer, we should just unwrap it
|
|
110
|
+
else if (el.classList.contains('bb-text')) {
|
|
111
|
+
// We can use a transparent group or a specialized inline node,
|
|
112
|
+
// but flattening it is best. We can use a temporary kind that gets flattened,
|
|
113
|
+
// or just use 'group' but wait, 'group' is block-like.
|
|
114
|
+
// Actually, if we just set it to 'text_wrapper', we can filter it out, but for now
|
|
115
|
+
// let's just make it a 'group' but ideally we shouldn't create a node at all.
|
|
116
|
+
// Wait, the simplest fix is to not create a node for bb-text and just append its children!
|
|
117
|
+
kind = 'bb_text_wrapper';
|
|
118
|
+
}
|
|
119
|
+
else kind = 'group';
|
|
120
|
+
break;
|
|
121
|
+
case 'iframe':
|
|
122
|
+
if (el.classList.contains('bb-youtube') || el.hasAttribute('data-youtube')) {
|
|
123
|
+
kind = 'youtube';
|
|
124
|
+
text = `=${el.getAttribute('data-youtube') || (el as HTMLIFrameElement).src.split('/embed/')[1]?.split('?')[0] || ''}`;
|
|
125
|
+
} else {
|
|
126
|
+
kind = 'group';
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
case 'audio':
|
|
130
|
+
kind = 'audio';
|
|
131
|
+
text = `=${el.getAttribute('src') || ''}`;
|
|
132
|
+
break;
|
|
133
|
+
case 'details':
|
|
134
|
+
kind = el.classList.contains('box') ? 'box' : 'spoilerbox';
|
|
135
|
+
const summary = el.querySelector('summary');
|
|
136
|
+
if (summary) text = `=${summary.textContent || ''}`;
|
|
137
|
+
break;
|
|
138
|
+
case 'p':
|
|
139
|
+
case 'section':
|
|
140
|
+
kind = 'group';
|
|
141
|
+
break;
|
|
142
|
+
case 'ul':
|
|
143
|
+
case 'ol':
|
|
144
|
+
kind = 'list';
|
|
145
|
+
text = tag === 'ol' ? '=1' : '';
|
|
146
|
+
break;
|
|
147
|
+
case 'li':
|
|
148
|
+
kind = 'list_item';
|
|
149
|
+
break;
|
|
150
|
+
case 'blockquote':
|
|
151
|
+
kind = 'quote';
|
|
152
|
+
const firstElem = el.firstElementChild as HTMLElement;
|
|
153
|
+
if (firstElem && firstElem.tagName.toLowerCase() === 'div') {
|
|
154
|
+
const strong = firstElem.querySelector('strong');
|
|
155
|
+
if (strong && strong.textContent && strong.textContent.endsWith(' wrote:')) {
|
|
156
|
+
const author = strong.textContent.slice(0, -7);
|
|
157
|
+
text = `=${author}`;
|
|
158
|
+
(firstElem as any).__quasar_extracted = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
case 'pre':
|
|
163
|
+
kind = 'code';
|
|
164
|
+
// Extract raw text as a child text node (DocumentEngine expects code content in children)
|
|
165
|
+
const codeText = el.textContent || '';
|
|
166
|
+
children.push(greenLeaf('text', codeText));
|
|
167
|
+
currentOffset += codeText.length;
|
|
168
|
+
break;
|
|
169
|
+
case 'code':
|
|
170
|
+
kind = el.classList.contains('inline') ? 'inline_code' : 'group';
|
|
171
|
+
break;
|
|
172
|
+
default:
|
|
173
|
+
kind = 'group';
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Convert children recursively unless it's a raw block like pre or imagemap
|
|
178
|
+
if (tag !== 'pre' && !(tag === 'div' && (el.classList.contains('bbcode-imagemap') || el.classList.contains('imagemap-container')))) {
|
|
179
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
|
180
|
+
const child = node.childNodes[i];
|
|
181
|
+
if ((child as any).__quasar_extracted) continue;
|
|
182
|
+
// Skip summary if we already extracted it for details
|
|
183
|
+
if (tag === 'details' && child.nodeName.toLowerCase() === 'summary') continue;
|
|
184
|
+
const parsedChild = convert(child);
|
|
185
|
+
if (Array.isArray(parsedChild)) {
|
|
186
|
+
children.push(...parsedChild);
|
|
187
|
+
} else {
|
|
188
|
+
children.push(parsedChild);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const end = currentOffset;
|
|
195
|
+
if (kind === 'bb_text_wrapper') {
|
|
196
|
+
// Just return the children array to be flattened by the parent
|
|
197
|
+
return children as any;
|
|
198
|
+
}
|
|
199
|
+
return greenNode(kind as string, text, children);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const rootChildren: GreenNode[] = [];
|
|
203
|
+
for (let i = 0; i < root.childNodes.length; i++) {
|
|
204
|
+
const parsed = convert(root.childNodes[i]);
|
|
205
|
+
if (Array.isArray(parsed)) {
|
|
206
|
+
rootChildren.push(...parsed);
|
|
207
|
+
} else {
|
|
208
|
+
rootChildren.push(parsed);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Normalize root inline nodes into paragraphs to match BBCode Parser behavior
|
|
213
|
+
const normalizedRoot: GreenNode[] = [];
|
|
214
|
+
let currentParagraph: GreenNode[] = [];
|
|
215
|
+
|
|
216
|
+
const flushParagraph = () => {
|
|
217
|
+
if (currentParagraph.length > 0) {
|
|
218
|
+
normalizedRoot.push(greenNode('paragraph', '', currentParagraph));
|
|
219
|
+
currentParagraph = [];
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
for (const child of rootChildren) {
|
|
224
|
+
// If it's a completely empty whitespace text node (0 newlines), ignore it
|
|
225
|
+
if (child.kind === 'text' && child.text.trim() === '') {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (isBlockKind(child.kind as NodeKind) || child.kind === 'empty_line') {
|
|
230
|
+
flushParagraph();
|
|
231
|
+
normalizedRoot.push(child);
|
|
232
|
+
} else {
|
|
233
|
+
currentParagraph.push(child);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
flushParagraph();
|
|
237
|
+
|
|
238
|
+
return greenNode('document', '', normalizedRoot);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function htmlStringToGreenTree(html: string): GreenNode {
|
|
242
|
+
if (typeof DOMParser === 'undefined') {
|
|
243
|
+
// If run on server, just wrap as text (or we could use a server-side DOM lib)
|
|
244
|
+
return greenLeaf('text', html);
|
|
245
|
+
}
|
|
246
|
+
const parser = new DOMParser();
|
|
247
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
248
|
+
return domToGreenTree(doc.body);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function greenToRedNode(green: GreenNode, parent?: RedNode | null): RedNode {
|
|
252
|
+
// Extract metadata based on HTML tag footprints
|
|
253
|
+
let metadata: Record<string, unknown> = {};
|
|
254
|
+
if (green.kind === 'heading' && green.text.startsWith('=')) {
|
|
255
|
+
metadata = { level: parseInt(green.text.slice(1)) || 1 };
|
|
256
|
+
} else if (green.kind === 'url' && green.text.startsWith('=')) {
|
|
257
|
+
metadata = { href: green.text.slice(1) };
|
|
258
|
+
} else if (green.kind === 'color' && green.text.startsWith('=')) {
|
|
259
|
+
metadata = { color: green.text.slice(1) };
|
|
260
|
+
} else if (green.kind === 'font_size' && green.text.startsWith('=')) {
|
|
261
|
+
metadata = { size: green.text.slice(1) };
|
|
262
|
+
} else if (green.kind === 'quote' && green.text.startsWith('=')) {
|
|
263
|
+
metadata = { author: green.text.slice(1) };
|
|
264
|
+
} else if ((green.kind === 'box' || green.kind === 'spoilerbox') && green.text.startsWith('=')) {
|
|
265
|
+
metadata = { title: green.text.slice(1) };
|
|
266
|
+
} else if (green.kind === 'image') {
|
|
267
|
+
metadata = { src: green.text };
|
|
268
|
+
} else if ((green.kind === 'youtube' || green.kind === 'audio') && green.text.startsWith('=')) {
|
|
269
|
+
metadata = green.kind === 'youtube' ? { videoId: green.text.slice(1) } : { src: green.text.slice(1) };
|
|
270
|
+
} else if (green.kind === 'list') {
|
|
271
|
+
metadata = { ordered: green.text === '=1' };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const red = new RedNode(green, {
|
|
275
|
+
parent: parent ?? null,
|
|
276
|
+
kind: (green.kind as NodeKind) || 'text',
|
|
277
|
+
metadata
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const greenChildren = green.children as GreenNode[];
|
|
281
|
+
if (greenChildren.length > 0) {
|
|
282
|
+
const kids: RedNode[] = new Array(greenChildren.length);
|
|
283
|
+
for (let i = 0; i < greenChildren.length; i++) {
|
|
284
|
+
kids[i] = greenToRedNode(greenChildren[i], red);
|
|
285
|
+
}
|
|
286
|
+
red.initChildren(kids);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return red;
|
|
290
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocumentEngine — ChangeTracker
|
|
3
|
+
*
|
|
4
|
+
* Tracks text changes made to the document source.
|
|
5
|
+
* Used by IncrementalParser to determine what needs to be re-parsed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface TextChange {
|
|
9
|
+
start: number
|
|
10
|
+
end: number
|
|
11
|
+
text: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Source range of a text edit, in BOTH coordinate systems.
|
|
16
|
+
*
|
|
17
|
+
* An edit sits between two documents: `start`/`end` describe the changed
|
|
18
|
+
* region in NEW-source coordinates (the current document), while `endOld` is
|
|
19
|
+
* the end of the replaced region in OLD-source coordinates. The incremental
|
|
20
|
+
* preview (`BlockPatcher`) uses `start`/`endOld` to locate the affected blocks
|
|
21
|
+
* in the PREVIOUS tree (whose offsets are pre-edit) and `start`/`end` in the
|
|
22
|
+
* new one — insertions or deletions between the two make the naive single-`end`
|
|
23
|
+
* wrong, which is exactly why both ends are kept.
|
|
24
|
+
*/
|
|
25
|
+
export interface TextChangeRange {
|
|
26
|
+
/** Start of the edited region (identical in both coordinate systems). */
|
|
27
|
+
start: number
|
|
28
|
+
/** End of the edited region, in new-source coordinates. */
|
|
29
|
+
end: number
|
|
30
|
+
/** End of the replaced region, in old-source coordinates. */
|
|
31
|
+
endOld: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TextChangeStats {
|
|
35
|
+
totalChanges: number
|
|
36
|
+
totalInserted: number
|
|
37
|
+
totalDeleted: number
|
|
38
|
+
lastChange: TextChange | null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class ChangeTracker {
|
|
42
|
+
private changes: TextChange[] = []
|
|
43
|
+
private maxHistory: number = 100
|
|
44
|
+
|
|
45
|
+
constructor(maxHistory?: number) {
|
|
46
|
+
if (maxHistory) this.maxHistory = maxHistory
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Track a text change.
|
|
51
|
+
*/
|
|
52
|
+
track(change: TextChange): void {
|
|
53
|
+
this.changes.push(change)
|
|
54
|
+
if (this.changes.length > this.maxHistory) {
|
|
55
|
+
this.changes.shift()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get all tracked changes.
|
|
61
|
+
*/
|
|
62
|
+
getAll(): TextChange[] {
|
|
63
|
+
return [...this.changes]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the most recent change.
|
|
68
|
+
*/
|
|
69
|
+
getLast(): TextChange | null {
|
|
70
|
+
return this.changes.length > 0 ? this.changes[this.changes.length - 1] : null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get the affected range for the last N changes.
|
|
75
|
+
* Returns null if no changes have been tracked.
|
|
76
|
+
*/
|
|
77
|
+
getAffectedRange(count: number = 1): { start: number; end: number } | null {
|
|
78
|
+
if (this.changes.length === 0) return null
|
|
79
|
+
|
|
80
|
+
const relevant = this.changes.slice(-count)
|
|
81
|
+
return {
|
|
82
|
+
start: Math.min(...relevant.map(c => c.start)),
|
|
83
|
+
end: Math.max(...relevant.map(c => c.end)),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Clear change history.
|
|
89
|
+
*/
|
|
90
|
+
clear(): void {
|
|
91
|
+
this.changes = []
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get statistics about tracked changes.
|
|
96
|
+
*/
|
|
97
|
+
getStats(): TextChangeStats {
|
|
98
|
+
return {
|
|
99
|
+
totalChanges: this.changes.length,
|
|
100
|
+
totalInserted: this.changes.reduce((sum, c) => sum + c.text.length, 0),
|
|
101
|
+
totalDeleted: this.changes.reduce((sum, c) => sum + (c.end - c.start), 0),
|
|
102
|
+
lastChange: this.getLast(),
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|