@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,276 @@
1
+ // ============================================================
2
+ // Quasar Studio — Color Math & Utilities
3
+ // Ported from TextStudio to provide native algorithms for Quasar
4
+ // ============================================================
5
+
6
+ export type Easing = "linear" | "easeIn" | "easeOut" | "easeInOut" | "easeIO" | "parabola" | "half-parabola" | string // string allows 'bezier(0.25,0.1,0.25,1.0)'
7
+
8
+ export function solveCubicBezierY(x: number, x1: number, y1: number, x2: number, y2: number): number {
9
+ if (x <= 0) return 0;
10
+ if (x >= 1) return 1;
11
+ let lower = 0;
12
+ let upper = 1;
13
+ let t = 0.5;
14
+ for (let i = 0; i < 15; i++) {
15
+ const currentX = 3 * Math.pow(1 - t, 2) * t * x1 + 3 * (1 - t) * Math.pow(t, 2) * x2 + Math.pow(t, 3);
16
+ if (Math.abs(currentX - x) < 0.001) break;
17
+ if (currentX < x) lower = t;
18
+ else upper = t;
19
+ t = (lower + upper) / 2;
20
+ }
21
+ return 3 * Math.pow(1 - t, 2) * t * y1 + 3 * (1 - t) * Math.pow(t, 2) * y2 + Math.pow(t, 3);
22
+ }
23
+
24
+ // Bezier easing approximation or simple evaluation
25
+ export function ease(t: number, easing: Easing, center: number = 0.5, power: number = 2): number {
26
+ t = Math.max(0, Math.min(1, t))
27
+
28
+ if (easing.startsWith('expr(')) {
29
+ // to be fixed, dont touch or use it
30
+ const expr = easing.slice(5, -1);
31
+ try {
32
+ const evaluate = new Function('x', `with(Math) { return ${expr}; }`);
33
+ return Number(evaluate(t)) || 0;
34
+ } catch (e) {
35
+ return t;
36
+ }
37
+ }
38
+
39
+ if (easing.startsWith('bezier')) {
40
+ const match = easing.match(/bezier\(([^,]+),([^,]+),([^,]+),([^)]+)\)/);
41
+ if (match) {
42
+ const x1 = parseFloat(match[1]);
43
+ const y1 = parseFloat(match[2]);
44
+ const x2 = parseFloat(match[3]);
45
+ const y2 = parseFloat(match[4]);
46
+ return solveCubicBezierY(t, x1, y1, x2, y2);
47
+ }
48
+ }
49
+ switch (easing) {
50
+ case "easeIn":
51
+ case "half-parabola": return Math.pow(t, power)
52
+ case "easeOut": return t * (2 - t)
53
+ case "easeInOut":
54
+ case "easeIO":
55
+ return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
56
+ case "parabola":
57
+ if (center <= 0) return Math.pow(t, power);
58
+ if (center >= 1) return Math.pow(1 - t, power);
59
+ return t < center
60
+ ? Math.pow((center - t) / center, power)
61
+ : Math.pow((t - center) / (1 - center), power);
62
+ default: return t
63
+ }
64
+ }
65
+
66
+
67
+ export function hslToHex(h: number, s: number, l: number): string {
68
+ l /= 100
69
+ const a = (s * Math.min(l, 1 - l)) / 100
70
+ const f = (n: number) => {
71
+ const k = (n + h / 30) % 12
72
+ const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)
73
+ return Math.round(255 * color).toString(16).padStart(2, "0")
74
+ }
75
+ return `#${f(0)}${f(8)}${f(4)}`
76
+ }
77
+
78
+ export function hexToRgb(hex: string): [number, number, number] {
79
+ const h = hex.replace("#", "")
80
+ if (h.length === 3) {
81
+ return [
82
+ parseInt(h[0] + h[0], 16),
83
+ parseInt(h[1] + h[1], 16),
84
+ parseInt(h[2] + h[2], 16),
85
+ ]
86
+ }
87
+ return [
88
+ parseInt(h.slice(0, 2), 16),
89
+ parseInt(h.slice(2, 4), 16),
90
+ parseInt(h.slice(4, 6), 16),
91
+ ]
92
+ }
93
+
94
+ export function hexToHsl(hex: string): [number, number, number] {
95
+ let [r, g, b] = hexToRgb(hex);
96
+ r /= 255;
97
+ g /= 255;
98
+ b /= 255;
99
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
100
+ let h = 0, s = 0, l = (max + min) / 2;
101
+
102
+ if (max !== min) {
103
+ const d = max - min;
104
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
105
+ switch (max) {
106
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
107
+ case g: h = (b - r) / d + 2; break;
108
+ case b: h = (r - g) / d + 4; break;
109
+ }
110
+ h /= 6;
111
+ }
112
+ return [h * 360, s * 100, l * 100];
113
+ }
114
+
115
+ export function mixHex(color1: string, color2: string, weight: number): string {
116
+ const [r1, g1, b1] = hexToRgb(color1)
117
+ const [r2, g2, b2] = hexToRgb(color2)
118
+ const w = Math.max(0, Math.min(1, weight))
119
+ const r = Math.round(r1 + (r2 - r1) * w)
120
+ const g = Math.round(g1 + (g2 - g1) * w)
121
+ const b = Math.round(b1 + (b2 - b1) * w)
122
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`
123
+ }
124
+
125
+ export function mixMultiple(colors: string[], weight: number): string {
126
+ if (colors.length === 0) return "#FFFFFF"
127
+ if (colors.length === 1) return colors[0]
128
+ weight = Math.max(0, Math.min(1, weight || 0))
129
+ const scaled = weight * (colors.length - 1)
130
+ const index = Math.floor(scaled)
131
+ if (index >= colors.length - 1) return colors[colors.length - 1]
132
+ const frac = scaled - index
133
+ return mixHex(colors[index], colors[index + 1], frac)
134
+ }
135
+
136
+ export interface ColorStop {
137
+ color: string;
138
+ position: number; // 0 to 1
139
+ }
140
+
141
+ export function mixMultipleStops(stops: ColorStop[], weight: number): string {
142
+ if (stops.length === 0) return "#FFFFFF";
143
+ if (stops.length === 1) return stops[0].color;
144
+ weight = Math.max(0, Math.min(1, weight || 0));
145
+
146
+ let i = 0;
147
+ while (i < stops.length - 1 && stops[i + 1].position <= weight) {
148
+ i++;
149
+ }
150
+
151
+ if (i >= stops.length - 1) return stops[stops.length - 1].color;
152
+ if (weight <= stops[i].position) return stops[i].color;
153
+
154
+ const range = stops[i + 1].position - stops[i].position;
155
+ const frac = (weight - stops[i].position) / range;
156
+ return mixHex(stops[i].color, stops[i + 1].color, frac);
157
+ }
158
+
159
+ // ═══════════════════════════════════════════════════════════════
160
+ // OKLab — Perceptual Color Space
161
+ // ═══════════════════════════════════════════════════════════════
162
+ //
163
+ // OKLab is a color space designed by Björn Ottosson in 2020 to be
164
+ // perceptually uniform: equal Euclidean distance ≈ equal perceived
165
+ // color difference. This makes it ideal for gradient detection.
166
+ //
167
+ // Reference: https://bottosson.github.io/posts/oklab/
168
+ //
169
+ // ═══════════════════════════════════════════════════════════════
170
+
171
+ // Linear sRGB → LMS (cone response) matrix
172
+ const SRGB_TO_LMS = [
173
+ 0.4122214708, 0.5363325363, 0.0514459929,
174
+ 0.2119034982, 0.6806995451, 0.1073969566,
175
+ 0.0883024619, 0.2817188376, 0.6299787005,
176
+ ] as const
177
+
178
+ // LMS → OKLab matrix
179
+ const LMS_TO_OKLAB = [
180
+ 0.2104542553, 0.7936177850, -0.0040720468,
181
+ 1.9779984951, -2.4285922050, 0.4505937099,
182
+ 0.0259040371, 0.7827717662, -0.8086757660,
183
+ ] as const
184
+
185
+ /**
186
+ * Convert sRGB [0-1] to linear RGB (gamma expansion).
187
+ */
188
+ function srgbToLinear(c: number): number {
189
+ return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
190
+ }
191
+
192
+ /**
193
+ * Convert linear RGB to sRGB (gamma compression for display).
194
+ */
195
+ function linearToSrgb(c: number): number {
196
+ return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055
197
+ }
198
+
199
+ /**
200
+ * Convert a hex colour to OKLab [L, a, b] components.
201
+ * L (lightness) in [0, 1], a/b (opponent axes) roughly in [-0.4, 0.4].
202
+ */
203
+ export function hexToOklab(hex: string): [number, number, number] {
204
+ const [r255, g255, b255] = hexToRgb(hex)
205
+ const r = srgbToLinear(r255 / 255)
206
+ const g = srgbToLinear(g255 / 255)
207
+ const b = srgbToLinear(b255 / 255)
208
+
209
+ // Linear sRGB → LMS
210
+ const l_ = r * SRGB_TO_LMS[0] + g * SRGB_TO_LMS[1] + b * SRGB_TO_LMS[2]
211
+ const m_ = r * SRGB_TO_LMS[3] + g * SRGB_TO_LMS[4] + b * SRGB_TO_LMS[5]
212
+ const s_ = r * SRGB_TO_LMS[6] + g * SRGB_TO_LMS[7] + b * SRGB_TO_LMS[8]
213
+
214
+ // Non-linear transform (cube root)
215
+ const l = Math.cbrt(l_)
216
+ const m = Math.cbrt(m_)
217
+ const s = Math.cbrt(s_)
218
+
219
+ // LMS → OKLab
220
+ return [
221
+ l * LMS_TO_OKLAB[0] + m * LMS_TO_OKLAB[1] + s * LMS_TO_OKLAB[2],
222
+ l * LMS_TO_OKLAB[3] + m * LMS_TO_OKLAB[4] + s * LMS_TO_OKLAB[5],
223
+ l * LMS_TO_OKLAB[6] + m * LMS_TO_OKLAB[7] + s * LMS_TO_OKLAB[8],
224
+ ]
225
+ }
226
+
227
+ /**
228
+ * Calculate the perceptual distance between two hex colours using OKLab.
229
+ * Returns a value in [0, ~0.4] where:
230
+ * < 0.02 ≈ imperceptible difference
231
+ * < 0.05 ≈ small but noticeable
232
+ * > 0.1 ≈ very noticeable
233
+ */
234
+ export function perceptualDistance(hex1: string, hex2: string): number {
235
+ const [L1, a1, b1] = hexToOklab(hex1)
236
+ const [L2, a2, b2] = hexToOklab(hex2)
237
+ const dL = L2 - L1
238
+ const da = a2 - a1
239
+ const db = b2 - b1
240
+ return Math.sqrt(dL * dL + da * da + db * db)
241
+ }
242
+
243
+ /**
244
+ * Mix two hex colours in OKLab space for perceptually uniform interpolation.
245
+ */
246
+ export function mixHexOklab(color1: string, color2: string, weight: number): string {
247
+ const [L1, a1, b1] = hexToOklab(color1)
248
+ const [L2, a2, b2] = hexToOklab(color2)
249
+ const w = Math.max(0, Math.min(1, weight))
250
+
251
+ const L = L1 + (L2 - L1) * w
252
+ const a = a1 + (a2 - a1) * w
253
+ const b = b1 + (b2 - b1) * w
254
+
255
+ // OKLab → LMS
256
+ const l_ = L + a * 0.3963377774 + b * 0.2158037573
257
+ const m_ = L - a * 0.1055613458 - b * 0.0638541728
258
+ const s_ = L - a * 0.0894841775 - b * 1.2914855480
259
+
260
+ // Cube (inverse of cube root)
261
+ const l = l_ * l_ * l_
262
+ const m = m_ * m_ * m_
263
+ const s = s_ * s_ * s_
264
+
265
+ // LMS → linear sRGB
266
+ const rLin = l * 4.0767416621 + m * -3.3077115913 + s * 0.2309699292
267
+ const gLin = l * -1.2684380046 + m * 2.6097574011 + s * -0.3413193965
268
+ const bLin = l * -0.0041960863 + m * -0.7034186147 + s * 1.7076147010
269
+
270
+ // Linear sRGB → gamma-corrected sRGB → hex
271
+ const r255 = Math.round(linearToSrgb(rLin) * 255)
272
+ const g255 = Math.round(linearToSrgb(gLin) * 255)
273
+ const b255 = Math.round(linearToSrgb(bLin) * 255)
274
+
275
+ return `#${r255.toString(16).padStart(2, '0')}${g255.toString(16).padStart(2, '0')}${b255.toString(16).padStart(2, '0')}`
276
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Quasar — Color Utilities
3
+ *
4
+ * Standalone color functions for gradient/grow/exporter interpolation.
5
+ * No external dependencies — pure math.
6
+ *
7
+ * Ported from TextStudio/lib/color.ts (originally backed by culori).
8
+ * These are the minimal subset needed for internal tag export.
9
+ */
10
+
11
+ // ─── Types ──────────────────────────────────────────────────
12
+
13
+ export type Easing = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut'
14
+
15
+ // ─── Parsing ────────────────────────────────────────────────
16
+
17
+ /** Parse "#RRGGBB" to [r, g, b] each in [0, 255] */
18
+ function parseHex(hex: string): [number, number, number] {
19
+ const h = hex.replace('#', '')
20
+ return [
21
+ parseInt(h.substring(0, 2), 16),
22
+ parseInt(h.substring(2, 4), 16),
23
+ parseInt(h.substring(4, 6), 16),
24
+ ]
25
+ }
26
+
27
+ /** Format [r, g, b] (0-255) to "#RRGGBB" */
28
+ function toHex(r: number, g: number, b: number): string {
29
+ const clamp = (v: number) => Math.max(0, Math.min(255, Math.round(v)))
30
+ return (
31
+ '#' +
32
+ clamp(r).toString(16).padStart(2, '0') +
33
+ clamp(g).toString(16).padStart(2, '0') +
34
+ clamp(b).toString(16).padStart(2, '0')
35
+ ).toUpperCase()
36
+ }
37
+
38
+ // ─── Interpolation ──────────────────────────────────────────
39
+
40
+ /**
41
+ * Mix two hex colors via linear RGB interpolation.
42
+ * t in [0, 1] — 0 = from, 1 = to.
43
+ */
44
+ export function mixHex(from: string, to: string, t: number): string {
45
+ const [r1, g1, b1] = parseHex(from)
46
+ const [r2, g2, b2] = parseHex(to)
47
+ return toHex(
48
+ r1 + (r2 - r1) * t,
49
+ g1 + (g2 - g1) * t,
50
+ b1 + (b2 - b1) * t,
51
+ )
52
+ }
53
+
54
+ /**
55
+ * Mix across multiple hex colors.
56
+ * t in [0, 1] maps across the color array linearly.
57
+ */
58
+ export function mixMultiple(colors: string[], t: number): string {
59
+ if (colors.length === 0) return '#FFFFFF'
60
+ if (colors.length === 1) return colors[0]
61
+ const segments = colors.length - 1
62
+ const segment = Math.min(Math.floor(t * segments), segments - 1)
63
+ const localFactor = t * segments - segment
64
+ return mixHex(colors[segment], colors[segment + 1], localFactor)
65
+ }
66
+
67
+ // ─── HSL → Hex ──────────────────────────────────────────────
68
+
69
+ /**
70
+ * Convert HSL to hex string.
71
+ * @param h hue in [0, 360]
72
+ * @param s saturation in [0, 1]
73
+ * @param l lightness in [0, 1]
74
+ */
75
+ export function hslToHex(h: number, s: number, l: number): string {
76
+ const hue = ((h % 360) + 360) % 360
77
+ const sat = Math.max(0, Math.min(1, s))
78
+ const lit = Math.max(0, Math.min(1, l))
79
+
80
+ const c = (1 - Math.abs(2 * lit - 1)) * sat
81
+ const x = c * (1 - Math.abs(((hue / 60) % 2) - 1))
82
+ const m = lit - c / 2
83
+
84
+ let r = 0, g = 0, b = 0
85
+ if (hue < 60) { r = c; g = x; }
86
+ else if (hue < 120) { r = x; g = c; }
87
+ else if (hue < 180) { g = c; b = x; }
88
+ else if (hue < 240) { g = x; b = c; }
89
+ else if (hue < 300) { r = x; b = c; }
90
+ else { r = c; b = x; }
91
+
92
+ return toHex((r + m) * 255, (g + m) * 255, (b + m) * 255)
93
+ }
94
+
95
+ // ─── Easing ─────────────────────────────────────────────────
96
+
97
+ export function clamp(n: number, min = 0, max = 1): number {
98
+ return Math.min(max, Math.max(min, n))
99
+ }
100
+
101
+ export function ease(t: number, kind: Easing): number {
102
+ switch (kind) {
103
+ case 'easeIn':
104
+ return t * t
105
+ case 'easeOut':
106
+ return 1 - (1 - t) * (1 - t)
107
+ case 'easeInOut':
108
+ return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2
109
+ default:
110
+ return t
111
+ }
112
+ }
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
+ import { domToSVGResult } from './dom-to-svg'
3
+
4
+ /**
5
+ * El vectorizador del Studio Vector no debe exportar los overlays de EDICIÓN
6
+ * del imagemap (las regiones clicables ni su tooltip de hover): son UI del
7
+ * preview, no contenido real. Antes el skip buscaba la clase legacy
8
+ * `bb-imagemap-area`, pero el HTMLRenderer genera `imagemap-area
9
+ * bbcode-imap-area` — las clases no coincidían y las regiones se colaban en
10
+ * el SVG. El contenedor y la imagen SÍ deben salir.
11
+ */
12
+
13
+ const IMAGEMAP_HTML = `
14
+ <div class="imagemap-container bbcode-imagemap" style="position:relative;display:inline-block;width:200px;height:100px;">
15
+ <img src="https://example.com/map.png" alt="imagemap" style="width:200px;height:100px;display:block;">
16
+ <a class="imagemap-area bbcode-imap-area" href="https://example.com/1" style="position:absolute;left:10%;top:10%;width:30%;height:30%;"></a>
17
+ <a class="imagemap-area bbcode-imap-area" href="https://example.com/2" style="position:absolute;left:50%;top:10%;width:30%;height:30%;"></a>
18
+ <div class="bb-imagemap-tooltip" style="position:absolute;">tooltip</div>
19
+ </div>
20
+ `
21
+
22
+ /** jsdom no hace layout: rects fake según la posición/el tamaño declarados. */
23
+ function mockLayout(root: HTMLElement) {
24
+ const rectFor = (el: HTMLElement): DOMRect => {
25
+ const style = window.getComputedStyle(el)
26
+ const left = parseFloat(style.left) || 0
27
+ const top = parseFloat(style.top) || 0
28
+ const width = parseFloat(style.width) || (el.tagName === 'IMG' ? 200 : 100)
29
+ const height = parseFloat(style.height) || (el.tagName === 'IMG' ? 100 : 50)
30
+ return new DOMRect(left, top, width, height)
31
+ }
32
+
33
+ const originals = new Map<Element, () => DOMRect>()
34
+ const walker = (node: Element | null) => {
35
+ if (!node) return
36
+ originals.set(node, node.getBoundingClientRect.bind(node))
37
+ node.getBoundingClientRect = () => rectFor(node as HTMLElement)
38
+ Array.from(node.children).forEach(walker)
39
+ }
40
+ walker(root)
41
+
42
+ return () => originals.forEach((orig, el) => {
43
+ el.getBoundingClientRect = orig
44
+ })
45
+ }
46
+
47
+ describe('dom-to-svg · imagemap overlays', () => {
48
+ let restore: () => void
49
+
50
+ beforeEach(() => {
51
+ document.body.innerHTML = IMAGEMAP_HTML
52
+ restore = mockLayout(document.body.firstElementChild as HTMLElement)
53
+ })
54
+
55
+ afterEach(() => {
56
+ restore()
57
+ document.body.innerHTML = ''
58
+ })
59
+
60
+ it('no exporta las regiones clicables (imagemap-area bbcode-imap-area)', () => {
61
+ const result = domToSVGResult(document.body.firstElementChild as HTMLElement, {
62
+ backgroundColor: '#1c1719',
63
+ scale: 1,
64
+ })
65
+ // Las áreas llevan href de las regiones: su ausencia es la prueba.
66
+ expect(result.svg).not.toContain('example.com/1')
67
+ expect(result.svg).not.toContain('example.com/2')
68
+ })
69
+
70
+ it('no exporta el tooltip de hover (bb-imagemap-tooltip)', () => {
71
+ const result = domToSVGResult(document.body.firstElementChild as HTMLElement, {
72
+ backgroundColor: '#1c1719',
73
+ scale: 1,
74
+ })
75
+ expect(result.svg).not.toContain('tooltip')
76
+ })
77
+
78
+ it('sí exporta la imagen real del imagemap', () => {
79
+ const result = domToSVGResult(document.body.firstElementChild as HTMLElement, {
80
+ backgroundColor: '#1c1719',
81
+ scale: 1,
82
+ })
83
+ expect(result.svg).toContain('map.png')
84
+ expect(result.svg).toContain('<image')
85
+ })
86
+ })