@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
|
@@ -56,8 +56,18 @@ import { RedNode } from '../Syntax/RedNode'
|
|
|
56
56
|
import { GreenNode } from '../Syntax/GreenNode'
|
|
57
57
|
import { spliceGreen, withChildrenSpliced, type SpineStep } from '../Syntax/greenEdit'
|
|
58
58
|
import { tagToNodeKind } from '../BBCode/BBCodeToGreenNode'
|
|
59
|
+
import { BracketDepthIndex } from './BracketIndex'
|
|
59
60
|
import type { TextChange } from './ChangeTracker'
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* A span of the source, `[start, end)`, in the coordinates of the text the
|
|
64
|
+
* result describes.
|
|
65
|
+
*/
|
|
66
|
+
export interface SourceSpan {
|
|
67
|
+
start: number
|
|
68
|
+
end: number
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
export interface EditOperation {
|
|
62
72
|
kind: 'insert' | 'delete' | 'replace'
|
|
63
73
|
start: number
|
|
@@ -97,6 +107,25 @@ export interface ReparseResult {
|
|
|
97
107
|
path: 'incremental' | 'full_rebuild'
|
|
98
108
|
/** When `full_rebuild`, why the incremental path was declined. */
|
|
99
109
|
reason?: FallbackReason
|
|
110
|
+
/**
|
|
111
|
+
* The span of `newSource` that went through the parser, in NEW coordinates,
|
|
112
|
+
* or `null` after a full rebuild (where the answer is "all of it").
|
|
113
|
+
*
|
|
114
|
+
* This is the contract the incremental semantic analysis is built on. The
|
|
115
|
+
* red tree that comes back is derived from a green tree that shares every
|
|
116
|
+
* subtree outside this span by reference with the previous one, so — with
|
|
117
|
+
* red-subtree reuse — every red node outside the span is the SAME object it
|
|
118
|
+
* was before the edit, and everything anyone computed about it (its
|
|
119
|
+
* diagnostics, above all) is still true of it, up to a shift in position.
|
|
120
|
+
* The nodes inside the span, plus the ancestors on the path down to it
|
|
121
|
+
* (rebuilt because their child lists changed), are the only ones that are
|
|
122
|
+
* new. `SemanticAnalyzer.analyzeWindow` re-validates exactly those.
|
|
123
|
+
*
|
|
124
|
+
* The span is closed on the widened window, not on the edit: the parser
|
|
125
|
+
* re-parses the sibling on each side of the change too (see
|
|
126
|
+
* `findReparseWindow`), and those siblings are new nodes as well.
|
|
127
|
+
*/
|
|
128
|
+
window: SourceSpan | null
|
|
100
129
|
}
|
|
101
130
|
|
|
102
131
|
export type FallbackReason =
|
|
@@ -108,6 +137,11 @@ export type FallbackReason =
|
|
|
108
137
|
| 'region-not-isolated'
|
|
109
138
|
/** An unclosed `[` before the region could claim a `]` the edit creates. */
|
|
110
139
|
| 'open-bracket-before'
|
|
140
|
+
/**
|
|
141
|
+
* The window changed what the parser's `autoClosed` set holds when the text
|
|
142
|
+
* after it is read — see `pendingAutoClosePreserved`.
|
|
143
|
+
*/
|
|
144
|
+
| 'pending-auto-close'
|
|
111
145
|
/** The container covers so much of the document that a rebuild is cheaper. */
|
|
112
146
|
| 'region-too-large'
|
|
113
147
|
/** The document is small enough that rebuilding it outright costs less. */
|
|
@@ -165,17 +199,296 @@ const MIN_SOURCE_LENGTH = 2500
|
|
|
165
199
|
/** An orphaned closing tag, as the parser preserves it: a `text` leaf of `[/tag]`. */
|
|
166
200
|
const ORPHAN_CLOSE_RE = /^\[\/([a-zA-Z0-9_*-]+)\]$/
|
|
167
201
|
|
|
202
|
+
/**
|
|
203
|
+
* What a stretch of tree does to the parser's `autoClosed` set.
|
|
204
|
+
*
|
|
205
|
+
* `autoClosed` is the one piece of parser state that flows RIGHTWARDS past a
|
|
206
|
+
* window without being visible in the tree at the window's edge (see
|
|
207
|
+
* `pendingAutoClosePreserved`). A stretch of text does three things to it:
|
|
208
|
+
*
|
|
209
|
+
* - ADDS a name, when a `[/x]` overtakes an inner `[y]` and closes it
|
|
210
|
+
* without its delimiter — the element ends up with `trailingWidth === 0`;
|
|
211
|
+
* - CONSUMES one, when a `[/y]` arrives while `y` is pending: the parser
|
|
212
|
+
* keeps it as a `discarded_tag` and drops the name;
|
|
213
|
+
* - DELETES one, on every opening tag, because reopening `[y]` retires the
|
|
214
|
+
* `y` that was still pending.
|
|
215
|
+
*/
|
|
216
|
+
interface AutoCloseProfile {
|
|
217
|
+
/**
|
|
218
|
+
* For each kind, the LAST thing the stretch did to it: `add` (a crossing
|
|
219
|
+
* left it pending) or `del` (an opening tag or a `discarded_tag` retired
|
|
220
|
+
* it). Only the last one matters — the operations are per-name and
|
|
221
|
+
* idempotent, so whatever happened before it has already been overwritten.
|
|
222
|
+
*/
|
|
223
|
+
last: Map<string, 'add' | 'del'>
|
|
224
|
+
/**
|
|
225
|
+
* The kinds still open when the stretch ENDS — the rightmost chain. Those
|
|
226
|
+
* are not adds yet: whatever closes them sits past the window and adds them
|
|
227
|
+
* all at once there, so they override everything in `last`.
|
|
228
|
+
*/
|
|
229
|
+
chain: Set<string>
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function emptyProfile(): AutoCloseProfile {
|
|
233
|
+
return { last: new Map(), chain: new Set() }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* A node the parser had on its tag stack, as opposed to a leaf or a grouping
|
|
238
|
+
* wrapper: exactly the nodes that own an opening delimiter. Leaves always
|
|
239
|
+
* carry a leading width of 0 (see `greenLeaf`), and so do `paragraph`,
|
|
240
|
+
* `group` and the document root, which the parser synthesises.
|
|
241
|
+
*/
|
|
242
|
+
function wasStackFrame(node: GreenNode): boolean {
|
|
243
|
+
return node.leadingWidth > 0
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Fill `out` from one node, in document order.
|
|
248
|
+
*
|
|
249
|
+
* `onChain` marks the rightmost chain of nodes still open when the stretch
|
|
250
|
+
* ends: they carry no closing delimiter because there was nothing left to
|
|
251
|
+
* close them, not because a crossing did.
|
|
252
|
+
*/
|
|
253
|
+
function profileNode(node: GreenNode, onChain: boolean, out: AutoCloseProfile): void {
|
|
254
|
+
if (node.kind === 'discarded_tag') {
|
|
255
|
+
const match = ORPHAN_CLOSE_RE.exec(node.text)
|
|
256
|
+
// Named by kind like every other event, so both sides compare on the same
|
|
257
|
+
// alphabet.
|
|
258
|
+
out.last.set(match === null ? node.text : tagToNodeKind(match[1]), 'del')
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
if (wasStackFrame(node)) out.last.set(node.kind, 'del')
|
|
262
|
+
|
|
263
|
+
const children = node.children as readonly GreenNode[]
|
|
264
|
+
// A node that owns a closing delimiter was closed where it stands, so
|
|
265
|
+
// nothing below it is still open at the end of the stretch.
|
|
266
|
+
const stillOpen = onChain && node.trailingWidth === 0
|
|
267
|
+
for (let i = 0; i < children.length; i++) {
|
|
268
|
+
profileNode(children[i], stillOpen && i === children.length - 1, out)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (node.trailingWidth > 0) {
|
|
272
|
+
// This delimiter closed the author's tag and, on the way, every frame
|
|
273
|
+
// still open inside it — the rightmost chain, which is what the parser's
|
|
274
|
+
// `autoClosedHere` collects.
|
|
275
|
+
for (
|
|
276
|
+
let c = children[children.length - 1] as GreenNode | undefined;
|
|
277
|
+
c !== undefined && wasStackFrame(c) && c.trailingWidth === 0;
|
|
278
|
+
c = c.children[c.children.length - 1] as GreenNode | undefined
|
|
279
|
+
) {
|
|
280
|
+
out.last.set(c.kind, 'add')
|
|
281
|
+
}
|
|
282
|
+
} else if (onChain && wasStackFrame(node)) {
|
|
283
|
+
out.chain.add(node.kind)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function profileSiblings(children: readonly GreenNode[], out: AutoCloseProfile): AutoCloseProfile {
|
|
288
|
+
for (let i = 0; i < children.length; i++) profileNode(children[i], i === children.length - 1, out)
|
|
289
|
+
return out
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Does the re-parsed window hand the text AFTER it the same `autoClosed` set
|
|
294
|
+
* the old one did?
|
|
295
|
+
*
|
|
296
|
+
* This is the leak the differential fuzz found, and it is invisible in the
|
|
297
|
+
* window itself. `[notice]
|
|
298
|
+
[b] [/notice]` auto-closes the `[b]`, leaving `b`
|
|
299
|
+
* pending; a `[/b]` further down the document is then a `discarded_tag` —
|
|
300
|
+
* invisible, not exported. Delete a `]` somewhere earlier so that the whole
|
|
301
|
+
* run gets swallowed into one literal-text token, and the `[b]` never opens,
|
|
302
|
+
* so the `[/b]` outside the window becomes visible text. The window parses
|
|
303
|
+
* correctly; the tree keeps the stale `discarded_tag` it adopted.
|
|
304
|
+
*
|
|
305
|
+
* ─── Why one entry per kind is the whole answer ─────────────────────────────
|
|
306
|
+
*
|
|
307
|
+
* `autoClosed` is a set of NAMES, and every operation on it names exactly one:
|
|
308
|
+
* a crossing adds one, an opening tag or a spent `[/tag]` deletes one. Names
|
|
309
|
+
* never interact, so the window's effect factorises into one function per
|
|
310
|
+
* kind, and each of those has only three possible shapes — leave the name as
|
|
311
|
+
* it arrived (the window did nothing to it), force it pending (the last thing
|
|
312
|
+
* the window did was add it), or force it absent (the last thing was a
|
|
313
|
+
* delete). That is why only the LAST operation per kind is recorded, and why a
|
|
314
|
+
* crossing the window resolves on the spot — `[b]` auto-closed by `[/notice]`
|
|
315
|
+
* and then reopened, which the fixture does 24 times — cancels out and costs
|
|
316
|
+
* nothing.
|
|
317
|
+
*
|
|
318
|
+
* The frames left open at the window's end are the exception in placement
|
|
319
|
+
* only: whatever closes them does so past the window, after every other
|
|
320
|
+
* operation, so `chain` overrides `last`.
|
|
321
|
+
*
|
|
322
|
+
* Two shapes still disagree without it mattering: "did nothing" and "deleted"
|
|
323
|
+
* are the same function whenever the name was not pending on the way in, which
|
|
324
|
+
* is a fact about the prefix that {@link PendingSpans} answers from a few
|
|
325
|
+
* dozen measured stretches. Without that relaxation an ordinary
|
|
326
|
+
* `[heading]TOP[/heading]` typed above the fixture's first block loses its
|
|
327
|
+
* window, because the block opens 190 tags and the new one is a 191st.
|
|
328
|
+
*
|
|
329
|
+
* The one approximation is naming an operation by its element KIND rather than
|
|
330
|
+
* by the tag as written, so two spellings of the same element — `[centre]` and
|
|
331
|
+
* `[center]` — look alike to it. Getting that wrong needs a pending cross of
|
|
332
|
+
* one of the two spellings over the window AND the edit to swap the window
|
|
333
|
+
* between them; it is the same order of blind spot as the plugin-tag note
|
|
334
|
+
* above, and it is recorded here rather than paid for on every keystroke.
|
|
335
|
+
*/
|
|
336
|
+
function pendingAutoClosePreserved(
|
|
337
|
+
oldChildren: readonly GreenNode[],
|
|
338
|
+
from: number,
|
|
339
|
+
to: number,
|
|
340
|
+
region: GreenNode,
|
|
341
|
+
pending: PendingSpans,
|
|
342
|
+
windowStart: number,
|
|
343
|
+
): boolean {
|
|
344
|
+
const after = profileSiblings(region.children as readonly GreenNode[], emptyProfile())
|
|
345
|
+
const before = profileSiblings(oldChildren.slice(from, to), emptyProfile())
|
|
346
|
+
|
|
347
|
+
const kinds = new Set<string>(before.last.keys())
|
|
348
|
+
for (const kind of after.last.keys()) kinds.add(kind)
|
|
349
|
+
for (const kind of before.chain) kinds.add(kind)
|
|
350
|
+
for (const kind of after.chain) kinds.add(kind)
|
|
351
|
+
|
|
352
|
+
for (const kind of kinds) {
|
|
353
|
+
const b = before.chain.has(kind) ? 'add' : before.last.get(kind) ?? 'none'
|
|
354
|
+
const a = after.chain.has(kind) ? 'add' : after.last.get(kind) ?? 'none'
|
|
355
|
+
if (b === a) continue
|
|
356
|
+
// `del` and `none` are the same function on a name that was not pending.
|
|
357
|
+
if (b !== 'add' && a !== 'add' && !pending.covers(kind, windowStart)) continue
|
|
358
|
+
return false
|
|
359
|
+
}
|
|
360
|
+
return true
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** A stretch of the document over which `kind` sits in the parser's `autoClosed`. */
|
|
364
|
+
interface PendingSpan {
|
|
365
|
+
kind: string
|
|
366
|
+
start: number
|
|
367
|
+
/** `Infinity` for a name nothing ever retired. */
|
|
368
|
+
end: number
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Where in the document a name is pending in the parser's `autoClosed`.
|
|
373
|
+
*
|
|
374
|
+
* The parser's own rules, replayed over the tree in one walk: a closing
|
|
375
|
+
* delimiter makes every frame it overtook pending from that point; an opening
|
|
376
|
+
* tag of the same name retires it (reopening `[b]` retires the pending `b`);
|
|
377
|
+
* so does a `discarded_tag`, which is the pending name being spent. What comes
|
|
378
|
+
* out is a handful of short spans — on the 547 KB fixture, a couple of dozen,
|
|
379
|
+
* none longer than a few KB — and outside them the incoming set is empty,
|
|
380
|
+
* which is what lets the window guard ignore the deletes almost always.
|
|
381
|
+
*
|
|
382
|
+
* The walk is O(nodes) and runs once per green root. An incremental splice
|
|
383
|
+
* carries it forward instead (`shifted`), because the guard that consults it
|
|
384
|
+
* only passes edits that leave the crossings where they were.
|
|
385
|
+
*/
|
|
386
|
+
class PendingSpans {
|
|
387
|
+
constructor(private readonly spans: readonly PendingSpan[]) {}
|
|
388
|
+
|
|
389
|
+
covers(kind: string, offset: number): boolean {
|
|
390
|
+
for (const span of this.spans) {
|
|
391
|
+
if (span.kind === kind && span.start <= offset && offset < span.end) return true
|
|
392
|
+
}
|
|
393
|
+
return false
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* The same spans over the text an edit produced.
|
|
398
|
+
*
|
|
399
|
+
* Anything wholly before the window is untouched and anything wholly after
|
|
400
|
+
* it moves by `delta`. An endpoint that falls INSIDE the window is clamped
|
|
401
|
+
* outwards, to the window's own edges: the guard has already established
|
|
402
|
+
* that the crossing and the tag that retires it are both still there, but
|
|
403
|
+
* not exactly where, and a span that claims to be pending for slightly
|
|
404
|
+
* longer than it is can only cost a rebuild, never correctness.
|
|
405
|
+
*/
|
|
406
|
+
shifted(windowStart: number, windowEndOld: number, delta: number): PendingSpans {
|
|
407
|
+
const windowEndNew = windowEndOld + delta
|
|
408
|
+
const move = (at: number): number => {
|
|
409
|
+
if (at === Number.POSITIVE_INFINITY || at >= windowEndOld) return at + delta
|
|
410
|
+
return at
|
|
411
|
+
}
|
|
412
|
+
const spans: PendingSpan[] = []
|
|
413
|
+
for (const span of this.spans) {
|
|
414
|
+
const start = span.start < windowStart ? span.start : Math.max(windowStart, move(span.start))
|
|
415
|
+
const end = span.end <= windowStart ? span.end : Math.max(windowEndNew, move(span.end))
|
|
416
|
+
spans.push({ kind: span.kind, start, end })
|
|
417
|
+
}
|
|
418
|
+
return new PendingSpans(spans)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function collectPendingSpans(root: GreenNode): PendingSpans {
|
|
423
|
+
const spans: PendingSpan[] = []
|
|
424
|
+
const open = new Map<string, number>()
|
|
425
|
+
const retire = (kind: string, at: number): void => {
|
|
426
|
+
const start = open.get(kind)
|
|
427
|
+
if (start === undefined) return
|
|
428
|
+
open.delete(kind)
|
|
429
|
+
spans.push({ kind, start, end: at })
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const visit = (node: GreenNode, start: number): void => {
|
|
433
|
+
if (node.kind === 'discarded_tag') {
|
|
434
|
+
const match = ORPHAN_CLOSE_RE.exec(node.text)
|
|
435
|
+
if (match !== null) retire(tagToNodeKind(match[1]), start)
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
if (wasStackFrame(node)) retire(node.kind, start)
|
|
439
|
+
|
|
440
|
+
const children = node.children as readonly GreenNode[]
|
|
441
|
+
let offset = start + node.leadingWidth
|
|
442
|
+
for (let i = 0; i < children.length; i++) {
|
|
443
|
+
visit(children[i], offset)
|
|
444
|
+
offset += children[i].width
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (node.trailingWidth > 0) {
|
|
448
|
+
const at = start + node.width
|
|
449
|
+
for (
|
|
450
|
+
let c = children[children.length - 1] as GreenNode | undefined;
|
|
451
|
+
c !== undefined && wasStackFrame(c) && c.trailingWidth === 0;
|
|
452
|
+
c = c.children[c.children.length - 1] as GreenNode | undefined
|
|
453
|
+
) {
|
|
454
|
+
if (!open.has(c.kind)) open.set(c.kind, at)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
visit(root, 0)
|
|
459
|
+
|
|
460
|
+
// Whatever the document never retired stays pending to its end and beyond:
|
|
461
|
+
// an edit appending text reads it with the name still set.
|
|
462
|
+
for (const [kind, start] of open) spans.push({ kind, start, end: Number.POSITIVE_INFINITY })
|
|
463
|
+
return new PendingSpans(spans)
|
|
464
|
+
}
|
|
465
|
+
|
|
168
466
|
/**
|
|
169
467
|
* Can this window be parsed on its own and mean the same thing it means in
|
|
170
468
|
* context? Four ways it cannot:
|
|
171
469
|
*
|
|
172
|
-
* - A closing tag with no opener inside the window,
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
470
|
+
* - A closing tag with no opener inside the window, in one of two situations.
|
|
471
|
+
* In isolation it is literal text; in the whole document it may be
|
|
472
|
+
* something else, and the window cannot tell which:
|
|
473
|
+
* · if the name matches an ANCESTOR it closes that ancestor, which moves
|
|
474
|
+
* the ancestor's own boundary;
|
|
475
|
+
* · otherwise it is a `discarded_tag` — invisible, not exported — exactly
|
|
476
|
+
* when the parser auto-closed a tag of that name EARLIER in the
|
|
477
|
+
* document and no later `[/name]` has claimed it since (see
|
|
478
|
+
* `autoClosed` in `Parser.ts`). That is a fact about the prefix, which
|
|
479
|
+
* a window parse never sees: the differential found a `[/b]` typed
|
|
480
|
+
* after `[quote][b]x[/quote]` coming back as visible text where the
|
|
481
|
+
* full parse discards it. {@link PendingSpans} is what makes that fact
|
|
482
|
+
* answerable here without reading the prefix — and answerable
|
|
483
|
+
* precisely, which matters: refusing every stray closer of a KNOWN tag
|
|
484
|
+
* instead cost the 20 KB mid-document delete its window on the 500 KB
|
|
485
|
+
* fixture, and a large delete strands a closer almost by definition.
|
|
486
|
+
* Anything else is text either way and is perfectly safe — the common case
|
|
487
|
+
* while editing, since a half-typed `[/colo` matches nothing and a finished
|
|
488
|
+
* `[/color]` normally closes a `[color]` inside the window or above it.
|
|
489
|
+
* Plugin tags are the one blind spot: `tagToNodeKind` sees the built-in
|
|
490
|
+
* dialects, not a document's registry, so a stray closer of a plugin tag is
|
|
491
|
+
* compared as `custom`.
|
|
179
492
|
* - A lone `[` with no `]` after it. The lexer's bracket matching would find
|
|
180
493
|
* a `]` beyond the window.
|
|
181
494
|
* - An unclosed `[code]`. Raw blocks swallow everything up to their closing
|
|
@@ -200,6 +513,7 @@ function regionIsSelfContained(
|
|
|
200
513
|
region: GreenNode,
|
|
201
514
|
ancestorKinds: ReadonlySet<string>,
|
|
202
515
|
window: { reachesEnd: boolean },
|
|
516
|
+
pendingKind: (kind: string) => boolean,
|
|
203
517
|
): boolean {
|
|
204
518
|
// Checked on the PARSED region rather than on a second token scan. Lexing the
|
|
205
519
|
// region twice — once to vet it, once to parse it — cost more than the whole
|
|
@@ -209,24 +523,38 @@ function regionIsSelfContained(
|
|
|
209
523
|
// Everything the guard needs survives into the tree, because the parser now
|
|
210
524
|
// keeps what it used to drop: a bare `[` and an orphaned `[/tag]` are both
|
|
211
525
|
// `text` leaves holding exactly their own source.
|
|
526
|
+
//
|
|
527
|
+
// Leaves inside a raw block are content, not syntax: `[code][/b][/code]`
|
|
528
|
+
// holds a text leaf that IS `[/b]`, and the lexer never matched brackets
|
|
529
|
+
// in there to begin with. The flag rides the stack beside the node.
|
|
212
530
|
const stack: GreenNode[] = [region]
|
|
531
|
+
const inCode: boolean[] = [false]
|
|
213
532
|
while (stack.length > 0) {
|
|
214
533
|
const node = stack.pop()!
|
|
534
|
+
const code = inCode.pop()! || node.kind === 'code' || node.kind === 'inline_code'
|
|
215
535
|
|
|
216
536
|
if (node.children.length === 0) {
|
|
217
|
-
if (node.kind === 'text') {
|
|
537
|
+
if (node.kind === 'text' && !code) {
|
|
218
538
|
// The lexer emits a bare '[' as text exactly when it found no matching
|
|
219
539
|
// bracket — the one case where its decision depends on what follows.
|
|
220
540
|
if (node.text === '[') return false
|
|
221
541
|
|
|
542
|
+
// A stray closer that is not text after all: an ancestor's, or one
|
|
543
|
+
// the prefix left pending — see the header. Compared by node kind,
|
|
544
|
+
// not tag name: `[centre]` and `[center]` are the same element and
|
|
545
|
+
// either spelling closes it.
|
|
222
546
|
const orphan = ORPHAN_CLOSE_RE.exec(node.text)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
547
|
+
if (orphan !== null) {
|
|
548
|
+
const kind = tagToNodeKind(orphan[1])
|
|
549
|
+
if (ancestorKinds.has(kind) || pendingKind(kind)) return false
|
|
550
|
+
}
|
|
226
551
|
}
|
|
227
552
|
continue
|
|
228
553
|
}
|
|
229
|
-
for (const child of node.children as readonly GreenNode[])
|
|
554
|
+
for (const child of node.children as readonly GreenNode[]) {
|
|
555
|
+
stack.push(child)
|
|
556
|
+
inCode.push(code)
|
|
557
|
+
}
|
|
230
558
|
}
|
|
231
559
|
|
|
232
560
|
// Tags still open at the end of the window are exactly the rightmost chain of
|
|
@@ -250,29 +578,6 @@ function regionIsSelfContained(
|
|
|
250
578
|
return true
|
|
251
579
|
}
|
|
252
580
|
|
|
253
|
-
/**
|
|
254
|
-
* Does every `[` before `end` find its `]` before `end` too?
|
|
255
|
-
*
|
|
256
|
-
* If one does not, the lexer's bracket matching for it scans onward into the
|
|
257
|
-
* region we are about to re-parse — and an edit that adds a `]` there (or
|
|
258
|
-
* deletes a `[` that was keeping the nesting depth up) changes what that
|
|
259
|
-
* OUTSIDE bracket means. The region would be re-parsed correctly and the text
|
|
260
|
-
* before it would silently become something else.
|
|
261
|
-
*
|
|
262
|
-
* A plain depth count is enough and is exact for this question: the lexer
|
|
263
|
-
* pairs brackets with a stack, so a `[` is unmatched precisely when the depth
|
|
264
|
-
* never returns to its level.
|
|
265
|
-
*/
|
|
266
|
-
function bracketsCloseBefore(source: string, end: number): boolean {
|
|
267
|
-
let depth = 0
|
|
268
|
-
for (let i = 0; i < end; i++) {
|
|
269
|
-
const c = source.charCodeAt(i)
|
|
270
|
-
if (c === 91 /* [ */) depth++
|
|
271
|
-
else if (c === 93 /* ] */ && depth > 0) depth--
|
|
272
|
-
}
|
|
273
|
-
return depth === 0
|
|
274
|
-
}
|
|
275
|
-
|
|
276
581
|
export interface IncrementalParserOptions {
|
|
277
582
|
/** Override `MIN_SOURCE_LENGTH`. Set to 0 to always attempt a splice. */
|
|
278
583
|
minSourceLength?: number
|
|
@@ -284,6 +589,37 @@ export class IncrementalParser {
|
|
|
284
589
|
private readonly minSourceLength: number
|
|
285
590
|
private readonly maxRegionFraction: number
|
|
286
591
|
|
|
592
|
+
/**
|
|
593
|
+
* Bracket-depth summary of the source, for the boundary check below.
|
|
594
|
+
*
|
|
595
|
+
* Keyed on the green root it was last synchronised with: a reparse whose
|
|
596
|
+
* `oldGreen` is that root brings the index across the edit by re-reading a
|
|
597
|
+
* few KB around it; any other root (a `rebuild`, a model handed a foreign
|
|
598
|
+
* tree) rebuilds it with one scan — the same scan every keystroke used to
|
|
599
|
+
* pay. See `BracketDepthIndex` for why the summary is exact.
|
|
600
|
+
*/
|
|
601
|
+
private readonly brackets = new BracketDepthIndex()
|
|
602
|
+
private bracketsRoot: GreenNode | null = null
|
|
603
|
+
/**
|
|
604
|
+
* Whether, within the current `reparse` call, the index has been brought to
|
|
605
|
+
* describe `newSource`. Explicit rather than inferred: a length comparison
|
|
606
|
+
* would confuse an unsynchronised index over an older text of the same
|
|
607
|
+
* length (insert one character, delete one) with a synchronised one.
|
|
608
|
+
*/
|
|
609
|
+
private bracketsSynced = false
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Where the document carries a pending `autoClosed` name, memoised on the
|
|
613
|
+
* green root it was measured over — see {@link PendingSpans}.
|
|
614
|
+
*
|
|
615
|
+
* It survives an incremental splice by construction: the window guard that
|
|
616
|
+
* consults it only lets through edits that leave the crossings on both
|
|
617
|
+
* sides of the window as they were, so the spans need only be moved. Any
|
|
618
|
+
* other outcome drops the key and the next edit pays one walk.
|
|
619
|
+
*/
|
|
620
|
+
private pendingRoot: GreenNode | null = null
|
|
621
|
+
private pendingSpans = new PendingSpans([])
|
|
622
|
+
|
|
287
623
|
/**
|
|
288
624
|
* The thresholds are constructor options because they are performance
|
|
289
625
|
* tuning, not semantics: the tree that comes out is the same either way, so
|
|
@@ -296,6 +632,57 @@ export class IncrementalParser {
|
|
|
296
632
|
this.maxRegionFraction = options.maxRegionFraction ?? MAX_REGION_FRACTION
|
|
297
633
|
}
|
|
298
634
|
|
|
635
|
+
/**
|
|
636
|
+
* Characters the last boundary check actually read — the tail of one index
|
|
637
|
+
* piece, never the prefix. Exposed so a test can pin the bound.
|
|
638
|
+
*/
|
|
639
|
+
get lastBoundaryScan(): number {
|
|
640
|
+
return this.brackets.lastScanned
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Does every `[` before `end` find its `]` before `end` too?
|
|
645
|
+
*
|
|
646
|
+
* If one does not, the lexer's bracket matching for it scans onward into
|
|
647
|
+
* the region we are about to re-parse — and an edit that adds a `]` there
|
|
648
|
+
* (or deletes a `[` that was keeping the nesting depth up) changes what that
|
|
649
|
+
* OUTSIDE bracket means. The region would be re-parsed correctly and the
|
|
650
|
+
* text before it would silently become something else.
|
|
651
|
+
*
|
|
652
|
+
* A clamped depth count is exact for this question: the lexer pairs
|
|
653
|
+
* brackets with a stack, so a `[` is unmatched precisely when the depth
|
|
654
|
+
* never returns to its level. The count used to be a scan of the whole
|
|
655
|
+
* prefix on every keystroke — 22% of a keystroke with the caret at the end
|
|
656
|
+
* of a post, 1.4 ms on the 547 KB fixture. The index answers it from piece
|
|
657
|
+
* summaries, reading at most one piece of text.
|
|
658
|
+
*
|
|
659
|
+
* The index is brought across the edit here, not earlier: the paths that
|
|
660
|
+
* return before this point never needed it, and on the next call the
|
|
661
|
+
* root-key mismatch simply rebuilds it. The caller re-keys it on whatever
|
|
662
|
+
* green root it returns.
|
|
663
|
+
*/
|
|
664
|
+
private bracketsCloseBefore(
|
|
665
|
+
oldGreen: GreenNode,
|
|
666
|
+
change: TextChange,
|
|
667
|
+
newSource: string,
|
|
668
|
+
end: number,
|
|
669
|
+
): boolean {
|
|
670
|
+
const inSync = this.bracketsRoot === oldGreen
|
|
671
|
+
// Unkeyed while it is being moved: should the parse callback throw
|
|
672
|
+
// halfway through this call, no later call can mistake the half-moved
|
|
673
|
+
// index for a description of any tree.
|
|
674
|
+
this.bracketsRoot = null
|
|
675
|
+
if (inSync) {
|
|
676
|
+
this.brackets.applyChange(newSource, change.start, change.end, change.text.length)
|
|
677
|
+
} else {
|
|
678
|
+
this.brackets.rebuild(newSource)
|
|
679
|
+
}
|
|
680
|
+
// Describes `newSource` from here on, whatever the outcome; the key is
|
|
681
|
+
// set once the root that owns that text exists (see `keyed`).
|
|
682
|
+
this.bracketsSynced = true
|
|
683
|
+
return this.brackets.depthAt(newSource, end) === 0
|
|
684
|
+
}
|
|
685
|
+
|
|
299
686
|
/**
|
|
300
687
|
* Reparse a tree after a text change.
|
|
301
688
|
*
|
|
@@ -313,6 +700,23 @@ export class IncrementalParser {
|
|
|
313
700
|
const startTime = performance.now()
|
|
314
701
|
const delta = change.text.length - (change.end - change.start)
|
|
315
702
|
|
|
703
|
+
// Whatever the outcome, the bracket index ends up keyed on the tree whose
|
|
704
|
+
// text it describes — or on nothing, so the next call rebuilds it. A call
|
|
705
|
+
// that returns before the boundary check never moved the index, and the
|
|
706
|
+
// tree it returns describes a text the index does not; the key it had
|
|
707
|
+
// (the previous root) is useless from here on, since the next call comes
|
|
708
|
+
// in with this call's root.
|
|
709
|
+
this.bracketsSynced = false
|
|
710
|
+
// The pending spans carry over only on the incremental path (see the
|
|
711
|
+
// field); anything else re-keys them to nothing so the next call remeasures.
|
|
712
|
+
let carriedSpans: PendingSpans | null = null
|
|
713
|
+
const keyed = (result: ReparseResult): ReparseResult => {
|
|
714
|
+
this.bracketsRoot = this.bracketsSynced ? result.green : null
|
|
715
|
+
this.pendingRoot = carriedSpans === null ? null : result.green
|
|
716
|
+
if (carriedSpans !== null) this.pendingSpans = carriedSpans
|
|
717
|
+
return result
|
|
718
|
+
}
|
|
719
|
+
|
|
316
720
|
const fullRebuild = (reason: FallbackReason, tFind: number): ReparseResult => {
|
|
317
721
|
const t0 = performance.now()
|
|
318
722
|
const green = parseCallback(newSource)
|
|
@@ -321,7 +725,7 @@ export class IncrementalParser {
|
|
|
321
725
|
const red = buildRedCallback(green)
|
|
322
726
|
const tBuild = performance.now() - t1
|
|
323
727
|
const total = performance.now() - startTime
|
|
324
|
-
return {
|
|
728
|
+
return keyed({
|
|
325
729
|
green,
|
|
326
730
|
red,
|
|
327
731
|
affectedNodes: [red],
|
|
@@ -336,7 +740,8 @@ export class IncrementalParser {
|
|
|
336
740
|
},
|
|
337
741
|
path: 'full_rebuild',
|
|
338
742
|
reason,
|
|
339
|
-
|
|
743
|
+
window: null,
|
|
744
|
+
})
|
|
340
745
|
}
|
|
341
746
|
|
|
342
747
|
if (newSource.length < this.minSourceLength) {
|
|
@@ -373,7 +778,7 @@ export class IncrementalParser {
|
|
|
373
778
|
if (region.length > (newSource.length + 1) * this.maxRegionFraction) {
|
|
374
779
|
return fullRebuild('region-too-large', tFind)
|
|
375
780
|
}
|
|
376
|
-
if (!bracketsCloseBefore(newSource, windowStart)) {
|
|
781
|
+
if (!this.bracketsCloseBefore(oldGreen, change, newSource, windowStart)) {
|
|
377
782
|
return fullRebuild('open-bracket-before', tFind)
|
|
378
783
|
}
|
|
379
784
|
const tBoundary = performance.now() - tBoundary0
|
|
@@ -385,10 +790,26 @@ export class IncrementalParser {
|
|
|
385
790
|
const parsedRegion = parseCallback(region, { normalizeParagraphs: isRoot })
|
|
386
791
|
const tParse = performance.now() - tParse0
|
|
387
792
|
|
|
793
|
+
// Measured on the OLD tree, which is the one the incoming set is a fact
|
|
794
|
+
// about; the guard below is what keeps the answer true of the new one.
|
|
795
|
+
// Both guards want it, so it is settled before either runs.
|
|
796
|
+
if (this.pendingRoot !== oldGreen) {
|
|
797
|
+
this.pendingSpans = collectPendingSpans(oldGreen)
|
|
798
|
+
this.pendingRoot = oldGreen
|
|
799
|
+
}
|
|
800
|
+
const spans = this.pendingSpans
|
|
801
|
+
const pendingKind = (kind: string): boolean => spans.covers(kind, windowStart)
|
|
802
|
+
|
|
388
803
|
const reachesEnd = to === parent.children.length
|
|
389
|
-
if (!regionIsSelfContained(parsedRegion, ancestorKinds, { reachesEnd })) {
|
|
804
|
+
if (!regionIsSelfContained(parsedRegion, ancestorKinds, { reachesEnd }, pendingKind)) {
|
|
390
805
|
return fullRebuild('region-not-isolated', tFind)
|
|
391
806
|
}
|
|
807
|
+
if (!pendingAutoClosePreserved(
|
|
808
|
+
parent.children as readonly GreenNode[], from, to, parsedRegion, spans, windowStart,
|
|
809
|
+
)) {
|
|
810
|
+
return fullRebuild('pending-auto-close', tFind)
|
|
811
|
+
}
|
|
812
|
+
carriedSpans = this.pendingSpans.shifted(windowStart, windowEnd, delta)
|
|
392
813
|
|
|
393
814
|
// No rebasing step: the parsed region has widths, not offsets, so it is
|
|
394
815
|
// already correct wherever it ends up.
|
|
@@ -405,7 +826,7 @@ export class IncrementalParser {
|
|
|
405
826
|
const tBuild = performance.now() - tBuild0
|
|
406
827
|
|
|
407
828
|
const total = performance.now() - startTime
|
|
408
|
-
return {
|
|
829
|
+
return keyed({
|
|
409
830
|
green: newGreenRoot,
|
|
410
831
|
red: newRed,
|
|
411
832
|
affectedNodes: [newRed],
|
|
@@ -419,7 +840,9 @@ export class IncrementalParser {
|
|
|
419
840
|
other: Math.max(0, total - tFind - tBoundary - tParse - tBuild - tMutate),
|
|
420
841
|
},
|
|
421
842
|
path: 'incremental',
|
|
422
|
-
|
|
843
|
+
// The region, in the coordinates of the text it now describes.
|
|
844
|
+
window: { start: windowStart, end: windowEnd + delta },
|
|
845
|
+
})
|
|
423
846
|
}
|
|
424
847
|
|
|
425
848
|
/**
|
package/src/Lexer/BBCodeLexer.ts
CHANGED
|
@@ -56,6 +56,28 @@ export type BBCodeToken =
|
|
|
56
56
|
| BBCodeTextToken
|
|
57
57
|
| BBCodeNewlineToken
|
|
58
58
|
|
|
59
|
+
// ─── Raw blocks ────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Tags whose content is strictly literal: no inner tags, no newline tokens.
|
|
63
|
+
*
|
|
64
|
+
* Exported because the lexer is the ONLY authority on this. Consumers that
|
|
65
|
+
* have to reproduce the rule — a syntax highlighter deciding where to stop
|
|
66
|
+
* tokenising, an editor deciding whether to indent an inserted body — used to
|
|
67
|
+
* carry their own copy of `['code', 'c']`, which is a second lexer waiting to
|
|
68
|
+
* disagree with this one. Adding a raw tag here now reaches them.
|
|
69
|
+
*/
|
|
70
|
+
export const BBCODE_RAW_TAGS: readonly string[] = Object.freeze(['code', 'c'])
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Set lookup instead of the two `===` it replaces.
|
|
74
|
+
*
|
|
75
|
+
* This branch runs once per OPENING TAG, not per character — 760 times on the
|
|
76
|
+
* reference document — so it is not on the per-character hot path the rest of
|
|
77
|
+
* this file is tuned around. Correctness of the shared list wins here.
|
|
78
|
+
*/
|
|
79
|
+
const RAW_TAG_SET: ReadonlySet<string> = new Set(BBCODE_RAW_TAGS)
|
|
80
|
+
|
|
59
81
|
// ─── Tag name validation ───────────────────────────────────────
|
|
60
82
|
//
|
|
61
83
|
// Tag names used to be handled with two regexes: one tested CHARACTER BY
|
|
@@ -295,9 +317,9 @@ export function scanBBCode(source: string): BBCodeToken[] {
|
|
|
295
317
|
})
|
|
296
318
|
pos = closeBracket + 1
|
|
297
319
|
|
|
298
|
-
// ── RAW BLOCK HANDLING (
|
|
299
|
-
// Contents of
|
|
300
|
-
if (tagName
|
|
320
|
+
// ── RAW BLOCK HANDLING (see BBCODE_RAW_TAGS) ──
|
|
321
|
+
// Contents of raw blocks are strictly literal. No inner tags or newline tokens.
|
|
322
|
+
if (RAW_TAG_SET.has(tagName)) {
|
|
301
323
|
const endTag = `[/${tagName}]`
|
|
302
324
|
const closeIdx = lowerOf().indexOf(endTag, pos)
|
|
303
325
|
|
package/src/Lexer/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { Lexer } from './Lexer'
|
|
2
2
|
export type { LexerOptions, LexerRule } from './Lexer'
|
|
3
|
-
export { scanBBCode, formatTokens } from './BBCodeLexer'
|
|
3
|
+
export { scanBBCode, formatTokens, BBCODE_RAW_TAGS } from './BBCodeLexer'
|
|
4
4
|
export type {
|
|
5
5
|
BBCodeToken,
|
|
6
6
|
BBCodeOpenToken,
|