@miliastry/quasar 1.0.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/LICENSE +119 -0
- package/README.md +45 -0
- package/dist/Visuals/lyne.css +1027 -0
- package/dist/Visuals/osu.css +257 -0
- package/dist/index.d.mts +4563 -0
- package/dist/index.d.ts +4563 -0
- package/dist/index.js +11291 -0
- package/dist/index.mjs +11197 -0
- package/package.json +51 -0
- package/src/Analysis/Contracts/AnalysisReport.ts +26 -0
- package/src/Analysis/Contracts/Contribution.ts +76 -0
- package/src/Analysis/Contracts/Pass.ts +76 -0
- package/src/Analysis/Contracts/PipelineContext.ts +48 -0
- package/src/Analysis/Passes/Analysis/GradientAnalyzer.ts +292 -0
- package/src/Analysis/Passes/Analysis/MergeableColorAnalyzer.ts +112 -0
- package/src/Analysis/Passes/Analysis/RainbowAnalyzer.ts +233 -0
- package/src/Analysis/Passes/Analysis/WaveAnalyzer.ts +211 -0
- package/src/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.ts +135 -0
- package/src/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.ts +84 -0
- package/src/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.ts +99 -0
- package/src/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.ts +119 -0
- package/src/Analysis/Passes/Decision/DefaultDecision.ts +139 -0
- package/src/Analysis/Passes/Decision/__tests__/DefaultDecision.test.ts +179 -0
- package/src/Analysis/Passes/Transform/CollapseGradientTransform.ts +176 -0
- package/src/Analysis/Passes/Transform/MergeColorsTransform.ts +126 -0
- package/src/Analysis/Passes/Transform/RainbowCollapseTransform.ts +83 -0
- package/src/Analysis/Passes/Transform/WaveCollapseTransform.ts +88 -0
- package/src/Analysis/Passes/Utility/CharacterCountAnalyzer.ts +45 -0
- package/src/Analysis/Pipeline/Pipeline.ts +133 -0
- package/src/Analysis/Pipeline/PipelineBuilder.ts +55 -0
- package/src/Analysis/Pipeline/PipelineStage.ts +19 -0
- package/src/Analysis/Utils/color-utils.ts +132 -0
- package/src/Analysis/__tests__/Integration.test.ts +162 -0
- package/src/Analysis/__tests__/Pipeline.test.ts +133 -0
- package/src/Analysis/index.ts +52 -0
- package/src/BBCode/BBCodeDocumentModel.ts +175 -0
- package/src/BBCode/BBCodeToGreenNode.ts +755 -0
- package/src/BBCode/Parser.ts +384 -0
- package/src/BBCode/index.ts +12 -0
- package/src/Collab/positions.ts +91 -0
- package/src/Commands/Command.ts +44 -0
- package/src/Commands/CommandRegistry.ts +78 -0
- package/src/Commands/DeleteNode.ts +20 -0
- package/src/Commands/InsertText.ts +21 -0
- package/src/Commands/SplitMerge.ts +28 -0
- package/src/Commands/WrapInTag.ts +21 -0
- package/src/Commands/index.ts +6 -0
- package/src/Diff/TreeDiffer.ts +264 -0
- package/src/Diff/__tests__/TreeDiffer.test.ts +65 -0
- package/src/Diff/index.ts +2 -0
- package/src/Events/EventBus.ts +160 -0
- package/src/Events/index.ts +2 -0
- package/src/Formatter/Formatter.ts +54 -0
- package/src/Formatter/index.ts +2 -0
- package/src/HTML/HTMLDocumentModel.ts +35 -0
- package/src/HTML/HTMLToGreenNode.ts +290 -0
- package/src/Incremental/ChangeTracker.ts +105 -0
- package/src/Incremental/IncrementalParser.ts +591 -0
- package/src/Incremental/__tests__/IncrementalParser.test.ts +164 -0
- package/src/Incremental/index.ts +4 -0
- package/src/Lexer/BBCodeLexer.ts +382 -0
- package/src/Lexer/Lexer.ts +181 -0
- package/src/Lexer/index.ts +10 -0
- package/src/Linter/Linter.ts +193 -0
- package/src/Linter/index.ts +2 -0
- package/src/Markdown/MarkdownAST.ts +112 -0
- package/src/Markdown/MarkdownDocumentModel.ts +55 -0
- package/src/Markdown/MarkdownLexer.ts +203 -0
- package/src/Markdown/MarkdownParser.ts +455 -0
- package/src/Markdown/MarkdownToGreenNode.ts +153 -0
- package/src/Model/DocumentModel.ts +694 -0
- package/src/Model/NodeFactory.ts +117 -0
- package/src/Model/TagRegistry.ts +495 -0
- package/src/Model/index.ts +5 -0
- package/src/Plugins/PluginAPI.ts +119 -0
- package/src/Plugins/PluginRegistry.ts +132 -0
- package/src/Plugins/index.ts +3 -0
- package/src/Queries/QueryEngine.ts +152 -0
- package/src/Queries/index.ts +1 -0
- package/src/RenderPipeline/RenderPipeline.ts +125 -0
- package/src/RenderPipeline/RenderTree.ts +134 -0
- package/src/RenderPipeline/index.ts +4 -0
- package/src/Semantic/SemanticAnalyzer.ts +506 -0
- package/src/Semantic/index.ts +2 -0
- package/src/Symbols/SymbolTable.ts +124 -0
- package/src/Symbols/index.ts +1 -0
- package/src/Syntax/GreenNode.ts +324 -0
- package/src/Syntax/GreenNodePool.ts +269 -0
- package/src/Syntax/NodeMatcher.ts +370 -0
- package/src/Syntax/RedNode.ts +569 -0
- package/src/Syntax/RedNodeStore.ts +184 -0
- package/src/Syntax/TreeBuilder.ts +214 -0
- package/src/Syntax/__tests__/GreenNode.test.ts +33 -0
- package/src/Syntax/__tests__/RedNode.test.ts +81 -0
- package/src/Syntax/__tests__/RedNodeStore.test.ts +104 -0
- package/src/Syntax/greenEdit.ts +110 -0
- package/src/Syntax/hash.ts +30 -0
- package/src/Syntax/index.ts +12 -0
- package/src/Syntax/partition.ts +161 -0
- package/src/Syntax/preserveNodeIds.ts +201 -0
- package/src/Tests/ASTOptimizerIdempotence.test.ts +77 -0
- package/src/Tests/BlockPatcher.test.ts +437 -0
- package/src/Tests/BlockPatcherWindowed.test.ts +364 -0
- package/src/Tests/BoxDrawer.test.ts +217 -0
- package/src/Tests/BoxRichTitle.test.ts +105 -0
- package/src/Tests/Chars500kBenchmark.test.ts +151 -0
- package/src/Tests/Chars500kEdits.test.ts +321 -0
- package/src/Tests/CollabPositions.test.ts +146 -0
- package/src/Tests/CompilerPathProfiling.test.ts +186 -0
- package/src/Tests/DOMMorpher.test.ts +142 -0
- package/src/Tests/DomPatchPerf.test.ts +60 -0
- package/src/Tests/EffectSegments.snapshot.json +616 -0
- package/src/Tests/EffectSegments.test.ts +68 -0
- package/src/Tests/FindNodeAtOffset.test.ts +65 -0
- package/src/Tests/Fuzzer.test.ts +166 -0
- package/src/Tests/GreenNodePool.test.ts +153 -0
- package/src/Tests/Lexer.test.ts +238 -0
- package/src/Tests/LyneMode.test.ts +187 -0
- package/src/Tests/ModelCoherence.test.ts +180 -0
- package/src/Tests/Partition.test.ts +238 -0
- package/src/Tests/PluginTags.test.ts +150 -0
- package/src/Tests/ProblematicSection.test.ts +46 -0
- package/src/Tests/ProblematicSectionHTML.test.ts +58 -0
- package/src/Tests/RedReuse.test.ts +134 -0
- package/src/Tests/ReproDelete20k.test.ts +62 -0
- package/src/Tests/SemanticValidators.test.ts +136 -0
- package/src/Tests/StableNodeIds.test.ts +210 -0
- package/src/Tests/StudioColorBloat.test.ts +25 -0
- package/src/Tests/StudioDebugText.test.ts +27 -0
- package/src/Tests/StudioTrailingChar.test.ts +25 -0
- package/src/Tests/StudioValidText.test.ts +25 -0
- package/src/Tests/UrlImgBug.test.ts +23 -0
- package/src/Tests/VisualBuilderFidelity.test.ts +105 -0
- package/src/Tests/referenceDocument.ts +119 -0
- package/src/Transactions/Transaction.ts +176 -0
- package/src/Transactions/UndoManager.ts +111 -0
- package/src/Transactions/index.ts +3 -0
- package/src/Transformers/ASTOptimizer.ts +315 -0
- package/src/Transformers/GradientTransformer.ts +143 -0
- package/src/Transformers/GrowTransformer.ts +115 -0
- package/src/Transformers/RainbowTransformer.ts +121 -0
- package/src/Transformers/SineWaveTransformer.ts +130 -0
- package/src/Transformers/Transformer.ts +22 -0
- package/src/Types/core.ts +270 -0
- package/src/Types/diagnostics.ts +156 -0
- package/src/Types/index.ts +23 -0
- package/src/Types/operations.ts +180 -0
- package/src/Types/queries.ts +121 -0
- package/src/Types/symbols.ts +69 -0
- package/src/Types/tokens.ts +186 -0
- package/src/Utils/BBCodeGenerator.ts +126 -0
- package/src/Utils/ColorMath.ts +276 -0
- package/src/Utils/color.ts +112 -0
- package/src/Utils/dom-to-svg.test.ts +86 -0
- package/src/Utils/dom-to-svg.ts +615 -0
- package/src/Utils/treeTransformers.ts +717 -0
- package/src/Visitors/BBBlocksExporter.ts +69 -0
- package/src/Visitors/BBCodeExporter.ts +318 -0
- package/src/Visitors/BlockPatcher.ts +963 -0
- package/src/Visitors/DOMMorpher.ts +134 -0
- package/src/Visitors/HTMLRenderer.ts +1077 -0
- package/src/Visitors/JSONExporter.ts +66 -0
- package/src/Visitors/MarkdownExporter.ts +99 -0
- package/src/Visitors/SVGRenderer.ts +35 -0
- package/src/Visitors/TiptapExporter.ts +145 -0
- package/src/Visitors/Visitor.ts +48 -0
- package/src/Visitors/index.ts +9 -0
- package/src/Visuals/BoxDrawer.ts +175 -0
- package/src/Visuals/index.ts +42 -0
- package/src/Visuals/lyne.css +1027 -0
- package/src/Visuals/osu.css +257 -0
- package/src/index.ts +197 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Visitor } from './Visitor'
|
|
2
|
+
import type { VisitorContext } from './Visitor'
|
|
3
|
+
import { RedNode } from '../Syntax/RedNode'
|
|
4
|
+
|
|
5
|
+
export interface UIBBBlock {
|
|
6
|
+
id: string
|
|
7
|
+
type: string
|
|
8
|
+
attributes?: Record<string, string>
|
|
9
|
+
children: UIBBBlock[]
|
|
10
|
+
content?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class BBBlocksExporter extends Visitor<UIBBBlock[]> {
|
|
14
|
+
private idCounter = 0
|
|
15
|
+
|
|
16
|
+
private generateId(): string {
|
|
17
|
+
this.idCounter++
|
|
18
|
+
return `block-${Date.now().toString(36)}-${this.idCounter}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
visit(node: RedNode, context?: VisitorContext): UIBBBlock[] {
|
|
22
|
+
if (context) this.context = context
|
|
23
|
+
|
|
24
|
+
// If it's a document node, we just return the exported children
|
|
25
|
+
if (node.kind === 'document') {
|
|
26
|
+
return node.children.flatMap(child => this.exportNode(child))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return [this.exportNode(node)]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export(root: RedNode): UIBBBlock[] {
|
|
33
|
+
return this.visit(root)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private exportNode(node: RedNode): UIBBBlock {
|
|
37
|
+
const block: UIBBBlock = {
|
|
38
|
+
id: this.generateId(),
|
|
39
|
+
type: node.kind,
|
|
40
|
+
children: []
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Process attributes
|
|
44
|
+
if (node.metadata) {
|
|
45
|
+
const attrs: Record<string, string> = {}
|
|
46
|
+
for (const [key, value] of Object.entries(node.metadata)) {
|
|
47
|
+
if (value !== undefined && value !== null) {
|
|
48
|
+
attrs[key] = String(value)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (Object.keys(attrs).length > 0) {
|
|
52
|
+
block.attributes = attrs
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Process content (leaf nodes or text nodes)
|
|
57
|
+
if (node.kind === 'text') {
|
|
58
|
+
block.content = node.text || ''
|
|
59
|
+
} else {
|
|
60
|
+
// Process children
|
|
61
|
+
block.children = node.children.map(child => this.exportNode(child))
|
|
62
|
+
|
|
63
|
+
// Some tags might store text content directly without children in RedNode representation depending on the parser,
|
|
64
|
+
// but RedNode generally structures text as child nodes of type 'text'.
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return block
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocumentEngine — BBCodeExporter
|
|
3
|
+
*
|
|
4
|
+
* Exports the Document Model / Red Tree back to BBCode text.
|
|
5
|
+
* This is how the internal representation becomes editable text.
|
|
6
|
+
*
|
|
7
|
+
* Uses the TagRegistry to determine how each node serializes.
|
|
8
|
+
* Plugins can register custom serializers for custom tags.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { RedNode } from '../Syntax/RedNode'
|
|
12
|
+
import { Visitor } from './Visitor'
|
|
13
|
+
import type { VisitorContext } from './Visitor'
|
|
14
|
+
import { TagRegistry, type TagDefinition } from '../Model/TagRegistry'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Export target for BBCodeExporter.
|
|
18
|
+
* - 'osu': Expands Miliastry/Lyne-native tags into osu!-compatible BBCode.
|
|
19
|
+
* - 'miliastry': Preserves Miliastry-native tags as-is for round-trip editing.
|
|
20
|
+
* - 'lyne': Preserves Lyne-native tags as-is for round-trip editing.
|
|
21
|
+
*/
|
|
22
|
+
export type ExportTarget = 'osu' | 'miliastry' | 'lyne'
|
|
23
|
+
|
|
24
|
+
/** Miliastry-native effect tags that osu! doesn't support natively */
|
|
25
|
+
const MILIASTRY_INTERNAL_TAGS = new Set(['gradient', 'grow', 'sinewave', 'rainbow'])
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Tags que existen SOLO en Miliastry (registrados en el TagRegistry) y que
|
|
29
|
+
* osu! NO renderiza. Si un usuario pega `[shadow]`/`[zalgo]`/etc. en osu!,
|
|
30
|
+
* el texto sale roto (los corchetes se ven literalmente). Cuando el target
|
|
31
|
+
* es 'osu', estos tags se DEGRADAN a su contenido plano: mejor perder el
|
|
32
|
+
* efecto que romper la userpage. El target 'miliastry' los conserva.
|
|
33
|
+
*/
|
|
34
|
+
export const MILIASTRY_ONLY_TAGS = new Set([
|
|
35
|
+
'font', 'zalgo', 'aesthetic', 'sparkle', 'bubble', 'flower', 'svg', 'group',
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
export const LYNE_ONLY_TAGS = new Set([
|
|
39
|
+
'boxw', 'wnotice', 'tables', 'table_row', 'table_col', 'table_th', 'gallery', 'columns',
|
|
40
|
+
'separator', 'scroll', 'sup', 'sub', 'abbr', 'mark', 'kbd', 'tooltip', 'flip',
|
|
41
|
+
'raw', 'plain', 'guild', 'map', 'align', 'effect', 'anim', 'container', 'style_tag',
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* kind → BBCode tag name, in the osu! spelling (`centre`, `youtube`).
|
|
46
|
+
*
|
|
47
|
+
* Module-level on purpose: this used to be an object literal inside
|
|
48
|
+
* `kindToTagName`, which allocated a fresh 30-key object for every node of
|
|
49
|
+
* every export. Note it is NOT interchangeable with `nodeKindToTag` from
|
|
50
|
+
* `BBCodeToGreenNode` — that map inverts the parser's table, where the last
|
|
51
|
+
* spelling registered wins (`center`), while exports must emit osu!'s.
|
|
52
|
+
*/
|
|
53
|
+
const KIND_TO_TAG_NAME: Record<string, string> = {
|
|
54
|
+
bold: 'b',
|
|
55
|
+
italic: 'i',
|
|
56
|
+
underline: 'u',
|
|
57
|
+
strikethrough: 's',
|
|
58
|
+
color: 'color',
|
|
59
|
+
font_size: 'size',
|
|
60
|
+
font: 'font',
|
|
61
|
+
inline_code: 'c',
|
|
62
|
+
code: 'code',
|
|
63
|
+
spoiler: 'spoiler',
|
|
64
|
+
center: 'centre',
|
|
65
|
+
left: 'left',
|
|
66
|
+
right: 'right',
|
|
67
|
+
heading: 'heading',
|
|
68
|
+
notice: 'notice',
|
|
69
|
+
wnotice: 'wnotice',
|
|
70
|
+
url: 'url',
|
|
71
|
+
email: 'email',
|
|
72
|
+
profile: 'profile',
|
|
73
|
+
guild: 'guild',
|
|
74
|
+
map: 'map',
|
|
75
|
+
image: 'img',
|
|
76
|
+
video: 'youtube',
|
|
77
|
+
audio: 'audio',
|
|
78
|
+
imagemap: 'imagemap',
|
|
79
|
+
quote: 'quote',
|
|
80
|
+
spoilerbox: 'spoilerbox',
|
|
81
|
+
box: 'box',
|
|
82
|
+
boxw: 'boxw',
|
|
83
|
+
list: 'list',
|
|
84
|
+
list_item: '*',
|
|
85
|
+
zalgo: 'zalgo',
|
|
86
|
+
aesthetic: 'aesthetic',
|
|
87
|
+
sparkle: 'sparkle',
|
|
88
|
+
bubble: 'bubble',
|
|
89
|
+
flower: 'flower',
|
|
90
|
+
gradient: 'gradient',
|
|
91
|
+
grow: 'grow',
|
|
92
|
+
align: 'align',
|
|
93
|
+
tables: 'tables',
|
|
94
|
+
table_row: 'row',
|
|
95
|
+
table_col: 'col',
|
|
96
|
+
table_th: 'th',
|
|
97
|
+
gallery: 'gallery',
|
|
98
|
+
columns: 'columns',
|
|
99
|
+
separator: 'separator',
|
|
100
|
+
scroll: 'scroll',
|
|
101
|
+
sup: 'sup',
|
|
102
|
+
sub: 'sub',
|
|
103
|
+
abbr: 'abbr',
|
|
104
|
+
mark: 'mark',
|
|
105
|
+
kbd: 'kbd',
|
|
106
|
+
tooltip: 'tooltip',
|
|
107
|
+
flip: 'flip',
|
|
108
|
+
raw: 'raw',
|
|
109
|
+
plain: 'plain',
|
|
110
|
+
effect: 'effect',
|
|
111
|
+
anim: 'anim',
|
|
112
|
+
container: 'container',
|
|
113
|
+
style_tag: 'style',
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
export class BBCodeExporter extends Visitor<string> {
|
|
118
|
+
private registry: TagRegistry
|
|
119
|
+
private depth: number = 0
|
|
120
|
+
private target: ExportTarget
|
|
121
|
+
|
|
122
|
+
constructor(registry: TagRegistry = new TagRegistry(), target: ExportTarget = 'osu') {
|
|
123
|
+
super()
|
|
124
|
+
this.registry = registry
|
|
125
|
+
this.target = target
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Set the export target. Controls how Miliastry-native tags are serialized.
|
|
130
|
+
*/
|
|
131
|
+
setTarget(target: ExportTarget): void {
|
|
132
|
+
this.target = target
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Export a RedNode tree to BBCode.
|
|
137
|
+
*/
|
|
138
|
+
visit(node: RedNode, context?: VisitorContext): string {
|
|
139
|
+
if (context) this.context = context
|
|
140
|
+
this.depth = 0
|
|
141
|
+
return this.exportNode(node)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Export the entire document to BBCode.
|
|
146
|
+
* Optionally override the export target for this specific call.
|
|
147
|
+
*/
|
|
148
|
+
export(root: RedNode, target?: ExportTarget): string {
|
|
149
|
+
if (target !== undefined) this.target = target
|
|
150
|
+
return this.visit(root)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private exportNode(node: RedNode): string {
|
|
154
|
+
const tagDef = this.registry.getByKind(node.kind)
|
|
155
|
+
|
|
156
|
+
// Text nodes
|
|
157
|
+
if (node.kind === 'text') {
|
|
158
|
+
let out = node.text
|
|
159
|
+
const style = node.metadata?.style as Record<string, string> | undefined
|
|
160
|
+
if (style) {
|
|
161
|
+
// Envolvemos desde adentro hacia afuera (o al revés, no importa mucho para osu)
|
|
162
|
+
if (style.fontWeight === 'bold') out = `[b]${out}[/b]`
|
|
163
|
+
if (style.fontStyle === 'italic') out = `[i]${out}[/i]`
|
|
164
|
+
if (style.textDecoration === 'underline') out = `[u]${out}[/u]`
|
|
165
|
+
if (style.textDecoration === 'line-through') out = `[s]${out}[/s]`
|
|
166
|
+
if (style.color) out = `[color=${style.color}]${out}[/color]`
|
|
167
|
+
if (style.fontSize) out = `[size=${style.fontSize}]${out}[/size]`
|
|
168
|
+
// Se pueden seguir sumando estilos dinámicos
|
|
169
|
+
}
|
|
170
|
+
return out
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Document root
|
|
174
|
+
if (node.kind === 'document') {
|
|
175
|
+
return node.children.map(c => this.exportNode(c)).join('')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Custom tag handlers from registry
|
|
179
|
+
// When target is 'miliastry', skip custom handlers for Miliastry-native tags
|
|
180
|
+
// so they serialize as-is instead of expanding to osu!-compatible formats.
|
|
181
|
+
if (tagDef?.toBBCode && !(this.target === 'miliastry' && MILIASTRY_INTERNAL_TAGS.has(node.kind))) {
|
|
182
|
+
return tagDef.toBBCode({
|
|
183
|
+
node,
|
|
184
|
+
source: this.context.source,
|
|
185
|
+
visitChildren: (n) => this.exportNode(n),
|
|
186
|
+
renderChild: () => ({ kind: 'text', text: '', children: [], props: {} }),
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Tags Miliastry-only / Lyne-only: en target 'osu' se degradan a contenido plano para
|
|
191
|
+
// que el BBCode generado SIEMPRE sea pegable en osu! sin tags rotos.
|
|
192
|
+
if (this.target === 'osu' && (MILIASTRY_ONLY_TAGS.has(node.kind) || LYNE_ONLY_TAGS.has(node.kind))) {
|
|
193
|
+
return node.children.map((c) => this.exportNode(c)).join('')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Default BBCode serialization
|
|
197
|
+
let tagName = this.kindToTagName(node.kind)
|
|
198
|
+
const attrs = this.getTagAttributes(node)
|
|
199
|
+
const content = node.children.map(c => this.exportNode(c)).join('')
|
|
200
|
+
|
|
201
|
+
if (!tagName) {
|
|
202
|
+
if (node.kind === 'spacing') return '\n'
|
|
203
|
+
if (node.kind === 'empty_line') return '\n'
|
|
204
|
+
// Plugin tags: not in the builtin kind→tag table, but registered, so
|
|
205
|
+
// they serialize under their registered name and round-trip. Strictly
|
|
206
|
+
// limited to NON-builtin registrations — builtins missing from the
|
|
207
|
+
// table (sinewave, rainbow, svg…) keep their existing content-only
|
|
208
|
+
// serialization, which Studio flows depend on.
|
|
209
|
+
if (tagDef && !this.registry.isBuiltin(tagDef.name)) {
|
|
210
|
+
tagName = tagDef.name
|
|
211
|
+
} else {
|
|
212
|
+
return content
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (node.kind === 'image' || node.kind === 'video' || node.kind === 'audio') {
|
|
217
|
+
// image lleva su tamaño/modificador en el attr (`[img=400x300]url[/img]`)
|
|
218
|
+
// y la URL en el contenido; video/audio usan el attr como src, así que
|
|
219
|
+
// solo image debe re-emitir atributos.
|
|
220
|
+
const mediaAttrs = node.kind === 'image' ? attrs : ''
|
|
221
|
+
return `[${tagName}${mediaAttrs}]${content}[/${tagName}]`
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const kindName: string = node.kind
|
|
225
|
+
if (kindName === 'list_item') {
|
|
226
|
+
return `[*]${content}`
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (content === '' && !node.children.length) {
|
|
230
|
+
if (kindName === 'list_item') return '[*]'
|
|
231
|
+
return `[${tagName}${attrs}]${content}[/${tagName}]`
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return `[${tagName}${attrs}]${content}[/${tagName}]`
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private kindToTagName(kind: string): string | null {
|
|
238
|
+
return KIND_TO_TAG_NAME[kind] ?? null
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private getTagAttributes(node: RedNode): string {
|
|
242
|
+
// For BBCode, attributes are in the node text or metadata
|
|
243
|
+
if (node.metadata) {
|
|
244
|
+
if (node.metadata.tagName) {
|
|
245
|
+
const tag = this.registry.get(node.metadata.tagName as string)
|
|
246
|
+
if (tag?.properties) {
|
|
247
|
+
const parts: string[] = []
|
|
248
|
+
for (const prop of tag.properties) {
|
|
249
|
+
const val = node.metadata[prop.name]
|
|
250
|
+
if (val !== undefined && val !== null) {
|
|
251
|
+
parts.push(`${prop.name}=${val}`)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (parts.length > 0) return `=${parts[0]}`
|
|
255
|
+
}
|
|
256
|
+
} else if (node.kind === 'font_size' && node.metadata.size !== undefined) {
|
|
257
|
+
return `=${node.metadata.size}`
|
|
258
|
+
} else if (node.kind === 'color' && node.metadata.color !== undefined) {
|
|
259
|
+
return `=${node.metadata.color}`
|
|
260
|
+
} else if (node.kind === 'font' && node.metadata.font !== undefined) {
|
|
261
|
+
return `=${node.metadata.font}`
|
|
262
|
+
} else if (node.kind === 'url' && node.metadata.href !== undefined) {
|
|
263
|
+
return `=${node.metadata.href}`
|
|
264
|
+
} else if (node.kind === 'email' && node.metadata.href !== undefined) {
|
|
265
|
+
return `=${(node.metadata.href as string).replace('mailto:', '')}`
|
|
266
|
+
} else if (node.kind === 'quote' && node.metadata.source !== undefined) {
|
|
267
|
+
return `="${node.metadata.source}"`
|
|
268
|
+
} else if ((node.kind === 'box' || node.kind === 'boxw' || node.kind === 'spoilerbox') && (node.metadata.rawTitle !== undefined || node.metadata.title !== undefined)) {
|
|
269
|
+
const titleVal = node.metadata.rawTitle !== undefined ? node.metadata.rawTitle : node.metadata.title
|
|
270
|
+
// `[box=Title:#hex]`: el color se guardó aparte en metadata; se vuelve
|
|
271
|
+
// a añadir aquí para que el round-trip no pierda el sufijo.
|
|
272
|
+
const color = node.metadata.color as string | undefined
|
|
273
|
+
return `=${titleVal}${color ? `:${color}` : ''}`
|
|
274
|
+
} else if ((node.kind === 'notice' || node.kind === 'wnotice') && node.metadata.color !== undefined) {
|
|
275
|
+
return `=${node.metadata.color}`
|
|
276
|
+
} else if (node.kind === 'tables' && node.metadata.variant !== undefined) {
|
|
277
|
+
const color = node.metadata.color as string | undefined
|
|
278
|
+
return `=${node.metadata.variant}${color ? `:${color}` : ''}`
|
|
279
|
+
} else if (node.kind === 'columns' && node.metadata.columns !== undefined) {
|
|
280
|
+
const color = node.metadata.color as string | undefined
|
|
281
|
+
return `=${node.metadata.columns}${color ? `:${color}` : ''}`
|
|
282
|
+
} else if (node.kind === 'separator' && node.metadata.variant !== undefined) {
|
|
283
|
+
return `=${node.metadata.variant}`
|
|
284
|
+
} else if (node.kind === 'scroll' && node.metadata.height !== undefined) {
|
|
285
|
+
return `=${node.metadata.height}`
|
|
286
|
+
} else if (node.kind === 'abbr' && node.metadata.title !== undefined) {
|
|
287
|
+
return `=${node.metadata.title}`
|
|
288
|
+
} else if (node.kind === 'tooltip' && node.metadata.tip !== undefined) {
|
|
289
|
+
return `=${node.metadata.tip}`
|
|
290
|
+
} else if (node.kind === 'guild' && node.metadata.tag !== undefined) {
|
|
291
|
+
return `=${node.metadata.tag}`
|
|
292
|
+
} else if (node.kind === 'map' && node.metadata.id !== undefined) {
|
|
293
|
+
return `=${node.metadata.id}`
|
|
294
|
+
} else if (node.kind === 'align' && node.metadata.align !== undefined) {
|
|
295
|
+
return `=${node.metadata.align}`
|
|
296
|
+
} else if (node.kind === 'effect' && node.metadata.effectType !== undefined) {
|
|
297
|
+
return `=${node.metadata.effectType}`
|
|
298
|
+
} else if (node.kind === 'image' && node.metadata.imgAttr !== undefined) {
|
|
299
|
+
return `=${node.metadata.imgAttr}`
|
|
300
|
+
} else if (node.kind === 'anim' && node.metadata.animType !== undefined) {
|
|
301
|
+
return `=${node.metadata.animType}`
|
|
302
|
+
} else if (node.kind === 'container' && node.metadata.containerType !== undefined) {
|
|
303
|
+
return `=${node.metadata.containerType}`
|
|
304
|
+
} else if (node.kind === 'style_tag' && node.metadata.style !== undefined) {
|
|
305
|
+
return `=${node.metadata.style}`
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Fallback: check node text for attribute info
|
|
310
|
+
const text = node.text || ''
|
|
311
|
+
if (text.startsWith('=') || text.startsWith(' ')) {
|
|
312
|
+
// The text is exactly the attributes string (e.g. "=50" or " min=50 max=200")
|
|
313
|
+
return text
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return ''
|
|
317
|
+
}
|
|
318
|
+
}
|