@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.
- package/dist/Visuals/lyne.css +117 -6
- package/dist/Visuals/osu.css +36 -5
- package/dist/index.js +9664 -3282
- package/dist/index.mjs +9535 -3283
- package/dist/types/Analysis/Contracts/AnalysisReport.d.ts +22 -0
- package/dist/types/Analysis/Contracts/Contribution.d.ts +68 -0
- package/dist/types/Analysis/Contracts/Pass.d.ts +59 -0
- package/dist/types/Analysis/Contracts/PipelineContext.d.ts +34 -0
- package/dist/types/Analysis/Passes/Analysis/ColorUsageAnalyzer.d.ts +56 -0
- package/dist/types/Analysis/Passes/Analysis/GradientAnalyzer.d.ts +111 -0
- package/dist/types/Analysis/Passes/Analysis/MergeableColorAnalyzer.d.ts +26 -0
- package/dist/types/Analysis/Passes/Analysis/RainbowAnalyzer.d.ts +47 -0
- package/dist/types/Analysis/Passes/Analysis/SymbolAnalyzer.d.ts +89 -0
- package/dist/types/Analysis/Passes/Analysis/WaveAnalyzer.d.ts +44 -0
- package/dist/types/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.d.ts +4 -0
- package/dist/types/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.d.ts +4 -0
- package/dist/types/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.d.ts +4 -0
- package/dist/types/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.d.ts +4 -0
- package/dist/types/Analysis/Passes/Decision/DefaultDecision.d.ts +30 -0
- package/dist/types/Analysis/Passes/Decision/PaletteRemapDecision.d.ts +169 -0
- package/dist/types/Analysis/Passes/Decision/__tests__/DefaultDecision.test.d.ts +4 -0
- package/dist/types/Analysis/Passes/Transform/CollapseGradientTransform.d.ts +37 -0
- package/dist/types/Analysis/Passes/Transform/MergeColorsTransform.d.ts +28 -0
- package/dist/types/Analysis/Passes/Transform/RainbowCollapseTransform.d.ts +19 -0
- package/dist/types/Analysis/Passes/Transform/WaveCollapseTransform.d.ts +20 -0
- package/dist/types/Analysis/Passes/Utility/CharacterCountAnalyzer.d.ts +19 -0
- package/dist/types/Analysis/Pipeline/Pipeline.d.ts +69 -0
- package/dist/types/Analysis/Pipeline/PipelineBuilder.d.ts +31 -0
- package/dist/types/Analysis/Pipeline/PipelineStage.d.ts +17 -0
- package/dist/types/Analysis/Utils/color-utils.d.ts +60 -0
- package/dist/types/Analysis/__tests__/Integration.test.d.ts +8 -0
- package/dist/types/Analysis/__tests__/PaletteRemap.test.d.ts +14 -0
- package/dist/types/Analysis/__tests__/Pipeline.test.d.ts +10 -0
- package/dist/types/Analysis/__tests__/RoundTrip.test.d.ts +30 -0
- package/dist/types/Analysis/__tests__/SymbolAnalyzer.test.d.ts +11 -0
- package/dist/types/Analysis/index.d.ts +40 -0
- package/dist/types/BBCode/BBCodeDocumentModel.d.ts +92 -0
- package/dist/types/BBCode/BBCodeToGreenNode.d.ts +111 -0
- package/dist/types/BBCode/Parser.d.ts +78 -0
- package/dist/types/BBCode/index.d.ts +4 -0
- package/dist/types/Collab/positions.d.ts +55 -0
- package/dist/types/Commands/Command.d.ts +37 -0
- package/dist/types/Commands/CommandRegistry.d.ts +48 -0
- package/dist/types/Commands/DeleteNode.d.ts +7 -0
- package/dist/types/Commands/InsertText.d.ts +8 -0
- package/dist/types/Commands/SplitMerge.d.ts +9 -0
- package/dist/types/Commands/WrapInTag.d.ts +8 -0
- package/dist/types/Commands/index.d.ts +6 -0
- package/dist/types/Diff/TreeDiffer.d.ts +52 -0
- package/dist/types/Diff/__tests__/TreeDiffer.test.d.ts +1 -0
- package/dist/types/Diff/index.d.ts +2 -0
- package/dist/types/Edits/EditPlan.d.ts +144 -0
- package/dist/types/Edits/Optimizer.d.ts +78 -0
- package/dist/types/Edits/Rules/Rule.d.ts +91 -0
- package/dist/types/Edits/Rules/dropEmptyTags.d.ts +40 -0
- package/dist/types/Edits/Rules/dropRedundantNesting.d.ts +59 -0
- package/dist/types/Edits/Rules/index.d.ts +9 -0
- package/dist/types/Edits/Rules/mergeAdjacent.d.ts +93 -0
- package/dist/types/Edits/Rules/reorderWrappers.d.ts +42 -0
- package/dist/types/Edits/Rules/shortenHex.d.ts +30 -0
- package/dist/types/Edits/Rules/tagValue.d.ts +56 -0
- package/dist/types/Edits/Rules/unwrapInvisibleColor.d.ts +47 -0
- package/dist/types/Edits/applyEdits.d.ts +36 -0
- package/dist/types/Edits/index.d.ts +6 -0
- package/dist/types/Events/EventBus.d.ts +103 -0
- package/dist/types/Events/index.d.ts +2 -0
- package/dist/types/Formatter/Formatter.d.ts +32 -0
- package/dist/types/Formatter/index.d.ts +2 -0
- package/dist/types/HTML/HTMLDocumentModel.d.ts +13 -0
- package/dist/types/HTML/HTMLToGreenNode.d.ts +4 -0
- package/dist/types/Incremental/BracketIndex.d.ts +101 -0
- package/dist/types/Incremental/ChangeTracker.d.ts +69 -0
- package/dist/types/Incremental/IncrementalParser.d.ts +246 -0
- package/dist/types/Incremental/__tests__/BracketIndex.test.d.ts +1 -0
- package/dist/types/Incremental/__tests__/IncrementalParser.test.d.ts +1 -0
- package/dist/types/Incremental/index.d.ts +4 -0
- package/dist/types/Lexer/BBCodeLexer.d.ts +74 -0
- package/dist/types/Lexer/Lexer.d.ts +54 -0
- package/dist/types/Lexer/index.d.ts +4 -0
- package/dist/types/Linter/Linter.d.ts +58 -0
- package/dist/types/Linter/index.d.ts +2 -0
- package/dist/types/Markdown/MarkdownAST.d.ts +122 -0
- package/dist/types/Markdown/MarkdownDocumentModel.d.ts +15 -0
- package/dist/types/Markdown/MarkdownLexer.d.ts +155 -0
- package/dist/types/Markdown/MarkdownParser.d.ts +42 -0
- package/dist/types/Markdown/MarkdownToGreenNode.d.ts +6 -0
- package/dist/types/MilHibri/MilHibriDocumentModel.d.ts +21 -0
- package/dist/types/Model/DocumentModel.d.ts +318 -0
- package/dist/types/Model/NodeFactory.d.ts +29 -0
- package/dist/types/Model/TagRegistry.d.ts +165 -0
- package/dist/types/Model/index.d.ts +5 -0
- package/dist/types/Plugins/PluginAPI.d.ts +44 -0
- package/dist/types/Plugins/PluginRegistry.d.ts +81 -0
- package/dist/types/Plugins/index.d.ts +3 -0
- package/dist/types/Queries/QueryEngine.d.ts +32 -0
- package/dist/types/Queries/index.d.ts +1 -0
- package/dist/types/Reconciler/SurgicalReconciler.d.ts +23 -0
- package/dist/types/RenderPipeline/RenderPipeline.d.ts +46 -0
- package/dist/types/RenderPipeline/RenderTree.d.ts +55 -0
- package/dist/types/RenderPipeline/index.d.ts +4 -0
- package/dist/types/Repair/NestingRepair.d.ts +48 -0
- package/dist/types/Repair/index.d.ts +1 -0
- package/dist/types/Semantic/SemanticAnalyzer.d.ts +321 -0
- package/dist/types/Semantic/index.d.ts +2 -0
- package/dist/types/Symbols/SymbolTable.d.ts +47 -0
- package/dist/types/Symbols/index.d.ts +1 -0
- package/dist/types/Syntax/GreenNode.d.ts +154 -0
- package/dist/types/Syntax/GreenNodePool.d.ts +134 -0
- package/dist/types/Syntax/NodeMatcher.d.ts +75 -0
- package/dist/types/Syntax/RedNode.d.ts +280 -0
- package/dist/types/Syntax/RedNodeStore.d.ts +116 -0
- package/dist/types/Syntax/TreeBuilder.d.ts +54 -0
- package/dist/types/Syntax/__tests__/GreenNode.test.d.ts +1 -0
- package/dist/types/Syntax/__tests__/RedNode.test.d.ts +1 -0
- package/dist/types/Syntax/__tests__/RedNodeStore.test.d.ts +1 -0
- package/dist/types/Syntax/greenEdit.d.ts +64 -0
- package/dist/types/Syntax/hash.d.ts +17 -0
- package/dist/types/Syntax/index.d.ts +8 -0
- package/dist/types/Syntax/nodeAttr.d.ts +63 -0
- package/dist/types/Syntax/partition.d.ts +67 -0
- package/dist/types/Syntax/preserveNodeIds.d.ts +47 -0
- package/dist/types/Tokens/TokenResolver.d.ts +25 -0
- package/dist/types/Tokens/index.d.ts +1 -0
- package/dist/types/Transactions/Transaction.d.ts +36 -0
- package/dist/types/Transactions/UndoManager.d.ts +63 -0
- package/dist/types/Transactions/index.d.ts +3 -0
- package/dist/types/Transformers/ASTOptimizer.d.ts +34 -0
- package/dist/types/Transformers/GradientTransformer.d.ts +18 -0
- package/dist/types/Transformers/GrowTransformer.d.ts +16 -0
- package/dist/types/Transformers/RainbowTransformer.d.ts +17 -0
- package/dist/types/Transformers/SineWaveTransformer.d.ts +17 -0
- package/dist/types/Transformers/Transformer.d.ts +17 -0
- package/dist/types/Types/core.d.ts +104 -0
- package/dist/types/Types/diagnostics.d.ts +85 -0
- package/dist/types/Types/index.d.ts +11 -0
- package/dist/types/Types/operations.d.ts +109 -0
- package/dist/types/Types/queries.d.ts +67 -0
- package/dist/types/Types/symbols.d.ts +42 -0
- package/dist/types/Types/tokens.d.ts +75 -0
- package/dist/types/Utils/BBCodeGenerator.d.ts +21 -0
- package/dist/types/Utils/ColorMath.d.ts +92 -0
- package/dist/types/Utils/EffectMath.d.ts +643 -0
- package/dist/types/Utils/TagVocabulary.d.ts +104 -0
- package/dist/types/Utils/TagVocabulary.test.d.ts +1 -0
- package/dist/types/Utils/color.d.ts +29 -0
- package/dist/types/Utils/dom-to-svg.d.ts +28 -0
- package/dist/types/Utils/dom-to-svg.test.d.ts +1 -0
- package/dist/types/Utils/treeTransformers.d.ts +160 -0
- package/dist/types/Visitors/BBBlocksExporter.d.ts +27 -0
- package/dist/types/Visitors/BBCodeExporter.d.ts +90 -0
- package/dist/types/Visitors/BlockPatcher.d.ts +91 -0
- package/dist/types/Visitors/DOMMorpher.d.ts +13 -0
- package/dist/types/Visitors/HTMLRenderer.d.ts +380 -0
- package/dist/types/Visitors/JSONExporter.d.ts +42 -0
- package/dist/types/Visitors/MarkdownExporter.d.ts +28 -0
- package/dist/types/Visitors/SVGRenderer.d.ts +16 -0
- package/dist/types/Visitors/TiptapExporter.d.ts +40 -0
- package/dist/types/Visitors/Visitor.d.ts +35 -0
- package/dist/types/Visitors/index.d.ts +9 -0
- package/dist/types/Visuals/BoxDrawer.d.ts +49 -0
- package/dist/types/Visuals/index.d.ts +38 -0
- package/dist/types/index.d.ts +154 -0
- package/package.json +14 -10
- package/src/Analysis/Passes/Analysis/ColorUsageAnalyzer.ts +103 -0
- package/src/Analysis/Passes/Analysis/GradientAnalyzer.ts +294 -20
- package/src/Analysis/Passes/Analysis/SymbolAnalyzer.ts +257 -0
- package/src/Analysis/Passes/Decision/PaletteRemapDecision.ts +425 -0
- package/src/Analysis/Pipeline/Pipeline.ts +15 -2
- package/src/Analysis/index.ts +8 -2
- package/src/BBCode/BBCodeDocumentModel.ts +3 -0
- package/src/BBCode/BBCodeToGreenNode.ts +18 -12
- package/src/BBCode/Parser.ts +38 -1
- package/src/Edits/EditPlan.ts +290 -0
- package/src/Edits/Optimizer.ts +185 -0
- package/src/Edits/Rules/Rule.ts +145 -0
- package/src/Edits/Rules/dropEmptyTags.ts +110 -0
- package/src/Edits/Rules/dropRedundantNesting.ts +120 -0
- package/src/Edits/Rules/index.ts +28 -0
- package/src/Edits/Rules/mergeAdjacent.ts +224 -0
- package/src/Edits/Rules/reorderWrappers.ts +154 -0
- package/src/Edits/Rules/shortenHex.ts +79 -0
- package/src/Edits/Rules/tagValue.ts +105 -0
- package/src/Edits/Rules/unwrapInvisibleColor.ts +93 -0
- package/src/Edits/applyEdits.ts +54 -0
- package/src/Edits/index.ts +22 -0
- package/src/Events/EventBus.ts +18 -0
- package/src/HTML/HTMLToGreenNode.ts +185 -37
- package/src/Incremental/BracketIndex.ts +249 -0
- package/src/Incremental/IncrementalParser.ts +464 -41
- package/src/Lexer/BBCodeLexer.ts +25 -3
- package/src/Lexer/index.ts +1 -1
- package/src/Markdown/MarkdownAST.ts +65 -2
- package/src/Markdown/MarkdownLexer.ts +69 -14
- package/src/Markdown/MarkdownParser.ts +749 -62
- package/src/Markdown/MarkdownToGreenNode.ts +148 -27
- package/src/MilHibri/MilHibriDocumentModel.ts +56 -0
- package/src/Model/DocumentModel.ts +138 -5
- package/src/Model/TagRegistry.ts +129 -134
- package/src/Reconciler/SurgicalReconciler.ts +460 -0
- package/src/Repair/NestingRepair.ts +116 -0
- package/src/Repair/index.ts +1 -0
- package/src/Semantic/SemanticAnalyzer.ts +1802 -65
- package/src/Syntax/GreenNodePool.ts +12 -12
- package/src/Syntax/RedNode.ts +17 -3
- package/src/Syntax/RedNodeStore.ts +9 -1
- package/src/Syntax/nodeAttr.ts +172 -0
- package/src/Tokens/TokenResolver.ts +55 -0
- package/src/Tokens/index.ts +1 -0
- package/src/Types/core.ts +15 -0
- package/src/Utils/ColorMath.ts +276 -55
- package/src/Utils/EffectMath.ts +2462 -0
- package/src/Utils/TagVocabulary.ts +141 -0
- package/src/Utils/dom-to-svg.ts +28 -12
- package/src/Visitors/BBBlocksExporter.ts +15 -1
- package/src/Visitors/BBCodeExporter.ts +319 -32
- package/src/Visitors/BlockPatcher.ts +87 -11
- package/src/Visitors/DOMMorpher.ts +15 -5
- package/src/Visitors/HTMLRenderer.ts +374 -151
- package/src/Visitors/MarkdownExporter.ts +98 -41
- package/src/Visitors/TiptapExporter.ts +1 -0
- package/src/Visuals/index.ts +7 -4
- package/src/Visuals/lyne.css +117 -6
- package/src/Visuals/osu.css +36 -5
- package/src/index.ts +172 -5
- package/dist/index.d.mts +0 -4612
- package/dist/index.d.ts +0 -4612
- package/src/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.ts +0 -135
- package/src/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.ts +0 -84
- package/src/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.ts +0 -99
- package/src/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.ts +0 -119
- package/src/Analysis/Passes/Decision/__tests__/DefaultDecision.test.ts +0 -179
- package/src/Analysis/__tests__/Integration.test.ts +0 -162
- package/src/Analysis/__tests__/Pipeline.test.ts +0 -133
- package/src/Diff/__tests__/TreeDiffer.test.ts +0 -65
- package/src/Incremental/__tests__/IncrementalParser.test.ts +0 -164
- package/src/Syntax/__tests__/GreenNode.test.ts +0 -33
- package/src/Syntax/__tests__/RedNode.test.ts +0 -81
- package/src/Syntax/__tests__/RedNodeStore.test.ts +0 -104
- package/src/Tests/ASTOptimizerIdempotence.test.ts +0 -77
- package/src/Tests/BlockPatcher.test.ts +0 -437
- package/src/Tests/BlockPatcherWindowed.test.ts +0 -364
- package/src/Tests/BoxDrawer.test.ts +0 -217
- package/src/Tests/BoxRichTitle.test.ts +0 -105
- package/src/Tests/Chars500kBenchmark.test.ts +0 -151
- package/src/Tests/Chars500kEdits.test.ts +0 -321
- package/src/Tests/CollabPositions.test.ts +0 -146
- package/src/Tests/CompilerPathProfiling.test.ts +0 -186
- package/src/Tests/DOMMorpher.test.ts +0 -142
- package/src/Tests/DomPatchPerf.test.ts +0 -60
- package/src/Tests/EffectSegments.snapshot.json +0 -616
- package/src/Tests/EffectSegments.test.ts +0 -68
- package/src/Tests/FindNodeAtOffset.test.ts +0 -65
- package/src/Tests/Fuzzer.test.ts +0 -166
- package/src/Tests/GreenNodePool.test.ts +0 -153
- package/src/Tests/Lexer.test.ts +0 -238
- package/src/Tests/LyneMode.test.ts +0 -291
- package/src/Tests/ModelCoherence.test.ts +0 -180
- package/src/Tests/Partition.test.ts +0 -238
- package/src/Tests/PluginTags.test.ts +0 -150
- package/src/Tests/ProblematicSection.test.ts +0 -46
- package/src/Tests/ProblematicSectionHTML.test.ts +0 -58
- package/src/Tests/RedReuse.test.ts +0 -134
- package/src/Tests/ReproDelete20k.test.ts +0 -62
- package/src/Tests/SemanticValidators.test.ts +0 -136
- package/src/Tests/StableNodeIds.test.ts +0 -210
- package/src/Tests/StudioColorBloat.test.ts +0 -25
- package/src/Tests/StudioDebugText.test.ts +0 -27
- package/src/Tests/StudioTrailingChar.test.ts +0 -25
- package/src/Tests/StudioValidText.test.ts +0 -25
- package/src/Tests/UrlImgBug.test.ts +0 -23
- package/src/Tests/VisualBuilderFidelity.test.ts +0 -105
- package/src/Tests/referenceDocument.ts +0 -119
- 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'
|
package/src/Events/EventBus.ts
CHANGED
|
@@ -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
|
}
|