@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
@@ -20,7 +20,10 @@ import type { RedNode } from '../Syntax/RedNode'
20
20
  import { GreenNode } from '../Syntax/GreenNode'
21
21
  import type { RenderNode } from '../RenderPipeline/RenderTree'
22
22
  import type { Validator } from '../Semantic/SemanticAnalyzer'
23
- import { mixMultiple, ease, clamp, hslToHex, type Easing } from '../Utils/color'
23
+ import {
24
+ evaluateEffect,
25
+ type EffectKind, type EffectParams, type EffectSpan, type StyledSegment,
26
+ } from '../Utils/EffectMath'
24
27
 
25
28
  // ─── Tag Handler ───────────────────────────────────────────────
26
29
 
@@ -101,25 +104,21 @@ export interface PropertyDefinition {
101
104
 
102
105
  // ─── Internal tag helpers (gradient/grow export) ──────────────
103
106
 
104
- /** Recursively extract plain text from a RedNode tree */
105
- function extractTextContent(node: RedNode): string {
107
+ /**
108
+ * Recursively extract plain text from a RedNode tree.
109
+ *
110
+ * A line break is a childless `spacing` leaf with no text of its own, so
111
+ * it has to be spelled out here or an effect spanning two lines is handed
112
+ * one line and exports without its break — silent data loss, and the
113
+ * reason every two-dimensional axis (`line`, `column`, `radial`) measured
114
+ * a block one line tall.
115
+ */
116
+ export function extractTextContent(node: RedNode): string {
106
117
  if (node.kind === 'text') return node.text
118
+ if (node.kind === 'spacing' || node.kind === 'empty_line') return '\n'
107
119
  return node.children.map(extractTextContent).join('')
108
120
  }
109
121
 
110
- /** Split text by unit: 'character', 'word', or 'line' */
111
- function splitByUnit(text: string, unit: string): string[] {
112
- if (!text) return []
113
- switch (unit) {
114
- case 'word':
115
- return text.split(/(\s+)/).filter(s => s.length > 0)
116
- case 'line':
117
- return text.split('\n')
118
- default: // 'character'
119
- return [...text]
120
- }
121
- }
122
-
123
122
  /** Escape [ and ] inside BBCode content so they're not parsed as tags */
124
123
  function escapeBracket(ch: string): string {
125
124
  // BBCode has no standard escape. For internal tags produced by TextStudio,
@@ -128,109 +127,37 @@ function escapeBracket(ch: string): string {
128
127
  return ch.replace(/\[/g, '\\[').replace(/\]/g, '\\]')
129
128
  }
130
129
 
131
- // ─── Effect segment math — one source of truth per effect ─────
132
- //
133
- // Each effect (gradient, sinewave, grow, rainbow) serializes two ways: to
134
- // osu!-compatible BBCode (`toBBCode`) and to a RenderNode tree
135
- // (`toRenderNode`). Both used to carry their own copy of the per-segment
136
- // math — t-progression, easing, color mixing, wave sizing — differing only in
137
- // presentation, which is exactly how the two drift apart. The math now lives
138
- // in ONE `*Segments` function per effect; the handlers are thin presenters.
130
+ // ─── Effect segments ──────────────────────────────────────────
139
131
  //
140
- // Note this is deliberately NOT the same algorithm as `HTMLRenderer`'s own
141
- // `renderGradient`: the preview paints a smooth CSS `linear-gradient` with
142
- // `background-clip: text`, while these segments reproduce the DISCRETE
143
- // per-chunk colors that the exported `[color=…]` BBCode will actually have.
144
-
145
- /**
146
- * A run of text with the style its effect computed. `color` and `size` are
147
- * mutually exclusive; neither set means the run passes through unstyled
148
- * (whitespace between sized words).
149
- */
150
- interface StyledSegment {
151
- text: string
152
- color?: string
153
- size?: number
154
- }
155
-
156
- /**
157
- * Effect progression t ∈ [0,1] for segment `i` of `count`, honoring the
158
- * document-wide fields (`globalOffset`/`documentLength`) that let one logical
159
- * effect span several nodes. `single` is the t for a one-segment run — the
160
- * effects legitimately disagree on it (gradient/rainbow use 0, grow 0.5).
161
- */
162
- function progressAt(i: number, count: number, node: RedNode, single: number): number {
163
- const globalOffset = (node.metadata?.globalOffset as number) || 0
164
- const documentLength = (node.metadata?.documentLength as number) || count
165
- if (documentLength > 1) return (globalOffset + i) / (documentLength - 1)
166
- return count > 1 ? i / (count - 1) : single
167
- }
168
-
169
- function gradientSegments(node: RedNode): StyledSegment[] {
170
- const colors = (node.metadata?.colors as string[]) || ['#FF0000', '#00FF00']
171
- const unit = (node.metadata?.unit as string) || 'character'
172
- const easing = (node.metadata?.easing as Easing) || 'linear'
173
- const text = extractTextContent(node)
174
- if (!text) return []
175
- const parts = splitByUnit(text, unit)
176
- return parts.map((seg, i) => ({
177
- text: seg,
178
- color: mixMultiple(colors, ease(clamp(progressAt(i, parts.length, node, 0)), easing)),
179
- }))
180
- }
181
-
182
- function sinewaveSegments(node: RedNode): StyledSegment[] {
183
- const min = (node.metadata?.min as number) ?? 20
184
- const max = (node.metadata?.max as number) ?? 80
185
- const freq = (node.metadata?.freq as number) ?? 0.4
186
- const step = (node.metadata?.step as string) ?? 'char'
187
- const text = extractTextContent(node)
188
- if (!text) return []
189
- const amplitude = (max - min) / 2
190
- const center = min + amplitude
191
-
192
- if (step === 'word') {
193
- let wordIndex = 0
194
- return text.split(/(\s+)/).map(chunk => {
195
- if (chunk.trim() === '') return { text: chunk }
196
- const size = Math.round(center + Math.sin(wordIndex * freq) * amplitude)
197
- wordIndex++
198
- return { text: chunk, size }
199
- })
132
+ // The maths lives in `Utils/EffectMath`, not here. It used to live in
133
+ // this file, in four `*Segments` functions, while Text Studio kept its
134
+ // own copy and the HTML renderer kept a third — so the same document
135
+ // exported one way, previewed another, and lost every parameter the
136
+ // studio grew. These handlers now only read the node and present the
137
+ // result.
138
+
139
+ /** Effect parameters carried on a node, plus its span in a longer effect. */
140
+ function effectParamsOf(node: RedNode): { params: EffectParams; span: EffectSpan } {
141
+ const meta = (node.metadata ?? {}) as Record<string, unknown>
142
+ const params: EffectParams = {}
143
+ for (const [key, value] of Object.entries(meta)) {
144
+ if (key === 'globalOffset' || key === 'documentLength') continue
145
+ ;(params as Record<string, unknown>)[key] = value
146
+ }
147
+ return {
148
+ params,
149
+ span: {
150
+ globalOffset: meta.globalOffset as number | undefined,
151
+ documentLength: meta.documentLength as number | undefined,
152
+ },
200
153
  }
201
-
202
- return [...text].map((ch, i) => ({
203
- text: ch,
204
- size: Math.round(center + Math.sin(i * freq) * amplitude),
205
- }))
206
- }
207
-
208
- function growSegments(node: RedNode): StyledSegment[] {
209
- const min = (node.metadata?.min as number) ?? 50
210
- const max = (node.metadata?.max as number) ?? 200
211
- const cycles = (node.metadata?.cycles as number) ?? 1
212
- const text = extractTextContent(node)
213
- if (!text) return []
214
- const chars = [...text]
215
- return chars.map((ch, i) => {
216
- const sine = Math.sin(progressAt(i, chars.length, node, 0.5) * cycles * Math.PI * 2)
217
- const normalized = (sine + 1) / 2
218
- return { text: ch, size: Math.round(min + (max - min) * normalized) }
219
- })
220
154
  }
221
155
 
222
- function rainbowSegments(node: RedNode): StyledSegment[] {
223
- const saturation = ((node.metadata?.saturation as number) ?? 80) / 100
224
- const lightness = ((node.metadata?.lightness as number) ?? 60) / 100
225
- const spread = (node.metadata?.spread as number) ?? 300
226
- const offset = (node.metadata?.offset as number) ?? 0
156
+ function effectSegments(node: RedNode, kind: EffectKind): StyledSegment[] {
227
157
  const text = extractTextContent(node)
228
158
  if (!text) return []
229
- const chars = [...text]
230
- return chars.map((ch, i) => ({
231
- text: ch,
232
- color: hslToHex(offset + progressAt(i, chars.length, node, 0) * spread, saturation, lightness),
233
- }))
159
+ const { params, span } = effectParamsOf(node)
160
+ return evaluateEffect(text, kind, params, span)
234
161
  }
235
162
 
236
163
  /** Present styled segments as osu!-compatible BBCode. */
@@ -264,11 +191,44 @@ export type TagHandler = {
264
191
  [K in keyof TagDefinition]: TagDefinition[K]
265
192
  }
266
193
 
194
+ /**
195
+ * The language's own tags, built once for the process.
196
+ *
197
+ * `registerBuiltins` allocates ~130 definition objects, several of them
198
+ * carrying closures. Constructing a registry cost 14.7 µs because of it, and
199
+ * a registry is constructed by every `DocumentModel`, every
200
+ * `SemanticAnalyzer` and every exporter that is handed no registry — so a
201
+ * throwaway parse paid for the whole table three times over. The table never
202
+ * changes, so it is shared, and a registry only takes a private copy when a
203
+ * plugin actually mutates it.
204
+ */
205
+ let BUILTIN_TAGS: ReadonlyMap<string, TagDefinition> | null = null
206
+ let BUILTIN_KINDS: ReadonlyMap<NodeKind, TagDefinition> | null = null
207
+ let BUILTIN_NAMES: ReadonlySet<string> | null = null
208
+
209
+ function ensureBuiltins(): void {
210
+ if (BUILTIN_TAGS !== null) return
211
+ const tags = new Map<string, TagDefinition>()
212
+ const kinds = new Map<NodeKind, TagDefinition>()
213
+ for (const tag of builtinTagDefinitions()) {
214
+ tags.set(tag.name, tag)
215
+ kinds.set(tag.kind, tag)
216
+ }
217
+ BUILTIN_TAGS = tags
218
+ BUILTIN_KINDS = kinds
219
+ BUILTIN_NAMES = new Set(tags.keys())
220
+ }
221
+
267
222
  export class TagRegistry {
268
- private tags: Map<string, TagDefinition> = new Map()
269
- private kinds: Map<NodeKind, TagDefinition> = new Map()
223
+ private tags: Map<string, TagDefinition>
224
+ private kinds: Map<NodeKind, TagDefinition>
270
225
  /** Names registered by the constructor — the language's own tags. */
271
- private builtins: Set<string> = new Set()
226
+ private builtins: ReadonlySet<string>
227
+ /**
228
+ * Whether `tags`/`kinds` are still the process-wide builtin maps. While
229
+ * they are, this registry must not write to them.
230
+ */
231
+ private shared: boolean = true
272
232
 
273
233
  /**
274
234
  * Bumped on every register/unregister, so consumers that derive something
@@ -278,14 +238,25 @@ export class TagRegistry {
278
238
  version: number = 0
279
239
 
280
240
  constructor() {
281
- this.registerBuiltins()
282
- for (const name of this.tags.keys()) this.builtins.add(name)
241
+ ensureBuiltins()
242
+ this.tags = BUILTIN_TAGS as Map<string, TagDefinition>
243
+ this.kinds = BUILTIN_KINDS as Map<NodeKind, TagDefinition>
244
+ this.builtins = BUILTIN_NAMES!
245
+ }
246
+
247
+ /** Take a private copy of the builtin maps before the first write. */
248
+ private own(): void {
249
+ if (!this.shared) return
250
+ this.tags = new Map(this.tags)
251
+ this.kinds = new Map(this.kinds)
252
+ this.shared = false
283
253
  }
284
254
 
285
255
  /**
286
256
  * Register a tag definition.
287
257
  */
288
258
  register(tag: TagDefinition): void {
259
+ this.own()
289
260
  this.tags.set(tag.name, tag)
290
261
  this.kinds.set(tag.kind, tag)
291
262
  this.version++
@@ -295,6 +266,8 @@ export class TagRegistry {
295
266
  * Unregister a tag definition.
296
267
  */
297
268
  unregister(name: string): void {
269
+ if (!this.tags.has(name)) return
270
+ this.own()
298
271
  const tag = this.tags.get(name)
299
272
  if (tag) {
300
273
  this.tags.delete(name)
@@ -320,6 +293,7 @@ export class TagRegistry {
320
293
  * costs one size comparison and no allocation.
321
294
  */
322
295
  customTags(): Map<string, NodeKind> | null {
296
+ if (this.shared) return null
323
297
  if (this.tags.size === this.builtins.size) return null
324
298
  const out = new Map<string, NodeKind>()
325
299
  for (const [name, def] of this.tags) {
@@ -377,10 +351,16 @@ export class TagRegistry {
377
351
  return this.getAll().filter(t => !t.isInline && !t.isSelfClosing)
378
352
  }
379
353
 
380
- /**
381
- * Register the built-in BBCode tags.
382
- */
383
- private registerBuiltins(): void {
354
+ }
355
+
356
+ /**
357
+ * The built-in BBCode tags.
358
+ *
359
+ * Called once per process, through `ensureBuiltins`. It used to be a private
360
+ * method that registered each definition into `this`, which meant rebuilding
361
+ * this whole table for every registry.
362
+ */
363
+ function builtinTagDefinitions(): TagDefinition[] {
384
364
  const tags: TagDefinition[] = [
385
365
  // ── Formatting ──
386
366
  { name: 'b', kind: 'bold', label: 'Bold', category: 'formatting', isInline: true, isSelfClosing: false, canHaveChildren: true, toolbar: { group: 'formatting', label: 'Bold', icon: 'Bold', shortcut: 'Ctrl+B' } },
@@ -409,10 +389,10 @@ export class TagRegistry {
409
389
  isSelfClosing: false,
410
390
  canHaveChildren: true,
411
391
  toBBCode: (ctx) => {
412
- const segments = gradientSegments(ctx.node)
392
+ const segments = effectSegments(ctx.node, 'gradient')
413
393
  return segments.length === 0 ? ctx.visitChildren(ctx.node) : segmentsToBBCode(segments)
414
394
  },
415
- toRenderNode: (ctx) => segmentsToRenderNode('gradient', gradientSegments(ctx.node)),
395
+ toRenderNode: (ctx) => segmentsToRenderNode('gradient', effectSegments(ctx.node, 'gradient')),
416
396
  },
417
397
  {
418
398
  name: 'sinewave',
@@ -423,10 +403,10 @@ export class TagRegistry {
423
403
  isSelfClosing: false,
424
404
  canHaveChildren: true,
425
405
  toBBCode: (ctx) => {
426
- const segments = sinewaveSegments(ctx.node)
406
+ const segments = effectSegments(ctx.node, 'sinewave')
427
407
  return segments.length === 0 ? ctx.visitChildren(ctx.node) : segmentsToBBCode(segments)
428
408
  },
429
- toRenderNode: (ctx) => segmentsToRenderNode('sinewave', sinewaveSegments(ctx.node)),
409
+ toRenderNode: (ctx) => segmentsToRenderNode('sinewave', effectSegments(ctx.node, 'sinewave')),
430
410
  },
431
411
  {
432
412
  name: 'grow',
@@ -437,10 +417,28 @@ export class TagRegistry {
437
417
  isSelfClosing: false,
438
418
  canHaveChildren: true,
439
419
  toBBCode: (ctx) => {
440
- const segments = growSegments(ctx.node)
420
+ const segments = effectSegments(ctx.node, 'grow')
421
+ return segments.length === 0 ? ctx.visitChildren(ctx.node) : segmentsToBBCode(segments)
422
+ },
423
+ toRenderNode: (ctx) => segmentsToRenderNode('grow', effectSegments(ctx.node, 'grow')),
424
+ },
425
+ {
426
+ // Colour driven by a picture rather than a ramp: the tag carries a
427
+ // small indexed grid, and each character takes the colour of the
428
+ // cell it lands on. Nothing about the text changes — this is the
429
+ // one effect whose whole point is that the art keeps its shape.
430
+ name: 'paint',
431
+ kind: 'paint',
432
+ label: 'Paint',
433
+ category: 'text',
434
+ isInline: true,
435
+ isSelfClosing: false,
436
+ canHaveChildren: true,
437
+ toBBCode: (ctx) => {
438
+ const segments = effectSegments(ctx.node, 'paint')
441
439
  return segments.length === 0 ? ctx.visitChildren(ctx.node) : segmentsToBBCode(segments)
442
440
  },
443
- toRenderNode: (ctx) => segmentsToRenderNode('grow', growSegments(ctx.node)),
441
+ toRenderNode: (ctx) => segmentsToRenderNode('paint', effectSegments(ctx.node, 'paint')),
444
442
  },
445
443
  {
446
444
  name: 'rainbow',
@@ -451,10 +449,10 @@ export class TagRegistry {
451
449
  isSelfClosing: false,
452
450
  canHaveChildren: true,
453
451
  toBBCode: (ctx) => {
454
- const segments = rainbowSegments(ctx.node)
452
+ const segments = effectSegments(ctx.node, 'rainbow')
455
453
  return segments.length === 0 ? ctx.visitChildren(ctx.node) : segmentsToBBCode(segments)
456
454
  },
457
- toRenderNode: (ctx) => segmentsToRenderNode('rainbow', rainbowSegments(ctx.node)),
455
+ toRenderNode: (ctx) => segmentsToRenderNode('rainbow', effectSegments(ctx.node, 'rainbow')),
458
456
  },
459
457
 
460
458
  // ── Layout ──
@@ -491,8 +489,5 @@ export class TagRegistry {
491
489
  { name: 'group', kind: 'group', label: 'Group', category: 'layout', isInline: false, isSelfClosing: false, canHaveChildren: true },
492
490
  ]
493
491
 
494
- for (const tag of tags) {
495
- this.register(tag)
496
- }
497
- }
492
+ return tags
498
493
  }