@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
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Quasar — Surgical Edit Plan & Conflict Resolution
3
+ *
4
+ * The optimizer has exactly one output: a set of `SurgicalEdit`s over the
5
+ * ORIGINAL source. Two appliers consume it — Monaco in-place, and a pure
6
+ * string rewrite for export — so neither ever re-serializes the document and
7
+ * neither can drift from the other.
8
+ *
9
+ * That design has one hard prerequisite, which is what this file is:
10
+ * **the edits handed to an applier must be pairwise conflict-free.**
11
+ * Monaco is explicit that it will not arbitrate:
12
+ *
13
+ * > Overlapping ranges are the caller's responsibility — Monaco has no
14
+ * > answer for two edits claiming the same character.
15
+ * — `packages/core/Editor/Core_Editor.tsx`
16
+ *
17
+ * Rules are authored independently and WILL collide: merging a run of
18
+ * identical `[color=#FF0000]` tags and shortening that same `#FF0000` to
19
+ * `#F00` claim overlapping bytes. The resolution must never be "last writer
20
+ * wins", because that makes the output depend on the order rules happen to be
21
+ * registered in. Everything here exists to make the outcome a pure function of
22
+ * the edit set itself.
23
+ *
24
+ * @see resolveEditConflicts
25
+ */
26
+ import type { SurgicalEdit } from '../Reconciler/SurgicalReconciler';
27
+ /**
28
+ * A `SurgicalEdit` plus the provenance the resolver needs to arbitrate and
29
+ * the UI needs to explain itself.
30
+ *
31
+ * `priority` is declared by the rule, not derived from the edit. A rule that
32
+ * rewrites a whole region outranks one that tweaks a token inside it, and
33
+ * that relationship is a property of the rules — not something you can read
34
+ * off two ranges.
35
+ */
36
+ export interface PlannedEdit extends SurgicalEdit {
37
+ /** Identifies the rule that produced this edit. Also the final tie-break. */
38
+ readonly ruleId: string;
39
+ /** Higher wins. See {@link compareEditPriority} for the full order. */
40
+ readonly priority: number;
41
+ /** Human-readable intent, for the "what did the minifier do" panel. */
42
+ readonly label?: string;
43
+ }
44
+ /**
45
+ * Why an edit did not make it into the applied set.
46
+ *
47
+ * - `invalid-range` — the rule emitted a range that is not a valid slice of
48
+ * the source. A rule bug; dropped rather than thrown so one broken rule
49
+ * cannot destroy an otherwise good batch.
50
+ * - `subsumed` — a winning edit's range fully contains this one (equal ranges
51
+ * included). The winner rewrites these bytes wholesale, so this edit's
52
+ * intent is either already covered or no longer meaningful.
53
+ * - `straddle` — partial overlap where NEITHER range contains the other.
54
+ * Reported separately from `subsumed` on purpose: see the note on
55
+ * {@link classifyOverlap}.
56
+ */
57
+ export type EditRejectionReason = 'invalid-range' | 'subsumed' | 'straddle';
58
+ export interface RejectedEdit {
59
+ readonly edit: PlannedEdit;
60
+ readonly reason: EditRejectionReason;
61
+ /** The accepted edit that displaced it. Absent for `invalid-range`. */
62
+ readonly winner?: PlannedEdit;
63
+ }
64
+ export interface ResolvedEditPlan {
65
+ /**
66
+ * Pairwise conflict-free, sorted by ascending `start`. Safe to hand
67
+ * verbatim to `applySurgicalEdits` (Monaco resolves original offsets
68
+ * itself) or to a back-to-front string rewrite.
69
+ */
70
+ readonly accepted: readonly PlannedEdit[];
71
+ /** Everything dropped, with the reason and the edit that displaced it. */
72
+ readonly rejected: readonly RejectedEdit[];
73
+ }
74
+ /** How two claimed ranges relate. */
75
+ export type OverlapRelation = 'disjoint' | 'subsumption' | 'straddle';
76
+ /**
77
+ * Classify two edits by the bytes they claim.
78
+ *
79
+ * Ranges are half-open `[start, end)` over the original source, so edits that
80
+ * merely touch (`a.end === b.start`) are disjoint — adjacent replacements are
81
+ * perfectly legal and common.
82
+ *
83
+ * Zero-width edits (insertions, `start === end`) need two special cases that
84
+ * pure interval intersection gets wrong:
85
+ *
86
+ * 1. Two insertions at the SAME offset have an empty intersection, yet both
87
+ * write at one point with no defined order between them. That is a
88
+ * conflict, classified as `subsumption` so priority decides it.
89
+ * 2. An insertion strictly inside a replacement also has an empty
90
+ * intersection, but the replacement destroys the bytes the insertion point
91
+ * refers to, so the insertion is meaningless. Also `subsumption`.
92
+ *
93
+ * An insertion exactly at a replacement's boundary stays disjoint — it is
94
+ * unambiguously before or after, and both appliers agree on that.
95
+ *
96
+ * `straddle` is deliberately its own relation rather than folded into
97
+ * `subsumption`. It is resolved the same way (drop the loser), but two
98
+ * *maximal* rules should never straddle each other: a straddle almost always
99
+ * means a rule computed a partial range instead of the whole normal form.
100
+ * Keeping it distinct is what makes that bug visible instead of silent.
101
+ */
102
+ export declare function classifyOverlap(a: SurgicalEdit, b: SurgicalEdit): OverlapRelation;
103
+ /** Whether two edits may not be applied together. */
104
+ export declare function editsConflict(a: SurgicalEdit, b: SurgicalEdit): boolean;
105
+ /**
106
+ * The arbitration order, as a comparator: negative means `a` is considered
107
+ * first and therefore wins any conflict against `b`.
108
+ *
109
+ * The chain is **total** — it never returns 0 for two distinguishable edits.
110
+ * That is the whole point. A partial order would leave ties broken by
111
+ * `Array.prototype.sort` stability, i.e. by the order rules were registered,
112
+ * which is exactly the non-determinism this contract exists to prevent.
113
+ *
114
+ * 1. `priority` descending — the rule's own declaration, and the only lever a
115
+ * rule author has.
116
+ * 2. Width descending — at equal priority the wider claim wins, because a
117
+ * broader rewrite subsumes the work of a narrower one more often than the
118
+ * reverse. (A zero-width insertion therefore loses to any replacement it
119
+ * ties with, which is what you want: the replacement carries text anyway.)
120
+ * 3. `start` ascending — earlier in the document.
121
+ * 4. `ruleId`, then `text`, lexicographically — no semantic meaning, purely
122
+ * the guarantee that the order is total.
123
+ */
124
+ export declare function compareEditPriority(a: PlannedEdit, b: PlannedEdit): number;
125
+ /**
126
+ * Reduce a rule-authored edit set to a conflict-free one.
127
+ *
128
+ * Greedy by {@link compareEditPriority}: walk the edits in arbitration order
129
+ * and accept each one that collides with nothing already accepted.
130
+ *
131
+ * This is deliberately **not** a maximum-weight independent set. Greedy can
132
+ * drop two small edits to keep one large one that saves fewer bytes, and that
133
+ * is an accepted trade: the optimizer's contract is that the same document
134
+ * always minifies to the same output, not that it minifies to the smallest
135
+ * possible output. Predictability is worth more than the last few bytes in a
136
+ * tool that rewrites someone's document.
137
+ *
138
+ * The result is order-independent — shuffling `edits` cannot change
139
+ * `accepted` — which is the property the tests pin down.
140
+ *
141
+ * @param edits Rule output, in any order.
142
+ * @param sourceLength Length of the ORIGINAL source every range indexes into.
143
+ */
144
+ export declare function resolveEditConflicts(edits: readonly PlannedEdit[], sourceLength: number): ResolvedEditPlan;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Quasar — BBCode Optimizer
3
+ *
4
+ * Parses a document, runs every optimization rule over the tree, arbitrates
5
+ * the edits they produce, and hands back one conflict-free `SurgicalEdit[]`
6
+ * addressed to the ORIGINAL source.
7
+ *
8
+ * That edit set is the optimizer's only output. There is deliberately no
9
+ * "optimized tree" — two appliers consume the edits instead:
10
+ *
11
+ * in-place → applySurgicalEdits(result.edits) (Monaco, one undo stop)
12
+ * export → result.output (applyEditsToSource)
13
+ *
14
+ * Keeping a tree optimizer *and* a range optimizer in step forever was the
15
+ * alternative, and it was rejected before the first rule was written. A tree
16
+ * optimizer's output can only reach the document through a whole-document
17
+ * re-export, which silently rewrites spellings the author chose (`[B]`→`[b]`,
18
+ * `\r\n`→`\n`, unclosed tags closed). That is fine for an export and
19
+ * unacceptable for someone's open buffer.
20
+ */
21
+ import type { BBCodeDialect } from '../BBCode/BBCodeToGreenNode';
22
+ import type { GreenNode } from '../Syntax/GreenNode';
23
+ import { type PlannedEdit, type ResolvedEditPlan } from './EditPlan';
24
+ import type { OptimizationRule } from './Rules/Rule';
25
+ /**
26
+ * The rules a plain "minify" run applies.
27
+ *
28
+ * Ordered by descending priority, which is also the order in which their
29
+ * regions subsume one another: a merge deletes the delimiters that a hex
30
+ * shortening inside them would have edited.
31
+ *
32
+ * `ReorderWrappersRule` is **not** here. It is a real, tested rule, but it
33
+ * saves exactly zero bytes — it canonicalises nesting order rather than
34
+ * removing anything. A minifier that rewrites someone's document without
35
+ * making it smaller is a diff with no upside, so it is opt-in.
36
+ */
37
+ export declare function defaultRules(): OptimizationRule[];
38
+ /** Every rule that exists, including the ones outside the default preset. */
39
+ export declare function allRules(): OptimizationRule[];
40
+ export interface RuleStat {
41
+ readonly ruleId: string;
42
+ readonly label: string;
43
+ /** Edits this rule contributed to the applied set. */
44
+ readonly edits: number;
45
+ /** Characters removed by those edits. Never negative for the shipped rules. */
46
+ readonly savedChars: number;
47
+ }
48
+ export interface OptimizationResult {
49
+ /** The document as given. */
50
+ readonly source: string;
51
+ /** Conflict-free, sorted by ascending start. Hand straight to either applier. */
52
+ readonly edits: readonly PlannedEdit[];
53
+ /** Full arbitration record, including what was dropped and why. */
54
+ readonly plan: ResolvedEditPlan;
55
+ /** `source` with `edits` applied. */
56
+ readonly output: string;
57
+ readonly savedChars: number;
58
+ /** Per-rule breakdown of the *applied* edits, in descending saving order. */
59
+ readonly stats: readonly RuleStat[];
60
+ }
61
+ export interface OptimizeOptions {
62
+ readonly rules?: readonly OptimizationRule[];
63
+ /** Dialect to parse against. Defaults to `'lyne'`, the superset. */
64
+ readonly dialect?: BBCodeDialect;
65
+ }
66
+ /**
67
+ * Optimize a parsed tree.
68
+ *
69
+ * Prefer this when the caller already holds a green tree — the editor reparses
70
+ * on every keystroke and has no reason to pay for another parse.
71
+ *
72
+ * `root.width` must equal `source.length`; the rules address offsets into that
73
+ * exact string. A mismatch means the tree describes a different document, and
74
+ * every range would land in the wrong place, so it is refused outright.
75
+ */
76
+ export declare function optimizeTree(source: string, root: GreenNode, options?: OptimizeOptions): OptimizationResult;
77
+ /** Parse `source` and optimize it. */
78
+ export declare function optimizeBBCode(source: string, options?: OptimizeOptions): OptimizationResult;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Quasar — Optimization Rule contract
3
+ *
4
+ * Every rule reads the Green Tree and returns `PlannedEdit[]` addressed to
5
+ * offsets in the ORIGINAL source. Rules never mutate the tree, never build a
6
+ * new one, and never see each other's output — the only thing that arbitrates
7
+ * between them is `resolveEditConflicts`.
8
+ *
9
+ * Three obligations, all of them load-bearing:
10
+ *
11
+ * 1. **Maximal.** A rule emits the final form in one pass. It may not rely on
12
+ * being run again: applying an edit invalidates every later offset, so
13
+ * there is no second pass to be had. Where `ASTOptimizer` merged pairwise
14
+ * and iterated to a fixpoint, a rule here recognises the whole run.
15
+ * 2. **Deterministic.** Same tree in, same edits out, in the same order.
16
+ * 3. **Spelling-preserving.** Prefer deleting the author's bytes over
17
+ * generating replacements. `[Color = "#FF0000"]` must survive a merge
18
+ * intact; the exporter would have rewritten it to `[color=#FF0000]`.
19
+ *
20
+ * @see resolveEditConflicts
21
+ */
22
+ import type { GreenNode } from '../../Syntax/GreenNode';
23
+ import type { PlannedEdit } from '../EditPlan';
24
+ export interface RuleContext {
25
+ /** The document exactly as the author wrote it. */
26
+ readonly source: string;
27
+ /** Green root, whose width equals `source.length`. */
28
+ readonly root: GreenNode;
29
+ }
30
+ export interface OptimizationRule {
31
+ /** Stable identifier; also the final tie-break in the conflict order. */
32
+ readonly id: string;
33
+ /**
34
+ * Arbitration weight. A rule that rewrites a whole region must outrank the
35
+ * rules that tweak tokens inside it, so the region's own normal form wins.
36
+ */
37
+ readonly priority: number;
38
+ /** Short human label, shown in the "what did the minifier do" list. */
39
+ readonly label: string;
40
+ run(context: RuleContext): PlannedEdit[];
41
+ }
42
+ /**
43
+ * A node together with its absolute start offset in the source.
44
+ *
45
+ * Green nodes carry widths, not positions, so every rule that wants a range
46
+ * has to accumulate offsets on the way down. This pairing is what the rules
47
+ * pass around instead of bare nodes.
48
+ */
49
+ export interface Positioned {
50
+ readonly node: GreenNode;
51
+ readonly start: number;
52
+ }
53
+ /** Absolute end offset (exclusive). */
54
+ export declare function endOf(p: Positioned): number;
55
+ /** The range of the opening delimiter — `[color=#F00]`. */
56
+ export declare function openRange(p: Positioned): {
57
+ start: number;
58
+ end: number;
59
+ };
60
+ /** The range of the closing delimiter — `[/color]`. */
61
+ export declare function closeRange(p: Positioned): {
62
+ start: number;
63
+ end: number;
64
+ };
65
+ /** Children paired with their absolute offsets, in document order. */
66
+ export declare function positionedChildren(p: Positioned): Positioned[];
67
+ /**
68
+ * A node that actually has both delimiters in the source.
69
+ *
70
+ * The parser synthesises a closing tag for `[b]never closed`, which comes back
71
+ * as `trailingWidth === 0`. Deleting a delimiter that occupies no bytes is a
72
+ * no-op at best; treating a synthetic close as if it were real is a
73
+ * correctness bug. Rules that move or remove delimiters check this first.
74
+ */
75
+ export declare function hasBothDelimiters(p: Positioned): boolean;
76
+ /** A deletion of an exact source range. */
77
+ export declare function deletion(range: {
78
+ start: number;
79
+ end: number;
80
+ }, ruleId: string, priority: number, label?: string): PlannedEdit;
81
+ /**
82
+ * Merge deletions that touch into single edits.
83
+ *
84
+ * Two deletions `[a,b)` and `[b,c)` are legal side by side — the conflict
85
+ * resolver calls them disjoint — but they describe one contiguous removal.
86
+ * Emitting them separately inflates the edit count for no benefit and makes
87
+ * the "N optimisations applied" number lie about how much happened.
88
+ *
89
+ * Input must be sorted by `start` and non-overlapping.
90
+ */
91
+ export declare function coalesceDeletions(edits: readonly PlannedEdit[]): PlannedEdit[];
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Quasar — `drop-empty-tags`
3
+ *
4
+ * Removes formatting tags that enclose nothing: `[b][/b]`, `[color=#F00][/color]`.
5
+ *
6
+ * ## Only tags whose absence is invisible
7
+ *
8
+ * The droppable set is an explicit allowlist of inline formatting, not "any
9
+ * tag with no children". An empty `[box]` still draws a frame, an empty
10
+ * `[quote]` still draws a quote block, and `[img]`/`[youtube]` carry their
11
+ * payload in the attribute rather than in children — deleting any of those
12
+ * changes what the reader sees. Anything not on the list stays, including
13
+ * tags this engine does not recognise.
14
+ *
15
+ * ## Outermost-first, so no second pass is needed
16
+ *
17
+ * `[b][i][/i][/b]` is empty all the way down. Dropping the inner tag would
18
+ * leave `[b][/b]`, which is empty again — the classic reason `ASTOptimizer`
19
+ * needs a fixpoint. Instead this rule asks whether a node renders nothing
20
+ * *transitively* and deletes the outermost one it finds, in a single edit,
21
+ * without descending into it.
22
+ */
23
+ import type { PlannedEdit } from '../EditPlan';
24
+ import { type OptimizationRule, type RuleContext } from './Rule';
25
+ export declare const DROP_EMPTY_PRIORITY = 80;
26
+ /**
27
+ * Inline formatting whose empty form is guaranteed to render nothing.
28
+ *
29
+ * Deliberately conservative. A kind is only here if an empty instance of it
30
+ * produces no glyphs, no box, no line break and no side effect.
31
+ */
32
+ export declare const DROPPABLE_WHEN_EMPTY: ReadonlySet<string>;
33
+ export declare class DropEmptyTagsRule implements OptimizationRule {
34
+ readonly id = "drop-empty-tags";
35
+ readonly priority = 80;
36
+ readonly label = "Remove empty formatting tags";
37
+ run(context: RuleContext): PlannedEdit[];
38
+ private scan;
39
+ private isDroppable;
40
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Quasar — `drop-redundant-nesting`
3
+ *
4
+ * Removes a tag nested directly inside an identical one:
5
+ *
6
+ * [color=#F00]a[color=#F00]b[/color][/color] → [color=#F00]ab[/color]
7
+ *
8
+ * The inner delimiters are the ones deleted, so the outer tag — and the
9
+ * author's spelling of it — is what survives.
10
+ *
11
+ * Identity is the same normalised comparison the merge rule uses, so
12
+ * `[color=#F00]` inside `[color=#FF0000]` counts as redundant: the colours are
13
+ * equal even though the spellings are not.
14
+ *
15
+ * Siblings do not disqualify anything. In `[color=X]a[color=X]b[/color][/color]`
16
+ * the inner tag is still setting a colour that is already in force, so
17
+ * dropping its delimiters cannot change the render.
18
+ *
19
+ * ## Only tags that do nothing when nested in themselves
20
+ *
21
+ * "Redundant" has to be earned, not assumed. `[size=50]` renders as
22
+ * `font-size:50%`, and a percentage is relative to its parent — so
23
+ * `[size=50][size=50]x[/size][/size]` is 25%, not 50%, and the inner tag is
24
+ * doing real work. `[sup]` stacks its vertical offset the same way. Removing
25
+ * either would shrink or lift the text, which is not a smaller document, it is
26
+ * a different one.
27
+ *
28
+ * @see IDEMPOTENT_WHEN_NESTED
29
+ */
30
+ import type { PlannedEdit } from '../EditPlan';
31
+ import { type OptimizationRule, type RuleContext } from './Rule';
32
+ export declare const DROP_REDUNDANT_NESTING_PRIORITY = 90;
33
+ /**
34
+ * Kinds whose effect is unchanged by being applied twice.
35
+ *
36
+ * A colour inside the same colour is still that colour; bold inside bold is
37
+ * still bold. Excluded are the properties that **compound**: `font_size`
38
+ * multiplies percentages, and `sup`/`sub` add their vertical offsets. Anything
39
+ * not listed is left alone, unknown tags included.
40
+ */
41
+ export declare const IDEMPOTENT_WHEN_NESTED: ReadonlySet<string>;
42
+ export declare class DropRedundantNestingRule implements OptimizationRule {
43
+ readonly id = "drop-redundant-nesting";
44
+ readonly priority = 90;
45
+ readonly label = "Remove tags nested inside an identical tag";
46
+ run(context: RuleContext): PlannedEdit[];
47
+ /**
48
+ * `inForce` maps a node kind to the identity its NEAREST ancestor of that
49
+ * kind established.
50
+ *
51
+ * Keyed by kind rather than carrying a single inherited identity, because an
52
+ * ancestor's colour survives an intervening tag of another kind:
53
+ * `[color=X][b][color=X]…` is still redundant. Overwriting on the way down
54
+ * is what makes it the *nearest* ancestor — in
55
+ * `[color=X][color=Y][color=X]…` the innermost tag restores a colour that Y
56
+ * had overridden, so it is doing real work and must stay.
57
+ */
58
+ private scan;
59
+ }
@@ -0,0 +1,9 @@
1
+ export type { OptimizationRule, RuleContext, Positioned } from './Rule';
2
+ export { endOf, openRange, closeRange, positionedChildren, hasBothDelimiters, deletion, coalesceDeletions, } from './Rule';
3
+ export { attributeValue, normalizeColorValue, shortenableHex, mergeIdentity, MERGEABLE_INLINE, BRIDGES_WHITESPACE, } from './tagValue';
4
+ export { MergeAdjacentRule, MERGE_ADJACENT_PRIORITY, type MergeAdjacentOptions } from './mergeAdjacent';
5
+ export { DropEmptyTagsRule, DROP_EMPTY_PRIORITY, DROPPABLE_WHEN_EMPTY } from './dropEmptyTags';
6
+ export { DropRedundantNestingRule, DROP_REDUNDANT_NESTING_PRIORITY } from './dropRedundantNesting';
7
+ export { ShortenHexRule, SHORTEN_HEX_PRIORITY } from './shortenHex';
8
+ export { UnwrapInvisibleColorRule, UNWRAP_INVISIBLE_COLOR_PRIORITY, isInvisibleWhitespace, } from './unwrapInvisibleColor';
9
+ export { ReorderWrappersRule, REORDER_WRAPPERS_PRIORITY } from './reorderWrappers';
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Quasar — `merge-adjacent`
3
+ *
4
+ * Fuses runs of identically-attributed sibling tags into one:
5
+ *
6
+ * [color=#FF0000]H[/color][color=#FF0000]e[/color][color=#FF0000]y[/color]
7
+ * → [color=#FF0000]Hey[/color]
8
+ *
9
+ * This is where the bytes are. A per-character gradient spends ~23 bytes of
10
+ * delimiter on every 1 byte of text; collapsing a run of identical colours is
11
+ * the single largest saving available on a real userpage.
12
+ *
13
+ * ## It only ever deletes
14
+ *
15
+ * The merged tag is not generated. The first member's opening delimiter and
16
+ * the last member's closing delimiter are left exactly as the author typed
17
+ * them, and only the seams between members are removed:
18
+ *
19
+ * [Color = "#F00"]a[/color][color=#ff0000]b[/color]
20
+ * [Color = "#F00"]a‹ deleted ›b[/color]
21
+ *
22
+ * So `[Color = "#F00"]` survives with its capital C, its spaces and its
23
+ * quotes. A rule that rebuilt the tag would have normalised all three, which
24
+ * is exactly the drift that makes whole-document re-export unusable in an
25
+ * editor.
26
+ *
27
+ * ## Maximal in one pass
28
+ *
29
+ * `ASTOptimizer` merges pairwise and iterates to a fixpoint. Edits addressed
30
+ * to the original source cannot do that — applying one invalidates every later
31
+ * offset — so this rule recognises the **whole run at once**, and then
32
+ * descends into the content the merge is about to create:
33
+ *
34
+ * [b][color=X]a[/color][/b][b][color=X]bc[/color][/b]
35
+ *
36
+ * Merging the two `[b]` makes their colour children siblings, and those merge
37
+ * too. The recursion walks that *virtual* child list, so both levels come out
38
+ * of a single pass. Crucially the inner seams sit inside the members' content
39
+ * and the outer seams sit between members, so the two sets never overlap.
40
+ *
41
+ * @see mergeIdentity — what "identically attributed" means
42
+ * @see BRIDGES_WHITESPACE — when a gap between members may be absorbed
43
+ */
44
+ import type { PlannedEdit } from '../EditPlan';
45
+ import { type OptimizationRule, type RuleContext } from './Rule';
46
+ export declare const MERGE_ADJACENT_PRIORITY = 100;
47
+ export interface MergeAdjacentOptions {
48
+ /**
49
+ * Restrict the rule to these node kinds. Omitted, every kind
50
+ * {@link mergeIdentity} recognises is eligible.
51
+ */
52
+ readonly kinds?: ReadonlySet<string>;
53
+ }
54
+ export declare class MergeAdjacentRule implements OptimizationRule {
55
+ readonly id = "merge-adjacent";
56
+ readonly priority = 100;
57
+ readonly label = "Merge adjacent identical tags";
58
+ private readonly kinds?;
59
+ constructor(options?: MergeAdjacentOptions);
60
+ run(context: RuleContext): PlannedEdit[];
61
+ private identityOf;
62
+ /**
63
+ * A node that may sit between two members and be absorbed by the merge.
64
+ *
65
+ * Blank text, or a colour tag wrapping nothing but blank text — the shape
66
+ * `unwrap-invisible-color` exists to delete. Treating it as a gap rather
67
+ * than as a wall lets `[color=A]x[/color][color=B] [/color][color=A]y[/color]`
68
+ * still fuse its two `A` members; the `B` tag survives this rule untouched
69
+ * and is removed by the other one, whose edits are disjoint from these.
70
+ */
71
+ private isBridge;
72
+ /**
73
+ * Walk one sibling list, fusing every maximal run it contains.
74
+ *
75
+ * The list is not always a real node's children: after a merge is planned,
76
+ * the members' children are spliced together and scanned as the sibling list
77
+ * the merged tag will actually have.
78
+ */
79
+ private scan;
80
+ /**
81
+ * Delete each member's closing delimiter and the next member's opening one.
82
+ *
83
+ * Two edits per seam rather than one span, because a bridge may sit between
84
+ * them and must survive. When nothing separates them the two deletions touch
85
+ * and `coalesceDeletions` folds them back into a single edit.
86
+ */
87
+ private emitSeams;
88
+ /**
89
+ * The sibling list the merged tag will hold: every member's children in
90
+ * order, with any bridging text kept in place between them.
91
+ */
92
+ private virtualContent;
93
+ }
@@ -0,0 +1,42 @@
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
+ import type { PlannedEdit } from '../EditPlan';
31
+ import { type OptimizationRule, type RuleContext } from './Rule';
32
+ export declare const REORDER_WRAPPERS_PRIORITY = 5;
33
+ export declare class ReorderWrappersRule implements OptimizationRule {
34
+ readonly id = "reorder-wrappers";
35
+ readonly priority = 5;
36
+ readonly label = "Canonicalise nested wrapper order";
37
+ run(context: RuleContext): PlannedEdit[];
38
+ private scan;
39
+ /** The maximal run of rankable single-child wrappers starting at `item`. */
40
+ private chainFrom;
41
+ private emitPermutation;
42
+ }
@@ -0,0 +1,30 @@
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
+ import type { PlannedEdit } from '../EditPlan';
21
+ import { type OptimizationRule, type RuleContext } from './Rule';
22
+ export declare const SHORTEN_HEX_PRIORITY = 10;
23
+ export declare class ShortenHexRule implements OptimizationRule {
24
+ readonly id = "shorten-hex";
25
+ readonly priority = 10;
26
+ readonly label = "Shorten 6-digit hex colours";
27
+ run(context: RuleContext): PlannedEdit[];
28
+ private scan;
29
+ private shorten;
30
+ }
@@ -0,0 +1,56 @@
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
+ import type { GreenNode } from '../../Syntax/GreenNode';
15
+ /**
16
+ * The attribute value a tag node carries, unquoted and trimmed.
17
+ *
18
+ * A node's `text` holds the raw attribute segment of its opening delimiter —
19
+ * `=#FF0000`, `= "#FF0000"`, or `''` for a bare tag.
20
+ */
21
+ export declare function attributeValue(node: GreenNode): string;
22
+ /**
23
+ * Canonical form of a colour value, for equality only.
24
+ *
25
+ * Never written back to the document — `#F00` and `#FF0000` normalise to the
26
+ * same string so the two tags can merge, but whichever one the author wrote is
27
+ * the one that survives.
28
+ */
29
+ export declare function normalizeColorValue(raw: string): string;
30
+ /** Six-digit hex whose channels are all doubled digits, e.g. `#FFAA00`. */
31
+ export declare function shortenableHex(raw: string): string | null;
32
+ /**
33
+ * The identity two sibling tags must share to be mergeable.
34
+ *
35
+ * Returns `null` for kinds this optimizer does not fuse.
36
+ */
37
+ export declare function mergeIdentity(node: GreenNode): string | null;
38
+ /**
39
+ * Inline formatting tags that fuse when adjacent and identically attributed.
40
+ *
41
+ * Block kinds are absent on purpose. Merging `[box]a[/box][box]b[/box]` into
42
+ * one box is not a rewrite of the same document — it renders one frame where
43
+ * the author drew two.
44
+ */
45
+ export declare const MERGEABLE_INLINE: ReadonlySet<string>;
46
+ /**
47
+ * Kinds that may absorb the whitespace sitting between two merged siblings.
48
+ *
49
+ * `[color=X]a[/color] [color=X]b[/color]` → `[color=X]a b[/color]` is safe: a
50
+ * coloured space and an uncoloured space are the same pixels. The same move on
51
+ * `[u]` is **not** — the underline would extend across the gap, which is a
52
+ * visible change to a document the user only asked to make smaller. Size and
53
+ * font are excluded for the same reason: a space in another size or face can
54
+ * change advance width and line height.
55
+ */
56
+ export declare const BRIDGES_WHITESPACE: ReadonlySet<string>;