@miliastry/quasar 1.0.6 → 1.2.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/README.md +1 -1
- package/dist/Visuals/lyne.css +374 -9
- package/dist/Visuals/osu.css +393 -11
- package/dist/index.js +10470 -3336
- package/dist/index.mjs +10336 -3337
- 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 +102 -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 +19 -0
- package/dist/types/Visitors/HTMLRenderer.d.ts +492 -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 +54 -0
- package/dist/types/Visuals/LyneAudio.d.ts +8 -0
- package/dist/types/Visuals/index.d.ts +39 -0
- package/dist/types/index.d.ts +155 -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 +39 -1
- 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 +193 -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 +1845 -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 +365 -32
- package/src/Visitors/BlockPatcher.ts +123 -22
- package/src/Visitors/DOMMorpher.ts +44 -28
- package/src/Visitors/HTMLRenderer.ts +771 -205
- package/src/Visitors/MarkdownExporter.ts +98 -41
- package/src/Visitors/TiptapExporter.ts +1 -0
- package/src/Visitors/index.ts +1 -1
- package/src/Visuals/BoxDrawer.ts +56 -10
- package/src/Visuals/LyneAudio.ts +346 -0
- package/src/Visuals/index.ts +8 -4
- package/src/Visuals/lyne.css +374 -9
- package/src/Visuals/osu.css +393 -11
- package/src/index.ts +178 -8
- 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');
|
|
@@ -96,46 +189,69 @@ function domToGreenTree(root: HTMLElement): GreenNode {
|
|
|
96
189
|
}
|
|
97
190
|
});
|
|
98
191
|
}
|
|
192
|
+
else if (el.classList.contains('lx-audio')) {
|
|
193
|
+
kind = 'audio';
|
|
194
|
+
const audioSrc = el.getAttribute('data-src') || el.querySelector('audio')?.getAttribute('src') || '';
|
|
195
|
+
if (audioSrc) {
|
|
196
|
+
children.push(greenLeaf('text', audioSrc));
|
|
197
|
+
currentOffset += audioSrc.length;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
99
200
|
else if (el.style.textAlign === 'center') kind = 'center';
|
|
100
201
|
else if (el.style.textAlign === 'right') kind = 'right';
|
|
101
202
|
else if (el.style.textAlign === 'left') kind = 'left';
|
|
102
203
|
else kind = 'group';
|
|
103
204
|
break;
|
|
205
|
+
}
|
|
104
206
|
case 'span':
|
|
105
|
-
if (el.classList.contains('
|
|
207
|
+
if (el.classList.contains('bb-empty-line')) {
|
|
208
|
+
const hasText = (el.textContent || '').replace(/\u200B/g, '').trim().length > 0;
|
|
209
|
+
kind = hasText ? 'paragraph' : 'empty_line';
|
|
210
|
+
}
|
|
211
|
+
else if (el.classList.contains('spoiler')) kind = 'spoiler';
|
|
106
212
|
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';
|
|
213
|
+
else if (el.style.color) { kind = 'color'; text = `=${normalizeColorToHex(rawStyleValue(el, 'color') || el.style.color)}`; }
|
|
214
|
+
else if (el.style.fontSize) { kind = 'font_size'; text = `=${(rawStyleValue(el, 'font-size') || el.style.fontSize).replace('%', '')}`; }
|
|
215
|
+
else if (el.classList.contains('bb-text') || el.classList.contains('bb-paragraph')) {
|
|
216
|
+
kind = 'bb_text_wrapper';
|
|
118
217
|
}
|
|
119
218
|
else kind = 'group';
|
|
120
219
|
break;
|
|
121
220
|
case 'iframe':
|
|
122
221
|
if (el.classList.contains('bb-youtube') || el.hasAttribute('data-youtube')) {
|
|
123
|
-
|
|
124
|
-
|
|
222
|
+
// El parser produce `video`, no `youtube`, y con el id en un hijo de
|
|
223
|
+
// texto. Emitir otra forma hacía que el exporter no encontrara el tag.
|
|
224
|
+
kind = 'video';
|
|
225
|
+
const videoId = el.getAttribute('data-youtube')
|
|
226
|
+
|| (el as HTMLIFrameElement).src.split('/embed/')[1]?.split('?')[0]
|
|
227
|
+
|| '';
|
|
228
|
+
if (videoId) {
|
|
229
|
+
children.push(greenLeaf('text', videoId));
|
|
230
|
+
currentOffset += videoId.length;
|
|
231
|
+
}
|
|
125
232
|
} else {
|
|
126
233
|
kind = 'group';
|
|
127
234
|
}
|
|
128
235
|
break;
|
|
129
|
-
case 'audio':
|
|
236
|
+
case 'audio': {
|
|
130
237
|
kind = 'audio';
|
|
131
|
-
|
|
238
|
+
const audioSrc = el.getAttribute('src') || '';
|
|
239
|
+
if (audioSrc) {
|
|
240
|
+
children.push(greenLeaf('text', audioSrc));
|
|
241
|
+
currentOffset += audioSrc.length;
|
|
242
|
+
}
|
|
132
243
|
break;
|
|
133
|
-
|
|
244
|
+
}
|
|
245
|
+
case 'details': {
|
|
134
246
|
kind = el.classList.contains('box') ? 'box' : 'spoilerbox';
|
|
135
|
-
|
|
136
|
-
|
|
247
|
+
// `data-bare-title` dice que el texto del summary lo puso el renderer.
|
|
248
|
+
// Leerlo como título del autor devolvía `[box=Box]` desde un `[box]`.
|
|
249
|
+
if (!el.hasAttribute('data-bare-title')) {
|
|
250
|
+
const summary = el.querySelector('summary');
|
|
251
|
+
if (summary) text = `=${summary.textContent || ''}`;
|
|
252
|
+
}
|
|
137
253
|
break;
|
|
138
|
-
|
|
254
|
+
}
|
|
139
255
|
case 'section':
|
|
140
256
|
kind = 'group';
|
|
141
257
|
break;
|
|
@@ -154,6 +270,8 @@ function domToGreenTree(root: HTMLElement): GreenNode {
|
|
|
154
270
|
const strong = firstElem.querySelector('strong');
|
|
155
271
|
if (strong && strong.textContent && strong.textContent.endsWith(' wrote:')) {
|
|
156
272
|
const author = strong.textContent.slice(0, -7);
|
|
273
|
+
// Clave `source`: es la que lee `BBCodeExporter.getTagAttributes`,
|
|
274
|
+
// que además vuelve a poner las comillas de `[quote="Nombre Largo"]`.
|
|
157
275
|
text = `=${author}`;
|
|
158
276
|
(firstElem as any).__quasar_extracted = true;
|
|
159
277
|
}
|
|
@@ -162,7 +280,16 @@ function domToGreenTree(root: HTMLElement): GreenNode {
|
|
|
162
280
|
case 'pre':
|
|
163
281
|
kind = 'code';
|
|
164
282
|
// Extract raw text as a child text node (DocumentEngine expects code content in children)
|
|
165
|
-
|
|
283
|
+
let codeText = el.textContent || '';
|
|
284
|
+
const padAttr = el.getAttribute('data-code-pad');
|
|
285
|
+
if (padAttr) {
|
|
286
|
+
try {
|
|
287
|
+
const [lead, trail] = (JSON.parse(padAttr) as string).split('\u0000');
|
|
288
|
+
codeText = `${lead}${codeText}${trail}`;
|
|
289
|
+
} catch {
|
|
290
|
+
// Atributo corrupto: mejor el contenido pelado que perder el bloque.
|
|
291
|
+
}
|
|
292
|
+
}
|
|
166
293
|
children.push(greenLeaf('text', codeText));
|
|
167
294
|
currentOffset += codeText.length;
|
|
168
295
|
break;
|
|
@@ -213,21 +340,43 @@ function domToGreenTree(root: HTMLElement): GreenNode {
|
|
|
213
340
|
const normalizedRoot: GreenNode[] = [];
|
|
214
341
|
let currentParagraph: GreenNode[] = [];
|
|
215
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Separa dos nodos de raíz con un solo salto.
|
|
345
|
+
*
|
|
346
|
+
* Nunca inventa una línea en blanco: una línea vacía del documento sólo
|
|
347
|
+
* existe si el autor la escribió, y el lienzo no puede añadir ninguna por su
|
|
348
|
+
* cuenta al traducir de vuelta.
|
|
349
|
+
*/
|
|
350
|
+
const pushSeparator = () => {
|
|
351
|
+
if (normalizedRoot.length === 0) return;
|
|
352
|
+
const prev = normalizedRoot[normalizedRoot.length - 1];
|
|
353
|
+
if (prev.kind === 'empty_line' || prev.kind === 'spacing') return;
|
|
354
|
+
normalizedRoot.push(greenLeaf('spacing', '\n'));
|
|
355
|
+
};
|
|
356
|
+
|
|
216
357
|
const flushParagraph = () => {
|
|
217
358
|
if (currentParagraph.length > 0) {
|
|
359
|
+
pushSeparator();
|
|
218
360
|
normalizedRoot.push(greenNode('paragraph', '', currentParagraph));
|
|
219
361
|
currentParagraph = [];
|
|
220
362
|
}
|
|
221
363
|
};
|
|
222
364
|
|
|
223
365
|
for (const child of rootChildren) {
|
|
224
|
-
// If it's a completely empty whitespace text node (0 newlines), ignore it
|
|
225
366
|
if (child.kind === 'text' && child.text.trim() === '') {
|
|
367
|
+
// Un espacio suelto entre dos tags inline (`[s]a[/s] [spoiler]b[/spoiler]`)
|
|
368
|
+
// es contenido del autor y se conserva; el resto es sangría del HTML.
|
|
369
|
+
const isInlineGap = !child.text.includes('\n') && currentParagraph.length > 0;
|
|
370
|
+
if (!isInlineGap) continue;
|
|
371
|
+
currentParagraph.push(child);
|
|
226
372
|
continue;
|
|
227
373
|
}
|
|
228
374
|
|
|
229
|
-
if (isBlockKind(child.kind as NodeKind) || child.kind === 'empty_line') {
|
|
375
|
+
if (isBlockKind(child.kind as NodeKind) || child.kind === 'empty_line' || child.kind === 'paragraph') {
|
|
230
376
|
flushParagraph();
|
|
377
|
+
if (child.kind !== 'empty_line' && child.kind !== 'spacing') {
|
|
378
|
+
pushSeparator();
|
|
379
|
+
}
|
|
231
380
|
normalizedRoot.push(child);
|
|
232
381
|
} else {
|
|
233
382
|
currentParagraph.push(child);
|
|
@@ -253,20 +402,27 @@ export function greenToRedNode(green: GreenNode, parent?: RedNode | null): RedNo
|
|
|
253
402
|
let metadata: Record<string, unknown> = {};
|
|
254
403
|
if (green.kind === 'heading' && green.text.startsWith('=')) {
|
|
255
404
|
metadata = { level: parseInt(green.text.slice(1)) || 1 };
|
|
256
|
-
} else if (green.kind === 'url' && green.text.startsWith('=')) {
|
|
405
|
+
} else if ((green.kind === 'url' || green.kind === 'email') && green.text.startsWith('=')) {
|
|
257
406
|
metadata = { href: green.text.slice(1) };
|
|
258
407
|
} else if (green.kind === 'color' && green.text.startsWith('=')) {
|
|
259
408
|
metadata = { color: green.text.slice(1) };
|
|
260
409
|
} else if (green.kind === 'font_size' && green.text.startsWith('=')) {
|
|
261
410
|
metadata = { size: green.text.slice(1) };
|
|
262
411
|
} else if (green.kind === 'quote' && green.text.startsWith('=')) {
|
|
263
|
-
metadata = {
|
|
412
|
+
metadata = { source: green.text.slice(1) };
|
|
264
413
|
} else if ((green.kind === 'box' || green.kind === 'spoilerbox') && green.text.startsWith('=')) {
|
|
265
414
|
metadata = { title: green.text.slice(1) };
|
|
266
415
|
} else if (green.kind === 'image') {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
416
|
+
const src = green.children.map(c => (c as GreenNode).text || '').join('');
|
|
417
|
+
metadata = green.text.startsWith('=')
|
|
418
|
+
? { src, imgAttr: green.text.slice(1) }
|
|
419
|
+
: { src };
|
|
420
|
+
} else if (green.kind === 'video' || green.kind === 'audio') {
|
|
421
|
+
const value = green.children.map(c => (c as GreenNode).text || '').join('');
|
|
422
|
+
metadata = green.kind === 'video' ? { videoId: value } : { src: value };
|
|
423
|
+
} else if (green.kind === 'profile' || green.kind === 'guild' || green.kind === 'map') {
|
|
424
|
+
const key = green.kind === 'profile' ? 'username' : green.kind === 'guild' ? 'tag' : 'id';
|
|
425
|
+
metadata = { [key]: green.text.startsWith('=') ? green.text.slice(1) : '' };
|
|
270
426
|
} else if (green.kind === 'list') {
|
|
271
427
|
metadata = { ordered: green.text === '=1' };
|
|
272
428
|
}
|
|
@@ -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
|
+
}
|