@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,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quasar Analysis Framework — Palette Remap Decision
|
|
3
|
+
*
|
|
4
|
+
* Turns an AnalysisReport into a plan for restyling a document with a
|
|
5
|
+
* different palette: recolour its colour tags, swap its decorative glyphs,
|
|
6
|
+
* replace its divider lines.
|
|
7
|
+
*
|
|
8
|
+
* ## The plan is applied by range, not by rebuilding the tree
|
|
9
|
+
*
|
|
10
|
+
* Every other DecisionPass in this framework feeds a TransformPass that
|
|
11
|
+
* returns a new Green Tree, which is then serialized. That is the wrong shape
|
|
12
|
+
* for restyling a document someone already wrote: `BBCodeExporter` rebuilds
|
|
13
|
+
* BBCode from node kinds and metadata rather than replaying source bytes, so
|
|
14
|
+
* re-exporting a whole document also rewrites the parts nobody asked to
|
|
15
|
+
* change — `[B]` becomes `[b]`, `[color = #F00]` loses its spacing, CRLF
|
|
16
|
+
* becomes LF.
|
|
17
|
+
*
|
|
18
|
+
* So the actions here carry a `range` and a literal `replacement` string, and
|
|
19
|
+
* are meant to be applied directly to the source text. Everything outside
|
|
20
|
+
* those ranges is never re-serialized and therefore cannot drift. The Decision
|
|
21
|
+
* stage is still the right home — it consumes a report and describes intended
|
|
22
|
+
* mutations without touching the tree — but no TransformPass consumes it.
|
|
23
|
+
*
|
|
24
|
+
* Apply the actions **last range first**. They are emitted in source order,
|
|
25
|
+
* and rewriting front to back invalidates every offset that follows.
|
|
26
|
+
*
|
|
27
|
+
* ## What each kind of thing becomes
|
|
28
|
+
*
|
|
29
|
+
* **Colours inside a gradient** keep the gradient's shape. Each member tag is
|
|
30
|
+
* recoloured by sampling the new palette at the member's own position along
|
|
31
|
+
* the ramp, passed through the easing the original gradient was detected with.
|
|
32
|
+
* The result reads as the same gradient wearing a different palette, rather
|
|
33
|
+
* than as a new gradient.
|
|
34
|
+
*
|
|
35
|
+
* **Standalone colours** map to their nearest palette entry by perceptual
|
|
36
|
+
* distance in OKLab, so a pink stays the palette's pink rather than becoming
|
|
37
|
+
* whichever colour happens to be first.
|
|
38
|
+
*
|
|
39
|
+
* **Symbols** are substituted glyph for glyph, and **separators** wholesale.
|
|
40
|
+
* Both substitutions are stable across the document: a given source glyph
|
|
41
|
+
* always maps to the same palette glyph, so a document that used `✧` as its
|
|
42
|
+
* motif comes out using one motif rather than a scatter.
|
|
43
|
+
*
|
|
44
|
+
* @see ColorUsageAnalyzer — supplies the per-tag offsets this pass edits
|
|
45
|
+
* @see GradientAnalyzer — supplies the ramp shape those tags belong to
|
|
46
|
+
* @see SymbolAnalyzer — supplies the glyph runs
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import type { DecisionPass, TransformationPlan, TransformAction } from '../../Contracts/Pass'
|
|
50
|
+
import type { AnalysisReport } from '../../Contracts/AnalysisReport'
|
|
51
|
+
import type { PipelineContext } from '../../Contracts/PipelineContext'
|
|
52
|
+
import type { SemanticContribution } from '../../Contracts/Contribution'
|
|
53
|
+
import { ContributionKind } from '../../Contracts/Contribution'
|
|
54
|
+
import { ease, mixHexOklab, perceptualDistance } from '../../../Utils/ColorMath'
|
|
55
|
+
import type { ColorUsageModel } from '../Analysis/ColorUsageAnalyzer'
|
|
56
|
+
import type { GradientModel } from '../Analysis/GradientAnalyzer'
|
|
57
|
+
import type { SymbolRunModel } from '../Analysis/SymbolAnalyzer'
|
|
58
|
+
|
|
59
|
+
// ── Configuration ─────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The palette to restyle towards.
|
|
63
|
+
*
|
|
64
|
+
* Structural, not imported: Quasar has no dependency on the application's
|
|
65
|
+
* theme catalogue, and any object with these three lists satisfies it.
|
|
66
|
+
*/
|
|
67
|
+
export interface Palette {
|
|
68
|
+
/** Hex colours, ordered as a ramp from first to last. */
|
|
69
|
+
readonly colors: readonly string[]
|
|
70
|
+
/** Decorative glyphs, in preference order. */
|
|
71
|
+
readonly symbols: readonly string[]
|
|
72
|
+
/** Whole divider lines. */
|
|
73
|
+
readonly separators: readonly string[]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface PaletteRemapOptions {
|
|
77
|
+
/**
|
|
78
|
+
* Below this confidence an action is still emitted, but marked
|
|
79
|
+
* `recommended: false`.
|
|
80
|
+
*
|
|
81
|
+
* Nothing is dropped. A caller that hides low-confidence findings can never
|
|
82
|
+
* offer them, and the glyphs that land down here — `≈` in a sum, `→` in a
|
|
83
|
+
* sentence — are exactly the ones a person should get to look at before
|
|
84
|
+
* they are rewritten. Filtering is the caller's decision; this pass only
|
|
85
|
+
* says which side of the line each action falls on.
|
|
86
|
+
*/
|
|
87
|
+
readonly minConfidence?: number
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const DEFAULT_MIN_CONFIDENCE = 0.7
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* One spelling for every hex this pass emits.
|
|
94
|
+
*
|
|
95
|
+
* `mixHexOklab` builds its result lowercase while palette entries are written
|
|
96
|
+
* however the caller wrote them, so a sampled ramp would otherwise alternate
|
|
97
|
+
* cases — `#ffb7c5` next to `#FFE4E1` — depending on whether a stop landed on
|
|
98
|
+
* a palette entry or between two. `extractHex` already uppercases what it
|
|
99
|
+
* reads, so matching it also keeps the "already this colour" check honest.
|
|
100
|
+
*/
|
|
101
|
+
function canonicalHex(hex: string): string {
|
|
102
|
+
return hex.toUpperCase()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Action payloads ───────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
/** Every action this pass emits shares one shape. */
|
|
108
|
+
export interface RemapAction extends TransformAction {
|
|
109
|
+
readonly kind: 'recolor' | 'resymbol' | 'reseparator'
|
|
110
|
+
readonly payload: {
|
|
111
|
+
/** Source span to overwrite. */
|
|
112
|
+
readonly range: { readonly start: number; readonly end: number }
|
|
113
|
+
/** Literal text to write in its place. */
|
|
114
|
+
readonly replacement: string
|
|
115
|
+
/** What is there now, for previewing the change. */
|
|
116
|
+
readonly from: string
|
|
117
|
+
/** What it becomes. */
|
|
118
|
+
readonly to: string
|
|
119
|
+
readonly confidence: number
|
|
120
|
+
readonly recommended: boolean
|
|
121
|
+
/** Only on `recolor`: whether the tag belonged to a detected gradient. */
|
|
122
|
+
readonly gradient?: boolean
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Decision ──────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
export class PaletteRemapDecision implements DecisionPass {
|
|
129
|
+
readonly id = 'palette-remap'
|
|
130
|
+
|
|
131
|
+
private readonly palette: Palette
|
|
132
|
+
private readonly minConfidence: number
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Substitution tables, rebuilt per `run` so the pass stays reusable and
|
|
136
|
+
* every run over the same report produces the same plan.
|
|
137
|
+
*/
|
|
138
|
+
private symbolMap = new Map<string, string>()
|
|
139
|
+
private separatorMap = new Map<string, string>()
|
|
140
|
+
|
|
141
|
+
constructor(palette: Palette, options: PaletteRemapOptions = {}) {
|
|
142
|
+
this.palette = palette
|
|
143
|
+
this.minConfidence = options.minConfidence ?? DEFAULT_MIN_CONFIDENCE
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
run(report: AnalysisReport, _context: PipelineContext): TransformationPlan {
|
|
147
|
+
this.symbolMap = new Map()
|
|
148
|
+
this.separatorMap = new Map()
|
|
149
|
+
|
|
150
|
+
const semantic = report.contributions.filter(
|
|
151
|
+
(c): c is SemanticContribution => c.kind === ContributionKind.Semantic,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
const byLabel = (label: string) =>
|
|
155
|
+
semantic
|
|
156
|
+
.filter(c => c.label === label)
|
|
157
|
+
.sort((a, b) => a.range.start - b.range.start)
|
|
158
|
+
|
|
159
|
+
const actions: RemapAction[] = [
|
|
160
|
+
...this.planColors(byLabel('Color'), byLabel('Gradient')),
|
|
161
|
+
...this.planSymbols(byLabel('Symbol')),
|
|
162
|
+
...this.planSeparators(byLabel('Separator')),
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
// Source order, so a caller can apply them back to front by reversing.
|
|
166
|
+
actions.sort((a, b) => a.payload.range.start - b.payload.range.start)
|
|
167
|
+
|
|
168
|
+
return { actions: Object.freeze(actions) }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Colours ─────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
private planColors(
|
|
174
|
+
colors: readonly SemanticContribution[],
|
|
175
|
+
gradients: readonly SemanticContribution[],
|
|
176
|
+
): RemapAction[] {
|
|
177
|
+
if (this.palette.colors.length === 0) return []
|
|
178
|
+
|
|
179
|
+
// Which gradient, if any, each colour tag belongs to. Membership is by
|
|
180
|
+
// containment: GradientAnalyzer reports the span its member tags cover.
|
|
181
|
+
const memberOf = new Map<SemanticContribution, SemanticContribution>()
|
|
182
|
+
const members = new Map<SemanticContribution, SemanticContribution[]>()
|
|
183
|
+
|
|
184
|
+
for (const gradient of gradients) {
|
|
185
|
+
// A doubtful gradient must not drive ramp sampling.
|
|
186
|
+
//
|
|
187
|
+
// GradientAnalyzer reports every colour run it *considers*, scoring the
|
|
188
|
+
// ones that are not ramps down rather than discarding them — a lone
|
|
189
|
+
// `#111111` sitting against a real gradient yields one six-colour
|
|
190
|
+
// candidate at 0.13 confidence. Sampling a palette across that would
|
|
191
|
+
// impose a ramp the document never had, so anything under the threshold
|
|
192
|
+
// falls through and its colours are matched individually instead.
|
|
193
|
+
if (gradient.confidence < this.minConfidence) continue
|
|
194
|
+
|
|
195
|
+
const inside = colors.filter(
|
|
196
|
+
c => c.range.start >= gradient.range.start && c.range.end <= gradient.range.end,
|
|
197
|
+
)
|
|
198
|
+
// A gradient needs at least two members before "position along it" means
|
|
199
|
+
// anything; a degenerate one is treated as standalone colours.
|
|
200
|
+
if (inside.length < 2) continue
|
|
201
|
+
|
|
202
|
+
members.set(gradient, inside)
|
|
203
|
+
for (const color of inside) memberOf.set(color, gradient)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const actions: RemapAction[] = []
|
|
207
|
+
|
|
208
|
+
for (const color of colors) {
|
|
209
|
+
const model = color.metadata.model as ColorUsageModel
|
|
210
|
+
const gradient = memberOf.get(color)
|
|
211
|
+
|
|
212
|
+
const to = gradient
|
|
213
|
+
? this.gradientColorFor(color, gradient, members.get(gradient)!)
|
|
214
|
+
: this.nearestPaletteColor(model.hex)
|
|
215
|
+
|
|
216
|
+
// A tag already wearing its target colour is not a change. Both sides
|
|
217
|
+
// are canonical here: `extractHex` uppercases, and so does `to`.
|
|
218
|
+
if (to === model.hex) continue
|
|
219
|
+
|
|
220
|
+
actions.push({
|
|
221
|
+
kind: 'recolor',
|
|
222
|
+
payload: {
|
|
223
|
+
range: { start: model.openStart, end: model.openEnd },
|
|
224
|
+
replacement: `[color=${to}]`,
|
|
225
|
+
from: model.hex,
|
|
226
|
+
to,
|
|
227
|
+
confidence: gradient ? gradient.confidence : color.confidence,
|
|
228
|
+
recommended: (gradient ? gradient.confidence : color.confidence) >= this.minConfidence,
|
|
229
|
+
gradient: gradient !== undefined,
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return actions
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Where this tag sits along its gradient, resampled onto the new palette.
|
|
239
|
+
*
|
|
240
|
+
* Position comes from the tag's ordinal among the gradient's members rather
|
|
241
|
+
* than from its offset, because the members are evenly spaced *as tags* — a
|
|
242
|
+
* gradient over `Hello` has five of them regardless of how wide each tag's
|
|
243
|
+
* text is.
|
|
244
|
+
*/
|
|
245
|
+
private gradientColorFor(
|
|
246
|
+
color: SemanticContribution,
|
|
247
|
+
gradient: SemanticContribution,
|
|
248
|
+
members: readonly SemanticContribution[],
|
|
249
|
+
): string {
|
|
250
|
+
const model = gradient.metadata.model as GradientModel
|
|
251
|
+
const index = members.indexOf(color)
|
|
252
|
+
const t = members.length > 1 ? index / (members.length - 1) : 0
|
|
253
|
+
|
|
254
|
+
return this.samplePalette(ease(t, model.easing))
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Sample the palette as a continuous ramp, interpolating in OKLab.
|
|
259
|
+
*
|
|
260
|
+
* `ColorMath.mixMultiple` does the same walk in RGB, which darkens and
|
|
261
|
+
* desaturates through the middle of a blend — the reason the gradient
|
|
262
|
+
* analyzers work in OKLab in the first place. Sampling rather than mapping
|
|
263
|
+
* palette entries one-to-one is what preserves the original stop count: a
|
|
264
|
+
* three-stop gradient stays three stops when restyled with a five-colour
|
|
265
|
+
* palette, instead of gaining two.
|
|
266
|
+
*/
|
|
267
|
+
private samplePalette(t: number): string {
|
|
268
|
+
const colors = this.palette.colors
|
|
269
|
+
if (colors.length === 1) return canonicalHex(colors[0])
|
|
270
|
+
|
|
271
|
+
const clamped = Math.max(0, Math.min(1, t))
|
|
272
|
+
const scaled = clamped * (colors.length - 1)
|
|
273
|
+
const index = Math.floor(scaled)
|
|
274
|
+
|
|
275
|
+
if (index >= colors.length - 1) return canonicalHex(colors[colors.length - 1])
|
|
276
|
+
|
|
277
|
+
return canonicalHex(mixHexOklab(colors[index], colors[index + 1], scaled - index))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The palette entry that looks most like the colour already there.
|
|
282
|
+
*
|
|
283
|
+
* Perceptual distance rather than index order: a document's pink should come
|
|
284
|
+
* back as the palette's pink, not as whichever colour the palette happens to
|
|
285
|
+
* list first.
|
|
286
|
+
*/
|
|
287
|
+
private nearestPaletteColor(hex: string): string {
|
|
288
|
+
let best = this.palette.colors[0]
|
|
289
|
+
let bestDistance = Infinity
|
|
290
|
+
|
|
291
|
+
for (const candidate of this.palette.colors) {
|
|
292
|
+
const distance = perceptualDistance(hex, candidate)
|
|
293
|
+
if (distance < bestDistance) {
|
|
294
|
+
bestDistance = distance
|
|
295
|
+
best = candidate
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return canonicalHex(best)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Glyphs ──────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
private planSymbols(symbols: readonly SemanticContribution[]): RemapAction[] {
|
|
305
|
+
if (this.palette.symbols.length === 0) return []
|
|
306
|
+
|
|
307
|
+
const actions: RemapAction[] = []
|
|
308
|
+
|
|
309
|
+
this.reserveIdentities(
|
|
310
|
+
this.symbolMap,
|
|
311
|
+
symbols.flatMap(run => (run.metadata.model as SymbolRunModel).glyphs.map(g => g.char)),
|
|
312
|
+
this.palette.symbols,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
for (const run of symbols) {
|
|
316
|
+
const model = run.metadata.model as SymbolRunModel
|
|
317
|
+
const replacement = model.glyphs
|
|
318
|
+
.map(glyph => this.substitute(this.symbolMap, glyph.char, this.palette.symbols))
|
|
319
|
+
.join('')
|
|
320
|
+
|
|
321
|
+
if (replacement === model.text) continue
|
|
322
|
+
|
|
323
|
+
actions.push({
|
|
324
|
+
kind: 'resymbol',
|
|
325
|
+
payload: {
|
|
326
|
+
range: run.range,
|
|
327
|
+
replacement,
|
|
328
|
+
from: model.text,
|
|
329
|
+
to: replacement,
|
|
330
|
+
confidence: run.confidence,
|
|
331
|
+
recommended: run.confidence >= this.minConfidence,
|
|
332
|
+
},
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return actions
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private planSeparators(separators: readonly SemanticContribution[]): RemapAction[] {
|
|
340
|
+
if (this.palette.separators.length === 0) return []
|
|
341
|
+
|
|
342
|
+
const actions: RemapAction[] = []
|
|
343
|
+
|
|
344
|
+
this.reserveIdentities(
|
|
345
|
+
this.separatorMap,
|
|
346
|
+
separators.map(run => (run.metadata.model as SymbolRunModel).text),
|
|
347
|
+
this.palette.separators,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
for (const run of separators) {
|
|
351
|
+
const model = run.metadata.model as SymbolRunModel
|
|
352
|
+
const replacement = this.substitute(this.separatorMap, model.text, this.palette.separators)
|
|
353
|
+
|
|
354
|
+
if (replacement === model.text) continue
|
|
355
|
+
|
|
356
|
+
actions.push({
|
|
357
|
+
kind: 'reseparator',
|
|
358
|
+
payload: {
|
|
359
|
+
range: run.range,
|
|
360
|
+
replacement,
|
|
361
|
+
from: model.text,
|
|
362
|
+
to: replacement,
|
|
363
|
+
confidence: run.confidence,
|
|
364
|
+
recommended: run.confidence >= this.minConfidence,
|
|
365
|
+
},
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return actions
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Assign a replacement for `source`, remembering it.
|
|
374
|
+
*
|
|
375
|
+
* Distinct sources are handed palette entries in first-seen order.
|
|
376
|
+
* Recording the choice is what keeps the document coherent: every later `✧`
|
|
377
|
+
* gets the same replacement as the first one, so a motif stays a motif.
|
|
378
|
+
*
|
|
379
|
+
* Entries already spoken for are skipped — including the identities
|
|
380
|
+
* `reserveIdentities` claimed before this ran — so distinct sources keep
|
|
381
|
+
* distinct replacements for as long as the palette has them. Once it runs
|
|
382
|
+
* out the assignment wraps, and glyphs begin to share.
|
|
383
|
+
*/
|
|
384
|
+
private substitute(
|
|
385
|
+
table: Map<string, string>,
|
|
386
|
+
source: string,
|
|
387
|
+
replacements: readonly string[],
|
|
388
|
+
): string {
|
|
389
|
+
const existing = table.get(source)
|
|
390
|
+
if (existing !== undefined) return existing
|
|
391
|
+
|
|
392
|
+
const taken = new Set(table.values())
|
|
393
|
+
const free = replacements.find(r => !taken.has(r))
|
|
394
|
+
const chosen = free ?? replacements[table.size % replacements.length]
|
|
395
|
+
|
|
396
|
+
table.set(source, chosen)
|
|
397
|
+
return chosen
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Let everything the palette already contains keep its own place, before
|
|
402
|
+
* anything else is handed out.
|
|
403
|
+
*
|
|
404
|
+
* This has to happen up front rather than as each source is reached.
|
|
405
|
+
* Assignment follows document order, so in `✧ then ✿` the unowned `✧` would
|
|
406
|
+
* otherwise claim `✿` first and the real `✿` would arrive to find its own
|
|
407
|
+
* identity taken — displacing it onto a glyph the document never used.
|
|
408
|
+
*
|
|
409
|
+
* Holding identities fixed is what makes restyling idempotent: a second pass
|
|
410
|
+
* with the same theme finds every glyph already in place and changes
|
|
411
|
+
* nothing.
|
|
412
|
+
*/
|
|
413
|
+
private reserveIdentities(
|
|
414
|
+
table: Map<string, string>,
|
|
415
|
+
sources: readonly string[],
|
|
416
|
+
replacements: readonly string[],
|
|
417
|
+
): void {
|
|
418
|
+
for (const source of sources) {
|
|
419
|
+
if (!table.has(source) && replacements.includes(source)) {
|
|
420
|
+
table.set(source, source)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
@@ -35,6 +35,16 @@ export interface PipelineResult {
|
|
|
35
35
|
readonly tree: GreenNode
|
|
36
36
|
/** The aggregated analysis report. */
|
|
37
37
|
readonly report: AnalysisReport
|
|
38
|
+
/**
|
|
39
|
+
* The plan the last DecisionPass produced, empty when none ran.
|
|
40
|
+
*
|
|
41
|
+
* Usually this is an intermediate the transform stage consumes and the
|
|
42
|
+
* caller never needs. It is surfaced because a plan is not always something
|
|
43
|
+
* to execute against the tree: a pass may describe edits meant to be applied
|
|
44
|
+
* to the source text by range, in which case the plan *is* the output and
|
|
45
|
+
* there is no transform stage to hand it to.
|
|
46
|
+
*/
|
|
47
|
+
readonly plan: TransformationPlan
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
export class Pipeline {
|
|
@@ -61,7 +71,10 @@ export class Pipeline {
|
|
|
61
71
|
break
|
|
62
72
|
|
|
63
73
|
case 'decision': {
|
|
64
|
-
|
|
74
|
+
// `elapsedMs` is a duration, not a timestamp: a decision pass that
|
|
75
|
+
// budgets work by time would otherwise read the pipeline's absolute
|
|
76
|
+
// origin (millions of ms into the page's life) and see no budget left.
|
|
77
|
+
const report = this.buildReport(allContributions, analysisPassCount, performance.now() - start)
|
|
65
78
|
latestPlan = this.runDecision(entry.pass as DecisionPass, report, context)
|
|
66
79
|
break
|
|
67
80
|
}
|
|
@@ -74,7 +87,7 @@ export class Pipeline {
|
|
|
74
87
|
|
|
75
88
|
const elapsed = performance.now() - start
|
|
76
89
|
const report = this.buildReport(allContributions, analysisPassCount, elapsed)
|
|
77
|
-
return { tree: currentTree, report }
|
|
90
|
+
return { tree: currentTree, report, plan: latestPlan }
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
/**
|
package/src/Analysis/index.ts
CHANGED
|
@@ -35,15 +35,21 @@ export { PipelineStage } from './Pipeline/PipelineStage'
|
|
|
35
35
|
// ── Analysis Passes ───────────────────────────────────────────────
|
|
36
36
|
export { CharacterCountAnalyzer } from './Passes/Utility/CharacterCountAnalyzer'
|
|
37
37
|
export { MergeableColorAnalyzer } from './Passes/Analysis/MergeableColorAnalyzer'
|
|
38
|
-
export { GradientAnalyzer } from './Passes/Analysis/GradientAnalyzer'
|
|
39
|
-
export type { GradientModel, GradientDiagnostics, GradientStop } from './Passes/Analysis/GradientAnalyzer'
|
|
38
|
+
export { GradientAnalyzer, findCollapsibleGradients, formatGradientTag } from './Passes/Analysis/GradientAnalyzer'
|
|
39
|
+
export type { GradientModel, GradientDiagnostics, GradientStop, CollapsibleGradient } from './Passes/Analysis/GradientAnalyzer'
|
|
40
40
|
export { RainbowAnalyzer } from './Passes/Analysis/RainbowAnalyzer'
|
|
41
41
|
export type { RainbowModel, RainbowDiagnostics } from './Passes/Analysis/RainbowAnalyzer'
|
|
42
42
|
export { WaveAnalyzer } from './Passes/Analysis/WaveAnalyzer'
|
|
43
43
|
export type { WaveModel, WaveDiagnostics } from './Passes/Analysis/WaveAnalyzer'
|
|
44
|
+
export { SymbolAnalyzer } from './Passes/Analysis/SymbolAnalyzer'
|
|
45
|
+
export type { SymbolGlyph, SymbolRunModel } from './Passes/Analysis/SymbolAnalyzer'
|
|
46
|
+
export { ColorUsageAnalyzer } from './Passes/Analysis/ColorUsageAnalyzer'
|
|
47
|
+
export type { ColorUsageModel } from './Passes/Analysis/ColorUsageAnalyzer'
|
|
44
48
|
|
|
45
49
|
// ── Decision Passes ───────────────────────────────────────────────
|
|
46
50
|
export { DefaultDecision } from './Passes/Decision/DefaultDecision'
|
|
51
|
+
export { PaletteRemapDecision } from './Passes/Decision/PaletteRemapDecision'
|
|
52
|
+
export type { Palette, PaletteRemapOptions, RemapAction } from './Passes/Decision/PaletteRemapDecision'
|
|
47
53
|
|
|
48
54
|
// ── Transform Passes ──────────────────────────────────────────────
|
|
49
55
|
export { CollapseGradientTransform } from './Passes/Transform/CollapseGradientTransform'
|
|
@@ -87,6 +87,9 @@ export class BBCodeDocumentModel extends DocumentModel {
|
|
|
87
87
|
})
|
|
88
88
|
this._strictMode = options.strictMode ?? false
|
|
89
89
|
this._dialect = options.dialect ?? options.mode ?? 'miliastry'
|
|
90
|
+
// The analyzer is built by `super()`, before this line could have run,
|
|
91
|
+
// so the dialect it suggests unknown-tag replacements from is set here.
|
|
92
|
+
this.semanticAnalyzer.dialect = this._dialect
|
|
90
93
|
this._interner = options.interning ? GreenNodePool.create(options.interning) : null
|
|
91
94
|
if (options.source) {
|
|
92
95
|
this.rebuild(options.source)
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* → DocumentModel
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { parseEffectParams } from '../Utils/EffectMath'
|
|
18
19
|
import { GreenNode, greenNode, greenLeaf } from '../Syntax/GreenNode'
|
|
19
20
|
import { RedNode } from '../Syntax/RedNode'
|
|
20
21
|
import { RedNodeStore } from '../Syntax/RedNodeStore'
|
|
@@ -73,6 +74,7 @@ const MILIASTRY_TAG_TO_KIND_ENTRIES: Record<string, NodeKind> = {
|
|
|
73
74
|
'grow': 'grow',
|
|
74
75
|
'sinewave': 'sinewave',
|
|
75
76
|
'rainbow': 'rainbow',
|
|
77
|
+
'paint': 'paint',
|
|
76
78
|
'svg': 'svg',
|
|
77
79
|
'group': 'group',
|
|
78
80
|
}
|
|
@@ -179,6 +181,7 @@ const LYNE_LEGACY_ALIASES: Record<string, NodeKind> = {
|
|
|
179
181
|
// Anim legacy aliases
|
|
180
182
|
'typewriter': 'anim',
|
|
181
183
|
'wave': 'anim',
|
|
184
|
+
'pulse': 'anim',
|
|
182
185
|
'sparkle': 'anim',
|
|
183
186
|
'glitch': 'anim',
|
|
184
187
|
'levitate': 'anim',
|
|
@@ -233,10 +236,6 @@ export const BBCODE_TAG_NAMES: readonly string[] = Object.freeze(
|
|
|
233
236
|
)
|
|
234
237
|
|
|
235
238
|
export function getBBCodeTagNames(dialect: BBCodeDialect = 'miliastry'): readonly string[] {
|
|
236
|
-
// Para Lyne, la superficie visible es la CANÓNICA: los legacy aliases se
|
|
237
|
-
// siguen parseando (compatibilidad) pero no se listan — así un consumidor
|
|
238
|
-
// (autocomplete, docs) solo ofrece las formas canónicas.
|
|
239
|
-
if (dialect === 'lyne') return Object.freeze([...Object.keys(LYNE_CANONICAL_TAG_TO_KIND)].sort())
|
|
240
239
|
const map = DIALECT_MAPS[dialect] || DIALECT_MAPS.miliastry
|
|
241
240
|
return Object.freeze([...map.keys()].sort())
|
|
242
241
|
}
|
|
@@ -411,7 +410,7 @@ export function extractGreenNodeMetadata(green: GreenNode): Record<string, unkno
|
|
|
411
410
|
// `[box=Mi Caja:#FF0055]` separa el color del título; el color se guarda
|
|
412
411
|
// aparte para que el renderer lo aplique como `--box-accent` y el
|
|
413
412
|
// exporter lo vuelva a emitir en el round-trip.
|
|
414
|
-
const colorMatch =
|
|
413
|
+
const colorMatch = /:(?:#[0-9a-fA-F]{3,8}|\$[a-zA-Z0-9_.-]+)$/.exec(value)
|
|
415
414
|
const color = colorMatch ? colorMatch[0].slice(1) : undefined
|
|
416
415
|
const titleValue = colorMatch ? value.slice(0, colorMatch.index) : value
|
|
417
416
|
return {
|
|
@@ -439,9 +438,16 @@ export function extractGreenNodeMetadata(green: GreenNode): Record<string, unkno
|
|
|
439
438
|
case 'video': return { videoId: value || firstChildText(green) }
|
|
440
439
|
case 'audio': return { src: value || firstChildText(green) }
|
|
441
440
|
case 'gradient':
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
441
|
+
case 'rainbow':
|
|
442
|
+
case 'grow':
|
|
443
|
+
case 'sinewave':
|
|
444
|
+
case 'paint':
|
|
445
|
+
// One grammar for every effect tag. `[gradient=#a,#b]` still parses
|
|
446
|
+
// as a bare colour list; `[gradient=#a,#b;easing=easeInOut;axis=line]`
|
|
447
|
+
// adds the modulation Text Studio configures. Before this, everything
|
|
448
|
+
// past the colours was dropped on the way in and re-invented from
|
|
449
|
+
// defaults on the way out, so a saved effect never came back.
|
|
450
|
+
return parseEffectParams(value) as Record<string, unknown>
|
|
445
451
|
case 'table_th':
|
|
446
452
|
case 'table_col': {
|
|
447
453
|
if (!value) return {}
|
|
@@ -452,7 +458,7 @@ export function extractGreenNodeMetadata(green: GreenNode): Record<string, unkno
|
|
|
452
458
|
res.colspan = parseInt(p, 10)
|
|
453
459
|
} else if (/^(center|centre|left|right|justify)$/i.test(p)) {
|
|
454
460
|
res.align = p.toLowerCase() === 'centre' ? 'center' : p.toLowerCase()
|
|
455
|
-
} else if (
|
|
461
|
+
} else if (/^(?:#[0-9a-fA-F]{3,8}|\$[a-zA-Z0-9_.-]+)$/.test(p)) {
|
|
456
462
|
res.color = p
|
|
457
463
|
}
|
|
458
464
|
}
|
|
@@ -462,11 +468,11 @@ export function extractGreenNodeMetadata(green: GreenNode): Record<string, unkno
|
|
|
462
468
|
case 'wnotice': return value ? { color: value } : {}
|
|
463
469
|
case 'tables':
|
|
464
470
|
case 'columns': {
|
|
465
|
-
// Sufijo de color `:#hex`
|
|
466
|
-
// `[columns=2:#FF0055]`. De ese color se deriva toda la paleta (bordes,
|
|
471
|
+
// Sufijo de color `:#hex` o `:$token`: `[tables=striped:#FF0055]`, `[tables=striped:$accent]`
|
|
472
|
+
// y `[columns=2:#FF0055]`. De ese color se deriva toda la paleta (bordes,
|
|
467
473
|
// filas, encabezado) vía `--table-accent` / `--columns-accent`.
|
|
468
474
|
if (!value) return {}
|
|
469
|
-
const colorMatch =
|
|
475
|
+
const colorMatch = /:(?:#[0-9a-fA-F]{3,8}|\$[a-zA-Z0-9_.-]+)$/.exec(value)
|
|
470
476
|
const clean = colorMatch ? value.slice(0, colorMatch.index) : value
|
|
471
477
|
const color = colorMatch ? colorMatch[0].slice(1) : undefined
|
|
472
478
|
const base = kind === 'tables' ? { variant: clean } : { columns: clean }
|
package/src/BBCode/Parser.ts
CHANGED
|
@@ -82,6 +82,21 @@ export function parseTokensToGreen(
|
|
|
82
82
|
const normalizeParagraphs = options.normalizeParagraphs ?? true;
|
|
83
83
|
const extraTags = options.extraTags;
|
|
84
84
|
const root: GreenNode[] = []
|
|
85
|
+
/**
|
|
86
|
+
* Etiquetas que un cierre mal emparejado cerró por su cuenta y cuyo `[/tag]`
|
|
87
|
+
* todavía está por llegar.
|
|
88
|
+
*
|
|
89
|
+
* Cuando `[/notice]` cierra un `[box]` que quedaba dentro, el `[/box]` que
|
|
90
|
+
* viene después ya no tiene a quién cerrar. osu! lo tira; subir por la pila a
|
|
91
|
+
* buscar otro `[box]` cierra uno que el autor no quería cerrar y saca del
|
|
92
|
+
* contenedor a todo lo que sigue. En `docs/ai/NyuPenyu` eso aplanaba 43 cajas
|
|
93
|
+
* anidadas a 9 y hacía la página un 78% más alta de lo que osu! muestra.
|
|
94
|
+
*
|
|
95
|
+
* La marca se consume al usarla y se borra si la etiqueta se vuelve a abrir,
|
|
96
|
+
* porque entonces el cierre sí es suyo.
|
|
97
|
+
*/
|
|
98
|
+
const autoClosed = new Set<string>()
|
|
99
|
+
|
|
85
100
|
const stack: {
|
|
86
101
|
/** The literal tag name. Closing matches on THIS, not on `kind`: `[centre]`
|
|
87
102
|
* and `[center]` share a kind but do not close each other, and that is
|
|
@@ -223,6 +238,7 @@ export function parseTokensToGreen(
|
|
|
223
238
|
if (stack.length > 0 && stack[stack.length - 1].tag === '*') {
|
|
224
239
|
addToParent(closeFrame(stack.pop()!, 0))
|
|
225
240
|
}
|
|
241
|
+
autoClosed.delete(tok.tag)
|
|
226
242
|
stack.push({
|
|
227
243
|
tag: tok.tag,
|
|
228
244
|
kind: openKind,
|
|
@@ -252,6 +268,7 @@ export function parseTokensToGreen(
|
|
|
252
268
|
}
|
|
253
269
|
|
|
254
270
|
// Push onto the stack — children will be added later.
|
|
271
|
+
autoClosed.delete(tok.tag)
|
|
255
272
|
stack.push({
|
|
256
273
|
tag: tok.tag,
|
|
257
274
|
kind: openKind,
|
|
@@ -283,6 +300,18 @@ export function parseTokensToGreen(
|
|
|
283
300
|
const errMsg = `Syntax Error: Expected /${expected}, got /${tok.tag}`
|
|
284
301
|
addToParent(createNode('error', errMsg, [createLeaf('text', text)]))
|
|
285
302
|
}
|
|
303
|
+
} else if (autoClosed.has(tok.tag)) {
|
|
304
|
+
// Este cierre llega tarde: su etiqueta ya se cerró sola cuando un
|
|
305
|
+
// cierre anterior pasó por encima de ella. No puede reclamar un
|
|
306
|
+
// ancestro del mismo nombre — hacerlo cerraba el contenedor de fuera
|
|
307
|
+
// y expulsaba de él a todo el resto del documento.
|
|
308
|
+
//
|
|
309
|
+
// Se conserva como nodo `discarded_tag`, que guarda su rango pero no
|
|
310
|
+
// se ve ni se exporta. Como texto volvía a salir del exportador
|
|
311
|
+
// convertido en etiqueta viva, y el documento dejaba de ser estable al
|
|
312
|
+
// reexportarlo (lo cazó `Fuzzer`).
|
|
313
|
+
autoClosed.delete(tok.tag)
|
|
314
|
+
addToParent(createLeaf('discarded_tag', source.slice(tok.start, tok.end)))
|
|
286
315
|
} else {
|
|
287
316
|
// LEGACY MODE: Walk backwards, auto-close inner tags, ignore orphaned closing tags.
|
|
288
317
|
let found = -1
|
|
@@ -300,13 +329,18 @@ export function parseTokensToGreen(
|
|
|
300
329
|
// trailing width of 0. This used to hand them `tok.end`, which made
|
|
301
330
|
// `[b][i]x[/b]` produce an `italic` and a `bold` both ending at 10,
|
|
302
331
|
// overlapping on the four characters of `[/b]`.
|
|
332
|
+
const autoClosedHere: string[] = []
|
|
303
333
|
while (stack.length - 1 > found) {
|
|
304
334
|
const inner = stack.pop()!
|
|
335
|
+
autoClosedHere.push(inner.tag)
|
|
305
336
|
stack[stack.length - 1].children.push(closeFrame(inner, 0))
|
|
306
337
|
}
|
|
307
338
|
|
|
308
339
|
// Close the matched tag itself — this one does own the delimiter.
|
|
309
340
|
addToParent(closeFrame(stack.pop()!, closeWidth))
|
|
341
|
+
// Las etiquetas que se cerraron solas quedan pendientes en el nivel
|
|
342
|
+
// que ha quedado abierto tras cerrar esta.
|
|
343
|
+
for (const tag of autoClosedHere) autoClosed.add(tag)
|
|
310
344
|
} else {
|
|
311
345
|
// Orphaned closing tag: no matching opener anywhere on the stack.
|
|
312
346
|
//
|
|
@@ -351,7 +385,10 @@ export function parseTokensToGreen(
|
|
|
351
385
|
for (const child of root) {
|
|
352
386
|
// GreenNode.kind is a widened `string`; the kind vocabulary is shared with
|
|
353
387
|
// NodeKind and every value reaching here came from tagToNodeKind().
|
|
354
|
-
|
|
388
|
+
// `discarded_tag` viaja suelto igual que `empty_line`: es un tramo de
|
|
389
|
+
// fuente que no se ve, y envolverlo en un párrafo dejaba un `<span>` vacío
|
|
390
|
+
// en el render por cada cierre descartado.
|
|
391
|
+
if (isBlockKind(child.kind as NodeKind) || child.kind === 'empty_line' || child.kind === 'discarded_tag') {
|
|
355
392
|
flushParagraph()
|
|
356
393
|
normalizedRoot.push(child)
|
|
357
394
|
} else {
|