@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,143 @@
1
+ import { DocumentModel } from '../Model/DocumentModel'
2
+ import { RedNode } from '../Syntax/RedNode'
3
+ import { greenNode, greenLeaf } from '../Syntax/GreenNode'
4
+ import { Transformer, TransformResult } from './Transformer'
5
+ import { Transaction } from '../Transactions/Transaction'
6
+ import type { Operation } from '../Types/operations'
7
+ import { createOperationId } from '../Types/operations'
8
+ import { ease, mixHex, type Easing } from '../Utils/color'
9
+
10
+ export interface GradientOptions {
11
+ from: string // hex color
12
+ to: string // hex color
13
+ unit: 'char' | 'word'
14
+ easing: Easing
15
+ selectionRange?: { start: number, end: number }
16
+ }
17
+
18
+ export class GradientTransformer implements Transformer {
19
+ private options: GradientOptions
20
+
21
+ constructor(options: Partial<GradientOptions> = {}) {
22
+ this.options = {
23
+ from: options.from ?? '#e8b04b',
24
+ to: options.to ?? '#d94f4f',
25
+ unit: options.unit ?? 'char',
26
+ easing: options.easing ?? 'linear',
27
+ selectionRange: options.selectionRange,
28
+ }
29
+ }
30
+
31
+ transform(document: DocumentModel): TransformResult {
32
+ if (!document.redRoot) return { document }
33
+
34
+ const { from, to, unit, easing, selectionRange } = this.options
35
+
36
+ let globalIndex = 0
37
+ let totalUnits = 0
38
+
39
+ // First pass to count total target units
40
+ const countWalk = (node: RedNode) => {
41
+ if (selectionRange) {
42
+ if (node.range.end <= selectionRange.start || node.range.start >= selectionRange.end) return
43
+ }
44
+ if (node.kind === 'text' && node.text) {
45
+ if (unit === 'char') {
46
+ totalUnits += node.text.replace(/\s+/g, '').length
47
+ } else {
48
+ totalUnits += node.text.split(/(\s+)/).filter(w => w.trim() !== '').length
49
+ }
50
+ return
51
+ }
52
+ for (const child of node.children) countWalk(child)
53
+ }
54
+ countWalk(document.redRoot)
55
+
56
+ const denom = Math.max(totalUnits - 1, 1)
57
+ const operations: Operation[] = []
58
+
59
+ // Second pass to apply the transformation
60
+ const walk = (node: RedNode) => {
61
+ if (selectionRange) {
62
+ if (node.range.end <= selectionRange.start || node.range.start >= selectionRange.end) return
63
+ }
64
+
65
+ if (node.kind === 'text' && node.text) {
66
+ const newChildren: RedNode[] = []
67
+
68
+ if (unit === 'char') {
69
+ const chars = [...node.text]
70
+ for (const ch of chars) {
71
+ if (ch.trim() === '') {
72
+ const synthGreen = greenLeaf('text', ch)
73
+ const spaceNode = new RedNode(synthGreen, { kind: 'text', metadata: {} })
74
+ newChildren.push(spaceNode)
75
+ } else {
76
+ const t = ease(totalUnits <= 1 ? 0 : globalIndex / denom, easing)
77
+ const colorHex = mixHex(from, to, t)
78
+
79
+ const synthGreen = greenLeaf('text', ch)
80
+ const charNode = new RedNode(synthGreen, {
81
+ kind: 'text',
82
+ metadata: { style: { color: colorHex } }
83
+ })
84
+ newChildren.push(charNode)
85
+ globalIndex++
86
+ }
87
+ }
88
+ } else if (unit === 'word') {
89
+ const words = node.text.split(/(\s+)/)
90
+ for (const chunk of words) {
91
+ if (chunk.trim() === '') {
92
+ const synthGreen = greenLeaf('text', chunk)
93
+ const spaceNode = new RedNode(synthGreen, { kind: 'text', metadata: {} })
94
+ newChildren.push(spaceNode)
95
+ } else {
96
+ const t = ease(totalUnits <= 1 ? 0 : globalIndex / denom, easing)
97
+ const colorHex = mixHex(from, to, t)
98
+
99
+ const synthGreen = greenLeaf('text', chunk)
100
+ const wordNode = new RedNode(synthGreen, {
101
+ kind: 'text',
102
+ metadata: { style: { color: colorHex } }
103
+ })
104
+ newChildren.push(wordNode)
105
+ globalIndex++
106
+ }
107
+ }
108
+ }
109
+
110
+ const emptyGreen = greenNode('group', '')
111
+ const groupNode = new RedNode(emptyGreen, { kind: 'group' })
112
+
113
+ RedNode.allowMutation(() => {
114
+ for (const child of newChildren) {
115
+ groupNode.appendChild(child)
116
+ }
117
+ })
118
+
119
+ operations.push({
120
+ kind: 'replace_node',
121
+ id: createOperationId(),
122
+ nodeId: node.id,
123
+ newNode: groupNode,
124
+ oldNode: node,
125
+ undoable: true,
126
+ timestamp: Date.now()
127
+ })
128
+ return
129
+ }
130
+
131
+ for (const child of [...node.children]) {
132
+ walk(child)
133
+ }
134
+ }
135
+
136
+ walk(document.redRoot)
137
+
138
+ const transaction = new Transaction(operations)
139
+ transaction.apply(document.redRoot)
140
+
141
+ return { document, transaction }
142
+ }
143
+ }
@@ -0,0 +1,115 @@
1
+ import { DocumentModel } from '../Model/DocumentModel'
2
+ import { RedNode } from '../Syntax/RedNode'
3
+ import { greenNode, greenLeaf } from '../Syntax/GreenNode'
4
+ import { Transformer, TransformResult } from './Transformer'
5
+ import { Transaction } from '../Transactions/Transaction'
6
+ import type { Operation } from '../Types/operations'
7
+ import { createOperationId } from '../Types/operations'
8
+
9
+ export interface GrowOptions {
10
+ minSize: number
11
+ maxSize: number
12
+ cycles: number
13
+ selectionRange?: { start: number, end: number }
14
+ }
15
+
16
+ export class GrowTransformer implements Transformer {
17
+ private options: GrowOptions
18
+
19
+ constructor(options: Partial<GrowOptions> = {}) {
20
+ this.options = {
21
+ minSize: options.minSize ?? 90,
22
+ maxSize: options.maxSize ?? 160,
23
+ cycles: options.cycles ?? 1,
24
+ selectionRange: options.selectionRange,
25
+ }
26
+ }
27
+
28
+ transform(document: DocumentModel): TransformResult {
29
+ if (!document.redRoot) return { document }
30
+
31
+ const { minSize, maxSize, cycles, selectionRange } = this.options
32
+
33
+ let globalIndex = 0
34
+ let totalChars = 0
35
+
36
+ // First pass to count total target units
37
+ const countWalk = (node: RedNode) => {
38
+ if (selectionRange) {
39
+ if (node.range.end <= selectionRange.start || node.range.start >= selectionRange.end) return
40
+ }
41
+ if (node.kind === 'text' && node.text) {
42
+ totalChars += node.text.replace(/\s+/g, '').length
43
+ return
44
+ }
45
+ for (const child of node.children) countWalk(child)
46
+ }
47
+ countWalk(document.redRoot)
48
+
49
+ const denom = Math.max(totalChars - 1, 1)
50
+ const operations: Operation[] = []
51
+
52
+ // Second pass to apply the transformation
53
+ const walk = (node: RedNode) => {
54
+ if (selectionRange) {
55
+ if (node.range.end <= selectionRange.start || node.range.start >= selectionRange.end) return
56
+ }
57
+
58
+ if (node.kind === 'text' && node.text) {
59
+ const newChildren: RedNode[] = []
60
+ const chars = [...node.text]
61
+
62
+ for (const ch of chars) {
63
+ if (ch.trim() === '') {
64
+ const synthGreen = greenLeaf('text', ch)
65
+ const spaceNode = new RedNode(synthGreen, { kind: 'text', metadata: {} })
66
+ newChildren.push(spaceNode)
67
+ } else {
68
+ const phase = (globalIndex / denom) * Math.PI * 2 * cycles
69
+ const t = (Math.sin(phase) + 1) / 2
70
+ const size = Math.round(minSize + (maxSize - minSize) * t)
71
+
72
+ const synthGreen = greenLeaf('text', ch)
73
+ const charNode = new RedNode(synthGreen, {
74
+ kind: 'text',
75
+ metadata: { style: { fontSize: size.toString() } }
76
+ })
77
+ newChildren.push(charNode)
78
+ globalIndex++
79
+ }
80
+ }
81
+
82
+ const emptyGreen = greenNode('group', '')
83
+ const groupNode = new RedNode(emptyGreen, { kind: 'group' })
84
+
85
+ RedNode.allowMutation(() => {
86
+ for (const child of newChildren) {
87
+ groupNode.appendChild(child)
88
+ }
89
+ })
90
+
91
+ operations.push({
92
+ kind: 'replace_node',
93
+ id: createOperationId(),
94
+ nodeId: node.id,
95
+ newNode: groupNode,
96
+ oldNode: node,
97
+ undoable: true,
98
+ timestamp: Date.now()
99
+ })
100
+ return
101
+ }
102
+
103
+ for (const child of [...node.children]) {
104
+ walk(child)
105
+ }
106
+ }
107
+
108
+ walk(document.redRoot)
109
+
110
+ const transaction = new Transaction(operations)
111
+ transaction.apply(document.redRoot)
112
+
113
+ return { document, transaction }
114
+ }
115
+ }
@@ -0,0 +1,121 @@
1
+ import { DocumentModel } from '../Model/DocumentModel'
2
+ import { RedNode } from '../Syntax/RedNode'
3
+ import { greenNode, greenLeaf } from '../Syntax/GreenNode'
4
+ import { Transformer, TransformResult } from './Transformer'
5
+ import { Transaction } from '../Transactions/Transaction'
6
+ import type { Operation } from '../Types/operations'
7
+ import { createOperationId } from '../Types/operations'
8
+ import { hslToHex } from '../Utils/color'
9
+
10
+ export interface RainbowOptions {
11
+ saturation: number // 0-100
12
+ lightness: number // 0-100
13
+ spread: number // degrees
14
+ offset: number // degrees
15
+ selectionRange?: { start: number, end: number }
16
+ }
17
+
18
+ export class RainbowTransformer implements Transformer {
19
+ private options: RainbowOptions
20
+
21
+ constructor(options: Partial<RainbowOptions> = {}) {
22
+ this.options = {
23
+ saturation: options.saturation ?? 80,
24
+ lightness: options.lightness ?? 60,
25
+ spread: options.spread ?? 300,
26
+ offset: options.offset ?? 0,
27
+ selectionRange: options.selectionRange,
28
+ }
29
+ }
30
+
31
+ transform(document: DocumentModel): TransformResult {
32
+ if (!document.redRoot) return { document }
33
+
34
+ const { saturation, lightness, spread, offset, selectionRange } = this.options
35
+
36
+ // Convert to 0-1 range for the color util
37
+ const s = saturation / 100
38
+ const l = lightness / 100
39
+
40
+ let globalIndex = 0
41
+ let totalChars = 0
42
+
43
+ // First pass to count total characters (for proper spread calculation)
44
+ const countWalk = (node: RedNode) => {
45
+ if (selectionRange) {
46
+ if (node.range.end <= selectionRange.start || node.range.start >= selectionRange.end) return
47
+ }
48
+ if (node.kind === 'text' && node.text) {
49
+ totalChars += node.text.replace(/\s+/g, '').length
50
+ return
51
+ }
52
+ for (const child of node.children) countWalk(child)
53
+ }
54
+ countWalk(document.redRoot)
55
+
56
+ const denom = Math.max(totalChars - 1, 1)
57
+ const operations: Operation[] = []
58
+
59
+ // Second pass to apply the transformation
60
+ const walk = (node: RedNode) => {
61
+ if (selectionRange) {
62
+ if (node.range.end <= selectionRange.start || node.range.start >= selectionRange.end) return
63
+ }
64
+
65
+ if (node.kind === 'text' && node.text) {
66
+ const newChildren: RedNode[] = []
67
+ const chars = [...node.text]
68
+
69
+ for (const ch of chars) {
70
+ if (ch.trim() === '') {
71
+ const synthGreen = greenLeaf('text', ch)
72
+ const spaceNode = new RedNode(synthGreen, { kind: 'text', metadata: {} })
73
+ newChildren.push(spaceNode)
74
+ } else {
75
+ const hue = offset + (globalIndex / denom) * spread
76
+ const colorHex = hslToHex(hue, s, l)
77
+
78
+ const synthGreen = greenLeaf('text', ch)
79
+ const charNode = new RedNode(synthGreen, {
80
+ kind: 'text',
81
+ metadata: { style: { color: colorHex } }
82
+ })
83
+ newChildren.push(charNode)
84
+ globalIndex++
85
+ }
86
+ }
87
+
88
+ const emptyGreen = greenNode('group', '')
89
+ const groupNode = new RedNode(emptyGreen, { kind: 'group' })
90
+
91
+ RedNode.allowMutation(() => {
92
+ for (const child of newChildren) {
93
+ groupNode.appendChild(child)
94
+ }
95
+ })
96
+
97
+ operations.push({
98
+ kind: 'replace_node',
99
+ id: createOperationId(),
100
+ nodeId: node.id,
101
+ newNode: groupNode,
102
+ oldNode: node,
103
+ undoable: true,
104
+ timestamp: Date.now()
105
+ })
106
+ return
107
+ }
108
+
109
+ for (const child of [...node.children]) {
110
+ walk(child)
111
+ }
112
+ }
113
+
114
+ walk(document.redRoot)
115
+
116
+ const transaction = new Transaction(operations)
117
+ transaction.apply(document.redRoot)
118
+
119
+ return { document, transaction }
120
+ }
121
+ }
@@ -0,0 +1,130 @@
1
+ import { DocumentModel } from '../Model/DocumentModel'
2
+ import { RedNode } from '../Syntax/RedNode'
3
+ import { greenNode, greenLeaf } from '../Syntax/GreenNode'
4
+ import { Transformer, TransformResult } from './Transformer'
5
+ import { Transaction } from '../Transactions/Transaction'
6
+ import type { Operation } from '../Types/operations'
7
+ import { createOperationId } from '../Types/operations'
8
+
9
+ export interface SineWaveOptions {
10
+ minSize: number
11
+ maxSize: number
12
+ frequency: number
13
+ step: 'char' | 'word'
14
+ selectionRange?: { start: number, end: number }
15
+ }
16
+
17
+ export class SineWaveTransformer implements Transformer {
18
+ private options: SineWaveOptions
19
+
20
+ constructor(options: Partial<SineWaveOptions> = {}) {
21
+ this.options = {
22
+ minSize: options.minSize ?? 50,
23
+ maxSize: options.maxSize ?? 150,
24
+ frequency: options.frequency ?? 0.5,
25
+ step: options.step ?? 'char',
26
+ selectionRange: options.selectionRange,
27
+ }
28
+ }
29
+
30
+ transform(document: DocumentModel): TransformResult {
31
+ if (!document.redRoot) return { document }
32
+
33
+ const { minSize, maxSize, frequency, step, selectionRange } = this.options
34
+ const amplitude = (maxSize - minSize) / 2
35
+ const center = minSize + amplitude
36
+
37
+ let globalIndex = 0
38
+ const operations: Operation[] = []
39
+
40
+ const walk = (node: RedNode) => {
41
+ // AST Intersection check
42
+ if (selectionRange) {
43
+ if (node.range.end <= selectionRange.start || node.range.start >= selectionRange.end) {
44
+ return // Node is completely outside the selection, skip it
45
+ }
46
+ }
47
+
48
+ if (node.kind === 'text' && node.text) {
49
+ const newChildren: RedNode[] = []
50
+
51
+ if (step === 'char') {
52
+ const chars = [...node.text]
53
+ for (const ch of chars) {
54
+ const wave = Math.sin(globalIndex * frequency)
55
+ const size = Math.round(center + wave * amplitude)
56
+
57
+ const synthGreen = greenLeaf('text', ch)
58
+ const charNode = new RedNode(synthGreen, {
59
+ kind: 'text',
60
+ metadata: { style: { fontSize: size.toString() } }
61
+ })
62
+ newChildren.push(charNode)
63
+
64
+ if (ch.trim() !== '') globalIndex++
65
+ }
66
+ } else if (step === 'word') {
67
+ const words = node.text.split(/(\s+)/)
68
+ for (const chunk of words) {
69
+ if (chunk.trim() === '') {
70
+ const synthGreen = greenLeaf('text', chunk)
71
+ const spaceNode = new RedNode(synthGreen, { kind: 'text', metadata: {} })
72
+ newChildren.push(spaceNode)
73
+ } else {
74
+ const wave = Math.sin(globalIndex * frequency)
75
+ const size = Math.round(center + wave * amplitude)
76
+
77
+ const synthGreen = greenLeaf('text', chunk)
78
+ const wordNode = new RedNode(synthGreen, {
79
+ kind: 'text',
80
+ metadata: { style: { fontSize: size.toString() } }
81
+ })
82
+ newChildren.push(wordNode)
83
+ globalIndex++
84
+ }
85
+ }
86
+ }
87
+
88
+ // We create a new Group RedNode to replace the text node
89
+ const emptyGreen = greenNode('group', '')
90
+ const groupNode = new RedNode(emptyGreen, { kind: 'group' })
91
+
92
+ RedNode.allowMutation(() => {
93
+ for (const child of newChildren) {
94
+ groupNode.appendChild(child)
95
+ }
96
+ })
97
+
98
+ // Emit an operation
99
+ operations.push({
100
+ kind: 'replace_node',
101
+ id: createOperationId(),
102
+ nodeId: node.id,
103
+ newNode: groupNode,
104
+ oldNode: node, // reference to old node
105
+ undoable: true,
106
+ timestamp: Date.now()
107
+ })
108
+
109
+ return // Prevent walking into the newly generated text fragments
110
+ }
111
+
112
+ const childrenToWalk = [...node.children]
113
+ for (const child of childrenToWalk) {
114
+ walk(child)
115
+ }
116
+ }
117
+
118
+ walk(document.redRoot)
119
+
120
+ // Build the transaction
121
+ const transaction = new Transaction(operations)
122
+
123
+ // For now, we apply it directly to the model as we still mutate the Red Tree,
124
+ // but going forward, a true immutable system would return a cloned model.
125
+ transaction.apply(document.redRoot)
126
+
127
+ return { document, transaction }
128
+ }
129
+ }
130
+
@@ -0,0 +1,22 @@
1
+ import { DocumentModel } from '../Model/DocumentModel'
2
+ import { Transaction } from '../Transactions/Transaction'
3
+
4
+ export interface TransformResult {
5
+ document: DocumentModel
6
+ transaction?: Transaction
7
+ }
8
+
9
+ export interface Transformer {
10
+ /**
11
+ * Transforms a DocumentModel.
12
+ * Should ideally return a Transaction that contains the operations applied.
13
+ */
14
+ transform(document: DocumentModel): TransformResult
15
+ }
16
+
17
+ /**
18
+ * Helper to apply a transformer.
19
+ */
20
+ export function applyTransformer(document: DocumentModel, transformer: Transformer): TransformResult {
21
+ return transformer.transform(document)
22
+ }