馃悕馃悕馃悕
1
2const TEXT = "builtin_text";
3const BREAK = "builtin_break";
4
5const cache = {};
6
7export async function parse(uri) {
8 if (uri in cache) {
9 return cache[uri];
10 }
11
12 const response = await fetch(uri);
13
14 if (!response.ok) {
15 throw new Error(`could not load ${uri}`);
16 }
17
18 const source = await response.text();
19
20 const parsed = parseSource(source);
21
22 if (parsed.scanPosition != source.length) {
23 console.warn(`parse concluded before end of file for ${uri}`);
24 }
25
26 parsed.source = source;
27
28 cache[uri] = parsed;
29
30 return parsed;
31}
32
33export async function debugParse(uri) {
34 const parsed = await parse(uri);
35 dumpNodes(parsed.nodes, parsed.source);
36}
37
38function parseSource(input, startIndex = 0) {
39 const nodes = [];
40 let scanPosition = startIndex;
41 let escapeNext = false;
42
43 let contentStart = null;
44 let contentEnd = null;
45 let tagStart = null;
46 let tagEnd = null;
47 let argStart = null;
48 let argEnd = null;
49
50 let newlineCount = 0;
51
52 const scanLimit = input.length;
53
54 while (scanPosition < scanLimit) {
55 const char = input[scanPosition];
56
57 if (!escapeNext && char === "\\") {
58 escapeNext = true;
59 scanPosition++;
60 continue;
61 }
62
63 if (!escapeNext && char === "]" && argStart !== null && argEnd === null) {
64 argEnd = scanPosition - 1;
65 scanPosition++;
66 continue;
67 }
68
69 if (!escapeNext && char === "{") {
70 if (contentStart !== null) {
71 nodes.push({
72 tag: {symbol: TEXT, start: null, end: null},
73 content: {start: contentStart, end: contentEnd},
74 args: {start: null, end: null},
75 origin: "text_before_block"
76 });
77
78 contentStart = null;
79 contentEnd = null;
80 }
81
82 scanPosition += 1; // skip the {
83
84 let tag;
85 if (argStart !== null && argEnd === null) {
86 // args-only tag => implicit div
87 tag = tagStart === argStart - 1 ? "div" : input.substring(tagStart, argStart - 1);
88 argEnd = tagEnd;
89 } else {
90 if (argStart !== null) {
91 tag = tagStart === argStart - 1 ? "div" : input.substring(tagStart, argStart - 1);
92 } else {
93 tag = tagStart === null ? TEXT : input.substring(tagStart, tagEnd + 1);
94 }
95 }
96
97 if (shouldParseContent(tag)) {
98 const parsed = parseSource(input, scanPosition);
99
100 nodes.push({
101 tag: {symbol: tag, start: tagStart, end: tagEnd},
102 content: {nodes: parsed.nodes, start: scanPosition, end: parsed.scanPosition},
103 args: {start: argStart, end: argEnd},
104 origin: "parsed_block"
105 });
106 scanPosition = parsed.scanPosition + 1;
107 } else {
108 // ideally this will actually pass off parsing to the module, which can ideally operate in a single pass & return its end position
109 const closingPosition = findClosingBracket(input, scanPosition);
110 nodes.push({
111 tag: {symbol: tag, start: tagStart, end: tagEnd},
112 content: {start: scanPosition, end: closingPosition},
113 args: {start: argStart, end: argEnd},
114 origin: "unparsed_block"
115 });
116 scanPosition = closingPosition + 1;
117 }
118
119 tagStart = null;
120 tagEnd = null;
121 argStart = null;
122 argEnd = null;
123
124 newlineCount = 0;
125
126 continue;
127 }
128
129 if (!escapeNext && char === "}") {
130 break;
131 }
132 escapeNext = false;
133
134 const isWhitespace = /\s/.test(char);
135 const takingArgs = argStart !== null && argEnd === null;
136 if (!takingArgs && isWhitespace) {
137 if (char === "\n") {
138 newlineCount += 1;
139 if (newlineCount === 2) {
140 newlineCount = 0;
141 if (contentStart !== null) {
142 nodes.push({
143 tag: {symbol: TEXT, start: null, end: null},
144 content: {start: contentStart, end: tagEnd},
145 args: {start: argStart, end: argEnd},
146 origin: "text_before_break"
147 });
148 contentStart = null;
149 contentEnd = null;
150 tagStart = null;
151 tagEnd = null;
152 argStart = null;
153 argEnd = null;
154 }
155 nodes.push({
156 tag: {symbol: BREAK, start: null, end: null},
157 content: {start: null, end: null},
158 args: {start: null, end: null},
159 origin: "break"
160 });
161 }
162 }
163 scanPosition += 1;
164 continue;
165 }
166
167 newlineCount = 0;
168
169 if (tagStart === null) {
170 tagStart = scanPosition;
171 tagEnd = scanPosition;
172 } else {
173 if (tagEnd === scanPosition - 1) {
174 tagEnd += 1;
175 } else {
176 if (contentStart === null) {
177 contentStart = tagStart;
178 }
179 contentEnd = tagEnd;
180 tagStart = scanPosition;
181 tagEnd = scanPosition;
182 }
183 }
184
185 if (char === "[" && !takingArgs) {
186 argStart = scanPosition + 1;
187 }
188
189 scanPosition += 1;
190 }
191
192 // Finish the current node if it has content
193 if (tagStart !== null) {
194 nodes.push({
195 tag: {symbol: TEXT, start: null, end: null},
196 content: { start: contentStart ?? tagStart, end: tagEnd },
197 args: {start: null, end: null},
198 origin: "trailing_text"
199 });
200 }
201
202 return { nodes, scanPosition };
203}
204
205function findClosingBracket(input, startIndex) {
206 let scanPosition = startIndex;
207 let depth = 1;
208 let escapeNext = false;
209
210 while (scanPosition < input.length && depth > 0) {
211 const char = input[scanPosition];
212
213 if (escapeNext) {
214 escapeNext = false;
215 } else if (char === "\\") {
216 escapeNext = true;
217 } else if (char === "{") {
218 depth++;
219 } else if (char === "}") {
220 depth--;
221 }
222
223 scanPosition++;
224 }
225
226 return scanPosition - 1;
227}
228
229function shouldParseContent(tag) {
230 return tag !== "$";
231}
232
233function dumpNodes(nodes, input) {
234 for (const node of nodes) {
235 if (node.content.nodes !== undefined) {
236 const args = node.args.start === null ? "" : input.substring(node.args.start, node.args.end + 1);
237 console.log(`<${node.tag.symbol}>[${args}](${node.origin}){next ${node.content.nodes.length} nodes}`);
238 dumpNodes(node.content.nodes, input);
239 } else {
240 const content = input.substring(node.content.start, node.content.end + 1);
241 const args = node.args.start === null ? "" : input.substring(node.args.start, node.args.end + 1);
242 console.log(`<${node.tag.symbol}>[${args}](${node.origin}){${content}}`);
243 }
244 }
245}
246