@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
|
@@ -12,6 +12,12 @@ import { RedNode } from '../Syntax/RedNode'
|
|
|
12
12
|
import { Visitor } from './Visitor'
|
|
13
13
|
import type { VisitorContext } from './Visitor'
|
|
14
14
|
import { TagRegistry, type TagDefinition } from '../Model/TagRegistry'
|
|
15
|
+
import {
|
|
16
|
+
toTokenResolver,
|
|
17
|
+
resolveTokenValue,
|
|
18
|
+
type TokenResolverFn,
|
|
19
|
+
type TokenSource,
|
|
20
|
+
} from '../Tokens'
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
23
|
* Export target for BBCodeExporter.
|
|
@@ -21,8 +27,42 @@ import { TagRegistry, type TagDefinition } from '../Model/TagRegistry'
|
|
|
21
27
|
*/
|
|
22
28
|
export type ExportTarget = 'osu' | 'miliastry' | 'lyne'
|
|
23
29
|
|
|
30
|
+
export interface BBCodeExporterOptions {
|
|
31
|
+
registry?: TagRegistry
|
|
32
|
+
target?: ExportTarget
|
|
33
|
+
tokens?: TokenSource
|
|
34
|
+
resolveTokens?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type BBCodeExportOptions = {
|
|
38
|
+
target?: ExportTarget
|
|
39
|
+
tokens?: TokenSource
|
|
40
|
+
resolveTokens?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Referencia a un token de diseño dentro de un texto: `$nombre`.
|
|
45
|
+
*
|
|
46
|
+
* A nivel de módulo porque el sitio caliente es la rama de nodo TEXTO, la
|
|
47
|
+
* clase de nodo más frecuente de cualquier documento: un literal de expresión
|
|
48
|
+
* regular ahí dentro construye un `RegExp` por nodo. Había además tres copias
|
|
49
|
+
* idénticas del mismo patrón repartidas por el fichero.
|
|
50
|
+
*/
|
|
51
|
+
const TOKEN_REF_RE = /\$([a-zA-Z_][a-zA-Z0-9_-]*)/g
|
|
52
|
+
|
|
53
|
+
/** Sustituye cada `$token` por su valor; deja intacto el que no resuelva. */
|
|
54
|
+
function expandTokenRefs(text: string, resolve: TokenResolverFn): string {
|
|
55
|
+
// `indexOf` antes que `replace`: la inmensa mayoría de los textos no lleva
|
|
56
|
+
// ningún `$`, y así no se pone en marcha el motor de expresiones regulares.
|
|
57
|
+
if (text.indexOf('$') === -1) return text
|
|
58
|
+
return text.replace(TOKEN_REF_RE, (match, name: string) => {
|
|
59
|
+
const resolved = resolve(name) ?? resolve(match)
|
|
60
|
+
return resolved !== undefined ? resolved : match
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
24
64
|
/** Miliastry-native effect tags that osu! doesn't support natively */
|
|
25
|
-
const MILIASTRY_INTERNAL_TAGS = new Set(['gradient', 'grow', 'sinewave', 'rainbow'])
|
|
65
|
+
const MILIASTRY_INTERNAL_TAGS = new Set(['gradient', 'grow', 'sinewave', 'rainbow', 'paint'])
|
|
26
66
|
|
|
27
67
|
/**
|
|
28
68
|
* Tags que existen SOLO en Miliastry (registrados en el TagRegistry) y que
|
|
@@ -39,6 +79,9 @@ export const LYNE_ONLY_TAGS = new Set([
|
|
|
39
79
|
'boxw', 'wnotice', 'tables', 'table_row', 'table_col', 'table_th', 'gallery', 'columns',
|
|
40
80
|
'separator', 'scroll', 'sup', 'sub', 'abbr', 'mark', 'kbd', 'tooltip', 'flip',
|
|
41
81
|
'raw', 'plain', 'guild', 'map', 'align', 'effect', 'anim', 'container', 'style_tag',
|
|
82
|
+
'neon', 'shimmer', 'glitch', 'typewriter', 'wave', 'fire', 'ice', 'ghost', 'glow',
|
|
83
|
+
'outline', 'emboss', 'engrave', 'pulse', 'bounce', 'shake', 'levitate', 'fade-in', 'fade-out',
|
|
84
|
+
'card', 'glass', 'neon-box', 'square', 'row', 'col', 'th',
|
|
42
85
|
])
|
|
43
86
|
|
|
44
87
|
/**
|
|
@@ -89,11 +132,17 @@ const KIND_TO_TAG_NAME: Record<string, string> = {
|
|
|
89
132
|
flower: 'flower',
|
|
90
133
|
gradient: 'gradient',
|
|
91
134
|
grow: 'grow',
|
|
135
|
+
rainbow: 'rainbow',
|
|
136
|
+
sinewave: 'sinewave',
|
|
137
|
+
paint: 'paint',
|
|
92
138
|
align: 'align',
|
|
93
139
|
tables: 'tables',
|
|
94
140
|
table_row: 'row',
|
|
95
141
|
table_col: 'col',
|
|
96
142
|
table_th: 'th',
|
|
143
|
+
row: 'row',
|
|
144
|
+
col: 'col',
|
|
145
|
+
th: 'th',
|
|
97
146
|
gallery: 'gallery',
|
|
98
147
|
columns: 'columns',
|
|
99
148
|
separator: 'separator',
|
|
@@ -111,18 +160,101 @@ const KIND_TO_TAG_NAME: Record<string, string> = {
|
|
|
111
160
|
anim: 'anim',
|
|
112
161
|
container: 'container',
|
|
113
162
|
style_tag: 'style',
|
|
163
|
+
neon: 'neon',
|
|
164
|
+
shimmer: 'shimmer',
|
|
165
|
+
glitch: 'glitch',
|
|
166
|
+
typewriter: 'typewriter',
|
|
167
|
+
wave: 'wave',
|
|
168
|
+
fire: 'fire',
|
|
169
|
+
ice: 'ice',
|
|
170
|
+
ghost: 'ghost',
|
|
171
|
+
glow: 'glow',
|
|
172
|
+
outline: 'outline',
|
|
173
|
+
emboss: 'emboss',
|
|
174
|
+
engrave: 'engrave',
|
|
175
|
+
pulse: 'pulse',
|
|
176
|
+
bounce: 'bounce',
|
|
177
|
+
shake: 'shake',
|
|
178
|
+
levitate: 'levitate',
|
|
179
|
+
'fade-in': 'fade-in',
|
|
180
|
+
'fade-out': 'fade-out',
|
|
181
|
+
card: 'card',
|
|
182
|
+
glass: 'glass',
|
|
183
|
+
'neon-box': 'neon-box',
|
|
184
|
+
square: 'square',
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Whether a parsed attribute actually carries a value worth re-emitting.
|
|
190
|
+
*
|
|
191
|
+
* The parser stores an empty string for the attribute of a bare tag, so a
|
|
192
|
+
* plain `[box]` arrives here with `rawTitle: ''` rather than `undefined`.
|
|
193
|
+
* The guards below used to test `!== undefined`, which an empty string
|
|
194
|
+
* passes — exporting `[box]` as `[box=]` and `[quote]` as `[quote=""]`.
|
|
195
|
+
* Both are corruption, not normalization: bare boxes and quotes are
|
|
196
|
+
* ordinary BBCode and must round-trip untouched.
|
|
197
|
+
*/
|
|
198
|
+
function hasAttrValue(value: unknown): boolean {
|
|
199
|
+
return value !== undefined && value !== null && String(value) !== ''
|
|
114
200
|
}
|
|
115
201
|
|
|
202
|
+
function normalizeColorToHex(color: string): string {
|
|
203
|
+
if (!color) return color
|
|
204
|
+
const trimmed = color.trim()
|
|
205
|
+
// Un hex ya es canónico: bajarlo a minúsculas reescribía `[color=#FF0000]`
|
|
206
|
+
// del autor en cada exportación, y `Analysis/RoundTrip` fija lo contrario.
|
|
207
|
+
if (trimmed.startsWith('#')) return trimmed
|
|
208
|
+
|
|
209
|
+
const rgbMatch = trimmed.match(/^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)$/i)
|
|
210
|
+
if (rgbMatch) {
|
|
211
|
+
const r = parseInt(rgbMatch[1], 10).toString(16).padStart(2, '0')
|
|
212
|
+
const g = parseInt(rgbMatch[2], 10).toString(16).padStart(2, '0')
|
|
213
|
+
const b = parseInt(rgbMatch[3], 10).toString(16).padStart(2, '0')
|
|
214
|
+
return `#${r}${g}${b}`.toLowerCase()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return trimmed
|
|
218
|
+
}
|
|
116
219
|
|
|
117
220
|
export class BBCodeExporter extends Visitor<string> {
|
|
118
221
|
private registry: TagRegistry
|
|
119
222
|
private depth: number = 0
|
|
120
223
|
private target: ExportTarget
|
|
224
|
+
private tokens?: TokenSource
|
|
225
|
+
private tokenResolver?: TokenResolverFn
|
|
226
|
+
private explicitResolveTokens?: boolean
|
|
227
|
+
/**
|
|
228
|
+
* El resolvedor a aplicar en ESTE recorrido, o `null` si no hay expansión.
|
|
229
|
+
*
|
|
230
|
+
* Se decide una vez en `visit`, no por nodo: `shouldResolveTokens()` da la
|
|
231
|
+
* misma respuesta para todo el documento, y se estaba llamando en la rama
|
|
232
|
+
* de nodo texto, la más frecuente que hay.
|
|
233
|
+
*/
|
|
234
|
+
private expandTokens: TokenResolverFn | null = null
|
|
121
235
|
|
|
122
|
-
constructor(
|
|
236
|
+
constructor(
|
|
237
|
+
registryOrOptions?: TagRegistry | BBCodeExporterOptions,
|
|
238
|
+
target: ExportTarget = 'osu',
|
|
239
|
+
options?: BBCodeExporterOptions | { tokens?: TokenSource; resolveTokens?: boolean },
|
|
240
|
+
) {
|
|
123
241
|
super()
|
|
124
|
-
|
|
125
|
-
|
|
242
|
+
if (registryOrOptions && !(registryOrOptions instanceof TagRegistry) && !('get' in registryOrOptions)) {
|
|
243
|
+
const opts = registryOrOptions as BBCodeExporterOptions
|
|
244
|
+
this.registry = opts.registry ?? new TagRegistry()
|
|
245
|
+
this.target = opts.target ?? 'osu'
|
|
246
|
+
this.tokens = opts.tokens
|
|
247
|
+
this.tokenResolver = toTokenResolver(opts.tokens)
|
|
248
|
+
this.explicitResolveTokens = opts.resolveTokens
|
|
249
|
+
} else {
|
|
250
|
+
this.registry = (registryOrOptions as TagRegistry) ?? new TagRegistry()
|
|
251
|
+
this.target = target
|
|
252
|
+
if (options) {
|
|
253
|
+
this.tokens = options.tokens
|
|
254
|
+
this.tokenResolver = toTokenResolver(options.tokens)
|
|
255
|
+
this.explicitResolveTokens = options.resolveTokens
|
|
256
|
+
}
|
|
257
|
+
}
|
|
126
258
|
}
|
|
127
259
|
|
|
128
260
|
/**
|
|
@@ -132,30 +264,95 @@ export class BBCodeExporter extends Visitor<string> {
|
|
|
132
264
|
this.target = target
|
|
133
265
|
}
|
|
134
266
|
|
|
267
|
+
setTokens(tokens?: TokenSource): void {
|
|
268
|
+
this.tokens = tokens
|
|
269
|
+
this.tokenResolver = toTokenResolver(tokens)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
getTokens(): TokenSource | undefined {
|
|
273
|
+
return this.tokens
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
getTokenResolver(): TokenResolverFn | undefined {
|
|
277
|
+
return this.tokenResolver
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private shouldResolveTokens(): boolean {
|
|
281
|
+
if (this.explicitResolveTokens !== undefined) {
|
|
282
|
+
return this.explicitResolveTokens
|
|
283
|
+
}
|
|
284
|
+
return this.target === 'osu'
|
|
285
|
+
}
|
|
286
|
+
|
|
135
287
|
/**
|
|
136
288
|
* Export a RedNode tree to BBCode.
|
|
137
289
|
*/
|
|
138
290
|
visit(node: RedNode, context?: VisitorContext): string {
|
|
139
291
|
if (context) this.context = context
|
|
140
292
|
this.depth = 0
|
|
293
|
+
this.expandTokens =
|
|
294
|
+
this.tokenResolver !== undefined && this.shouldResolveTokens() ? this.tokenResolver : null
|
|
141
295
|
return this.exportNode(node)
|
|
142
296
|
}
|
|
143
297
|
|
|
144
298
|
/**
|
|
145
299
|
* Export the entire document to BBCode.
|
|
146
|
-
* Optionally override the export target for this specific call.
|
|
300
|
+
* Optionally override the export target or options for this specific call.
|
|
147
301
|
*/
|
|
148
|
-
export(
|
|
149
|
-
|
|
302
|
+
export(
|
|
303
|
+
root: RedNode,
|
|
304
|
+
targetOrOptions?: ExportTarget | BBCodeExportOptions,
|
|
305
|
+
options?: BBCodeExportOptions,
|
|
306
|
+
): string {
|
|
307
|
+
if (typeof targetOrOptions === 'string') {
|
|
308
|
+
this.target = targetOrOptions
|
|
309
|
+
if (options?.tokens !== undefined) {
|
|
310
|
+
this.setTokens(options.tokens)
|
|
311
|
+
}
|
|
312
|
+
if (options?.resolveTokens !== undefined) {
|
|
313
|
+
this.explicitResolveTokens = options.resolveTokens
|
|
314
|
+
}
|
|
315
|
+
} else if (targetOrOptions && typeof targetOrOptions === 'object') {
|
|
316
|
+
if (targetOrOptions.target !== undefined) {
|
|
317
|
+
this.target = targetOrOptions.target
|
|
318
|
+
}
|
|
319
|
+
if (targetOrOptions.tokens !== undefined) {
|
|
320
|
+
this.setTokens(targetOrOptions.tokens)
|
|
321
|
+
}
|
|
322
|
+
if (targetOrOptions.resolveTokens !== undefined) {
|
|
323
|
+
this.explicitResolveTokens = targetOrOptions.resolveTokens
|
|
324
|
+
}
|
|
325
|
+
}
|
|
150
326
|
return this.visit(root)
|
|
151
327
|
}
|
|
152
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Exporta los hijos directos y los concatena.
|
|
331
|
+
*
|
|
332
|
+
* Reemplaza el `children.map(...).join('')`, que asignaba un cierre y un
|
|
333
|
+
* array intermedio de N cadenas en cada nivel del árbol. `HTMLRenderer`
|
|
334
|
+
* hizo este mismo cambio y dejó la nota; el exportador se quedó atrás, y es
|
|
335
|
+
* el camino que corre bajo un límite de 60.000 caracteres.
|
|
336
|
+
*/
|
|
337
|
+
private exportChildren(node: RedNode): string {
|
|
338
|
+
const children = node.children
|
|
339
|
+
let out = ''
|
|
340
|
+
for (let i = 0; i < children.length; i++) out += this.exportNode(children[i])
|
|
341
|
+
return out
|
|
342
|
+
}
|
|
343
|
+
|
|
153
344
|
private exportNode(node: RedNode): string {
|
|
345
|
+
// Un cierre que no cerró nada no vuelve al source. Escribirlo hacía que el
|
|
346
|
+
// siguiente parseo lo leyera otra vez como etiqueta viva, y el documento no
|
|
347
|
+
// convergía al reexportarlo.
|
|
348
|
+
if (node.kind === 'discarded_tag') return ''
|
|
349
|
+
|
|
154
350
|
const tagDef = this.registry.getByKind(node.kind)
|
|
155
351
|
|
|
156
352
|
// Text nodes
|
|
157
353
|
if (node.kind === 'text') {
|
|
158
354
|
let out = node.text
|
|
355
|
+
if (this.expandTokens !== null) out = expandTokenRefs(out, this.expandTokens)
|
|
159
356
|
const style = node.metadata?.style as Record<string, string> | undefined
|
|
160
357
|
if (style) {
|
|
161
358
|
// Envolvemos desde adentro hacia afuera (o al revés, no importa mucho para osu)
|
|
@@ -163,8 +360,20 @@ export class BBCodeExporter extends Visitor<string> {
|
|
|
163
360
|
if (style.fontStyle === 'italic') out = `[i]${out}[/i]`
|
|
164
361
|
if (style.textDecoration === 'underline') out = `[u]${out}[/u]`
|
|
165
362
|
if (style.textDecoration === 'line-through') out = `[s]${out}[/s]`
|
|
166
|
-
if (style.color)
|
|
167
|
-
|
|
363
|
+
if (style.color) {
|
|
364
|
+
let col = style.color
|
|
365
|
+
if (this.shouldResolveTokens() && col.startsWith('$')) {
|
|
366
|
+
col = resolveTokenValue(col, this.tokenResolver)
|
|
367
|
+
}
|
|
368
|
+
out = `[color=${normalizeColorToHex(col)}]${out}[/color]`
|
|
369
|
+
}
|
|
370
|
+
if (style.fontSize) {
|
|
371
|
+
let size = style.fontSize
|
|
372
|
+
if (this.shouldResolveTokens() && size.startsWith('$')) {
|
|
373
|
+
size = resolveTokenValue(size, this.tokenResolver)
|
|
374
|
+
}
|
|
375
|
+
out = `[size=${size}]${out}[/size]`
|
|
376
|
+
}
|
|
168
377
|
// Se pueden seguir sumando estilos dinámicos
|
|
169
378
|
}
|
|
170
379
|
return out
|
|
@@ -172,7 +381,7 @@ export class BBCodeExporter extends Visitor<string> {
|
|
|
172
381
|
|
|
173
382
|
// Document root
|
|
174
383
|
if (node.kind === 'document') {
|
|
175
|
-
return
|
|
384
|
+
return this.exportChildren(node)
|
|
176
385
|
}
|
|
177
386
|
|
|
178
387
|
// Custom tag handlers from registry
|
|
@@ -196,7 +405,7 @@ export class BBCodeExporter extends Visitor<string> {
|
|
|
196
405
|
// Default BBCode serialization
|
|
197
406
|
let tagName = this.kindToTagName(node.kind)
|
|
198
407
|
const attrs = this.getTagAttributes(node)
|
|
199
|
-
const content =
|
|
408
|
+
const content = this.exportChildren(node)
|
|
200
409
|
|
|
201
410
|
if (!tagName) {
|
|
202
411
|
if (node.kind === 'spacing') return '\n'
|
|
@@ -246,38 +455,78 @@ export class BBCodeExporter extends Visitor<string> {
|
|
|
246
455
|
if (tag?.properties) {
|
|
247
456
|
const parts: string[] = []
|
|
248
457
|
for (const prop of tag.properties) {
|
|
249
|
-
|
|
458
|
+
let val = node.metadata[prop.name]
|
|
250
459
|
if (val !== undefined && val !== null) {
|
|
460
|
+
if (this.shouldResolveTokens() && typeof val === 'string' && val.startsWith('$')) {
|
|
461
|
+
val = resolveTokenValue(val, this.tokenResolver)
|
|
462
|
+
}
|
|
251
463
|
parts.push(`${prop.name}=${val}`)
|
|
252
464
|
}
|
|
253
465
|
}
|
|
254
466
|
if (parts.length > 0) return `=${parts[0]}`
|
|
255
467
|
}
|
|
256
|
-
} else if (node.kind === 'font_size' && node.metadata.size
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return `=${
|
|
468
|
+
} else if (node.kind === 'font_size' && hasAttrValue(node.metadata.size)) {
|
|
469
|
+
let size = String(node.metadata.size)
|
|
470
|
+
if (this.shouldResolveTokens() && size.startsWith('$')) {
|
|
471
|
+
size = resolveTokenValue(size, this.tokenResolver)
|
|
472
|
+
}
|
|
473
|
+
return `=${size}`
|
|
474
|
+
} else if (node.kind === 'color' && hasAttrValue(node.metadata.color)) {
|
|
475
|
+
let color = String(node.metadata.color)
|
|
476
|
+
if (this.shouldResolveTokens() && color.startsWith('$')) {
|
|
477
|
+
color = resolveTokenValue(color, this.tokenResolver)
|
|
478
|
+
}
|
|
479
|
+
return `=${normalizeColorToHex(color)}`
|
|
480
|
+
} else if (node.kind === 'font' && hasAttrValue(node.metadata.font)) {
|
|
481
|
+
let font = String(node.metadata.font)
|
|
482
|
+
if (this.shouldResolveTokens() && font.startsWith('$')) {
|
|
483
|
+
font = resolveTokenValue(font, this.tokenResolver)
|
|
484
|
+
}
|
|
485
|
+
return `=${font}`
|
|
262
486
|
} else if (node.kind === 'url' && node.metadata.href !== undefined) {
|
|
263
487
|
return `=${node.metadata.href}`
|
|
264
|
-
} else if (node.kind === 'email' && node.metadata.href
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
488
|
+
} else if (node.kind === 'email' && hasAttrValue(node.metadata.href)) {
|
|
489
|
+
// The parser prefixes the address with `mailto:`, so a bare `[email]`
|
|
490
|
+
// arrives as exactly that prefix and nothing else — test the address.
|
|
491
|
+
const address = (node.metadata.href as string).replace('mailto:', '')
|
|
492
|
+
return address === '' ? '' : `=${address}`
|
|
493
|
+
} else if (node.kind === 'quote' && hasAttrValue(node.metadata.source)) {
|
|
494
|
+
let src = String(node.metadata.source)
|
|
495
|
+
if (this.expandTokens !== null) src = expandTokenRefs(src, this.expandTokens)
|
|
496
|
+
return `="${src}"`
|
|
497
|
+
} else if (node.kind === 'box' || node.kind === 'boxw' || node.kind === 'spoilerbox') {
|
|
498
|
+
// `rawTitle` is the source spelling; `title` is the parsed one, which
|
|
499
|
+
// falls back to a synthetic "Box"/"Spoiler" when the tag is bare. Only
|
|
500
|
+
// the raw form can be trusted to reproduce the input, so it wins.
|
|
501
|
+
const raw = node.metadata.rawTitle
|
|
502
|
+
let titleVal = String((raw !== undefined ? raw : node.metadata.title) ?? '')
|
|
503
|
+
if (this.expandTokens !== null) titleVal = expandTokenRefs(titleVal, this.expandTokens)
|
|
270
504
|
// `[box=Title:#hex]`: el color se guardó aparte en metadata; se vuelve
|
|
271
505
|
// a añadir aquí para que el round-trip no pierda el sufijo.
|
|
272
|
-
|
|
506
|
+
let color = node.metadata.color as string | undefined
|
|
507
|
+
if (this.shouldResolveTokens() && color && color.startsWith('$')) {
|
|
508
|
+
color = resolveTokenValue(color, this.tokenResolver)
|
|
509
|
+
}
|
|
510
|
+
// A bare `[box]` has neither, and must stay bare.
|
|
511
|
+
if (titleVal === '' && !color) return ''
|
|
273
512
|
return `=${titleVal}${color ? `:${color}` : ''}`
|
|
274
513
|
} else if ((node.kind === 'notice' || node.kind === 'wnotice') && node.metadata.color !== undefined) {
|
|
275
|
-
|
|
514
|
+
let color = String(node.metadata.color)
|
|
515
|
+
if (this.shouldResolveTokens() && color.startsWith('$')) {
|
|
516
|
+
color = resolveTokenValue(color, this.tokenResolver)
|
|
517
|
+
}
|
|
518
|
+
return `=${color}`
|
|
276
519
|
} else if (node.kind === 'tables' && node.metadata.variant !== undefined) {
|
|
277
|
-
|
|
520
|
+
let color = node.metadata.color as string | undefined
|
|
521
|
+
if (this.shouldResolveTokens() && color && color.startsWith('$')) {
|
|
522
|
+
color = resolveTokenValue(color, this.tokenResolver)
|
|
523
|
+
}
|
|
278
524
|
return `=${node.metadata.variant}${color ? `:${color}` : ''}`
|
|
279
525
|
} else if (node.kind === 'columns' && node.metadata.columns !== undefined) {
|
|
280
|
-
|
|
526
|
+
let color = node.metadata.color as string | undefined
|
|
527
|
+
if (this.shouldResolveTokens() && color && color.startsWith('$')) {
|
|
528
|
+
color = resolveTokenValue(color, this.tokenResolver)
|
|
529
|
+
}
|
|
281
530
|
return `=${node.metadata.columns}${color ? `:${color}` : ''}`
|
|
282
531
|
} else if (node.kind === 'separator' && node.metadata.variant !== undefined) {
|
|
283
532
|
return `=${node.metadata.variant}`
|
|
@@ -294,22 +543,60 @@ export class BBCodeExporter extends Visitor<string> {
|
|
|
294
543
|
} else if (node.kind === 'align' && node.metadata.align !== undefined) {
|
|
295
544
|
return `=${node.metadata.align}`
|
|
296
545
|
} else if (node.kind === 'effect' && node.metadata.effectType !== undefined) {
|
|
297
|
-
|
|
546
|
+
let color = node.metadata.color as string | undefined
|
|
547
|
+
if (this.shouldResolveTokens() && color && color.startsWith('$')) {
|
|
548
|
+
color = resolveTokenValue(color, this.tokenResolver)
|
|
549
|
+
}
|
|
550
|
+
return `=${node.metadata.effectType}${color ? `:${color}` : ''}`
|
|
298
551
|
} else if (node.kind === 'image' && node.metadata.imgAttr !== undefined) {
|
|
299
552
|
return `=${node.metadata.imgAttr}`
|
|
300
553
|
} else if (node.kind === 'anim' && node.metadata.animType !== undefined) {
|
|
301
554
|
return `=${node.metadata.animType}`
|
|
302
555
|
} else if (node.kind === 'container' && node.metadata.containerType !== undefined) {
|
|
303
|
-
|
|
556
|
+
let color = node.metadata.color as string | undefined
|
|
557
|
+
if (this.shouldResolveTokens() && color && color.startsWith('$')) {
|
|
558
|
+
color = resolveTokenValue(color, this.tokenResolver)
|
|
559
|
+
}
|
|
560
|
+
return `=${node.metadata.containerType}${color ? `:${color}` : ''}`
|
|
304
561
|
} else if (node.kind === 'style_tag' && node.metadata.style !== undefined) {
|
|
305
|
-
|
|
562
|
+
let style = String(node.metadata.style)
|
|
563
|
+
if (this.shouldResolveTokens()) {
|
|
564
|
+
style = style.replace(/\$([a-zA-Z0-9_.-]+)/g, (match) => {
|
|
565
|
+
return resolveTokenValue(match, this.tokenResolver)
|
|
566
|
+
})
|
|
567
|
+
}
|
|
568
|
+
return `=${style}`
|
|
569
|
+
} else if (hasAttrValue(node.metadata.value)) {
|
|
570
|
+
let val = String(node.metadata.value)
|
|
571
|
+
if (this.shouldResolveTokens() && val.startsWith('$')) {
|
|
572
|
+
val = resolveTokenValue(val, this.tokenResolver)
|
|
573
|
+
}
|
|
574
|
+
return `=${val}`
|
|
575
|
+
} else if (hasAttrValue(node.metadata.raw)) {
|
|
576
|
+
let raw = String(node.metadata.raw)
|
|
577
|
+
if (this.shouldResolveTokens() && raw.startsWith('$')) {
|
|
578
|
+
raw = resolveTokenValue(raw, this.tokenResolver)
|
|
579
|
+
}
|
|
580
|
+
return `=${raw}`
|
|
306
581
|
}
|
|
307
582
|
}
|
|
308
583
|
|
|
309
584
|
// Fallback: check node text for attribute info
|
|
310
585
|
const text = node.text || ''
|
|
311
586
|
if (text.startsWith('=') || text.startsWith(' ')) {
|
|
312
|
-
|
|
587
|
+
if (node.kind === 'color' && text.startsWith('=')) {
|
|
588
|
+
let col = text.slice(1)
|
|
589
|
+
if (this.shouldResolveTokens() && col.startsWith('$')) {
|
|
590
|
+
col = resolveTokenValue(col, this.tokenResolver)
|
|
591
|
+
}
|
|
592
|
+
return `=${normalizeColorToHex(col)}`
|
|
593
|
+
}
|
|
594
|
+
if (this.shouldResolveTokens() && text.startsWith('=')) {
|
|
595
|
+
let val = text.slice(1)
|
|
596
|
+
if (val.startsWith('$')) {
|
|
597
|
+
return `=${resolveTokenValue(val, this.tokenResolver)}`
|
|
598
|
+
}
|
|
599
|
+
}
|
|
313
600
|
return text
|
|
314
601
|
}
|
|
315
602
|
|
|
@@ -67,6 +67,8 @@ export interface PatchBlocksOptions {
|
|
|
67
67
|
* path on small documents.
|
|
68
68
|
*/
|
|
69
69
|
minWindowedBlocks?: number
|
|
70
|
+
/** If true, forces a full rebuild of the container instead of incremental patching. */
|
|
71
|
+
forceRebuild?: boolean
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
export interface PatchBlocksStats {
|
|
@@ -143,6 +145,53 @@ interface PatchCache {
|
|
|
143
145
|
|
|
144
146
|
const caches = new WeakMap<HTMLElement, PatchCache>()
|
|
145
147
|
|
|
148
|
+
/**
|
|
149
|
+
* ¿Está encendido el volcado verboso? (`globalThis.__BP_DEBUG__ = true`)
|
|
150
|
+
*
|
|
151
|
+
* Imprime por consola el detalle de la reconciliación por ventana. Es caro:
|
|
152
|
+
* sirve para leer un caso a mano, NO para medir con él encendido.
|
|
153
|
+
*/
|
|
154
|
+
function BP_DEBUG(): boolean {
|
|
155
|
+
return (globalThis as { __BP_DEBUG__?: boolean }).__BP_DEBUG__ === true
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Deja constancia de POR QUÉ `patchBlocksInto` tomó el camino que tomó, en
|
|
160
|
+
* `globalThis.__BP_LAST__` (`globalThis.__BP_TRACE__ = true` para encenderlo).
|
|
161
|
+
*
|
|
162
|
+
* Va aparte del volcado verboso a propósito: esta traza es UNA asignación por
|
|
163
|
+
* parcheo, así que se puede dejar encendida MIENTRAS SE MIDE sin falsear la
|
|
164
|
+
* medida, que es justo cuando hace falta.
|
|
165
|
+
*
|
|
166
|
+
* Existe porque "¿por qué esto no usó el camino incremental?" es cara de
|
|
167
|
+
* contestar desde fuera: el bundle está minificado, `patchBlocksInto` devuelve
|
|
168
|
+
* `mode` pero no el motivo, y quien lo llama (el preview) tira las
|
|
169
|
+
* estadísticas. Diagnosticar la caída a `fullRebuild` por tecla en el
|
|
170
|
+
* documento de 547 KB costó desminificar a mano una función dentro de una
|
|
171
|
+
* línea de 75.000 caracteres; con esto se lee `__BP_LAST__` y ya está.
|
|
172
|
+
*
|
|
173
|
+
* Coste apagada: una comparación de identidad por parcheo.
|
|
174
|
+
*/
|
|
175
|
+
function bpTrace(reason: Record<string, unknown>): void {
|
|
176
|
+
if ((globalThis as { __BP_TRACE__?: boolean }).__BP_TRACE__ === true) {
|
|
177
|
+
;(globalThis as { __BP_LAST__?: unknown }).__BP_LAST__ = reason
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Evict `container` from the patch cache so the next patch rebuilds afresh.
|
|
183
|
+
*/
|
|
184
|
+
export function clearPatchCache(container: HTMLElement): void {
|
|
185
|
+
// Contador de invalidaciones bajo la misma traza barata: "cuántas veces se
|
|
186
|
+
// tira la caché por pulsación" es la pregunta que destapó el fullRebuild por
|
|
187
|
+
// tecla, y no se puede contestar desde fuera del módulo.
|
|
188
|
+
if ((globalThis as { __BP_TRACE__?: boolean }).__BP_TRACE__ === true) {
|
|
189
|
+
const g = globalThis as { __BP_CLEARS__?: number }
|
|
190
|
+
g.__BP_CLEARS__ = (g.__BP_CLEARS__ ?? 0) + 1
|
|
191
|
+
}
|
|
192
|
+
caches.delete(container)
|
|
193
|
+
}
|
|
194
|
+
|
|
146
195
|
const defaultRenderer = new HTMLRenderer()
|
|
147
196
|
|
|
148
197
|
function getCache(container: HTMLElement): PatchCache {
|
|
@@ -607,10 +656,15 @@ function reconcileWindowed(
|
|
|
607
656
|
|
|
608
657
|
const oldRuns = cache.lastRuns
|
|
609
658
|
|
|
610
|
-
const DBG = (
|
|
659
|
+
const DBG = BP_DEBUG()
|
|
611
660
|
const dbg = (...a: unknown[]): void => {
|
|
612
661
|
if (DBG) console.log('[BP]', ...a)
|
|
613
662
|
}
|
|
663
|
+
// OJO: `dbg(...)` evalúa sus argumentos ANTES de entrar, así que las
|
|
664
|
+
// llamadas que construyen algo —`.map()`, plantillas, `slice`— van dentro
|
|
665
|
+
// de un `if (DBG)`. Este es el camino de cada pulsación: los cuatro
|
|
666
|
+
// volcados de abajo recorrían la ventana entera y construían una cadena por
|
|
667
|
+
// ejecución, con la depuración apagada, para tirarlo todo.
|
|
614
668
|
|
|
615
669
|
|
|
616
670
|
// ── Old window (OLD coordinates). ──────────────────────────────────────
|
|
@@ -749,14 +803,22 @@ function reconcileWindowed(
|
|
|
749
803
|
const oldWinRunsFinal = oldRuns.slice(oldWinFrom2, oldWinTo)
|
|
750
804
|
const newWinRunsFinal = newWinRuns
|
|
751
805
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
806
|
+
if (DBG) {
|
|
807
|
+
dbg('change', { start: change.start, end: change.end, endOld: change.endOld })
|
|
808
|
+
dbg('oldWin', oldWinFrom2, oldWinTo, 'keys', oldWinRunsFinal.map((r) => `${r.key}:${r.node.kind}`).slice(0, 40))
|
|
809
|
+
dbg('newWin', fromNew, toNew, 'keys', newWinRunsFinal.map((r) => `${r.key}:${r.node.kind}`).slice(0, 40))
|
|
810
|
+
dbg('newWinKeySet', [...newWinRunsFinal.map((r) => r.key).slice(0, 40)])
|
|
811
|
+
}
|
|
756
812
|
|
|
757
813
|
// DOM anchor at the window start, and the window's old nodes.
|
|
758
|
-
|
|
759
|
-
for
|
|
814
|
+
// Indexed, not walked. `childNodes` is a live NodeList with indexed access,
|
|
815
|
+
// so asking for the window's first node is one lookup; stepping to it one
|
|
816
|
+
// `nextSibling` at a time was O(blocks before the edit) and, on a document
|
|
817
|
+
// of 30.000 top-level blocks, the single largest term left in this
|
|
818
|
+
// function's bookkeeping — 11 ms per keystroke, more than the reconcile it
|
|
819
|
+
// was preparing for. The two are exactly equivalent: the k-th child is the
|
|
820
|
+
// k-th child.
|
|
821
|
+
const anchorNode: Node | null = container.childNodes[oldWinFrom2] ?? null
|
|
760
822
|
const oldWinNodes: Node[] = []
|
|
761
823
|
let node: Node | null = anchorNode
|
|
762
824
|
for (let i = oldWinFrom2; i < oldWinTo; i++) {
|
|
@@ -800,7 +862,7 @@ function reconcileWindowed(
|
|
|
800
862
|
const k = oldWinRunsFinal[i].key
|
|
801
863
|
if (newKeySet.has(k) || newByNode.has(oldWinRunsFinal[i].node)) continue
|
|
802
864
|
const el = oldWinNodes[i]
|
|
803
|
-
dbg('orphan?', k, oldWinRunsFinal[i].node.kind, 'el=', el ? (el.nodeType === 1 ? (el as Element).tagName : `text:'${String((el as Text).data ?? '').slice(0, 20)}'`) : 'null')
|
|
865
|
+
if (DBG) dbg('orphan?', k, oldWinRunsFinal[i].node.kind, 'el=', el ? (el.nodeType === 1 ? (el as Element).tagName : `text:'${String((el as Text).data ?? '').slice(0, 20)}'`) : 'null')
|
|
804
866
|
if (el && !removed.has(el)) {
|
|
805
867
|
removed.add(el)
|
|
806
868
|
container.removeChild(el)
|
|
@@ -828,7 +890,7 @@ function reconcileWindowed(
|
|
|
828
890
|
const prev = oldRunByKey.get(run.key) ?? oldRunByNode.get(run.node)
|
|
829
891
|
const element = oldByKey.get(run.key) ?? oldByNode.get(run.node)
|
|
830
892
|
|
|
831
|
-
dbg('WALK', run.key, run.node.kind, 'element=', element ? (element.nodeType === 1 ? (element as Element).tagName : 'text') : 'NEW', 'cursor=', cursor ? (cursor.nodeType === 1 ? (cursor as Element).tagName : 'text') : 'null')
|
|
893
|
+
if (DBG) dbg('WALK', run.key, run.node.kind, 'element=', element ? (element.nodeType === 1 ? (element as Element).tagName : 'text') : 'NEW', 'cursor=', cursor ? (cursor.nodeType === 1 ? (cursor as Element).tagName : 'text') : 'null')
|
|
832
894
|
|
|
833
895
|
if (!element) {
|
|
834
896
|
container.insertBefore(nodeFromHtml(run.html), cursor)
|
|
@@ -858,7 +920,7 @@ function reconcileWindowed(
|
|
|
858
920
|
// the first suffix node. Leftovers of runs that rendered to a different
|
|
859
921
|
// node count sit between the walk's end and the suffix — drop them.
|
|
860
922
|
let tail = cursor
|
|
861
|
-
dbg('tail-safety: cursor=', cursor ? (cursor.nodeType === 1 ? (cursor as Element).tagName : `text:'${String((cursor as Text).data ?? '').slice(0, 15)}'`) : 'null', 'afterWindow=', afterWindow ? (afterWindow.nodeType === 1 ? (afterWindow as Element).tagName : `text:'${String((afterWindow as Text).data ?? '').slice(0, 15)}'`) : 'null')
|
|
923
|
+
if (DBG) dbg('tail-safety: cursor=', cursor ? (cursor.nodeType === 1 ? (cursor as Element).tagName : `text:'${String((cursor as Text).data ?? '').slice(0, 15)}'`) : 'null', 'afterWindow=', afterWindow ? (afterWindow.nodeType === 1 ? (afterWindow as Element).tagName : `text:'${String((afterWindow as Text).data ?? '').slice(0, 15)}'`) : 'null')
|
|
862
924
|
let tailCount = 0
|
|
863
925
|
while (tail !== afterWindow) {
|
|
864
926
|
if (!tail) break
|
|
@@ -923,9 +985,15 @@ export function patchBlocksInto(
|
|
|
923
985
|
return { mode: 'full', total: 0, patched: 0 }
|
|
924
986
|
}
|
|
925
987
|
|
|
988
|
+
if (options.forceRebuild) {
|
|
989
|
+
bpTrace({ why: 'forceRebuild' })
|
|
990
|
+
return fullRebuild(container, rootNode, renderer, cache)
|
|
991
|
+
}
|
|
992
|
+
|
|
926
993
|
// DOM out of sync with the cache (a host replaced the innerHTML behind our
|
|
927
994
|
// back, or a run rendered to a different node count than last time).
|
|
928
995
|
if (container.childNodes.length !== cache.lastRuns.length) {
|
|
996
|
+
bpTrace({ why: 'desync', kids: container.childNodes.length, runs: cache.lastRuns.length })
|
|
929
997
|
return fullRebuild(container, rootNode, renderer, cache)
|
|
930
998
|
}
|
|
931
999
|
|
|
@@ -938,7 +1006,13 @@ export function patchBlocksInto(
|
|
|
938
1006
|
undefined
|
|
939
1007
|
if (change) {
|
|
940
1008
|
const windowed = reconcileWindowed(container, rootNode, change, renderer, cache, options)
|
|
941
|
-
if (windowed)
|
|
1009
|
+
if (windowed) {
|
|
1010
|
+
bpTrace({ why: 'windowed' })
|
|
1011
|
+
return windowed
|
|
1012
|
+
}
|
|
1013
|
+
bpTrace({ why: 'windowed-declinado' })
|
|
1014
|
+
} else {
|
|
1015
|
+
bpTrace({ why: 'sin-changeRange' })
|
|
942
1016
|
}
|
|
943
1017
|
|
|
944
1018
|
const blocks = rootNode.children
|
|
@@ -956,8 +1030,10 @@ export function patchBlocksInto(
|
|
|
956
1030
|
oldSet.add(k)
|
|
957
1031
|
}
|
|
958
1032
|
if (added >= cache.lastKeys.length) {
|
|
1033
|
+
bpTrace({ why: 'added>=lastKeys', added, lastKeys: cache.lastKeys.length })
|
|
959
1034
|
return fullRebuild(container, rootNode, renderer, cache)
|
|
960
1035
|
}
|
|
961
1036
|
|
|
1037
|
+
bpTrace({ why: 'keyed' })
|
|
962
1038
|
return reconcileKeyed(container, rootNode, keys, renderer, cache, options)
|
|
963
1039
|
}
|