@miliastry/quasar 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/LICENSE +119 -0
  2. package/README.md +45 -0
  3. package/dist/Visuals/lyne.css +1027 -0
  4. package/dist/Visuals/osu.css +257 -0
  5. package/dist/index.d.mts +4563 -0
  6. package/dist/index.d.ts +4563 -0
  7. package/dist/index.js +11291 -0
  8. package/dist/index.mjs +11197 -0
  9. package/package.json +51 -0
  10. package/src/Analysis/Contracts/AnalysisReport.ts +26 -0
  11. package/src/Analysis/Contracts/Contribution.ts +76 -0
  12. package/src/Analysis/Contracts/Pass.ts +76 -0
  13. package/src/Analysis/Contracts/PipelineContext.ts +48 -0
  14. package/src/Analysis/Passes/Analysis/GradientAnalyzer.ts +292 -0
  15. package/src/Analysis/Passes/Analysis/MergeableColorAnalyzer.ts +112 -0
  16. package/src/Analysis/Passes/Analysis/RainbowAnalyzer.ts +233 -0
  17. package/src/Analysis/Passes/Analysis/WaveAnalyzer.ts +211 -0
  18. package/src/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.ts +135 -0
  19. package/src/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.ts +84 -0
  20. package/src/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.ts +99 -0
  21. package/src/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.ts +119 -0
  22. package/src/Analysis/Passes/Decision/DefaultDecision.ts +139 -0
  23. package/src/Analysis/Passes/Decision/__tests__/DefaultDecision.test.ts +179 -0
  24. package/src/Analysis/Passes/Transform/CollapseGradientTransform.ts +176 -0
  25. package/src/Analysis/Passes/Transform/MergeColorsTransform.ts +126 -0
  26. package/src/Analysis/Passes/Transform/RainbowCollapseTransform.ts +83 -0
  27. package/src/Analysis/Passes/Transform/WaveCollapseTransform.ts +88 -0
  28. package/src/Analysis/Passes/Utility/CharacterCountAnalyzer.ts +45 -0
  29. package/src/Analysis/Pipeline/Pipeline.ts +133 -0
  30. package/src/Analysis/Pipeline/PipelineBuilder.ts +55 -0
  31. package/src/Analysis/Pipeline/PipelineStage.ts +19 -0
  32. package/src/Analysis/Utils/color-utils.ts +132 -0
  33. package/src/Analysis/__tests__/Integration.test.ts +162 -0
  34. package/src/Analysis/__tests__/Pipeline.test.ts +133 -0
  35. package/src/Analysis/index.ts +52 -0
  36. package/src/BBCode/BBCodeDocumentModel.ts +175 -0
  37. package/src/BBCode/BBCodeToGreenNode.ts +755 -0
  38. package/src/BBCode/Parser.ts +384 -0
  39. package/src/BBCode/index.ts +12 -0
  40. package/src/Collab/positions.ts +91 -0
  41. package/src/Commands/Command.ts +44 -0
  42. package/src/Commands/CommandRegistry.ts +78 -0
  43. package/src/Commands/DeleteNode.ts +20 -0
  44. package/src/Commands/InsertText.ts +21 -0
  45. package/src/Commands/SplitMerge.ts +28 -0
  46. package/src/Commands/WrapInTag.ts +21 -0
  47. package/src/Commands/index.ts +6 -0
  48. package/src/Diff/TreeDiffer.ts +264 -0
  49. package/src/Diff/__tests__/TreeDiffer.test.ts +65 -0
  50. package/src/Diff/index.ts +2 -0
  51. package/src/Events/EventBus.ts +160 -0
  52. package/src/Events/index.ts +2 -0
  53. package/src/Formatter/Formatter.ts +54 -0
  54. package/src/Formatter/index.ts +2 -0
  55. package/src/HTML/HTMLDocumentModel.ts +35 -0
  56. package/src/HTML/HTMLToGreenNode.ts +290 -0
  57. package/src/Incremental/ChangeTracker.ts +105 -0
  58. package/src/Incremental/IncrementalParser.ts +591 -0
  59. package/src/Incremental/__tests__/IncrementalParser.test.ts +164 -0
  60. package/src/Incremental/index.ts +4 -0
  61. package/src/Lexer/BBCodeLexer.ts +382 -0
  62. package/src/Lexer/Lexer.ts +181 -0
  63. package/src/Lexer/index.ts +10 -0
  64. package/src/Linter/Linter.ts +193 -0
  65. package/src/Linter/index.ts +2 -0
  66. package/src/Markdown/MarkdownAST.ts +112 -0
  67. package/src/Markdown/MarkdownDocumentModel.ts +55 -0
  68. package/src/Markdown/MarkdownLexer.ts +203 -0
  69. package/src/Markdown/MarkdownParser.ts +455 -0
  70. package/src/Markdown/MarkdownToGreenNode.ts +153 -0
  71. package/src/Model/DocumentModel.ts +694 -0
  72. package/src/Model/NodeFactory.ts +117 -0
  73. package/src/Model/TagRegistry.ts +495 -0
  74. package/src/Model/index.ts +5 -0
  75. package/src/Plugins/PluginAPI.ts +119 -0
  76. package/src/Plugins/PluginRegistry.ts +132 -0
  77. package/src/Plugins/index.ts +3 -0
  78. package/src/Queries/QueryEngine.ts +152 -0
  79. package/src/Queries/index.ts +1 -0
  80. package/src/RenderPipeline/RenderPipeline.ts +125 -0
  81. package/src/RenderPipeline/RenderTree.ts +134 -0
  82. package/src/RenderPipeline/index.ts +4 -0
  83. package/src/Semantic/SemanticAnalyzer.ts +506 -0
  84. package/src/Semantic/index.ts +2 -0
  85. package/src/Symbols/SymbolTable.ts +124 -0
  86. package/src/Symbols/index.ts +1 -0
  87. package/src/Syntax/GreenNode.ts +324 -0
  88. package/src/Syntax/GreenNodePool.ts +269 -0
  89. package/src/Syntax/NodeMatcher.ts +370 -0
  90. package/src/Syntax/RedNode.ts +569 -0
  91. package/src/Syntax/RedNodeStore.ts +184 -0
  92. package/src/Syntax/TreeBuilder.ts +214 -0
  93. package/src/Syntax/__tests__/GreenNode.test.ts +33 -0
  94. package/src/Syntax/__tests__/RedNode.test.ts +81 -0
  95. package/src/Syntax/__tests__/RedNodeStore.test.ts +104 -0
  96. package/src/Syntax/greenEdit.ts +110 -0
  97. package/src/Syntax/hash.ts +30 -0
  98. package/src/Syntax/index.ts +12 -0
  99. package/src/Syntax/partition.ts +161 -0
  100. package/src/Syntax/preserveNodeIds.ts +201 -0
  101. package/src/Tests/ASTOptimizerIdempotence.test.ts +77 -0
  102. package/src/Tests/BlockPatcher.test.ts +437 -0
  103. package/src/Tests/BlockPatcherWindowed.test.ts +364 -0
  104. package/src/Tests/BoxDrawer.test.ts +217 -0
  105. package/src/Tests/BoxRichTitle.test.ts +105 -0
  106. package/src/Tests/Chars500kBenchmark.test.ts +151 -0
  107. package/src/Tests/Chars500kEdits.test.ts +321 -0
  108. package/src/Tests/CollabPositions.test.ts +146 -0
  109. package/src/Tests/CompilerPathProfiling.test.ts +186 -0
  110. package/src/Tests/DOMMorpher.test.ts +142 -0
  111. package/src/Tests/DomPatchPerf.test.ts +60 -0
  112. package/src/Tests/EffectSegments.snapshot.json +616 -0
  113. package/src/Tests/EffectSegments.test.ts +68 -0
  114. package/src/Tests/FindNodeAtOffset.test.ts +65 -0
  115. package/src/Tests/Fuzzer.test.ts +166 -0
  116. package/src/Tests/GreenNodePool.test.ts +153 -0
  117. package/src/Tests/Lexer.test.ts +238 -0
  118. package/src/Tests/LyneMode.test.ts +187 -0
  119. package/src/Tests/ModelCoherence.test.ts +180 -0
  120. package/src/Tests/Partition.test.ts +238 -0
  121. package/src/Tests/PluginTags.test.ts +150 -0
  122. package/src/Tests/ProblematicSection.test.ts +46 -0
  123. package/src/Tests/ProblematicSectionHTML.test.ts +58 -0
  124. package/src/Tests/RedReuse.test.ts +134 -0
  125. package/src/Tests/ReproDelete20k.test.ts +62 -0
  126. package/src/Tests/SemanticValidators.test.ts +136 -0
  127. package/src/Tests/StableNodeIds.test.ts +210 -0
  128. package/src/Tests/StudioColorBloat.test.ts +25 -0
  129. package/src/Tests/StudioDebugText.test.ts +27 -0
  130. package/src/Tests/StudioTrailingChar.test.ts +25 -0
  131. package/src/Tests/StudioValidText.test.ts +25 -0
  132. package/src/Tests/UrlImgBug.test.ts +23 -0
  133. package/src/Tests/VisualBuilderFidelity.test.ts +105 -0
  134. package/src/Tests/referenceDocument.ts +119 -0
  135. package/src/Transactions/Transaction.ts +176 -0
  136. package/src/Transactions/UndoManager.ts +111 -0
  137. package/src/Transactions/index.ts +3 -0
  138. package/src/Transformers/ASTOptimizer.ts +315 -0
  139. package/src/Transformers/GradientTransformer.ts +143 -0
  140. package/src/Transformers/GrowTransformer.ts +115 -0
  141. package/src/Transformers/RainbowTransformer.ts +121 -0
  142. package/src/Transformers/SineWaveTransformer.ts +130 -0
  143. package/src/Transformers/Transformer.ts +22 -0
  144. package/src/Types/core.ts +270 -0
  145. package/src/Types/diagnostics.ts +156 -0
  146. package/src/Types/index.ts +23 -0
  147. package/src/Types/operations.ts +180 -0
  148. package/src/Types/queries.ts +121 -0
  149. package/src/Types/symbols.ts +69 -0
  150. package/src/Types/tokens.ts +186 -0
  151. package/src/Utils/BBCodeGenerator.ts +126 -0
  152. package/src/Utils/ColorMath.ts +276 -0
  153. package/src/Utils/color.ts +112 -0
  154. package/src/Utils/dom-to-svg.test.ts +86 -0
  155. package/src/Utils/dom-to-svg.ts +615 -0
  156. package/src/Utils/treeTransformers.ts +717 -0
  157. package/src/Visitors/BBBlocksExporter.ts +69 -0
  158. package/src/Visitors/BBCodeExporter.ts +318 -0
  159. package/src/Visitors/BlockPatcher.ts +963 -0
  160. package/src/Visitors/DOMMorpher.ts +134 -0
  161. package/src/Visitors/HTMLRenderer.ts +1077 -0
  162. package/src/Visitors/JSONExporter.ts +66 -0
  163. package/src/Visitors/MarkdownExporter.ts +99 -0
  164. package/src/Visitors/SVGRenderer.ts +35 -0
  165. package/src/Visitors/TiptapExporter.ts +145 -0
  166. package/src/Visitors/Visitor.ts +48 -0
  167. package/src/Visitors/index.ts +9 -0
  168. package/src/Visuals/BoxDrawer.ts +175 -0
  169. package/src/Visuals/index.ts +42 -0
  170. package/src/Visuals/lyne.css +1027 -0
  171. package/src/Visuals/osu.css +257 -0
  172. package/src/index.ts +197 -0
@@ -0,0 +1,4563 @@
1
+ /**
2
+ * DocumentEngine — GreenNode (Immutable Syntax Tree)
3
+ *
4
+ * The Green Tree is IMMUTABLE. It never changes after creation.
5
+ * This is the "compiled" representation of the source text.
6
+ *
7
+ * Properties:
8
+ * - Fully immutable (readonly)
9
+ * - Contains only structural information (kind, text, children, widths)
10
+ * - **No absolute position** — see below
11
+ * - No parent references
12
+ * - No identity (IDs live in Red Tree)
13
+ * - Can be shared across document versions
14
+ * - Safe to cache
15
+ *
16
+ * ─── Why there is no `range` ────────────────────────────────────────────────
17
+ *
18
+ * A green node knows how WIDE it is, not WHERE it is. That is the whole point:
19
+ * the two `[color=#e8b04b]` runs in a gradient are the same structure, and if
20
+ * each carried its own offsets they could never be the same object. Measured on
21
+ * the reference document: 1736 nodes, but only 702 distinct structures — 59.6%
22
+ * of the tree is duplicate.
23
+ *
24
+ * Position is a property of a node's PLACE in a tree, so it lives on the red
25
+ * node, which is the thing that has a place. `RedNode.range` computes it once
26
+ * at construction by accumulating widths from the root and caches it, so
27
+ * consumers see exactly the same `{start, end}` they always did.
28
+ *
29
+ * This also makes incremental editing cheaper: text inserted before a subtree
30
+ * changes where it is, not what it is, so nothing about it needs rebuilding.
31
+ * `shiftGreen` — which used to deep-copy every following sibling to bump its
32
+ * offsets — no longer exists, because there is nothing left to shift.
33
+ *
34
+ * ─── Why `width` is derived and never passed ────────────────────────────────
35
+ *
36
+ * `width = leadingWidth + (children ? Σ child widths : ownWidth) + trailingWidth`
37
+ *
38
+ * The partition invariant of roadmap point 14 — every character owned exactly
39
+ * once — used to be a runtime check that a careless caller could violate. Now
40
+ * it is arithmetic the constructor performs, so a tree that does not partition
41
+ * its source cannot be built. `Syntax/partition.ts` still exists to verify the
42
+ * one thing this cannot: that the root's width matches the source length.
43
+ *
44
+ * Inspired by Roslyn's Green Tree and SwiftSyntax.
45
+ */
46
+ declare class GreenNode {
47
+ /** Syntax kind — maps to TokenKind for leaves, NodeKind for internals */
48
+ readonly kind: string;
49
+ /** Raw text of this node (empty for internal nodes) */
50
+ readonly text: string;
51
+ /** Child nodes */
52
+ readonly children: readonly GreenNode[];
53
+ /** Total width of this node in characters, delimiters included */
54
+ readonly width: number;
55
+ /** Whether this is a leaf (token) or internal node */
56
+ readonly isLeaf: boolean;
57
+ /**
58
+ * Characters this node owns BEFORE its first child — its opening delimiter.
59
+ * For `[b]hola[/b]` that is the 3 characters of `[b]`.
60
+ *
61
+ * `declare` is load-bearing here for the same reason as `_hashCache` below:
62
+ * a plain field declaration would emit a `defineProperty` per construction.
63
+ */
64
+ readonly leadingWidth: number;
65
+ /** Characters this node owns AFTER its last child — its closing delimiter. */
66
+ readonly trailingWidth: number;
67
+ /**
68
+ * Memoized structural hash — computed on first access, never invalidated.
69
+ *
70
+ * `declare` is load-bearing: with `target: ES2022`, TypeScript defaults
71
+ * `useDefineForClassFields` to true, so a plain field declaration emits a
72
+ * per-construction `defineProperty`. Measured on a 19.6 KB document that
73
+ * cost 23% of the parse phase (0.583 ms → 0.718 ms) for a slot most nodes
74
+ * never read. `declare` emits nothing; the constructor assigns it directly,
75
+ * which keeps the object shape monomorphic without the field-init overhead.
76
+ */
77
+ private _hashCache;
78
+ /**
79
+ * @param ownWidth Width of a childless node's own content: the length of a
80
+ * `text` token, or the 1-2 characters of a newline. Ignored
81
+ * when there are children, whose widths sum to the same
82
+ * thing. An empty element like `[b][/b]` passes 0 and gets
83
+ * its width from its two delimiters.
84
+ */
85
+ constructor(kind: string, text: string, children?: GreenNode[], leadingWidth?: number, trailingWidth?: number, ownWidth?: number);
86
+ /**
87
+ * Structural hash of this subtree: `(kind, text, widths, children hashes)`.
88
+ *
89
+ * Two subtrees that render identically at different offsets ARE structurally
90
+ * identical — that is the entire premise of structural sharing, and now that
91
+ * green nodes carry no position it is simply true rather than something the
92
+ * hash had to be careful to arrange.
93
+ *
94
+ * The widths ARE folded in: they are structure, not position. A `text` leaf
95
+ * of width 4 and one of width 5 are different nodes.
96
+ *
97
+ * Computed lazily: parsing paths that never intern pay nothing. Safe to
98
+ * memoize because a GreenNode is immutable once constructed.
99
+ */
100
+ get _hash(): number;
101
+ /** Get child at index */
102
+ childAt(index: number): GreenNode | undefined;
103
+ /** Find first child matching predicate */
104
+ findChild(predicate: (n: GreenNode) => boolean): GreenNode | undefined;
105
+ /** Count of direct children */
106
+ get childCount(): number;
107
+ /** Check if this node has a specific kind */
108
+ isKind(kind: string): boolean;
109
+ /**
110
+ * Offset of this node's first child, relative to this node's own start.
111
+ * Equal to `leadingWidth`; named for the places that read it as a position.
112
+ */
113
+ get innerOffset(): number;
114
+ /** Width of the span between this node's delimiters. */
115
+ get innerWidth(): number;
116
+ /** Walk all descendants in pre-order */
117
+ walk(visitor: (node: GreenNode, depth: number) => void | 'skip', depth?: number): void;
118
+ /**
119
+ * Walk all descendants in pre-order, with each node's absolute offset.
120
+ *
121
+ * The replacement for the walks that used to read `node.range`. Offsets are
122
+ * accumulated on the way down, which is the only place they exist.
123
+ */
124
+ walkWithOffset(visitor: (node: GreenNode, start: number, depth: number) => void | 'skip', start?: number, depth?: number): void;
125
+ /** Convert to a debug string */
126
+ toString(depth?: number, start?: number): string;
127
+ /** Get the source text for this node, given where it starts. */
128
+ getSourceText(source: string, start: number): string;
129
+ }
130
+ /**
131
+ * Build an internal node. Its width comes from its children plus its own
132
+ * delimiters — there is no way to declare a width that disagrees with them.
133
+ */
134
+ declare function greenNode(kind: string, text: string, children?: GreenNode[], leadingWidth?: number, trailingWidth?: number, ownWidth?: number): GreenNode;
135
+ /**
136
+ * Build a token. `width` defaults to the text's own length, which is right for
137
+ * every leaf whose text IS its source (`text` nodes). Leaves whose `text` holds
138
+ * something else — `spacing` and `empty_line` carry `''` but occupy 1-2
139
+ * characters, element nodes carry their attributes — must pass it.
140
+ */
141
+ declare function greenLeaf(kind: string, text: string, width?: number): GreenNode;
142
+
143
+ /**
144
+ * DocumentEngine — Token & Trivia Types
145
+ *
146
+ * Tokens are the output of the Lexer and input to the Parser.
147
+ * Trivia is whitespace, newlines, formatting that is preserved
148
+ * but not semantically meaningful — inspired by Roslyn.
149
+ */
150
+ type TriviaKind = 'whitespace' | 'newline' | 'carriage_return' | 'comment' | 'line_continuation';
151
+ interface Trivia {
152
+ kind: TriviaKind;
153
+ text: string;
154
+ range: Range;
155
+ }
156
+ interface Range {
157
+ start: number;
158
+ end: number;
159
+ }
160
+ type TokenKind = 'open_tag' | 'close_tag' | 'self_closing_tag' | 'text' | 'newline' | 'whitespace' | 'attribute_name' | 'attribute_value' | 'equals' | 'quote' | 'end_of_file' | 'error';
161
+ interface Token {
162
+ kind: TokenKind;
163
+ text: string;
164
+ range: Range;
165
+ /** Tag name or attribute name if applicable */
166
+ name?: string;
167
+ /** Full raw tag text including brackets: [b], [/color], [*] */
168
+ tagText?: string;
169
+ /** Raw attribute string (everything between tag name and closing ]) */
170
+ attrs?: string;
171
+ /** Trivia attached BEFORE this token (whitespace, newlines) */
172
+ leadingTrivia: Trivia[];
173
+ /** Trivia attached AFTER this token */
174
+ trailingTrivia: Trivia[];
175
+ }
176
+ interface TokenStream {
177
+ /** Peek at the current token without consuming */
178
+ peek(): Token;
179
+ /** Consume and return the current token, advance */
180
+ advance(): Token;
181
+ /** Look ahead n tokens (0 = next) */
182
+ lookAhead(n: number): Token;
183
+ /** Whether we've consumed all tokens */
184
+ isEof(): boolean;
185
+ /** Get current position in the stream */
186
+ position(): number;
187
+ /** Reset to a position */
188
+ seek(pos: number): void;
189
+ /** Get all remaining tokens */
190
+ remaining(): Token[];
191
+ /** Get all tokens */
192
+ getAll(): Token[];
193
+ }
194
+
195
+ /**
196
+ * DocumentEngine — Diagnostic Types
197
+ *
198
+ * Diagnostics are messages attached to specific nodes or ranges
199
+ * in the document tree. Inspired by VSCode's diagnostic model.
200
+ */
201
+
202
+ type DiagnosticSeverity = 'hint' | 'info' | 'warning' | 'error';
203
+ type DiagnosticTag = 'unnecessary' | 'deprecated' | 'unused' | 'redundant';
204
+ interface Diagnostic {
205
+ /** Machine-readable code (e.g. 'invalid-color', 'missing-close-tag') */
206
+ code: string;
207
+ /** Human-readable message */
208
+ message: string;
209
+ /** Severity level */
210
+ severity: DiagnosticSeverity;
211
+ /** Optional tags for additional categorization */
212
+ tags: DiagnosticTag[];
213
+ /** Source range in the original text */
214
+ range: Range | null;
215
+ /** Node ID this diagnostic is attached to */
216
+ nodeId: NodeId | null;
217
+ /** Node kind for context */
218
+ nodeKind: NodeKind | null;
219
+ /** Source of this diagnostic (built-in, plugin name, etc.) */
220
+ source: string;
221
+ /** Optional related information */
222
+ related?: DiagnosticRelatedInfo[];
223
+ /** Optional fix suggestions */
224
+ fixes?: DiagnosticFix[];
225
+ }
226
+ interface DiagnosticRelatedInfo {
227
+ message: string;
228
+ range: Range | null;
229
+ nodeId: NodeId | null;
230
+ }
231
+ interface DiagnosticFix {
232
+ description: string;
233
+ /** Whether this fix is automatic or requires user confirmation */
234
+ isAutomatic: boolean;
235
+ /** The operations to apply as a fix */
236
+ operations: FixOperation[];
237
+ }
238
+ type FixOperation = {
239
+ kind: 'replace_text';
240
+ range: Range;
241
+ newText: string;
242
+ } | {
243
+ kind: 'insert_text';
244
+ position: number;
245
+ text: string;
246
+ } | {
247
+ kind: 'delete_range';
248
+ range: Range;
249
+ } | {
250
+ kind: 'wrap_in_tag';
251
+ tagName: string;
252
+ range: Range;
253
+ };
254
+ interface DiagnosticCollection {
255
+ /** All diagnostics */
256
+ items: Diagnostic[];
257
+ /** Count by severity */
258
+ errorCount: number;
259
+ warningCount: number;
260
+ infoCount: number;
261
+ hintCount: number;
262
+ /** Quick access: only errors */
263
+ errors: Diagnostic[];
264
+ /** Quick access: only warnings */
265
+ warnings: Diagnostic[];
266
+ }
267
+
268
+ /**
269
+ * DocumentEngine — Core Types
270
+ *
271
+ * The foundational types for the entire Language Platform.
272
+ * Every layer builds on these.
273
+ */
274
+
275
+ /** Stable identifier that survives incremental re-parses */
276
+ type NodeId = string & {
277
+ readonly __brand: 'NodeId';
278
+ };
279
+ /**
280
+ * The semantic kind of a node in the document tree.
281
+ *
282
+ * Language-agnostic at the core — BBCode tags, Markdown headings,
283
+ * HTML elements, etc. all map to these.
284
+ */
285
+ type NodeKind = 'document' | 'paragraph' | 'text' | 'bold' | 'italic' | 'underline' | 'strikethrough' | 'color' | 'font_size' | 'font' | 'code' | 'inline_code' | 'spoiler' | 'heading' | 'center' | 'right' | 'left' | 'url' | 'email' | 'profile' | 'image' | 'video' | 'audio' | 'imagemap' | 'imagemap_area' | 'svg' | 'quote' | 'notice' | 'spoilerbox' | 'box' | 'boxw' | 'list' | 'list_item' | 'zalgo' | 'aesthetic' | 'sparkle' | 'bubble' | 'flower' | 'gradient' | 'grow' | 'sinewave' | 'rainbow' | 'spacing' | 'empty_line' | 'group' | 'wnotice' | 'align' | 'tables' | 'table_row' | 'table_col' | 'table_th' | 'gallery' | 'columns' | 'separator' | 'scroll' | 'sup' | 'sub' | 'abbr' | 'mark' | 'kbd' | 'tooltip' | 'flip' | 'raw' | 'plain' | 'effect' | 'anim' | 'container' | 'style_tag' | 'guild' | 'map' | 'custom' | 'error' | 'unknown';
286
+ type NodeAttributes = Record<string, string | number | boolean | null | undefined>;
287
+ interface NodeMetadata {
288
+ /** Arbitrary extensions data (plugins can add here) */
289
+ [key: string]: unknown;
290
+ }
291
+ interface SourceRange {
292
+ start: number;
293
+ end: number;
294
+ }
295
+ /**
296
+ * The primary user-facing node in the Document Model.
297
+ *
298
+ * This is the Red Tree node — mutable, with identity.
299
+ * It wraps/holds reference to the Green Tree node for structural info.
300
+ */
301
+ interface DocumentNode {
302
+ /** Stable ID that survives incremental re-parses */
303
+ id: NodeId;
304
+ /** Monotonically increasing version number for change tracking */
305
+ version: number;
306
+ /** Semantic kind */
307
+ kind: NodeKind;
308
+ /** Raw text content (for text leaves) */
309
+ text: string;
310
+ /** Tag-level attributes (e.g. color=#ff0000, size=150) */
311
+ attributes: NodeAttributes;
312
+ /** Arbitrary metadata (extensible by plugins) */
313
+ metadata: NodeMetadata;
314
+ /** Child nodes */
315
+ children: DocumentNode[];
316
+ /** Diagnostics attached to this specific node */
317
+ diagnostics: Diagnostic[];
318
+ /** Source position in the original text (for mapping back) */
319
+ sourceRange: SourceRange | null;
320
+ /** Reference back to parent (null for root) */
321
+ parentId: NodeId | null;
322
+ /** Whether this node is "synthetic" (not from source, e.g. auto-generated) */
323
+ isSynthetic: boolean;
324
+ }
325
+ type DocumentChangeKind = 'node_inserted' | 'node_deleted' | 'node_updated' | 'node_moved' | 'attribute_changed' | 'text_changed' | 'source_changed' | 'full_rebuild';
326
+ interface DocumentChangeEvent {
327
+ kind: DocumentChangeKind;
328
+ nodeId: NodeId;
329
+ /** The affected node (after the change) */
330
+ node: DocumentNode | null;
331
+ /** Previous version of the node (before the change) */
332
+ previousNode: DocumentNode | null;
333
+ /** Additional context data */
334
+ metadata?: Record<string, unknown>;
335
+ }
336
+
337
+ /**
338
+ * DocumentEngine — RedNode (Mutable Syntax Tree)
339
+ *
340
+ * The Red Tree wraps GreenNode and adds mutable state:
341
+ * - Parent references
342
+ * - Stable Node IDs
343
+ * - Version tracking
344
+ * - Diagnostic storage
345
+ *
346
+ * Multiple Red Trees can reference the same Green Tree
347
+ * (different views, different versions of edits).
348
+ *
349
+ * Inspired by Roslyn's Red Tree.
350
+ */
351
+
352
+ declare class RedNode {
353
+ /** The underlying immutable green node */
354
+ readonly green: GreenNode;
355
+ /** Stable identity */
356
+ readonly id: NodeId;
357
+ /** Parent reference (null for root) */
358
+ parent: RedNode | null;
359
+ /** Children as red nodes */
360
+ children: RedNode[];
361
+ /** Version counter for change tracking */
362
+ version: number;
363
+ /** Diagnostics for this node */
364
+ diagnostics: Diagnostic[];
365
+ /** The semantic kind */
366
+ kind: NodeKind;
367
+ /** Additional metadata */
368
+ metadata: NodeMetadata;
369
+ /**
370
+ * Cached position of this node inside `parent.children`.
371
+ *
372
+ * `declare` is load-bearing — see the note on `GreenNode._hashCache`: with
373
+ * `target: ES2022` a plain field declaration emits a `defineProperty` on every
374
+ * construction, and this class is instantiated once per node per parse.
375
+ *
376
+ * The cache is *self-validating*: `index` only trusts it when
377
+ * `parent.children[_idxCache] === this`. That single reference compare is what
378
+ * makes it safe against code that mutates the `children` array directly
379
+ * (`treeTransformers` does `cloned.children = []`) instead of going through the
380
+ * mutation methods — a stale entry is detected and recomputed rather than
381
+ * silently returning a wrong index. `-1` never validates, so it is a safe
382
+ * initial value.
383
+ */
384
+ private _idxCache;
385
+ /**
386
+ * Absolute start offset in the source.
387
+ *
388
+ * This is the red tree's job now: a green node knows its width, not its
389
+ * place, so that identical structures can be the same object (see the header
390
+ * of `GreenNode.ts`). The offset is accumulated on the way down during
391
+ * construction — one addition per node — and stored, so `range` stays the
392
+ * O(1) property every consumer already assumes it is.
393
+ *
394
+ * Held as the `Range` object itself rather than a number plus a getter that
395
+ * builds one: `range` is read from 148 call sites, and allocating there would
396
+ * trade a parse-time win for a read-time cost on every consumer. This is the
397
+ * same shape green nodes used to hold, so nothing downstream changes.
398
+ *
399
+ * `declare` for the usual reason: no `defineProperty` per construction.
400
+ */
401
+ private _range;
402
+ /**
403
+ * Pending offset shift for this subtree, applied on first read.
404
+ *
405
+ * `setStart` defers the walk: it records how far the subtree moved instead of
406
+ * adding the delta to every node's `_range` on the spot. The shift is a single
407
+ * integer here, on the adopted subtree ROOT — the node `setStart` was called
408
+ * on. Any offset read (`range`, `innerStart`, `innerEnd`, `findNodeAtOffset`)
409
+ * materializes the nearest pending ancestor's subtree via {@link materialize}.
410
+ *
411
+ * Why this is sound: a mid-document edit displaces every adopted block after
412
+ * it by the same delta, so adoption alone (thousands of `setStart` calls) was
413
+ * walking thousands of nodes per keystroke — measured as the dominant phase
414
+ * of `buildRed` (3.5-5.8 ms on the 547 KB fixture). The BlockPatcher locates
415
+ * the edited window by reference identity and only ever reads the few changed
416
+ * blocks' ranges, so the displaced-but-unchanged subtrees stay pending
417
+ * indefinitely — the walk never happens.
418
+ *
419
+ * Contract for future readers: app-layer interaction code (hover, links,
420
+ * selection ranges) DOES read `range` on user interaction. The first such
421
+ * read after a mid-document edit fires the deferred walk for the displaced
422
+ * tail — a one-time cost off the keystroke path, which is exactly the trade
423
+ * the laziness makes. Do not "optimize" by reading ranges back into the
424
+ * per-keystroke pipeline; the patcher stays churn-based by design.
425
+ *
426
+ * Composition across reparses: a node re-adopted while its ancestor is still
427
+ * pending must end up at the SUM of both deltas. `setStart` recomputes the
428
+ * delta from the untouched base `_range`, which is exactly the pending
429
+ * ancestor's base too, so composing during `materialize` (add the ancestor's
430
+ * delta into a child's pending delta) stays consistent.
431
+ */
432
+ private _lazyShift;
433
+ constructor(green: GreenNode, options?: {
434
+ id?: NodeId;
435
+ parent?: RedNode | null;
436
+ kind?: NodeKind;
437
+ diagnostics?: Diagnostic[];
438
+ metadata?: NodeMetadata;
439
+ /** Absolute start offset. Defaults to the parent's inner offset. */
440
+ start?: number;
441
+ });
442
+ /**
443
+ * Move this node (and its subtree) to a new absolute offset.
444
+ *
445
+ * Only for the tree-building paths, which create a node before they know
446
+ * where its parent will put it. Callers that mutate a live tree should
447
+ * rebuild instead — a red node's offset must always agree with its place.
448
+ *
449
+ * The move is deferred, not walked: the delta is recorded as a pending
450
+ * shift and applied by any offset read the first time the subtree is
451
+ * actually touched. Adoption calls this once per displaced block (thousands
452
+ * on a mid-document edit), and almost none of those subtrees are read on the
453
+ * same keystroke's hot path — the BlockPatcher locates the edit window by
454
+ * reference identity and only reads the few changed blocks.
455
+ *
456
+ * Re-adoption composes: a node whose subtree was shifted in a previous
457
+ * reparse and never read still carries a pending `_lazyShift`. The new
458
+ * target is absolute, so the delta is measured from the CURRENT effective
459
+ * start (`_range.start + _lazyShift`) and accumulated, not overwritten —
460
+ * otherwise the earlier shift would be applied twice.
461
+ */
462
+ setStart(start: number): void;
463
+ /**
464
+ * Apply `delta` to `node`'s subtree, composing with any nested pending shift.
465
+ *
466
+ * A node with its own pending shift has a base `_range` the parent's delta is
467
+ * relative to as well (both were computed from the same pre-shift tree), so
468
+ * the parent's delta can be folded into the child's pending delta instead of
469
+ * into its `_range` — the child's later materialization applies the sum. This
470
+ * is what makes nested shifts across reparses compose without double counting.
471
+ */
472
+ private static applyShift;
473
+ /**
474
+ * Materialize every pending shift on the path from here to the root.
475
+ *
476
+ * Walks ancestors top-down (root-most first), applying each pending delta to
477
+ * its subtree. Top-down order matters: an ancestor's delta must land in a
478
+ * child's pending delta BEFORE the child's own materialization runs, or the
479
+ * two shifts would be applied to different bases. After the ancestors are
480
+ * settled, this node itself is materialized if it still carries a shift.
481
+ *
482
+ * The common case — nothing pending anywhere on the path — is one upward
483
+ * pointer walk that allocates nothing: `_range` is read from 148 call sites,
484
+ * and a per-read allocation there would show up in every phase. The chain
485
+ * array is only built after a pending shift is actually found.
486
+ */
487
+ private materialize;
488
+ private static _isMutating;
489
+ /**
490
+ * Run `fn` inside a mutation boundary, so the structural mutators below are
491
+ * allowed to run. Nests safely; the previous state is restored on exit.
492
+ *
493
+ * Not needed for building a fresh subtree — see {@link initChildren}.
494
+ */
495
+ static allowMutation<T>(fn: () => T): T;
496
+ private assertMutating;
497
+ get text(): string;
498
+ /** Absolute span in the source. */
499
+ get range(): Range;
500
+ /** Absolute offset of this node's first child, past its opening delimiter. */
501
+ get innerStart(): number;
502
+ /** Absolute offset where this node's closing delimiter begins. */
503
+ get innerEnd(): number;
504
+ get isLeaf(): boolean;
505
+ get childCount(): number;
506
+ childAt(index: number): RedNode | undefined;
507
+ /** Get the root of the tree */
508
+ get root(): RedNode;
509
+ /** Get the depth from root */
510
+ get depth(): number;
511
+ /** Get previous sibling */
512
+ get previousSibling(): RedNode | null;
513
+ /** Get next sibling */
514
+ get nextSibling(): RedNode | null;
515
+ /**
516
+ * Position of this node within its parent, or -1 for a root.
517
+ *
518
+ * O(1) in the steady state. The cached index is seeded by the mutation methods
519
+ * (so the construction path never searches at all) and verified by a single
520
+ * reference compare on read, falling back to `indexOf` only when it is stale.
521
+ *
522
+ * This getter is hot: `HTMLRenderer.isPrevBlockBoundary` walks siblings
523
+ * backwards, so an O(n) implementation here made rendering quadratic —
524
+ * measured 14.11 ms for 4000 root siblings, growing 2.6x per doubling.
525
+ */
526
+ get index(): number;
527
+ /** Walk all descendants in pre-order */
528
+ walk(visitor: (node: RedNode, depth: number) => void | 'skip', depth?: number): void;
529
+ /** Find a descendant by ID */
530
+ findById(id: NodeId): RedNode | null;
531
+ /**
532
+ * Find the deepest RedNode containing the given text offset.
533
+ *
534
+ * The range is half-open — a node owns `[start, end)` — y eso no es un
535
+ * detalle: con el final inclusivo, un offset que cae justo en una frontera
536
+ * pertenecía a DOS nodos, al que termina ahí y al que empieza. Como los
537
+ * hijos se recorren en orden, ganaba el que termina, así que preguntar por
538
+ * el principio de un nodo devolvía **el nodo anterior**.
539
+ *
540
+ * Se veía en el preview: en un degradado, cada carácter es su propio nodo
541
+ * de color, y hacer clic en la `m` de «Welcome» resaltaba la `o`. Monaco
542
+ * seleccionaba bien —usa el rango del nodo, no esta búsqueda—, así que el
543
+ * desfase era solo del resaltado, que es justo lo que hacía difícil verlo.
544
+ *
545
+ * La única excepción es el final del documento: un cursor aparcado tras el
546
+ * último carácter no tiene carácter que lo contenga, y la raíz lo reclama
547
+ * para que quien pregunte reciba algo con sentido.
548
+ */
549
+ findNodeAtOffset(offset: number): RedNode | null;
550
+ /**
551
+ * Adopt a fully-built array of children in one shot.
552
+ *
553
+ * Construction-only, and deliberately outside the mutation lock. The tree
554
+ * builders used to call `allowMutation(() => { for (…) appendChild(…) })` once
555
+ * **per node**, which on a 1736-node document meant 1736 closures, 1736
556
+ * `try/finally` frames and 1736 static-flag saves — pure ceremony, since a
557
+ * node that no caller has seen yet cannot be observed mid-mutation. This
558
+ * populates `children`, `parent` and the index cache in a single pass instead.
559
+ *
560
+ * `version` intentionally stays at 1: a freshly built node has not been
561
+ * *edited*, and bumping once per child would make the initial version an
562
+ * accidental child count.
563
+ *
564
+ * Only safe while `this` is still unreachable from the rest of the tree. Use
565
+ * `appendChild` and friends for anything after that.
566
+ */
567
+ initChildren(children: RedNode[]): void;
568
+ /**
569
+ * Append a child node.
570
+ * This creates a new RedNode wrapping the green node.
571
+ */
572
+ appendChild(child: RedNode): void;
573
+ /**
574
+ * Refresh the cached index of every child from `from` onwards.
575
+ *
576
+ * Called after a splice, which shifts the tail. This is O(n) but so is the
577
+ * splice it follows, so it costs nothing asymptotically and keeps reads O(1).
578
+ */
579
+ private reindexFrom;
580
+ /**
581
+ * Insert a child at a specific index.
582
+ */
583
+ insertChildAt(index: number, child: RedNode): void;
584
+ /**
585
+ * Remove a child by ID.
586
+ */
587
+ removeChild(id: NodeId): RedNode | null;
588
+ /**
589
+ * Replace a child with a new one by ID.
590
+ */
591
+ replaceChild(id: NodeId, newChild: RedNode): boolean;
592
+ /**
593
+ * Bump version (when attributes/metadata change without structural change).
594
+ */
595
+ bumpVersion(): void;
596
+ /** Convert to the public DocumentNode interface */
597
+ toDocumentNode(): DocumentNode;
598
+ /** Debug string */
599
+ toString(depth?: number): string;
600
+ }
601
+
602
+ /**
603
+ * DocumentEngine — NodeMatcher
604
+ *
605
+ * Matches nodes between old and new syntax trees after edits.
606
+ * Uses stable IDs, fingerprints, and source ranges to determine
607
+ * which nodes were moved, inserted, deleted, or unchanged.
608
+ *
609
+ * Critical for:
610
+ * - Maintaining selection across re-parses
611
+ * - Preserving scroll position
612
+ * - Smooth animations in visual blocks mode
613
+ * - Undo/redo integrity
614
+ */
615
+
616
+ type MatchStatus = 'exact' | 'moved' | 'updated' | 'inserted' | 'deleted' | 'merged' | 'split';
617
+ interface MatchResult {
618
+ oldNode: RedNode | null;
619
+ newNode: RedNode | null;
620
+ status: MatchStatus;
621
+ confidence: number;
622
+ }
623
+ interface NodeMatch {
624
+ /** Map from old node ID to new node */
625
+ oldToNew: Map<NodeId, RedNode>;
626
+ /** Map from new node ID to old node */
627
+ newToOld: Map<NodeId, RedNode>;
628
+ /** All matches */
629
+ matches: MatchResult[];
630
+ /** Statistics */
631
+ stats: {
632
+ exact: number;
633
+ moved: number;
634
+ updated: number;
635
+ inserted: number;
636
+ deleted: number;
637
+ };
638
+ }
639
+ declare class NodeMatcher {
640
+ /**
641
+ * Match nodes between old and new trees.
642
+ *
643
+ * Strategy (in order):
644
+ * 1. Match by stable ID (if present)
645
+ * 2. Match by fingerprint (content hash)
646
+ * 3. Match by source range overlap
647
+ * 4. Fall back to positional heuristic
648
+ */
649
+ match(oldRoot: RedNode, newRoot: RedNode): NodeMatch;
650
+ /**
651
+ * Phase 0 — descend in lockstep, pairing every subtree that did not change.
652
+ *
653
+ * At each level the common prefix and common suffix are trimmed by comparing
654
+ * `green._hash`, and only the window between them is descended into. That is
655
+ * what makes this robust to an edit that changes the number of children:
656
+ * requiring equal child counts before recursing sounds reasonable but means a
657
+ * single inserted node near the root disables the fast path for the whole
658
+ * tree — measured, it left `match` at 5.11 ms instead of 0.96 ms.
659
+ *
660
+ * Whatever is left in the middle window is handed to the fingerprint
661
+ * heuristic, which is the part that can cope with reordering.
662
+ */
663
+ private pairUnchanged;
664
+ /**
665
+ * Compute a content fingerprint for a node's subtree.
666
+ *
667
+ * Two nodes share a fingerprint iff their subtrees have the same pre-order
668
+ * sequence of (kind, text) pairs — the same equivalence the previous
669
+ * string-concatenating implementation tested, but as a 64-bit structural
670
+ * hash instead of an O(subtree) string built on every comparison.
671
+ */
672
+ fingerprint(node: RedNode): string;
673
+ /** Memoized, bottom-up fingerprint. O(n) across a whole tree. */
674
+ private fingerprintInto;
675
+ }
676
+
677
+ /**
678
+ * DocumentEngine — ChangeTracker
679
+ *
680
+ * Tracks text changes made to the document source.
681
+ * Used by IncrementalParser to determine what needs to be re-parsed.
682
+ */
683
+ interface TextChange {
684
+ start: number;
685
+ end: number;
686
+ text: string;
687
+ }
688
+ /**
689
+ * Source range of a text edit, in BOTH coordinate systems.
690
+ *
691
+ * An edit sits between two documents: `start`/`end` describe the changed
692
+ * region in NEW-source coordinates (the current document), while `endOld` is
693
+ * the end of the replaced region in OLD-source coordinates. The incremental
694
+ * preview (`BlockPatcher`) uses `start`/`endOld` to locate the affected blocks
695
+ * in the PREVIOUS tree (whose offsets are pre-edit) and `start`/`end` in the
696
+ * new one — insertions or deletions between the two make the naive single-`end`
697
+ * wrong, which is exactly why both ends are kept.
698
+ */
699
+ interface TextChangeRange {
700
+ /** Start of the edited region (identical in both coordinate systems). */
701
+ start: number;
702
+ /** End of the edited region, in new-source coordinates. */
703
+ end: number;
704
+ /** End of the replaced region, in old-source coordinates. */
705
+ endOld: number;
706
+ }
707
+ interface TextChangeStats {
708
+ totalChanges: number;
709
+ totalInserted: number;
710
+ totalDeleted: number;
711
+ lastChange: TextChange | null;
712
+ }
713
+ declare class ChangeTracker {
714
+ private changes;
715
+ private maxHistory;
716
+ constructor(maxHistory?: number);
717
+ /**
718
+ * Track a text change.
719
+ */
720
+ track(change: TextChange): void;
721
+ /**
722
+ * Get all tracked changes.
723
+ */
724
+ getAll(): TextChange[];
725
+ /**
726
+ * Get the most recent change.
727
+ */
728
+ getLast(): TextChange | null;
729
+ /**
730
+ * Get the affected range for the last N changes.
731
+ * Returns null if no changes have been tracked.
732
+ */
733
+ getAffectedRange(count?: number): {
734
+ start: number;
735
+ end: number;
736
+ } | null;
737
+ /**
738
+ * Clear change history.
739
+ */
740
+ clear(): void;
741
+ /**
742
+ * Get statistics about tracked changes.
743
+ */
744
+ getStats(): TextChangeStats;
745
+ }
746
+
747
+ /**
748
+ * DocumentEngine — IncrementalParser
749
+ *
750
+ * Re-parses the smallest region a text change can have affected, instead of
751
+ * the whole document.
752
+ *
753
+ * ─── Why this was rewritten (roadmap S5 / point 9) ──────────────────────────
754
+ *
755
+ * The previous implementation spliced RED nodes in place and never touched
756
+ * ranges. Three consequences, all measured:
757
+ *
758
+ * - The re-parsed subtree came from `newSource.slice(start, end)`, so its
759
+ * ranges were based at 0 and were grafted in without rebasing. Every offset
760
+ * inside the edited region was wrong by `start`.
761
+ * - Siblings after the splice and every ancestor kept their old ranges, so
762
+ * the tree silently disagreed with the text it claimed to describe.
763
+ * - It returned `newRootRed.green` — the OLD green root — so the model's
764
+ * `_greenRoot` and `_redRoot` desynchronised permanently after the first
765
+ * incremental edit.
766
+ *
767
+ * Rendering the result and comparing it against a full rebuild of the same
768
+ * final text: 3 of 6 realistic editing scenarios produced DIFFERENT HTML, and
769
+ * one grew 14 phantom nodes (1752 vs 1738).
770
+ *
771
+ * ─── How it works now ───────────────────────────────────────────────────────
772
+ *
773
+ * Everything happens on the GREEN tree, and the red tree is derived from it.
774
+ * That is not a stylistic choice: green nodes carry widths and no position, so
775
+ * a position is something only a red node has — and therefore only a red node
776
+ * can be wrong about.
777
+ *
778
+ * 1. Descend to the deepest node whose inner span (the part between its
779
+ * delimiters) contains the change, refusing to enter the kinds whose
780
+ * children depend on context outside them (see `OPAQUE_KINDS`).
781
+ * 2. Take the run of that node's children the change touches, widened by one
782
+ * on each side, and re-parse just those.
783
+ * 3. Splice the result back over that run and rebuild the ancestor spine,
784
+ * sharing every untouched subtree by reference.
785
+ *
786
+ * The unit is a RUN OF SIBLINGS, not a whole node. Re-parsing a container's
787
+ * entire contents because one character changed inside it meant typing into a
788
+ * 13 KB `[notice]` re-lexed 66% of the document per keystroke — and an edit at
789
+ * document level had no enclosing container at all, so it fell back to a full
790
+ * rebuild, which is exactly where the caret sits while you write the end of a
791
+ * post.
792
+ *
793
+ * Step 1 relies on the partition invariant from point 14: without it, "the
794
+ * part between the delimiters" is not a well-defined range, which is exactly
795
+ * why this repair was blocked on that work.
796
+ *
797
+ * When any precondition fails the parser returns a full rebuild rather than a
798
+ * plausible-looking wrong tree. `path` says which happened, `reason` says why.
799
+ */
800
+
801
+ interface EditOperation {
802
+ kind: 'insert' | 'delete' | 'replace';
803
+ start: number;
804
+ end: number;
805
+ text: string;
806
+ /** The minimal range that needs re-parsing */
807
+ affectedStart: number;
808
+ affectedEnd: number;
809
+ }
810
+ /** Options a caller's parse callback must understand. */
811
+ interface ReparseParseOptions {
812
+ /**
813
+ * Whether the text being parsed is document-level content. Inner spans of
814
+ * containers are not, and must not be grouped into paragraphs.
815
+ */
816
+ normalizeParagraphs: boolean;
817
+ }
818
+ interface ReparseResult {
819
+ green: GreenNode;
820
+ red: RedNode;
821
+ /** Nodes that were affected by the change */
822
+ affectedNodes: RedNode[];
823
+ /** Time taken in ms (total) */
824
+ duration: number;
825
+ /** Per-phase timing breakdown in ms */
826
+ timings: {
827
+ findAffected: number;
828
+ safeBoundary: number;
829
+ parse: number;
830
+ buildRed: number;
831
+ mutate: number;
832
+ other: number;
833
+ };
834
+ /** Which path was used */
835
+ path: 'incremental' | 'full_rebuild';
836
+ /** When `full_rebuild`, why the incremental path was declined. */
837
+ reason?: FallbackReason;
838
+ }
839
+ type FallbackReason =
840
+ /** No sibling window could be formed around the change. */
841
+ 'no-window'
842
+ /** The change touches a container's own delimiter. */
843
+ | 'touches-delimiter'
844
+ /** The region cannot be lexed in isolation — see `regionIsSelfContained`. */
845
+ | 'region-not-isolated'
846
+ /** An unclosed `[` before the region could claim a `]` the edit creates. */
847
+ | 'open-bracket-before'
848
+ /** The container covers so much of the document that a rebuild is cheaper. */
849
+ | 'region-too-large'
850
+ /** The document is small enough that rebuilding it outright costs less. */
851
+ | 'document-too-small'
852
+ /** The tree's ranges disagree with the source it is supposed to describe. */
853
+ | 'stale-ranges';
854
+ interface IncrementalParserOptions {
855
+ /** Override `MIN_SOURCE_LENGTH`. Set to 0 to always attempt a splice. */
856
+ minSourceLength?: number;
857
+ /** Override `MAX_REGION_FRACTION`. */
858
+ maxRegionFraction?: number;
859
+ }
860
+ declare class IncrementalParser {
861
+ private readonly minSourceLength;
862
+ private readonly maxRegionFraction;
863
+ /**
864
+ * The thresholds are constructor options because they are performance
865
+ * tuning, not semantics: the tree that comes out is the same either way, so
866
+ * a caller with a different document profile — or a test that wants to
867
+ * exercise the splice on a two-line document — can move them without
868
+ * changing what the parser means.
869
+ */
870
+ constructor(options?: IncrementalParserOptions);
871
+ /**
872
+ * Reparse a tree after a text change.
873
+ *
874
+ * Always returns a result — either an incremental splice or a full rebuild.
875
+ * It never returns a tree whose ranges do not describe `newSource`.
876
+ */
877
+ reparse(oldRed: RedNode, oldGreen: GreenNode, change: TextChange, newSource: string, parseCallback: (text: string, options?: ReparseParseOptions) => GreenNode, buildRedCallback: (green: GreenNode) => RedNode): ReparseResult;
878
+ /**
879
+ * Find the run of sibling children a change can have affected.
880
+ *
881
+ * Two steps. First descend to the deepest node whose INNER span contains the
882
+ * change — inner rather than full, so a container's own delimiters never go
883
+ * back through the parser: an edit that touches `[colo|r=red]` changes what
884
+ * that element IS, and is handled by re-parsing it as part of its parent's
885
+ * window instead.
886
+ *
887
+ * Then pick the children that the change touches, widened by one on each
888
+ * side. The widening is what lets a deletion MERGE two nodes: removing the
889
+ * blank line between two paragraphs changes only the node in between, and
890
+ * without a neighbour on each side the re-parse could not see that the two
891
+ * survivors have to become one.
892
+ */
893
+ private findReparseWindow;
894
+ /**
895
+ * Nodes on the path from the root down to the change.
896
+ *
897
+ * Kept because it is part of the public surface and is genuinely useful for
898
+ * callers that want to know what an edit touched; the reparse itself no
899
+ * longer needs it.
900
+ */
901
+ findAffectedNodes(root: RedNode, change: TextChange): RedNode[];
902
+ }
903
+
904
+ /**
905
+ * DocumentEngine — SemanticAnalyzer
906
+ *
907
+ * Walks the Red Tree and produces diagnostics by applying
908
+ * semantic rules to the syntax tree.
909
+ *
910
+ * This is where language-specific validation happens.
911
+ * The analyzer is extensible via registered validators.
912
+ *
913
+ * Inspired by Roslyn's Semantic Model and LSP diagnostics.
914
+ */
915
+
916
+ interface Validator {
917
+ /** Unique code for this validator (e.g. 'invalid-color') */
918
+ code: string;
919
+ /** Severity of issues found by this validator */
920
+ severity: DiagnosticSeverity;
921
+ /**
922
+ * Node kinds this validator can ever fire on. Omit to run on every node.
923
+ *
924
+ * Three of the five built-ins open with nothing but a kind test, so on a
925
+ * document of 1736 nodes they were called 1736 times each to answer a
926
+ * question the dispatcher can answer once. Declaring the kinds turns the
927
+ * call into a lookup that never happens.
928
+ */
929
+ kinds?: readonly string[];
930
+ /** Validate a node. Return diagnostics or null */
931
+ validate(node: RedNode, context: AnalyzerContext): Diagnostic | Diagnostic[] | null;
932
+ }
933
+ interface AnalyzerContext {
934
+ /**
935
+ * Every node in the tree, by id, for cross-reference validation.
936
+ *
937
+ * A getter, and deliberately: building this Map cost a second full walk of
938
+ * the tree plus 1736 `Map.set` calls — 23% of `analyze()` — and **no
939
+ * validator has ever read it**. It was built so that the caller could look up
940
+ * by id the node to attach each diagnostic to, which is the node the
941
+ * validator was looking at when it produced it. Validators that genuinely
942
+ * need cross-references still get it; everyone else stops paying for it.
943
+ */
944
+ readonly allNodes: Map<string, RedNode>;
945
+ /** Previously collected diagnostics */
946
+ diagnostics: DiagnosticCollection;
947
+ /** Source text for position lookups */
948
+ source: string;
949
+ }
950
+ interface AnalyzeResult {
951
+ diagnostics: DiagnosticCollection;
952
+ /** Time taken in ms */
953
+ duration: number;
954
+ /** Number of nodes analyzed */
955
+ nodesAnalyzed: number;
956
+ }
957
+ /**
958
+ * What `analyze()` actually returns: an {@link AnalyzeResult} plus the node
959
+ * index it had to build anyway for cross-reference lookups.
960
+ *
961
+ * Kept as a separate type, and deliberately NOT part of `AnalyzeResult`,
962
+ * because the index holds a strong reference to every node in the tree. A
963
+ * caller that stores an `AnalyzeResult` long-term (as `DocumentModel` does)
964
+ * must not pin an entire stale tree; one that needs the index gets it here and
965
+ * owns that decision explicitly.
966
+ */
967
+ interface IndexedAnalyzeResult extends AnalyzeResult {
968
+ allNodes: Map<string, RedNode>;
969
+ }
970
+ declare class SemanticAnalyzer {
971
+ private validators;
972
+ /**
973
+ * The same validators, arranged for the walk instead of for lookup.
974
+ *
975
+ * `_always` run on every node; `_byKind` are the ones that declared their
976
+ * kinds. Iterating the Map itself allocated an iterator and a destructuring
977
+ * pair per node — 9% of `analyze()` spent on bookkeeping, not on validating.
978
+ *
979
+ * Rebuilt on register/unregister, which happen once at construction and
980
+ * essentially never afterwards.
981
+ */
982
+ private _always;
983
+ private _byKind;
984
+ constructor();
985
+ /**
986
+ * Register a validator.
987
+ */
988
+ register(validator: Validator): void;
989
+ /**
990
+ * Remove a validator.
991
+ */
992
+ unregister(code: string): void;
993
+ private rebuildDispatch;
994
+ /**
995
+ * Analyze a Red Tree and produce diagnostics.
996
+ */
997
+ analyze(root: RedNode, source: string): IndexedAnalyzeResult;
998
+ private registerBuiltinValidators;
999
+ /**
1000
+ * Create a validator for a specific tag/kind.
1001
+ * Convenience method for plugin authors.
1002
+ */
1003
+ createValidator(code: string, severity: DiagnosticSeverity, predicate: (node: RedNode, ctx: AnalyzerContext) => string | null): Validator;
1004
+ }
1005
+
1006
+ /**
1007
+ * DocumentEngine — Operation & Transaction Types
1008
+ *
1009
+ * Operations are the atomic units of change in the document.
1010
+ * Every modification goes through operations, never direct mutation.
1011
+ *
1012
+ * Inspired by ProseMirror and Roslyn.
1013
+ */
1014
+
1015
+ type OperationKind = 'insert_node' | 'delete_node' | 'replace_node' | 'move_node' | 'update_attributes' | 'set_text' | 'insert_text' | 'delete_text' | 'replace_text' | 'wrap_in_tag' | 'unwrap_node' | 'split_node' | 'merge_nodes';
1016
+ interface BaseOperation {
1017
+ kind: OperationKind;
1018
+ /** Stable ID for the operation (for undo/redo pairing) */
1019
+ id: string;
1020
+ /** Whether this operation can be undone */
1021
+ undoable: boolean;
1022
+ /** Timestamp */
1023
+ timestamp: number;
1024
+ }
1025
+ interface InsertNodeOperation extends BaseOperation {
1026
+ kind: 'insert_node';
1027
+ parentId: NodeId;
1028
+ index: number;
1029
+ node: RedNode;
1030
+ }
1031
+ interface DeleteNodeOperation extends BaseOperation {
1032
+ kind: 'delete_node';
1033
+ nodeId: NodeId;
1034
+ /** The deleted node (stored for undo) */
1035
+ node: RedNode;
1036
+ parentId: NodeId;
1037
+ index: number;
1038
+ }
1039
+ interface ReplaceNodeOperation extends BaseOperation {
1040
+ kind: 'replace_node';
1041
+ nodeId: NodeId;
1042
+ newNode: RedNode;
1043
+ oldNode: RedNode;
1044
+ }
1045
+ interface MoveNodeOperation extends BaseOperation {
1046
+ kind: 'move_node';
1047
+ nodeId: NodeId;
1048
+ fromParentId: NodeId;
1049
+ fromIndex: number;
1050
+ toParentId: NodeId;
1051
+ toIndex: number;
1052
+ }
1053
+ interface UpdateAttributesOperation extends BaseOperation {
1054
+ kind: 'update_attributes';
1055
+ nodeId: NodeId;
1056
+ newAttributes: NodeAttributes;
1057
+ oldAttributes: NodeAttributes;
1058
+ }
1059
+ interface SetTextOperation extends BaseOperation {
1060
+ kind: 'set_text';
1061
+ nodeId: NodeId;
1062
+ newText: string;
1063
+ oldText: string;
1064
+ }
1065
+ interface InsertTextOperation extends BaseOperation {
1066
+ kind: 'insert_text';
1067
+ nodeId: NodeId;
1068
+ position: number;
1069
+ text: string;
1070
+ }
1071
+ interface DeleteTextOperation extends BaseOperation {
1072
+ kind: 'delete_text';
1073
+ nodeId: NodeId;
1074
+ position: number;
1075
+ length: number;
1076
+ text: string;
1077
+ }
1078
+ interface ReplaceTextOperation extends BaseOperation {
1079
+ kind: 'replace_text';
1080
+ nodeId: NodeId;
1081
+ position: number;
1082
+ length: number;
1083
+ newText: string;
1084
+ oldText: string;
1085
+ }
1086
+ interface WrapInTagOperation extends BaseOperation {
1087
+ kind: 'wrap_in_tag';
1088
+ nodeId: NodeId;
1089
+ tagName: string;
1090
+ attributes: NodeAttributes;
1091
+ }
1092
+ interface UnwrapNodeOperation extends BaseOperation {
1093
+ kind: 'unwrap_node';
1094
+ nodeId: NodeId;
1095
+ parentId: NodeId;
1096
+ children: RedNode[];
1097
+ }
1098
+ interface SplitNodeOperation extends BaseOperation {
1099
+ kind: 'split_node';
1100
+ nodeId: NodeId;
1101
+ position: number;
1102
+ leftNode: RedNode;
1103
+ rightNode: RedNode;
1104
+ }
1105
+ interface MergeNodesOperation extends BaseOperation {
1106
+ kind: 'merge_nodes';
1107
+ leftNodeId: NodeId;
1108
+ rightNodeId: NodeId;
1109
+ mergedNode: RedNode;
1110
+ }
1111
+ type Operation = InsertNodeOperation | DeleteNodeOperation | ReplaceNodeOperation | MoveNodeOperation | UpdateAttributesOperation | SetTextOperation | InsertTextOperation | DeleteTextOperation | ReplaceTextOperation | WrapInTagOperation | UnwrapNodeOperation | SplitNodeOperation | MergeNodesOperation;
1112
+
1113
+ /**
1114
+ * DocumentEngine — UndoManager
1115
+ *
1116
+ * Manages the undo/redo history stack.
1117
+ *
1118
+ * Entries are source-text snapshots, not stored tree mutations: the model
1119
+ * rebuilds from text on undo/redo, so an entry stays replayable even after
1120
+ * intervening rebuilds have replaced every node id it could have referenced.
1121
+ * (The previous design stored `Transaction`s, whose `invert()` was only
1122
+ * defined for one of the six operation kinds and whose one-shot `_applied`
1123
+ * flag made redo a silent no-op.)
1124
+ *
1125
+ * Inspired by VSCode's undo stack.
1126
+ */
1127
+ interface UndoEntry {
1128
+ /** Source text before the change */
1129
+ before: string;
1130
+ /** Source text after the change */
1131
+ after: string;
1132
+ label: string;
1133
+ timestamp: number;
1134
+ }
1135
+ declare class UndoManager {
1136
+ private undoStack;
1137
+ private redoStack;
1138
+ private maxSize;
1139
+ constructor(maxSize?: number);
1140
+ /**
1141
+ * Push a change (as before/after source snapshots) onto the undo stack.
1142
+ */
1143
+ push(snapshot: {
1144
+ before: string;
1145
+ after: string;
1146
+ }, label?: string): void;
1147
+ /**
1148
+ * Undo the last change.
1149
+ */
1150
+ undo(): UndoEntry | null;
1151
+ /**
1152
+ * Redo the last undone change.
1153
+ */
1154
+ redo(): UndoEntry | null;
1155
+ /**
1156
+ * Peek at the top of the undo stack without removing.
1157
+ */
1158
+ peekUndo(): UndoEntry | null;
1159
+ /**
1160
+ * Peek at the top of the redo stack without removing.
1161
+ */
1162
+ peekRedo(): UndoEntry | null;
1163
+ /**
1164
+ * Clear both stacks.
1165
+ */
1166
+ clear(): void;
1167
+ /**
1168
+ * Get the number of undoable actions.
1169
+ */
1170
+ get undoCount(): number;
1171
+ /**
1172
+ * Get the number of redoable actions.
1173
+ */
1174
+ get redoCount(): number;
1175
+ }
1176
+
1177
+ /**
1178
+ * DocumentEngine — TreeBuilder
1179
+ *
1180
+ * Converts a TokenStream into a GreenNode tree.
1181
+ * This is the bridge between the Lexer and the Syntax Tree.
1182
+ *
1183
+ * The TreeBuilder is language-agnostic — it uses registry-based
1184
+ * node factories to build the appropriate tree structure.
1185
+ */
1186
+
1187
+ interface BuildResult {
1188
+ green: GreenNode;
1189
+ red: RedNode;
1190
+ tokens: Token[];
1191
+ }
1192
+ interface NodeFactory$1 {
1193
+ /** Create a GreenNode from a token or sequence of tokens */
1194
+ createGreen(tokens: Token[], stream: TokenStream): GreenNode | null;
1195
+ /** Create a RedNode from a green node */
1196
+ createRed(green: GreenNode, parent?: RedNode | null): RedNode;
1197
+ }
1198
+ declare class TreeBuilder {
1199
+ private factories;
1200
+ private defaultKind;
1201
+ constructor(defaultKind?: NodeKind);
1202
+ /**
1203
+ * Register a node factory for a specific token kind or tag name.
1204
+ */
1205
+ register(kind: string, factory: NodeFactory$1): void;
1206
+ /**
1207
+ * Build a tree from a token stream.
1208
+ */
1209
+ build(stream: TokenStream): BuildResult;
1210
+ /**
1211
+ * Build a green tree from a token array.
1212
+ * Uses registered factories to determine node structure.
1213
+ *
1214
+ * By default, creates a flat document structure.
1215
+ * Language-specific builders (like BBCodeParser) override this.
1216
+ */
1217
+ buildGreen(tokens: Token[]): GreenNode;
1218
+ /**
1219
+ * Build a red tree from a green tree.
1220
+ */
1221
+ buildRed(green: GreenNode, parent?: RedNode | null, kind?: NodeKind): RedNode;
1222
+ /**
1223
+ * Create a RedNode directly from children (for language-specific parsers).
1224
+ */
1225
+ createRedFromChildren(kind: NodeKind, text: string, children: RedNode[], parent?: RedNode | null): RedNode;
1226
+ private tagToKind;
1227
+ }
1228
+
1229
+ /**
1230
+ * DocumentEngine — Query Types
1231
+ *
1232
+ * CSS-inspired query selectors for the document tree.
1233
+ * Enables queries like: `paragraph > bold`, `quote text`, `[color]`
1234
+ */
1235
+
1236
+ type QuerySelector = {
1237
+ type: 'kind';
1238
+ kind: NodeKind;
1239
+ } | {
1240
+ type: 'tag';
1241
+ tag: string;
1242
+ } | {
1243
+ type: 'attribute';
1244
+ name: string;
1245
+ value?: string;
1246
+ } | {
1247
+ type: 'has_attribute';
1248
+ name: string;
1249
+ } | {
1250
+ type: 'text';
1251
+ pattern: string;
1252
+ } | {
1253
+ type: 'has_child';
1254
+ selector: QuerySelector;
1255
+ } | {
1256
+ type: 'has_descendant';
1257
+ selector: QuerySelector;
1258
+ } | {
1259
+ type: 'nth_child';
1260
+ n: number;
1261
+ } | {
1262
+ type: 'first_child';
1263
+ } | {
1264
+ type: 'last_child';
1265
+ } | {
1266
+ type: 'position';
1267
+ min?: number;
1268
+ max?: number;
1269
+ } | {
1270
+ type: 'depth';
1271
+ min?: number;
1272
+ max?: number;
1273
+ };
1274
+ type QueryCombinator = 'child' | 'descendant' | 'adjacent' | 'sibling';
1275
+ interface QueryStep {
1276
+ selector: QuerySelector[];
1277
+ combinator: QueryCombinator | 'root';
1278
+ }
1279
+ interface Query {
1280
+ steps: QueryStep[];
1281
+ }
1282
+ interface QueryMatch {
1283
+ node: RedNode;
1284
+ /** How well this node matches (for sorting results) */
1285
+ score: number;
1286
+ /** The specific selectors that matched */
1287
+ matchedSelectors: string[];
1288
+ }
1289
+ interface QueryResult {
1290
+ matches: QueryMatch[];
1291
+ total: number;
1292
+ time: number;
1293
+ }
1294
+
1295
+ /**
1296
+ * DocumentEngine — QueryEngine
1297
+ *
1298
+ * CSS-inspired query engine for the document tree.
1299
+ * Enables queries like:
1300
+ * query('paragraph > bold')
1301
+ * query('quote text')
1302
+ * query('[color]')
1303
+ * query('list > *')
1304
+ *
1305
+ * Useful for:
1306
+ * - Finding nodes by structure
1307
+ * - AI context gathering
1308
+ * - Plugin discovery
1309
+ * - Lint rule targeting
1310
+ */
1311
+
1312
+ declare class QueryEngine {
1313
+ /**
1314
+ * Execute a query against a RedNode tree.
1315
+ */
1316
+ execute(query: Query, root: RedNode): QueryResult;
1317
+ /**
1318
+ * Filter candidates by applying a query step's selectors.
1319
+ */
1320
+ private matchStep;
1321
+ /**
1322
+ * Check if a single node matches a single selector.
1323
+ */
1324
+ private matchesSelector;
1325
+ }
1326
+
1327
+ /**
1328
+ * DocumentEngine — EventBus
1329
+ *
1330
+ * Publish/subscribe event system for document changes.
1331
+ * Each DocumentModel has its own EventBus instance.
1332
+ *
1333
+ * Events include:
1334
+ * - document_changed (rebuild, edit, undo/redo)
1335
+ * - node_changed (insert, delete, update, move)
1336
+ * - diagnostic_updated
1337
+ * - transaction_applied
1338
+ * - cursor_moved
1339
+ * - selection_changed
1340
+ */
1341
+
1342
+ type DocumentEventType = 'document_changed' | 'node_inserted' | 'node_deleted' | 'node_updated' | 'diagnostics_updated' | 'transaction_applied' | 'undo_performed' | 'redo_performed' | 'cursor_moved' | 'selection_changed';
1343
+ interface DocumentEvent {
1344
+ type: DocumentEventType;
1345
+ kind?: DocumentChangeKind;
1346
+ version?: number;
1347
+ source?: string;
1348
+ nodeId?: NodeId;
1349
+ nodeMatch?: NodeMatch | null;
1350
+ change?: TextChange;
1351
+ operations?: Operation[];
1352
+ /**
1353
+ * Where the change came from: `'local'` for user editing, anything else for
1354
+ * programmatic/synced sources. Lets a collaboration layer ignore the echo
1355
+ * of changes it applied itself. Absent on events with no single cause.
1356
+ */
1357
+ origin?: string;
1358
+ timestamp: number;
1359
+ [key: string]: unknown;
1360
+ }
1361
+ type DocumentEventHandler = (event: DocumentEvent) => void;
1362
+ declare class DocumentEventBus {
1363
+ private listeners;
1364
+ private history;
1365
+ private maxHistory;
1366
+ /**
1367
+ * Record emitted events so `getHistory()` returns them. Off by default:
1368
+ * nothing reads the history today, and a retained event pins its `source`
1369
+ * string — and, through the lazy `nodeMatch` accessor, the entire previous
1370
+ * red/green tree — so an always-on history held megabytes per document
1371
+ * for nobody. Turn it on for debugging or replay tooling.
1372
+ */
1373
+ recordHistory: boolean;
1374
+ /**
1375
+ * Subscribe to an event type.
1376
+ */
1377
+ on(type: DocumentEventType, handler: DocumentEventHandler): () => void;
1378
+ /**
1379
+ * Whether any handler is subscribed to `type` (or to anything, if omitted).
1380
+ *
1381
+ * Emitters use this to skip building event payloads nobody will see — the
1382
+ * common case, since a headless model has no subscribers at all.
1383
+ */
1384
+ hasListeners(type?: DocumentEventType): boolean;
1385
+ /**
1386
+ * Subscribe to all events.
1387
+ */
1388
+ onAny(handler: DocumentEventHandler): () => void;
1389
+ /**
1390
+ * Emit an event.
1391
+ */
1392
+ emit(event: DocumentEvent): void;
1393
+ /**
1394
+ * Remove a specific handler.
1395
+ */
1396
+ off(type: DocumentEventType, handler: DocumentEventHandler): void;
1397
+ /**
1398
+ * Get event history.
1399
+ */
1400
+ getHistory(): DocumentEvent[];
1401
+ /**
1402
+ * Clear all listeners and history.
1403
+ */
1404
+ clear(): void;
1405
+ }
1406
+
1407
+ /**
1408
+ * DocumentEngine — RenderTree
1409
+ *
1410
+ * An intermediate representation between DocumentModel and final output.
1411
+ * The RenderTree is what the RenderPipeline processes.
1412
+ *
1413
+ * This abstraction allows:
1414
+ * - Same DocumentModel → multiple render outputs (HTML, Canvas, SVG, PDF)
1415
+ * - Plugin renderers that intercept/modify render nodes
1416
+ * - Smooth visual transitions between document states
1417
+ * - Partial rendering for performance
1418
+ */
1419
+
1420
+ type RenderVariant = 'html' | 'canvas' | 'svg' | 'pdf' | 'react' | 'text';
1421
+ interface RenderNode {
1422
+ /** Semantic kind */
1423
+ kind: NodeKind | string;
1424
+ /** Text content */
1425
+ text: string;
1426
+ /** Child render nodes */
1427
+ children: RenderNode[];
1428
+ /** Props/attributes for the renderer */
1429
+ props: Record<string, unknown>;
1430
+ /** Which render variant this node targets */
1431
+ variant?: RenderVariant;
1432
+ /** CSS class names */
1433
+ className?: string;
1434
+ /** Inline styles */
1435
+ style?: Record<string, string | number>;
1436
+ /** Events/handlers (for interactive renders) */
1437
+ events?: Record<string, string>;
1438
+ /** Metadata for renderers */
1439
+ metadata?: Record<string, unknown>;
1440
+ }
1441
+ declare class RenderTree {
1442
+ /**
1443
+ * Create a render tree from a document node.
1444
+ */
1445
+ static fromNode(kind: NodeKind | string, text?: string, children?: RenderNode[], props?: Record<string, unknown>): RenderNode;
1446
+ /**
1447
+ * Create a text render node.
1448
+ */
1449
+ static text(content: string): RenderNode;
1450
+ /**
1451
+ * Create a container render node.
1452
+ */
1453
+ static container(kind: NodeKind | string, children: RenderNode[], props?: Record<string, unknown>): RenderNode;
1454
+ /**
1455
+ * Serialize a render tree to HTML.
1456
+ */
1457
+ static toHTML(node: RenderNode): string;
1458
+ private static kindToTag;
1459
+ private static propsToHtml;
1460
+ private static escapeHtml;
1461
+ }
1462
+
1463
+ /**
1464
+ * DocumentEngine — TagRegistry
1465
+ *
1466
+ * The central registry for all known tags/node kinds.
1467
+ * This is the plugin extension point for adding new BBCode tags,
1468
+ * custom node types, or even entire new languages.
1469
+ *
1470
+ * Each tag definition includes:
1471
+ * - Parser handler (how to parse this tag from tokens)
1472
+ * - Renderer (how to render this tag to HTML, Markdown, etc.)
1473
+ * - Validator (semantic validation rules)
1474
+ * - Toolbar definition (for the visual editor)
1475
+ * - Property editor (for the properties panel)
1476
+ *
1477
+ * Inspired by VSCode's contribution points.
1478
+ */
1479
+
1480
+ interface TagHandlerContext {
1481
+ node: RedNode;
1482
+ source: string;
1483
+ visitChildren: (node: RedNode) => string;
1484
+ renderChild: (node: RedNode) => RenderNode;
1485
+ }
1486
+ interface TagDefinition {
1487
+ /** The tag name (e.g. 'color', 'b', 'img') */
1488
+ name: string;
1489
+ /** Semantic kind */
1490
+ kind: NodeKind;
1491
+ /** Human-readable label */
1492
+ label: string;
1493
+ /** Category for grouping in UI */
1494
+ category?: 'formatting' | 'layout' | 'media' | 'special' | 'text';
1495
+ /** Icon name for UI */
1496
+ icon?: string;
1497
+ /** Whether this tag is inline (span) or block (div) */
1498
+ isInline: boolean;
1499
+ /** Whether this tag is self-closing */
1500
+ isSelfClosing: boolean;
1501
+ /** Whether this tag can contain children */
1502
+ canHaveChildren: boolean;
1503
+ /** Whether this tag is deprecated */
1504
+ isDeprecated?: boolean;
1505
+ /** Replacement tag name if deprecated */
1506
+ deprecatedReplacement?: string;
1507
+ /** Convert a RedNode back to BBCode text */
1508
+ toBBCode?: (ctx: TagHandlerContext) => string;
1509
+ /** Convert a RedNode to a RenderNode for the render pipeline */
1510
+ toRenderNode?: (ctx: TagHandlerContext) => RenderNode;
1511
+ /** Convert a RedNode to HTML string (legacy) */
1512
+ toHTML?: (ctx: TagHandlerContext) => string;
1513
+ /** Default attributes for new instances */
1514
+ defaultAttributes?: () => NodeAttributes;
1515
+ /** Additional validator for this specific tag */
1516
+ validator?: Validator;
1517
+ /** Toolbar button definition */
1518
+ toolbar?: {
1519
+ group: string;
1520
+ label: string;
1521
+ icon?: string;
1522
+ shortcut?: string;
1523
+ };
1524
+ /** Properties panel definition */
1525
+ properties?: PropertyDefinition[];
1526
+ }
1527
+ interface PropertyDefinition {
1528
+ name: string;
1529
+ label: string;
1530
+ type: 'text' | 'color' | 'number' | 'select' | 'boolean' | 'slider';
1531
+ defaultValue?: string | number | boolean;
1532
+ options?: {
1533
+ label: string;
1534
+ value: string;
1535
+ }[];
1536
+ min?: number;
1537
+ max?: number;
1538
+ step?: number;
1539
+ placeholder?: string;
1540
+ description?: string;
1541
+ }
1542
+ type TagHandler = {
1543
+ [K in keyof TagDefinition]: TagDefinition[K];
1544
+ };
1545
+ declare class TagRegistry {
1546
+ private tags;
1547
+ private kinds;
1548
+ /** Names registered by the constructor — the language's own tags. */
1549
+ private builtins;
1550
+ /**
1551
+ * Bumped on every register/unregister, so consumers that derive something
1552
+ * from the registry (the parser's `extraTags` map) can memoize against it
1553
+ * instead of rebuilding per parse.
1554
+ */
1555
+ version: number;
1556
+ constructor();
1557
+ /**
1558
+ * Register a tag definition.
1559
+ */
1560
+ register(tag: TagDefinition): void;
1561
+ /**
1562
+ * Unregister a tag definition.
1563
+ */
1564
+ unregister(name: string): void;
1565
+ /**
1566
+ * Whether `name` is one of the language's own tags, as opposed to a
1567
+ * plugin registration. The distinction matters: plugin tags must reach the
1568
+ * parser (via `ParseOptions.extraTags`) and serialize with their own name,
1569
+ * while builtins already have both behaviors hardcoded — routing them
1570
+ * through the plugin path would change engine semantics.
1571
+ */
1572
+ isBuiltin(name: string): boolean;
1573
+ /**
1574
+ * tag name → kind for every non-builtin registration: the parser's
1575
+ * `extraTags`. Returns `null` when there are none, so the common case
1576
+ * costs one size comparison and no allocation.
1577
+ */
1578
+ customTags(): Map<string, NodeKind> | null;
1579
+ /**
1580
+ * Get a tag definition by name.
1581
+ */
1582
+ get(name: string): TagDefinition | undefined;
1583
+ /**
1584
+ * Get a tag definition by semantic kind.
1585
+ */
1586
+ getByKind(kind: NodeKind): TagDefinition | undefined;
1587
+ /**
1588
+ * Check if a tag is registered.
1589
+ */
1590
+ has(name: string): boolean;
1591
+ /**
1592
+ * Get all registered tags.
1593
+ */
1594
+ getAll(): TagDefinition[];
1595
+ /**
1596
+ * Get tags by category.
1597
+ */
1598
+ getByCategory(category: string): TagDefinition[];
1599
+ /**
1600
+ * Get tags by whether they are inline.
1601
+ */
1602
+ getInline(): TagDefinition[];
1603
+ /**
1604
+ * Get block-level tags.
1605
+ */
1606
+ getBlock(): TagDefinition[];
1607
+ /**
1608
+ * Register the built-in BBCode tags.
1609
+ */
1610
+ private registerBuiltins;
1611
+ }
1612
+
1613
+ /**
1614
+ * DocumentEngine — NodeFactory
1615
+ *
1616
+ * Creates DocumentNode instances, RedNode instances, and GreenNode
1617
+ * instances from tag definitions. Centralizes node creation so
1618
+ * plugins can provide custom node factories.
1619
+ */
1620
+
1621
+ declare class NodeFactory {
1622
+ private registry;
1623
+ constructor(registry: TagRegistry);
1624
+ /**
1625
+ * Create a RedNode from a tag definition.
1626
+ */
1627
+ createFromTag(tagName: string, attrs?: string, start?: number, end?: number, children?: RedNode[]): RedNode;
1628
+ /**
1629
+ * Create a text RedNode.
1630
+ */
1631
+ createText(text: string, start?: number, end?: number): RedNode;
1632
+ /**
1633
+ * Create a self-closing tag RedNode (like [*], [img]src[/img]).
1634
+ */
1635
+ createSelfClosing(tagName: string, attrs?: string, start?: number, end?: number): RedNode;
1636
+ /**
1637
+ * Create a document root RedNode.
1638
+ */
1639
+ createDocument(children?: RedNode[]): RedNode;
1640
+ }
1641
+
1642
+ /**
1643
+ * DocumentEngine — DocumentModel
1644
+ *
1645
+ * THE CORE of the Language Platform.
1646
+ *
1647
+ * The DocumentModel is the single source of truth for the document.
1648
+ * BBCode is just ONE representation of this model.
1649
+ * The model is language-agnostic and format-agnostic.
1650
+ *
1651
+ * Responsibilities:
1652
+ * - Hold the current Red Tree
1653
+ * - Manage incremental parsing
1654
+ * - Coordinate transactions (undo/redo)
1655
+ * - Emit change events
1656
+ * - Provide query/find capabilities
1657
+ * - Manage diagnostics
1658
+ *
1659
+ * Inspired by ProseMirror's EditorState and Roslyn's Document.
1660
+ */
1661
+
1662
+ interface DocumentModelOptions {
1663
+ /** Initial source text */
1664
+ source?: string;
1665
+ /** Language identifier */
1666
+ language?: string;
1667
+ /** Maximum undo stack size */
1668
+ maxUndo?: number;
1669
+ /** Whether to auto-analyze on change */
1670
+ autoAnalyze?: boolean;
1671
+ /**
1672
+ * Attempt incremental reparse on `applyChange`. Default `true`.
1673
+ *
1674
+ * Setting this to `false` makes every change a full rebuild. The result is
1675
+ * identical either way — that is the incremental parser's contract, checked
1676
+ * differentially over 22.100 edits — so this is a performance switch and a
1677
+ * debugging aid, not a correctness one.
1678
+ */
1679
+ incremental?: boolean;
1680
+ /**
1681
+ * Reuse red subtrees across incremental reparses. Default `true`.
1682
+ *
1683
+ * When the splice shares a green subtree by reference, the old red subtree
1684
+ * is adopted into the new tree instead of being rebuilt — building red was
1685
+ * the largest phase of a keystroke. The trade is a contract: the previous
1686
+ * `redRoot` is consumed by the adoption and must not be walked afterwards.
1687
+ * No engine or app code does; this switch exists for any future consumer
1688
+ * that needs the superseded tree to stay intact.
1689
+ */
1690
+ reuseRed?: boolean;
1691
+ }
1692
+ declare class DocumentModel {
1693
+ private _source;
1694
+ private _language;
1695
+ private _redRoot;
1696
+ private _greenRoot;
1697
+ private _version;
1698
+ readonly tagRegistry: TagRegistry;
1699
+ readonly nodeFactory: NodeFactory;
1700
+ readonly treeBuilder: TreeBuilder;
1701
+ readonly nodeMatcher: NodeMatcher;
1702
+ readonly semanticAnalyzer: SemanticAnalyzer;
1703
+ readonly incrementalParser: IncrementalParser;
1704
+ readonly changeTracker: ChangeTracker;
1705
+ readonly undoManager: UndoManager;
1706
+ readonly queryEngine: QueryEngine;
1707
+ readonly events: DocumentEventBus;
1708
+ private _diagnostics;
1709
+ private _lastAnalyzeResult;
1710
+ /**
1711
+ * Source range of the last applied edit — see {@link TextChangeRange}.
1712
+ * `null` after a full rebuild, which is not an edit.
1713
+ */
1714
+ private _lastChangeRange;
1715
+ private _options;
1716
+ private _analyzeTimeout;
1717
+ /** The debounced post-edit work, kept so `ensureAnalyzed` can run it early. */
1718
+ private _pendingAnalyze;
1719
+ /** Last reparse timings breakdown (exposed for debugging) */
1720
+ lastReparsePath: string;
1721
+ /** Why the last reparse fell back to a full rebuild, if it did. */
1722
+ lastReparseFallbackReason: FallbackReason | null;
1723
+ lastReparseTimings: {
1724
+ findAffected: number;
1725
+ safeBoundary: number;
1726
+ parse: number;
1727
+ buildRed: number;
1728
+ mutate: number;
1729
+ other: number;
1730
+ } | null;
1731
+ constructor(options?: DocumentModelOptions);
1732
+ get source(): string;
1733
+ get language(): string;
1734
+ get version(): number;
1735
+ get redRoot(): RedNode | null;
1736
+ get greenRoot(): GreenNode | null;
1737
+ get diagnostics(): DiagnosticCollection | null;
1738
+ get lastAnalyze(): AnalyzeResult | null;
1739
+ /**
1740
+ * The source range of the last applied edit, in new-source coordinates.
1741
+ *
1742
+ * `null` after a `rebuild` (there is no incremental edit to point at). The
1743
+ * BlockPatcher reads this to reconcile only the blocks around the edit
1744
+ * instead of walking the whole document. The same range is also attached to
1745
+ * the current `redRoot` (`__changeRange`), so a consumer that only holds the
1746
+ * AST — like the live preview — can find it without plumbing the model.
1747
+ */
1748
+ get lastChangeRange(): TextChangeRange | null;
1749
+ /**
1750
+ * Full rebuild from source text.
1751
+ * Used for initial load or when incremental parsing isn't possible.
1752
+ */
1753
+ rebuild(source: string): void;
1754
+ /**
1755
+ * Install `nodeMatch` on an event as an accessor, so the match is only
1756
+ * computed if a subscriber actually reads it.
1757
+ *
1758
+ * Matching two 1736-node trees costs ~3.5 ms and allocates a result entry per
1759
+ * node plus five Maps/Sets — and it ran on *every* keystroke to populate a
1760
+ * field that no subscriber in this codebase reads. The capability is real
1761
+ * (preserving selection and scroll across re-parses is what it is for), so it
1762
+ * stays available; it just stops running speculatively.
1763
+ *
1764
+ * Callers still write `event.nodeMatch` and see a `NodeMatch | null`. Note the
1765
+ * closure keeps `oldRoot` alive for as long as the event object is retained.
1766
+ */
1767
+ private defineLazyNodeMatch;
1768
+ /**
1769
+ * Attach the last change range to a root node.
1770
+ *
1771
+ * The range travels with the AST so the preview can read it from the root it
1772
+ * is about to patch — no prop drilling through workspace → window → preview.
1773
+ * `__changeRange` is deliberately not part of RedNode's API; it is a runtime
1774
+ * marker owned by the model (the BlockPatcher reads it with a cast).
1775
+ */
1776
+ private _attachChangeRange;
1777
+ /**
1778
+ * Apply a source text change (e.g. from Monaco editor input).
1779
+ * Uses incremental parsing when possible.
1780
+ *
1781
+ * `origin` tags where the change came from — `'local'` (default) for the
1782
+ * user's own editing, anything else for programmatic or synced sources
1783
+ * (`'remote'`, `'sync'`, a peer id…). It travels on the emitted events, so
1784
+ * a collaboration layer can tell its own echo apart from user edits — the
1785
+ * classic infinite-loop bug when wiring a CRDT. See `QuasarCollab.MD`.
1786
+ */
1787
+ applyChange(change: TextChange, origin?: string): void;
1788
+ /**
1789
+ * Flush the debounced post-edit analysis, so `diagnostics` describes the
1790
+ * *current* tree.
1791
+ *
1792
+ * `applyChange` defers analysis by a few milliseconds to keep the keystroke
1793
+ * path lean. A caller that reads `diagnostics` synchronously right after an
1794
+ * edit would otherwise be looking at the previous document's errors — which
1795
+ * is exactly what the editor's error panel did.
1796
+ */
1797
+ ensureAnalyzed(): void;
1798
+ /**
1799
+ * Calculate a simple diff between the current source and the new source,
1800
+ * then apply the change.
1801
+ */
1802
+ applyTextUpdate(newSource: string, origin?: string): void;
1803
+ /**
1804
+ * Execute operations within a transaction (undoable).
1805
+ *
1806
+ * Coherence contract: after a `transact` the model's `source`, green tree
1807
+ * and red tree all describe the same document. The transaction mutates the
1808
+ * red tree, the result is serialized back to text via `exportSource`, and
1809
+ * the model rebuilds from that text — parse stays the single source of
1810
+ * truth, exactly as in `applyChange`. (The previous implementation swapped
1811
+ * `_redRoot` and left `_source`/`_greenRoot` describing the old document,
1812
+ * so the next text edit diffed against a source the tree no longer matched.)
1813
+ *
1814
+ * Returns `true` if the document changed.
1815
+ */
1816
+ transact(operations: Operation[], label?: string): boolean;
1817
+ /**
1818
+ * Undo the last transaction.
1819
+ *
1820
+ * Undo/redo replay *source snapshots*, not stored tree mutations: a rebuild
1821
+ * replaces every node id, so a retained `Transaction` could never be
1822
+ * re-applied against the current tree — and its `invert()` was only defined
1823
+ * for one of the six operation kinds anyway. Text is always replayable.
1824
+ */
1825
+ undo(): boolean;
1826
+ /**
1827
+ * Redo the last undone transaction. See `undo` for why this replays text.
1828
+ */
1829
+ redo(): boolean;
1830
+ /**
1831
+ * Run semantic analysis on the current tree.
1832
+ */
1833
+ analyze(): AnalyzeResult;
1834
+ /**
1835
+ * Find a node by its ID.
1836
+ */
1837
+ findNode(id: NodeId): RedNode | null;
1838
+ /**
1839
+ * Query the document tree.
1840
+ */
1841
+ query(q: Query): QueryResult;
1842
+ /**
1843
+ * Register a semantic validator.
1844
+ */
1845
+ registerValidator(validator: Validator): void;
1846
+ /**
1847
+ * Get the current document as a DocumentNode tree.
1848
+ */
1849
+ toDocumentNode(): DocumentNode | null;
1850
+ /**
1851
+ * Get the current source text.
1852
+ */
1853
+ toString(): string;
1854
+ /**
1855
+ * Parse source text to a GreenNode tree.
1856
+ * Override this for language-specific parsing.
1857
+ *
1858
+ * `options.normalizeParagraphs === false` means the text is the inner span
1859
+ * of a container rather than a whole document, and root-level grouping must
1860
+ * be skipped. Languages without such grouping can ignore it.
1861
+ */
1862
+ protected parseToGreen(source: string, options?: ReparseParseOptions): GreenNode;
1863
+ /**
1864
+ * Build a RedNode tree from a GreenNode.
1865
+ */
1866
+ protected buildRedFromGreen(green: GreenNode): RedNode;
1867
+ /**
1868
+ * Build a red tree for `green`, reusing subtrees of `oldRed` where the green
1869
+ * is shared by reference. The base model has no reuse-aware builder, so it
1870
+ * falls back to a full build; language models override this
1871
+ * (BBCodeDocumentModel uses `greenToRedNodeReusing`).
1872
+ */
1873
+ protected buildRedFromGreenReusing(green: GreenNode, oldRed: RedNode, stats?: {
1874
+ adopted: number;
1875
+ }): RedNode;
1876
+ /**
1877
+ * Serialize a red tree back to source text. `transact`/`undo`/`redo` rebuild
1878
+ * from this text to keep source, green and red coherent.
1879
+ *
1880
+ * The base model has no syntax to serialize to, so this throws; language
1881
+ * models override it (BBCodeDocumentModel uses the BBCodeExporter).
1882
+ * Failing loudly here beats the alternative: silently desynchronizing the
1883
+ * model's three representations.
1884
+ */
1885
+ protected exportSource(root: RedNode): string;
1886
+ }
1887
+
1888
+ /**
1889
+ * DocumentEngine — Lexer
1890
+ *
1891
+ * Tokenizes source text into a stream of Tokens with Trivia.
1892
+ * Trivia includes whitespace, newlines, and formatting that is
1893
+ * preserved in the token stream but not semantically significant.
1894
+ *
1895
+ * This is a LANGUAGE-INDEPENDENT lexer framework.
1896
+ * Language-specific tokenization is provided via the LexerOptions.
1897
+ *
1898
+ * Inspired by Roslyn's lexer architecture.
1899
+ */
1900
+
1901
+ interface LexerRule {
1902
+ /** Pattern to match at the current position */
1903
+ pattern: RegExp;
1904
+ /** Token kind to produce */
1905
+ kind: TokenKind;
1906
+ /** Optional: extract a name from the match (for tags) */
1907
+ name?: (match: RegExpExecArray) => string;
1908
+ /** Optional: extract attributes from the match */
1909
+ attrs?: (match: RegExpExecArray) => string;
1910
+ }
1911
+ interface LexerOptions {
1912
+ /** Language-specific tokenization rules */
1913
+ rules: LexerRule[];
1914
+ /** Characters that count as whitespace trivia */
1915
+ whitespaceChars?: RegExp;
1916
+ /** Characters that start a newline trivia */
1917
+ newlineChars?: RegExp;
1918
+ /** Whether to collect trivia */
1919
+ collectTrivia?: boolean;
1920
+ /** Lexer hooks: called before each token */
1921
+ onToken?: (token: Token) => void;
1922
+ }
1923
+ declare class Lexer {
1924
+ private options;
1925
+ private source;
1926
+ private pos;
1927
+ private tokens;
1928
+ constructor(options: LexerOptions);
1929
+ /**
1930
+ * Tokenize source text into a TokenStream.
1931
+ */
1932
+ tokenize(source: string): TokenStream;
1933
+ /**
1934
+ * Re-tokenize a portion of the source (for incremental parsing).
1935
+ * Returns just the new tokens for the affected range.
1936
+ */
1937
+ retokenize(source: string, start: number, end: number): Token[];
1938
+ private tryRules;
1939
+ private scanTrivia;
1940
+ }
1941
+
1942
+ /**
1943
+ * DocumentEngine — RedNodeStore
1944
+ *
1945
+ * Canonical store for RedNodes. Ensures that structurally identical subtrees
1946
+ * share the EXACT SAME RedNode instance in RAM.
1947
+ *
1948
+ * This is the RedNode-level complement to GreenNodePool (which handles
1949
+ * structural sharing at the GreenNode level).
1950
+ *
1951
+ * Architecture:
1952
+ * - Keyed by GreenNode._hash (deterministic structural hash)
1953
+ * - Bottom-up: children are stored first, then parents reference them
1954
+ * - Immutable structural data: once a RedNode is in the store, its `children`
1955
+ * array and `green` reference never change
1956
+ * - Mutable metadata: the store's RedNode has canonical metadata (derived from
1957
+ * green.text). Position-specific metadata lives in PositionRef.localOverlay.
1958
+ *
1959
+ * Benefits:
1960
+ * - Memory deduplication: 100 identical blocks = 1 RedNode + 99 lightweight refs
1961
+ * - Render caching: BBCodeCanvas can memoize by canonicalId
1962
+ * - Faster incremental parse: unchanged subtrees keep their RedNode identity
1963
+ * - HTMLRenderer cache: same canonicalId = same HTML (cache hits)
1964
+ */
1965
+
1966
+ /**
1967
+ * Derives a node's canonical metadata from its GreenNode.
1968
+ *
1969
+ * Injected rather than hardcoded: metadata extraction is *language* semantics
1970
+ * (what `=#ff0000,#00ff00` means for a `gradient`), and this module sits in the
1971
+ * language-agnostic `Syntax` layer. The BBCode implementation lives in
1972
+ * `BBCode/BBCodeToGreenNode.ts`; pass it via the constructor.
1973
+ */
1974
+ type MetadataExtractor = (green: GreenNode) => NodeMetadata;
1975
+ declare class RedNodeStore {
1976
+ private canonicals;
1977
+ private _hits;
1978
+ private _misses;
1979
+ private _collisions;
1980
+ private readonly extractMetadata;
1981
+ /**
1982
+ * @param extractMetadata How to derive canonical metadata from a green node.
1983
+ * Defaults to producing none — the store stays language-agnostic unless a
1984
+ * caller supplies the mapping.
1985
+ */
1986
+ constructor(extractMetadata?: MetadataExtractor);
1987
+ /**
1988
+ * Get or create a canonical RedNode for the given GreenNode.
1989
+ *
1990
+ * The store is keyed by `green._hash` — a 32-bit FNV structural hash — so a
1991
+ * matching key is necessary but NOT sufficient proof of identity. The
1992
+ * canonical pipeline (GreenNodePool interning) guarantees that structurally
1993
+ * identical greens ARE the same object, which is what makes reference
1994
+ * equality a sound discriminator:
1995
+ *
1996
+ * - same hash + same green reference → HIT, reuse the canonical
1997
+ * - same hash + different green → COLLISION: a distinct subtree shares
1998
+ * the 32-bit hash. It is never reused as the registered canonical and never
1999
+ * overwrites it (that would corrupt every future hit for the registered
2000
+ * node). The colliding subtree simply loses canonicalization — rebuilt on
2001
+ * each call. Guarding correctness costs a rare dedup miss; the alternative
2002
+ * costs a wrong AST.
2003
+ * - no hash entry → MISS, create and register
2004
+ *
2005
+ * Invariant preserved: a hash collision can never make one structure be
2006
+ * represented by another structure's RedNode.
2007
+ *
2008
+ * @param green The GreenNode to wrap
2009
+ * @param metadata Optional metadata to set (overrides intrinsic metadata)
2010
+ * @param kind Optional kind override
2011
+ * @returns The canonical RedNode (shared if already exists)
2012
+ */
2013
+ getOrCreate(green: GreenNode, metadata?: NodeMetadata, kind?: NodeKind): RedNode;
2014
+ /**
2015
+ * Build a canonical RedNode for `green` WITHOUT registering it.
2016
+ *
2017
+ * Shared by the miss path (which registers the result) and the collision
2018
+ * path (which must not). Both need the same bottom-up construction:
2019
+ * children are canonicalized first, then the parent references them.
2020
+ */
2021
+ private buildCanonical;
2022
+ /**
2023
+ * Look up a canonical RedNode by its structural hash.
2024
+ */
2025
+ getByHash(hash: string): RedNode | undefined;
2026
+ /**
2027
+ * Check if a GreenNode already has a canonical RedNode.
2028
+ */
2029
+ has(green: GreenNode): boolean;
2030
+ /**
2031
+ * Get the number of unique canonical nodes.
2032
+ */
2033
+ get size(): number;
2034
+ /**
2035
+ * Get store statistics.
2036
+ */
2037
+ get stats(): {
2038
+ size: number;
2039
+ hits: number;
2040
+ misses: number;
2041
+ collisions: number;
2042
+ deduplicationRatio: string;
2043
+ };
2044
+ /**
2045
+ * Clear all canonical nodes.
2046
+ */
2047
+ clear(): void;
2048
+ }
2049
+
2050
+ /**
2051
+ * DocumentEngine — TreeDiffer
2052
+ *
2053
+ * Computes structural differences between two Red Trees.
2054
+ * Produces a list of DiffOperations that can be used for:
2055
+ * - Updating the UI with minimal DOM changes
2056
+ * - Syncing with collaboration servers
2057
+ * - Animation interpolation
2058
+ * - Undo/redo granularity
2059
+ *
2060
+ * Inspired by React's reconciliation and ProseMirror's diff.
2061
+ */
2062
+
2063
+ type DiffKind = 'insert' | 'delete' | 'update' | 'move' | 'preserve';
2064
+ interface DiffOperation {
2065
+ kind: DiffKind;
2066
+ node: RedNode;
2067
+ oldNode?: RedNode;
2068
+ /** Index in parent */
2069
+ oldIndex?: number;
2070
+ newIndex?: number;
2071
+ /** Parent node ID */
2072
+ parentId?: string;
2073
+ }
2074
+ interface DiffResult {
2075
+ operations: DiffOperation[];
2076
+ /** Count by kind */
2077
+ stats: {
2078
+ inserts: number;
2079
+ deletes: number;
2080
+ updates: number;
2081
+ moves: number;
2082
+ preserves: number;
2083
+ };
2084
+ /** Time taken in ms */
2085
+ duration: number;
2086
+ }
2087
+ declare class TreeDiffer {
2088
+ /**
2089
+ * Compute the diff between two Red Trees.
2090
+ */
2091
+ diff(oldRoot: RedNode, newRoot: RedNode): DiffResult;
2092
+ private diffChildren;
2093
+ /**
2094
+ * Check if a node's content has changed.
2095
+ */
2096
+ private nodeContentChanged;
2097
+ /**
2098
+ * Deep equality check for content equivalence when pairing unmatched nodes.
2099
+ */
2100
+ private nodesAreEquivalent;
2101
+ }
2102
+
2103
+ /**
2104
+ * DocumentEngine — Visitor
2105
+ *
2106
+ * Base visitor pattern for traversing the Red Tree.
2107
+ * All exporters and renderers extend this.
2108
+ *
2109
+ * Instead of switch statements on tag names, use the visitor pattern.
2110
+ * New languages/exporters don't modify existing code.
2111
+ *
2112
+ * Inspired by SwiftSyntax's Visitor pattern.
2113
+ */
2114
+
2115
+ interface VisitorContext {
2116
+ source: string;
2117
+ options: Record<string, unknown>;
2118
+ }
2119
+ declare abstract class Visitor<T = string> {
2120
+ protected context: VisitorContext;
2121
+ /**
2122
+ * Visit a node and produce an output value.
2123
+ */
2124
+ abstract visit(node: RedNode, context?: VisitorContext): T;
2125
+ /**
2126
+ * Visit all children and combine results.
2127
+ */
2128
+ visitChildren(node: RedNode, separator?: string): T[];
2129
+ /**
2130
+ * Get the text content of a node.
2131
+ */
2132
+ getText(node: RedNode): string;
2133
+ /**
2134
+ * Set the visitor context.
2135
+ */
2136
+ setContext(context: VisitorContext): void;
2137
+ }
2138
+
2139
+ /**
2140
+ * DocumentEngine — BBCodeExporter
2141
+ *
2142
+ * Exports the Document Model / Red Tree back to BBCode text.
2143
+ * This is how the internal representation becomes editable text.
2144
+ *
2145
+ * Uses the TagRegistry to determine how each node serializes.
2146
+ * Plugins can register custom serializers for custom tags.
2147
+ */
2148
+
2149
+ /**
2150
+ * Export target for BBCodeExporter.
2151
+ * - 'osu': Expands Miliastry/Lyne-native tags into osu!-compatible BBCode.
2152
+ * - 'miliastry': Preserves Miliastry-native tags as-is for round-trip editing.
2153
+ * - 'lyne': Preserves Lyne-native tags as-is for round-trip editing.
2154
+ */
2155
+ type ExportTarget$1 = 'osu' | 'miliastry' | 'lyne';
2156
+ /**
2157
+ * Tags que existen SOLO en Miliastry (registrados en el TagRegistry) y que
2158
+ * osu! NO renderiza. Si un usuario pega `[shadow]`/`[zalgo]`/etc. en osu!,
2159
+ * el texto sale roto (los corchetes se ven literalmente). Cuando el target
2160
+ * es 'osu', estos tags se DEGRADAN a su contenido plano: mejor perder el
2161
+ * efecto que romper la userpage. El target 'miliastry' los conserva.
2162
+ */
2163
+ declare const MILIASTRY_ONLY_TAGS: Set<string>;
2164
+ declare const LYNE_ONLY_TAGS: Set<string>;
2165
+ declare class BBCodeExporter extends Visitor<string> {
2166
+ private registry;
2167
+ private depth;
2168
+ private target;
2169
+ constructor(registry?: TagRegistry, target?: ExportTarget$1);
2170
+ /**
2171
+ * Set the export target. Controls how Miliastry-native tags are serialized.
2172
+ */
2173
+ setTarget(target: ExportTarget$1): void;
2174
+ /**
2175
+ * Export a RedNode tree to BBCode.
2176
+ */
2177
+ visit(node: RedNode, context?: VisitorContext): string;
2178
+ /**
2179
+ * Export the entire document to BBCode.
2180
+ * Optionally override the export target for this specific call.
2181
+ */
2182
+ export(root: RedNode, target?: ExportTarget$1): string;
2183
+ private exportNode;
2184
+ private kindToTagName;
2185
+ private getTagAttributes;
2186
+ }
2187
+
2188
+ interface UIBBBlock {
2189
+ id: string;
2190
+ type: string;
2191
+ attributes?: Record<string, string>;
2192
+ children: UIBBBlock[];
2193
+ content?: string;
2194
+ }
2195
+ declare class BBBlocksExporter extends Visitor<UIBBBlock[]> {
2196
+ private idCounter;
2197
+ private generateId;
2198
+ visit(node: RedNode, context?: VisitorContext): UIBBBlock[];
2199
+ export(root: RedNode): UIBBBlock[];
2200
+ private exportNode;
2201
+ }
2202
+
2203
+ /**
2204
+ * DocumentEngine — BBCodeToGreenNode
2205
+ *
2206
+ * Bridge between the existing BBCode Parser (BBBlock[]) and the
2207
+ * DocumentEngine's GreenNode/RedNode syntax tree.
2208
+ *
2209
+ * This is how we integrate the mature, battle-tested BBCode parser
2210
+ * with the new Language Platform architecture.
2211
+ *
2212
+ * The flow:
2213
+ * BBCode text
2214
+ * → parseBBCode() [existing] → BBBlock[]
2215
+ * → convertToGreenNode() [this] → GreenNode
2216
+ * → buildRedNode() [this] → RedNode
2217
+ * → DocumentModel
2218
+ */
2219
+
2220
+ type BBCodeDialect = 'osu' | 'miliastry' | 'lyne';
2221
+ declare const BBCODE_TAG_NAMES: readonly string[];
2222
+ declare function getBBCodeTagNames(dialect?: BBCodeDialect): readonly string[];
2223
+ declare function tagToNodeKind(tag: string | null, dialect?: BBCodeDialect): NodeKind;
2224
+ declare function nodeKindToTag(kind: NodeKind): string | null;
2225
+ declare function isBlockKind(kind: NodeKind): boolean;
2226
+ interface BBBlock {
2227
+ id: string;
2228
+ tag: string | null;
2229
+ attrs: string;
2230
+ content: string;
2231
+ rawStart: number;
2232
+ rawEnd: number;
2233
+ children: BBBlock[];
2234
+ attrChildren?: BBBlock[];
2235
+ html?: string;
2236
+ }
2237
+ /**
2238
+ * Convert a single BBBlock to a GreenNode.
2239
+ * Recursively converts children.
2240
+ */
2241
+ declare function bbBlockToGreenNode(block: BBBlock): GreenNode;
2242
+ /**
2243
+ * Convert an array of BBBlock[] (the root of the existing parser's output)
2244
+ * to a GreenNode tree.
2245
+ *
2246
+ * Note: This function is kept for backward compatibility with the old parser.
2247
+ * The new DocumentEngine parser (Parser.ts + BBCodeLexer) produces GreenNode
2248
+ * directly and does NOT go through BBBlock[].
2249
+ */
2250
+ declare function bbBlocksToGreenTree(blocks: BBBlock[], source: string): GreenNode;
2251
+ /**
2252
+ * Build a RedNode tree from a GreenNode.
2253
+ * Similar to TreeBuilder.buildRed but uses actual kind from the green node
2254
+ * and extracts BBCode metadata from attributes.
2255
+ *
2256
+ * When a RedNodeStore is provided, instance RedNodes are created with
2257
+ * correct parent references copied from the canonical's metadata.
2258
+ * The canonicalId (from green._hash) enables:
2259
+ * - React.memo in BBCodeCanvas by canonicalId
2260
+ * - HTMLRenderer cache by canonicalId
2261
+ * - Diff optimization (same canonicalId = unchanged subtree)
2262
+ * - Future PositionRef-based sharing
2263
+ */
2264
+ declare function greenToRedNode(green: GreenNode, parent?: RedNode | null, store?: RedNodeStore, start?: number): RedNode;
2265
+ /**
2266
+ * Convert a BBBlock array directly to a RedNode root.
2267
+ * This is the main entry point for the bridge.
2268
+ */
2269
+ declare function bbBlocksToRedTree(blocks: BBBlock[], source: string): RedNode;
2270
+
2271
+ /**
2272
+ * DocumentEngine — HTMLRenderer
2273
+ *
2274
+ * Renders the Document Model to HTML for preview.
2275
+ * This is the main renderer for the visual BBCode preview.
2276
+ *
2277
+ * Uses the TagRegistry for custom rendering.
2278
+ * Plugins can register custom renderers for preview components.
2279
+ */
2280
+
2281
+ interface HTMLRendererOptions {
2282
+ /**
2283
+ * Replicate osu! forum BBCode spacing quirks.
2284
+ * Defaults to true for full compatibility with Miliastry.
2285
+ * Set to false for a more logical, predictable rendering engine.
2286
+ */
2287
+ osuBehaviour?: boolean;
2288
+ /** Registry for resolving custom tags */
2289
+ registry?: TagRegistry;
2290
+ /** BBCode dialect to render for ('osu' | 'miliastry' | 'lyne') */
2291
+ dialect?: BBCodeDialect;
2292
+ /** Visual theme for markup classes ('osu' | 'lyne' | 'miliastry') */
2293
+ theme?: 'osu' | 'lyne' | 'miliastry';
2294
+ /** Safe media proxy callback to rewrite image and media URLs */
2295
+ mediaProxy?: (url: string) => string;
2296
+ /** Resolver for entity links like profile, guild, map */
2297
+ entityLinkResolver?: (kind: string, value: string) => {
2298
+ href: string;
2299
+ external?: boolean;
2300
+ } | null;
2301
+ }
2302
+ declare class HTMLRenderer extends Visitor<string> {
2303
+ private options;
2304
+ constructor(options?: HTMLRendererOptions);
2305
+ private readonly BLOCK_TAGS;
2306
+ private readonly INLINE_TAGS;
2307
+ /**
2308
+ * The ` data-node-id="…"` attribute, or `''` for nodes nobody looks up.
2309
+ *
2310
+ * ─── Why this is not emitted on everything ──────────────────────────────
2311
+ *
2312
+ * It used to be, carrying `node.id` — a process-global counter minted per
2313
+ * RedNode, so a reparse renames every node in the document. Measured on a
2314
+ * one-character edit: zero ids survive.
2315
+ *
2316
+ * `DOMMorpher` already noted that this rules the ids out as morph KEYS. The
2317
+ * larger cost is that it also breaks the morpher's prefix/suffix trim, which
2318
+ * uses `isEqualNode` — a comparison that includes attributes. An attribute
2319
+ * that always differs makes every element compare unequal, so the trim never
2320
+ * fires and the morpher walks the whole document. Measured on one keystroke
2321
+ * in the reference document: **1665 `setAttribute` calls across 1725
2322
+ * elements**, to write new numbers meaning the same thing, when the actual
2323
+ * change was 10 insertions and 9 removals.
2324
+ *
2325
+ * ─── Why BLOCKS, and not stable ids ─────────────────────────────────────
2326
+ *
2327
+ * Making the id stable was the obvious repair and it does not work. Keying on
2328
+ * the node's SPAN was measured: an edit at the END drops the writes to 6, but
2329
+ * an insertion shifts every offset after it, so an edit near the START still
2330
+ * cost 1637 — and spans are longer strings than `nN`, so the emitted HTML grew
2331
+ * 10%. Stability under insertion is not something a position can have.
2332
+ *
2333
+ * The real observation is that no consumer ever wanted these on inline nodes.
2334
+ * Both readers in the app are block-level: `usePreviewClick` walks up with
2335
+ * `closest('[data-node-id]')` for *block selection*, and `useBlockHighlight`
2336
+ * highlights and scrolls to a *block*. Emitting ids on the per-character
2337
+ * spans of a gradient did not just cost — it made `closest()` stop at a
2338
+ * character instead of the block the click meant.
2339
+ *
2340
+ * So the attribute goes where it is read. Inline nodes carry no id, compare
2341
+ * equal, and let `isEqualNode` skip their subtrees natively; the handful of
2342
+ * block containers that do carry one are few enough that their churn is
2343
+ * noise.
2344
+ */
2345
+ /**
2346
+ * Text leaves emit their content directly, with no wrapper element.
2347
+ *
2348
+ * They used to come wrapped in `<span class="bb-text">` — one extra DOM
2349
+ * element per text leaf, which is HALF the preview's elements (measured:
2350
+ * 1726 → 851 on a 19.6 KB post, 17251 → 8510 on a 196 KB one). The class
2351
+ * earned none of it: it has no CSS rule anywhere in the repo, carries no
2352
+ * `data-node-id` (text is not an id-bearing kind, so click mapping and
2353
+ * highlighting never looked at it), and every style a text leaf can have
2354
+ * still emits its own `<span style="…">` below.
2355
+ *
2356
+ * A/B in a production build, steady state: flush 41.4 → 39.7 ms at 19.6 KB
2357
+ * and 55.4 → 46.4 ms at 196 KB, plus ~27% less HTML to serialize and parse.
2358
+ */
2359
+ private textWrap;
2360
+ /**
2361
+ * A qué elementos se les pone `data-node-id`.
2362
+ *
2363
+ * `'all'` (por defecto) — a todos. Es lo que permite que un clic en
2364
+ * CUALQUIER punto del preview señale ese nodo exacto en el editor: en un
2365
+ * degradado cada carácter es su propio nodo, y sin id no hay nada a lo que
2366
+ * `closest()` pueda agarrarse.
2367
+ *
2368
+ * Esto estuvo desactivado por una buena razón que ya no aplica. Los ids se
2369
+ * regeneraban en cada parseo, así que el atributo cambiaba en TODOS los
2370
+ * elementos por pulsación: `isEqualNode` no casaba nunca, el morpher no
2371
+ * podía saltarse ningún subárbol y se medían 1665 `setAttribute` sobre 1725
2372
+ * elementos para escribir números nuevos que significaban lo mismo. Con la
2373
+ * identidad estable entre reparseos eso desapareció: un nodo que no cambia
2374
+ * conserva su id, su HTML es idéntico y el camino rápido del morpher sigue
2375
+ * funcionando.
2376
+ *
2377
+ * El coste que queda es el tamaño: ~10 bytes por elemento, que hay que
2378
+ * serializar y parsear. Medido con A/B en la misma sesión sobre el post con
2379
+ * degradados (19,6 KB, 852 elementos):
2380
+ *
2381
+ * solo bloques (9 con id): latencia 48 ms · p95 72 ms
2382
+ * todos (808 con id): latencia 56 ms · p95 104 ms
2383
+ *
2384
+ * Se paga a propósito: la precisión del clic es una función que se pidió, y
2385
+ * 56 ms sigue holgadamente dentro de lo que se percibe como inmediato.
2386
+ * `'blocks'` queda disponible para quien priorice la latencia, y `'none'`
2387
+ * para los consumidores de solo lectura (foros, render estático): sin
2388
+ * `data-node-id` en absoluto, el HTML es más pequeño y no hay nada que
2389
+ * mantenga vivos los nodos del árbol.
2390
+ */
2391
+ static idMode: 'blocks' | 'all' | 'none';
2392
+ private idAttr;
2393
+ /**
2394
+ * Kinds that carry `data-node-id`.
2395
+ *
2396
+ * The block containers a user can select or be scrolled to, plus the media
2397
+ * nodes, which are atomic and clickable in their own right. Deliberately NOT
2398
+ * `text` or any inline formatting kind — see {@link idAttr}.
2399
+ */
2400
+ private static readonly ID_BEARING_KINDS;
2401
+ visit(node: RedNode): string;
2402
+ render(root: RedNode): string;
2403
+ /**
2404
+ * Render every child and concatenate.
2405
+ *
2406
+ * Replaces the `children.map(c => this.renderNode(c)).join('')` idiom, which
2407
+ * was repeated at 16 call sites and allocated a closure plus an intermediate
2408
+ * array of N strings at every level of the tree. Appending to one string lets
2409
+ * the engine use its rope representation instead.
2410
+ */
2411
+ /**
2412
+ * Render the direct children of `node` to HTML (no wrapper element).
2413
+ *
2414
+ * Public so the incremental preview (`BlockPatcher`) can morph a block
2415
+ * element's inner content without re-rendering the whole document.
2416
+ */
2417
+ renderChildren(node: RedNode): string;
2418
+ private renderNode;
2419
+ /** osu! quirk: newlines immediately preceding a [code] block are completely ignored */
2420
+ private isNextCodeBlock;
2421
+ /** Checks if this is the LAST empty_line right before a code block (skipping only spacing) */
2422
+ private isImmediateEmptyLineBeforeCode;
2423
+ private isPrevBlockBoundary;
2424
+ private isTrailingBlockBoundary;
2425
+ private renderError;
2426
+ private wrapInline;
2427
+ private wrapBlock;
2428
+ /** #rgb / #rgba / #rrggbb / #rrggbbaa, a bare CSS color keyword, or rgb()/hsl(). */
2429
+ private static readonly CSS_COLOR_RE;
2430
+ /** Bare number — interpolated as a percentage. */
2431
+ private static readonly CSS_SIZE_RE;
2432
+ /** Font family list. Quotes are rejected outright; unquoted names are valid CSS. */
2433
+ private static readonly CSS_FONT_RE;
2434
+ /** Characters `escapeHtml` has to rewrite. Non-global on purpose: `test` must not carry `lastIndex`. */
2435
+ private static readonly HTML_ESCAPE_RE;
2436
+ private sanitizeColor;
2437
+ private sanitizeFontSize;
2438
+ private sanitizeFontFamily;
2439
+ /** Keyword-or-number CSS values (font-weight, font-style, text-decoration). */
2440
+ private isCssKeyword;
2441
+ /** Read a metadata field, falling back to the raw tag attribute. */
2442
+ private metaOrAttr;
2443
+ private colorStyle;
2444
+ private fontSizeStyle;
2445
+ private fontStyle;
2446
+ /**
2447
+ * Extract the attribute value from a BBCode tag node.
2448
+ *
2449
+ * BBCode attributes come in the format `=VALUE` (e.g. `=#61afef`, `="Author"`,
2450
+ * `=https://osu.ppy.sh`). This strips the leading `=` and any surrounding quotes.
2451
+ *
2452
+ * For tags without attrs (like text nodes, img content), returns the node text as-is.
2453
+ */
2454
+ private extractValue;
2455
+ private renderLink;
2456
+ private renderProfile;
2457
+ private renderEntity;
2458
+ private parseImgAttr;
2459
+ private renderImage;
2460
+ private renderVideo;
2461
+ private renderAudio;
2462
+ private hexToRgba;
2463
+ private renderNotice;
2464
+ private renderTables;
2465
+ private renderGallery;
2466
+ private renderColumns;
2467
+ private renderSeparator;
2468
+ private renderScroll;
2469
+ private renderAbbr;
2470
+ private renderTooltip;
2471
+ private renderRaw;
2472
+ private renderPlain;
2473
+ private renderAlign;
2474
+ private renderEffect;
2475
+ private renderAnim;
2476
+ private renderContainer;
2477
+ private static readonly STYLE_PROP_WHITELIST;
2478
+ private renderStyleTag;
2479
+ private renderQuote;
2480
+ private renderSpoilerbox;
2481
+ private renderBox;
2482
+ private renderTitle;
2483
+ /**
2484
+ * `[box=Title:#hex]`, `[tables=striped:#hex]`, `[columns=2:#hex]` → un
2485
+ * ` style="--<suffix>-accent:#hex;"` del que el CSS deriva la paleta
2486
+ * (título + chevron y borde en boxw; gradientes/header/hover en tables;
2487
+ * borde + fondo en columns). Devuelve '' si no hay color válido.
2488
+ */
2489
+ private boxAccentStyle;
2490
+ private renderList;
2491
+ private renderListItem;
2492
+ private renderCode;
2493
+ private renderSVG;
2494
+ /**
2495
+ * Render an osu! BBCode imagemap.
2496
+ *
2497
+ * Structure:
2498
+ * [imagemap]
2499
+ * https://example.com/image.png ← first line = image URL
2500
+ * 10 20 50 60 https://... Label ← subsequent lines = clickable areas
2501
+ * [/imagemap]
2502
+ *
2503
+ * Each area line format: x y width height url [label]
2504
+ * All values are PERCENTAGES (0–100) relative to the image dimensions.
2505
+ * Uses CSS absolute positioning with percentage coordinates.
2506
+ */
2507
+ private renderImagemap;
2508
+ private collectNodeText;
2509
+ private renderGradient;
2510
+ /**
2511
+ * Escape for both text content and double-quoted attribute values.
2512
+ *
2513
+ * `'` is included because attribute values elsewhere in the codebase (and in
2514
+ * consumer-supplied tag handlers) may be single-quoted; leaving it raw makes
2515
+ * the escaping context-dependent, which is how injections get reintroduced.
2516
+ */
2517
+ /**
2518
+ * Escape the five HTML-significant characters.
2519
+ *
2520
+ * Was five chained `.replace(/x/g, …)` calls: five full scans of the string
2521
+ * and up to five intermediate allocations for *every* text node, even though
2522
+ * ordinary prose contains none of these characters. This tests once and
2523
+ * returns the input untouched in that common case, then does a single pass
2524
+ * when there is actually something to escape.
2525
+ */
2526
+ private escapeHtml;
2527
+ }
2528
+
2529
+ /**
2530
+ * DocumentEngine — DOMMorpher
2531
+ *
2532
+ * Reconciles and morphs an existing HTML DOM tree in-place to match a new HTML string.
2533
+ * Operating on pure HTML string output from HTMLRenderer means ZERO changes to Quasar AST schemas.
2534
+ *
2535
+ * Features:
2536
+ * - In-place attribute synchronization
2537
+ * - In-place text node value updates
2538
+ * - Preserves state of unchanged elements (spoilers, audio/video, selections)
2539
+ * - Zero extra wrapper <div> elements
2540
+ */
2541
+ declare function morphHTML(container: HTMLElement, newHTML: string): void;
2542
+
2543
+ /**
2544
+ * DocumentEngine — BlockPatcher
2545
+ *
2546
+ * Incremental rendering for the HTML preview.
2547
+ *
2548
+ * The naive path — `renderToHTML(root)` + `morphHTML(container, html)` on every
2549
+ * keystroke — re-serializes the ENTIRE document to a string and, worse, makes
2550
+ * the browser re-parse that whole string (`template.innerHTML`) even when only
2551
+ * one character changed. Profiled on an 18.5 KB document: `morphHTML` ≈ 29.8 ms
2552
+ * per keystroke, of which ~27 ms is the full-HTML parse (morph ≈ baseline
2553
+ * `innerHTML=`). The parser itself is ~0.5 ms.
2554
+ *
2555
+ * This module makes the DOM update O(changed block) instead of O(document):
2556
+ *
2557
+ * - The document's top-level children are the independent "blocks".
2558
+ * - With stable `data-node-id`s (see `preserveNodeIds`) a block that did not
2559
+ * change renders to the SAME html string, or is even the SAME RedNode
2560
+ * object (red-subtree reuse). Either way we can detect "unchanged" without
2561
+ * touching the DOM.
2562
+ * - Only changed blocks are re-rendered and re-morphed, in place, keyed by
2563
+ * their element.
2564
+ * - Structural edits — a block inserted, removed or reordered (Enter,
2565
+ * backspace between blocks, moving a block in the visual builder) — are
2566
+ * reconciled BY KEY too. The stable ids let us insert/remove/move ONLY the
2567
+ * affected elements, so every untouched block keeps its DOM identity and
2568
+ * runtime state (an open `<details>`, media playback). When the edit
2569
+ * script adds more blocks than the previous document had (a whole new
2570
+ * document patched into the same container, ids regenerated en masse) we
2571
+ * fall back to a full rebuild: one `innerHTML` parse beats N per-block
2572
+ * ones.
2573
+ *
2574
+ * DOM alignment — "runs": a top-level block may render to an element, to a
2575
+ * bare text node (`text`, osu `spacing`/`empty_line` quirks) or to nothing
2576
+ * (`''`, an empty text leaf). Two adjacent bare-text blocks are merged into a
2577
+ * single text node by the HTML parser, so a block↔node 1:1 pairing would
2578
+ * silently misalign after such a merge. The reconciliation therefore works on
2579
+ * RUNS: consecutive bare-text blocks form one "text run" that maps to exactly
2580
+ * one text node (mirroring the parser), and element blocks are their own runs.
2581
+ * The run list is the exact 1:1 mirror of `container.childNodes`.
2582
+ */
2583
+
2584
+ interface PatchBlocksOptions {
2585
+ /** Renderer used to serialize blocks. Defaults to a shared HTMLRenderer. */
2586
+ renderer?: HTMLRenderer;
2587
+ /** Called when a block fails to morph; the caller can log it. */
2588
+ onError?: (err: unknown) => void;
2589
+ /**
2590
+ * The source range of the edit that produced `rootNode`, if known.
2591
+ *
2592
+ * Lets the patcher reconcile ONLY the runs overlapping the edit (O(edit))
2593
+ * instead of walking the whole document — the difference between ~2ms and
2594
+ * ~100ms per keystroke on a 500k-character document. Coordinates are in
2595
+ * new-source space with the old end kept separately; see `TextChangeRange`.
2596
+ * When omitted, the patcher falls back to the full keyed reconcile — and it
2597
+ * also reads the range attached to the root by `DocumentModel`.
2598
+ */
2599
+ change?: TextChangeRange;
2600
+ /**
2601
+ * Minimum top-level blocks for the windowed path to run; below it the full
2602
+ * keyed walk is cheaper than the windowed bookkeeping. Defaults to
2603
+ * `MIN_WINDOWED_BLOCKS` (200). Tests force `0` to exercise the windowed
2604
+ * path on small documents.
2605
+ */
2606
+ minWindowedBlocks?: number;
2607
+ }
2608
+ interface PatchBlocksStats {
2609
+ /** Which path was taken. */
2610
+ mode: 'full' | 'blocks';
2611
+ /** Number of top-level blocks. */
2612
+ total: number;
2613
+ /** Blocks whose DOM was actually updated (0 = nothing changed). */
2614
+ patched: number;
2615
+ /** True when the windowed (O(edit)) reconcile ran, not the full keyed walk. */
2616
+ windowed?: boolean;
2617
+ }
2618
+ /**
2619
+ * Reconcile `container`'s children against `rootNode`'s top-level blocks.
2620
+ *
2621
+ * Inline edits morph only the changed block; structural edits (insert, remove,
2622
+ * reorder) are reconciled by key so untouched blocks keep their DOM identity.
2623
+ * Returns what was done so callers/tests can assert the fast path fired.
2624
+ */
2625
+ declare function patchBlocksInto(container: HTMLElement, rootNode: RedNode | null, options?: PatchBlocksOptions): PatchBlocksStats;
2626
+
2627
+ /**
2628
+ * SVGRenderer (Runtime ForeignObject Strategy)
2629
+ *
2630
+ * Instead of attempting to calculate CSS layouts manually,
2631
+ * this runtime renderer wraps the exact HTML output in an SVG <foreignObject>.
2632
+ *
2633
+ * This gives 100% precision with zero DOM scanning overhead for real-time previews.
2634
+ */
2635
+ declare class SVGRenderer extends Visitor<string> {
2636
+ private htmlRenderer;
2637
+ visit(node: RedNode, context?: VisitorContext): string;
2638
+ render(root: RedNode): string;
2639
+ }
2640
+
2641
+ /**
2642
+ * DocumentEngine — MarkdownExporter
2643
+ *
2644
+ * Exports the Document Model to Markdown.
2645
+ * Demonstrates the format-agnostic power of the engine:
2646
+ * BBCode in → DocumentModel → Markdown out.
2647
+ *
2648
+ * A single visitor, no parser changes needed.
2649
+ */
2650
+
2651
+ declare class MarkdownExporter extends Visitor<string> {
2652
+ visit(node: RedNode): string;
2653
+ export(node: RedNode): string;
2654
+ private exportNode;
2655
+ private renderTitleMarkdown;
2656
+ /**
2657
+ * Read a BBCode tag's `=VALUE` attribute.
2658
+ *
2659
+ * This previously read `node.properties` / `node.attributes`, neither of
2660
+ * which exists on RedNode — so it silently returned `undefined` for every
2661
+ * node and every call site fell through to its placeholder. That is why
2662
+ * Markdown export dropped link targets and quote/box titles.
2663
+ */
2664
+ private extractValue;
2665
+ }
2666
+
2667
+ /**
2668
+ * DocumentEngine — JSONExporter
2669
+ *
2670
+ * Exports the Document Model to JSON.
2671
+ * Useful for:
2672
+ * - Saving/loading documents in a structured format
2673
+ * - Sending over the wire (collaboration)
2674
+ * - Debugging and inspecting the document tree
2675
+ * - Import/export between different tools
2676
+ */
2677
+
2678
+ interface JSONDocument {
2679
+ version: number;
2680
+ kind: string;
2681
+ nodes: JSONNode[];
2682
+ metadata?: Record<string, unknown>;
2683
+ }
2684
+ interface JSONNode {
2685
+ id: string;
2686
+ kind: string;
2687
+ text: string;
2688
+ version: number;
2689
+ children: JSONNode[];
2690
+ attributes?: Record<string, unknown>;
2691
+ metadata?: Record<string, unknown>;
2692
+ range?: {
2693
+ start: number;
2694
+ end: number;
2695
+ };
2696
+ }
2697
+ declare class JSONExporter extends Visitor<string> {
2698
+ /**
2699
+ * Export a RedNode tree to a JSON string.
2700
+ */
2701
+ visit(node: RedNode): string;
2702
+ /**
2703
+ * Export to a JSON object.
2704
+ */
2705
+ toJSON(node: RedNode): JSONDocument;
2706
+ private serializeNode;
2707
+ }
2708
+
2709
+ /**
2710
+ * DocumentEngine — TiptapExporter
2711
+ *
2712
+ * Exports the Document Model to Tiptap (ProseMirror) JSON format.
2713
+ * This proves how easily Quasar's AST bridges to visual WYSIWYG editors.
2714
+ *
2715
+ * Tiptap uses a JSON format structured like:
2716
+ * {
2717
+ * type: "doc",
2718
+ * content: [
2719
+ * {
2720
+ * type: "paragraph",
2721
+ * content: [
2722
+ * { type: "text", text: "Hello ", marks: [{ type: "bold" }] }
2723
+ * ]
2724
+ * }
2725
+ * ]
2726
+ * }
2727
+ */
2728
+
2729
+ interface TiptapNode {
2730
+ type: string;
2731
+ attrs?: Record<string, any>;
2732
+ content?: TiptapNode[];
2733
+ marks?: TiptapMark[];
2734
+ text?: string;
2735
+ }
2736
+ interface TiptapMark {
2737
+ type: string;
2738
+ attrs?: Record<string, any>;
2739
+ }
2740
+ declare class TiptapExporter extends Visitor<string> {
2741
+ visit(node: RedNode): string;
2742
+ toTiptap(node: RedNode): TiptapNode;
2743
+ private processChildren;
2744
+ private processNode;
2745
+ private kindToMark;
2746
+ private kindToBlockType;
2747
+ }
2748
+
2749
+ /**
2750
+ * DocumentEngine — Transaction
2751
+ *
2752
+ * Groups operations into an atomic, undoable unit.
2753
+ * Every modification to the document MUST go through a Transaction.
2754
+ *
2755
+ * Inspired by ProseMirror's Transaction system.
2756
+ */
2757
+
2758
+ declare class Transaction {
2759
+ readonly operations: Operation[];
2760
+ readonly id: string;
2761
+ readonly timestamp: number;
2762
+ private _applied;
2763
+ constructor(operations: Operation[]);
2764
+ /**
2765
+ * Apply the transaction to a RedNode tree.
2766
+ * Returns the updated tree or null if application failed.
2767
+ */
2768
+ apply(root: RedNode): RedNode | null;
2769
+ /**
2770
+ * Create the inverse transaction (for undo).
2771
+ */
2772
+ invert(root: RedNode): Transaction;
2773
+ /**
2774
+ * Get a human-readable label for this transaction.
2775
+ */
2776
+ getLabel(): string;
2777
+ private applyOperation;
2778
+ private applyInsert;
2779
+ private applyDelete;
2780
+ private applyReplace;
2781
+ private applyMove;
2782
+ private applySetText;
2783
+ }
2784
+
2785
+ /**
2786
+ * DocumentEngine — Command
2787
+ *
2788
+ * Commands are high-level user actions that MAY produce
2789
+ * a Transaction (or multiple) to modify the document.
2790
+ *
2791
+ * Commands are stateless — they receive context and produce operations.
2792
+ * This makes them testable, undoable, and safe for AI use.
2793
+ *
2794
+ * Inspired by ProseMirror's Commands and VSCode's Command system.
2795
+ */
2796
+
2797
+ interface CommandContext {
2798
+ model: DocumentModel;
2799
+ root: RedNode;
2800
+ selection?: {
2801
+ nodeId: string;
2802
+ start: number;
2803
+ end: number;
2804
+ };
2805
+ }
2806
+ type CommandResult = {
2807
+ success: boolean;
2808
+ operations?: Operation[];
2809
+ message?: string;
2810
+ };
2811
+ interface Command {
2812
+ readonly id: string;
2813
+ readonly label: string;
2814
+ readonly description?: string;
2815
+ readonly shortcut?: string;
2816
+ execute(ctx: CommandContext): CommandResult;
2817
+ canExecute?(ctx: CommandContext): boolean;
2818
+ }
2819
+
2820
+ /**
2821
+ * DocumentEngine — CommandRegistry
2822
+ *
2823
+ * Central registry for all document commands.
2824
+ * Commands can be added by plugins.
2825
+ *
2826
+ * Enables:
2827
+ * - Keyboard shortcut binding
2828
+ * - Toolbar integration
2829
+ * - AI command execution
2830
+ * - Menu integration
2831
+ */
2832
+
2833
+ declare class CommandRegistry {
2834
+ private commands;
2835
+ /**
2836
+ * Register a command.
2837
+ */
2838
+ register(command: Command): void;
2839
+ /**
2840
+ * Unregister a command.
2841
+ */
2842
+ unregister(id: string): void;
2843
+ /**
2844
+ * Get a command by ID.
2845
+ */
2846
+ get(id: string): Command | undefined;
2847
+ /**
2848
+ * Check if a command is registered.
2849
+ */
2850
+ has(id: string): boolean;
2851
+ /**
2852
+ * Execute a command by ID.
2853
+ */
2854
+ execute(id: string, ctx: CommandContext): CommandResult;
2855
+ /**
2856
+ * Get all registered commands.
2857
+ */
2858
+ getAll(): Command[];
2859
+ /**
2860
+ * Get commands by a filter function.
2861
+ */
2862
+ filter(predicate: (cmd: Command) => boolean): Command[];
2863
+ /**
2864
+ * Get the count of registered commands.
2865
+ */
2866
+ get size(): number;
2867
+ }
2868
+
2869
+ /**
2870
+ * DocumentEngine — InsertText Command
2871
+ *
2872
+ * Inserts text at the current cursor position.
2873
+ * Creates or reuses a text node.
2874
+ */
2875
+
2876
+ declare const InsertText: Command;
2877
+
2878
+ /**
2879
+ * DocumentEngine — DeleteNode Command
2880
+ *
2881
+ * Deletes a node from the document tree.
2882
+ */
2883
+
2884
+ declare const DeleteNode: Command;
2885
+
2886
+ /**
2887
+ * DocumentEngine — WrapInTag Command
2888
+ *
2889
+ * Wraps the current selection in a BBCode tag.
2890
+ * E.g. selected text → [b]selected text[/b]
2891
+ */
2892
+
2893
+ declare const WrapInTag: Command;
2894
+
2895
+ /**
2896
+ * DocumentEngine — SplitNode & MergeNodes Commands
2897
+ *
2898
+ * Split a node at a position, or merge two adjacent nodes.
2899
+ * Essential for block editing and backspace handling.
2900
+ */
2901
+
2902
+ declare const SplitNode: Command;
2903
+ declare const MergeNode: Command;
2904
+
2905
+ /**
2906
+ * DocumentEngine — Formatter
2907
+ *
2908
+ * Formats the document tree into "pretty" BBCode.
2909
+ * Normalizes indentation, spacing, and whitespace.
2910
+ *
2911
+ * Uses the TagRegistry and Visitor pattern.
2912
+ * Plugins can register custom formatters for custom tags.
2913
+ */
2914
+
2915
+ interface FormatOptions {
2916
+ /** Whether to add newlines between block elements */
2917
+ blockNewlines?: boolean;
2918
+ /** Whether to normalize whitespace inside inline elements */
2919
+ normalizeInlineWhitespace?: boolean;
2920
+ /** Maximum line width (0 = no limit) */
2921
+ maxLineWidth?: number;
2922
+ /** Whether to collapse consecutive empty lines */
2923
+ collapseEmptyLines?: boolean;
2924
+ /** Indentation string */
2925
+ indent?: string;
2926
+ }
2927
+ declare class Formatter {
2928
+ /**
2929
+ * Format a RedNode tree (extracts text while preserving exact layout/spacing).
2930
+ */
2931
+ format(root: RedNode): string;
2932
+ /**
2933
+ * Format a single node at a given depth.
2934
+ */
2935
+ private formatNode;
2936
+ }
2937
+
2938
+ /**
2939
+ * DocumentEngine — Linter
2940
+ *
2941
+ * Lints the document tree for issues.
2942
+ * Similar to ESLint but for BBCode documents.
2943
+ *
2944
+ * Rules can be added via the Plugin API.
2945
+ */
2946
+
2947
+ type LintSeverity = 'error' | 'warning' | 'info' | 'hint';
2948
+ interface LintRule {
2949
+ code: string;
2950
+ severity: LintSeverity;
2951
+ description: string;
2952
+ validate(node: RedNode, context: LintContext): LintIssue | LintIssue[] | null;
2953
+ }
2954
+ interface LintContext {
2955
+ source: string;
2956
+ allNodes: Map<string, RedNode>;
2957
+ }
2958
+ interface LintIssue {
2959
+ code: string;
2960
+ message: string;
2961
+ severity: LintSeverity;
2962
+ nodeId: string;
2963
+ range: {
2964
+ start: number;
2965
+ end: number;
2966
+ } | null;
2967
+ fix?: {
2968
+ description: string;
2969
+ apply: () => void;
2970
+ };
2971
+ }
2972
+ interface LintResult {
2973
+ issues: LintIssue[];
2974
+ errorCount: number;
2975
+ warningCount: number;
2976
+ infoCount: number;
2977
+ hintCount: number;
2978
+ }
2979
+ declare class Linter {
2980
+ private rules;
2981
+ constructor();
2982
+ /**
2983
+ * Register a lint rule.
2984
+ */
2985
+ register(rule: LintRule): void;
2986
+ /**
2987
+ * Unregister a lint rule.
2988
+ */
2989
+ unregister(code: string): void;
2990
+ /**
2991
+ * Lint a RedNode tree.
2992
+ */
2993
+ lint(root: RedNode, source: string): LintResult;
2994
+ private registerBuiltinRules;
2995
+ }
2996
+
2997
+ /**
2998
+ * DocumentEngine — Symbol Types
2999
+ *
3000
+ * Symbol system for definitions and references in BBCode documents.
3001
+ * Enables: go-to-definition, find-all-references, rename, etc.
3002
+ *
3003
+ * Inspired by Language Server Protocol (LSP).
3004
+ */
3005
+
3006
+ type SymbolKind = 'tag' | 'id_definition' | 'id_reference' | 'class' | 'property' | 'variable' | 'function' | 'module' | 'snippet' | 'template' | 'other';
3007
+ interface SymbolInfo {
3008
+ /** Unique ID for this symbol */
3009
+ id: string;
3010
+ /** Symbol name (e.g. 'hero', 'my-block') */
3011
+ name: string;
3012
+ /** Symbol kind */
3013
+ kind: SymbolKind;
3014
+ /** The node that defines this symbol */
3015
+ definitionNodeId: NodeId;
3016
+ /** Source range of the definition */
3017
+ definitionRange: SourceRange;
3018
+ /** Container node (e.g. the document) */
3019
+ containerNodeId: NodeId;
3020
+ /** All references to this symbol */
3021
+ references: Reference[];
3022
+ /** Additional data */
3023
+ data?: Record<string, unknown>;
3024
+ }
3025
+ interface Reference {
3026
+ nodeId: NodeId;
3027
+ range: SourceRange;
3028
+ kind: 'definition' | 'reference' | 'implementation';
3029
+ }
3030
+ interface SymbolSearchResult {
3031
+ symbol: SymbolInfo;
3032
+ score: number;
3033
+ }
3034
+
3035
+ /**
3036
+ * DocumentEngine — SymbolTable
3037
+ *
3038
+ * Tracks symbols (definitions and references) in the document.
3039
+ * Enables IDE features like:
3040
+ * - Go to definition
3041
+ * - Find all references
3042
+ * - Rename symbol
3043
+ * - Document outline
3044
+ *
3045
+ * In BBCode, symbols include:
3046
+ * - [id=hero] → Definition
3047
+ * - [goto=hero] → Reference
3048
+ * - Named anchors and targets
3049
+ */
3050
+
3051
+ declare class SymbolTable {
3052
+ private symbols;
3053
+ private nodeToSymbolId;
3054
+ /**
3055
+ * Build the symbol table from a RedNode tree.
3056
+ */
3057
+ build(root: RedNode): void;
3058
+ /**
3059
+ * Get a symbol by name.
3060
+ */
3061
+ get(name: string): SymbolInfo | undefined;
3062
+ /**
3063
+ * Get the symbol associated with a node.
3064
+ */
3065
+ getSymbolForNode(nodeId: string): SymbolInfo | undefined;
3066
+ /**
3067
+ * Search for symbols by name.
3068
+ */
3069
+ search(query: string, maxResults?: number): SymbolSearchResult[];
3070
+ /**
3071
+ * Get all symbols.
3072
+ */
3073
+ getAll(): SymbolInfo[];
3074
+ /**
3075
+ * Clear all symbols.
3076
+ */
3077
+ clear(): void;
3078
+ private addSymbol;
3079
+ private createSymbolId;
3080
+ }
3081
+
3082
+ /**
3083
+ * DocumentEngine — RenderPipeline
3084
+ *
3085
+ * The Render Pipeline transforms a DocumentModel into renderable output.
3086
+ * It sits between the model and the view layer.
3087
+ *
3088
+ * The pipeline is:
3089
+ * DocumentModel → RenderTree → HTML/SVG/Canvas/React
3090
+ *
3091
+ * Plugins can intercept at any stage:
3092
+ * - Add/transform render nodes
3093
+ * - Provide custom renderers for custom tags
3094
+ * - Post-process the output
3095
+ */
3096
+
3097
+ type RenderPhase = 'build' | 'transform' | 'serialize';
3098
+ interface RenderHook {
3099
+ phase: RenderPhase;
3100
+ hook: (node: RenderNode, context: RenderContext) => RenderNode | null;
3101
+ }
3102
+ interface RenderContext {
3103
+ variant: RenderVariant;
3104
+ registry: TagRegistry;
3105
+ source: string;
3106
+ }
3107
+ declare class RenderPipeline {
3108
+ private hooks;
3109
+ /**
3110
+ * Register a render hook.
3111
+ */
3112
+ register(hook: RenderHook): void;
3113
+ /**
3114
+ * Unregister a render hook.
3115
+ */
3116
+ unregister(hook: RenderHook): void;
3117
+ /**
3118
+ * Render a RedNode tree to a string.
3119
+ */
3120
+ render(root: RedNode, variant: RenderVariant | undefined, registry: TagRegistry, source?: string): string;
3121
+ /**
3122
+ * Build a RenderNode tree from a RedNode tree.
3123
+ */
3124
+ private buildRenderTree;
3125
+ }
3126
+
3127
+ /**
3128
+ * DocumentEngine — PluginRegistry
3129
+ *
3130
+ * Manages the lifecycle of plugins/extensions.
3131
+ * Each plugin registers contributions (tags, commands, validators, etc.)
3132
+ * and can be loaded/unloaded at runtime.
3133
+ *
3134
+ * Inspired by VSCode's extension system.
3135
+ */
3136
+
3137
+ interface PluginManifest {
3138
+ name: string;
3139
+ version: string;
3140
+ description?: string;
3141
+ author?: string;
3142
+ /** Dependencies on other plugins */
3143
+ dependencies?: string[];
3144
+ }
3145
+ interface PluginContribution {
3146
+ /** Tags to register */
3147
+ tags?: TagDefinition[];
3148
+ /** Commands to register */
3149
+ commands?: Command[];
3150
+ /** Semantic validators */
3151
+ validators?: Validator[];
3152
+ /** Lint rules */
3153
+ lintRules?: LintRule[];
3154
+ /** Render hooks */
3155
+ renderHooks?: RenderHook[];
3156
+ /** CSS to inject */
3157
+ styles?: string[];
3158
+ /** Initialization function */
3159
+ activate?: () => void | Promise<void>;
3160
+ /** Cleanup function */
3161
+ deactivate?: () => void;
3162
+ }
3163
+ interface Plugin {
3164
+ manifest: PluginManifest;
3165
+ contributions: PluginContribution;
3166
+ /** Whether the plugin is currently active */
3167
+ active: boolean;
3168
+ }
3169
+ declare class PluginRegistry {
3170
+ private plugins;
3171
+ /**
3172
+ * Register a plugin.
3173
+ */
3174
+ register(manifest: PluginManifest, contributions: PluginContribution): Plugin;
3175
+ /**
3176
+ * Unregister and deactivate a plugin.
3177
+ */
3178
+ unregister(name: string): boolean;
3179
+ /**
3180
+ * Get a plugin by name.
3181
+ */
3182
+ get(name: string): Plugin | undefined;
3183
+ /**
3184
+ * Get all registered plugins.
3185
+ */
3186
+ getAll(): Plugin[];
3187
+ /**
3188
+ * Activate a plugin (calls activate function).
3189
+ */
3190
+ activate(plugin: Plugin): void;
3191
+ /**
3192
+ * Deactivate a plugin (calls deactivate function).
3193
+ */
3194
+ deactivate(plugin: Plugin): void;
3195
+ /**
3196
+ * Activate all registered plugins.
3197
+ */
3198
+ activateAll(): void;
3199
+ /**
3200
+ * Deactivate all plugins.
3201
+ */
3202
+ deactivateAll(): void;
3203
+ }
3204
+
3205
+ /**
3206
+ * DocumentEngine — PluginAPI
3207
+ *
3208
+ * The public API exposed to plugins.
3209
+ * Plugin authors use this to register tags, commands, validators, etc.
3210
+ *
3211
+ * Inspired by VSCode's `vscode` module.
3212
+ */
3213
+
3214
+ declare class PluginAPI {
3215
+ readonly model: DocumentModel;
3216
+ readonly tags: TagRegistry;
3217
+ readonly commands: CommandRegistry;
3218
+ readonly semantic: SemanticAnalyzer;
3219
+ readonly linter: Linter;
3220
+ readonly renderer: RenderPipeline;
3221
+ readonly plugins: PluginRegistry;
3222
+ constructor(model: DocumentModel);
3223
+ /**
3224
+ * Register a plugin with all its contributions.
3225
+ *
3226
+ * Example:
3227
+ * ```ts
3228
+ * api.registerPlugin({
3229
+ * name: 'my-plugin',
3230
+ * version: '1.0.0'
3231
+ * }, {
3232
+ * tags: [{ name: 'blur', kind: 'custom', ... }],
3233
+ * commands: [{ id: 'my-command', ... }]
3234
+ * })
3235
+ * ```
3236
+ */
3237
+ registerPlugin(manifest: PluginManifest, contributions: PluginContribution): void;
3238
+ /**
3239
+ * Unregister a plugin and remove its contributions.
3240
+ */
3241
+ unregisterPlugin(name: string): void;
3242
+ }
3243
+
3244
+ /**
3245
+ * DocumentEngine — GreenNodePool (Structural Sharing via Interning)
3246
+ *
3247
+ * Roslyn-grade GreenNode Deduplication Pool.
3248
+ *
3249
+ * Ensures that structurally identical subtrees share the EXACT SAME
3250
+ * GreenNode memory address in RAM. This is structural sharing:
3251
+ * when the same BBCode block is pasted 10 times, the parser creates
3252
+ * GreenNodes for it ONCE, and all 10 copies reference the same objects.
3253
+ *
3254
+ * Architecture:
3255
+ * - Flyweight Pattern: GreenNodes are pure structural data without parent pointers.
3256
+ * - Hash-based lookup: O(1) amortized via _hash property on GreenNode.
3257
+ * - Reference-equality verification: identical children = same reference (because
3258
+ * children are interned first, bottom-up construction guarantees this).
3259
+ * - Per-document scope: interners are created per BBCodeDocumentModel, cleared on dispose.
3260
+ *
3261
+ * Key insight: because GreenNodes are immutable and children are interned bottom-up,
3262
+ * two nodes with the same (kind, text, widths, children-references) are structurally
3263
+ * identical. We use _hash for fast lookup, then verify with reference equality on
3264
+ * children.
3265
+ *
3266
+ * ─── Why this used to be broken (roadmap S2) ────────────────────────────────
3267
+ *
3268
+ * Green nodes used to carry an absolute `range`, and the two halves of the pool
3269
+ * disagreed about what to do with it:
3270
+ *
3271
+ * - `internLeaf` keyed on `kind:text` alone, so the second `"Hello"` in a
3272
+ * document got back the node built for the FIRST one — carrying the first
3273
+ * one's offsets. Enabling the interner made every position downstream lie
3274
+ * silently: incremental reparse, `findNodeAtOffset`, diagnostics, the
3275
+ * Monaco↔AST mapping.
3276
+ * - `internNode` did the opposite and required identical start AND end before
3277
+ * deduplicating, which no two distinct occurrences can have. The same block
3278
+ * pasted ten times gave 0% dedup and 100% of the scanning cost.
3279
+ *
3280
+ * Neither was fixable while position lived on the green node, because those are
3281
+ * the only two options: ignore it and lie, or respect it and never match. The
3282
+ * fix was to remove position from the green tree entirely — see `GreenNode.ts`.
3283
+ * Now `kind:text:width` genuinely identifies a leaf, and interning is simply
3284
+ * correct.
3285
+ */
3286
+
3287
+ /**
3288
+ * How much of the tree to deduplicate.
3289
+ *
3290
+ * Measured on the 19.6 KB reference document, against no interning at all:
3291
+ *
3292
+ * | mode | parse time | green tree memory |
3293
+ * |----------|------------|-------------------|
3294
+ * | `leaves` | +16% | −42% |
3295
+ * | `full` | +110% | −56% |
3296
+ *
3297
+ * `leaves` is the default because it buys three quarters of the memory for a
3298
+ * seventh of the time. Interning an internal node means hashing its whole
3299
+ * subtree — `_hash` is lazy, so a parse that never interns never pays for it —
3300
+ * and then walking its children to verify a bucket hit. Leaves cost one string
3301
+ * key and one Map lookup, and they are where the duplication is: the reference
3302
+ * document has 930 leaves and only 122 distinct ones.
3303
+ */
3304
+ type InterningMode = 'leaves' | 'full';
3305
+
3306
+ /**
3307
+ * DocumentEngine — BBCodeDocumentModel
3308
+ *
3309
+ * A DocumentModel subclass that uses the DocumentEngine's built-in
3310
+ * BBCode Lexer + Parser to produce GreenNode/RedNode trees.
3311
+ *
3312
+ * This COMPLETELY replaces the old BBCode parser (MiliastryNovaFeatures/BBCode/Parser).
3313
+ * The old parser is DEPRECATED.
3314
+ *
3315
+ * Architecture:
3316
+ * BBCode text
3317
+ * ↓ BBCodeLexer.scanBBCode()
3318
+ * BBCodeToken[]
3319
+ * ↓ Parser.parseTokensToGreen()
3320
+ * GreenNode
3321
+ * ↓ BBCodeToGreenNode.greenToRedNode()
3322
+ * RedNode
3323
+ * ↓ HTMLRenderer.render()
3324
+ * HTML preview
3325
+ *
3326
+ * Usage:
3327
+ * const model = new BBCodeDocumentModel({ source: '[b]Hello[/b]' })
3328
+ * const html = new HTMLRenderer().render(model.redRoot!)
3329
+ */
3330
+
3331
+ interface BBCodeDocumentModelOptions extends DocumentModelOptions {
3332
+ /** Source BBCode text to parse */
3333
+ source?: string;
3334
+ /** Language identifier (default: 'bbcode') */
3335
+ language?: string;
3336
+ /** Enforce strict tag nesting (disables osu! auto-close legacy behavior) */
3337
+ strictMode?: boolean;
3338
+ /** BBCode dialect to parse against ('osu' | 'miliastry' | 'lyne'). Default: 'miliastry'. */
3339
+ dialect?: BBCodeDialect;
3340
+ /** Alias for dialect ('osu' | 'miliastry' | 'lyne') */
3341
+ mode?: BBCodeDialect;
3342
+ /**
3343
+ * Deduplicate structurally identical green nodes across the document.
3344
+ *
3345
+ * Off by default. It is correct now — see the header of `GreenNodePool.ts`
3346
+ * for why it was not — but it is a memory/latency trade, and this engine's
3347
+ * declared budget is latency: on the 19.6 KB reference document `'leaves'`
3348
+ * costs 16% more parse time to save 42% of the green tree's memory. Turn it
3349
+ * on for workloads that hold many documents at once; leave it off to type
3350
+ * into one.
3351
+ */
3352
+ interning?: InterningMode;
3353
+ }
3354
+ declare class BBCodeDocumentModel extends DocumentModel {
3355
+ private _strictMode;
3356
+ private _dialect;
3357
+ /** Per-document interner, or null when interning is off. */
3358
+ private _interner;
3359
+ /** Memoized parser `extraTags`, rebuilt only when the registry changes. */
3360
+ private _extraTags;
3361
+ private _extraTagsVersion;
3362
+ constructor(options?: BBCodeDocumentModelOptions);
3363
+ get dialect(): BBCodeDialect;
3364
+ get root(): RedNode | null;
3365
+ toHTML(renderer?: HTMLRenderer): string;
3366
+ /**
3367
+ * Parse BBCode text directly to a GreenNode tree using the
3368
+ * DocumentEngine's built-in BBCode Lexer + Parser.
3369
+ * No dependency on the deprecated old parser.
3370
+ */
3371
+ protected parseToGreen(source: string, options?: ReparseParseOptions): GreenNode;
3372
+ /**
3373
+ * Build a RedNode from a GreenNode, extracting metadata
3374
+ * from BBCode attributes in the process.
3375
+ */
3376
+ protected buildRedFromGreen(green: GreenNode): RedNode;
3377
+ /**
3378
+ * Reuse-aware red build for the incremental path: adopts old red subtrees
3379
+ * wherever the splice shared their green by reference. See the contract on
3380
+ * `greenToRedNodeReusing` — the old tree is consumed.
3381
+ */
3382
+ protected buildRedFromGreenReusing(green: GreenNode, oldRed: RedNode, stats?: {
3383
+ adopted: number;
3384
+ }): RedNode;
3385
+ /**
3386
+ * Serialize the red tree back to BBCode, so `transact`/`undo`/`redo` can
3387
+ * rebuild from text. Target 'miliastry' keeps native tags (gradient, grow,
3388
+ * …) as-is instead of expanding them, which is what round-tripping needs.
3389
+ */
3390
+ protected exportSource(root: RedNode): string;
3391
+ }
3392
+
3393
+ /**
3394
+ * DocumentEngine — Position transforms
3395
+ *
3396
+ * Where does a position end up after a text edit?
3397
+ *
3398
+ * This is the primitive collaboration stands on: keeping carets, selections
3399
+ * and remote cursors pointing at the same *content* while the text shifts
3400
+ * under them — whether the edit came from the local user, a remote peer, or
3401
+ * a programmatic `transact`. See `QuasarCollab.MD` for the architecture; the
3402
+ * short version is that Quasar syncs TEXT, so mapping positions through
3403
+ * `TextChange`s is all the transform machinery the engine needs. This module
3404
+ * transforms positions, not changes-against-changes: convergence of
3405
+ * concurrent edits is the CRDT's job, not ours.
3406
+ */
3407
+
3408
+ /**
3409
+ * Which side a position sticks to when an edit happens exactly at it.
3410
+ *
3411
+ * A caret usually wants `'right'`: text inserted at the caret by someone else
3412
+ * should push it forward (you keep typing after their insertion). The start
3413
+ * of a persistent highlight usually wants `'left'`: text inserted exactly at
3414
+ * its start belongs before the highlight, not inside it.
3415
+ */
3416
+ type TransformBias = 'left' | 'right';
3417
+ /**
3418
+ * Map `offset` through one change or an ordered sequence of changes.
3419
+ *
3420
+ * Rules, in order:
3421
+ * - strictly before the edit → unchanged;
3422
+ * - strictly after the replaced span → shifted by the length delta;
3423
+ * - inside the replaced span → collapsed to the edit's boundary (`'left'` →
3424
+ * where the replacement starts, `'right'` → where it ends). A position
3425
+ * inside deleted text has no content to point at anymore; the boundary is
3426
+ * the only honest answer;
3427
+ * - exactly at a pure insertion point → `bias` decides which side of the
3428
+ * inserted text it lands on.
3429
+ */
3430
+ declare function transformOffset(offset: number, changes: TextChange | readonly TextChange[], bias?: TransformBias): number;
3431
+ /**
3432
+ * Map a `{start, end}` range through one change or a sequence.
3433
+ *
3434
+ * The start sticks RIGHT and the end sticks LEFT, which is what preserves the
3435
+ * selected content: text inserted exactly at a boundary lands OUTSIDE the
3436
+ * range, so the range keeps covering exactly the characters it covered — it
3437
+ * neither absorbs a neighbour's insertion nor leaks its own content. The
3438
+ * result is clamped so it can never come out inverted; a range entirely
3439
+ * inside deleted text collapses to a point at the edit boundary.
3440
+ */
3441
+ declare function transformRange(range: {
3442
+ start: number;
3443
+ end: number;
3444
+ }, changes: TextChange | readonly TextChange[]): {
3445
+ start: number;
3446
+ end: number;
3447
+ };
3448
+
3449
+ /**
3450
+ * DocumentEngine — BoxDrawer
3451
+ *
3452
+ * Drawer-style open/close animation for the `<details>` elements that
3453
+ * HTMLRenderer emits for `[box]` and `[spoilerbox]`. The box grows downwards
3454
+ * and progressively uncovers its content, which stays anchored under the
3455
+ * summary instead of popping in all at once.
3456
+ *
3457
+ * This lives in Quasar rather than in a consumer because Quasar is what emits
3458
+ * the `<details>` in the first place — the same reason `morphHTML` lives here.
3459
+ * Any host that renders Quasar output gets the behaviour by binding it once.
3460
+ *
3461
+ * Why not CSS: while a `<details>` is closed its content is not rendered, so
3462
+ * there is no height to interpolate between. `::details-content` combined with
3463
+ * `interpolate-size: allow-keywords` would solve it, but it is Chromium-only in
3464
+ * practice and hosts running on WebKit would silently lose the animation.
3465
+ * Measuring the real height and driving it with the Web Animations API works on
3466
+ * every engine.
3467
+ *
3468
+ * Heights are measured, not computed from summary + padding + borders, because
3469
+ * that arithmetic breaks as soon as a theme puts margins on the summary or the
3470
+ * boxes are nested. Toggling `open` and reading the rect yields the exact
3471
+ * height the element will settle at, whatever the stylesheet does.
3472
+ */
3473
+ interface BoxDrawerOptions {
3474
+ /** Length of a full open or close, in milliseconds. Partial travel is scaled down. */
3475
+ durationMs?: number;
3476
+ /** CSS easing function. */
3477
+ easing?: string;
3478
+ }
3479
+ /**
3480
+ * Animate one `<details>` to its opposite state, reversing cleanly when it is
3481
+ * already mid-animation.
3482
+ *
3483
+ * Exported so hosts that toggle boxes programmatically (a keyboard command, an
3484
+ * "expand all" action) go through the same animation as a click.
3485
+ */
3486
+ declare function toggleBoxWithDrawer(details: HTMLDetailsElement, options?: BoxDrawerOptions): void;
3487
+ /**
3488
+ * Intercept the native toggle of every box inside `root`.
3489
+ *
3490
+ * The listener is delegated rather than attached per box: hosts typically feed
3491
+ * the preview through `morphHTML` on every keystroke, and per-element listeners
3492
+ * would be torn down and re-attached continuously.
3493
+ *
3494
+ * @param root - container holding the rendered BBCode
3495
+ * @returns disposer that removes the listener
3496
+ */
3497
+ declare function bindBoxDrawer(root: HTMLElement, options?: BoxDrawerOptions): () => void;
3498
+
3499
+ /**
3500
+ * DocumentEngine — Visuals
3501
+ *
3502
+ * Catálogo de estilos visuales por plataforma para la previsualización
3503
+ * de BBCode. Cada plataforma puede tener su propia apariencia sin
3504
+ * modificar el renderer HTML.
3505
+ *
3506
+ * Uso:
3507
+ * // Importar como CSS directo (soportado por Next.js, Vite, Webpack):
3508
+ * import 'MiliastryPlatform/DocumentEngine/Visuals/osu.css'
3509
+ *
3510
+ * // O desde un componente React:
3511
+ * import 'MiliastryPlatform/DocumentEngine/Visuals/osu.css'
3512
+ * // Luego en el JSX: <div className="bbcode-preview" ...
3513
+ *
3514
+ * Para cambiar de tema visual, solo cambia el import del CSS.
3515
+ *
3516
+ * Temas disponibles:
3517
+ * - osu → Estilo visual inspirado en los foros de osu!
3518
+ * - miliastry → Estilo por defecto de Miliastry (próximamente)
3519
+ */
3520
+
3521
+ declare const visualThemes: readonly [{
3522
+ readonly id: "osu";
3523
+ readonly name: "osu! Forum Style";
3524
+ readonly description: "Estilo visual inspirado en los foros de osu!";
3525
+ readonly cssFile: "osu.css";
3526
+ }, {
3527
+ readonly id: "lyne";
3528
+ readonly name: "Lyne Style";
3529
+ readonly description: "Estilo visual cyberpunk con cortes a 45° inspirado en Lyne";
3530
+ readonly cssFile: "lyne.css";
3531
+ }];
3532
+ type VisualThemeId = (typeof visualThemes)[number]['id'];
3533
+
3534
+ /**
3535
+ * Quasar — Color Utilities
3536
+ *
3537
+ * Standalone color functions for gradient/grow/exporter interpolation.
3538
+ * No external dependencies — pure math.
3539
+ *
3540
+ * Ported from TextStudio/lib/color.ts (originally backed by culori).
3541
+ * These are the minimal subset needed for internal tag export.
3542
+ */
3543
+ type Easing = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';
3544
+
3545
+ /**
3546
+ * Quasar — Tree Transformers
3547
+ *
3548
+ * Apply effects (gradient, grow, rainbow, central, multi) across an entire RedNode tree.
3549
+ * These functions walk the tree, track global text position,
3550
+ * and wrap each text leaf in internal effect nodes
3551
+ * with position metadata so the TagRegistry handlers produce
3552
+ * correctly interpolated output.
3553
+ *
3554
+ * DESIGN:
3555
+ * Two-pass approach:
3556
+ * 1. Collect total text length (for normalization)
3557
+ * 2. Walk again, wrapping text nodes with effect nodes
3558
+ *
3559
+ * Non-text nodes (img, video, audio, code blocks) are preserved unchanged.
3560
+ * Structural nodes (box, quote, list) recurse into children.
3561
+ * Formatting nodes (b, i, u, color) recurse — effects go INSIDE formatting.
3562
+ *
3563
+ * Example:
3564
+ * Input: [box][b]Hello[/b] World[/box]
3565
+ * Output: [box][b][gradient][b]H[/b][gradient][b]e[/b]...[/b]
3566
+ * [gradient] W[/gradient]...[/gradient]
3567
+ *
3568
+ * Actually simpler — each text leaf gets its own gradient node with
3569
+ * globalOffset metadata, and the handler does the interpolation.
3570
+ */
3571
+
3572
+ interface GradientEffect {
3573
+ kind: 'gradient';
3574
+ colors: string[];
3575
+ unit?: 'character' | 'word' | 'line';
3576
+ easing?: Easing;
3577
+ }
3578
+ interface GrowEffect {
3579
+ kind: 'grow';
3580
+ min?: number;
3581
+ max?: number;
3582
+ cycles?: number;
3583
+ }
3584
+ interface RainbowEffect {
3585
+ kind: 'rainbow';
3586
+ saturation?: number;
3587
+ lightness?: number;
3588
+ spread?: number;
3589
+ offset?: number;
3590
+ }
3591
+ interface CentralGradientEffect {
3592
+ kind: 'central_gradient';
3593
+ colors: string[];
3594
+ unit?: 'character' | 'word' | 'line';
3595
+ easing?: Easing;
3596
+ }
3597
+ interface MultiGradientEffect {
3598
+ kind: 'multi_gradient';
3599
+ colors: string[];
3600
+ mode?: 'wave' | 'pulse';
3601
+ unit?: 'character' | 'word' | 'line';
3602
+ easing?: Easing;
3603
+ }
3604
+ type TreeEffect = GradientEffect | GrowEffect | RainbowEffect | CentralGradientEffect | MultiGradientEffect;
3605
+ /**
3606
+ * Count total plain text length across all text leaves in a tree.
3607
+ */
3608
+ declare function countTextLength(node: RedNode): number;
3609
+ /**
3610
+ * Apply a gradient effect across an entire RedNode tree.
3611
+ *
3612
+ * The gradient is computed across ALL text content in the tree,
3613
+ * so color transitions span across structural boundaries:
3614
+ *
3615
+ * [b]Hello[/b] World → [b][color=#FF0000]H[/color]...[/b][color=#00FF00]W[/color]...
3616
+ *
3617
+ * @param root The RedNode tree to transform
3618
+ * @param colors Array of hex colors to interpolate across
3619
+ * @param options.unit Split unit: 'character' (default), 'word', or 'line'
3620
+ * @param options.easing Easing function: 'linear' (default), 'easeIn', 'easeOut', 'easeInOut'
3621
+ * @returns A new RedNode tree with gradient applied
3622
+ */
3623
+ declare function applyGradient(root: RedNode, colors: string[], options?: {
3624
+ unit?: 'character' | 'word' | 'line';
3625
+ easing?: Easing;
3626
+ }): RedNode;
3627
+ /**
3628
+ * Apply a grow (size wave) effect across an entire RedNode tree.
3629
+ *
3630
+ * Size oscillates via sine wave across ALL text content:
3631
+ *
3632
+ * [b]Hello[/b] World → [b][size=90]H[/size]...[/b][size=160]W[/size]...
3633
+ *
3634
+ * @param root The RedNode tree to transform
3635
+ * @param options.min Minimum size % (default: 90)
3636
+ * @param options.max Maximum size % (default: 160)
3637
+ * @param options.cycles Number of sine cycles across text (default: 1)
3638
+ * @returns A new RedNode tree with grow applied
3639
+ */
3640
+ declare function applyGrow(root: RedNode, options?: {
3641
+ min?: number;
3642
+ max?: number;
3643
+ cycles?: number;
3644
+ }): RedNode;
3645
+ /**
3646
+ * Apply a rainbow effect across an entire RedNode tree.
3647
+ *
3648
+ * Hue rotates across ALL text content using HSL color space:
3649
+ *
3650
+ * [b]Hello[/b] World → [b][color=#FF0000]H[/color]...[/b][color=#00FF00]W[/color]...
3651
+ *
3652
+ * @param root The RedNode tree to transform
3653
+ * @param options.saturation Saturation 0-100 (default: 80)
3654
+ * @param options.lightness Lightness 0-100 (default: 60)
3655
+ * @param options.spread Hue spread in degrees (default: 300)
3656
+ * @param options.offset Starting hue offset in degrees (default: 0)
3657
+ * @returns A new RedNode tree with rainbow applied
3658
+ */
3659
+ declare function applyRainbow(root: RedNode, options?: {
3660
+ saturation?: number;
3661
+ lightness?: number;
3662
+ spread?: number;
3663
+ offset?: number;
3664
+ }): RedNode;
3665
+ /**
3666
+ * Apply a central gradient across an entire RedNode tree.
3667
+ *
3668
+ * Gradient radiates from center outward (mirrored):
3669
+ *
3670
+ * [b]Hello[/b] World → [b][color=#FF0000]H[/color]...[/b][color=#00FF00]W[/color]...
3671
+ *
3672
+ * @param root The RedNode tree to transform
3673
+ * @param colors Array of hex colors to interpolate across
3674
+ * @param options.unit Split unit: 'character' (default), 'word', or 'line'
3675
+ * @param options.easing Easing function: 'linear' (default), 'easeIn', 'easeOut', 'easeInOut'
3676
+ * @returns A new RedNode tree with central gradient applied
3677
+ */
3678
+ declare function applyCentralGradient(root: RedNode, colors: string[], options?: {
3679
+ unit?: 'character' | 'word' | 'line';
3680
+ easing?: Easing;
3681
+ }): RedNode;
3682
+ /**
3683
+ * Apply a multi gradient (wave or pulse) across an entire RedNode tree.
3684
+ *
3685
+ * Wave mode: sine wave pattern across colors.
3686
+ * Pulse mode: repeating gradient segments.
3687
+ *
3688
+ * @param root The RedNode tree to transform
3689
+ * @param colors Array of hex colors to interpolate across
3690
+ * @param options.mode 'wave' (default) or 'pulse'
3691
+ * @param options.unit Split unit: 'character' (default), 'word', or 'line'
3692
+ * @param options.easing Easing function: 'linear' (default), 'easeIn', 'easeOut', 'easeInOut'
3693
+ * @returns A new RedNode tree with multi gradient applied
3694
+ */
3695
+ declare function applyMultiGradient(root: RedNode, colors: string[], options?: {
3696
+ mode?: 'wave' | 'pulse';
3697
+ unit?: 'character' | 'word' | 'line';
3698
+ easing?: Easing;
3699
+ }): RedNode;
3700
+ /**
3701
+ * Apply any supported effect across an entire RedNode tree.
3702
+ */
3703
+ declare function applyEffect(root: RedNode, effect: TreeEffect): RedNode;
3704
+
3705
+ interface DomToSVGOptions {
3706
+ backgroundColor?: string;
3707
+ includeBackground?: boolean;
3708
+ /** Scale factor applied to all coordinates (default 1) */
3709
+ scale?: number;
3710
+ }
3711
+ interface SVGLayerInfo {
3712
+ id: string;
3713
+ tag: string;
3714
+ depth: number;
3715
+ x: number;
3716
+ y: number;
3717
+ width: number;
3718
+ height: number;
3719
+ type: 'image' | 'iframe' | 'audio' | 'background' | 'border' | 'group' | 'text';
3720
+ text?: string;
3721
+ }
3722
+ interface DomToSVGResult {
3723
+ svg: string;
3724
+ width: number;
3725
+ height: number;
3726
+ layerCount: number;
3727
+ layers: SVGLayerInfo[];
3728
+ }
3729
+ declare function domToSVG(root: HTMLElement, options?: DomToSVGOptions): string;
3730
+ declare function domToSVGResult(root: HTMLElement, options?: DomToSVGOptions): DomToSVGResult;
3731
+
3732
+ interface TransformResult {
3733
+ document: DocumentModel;
3734
+ transaction?: Transaction;
3735
+ }
3736
+ interface Transformer {
3737
+ /**
3738
+ * Transforms a DocumentModel.
3739
+ * Should ideally return a Transaction that contains the operations applied.
3740
+ */
3741
+ transform(document: DocumentModel): TransformResult;
3742
+ }
3743
+
3744
+ declare class ASTOptimizer implements Transformer {
3745
+ transform(document: DocumentModel): TransformResult;
3746
+ private applyRules;
3747
+ private static VOID_TAGS;
3748
+ private static UNMERGEABLE;
3749
+ private isEmptyNode;
3750
+ /**
3751
+ * Two `paragraph` siblings with *nothing* between them.
3752
+ *
3753
+ * `paragraph` is in {@link UNMERGEABLE} for good reason — paragraphs are block
3754
+ * separators and collapsing them normally would change the document. But this
3755
+ * particular shape is one the optimizer creates itself and the parser never
3756
+ * does: paragraphs exist because a block element split the inline content, so
3757
+ * when a rule deletes that block (an empty `[notice]`, say) the two halves are
3758
+ * left as adjacent paragraphs that no longer have anything separating them.
3759
+ *
3760
+ * That was the whole non-idempotence bug. Exporting such a tree emits no
3761
+ * separator, so re-parsing folds the two paragraphs back into one and a second
3762
+ * optimizer pass finds more work to do. Merging here makes the tree agree with
3763
+ * its own round-trip.
3764
+ *
3765
+ * Verified empirically before relying on it: across 202.764 nodes (both real
3766
+ * fixtures plus 20.000 seeded random documents) the parser produced this shape
3767
+ * **zero** times, so the rule can only ever fire on optimizer-made trees.
3768
+ *
3769
+ * `spaces` must be empty — paragraphs separated by real spacing or blank lines
3770
+ * are a genuine authored break and must survive.
3771
+ */
3772
+ private isOrphanedParagraphPair;
3773
+ private canMerge;
3774
+ private getRank;
3775
+ }
3776
+
3777
+ interface SineWaveOptions {
3778
+ minSize: number;
3779
+ maxSize: number;
3780
+ frequency: number;
3781
+ step: 'char' | 'word';
3782
+ selectionRange?: {
3783
+ start: number;
3784
+ end: number;
3785
+ };
3786
+ }
3787
+ declare class SineWaveTransformer implements Transformer {
3788
+ private options;
3789
+ constructor(options?: Partial<SineWaveOptions>);
3790
+ transform(document: DocumentModel): TransformResult;
3791
+ }
3792
+
3793
+ interface RainbowOptions {
3794
+ saturation: number;
3795
+ lightness: number;
3796
+ spread: number;
3797
+ offset: number;
3798
+ selectionRange?: {
3799
+ start: number;
3800
+ end: number;
3801
+ };
3802
+ }
3803
+ declare class RainbowTransformer implements Transformer {
3804
+ private options;
3805
+ constructor(options?: Partial<RainbowOptions>);
3806
+ transform(document: DocumentModel): TransformResult;
3807
+ }
3808
+
3809
+ interface GradientOptions {
3810
+ from: string;
3811
+ to: string;
3812
+ unit: 'char' | 'word';
3813
+ easing: Easing;
3814
+ selectionRange?: {
3815
+ start: number;
3816
+ end: number;
3817
+ };
3818
+ }
3819
+ declare class GradientTransformer implements Transformer {
3820
+ private options;
3821
+ constructor(options?: Partial<GradientOptions>);
3822
+ transform(document: DocumentModel): TransformResult;
3823
+ }
3824
+
3825
+ interface GrowOptions {
3826
+ minSize: number;
3827
+ maxSize: number;
3828
+ cycles: number;
3829
+ selectionRange?: {
3830
+ start: number;
3831
+ end: number;
3832
+ };
3833
+ }
3834
+ declare class GrowTransformer implements Transformer {
3835
+ private options;
3836
+ constructor(options?: Partial<GrowOptions>);
3837
+ transform(document: DocumentModel): TransformResult;
3838
+ }
3839
+
3840
+ interface MarkdownDocumentModelOptions extends DocumentModelOptions {
3841
+ source?: string;
3842
+ language?: string;
3843
+ }
3844
+ declare class MarkdownDocumentModel extends DocumentModel {
3845
+ constructor(options?: MarkdownDocumentModelOptions);
3846
+ static fromMarkdown(source: string): MarkdownDocumentModel;
3847
+ protected parseToGreen(source: string): GreenNode;
3848
+ protected buildRedFromGreen(green: GreenNode): RedNode;
3849
+ applyTextUpdate(newSource: string): void;
3850
+ applyChange(change: any): void;
3851
+ }
3852
+
3853
+ /**
3854
+ * DocumentEngine — Markdown AST Nodes
3855
+ */
3856
+ type MarkdownNode = MarkdownDocument | MarkdownParagraph | MarkdownHeading | MarkdownText | MarkdownCodeBlock | MarkdownCodeInline | MarkdownStrong | MarkdownEmphasis | MarkdownLink | MarkdownImage | MarkdownList | MarkdownListItem | MarkdownBlockquote | MarkdownSpoiler | MarkdownNotice | MarkdownSpacing | MarkdownEmptyLine;
3857
+ interface MarkdownDocument {
3858
+ type: 'document';
3859
+ children: MarkdownNode[];
3860
+ }
3861
+ interface MarkdownParagraph {
3862
+ type: 'paragraph';
3863
+ children: MarkdownNode[];
3864
+ }
3865
+ interface MarkdownHeading {
3866
+ type: 'heading';
3867
+ level: number;
3868
+ children: MarkdownNode[];
3869
+ }
3870
+ interface MarkdownText {
3871
+ type: 'text';
3872
+ value: string;
3873
+ }
3874
+ interface MarkdownCodeBlock {
3875
+ type: 'code_block';
3876
+ lang: string;
3877
+ value: string;
3878
+ }
3879
+ interface MarkdownCodeInline {
3880
+ type: 'code_inline';
3881
+ value: string;
3882
+ }
3883
+ interface MarkdownStrong {
3884
+ type: 'strong';
3885
+ children: MarkdownNode[];
3886
+ }
3887
+ interface MarkdownEmphasis {
3888
+ type: 'emphasis';
3889
+ children: MarkdownNode[];
3890
+ }
3891
+ interface MarkdownLink {
3892
+ type: 'link';
3893
+ url: string;
3894
+ children: MarkdownNode[];
3895
+ }
3896
+ interface MarkdownImage {
3897
+ type: 'image';
3898
+ url: string;
3899
+ alt: string;
3900
+ }
3901
+ interface MarkdownList {
3902
+ type: 'list';
3903
+ ordered: boolean;
3904
+ children: MarkdownListItem[];
3905
+ }
3906
+ interface MarkdownListItem {
3907
+ type: 'list_item';
3908
+ children: MarkdownNode[];
3909
+ }
3910
+ interface MarkdownBlockquote {
3911
+ type: 'blockquote';
3912
+ children: MarkdownNode[];
3913
+ }
3914
+ /** `>!` spoiler block — produced by MarkdownParser, maps to BBCode [spoiler]. */
3915
+ interface MarkdownSpoiler {
3916
+ type: 'spoiler';
3917
+ children: MarkdownNode[];
3918
+ }
3919
+ /** `> [!NOTE]` callout — produced by MarkdownParser, maps to BBCode [notice]. */
3920
+ interface MarkdownNotice {
3921
+ type: 'notice';
3922
+ children: MarkdownNode[];
3923
+ }
3924
+ interface MarkdownSpacing {
3925
+ type: 'spacing';
3926
+ }
3927
+ interface MarkdownEmptyLine {
3928
+ type: 'empty_line';
3929
+ }
3930
+
3931
+ declare function markdownAstToGreenTree(root: MarkdownNode): GreenNode;
3932
+ declare function markdownAstToRedTree(root: MarkdownNode): RedNode;
3933
+
3934
+ interface HTMLDocumentModelOptions extends DocumentModelOptions {
3935
+ source?: string;
3936
+ language?: string;
3937
+ }
3938
+ declare class HTMLDocumentModel extends DocumentModel {
3939
+ constructor(options?: HTMLDocumentModelOptions);
3940
+ static fromHTML(source: string): HTMLDocumentModel;
3941
+ protected parseToGreen(source: string): GreenNode;
3942
+ protected buildRedFromGreen(green: GreenNode): RedNode;
3943
+ }
3944
+
3945
+ declare function htmlStringToGreenTree(html: string): GreenNode;
3946
+
3947
+ /**
3948
+ * Quasar Analysis Framework — Pipeline Context
3949
+ *
3950
+ * PipelineContext is **read-only by contract**. It is built *before*
3951
+ * the pipeline starts and MUST NOT be modified by any pass.
3952
+ *
3953
+ * If a pass needs to make information available to downstream passes it
3954
+ * should emit a Contribution rather than mutating the context.
3955
+ *
3956
+ * @immutable
3957
+ */
3958
+ declare const PipelineMode: {
3959
+ readonly Interactive: "interactive";
3960
+ readonly Batch: "batch";
3961
+ readonly Import: "import";
3962
+ };
3963
+ type PipelineMode = (typeof PipelineMode)[keyof typeof PipelineMode];
3964
+ declare const ExportTarget: {
3965
+ readonly Osu: "osu";
3966
+ readonly Miliastry: "miliastry";
3967
+ readonly HTML: "html";
3968
+ readonly Markdown: "markdown";
3969
+ };
3970
+ type ExportTarget = (typeof ExportTarget)[keyof typeof ExportTarget];
3971
+ interface PipelineContext {
3972
+ /** Where this pipeline run originates from. */
3973
+ readonly mode: PipelineMode;
3974
+ /** The format we intend to export to (influences decisions). */
3975
+ readonly target: ExportTarget;
3976
+ /** Feature flags that may enable/disable certain behaviours. */
3977
+ readonly featureFlags: Readonly<Record<string, boolean>>;
3978
+ /** Free-form metadata provided by the caller (e.g. selection range). */
3979
+ readonly metadata: Readonly<Record<string, unknown>>;
3980
+ }
3981
+
3982
+ /**
3983
+ * Quasar Analysis Framework — Contribution Types
3984
+ *
3985
+ * Every AnalyzerPass produces an array of Contributions. A Contribution
3986
+ * is a discriminated union so that downstream consumers (DecisionPass,
3987
+ * AnalysisReport consumers, debug UI) can switch on `kind` and get
3988
+ * perfect TypeScript narrowing.
3989
+ *
3990
+ * @see AnalysisReport
3991
+ */
3992
+ declare const ContributionKind: {
3993
+ readonly Semantic: "semantic";
3994
+ readonly Diagnostic: "diagnostic";
3995
+ readonly Optimization: "optimization";
3996
+ readonly Metrics: "metrics";
3997
+ };
3998
+ type ContributionKind = (typeof ContributionKind)[keyof typeof ContributionKind];
3999
+ /**
4000
+ * A semantic pattern recognised in the tree (gradient, wave, rainbow,…).
4001
+ * These describe *meaning* of the document, not just surface syntax.
4002
+ */
4003
+ interface SemanticContribution {
4004
+ readonly kind: typeof ContributionKind.Semantic;
4005
+ readonly label: string;
4006
+ readonly confidence: number;
4007
+ readonly range: {
4008
+ start: number;
4009
+ end: number;
4010
+ };
4011
+ readonly metadata: Record<string, unknown>;
4012
+ /** Human-readable description (optional). */
4013
+ readonly description?: string;
4014
+ }
4015
+ /**
4016
+ * A diagnostic (warning, error, info) attached to a range in the tree.
4017
+ */
4018
+ interface DiagnosticContribution {
4019
+ readonly kind: typeof ContributionKind.Diagnostic;
4020
+ readonly severity: 'error' | 'warning' | 'info' | 'hint';
4021
+ readonly message: string;
4022
+ readonly range?: {
4023
+ start: number;
4024
+ end: number;
4025
+ };
4026
+ readonly code?: string;
4027
+ }
4028
+ /**
4029
+ * An optimisation opportunity detected in the tree (mergeable colours,
4030
+ * redundant wrapping, empty tags, …).
4031
+ */
4032
+ interface OptimizationContribution {
4033
+ readonly kind: typeof ContributionKind.Optimization;
4034
+ readonly label: string;
4035
+ readonly description: string;
4036
+ readonly estimatedImprovement?: string;
4037
+ readonly range: {
4038
+ start: number;
4039
+ end: number;
4040
+ };
4041
+ }
4042
+ /**
4043
+ * Aggregate metrics about the tree (character count, node depth, …).
4044
+ */
4045
+ interface MetricsContribution {
4046
+ readonly kind: typeof ContributionKind.Metrics;
4047
+ readonly metrics: Record<string, number | string>;
4048
+ }
4049
+ type Contribution = SemanticContribution | DiagnosticContribution | OptimizationContribution | MetricsContribution;
4050
+
4051
+ /**
4052
+ * Quasar Analysis Framework — Analysis Report
4053
+ *
4054
+ * Once all AnalyzerPasses have run, their Contributions are collected
4055
+ * into an immutable AnalysisReport. This report is the sole input to
4056
+ * the DecisionPass(es).
4057
+ *
4058
+ * We deliberately keep the aggregator simple for now — just an array
4059
+ * of contributions. As more pass types emerge we may introduce a
4060
+ * structured Aggregator that indexes/categorises contributions.
4061
+ *
4062
+ * @see DecisionPass
4063
+ */
4064
+
4065
+ interface AnalysisReport {
4066
+ /** All contributions gathered from every AnalyzerPass. */
4067
+ readonly contributions: readonly Contribution[];
4068
+ /** Total number of analysis passes that ran. */
4069
+ readonly passCount: number;
4070
+ /** Wall-clock time spent in analysis (ms). */
4071
+ readonly elapsedMs: number;
4072
+ }
4073
+
4074
+ /**
4075
+ * Quasar Analysis Framework — Pass Contracts
4076
+ *
4077
+ * Each pass in the analysis/transform pipeline implements one of these
4078
+ * specialized interfaces. The three kinds mirror LLVM's pass structure:
4079
+ *
4080
+ * Analysis — observe the tree, produce Contributions (never mutate)
4081
+ * Decision — consume an AnalysisReport, produce a TransformationPlan
4082
+ * Transform — mutate the tree according to a plan
4083
+ *
4084
+ * @see ARCHITECTURE.md (Analysis Framework)
4085
+ */
4086
+
4087
+ /** Every pass has an identity string and belongs to a stage. */
4088
+ interface Pass {
4089
+ readonly id: string;
4090
+ }
4091
+ /**
4092
+ * An AnalyzerPass observes the Green Tree and produces one or more
4093
+ * Contributions. It MUST NOT mutate the tree or the context.
4094
+ */
4095
+ interface AnalyzerPass extends Pass {
4096
+ run(tree: GreenNode, context: PipelineContext): Contribution[];
4097
+ }
4098
+ /**
4099
+ * A DecisionPass consumes an AnalysisReport (which itself is the
4100
+ * aggregated output of all AnalyzerPasses) and produces a
4101
+ * TransformationPlan that describes *intended* tree mutations.
4102
+ */
4103
+ interface DecisionPass extends Pass {
4104
+ run(report: AnalysisReport, context: PipelineContext): TransformationPlan;
4105
+ }
4106
+ /**
4107
+ * A TransformPass receives the current Green Tree together with a
4108
+ * TransformationPlan and returns a **new** Green Tree. It MUST NOT
4109
+ * mutate the original tree — immutability guarantees deterministic
4110
+ * replay and debugging.
4111
+ */
4112
+ interface TransformPass extends Pass {
4113
+ run(tree: GreenNode, plan: TransformationPlan, context: PipelineContext): GreenNode;
4114
+ }
4115
+ /**
4116
+ * A single action described by a TransformationPlan.
4117
+ * `kind` tells the TransformPass what to do; `payload` carries
4118
+ * per-kind parameters.
4119
+ */
4120
+ interface TransformAction {
4121
+ readonly kind: string;
4122
+ readonly payload: Record<string, unknown>;
4123
+ }
4124
+ /**
4125
+ * Ordered set of actions that a DecisionPass produces.
4126
+ */
4127
+ interface TransformationPlan {
4128
+ readonly actions: readonly TransformAction[];
4129
+ }
4130
+
4131
+ /**
4132
+ * Quasar Analysis Framework — Pipeline Stage
4133
+ *
4134
+ * Passes are grouped into stages that execute in order:
4135
+ *
4136
+ * 1. Analysis — observe the tree, produce Contributions
4137
+ * 2. Decision — consume the AnalysisReport, produce a TransformationPlan
4138
+ * 3. Transform — apply the plan to produce a new Green Tree
4139
+ *
4140
+ * @see Pass
4141
+ */
4142
+ declare const PipelineStage: {
4143
+ readonly Analysis: "analysis";
4144
+ readonly Decision: "decision";
4145
+ readonly Transform: "transform";
4146
+ };
4147
+ type PipelineStage = (typeof PipelineStage)[keyof typeof PipelineStage];
4148
+
4149
+ /**
4150
+ * Quasar Analysis Framework — Pipeline
4151
+ *
4152
+ * The Pipeline is itself a Pass — it can be composed inside larger
4153
+ * pipelines. Execution is strictly ordered:
4154
+ *
4155
+ * 1. All analysis passes run, collecting contributions.
4156
+ * 2. The aggregated AnalysisReport is fed to each decision pass.
4157
+ * 3. Each transform pass receives the current tree + plan and
4158
+ * returns a new tree (never mutating the original).
4159
+ *
4160
+ * @example
4161
+ * ```ts
4162
+ * const pipeline = new Pipeline([
4163
+ * { stage: 'analysis', pass: new CharacterCountAnalyzer() },
4164
+ * ])
4165
+ * const report = pipeline.run(tree, context)
4166
+ * ```
4167
+ */
4168
+
4169
+ interface StageEntry {
4170
+ readonly stage: PipelineStage;
4171
+ readonly pass: AnalyzerPass | DecisionPass | TransformPass;
4172
+ }
4173
+ interface PipelineResult {
4174
+ /** The final (possibly transformed) Green Tree. */
4175
+ readonly tree: GreenNode;
4176
+ /** The aggregated analysis report. */
4177
+ readonly report: AnalysisReport;
4178
+ }
4179
+ declare class Pipeline {
4180
+ private readonly stages;
4181
+ constructor(stages: readonly StageEntry[]);
4182
+ /**
4183
+ * Execute the full pipeline against `tree` with the given `context`.
4184
+ *
4185
+ * @returns The final tree and analysis report.
4186
+ */
4187
+ run(tree: GreenNode, context: PipelineContext): PipelineResult;
4188
+ /**
4189
+ * Use PipelineBuilder to construct a Pipeline:
4190
+ *
4191
+ * ```ts
4192
+ * import { PipelineBuilder } from './PipelineBuilder'
4193
+ * const pipeline = new PipelineBuilder()
4194
+ * .analysis(new CharacterCountAnalyzer())
4195
+ * .build()
4196
+ * ```
4197
+ */
4198
+ private runAnalysis;
4199
+ private runDecision;
4200
+ private runTransform;
4201
+ private buildReport;
4202
+ }
4203
+
4204
+ /**
4205
+ * Quasar Analysis Framework — Pipeline Builder
4206
+ *
4207
+ * Provides a fluent API for assembling a Pipeline from individually
4208
+ * registered passes. Each stage (analysis / decision / transform)
4209
+ * has its own registration method so TypeScript catches misuse at
4210
+ * compile time.
4211
+ *
4212
+ * @example
4213
+ * ```ts
4214
+ * const pipeline = Pipeline
4215
+ * .builder()
4216
+ * .analysis(new CharacterCountAnalyzer())
4217
+ * .decision(new DefaultDecision())
4218
+ * .transform(new CollapseGradientTransform())
4219
+ * .build()
4220
+ * ```
4221
+ */
4222
+
4223
+ declare class PipelineBuilder {
4224
+ private readonly passes;
4225
+ /** Register one or more AnalyzerPasses (observation stage). */
4226
+ analysis(...passes: AnalyzerPass[]): this;
4227
+ /** Register one or more DecisionPasses (planning stage). */
4228
+ decision(...passes: DecisionPass[]): this;
4229
+ /** Register one or more TransformPasses (mutation stage). */
4230
+ transform(...passes: TransformPass[]): this;
4231
+ /** Build the Pipeline from the registered passes. */
4232
+ build(): Pipeline;
4233
+ }
4234
+
4235
+ /**
4236
+ * Quasar Analysis Framework — Character Count Analyzer (Proof of Concept)
4237
+ *
4238
+ * The simplest possible AnalyzerPass: it walks the Green Tree and counts
4239
+ * text characters. This validates that the full pipeline works:
4240
+ *
4241
+ * Green Tree → Analyzer → Contribution → Aggregated Report
4242
+ *
4243
+ * No algorithms, no colours, no decisions — just validating the pipe.
4244
+ */
4245
+
4246
+ declare class CharacterCountAnalyzer implements AnalyzerPass {
4247
+ readonly id = "character-count";
4248
+ run(tree: GreenNode, _context: PipelineContext): Contribution[];
4249
+ private countChars;
4250
+ }
4251
+
4252
+ /**
4253
+ * Quasar Analysis Framework — Mergeable Color Analyzer
4254
+ *
4255
+ * Detects sequences of consecutive [color=#HEX] wrappers where ALL
4256
+ * adjacent colours are identical. These are optimisation opportunities:
4257
+ * the user could merge them into a single `[color]` tag.
4258
+ *
4259
+ * Example:
4260
+ * [color=#FF0000]H[/color][color=#FF0000]e[/color] → mergeable
4261
+ * [color=#FF0000]H[/color][color=#EE1100]e[/color] → NOT mergeable
4262
+ *
4263
+ * @see OptimizationContribution
4264
+ */
4265
+
4266
+ declare class MergeableColorAnalyzer implements AnalyzerPass {
4267
+ readonly id = "mergeable-color";
4268
+ run(tree: GreenNode, _context: PipelineContext): Contribution[];
4269
+ /**
4270
+ * Walk the tree and find consecutive color nodes with identical colours.
4271
+ */
4272
+ private findMergeableColorSequences;
4273
+ private emitMergeable;
4274
+ }
4275
+
4276
+ /**
4277
+ * Quasar Analysis Framework — Gradient Analyzer (v2)
4278
+ *
4279
+ * Detects sequences of [color=#HEX] tags that form a gradient pattern
4280
+ * and reports them as SemanticContributions with a confidence score.
4281
+ *
4282
+ * ## Refinements over v1
4283
+ *
4284
+ * 1. **OKLab color space** — Uses OKLab (Bottosson 2020) for perceptual
4285
+ * colour distance instead of RGB. Two colours with small OKLab distance
4286
+ * look nearly identical to the human eye, making gradient detection
4287
+ * much more accurate.
4288
+ *
4289
+ * 2. **Change-point detection** — Identifies gradient *stops* even when
4290
+ * plateaus (runs of identical colour) exist, so a sequence like
4291
+ * RRRR→G→BBBB is correctly parsed as a 3-stop gradient rather than
4292
+ * a failed linear interpolation.
4293
+ *
4294
+ * 3. **Sharper sigmoid (k=6)** — Better separation between "likely
4295
+ * gradient" and "maybe gradient" at the decision thresholds.
4296
+ *
4297
+ * ## Confidence features (weights sum to 1.0)
4298
+ *
4299
+ * - uniformPerceptualSpacing (+0.30)
4300
+ * - contiguousWrappers (+0.25)
4301
+ * - noFormattingBreaks (+0.15)
4302
+ * - monotonicProgression (+0.20)
4303
+ * - lowPerceptualError (+0.10)
4304
+ *
4305
+ * @see SemanticContribution
4306
+ */
4307
+
4308
+ interface GradientModel {
4309
+ readonly colors: string[];
4310
+ readonly easing: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';
4311
+ readonly stops: GradientStop[];
4312
+ readonly rangeStart: number;
4313
+ readonly rangeEnd: number;
4314
+ readonly charCount: number;
4315
+ readonly diagnostics: GradientDiagnostics;
4316
+ }
4317
+ interface GradientStop {
4318
+ /** Hex colour at this stop */
4319
+ readonly color: string;
4320
+ /** Normalised position 0-1 */
4321
+ readonly position: number;
4322
+ }
4323
+ interface GradientDiagnostics {
4324
+ readonly uniformSpacing: boolean;
4325
+ readonly monotonic: boolean;
4326
+ readonly plateauCount: number;
4327
+ readonly maxPerceptualError: number;
4328
+ readonly stopCount: number;
4329
+ readonly featureScores: Readonly<Record<string, number>>;
4330
+ }
4331
+ declare class GradientAnalyzer implements AnalyzerPass {
4332
+ readonly id = "gradient-analyzer";
4333
+ run(tree: GreenNode, _context: PipelineContext): Contribution[];
4334
+ private findGradients;
4335
+ /**
4336
+ * Detect gradient stops from a sequence of hex colours, handling plateaus.
4337
+ *
4338
+ * Algorithm: Scan for "significant changes" in perceptual distance.
4339
+ * Wherever the cumulative perceptual distance from the last stop exceeds a
4340
+ * threshold, a new stop is recorded. This naturally skips over plateaus.
4341
+ */
4342
+ private detectStops;
4343
+ private buildDiagnostics;
4344
+ private calculateRawScore;
4345
+ }
4346
+
4347
+ /**
4348
+ * Quasar Analysis Framework — Rainbow Analyzer
4349
+ *
4350
+ * Detects sequences of [color=#HEX] tags where the HSL hue rotates
4351
+ * continuously while saturation and lightness remain approximately
4352
+ * constant — the classic "rainbow" pattern.
4353
+ *
4354
+ * ## Confidence features
4355
+ *
4356
+ * - hueRotation (+0.35) — hue changes monotonically (increasing or decreasing)
4357
+ * - stableSaturation (+0.20) — saturation stays within a narrow band
4358
+ * - stableLightness (+0.20) — lightness stays within a narrow band
4359
+ * - contiguousWrappers (+0.15) — no gaps in the colour sequence
4360
+ * - noFormattingBreaks (+0.10) — no formatting tags between colours
4361
+ *
4362
+ * @see SemanticContribution
4363
+ */
4364
+
4365
+ interface RainbowModel {
4366
+ readonly colors: string[];
4367
+ readonly hueStart: number;
4368
+ readonly hueEnd: number;
4369
+ readonly avgSaturation: number;
4370
+ readonly avgLightness: number;
4371
+ readonly rangeStart: number;
4372
+ readonly rangeEnd: number;
4373
+ readonly charCount: number;
4374
+ readonly diagnostics: RainbowDiagnostics;
4375
+ }
4376
+ interface RainbowDiagnostics {
4377
+ readonly hueDelta: number;
4378
+ readonly saturationDeviation: number;
4379
+ readonly lightnessDeviation: number;
4380
+ readonly isContinuous: boolean;
4381
+ readonly featureScores: Readonly<Record<string, number>>;
4382
+ }
4383
+ declare class RainbowAnalyzer implements AnalyzerPass {
4384
+ readonly id = "rainbow-analyzer";
4385
+ run(tree: GreenNode, _context: PipelineContext): Contribution[];
4386
+ private findRainbows;
4387
+ private extractHSL;
4388
+ private analyzeRainbow;
4389
+ private calculateRawScore;
4390
+ }
4391
+
4392
+ /**
4393
+ * Quasar Analysis Framework — Wave (Size Oscillation) Analyzer
4394
+ *
4395
+ * Detects sequences of [size=N] tags where the sizes follow a sinusoidal
4396
+ * (or other periodic) oscillation pattern — the classic "wave/grow" effect.
4397
+ *
4398
+ * ## Confidence features
4399
+ *
4400
+ * - periodicPattern (+0.35) — sizes follow a clear periodic pattern
4401
+ * - boundedRange (+0.20) — sizes stay within a plausible min-max range
4402
+ * - smoothTransitions (+0.20) — adjacent size differences are small
4403
+ * - contiguousWrappers (+0.15) — no gaps in the size sequence
4404
+ * - noFormattingBreaks (+0.10) — no formatting tags between size nodes
4405
+ *
4406
+ * @see SemanticContribution
4407
+ */
4408
+
4409
+ interface WaveModel {
4410
+ readonly sizes: number[];
4411
+ readonly minSize: number;
4412
+ readonly maxSize: number;
4413
+ readonly estimatedFrequency: number;
4414
+ readonly rangeStart: number;
4415
+ readonly rangeEnd: number;
4416
+ readonly charCount: number;
4417
+ readonly diagnostics: WaveDiagnostics;
4418
+ }
4419
+ interface WaveDiagnostics {
4420
+ readonly periodicityScore: number;
4421
+ readonly hasSymmetry: boolean;
4422
+ readonly isBounded: boolean;
4423
+ readonly transitionSmoothness: number;
4424
+ readonly featureScores: Readonly<Record<string, number>>;
4425
+ }
4426
+ declare class WaveAnalyzer implements AnalyzerPass {
4427
+ readonly id = "wave-analyzer";
4428
+ run(tree: GreenNode, _context: PipelineContext): Contribution[];
4429
+ private findWaves;
4430
+ private analyzeWave;
4431
+ private calculateRawScore;
4432
+ }
4433
+
4434
+ /**
4435
+ * Quasar Analysis Framework — Default Decision
4436
+ *
4437
+ * Consumes an AnalysisReport and produces a TransformationPlan.
4438
+ * The decision logic depends on the PipelineContext:
4439
+ *
4440
+ * - target === 'miliastry':
4441
+ * Aggressively collapses detected gradients (confidence ≥ 0.6).
4442
+ * - target === 'osu':
4443
+ * Only collapses very high-confidence gradients (confidence ≥ 0.95).
4444
+ * - mode === 'batch' || mode === 'import':
4445
+ * Uses the OSU threshold by default.
4446
+ *
4447
+ * Optimisation opportunities (mergeable colours) are always included
4448
+ * regardless of target, but only when the count exceeds 1.
4449
+ *
4450
+ * @see DecisionPass
4451
+ * @see TransformationPlan
4452
+ */
4453
+
4454
+ declare class DefaultDecision implements DecisionPass {
4455
+ readonly id = "default-decision";
4456
+ run(report: AnalysisReport, context: PipelineContext): TransformationPlan;
4457
+ /** Map contribution label → transform action kind */
4458
+ private labelToActionKind;
4459
+ private handleSemantic;
4460
+ private handleOptimization;
4461
+ }
4462
+
4463
+ /**
4464
+ * Quasar Analysis Framework — Collapse Gradient Transform
4465
+ *
4466
+ * Applies `collapse-gradient` actions from a TransformationPlan.
4467
+ * Each action targets a sequence of adjacent [color] nodes and replaces
4468
+ * them with a single [gradient] node containing all text children.
4469
+ *
4470
+ * The transform operates on the Green Tree and returns a new tree —
4471
+ * it NEVER mutates the original.
4472
+ *
4473
+ * @example
4474
+ * Before:
4475
+ * [color=#FF0000]H[/color][color=#EE1100]e[/color][color=#DD2200]l[/color]
4476
+ * After:
4477
+ * [gradient=#FF0000,#EE1100,#DD2200]Hello[/gradient]
4478
+ *
4479
+ * @see TransformPass
4480
+ */
4481
+
4482
+ declare class CollapseGradientTransform implements TransformPass {
4483
+ readonly id = "collapse-gradient";
4484
+ run(tree: GreenNode, plan: TransformationPlan, _context: PipelineContext): GreenNode;
4485
+ /**
4486
+ * Walk the tree and collapse any sequences that fall within our ranges.
4487
+ */
4488
+ private transformNode;
4489
+ /**
4490
+ * Rebuild a node with the same kind/text/range but new children.
4491
+ */
4492
+ private rebuildNode;
4493
+ /**
4494
+ * Extract hex colour from a color node's text.
4495
+ */
4496
+ private extractHex;
4497
+ }
4498
+
4499
+ /**
4500
+ * Quasar Analysis Framework — Rainbow Collapse Transform
4501
+ *
4502
+ * Applies `collapse-rainbow` actions from a TransformationPlan.
4503
+ * Replaces sequences of [color] nodes forming a hue-rotation pattern
4504
+ * with a single [rainbow] node containing all text children.
4505
+ *
4506
+ * @example
4507
+ * Before: [color=#FF0000]R[/color][color=#FFFF00]O[/color][color=#00FF00]Y[/color]
4508
+ * After: [rainbow]ROY[/rainbow]
4509
+ */
4510
+
4511
+ declare class RainbowCollapseTransform implements TransformPass {
4512
+ readonly id = "rainbow-collapse";
4513
+ run(tree: GreenNode, plan: TransformationPlan, _context: PipelineContext): GreenNode;
4514
+ private transformNode;
4515
+ }
4516
+
4517
+ /**
4518
+ * Quasar Analysis Framework — Wave Collapse Transform
4519
+ *
4520
+ * Applies `collapse-wave` actions from a TransformationPlan.
4521
+ * Replaces sequences of [size=N] nodes forming a sinusoidal pattern
4522
+ * with a single [grow] node containing all text children and the
4523
+ * detected size range in metadata.
4524
+ *
4525
+ * @example
4526
+ * Before: [size=90]A[/size][size=150]B[/size][size=90]C[/size]
4527
+ * After: [grow]ABC[/grow] (with metadata: min=90, max=150)
4528
+ */
4529
+
4530
+ declare class WaveCollapseTransform implements TransformPass {
4531
+ readonly id = "wave-collapse";
4532
+ run(tree: GreenNode, plan: TransformationPlan, _context: PipelineContext): GreenNode;
4533
+ private transformNode;
4534
+ }
4535
+
4536
+ /**
4537
+ * Quasar Analysis Framework — Merge Colors Transform
4538
+ *
4539
+ * Applies `merge-colors` actions from a TransformationPlan.
4540
+ * Each action targets a sequence of identical consecutive [color=#HEX]
4541
+ * nodes and merges them into a single `[color]` wrapper with combined text.
4542
+ *
4543
+ * The transform operates on the Green Tree and returns a new tree —
4544
+ * it NEVER mutates the original.
4545
+ *
4546
+ * @example
4547
+ * Before:
4548
+ * [color=#FF0000]H[/color][color=#FF0000]e[/color][color=#FF0000]l[/color]
4549
+ * After:
4550
+ * [color=#FF0000]Hel[/color]
4551
+ *
4552
+ * @see TransformPass
4553
+ */
4554
+
4555
+ declare class MergeColorsTransform implements TransformPass {
4556
+ readonly id = "merge-colors";
4557
+ run(tree: GreenNode, plan: TransformationPlan, _context: PipelineContext): GreenNode;
4558
+ private transformNode;
4559
+ private rebuildNode;
4560
+ private extractHex;
4561
+ }
4562
+
4563
+ export { ASTOptimizer, type AnalysisReport, type AnalyzeResult, type AnalyzerPass, type BBBlock, BBBlocksExporter, BBCODE_TAG_NAMES, type BBCodeDialect, BBCodeDocumentModel, type BBCodeDocumentModelOptions, BBCodeExporter, type BoxDrawerOptions, type BuildResult, type CentralGradientEffect, ChangeTracker, CharacterCountAnalyzer, CollapseGradientTransform, type Command, type CommandContext, CommandRegistry, type CommandResult, type Contribution, ContributionKind, type DecisionPass, DefaultDecision, DeleteNode, type Diagnostic, type DiagnosticContribution, type DiagnosticSeverity, type DiagnosticTag, type DiffKind, type DiffOperation, type DocumentChangeEvent, type DocumentEvent, DocumentEventBus, type DocumentEventHandler, DocumentModel, type DocumentNode, type DomToSVGOptions, type DomToSVGResult, type EditOperation, type ExportTarget$1 as ExportTarget, type FormatOptions, Formatter, GradientAnalyzer, type GradientDiagnostics, type GradientEffect, type GradientModel, type GradientOptions, type GradientStop, GradientTransformer, GreenNode, type GrowEffect, type GrowOptions, GrowTransformer, HTMLDocumentModel, HTMLRenderer, type HTMLRendererOptions, IncrementalParser, InsertText, JSONExporter, LYNE_ONLY_TAGS, Lexer, type LexerOptions, type LintResult, type LintRule, Linter, MILIASTRY_ONLY_TAGS, MarkdownDocumentModel, MarkdownExporter, type MatchResult, MergeColorsTransform, MergeNode, MergeableColorAnalyzer, type MetricsContribution, type MultiGradientEffect, type NodeAttributes, NodeFactory, type NodeKind, NodeMatcher, type NodeMetadata, type Operation, type OptimizationContribution, type Pass, Pipeline, PipelineBuilder, type PipelineContext, PipelineMode, type PipelineResult, PipelineStage, PluginAPI, type PluginContribution, type PluginManifest, PluginRegistry, type Query, QueryEngine, type QueryMatch, type QueryResult, RainbowAnalyzer, RainbowCollapseTransform, type RainbowDiagnostics, type RainbowEffect, type RainbowModel, type RainbowOptions, RainbowTransformer, RedNode, RedNodeStore, type Reference, type RenderNode, RenderPipeline, RenderTree, type RenderVariant, type SVGLayerInfo, SVGRenderer, SemanticAnalyzer, type SemanticContribution, type SineWaveOptions, SineWaveTransformer, type SourceRange, SplitNode, type SymbolInfo, type SymbolKind, type SymbolSearchResult, SymbolTable, type TagDefinition, type TagHandler, TagRegistry, type TextChange, TiptapExporter, type Token, type TokenKind, Transaction, type TransformAction, type TransformBias, type TransformPass, type TransformationPlan, type Transformer, TreeBuilder, TreeDiffer, type TreeEffect, type Trivia, type TriviaKind, type UIBBBlock, type UndoEntry, UndoManager, Visitor, type VisitorContext, type VisualThemeId, WaveAnalyzer, WaveCollapseTransform, type WaveDiagnostics, type WaveModel, WrapInTag, applyCentralGradient, applyEffect, applyGradient, applyGrow, applyMultiGradient, applyRainbow, bbBlockToGreenNode, bbBlocksToGreenTree, bbBlocksToRedTree, bindBoxDrawer, countTextLength, domToSVG, domToSVGResult, getBBCodeTagNames, greenLeaf, greenNode, greenToRedNode, htmlStringToGreenTree, isBlockKind, markdownAstToGreenTree, markdownAstToRedTree, morphHTML, nodeKindToTag, patchBlocksInto, tagToNodeKind, toggleBoxWithDrawer, transformOffset, transformRange, visualThemes };