import { htmlAttributes } from './html-attributes'; export interface IndexedHtmlElement { tagName: string; start: number; bodyStart: number; selfClosing: boolean; } /** Import-local index over already protected/sanitized lexical markup. No DOM * or persistent cache: offsets belong only to this exact immutable string. */ export function indexHtmlElements(markup: string) { const byId = new Map(); const namedAnchors = new Map(); const definitions = new Map(); const pattern = /<([a-zA-Z][\w:-]*)\b([^>]*)>/g; let match: RegExpExecArray | null; while ((match = pattern.exec(markup))) { let id: string | undefined; let name: string | undefined; for (const attribute of htmlAttributes(match[2] ?? '')) { if (attribute.name === 'id' && id === undefined) id = attribute.value; if (attribute.name === 'name' && name === undefined) name = attribute.value; } if (!id && !name) continue; const element = { tagName: match[1], start: match.index, bodyStart: pattern.lastIndex, selfClosing: /\/\s*>$/.test(match[0]) }; if (id && !byId.has(id)) byId.set(id, element); if (name && element.tagName.toLowerCase() === 'a' && !namedAnchors.has(name)) namedAnchors.set(name, element); for (const target of new Set([id, name])) { if (!target) continue; const previous = definitions.get(target); if (previous) previous.count++; else definitions.set(target, { count: 1, start: element.start }); } } return { byId, namedAnchors, definitions }; } export type HtmlElementIndex = ReturnType;