@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.
- package/LICENSE +119 -0
- package/README.md +45 -0
- package/dist/Visuals/lyne.css +1027 -0
- package/dist/Visuals/osu.css +257 -0
- package/dist/index.d.mts +4563 -0
- package/dist/index.d.ts +4563 -0
- package/dist/index.js +11291 -0
- package/dist/index.mjs +11197 -0
- package/package.json +51 -0
- package/src/Analysis/Contracts/AnalysisReport.ts +26 -0
- package/src/Analysis/Contracts/Contribution.ts +76 -0
- package/src/Analysis/Contracts/Pass.ts +76 -0
- package/src/Analysis/Contracts/PipelineContext.ts +48 -0
- package/src/Analysis/Passes/Analysis/GradientAnalyzer.ts +292 -0
- package/src/Analysis/Passes/Analysis/MergeableColorAnalyzer.ts +112 -0
- package/src/Analysis/Passes/Analysis/RainbowAnalyzer.ts +233 -0
- package/src/Analysis/Passes/Analysis/WaveAnalyzer.ts +211 -0
- package/src/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.ts +135 -0
- package/src/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.ts +84 -0
- package/src/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.ts +99 -0
- package/src/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.ts +119 -0
- package/src/Analysis/Passes/Decision/DefaultDecision.ts +139 -0
- package/src/Analysis/Passes/Decision/__tests__/DefaultDecision.test.ts +179 -0
- package/src/Analysis/Passes/Transform/CollapseGradientTransform.ts +176 -0
- package/src/Analysis/Passes/Transform/MergeColorsTransform.ts +126 -0
- package/src/Analysis/Passes/Transform/RainbowCollapseTransform.ts +83 -0
- package/src/Analysis/Passes/Transform/WaveCollapseTransform.ts +88 -0
- package/src/Analysis/Passes/Utility/CharacterCountAnalyzer.ts +45 -0
- package/src/Analysis/Pipeline/Pipeline.ts +133 -0
- package/src/Analysis/Pipeline/PipelineBuilder.ts +55 -0
- package/src/Analysis/Pipeline/PipelineStage.ts +19 -0
- package/src/Analysis/Utils/color-utils.ts +132 -0
- package/src/Analysis/__tests__/Integration.test.ts +162 -0
- package/src/Analysis/__tests__/Pipeline.test.ts +133 -0
- package/src/Analysis/index.ts +52 -0
- package/src/BBCode/BBCodeDocumentModel.ts +175 -0
- package/src/BBCode/BBCodeToGreenNode.ts +755 -0
- package/src/BBCode/Parser.ts +384 -0
- package/src/BBCode/index.ts +12 -0
- package/src/Collab/positions.ts +91 -0
- package/src/Commands/Command.ts +44 -0
- package/src/Commands/CommandRegistry.ts +78 -0
- package/src/Commands/DeleteNode.ts +20 -0
- package/src/Commands/InsertText.ts +21 -0
- package/src/Commands/SplitMerge.ts +28 -0
- package/src/Commands/WrapInTag.ts +21 -0
- package/src/Commands/index.ts +6 -0
- package/src/Diff/TreeDiffer.ts +264 -0
- package/src/Diff/__tests__/TreeDiffer.test.ts +65 -0
- package/src/Diff/index.ts +2 -0
- package/src/Events/EventBus.ts +160 -0
- package/src/Events/index.ts +2 -0
- package/src/Formatter/Formatter.ts +54 -0
- package/src/Formatter/index.ts +2 -0
- package/src/HTML/HTMLDocumentModel.ts +35 -0
- package/src/HTML/HTMLToGreenNode.ts +290 -0
- package/src/Incremental/ChangeTracker.ts +105 -0
- package/src/Incremental/IncrementalParser.ts +591 -0
- package/src/Incremental/__tests__/IncrementalParser.test.ts +164 -0
- package/src/Incremental/index.ts +4 -0
- package/src/Lexer/BBCodeLexer.ts +382 -0
- package/src/Lexer/Lexer.ts +181 -0
- package/src/Lexer/index.ts +10 -0
- package/src/Linter/Linter.ts +193 -0
- package/src/Linter/index.ts +2 -0
- package/src/Markdown/MarkdownAST.ts +112 -0
- package/src/Markdown/MarkdownDocumentModel.ts +55 -0
- package/src/Markdown/MarkdownLexer.ts +203 -0
- package/src/Markdown/MarkdownParser.ts +455 -0
- package/src/Markdown/MarkdownToGreenNode.ts +153 -0
- package/src/Model/DocumentModel.ts +694 -0
- package/src/Model/NodeFactory.ts +117 -0
- package/src/Model/TagRegistry.ts +495 -0
- package/src/Model/index.ts +5 -0
- package/src/Plugins/PluginAPI.ts +119 -0
- package/src/Plugins/PluginRegistry.ts +132 -0
- package/src/Plugins/index.ts +3 -0
- package/src/Queries/QueryEngine.ts +152 -0
- package/src/Queries/index.ts +1 -0
- package/src/RenderPipeline/RenderPipeline.ts +125 -0
- package/src/RenderPipeline/RenderTree.ts +134 -0
- package/src/RenderPipeline/index.ts +4 -0
- package/src/Semantic/SemanticAnalyzer.ts +506 -0
- package/src/Semantic/index.ts +2 -0
- package/src/Symbols/SymbolTable.ts +124 -0
- package/src/Symbols/index.ts +1 -0
- package/src/Syntax/GreenNode.ts +324 -0
- package/src/Syntax/GreenNodePool.ts +269 -0
- package/src/Syntax/NodeMatcher.ts +370 -0
- package/src/Syntax/RedNode.ts +569 -0
- package/src/Syntax/RedNodeStore.ts +184 -0
- package/src/Syntax/TreeBuilder.ts +214 -0
- package/src/Syntax/__tests__/GreenNode.test.ts +33 -0
- package/src/Syntax/__tests__/RedNode.test.ts +81 -0
- package/src/Syntax/__tests__/RedNodeStore.test.ts +104 -0
- package/src/Syntax/greenEdit.ts +110 -0
- package/src/Syntax/hash.ts +30 -0
- package/src/Syntax/index.ts +12 -0
- package/src/Syntax/partition.ts +161 -0
- package/src/Syntax/preserveNodeIds.ts +201 -0
- package/src/Tests/ASTOptimizerIdempotence.test.ts +77 -0
- package/src/Tests/BlockPatcher.test.ts +437 -0
- package/src/Tests/BlockPatcherWindowed.test.ts +364 -0
- package/src/Tests/BoxDrawer.test.ts +217 -0
- package/src/Tests/BoxRichTitle.test.ts +105 -0
- package/src/Tests/Chars500kBenchmark.test.ts +151 -0
- package/src/Tests/Chars500kEdits.test.ts +321 -0
- package/src/Tests/CollabPositions.test.ts +146 -0
- package/src/Tests/CompilerPathProfiling.test.ts +186 -0
- package/src/Tests/DOMMorpher.test.ts +142 -0
- package/src/Tests/DomPatchPerf.test.ts +60 -0
- package/src/Tests/EffectSegments.snapshot.json +616 -0
- package/src/Tests/EffectSegments.test.ts +68 -0
- package/src/Tests/FindNodeAtOffset.test.ts +65 -0
- package/src/Tests/Fuzzer.test.ts +166 -0
- package/src/Tests/GreenNodePool.test.ts +153 -0
- package/src/Tests/Lexer.test.ts +238 -0
- package/src/Tests/LyneMode.test.ts +187 -0
- package/src/Tests/ModelCoherence.test.ts +180 -0
- package/src/Tests/Partition.test.ts +238 -0
- package/src/Tests/PluginTags.test.ts +150 -0
- package/src/Tests/ProblematicSection.test.ts +46 -0
- package/src/Tests/ProblematicSectionHTML.test.ts +58 -0
- package/src/Tests/RedReuse.test.ts +134 -0
- package/src/Tests/ReproDelete20k.test.ts +62 -0
- package/src/Tests/SemanticValidators.test.ts +136 -0
- package/src/Tests/StableNodeIds.test.ts +210 -0
- package/src/Tests/StudioColorBloat.test.ts +25 -0
- package/src/Tests/StudioDebugText.test.ts +27 -0
- package/src/Tests/StudioTrailingChar.test.ts +25 -0
- package/src/Tests/StudioValidText.test.ts +25 -0
- package/src/Tests/UrlImgBug.test.ts +23 -0
- package/src/Tests/VisualBuilderFidelity.test.ts +105 -0
- package/src/Tests/referenceDocument.ts +119 -0
- package/src/Transactions/Transaction.ts +176 -0
- package/src/Transactions/UndoManager.ts +111 -0
- package/src/Transactions/index.ts +3 -0
- package/src/Transformers/ASTOptimizer.ts +315 -0
- package/src/Transformers/GradientTransformer.ts +143 -0
- package/src/Transformers/GrowTransformer.ts +115 -0
- package/src/Transformers/RainbowTransformer.ts +121 -0
- package/src/Transformers/SineWaveTransformer.ts +130 -0
- package/src/Transformers/Transformer.ts +22 -0
- package/src/Types/core.ts +270 -0
- package/src/Types/diagnostics.ts +156 -0
- package/src/Types/index.ts +23 -0
- package/src/Types/operations.ts +180 -0
- package/src/Types/queries.ts +121 -0
- package/src/Types/symbols.ts +69 -0
- package/src/Types/tokens.ts +186 -0
- package/src/Utils/BBCodeGenerator.ts +126 -0
- package/src/Utils/ColorMath.ts +276 -0
- package/src/Utils/color.ts +112 -0
- package/src/Utils/dom-to-svg.test.ts +86 -0
- package/src/Utils/dom-to-svg.ts +615 -0
- package/src/Utils/treeTransformers.ts +717 -0
- package/src/Visitors/BBBlocksExporter.ts +69 -0
- package/src/Visitors/BBCodeExporter.ts +318 -0
- package/src/Visitors/BlockPatcher.ts +963 -0
- package/src/Visitors/DOMMorpher.ts +134 -0
- package/src/Visitors/HTMLRenderer.ts +1077 -0
- package/src/Visitors/JSONExporter.ts +66 -0
- package/src/Visitors/MarkdownExporter.ts +99 -0
- package/src/Visitors/SVGRenderer.ts +35 -0
- package/src/Visitors/TiptapExporter.ts +145 -0
- package/src/Visitors/Visitor.ts +48 -0
- package/src/Visitors/index.ts +9 -0
- package/src/Visuals/BoxDrawer.ts +175 -0
- package/src/Visuals/index.ts +42 -0
- package/src/Visuals/lyne.css +1027 -0
- package/src/Visuals/osu.css +257 -0
- 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
|
+
}
|