40 lines
1.7 KiB
TypeScript
40 lines
1.7 KiB
TypeScript
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<string, IndexedHtmlElement>();
|
|
const namedAnchors = new Map<string, IndexedHtmlElement>();
|
|
const definitions = new Map<string, { count: number; start: number }>();
|
|
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<typeof indexHtmlElements>;
|