@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.
Files changed (172) hide show
  1. package/LICENSE +119 -0
  2. package/README.md +45 -0
  3. package/dist/Visuals/lyne.css +1027 -0
  4. package/dist/Visuals/osu.css +257 -0
  5. package/dist/index.d.mts +4563 -0
  6. package/dist/index.d.ts +4563 -0
  7. package/dist/index.js +11291 -0
  8. package/dist/index.mjs +11197 -0
  9. package/package.json +51 -0
  10. package/src/Analysis/Contracts/AnalysisReport.ts +26 -0
  11. package/src/Analysis/Contracts/Contribution.ts +76 -0
  12. package/src/Analysis/Contracts/Pass.ts +76 -0
  13. package/src/Analysis/Contracts/PipelineContext.ts +48 -0
  14. package/src/Analysis/Passes/Analysis/GradientAnalyzer.ts +292 -0
  15. package/src/Analysis/Passes/Analysis/MergeableColorAnalyzer.ts +112 -0
  16. package/src/Analysis/Passes/Analysis/RainbowAnalyzer.ts +233 -0
  17. package/src/Analysis/Passes/Analysis/WaveAnalyzer.ts +211 -0
  18. package/src/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.ts +135 -0
  19. package/src/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.ts +84 -0
  20. package/src/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.ts +99 -0
  21. package/src/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.ts +119 -0
  22. package/src/Analysis/Passes/Decision/DefaultDecision.ts +139 -0
  23. package/src/Analysis/Passes/Decision/__tests__/DefaultDecision.test.ts +179 -0
  24. package/src/Analysis/Passes/Transform/CollapseGradientTransform.ts +176 -0
  25. package/src/Analysis/Passes/Transform/MergeColorsTransform.ts +126 -0
  26. package/src/Analysis/Passes/Transform/RainbowCollapseTransform.ts +83 -0
  27. package/src/Analysis/Passes/Transform/WaveCollapseTransform.ts +88 -0
  28. package/src/Analysis/Passes/Utility/CharacterCountAnalyzer.ts +45 -0
  29. package/src/Analysis/Pipeline/Pipeline.ts +133 -0
  30. package/src/Analysis/Pipeline/PipelineBuilder.ts +55 -0
  31. package/src/Analysis/Pipeline/PipelineStage.ts +19 -0
  32. package/src/Analysis/Utils/color-utils.ts +132 -0
  33. package/src/Analysis/__tests__/Integration.test.ts +162 -0
  34. package/src/Analysis/__tests__/Pipeline.test.ts +133 -0
  35. package/src/Analysis/index.ts +52 -0
  36. package/src/BBCode/BBCodeDocumentModel.ts +175 -0
  37. package/src/BBCode/BBCodeToGreenNode.ts +755 -0
  38. package/src/BBCode/Parser.ts +384 -0
  39. package/src/BBCode/index.ts +12 -0
  40. package/src/Collab/positions.ts +91 -0
  41. package/src/Commands/Command.ts +44 -0
  42. package/src/Commands/CommandRegistry.ts +78 -0
  43. package/src/Commands/DeleteNode.ts +20 -0
  44. package/src/Commands/InsertText.ts +21 -0
  45. package/src/Commands/SplitMerge.ts +28 -0
  46. package/src/Commands/WrapInTag.ts +21 -0
  47. package/src/Commands/index.ts +6 -0
  48. package/src/Diff/TreeDiffer.ts +264 -0
  49. package/src/Diff/__tests__/TreeDiffer.test.ts +65 -0
  50. package/src/Diff/index.ts +2 -0
  51. package/src/Events/EventBus.ts +160 -0
  52. package/src/Events/index.ts +2 -0
  53. package/src/Formatter/Formatter.ts +54 -0
  54. package/src/Formatter/index.ts +2 -0
  55. package/src/HTML/HTMLDocumentModel.ts +35 -0
  56. package/src/HTML/HTMLToGreenNode.ts +290 -0
  57. package/src/Incremental/ChangeTracker.ts +105 -0
  58. package/src/Incremental/IncrementalParser.ts +591 -0
  59. package/src/Incremental/__tests__/IncrementalParser.test.ts +164 -0
  60. package/src/Incremental/index.ts +4 -0
  61. package/src/Lexer/BBCodeLexer.ts +382 -0
  62. package/src/Lexer/Lexer.ts +181 -0
  63. package/src/Lexer/index.ts +10 -0
  64. package/src/Linter/Linter.ts +193 -0
  65. package/src/Linter/index.ts +2 -0
  66. package/src/Markdown/MarkdownAST.ts +112 -0
  67. package/src/Markdown/MarkdownDocumentModel.ts +55 -0
  68. package/src/Markdown/MarkdownLexer.ts +203 -0
  69. package/src/Markdown/MarkdownParser.ts +455 -0
  70. package/src/Markdown/MarkdownToGreenNode.ts +153 -0
  71. package/src/Model/DocumentModel.ts +694 -0
  72. package/src/Model/NodeFactory.ts +117 -0
  73. package/src/Model/TagRegistry.ts +495 -0
  74. package/src/Model/index.ts +5 -0
  75. package/src/Plugins/PluginAPI.ts +119 -0
  76. package/src/Plugins/PluginRegistry.ts +132 -0
  77. package/src/Plugins/index.ts +3 -0
  78. package/src/Queries/QueryEngine.ts +152 -0
  79. package/src/Queries/index.ts +1 -0
  80. package/src/RenderPipeline/RenderPipeline.ts +125 -0
  81. package/src/RenderPipeline/RenderTree.ts +134 -0
  82. package/src/RenderPipeline/index.ts +4 -0
  83. package/src/Semantic/SemanticAnalyzer.ts +506 -0
  84. package/src/Semantic/index.ts +2 -0
  85. package/src/Symbols/SymbolTable.ts +124 -0
  86. package/src/Symbols/index.ts +1 -0
  87. package/src/Syntax/GreenNode.ts +324 -0
  88. package/src/Syntax/GreenNodePool.ts +269 -0
  89. package/src/Syntax/NodeMatcher.ts +370 -0
  90. package/src/Syntax/RedNode.ts +569 -0
  91. package/src/Syntax/RedNodeStore.ts +184 -0
  92. package/src/Syntax/TreeBuilder.ts +214 -0
  93. package/src/Syntax/__tests__/GreenNode.test.ts +33 -0
  94. package/src/Syntax/__tests__/RedNode.test.ts +81 -0
  95. package/src/Syntax/__tests__/RedNodeStore.test.ts +104 -0
  96. package/src/Syntax/greenEdit.ts +110 -0
  97. package/src/Syntax/hash.ts +30 -0
  98. package/src/Syntax/index.ts +12 -0
  99. package/src/Syntax/partition.ts +161 -0
  100. package/src/Syntax/preserveNodeIds.ts +201 -0
  101. package/src/Tests/ASTOptimizerIdempotence.test.ts +77 -0
  102. package/src/Tests/BlockPatcher.test.ts +437 -0
  103. package/src/Tests/BlockPatcherWindowed.test.ts +364 -0
  104. package/src/Tests/BoxDrawer.test.ts +217 -0
  105. package/src/Tests/BoxRichTitle.test.ts +105 -0
  106. package/src/Tests/Chars500kBenchmark.test.ts +151 -0
  107. package/src/Tests/Chars500kEdits.test.ts +321 -0
  108. package/src/Tests/CollabPositions.test.ts +146 -0
  109. package/src/Tests/CompilerPathProfiling.test.ts +186 -0
  110. package/src/Tests/DOMMorpher.test.ts +142 -0
  111. package/src/Tests/DomPatchPerf.test.ts +60 -0
  112. package/src/Tests/EffectSegments.snapshot.json +616 -0
  113. package/src/Tests/EffectSegments.test.ts +68 -0
  114. package/src/Tests/FindNodeAtOffset.test.ts +65 -0
  115. package/src/Tests/Fuzzer.test.ts +166 -0
  116. package/src/Tests/GreenNodePool.test.ts +153 -0
  117. package/src/Tests/Lexer.test.ts +238 -0
  118. package/src/Tests/LyneMode.test.ts +187 -0
  119. package/src/Tests/ModelCoherence.test.ts +180 -0
  120. package/src/Tests/Partition.test.ts +238 -0
  121. package/src/Tests/PluginTags.test.ts +150 -0
  122. package/src/Tests/ProblematicSection.test.ts +46 -0
  123. package/src/Tests/ProblematicSectionHTML.test.ts +58 -0
  124. package/src/Tests/RedReuse.test.ts +134 -0
  125. package/src/Tests/ReproDelete20k.test.ts +62 -0
  126. package/src/Tests/SemanticValidators.test.ts +136 -0
  127. package/src/Tests/StableNodeIds.test.ts +210 -0
  128. package/src/Tests/StudioColorBloat.test.ts +25 -0
  129. package/src/Tests/StudioDebugText.test.ts +27 -0
  130. package/src/Tests/StudioTrailingChar.test.ts +25 -0
  131. package/src/Tests/StudioValidText.test.ts +25 -0
  132. package/src/Tests/UrlImgBug.test.ts +23 -0
  133. package/src/Tests/VisualBuilderFidelity.test.ts +105 -0
  134. package/src/Tests/referenceDocument.ts +119 -0
  135. package/src/Transactions/Transaction.ts +176 -0
  136. package/src/Transactions/UndoManager.ts +111 -0
  137. package/src/Transactions/index.ts +3 -0
  138. package/src/Transformers/ASTOptimizer.ts +315 -0
  139. package/src/Transformers/GradientTransformer.ts +143 -0
  140. package/src/Transformers/GrowTransformer.ts +115 -0
  141. package/src/Transformers/RainbowTransformer.ts +121 -0
  142. package/src/Transformers/SineWaveTransformer.ts +130 -0
  143. package/src/Transformers/Transformer.ts +22 -0
  144. package/src/Types/core.ts +270 -0
  145. package/src/Types/diagnostics.ts +156 -0
  146. package/src/Types/index.ts +23 -0
  147. package/src/Types/operations.ts +180 -0
  148. package/src/Types/queries.ts +121 -0
  149. package/src/Types/symbols.ts +69 -0
  150. package/src/Types/tokens.ts +186 -0
  151. package/src/Utils/BBCodeGenerator.ts +126 -0
  152. package/src/Utils/ColorMath.ts +276 -0
  153. package/src/Utils/color.ts +112 -0
  154. package/src/Utils/dom-to-svg.test.ts +86 -0
  155. package/src/Utils/dom-to-svg.ts +615 -0
  156. package/src/Utils/treeTransformers.ts +717 -0
  157. package/src/Visitors/BBBlocksExporter.ts +69 -0
  158. package/src/Visitors/BBCodeExporter.ts +318 -0
  159. package/src/Visitors/BlockPatcher.ts +963 -0
  160. package/src/Visitors/DOMMorpher.ts +134 -0
  161. package/src/Visitors/HTMLRenderer.ts +1077 -0
  162. package/src/Visitors/JSONExporter.ts +66 -0
  163. package/src/Visitors/MarkdownExporter.ts +99 -0
  164. package/src/Visitors/SVGRenderer.ts +35 -0
  165. package/src/Visitors/TiptapExporter.ts +145 -0
  166. package/src/Visitors/Visitor.ts +48 -0
  167. package/src/Visitors/index.ts +9 -0
  168. package/src/Visuals/BoxDrawer.ts +175 -0
  169. package/src/Visuals/index.ts +42 -0
  170. package/src/Visuals/lyne.css +1027 -0
  171. package/src/Visuals/osu.css +257 -0
  172. package/src/index.ts +197 -0
@@ -0,0 +1,66 @@
1
+ /**
2
+ * DocumentEngine — JSONExporter
3
+ *
4
+ * Exports the Document Model to JSON.
5
+ * Useful for:
6
+ * - Saving/loading documents in a structured format
7
+ * - Sending over the wire (collaboration)
8
+ * - Debugging and inspecting the document tree
9
+ * - Import/export between different tools
10
+ */
11
+
12
+ import { RedNode } from '../Syntax/RedNode'
13
+ import { Visitor } from './Visitor'
14
+
15
+ export interface JSONDocument {
16
+ version: number
17
+ kind: string
18
+ nodes: JSONNode[]
19
+ metadata?: Record<string, unknown>
20
+ }
21
+
22
+ export interface JSONNode {
23
+ id: string
24
+ kind: string
25
+ text: string
26
+ version: number
27
+ children: JSONNode[]
28
+ attributes?: Record<string, unknown>
29
+ metadata?: Record<string, unknown>
30
+ range?: { start: number; end: number }
31
+ }
32
+
33
+ export class JSONExporter extends Visitor<string> {
34
+ /**
35
+ * Export a RedNode tree to a JSON string.
36
+ */
37
+ visit(node: RedNode): string {
38
+ return JSON.stringify(this.toJSON(node), null, 2)
39
+ }
40
+
41
+ /**
42
+ * Export to a JSON object.
43
+ */
44
+ toJSON(node: RedNode): JSONDocument {
45
+ return {
46
+ version: node.version,
47
+ kind: node.kind,
48
+ nodes: node.children.map(c => this.serializeNode(c)),
49
+ }
50
+ }
51
+
52
+ private serializeNode(node: RedNode): JSONNode {
53
+ return {
54
+ id: node.id,
55
+ kind: node.kind,
56
+ text: node.text,
57
+ version: node.version,
58
+ children: node.children.map(c => this.serializeNode(c)),
59
+ attributes: node.metadata?.tagName
60
+ ? { tag: node.metadata.tagName as string }
61
+ : undefined,
62
+ metadata: Object.keys(node.metadata).length > 0 ? node.metadata : undefined,
63
+ range: { start: node.range.start, end: node.range.end },
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * DocumentEngine — MarkdownExporter
3
+ *
4
+ * Exports the Document Model to Markdown.
5
+ * Demonstrates the format-agnostic power of the engine:
6
+ * BBCode in → DocumentModel → Markdown out.
7
+ *
8
+ * A single visitor, no parser changes needed.
9
+ */
10
+
11
+ import { RedNode } from '../Syntax/RedNode'
12
+ import { Visitor } from './Visitor'
13
+
14
+ export class MarkdownExporter extends Visitor<string> {
15
+ visit(node: RedNode): string {
16
+ return this.exportNode(node).replace(/(?:\r?\n){3,}/g, '\n\n')
17
+ }
18
+
19
+ export(node: RedNode): string {
20
+ return this.visit(node)
21
+ }
22
+
23
+ private exportNode(node: RedNode, depth: number = 0): string {
24
+ if (node.children.length === 0 && node.kind === 'text') {
25
+ return node.text
26
+ }
27
+
28
+ if (node.kind === 'document') {
29
+ return node.children.map(c => this.exportNode(c, depth)).join('')
30
+ }
31
+
32
+ const content = node.children.map(c => this.exportNode(c, depth)).join('')
33
+
34
+ switch (node.kind) {
35
+ case 'bold': return `**${content}**`
36
+ case 'italic': return `*${content}*`
37
+ case 'underline': return content
38
+ case 'strikethrough': return `~~${content}~~`
39
+ case 'inline_code': return `\`${content}\``
40
+ case 'code': return `\`\`\`\n${content}\n\`\`\``
41
+ case 'spoiler': return `||${content}||`
42
+ case 'color': return content
43
+ case 'font_size': return content
44
+ case 'font': return content
45
+ case 'heading': return `${'#'.repeat(Math.min(depth + 1, 6))} ${content}`
46
+ case 'center': return content
47
+ case 'right': return content
48
+ case 'left': return content
49
+ case 'url': return `[${content}](${this.extractValue(node) || content})`
50
+ case 'email': return `[${content}](mailto:${this.extractValue(node) || content})`
51
+ case 'profile': return `[${content}](https://osu.ppy.sh/users/${this.extractValue(node) || content})`
52
+ case 'image': return `![Image](${node.text || ''})`
53
+ case 'video': return `[🎥 YouTube Video](https://youtube.com/watch?v=${node.text || ''})`
54
+ case 'audio': return `[🎵 Audio](${node.text || ''})`
55
+ case 'quote': return `> **${this.extractValue(node) || 'Quote'}**\n> ${content.replace(/\\n/g, '\n> ')}`
56
+ case 'notice': return `> [!NOTE]\n> ${content.replace(/\\n/g, '\n> ')}`
57
+ case 'spoilerbox': return `**${this.renderTitleMarkdown(node) || 'Spoiler'}**\n${content}`
58
+ case 'box':
59
+ case 'boxw': return `**${this.renderTitleMarkdown(node) || 'Box'}**\n${content}`
60
+ case 'list': return content
61
+ case 'list_item': return `${' '.repeat(Math.max(0, depth - 1))}- ${content.trim()}`
62
+ case 'spacing': return '\n'
63
+ case 'empty_line': return '\n'
64
+ case 'text':
65
+ default: return content || node.text || ''
66
+ }
67
+ }
68
+
69
+ private renderTitleMarkdown(node: RedNode): string {
70
+ const titleNodes = node.metadata?.titleNodes as RedNode[] | undefined
71
+ if (titleNodes && titleNodes.length > 0) {
72
+ return titleNodes.map(c => this.exportNode(c, 0)).join('')
73
+ }
74
+ return (node.metadata?.title as string) || this.extractValue(node) || ''
75
+ }
76
+
77
+ /**
78
+ * Read a BBCode tag's `=VALUE` attribute.
79
+ *
80
+ * This previously read `node.properties` / `node.attributes`, neither of
81
+ * which exists on RedNode — so it silently returned `undefined` for every
82
+ * node and every call site fell through to its placeholder. That is why
83
+ * Markdown export dropped link targets and quote/box titles.
84
+ */
85
+ private extractValue(node: RedNode): string | undefined {
86
+ const text = node.text || ''
87
+ const eqIdx = text.indexOf('=')
88
+ if (eqIdx < 0) return undefined
89
+
90
+ let value = text.slice(eqIdx + 1)
91
+ if (
92
+ (value.startsWith('"') && value.endsWith('"')) ||
93
+ (value.startsWith("'") && value.endsWith("'"))
94
+ ) {
95
+ value = value.slice(1, -1)
96
+ }
97
+ return value || undefined
98
+ }
99
+ }
@@ -0,0 +1,35 @@
1
+ import { RedNode } from '../Syntax/RedNode'
2
+ import { Visitor } from './Visitor'
3
+ import { HTMLRenderer } from './HTMLRenderer'
4
+ import type { VisitorContext } from './Visitor'
5
+
6
+ /**
7
+ * SVGRenderer (Runtime ForeignObject Strategy)
8
+ *
9
+ * Instead of attempting to calculate CSS layouts manually,
10
+ * this runtime renderer wraps the exact HTML output in an SVG <foreignObject>.
11
+ *
12
+ * This gives 100% precision with zero DOM scanning overhead for real-time previews.
13
+ */
14
+ export class SVGRenderer extends Visitor<string> {
15
+ private htmlRenderer = new HTMLRenderer()
16
+
17
+ visit(node: RedNode, context?: VisitorContext): string {
18
+ if (context) this.context = context
19
+ return this.render(node)
20
+ }
21
+
22
+ render(root: RedNode): string {
23
+ const htmlContent = this.htmlRenderer.render(root)
24
+
25
+ // We wrap the HTML in a foreignObject. The container gets the standard BBCode preview classes
26
+ // so it inherits all global CSS (osu.css, tailwind, etc.) perfectly.
27
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%">
28
+ <foreignObject width="100%" height="100%">
29
+ <div xmlns="http://www.w3.org/1999/xhtml" class="bb-preview bbcode-preview text-sm leading-relaxed text-[#E8E0E4] wrap-break-word min-w-0 w-full h-full p-4 box-border">
30
+ ${htmlContent}
31
+ </div>
32
+ </foreignObject>
33
+ </svg>`
34
+ }
35
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * DocumentEngine — TiptapExporter
3
+ *
4
+ * Exports the Document Model to Tiptap (ProseMirror) JSON format.
5
+ * This proves how easily Quasar's AST bridges to visual WYSIWYG editors.
6
+ *
7
+ * Tiptap uses a JSON format structured like:
8
+ * {
9
+ * type: "doc",
10
+ * content: [
11
+ * {
12
+ * type: "paragraph",
13
+ * content: [
14
+ * { type: "text", text: "Hello ", marks: [{ type: "bold" }] }
15
+ * ]
16
+ * }
17
+ * ]
18
+ * }
19
+ */
20
+
21
+ import { RedNode } from '../Syntax/RedNode'
22
+ import { Visitor } from './Visitor'
23
+
24
+ export interface TiptapNode {
25
+ type: string
26
+ attrs?: Record<string, any>
27
+ content?: TiptapNode[]
28
+ marks?: TiptapMark[]
29
+ text?: string
30
+ }
31
+
32
+ export interface TiptapMark {
33
+ type: string
34
+ attrs?: Record<string, any>
35
+ }
36
+
37
+ export class TiptapExporter extends Visitor<string> {
38
+ visit(node: RedNode): string {
39
+ return JSON.stringify(this.toTiptap(node), null, 2)
40
+ }
41
+
42
+ toTiptap(node: RedNode): TiptapNode {
43
+ if (node.kind === 'document') {
44
+ return {
45
+ type: 'doc',
46
+ content: this.processChildren(node.children, []),
47
+ }
48
+ }
49
+ return this.processNode(node, [])
50
+ }
51
+
52
+ private processChildren(children: RedNode[], marks: TiptapMark[]): TiptapNode[] {
53
+ const content: TiptapNode[] = []
54
+
55
+ for (const child of children) {
56
+ // empty_line and spacing become empty paragraphs or brs depending on context
57
+ if (child.kind === 'empty_line' || child.kind === 'spacing') {
58
+ continue // Simplification: in a real bridge, these would divide paragraphs
59
+ }
60
+ content.push(this.processNode(child, marks))
61
+ }
62
+
63
+ return content
64
+ }
65
+
66
+ private processNode(node: RedNode, activeMarks: TiptapMark[]): TiptapNode {
67
+ // 1. Check if this node is a mark (inline formatting)
68
+ const newMark = this.kindToMark(node)
69
+ if (newMark) {
70
+ const nextMarks = [...activeMarks, newMark]
71
+ // Inline tags shouldn't exist as blocks in Tiptap, they just wrap text
72
+ // We return a text node if it's the leaf, otherwise we flatten children
73
+ if (node.children.length === 1 && node.children[0].kind === 'text') {
74
+ return {
75
+ type: 'text',
76
+ text: node.children[0].text,
77
+ marks: nextMarks
78
+ }
79
+ } else if (node.children.length === 0) {
80
+ return { type: 'text', text: '', marks: nextMarks }
81
+ }
82
+
83
+ // Simplification: if multiple children, we wrap them all in this mark
84
+ // Tiptap actually expects a flat list of text nodes with marks.
85
+ // So this requires flattening. For MVP, we return a custom block or flat text.
86
+ // We will just create a custom inline block for now.
87
+ return {
88
+ type: 'text',
89
+ text: node.children.map(c => c.text || '').join(''),
90
+ marks: nextMarks
91
+ }
92
+ }
93
+
94
+ // 2. Leaf Text Node
95
+ if (node.kind === 'text') {
96
+ return {
97
+ type: 'text',
98
+ text: node.text,
99
+ marks: activeMarks.length > 0 ? activeMarks : undefined
100
+ }
101
+ }
102
+
103
+ // 3. Block Nodes
104
+ return {
105
+ type: this.kindToBlockType(node.kind),
106
+ attrs: Object.keys(node.metadata).length > 0 ? node.metadata : undefined,
107
+ content: node.children.length > 0 ? this.processChildren(node.children, activeMarks) : undefined
108
+ }
109
+ }
110
+
111
+ private kindToMark(node: RedNode): TiptapMark | null {
112
+ switch (node.kind) {
113
+ case 'bold': return { type: 'bold' }
114
+ case 'italic': return { type: 'italic' }
115
+ case 'underline': return { type: 'underline' }
116
+ case 'strikethrough': return { type: 'strike' }
117
+ case 'inline_code': return { type: 'code' }
118
+ case 'color': return { type: 'textStyle', attrs: { color: node.metadata.color } }
119
+ case 'url': return { type: 'link', attrs: { href: node.metadata.href } }
120
+ case 'gradient':
121
+ case 'rainbow':
122
+ case 'grow':
123
+ case 'sinewave':
124
+ return { type: 'miliastryEffect', attrs: { effect: node.kind, ...node.metadata } }
125
+ }
126
+ return null
127
+ }
128
+
129
+ private kindToBlockType(kind: string): string {
130
+ const map: Record<string, string> = {
131
+ heading: 'heading',
132
+ paragraph: 'paragraph',
133
+ quote: 'blockquote',
134
+ code: 'codeBlock',
135
+ list: 'bulletList',
136
+ list_item: 'listItem',
137
+ image: 'image',
138
+ center: 'centerBlock',
139
+ notice: 'noticeBlock',
140
+ box: 'boxBlock',
141
+ spoilerbox: 'spoilerboxBlock'
142
+ }
143
+ return map[kind] ?? 'paragraph'
144
+ }
145
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * DocumentEngine — Visitor
3
+ *
4
+ * Base visitor pattern for traversing the Red Tree.
5
+ * All exporters and renderers extend this.
6
+ *
7
+ * Instead of switch statements on tag names, use the visitor pattern.
8
+ * New languages/exporters don't modify existing code.
9
+ *
10
+ * Inspired by SwiftSyntax's Visitor pattern.
11
+ */
12
+
13
+ import { RedNode } from '../Syntax/RedNode'
14
+
15
+ export interface VisitorContext {
16
+ source: string
17
+ options: Record<string, unknown>
18
+ }
19
+
20
+ export abstract class Visitor<T = string> {
21
+ protected context: VisitorContext = { source: '', options: {} }
22
+
23
+ /**
24
+ * Visit a node and produce an output value.
25
+ */
26
+ abstract visit(node: RedNode, context?: VisitorContext): T
27
+
28
+ /**
29
+ * Visit all children and combine results.
30
+ */
31
+ visitChildren(node: RedNode, separator: string = ''): T[] {
32
+ return node.children.map(child => this.visit(child))
33
+ }
34
+
35
+ /**
36
+ * Get the text content of a node.
37
+ */
38
+ getText(node: RedNode): string {
39
+ return node.text || node.children.map(c => c.text).join('')
40
+ }
41
+
42
+ /**
43
+ * Set the visitor context.
44
+ */
45
+ setContext(context: VisitorContext): void {
46
+ this.context = context
47
+ }
48
+ }
@@ -0,0 +1,9 @@
1
+ export { Visitor } from './Visitor'
2
+ export type { VisitorContext } from './Visitor'
3
+ export { BBCodeExporter } from './BBCodeExporter'
4
+ export { HTMLRenderer } from './HTMLRenderer'
5
+ export { morphHTML } from './DOMMorpher'
6
+ export { MarkdownExporter } from './MarkdownExporter'
7
+ export { JSONExporter } from './JSONExporter'
8
+ export { SVGRenderer } from './SVGRenderer'
9
+ export type { JSONDocument, JSONNode } from './JSONExporter'
@@ -0,0 +1,175 @@
1
+ /**
2
+ * DocumentEngine — BoxDrawer
3
+ *
4
+ * Drawer-style open/close animation for the `<details>` elements that
5
+ * HTMLRenderer emits for `[box]` and `[spoilerbox]`. The box grows downwards
6
+ * and progressively uncovers its content, which stays anchored under the
7
+ * summary instead of popping in all at once.
8
+ *
9
+ * This lives in Quasar rather than in a consumer because Quasar is what emits
10
+ * the `<details>` in the first place — the same reason `morphHTML` lives here.
11
+ * Any host that renders Quasar output gets the behaviour by binding it once.
12
+ *
13
+ * Why not CSS: while a `<details>` is closed its content is not rendered, so
14
+ * there is no height to interpolate between. `::details-content` combined with
15
+ * `interpolate-size: allow-keywords` would solve it, but it is Chromium-only in
16
+ * practice and hosts running on WebKit would silently lose the animation.
17
+ * Measuring the real height and driving it with the Web Animations API works on
18
+ * every engine.
19
+ *
20
+ * Heights are measured, not computed from summary + padding + borders, because
21
+ * that arithmetic breaks as soon as a theme puts margins on the summary or the
22
+ * boxes are nested. Toggling `open` and reading the rect yields the exact
23
+ * height the element will settle at, whatever the stylesheet does.
24
+ */
25
+
26
+ export interface BoxDrawerOptions {
27
+ /** Length of a full open or close, in milliseconds. Partial travel is scaled down. */
28
+ durationMs?: number
29
+ /** CSS easing function. */
30
+ easing?: string
31
+ }
32
+
33
+ /** Time for the full collapsed↔expanded travel; shorter distances scale down. */
34
+ const DEFAULT_DURATION_MS = 400
35
+ /** Floor so an interruption near the end still reads as motion, not a snap. */
36
+ const MIN_DURATION_MS = 120
37
+ /** easeOutCubic — fast to start, settles softly, no overshoot. */
38
+ const DEFAULT_EASING = 'cubic-bezier(0.33, 1, 0.68, 1)'
39
+
40
+ interface DrawerState {
41
+ animation: Animation
42
+ /** Where the box is heading, which is what a new click has to reverse. */
43
+ opening: boolean
44
+ /**
45
+ * Inline `overflow` from before the FIRST animation of a chain. Carried
46
+ * across interruptions so a reversal restores the author's value and not the
47
+ * `hidden` that the previous animation had just installed.
48
+ */
49
+ restoreOverflow: string
50
+ }
51
+
52
+ /** In-flight state per element; a WeakMap so morphed-away nodes are freed. */
53
+ const running = new WeakMap<HTMLDetailsElement, DrawerState>()
54
+
55
+ function prefersReducedMotion(): boolean {
56
+ return typeof window !== 'undefined'
57
+ && typeof window.matchMedia === 'function'
58
+ && window.matchMedia('(prefers-reduced-motion: reduce)').matches
59
+ }
60
+
61
+ /**
62
+ * Animate one `<details>` to its opposite state, reversing cleanly when it is
63
+ * already mid-animation.
64
+ *
65
+ * Exported so hosts that toggle boxes programmatically (a keyboard command, an
66
+ * "expand all" action) go through the same animation as a click.
67
+ */
68
+ export function toggleBoxWithDrawer(
69
+ details: HTMLDetailsElement,
70
+ options: BoxDrawerOptions = {}
71
+ ): void {
72
+ if (typeof details.animate !== 'function' || prefersReducedMotion()) {
73
+ details.open = !details.open
74
+ return
75
+ }
76
+
77
+ // Height as painted right now — read BEFORE cancelling, so an interrupted
78
+ // animation resumes from where the eye last saw it instead of snapping.
79
+ const from = details.getBoundingClientRect().height
80
+
81
+ const previous = running.get(details)
82
+ const restoreOverflow = previous ? previous.restoreOverflow : details.style.overflow
83
+
84
+ // Direction comes from the running animation's intent, never from
85
+ // `details.open`: a closing box is kept open so its content stays visible, so
86
+ // reading the attribute would report "open" and close it a second time,
87
+ // making a mid-close click impossible to reverse.
88
+ const opening = previous ? !previous.opening : !details.open
89
+
90
+ previous?.animation.cancel()
91
+
92
+ // Both extremes are needed to scale the duration by the distance actually
93
+ // travelled. `open` is left true either way so the content stays visible
94
+ // while the box grows or shrinks over it.
95
+ let collapsed: number
96
+ let expanded: number
97
+ if (details.open) {
98
+ expanded = details.getBoundingClientRect().height
99
+ details.open = false
100
+ collapsed = details.getBoundingClientRect().height
101
+ details.open = true
102
+ } else {
103
+ collapsed = details.getBoundingClientRect().height
104
+ details.open = true
105
+ expanded = details.getBoundingClientRect().height
106
+ }
107
+
108
+ const to = opening ? expanded : collapsed
109
+ details.style.overflow = 'hidden'
110
+
111
+ // A reversal 90% of the way open only has 10% left to travel; running the
112
+ // full duration over it would crawl.
113
+ const fullTravel = expanded - collapsed
114
+ const ratio = fullTravel > 0 ? Math.min(Math.abs(to - from) / fullTravel, 1) : 1
115
+ const duration = Math.max(
116
+ (options.durationMs ?? DEFAULT_DURATION_MS) * ratio,
117
+ MIN_DURATION_MS
118
+ )
119
+
120
+ const animation = details.animate(
121
+ { height: [`${from}px`, `${to}px`] },
122
+ { duration, easing: options.easing ?? DEFAULT_EASING }
123
+ )
124
+ running.set(details, { animation, opening, restoreOverflow })
125
+
126
+ /**
127
+ * `finish` and `cancel` are dispatched asynchronously, so by the time either
128
+ * lands a newer toggle may already own the element. Without this guard the
129
+ * outgoing animation strips the incoming one's `overflow: hidden` mid-flight
130
+ * — content spilling out of a small box — or applies its own `open` and
131
+ * freezes the box shut.
132
+ */
133
+ const settle = (finished: boolean) => {
134
+ if (running.get(details)?.animation !== animation) return
135
+ running.delete(details)
136
+ if (finished) details.open = opening
137
+ details.style.overflow = restoreOverflow
138
+ }
139
+
140
+ animation.addEventListener('finish', () => settle(true))
141
+ animation.addEventListener('cancel', () => settle(false))
142
+ }
143
+
144
+ /**
145
+ * Intercept the native toggle of every box inside `root`.
146
+ *
147
+ * The listener is delegated rather than attached per box: hosts typically feed
148
+ * the preview through `morphHTML` on every keystroke, and per-element listeners
149
+ * would be torn down and re-attached continuously.
150
+ *
151
+ * @param root - container holding the rendered BBCode
152
+ * @returns disposer that removes the listener
153
+ */
154
+ export function bindBoxDrawer(
155
+ root: HTMLElement,
156
+ options: BoxDrawerOptions = {}
157
+ ): () => void {
158
+ const handleClick = (e: MouseEvent) => {
159
+ const target = e.target as HTMLElement | null
160
+ const summary = target?.closest('summary')
161
+ if (!summary) return
162
+
163
+ const details = summary.parentElement
164
+ if (!(details instanceof HTMLDetailsElement)) return
165
+ if (!root.contains(details)) return
166
+
167
+ // Enter/Space on the summary also arrive here as a click, so the keyboard
168
+ // is covered without a separate handler.
169
+ e.preventDefault()
170
+ toggleBoxWithDrawer(details, options)
171
+ }
172
+
173
+ root.addEventListener('click', handleClick)
174
+ return () => root.removeEventListener('click', handleClick)
175
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * DocumentEngine — Visuals
3
+ *
4
+ * Catálogo de estilos visuales por plataforma para la previsualización
5
+ * de BBCode. Cada plataforma puede tener su propia apariencia sin
6
+ * modificar el renderer HTML.
7
+ *
8
+ * Uso:
9
+ * // Importar como CSS directo (soportado por Next.js, Vite, Webpack):
10
+ * import 'MiliastryPlatform/DocumentEngine/Visuals/osu.css'
11
+ *
12
+ * // O desde un componente React:
13
+ * import 'MiliastryPlatform/DocumentEngine/Visuals/osu.css'
14
+ * // Luego en el JSX: <div className="bbcode-preview" ...
15
+ *
16
+ * Para cambiar de tema visual, solo cambia el import del CSS.
17
+ *
18
+ * Temas disponibles:
19
+ * - osu → Estilo visual inspirado en los foros de osu!
20
+ * - miliastry → Estilo por defecto de Miliastry (próximamente)
21
+ */
22
+
23
+ export { bindBoxDrawer, toggleBoxWithDrawer } from './BoxDrawer'
24
+ export type { BoxDrawerOptions } from './BoxDrawer'
25
+
26
+ export const visualThemes = [
27
+ {
28
+ id: 'osu',
29
+ name: 'osu! Forum Style',
30
+ description: 'Estilo visual inspirado en los foros de osu!',
31
+ cssFile: 'osu.css',
32
+ },
33
+ {
34
+ id: 'lyne',
35
+ name: 'Lyne Style',
36
+ description: 'Estilo visual cyberpunk con cortes a 45° inspirado en Lyne',
37
+ cssFile: 'lyne.css',
38
+ },
39
+ ] as const
40
+
41
+ export type VisualThemeId = (typeof visualThemes)[number]['id']
42
+