@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,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
  }