@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,210 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { BBCodeDocumentModel } from '../BBCode/BBCodeDocumentModel'
3
+ import { HTMLRenderer } from '../Visitors/HTMLRenderer'
4
+ import { morphHTML } from '../Visitors/DOMMorpher'
5
+ import type { RedNode } from '../Syntax/RedNode'
6
+
7
+ /**
8
+ * Node ids must survive a reparse for every node that did not change.
9
+ *
10
+ * Ids are embedded in the rendered HTML as `data-node-id` (the preview→Monaco
11
+ * click mapping reads them), so regenerating them on every parse made the HTML
12
+ * of *unchanged* subtrees differ between keystrokes — which defeated the
13
+ * DOMMorpher's isEqualNode fast path and rewrote DOM the user was looking at.
14
+ *
15
+ * The last test is the end-to-end payoff: after a one-character edit, the DOM
16
+ * elements of untouched content must keep their object identity through a
17
+ * morph, because their HTML is now byte-identical.
18
+ */
19
+
20
+ /** Map every text node's content to its id (text content identifies the leaf). */
21
+ function textIds(root: RedNode): Map<string, string> {
22
+ const out = new Map<string, string>()
23
+ root.walk(n => {
24
+ if (n.kind === 'text' && n.text.trim() !== '') out.set(n.text, String(n.id))
25
+ })
26
+ return out
27
+ }
28
+
29
+ function subtreeText(node: RedNode): string {
30
+ let out = ''
31
+ node.walk(n => { if (n.kind === 'text') out += n.text })
32
+ return out
33
+ }
34
+
35
+ function allIds(root: RedNode): string[] {
36
+ const out: string[] = []
37
+ root.walk(n => { out.push(String(n.id)) })
38
+ return out
39
+ }
40
+
41
+ function mulberry32(seed: number): () => number {
42
+ let a = seed >>> 0
43
+ return () => {
44
+ a |= 0; a = (a + 0x6D2B79F5) | 0
45
+ let t = Math.imul(a ^ (a >>> 15), 1 | a)
46
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
47
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
48
+ }
49
+ }
50
+
51
+ describe('stable node ids across reparses', () => {
52
+ it('an edit in the middle keeps the ids of untouched content (incremental path)', () => {
53
+ const model = new BBCodeDocumentModel({
54
+ source: 'uno\n\ndos\n\ntres\n\n[b]cuatro[/b]',
55
+ })
56
+ const before = textIds(model.redRoot!)
57
+
58
+ model.applyTextUpdate('uno\n\ndos EDITADO\n\ntres\n\n[b]cuatro[/b]')
59
+ const after = textIds(model.redRoot!)
60
+
61
+ expect(after.get('uno')).toBe(before.get('uno'))
62
+ expect(after.get('tres')).toBe(before.get('tres'))
63
+ expect(after.get('cuatro')).toBe(before.get('cuatro'))
64
+ })
65
+
66
+ it('typing at the end keeps every previous id', () => {
67
+ const model = new BBCodeDocumentModel({
68
+ source: 'uno\n\n[i]dos[/i]\n\ntres',
69
+ })
70
+ const before = textIds(model.redRoot!)
71
+
72
+ model.applyTextUpdate('uno\n\n[i]dos[/i]\n\ntres y algo mas')
73
+ const after = textIds(model.redRoot!)
74
+
75
+ expect(after.get('uno')).toBe(before.get('uno'))
76
+ expect(after.get('dos')).toBe(before.get('dos'))
77
+ })
78
+
79
+ it('a full rebuild also preserves ids, via hash matching', () => {
80
+ const model = new BBCodeDocumentModel({
81
+ source: 'uno\n\ndos\n\n[b]tres[/b]',
82
+ })
83
+ const before = textIds(model.redRoot!)
84
+
85
+ // rebuild() reparses everything: no green is shared by reference, so this
86
+ // exercises the hash-verified tier.
87
+ model.rebuild('uno\n\ndos!\n\n[b]tres[/b]')
88
+ const after = textIds(model.redRoot!)
89
+
90
+ expect(after.get('uno')).toBe(before.get('uno'))
91
+ expect(after.get('tres')).toBe(before.get('tres'))
92
+ })
93
+
94
+ it('ids stay unique through seeded random edit sequences', () => {
95
+ const rand = mulberry32(1234)
96
+ const alphabet = ['a', ' ', '\n', '[', ']', 'b', '[b]', '[/b]', '[i]', '[/i]', '[list]', '[*]']
97
+
98
+ for (let doc = 0; doc < 8; doc++) {
99
+ let source = ''
100
+ const parts = 40 + Math.floor(rand() * 120)
101
+ for (let i = 0; i < parts; i++) {
102
+ source += alphabet[Math.floor(rand() * alphabet.length)]
103
+ }
104
+ const model = new BBCodeDocumentModel({ source })
105
+
106
+ for (let edit = 0; edit < 25; edit++) {
107
+ const pos = Math.floor(rand() * (model.source.length + 1))
108
+ const insert = alphabet[Math.floor(rand() * alphabet.length)]
109
+ const next = rand() < 0.75
110
+ ? model.source.slice(0, pos) + insert + model.source.slice(pos)
111
+ : model.source.slice(0, pos) + model.source.slice(Math.min(pos + 1, model.source.length))
112
+ model.applyTextUpdate(next)
113
+
114
+ const ids = allIds(model.redRoot!)
115
+ expect(new Set(ids).size).toBe(ids.length)
116
+ expect(model.redRoot!.green).toBe(model.greenRoot)
117
+ }
118
+ }
119
+ })
120
+
121
+ it('inserting a block does not renumber the blocks that survive it', () => {
122
+ // Regression. Identity is carried either by red-subtree REUSE (the nodes
123
+ // are the previous objects) or by the id walk (every node is fresh) —
124
+ // never both. When both ran, the walk paired positionally, so inserting a
125
+ // block made each survivor pair with its NEIGHBOUR: it overwrote a live
126
+ // node's id with a different node's, which both duplicated ids and
127
+ // changed `data-node-id` on blocks that had not changed — defeating the
128
+ // morpher's fast path exactly when it matters most.
129
+ let src = ''
130
+ for (let i = 0; i < 40; i++) src += `[notice]bloque ${i}[/notice]\n\n`
131
+ const model = new BBCodeDocumentModel({ source: src })
132
+
133
+ const idOf = (root: RedNode, text: string): string | null => {
134
+ let found: string | null = null
135
+ root.walk(n => {
136
+ if (!found && n.kind === 'notice' && n.text === '' && subtreeText(n).includes(text)) {
137
+ found = String(n.id)
138
+ }
139
+ })
140
+ return found
141
+ }
142
+ const before10 = idOf(model.redRoot!, 'bloque 10')
143
+ const before39 = idOf(model.redRoot!, 'bloque 39')
144
+ expect(before10).toBeTruthy()
145
+
146
+ // Insert a whole new block near the start: everything after it shifts.
147
+ model.applyTextUpdate('[notice]NUEVO[/notice]\n\n' + src)
148
+
149
+ expect(idOf(model.redRoot!, 'bloque 10')).toBe(before10)
150
+ expect(idOf(model.redRoot!, 'bloque 39')).toBe(before39)
151
+
152
+ const ids = allIds(model.redRoot!)
153
+ expect(new Set(ids).size).toBe(ids.length)
154
+ })
155
+
156
+ it('end to end: untouched id-bearing blocks keep DOM identity through a morph', () => {
157
+ // Only block containers carry data-node-id (see HTMLRenderer.ID_BEARING_KINDS),
158
+ // so blocks are where id churn used to break the morpher's fast path.
159
+ const model = new BBCodeDocumentModel({
160
+ source: '[notice]uno[/notice]\n\n[quote]dos[/quote]\n\n[notice]tres[/notice]',
161
+ })
162
+ const renderer = new HTMLRenderer()
163
+ const el = document.createElement('div')
164
+ el.innerHTML = renderer.render(model.redRoot!)
165
+
166
+ const findBlock = (text: string): Element | null => {
167
+ for (const block of Array.from(el.querySelectorAll('[data-node-id]'))) {
168
+ if (block.textContent?.includes(text)) return block
169
+ }
170
+ return null
171
+ }
172
+
173
+ const unoBefore = findBlock('uno')
174
+ const tresBefore = findBlock('tres')
175
+ expect(unoBefore).toBeTruthy()
176
+ expect(tresBefore).toBeTruthy()
177
+ const unoId = unoBefore!.getAttribute('data-node-id')
178
+ const tresId = tresBefore!.getAttribute('data-node-id')
179
+
180
+ model.applyTextUpdate('[notice]uno[/notice]\n\n[quote]dos EDITADO[/quote]\n\n[notice]tres[/notice]')
181
+ morphHTML(el, renderer.render(model.redRoot!))
182
+
183
+ // Same DOM objects, not equivalent ones: their HTML — ids included — did
184
+ // not change, so the morpher's isEqualNode fast path skipped them.
185
+ expect(findBlock('uno')).toBe(unoBefore)
186
+ expect(findBlock('tres')).toBe(tresBefore)
187
+ expect(unoBefore!.getAttribute('data-node-id')).toBe(unoId)
188
+ expect(tresBefore!.getAttribute('data-node-id')).toBe(tresId)
189
+ // And the edited block is actually updated.
190
+ expect(findBlock('dos EDITADO')).toBeTruthy()
191
+ })
192
+
193
+ it('every data-node-id in fresh HTML resolves to a node in the current tree', () => {
194
+ const model = new BBCodeDocumentModel({
195
+ source: '[notice]uno[/notice]\n\n[list][*]dos[*]tres[/list]',
196
+ })
197
+ model.applyTextUpdate('[notice]uno![/notice]\n\n[list][*]dos[*]tres[/list]')
198
+
199
+ const html = new HTMLRenderer().render(model.redRoot!)
200
+ const el = document.createElement('div')
201
+ el.innerHTML = html
202
+
203
+ const withIds = Array.from(el.querySelectorAll('[data-node-id]'))
204
+ expect(withIds.length).toBeGreaterThan(0)
205
+ for (const node of withIds) {
206
+ const id = node.getAttribute('data-node-id')!
207
+ expect(model.findNode(id as never)).toBeTruthy()
208
+ }
209
+ })
210
+ })
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { TagRegistry } from '../Model/TagRegistry'
3
+ import { BBCodeExporter } from '../Visitors/BBCodeExporter'
4
+ import { processStudioAST } from '@miliastry/quasar-studio'
5
+
6
+ describe('Studio Effects Color Bloat', () => {
7
+ it('should remove redundant outer color tags when a gradient is applied', () => {
8
+ const text = "[color=#000000]Hello[/color]"
9
+
10
+ const layers = [
11
+ { id: 'grad', type: 'gradient', enabled: true, value: 100, properties: { color1: '#ff0000', color2: '#0000ff' }, colors: '#ff0000,#0000ff', opacity: 1.0, easing: 'linear' }
12
+ ]
13
+
14
+ const registry = new TagRegistry()
15
+ const { redRoot } = processStudioAST(text, layers as any, {})
16
+
17
+ const exporter = new BBCodeExporter(registry)
18
+ const exportedBBCode = exporter.export(redRoot)
19
+
20
+ // The output should NOT contain [color=#000000]
21
+ expect(exportedBBCode).not.toContain('#000000')
22
+ // It should ONLY contain the gradient colors
23
+ expect(exportedBBCode).toContain('#ff0000')
24
+ })
25
+ })
@@ -0,0 +1,27 @@
1
+ import { describe, it } from 'vitest'
2
+ import { TagRegistry } from '../Model/TagRegistry'
3
+ import { BBCodeExporter } from '../Visitors/BBCodeExporter'
4
+ import { processStudioAST } from '@miliastry/quasar-studio'
5
+
6
+ describe('Studio Effects Debug Text', () => {
7
+ it('should print exported BBCode for debugging', () => {
8
+ const text = "Hello World\nTest\n\nDouble newline."
9
+
10
+ const layers = [
11
+ { id: 'grad', type: 'gradient', active: true, value: 100, properties: { color1: '#ff0000', color2: '#0000ff' } }
12
+ ]
13
+
14
+ const registry = new TagRegistry()
15
+ const { redRoot } = processStudioAST(text, layers as any, {})
16
+
17
+ const exporter = new BBCodeExporter(registry)
18
+ const exportedBBCode = exporter.export(redRoot)
19
+
20
+ console.log("================================")
21
+ console.log("ORIGINAL TEXT:")
22
+ console.log(JSON.stringify(text))
23
+ console.log("EXPORTED BBCODE:")
24
+ console.log(JSON.stringify(exportedBBCode))
25
+ console.log("================================")
26
+ })
27
+ })
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { TagRegistry } from '../Model/TagRegistry'
3
+ import { BBCodeExporter } from '../Visitors/BBCodeExporter'
4
+ import { processStudioAST } from '@miliastry/quasar-studio'
5
+
6
+ describe('Studio Effects Trailing Char Bug', () => {
7
+ it('should not duplicate trailing braille blanks', () => {
8
+ const text = "[notice]Hello⠀[/notice]"
9
+
10
+ const layers = [
11
+ { id: 'grad', type: 'gradient', enabled: true, value: 100, properties: { color1: '#ff0000', color2: '#0000ff' }, colors: '#ff0000,#0000ff', opacity: 1.0, easing: 'linear' }
12
+ ]
13
+
14
+ const registry = new TagRegistry()
15
+ const { redRoot } = processStudioAST(text, layers as any, {})
16
+
17
+ const exporter = new BBCodeExporter(registry)
18
+ const exportedBBCode = exporter.export(redRoot)
19
+
20
+ console.log("EXPORTED:", JSON.stringify(exportedBBCode))
21
+
22
+ // Ensure we don't have an extra braille blank at the end
23
+ expect(exportedBBCode.match(/⠀/g)?.length).toBe(1)
24
+ })
25
+ })
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { TagRegistry } from '../Model/TagRegistry'
3
+ import { BBCodeExporter } from '../Visitors/BBCodeExporter'
4
+ import { processStudioAST } from '@miliastry/quasar-studio'
5
+
6
+ describe('Studio Effects Valid Structure', () => {
7
+ it('should not alter structure of valid bbcode', () => {
8
+ const text = "[notice]\n [box=Hello]\n [centre]Content[/centre]\n [/box]\n[/notice]"
9
+
10
+ const layers = [
11
+ { id: 'grad', type: 'gradient', active: true, value: 100, properties: { color1: '#ff0000', color2: '#0000ff' } }
12
+ ]
13
+
14
+ const registry = new TagRegistry()
15
+ const { redRoot } = processStudioAST(text, layers as any, {})
16
+
17
+ const exporter = new BBCodeExporter(registry)
18
+ const exportedBBCode = exporter.export(redRoot)
19
+
20
+ // Strip colors to verify exact match
21
+ const stripped = exportedBBCode.replace(/\[\/?color[^\]]*\]/g, '')
22
+
23
+ expect(stripped).toEqual(text)
24
+ })
25
+ })
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { TagRegistry } from '../Model/TagRegistry'
3
+ import { HTMLRenderer } from '../Visitors/HTMLRenderer'
4
+ import { processStudioAST } from '@miliastry/quasar-studio'
5
+ import { BBCodeExporter } from '../Visitors/BBCodeExporter'
6
+ import fs from 'fs'
7
+
8
+ describe('URL IMG bug', () => {
9
+ it('should render URL with IMG inside correctly', () => {
10
+ const text = '[notice][centre][url=https://osekai.net/profiles?user=11624101][img]https://osekai.net/profiles/img/banner.svg?id=11624101[/img][/url][/centre][/notice]'
11
+
12
+ const registry = new TagRegistry()
13
+ const htmlRenderer = new HTMLRenderer({ registry })
14
+ const exporter = new BBCodeExporter(registry)
15
+
16
+ const { redRoot } = processStudioAST(text, [], {})
17
+ const html = htmlRenderer.render(redRoot)
18
+ const exported = exporter.export(redRoot)
19
+
20
+ console.log('HTML:\n', html)
21
+ console.log('Exported BBCode:\n', exported)
22
+ })
23
+ })
@@ -0,0 +1,105 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { TagRegistry } from '../Model/TagRegistry'
3
+ import { HTMLRenderer } from '../Visitors/HTMLRenderer'
4
+ import { processStudioAST } from '@miliastry/quasar-studio'
5
+
6
+ /**
7
+ * Every tag the visual builder can produce must reach the HTML with the
8
+ * element the preview's stylesheet expects.
9
+ *
10
+ * This file used to hold a 6 KB hardcoded "expected HTML" that was never
11
+ * compared against anything — it was written to `visual-builder-expected.html`
12
+ * and `visual-builder-actual.html` in the repo root (tracked files, dirtied on
13
+ * every run) for a human to eyeball. The only assertions were that the output
14
+ * contained `class="bb-text"` and `data-node-id`. The first stopped being true
15
+ * when text leaves lost their wrapper span, which is what prompted writing
16
+ * real assertions instead.
17
+ */
18
+ describe('Visual Builder HTML Fidelity', () => {
19
+ const bbcode = `[heading]Quasar Engine Test[/heading]
20
+
21
+ [b]Bold[/b], [i]Italic[/i], [u]Underline[/u], [s]Strikethrough[/s]
22
+
23
+ [color=#61afef]Colored text[/color] and [size=150]Large text[/size]
24
+
25
+ [quote="Author"]This is a quote block with [b]formatting[/b] inside[/quote]
26
+
27
+ [code]
28
+ function hello() {
29
+ console.log("Hello World!")
30
+ }
31
+ [/code]
32
+
33
+ [list]
34
+ [*]Item one
35
+ [*]Item two
36
+ [*]Item three
37
+ [/list]
38
+
39
+ [centre][b]Centered content[/b][/centre]
40
+
41
+ [notice]This is an important notice![/notice]
42
+
43
+ [spoiler]Hidden content revealed on hover[/spoiler]
44
+
45
+ [url=https://osu.ppy.sh]osu! website[/url]
46
+
47
+ Esto
48
+
49
+ Y esto`
50
+
51
+ const render = () => {
52
+ const registry = new TagRegistry()
53
+ const renderer = new HTMLRenderer({ registry })
54
+ const { redRoot } = processStudioAST(bbcode, [], {})
55
+ return renderer.render(redRoot)
56
+ }
57
+
58
+ it('maps every tag to its expected element', () => {
59
+ const html = render()
60
+
61
+ const cases: Array<[string, RegExp]> = [
62
+ ['heading', /<h2[^>]*>Quasar Engine Test<\/h2>/],
63
+ ['bold', /<strong[^>]*>Bold<\/strong>/],
64
+ ['italic', /<em[^>]*>Italic<\/em>/],
65
+ ['underline', /<u[^>]*>Underline<\/u>/],
66
+ ['strikethrough', /<s[^>]*>Strikethrough<\/s>/],
67
+ ['color', /<span[^>]*style="color:#61afef;"[^>]*>Colored text<\/span>/],
68
+ ['size', /<span[^>]*style="font-size:150%;"[^>]*>Large text<\/span>/],
69
+ ['quote', /<blockquote[^>]*>/],
70
+ ['code', /<pre[^>]*><code>/],
71
+ ['list', /<ul[^>]*>/],
72
+ ['list item', /<li[^>]*>Item one/],
73
+ ['centre', /<div[^>]*style="text-align:center;"[^>]*>/],
74
+ ['notice', /<div[^>]*class="notice"[^>]*>/],
75
+ ['spoiler', /<span[^>]*class="spoiler"[^>]*>/],
76
+ ['url', /<a[^>]*href="https:\/\/osu\.ppy\.sh"[^>]*>osu! website<\/a>/],
77
+ ]
78
+
79
+ for (const [label, pattern] of cases) {
80
+ expect(pattern.test(html), `${label} — no encontrado en el HTML`).toBe(true)
81
+ }
82
+ })
83
+
84
+ it('keeps the quote author and the raw code content', () => {
85
+ const html = render()
86
+ expect(html).toContain('Author')
87
+ expect(html).toContain('function hello()')
88
+ // Code content is raw text, so its quotes must be escaped, not parsed.
89
+ expect(html).toContain('&quot;Hello World!&quot;')
90
+ })
91
+
92
+ it('carries data-node-id on blocks, so preview clicks map back to nodes', () => {
93
+ const html = render()
94
+ // Blocks are id-bearing; text leaves deliberately are not.
95
+ expect(html).toMatch(/<h2 data-node-id="[^"]+"/)
96
+ expect(html).toMatch(/<blockquote data-node-id="[^"]+"/)
97
+ expect(html).toMatch(/<div data-node-id="[^"]+" class="notice"/)
98
+ })
99
+
100
+ it('renders trailing prose after the last tag', () => {
101
+ const html = render()
102
+ expect(html).toContain('Esto')
103
+ expect(html).toContain('Y esto')
104
+ })
105
+ })
@@ -0,0 +1,119 @@
1
+ /**
2
+ * The reference document the heavier suites parse.
3
+ *
4
+ * This replaces two `.milia` fixtures that used to sit next to `src/` and were
5
+ * read with `fs.readFileSync`. They were never committed, so the moment they
6
+ * were gone seven tests failed with `ENOENT` and nothing in the repository
7
+ * could bring them back. A fixture a test cannot survive without belongs in the
8
+ * repository, and the cheapest way to guarantee that is to make it code.
9
+ *
10
+ * Honest limitation: the originals were real posts from the wild, ~12 KB each.
11
+ * This is a reconstruction. It reproduces the *shapes* that made those files
12
+ * worth parsing — the ones listed below — but it cannot reproduce the surprise
13
+ * of real user input. Anything found in a real document that this misses should
14
+ * be added here rather than kept in an untracked file.
15
+ *
16
+ * What it deliberately contains:
17
+ *
18
+ * - Deep nesting, and tags closed out of order by ordinary authors
19
+ * - Braille blanks (`⠀`) used as layout padding, which the exporter must not
20
+ * invent or drop
21
+ * - CRLF alongside LF, and runs of blank lines
22
+ * - Bracketed prose that is *not* a tag — `[90 misses]`, `[Gateron]` — which
23
+ * the parser renders literally and the validators must stay quiet about
24
+ * - Non-ASCII text and emoji, so offsets are exercised beyond one byte
25
+ * - Attribute values that themselves contain markup (`[box=[b]t[/b]]`)
26
+ * - Long stretches of plain text, so a gradient has characters to colour
27
+ *
28
+ * It must stay **valid**: `SemanticValidators` asserts zero errors and zero
29
+ * warnings on it, on the grounds that a checker crying wolf on an ordinary
30
+ * document is worse than a silent one. Adding a deprecated tag here would break
31
+ * that on purpose, so don't.
32
+ */
33
+
34
+ const SECTION_PAD = '⠀'.repeat(8)
35
+
36
+ export const REFERENCE_DOCUMENT = [
37
+ '[centre]',
38
+ `${SECTION_PAD}[size=150][b]perfil de ejemplo[/b][/size]${SECTION_PAD}`,
39
+ '[/centre]',
40
+ '',
41
+ '[box=[b]sobre mí[/b]]',
42
+ 'Llevo jugando desde 2017 y sigo fallando los mismos patrones.',
43
+ 'Mi mejor racha this season fue un [90 misses] limpio — sí, limpio.',
44
+ 'Teclado [Gateron] rojo, tableta pequeña, mucha paciencia. 🎵',
45
+ '[/box]',
46
+ '',
47
+ '[centre]',
48
+ // Author colours must stay OUTSIDE `REFERENCE_GRADIENT_COLORS`: the HTML
49
+ // suite proves a gradient's stops are absent without a gradient layer, and a
50
+ // collision here would make that assertion unprovable.
51
+ '[color=#7aa2f7]━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/color]',
52
+ '[/centre]',
53
+ '',
54
+ '[b]Cosas que me gustan[/b]',
55
+ '[list]',
56
+ '[*]Mapas de stream largos, aunque no los pase',
57
+ '[*]Skins minimalistas con hitsounds fuertes',
58
+ '[*]Los mapas de [i]Sotarks[/i] cuando está inspirado',
59
+ '[/list]',
60
+ '',
61
+ // Deliberate CRLF island: the lexer folds line endings and the partition
62
+ // invariant has to keep covering both bytes of a `\r\n`.
63
+ 'Una sección con saltos de Windows:\r\nsegunda línea\r\ntercera línea',
64
+ '',
65
+ '',
66
+ '',
67
+ '[quote]',
68
+ '"El ritmo no se piensa, se siente." — alguien en el chat, 3 AM',
69
+ '[/quote]',
70
+ '',
71
+ '[b][i]Texto anidado[/i] que cierra en otro orden[/b]',
72
+ '',
73
+ 'Un enlace normal: [url=https://osu.ppy.sh]mi perfil[/url]',
74
+ 'Y otro suelto: [url]https://osu.ppy.sh/beatmapsets[/url]',
75
+ '',
76
+ '[centre]',
77
+ `${SECTION_PAD}[color=#9ece6a]gracias por leer[/color]${SECTION_PAD}`,
78
+ '',
79
+ 'Texto largo para que un degradado tenga suficientes caracteres que colorear ',
80
+ 'y el render produzca un nodo por carácter sin quedarse corto en la prueba: ',
81
+ 'áéíóú ñ ü € — signos que ocupan más de un byte y mueven los offsets.',
82
+ '[/centre]',
83
+ '',
84
+ `${SECTION_PAD}`,
85
+ ].join('\n')
86
+
87
+ /**
88
+ * The same document under a Studio gradient layer.
89
+ *
90
+ * The second fixture was the first one after the visual builder had run over
91
+ * it, so the two differed in exactly this: colour spans wrapped around
92
+ * individual characters. Kept as a separate export because several suites parse
93
+ * both and a difference between them is the point.
94
+ */
95
+ export const REFERENCE_DOCUMENT_WITH_GRADIENT = [
96
+ '[centre]',
97
+ '[color=#e8b04b]p[/color][color=#e8ae4b]e[/color][color=#e7ad4b]r[/color]',
98
+ '[color=#e7aa4b]f[/color][color=#e7a84b]i[/color][color=#e6a64b]l[/color]',
99
+ '[/centre]',
100
+ '',
101
+ REFERENCE_DOCUMENT,
102
+ ].join('\n')
103
+
104
+ /** The gradient stops the Studio layer tests colour with. */
105
+ export const REFERENCE_GRADIENT_COLORS =
106
+ '#e8b04b,#e8ae4b,#e7ad4b,#e7aa4b,#e7a84b,#e6a64b,#e6a14c,#e59f4c,#e4994c,'
107
+ + '#e3914c,#e28b4d,#e1854d,#e07c4d,#df754d,#de6f4e,#dd684e,#dc614e,#db5b4f,#da564f'
108
+
109
+ /** The Studio layer shape those suites feed to `processStudioAST`. */
110
+ export const REFERENCE_GRADIENT_LAYER = {
111
+ id: 'grad',
112
+ type: 'gradient',
113
+ enabled: true,
114
+ value: 100,
115
+ properties: { color1: '#ff0000', color2: '#0000ff' },
116
+ colors: REFERENCE_GRADIENT_COLORS,
117
+ opacity: 1.0,
118
+ easing: 'linear',
119
+ }