@miliastry/quasar 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +119 -0
- package/README.md +45 -0
- package/dist/Visuals/lyne.css +1027 -0
- package/dist/Visuals/osu.css +257 -0
- package/dist/index.d.mts +4563 -0
- package/dist/index.d.ts +4563 -0
- package/dist/index.js +11291 -0
- package/dist/index.mjs +11197 -0
- package/package.json +51 -0
- package/src/Analysis/Contracts/AnalysisReport.ts +26 -0
- package/src/Analysis/Contracts/Contribution.ts +76 -0
- package/src/Analysis/Contracts/Pass.ts +76 -0
- package/src/Analysis/Contracts/PipelineContext.ts +48 -0
- package/src/Analysis/Passes/Analysis/GradientAnalyzer.ts +292 -0
- package/src/Analysis/Passes/Analysis/MergeableColorAnalyzer.ts +112 -0
- package/src/Analysis/Passes/Analysis/RainbowAnalyzer.ts +233 -0
- package/src/Analysis/Passes/Analysis/WaveAnalyzer.ts +211 -0
- package/src/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.ts +135 -0
- package/src/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.ts +84 -0
- package/src/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.ts +99 -0
- package/src/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.ts +119 -0
- package/src/Analysis/Passes/Decision/DefaultDecision.ts +139 -0
- package/src/Analysis/Passes/Decision/__tests__/DefaultDecision.test.ts +179 -0
- package/src/Analysis/Passes/Transform/CollapseGradientTransform.ts +176 -0
- package/src/Analysis/Passes/Transform/MergeColorsTransform.ts +126 -0
- package/src/Analysis/Passes/Transform/RainbowCollapseTransform.ts +83 -0
- package/src/Analysis/Passes/Transform/WaveCollapseTransform.ts +88 -0
- package/src/Analysis/Passes/Utility/CharacterCountAnalyzer.ts +45 -0
- package/src/Analysis/Pipeline/Pipeline.ts +133 -0
- package/src/Analysis/Pipeline/PipelineBuilder.ts +55 -0
- package/src/Analysis/Pipeline/PipelineStage.ts +19 -0
- package/src/Analysis/Utils/color-utils.ts +132 -0
- package/src/Analysis/__tests__/Integration.test.ts +162 -0
- package/src/Analysis/__tests__/Pipeline.test.ts +133 -0
- package/src/Analysis/index.ts +52 -0
- package/src/BBCode/BBCodeDocumentModel.ts +175 -0
- package/src/BBCode/BBCodeToGreenNode.ts +755 -0
- package/src/BBCode/Parser.ts +384 -0
- package/src/BBCode/index.ts +12 -0
- package/src/Collab/positions.ts +91 -0
- package/src/Commands/Command.ts +44 -0
- package/src/Commands/CommandRegistry.ts +78 -0
- package/src/Commands/DeleteNode.ts +20 -0
- package/src/Commands/InsertText.ts +21 -0
- package/src/Commands/SplitMerge.ts +28 -0
- package/src/Commands/WrapInTag.ts +21 -0
- package/src/Commands/index.ts +6 -0
- package/src/Diff/TreeDiffer.ts +264 -0
- package/src/Diff/__tests__/TreeDiffer.test.ts +65 -0
- package/src/Diff/index.ts +2 -0
- package/src/Events/EventBus.ts +160 -0
- package/src/Events/index.ts +2 -0
- package/src/Formatter/Formatter.ts +54 -0
- package/src/Formatter/index.ts +2 -0
- package/src/HTML/HTMLDocumentModel.ts +35 -0
- package/src/HTML/HTMLToGreenNode.ts +290 -0
- package/src/Incremental/ChangeTracker.ts +105 -0
- package/src/Incremental/IncrementalParser.ts +591 -0
- package/src/Incremental/__tests__/IncrementalParser.test.ts +164 -0
- package/src/Incremental/index.ts +4 -0
- package/src/Lexer/BBCodeLexer.ts +382 -0
- package/src/Lexer/Lexer.ts +181 -0
- package/src/Lexer/index.ts +10 -0
- package/src/Linter/Linter.ts +193 -0
- package/src/Linter/index.ts +2 -0
- package/src/Markdown/MarkdownAST.ts +112 -0
- package/src/Markdown/MarkdownDocumentModel.ts +55 -0
- package/src/Markdown/MarkdownLexer.ts +203 -0
- package/src/Markdown/MarkdownParser.ts +455 -0
- package/src/Markdown/MarkdownToGreenNode.ts +153 -0
- package/src/Model/DocumentModel.ts +694 -0
- package/src/Model/NodeFactory.ts +117 -0
- package/src/Model/TagRegistry.ts +495 -0
- package/src/Model/index.ts +5 -0
- package/src/Plugins/PluginAPI.ts +119 -0
- package/src/Plugins/PluginRegistry.ts +132 -0
- package/src/Plugins/index.ts +3 -0
- package/src/Queries/QueryEngine.ts +152 -0
- package/src/Queries/index.ts +1 -0
- package/src/RenderPipeline/RenderPipeline.ts +125 -0
- package/src/RenderPipeline/RenderTree.ts +134 -0
- package/src/RenderPipeline/index.ts +4 -0
- package/src/Semantic/SemanticAnalyzer.ts +506 -0
- package/src/Semantic/index.ts +2 -0
- package/src/Symbols/SymbolTable.ts +124 -0
- package/src/Symbols/index.ts +1 -0
- package/src/Syntax/GreenNode.ts +324 -0
- package/src/Syntax/GreenNodePool.ts +269 -0
- package/src/Syntax/NodeMatcher.ts +370 -0
- package/src/Syntax/RedNode.ts +569 -0
- package/src/Syntax/RedNodeStore.ts +184 -0
- package/src/Syntax/TreeBuilder.ts +214 -0
- package/src/Syntax/__tests__/GreenNode.test.ts +33 -0
- package/src/Syntax/__tests__/RedNode.test.ts +81 -0
- package/src/Syntax/__tests__/RedNodeStore.test.ts +104 -0
- package/src/Syntax/greenEdit.ts +110 -0
- package/src/Syntax/hash.ts +30 -0
- package/src/Syntax/index.ts +12 -0
- package/src/Syntax/partition.ts +161 -0
- package/src/Syntax/preserveNodeIds.ts +201 -0
- package/src/Tests/ASTOptimizerIdempotence.test.ts +77 -0
- package/src/Tests/BlockPatcher.test.ts +437 -0
- package/src/Tests/BlockPatcherWindowed.test.ts +364 -0
- package/src/Tests/BoxDrawer.test.ts +217 -0
- package/src/Tests/BoxRichTitle.test.ts +105 -0
- package/src/Tests/Chars500kBenchmark.test.ts +151 -0
- package/src/Tests/Chars500kEdits.test.ts +321 -0
- package/src/Tests/CollabPositions.test.ts +146 -0
- package/src/Tests/CompilerPathProfiling.test.ts +186 -0
- package/src/Tests/DOMMorpher.test.ts +142 -0
- package/src/Tests/DomPatchPerf.test.ts +60 -0
- package/src/Tests/EffectSegments.snapshot.json +616 -0
- package/src/Tests/EffectSegments.test.ts +68 -0
- package/src/Tests/FindNodeAtOffset.test.ts +65 -0
- package/src/Tests/Fuzzer.test.ts +166 -0
- package/src/Tests/GreenNodePool.test.ts +153 -0
- package/src/Tests/Lexer.test.ts +238 -0
- package/src/Tests/LyneMode.test.ts +187 -0
- package/src/Tests/ModelCoherence.test.ts +180 -0
- package/src/Tests/Partition.test.ts +238 -0
- package/src/Tests/PluginTags.test.ts +150 -0
- package/src/Tests/ProblematicSection.test.ts +46 -0
- package/src/Tests/ProblematicSectionHTML.test.ts +58 -0
- package/src/Tests/RedReuse.test.ts +134 -0
- package/src/Tests/ReproDelete20k.test.ts +62 -0
- package/src/Tests/SemanticValidators.test.ts +136 -0
- package/src/Tests/StableNodeIds.test.ts +210 -0
- package/src/Tests/StudioColorBloat.test.ts +25 -0
- package/src/Tests/StudioDebugText.test.ts +27 -0
- package/src/Tests/StudioTrailingChar.test.ts +25 -0
- package/src/Tests/StudioValidText.test.ts +25 -0
- package/src/Tests/UrlImgBug.test.ts +23 -0
- package/src/Tests/VisualBuilderFidelity.test.ts +105 -0
- package/src/Tests/referenceDocument.ts +119 -0
- package/src/Transactions/Transaction.ts +176 -0
- package/src/Transactions/UndoManager.ts +111 -0
- package/src/Transactions/index.ts +3 -0
- package/src/Transformers/ASTOptimizer.ts +315 -0
- package/src/Transformers/GradientTransformer.ts +143 -0
- package/src/Transformers/GrowTransformer.ts +115 -0
- package/src/Transformers/RainbowTransformer.ts +121 -0
- package/src/Transformers/SineWaveTransformer.ts +130 -0
- package/src/Transformers/Transformer.ts +22 -0
- package/src/Types/core.ts +270 -0
- package/src/Types/diagnostics.ts +156 -0
- package/src/Types/index.ts +23 -0
- package/src/Types/operations.ts +180 -0
- package/src/Types/queries.ts +121 -0
- package/src/Types/symbols.ts +69 -0
- package/src/Types/tokens.ts +186 -0
- package/src/Utils/BBCodeGenerator.ts +126 -0
- package/src/Utils/ColorMath.ts +276 -0
- package/src/Utils/color.ts +112 -0
- package/src/Utils/dom-to-svg.test.ts +86 -0
- package/src/Utils/dom-to-svg.ts +615 -0
- package/src/Utils/treeTransformers.ts +717 -0
- package/src/Visitors/BBBlocksExporter.ts +69 -0
- package/src/Visitors/BBCodeExporter.ts +318 -0
- package/src/Visitors/BlockPatcher.ts +963 -0
- package/src/Visitors/DOMMorpher.ts +134 -0
- package/src/Visitors/HTMLRenderer.ts +1077 -0
- package/src/Visitors/JSONExporter.ts +66 -0
- package/src/Visitors/MarkdownExporter.ts +99 -0
- package/src/Visitors/SVGRenderer.ts +35 -0
- package/src/Visitors/TiptapExporter.ts +145 -0
- package/src/Visitors/Visitor.ts +48 -0
- package/src/Visitors/index.ts +9 -0
- package/src/Visuals/BoxDrawer.ts +175 -0
- package/src/Visuals/index.ts +42 -0
- package/src/Visuals/lyne.css +1027 -0
- package/src/Visuals/osu.css +257 -0
- package/src/index.ts +197 -0
|
@@ -0,0 +1,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
|
+
}
|