/*
Server-side HTML post-processor that wraps qualifying inline numbers in prose HTML with GSAP-ready span elements. Zero DOM access — safe to call in Astro SSR/SSG frontmatter.
- Do not import or call this from browser scripts — server-only.
- Do not wrap numbers inside h1–h4, code, pre, or existing js-inline-number spans.
- Do not use a full HTML parser — keep dependency-free.
- RFC-0041: Created as SSR inline number pre-wrap utility in @warpgogol/werkstatt-shared/text.
*/
// @ai-invariant: RFC-0041. Server-only. Never import in browser scripts.
export interface WrapInlineNumbersOptions {
/** Also wrap year numbers (1200–currentYear+120). Default false — years are excluded by default. */
animateYears?: boolean;
/** GSAP tween duration in seconds written into data-duration. Default 3.0. */
duration?: number;
}
// Matches integers ≥ 2 digits bounded by whitespace, punctuation, or string edges.
// Does NOT match:
// - compound tokens: 14-jährig, 1st, e.V., 1.234 (trailing period or hyphen)
// - paragraph refs: § 53 (lookbehind excludes § via negative lookahead on the preceding char)
// - registration IDs: VR 1520 BHV (uppercase-letter word before the number)
// Lookbehind: preceded by start-of-string OR whitespace/opening punctuation,
// AND NOT immediately preceded by an uppercase letter sequence + space (abbrev/code)
// Lookahead: followed by end-of-string OR whitespace/closing punctuation
const NUMBER_RE = /(?<=^|[\s(\[«"–—,;:!?])(\d{2,})(?=[\s).,;:!?»"–—]|$)/gm;
// Tags whose text content must not be wrapped.
const SKIP_TAGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6", "code", "pre", "li", "ol"]);
/**
* Splits an HTML string into alternating text and tag tokens.
* Tags are strings starting with "<"; text segments are everything between tags.
*/
function tokenize(html: string): string[] {
return html.split(/(<[^>]*>)/);
}
/**
* Extracts the lowercase tag name from a tag token, e.g. "
" → "h2".
* Returns empty string for comments, doctypes, etc.
*/
function tagName(token: string): string {
const m = token.match(/^<\/?([a-zA-Z][a-zA-Z0-9]*)/);
return m ? m[1].toLowerCase() : "";
}
function isOpeningTag(token: string): boolean {
return token.startsWith("<") && !token.startsWith("") && !token.startsWith("