Assessor_2.0/src/importers/html-attribute-markup.ts

62 lines
2.8 KiB
TypeScript

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)) {
const end = text.indexOf('-->', 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 === '<' ? '&lt;' : '&gt;');
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('</' + opening[1] + '\\s*>', '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;
}