馃悕馃悕馃悕
at dev 7.5 kB view raw
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