@miliastry/quasar 1.0.6 → 1.1.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 (273) hide show
  1. package/dist/Visuals/lyne.css +117 -6
  2. package/dist/Visuals/osu.css +36 -5
  3. package/dist/index.js +9664 -3282
  4. package/dist/index.mjs +9535 -3283
  5. package/dist/types/Analysis/Contracts/AnalysisReport.d.ts +22 -0
  6. package/dist/types/Analysis/Contracts/Contribution.d.ts +68 -0
  7. package/dist/types/Analysis/Contracts/Pass.d.ts +59 -0
  8. package/dist/types/Analysis/Contracts/PipelineContext.d.ts +34 -0
  9. package/dist/types/Analysis/Passes/Analysis/ColorUsageAnalyzer.d.ts +56 -0
  10. package/dist/types/Analysis/Passes/Analysis/GradientAnalyzer.d.ts +111 -0
  11. package/dist/types/Analysis/Passes/Analysis/MergeableColorAnalyzer.d.ts +26 -0
  12. package/dist/types/Analysis/Passes/Analysis/RainbowAnalyzer.d.ts +47 -0
  13. package/dist/types/Analysis/Passes/Analysis/SymbolAnalyzer.d.ts +89 -0
  14. package/dist/types/Analysis/Passes/Analysis/WaveAnalyzer.d.ts +44 -0
  15. package/dist/types/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.d.ts +4 -0
  16. package/dist/types/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.d.ts +4 -0
  17. package/dist/types/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.d.ts +4 -0
  18. package/dist/types/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.d.ts +4 -0
  19. package/dist/types/Analysis/Passes/Decision/DefaultDecision.d.ts +30 -0
  20. package/dist/types/Analysis/Passes/Decision/PaletteRemapDecision.d.ts +169 -0
  21. package/dist/types/Analysis/Passes/Decision/__tests__/DefaultDecision.test.d.ts +4 -0
  22. package/dist/types/Analysis/Passes/Transform/CollapseGradientTransform.d.ts +37 -0
  23. package/dist/types/Analysis/Passes/Transform/MergeColorsTransform.d.ts +28 -0
  24. package/dist/types/Analysis/Passes/Transform/RainbowCollapseTransform.d.ts +19 -0
  25. package/dist/types/Analysis/Passes/Transform/WaveCollapseTransform.d.ts +20 -0
  26. package/dist/types/Analysis/Passes/Utility/CharacterCountAnalyzer.d.ts +19 -0
  27. package/dist/types/Analysis/Pipeline/Pipeline.d.ts +69 -0
  28. package/dist/types/Analysis/Pipeline/PipelineBuilder.d.ts +31 -0
  29. package/dist/types/Analysis/Pipeline/PipelineStage.d.ts +17 -0
  30. package/dist/types/Analysis/Utils/color-utils.d.ts +60 -0
  31. package/dist/types/Analysis/__tests__/Integration.test.d.ts +8 -0
  32. package/dist/types/Analysis/__tests__/PaletteRemap.test.d.ts +14 -0
  33. package/dist/types/Analysis/__tests__/Pipeline.test.d.ts +10 -0
  34. package/dist/types/Analysis/__tests__/RoundTrip.test.d.ts +30 -0
  35. package/dist/types/Analysis/__tests__/SymbolAnalyzer.test.d.ts +11 -0
  36. package/dist/types/Analysis/index.d.ts +40 -0
  37. package/dist/types/BBCode/BBCodeDocumentModel.d.ts +92 -0
  38. package/dist/types/BBCode/BBCodeToGreenNode.d.ts +111 -0
  39. package/dist/types/BBCode/Parser.d.ts +78 -0
  40. package/dist/types/BBCode/index.d.ts +4 -0
  41. package/dist/types/Collab/positions.d.ts +55 -0
  42. package/dist/types/Commands/Command.d.ts +37 -0
  43. package/dist/types/Commands/CommandRegistry.d.ts +48 -0
  44. package/dist/types/Commands/DeleteNode.d.ts +7 -0
  45. package/dist/types/Commands/InsertText.d.ts +8 -0
  46. package/dist/types/Commands/SplitMerge.d.ts +9 -0
  47. package/dist/types/Commands/WrapInTag.d.ts +8 -0
  48. package/dist/types/Commands/index.d.ts +6 -0
  49. package/dist/types/Diff/TreeDiffer.d.ts +52 -0
  50. package/dist/types/Diff/__tests__/TreeDiffer.test.d.ts +1 -0
  51. package/dist/types/Diff/index.d.ts +2 -0
  52. package/dist/types/Edits/EditPlan.d.ts +144 -0
  53. package/dist/types/Edits/Optimizer.d.ts +78 -0
  54. package/dist/types/Edits/Rules/Rule.d.ts +91 -0
  55. package/dist/types/Edits/Rules/dropEmptyTags.d.ts +40 -0
  56. package/dist/types/Edits/Rules/dropRedundantNesting.d.ts +59 -0
  57. package/dist/types/Edits/Rules/index.d.ts +9 -0
  58. package/dist/types/Edits/Rules/mergeAdjacent.d.ts +93 -0
  59. package/dist/types/Edits/Rules/reorderWrappers.d.ts +42 -0
  60. package/dist/types/Edits/Rules/shortenHex.d.ts +30 -0
  61. package/dist/types/Edits/Rules/tagValue.d.ts +56 -0
  62. package/dist/types/Edits/Rules/unwrapInvisibleColor.d.ts +47 -0
  63. package/dist/types/Edits/applyEdits.d.ts +36 -0
  64. package/dist/types/Edits/index.d.ts +6 -0
  65. package/dist/types/Events/EventBus.d.ts +103 -0
  66. package/dist/types/Events/index.d.ts +2 -0
  67. package/dist/types/Formatter/Formatter.d.ts +32 -0
  68. package/dist/types/Formatter/index.d.ts +2 -0
  69. package/dist/types/HTML/HTMLDocumentModel.d.ts +13 -0
  70. package/dist/types/HTML/HTMLToGreenNode.d.ts +4 -0
  71. package/dist/types/Incremental/BracketIndex.d.ts +101 -0
  72. package/dist/types/Incremental/ChangeTracker.d.ts +69 -0
  73. package/dist/types/Incremental/IncrementalParser.d.ts +246 -0
  74. package/dist/types/Incremental/__tests__/BracketIndex.test.d.ts +1 -0
  75. package/dist/types/Incremental/__tests__/IncrementalParser.test.d.ts +1 -0
  76. package/dist/types/Incremental/index.d.ts +4 -0
  77. package/dist/types/Lexer/BBCodeLexer.d.ts +74 -0
  78. package/dist/types/Lexer/Lexer.d.ts +54 -0
  79. package/dist/types/Lexer/index.d.ts +4 -0
  80. package/dist/types/Linter/Linter.d.ts +58 -0
  81. package/dist/types/Linter/index.d.ts +2 -0
  82. package/dist/types/Markdown/MarkdownAST.d.ts +122 -0
  83. package/dist/types/Markdown/MarkdownDocumentModel.d.ts +15 -0
  84. package/dist/types/Markdown/MarkdownLexer.d.ts +155 -0
  85. package/dist/types/Markdown/MarkdownParser.d.ts +42 -0
  86. package/dist/types/Markdown/MarkdownToGreenNode.d.ts +6 -0
  87. package/dist/types/MilHibri/MilHibriDocumentModel.d.ts +21 -0
  88. package/dist/types/Model/DocumentModel.d.ts +318 -0
  89. package/dist/types/Model/NodeFactory.d.ts +29 -0
  90. package/dist/types/Model/TagRegistry.d.ts +165 -0
  91. package/dist/types/Model/index.d.ts +5 -0
  92. package/dist/types/Plugins/PluginAPI.d.ts +44 -0
  93. package/dist/types/Plugins/PluginRegistry.d.ts +81 -0
  94. package/dist/types/Plugins/index.d.ts +3 -0
  95. package/dist/types/Queries/QueryEngine.d.ts +32 -0
  96. package/dist/types/Queries/index.d.ts +1 -0
  97. package/dist/types/Reconciler/SurgicalReconciler.d.ts +23 -0
  98. package/dist/types/RenderPipeline/RenderPipeline.d.ts +46 -0
  99. package/dist/types/RenderPipeline/RenderTree.d.ts +55 -0
  100. package/dist/types/RenderPipeline/index.d.ts +4 -0
  101. package/dist/types/Repair/NestingRepair.d.ts +48 -0
  102. package/dist/types/Repair/index.d.ts +1 -0
  103. package/dist/types/Semantic/SemanticAnalyzer.d.ts +321 -0
  104. package/dist/types/Semantic/index.d.ts +2 -0
  105. package/dist/types/Symbols/SymbolTable.d.ts +47 -0
  106. package/dist/types/Symbols/index.d.ts +1 -0
  107. package/dist/types/Syntax/GreenNode.d.ts +154 -0
  108. package/dist/types/Syntax/GreenNodePool.d.ts +134 -0
  109. package/dist/types/Syntax/NodeMatcher.d.ts +75 -0
  110. package/dist/types/Syntax/RedNode.d.ts +280 -0
  111. package/dist/types/Syntax/RedNodeStore.d.ts +116 -0
  112. package/dist/types/Syntax/TreeBuilder.d.ts +54 -0
  113. package/dist/types/Syntax/__tests__/GreenNode.test.d.ts +1 -0
  114. package/dist/types/Syntax/__tests__/RedNode.test.d.ts +1 -0
  115. package/dist/types/Syntax/__tests__/RedNodeStore.test.d.ts +1 -0
  116. package/dist/types/Syntax/greenEdit.d.ts +64 -0
  117. package/dist/types/Syntax/hash.d.ts +17 -0
  118. package/dist/types/Syntax/index.d.ts +8 -0
  119. package/dist/types/Syntax/nodeAttr.d.ts +63 -0
  120. package/dist/types/Syntax/partition.d.ts +67 -0
  121. package/dist/types/Syntax/preserveNodeIds.d.ts +47 -0
  122. package/dist/types/Tokens/TokenResolver.d.ts +25 -0
  123. package/dist/types/Tokens/index.d.ts +1 -0
  124. package/dist/types/Transactions/Transaction.d.ts +36 -0
  125. package/dist/types/Transactions/UndoManager.d.ts +63 -0
  126. package/dist/types/Transactions/index.d.ts +3 -0
  127. package/dist/types/Transformers/ASTOptimizer.d.ts +34 -0
  128. package/dist/types/Transformers/GradientTransformer.d.ts +18 -0
  129. package/dist/types/Transformers/GrowTransformer.d.ts +16 -0
  130. package/dist/types/Transformers/RainbowTransformer.d.ts +17 -0
  131. package/dist/types/Transformers/SineWaveTransformer.d.ts +17 -0
  132. package/dist/types/Transformers/Transformer.d.ts +17 -0
  133. package/dist/types/Types/core.d.ts +104 -0
  134. package/dist/types/Types/diagnostics.d.ts +85 -0
  135. package/dist/types/Types/index.d.ts +11 -0
  136. package/dist/types/Types/operations.d.ts +109 -0
  137. package/dist/types/Types/queries.d.ts +67 -0
  138. package/dist/types/Types/symbols.d.ts +42 -0
  139. package/dist/types/Types/tokens.d.ts +75 -0
  140. package/dist/types/Utils/BBCodeGenerator.d.ts +21 -0
  141. package/dist/types/Utils/ColorMath.d.ts +92 -0
  142. package/dist/types/Utils/EffectMath.d.ts +643 -0
  143. package/dist/types/Utils/TagVocabulary.d.ts +104 -0
  144. package/dist/types/Utils/TagVocabulary.test.d.ts +1 -0
  145. package/dist/types/Utils/color.d.ts +29 -0
  146. package/dist/types/Utils/dom-to-svg.d.ts +28 -0
  147. package/dist/types/Utils/dom-to-svg.test.d.ts +1 -0
  148. package/dist/types/Utils/treeTransformers.d.ts +160 -0
  149. package/dist/types/Visitors/BBBlocksExporter.d.ts +27 -0
  150. package/dist/types/Visitors/BBCodeExporter.d.ts +90 -0
  151. package/dist/types/Visitors/BlockPatcher.d.ts +91 -0
  152. package/dist/types/Visitors/DOMMorpher.d.ts +13 -0
  153. package/dist/types/Visitors/HTMLRenderer.d.ts +380 -0
  154. package/dist/types/Visitors/JSONExporter.d.ts +42 -0
  155. package/dist/types/Visitors/MarkdownExporter.d.ts +28 -0
  156. package/dist/types/Visitors/SVGRenderer.d.ts +16 -0
  157. package/dist/types/Visitors/TiptapExporter.d.ts +40 -0
  158. package/dist/types/Visitors/Visitor.d.ts +35 -0
  159. package/dist/types/Visitors/index.d.ts +9 -0
  160. package/dist/types/Visuals/BoxDrawer.d.ts +49 -0
  161. package/dist/types/Visuals/index.d.ts +38 -0
  162. package/dist/types/index.d.ts +154 -0
  163. package/package.json +14 -10
  164. package/src/Analysis/Passes/Analysis/ColorUsageAnalyzer.ts +103 -0
  165. package/src/Analysis/Passes/Analysis/GradientAnalyzer.ts +294 -20
  166. package/src/Analysis/Passes/Analysis/SymbolAnalyzer.ts +257 -0
  167. package/src/Analysis/Passes/Decision/PaletteRemapDecision.ts +425 -0
  168. package/src/Analysis/Pipeline/Pipeline.ts +15 -2
  169. package/src/Analysis/index.ts +8 -2
  170. package/src/BBCode/BBCodeDocumentModel.ts +3 -0
  171. package/src/BBCode/BBCodeToGreenNode.ts +18 -12
  172. package/src/BBCode/Parser.ts +38 -1
  173. package/src/Edits/EditPlan.ts +290 -0
  174. package/src/Edits/Optimizer.ts +185 -0
  175. package/src/Edits/Rules/Rule.ts +145 -0
  176. package/src/Edits/Rules/dropEmptyTags.ts +110 -0
  177. package/src/Edits/Rules/dropRedundantNesting.ts +120 -0
  178. package/src/Edits/Rules/index.ts +28 -0
  179. package/src/Edits/Rules/mergeAdjacent.ts +224 -0
  180. package/src/Edits/Rules/reorderWrappers.ts +154 -0
  181. package/src/Edits/Rules/shortenHex.ts +79 -0
  182. package/src/Edits/Rules/tagValue.ts +105 -0
  183. package/src/Edits/Rules/unwrapInvisibleColor.ts +93 -0
  184. package/src/Edits/applyEdits.ts +54 -0
  185. package/src/Edits/index.ts +22 -0
  186. package/src/Events/EventBus.ts +18 -0
  187. package/src/HTML/HTMLToGreenNode.ts +185 -37
  188. package/src/Incremental/BracketIndex.ts +249 -0
  189. package/src/Incremental/IncrementalParser.ts +464 -41
  190. package/src/Lexer/BBCodeLexer.ts +25 -3
  191. package/src/Lexer/index.ts +1 -1
  192. package/src/Markdown/MarkdownAST.ts +65 -2
  193. package/src/Markdown/MarkdownLexer.ts +69 -14
  194. package/src/Markdown/MarkdownParser.ts +749 -62
  195. package/src/Markdown/MarkdownToGreenNode.ts +148 -27
  196. package/src/MilHibri/MilHibriDocumentModel.ts +56 -0
  197. package/src/Model/DocumentModel.ts +138 -5
  198. package/src/Model/TagRegistry.ts +129 -134
  199. package/src/Reconciler/SurgicalReconciler.ts +460 -0
  200. package/src/Repair/NestingRepair.ts +116 -0
  201. package/src/Repair/index.ts +1 -0
  202. package/src/Semantic/SemanticAnalyzer.ts +1802 -65
  203. package/src/Syntax/GreenNodePool.ts +12 -12
  204. package/src/Syntax/RedNode.ts +17 -3
  205. package/src/Syntax/RedNodeStore.ts +9 -1
  206. package/src/Syntax/nodeAttr.ts +172 -0
  207. package/src/Tokens/TokenResolver.ts +55 -0
  208. package/src/Tokens/index.ts +1 -0
  209. package/src/Types/core.ts +15 -0
  210. package/src/Utils/ColorMath.ts +276 -55
  211. package/src/Utils/EffectMath.ts +2462 -0
  212. package/src/Utils/TagVocabulary.ts +141 -0
  213. package/src/Utils/dom-to-svg.ts +28 -12
  214. package/src/Visitors/BBBlocksExporter.ts +15 -1
  215. package/src/Visitors/BBCodeExporter.ts +319 -32
  216. package/src/Visitors/BlockPatcher.ts +87 -11
  217. package/src/Visitors/DOMMorpher.ts +15 -5
  218. package/src/Visitors/HTMLRenderer.ts +374 -151
  219. package/src/Visitors/MarkdownExporter.ts +98 -41
  220. package/src/Visitors/TiptapExporter.ts +1 -0
  221. package/src/Visuals/index.ts +7 -4
  222. package/src/Visuals/lyne.css +117 -6
  223. package/src/Visuals/osu.css +36 -5
  224. package/src/index.ts +172 -5
  225. package/dist/index.d.mts +0 -4612
  226. package/dist/index.d.ts +0 -4612
  227. package/src/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.ts +0 -135
  228. package/src/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.ts +0 -84
  229. package/src/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.ts +0 -99
  230. package/src/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.ts +0 -119
  231. package/src/Analysis/Passes/Decision/__tests__/DefaultDecision.test.ts +0 -179
  232. package/src/Analysis/__tests__/Integration.test.ts +0 -162
  233. package/src/Analysis/__tests__/Pipeline.test.ts +0 -133
  234. package/src/Diff/__tests__/TreeDiffer.test.ts +0 -65
  235. package/src/Incremental/__tests__/IncrementalParser.test.ts +0 -164
  236. package/src/Syntax/__tests__/GreenNode.test.ts +0 -33
  237. package/src/Syntax/__tests__/RedNode.test.ts +0 -81
  238. package/src/Syntax/__tests__/RedNodeStore.test.ts +0 -104
  239. package/src/Tests/ASTOptimizerIdempotence.test.ts +0 -77
  240. package/src/Tests/BlockPatcher.test.ts +0 -437
  241. package/src/Tests/BlockPatcherWindowed.test.ts +0 -364
  242. package/src/Tests/BoxDrawer.test.ts +0 -217
  243. package/src/Tests/BoxRichTitle.test.ts +0 -105
  244. package/src/Tests/Chars500kBenchmark.test.ts +0 -151
  245. package/src/Tests/Chars500kEdits.test.ts +0 -321
  246. package/src/Tests/CollabPositions.test.ts +0 -146
  247. package/src/Tests/CompilerPathProfiling.test.ts +0 -186
  248. package/src/Tests/DOMMorpher.test.ts +0 -142
  249. package/src/Tests/DomPatchPerf.test.ts +0 -60
  250. package/src/Tests/EffectSegments.snapshot.json +0 -616
  251. package/src/Tests/EffectSegments.test.ts +0 -68
  252. package/src/Tests/FindNodeAtOffset.test.ts +0 -65
  253. package/src/Tests/Fuzzer.test.ts +0 -166
  254. package/src/Tests/GreenNodePool.test.ts +0 -153
  255. package/src/Tests/Lexer.test.ts +0 -238
  256. package/src/Tests/LyneMode.test.ts +0 -291
  257. package/src/Tests/ModelCoherence.test.ts +0 -180
  258. package/src/Tests/Partition.test.ts +0 -238
  259. package/src/Tests/PluginTags.test.ts +0 -150
  260. package/src/Tests/ProblematicSection.test.ts +0 -46
  261. package/src/Tests/ProblematicSectionHTML.test.ts +0 -58
  262. package/src/Tests/RedReuse.test.ts +0 -134
  263. package/src/Tests/ReproDelete20k.test.ts +0 -62
  264. package/src/Tests/SemanticValidators.test.ts +0 -136
  265. package/src/Tests/StableNodeIds.test.ts +0 -210
  266. package/src/Tests/StudioColorBloat.test.ts +0 -25
  267. package/src/Tests/StudioDebugText.test.ts +0 -27
  268. package/src/Tests/StudioTrailingChar.test.ts +0 -25
  269. package/src/Tests/StudioValidText.test.ts +0 -25
  270. package/src/Tests/UrlImgBug.test.ts +0 -23
  271. package/src/Tests/VisualBuilderFidelity.test.ts +0 -105
  272. package/src/Tests/referenceDocument.ts +0 -119
  273. package/src/Utils/dom-to-svg.test.ts +0 -86
@@ -21,19 +21,102 @@ export function solveCubicBezierY(x: number, x1: number, y1: number, x2: number,
21
21
  return 3 * Math.pow(1 - t, 2) * t * y1 + 3 * (1 - t) * Math.pow(t, 2) * y2 + Math.pow(t, 3);
22
22
  }
23
23
 
24
+ /**
25
+ * Set by EffectMath at module load. ColorMath cannot import it directly —
26
+ * EffectMath depends on ColorMath — and `ease` is the only place that
27
+ * needs it, so the dependency is injected rather than inverted.
28
+ */
29
+ type ExpressionFn = (vars: Record<string, number>) => number
30
+ let compileExpressionRef: ((src: string) => ExpressionFn | null) | null = null
31
+
32
+ /** @internal Wire the expression compiler into `ease`. */
33
+ export function __setExpressionCompiler(fn: (src: string) => ExpressionFn | null): void {
34
+ compileExpressionRef = fn
35
+ }
36
+
37
+ /**
38
+ * Resuelve una vez todo lo que en `ease` no depende de `t`, y devuelve la
39
+ * función que sí lo hace.
40
+ *
41
+ * `evaluateEffect` llama a `ease` UNA VEZ POR CARÁCTER con la misma cadena de
42
+ * suavizado. Para `bezier(a,b,c,d)` eso significaba volver a lanzar la
43
+ * expresión regular y cuatro `parseFloat` sobre la misma cadena en cada
44
+ * carácter; para `expr(...)`, un `slice` por carácter. Compilar arriba del
45
+ * bucle deja el trabajo por carácter en la aritmética que de verdad varía.
46
+ *
47
+ * Medido sobre 150 líneas de `[gradient …;easing=bezier(...)]` (19 KiB): el
48
+ * render pasó de 16,4 ms a 1,2 ms.
49
+ */
50
+ export function compileEase(
51
+ easing: Easing,
52
+ center: number = 0.5,
53
+ power: number = 2,
54
+ ): (t: number) => number {
55
+ if (typeof easing !== 'string' || easing === 'linear') return identityEase
56
+
57
+ if (easing.startsWith('expr(')) {
58
+ const fn = compileExpressionRef?.(easing.slice(5, -1))
59
+ if (!fn) return identityEase
60
+ return (t) => {
61
+ const u = clamp01(t)
62
+ const out = fn({ u, t: u, x: u, i: 0, n: 1, line: 0, lines: 1, col: 0, cols: 1, word: 0, words: 1, rnd: 0 })
63
+ return Number.isFinite(out) ? out : u
64
+ }
65
+ }
66
+
67
+ if (easing.startsWith('bezier')) {
68
+ const match = easing.match(BEZIER_RE)
69
+ if (match) {
70
+ const x1 = parseFloat(match[1])
71
+ const y1 = parseFloat(match[2])
72
+ const x2 = parseFloat(match[3])
73
+ const y2 = parseFloat(match[4])
74
+ return (t) => solveCubicBezierY(clamp01(t), x1, y1, x2, y2)
75
+ }
76
+ return identityEase
77
+ }
78
+
79
+ switch (easing) {
80
+ case "easeIn":
81
+ case "ease-in":
82
+ case "half-parabola": return (t) => Math.pow(clamp01(t), power)
83
+ case "easeOut":
84
+ case "ease-out": return (t) => { const u = clamp01(t); return u * (2 - u) }
85
+ case "easeInOut":
86
+ case "ease-in-out":
87
+ case "easeIO":
88
+ return (t) => { const u = clamp01(t); return u < 0.5 ? 2 * u * u : -1 + (4 - 2 * u) * u }
89
+ case "parabola":
90
+ if (center <= 0) return (t) => Math.pow(clamp01(t), power)
91
+ if (center >= 1) return (t) => Math.pow(1 - clamp01(t), power)
92
+ return (t) => {
93
+ const u = clamp01(t)
94
+ return u < center
95
+ ? Math.pow((center - u) / center, power)
96
+ : Math.pow((u - center) / (1 - center), power)
97
+ }
98
+ default: return identityEase
99
+ }
100
+ }
101
+
102
+ const BEZIER_RE = /bezier\(([^,]+),([^,]+),([^,]+),([^)]+)\)/
103
+ function clamp01(t: number): number { return t < 0 ? 0 : t > 1 ? 1 : t }
104
+ function identityEase(t: number): number { return clamp01(t) }
105
+
24
106
  // Bezier easing approximation or simple evaluation
25
107
  export function ease(t: number, easing: Easing, center: number = 0.5, power: number = 2): number {
26
108
  t = Math.max(0, Math.min(1, t))
27
-
109
+ if (typeof easing !== 'string' || easing === 'linear') return t
110
+
28
111
  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
- }
112
+ // `easing` can arrive straight from a BBCode attribute, so the body is
113
+ // compiled through the validating evaluator in EffectMath rather than
114
+ // handed to `new Function` as-is. A rejected expression is the
115
+ // identity, which degrades the effect instead of the document.
116
+ const fn = compileExpressionRef?.(easing.slice(5, -1))
117
+ if (!fn) return t
118
+ const out = fn({ u: t, t, x: t, i: 0, n: 1, line: 0, lines: 1, col: 0, cols: 1, word: 0, words: 1, rnd: 0 })
119
+ return Number.isFinite(out) ? out : t
37
120
  }
38
121
 
39
122
  if (easing.startsWith('bezier')) {
@@ -47,11 +130,14 @@ export function ease(t: number, easing: Easing, center: number = 0.5, power: num
47
130
  }
48
131
  }
49
132
  switch (easing) {
50
- case "easeIn":
133
+ case "easeIn":
134
+ case "ease-in":
51
135
  case "half-parabola": return Math.pow(t, power)
52
- case "easeOut": return t * (2 - t)
53
- case "easeInOut":
54
- case "easeIO":
136
+ case "easeOut":
137
+ case "ease-out": return t * (2 - t)
138
+ case "easeInOut":
139
+ case "ease-in-out":
140
+ case "easeIO":
55
141
  return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
56
142
  case "parabola":
57
143
  if (center <= 0) return Math.pow(t, power);
@@ -69,26 +155,92 @@ export function hslToHex(h: number, s: number, l: number): string {
69
155
  const a = (s * Math.min(l, 1 - l)) / 100
70
156
  const f = (n: number) => {
71
157
  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")
158
+ return byteHex(255 * (l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)))
74
159
  }
75
160
  return `#${f(0)}${f(8)}${f(4)}`
76
161
  }
77
162
 
163
+ /** Hex digit value per char code, `-1` for anything else. */
164
+ const HEX_VALUE: Int8Array = (() => {
165
+ const table = new Int8Array(128).fill(-1)
166
+ for (let c = 0; c < 10; c++) table[48 + c] = c // 0-9
167
+ for (let c = 0; c < 6; c++) table[97 + c] = 10 + c // a-f
168
+ for (let c = 0; c < 6; c++) table[65 + c] = 10 + c // A-F
169
+ return table
170
+ })()
171
+
172
+ /** Two-character lowercase hex for every byte, built once. */
173
+ const BYTE_HEX: string[] = Array.from({ length: 256 }, (_, i) =>
174
+ i.toString(16).padStart(2, "0"))
175
+
176
+ function hexAt(s: string, i: number): number {
177
+ const c = s.charCodeAt(i)
178
+ return c < 128 ? HEX_VALUE[c] : -1
179
+ }
180
+
181
+ /**
182
+ * A hex colour token to RGB.
183
+ *
184
+ * Accepts `#rgb`, `#rgba`, `#rrggbb` and `#rrggbbaa`, hash optional.
185
+ * Anything else returns black instead of `NaN`: these strings come from
186
+ * BBCode attributes and from a text field the user is mid-way through
187
+ * typing, and a `NaN` channel propagates into `#NaNNaNNaN`, corrupting
188
+ * the whole gradient rather than the one bad stop.
189
+ *
190
+ * Parsed by char code rather than by regex + `parseInt`. This runs once
191
+ * per character per colour layer per keystroke — two calls for every
192
+ * `mixHex` — and the regex version was the single largest cost in
193
+ * compiling a long document.
194
+ */
78
195
  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
- ]
196
+ if (typeof hex !== "string") return [0, 0, 0]
197
+ let start = 0
198
+ let end = hex.length
199
+ while (start < end && hex.charCodeAt(start) <= 32) start++
200
+ while (end > start && hex.charCodeAt(end - 1) <= 32) end--
201
+ if (start < end && hex.charCodeAt(start) === 35 /* # */) start++
202
+
203
+ const len = end - start
204
+ const short = len === 3 || len === 4
205
+ if (!short && len !== 6 && len !== 8) return [0, 0, 0]
206
+
207
+ const digits = short ? 3 : 6
208
+ const out: [number, number, number] = [0, 0, 0]
209
+ for (let i = 0; i < digits; i += short ? 1 : 2) {
210
+ const hi = hexAt(hex, start + i)
211
+ if (hi < 0) return [0, 0, 0]
212
+ if (short) {
213
+ out[i] = hi * 17
214
+ } else {
215
+ const lo = hexAt(hex, start + i + 1)
216
+ if (lo < 0) return [0, 0, 0]
217
+ out[i >> 1] = hi * 16 + lo
218
+ }
86
219
  }
87
- return [
88
- parseInt(h.slice(0, 2), 16),
89
- parseInt(h.slice(2, 4), 16),
90
- parseInt(h.slice(4, 6), 16),
91
- ]
220
+ return out
221
+ }
222
+
223
+ /** True when a token is a hex colour `hexToRgb` can read exactly. */
224
+ export function isHexColor(value: string): boolean {
225
+ if (typeof value !== "string") return false
226
+ let start = 0
227
+ let end = value.length
228
+ while (start < end && value.charCodeAt(start) <= 32) start++
229
+ while (end > start && value.charCodeAt(end - 1) <= 32) end--
230
+ if (start < end && value.charCodeAt(start) === 35) start++
231
+
232
+ const len = end - start
233
+ if (len !== 3 && len !== 4 && len !== 6 && len !== 8) return false
234
+ for (let i = start; i < end; i++) {
235
+ if (hexAt(value, i) < 0) return false
236
+ }
237
+ return true
238
+ }
239
+
240
+ /** Clamp to a byte and format as two lowercase hex characters. */
241
+ function byteHex(v: number): string {
242
+ const n = v < 0 ? 0 : v > 255 ? 255 : Math.round(v)
243
+ return BYTE_HEX[n]
92
244
  }
93
245
 
94
246
  export function hexToHsl(hex: string): [number, number, number] {
@@ -113,13 +265,13 @@ export function hexToHsl(hex: string): [number, number, number] {
113
265
  }
114
266
 
115
267
  export function mixHex(color1: string, color2: string, weight: number): string {
268
+ // Identical endpoints are the common case in a flat run; skipping the
269
+ // interpolation also skips two parses and a string build.
270
+ if (color1 === color2) return color1
116
271
  const [r1, g1, b1] = hexToRgb(color1)
117
272
  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")}`
273
+ const w = weight < 0 ? 0 : weight > 1 ? 1 : weight
274
+ return `#${byteHex(r1 + (r2 - r1) * w)}${byteHex(g1 + (g2 - g1) * w)}${byteHex(b1 + (b2 - b1) * w)}`
123
275
  }
124
276
 
125
277
  export function mixMultiple(colors: string[], weight: number): string {
@@ -138,22 +290,34 @@ export interface ColorStop {
138
290
  position: number; // 0 to 1
139
291
  }
140
292
 
141
- export function mixMultipleStops(stops: ColorStop[], weight: number): string {
293
+ /**
294
+ * Sample a stop list at `weight`.
295
+ *
296
+ * `perceptual` interpolates in OKLab instead of sRGB. The difference
297
+ * shows wherever two stops differ in hue: an sRGB ramp from blue to
298
+ * yellow dips through a desaturated grey at the midpoint, while OKLab
299
+ * holds chroma. Two conversions per character, so it stays opt-in.
300
+ */
301
+ export function mixMultipleStops(stops: ColorStop[], weight: number, perceptual = false): string {
142
302
  if (stops.length === 0) return "#FFFFFF";
143
303
  if (stops.length === 1) return stops[0].color;
144
304
  weight = Math.max(0, Math.min(1, weight || 0));
145
-
305
+
146
306
  let i = 0;
147
307
  while (i < stops.length - 1 && stops[i + 1].position <= weight) {
148
308
  i++;
149
309
  }
150
-
310
+
151
311
  if (i >= stops.length - 1) return stops[stops.length - 1].color;
152
312
  if (weight <= stops[i].position) return stops[i].color;
153
-
313
+
154
314
  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);
315
+ // Coincident stops are a hard colour break, not a zero-width ramp:
316
+ // dividing by the gap would yield Infinity.
317
+ const frac = range <= 0 ? 1 : (weight - stops[i].position) / range;
318
+ return perceptual
319
+ ? mixHexOklab(stops[i].color, stops[i + 1].color, frac)
320
+ : mixHex(stops[i].color, stops[i + 1].color, frac);
157
321
  }
158
322
 
159
323
  // ═══════════════════════════════════════════════════════════════
@@ -189,6 +353,37 @@ function srgbToLinear(c: number): number {
189
353
  return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
190
354
  }
191
355
 
356
+ /**
357
+ * `srgbToLinear` for the 256 byte values, precomputed.
358
+ *
359
+ * The only caller is `hexToOklab`, whose input is always an integer channel
360
+ * over 255, so the continuous function has exactly 256 reachable results.
361
+ * It was the single largest cost in a document analysis — 15% of the whole
362
+ * semantic pass — because `Math.pow` ran three times per colour per
363
+ * comparison, and gradient detection compares every stop against every
364
+ * other. The table is 2 KB and the values are bit-identical to the call it
365
+ * replaces.
366
+ */
367
+ const SRGB_TO_LINEAR_BYTE: Float64Array = (() => {
368
+ const table = new Float64Array(256)
369
+ for (let i = 0; i < 256; i++) table[i] = srgbToLinear(i / 255)
370
+ return table
371
+ })()
372
+
373
+ /**
374
+ * `hexToOklab` results keyed by the hex string that produced them.
375
+ *
376
+ * A document draws from a small palette — tens of distinct colours — but a
377
+ * gradient scan converts each of them hundreds of times: once per chord
378
+ * check, once per plateau test, once per perceptual distance. The cache is
379
+ * bounded because the key space is user input (a half-typed colour in the
380
+ * editor produces a new string on every keystroke); on overflow it is
381
+ * cleared outright rather than evicted one by one, which keeps the hot path
382
+ * a single `Map.get` with no bookkeeping.
383
+ */
384
+ const OKLAB_CACHE = new Map<string, readonly [number, number, number]>()
385
+ const OKLAB_CACHE_LIMIT = 4096
386
+
192
387
  /**
193
388
  * Convert linear RGB to sRGB (gamma compression for display).
194
389
  */
@@ -202,9 +397,9 @@ function linearToSrgb(c: number): number {
202
397
  */
203
398
  export function hexToOklab(hex: string): [number, number, number] {
204
399
  const [r255, g255, b255] = hexToRgb(hex)
205
- const r = srgbToLinear(r255 / 255)
206
- const g = srgbToLinear(g255 / 255)
207
- const b = srgbToLinear(b255 / 255)
400
+ const r = SRGB_TO_LINEAR_BYTE[r255]
401
+ const g = SRGB_TO_LINEAR_BYTE[g255]
402
+ const b = SRGB_TO_LINEAR_BYTE[b255]
208
403
 
209
404
  // Linear sRGB → LMS
210
405
  const l_ = r * SRGB_TO_LMS[0] + g * SRGB_TO_LMS[1] + b * SRGB_TO_LMS[2]
@@ -217,11 +412,13 @@ export function hexToOklab(hex: string): [number, number, number] {
217
412
  const s = Math.cbrt(s_)
218
413
 
219
414
  // LMS → OKLab
220
- return [
415
+ const out: [number, number, number] = [
221
416
  l * LMS_TO_OKLAB[0] + m * LMS_TO_OKLAB[1] + s * LMS_TO_OKLAB[2],
222
417
  l * LMS_TO_OKLAB[3] + m * LMS_TO_OKLAB[4] + s * LMS_TO_OKLAB[5],
223
418
  l * LMS_TO_OKLAB[6] + m * LMS_TO_OKLAB[7] + s * LMS_TO_OKLAB[8],
224
419
  ]
420
+
421
+ return out
225
422
  }
226
423
 
227
424
  /**
@@ -232,11 +429,24 @@ export function hexToOklab(hex: string): [number, number, number] {
232
429
  * > 0.1 ≈ very noticeable
233
430
  */
234
431
  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
432
+ return perceptualDistanceOklab(hexToOklab(hex1), hexToOklab(hex2))
433
+ }
434
+
435
+ /**
436
+ * `perceptualDistance` for callers that already hold both OKLab triples.
437
+ *
438
+ * Gradient detection converts a colour to OKLab once per stop and then asks
439
+ * for distances between neighbours, between chord endpoints and against an
440
+ * ideal ramp — each of which re-derived both triples from their hex strings.
441
+ * The results are identical; this just stops paying for the conversion again.
442
+ */
443
+ export function perceptualDistanceOklab(
444
+ c1: readonly [number, number, number],
445
+ c2: readonly [number, number, number],
446
+ ): number {
447
+ const dL = c2[0] - c1[0]
448
+ const da = c2[1] - c1[1]
449
+ const db = c2[2] - c1[2]
240
450
  return Math.sqrt(dL * dL + da * da + db * db)
241
451
  }
242
452
 
@@ -244,8 +454,21 @@ export function perceptualDistance(hex1: string, hex2: string): number {
244
454
  * Mix two hex colours in OKLab space for perceptually uniform interpolation.
245
455
  */
246
456
  export function mixHexOklab(color1: string, color2: string, weight: number): string {
247
- const [L1, a1, b1] = hexToOklab(color1)
248
- const [L2, a2, b2] = hexToOklab(color2)
457
+ return mixOklabToHex(hexToOklab(color1), hexToOklab(color2), weight)
458
+ }
459
+
460
+ /**
461
+ * `mixHexOklab` for callers that already hold both OKLab triples — the same
462
+ * saving as {@link perceptualDistanceOklab}, for the ideal-ramp comparison
463
+ * that mixes the *same* two endpoints once per colour in the sequence.
464
+ */
465
+ export function mixOklabToHex(
466
+ c1: readonly [number, number, number],
467
+ c2: readonly [number, number, number],
468
+ weight: number,
469
+ ): string {
470
+ const [L1, a1, b1] = c1
471
+ const [L2, a2, b2] = c2
249
472
  const w = Math.max(0, Math.min(1, weight))
250
473
 
251
474
  const L = L1 + (L2 - L1) * w
@@ -267,10 +490,8 @@ export function mixHexOklab(color1: string, color2: string, weight: number): str
267
490
  const gLin = l * -1.2684380046 + m * 2.6097574011 + s * -0.3413193965
268
491
  const bLin = l * -0.0041960863 + m * -0.7034186147 + s * 1.7076147010
269
492
 
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')}`
493
+ // Linear sRGB → gamma-corrected sRGB → hex. `byteHex` clamps, which is
494
+ // what keeps an out-of-gamut OKLab mix from producing a negative or
495
+ // five-digit channel.
496
+ return `#${byteHex(linearToSrgb(rLin) * 255)}${byteHex(linearToSrgb(gLin) * 255)}${byteHex(linearToSrgb(bLin) * 255)}`
276
497
  }