@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,176 @@
1
+ /**
2
+ * Quasar Analysis Framework — Collapse Gradient Transform
3
+ *
4
+ * Applies `collapse-gradient` actions from a TransformationPlan.
5
+ * Each action targets a sequence of adjacent [color] nodes and replaces
6
+ * them with a single [gradient] node containing all text children.
7
+ *
8
+ * The transform operates on the Green Tree and returns a new tree —
9
+ * it NEVER mutates the original.
10
+ *
11
+ * @example
12
+ * Before:
13
+ * [color=#FF0000]H[/color][color=#EE1100]e[/color][color=#DD2200]l[/color]
14
+ * After:
15
+ * [gradient=#FF0000,#EE1100,#DD2200]Hello[/gradient]
16
+ *
17
+ * @see TransformPass
18
+ */
19
+
20
+ import type { TransformPass, TransformationPlan } from '../../Contracts/Pass'
21
+ import type { PipelineContext } from '../../Contracts/PipelineContext'
22
+ import type { GreenNode } from '../../../Syntax/GreenNode'
23
+ import { greenNode, greenLeaf, childOffsets } from '../../../Syntax/GreenNode'
24
+
25
+ // ── Supported action kinds ────────────────────────────────────────
26
+
27
+ const COLLAPSE_KINDS = new Set(['collapse-gradient', 'collapse-gradient-auto'])
28
+
29
+ export class CollapseGradientTransform implements TransformPass {
30
+ readonly id = 'collapse-gradient'
31
+
32
+ run(tree: GreenNode, plan: TransformationPlan, _context: PipelineContext): GreenNode {
33
+ // Filter only collapse-gradient actions
34
+ const actions = plan.actions.filter(a => COLLAPSE_KINDS.has(a.kind))
35
+
36
+ if (actions.length === 0) {
37
+ return tree // No changes
38
+ }
39
+
40
+ // Build a set of ranges to collapse (sorted by start position)
41
+ const ranges = actions
42
+ .map(a => ({
43
+ start: (a.payload.range as { start: number; end: number }).start,
44
+ end: (a.payload.range as { start: number; end: number }).end,
45
+ colors: ((a.payload.model as Record<string, unknown>)?.colors ?? []) as string[],
46
+ easing: ((a.payload.model as Record<string, unknown>)?.easing ?? 'linear') as string,
47
+ }))
48
+ .sort((a, b) => a.start - b.start)
49
+
50
+ // Apply transforms recursively
51
+ return this.transformNode(tree, ranges)
52
+ }
53
+
54
+ // ── Private ─────────────────────────────────────────────────────
55
+
56
+ /**
57
+ * Walk the tree and collapse any sequences that fall within our ranges.
58
+ */
59
+ private transformNode(
60
+ node: GreenNode,
61
+ ranges: Array<{ start: number; end: number; colors: string[]; easing: string }>,
62
+ treeStart: number = 0,
63
+ ): GreenNode {
64
+ // Leaf node — no children to transform
65
+ if (node.children.length === 0) {
66
+ return node
67
+ }
68
+
69
+ const children = node.children as GreenNode[]
70
+
71
+ // Green nodes carry widths, not positions. The ranges being matched here
72
+ // come from the analyzers and are absolute, so the walk accumulates the
73
+ // offsets it needs as it descends.
74
+ const offsets = childOffsets(node, treeStart)
75
+
76
+ // Check if any range overlaps with this node's children
77
+ const nodeStart = offsets[0]
78
+ const nodeEnd = offsets[children.length]
79
+ const activeRanges = ranges.filter(
80
+ r => r.start >= nodeStart && r.end <= nodeEnd,
81
+ )
82
+
83
+ if (activeRanges.length === 0) {
84
+ // No ranges in this subtree — recurse into children
85
+ const newChildren = children.map((c, ci) => this.transformNode(c, ranges, offsets[ci]))
86
+ return this.rebuildNode(node, newChildren)
87
+ }
88
+
89
+ // Build new children, collapsing ranges into gradient nodes
90
+ const newChildren: GreenNode[] = []
91
+ let i = 0
92
+
93
+ while (i < children.length) {
94
+ const child = children[i]
95
+ const matchingRange = activeRanges.find(
96
+ r => offsets[i] >= r.start && offsets[i + 1] <= r.end,
97
+ )
98
+
99
+ if (matchingRange) {
100
+ // Find all children within this range
101
+ const rangeChildren: GreenNode[] = []
102
+ while (i < children.length && offsets[i + 1] <= matchingRange.end) {
103
+ rangeChildren.push(children[i])
104
+ i++
105
+ }
106
+
107
+ // Collapse into a single gradient node
108
+ const textNodes: string[] = []
109
+ const colorNodes: GreenNode[] = []
110
+
111
+ for (const rc of rangeChildren) {
112
+ if (rc.kind === 'color') {
113
+ colorNodes.push(rc)
114
+ // Collect text from inside the colour node
115
+ for (const textChild of rc.children as GreenNode[]) {
116
+ if (textChild.kind === 'text') {
117
+ textNodes.push(textChild.text)
118
+ }
119
+ }
120
+ } else if (rc.kind === 'text') {
121
+ textNodes.push(rc.text)
122
+ } else {
123
+ textNodes.push(rc.text || '')
124
+ }
125
+ }
126
+
127
+ // Create a text leaf inside the gradient node
128
+ const combinedText = textNodes.join('')
129
+ const textLeaf = greenLeaf('text', combinedText)
130
+
131
+ // Use the detected colours; fall back to extracted colours from nodes
132
+ let colors = matchingRange.colors
133
+ if (colors.length === 0) {
134
+ colors = colorNodes
135
+ .map(cn => this.extractHex(cn))
136
+ .filter((h): h is string => h !== null)
137
+ }
138
+
139
+ const colorsAttr = colors.join(',')
140
+ const gradientNode = greenNode('gradient', `=${colorsAttr}`, [textLeaf])
141
+ newChildren.push(gradientNode)
142
+ } else {
143
+ // Not in a collapse range — recurse and preserve
144
+ newChildren.push(this.transformNode(child, ranges, offsets[i]))
145
+ i++
146
+ }
147
+ }
148
+
149
+ return this.rebuildNode(node, newChildren)
150
+ }
151
+
152
+ /**
153
+ * Rebuild a node with the same kind/text/range but new children.
154
+ */
155
+ private rebuildNode(node: GreenNode, newChildren: GreenNode[]): GreenNode {
156
+ // If the node is a colour node and all children were collapsed,
157
+ // just return the first child instead (no need for empty colour)
158
+ if (node.kind === 'color' && newChildren.length === 0) {
159
+ return greenLeaf('text', node.text)
160
+ }
161
+
162
+ return greenNode(node.kind, node.text, newChildren,
163
+ )
164
+ }
165
+
166
+ /**
167
+ * Extract hex colour from a color node's text.
168
+ */
169
+ private extractHex(node: GreenNode): string | null {
170
+ if (node.kind !== 'color') return null
171
+ const text = node.text || ''
172
+ const eqIdx = text.indexOf('=')
173
+ const hex = eqIdx >= 0 ? text.slice(eqIdx + 1).trim() : text.trim()
174
+ return /^#[0-9A-Fa-f]{6}$/.test(hex) ? hex.toUpperCase() : null
175
+ }
176
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Quasar Analysis Framework — Merge Colors Transform
3
+ *
4
+ * Applies `merge-colors` actions from a TransformationPlan.
5
+ * Each action targets a sequence of identical consecutive [color=#HEX]
6
+ * nodes and merges them into a single `[color]` wrapper with combined text.
7
+ *
8
+ * The transform operates on the Green Tree and returns a new tree —
9
+ * it NEVER mutates the original.
10
+ *
11
+ * @example
12
+ * Before:
13
+ * [color=#FF0000]H[/color][color=#FF0000]e[/color][color=#FF0000]l[/color]
14
+ * After:
15
+ * [color=#FF0000]Hel[/color]
16
+ *
17
+ * @see TransformPass
18
+ */
19
+
20
+ import type { TransformPass, TransformationPlan } from '../../Contracts/Pass'
21
+ import type { PipelineContext } from '../../Contracts/PipelineContext'
22
+ import type { GreenNode } from '../../../Syntax/GreenNode'
23
+ import { greenNode, greenLeaf, childOffsets } from '../../../Syntax/GreenNode'
24
+
25
+ export class MergeColorsTransform implements TransformPass {
26
+ readonly id = 'merge-colors'
27
+
28
+ run(tree: GreenNode, plan: TransformationPlan, _context: PipelineContext): GreenNode {
29
+ const mergeActions = plan.actions.filter(a => a.kind === 'merge-colors')
30
+
31
+ if (mergeActions.length === 0) {
32
+ return tree
33
+ }
34
+
35
+ const ranges = mergeActions
36
+ .map(a => ({
37
+ start: (a.payload.range as { start: number; end: number }).start,
38
+ end: (a.payload.range as { start: number; end: number }).end,
39
+ }))
40
+ .sort((a, b) => a.start - b.start)
41
+
42
+ return this.transformNode(tree, ranges)
43
+ }
44
+
45
+ // ── Private ─────────────────────────────────────────────────────
46
+
47
+ private transformNode(
48
+ node: GreenNode,
49
+ ranges: Array<{ start: number; end: number }>,
50
+ treeStart: number = 0,
51
+ ): GreenNode {
52
+ if (node.children.length === 0) return node
53
+
54
+ const children = node.children as GreenNode[]
55
+ // Green nodes carry widths, not positions. The ranges being matched here
56
+ // come from the analyzers and are absolute, so the walk accumulates the
57
+ // offsets it needs as it descends.
58
+ const offsets = childOffsets(node, treeStart)
59
+ const nodeStart = offsets[0]
60
+ const nodeEnd = offsets[children.length]
61
+
62
+ const activeRanges = ranges.filter(
63
+ r => r.start >= nodeStart && r.end <= nodeEnd,
64
+ )
65
+
66
+ if (activeRanges.length === 0) {
67
+ const newChildren = children.map((c, ci) => this.transformNode(c, ranges, offsets[ci]))
68
+ return this.rebuildNode(node, newChildren)
69
+ }
70
+
71
+ const newChildren: GreenNode[] = []
72
+ let i = 0
73
+
74
+ while (i < children.length) {
75
+ const child = children[i]
76
+ const matchingRange = activeRanges.find(
77
+ r => offsets[i] >= r.start && offsets[i + 1] <= r.end,
78
+ )
79
+
80
+ if (matchingRange && child.kind === 'color') {
81
+ // Collect all consecutive colour nodes with the SAME hex
82
+ const hex = this.extractHex(child)
83
+ const mergedTexts: string[] = []
84
+
85
+ while (
86
+ i < children.length &&
87
+ children[i].kind === 'color' &&
88
+ this.extractHex(children[i]) === hex &&
89
+ offsets[i + 1] <= matchingRange.end
90
+ ) {
91
+ // Get text from inside the colour node's children
92
+ for (const textChild of children[i].children as GreenNode[]) {
93
+ if (textChild.kind === 'text') {
94
+ mergedTexts.push(textChild.text)
95
+ }
96
+ }
97
+ i++
98
+ }
99
+
100
+ // Create a single merged colour node
101
+ const combinedText = mergedTexts.join('')
102
+ const textLeaf = greenLeaf('text', combinedText)
103
+ const merged = greenNode('color', child.text, [textLeaf], child.leadingWidth, child.trailingWidth)
104
+ newChildren.push(merged)
105
+ } else {
106
+ newChildren.push(this.transformNode(child, ranges, offsets[i]))
107
+ i++
108
+ }
109
+ }
110
+
111
+ return this.rebuildNode(node, newChildren)
112
+ }
113
+
114
+ private rebuildNode(node: GreenNode, newChildren: GreenNode[]): GreenNode {
115
+ return greenNode(node.kind, node.text, newChildren,
116
+ )
117
+ }
118
+
119
+ private extractHex(node: GreenNode): string | null {
120
+ if (node.kind !== 'color') return null
121
+ const text = node.text || ''
122
+ const eqIdx = text.indexOf('=')
123
+ const hex = eqIdx >= 0 ? text.slice(eqIdx + 1).trim() : text.trim()
124
+ return /^#[0-9A-Fa-f]{6}$/.test(hex) ? hex.toUpperCase() : null
125
+ }
126
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Quasar Analysis Framework — Rainbow Collapse Transform
3
+ *
4
+ * Applies `collapse-rainbow` actions from a TransformationPlan.
5
+ * Replaces sequences of [color] nodes forming a hue-rotation pattern
6
+ * with a single [rainbow] node containing all text children.
7
+ *
8
+ * @example
9
+ * Before: [color=#FF0000]R[/color][color=#FFFF00]O[/color][color=#00FF00]Y[/color]
10
+ * After: [rainbow]ROY[/rainbow]
11
+ */
12
+
13
+ import type { TransformPass, TransformationPlan } from '../../Contracts/Pass'
14
+ import type { PipelineContext } from '../../Contracts/PipelineContext'
15
+ import type { GreenNode } from '../../../Syntax/GreenNode'
16
+ import { greenNode, greenLeaf, childOffsets } from '../../../Syntax/GreenNode'
17
+
18
+ export class RainbowCollapseTransform implements TransformPass {
19
+ readonly id = 'rainbow-collapse'
20
+
21
+ run(tree: GreenNode, plan: TransformationPlan, _context: PipelineContext): GreenNode {
22
+ const actions = plan.actions.filter(a => a.kind === 'collapse-rainbow')
23
+ if (actions.length === 0) return tree
24
+
25
+ const ranges = actions.map(a => ({
26
+ start: (a.payload.range as { start: number; end: number }).start,
27
+ end: (a.payload.range as { start: number; end: number }).end,
28
+ })).sort((a, b) => a.start - b.start)
29
+
30
+ return this.transformNode(tree, ranges)
31
+ }
32
+
33
+ private transformNode(node: GreenNode, ranges: Array<{ start: number; end: number }>, treeStart: number = 0): GreenNode {
34
+ if (node.children.length === 0) return node
35
+
36
+ const children = node.children as GreenNode[]
37
+ // Green nodes carry widths, not positions. The ranges being matched here
38
+ // come from the analyzers and are absolute, so the walk accumulates the
39
+ // offsets it needs as it descends.
40
+ const offsets = childOffsets(node, treeStart)
41
+ const nodeStart = offsets[0]
42
+ const nodeEnd = offsets[children.length]
43
+ const activeRanges = ranges.filter(r => r.start >= nodeStart && r.end <= nodeEnd)
44
+
45
+ if (activeRanges.length === 0) {
46
+ return greenNode(node.kind, node.text, children.map((c, ci) => this.transformNode(c, ranges, offsets[ci])))
47
+ }
48
+
49
+ const newChildren: GreenNode[] = []
50
+ let i = 0
51
+
52
+ while (i < children.length) {
53
+ const child = children[i]
54
+ const range = activeRanges.find(r => offsets[i] >= r.start && offsets[i + 1] <= r.end)
55
+
56
+ if (range && child.kind === 'color') {
57
+ const rangeChildren: GreenNode[] = []
58
+ const texts: string[] = []
59
+
60
+ while (i < children.length && offsets[i + 1] <= range.end) {
61
+ rangeChildren.push(children[i])
62
+ if (children[i].kind === 'color') {
63
+ for (const tc of children[i].children as GreenNode[]) {
64
+ if (tc.kind === 'text') texts.push(tc.text)
65
+ }
66
+ } else if (children[i].kind === 'text') {
67
+ texts.push(children[i].text)
68
+ }
69
+ i++
70
+ }
71
+
72
+ const combined = texts.join('')
73
+ const textLeaf = greenLeaf('text', combined)
74
+ newChildren.push(greenNode('rainbow', '', [textLeaf]))
75
+ } else {
76
+ newChildren.push(this.transformNode(child, ranges, offsets[i]))
77
+ i++
78
+ }
79
+ }
80
+
81
+ return greenNode(node.kind, node.text, newChildren)
82
+ }
83
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Quasar Analysis Framework — Wave Collapse Transform
3
+ *
4
+ * Applies `collapse-wave` actions from a TransformationPlan.
5
+ * Replaces sequences of [size=N] nodes forming a sinusoidal pattern
6
+ * with a single [grow] node containing all text children and the
7
+ * detected size range in metadata.
8
+ *
9
+ * @example
10
+ * Before: [size=90]A[/size][size=150]B[/size][size=90]C[/size]
11
+ * After: [grow]ABC[/grow] (with metadata: min=90, max=150)
12
+ */
13
+
14
+ import type { TransformPass, TransformationPlan } from '../../Contracts/Pass'
15
+ import type { PipelineContext } from '../../Contracts/PipelineContext'
16
+ import type { GreenNode } from '../../../Syntax/GreenNode'
17
+ import { greenNode, greenLeaf, childOffsets } from '../../../Syntax/GreenNode'
18
+
19
+ export class WaveCollapseTransform implements TransformPass {
20
+ readonly id = 'wave-collapse'
21
+
22
+ run(tree: GreenNode, plan: TransformationPlan, _context: PipelineContext): GreenNode {
23
+ const actions = plan.actions.filter(a => a.kind === 'collapse-wave')
24
+ if (actions.length === 0) return tree
25
+
26
+ const ranges = actions.map(a => ({
27
+ start: (a.payload.range as { start: number; end: number }).start,
28
+ end: (a.payload.range as { start: number; end: number }).end,
29
+ min: (a.payload.model as Record<string, unknown>)?.minSize as number ?? 90,
30
+ max: (a.payload.model as Record<string, unknown>)?.maxSize as number ?? 160,
31
+ })).sort((a, b) => a.start - b.start)
32
+
33
+ return this.transformNode(tree, ranges)
34
+ }
35
+
36
+ private transformNode(
37
+ node: GreenNode,
38
+ ranges: Array<{ start: number; end: number; min: number; max: number }>,
39
+ treeStart: number = 0,
40
+ ): GreenNode {
41
+ if (node.children.length === 0) return node
42
+
43
+ const children = node.children as GreenNode[]
44
+ // Green nodes carry widths, not positions. The ranges being matched here
45
+ // come from the analyzers and are absolute, so the walk accumulates the
46
+ // offsets it needs as it descends.
47
+ const offsets = childOffsets(node, treeStart)
48
+ const nodeStart = offsets[0]
49
+ const nodeEnd = offsets[children.length]
50
+ const activeRanges = ranges.filter(r => r.start >= nodeStart && r.end <= nodeEnd)
51
+
52
+ if (activeRanges.length === 0) {
53
+ return greenNode(node.kind, node.text, children.map((c, ci) => this.transformNode(c, ranges, offsets[ci])))
54
+ }
55
+
56
+ const newChildren: GreenNode[] = []
57
+ let i = 0
58
+
59
+ while (i < children.length) {
60
+ const child = children[i]
61
+ const range = activeRanges.find(r => offsets[i] >= r.start && offsets[i + 1] <= r.end)
62
+
63
+ if (range && ['font_size', 'size'].includes(child.kind)) {
64
+ const texts: string[] = []
65
+ while (i < children.length && offsets[i + 1] <= range.end) {
66
+ if (['font_size', 'size'].includes(children[i].kind)) {
67
+ for (const tc of children[i].children as GreenNode[]) {
68
+ if (tc.kind === 'text') texts.push(tc.text)
69
+ }
70
+ } else if (children[i].kind === 'text') {
71
+ texts.push(children[i].text)
72
+ }
73
+ i++
74
+ }
75
+
76
+ const combined = texts.join('')
77
+ const textLeaf = greenLeaf('text', combined)
78
+ const attr = `=${range.min},${range.max}`
79
+ newChildren.push(greenNode('grow', attr, [textLeaf]))
80
+ } else {
81
+ newChildren.push(this.transformNode(child, ranges, offsets[i]))
82
+ i++
83
+ }
84
+ }
85
+
86
+ return greenNode(node.kind, node.text, newChildren)
87
+ }
88
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Quasar Analysis Framework — Character Count Analyzer (Proof of Concept)
3
+ *
4
+ * The simplest possible AnalyzerPass: it walks the Green Tree and counts
5
+ * text characters. This validates that the full pipeline works:
6
+ *
7
+ * Green Tree → Analyzer → Contribution → Aggregated Report
8
+ *
9
+ * No algorithms, no colours, no decisions — just validating the pipe.
10
+ */
11
+
12
+ import type { AnalyzerPass } from '../../Contracts/Pass'
13
+ import type { PipelineContext } from '../../Contracts/PipelineContext'
14
+ import type { Contribution } from '../../Contracts/Contribution'
15
+ import { ContributionKind } from '../../Contracts/Contribution'
16
+ import type { GreenNode } from '../../../Syntax/GreenNode'
17
+
18
+ export class CharacterCountAnalyzer implements AnalyzerPass {
19
+ readonly id = 'character-count'
20
+
21
+ run(tree: GreenNode, _context: PipelineContext): Contribution[] {
22
+ const count = this.countChars(tree)
23
+
24
+ return [
25
+ {
26
+ kind: ContributionKind.Metrics,
27
+ metrics: {
28
+ characters: count,
29
+ },
30
+ },
31
+ ]
32
+ }
33
+
34
+ // ── Private ─────────────────────────────────────────────────────
35
+
36
+ private countChars(node: GreenNode): number {
37
+ let total = node.text?.length ?? 0
38
+ if (node.children) {
39
+ for (const child of node.children) {
40
+ total += this.countChars(child)
41
+ }
42
+ }
43
+ return total
44
+ }
45
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Quasar Analysis Framework — Pipeline
3
+ *
4
+ * The Pipeline is itself a Pass — it can be composed inside larger
5
+ * pipelines. Execution is strictly ordered:
6
+ *
7
+ * 1. All analysis passes run, collecting contributions.
8
+ * 2. The aggregated AnalysisReport is fed to each decision pass.
9
+ * 3. Each transform pass receives the current tree + plan and
10
+ * returns a new tree (never mutating the original).
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const pipeline = new Pipeline([
15
+ * { stage: 'analysis', pass: new CharacterCountAnalyzer() },
16
+ * ])
17
+ * const report = pipeline.run(tree, context)
18
+ * ```
19
+ */
20
+
21
+ import type { AnalyzerPass, DecisionPass, TransformPass, TransformationPlan } from '../Contracts/Pass'
22
+ import type { AnalysisReport } from '../Contracts/AnalysisReport'
23
+ import type { PipelineContext } from '../Contracts/PipelineContext'
24
+ import type { PipelineStage } from './PipelineStage'
25
+ import type { GreenNode } from '../../Syntax/GreenNode'
26
+ import type { Contribution } from '../Contracts/Contribution'
27
+
28
+ interface StageEntry {
29
+ readonly stage: PipelineStage
30
+ readonly pass: AnalyzerPass | DecisionPass | TransformPass
31
+ }
32
+
33
+ export interface PipelineResult {
34
+ /** The final (possibly transformed) Green Tree. */
35
+ readonly tree: GreenNode
36
+ /** The aggregated analysis report. */
37
+ readonly report: AnalysisReport
38
+ }
39
+
40
+ export class Pipeline {
41
+ constructor(private readonly stages: readonly StageEntry[]) {}
42
+
43
+ // ── Public API ──────────────────────────────────────────────────
44
+
45
+ /**
46
+ * Execute the full pipeline against `tree` with the given `context`.
47
+ *
48
+ * @returns The final tree and analysis report.
49
+ */
50
+ run(tree: GreenNode, context: PipelineContext): PipelineResult {
51
+ const start = performance.now()
52
+ let currentTree = tree
53
+ const allContributions: Contribution[] = []
54
+ let latestPlan: TransformationPlan = { actions: [] }
55
+ const analysisPassCount: number = this.stages.filter(s => s.stage === 'analysis').length
56
+
57
+ for (const entry of this.stages) {
58
+ switch (entry.stage) {
59
+ case 'analysis':
60
+ this.runAnalysis(entry.pass as AnalyzerPass, currentTree, context, allContributions)
61
+ break
62
+
63
+ case 'decision': {
64
+ const report = this.buildReport(allContributions, analysisPassCount, start)
65
+ latestPlan = this.runDecision(entry.pass as DecisionPass, report, context)
66
+ break
67
+ }
68
+
69
+ case 'transform':
70
+ currentTree = this.runTransform(entry.pass as TransformPass, currentTree, latestPlan, context)
71
+ break
72
+ }
73
+ }
74
+
75
+ const elapsed = performance.now() - start
76
+ const report = this.buildReport(allContributions, analysisPassCount, elapsed)
77
+ return { tree: currentTree, report }
78
+ }
79
+
80
+ /**
81
+ * Use PipelineBuilder to construct a Pipeline:
82
+ *
83
+ * ```ts
84
+ * import { PipelineBuilder } from './PipelineBuilder'
85
+ * const pipeline = new PipelineBuilder()
86
+ * .analysis(new CharacterCountAnalyzer())
87
+ * .build()
88
+ * ```
89
+ */
90
+
91
+ // ── Internal helpers ────────────────────────────────────────────
92
+
93
+ private runAnalysis(
94
+ pass: AnalyzerPass,
95
+ tree: GreenNode,
96
+ context: PipelineContext,
97
+ sink: Contribution[],
98
+ ): void {
99
+ const contributions = pass.run(tree, context)
100
+ for (const c of contributions) {
101
+ sink.push(c)
102
+ }
103
+ }
104
+
105
+ private runDecision(
106
+ pass: DecisionPass,
107
+ report: AnalysisReport,
108
+ context: PipelineContext,
109
+ ): TransformationPlan {
110
+ return pass.run(report, context)
111
+ }
112
+
113
+ private runTransform(
114
+ pass: TransformPass,
115
+ tree: GreenNode,
116
+ plan: TransformationPlan,
117
+ context: PipelineContext,
118
+ ): GreenNode {
119
+ return pass.run(tree, plan, context)
120
+ }
121
+
122
+ private buildReport(
123
+ contributions: Contribution[],
124
+ passCount: number,
125
+ elapsedMs: number,
126
+ ): AnalysisReport {
127
+ return {
128
+ contributions: Object.freeze([...contributions]),
129
+ passCount,
130
+ elapsedMs,
131
+ }
132
+ }
133
+ }