import { html } from 'typed-dom/dom';
const UNICODE_REPLACEMENT_CHARACTER = '\uFFFD';
assert(UNICODE_REPLACEMENT_CHARACTER.trim());
export function normalize(source: string): string {
return sanitize(format(source));
}
function format(source: string): string {
return source.replace(/\r\n?|[\u2028\u2029]/g, '\n');
}
const invalid = new RegExp([
/(?![\t\r\n])[\x00-\x1F\x7F]/g.source,
/(?![\u200C\u200D])[\u2006\u200B-\u200F\u202A-\u202F\u2060\uFEFF]/g.source,
// 後読みが重い
///(? (entity: string): string => {
if (entity === '
') return entity;
el.innerHTML = entity;
return el.textContent!;
})(html('span'));
export const invisibleBlankHTMLEntityNames: readonly string[] = invisibleHTMLEntityNames
.filter(name => parser(`&${name};`).trimStart() === '');
assert(invisibleBlankHTMLEntityNames.every(name => /^\s$/.test(parser(`&${name};`))));
export const invisibleGraphHTMLEntityNames: readonly string[] = invisibleHTMLEntityNames
.filter(name => parser(`&${name};`).trimStart() !== '');
const unreadableEscapeHTMLEntityNames = invisibleHTMLEntityNames.filter(name => ![
'Tab',
'NewLine',
'NonBreakingSpace',
'nbsp',
'zwj',
'zwnj',
].includes(name));
const unreadableEscapeCharacters = unreadableEscapeHTMLEntityNames
.map(name => parser(`&${name};`));
assert(unreadableEscapeCharacters.length === unreadableEscapeHTMLEntityNames.length);
assert(unreadableEscapeCharacters.every(c => c.length === 1));
const unreadableEscapeCharacter = new RegExp(`[${unreadableEscapeCharacters.join('')}]`, 'g');
assert(!unreadableEscapeCharacter.source.includes('&'));
// https://www.pandanoir.info/entry/2018/03/11/193000
// http://anti.rosx.net/etc/memo/002_space.html
// http://nicowiki.com/%E7%A9%BA%E7%99%BD%E3%83%BB%E7%89%B9%E6%AE%8A%E8%A8%98%E5%8F%B7.html
const unreadableSpecialCharacters = [
// SIX-PER-EM SPACE
'\u2006',
// ZERO WIDTH SPACE
'\u200B',
// ZERO WIDTH NON-JOINER
//'\u200C',
// ZERO WIDTH JOINER
//'\u200D',
// LEFT-TO-RIGHT MARK
'\u200E',
// RIGHT-TO-LEFT MARK
'\u200F',
// LEFT-TO-RIGHT EMBEDDING
'\u202A',
// RIGHT-TO-LEFT EMBEDDING
'\u202B',
// POP DIRECTIONAL FORMATTING
'\u202C',
// LEFT-TO-RIGHT OVERRIDE
'\u202D',
// RIGHT-TO-LEFT OVERRIDE
'\u202E',
// NARROW NO-BREAK SPACE
'\u202F',
// WORD JOINER
'\u2060',
// ZERO WIDTH NON-BREAKING SPACE
'\uFEFF',
] as const;
assert(unreadableSpecialCharacters.every(c => sanitize(c) === UNICODE_REPLACEMENT_CHARACTER));
// 特殊不可視文字はエディタおよびソースビューアでは等幅および強調表示により可視化する
export function escape(source: string): string {
return source.replace(unreadableEscapeCharacter, char =>
`&${unreadableEscapeHTMLEntityNames[unreadableEscapeCharacters.indexOf(char)]};`);
}