62 lines
2.8 KiB
TypeScript
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 === '<' ? '<' : '>');
|
|
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;
|
|
}
|