import { ImportError } from '../shared/errors'; /** * Keep angle brackets inside quoted attributes inert for the bounded lexical * readers. This is not a sanitizer: values and entities remain unchanged apart * from those delimiters. Raw-text elements and comments are never interpreted * as nested tags. The original document remains untouched. */ export function protectHtmlAttributeMarkup(text: string, allowIncompleteFinalTag = false): string { const output: string[] = []; let copied = 0; let position = 0; while ((position = text.indexOf('<', position)) >= 0) { if (text.startsWith('', position + 4); position = end < 0 ? text.length : end + 3; continue; } const opening = /^<\/?([a-z][\w:-]*)(?=[\s/>])/i.exec(text.slice(position, position + 256)); if (!opening) { position++; continue; } const start = position; const previousOutputLength = output.length; const previousCopied = copied; let quote = ''; let expectingValue = false; let unquotedValue = false; let closed = false; for (position += opening[0].length; position < text.length; position++) { const character = text[position]; if (quote) { if (character === quote) quote = ''; else if (character === '<' || character === '>') { output.push(text.slice(copied, position), character === '<' ? '<' : '>'); copied = position + 1; } } else if (character === '>') { position++; closed = true; break; } else if (unquotedValue) { if (/\s/.test(character)) unquotedValue = false; } else if (expectingValue) { if (/\s/.test(character)) continue; expectingValue = false; if (character === '"' || character === "'") quote = character; else unquotedValue = true; } else if (character === '=') expectingValue = true; } // Bounded text fragments may end within a tag even though the complete // document was valid. Drop only that unfinished tag, never expose its tail. if (!closed && allowIncompleteFinalTag) return output.slice(0, previousOutputLength).join('') + text.slice(previousCopied, start); if (!closed && quote) throw new ImportError('HTML enthält ein nicht abgeschlossenes Attribut.', [{ severity: 'error', code: 'html-unclosed-attribute', stage: 'structure-decoding', message: 'Eine Attributzeichenkette endet nicht innerhalb des HTML-Dokuments.' }]); if (text[start + 1] !== '/' && /^(script|style|textarea|title|xmp|iframe|noembed|noframes)$/i.test(opening[1])) { const closing = new RegExp('', 'ig'); closing.lastIndex = position; const end = closing.exec(text); position = end ? end.index + end[0].length : text.length; } } return output.length ? output.join('') + text.slice(copied) : text; }