| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const QUOTE_MODES = { "'": 'squote', '"': 'dquote' }; |
| const CLOSING_QUOTE = { squote: "'", dquote: '"' }; |
|
|
| const IDENTIFIER_CHAR = /[\w$]/; |
|
|
| |
| |
| |
| |
| |
| const REGEX_PRECEDING = new Set([ |
| '', '(', ',', '=', ':', '[', '!', '&', '|', '?', '{', '}', ';', '+', '-', '*', '%', '~', '^', '<', '>', |
| ]); |
|
|
| |
| |
| |
| |
| |
| |
| const REGEX_PRECEDING_KEYWORDS = new Set([ |
| 'return', 'typeof', 'instanceof', 'in', 'new', 'delete', 'void', |
| 'throw', 'case', 'do', 'else', 'yield', 'await', |
| ]); |
|
|
| |
| |
| |
| |
| function precedingWord(text, index) { |
| let end = index; |
| while (end > 0 && /\s/.test(text[end - 1])) end -= 1; |
| let start = end; |
| while (start > 0 && IDENTIFIER_CHAR.test(text[start - 1])) start -= 1; |
| return text.slice(start, end); |
| } |
|
|
| |
| |
| |
| function opensRegex(text, index, prevSignificant) { |
| if (REGEX_PRECEDING.has(prevSignificant)) return true; |
| if (!IDENTIFIER_CHAR.test(prevSignificant)) return false; |
| const word = precedingWord(text, index); |
| let wordStart = index; |
| while (wordStart > 0 && /\s/.test(text[wordStart - 1])) wordStart -= 1; |
| wordStart -= word.length; |
| if (text[wordStart - 1] === '.') return false; |
| return REGEX_PRECEDING_KEYWORDS.has(word); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| function skipRegexLiteral(text, index) { |
| let i = index + 1; |
| let inClass = false; |
| while (i < text.length) { |
| const ch = text[i]; |
| if (ch === '\\') { i += 2; continue; } |
| if (ch === '\n') return index + 1; |
| if (ch === '[') inClass = true; |
| else if (ch === ']') inClass = false; |
| else if (ch === '/' && !inClass) return i + 1; |
| i += 1; |
| } |
| return index + 1; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function stripJsComments(source) { |
| |
| |
| |
| |
| |
| const out = source.split(''); |
| const length = source.length; |
| const stack = []; |
| let prevSignificant = ''; |
| let i = 0; |
|
|
| const blank = (from, to) => { |
| for (let n = from; n < to; n += 1) { |
| if (source[n] !== '\n') out[n] = ' '; |
| } |
| }; |
|
|
| while (i < length) { |
| const mode = stack[stack.length - 1]; |
| const ch = source[i]; |
|
|
| if (mode === 'squote' || mode === 'dquote') { |
| if (ch === '\\') { i += 2; continue; } |
| if (ch === CLOSING_QUOTE[mode]) { stack.pop(); prevSignificant = ch; } |
| |
| if (ch === '\n') stack.pop(); |
| i += 1; |
| continue; |
| } |
|
|
| if (mode === 'template') { |
| if (ch === '\\') { i += 2; continue; } |
| if (ch === '`') { stack.pop(); prevSignificant = ch; i += 1; continue; } |
| if (ch === '$' && source[i + 1] === '{') { stack.push('interp'); i += 2; continue; } |
| i += 1; |
| continue; |
| } |
|
|
| if (ch === '/' && source[i + 1] === '/') { |
| const end = source.indexOf('\n', i); |
| blank(i, end === -1 ? length : end); |
| i = end === -1 ? length : end; |
| continue; |
| } |
|
|
| if (ch === '/' && source[i + 1] === '*') { |
| const close = source.indexOf('*/', i + 2); |
| const end = close === -1 ? length : close + 2; |
| blank(i, end); |
| i = end; |
| continue; |
| } |
|
|
| if (ch === '/' && opensRegex(source, i, prevSignificant)) { |
| i = skipRegexLiteral(source, i); |
| prevSignificant = '/'; |
| continue; |
| } |
|
|
| if (QUOTE_MODES[ch]) { stack.push(QUOTE_MODES[ch]); i += 1; continue; } |
| if (ch === '`') { stack.push('template'); i += 1; continue; } |
| if (ch === '{') { stack.push('brace'); } |
| if (ch === '}' && (mode === 'brace' || mode === 'interp')) { stack.pop(); } |
|
|
| if (!/\s/.test(ch)) prevSignificant = ch; |
| i += 1; |
| } |
|
|
| return out.join(''); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function walkCode(text, start, onCodeChar) { |
| const stack = []; |
| let i = start; |
| |
| |
| let prevSignificant = text.slice(0, start).trimEnd().slice(-1); |
|
|
| while (i < text.length) { |
| const mode = stack[stack.length - 1]; |
| const ch = text[i]; |
|
|
| if (mode === 'squote' || mode === 'dquote') { |
| if (ch === '\\') { i += 2; continue; } |
| if (ch === CLOSING_QUOTE[mode] || ch === '\n') { stack.pop(); prevSignificant = ch; } |
| i += 1; |
| continue; |
| } |
|
|
| if (mode === 'template') { |
| if (ch === '\\') { i += 2; continue; } |
| if (ch === '`') { stack.pop(); prevSignificant = ch; } |
| else if (ch === '$' && text[i + 1] === '{') { stack.push('interp'); i += 2; continue; } |
| i += 1; |
| continue; |
| } |
|
|
| |
| |
| |
| |
| |
| if (ch === '/' && opensRegex(text, i, prevSignificant)) { |
| i = skipRegexLiteral(text, i); |
| prevSignificant = '/'; |
| continue; |
| } |
|
|
| if (QUOTE_MODES[ch]) stack.push(QUOTE_MODES[ch]); |
| else if (ch === '`') stack.push('template'); |
| else if (ch === '{') stack.push('brace'); |
| else if (ch === '}') { |
| if (mode === 'interp') { stack.pop(); i += 1; continue; } |
| if (mode === 'brace') stack.pop(); |
| } |
|
|
| if (onCodeChar(ch, i) === false) return; |
| if (!/\s/.test(ch)) prevSignificant = ch; |
| i += 1; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| function isTokenBoundedMatch(text, anchor, index) { |
| const before = text[index - 1]; |
| if (before !== undefined && IDENTIFIER_CHAR.test(anchor[0]) && IDENTIFIER_CHAR.test(before)) { |
| return false; |
| } |
| const after = text[index + anchor.length]; |
| const lastAnchorChar = anchor[anchor.length - 1]; |
| if (after !== undefined && IDENTIFIER_CHAR.test(lastAnchorChar) && IDENTIFIER_CHAR.test(after)) { |
| return false; |
| } |
| return true; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function extractDelimitedBlock(source, anchor, open = '{', close = '}') { |
| const text = stripJsComments(source); |
|
|
| let anchorEnd = -1; |
| walkCode(text, 0, (_ch, index) => { |
| if (!text.startsWith(anchor, index)) return true; |
| if (!isTokenBoundedMatch(text, anchor, index)) return true; |
| anchorEnd = index + anchor.length; |
| return false; |
| }); |
| if (anchorEnd === -1) return null; |
|
|
| let bodyStart = -1; |
| let bodyEnd = -1; |
| let depth = 0; |
| walkCode(text, anchorEnd, (ch, index) => { |
| if (ch === open) { |
| depth += 1; |
| if (depth === 1) bodyStart = index + 1; |
| return true; |
| } |
| if (ch === close && depth > 0) { |
| depth -= 1; |
| if (depth === 0) { bodyEnd = index; return false; } |
| } |
| return true; |
| }); |
|
|
| if (bodyStart === -1 || bodyEnd === -1) return null; |
| return text.slice(bodyStart, bodyEnd); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function splitTopLevelEntries(body) { |
| const text = stripJsComments(body); |
| const entries = []; |
| let start = 0; |
| let depth = 0; |
|
|
| walkCode(text, 0, (ch, index) => { |
| if (ch === '{' || ch === '[' || ch === '(') depth += 1; |
| else if (ch === '}' || ch === ']' || ch === ')') depth -= 1; |
| else if (ch === ',' && depth === 0) { |
| entries.push(text.slice(start, index)); |
| start = index + 1; |
| } |
| return true; |
| }); |
| entries.push(text.slice(start)); |
|
|
| return entries.map((entry) => entry.trim()).filter((entry) => entry.length > 0); |
| } |
|
|
| const STRING_LITERAL = /^'((?:[^'\\]|\\.)*)'|^"((?:[^"\\]|\\.)*)"/; |
| const BARE_KEY = /^([A-Za-z_$][\w$]*)\s*:/; |
|
|
| |
| |
| |
| |
| |
| const SIMPLE_ESCAPES = { n: '\n', t: '\t', r: '\r', b: '\b', f: '\f', v: '\v', 0: '\0' }; |
|
|
| function unescapeStringLiteral(raw) { |
| return raw.replace(/\\(.)/g, (_match, ch) => SIMPLE_ESCAPES[ch] ?? ch); |
| } |
|
|
| function readStringLiteral(entry) { |
| const match = STRING_LITERAL.exec(entry); |
| if (!match) return null; |
| const raw = match[1] ?? match[2]; |
| return { value: unescapeStringLiteral(raw), length: match[0].length }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function objectLiteralEntryValue(body, key) { |
| for (const entry of splitTopLevelEntries(body)) { |
| const literal = readStringLiteral(entry); |
| if (literal) { |
| const rest = entry.slice(literal.length); |
| if (literal.value === key && /^\s*:/.test(rest)) return rest.replace(/^\s*:\s*/, '').trim(); |
| continue; |
| } |
| const bare = BARE_KEY.exec(entry); |
| if (bare && bare[1] === key) return entry.slice(bare[0].length).trim(); |
| } |
| return null; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function arrayLiteralHasStringMember(body, value) { |
| return splitTopLevelEntries(body).some((entry) => { |
| const literal = readStringLiteral(entry); |
| return literal !== null && literal.length === entry.length && literal.value === value; |
| }); |
| } |
|
|