@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,203 @@
1
+ /**
2
+ * DocumentEngine — MarkdownLexer
3
+ *
4
+ * Tokenizes standard Markdown into a flat token stream.
5
+ * Produces explicit newline tokens for the parser to make semantic
6
+ * decisions about paragraph breaks.
7
+ */
8
+
9
+ export interface MarkdownHashToken { kind: 'hash'; value: string; start: number; end: number }
10
+ export interface MarkdownGtToken { kind: 'gt'; start: number; end: number }
11
+ export interface MarkdownStarToken { kind: 'star'; value: string; start: number; end: number }
12
+ export interface MarkdownUnderscoreToken { kind: 'underscore'; value: string; start: number; end: number }
13
+ export interface MarkdownTildeToken { kind: 'tilde'; value: string; start: number; end: number }
14
+ export interface MarkdownBacktickToken { kind: 'backtick'; value: string; start: number; end: number }
15
+ export interface MarkdownBracketOpenToken { kind: 'bracket_open'; start: number; end: number }
16
+ export interface MarkdownBracketCloseToken { kind: 'bracket_close'; start: number; end: number }
17
+ export interface MarkdownParenOpenToken { kind: 'paren_open'; start: number; end: number }
18
+ export interface MarkdownParenCloseToken { kind: 'paren_close'; start: number; end: number }
19
+ export interface MarkdownBangToken { kind: 'bang'; start: number; end: number }
20
+ export interface MarkdownDashToken { kind: 'dash'; value: string; start: number; end: number }
21
+ export interface MarkdownPlusToken { kind: 'plus'; start: number; end: number }
22
+ export interface MarkdownDotToken { kind: 'dot'; start: number; end: number }
23
+ export interface MarkdownTextToken { kind: 'text'; value: string; start: number; end: number }
24
+ export interface MarkdownNewlineToken { kind: 'newline'; value: string; start: number; end: number }
25
+ export interface MarkdownFenceToken { kind: 'fence'; value: string; lang: string; start: number; end: number }
26
+ export interface MarkdownHrToken { kind: 'hr'; start: number; end: number }
27
+ export interface MarkdownEscapeToken { kind: 'escape'; value: string; start: number; end: number }
28
+ export interface MarkdownAngleToken { kind: 'angle'; value: string; start: number; end: number }
29
+
30
+ export type MarkdownToken =
31
+ | MarkdownHashToken | MarkdownGtToken | MarkdownStarToken
32
+ | MarkdownUnderscoreToken | MarkdownTildeToken | MarkdownBacktickToken
33
+ | MarkdownBracketOpenToken | MarkdownBracketCloseToken
34
+ | MarkdownParenOpenToken | MarkdownParenCloseToken
35
+ | MarkdownBangToken | MarkdownDashToken | MarkdownPlusToken
36
+ | MarkdownDotToken | MarkdownTextToken | MarkdownNewlineToken
37
+ | MarkdownFenceToken | MarkdownHrToken | MarkdownEscapeToken
38
+ | MarkdownAngleToken;
39
+
40
+ function pushToken(tokens: MarkdownToken[], kind: MarkdownToken['kind'], start: number, end: number, extra?: Record<string, string>) {
41
+ switch (kind) {
42
+ case 'hash': tokens.push({ kind, value: extra?.value ?? '', start, end }); break;
43
+ case 'gt': case 'bracket_open': case 'bracket_close':
44
+ case 'paren_open': case 'paren_close': case 'bang':
45
+ case 'plus': case 'dot': case 'hr':
46
+ tokens.push({ kind, start, end } as MarkdownToken); break;
47
+ case 'star': case 'underscore': case 'tilde':
48
+ case 'backtick': case 'dash': case 'text':
49
+ case 'newline': case 'escape': case 'angle':
50
+ tokens.push({ kind, value: extra?.value ?? '', start, end } as MarkdownToken); break;
51
+ case 'fence':
52
+ tokens.push({ kind, value: extra?.value ?? '', lang: extra?.lang ?? '', start, end }); break;
53
+ }
54
+ }
55
+
56
+ function isMarkdownSpecial(ch: string): boolean {
57
+ return '\\`*_{}[]()#+-.!|~\'"<>'.includes(ch) || ch === '\n' || ch === '\r';
58
+ }
59
+
60
+ /**
61
+ * Scan Markdown source text into tokens.
62
+ * Pure function: input → output, no state.
63
+ */
64
+ export function scanMarkdown(source: string): MarkdownToken[] {
65
+ const tokens: MarkdownToken[] = [];
66
+ let i = 0;
67
+
68
+ while (i < source.length) {
69
+ const ch = source[i];
70
+ const start = i;
71
+
72
+ // Newline
73
+ if (ch === '\n') {
74
+ i++;
75
+ pushToken(tokens, 'newline', start, i, { value: '\n' });
76
+ continue;
77
+ }
78
+ if (ch === '\r') {
79
+ if (i + 1 < source.length && source[i + 1] === '\n') i++;
80
+ i++;
81
+ pushToken(tokens, 'newline', start, i, { value: '\r\n' });
82
+ continue;
83
+ }
84
+
85
+ // Escape character
86
+ if (ch === '\\' && i + 1 < source.length) {
87
+ i += 2;
88
+ pushToken(tokens, 'escape', start, i, { value: source.slice(start, i) });
89
+ continue;
90
+ }
91
+
92
+ // Hash (heading)
93
+ if (ch === '#') {
94
+ let count = 1;
95
+ i++;
96
+ while (i < source.length && source[i] === '#' && count < 6) { count++; i++; }
97
+ pushToken(tokens, 'hash', start, i, { value: source.slice(start, i) });
98
+ continue;
99
+ }
100
+
101
+ // GT (blockquote)
102
+ if (ch === '>') {
103
+ i++;
104
+ pushToken(tokens, 'gt', start, i);
105
+ continue;
106
+ }
107
+
108
+ // Bang (image)
109
+ if (ch === '!' && i + 1 < source.length && source[i + 1] === '[') {
110
+ i++;
111
+ pushToken(tokens, 'bang', start, i);
112
+ continue;
113
+ }
114
+
115
+ // Brackets and parens
116
+ if (ch === '[') { i++; pushToken(tokens, 'bracket_open', start, i); continue; }
117
+ if (ch === ']') { i++; pushToken(tokens, 'bracket_close', start, i); continue; }
118
+ if (ch === '(') { i++; pushToken(tokens, 'paren_open', start, i); continue; }
119
+ if (ch === ')') { i++; pushToken(tokens, 'paren_close', start, i); continue; }
120
+
121
+ // Star (*, **, ***)
122
+ if (ch === '*') {
123
+ let count = 1;
124
+ i++;
125
+ while (i < source.length && source[i] === '*' && count < 3) { count++; i++; }
126
+ pushToken(tokens, 'star', start, i, { value: source.slice(start, i) });
127
+ continue;
128
+ }
129
+
130
+ // Underscore (_, __, ___)
131
+ if (ch === '_') {
132
+ let count = 1;
133
+ i++;
134
+ while (i < source.length && source[i] === '_' && count < 3) { count++; i++; }
135
+ pushToken(tokens, 'underscore', start, i, { value: source.slice(start, i) });
136
+ continue;
137
+ }
138
+
139
+ // Tilde (~, ~~)
140
+ if (ch === '~') {
141
+ let count = 1;
142
+ i++;
143
+ while (i < source.length && source[i] === '~' && count < 2) { count++; i++; }
144
+ pushToken(tokens, 'tilde', start, i, { value: source.slice(start, i) });
145
+ continue;
146
+ }
147
+
148
+ // Backtick (inline or fence)
149
+ if (ch === '`') {
150
+ let count = 1;
151
+ i++;
152
+ while (i < source.length && source[i] === '`' && count < 3) { count++; i++; }
153
+ if (count === 3) {
154
+ const langStart = i;
155
+ while (i < source.length && source[i] !== '\n' && source[i] !== '\r') i++;
156
+ pushToken(tokens, 'fence', start, i, { value: '```', lang: source.slice(langStart, i).trim() });
157
+ } else {
158
+ pushToken(tokens, 'backtick', start, i, { value: source.slice(start, i) });
159
+ }
160
+ continue;
161
+ }
162
+
163
+ // Dash (-, --, ---)
164
+ if (ch === '-') {
165
+ let count = 1;
166
+ i++;
167
+ while (i < source.length && source[i] === '-' && count < 3) { count++; i++; }
168
+ // Hr if --- and alone on line (simplified, normally requires parser validation)
169
+ if (count === 3) {
170
+ pushToken(tokens, 'dash', start, i, { value: source.slice(start, i) });
171
+ } else {
172
+ pushToken(tokens, 'dash', start, i, { value: source.slice(start, i) });
173
+ }
174
+ continue;
175
+ }
176
+
177
+ // Plus
178
+ if (ch === '+') { i++; pushToken(tokens, 'plus', start, i); continue; }
179
+
180
+ // Dot
181
+ if (ch === '.') { i++; pushToken(tokens, 'dot', start, i); continue; }
182
+
183
+ // Angle
184
+ if (ch === '<' || ch === '>') {
185
+ i++;
186
+ pushToken(tokens, 'angle', start, i, { value: ch });
187
+ continue;
188
+ }
189
+
190
+ // Default: accumulate text
191
+ const textStart = i;
192
+ while (i < source.length && !isMarkdownSpecial(source[i])) i++;
193
+ if (i > textStart) {
194
+ pushToken(tokens, 'text', textStart, i, { value: source.slice(textStart, i) });
195
+ } else {
196
+ // If it's a special char not handled above, treat as text to avoid infinite loop
197
+ pushToken(tokens, 'text', textStart, i + 1, { value: source[i] });
198
+ i++;
199
+ }
200
+ }
201
+
202
+ return tokens;
203
+ }
@@ -0,0 +1,455 @@
1
+ import { MarkdownToken, MarkdownFenceToken } from './MarkdownLexer';
2
+ import { MarkdownNode, MarkdownDocument, MarkdownParagraph, MarkdownText, MarkdownHeading } from './MarkdownAST';
3
+
4
+ /**
5
+ * DocumentEngine — MarkdownParser
6
+ *
7
+ * Basic implementation to parse a token stream into an AST.
8
+ */
9
+ export class MarkdownParser {
10
+ private tokens: MarkdownToken[];
11
+ private current: number = 0;
12
+
13
+ constructor(tokens: MarkdownToken[]) {
14
+ this.tokens = tokens;
15
+ }
16
+
17
+ public parse(): MarkdownDocument {
18
+ const doc: MarkdownDocument = {
19
+ type: 'document',
20
+ children: []
21
+ };
22
+
23
+ while (!this.isAtEnd()) {
24
+ let newlines = 0;
25
+ while (this.match('newline')) newlines++;
26
+
27
+ if (newlines > 0) {
28
+ // Since blocks like parseParagraph consume their trailing newline,
29
+ // seeing 1 newline here means there were 2 newlines in the source (a blank line).
30
+ // 1 newline here -> 1 empty_line
31
+ // 2 newlines here -> 2 empty_lines
32
+ for (let i = 0; i < newlines; i++) {
33
+ doc.children.push({ type: 'empty_line' } as any);
34
+ }
35
+ }
36
+
37
+ if (this.isAtEnd()) break;
38
+
39
+ const node = this.parseBlock();
40
+ if (node) {
41
+ doc.children.push(node);
42
+ }
43
+ }
44
+
45
+ return doc;
46
+ }
47
+
48
+ private parseBlock(): MarkdownNode | null {
49
+ if (this.isAtEnd()) return null;
50
+
51
+ const token = this.peek();
52
+
53
+ if (token.kind === 'hash') {
54
+ return this.parseHeading();
55
+ }
56
+
57
+ if (token.kind === 'gt') {
58
+ const saved = this.current;
59
+ this.advance(); // consume gt
60
+ const next1 = this.peek();
61
+ if (next1 && next1.kind === 'text' && ('value' in next1) && (next1.value as string).trim() === '') {
62
+ this.advance(); // consume space
63
+ }
64
+ const next2 = this.peek();
65
+ if (next2 && next2.kind === 'bracket_open') {
66
+ const next3 = this.tokens[this.current + 1];
67
+ if (next3 && next3.kind === 'bang') {
68
+ this.current = saved; // restore
69
+ return this.parseNotice();
70
+ }
71
+ }
72
+ this.current = saved; // restore
73
+ return this.parseBlockquote();
74
+ }
75
+
76
+ if (token.kind === 'fence') {
77
+ return this.parseFence();
78
+ }
79
+
80
+ if (token.kind === 'dash' || token.kind === 'star') {
81
+ // Must be followed by a space to be a list!
82
+ const next = this.tokens[this.current + 1];
83
+ if (next && next.kind === 'text' && ('value' in next) && (next.value as string).startsWith(' ')) {
84
+ return this.parseList();
85
+ }
86
+ }
87
+
88
+ // Default to paragraph
89
+ return this.parseParagraph();
90
+ }
91
+
92
+ private parseHeading(): MarkdownHeading {
93
+ const hash = this.advance();
94
+ const level = hash.kind === 'hash' ? Math.min(hash.value.length, 6) : 1;
95
+
96
+ // We don't want to blindly consume a text token if it contains actual text.
97
+ // Let's just let parseInline handle it. The renderer can deal with leading spaces,
98
+ // or we can trim the first child if it's text.
99
+ const children: MarkdownNode[] = [];
100
+ while (!this.isAtEnd() && !this.check('newline')) {
101
+ const inlineNode = this.parseInline();
102
+ if (inlineNode) {
103
+ children.push(inlineNode);
104
+ }
105
+ }
106
+
107
+ // Trim leading space from the first child if it's text
108
+ if (children.length > 0 && children[0].type === 'text') {
109
+ children[0].value = (children[0] as any).value.replace(/^\\s+/, '');
110
+ }
111
+
112
+ this.match('newline');
113
+
114
+ return {
115
+ type: 'heading',
116
+ level,
117
+ children
118
+ };
119
+ }
120
+
121
+ private parseBlockquote(): MarkdownNode {
122
+ const children: MarkdownNode[] = [];
123
+
124
+ while (!this.isAtEnd()) {
125
+ if (this.match('gt')) {
126
+ // blockquote line prefix
127
+ }
128
+
129
+ if (this.check('newline')) {
130
+ this.advance();
131
+ if (this.check('newline') || this.isAtEnd()) {
132
+ break; // Empty line ends blockquote
133
+ }
134
+ children.push({ type: 'text', value: '\n' });
135
+ } else {
136
+ const inlineNode = this.parseInline();
137
+ if (inlineNode) children.push(inlineNode);
138
+ }
139
+ }
140
+
141
+ return {
142
+ type: 'blockquote',
143
+ children
144
+ };
145
+ }
146
+
147
+ private parseNotice(): MarkdownNode {
148
+ const children: MarkdownNode[] = [];
149
+ this.advance(); // consume gt
150
+
151
+ // consume the [!NOTE] part
152
+ while (!this.isAtEnd() && !this.check('newline')) {
153
+ if (this.match('bracket_close')) break;
154
+ this.advance();
155
+ }
156
+
157
+ while (!this.isAtEnd()) {
158
+ if (this.match('gt')) {
159
+ // blockquote line prefix
160
+ }
161
+ if (this.check('newline')) {
162
+ this.advance();
163
+ if (this.check('newline') || this.isAtEnd()) break;
164
+ children.push({ type: 'text', value: '\n' });
165
+ } else {
166
+ const inlineNode = this.parseInline();
167
+ if (inlineNode) children.push(inlineNode);
168
+ }
169
+ }
170
+
171
+ return { type: 'notice', children };
172
+ }
173
+
174
+ private tokenValue(token: MarkdownToken): string {
175
+ if ('value' in token) return (token as any).value;
176
+ switch (token.kind) {
177
+ case 'bracket_open': return '[';
178
+ case 'bracket_close': return ']';
179
+ case 'paren_open': return '(';
180
+ case 'paren_close': return ')';
181
+ case 'bang': return '!';
182
+ case 'gt': return '>';
183
+ case 'plus': return '+';
184
+ case 'dot': return '.';
185
+ case 'hr': return '---';
186
+ default: return '';
187
+ }
188
+ }
189
+
190
+ private parseFence(): MarkdownNode {
191
+ const token = this.advance() as MarkdownFenceToken;
192
+ const value = token.lang;
193
+ let code = '';
194
+ while (!this.isAtEnd()) {
195
+ if (this.check('fence')) {
196
+ this.advance(); // close fence
197
+ break;
198
+ }
199
+ const p = this.advance();
200
+ code += this.tokenValue(p);
201
+ }
202
+ return { type: 'code_block', lang: value, value: code.trim() };
203
+ }
204
+
205
+ private parseList(): MarkdownNode {
206
+ const items: any[] = [];
207
+ while (!this.isAtEnd() && (this.check('dash') || this.check('star'))) {
208
+ this.advance(); // consume dash/star
209
+ const children: MarkdownNode[] = [];
210
+ while (!this.isAtEnd() && !this.check('newline')) {
211
+ const inlineNode = this.parseInline();
212
+ if (inlineNode) children.push(inlineNode);
213
+ }
214
+ // Trim leading space from the first child if it's text
215
+ if (children.length > 0 && children[0].type === 'text') {
216
+ children[0].value = (children[0] as any).value.replace(/^\\s+/, '');
217
+ }
218
+ items.push({ type: 'list_item', children });
219
+ if (this.match('newline')) {
220
+ if (this.check('newline')) break; // End of list on double newline
221
+ }
222
+ }
223
+ return { type: 'list', ordered: false, children: items };
224
+ }
225
+
226
+ private parseParagraph(): MarkdownParagraph {
227
+ const children: MarkdownNode[] = [];
228
+
229
+ while (!this.isAtEnd()) {
230
+ if (this.check('newline')) {
231
+ this.advance();
232
+ // Two newlines mean end of paragraph
233
+ if (this.check('newline') || this.isAtEnd()) {
234
+ break;
235
+ } else {
236
+ // A single newline could mean lazy continuation, BUT we must check if a new block starts!
237
+ if (this.check('hash') || this.check('gt') || this.check('fence') || this.check('hr') || this.check('dash')) {
238
+ break; // Paragraph interrupted by block
239
+ }
240
+ // Single newline treated as space in paragraph
241
+ children.push({ type: 'text', value: '\n' });
242
+ }
243
+ } else {
244
+ const inlineNode = this.parseInline();
245
+ if (inlineNode) {
246
+ children.push(inlineNode);
247
+ }
248
+ }
249
+ }
250
+
251
+ return {
252
+ type: 'paragraph',
253
+ children
254
+ };
255
+ }
256
+
257
+ private parseInline(terminator?: { kind: string, value?: string }): MarkdownNode | null {
258
+ if (this.isAtEnd()) return null;
259
+
260
+ if (terminator) {
261
+ const peekToken = this.peek();
262
+ if (peekToken.kind === terminator.kind && (!terminator.value || ('value' in peekToken && peekToken.value === terminator.value))) {
263
+ return null;
264
+ }
265
+ }
266
+
267
+ const token = this.advance();
268
+
269
+ // Bold and Italic
270
+ if (token.kind === 'star' || token.kind === 'underscore') {
271
+ const val = ('value' in token) ? token.value as string : '';
272
+ if (val === '**' || val === '__' || val === '*' || val === '_') {
273
+ const type = (val.length === 2) ? 'strong' : 'emphasis';
274
+
275
+ const savedCurrent = this.current;
276
+ const children: MarkdownNode[] = [];
277
+ let closed = false;
278
+
279
+ while (!this.isAtEnd()) {
280
+ const next = this.peek();
281
+ if (next.kind === 'newline') break; // Don't span across lines in this basic parser
282
+
283
+ if (next.kind === token.kind && ('value' in next && next.value === val)) {
284
+ this.advance(); // consume closing token
285
+ closed = true;
286
+ break;
287
+ }
288
+
289
+ const child = this.parseInline({ kind: token.kind, value: val });
290
+ if (child) {
291
+ children.push(child);
292
+ } else {
293
+ break;
294
+ }
295
+ }
296
+
297
+ if (closed) {
298
+ return { type, children };
299
+ } else {
300
+ // Backtrack and treat as text
301
+ this.current = savedCurrent;
302
+ return { type: 'text', value: val };
303
+ }
304
+ }
305
+ }
306
+
307
+ // Strikethrough
308
+ if (token.kind === 'tilde') {
309
+ const val = ('value' in token) ? token.value as string : '';
310
+ if (val === '~~') {
311
+ const savedCurrent = this.current;
312
+ const children: MarkdownNode[] = [];
313
+ let closed = false;
314
+
315
+ while (!this.isAtEnd()) {
316
+ const next = this.peek();
317
+ if (next.kind === 'newline') break;
318
+
319
+ if (next.kind === 'tilde' && ('value' in next && next.value === val)) {
320
+ this.advance();
321
+ closed = true;
322
+ break;
323
+ }
324
+
325
+ const child = this.parseInline({ kind: 'tilde', value: val });
326
+ if (child) children.push(child);
327
+ else break;
328
+ }
329
+
330
+ if (closed) {
331
+ return { type: 'strikethrough', children } as any;
332
+ } else {
333
+ this.current = savedCurrent;
334
+ return { type: 'text', value: val };
335
+ }
336
+ }
337
+ }
338
+
339
+ // Spoiler (||)
340
+ if (token.kind === 'text' && token.value === '||') {
341
+ const savedCurrent = this.current;
342
+ const children: MarkdownNode[] = [];
343
+ let closed = false;
344
+ while (!this.isAtEnd()) {
345
+ const next = this.peek();
346
+ if (next.kind === 'newline') break;
347
+ if (next.kind === 'text' && next.value === '||') {
348
+ this.advance();
349
+ closed = true;
350
+ break;
351
+ }
352
+ const child = this.parseInline({ kind: 'text', value: '||' });
353
+ if (child) children.push(child);
354
+ else break;
355
+ }
356
+ if (closed) {
357
+ return { type: 'spoiler', children };
358
+ } else {
359
+ this.current = savedCurrent;
360
+ return { type: 'text', value: '||' };
361
+ }
362
+ }
363
+
364
+ // Link or Image
365
+ if (token.kind === 'bracket_open' || token.kind === 'bang') {
366
+ const isImage = token.kind === 'bang';
367
+ if (isImage) {
368
+ if (!this.match('bracket_open')) {
369
+ return { type: 'text', value: '!' };
370
+ }
371
+ }
372
+
373
+ const savedCurrent = this.current;
374
+ let text = '';
375
+ let closedBracket = false;
376
+
377
+ while (!this.isAtEnd() && !this.check('newline')) {
378
+ if (this.match('bracket_close')) {
379
+ closedBracket = true;
380
+ break;
381
+ }
382
+ const p = this.advance();
383
+ text += ('value' in p) ? (p as any).value : '';
384
+ }
385
+
386
+ if (closedBracket && this.match('paren_open')) {
387
+ let url = '';
388
+ let closedParen = false;
389
+ while (!this.isAtEnd() && !this.check('newline')) {
390
+ if (this.match('paren_close')) {
391
+ closedParen = true;
392
+ break;
393
+ }
394
+ const p = this.advance();
395
+ url += ('value' in p) ? (p as any).value : '';
396
+ }
397
+
398
+ if (closedParen) {
399
+ if (isImage) {
400
+ return { type: 'image', url, alt: text };
401
+ } else {
402
+ return { type: 'link', url, children: [{ type: 'text', value: text }] };
403
+ }
404
+ }
405
+ }
406
+
407
+ // Fallback
408
+ this.current = savedCurrent;
409
+ return { type: 'text', value: isImage ? '![' : '[' };
410
+ }
411
+
412
+ return {
413
+ type: 'text',
414
+ value: this.tokenValue(token)
415
+ };
416
+ }
417
+
418
+ // --- Helpers ---
419
+
420
+ private peek(): MarkdownToken {
421
+ return this.tokens[this.current];
422
+ }
423
+
424
+ private previous(): MarkdownToken {
425
+ return this.tokens[this.current - 1];
426
+ }
427
+
428
+ private isAtEnd(): boolean {
429
+ return this.current >= this.tokens.length;
430
+ }
431
+
432
+ private advance(): MarkdownToken {
433
+ if (!this.isAtEnd()) this.current++;
434
+ return this.previous();
435
+ }
436
+
437
+ private check(kind: MarkdownToken['kind']): boolean {
438
+ if (this.isAtEnd()) return false;
439
+ return this.peek().kind === kind;
440
+ }
441
+
442
+ private match(kind: MarkdownToken['kind']): boolean {
443
+ if (this.check(kind)) {
444
+ this.advance();
445
+ return true;
446
+ }
447
+ return false;
448
+
449
+ }
450
+ }
451
+
452
+ export function parseMarkdown(tokens: MarkdownToken[]): MarkdownDocument {
453
+ const parser = new MarkdownParser(tokens);
454
+ return parser.parse();
455
+ }