/** * The low-level parsing behind `tabStopsIn`: balanced-bracket and value scanning, * a tag's attribute value, the binding/import RESOLVER (name → its home file and * definition, expanded across hops), and the two per-tag predicates — * `tagDeclaresTab` (is this a tab stop?) and `taiClassesOf` (which `tai-*` classes * does it wear?). */ /** * The text from `start` to the character closing the bracket it opens, quotes * respected so a brace inside a string neither opens nor closes anything. */ export function balancedFrom(source: string, start: number, open: string, close: string): string { let depth = 0; let quote: string | undefined; for (let index = start; index < source.length; index += 1) { const character = source[index]; if (quote !== undefined) { if (character === '\\') { index += 1; continue; } if (character === quote) quote = undefined; continue; } if (character === '"' || character === "'" || character === '`') { quote = character; continue; } if (character === open) depth += 1; else if (character === close) { depth -= 1; if (depth === 0) return source.slice(start, index + 1); } } return source.slice(start); } /** * The RHS of a `const`/`let` assignment beginning at `start`: text up to the * top-level `;`, with brackets and quotes tracked so a `;` inside either does not * end it. */ const QUOTE_CHARS = '"\'`'; const OPEN_BRACKETS = '{(['; const CLOSE_BRACKETS = '})]'; export function readAssignmentValue(source: string, start: number): string { let end = start; let depth = 0; let quote: string | undefined; for (; end < source.length; end += 1) { const character = source[end] ?? ''; if (quote !== undefined) { if (character === '\\') end += 1; else if (character === quote) quote = undefined; continue; } if (QUOTE_CHARS.includes(character)) quote = character; else if (OPEN_BRACKETS.includes(character)) depth += 1; else if (CLOSE_BRACKETS.includes(character)) depth -= 1; else if (character === ';' && depth === 0) break; } return source.slice(start, end); } /** * The index of the `>` closing an opening tag whose attribute run starts at * `from`, quotes and brace depth tracked so a `>` inside a `className` expression * (`{x > 0 ? a : b}`) is not mistaken for it. */ export function scanToTagClose(source: string, from: number): number { let end = from; let depth = 0; let quote: string | undefined; for (; end < source.length; end += 1) { const character = source[end]; if (quote !== undefined) { if (character === '\\') end += 1; else if (character === quote) quote = undefined; continue; } if (character === '"' || character === "'" || character === '`') quote = character; else if (character === '{') depth += 1; else if (character === '}') depth -= 1; else if (character === '>' && depth === 0) break; } return end; } /** One opening JSX tag: its element name and everything between that and the `>`. */ export interface OpeningTag { readonly name: string; readonly attributes: string; } /** * The value written for `attribute`, unquoted or unbraced; `undefined` when the * tag does not write it at all, which is the difference between "no `tabIndex`" * and "`tabIndex={-1}`". */ export function attributeValue(attributes: string, attribute: string): string | undefined { const written = new RegExp(String.raw`(?): string; } /** A resolver over pre-scanned per-file binding and import maps. */ export function createTabStopResolver( bindings: ReadonlyMap>, imports: ReadonlyMap>, ): TabStopResolver { function homeOf(file: string, name: string): string | undefined { if (bindings.get(file)?.has(name) === true) return file; return imports.get(file)?.get(name); } function definitionOf(home: string, name: string): string | undefined { return bindings.get(home)?.get(name); } function expand(text: string, file: string, depth: number, seen: Set): string { if (depth === 0) return text; let out = text; for (const match of text.matchAll(/(? `.${match[1] ?? ''}`) .filter((name) => !name.endsWith('-')), ), ]; }