@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,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocumentEngine — NodeFactory
|
|
3
|
+
*
|
|
4
|
+
* Creates DocumentNode instances, RedNode instances, and GreenNode
|
|
5
|
+
* instances from tag definitions. Centralizes node creation so
|
|
6
|
+
* plugins can provide custom node factories.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { NodeKind, NodeAttributes, NodeMetadata, DocumentNode } from '../Types/core'
|
|
10
|
+
import { createNodeId, createDocumentNode } from '../Types/core'
|
|
11
|
+
import { RedNode } from '../Syntax/RedNode'
|
|
12
|
+
import { greenNode, greenLeaf } from '../Syntax/GreenNode'
|
|
13
|
+
import { TagRegistry, type TagDefinition } from './TagRegistry'
|
|
14
|
+
|
|
15
|
+
export class NodeFactory {
|
|
16
|
+
private registry: TagRegistry
|
|
17
|
+
|
|
18
|
+
constructor(registry: TagRegistry) {
|
|
19
|
+
this.registry = registry
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a RedNode from a tag definition.
|
|
24
|
+
*/
|
|
25
|
+
createFromTag(
|
|
26
|
+
tagName: string,
|
|
27
|
+
attrs: string = '',
|
|
28
|
+
start: number = 0,
|
|
29
|
+
end: number = 0,
|
|
30
|
+
children: RedNode[] = [],
|
|
31
|
+
): RedNode {
|
|
32
|
+
const def = this.registry.get(tagName)
|
|
33
|
+
const kind = def?.kind ?? 'custom'
|
|
34
|
+
const text = `[${tagName}${attrs ? '=' + attrs : ''}]`
|
|
35
|
+
|
|
36
|
+
const attributes: NodeAttributes = {}
|
|
37
|
+
|
|
38
|
+
// Parse attribute string into named attributes
|
|
39
|
+
if (attrs) {
|
|
40
|
+
if (def?.properties) {
|
|
41
|
+
// Use property definitions to parse
|
|
42
|
+
const parts = attrs.split(/,(?![^\[]*\])/g)
|
|
43
|
+
for (let i = 0; i < parts.length && i < def.properties.length; i++) {
|
|
44
|
+
const prop = def.properties[i]
|
|
45
|
+
const value = parts[i].trim()
|
|
46
|
+
if (prop.type === 'number') {
|
|
47
|
+
attributes[prop.name] = parseFloat(value) || prop.defaultValue
|
|
48
|
+
} else if (prop.type === 'boolean') {
|
|
49
|
+
attributes[prop.name] = value === 'true' || value === '1'
|
|
50
|
+
} else {
|
|
51
|
+
attributes[prop.name] = value
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
attributes['value'] = attrs
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// For named attributes like color=#FF0000
|
|
60
|
+
if (attrs.includes('=') && !attrs.startsWith('=')) {
|
|
61
|
+
const eqIdx = attrs.indexOf('=')
|
|
62
|
+
attributes[attrs.slice(0, eqIdx).trim()] = attrs.slice(eqIdx + 1).trim()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const green = greenNode(kind, text, children.map(c => c.green))
|
|
66
|
+
|
|
67
|
+
const red = new RedNode(green, {
|
|
68
|
+
kind,
|
|
69
|
+
metadata: { tagName },
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
for (const child of children) {
|
|
73
|
+
child.parent = red
|
|
74
|
+
red.appendChild(child)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return red
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a text RedNode.
|
|
82
|
+
*/
|
|
83
|
+
createText(text: string, start: number = 0, end: number = 0): RedNode {
|
|
84
|
+
const green = greenLeaf('text', text)
|
|
85
|
+
return new RedNode(green, { kind: 'text' })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create a self-closing tag RedNode (like [*], [img]src[/img]).
|
|
90
|
+
*/
|
|
91
|
+
createSelfClosing(
|
|
92
|
+
tagName: string,
|
|
93
|
+
attrs: string = '',
|
|
94
|
+
start: number = 0,
|
|
95
|
+
end: number = 0,
|
|
96
|
+
): RedNode {
|
|
97
|
+
const def = this.registry.get(tagName)
|
|
98
|
+
const kind = def?.kind ?? 'custom'
|
|
99
|
+
const text = attrs || ''
|
|
100
|
+
|
|
101
|
+
const green = greenLeaf(kind, text)
|
|
102
|
+
return new RedNode(green, { kind, metadata: { tagName, isSelfClosing: true } })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create a document root RedNode.
|
|
107
|
+
*/
|
|
108
|
+
createDocument(children: RedNode[] = []): RedNode {
|
|
109
|
+
const green = greenNode('document', '', children.map(c => c.green))
|
|
110
|
+
const red = new RedNode(green, { kind: 'document' })
|
|
111
|
+
for (const child of children) {
|
|
112
|
+
child.parent = red
|
|
113
|
+
red.appendChild(child)
|
|
114
|
+
}
|
|
115
|
+
return red
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocumentEngine — TagRegistry
|
|
3
|
+
*
|
|
4
|
+
* The central registry for all known tags/node kinds.
|
|
5
|
+
* This is the plugin extension point for adding new BBCode tags,
|
|
6
|
+
* custom node types, or even entire new languages.
|
|
7
|
+
*
|
|
8
|
+
* Each tag definition includes:
|
|
9
|
+
* - Parser handler (how to parse this tag from tokens)
|
|
10
|
+
* - Renderer (how to render this tag to HTML, Markdown, etc.)
|
|
11
|
+
* - Validator (semantic validation rules)
|
|
12
|
+
* - Toolbar definition (for the visual editor)
|
|
13
|
+
* - Property editor (for the properties panel)
|
|
14
|
+
*
|
|
15
|
+
* Inspired by VSCode's contribution points.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { NodeKind, NodeAttributes } from '../Types/core'
|
|
19
|
+
import type { RedNode } from '../Syntax/RedNode'
|
|
20
|
+
import { GreenNode } from '../Syntax/GreenNode'
|
|
21
|
+
import type { RenderNode } from '../RenderPipeline/RenderTree'
|
|
22
|
+
import type { Validator } from '../Semantic/SemanticAnalyzer'
|
|
23
|
+
import { mixMultiple, ease, clamp, hslToHex, type Easing } from '../Utils/color'
|
|
24
|
+
|
|
25
|
+
// ─── Tag Handler ───────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export interface TagHandlerContext {
|
|
28
|
+
node: RedNode
|
|
29
|
+
source: string
|
|
30
|
+
visitChildren: (node: RedNode) => string
|
|
31
|
+
renderChild: (node: RedNode) => RenderNode
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TagDefinition {
|
|
35
|
+
/** The tag name (e.g. 'color', 'b', 'img') */
|
|
36
|
+
name: string
|
|
37
|
+
/** Semantic kind */
|
|
38
|
+
kind: NodeKind
|
|
39
|
+
/** Human-readable label */
|
|
40
|
+
label: string
|
|
41
|
+
/** Category for grouping in UI */
|
|
42
|
+
category?: 'formatting' | 'layout' | 'media' | 'special' | 'text'
|
|
43
|
+
/** Icon name for UI */
|
|
44
|
+
icon?: string
|
|
45
|
+
/** Whether this tag is inline (span) or block (div) */
|
|
46
|
+
isInline: boolean
|
|
47
|
+
/** Whether this tag is self-closing */
|
|
48
|
+
isSelfClosing: boolean
|
|
49
|
+
/** Whether this tag can contain children */
|
|
50
|
+
canHaveChildren: boolean
|
|
51
|
+
/** Whether this tag is deprecated */
|
|
52
|
+
isDeprecated?: boolean
|
|
53
|
+
/** Replacement tag name if deprecated */
|
|
54
|
+
deprecatedReplacement?: string
|
|
55
|
+
|
|
56
|
+
// ─── Handlers ───────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
/** Convert a RedNode back to BBCode text */
|
|
59
|
+
toBBCode?: (ctx: TagHandlerContext) => string
|
|
60
|
+
/** Convert a RedNode to a RenderNode for the render pipeline */
|
|
61
|
+
toRenderNode?: (ctx: TagHandlerContext) => RenderNode
|
|
62
|
+
/** Convert a RedNode to HTML string (legacy) */
|
|
63
|
+
toHTML?: (ctx: TagHandlerContext) => string
|
|
64
|
+
/** Default attributes for new instances */
|
|
65
|
+
defaultAttributes?: () => NodeAttributes
|
|
66
|
+
|
|
67
|
+
// ─── Validation ─────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/** Additional validator for this specific tag */
|
|
70
|
+
validator?: Validator
|
|
71
|
+
|
|
72
|
+
// ─── UI Metadata ────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/** Toolbar button definition */
|
|
75
|
+
toolbar?: {
|
|
76
|
+
group: string
|
|
77
|
+
label: string
|
|
78
|
+
icon?: string
|
|
79
|
+
shortcut?: string
|
|
80
|
+
}
|
|
81
|
+
/** Properties panel definition */
|
|
82
|
+
properties?: PropertyDefinition[]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Property Definition ───────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
export interface PropertyDefinition {
|
|
88
|
+
name: string
|
|
89
|
+
label: string
|
|
90
|
+
type: 'text' | 'color' | 'number' | 'select' | 'boolean' | 'slider'
|
|
91
|
+
defaultValue?: string | number | boolean
|
|
92
|
+
options?: { label: string; value: string }[]
|
|
93
|
+
min?: number
|
|
94
|
+
max?: number
|
|
95
|
+
step?: number
|
|
96
|
+
placeholder?: string
|
|
97
|
+
description?: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── TagRegistry ───────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
// ─── Internal tag helpers (gradient/grow export) ──────────────
|
|
103
|
+
|
|
104
|
+
/** Recursively extract plain text from a RedNode tree */
|
|
105
|
+
function extractTextContent(node: RedNode): string {
|
|
106
|
+
if (node.kind === 'text') return node.text
|
|
107
|
+
return node.children.map(extractTextContent).join('')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Split text by unit: 'character', 'word', or 'line' */
|
|
111
|
+
function splitByUnit(text: string, unit: string): string[] {
|
|
112
|
+
if (!text) return []
|
|
113
|
+
switch (unit) {
|
|
114
|
+
case 'word':
|
|
115
|
+
return text.split(/(\s+)/).filter(s => s.length > 0)
|
|
116
|
+
case 'line':
|
|
117
|
+
return text.split('\n')
|
|
118
|
+
default: // 'character'
|
|
119
|
+
return [...text]
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Escape [ and ] inside BBCode content so they're not parsed as tags */
|
|
124
|
+
function escapeBracket(ch: string): string {
|
|
125
|
+
// BBCode has no standard escape. For internal tags produced by TextStudio,
|
|
126
|
+
// text never contains brackets. If it does, we use the backslash escape
|
|
127
|
+
// that some BBCode parsers support.
|
|
128
|
+
return ch.replace(/\[/g, '\\[').replace(/\]/g, '\\]')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ─── Effect segment math — one source of truth per effect ─────
|
|
132
|
+
//
|
|
133
|
+
// Each effect (gradient, sinewave, grow, rainbow) serializes two ways: to
|
|
134
|
+
// osu!-compatible BBCode (`toBBCode`) and to a RenderNode tree
|
|
135
|
+
// (`toRenderNode`). Both used to carry their own copy of the per-segment
|
|
136
|
+
// math — t-progression, easing, color mixing, wave sizing — differing only in
|
|
137
|
+
// presentation, which is exactly how the two drift apart. The math now lives
|
|
138
|
+
// in ONE `*Segments` function per effect; the handlers are thin presenters.
|
|
139
|
+
//
|
|
140
|
+
// Note this is deliberately NOT the same algorithm as `HTMLRenderer`'s own
|
|
141
|
+
// `renderGradient`: the preview paints a smooth CSS `linear-gradient` with
|
|
142
|
+
// `background-clip: text`, while these segments reproduce the DISCRETE
|
|
143
|
+
// per-chunk colors that the exported `[color=…]` BBCode will actually have.
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* A run of text with the style its effect computed. `color` and `size` are
|
|
147
|
+
* mutually exclusive; neither set means the run passes through unstyled
|
|
148
|
+
* (whitespace between sized words).
|
|
149
|
+
*/
|
|
150
|
+
interface StyledSegment {
|
|
151
|
+
text: string
|
|
152
|
+
color?: string
|
|
153
|
+
size?: number
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Effect progression t ∈ [0,1] for segment `i` of `count`, honoring the
|
|
158
|
+
* document-wide fields (`globalOffset`/`documentLength`) that let one logical
|
|
159
|
+
* effect span several nodes. `single` is the t for a one-segment run — the
|
|
160
|
+
* effects legitimately disagree on it (gradient/rainbow use 0, grow 0.5).
|
|
161
|
+
*/
|
|
162
|
+
function progressAt(i: number, count: number, node: RedNode, single: number): number {
|
|
163
|
+
const globalOffset = (node.metadata?.globalOffset as number) || 0
|
|
164
|
+
const documentLength = (node.metadata?.documentLength as number) || count
|
|
165
|
+
if (documentLength > 1) return (globalOffset + i) / (documentLength - 1)
|
|
166
|
+
return count > 1 ? i / (count - 1) : single
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function gradientSegments(node: RedNode): StyledSegment[] {
|
|
170
|
+
const colors = (node.metadata?.colors as string[]) || ['#FF0000', '#00FF00']
|
|
171
|
+
const unit = (node.metadata?.unit as string) || 'character'
|
|
172
|
+
const easing = (node.metadata?.easing as Easing) || 'linear'
|
|
173
|
+
const text = extractTextContent(node)
|
|
174
|
+
if (!text) return []
|
|
175
|
+
const parts = splitByUnit(text, unit)
|
|
176
|
+
return parts.map((seg, i) => ({
|
|
177
|
+
text: seg,
|
|
178
|
+
color: mixMultiple(colors, ease(clamp(progressAt(i, parts.length, node, 0)), easing)),
|
|
179
|
+
}))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function sinewaveSegments(node: RedNode): StyledSegment[] {
|
|
183
|
+
const min = (node.metadata?.min as number) ?? 20
|
|
184
|
+
const max = (node.metadata?.max as number) ?? 80
|
|
185
|
+
const freq = (node.metadata?.freq as number) ?? 0.4
|
|
186
|
+
const step = (node.metadata?.step as string) ?? 'char'
|
|
187
|
+
const text = extractTextContent(node)
|
|
188
|
+
if (!text) return []
|
|
189
|
+
const amplitude = (max - min) / 2
|
|
190
|
+
const center = min + amplitude
|
|
191
|
+
|
|
192
|
+
if (step === 'word') {
|
|
193
|
+
let wordIndex = 0
|
|
194
|
+
return text.split(/(\s+)/).map(chunk => {
|
|
195
|
+
if (chunk.trim() === '') return { text: chunk }
|
|
196
|
+
const size = Math.round(center + Math.sin(wordIndex * freq) * amplitude)
|
|
197
|
+
wordIndex++
|
|
198
|
+
return { text: chunk, size }
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return [...text].map((ch, i) => ({
|
|
203
|
+
text: ch,
|
|
204
|
+
size: Math.round(center + Math.sin(i * freq) * amplitude),
|
|
205
|
+
}))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function growSegments(node: RedNode): StyledSegment[] {
|
|
209
|
+
const min = (node.metadata?.min as number) ?? 50
|
|
210
|
+
const max = (node.metadata?.max as number) ?? 200
|
|
211
|
+
const cycles = (node.metadata?.cycles as number) ?? 1
|
|
212
|
+
const text = extractTextContent(node)
|
|
213
|
+
if (!text) return []
|
|
214
|
+
const chars = [...text]
|
|
215
|
+
return chars.map((ch, i) => {
|
|
216
|
+
const sine = Math.sin(progressAt(i, chars.length, node, 0.5) * cycles * Math.PI * 2)
|
|
217
|
+
const normalized = (sine + 1) / 2
|
|
218
|
+
return { text: ch, size: Math.round(min + (max - min) * normalized) }
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function rainbowSegments(node: RedNode): StyledSegment[] {
|
|
223
|
+
const saturation = ((node.metadata?.saturation as number) ?? 80) / 100
|
|
224
|
+
const lightness = ((node.metadata?.lightness as number) ?? 60) / 100
|
|
225
|
+
const spread = (node.metadata?.spread as number) ?? 300
|
|
226
|
+
const offset = (node.metadata?.offset as number) ?? 0
|
|
227
|
+
const text = extractTextContent(node)
|
|
228
|
+
if (!text) return []
|
|
229
|
+
const chars = [...text]
|
|
230
|
+
return chars.map((ch, i) => ({
|
|
231
|
+
text: ch,
|
|
232
|
+
color: hslToHex(offset + progressAt(i, chars.length, node, 0) * spread, saturation, lightness),
|
|
233
|
+
}))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Present styled segments as osu!-compatible BBCode. */
|
|
237
|
+
function segmentsToBBCode(segments: StyledSegment[]): string {
|
|
238
|
+
let out = ''
|
|
239
|
+
for (const s of segments) {
|
|
240
|
+
if (s.color !== undefined) out += `[color=${s.color}]${escapeBracket(s.text)}[/color]`
|
|
241
|
+
else if (s.size !== undefined) out += `[size=${s.size}]${escapeBracket(s.text)}[/size]`
|
|
242
|
+
else out += s.text
|
|
243
|
+
}
|
|
244
|
+
return out
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Present styled segments as a RenderNode tree. */
|
|
248
|
+
function segmentsToRenderNode(kind: string, segments: StyledSegment[]): RenderNode {
|
|
249
|
+
return {
|
|
250
|
+
kind,
|
|
251
|
+
text: '',
|
|
252
|
+
children: segments.map((s): RenderNode => {
|
|
253
|
+
const style: Record<string, string> =
|
|
254
|
+
s.color !== undefined ? { color: s.color }
|
|
255
|
+
: s.size !== undefined ? { fontSize: `${s.size}%` }
|
|
256
|
+
: {}
|
|
257
|
+
return { kind: 'text', text: s.text, children: [], props: {}, style }
|
|
258
|
+
}),
|
|
259
|
+
props: {},
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export type TagHandler = {
|
|
264
|
+
[K in keyof TagDefinition]: TagDefinition[K]
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export class TagRegistry {
|
|
268
|
+
private tags: Map<string, TagDefinition> = new Map()
|
|
269
|
+
private kinds: Map<NodeKind, TagDefinition> = new Map()
|
|
270
|
+
/** Names registered by the constructor — the language's own tags. */
|
|
271
|
+
private builtins: Set<string> = new Set()
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Bumped on every register/unregister, so consumers that derive something
|
|
275
|
+
* from the registry (the parser's `extraTags` map) can memoize against it
|
|
276
|
+
* instead of rebuilding per parse.
|
|
277
|
+
*/
|
|
278
|
+
version: number = 0
|
|
279
|
+
|
|
280
|
+
constructor() {
|
|
281
|
+
this.registerBuiltins()
|
|
282
|
+
for (const name of this.tags.keys()) this.builtins.add(name)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Register a tag definition.
|
|
287
|
+
*/
|
|
288
|
+
register(tag: TagDefinition): void {
|
|
289
|
+
this.tags.set(tag.name, tag)
|
|
290
|
+
this.kinds.set(tag.kind, tag)
|
|
291
|
+
this.version++
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Unregister a tag definition.
|
|
296
|
+
*/
|
|
297
|
+
unregister(name: string): void {
|
|
298
|
+
const tag = this.tags.get(name)
|
|
299
|
+
if (tag) {
|
|
300
|
+
this.tags.delete(name)
|
|
301
|
+
this.kinds.delete(tag.kind)
|
|
302
|
+
this.version++
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Whether `name` is one of the language's own tags, as opposed to a
|
|
308
|
+
* plugin registration. The distinction matters: plugin tags must reach the
|
|
309
|
+
* parser (via `ParseOptions.extraTags`) and serialize with their own name,
|
|
310
|
+
* while builtins already have both behaviors hardcoded — routing them
|
|
311
|
+
* through the plugin path would change engine semantics.
|
|
312
|
+
*/
|
|
313
|
+
isBuiltin(name: string): boolean {
|
|
314
|
+
return this.builtins.has(name)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* tag name → kind for every non-builtin registration: the parser's
|
|
319
|
+
* `extraTags`. Returns `null` when there are none, so the common case
|
|
320
|
+
* costs one size comparison and no allocation.
|
|
321
|
+
*/
|
|
322
|
+
customTags(): Map<string, NodeKind> | null {
|
|
323
|
+
if (this.tags.size === this.builtins.size) return null
|
|
324
|
+
const out = new Map<string, NodeKind>()
|
|
325
|
+
for (const [name, def] of this.tags) {
|
|
326
|
+
if (!this.builtins.has(name)) out.set(name, def.kind)
|
|
327
|
+
}
|
|
328
|
+
return out.size > 0 ? out : null
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Get a tag definition by name.
|
|
333
|
+
*/
|
|
334
|
+
get(name: string): TagDefinition | undefined {
|
|
335
|
+
return this.tags.get(name)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get a tag definition by semantic kind.
|
|
340
|
+
*/
|
|
341
|
+
getByKind(kind: NodeKind): TagDefinition | undefined {
|
|
342
|
+
return this.kinds.get(kind)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Check if a tag is registered.
|
|
347
|
+
*/
|
|
348
|
+
has(name: string): boolean {
|
|
349
|
+
return this.tags.has(name)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Get all registered tags.
|
|
354
|
+
*/
|
|
355
|
+
getAll(): TagDefinition[] {
|
|
356
|
+
return Array.from(this.tags.values())
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Get tags by category.
|
|
361
|
+
*/
|
|
362
|
+
getByCategory(category: string): TagDefinition[] {
|
|
363
|
+
return this.getAll().filter(t => t.category === category)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Get tags by whether they are inline.
|
|
368
|
+
*/
|
|
369
|
+
getInline(): TagDefinition[] {
|
|
370
|
+
return this.getAll().filter(t => t.isInline)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Get block-level tags.
|
|
375
|
+
*/
|
|
376
|
+
getBlock(): TagDefinition[] {
|
|
377
|
+
return this.getAll().filter(t => !t.isInline && !t.isSelfClosing)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Register the built-in BBCode tags.
|
|
382
|
+
*/
|
|
383
|
+
private registerBuiltins(): void {
|
|
384
|
+
const tags: TagDefinition[] = [
|
|
385
|
+
// ── Formatting ──
|
|
386
|
+
{ name: 'b', kind: 'bold', label: 'Bold', category: 'formatting', isInline: true, isSelfClosing: false, canHaveChildren: true, toolbar: { group: 'formatting', label: 'Bold', icon: 'Bold', shortcut: 'Ctrl+B' } },
|
|
387
|
+
{ name: 'i', kind: 'italic', label: 'Italic', category: 'formatting', isInline: true, isSelfClosing: false, canHaveChildren: true, toolbar: { group: 'formatting', label: 'Italic', icon: 'Italic', shortcut: 'Ctrl+I' } },
|
|
388
|
+
{ name: 'u', kind: 'underline', label: 'Underline', category: 'formatting', isInline: true, isSelfClosing: false, canHaveChildren: true, toolbar: { group: 'formatting', label: 'Underline', icon: 'Underline', shortcut: 'Ctrl+U' } },
|
|
389
|
+
{ name: 's', kind: 'strikethrough', label: 'Strikethrough', category: 'formatting', isInline: true, isSelfClosing: false, canHaveChildren: true },
|
|
390
|
+
{ name: 'strike', kind: 'strikethrough', label: 'Strikethrough (deprecated)', category: 'formatting', isInline: true, isSelfClosing: false, canHaveChildren: true, isDeprecated: true, deprecatedReplacement: 's' },
|
|
391
|
+
{ name: 'color', kind: 'color', label: 'Color', category: 'formatting', isInline: true, isSelfClosing: false, canHaveChildren: true, toolbar: { group: 'formatting', label: 'Color', icon: 'Palette' }, properties: [{ name: 'color', label: 'Color', type: 'color', defaultValue: '#FF66AB' }] },
|
|
392
|
+
{ name: 'size', kind: 'font_size', label: 'Font Size', category: 'formatting', isInline: true, isSelfClosing: false, canHaveChildren: true, properties: [{ name: 'size', label: 'Size (%)', type: 'slider', defaultValue: 100, min: 50, max: 200, step: 5 }] },
|
|
393
|
+
{ name: 'c', kind: 'inline_code', label: 'Inline Code', category: 'formatting', isInline: true, isSelfClosing: false, canHaveChildren: true },
|
|
394
|
+
{ name: 'font', kind: 'font', label: 'Font', category: 'formatting', isInline: true, isSelfClosing: false, canHaveChildren: true },
|
|
395
|
+
{ name: 'spoiler', kind: 'spoiler', label: 'Spoiler', category: 'formatting', isInline: true, isSelfClosing: false, canHaveChildren: true, toolbar: { group: 'formatting', label: 'Spoiler', icon: 'EyeOff' } },
|
|
396
|
+
{ name: 'zalgo', kind: 'zalgo', label: 'Zalgo', category: 'text', isInline: true, isSelfClosing: false, canHaveChildren: true },
|
|
397
|
+
{ name: 'aesthetic', kind: 'aesthetic', label: 'Aesthetic', category: 'text', isInline: true, isSelfClosing: false, canHaveChildren: true },
|
|
398
|
+
{ name: 'sparkle', kind: 'sparkle', label: 'Sparkle', category: 'text', isInline: true, isSelfClosing: false, canHaveChildren: true },
|
|
399
|
+
{ name: 'bubble', kind: 'bubble', label: 'Bubble', category: 'text', isInline: true, isSelfClosing: false, canHaveChildren: true },
|
|
400
|
+
{ name: 'flower', kind: 'flower', label: 'Flower', category: 'text', isInline: true, isSelfClosing: false, canHaveChildren: true },
|
|
401
|
+
|
|
402
|
+
// ── Internal (export-only, not user-facing BBCode) ──
|
|
403
|
+
{
|
|
404
|
+
name: 'gradient',
|
|
405
|
+
kind: 'gradient',
|
|
406
|
+
label: 'Gradient',
|
|
407
|
+
category: 'text',
|
|
408
|
+
isInline: true,
|
|
409
|
+
isSelfClosing: false,
|
|
410
|
+
canHaveChildren: true,
|
|
411
|
+
toBBCode: (ctx) => {
|
|
412
|
+
const segments = gradientSegments(ctx.node)
|
|
413
|
+
return segments.length === 0 ? ctx.visitChildren(ctx.node) : segmentsToBBCode(segments)
|
|
414
|
+
},
|
|
415
|
+
toRenderNode: (ctx) => segmentsToRenderNode('gradient', gradientSegments(ctx.node)),
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: 'sinewave',
|
|
419
|
+
kind: 'sinewave',
|
|
420
|
+
label: 'Sine Wave',
|
|
421
|
+
category: 'text',
|
|
422
|
+
isInline: true,
|
|
423
|
+
isSelfClosing: false,
|
|
424
|
+
canHaveChildren: true,
|
|
425
|
+
toBBCode: (ctx) => {
|
|
426
|
+
const segments = sinewaveSegments(ctx.node)
|
|
427
|
+
return segments.length === 0 ? ctx.visitChildren(ctx.node) : segmentsToBBCode(segments)
|
|
428
|
+
},
|
|
429
|
+
toRenderNode: (ctx) => segmentsToRenderNode('sinewave', sinewaveSegments(ctx.node)),
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: 'grow',
|
|
433
|
+
kind: 'grow',
|
|
434
|
+
label: 'Grow',
|
|
435
|
+
category: 'text',
|
|
436
|
+
isInline: true,
|
|
437
|
+
isSelfClosing: false,
|
|
438
|
+
canHaveChildren: true,
|
|
439
|
+
toBBCode: (ctx) => {
|
|
440
|
+
const segments = growSegments(ctx.node)
|
|
441
|
+
return segments.length === 0 ? ctx.visitChildren(ctx.node) : segmentsToBBCode(segments)
|
|
442
|
+
},
|
|
443
|
+
toRenderNode: (ctx) => segmentsToRenderNode('grow', growSegments(ctx.node)),
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: 'rainbow',
|
|
447
|
+
kind: 'rainbow',
|
|
448
|
+
label: 'Rainbow',
|
|
449
|
+
category: 'text',
|
|
450
|
+
isInline: true,
|
|
451
|
+
isSelfClosing: false,
|
|
452
|
+
canHaveChildren: true,
|
|
453
|
+
toBBCode: (ctx) => {
|
|
454
|
+
const segments = rainbowSegments(ctx.node)
|
|
455
|
+
return segments.length === 0 ? ctx.visitChildren(ctx.node) : segmentsToBBCode(segments)
|
|
456
|
+
},
|
|
457
|
+
toRenderNode: (ctx) => segmentsToRenderNode('rainbow', rainbowSegments(ctx.node)),
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
// ── Layout ──
|
|
461
|
+
{ name: 'centre', kind: 'center', label: 'Centre', category: 'layout', isInline: false, isSelfClosing: false, canHaveChildren: true, toolbar: { group: 'layout', label: 'Centre', icon: 'AlignCenter' } },
|
|
462
|
+
{ name: 'center', kind: 'center', label: 'Center', category: 'layout', isInline: false, isSelfClosing: false, canHaveChildren: true, isDeprecated: true, deprecatedReplacement: 'centre' },
|
|
463
|
+
{ name: 'right', kind: 'right', label: 'Right', category: 'layout', isInline: false, isSelfClosing: false, canHaveChildren: true },
|
|
464
|
+
{ name: 'left', kind: 'left', label: 'Left', category: 'layout', isInline: false, isSelfClosing: false, canHaveChildren: true },
|
|
465
|
+
{ name: 'heading', kind: 'heading', label: 'Heading', category: 'layout', isInline: false, isSelfClosing: false, canHaveChildren: true },
|
|
466
|
+
{ name: 'notice', kind: 'notice', label: 'Notice', category: 'layout', isInline: false, isSelfClosing: false, canHaveChildren: true },
|
|
467
|
+
{ name: 'spacing', kind: 'spacing', label: 'Spacing', category: 'layout', isInline: false, isSelfClosing: true, canHaveChildren: false },
|
|
468
|
+
{ name: 'empty_line', kind: 'empty_line', label: 'Empty Line', category: 'layout', isInline: false, isSelfClosing: true, canHaveChildren: false },
|
|
469
|
+
|
|
470
|
+
// ── Media ──
|
|
471
|
+
{ name: 'img', kind: 'image', label: 'Image', category: 'media', isInline: false, isSelfClosing: false, canHaveChildren: false },
|
|
472
|
+
{ name: 'youtube', kind: 'video', label: 'YouTube', category: 'media', isInline: false, isSelfClosing: false, canHaveChildren: false },
|
|
473
|
+
{ name: 'audio', kind: 'audio', label: 'Audio', category: 'media', isInline: false, isSelfClosing: false, canHaveChildren: false },
|
|
474
|
+
{ name: 'imagemap', kind: 'imagemap', label: 'Image Map', category: 'media', isInline: false, isSelfClosing: false, canHaveChildren: true },
|
|
475
|
+
|
|
476
|
+
// ── Special ──
|
|
477
|
+
{ name: 'quote', kind: 'quote', label: 'Quote', category: 'special', isInline: false, isSelfClosing: false, canHaveChildren: true, toolbar: { group: 'special', label: 'Quote', icon: 'Quote' } },
|
|
478
|
+
{ name: 'code', kind: 'code', label: 'Code Block', category: 'special', isInline: false, isSelfClosing: false, canHaveChildren: true },
|
|
479
|
+
{ name: 'svg', kind: 'svg', label: 'SVG Canvas', category: 'special', isInline: false, isSelfClosing: false, canHaveChildren: true },
|
|
480
|
+
{ name: 'spoilerbox', kind: 'spoilerbox', label: 'Spoiler Box', category: 'special', isInline: false, isSelfClosing: false, canHaveChildren: true, toolbar: { group: 'special', label: 'Spoiler Box', icon: 'ChevronDown' } },
|
|
481
|
+
{ name: 'box', kind: 'box', label: 'Box', category: 'special', isInline: false, isSelfClosing: false, canHaveChildren: true },
|
|
482
|
+
{ name: 'boxw', kind: 'boxw', label: 'Box (líneas)', category: 'special', isInline: false, isSelfClosing: false, canHaveChildren: true },
|
|
483
|
+
{ name: 'list', kind: 'list', label: 'List', category: 'special', isInline: false, isSelfClosing: false, canHaveChildren: true },
|
|
484
|
+
{ name: '*', kind: 'list_item', label: 'List Item', category: 'special', isInline: false, isSelfClosing: true, canHaveChildren: false },
|
|
485
|
+
{ name: 'url', kind: 'url', label: 'URL', category: 'special', isInline: true, isSelfClosing: false, canHaveChildren: false },
|
|
486
|
+
{ name: 'email', kind: 'email', label: 'Email', category: 'special', isInline: true, isSelfClosing: false, canHaveChildren: false },
|
|
487
|
+
{ name: 'profile', kind: 'profile', label: 'Profile', category: 'special', isInline: true, isSelfClosing: false, canHaveChildren: false },
|
|
488
|
+
{ name: 'group', kind: 'group', label: 'Group', category: 'layout', isInline: false, isSelfClosing: false, canHaveChildren: true },
|
|
489
|
+
]
|
|
490
|
+
|
|
491
|
+
for (const tag of tags) {
|
|
492
|
+
this.register(tag)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { DocumentModel } from './DocumentModel'
|
|
2
|
+
export type { DocumentModelOptions } from './DocumentModel'
|
|
3
|
+
export { TagRegistry } from './TagRegistry'
|
|
4
|
+
export type { TagDefinition, TagHandler, PropertyDefinition, TagHandlerContext } from './TagRegistry'
|
|
5
|
+
export { NodeFactory } from './NodeFactory'
|