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)]};`); }