@miliastry/quasar 1.0.6 → 1.2.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 (278) hide show
  1. package/README.md +1 -1
  2. package/dist/Visuals/lyne.css +374 -9
  3. package/dist/Visuals/osu.css +393 -11
  4. package/dist/index.js +10470 -3336
  5. package/dist/index.mjs +10336 -3337
  6. package/dist/types/Analysis/Contracts/AnalysisReport.d.ts +22 -0
  7. package/dist/types/Analysis/Contracts/Contribution.d.ts +68 -0
  8. package/dist/types/Analysis/Contracts/Pass.d.ts +59 -0
  9. package/dist/types/Analysis/Contracts/PipelineContext.d.ts +34 -0
  10. package/dist/types/Analysis/Passes/Analysis/ColorUsageAnalyzer.d.ts +56 -0
  11. package/dist/types/Analysis/Passes/Analysis/GradientAnalyzer.d.ts +111 -0
  12. package/dist/types/Analysis/Passes/Analysis/MergeableColorAnalyzer.d.ts +26 -0
  13. package/dist/types/Analysis/Passes/Analysis/RainbowAnalyzer.d.ts +47 -0
  14. package/dist/types/Analysis/Passes/Analysis/SymbolAnalyzer.d.ts +89 -0
  15. package/dist/types/Analysis/Passes/Analysis/WaveAnalyzer.d.ts +44 -0
  16. package/dist/types/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.d.ts +4 -0
  17. package/dist/types/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.d.ts +4 -0
  18. package/dist/types/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.d.ts +4 -0
  19. package/dist/types/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.d.ts +4 -0
  20. package/dist/types/Analysis/Passes/Decision/DefaultDecision.d.ts +30 -0
  21. package/dist/types/Analysis/Passes/Decision/PaletteRemapDecision.d.ts +169 -0
  22. package/dist/types/Analysis/Passes/Decision/__tests__/DefaultDecision.test.d.ts +4 -0
  23. package/dist/types/Analysis/Passes/Transform/CollapseGradientTransform.d.ts +37 -0
  24. package/dist/types/Analysis/Passes/Transform/MergeColorsTransform.d.ts +28 -0
  25. package/dist/types/Analysis/Passes/Transform/RainbowCollapseTransform.d.ts +19 -0
  26. package/dist/types/Analysis/Passes/Transform/WaveCollapseTransform.d.ts +20 -0
  27. package/dist/types/Analysis/Passes/Utility/CharacterCountAnalyzer.d.ts +19 -0
  28. package/dist/types/Analysis/Pipeline/Pipeline.d.ts +69 -0
  29. package/dist/types/Analysis/Pipeline/PipelineBuilder.d.ts +31 -0
  30. package/dist/types/Analysis/Pipeline/PipelineStage.d.ts +17 -0
  31. package/dist/types/Analysis/Utils/color-utils.d.ts +60 -0
  32. package/dist/types/Analysis/__tests__/Integration.test.d.ts +8 -0
  33. package/dist/types/Analysis/__tests__/PaletteRemap.test.d.ts +14 -0
  34. package/dist/types/Analysis/__tests__/Pipeline.test.d.ts +10 -0
  35. package/dist/types/Analysis/__tests__/RoundTrip.test.d.ts +30 -0
  36. package/dist/types/Analysis/__tests__/SymbolAnalyzer.test.d.ts +11 -0
  37. package/dist/types/Analysis/index.d.ts +40 -0
  38. package/dist/types/BBCode/BBCodeDocumentModel.d.ts +102 -0
  39. package/dist/types/BBCode/BBCodeToGreenNode.d.ts +111 -0
  40. package/dist/types/BBCode/Parser.d.ts +78 -0
  41. package/dist/types/BBCode/index.d.ts +4 -0
  42. package/dist/types/Collab/positions.d.ts +55 -0
  43. package/dist/types/Commands/Command.d.ts +37 -0
  44. package/dist/types/Commands/CommandRegistry.d.ts +48 -0
  45. package/dist/types/Commands/DeleteNode.d.ts +7 -0
  46. package/dist/types/Commands/InsertText.d.ts +8 -0
  47. package/dist/types/Commands/SplitMerge.d.ts +9 -0
  48. package/dist/types/Commands/WrapInTag.d.ts +8 -0
  49. package/dist/types/Commands/index.d.ts +6 -0
  50. package/dist/types/Diff/TreeDiffer.d.ts +52 -0
  51. package/dist/types/Diff/__tests__/TreeDiffer.test.d.ts +1 -0
  52. package/dist/types/Diff/index.d.ts +2 -0
  53. package/dist/types/Edits/EditPlan.d.ts +144 -0
  54. package/dist/types/Edits/Optimizer.d.ts +78 -0
  55. package/dist/types/Edits/Rules/Rule.d.ts +91 -0
  56. package/dist/types/Edits/Rules/dropEmptyTags.d.ts +40 -0
  57. package/dist/types/Edits/Rules/dropRedundantNesting.d.ts +59 -0
  58. package/dist/types/Edits/Rules/index.d.ts +9 -0
  59. package/dist/types/Edits/Rules/mergeAdjacent.d.ts +93 -0
  60. package/dist/types/Edits/Rules/reorderWrappers.d.ts +42 -0
  61. package/dist/types/Edits/Rules/shortenHex.d.ts +30 -0
  62. package/dist/types/Edits/Rules/tagValue.d.ts +56 -0
  63. package/dist/types/Edits/Rules/unwrapInvisibleColor.d.ts +47 -0
  64. package/dist/types/Edits/applyEdits.d.ts +36 -0
  65. package/dist/types/Edits/index.d.ts +6 -0
  66. package/dist/types/Events/EventBus.d.ts +103 -0
  67. package/dist/types/Events/index.d.ts +2 -0
  68. package/dist/types/Formatter/Formatter.d.ts +32 -0
  69. package/dist/types/Formatter/index.d.ts +2 -0
  70. package/dist/types/HTML/HTMLDocumentModel.d.ts +13 -0
  71. package/dist/types/HTML/HTMLToGreenNode.d.ts +4 -0
  72. package/dist/types/Incremental/BracketIndex.d.ts +101 -0
  73. package/dist/types/Incremental/ChangeTracker.d.ts +69 -0
  74. package/dist/types/Incremental/IncrementalParser.d.ts +246 -0
  75. package/dist/types/Incremental/__tests__/BracketIndex.test.d.ts +1 -0
  76. package/dist/types/Incremental/__tests__/IncrementalParser.test.d.ts +1 -0
  77. package/dist/types/Incremental/index.d.ts +4 -0
  78. package/dist/types/Lexer/BBCodeLexer.d.ts +74 -0
  79. package/dist/types/Lexer/Lexer.d.ts +54 -0
  80. package/dist/types/Lexer/index.d.ts +4 -0
  81. package/dist/types/Linter/Linter.d.ts +58 -0
  82. package/dist/types/Linter/index.d.ts +2 -0
  83. package/dist/types/Markdown/MarkdownAST.d.ts +122 -0
  84. package/dist/types/Markdown/MarkdownDocumentModel.d.ts +15 -0
  85. package/dist/types/Markdown/MarkdownLexer.d.ts +155 -0
  86. package/dist/types/Markdown/MarkdownParser.d.ts +42 -0
  87. package/dist/types/Markdown/MarkdownToGreenNode.d.ts +6 -0
  88. package/dist/types/MilHibri/MilHibriDocumentModel.d.ts +21 -0
  89. package/dist/types/Model/DocumentModel.d.ts +318 -0
  90. package/dist/types/Model/NodeFactory.d.ts +29 -0
  91. package/dist/types/Model/TagRegistry.d.ts +165 -0
  92. package/dist/types/Model/index.d.ts +5 -0
  93. package/dist/types/Plugins/PluginAPI.d.ts +44 -0
  94. package/dist/types/Plugins/PluginRegistry.d.ts +81 -0
  95. package/dist/types/Plugins/index.d.ts +3 -0
  96. package/dist/types/Queries/QueryEngine.d.ts +32 -0
  97. package/dist/types/Queries/index.d.ts +1 -0
  98. package/dist/types/Reconciler/SurgicalReconciler.d.ts +23 -0
  99. package/dist/types/RenderPipeline/RenderPipeline.d.ts +46 -0
  100. package/dist/types/RenderPipeline/RenderTree.d.ts +55 -0
  101. package/dist/types/RenderPipeline/index.d.ts +4 -0
  102. package/dist/types/Repair/NestingRepair.d.ts +48 -0
  103. package/dist/types/Repair/index.d.ts +1 -0
  104. package/dist/types/Semantic/SemanticAnalyzer.d.ts +321 -0
  105. package/dist/types/Semantic/index.d.ts +2 -0
  106. package/dist/types/Symbols/SymbolTable.d.ts +47 -0
  107. package/dist/types/Symbols/index.d.ts +1 -0
  108. package/dist/types/Syntax/GreenNode.d.ts +154 -0
  109. package/dist/types/Syntax/GreenNodePool.d.ts +134 -0
  110. package/dist/types/Syntax/NodeMatcher.d.ts +75 -0
  111. package/dist/types/Syntax/RedNode.d.ts +280 -0
  112. package/dist/types/Syntax/RedNodeStore.d.ts +116 -0
  113. package/dist/types/Syntax/TreeBuilder.d.ts +54 -0
  114. package/dist/types/Syntax/__tests__/GreenNode.test.d.ts +1 -0
  115. package/dist/types/Syntax/__tests__/RedNode.test.d.ts +1 -0
  116. package/dist/types/Syntax/__tests__/RedNodeStore.test.d.ts +1 -0
  117. package/dist/types/Syntax/greenEdit.d.ts +64 -0
  118. package/dist/types/Syntax/hash.d.ts +17 -0
  119. package/dist/types/Syntax/index.d.ts +8 -0
  120. package/dist/types/Syntax/nodeAttr.d.ts +63 -0
  121. package/dist/types/Syntax/partition.d.ts +67 -0
  122. package/dist/types/Syntax/preserveNodeIds.d.ts +47 -0
  123. package/dist/types/Tokens/TokenResolver.d.ts +25 -0
  124. package/dist/types/Tokens/index.d.ts +1 -0
  125. package/dist/types/Transactions/Transaction.d.ts +36 -0
  126. package/dist/types/Transactions/UndoManager.d.ts +63 -0
  127. package/dist/types/Transactions/index.d.ts +3 -0
  128. package/dist/types/Transformers/ASTOptimizer.d.ts +34 -0
  129. package/dist/types/Transformers/GradientTransformer.d.ts +18 -0
  130. package/dist/types/Transformers/GrowTransformer.d.ts +16 -0
  131. package/dist/types/Transformers/RainbowTransformer.d.ts +17 -0
  132. package/dist/types/Transformers/SineWaveTransformer.d.ts +17 -0
  133. package/dist/types/Transformers/Transformer.d.ts +17 -0
  134. package/dist/types/Types/core.d.ts +104 -0
  135. package/dist/types/Types/diagnostics.d.ts +85 -0
  136. package/dist/types/Types/index.d.ts +11 -0
  137. package/dist/types/Types/operations.d.ts +109 -0
  138. package/dist/types/Types/queries.d.ts +67 -0
  139. package/dist/types/Types/symbols.d.ts +42 -0
  140. package/dist/types/Types/tokens.d.ts +75 -0
  141. package/dist/types/Utils/BBCodeGenerator.d.ts +21 -0
  142. package/dist/types/Utils/ColorMath.d.ts +92 -0
  143. package/dist/types/Utils/EffectMath.d.ts +643 -0
  144. package/dist/types/Utils/TagVocabulary.d.ts +104 -0
  145. package/dist/types/Utils/TagVocabulary.test.d.ts +1 -0
  146. package/dist/types/Utils/color.d.ts +29 -0
  147. package/dist/types/Utils/dom-to-svg.d.ts +28 -0
  148. package/dist/types/Utils/dom-to-svg.test.d.ts +1 -0
  149. package/dist/types/Utils/treeTransformers.d.ts +160 -0
  150. package/dist/types/Visitors/BBBlocksExporter.d.ts +27 -0
  151. package/dist/types/Visitors/BBCodeExporter.d.ts +90 -0
  152. package/dist/types/Visitors/BlockPatcher.d.ts +91 -0
  153. package/dist/types/Visitors/DOMMorpher.d.ts +19 -0
  154. package/dist/types/Visitors/HTMLRenderer.d.ts +492 -0
  155. package/dist/types/Visitors/JSONExporter.d.ts +42 -0
  156. package/dist/types/Visitors/MarkdownExporter.d.ts +28 -0
  157. package/dist/types/Visitors/SVGRenderer.d.ts +16 -0
  158. package/dist/types/Visitors/TiptapExporter.d.ts +40 -0
  159. package/dist/types/Visitors/Visitor.d.ts +35 -0
  160. package/dist/types/Visitors/index.d.ts +9 -0
  161. package/dist/types/Visuals/BoxDrawer.d.ts +54 -0
  162. package/dist/types/Visuals/LyneAudio.d.ts +8 -0
  163. package/dist/types/Visuals/index.d.ts +39 -0
  164. package/dist/types/index.d.ts +155 -0
  165. package/package.json +14 -10
  166. package/src/Analysis/Passes/Analysis/ColorUsageAnalyzer.ts +103 -0
  167. package/src/Analysis/Passes/Analysis/GradientAnalyzer.ts +294 -20
  168. package/src/Analysis/Passes/Analysis/SymbolAnalyzer.ts +257 -0
  169. package/src/Analysis/Passes/Decision/PaletteRemapDecision.ts +425 -0
  170. package/src/Analysis/Pipeline/Pipeline.ts +15 -2
  171. package/src/Analysis/index.ts +8 -2
  172. package/src/BBCode/BBCodeDocumentModel.ts +39 -1
  173. package/src/BBCode/BBCodeToGreenNode.ts +18 -12
  174. package/src/BBCode/Parser.ts +38 -1
  175. package/src/Edits/EditPlan.ts +290 -0
  176. package/src/Edits/Optimizer.ts +185 -0
  177. package/src/Edits/Rules/Rule.ts +145 -0
  178. package/src/Edits/Rules/dropEmptyTags.ts +110 -0
  179. package/src/Edits/Rules/dropRedundantNesting.ts +120 -0
  180. package/src/Edits/Rules/index.ts +28 -0
  181. package/src/Edits/Rules/mergeAdjacent.ts +224 -0
  182. package/src/Edits/Rules/reorderWrappers.ts +154 -0
  183. package/src/Edits/Rules/shortenHex.ts +79 -0
  184. package/src/Edits/Rules/tagValue.ts +105 -0
  185. package/src/Edits/Rules/unwrapInvisibleColor.ts +93 -0
  186. package/src/Edits/applyEdits.ts +54 -0
  187. package/src/Edits/index.ts +22 -0
  188. package/src/Events/EventBus.ts +18 -0
  189. package/src/HTML/HTMLToGreenNode.ts +193 -37
  190. package/src/Incremental/BracketIndex.ts +249 -0
  191. package/src/Incremental/IncrementalParser.ts +464 -41
  192. package/src/Lexer/BBCodeLexer.ts +25 -3
  193. package/src/Lexer/index.ts +1 -1
  194. package/src/Markdown/MarkdownAST.ts +65 -2
  195. package/src/Markdown/MarkdownLexer.ts +69 -14
  196. package/src/Markdown/MarkdownParser.ts +749 -62
  197. package/src/Markdown/MarkdownToGreenNode.ts +148 -27
  198. package/src/MilHibri/MilHibriDocumentModel.ts +56 -0
  199. package/src/Model/DocumentModel.ts +138 -5
  200. package/src/Model/TagRegistry.ts +129 -134
  201. package/src/Reconciler/SurgicalReconciler.ts +460 -0
  202. package/src/Repair/NestingRepair.ts +116 -0
  203. package/src/Repair/index.ts +1 -0
  204. package/src/Semantic/SemanticAnalyzer.ts +1845 -65
  205. package/src/Syntax/GreenNodePool.ts +12 -12
  206. package/src/Syntax/RedNode.ts +17 -3
  207. package/src/Syntax/RedNodeStore.ts +9 -1
  208. package/src/Syntax/nodeAttr.ts +172 -0
  209. package/src/Tokens/TokenResolver.ts +55 -0
  210. package/src/Tokens/index.ts +1 -0
  211. package/src/Types/core.ts +15 -0
  212. package/src/Utils/ColorMath.ts +276 -55
  213. package/src/Utils/EffectMath.ts +2462 -0
  214. package/src/Utils/TagVocabulary.ts +141 -0
  215. package/src/Utils/dom-to-svg.ts +28 -12
  216. package/src/Visitors/BBBlocksExporter.ts +15 -1
  217. package/src/Visitors/BBCodeExporter.ts +365 -32
  218. package/src/Visitors/BlockPatcher.ts +123 -22
  219. package/src/Visitors/DOMMorpher.ts +44 -28
  220. package/src/Visitors/HTMLRenderer.ts +771 -205
  221. package/src/Visitors/MarkdownExporter.ts +98 -41
  222. package/src/Visitors/TiptapExporter.ts +1 -0
  223. package/src/Visitors/index.ts +1 -1
  224. package/src/Visuals/BoxDrawer.ts +56 -10
  225. package/src/Visuals/LyneAudio.ts +346 -0
  226. package/src/Visuals/index.ts +8 -4
  227. package/src/Visuals/lyne.css +374 -9
  228. package/src/Visuals/osu.css +393 -11
  229. package/src/index.ts +178 -8
  230. package/dist/index.d.mts +0 -4612
  231. package/dist/index.d.ts +0 -4612
  232. package/src/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.ts +0 -135
  233. package/src/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.ts +0 -84
  234. package/src/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.ts +0 -99
  235. package/src/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.ts +0 -119
  236. package/src/Analysis/Passes/Decision/__tests__/DefaultDecision.test.ts +0 -179
  237. package/src/Analysis/__tests__/Integration.test.ts +0 -162
  238. package/src/Analysis/__tests__/Pipeline.test.ts +0 -133
  239. package/src/Diff/__tests__/TreeDiffer.test.ts +0 -65
  240. package/src/Incremental/__tests__/IncrementalParser.test.ts +0 -164
  241. package/src/Syntax/__tests__/GreenNode.test.ts +0 -33
  242. package/src/Syntax/__tests__/RedNode.test.ts +0 -81
  243. package/src/Syntax/__tests__/RedNodeStore.test.ts +0 -104
  244. package/src/Tests/ASTOptimizerIdempotence.test.ts +0 -77
  245. package/src/Tests/BlockPatcher.test.ts +0 -437
  246. package/src/Tests/BlockPatcherWindowed.test.ts +0 -364
  247. package/src/Tests/BoxDrawer.test.ts +0 -217
  248. package/src/Tests/BoxRichTitle.test.ts +0 -105
  249. package/src/Tests/Chars500kBenchmark.test.ts +0 -151
  250. package/src/Tests/Chars500kEdits.test.ts +0 -321
  251. package/src/Tests/CollabPositions.test.ts +0 -146
  252. package/src/Tests/CompilerPathProfiling.test.ts +0 -186
  253. package/src/Tests/DOMMorpher.test.ts +0 -142
  254. package/src/Tests/DomPatchPerf.test.ts +0 -60
  255. package/src/Tests/EffectSegments.snapshot.json +0 -616
  256. package/src/Tests/EffectSegments.test.ts +0 -68
  257. package/src/Tests/FindNodeAtOffset.test.ts +0 -65
  258. package/src/Tests/Fuzzer.test.ts +0 -166
  259. package/src/Tests/GreenNodePool.test.ts +0 -153
  260. package/src/Tests/Lexer.test.ts +0 -238
  261. package/src/Tests/LyneMode.test.ts +0 -291
  262. package/src/Tests/ModelCoherence.test.ts +0 -180
  263. package/src/Tests/Partition.test.ts +0 -238
  264. package/src/Tests/PluginTags.test.ts +0 -150
  265. package/src/Tests/ProblematicSection.test.ts +0 -46
  266. package/src/Tests/ProblematicSectionHTML.test.ts +0 -58
  267. package/src/Tests/RedReuse.test.ts +0 -134
  268. package/src/Tests/ReproDelete20k.test.ts +0 -62
  269. package/src/Tests/SemanticValidators.test.ts +0 -136
  270. package/src/Tests/StableNodeIds.test.ts +0 -210
  271. package/src/Tests/StudioColorBloat.test.ts +0 -25
  272. package/src/Tests/StudioDebugText.test.ts +0 -27
  273. package/src/Tests/StudioTrailingChar.test.ts +0 -25
  274. package/src/Tests/StudioValidText.test.ts +0 -25
  275. package/src/Tests/UrlImgBug.test.ts +0 -23
  276. package/src/Tests/VisualBuilderFidelity.test.ts +0 -105
  277. package/src/Tests/referenceDocument.ts +0 -119
  278. package/src/Utils/dom-to-svg.test.ts +0 -86
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Quasar — `reorder-wrappers` (opt-in)
3
+ *
4
+ * Puts a chain of single-child wrappers into a canonical order:
5
+ *
6
+ * [color=#F00][b]x[/b][/color] → [b][color=#F00]x[/color][/b]
7
+ *
8
+ * ## It saves no bytes, and that is why it is not in the default preset
9
+ *
10
+ * Reordering removes nothing. Its value is canonical form — two documents that
11
+ * differ only in wrapper order become byte-identical, which is what lets other
12
+ * rules recognise runs they would otherwise miss. On its own, in a *minifier*,
13
+ * it produces a diff across someone's document in exchange for zero
14
+ * characters, so `defaultRules()` leaves it out and callers opt in.
15
+ *
16
+ * ## Byte-swaps, never regeneration
17
+ *
18
+ * The delimiters are moved, not rebuilt: each slot receives the exact source
19
+ * bytes of the delimiter that belongs there. `[Color = "#F00"]` comes out the
20
+ * other side character for character, which a tag-rebuilding implementation
21
+ * could not promise.
22
+ *
23
+ * ## Maximal
24
+ *
25
+ * The whole chain is sorted at once. Swapping one adjacent pair at a time
26
+ * would need repeated passes over offsets that no longer exist, so
27
+ * `[color][b][i]x[/i][/b][/color]` is permuted in a single step. The sort is
28
+ * stable, so equal-rank wrappers keep the order the author wrote.
29
+ */
30
+
31
+ import type { PlannedEdit } from '../EditPlan'
32
+ import {
33
+ type OptimizationRule,
34
+ type RuleContext,
35
+ type Positioned,
36
+ positionedChildren,
37
+ openRange,
38
+ closeRange,
39
+ hasBothDelimiters,
40
+ } from './Rule'
41
+
42
+ export const REORDER_WRAPPERS_PRIORITY = 5
43
+
44
+ /**
45
+ * Nesting rank — lower belongs further out.
46
+ *
47
+ * Links outermost (a link wrapping styled text is the common authoring
48
+ * intent), then metrics, then weight/decoration, then colour innermost. Kinds
49
+ * absent from the table are not reordered at all: an unranked tag may carry
50
+ * layout or semantics that nesting order changes.
51
+ */
52
+ const RANK: Readonly<Record<string, number>> = {
53
+ url: 1,
54
+ email: 1,
55
+ profile: 1,
56
+ font_size: 2,
57
+ font: 2,
58
+ bold: 3,
59
+ italic: 3,
60
+ underline: 3,
61
+ strikethrough: 3,
62
+ color: 4,
63
+ }
64
+
65
+ export class ReorderWrappersRule implements OptimizationRule {
66
+ readonly id = 'reorder-wrappers'
67
+ readonly priority = REORDER_WRAPPERS_PRIORITY
68
+ readonly label = 'Canonicalise nested wrapper order'
69
+
70
+ run(context: RuleContext): PlannedEdit[] {
71
+ const sink: PlannedEdit[] = []
72
+ this.scan({ node: context.root, start: 0 }, context.source, sink)
73
+ sink.sort((a, b) => a.start - b.start || a.end - b.end)
74
+ return sink
75
+ }
76
+
77
+ // ── Private ─────────────────────────────────────────────────────
78
+
79
+ private scan(item: Positioned, source: string, sink: PlannedEdit[]): void {
80
+ for (const child of positionedChildren(item)) {
81
+ const chain = this.chainFrom(child)
82
+
83
+ if (chain.length >= 2) {
84
+ this.emitPermutation(chain, source, sink)
85
+ // Continue below the chain. `scan` iterates its argument's children,
86
+ // so the innermost wrapper is what gets handed down — passing its
87
+ // children instead would skip a level.
88
+ this.scan(chain[chain.length - 1], source, sink)
89
+ continue
90
+ }
91
+
92
+ this.scan(child, source, sink)
93
+ }
94
+ }
95
+
96
+ /** The maximal run of rankable single-child wrappers starting at `item`. */
97
+ private chainFrom(item: Positioned): Positioned[] {
98
+ const chain: Positioned[] = []
99
+ let current: Positioned | undefined = item
100
+
101
+ while (
102
+ current &&
103
+ RANK[current.node.kind] !== undefined &&
104
+ hasBothDelimiters(current) &&
105
+ current.node.children.length === 1
106
+ ) {
107
+ chain.push(current)
108
+ const [only] = positionedChildren(current)
109
+ current = only
110
+ }
111
+ return chain
112
+ }
113
+
114
+ private emitPermutation(
115
+ chain: readonly Positioned[],
116
+ source: string,
117
+ sink: PlannedEdit[],
118
+ ): void {
119
+ const order = chain.map((_, index) => index)
120
+ // Stable by construction: ties fall back to the original index.
121
+ order.sort((a, b) => RANK[chain[a].node.kind] - RANK[chain[b].node.kind] || a - b)
122
+
123
+ if (order.every((sourceIndex, slot) => sourceIndex === slot)) return
124
+
125
+ const label = `Canonicalise ${chain.map(c => `[${c.node.kind}]`).join('')}`
126
+
127
+ for (let slot = 0; slot < chain.length; slot++) {
128
+ const from = chain[order[slot]]
129
+ if (order[slot] === slot) continue
130
+
131
+ const openSlot = openRange(chain[slot])
132
+ const closeSlot = closeRange(chain[slot])
133
+ const openFrom = openRange(from)
134
+ const closeFrom = closeRange(from)
135
+
136
+ sink.push({
137
+ start: openSlot.start,
138
+ end: openSlot.end,
139
+ text: source.slice(openFrom.start, openFrom.end),
140
+ ruleId: this.id,
141
+ priority: this.priority,
142
+ label,
143
+ })
144
+ sink.push({
145
+ start: closeSlot.start,
146
+ end: closeSlot.end,
147
+ text: source.slice(closeFrom.start, closeFrom.end),
148
+ ruleId: this.id,
149
+ priority: this.priority,
150
+ label,
151
+ })
152
+ }
153
+ }
154
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Quasar — `shorten-hex`
3
+ *
4
+ * `[color=#FFAA00]` → `[color=#FA0]`, three characters per tag.
5
+ *
6
+ * Only fires when every channel is a doubled digit, which is exactly the set
7
+ * of six-digit colours the three-digit form can express — `#FA0` expands back
8
+ * to `#FFAA00` by duplication, so the rendered colour is bit-identical. This
9
+ * was verified against the engine before the rule was written: three-digit hex
10
+ * parses to a `color` node and reaches CSS as `color:#FA0`.
11
+ *
12
+ * The edit replaces the hex token alone, located inside the opening
13
+ * delimiter — never the delimiter itself. `[Color = "#FFAA00"]` therefore
14
+ * becomes `[Color = "#FA0"]`, keeping the capital C, the spaces and the
15
+ * quotes the author typed.
16
+ *
17
+ * Three bytes sounds trivial and is not: an expanded gradient spends one
18
+ * colour tag per character, so a 400-character ramp gives back 1.200.
19
+ */
20
+
21
+ import type { PlannedEdit } from '../EditPlan'
22
+ import {
23
+ type OptimizationRule,
24
+ type RuleContext,
25
+ type Positioned,
26
+ positionedChildren,
27
+ openRange,
28
+ } from './Rule'
29
+ import { attributeValue, shortenableHex } from './tagValue'
30
+
31
+ export const SHORTEN_HEX_PRIORITY = 10
32
+
33
+ const HEX6_TOKEN = /#[0-9a-fA-F]{6}/
34
+
35
+ export class ShortenHexRule implements OptimizationRule {
36
+ readonly id = 'shorten-hex'
37
+ readonly priority = SHORTEN_HEX_PRIORITY
38
+ readonly label = 'Shorten 6-digit hex colours'
39
+
40
+ run(context: RuleContext): PlannedEdit[] {
41
+ const sink: PlannedEdit[] = []
42
+ this.scan({ node: context.root, start: 0 }, context.source, sink)
43
+ return sink
44
+ }
45
+
46
+ // ── Private ─────────────────────────────────────────────────────
47
+
48
+ private scan(item: Positioned, source: string, sink: PlannedEdit[]): void {
49
+ for (const child of positionedChildren(item)) {
50
+ if (child.node.kind === 'color') {
51
+ const edit = this.shorten(child, source)
52
+ if (edit) sink.push(edit)
53
+ }
54
+ this.scan(child, source, sink)
55
+ }
56
+ }
57
+
58
+ private shorten(item: Positioned, source: string): PlannedEdit | null {
59
+ const short = shortenableHex(attributeValue(item.node))
60
+ if (short === null) return null
61
+
62
+ const open = openRange(item)
63
+ const delimiter = source.slice(open.start, open.end)
64
+ const match = HEX6_TOKEN.exec(delimiter)
65
+ // The attribute said there was a six-digit hex; if it is not visible in the
66
+ // delimiter the two disagree, and guessing an offset would corrupt bytes.
67
+ if (!match) return null
68
+
69
+ const start = open.start + match.index
70
+ return {
71
+ start,
72
+ end: start + match[0].length,
73
+ text: short,
74
+ ruleId: this.id,
75
+ priority: this.priority,
76
+ label: `${match[0]} → ${short}`,
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Quasar — Tag attribute reading & normalisation for optimization rules
3
+ *
4
+ * Rules compare tags by what they *mean*, not by how they are spelled:
5
+ * `[Color = "#FF0000"]`, `[color=#ff0000]` and `[color=#F00]` all name the
6
+ * same colour and must be treated as one. Normalising here is what lets the
7
+ * merge rule fuse them while still leaving the author's own bytes in place.
8
+ *
9
+ * `Analysis/Utils/color-utils.extractHex` is deliberately stricter — it
10
+ * accepts only six-digit hex, because a *recolouring* pass must not rewrite a
11
+ * spelling the author chose. These rules have the opposite need: they only
12
+ * ever delete or shorten, so recognising more spellings is pure upside.
13
+ */
14
+
15
+ import type { GreenNode } from '../../Syntax/GreenNode'
16
+
17
+ const QUOTES = new Set(['"', "'"])
18
+
19
+ /**
20
+ * The attribute value a tag node carries, unquoted and trimmed.
21
+ *
22
+ * A node's `text` holds the raw attribute segment of its opening delimiter —
23
+ * `=#FF0000`, `= "#FF0000"`, or `''` for a bare tag.
24
+ */
25
+ export function attributeValue(node: GreenNode): string {
26
+ const text = node.text || ''
27
+ const eq = text.indexOf('=')
28
+ let value = (eq >= 0 ? text.slice(eq + 1) : text).trim()
29
+
30
+ if (value.length >= 2 && QUOTES.has(value[0]) && value[value.length - 1] === value[0]) {
31
+ value = value.slice(1, -1).trim()
32
+ }
33
+ return value
34
+ }
35
+
36
+ const HEX6 = /^#[0-9a-fA-F]{6}$/
37
+ const HEX3 = /^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$/
38
+
39
+ /**
40
+ * Canonical form of a colour value, for equality only.
41
+ *
42
+ * Never written back to the document — `#F00` and `#FF0000` normalise to the
43
+ * same string so the two tags can merge, but whichever one the author wrote is
44
+ * the one that survives.
45
+ */
46
+ export function normalizeColorValue(raw: string): string {
47
+ const short = HEX3.exec(raw)
48
+ if (short) return `#${short[1]}${short[1]}${short[2]}${short[2]}${short[3]}${short[3]}`.toUpperCase()
49
+ if (HEX6.test(raw)) return raw.toUpperCase()
50
+ // Keywords (`red`, `Transparent`) are case-insensitive in CSS.
51
+ return raw.toLowerCase()
52
+ }
53
+
54
+ /** Six-digit hex whose channels are all doubled digits, e.g. `#FFAA00`. */
55
+ export function shortenableHex(raw: string): string | null {
56
+ if (!HEX6.test(raw)) return null
57
+ const r = raw.slice(1)
58
+ if (r[0].toLowerCase() !== r[1].toLowerCase()) return null
59
+ if (r[2].toLowerCase() !== r[3].toLowerCase()) return null
60
+ if (r[4].toLowerCase() !== r[5].toLowerCase()) return null
61
+ // Keep the author's own digit casing; only the redundant half is dropped.
62
+ return `#${r[0]}${r[2]}${r[4]}`
63
+ }
64
+
65
+ /**
66
+ * The identity two sibling tags must share to be mergeable.
67
+ *
68
+ * Returns `null` for kinds this optimizer does not fuse.
69
+ */
70
+ export function mergeIdentity(node: GreenNode): string | null {
71
+ if (node.kind === 'color') return `color|${normalizeColorValue(attributeValue(node))}`
72
+ if (MERGEABLE_INLINE.has(node.kind)) return `${node.kind}|${attributeValue(node).toLowerCase()}`
73
+ return null
74
+ }
75
+
76
+ /**
77
+ * Inline formatting tags that fuse when adjacent and identically attributed.
78
+ *
79
+ * Block kinds are absent on purpose. Merging `[box]a[/box][box]b[/box]` into
80
+ * one box is not a rewrite of the same document — it renders one frame where
81
+ * the author drew two.
82
+ */
83
+ export const MERGEABLE_INLINE: ReadonlySet<string> = new Set([
84
+ 'bold',
85
+ 'italic',
86
+ 'underline',
87
+ 'strikethrough',
88
+ 'font_size',
89
+ 'font',
90
+ 'sup',
91
+ 'sub',
92
+ 'mark',
93
+ ])
94
+
95
+ /**
96
+ * Kinds that may absorb the whitespace sitting between two merged siblings.
97
+ *
98
+ * `[color=X]a[/color] [color=X]b[/color]` → `[color=X]a b[/color]` is safe: a
99
+ * coloured space and an uncoloured space are the same pixels. The same move on
100
+ * `[u]` is **not** — the underline would extend across the gap, which is a
101
+ * visible change to a document the user only asked to make smaller. Size and
102
+ * font are excluded for the same reason: a space in another size or face can
103
+ * change advance width and line height.
104
+ */
105
+ export const BRIDGES_WHITESPACE: ReadonlySet<string> = new Set(['color', 'bold', 'italic'])
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Quasar — `unwrap-invisible-color`
3
+ *
4
+ * Removes a `[color]` whose entire content is whitespace.
5
+ *
6
+ * [color=#6A4C93]BARCA[/color][color=#6F518D] [/color][color=#755687]·[/color]
7
+ * ╰── 24 bytes to colour a space ──╯
8
+ *
9
+ * A space has no glyph, and `[color]` sets `color` and nothing else — no
10
+ * background, no decoration — so painting one is a guaranteed no-op. Deleting
11
+ * the two delimiters turns 24 bytes into 1 and cannot change a pixel.
12
+ *
13
+ * This is the dominant waste in expanded gradients, and it is easy to see why:
14
+ * the tool that expands `[gradient]` into per-character `[color]` spans walks
15
+ * characters without asking which of them draw ink, so every space in the ramp
16
+ * gets its own fully-spelled tag.
17
+ *
18
+ * ## Only colour
19
+ *
20
+ * The same shape is *not* removable for other tags, and the difference is not
21
+ * subtle:
22
+ *
23
+ * - `[u]` / `[s]` draw a line through the gap — visible on a space.
24
+ * - `[mark]` paints a background — visible on a space.
25
+ * - `[size]` and `[font]` change the advance width of the space itself.
26
+ *
27
+ * Only `color` is invisible on whitespace, so only `color` is unwrapped.
28
+ */
29
+
30
+ import type { GreenNode } from '../../Syntax/GreenNode'
31
+ import type { PlannedEdit } from '../EditPlan'
32
+ import {
33
+ type OptimizationRule,
34
+ type RuleContext,
35
+ type Positioned,
36
+ positionedChildren,
37
+ openRange,
38
+ closeRange,
39
+ hasBothDelimiters,
40
+ deletion,
41
+ } from './Rule'
42
+
43
+ export const UNWRAP_INVISIBLE_COLOR_PRIORITY = 85
44
+
45
+ /**
46
+ * Whether a node contributes nothing but whitespace.
47
+ *
48
+ * Recursive through nested colours, so `[color=A][color=B] [/color][/color]`
49
+ * counts. `spacing` is whitespace too — a newline inside a colour tag is as
50
+ * invisible as a space.
51
+ */
52
+ export function isInvisibleWhitespace(node: GreenNode): boolean {
53
+ if (node.kind === 'text') return /^\s*$/.test(node.text ?? '')
54
+ if (node.kind === 'spacing' || node.kind === 'empty_line') return true
55
+ if (node.kind !== 'color') return false
56
+
57
+ const children = node.children as readonly GreenNode[]
58
+ if (children.length === 0) return node.width - node.leadingWidth - node.trailingWidth === 0
59
+ return children.every(isInvisibleWhitespace)
60
+ }
61
+
62
+ export class UnwrapInvisibleColorRule implements OptimizationRule {
63
+ readonly id = 'unwrap-invisible-color'
64
+ readonly priority = UNWRAP_INVISIBLE_COLOR_PRIORITY
65
+ readonly label = 'Unwrap colour tags around whitespace'
66
+
67
+ run(context: RuleContext): PlannedEdit[] {
68
+ const sink: PlannedEdit[] = []
69
+ this.scan({ node: context.root, start: 0 }, sink)
70
+ sink.sort((a, b) => a.start - b.start || a.end - b.end)
71
+ return sink
72
+ }
73
+
74
+ // ── Private ─────────────────────────────────────────────────────
75
+
76
+ private scan(item: Positioned, sink: PlannedEdit[]): void {
77
+ for (const child of positionedChildren(item)) {
78
+ if (
79
+ child.node.kind === 'color' &&
80
+ hasBothDelimiters(child) &&
81
+ isInvisibleWhitespace(child.node)
82
+ ) {
83
+ const label = 'Unwrap colour around whitespace'
84
+ sink.push(deletion(openRange(child), this.id, this.priority, label))
85
+ sink.push(deletion(closeRange(child), this.id, this.priority, label))
86
+ // Nested colours inside are invisible too; keep unwrapping them.
87
+ this.scan(child, sink)
88
+ continue
89
+ }
90
+ this.scan(child, sink)
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Quasar — applying a resolved edit set to source text
3
+ *
4
+ * The string-rewriting half of the two appliers. Monaco is the other: it takes
5
+ * the very same `SurgicalEdit[]` and resolves the offsets against its model
6
+ * itself, which is why neither side ever re-serializes the document and the
7
+ * two cannot drift.
8
+ */
9
+
10
+ import type { SurgicalEdit } from '../Reconciler/SurgicalReconciler'
11
+
12
+ /**
13
+ * Apply edits carrying ORIGINAL offsets, in one pass.
14
+ *
15
+ * The obvious implementation splices the string once per edit:
16
+ *
17
+ * for (const edit of backToFront) out = out.slice(0, s) + text + out.slice(e)
18
+ *
19
+ * which is correct and quadratic — every splice copies the whole document, so
20
+ * a 250.000-character page with ~3.800 edits copies roughly a billion
21
+ * characters and takes about half a second. Measured, not guessed. Collecting
22
+ * the untouched spans and joining them once is linear and turns that into a
23
+ * few milliseconds, which is the difference between a minify command that
24
+ * feels instant and one that visibly stalls the editor.
25
+ *
26
+ * Going forwards rather than backwards is what makes the single pass possible,
27
+ * and it costs nothing: each edit is measured against the original string
28
+ * either way.
29
+ *
30
+ * ## Contract
31
+ *
32
+ * The edits must be pairwise conflict-free — run them through
33
+ * `resolveEditConflicts` first. An edit that reaches back into a span already
34
+ * written is a contract violation with no meaningful answer, so it is skipped
35
+ * rather than allowed to interleave into nonsense; the result stays a valid
36
+ * document that is merely missing one change.
37
+ */
38
+ export function applyEditsToSource(source: string, edits: readonly SurgicalEdit[]): string {
39
+ if (edits.length === 0) return source
40
+
41
+ const ordered = [...edits].sort((a, b) => a.start - b.start || a.end - b.end)
42
+ const segments: string[] = []
43
+ let cursor = 0
44
+
45
+ for (const edit of ordered) {
46
+ if (edit.start < cursor) continue
47
+ if (edit.start > cursor) segments.push(source.slice(cursor, edit.start))
48
+ if (edit.text) segments.push(edit.text)
49
+ cursor = edit.end
50
+ }
51
+
52
+ if (cursor < source.length) segments.push(source.slice(cursor))
53
+ return segments.join('')
54
+ }
@@ -0,0 +1,22 @@
1
+ export {
2
+ classifyOverlap,
3
+ editsConflict,
4
+ compareEditPriority,
5
+ resolveEditConflicts,
6
+ } from './EditPlan'
7
+ export type {
8
+ PlannedEdit,
9
+ RejectedEdit,
10
+ ResolvedEditPlan,
11
+ EditRejectionReason,
12
+ OverlapRelation,
13
+ } from './EditPlan'
14
+ export { applyEditsToSource } from './applyEdits'
15
+ export {
16
+ optimizeBBCode,
17
+ optimizeTree,
18
+ defaultRules,
19
+ allRules,
20
+ } from './Optimizer'
21
+ export type { OptimizationResult, OptimizeOptions, RuleStat } from './Optimizer'
22
+ export * from './Rules'
@@ -45,6 +45,24 @@ export interface DocumentEvent {
45
45
  * of changes it applied itself. Absent on events with no single cause.
46
46
  */
47
47
  origin?: string
48
+ /**
49
+ * On `diagnostics_updated`: how the analysis that produced them was
50
+ * obtained — `'full'` (every node revalidated) or `'window'` (only the
51
+ * nodes the edit could have changed; every other node kept its verdict).
52
+ *
53
+ * The `diagnostics` on the event are the WHOLE document's either way, so a
54
+ * subscriber renders them the same in both cases. This is here for the
55
+ * cases where the difference is the point: a performance overlay, a test
56
+ * that must notice the incremental path going away, a log line explaining a
57
+ * slow frame. See `AnalyzeScope`.
58
+ */
59
+ analysisScope?: 'full' | 'window'
60
+ /**
61
+ * On `diagnostics_updated` with `analysisScope === 'window'`: the span of
62
+ * the current source that was re-validated, in current coordinates.
63
+ * `null`/absent when the whole document was.
64
+ */
65
+ analysisWindow?: { start: number; end: number } | null
48
66
  timestamp: number
49
67
  [key: string]: unknown
50
68
  }