@miliastry/quasar 1.0.6 → 1.1.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/dist/Visuals/lyne.css +117 -6
- package/dist/Visuals/osu.css +36 -5
- package/dist/index.js +9664 -3282
- package/dist/index.mjs +9535 -3283
- package/dist/types/Analysis/Contracts/AnalysisReport.d.ts +22 -0
- package/dist/types/Analysis/Contracts/Contribution.d.ts +68 -0
- package/dist/types/Analysis/Contracts/Pass.d.ts +59 -0
- package/dist/types/Analysis/Contracts/PipelineContext.d.ts +34 -0
- package/dist/types/Analysis/Passes/Analysis/ColorUsageAnalyzer.d.ts +56 -0
- package/dist/types/Analysis/Passes/Analysis/GradientAnalyzer.d.ts +111 -0
- package/dist/types/Analysis/Passes/Analysis/MergeableColorAnalyzer.d.ts +26 -0
- package/dist/types/Analysis/Passes/Analysis/RainbowAnalyzer.d.ts +47 -0
- package/dist/types/Analysis/Passes/Analysis/SymbolAnalyzer.d.ts +89 -0
- package/dist/types/Analysis/Passes/Analysis/WaveAnalyzer.d.ts +44 -0
- package/dist/types/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.d.ts +4 -0
- package/dist/types/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.d.ts +4 -0
- package/dist/types/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.d.ts +4 -0
- package/dist/types/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.d.ts +4 -0
- package/dist/types/Analysis/Passes/Decision/DefaultDecision.d.ts +30 -0
- package/dist/types/Analysis/Passes/Decision/PaletteRemapDecision.d.ts +169 -0
- package/dist/types/Analysis/Passes/Decision/__tests__/DefaultDecision.test.d.ts +4 -0
- package/dist/types/Analysis/Passes/Transform/CollapseGradientTransform.d.ts +37 -0
- package/dist/types/Analysis/Passes/Transform/MergeColorsTransform.d.ts +28 -0
- package/dist/types/Analysis/Passes/Transform/RainbowCollapseTransform.d.ts +19 -0
- package/dist/types/Analysis/Passes/Transform/WaveCollapseTransform.d.ts +20 -0
- package/dist/types/Analysis/Passes/Utility/CharacterCountAnalyzer.d.ts +19 -0
- package/dist/types/Analysis/Pipeline/Pipeline.d.ts +69 -0
- package/dist/types/Analysis/Pipeline/PipelineBuilder.d.ts +31 -0
- package/dist/types/Analysis/Pipeline/PipelineStage.d.ts +17 -0
- package/dist/types/Analysis/Utils/color-utils.d.ts +60 -0
- package/dist/types/Analysis/__tests__/Integration.test.d.ts +8 -0
- package/dist/types/Analysis/__tests__/PaletteRemap.test.d.ts +14 -0
- package/dist/types/Analysis/__tests__/Pipeline.test.d.ts +10 -0
- package/dist/types/Analysis/__tests__/RoundTrip.test.d.ts +30 -0
- package/dist/types/Analysis/__tests__/SymbolAnalyzer.test.d.ts +11 -0
- package/dist/types/Analysis/index.d.ts +40 -0
- package/dist/types/BBCode/BBCodeDocumentModel.d.ts +92 -0
- package/dist/types/BBCode/BBCodeToGreenNode.d.ts +111 -0
- package/dist/types/BBCode/Parser.d.ts +78 -0
- package/dist/types/BBCode/index.d.ts +4 -0
- package/dist/types/Collab/positions.d.ts +55 -0
- package/dist/types/Commands/Command.d.ts +37 -0
- package/dist/types/Commands/CommandRegistry.d.ts +48 -0
- package/dist/types/Commands/DeleteNode.d.ts +7 -0
- package/dist/types/Commands/InsertText.d.ts +8 -0
- package/dist/types/Commands/SplitMerge.d.ts +9 -0
- package/dist/types/Commands/WrapInTag.d.ts +8 -0
- package/dist/types/Commands/index.d.ts +6 -0
- package/dist/types/Diff/TreeDiffer.d.ts +52 -0
- package/dist/types/Diff/__tests__/TreeDiffer.test.d.ts +1 -0
- package/dist/types/Diff/index.d.ts +2 -0
- package/dist/types/Edits/EditPlan.d.ts +144 -0
- package/dist/types/Edits/Optimizer.d.ts +78 -0
- package/dist/types/Edits/Rules/Rule.d.ts +91 -0
- package/dist/types/Edits/Rules/dropEmptyTags.d.ts +40 -0
- package/dist/types/Edits/Rules/dropRedundantNesting.d.ts +59 -0
- package/dist/types/Edits/Rules/index.d.ts +9 -0
- package/dist/types/Edits/Rules/mergeAdjacent.d.ts +93 -0
- package/dist/types/Edits/Rules/reorderWrappers.d.ts +42 -0
- package/dist/types/Edits/Rules/shortenHex.d.ts +30 -0
- package/dist/types/Edits/Rules/tagValue.d.ts +56 -0
- package/dist/types/Edits/Rules/unwrapInvisibleColor.d.ts +47 -0
- package/dist/types/Edits/applyEdits.d.ts +36 -0
- package/dist/types/Edits/index.d.ts +6 -0
- package/dist/types/Events/EventBus.d.ts +103 -0
- package/dist/types/Events/index.d.ts +2 -0
- package/dist/types/Formatter/Formatter.d.ts +32 -0
- package/dist/types/Formatter/index.d.ts +2 -0
- package/dist/types/HTML/HTMLDocumentModel.d.ts +13 -0
- package/dist/types/HTML/HTMLToGreenNode.d.ts +4 -0
- package/dist/types/Incremental/BracketIndex.d.ts +101 -0
- package/dist/types/Incremental/ChangeTracker.d.ts +69 -0
- package/dist/types/Incremental/IncrementalParser.d.ts +246 -0
- package/dist/types/Incremental/__tests__/BracketIndex.test.d.ts +1 -0
- package/dist/types/Incremental/__tests__/IncrementalParser.test.d.ts +1 -0
- package/dist/types/Incremental/index.d.ts +4 -0
- package/dist/types/Lexer/BBCodeLexer.d.ts +74 -0
- package/dist/types/Lexer/Lexer.d.ts +54 -0
- package/dist/types/Lexer/index.d.ts +4 -0
- package/dist/types/Linter/Linter.d.ts +58 -0
- package/dist/types/Linter/index.d.ts +2 -0
- package/dist/types/Markdown/MarkdownAST.d.ts +122 -0
- package/dist/types/Markdown/MarkdownDocumentModel.d.ts +15 -0
- package/dist/types/Markdown/MarkdownLexer.d.ts +155 -0
- package/dist/types/Markdown/MarkdownParser.d.ts +42 -0
- package/dist/types/Markdown/MarkdownToGreenNode.d.ts +6 -0
- package/dist/types/MilHibri/MilHibriDocumentModel.d.ts +21 -0
- package/dist/types/Model/DocumentModel.d.ts +318 -0
- package/dist/types/Model/NodeFactory.d.ts +29 -0
- package/dist/types/Model/TagRegistry.d.ts +165 -0
- package/dist/types/Model/index.d.ts +5 -0
- package/dist/types/Plugins/PluginAPI.d.ts +44 -0
- package/dist/types/Plugins/PluginRegistry.d.ts +81 -0
- package/dist/types/Plugins/index.d.ts +3 -0
- package/dist/types/Queries/QueryEngine.d.ts +32 -0
- package/dist/types/Queries/index.d.ts +1 -0
- package/dist/types/Reconciler/SurgicalReconciler.d.ts +23 -0
- package/dist/types/RenderPipeline/RenderPipeline.d.ts +46 -0
- package/dist/types/RenderPipeline/RenderTree.d.ts +55 -0
- package/dist/types/RenderPipeline/index.d.ts +4 -0
- package/dist/types/Repair/NestingRepair.d.ts +48 -0
- package/dist/types/Repair/index.d.ts +1 -0
- package/dist/types/Semantic/SemanticAnalyzer.d.ts +321 -0
- package/dist/types/Semantic/index.d.ts +2 -0
- package/dist/types/Symbols/SymbolTable.d.ts +47 -0
- package/dist/types/Symbols/index.d.ts +1 -0
- package/dist/types/Syntax/GreenNode.d.ts +154 -0
- package/dist/types/Syntax/GreenNodePool.d.ts +134 -0
- package/dist/types/Syntax/NodeMatcher.d.ts +75 -0
- package/dist/types/Syntax/RedNode.d.ts +280 -0
- package/dist/types/Syntax/RedNodeStore.d.ts +116 -0
- package/dist/types/Syntax/TreeBuilder.d.ts +54 -0
- package/dist/types/Syntax/__tests__/GreenNode.test.d.ts +1 -0
- package/dist/types/Syntax/__tests__/RedNode.test.d.ts +1 -0
- package/dist/types/Syntax/__tests__/RedNodeStore.test.d.ts +1 -0
- package/dist/types/Syntax/greenEdit.d.ts +64 -0
- package/dist/types/Syntax/hash.d.ts +17 -0
- package/dist/types/Syntax/index.d.ts +8 -0
- package/dist/types/Syntax/nodeAttr.d.ts +63 -0
- package/dist/types/Syntax/partition.d.ts +67 -0
- package/dist/types/Syntax/preserveNodeIds.d.ts +47 -0
- package/dist/types/Tokens/TokenResolver.d.ts +25 -0
- package/dist/types/Tokens/index.d.ts +1 -0
- package/dist/types/Transactions/Transaction.d.ts +36 -0
- package/dist/types/Transactions/UndoManager.d.ts +63 -0
- package/dist/types/Transactions/index.d.ts +3 -0
- package/dist/types/Transformers/ASTOptimizer.d.ts +34 -0
- package/dist/types/Transformers/GradientTransformer.d.ts +18 -0
- package/dist/types/Transformers/GrowTransformer.d.ts +16 -0
- package/dist/types/Transformers/RainbowTransformer.d.ts +17 -0
- package/dist/types/Transformers/SineWaveTransformer.d.ts +17 -0
- package/dist/types/Transformers/Transformer.d.ts +17 -0
- package/dist/types/Types/core.d.ts +104 -0
- package/dist/types/Types/diagnostics.d.ts +85 -0
- package/dist/types/Types/index.d.ts +11 -0
- package/dist/types/Types/operations.d.ts +109 -0
- package/dist/types/Types/queries.d.ts +67 -0
- package/dist/types/Types/symbols.d.ts +42 -0
- package/dist/types/Types/tokens.d.ts +75 -0
- package/dist/types/Utils/BBCodeGenerator.d.ts +21 -0
- package/dist/types/Utils/ColorMath.d.ts +92 -0
- package/dist/types/Utils/EffectMath.d.ts +643 -0
- package/dist/types/Utils/TagVocabulary.d.ts +104 -0
- package/dist/types/Utils/TagVocabulary.test.d.ts +1 -0
- package/dist/types/Utils/color.d.ts +29 -0
- package/dist/types/Utils/dom-to-svg.d.ts +28 -0
- package/dist/types/Utils/dom-to-svg.test.d.ts +1 -0
- package/dist/types/Utils/treeTransformers.d.ts +160 -0
- package/dist/types/Visitors/BBBlocksExporter.d.ts +27 -0
- package/dist/types/Visitors/BBCodeExporter.d.ts +90 -0
- package/dist/types/Visitors/BlockPatcher.d.ts +91 -0
- package/dist/types/Visitors/DOMMorpher.d.ts +13 -0
- package/dist/types/Visitors/HTMLRenderer.d.ts +380 -0
- package/dist/types/Visitors/JSONExporter.d.ts +42 -0
- package/dist/types/Visitors/MarkdownExporter.d.ts +28 -0
- package/dist/types/Visitors/SVGRenderer.d.ts +16 -0
- package/dist/types/Visitors/TiptapExporter.d.ts +40 -0
- package/dist/types/Visitors/Visitor.d.ts +35 -0
- package/dist/types/Visitors/index.d.ts +9 -0
- package/dist/types/Visuals/BoxDrawer.d.ts +49 -0
- package/dist/types/Visuals/index.d.ts +38 -0
- package/dist/types/index.d.ts +154 -0
- package/package.json +14 -10
- package/src/Analysis/Passes/Analysis/ColorUsageAnalyzer.ts +103 -0
- package/src/Analysis/Passes/Analysis/GradientAnalyzer.ts +294 -20
- package/src/Analysis/Passes/Analysis/SymbolAnalyzer.ts +257 -0
- package/src/Analysis/Passes/Decision/PaletteRemapDecision.ts +425 -0
- package/src/Analysis/Pipeline/Pipeline.ts +15 -2
- package/src/Analysis/index.ts +8 -2
- package/src/BBCode/BBCodeDocumentModel.ts +3 -0
- package/src/BBCode/BBCodeToGreenNode.ts +18 -12
- package/src/BBCode/Parser.ts +38 -1
- package/src/Edits/EditPlan.ts +290 -0
- package/src/Edits/Optimizer.ts +185 -0
- package/src/Edits/Rules/Rule.ts +145 -0
- package/src/Edits/Rules/dropEmptyTags.ts +110 -0
- package/src/Edits/Rules/dropRedundantNesting.ts +120 -0
- package/src/Edits/Rules/index.ts +28 -0
- package/src/Edits/Rules/mergeAdjacent.ts +224 -0
- package/src/Edits/Rules/reorderWrappers.ts +154 -0
- package/src/Edits/Rules/shortenHex.ts +79 -0
- package/src/Edits/Rules/tagValue.ts +105 -0
- package/src/Edits/Rules/unwrapInvisibleColor.ts +93 -0
- package/src/Edits/applyEdits.ts +54 -0
- package/src/Edits/index.ts +22 -0
- package/src/Events/EventBus.ts +18 -0
- package/src/HTML/HTMLToGreenNode.ts +185 -37
- package/src/Incremental/BracketIndex.ts +249 -0
- package/src/Incremental/IncrementalParser.ts +464 -41
- package/src/Lexer/BBCodeLexer.ts +25 -3
- package/src/Lexer/index.ts +1 -1
- package/src/Markdown/MarkdownAST.ts +65 -2
- package/src/Markdown/MarkdownLexer.ts +69 -14
- package/src/Markdown/MarkdownParser.ts +749 -62
- package/src/Markdown/MarkdownToGreenNode.ts +148 -27
- package/src/MilHibri/MilHibriDocumentModel.ts +56 -0
- package/src/Model/DocumentModel.ts +138 -5
- package/src/Model/TagRegistry.ts +129 -134
- package/src/Reconciler/SurgicalReconciler.ts +460 -0
- package/src/Repair/NestingRepair.ts +116 -0
- package/src/Repair/index.ts +1 -0
- package/src/Semantic/SemanticAnalyzer.ts +1802 -65
- package/src/Syntax/GreenNodePool.ts +12 -12
- package/src/Syntax/RedNode.ts +17 -3
- package/src/Syntax/RedNodeStore.ts +9 -1
- package/src/Syntax/nodeAttr.ts +172 -0
- package/src/Tokens/TokenResolver.ts +55 -0
- package/src/Tokens/index.ts +1 -0
- package/src/Types/core.ts +15 -0
- package/src/Utils/ColorMath.ts +276 -55
- package/src/Utils/EffectMath.ts +2462 -0
- package/src/Utils/TagVocabulary.ts +141 -0
- package/src/Utils/dom-to-svg.ts +28 -12
- package/src/Visitors/BBBlocksExporter.ts +15 -1
- package/src/Visitors/BBCodeExporter.ts +319 -32
- package/src/Visitors/BlockPatcher.ts +87 -11
- package/src/Visitors/DOMMorpher.ts +15 -5
- package/src/Visitors/HTMLRenderer.ts +374 -151
- package/src/Visitors/MarkdownExporter.ts +98 -41
- package/src/Visitors/TiptapExporter.ts +1 -0
- package/src/Visuals/index.ts +7 -4
- package/src/Visuals/lyne.css +117 -6
- package/src/Visuals/osu.css +36 -5
- package/src/index.ts +172 -5
- package/dist/index.d.mts +0 -4612
- package/dist/index.d.ts +0 -4612
- package/src/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.ts +0 -135
- package/src/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.ts +0 -84
- package/src/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.ts +0 -99
- package/src/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.ts +0 -119
- package/src/Analysis/Passes/Decision/__tests__/DefaultDecision.test.ts +0 -179
- package/src/Analysis/__tests__/Integration.test.ts +0 -162
- package/src/Analysis/__tests__/Pipeline.test.ts +0 -133
- package/src/Diff/__tests__/TreeDiffer.test.ts +0 -65
- package/src/Incremental/__tests__/IncrementalParser.test.ts +0 -164
- package/src/Syntax/__tests__/GreenNode.test.ts +0 -33
- package/src/Syntax/__tests__/RedNode.test.ts +0 -81
- package/src/Syntax/__tests__/RedNodeStore.test.ts +0 -104
- package/src/Tests/ASTOptimizerIdempotence.test.ts +0 -77
- package/src/Tests/BlockPatcher.test.ts +0 -437
- package/src/Tests/BlockPatcherWindowed.test.ts +0 -364
- package/src/Tests/BoxDrawer.test.ts +0 -217
- package/src/Tests/BoxRichTitle.test.ts +0 -105
- package/src/Tests/Chars500kBenchmark.test.ts +0 -151
- package/src/Tests/Chars500kEdits.test.ts +0 -321
- package/src/Tests/CollabPositions.test.ts +0 -146
- package/src/Tests/CompilerPathProfiling.test.ts +0 -186
- package/src/Tests/DOMMorpher.test.ts +0 -142
- package/src/Tests/DomPatchPerf.test.ts +0 -60
- package/src/Tests/EffectSegments.snapshot.json +0 -616
- package/src/Tests/EffectSegments.test.ts +0 -68
- package/src/Tests/FindNodeAtOffset.test.ts +0 -65
- package/src/Tests/Fuzzer.test.ts +0 -166
- package/src/Tests/GreenNodePool.test.ts +0 -153
- package/src/Tests/Lexer.test.ts +0 -238
- package/src/Tests/LyneMode.test.ts +0 -291
- package/src/Tests/ModelCoherence.test.ts +0 -180
- package/src/Tests/Partition.test.ts +0 -238
- package/src/Tests/PluginTags.test.ts +0 -150
- package/src/Tests/ProblematicSection.test.ts +0 -46
- package/src/Tests/ProblematicSectionHTML.test.ts +0 -58
- package/src/Tests/RedReuse.test.ts +0 -134
- package/src/Tests/ReproDelete20k.test.ts +0 -62
- package/src/Tests/SemanticValidators.test.ts +0 -136
- package/src/Tests/StableNodeIds.test.ts +0 -210
- package/src/Tests/StudioColorBloat.test.ts +0 -25
- package/src/Tests/StudioDebugText.test.ts +0 -27
- package/src/Tests/StudioTrailingChar.test.ts +0 -25
- package/src/Tests/StudioValidText.test.ts +0 -25
- package/src/Tests/UrlImgBug.test.ts +0 -23
- package/src/Tests/VisualBuilderFidelity.test.ts +0 -105
- package/src/Tests/referenceDocument.ts +0 -119
- package/src/Utils/dom-to-svg.test.ts +0 -86
|
@@ -3,6 +3,38 @@ import { RedNode } from '../Syntax/RedNode';
|
|
|
3
3
|
import type { NodeKind } from '../Types/core';
|
|
4
4
|
import { isBlockKind } from '../BBCode/BBCodeToGreenNode';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* The colour exactly as the author wrote it in the `style` attribute.
|
|
8
|
+
*
|
|
9
|
+
* `el.style.color` goes through CSSOM, which re-spells the value: `#FFE6F0`
|
|
10
|
+
* comes back lowercased. Re-serialising a block then rewrote every hex in it,
|
|
11
|
+
* a change the author never made.
|
|
12
|
+
*/
|
|
13
|
+
function rawStyleValue(el: HTMLElement, prop: string): string {
|
|
14
|
+
const attr = el.getAttribute('style')
|
|
15
|
+
if (!attr) return ''
|
|
16
|
+
const match = new RegExp(`(?:^|;)\\s*${prop}\\s*:\\s*([^;]+)`, 'i').exec(attr)
|
|
17
|
+
return match ? match[1].trim() : ''
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeColorToHex(color: string): string {
|
|
21
|
+
if (!color) return color;
|
|
22
|
+
const trimmed = color.trim();
|
|
23
|
+
// Se respeta la caja que escribió el autor: pasarlo a minúsculas convertía
|
|
24
|
+
// `[color=#FF00AA]` en `[color=#ff00aa]` en cada ida y vuelta por el lienzo.
|
|
25
|
+
if (trimmed.startsWith('#')) return trimmed;
|
|
26
|
+
|
|
27
|
+
const rgbMatch = trimmed.match(/^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)$/i);
|
|
28
|
+
if (rgbMatch) {
|
|
29
|
+
const r = parseInt(rgbMatch[1], 10).toString(16).padStart(2, '0');
|
|
30
|
+
const g = parseInt(rgbMatch[2], 10).toString(16).padStart(2, '0');
|
|
31
|
+
const b = parseInt(rgbMatch[3], 10).toString(16).padStart(2, '0');
|
|
32
|
+
return `#${r}${g}${b}`.toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return trimmed;
|
|
36
|
+
}
|
|
37
|
+
|
|
6
38
|
function domToGreenTree(root: HTMLElement): GreenNode {
|
|
7
39
|
let currentOffset = 0;
|
|
8
40
|
|
|
@@ -44,25 +76,74 @@ function domToGreenTree(root: HTMLElement): GreenNode {
|
|
|
44
76
|
|
|
45
77
|
switch (tag) {
|
|
46
78
|
case 'b':
|
|
47
|
-
case 'strong':
|
|
79
|
+
case 'strong': {
|
|
80
|
+
// El renderer envuelve las entidades en `<strong><a>`; sin mirar la
|
|
81
|
+
// marca volvían desazucaradas como `[b][url=…]…[/url][/b]`.
|
|
82
|
+
const entity = el.getAttribute('data-entity');
|
|
83
|
+
if (entity === 'profile' || entity === 'guild' || entity === 'map') {
|
|
84
|
+
kind = entity;
|
|
85
|
+
const label = el.textContent || '';
|
|
86
|
+
if (label) {
|
|
87
|
+
children.push(greenLeaf('text', label));
|
|
88
|
+
currentOffset += label.length;
|
|
89
|
+
}
|
|
90
|
+
// `[profile=5458323]ElMick33[/profile]` lleva el id en el atributo y
|
|
91
|
+
// el nombre en el contenido; sin el primero volvía como `[profile]`.
|
|
92
|
+
const valor = el.getAttribute('data-entity-value') || '';
|
|
93
|
+
return greenNode(kind, valor ? `=${valor}` : '', children);
|
|
94
|
+
}
|
|
95
|
+
kind = 'bold';
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
48
98
|
case 'i':
|
|
49
99
|
case 'em': kind = 'italic'; break;
|
|
50
100
|
case 'u': kind = 'underline'; break;
|
|
51
101
|
case 's':
|
|
52
102
|
case 'strike':
|
|
53
103
|
case 'del': kind = 'strikethrough'; break;
|
|
54
|
-
case 'a':
|
|
55
|
-
|
|
56
|
-
|
|
104
|
+
case 'a': {
|
|
105
|
+
const rawHref = el.getAttribute('href') || '';
|
|
106
|
+
if (rawHref.startsWith('mailto:')) {
|
|
107
|
+
// `[email]` guarda la dirección en el hijo de texto y sólo el prefijo
|
|
108
|
+
// en el atributo; el exporter reconstruye el resto.
|
|
109
|
+
kind = 'email';
|
|
110
|
+
text = '=mailto:';
|
|
111
|
+
} else {
|
|
112
|
+
kind = 'url';
|
|
113
|
+
text = `=${rawHref}`;
|
|
114
|
+
}
|
|
57
115
|
break;
|
|
58
|
-
|
|
116
|
+
}
|
|
117
|
+
case 'img': {
|
|
118
|
+
// El parser de BBCode deja la URL en un hijo de texto y el modificador
|
|
119
|
+
// en el atributo. Copiando esa forma el exporter la vuelve a emitir;
|
|
120
|
+
// cuando la URL vivía sólo en `text` salía `[img][/img]`.
|
|
59
121
|
kind = 'image';
|
|
60
|
-
|
|
122
|
+
const imgAttr = el.getAttribute('data-img-attr') || '';
|
|
123
|
+
text = imgAttr ? `=${imgAttr}` : '';
|
|
124
|
+
const imgSrc = el.getAttribute('src') || '';
|
|
125
|
+
if (imgSrc) {
|
|
126
|
+
children.push(greenLeaf('text', imgSrc));
|
|
127
|
+
currentOffset += imgSrc.length;
|
|
128
|
+
}
|
|
61
129
|
break;
|
|
130
|
+
}
|
|
62
131
|
case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
|
|
63
132
|
kind = 'heading';
|
|
64
|
-
|
|
65
|
-
|
|
133
|
+
// Un `[heading]` pelado se pinta como h2 por convención del renderer;
|
|
134
|
+
// devolverlo como `[heading=2]` reescribía el source del autor.
|
|
135
|
+
text = el.hasAttribute('data-bare-level') ? '' : `=${parseInt(tag.charAt(1))}`;
|
|
136
|
+
break;
|
|
137
|
+
case 'font':
|
|
138
|
+
if (el.getAttribute('color')) {
|
|
139
|
+
kind = 'color';
|
|
140
|
+
text = `=${normalizeColorToHex(el.getAttribute('color') || '')}`;
|
|
141
|
+
} else if (el.getAttribute('size')) {
|
|
142
|
+
kind = 'font_size';
|
|
143
|
+
text = `=${el.getAttribute('size') || ''}`;
|
|
144
|
+
} else {
|
|
145
|
+
kind = 'group';
|
|
146
|
+
}
|
|
66
147
|
break;
|
|
67
148
|
case 'br':
|
|
68
149
|
kind = 'spacing';
|
|
@@ -70,9 +151,21 @@ function domToGreenTree(root: HTMLElement): GreenNode {
|
|
|
70
151
|
currentOffset += 1;
|
|
71
152
|
break;
|
|
72
153
|
case 'div':
|
|
154
|
+
case 'p': {
|
|
155
|
+
// Un aviso de medio vacío vuelve a ser su etiqueta, no la frase que
|
|
156
|
+
// el renderer puso en su sitio. El atributo lleva el nombre BBCode;
|
|
157
|
+
// aquí hace falta el `NodeKind`, que no siempre coincide.
|
|
158
|
+
const vacio = el.getAttribute('data-bb-empty');
|
|
159
|
+
if (vacio) {
|
|
160
|
+
const KIND_DE_TAG: Record<string, string> = { img: 'image', youtube: 'video', imagemap: 'imagemap' };
|
|
161
|
+
return greenNode(KIND_DE_TAG[vacio] ?? vacio, '', []);
|
|
162
|
+
}
|
|
73
163
|
if (el.classList.contains('notice')) kind = 'notice';
|
|
164
|
+
else if (el.classList.contains('bb-empty-line') || el.classList.contains('bbcode-para')) {
|
|
165
|
+
const hasText = (el.textContent || '').replace(/\u200B/g, '').trim().length > 0;
|
|
166
|
+
kind = hasText ? 'paragraph' : 'empty_line';
|
|
167
|
+
}
|
|
74
168
|
else if (el.classList.contains('bbcode-sp')) kind = 'spacing';
|
|
75
|
-
else if (el.classList.contains('bbcode-para')) kind = 'empty_line';
|
|
76
169
|
else if (el.classList.contains('bbcode-imagemap') || el.classList.contains('imagemap-container')) {
|
|
77
170
|
kind = 'imagemap';
|
|
78
171
|
const img = el.querySelector('img');
|
|
@@ -101,41 +194,56 @@ function domToGreenTree(root: HTMLElement): GreenNode {
|
|
|
101
194
|
else if (el.style.textAlign === 'left') kind = 'left';
|
|
102
195
|
else kind = 'group';
|
|
103
196
|
break;
|
|
197
|
+
}
|
|
104
198
|
case 'span':
|
|
105
|
-
if (el.classList.contains('
|
|
199
|
+
if (el.classList.contains('bb-empty-line')) {
|
|
200
|
+
const hasText = (el.textContent || '').replace(/\u200B/g, '').trim().length > 0;
|
|
201
|
+
kind = hasText ? 'paragraph' : 'empty_line';
|
|
202
|
+
}
|
|
203
|
+
else if (el.classList.contains('spoiler')) kind = 'spoiler';
|
|
106
204
|
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
|
-
|
|
110
|
-
|
|
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';
|
|
205
|
+
else if (el.style.color) { kind = 'color'; text = `=${normalizeColorToHex(rawStyleValue(el, 'color') || el.style.color)}`; }
|
|
206
|
+
else if (el.style.fontSize) { kind = 'font_size'; text = `=${(rawStyleValue(el, 'font-size') || el.style.fontSize).replace('%', '')}`; }
|
|
207
|
+
else if (el.classList.contains('bb-text') || el.classList.contains('bb-paragraph')) {
|
|
208
|
+
kind = 'bb_text_wrapper';
|
|
118
209
|
}
|
|
119
210
|
else kind = 'group';
|
|
120
211
|
break;
|
|
121
212
|
case 'iframe':
|
|
122
213
|
if (el.classList.contains('bb-youtube') || el.hasAttribute('data-youtube')) {
|
|
123
|
-
|
|
124
|
-
|
|
214
|
+
// El parser produce `video`, no `youtube`, y con el id en un hijo de
|
|
215
|
+
// texto. Emitir otra forma hacía que el exporter no encontrara el tag.
|
|
216
|
+
kind = 'video';
|
|
217
|
+
const videoId = el.getAttribute('data-youtube')
|
|
218
|
+
|| (el as HTMLIFrameElement).src.split('/embed/')[1]?.split('?')[0]
|
|
219
|
+
|| '';
|
|
220
|
+
if (videoId) {
|
|
221
|
+
children.push(greenLeaf('text', videoId));
|
|
222
|
+
currentOffset += videoId.length;
|
|
223
|
+
}
|
|
125
224
|
} else {
|
|
126
225
|
kind = 'group';
|
|
127
226
|
}
|
|
128
227
|
break;
|
|
129
|
-
case 'audio':
|
|
228
|
+
case 'audio': {
|
|
130
229
|
kind = 'audio';
|
|
131
|
-
|
|
230
|
+
const audioSrc = el.getAttribute('src') || '';
|
|
231
|
+
if (audioSrc) {
|
|
232
|
+
children.push(greenLeaf('text', audioSrc));
|
|
233
|
+
currentOffset += audioSrc.length;
|
|
234
|
+
}
|
|
132
235
|
break;
|
|
133
|
-
|
|
236
|
+
}
|
|
237
|
+
case 'details': {
|
|
134
238
|
kind = el.classList.contains('box') ? 'box' : 'spoilerbox';
|
|
135
|
-
|
|
136
|
-
|
|
239
|
+
// `data-bare-title` dice que el texto del summary lo puso el renderer.
|
|
240
|
+
// Leerlo como título del autor devolvía `[box=Box]` desde un `[box]`.
|
|
241
|
+
if (!el.hasAttribute('data-bare-title')) {
|
|
242
|
+
const summary = el.querySelector('summary');
|
|
243
|
+
if (summary) text = `=${summary.textContent || ''}`;
|
|
244
|
+
}
|
|
137
245
|
break;
|
|
138
|
-
|
|
246
|
+
}
|
|
139
247
|
case 'section':
|
|
140
248
|
kind = 'group';
|
|
141
249
|
break;
|
|
@@ -154,6 +262,8 @@ function domToGreenTree(root: HTMLElement): GreenNode {
|
|
|
154
262
|
const strong = firstElem.querySelector('strong');
|
|
155
263
|
if (strong && strong.textContent && strong.textContent.endsWith(' wrote:')) {
|
|
156
264
|
const author = strong.textContent.slice(0, -7);
|
|
265
|
+
// Clave `source`: es la que lee `BBCodeExporter.getTagAttributes`,
|
|
266
|
+
// que además vuelve a poner las comillas de `[quote="Nombre Largo"]`.
|
|
157
267
|
text = `=${author}`;
|
|
158
268
|
(firstElem as any).__quasar_extracted = true;
|
|
159
269
|
}
|
|
@@ -162,7 +272,16 @@ function domToGreenTree(root: HTMLElement): GreenNode {
|
|
|
162
272
|
case 'pre':
|
|
163
273
|
kind = 'code';
|
|
164
274
|
// Extract raw text as a child text node (DocumentEngine expects code content in children)
|
|
165
|
-
|
|
275
|
+
let codeText = el.textContent || '';
|
|
276
|
+
const padAttr = el.getAttribute('data-code-pad');
|
|
277
|
+
if (padAttr) {
|
|
278
|
+
try {
|
|
279
|
+
const [lead, trail] = (JSON.parse(padAttr) as string).split('\u0000');
|
|
280
|
+
codeText = `${lead}${codeText}${trail}`;
|
|
281
|
+
} catch {
|
|
282
|
+
// Atributo corrupto: mejor el contenido pelado que perder el bloque.
|
|
283
|
+
}
|
|
284
|
+
}
|
|
166
285
|
children.push(greenLeaf('text', codeText));
|
|
167
286
|
currentOffset += codeText.length;
|
|
168
287
|
break;
|
|
@@ -213,21 +332,43 @@ function domToGreenTree(root: HTMLElement): GreenNode {
|
|
|
213
332
|
const normalizedRoot: GreenNode[] = [];
|
|
214
333
|
let currentParagraph: GreenNode[] = [];
|
|
215
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Separa dos nodos de raíz con un solo salto.
|
|
337
|
+
*
|
|
338
|
+
* Nunca inventa una línea en blanco: una línea vacía del documento sólo
|
|
339
|
+
* existe si el autor la escribió, y el lienzo no puede añadir ninguna por su
|
|
340
|
+
* cuenta al traducir de vuelta.
|
|
341
|
+
*/
|
|
342
|
+
const pushSeparator = () => {
|
|
343
|
+
if (normalizedRoot.length === 0) return;
|
|
344
|
+
const prev = normalizedRoot[normalizedRoot.length - 1];
|
|
345
|
+
if (prev.kind === 'empty_line' || prev.kind === 'spacing') return;
|
|
346
|
+
normalizedRoot.push(greenLeaf('spacing', '\n'));
|
|
347
|
+
};
|
|
348
|
+
|
|
216
349
|
const flushParagraph = () => {
|
|
217
350
|
if (currentParagraph.length > 0) {
|
|
351
|
+
pushSeparator();
|
|
218
352
|
normalizedRoot.push(greenNode('paragraph', '', currentParagraph));
|
|
219
353
|
currentParagraph = [];
|
|
220
354
|
}
|
|
221
355
|
};
|
|
222
356
|
|
|
223
357
|
for (const child of rootChildren) {
|
|
224
|
-
// If it's a completely empty whitespace text node (0 newlines), ignore it
|
|
225
358
|
if (child.kind === 'text' && child.text.trim() === '') {
|
|
359
|
+
// Un espacio suelto entre dos tags inline (`[s]a[/s] [spoiler]b[/spoiler]`)
|
|
360
|
+
// es contenido del autor y se conserva; el resto es sangría del HTML.
|
|
361
|
+
const isInlineGap = !child.text.includes('\n') && currentParagraph.length > 0;
|
|
362
|
+
if (!isInlineGap) continue;
|
|
363
|
+
currentParagraph.push(child);
|
|
226
364
|
continue;
|
|
227
365
|
}
|
|
228
366
|
|
|
229
|
-
if (isBlockKind(child.kind as NodeKind) || child.kind === 'empty_line') {
|
|
367
|
+
if (isBlockKind(child.kind as NodeKind) || child.kind === 'empty_line' || child.kind === 'paragraph') {
|
|
230
368
|
flushParagraph();
|
|
369
|
+
if (child.kind !== 'empty_line' && child.kind !== 'spacing') {
|
|
370
|
+
pushSeparator();
|
|
371
|
+
}
|
|
231
372
|
normalizedRoot.push(child);
|
|
232
373
|
} else {
|
|
233
374
|
currentParagraph.push(child);
|
|
@@ -253,20 +394,27 @@ export function greenToRedNode(green: GreenNode, parent?: RedNode | null): RedNo
|
|
|
253
394
|
let metadata: Record<string, unknown> = {};
|
|
254
395
|
if (green.kind === 'heading' && green.text.startsWith('=')) {
|
|
255
396
|
metadata = { level: parseInt(green.text.slice(1)) || 1 };
|
|
256
|
-
} else if (green.kind === 'url' && green.text.startsWith('=')) {
|
|
397
|
+
} else if ((green.kind === 'url' || green.kind === 'email') && green.text.startsWith('=')) {
|
|
257
398
|
metadata = { href: green.text.slice(1) };
|
|
258
399
|
} else if (green.kind === 'color' && green.text.startsWith('=')) {
|
|
259
400
|
metadata = { color: green.text.slice(1) };
|
|
260
401
|
} else if (green.kind === 'font_size' && green.text.startsWith('=')) {
|
|
261
402
|
metadata = { size: green.text.slice(1) };
|
|
262
403
|
} else if (green.kind === 'quote' && green.text.startsWith('=')) {
|
|
263
|
-
metadata = {
|
|
404
|
+
metadata = { source: green.text.slice(1) };
|
|
264
405
|
} else if ((green.kind === 'box' || green.kind === 'spoilerbox') && green.text.startsWith('=')) {
|
|
265
406
|
metadata = { title: green.text.slice(1) };
|
|
266
407
|
} else if (green.kind === 'image') {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
408
|
+
const src = green.children.map(c => (c as GreenNode).text || '').join('');
|
|
409
|
+
metadata = green.text.startsWith('=')
|
|
410
|
+
? { src, imgAttr: green.text.slice(1) }
|
|
411
|
+
: { src };
|
|
412
|
+
} else if (green.kind === 'video' || green.kind === 'audio') {
|
|
413
|
+
const value = green.children.map(c => (c as GreenNode).text || '').join('');
|
|
414
|
+
metadata = green.kind === 'video' ? { videoId: value } : { src: value };
|
|
415
|
+
} else if (green.kind === 'profile' || green.kind === 'guild' || green.kind === 'map') {
|
|
416
|
+
const key = green.kind === 'profile' ? 'username' : green.kind === 'guild' ? 'tag' : 'id';
|
|
417
|
+
metadata = { [key]: green.text.startsWith('=') ? green.text.slice(1) : '' };
|
|
270
418
|
} else if (green.kind === 'list') {
|
|
271
419
|
metadata = { ordered: green.text === '=1' };
|
|
272
420
|
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocumentEngine — BracketDepthIndex
|
|
3
|
+
*
|
|
4
|
+
* Answers the incremental parser's one question about the text OUTSIDE its
|
|
5
|
+
* window — "does every `[` before this offset find its `]` before it too?" —
|
|
6
|
+
* without reading the text before the offset.
|
|
7
|
+
*
|
|
8
|
+
* ─── Why the scan had to go ─────────────────────────────────────────────────
|
|
9
|
+
*
|
|
10
|
+
* `bracketsCloseBefore` walked the whole prefix on every keystroke: with the
|
|
11
|
+
* caret at the end of the 547 KB fixture that is 547.000 `charCodeAt` calls
|
|
12
|
+
* per key, measured at 2.2 ms — more than the parse, the splice and the red
|
|
13
|
+
* build of that same keystroke put together. Caching was declined once on the
|
|
14
|
+
* grounds that the reasoning is subtle: an edit can expose a `]` that a
|
|
15
|
+
* deleted `[` used to claim, so any cache keyed on "the prefix has not
|
|
16
|
+
* changed" is wrong the moment it is asked about a prefix that has.
|
|
17
|
+
*
|
|
18
|
+
* ─── Why this is not that cache ─────────────────────────────────────────────
|
|
19
|
+
*
|
|
20
|
+
* The scan computes a CLAMPED running depth — `d ← max(0, d + a)` where `a`
|
|
21
|
+
* is +1 for `[`, −1 for `]`, 0 otherwise — and a clamped running sum has an
|
|
22
|
+
* exact, composable summary. Over any stretch of text with prefix sums
|
|
23
|
+
* P₁…Pₙ, the depth after the stretch is a function of the depth before it:
|
|
24
|
+
*
|
|
25
|
+
* d' = Pₙ + max(d, −min(0, P₁, …, Pₙ))
|
|
26
|
+
*
|
|
27
|
+
* so a stretch is fully described by two integers, its net sum and its lowest
|
|
28
|
+
* dip. The index keeps the source as a sequence of PIECES (a few KB each),
|
|
29
|
+
* each carrying that pair. An edit re-summarises only the piece(s) it touched,
|
|
30
|
+
* from the NEW text, and every other piece keeps a summary that is still
|
|
31
|
+
* exactly true of its (unchanged, merely displaced) characters. A query walks
|
|
32
|
+
* the piece summaries up to the offset and scans only the tail of the piece
|
|
33
|
+
* the offset falls in. Nothing here is a guess about what the edit exposed:
|
|
34
|
+
* the touched piece is re-read verbatim, and composition is arithmetic.
|
|
35
|
+
*
|
|
36
|
+
* Cost per keystroke: one summary of ≤ 8 KB plus one walk over ~140 piece
|
|
37
|
+
* summaries at 547 KB — a few microseconds, and independent of where the
|
|
38
|
+
* caret is. Verified against the plain scan by `BracketIndex.test.ts`: 2.000
|
|
39
|
+
* random edits over documents full of stray brackets, every offset agreeing.
|
|
40
|
+
*
|
|
41
|
+
* The index does not know which document it describes; the parser keys it on
|
|
42
|
+
* the green root it was last synchronised with and rebuilds it (one scan) when
|
|
43
|
+
* the root it is handed is not that one. See `IncrementalParser.reparse`.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/** Pieces are re-cut to this size when an edit grows one past `MAX_PIECE`. */
|
|
47
|
+
const TARGET_PIECE = 4096
|
|
48
|
+
/** A piece may grow to this before it is split. */
|
|
49
|
+
const MAX_PIECE = TARGET_PIECE * 2
|
|
50
|
+
/** A piece that shrinks below this merges into a neighbour when that fits. */
|
|
51
|
+
const MIN_PIECE = TARGET_PIECE / 4
|
|
52
|
+
|
|
53
|
+
export class BracketDepthIndex {
|
|
54
|
+
/** Length of each piece, in characters. Pieces partition `[0, length)`. */
|
|
55
|
+
private lengths: number[] = []
|
|
56
|
+
/** Net bracket sum of each piece: `[` counts +1, `]` counts −1. */
|
|
57
|
+
private sums: number[] = []
|
|
58
|
+
/** Lowest prefix sum inside each piece, capped at 0 (see the header). */
|
|
59
|
+
private dips: number[] = []
|
|
60
|
+
private _length = 0
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Characters read by the last `depthAt` — the tail of one piece, never the
|
|
64
|
+
* prefix. Exposed so a regression back to an O(prefix) scan is something a
|
|
65
|
+
* test can assert on, rather than something a profile has to notice.
|
|
66
|
+
*/
|
|
67
|
+
lastScanned = 0
|
|
68
|
+
|
|
69
|
+
/** The length of the text this index currently describes. */
|
|
70
|
+
get length(): number {
|
|
71
|
+
return this._length
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Number of pieces — for tests; it should stay ~`length / TARGET_PIECE`. */
|
|
75
|
+
get pieceCount(): number {
|
|
76
|
+
return this.lengths.length
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Describe `source` from scratch. O(n) — one scan, the same the query used to be. */
|
|
80
|
+
rebuild(source: string): void {
|
|
81
|
+
this.lengths.length = 0
|
|
82
|
+
this.sums.length = 0
|
|
83
|
+
this.dips.length = 0
|
|
84
|
+
this._length = source.length
|
|
85
|
+
for (let at = 0; at < source.length; at += TARGET_PIECE) {
|
|
86
|
+
this.pushPiece(source, at, Math.min(source.length, at + TARGET_PIECE))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Bring the index from the text before an edit to `newSource`, the text
|
|
92
|
+
* after it. `start`/`endOld` bound the replaced region in OLD coordinates;
|
|
93
|
+
* `insertedLength` is the length of the text that replaced it.
|
|
94
|
+
*
|
|
95
|
+
* Only the pieces overlapping the replaced region are re-read. They are
|
|
96
|
+
* re-summarised as one piece over the same span in NEW coordinates, and
|
|
97
|
+
* that piece is split if it has grown past `MAX_PIECE` — so a long paste
|
|
98
|
+
* ends up as several normal pieces, not one giant one that every later
|
|
99
|
+
* query near it would have to scan.
|
|
100
|
+
*/
|
|
101
|
+
applyChange(newSource: string, start: number, endOld: number, insertedLength: number): void {
|
|
102
|
+
const delta = insertedLength - (endOld - start)
|
|
103
|
+
const oldLength = this._length
|
|
104
|
+
if (endOld > oldLength || start > endOld || newSource.length !== oldLength + delta) {
|
|
105
|
+
// The caller's picture of the previous text and ours disagree. There is
|
|
106
|
+
// no way to reconcile them locally; describe the new text outright.
|
|
107
|
+
this.rebuild(newSource)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The piece containing `start` — the LAST piece when the edit sits at the
|
|
112
|
+
// very end — and the piece whose closed span `[pieceStart, pieceEnd]`
|
|
113
|
+
// contains `endOld`, so an edit that ends exactly on a boundary does not
|
|
114
|
+
// reach into the piece after it.
|
|
115
|
+
const lengths = this.lengths
|
|
116
|
+
let i = 0
|
|
117
|
+
let pieceStart = 0
|
|
118
|
+
while (i < lengths.length - 1 && pieceStart + lengths[i] <= start) {
|
|
119
|
+
pieceStart += lengths[i]
|
|
120
|
+
i++
|
|
121
|
+
}
|
|
122
|
+
let j = i
|
|
123
|
+
let pieceEnd = pieceStart + (lengths.length === 0 ? 0 : lengths[i])
|
|
124
|
+
while (j < lengths.length - 1 && pieceEnd < endOld) {
|
|
125
|
+
j++
|
|
126
|
+
pieceEnd += lengths[j]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const newEnd = pieceEnd + delta
|
|
130
|
+
const removed = lengths.length === 0 ? 0 : j - i + 1
|
|
131
|
+
this.spliceSummaries(i, removed, newSource, pieceStart, newEnd)
|
|
132
|
+
this._length = newSource.length
|
|
133
|
+
|
|
134
|
+
// Keep the pieces around the edit from dwindling: a piece that lost most
|
|
135
|
+
// of its text merges into a neighbour so the piece count stays bounded by
|
|
136
|
+
// the text length, not by the number of edits it has seen.
|
|
137
|
+
this.mergeSmallAround(i)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The clamped bracket depth just before `end` — what the plain scan of
|
|
142
|
+
* `[0, end)` would return. `end === 0` is depth 0; `end > length` is
|
|
143
|
+
* clamped to the text.
|
|
144
|
+
*/
|
|
145
|
+
depthAt(source: string, end: number): number {
|
|
146
|
+
if (end > this._length) end = this._length
|
|
147
|
+
const lengths = this.lengths
|
|
148
|
+
let depth = 0
|
|
149
|
+
let pos = 0
|
|
150
|
+
let i = 0
|
|
151
|
+
// Whole pieces before `end`: compose their summaries.
|
|
152
|
+
for (; i < lengths.length && pos + lengths[i] <= end; i++) {
|
|
153
|
+
const dip = this.dips[i]
|
|
154
|
+
depth = this.sums[i] + (depth > -dip ? depth : -dip)
|
|
155
|
+
pos += lengths[i]
|
|
156
|
+
}
|
|
157
|
+
// The tail of the piece `end` falls in: scan it.
|
|
158
|
+
this.lastScanned = end - pos
|
|
159
|
+
for (let k = pos; k < end; k++) {
|
|
160
|
+
const c = source.charCodeAt(k)
|
|
161
|
+
if (c === 91 /* [ */) depth++
|
|
162
|
+
else if (c === 93 /* ] */ && depth > 0) depth--
|
|
163
|
+
}
|
|
164
|
+
return depth
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Summarise `[from, to)` of `source` and append it as a piece. */
|
|
168
|
+
private pushPiece(source: string, from: number, to: number): void {
|
|
169
|
+
let sum = 0
|
|
170
|
+
let dip = 0
|
|
171
|
+
for (let k = from; k < to; k++) {
|
|
172
|
+
const c = source.charCodeAt(k)
|
|
173
|
+
if (c === 91 /* [ */) sum++
|
|
174
|
+
else if (c === 93 /* ] */) {
|
|
175
|
+
sum--
|
|
176
|
+
if (sum < dip) dip = sum
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
this.lengths.push(to - from)
|
|
180
|
+
this.sums.push(sum)
|
|
181
|
+
this.dips.push(dip)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Replace `count` pieces at `index` with the summaries of `[from, to)` of
|
|
186
|
+
* `source`, cut into pieces of at most `MAX_PIECE`.
|
|
187
|
+
*/
|
|
188
|
+
private spliceSummaries(index: number, count: number, source: string, from: number, to: number): void {
|
|
189
|
+
const savedLengths = this.lengths
|
|
190
|
+
const savedSums = this.sums
|
|
191
|
+
const savedDips = this.dips
|
|
192
|
+
this.lengths = []
|
|
193
|
+
this.sums = []
|
|
194
|
+
this.dips = []
|
|
195
|
+
const span = to - from
|
|
196
|
+
if (span > MAX_PIECE) {
|
|
197
|
+
const parts = Math.ceil(span / TARGET_PIECE)
|
|
198
|
+
const step = Math.ceil(span / parts)
|
|
199
|
+
for (let at = from; at < to; at += step) this.pushPiece(source, at, Math.min(to, at + step))
|
|
200
|
+
} else if (span > 0 || savedLengths.length === 0) {
|
|
201
|
+
// An empty document keeps one empty piece so `applyChange` always has
|
|
202
|
+
// a piece to anchor on; anywhere else an emptied piece simply goes.
|
|
203
|
+
this.pushPiece(source, from, to)
|
|
204
|
+
}
|
|
205
|
+
const fresh = this.lengths
|
|
206
|
+
const freshSums = this.sums
|
|
207
|
+
const freshDips = this.dips
|
|
208
|
+
savedLengths.splice(index, count, ...fresh)
|
|
209
|
+
savedSums.splice(index, count, ...freshSums)
|
|
210
|
+
savedDips.splice(index, count, ...freshDips)
|
|
211
|
+
this.lengths = savedLengths
|
|
212
|
+
this.sums = savedSums
|
|
213
|
+
this.dips = savedDips
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Merge the piece at `index` (and the one after it) into a neighbour when
|
|
218
|
+
* it has shrunk below `MIN_PIECE` and the pair stays under `MAX_PIECE`.
|
|
219
|
+
*
|
|
220
|
+
* Merging two summaries is the composition from the header applied to the
|
|
221
|
+
* pair: the sum adds, and the dip of the second is measured from the end of
|
|
222
|
+
* the first. No text is read.
|
|
223
|
+
*/
|
|
224
|
+
private mergeSmallAround(index: number): void {
|
|
225
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
226
|
+
const i = Math.min(index, this.lengths.length - 1)
|
|
227
|
+
if (i < 0) return
|
|
228
|
+
if (this.lengths[i] >= MIN_PIECE || this.lengths.length < 2) return
|
|
229
|
+
// Prefer the previous neighbour (it keeps `index` valid for the next pass).
|
|
230
|
+
const left = i > 0 && this.lengths[i - 1] + this.lengths[i] <= MAX_PIECE
|
|
231
|
+
const right = i + 1 < this.lengths.length && this.lengths[i] + this.lengths[i + 1] <= MAX_PIECE
|
|
232
|
+
if (left) this.mergePair(i - 1)
|
|
233
|
+
else if (right) this.mergePair(i)
|
|
234
|
+
else return
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Fold piece `i + 1` into piece `i`. */
|
|
239
|
+
private mergePair(i: number): void {
|
|
240
|
+
const sumA = this.sums[i]
|
|
241
|
+
const dipB = this.dips[i + 1] + sumA
|
|
242
|
+
this.dips[i] = Math.min(this.dips[i], dipB)
|
|
243
|
+
this.sums[i] = sumA + this.sums[i + 1]
|
|
244
|
+
this.lengths[i] += this.lengths[i + 1]
|
|
245
|
+
this.lengths.splice(i + 1, 1)
|
|
246
|
+
this.sums.splice(i + 1, 1)
|
|
247
|
+
this.dips.splice(i + 1, 1)
|
|
248
|
+
}
|
|
249
|
+
}
|