import { Parser, List, Node } from '../combinator/parser'; import { Input, Command } from './context'; import { always, fmap } from '../combinator'; import { invisibleBlankHTMLEntityNames } from '../api/normalize'; import { isWhitespace } from './source'; namespace blank { export const line = new RegExp( /((?:^|\n)[^\S\r\n]*(?=\S))((?:[^\S\r\n]|\\(?=$|\s)|&IBHN;|)+(?=$|\r?\n))/g.source .replace('IBHN', `(?:${invisibleBlankHTMLEntityNames.join('|')})`), 'g'); export const start = new RegExp( /(?:[^\S\r\n]|\\(?=$|\s)|&IBHN;|)+/y.source .replace('IBHN', `(?:${invisibleBlankHTMLEntityNames.join('|')})`), 'y'); export const unit = new RegExp( /(?:[^\S\r\n]|\\(?=$|\s)|&IBHN;|)/y.source .replace('IBHN', `(?:${invisibleBlankHTMLEntityNames.join('|')})`), 'y'); } export function visualize

(parser: P): P; export function visualize(parser: Parser): Parser { interface Memory { readonly scope: boolean; } return always>>([ (input, output) => { const { source, position } = input; const src = source.slice(position).replace(blank.line, `$1${Command.Escape}$2`); input.memory = { scope: src.length !== source.length - position, }; if (input.memory.scope) { input.scope.push(src); } return output.context; }, parser, (input, output) => { if (input.memory.scope) { input = input.scope.pop(); } input.position = input.source.length; return output.context; }, ]); } export const beforeNonblank = beforeNonblankWith(''); export const afterNonblank = afterNonblankWith(''); export function blankWith(starts: '\n', delimiter: string | RegExp): RegExp { return new RegExp([ // 空行除去 // 完全な空行はエスケープ済みなので再帰的バックトラックにはならない。 String.raw`(?:${starts}(?:\\?\s|&(?:${invisibleBlankHTMLEntityNames.join('|')});|)*)?`, typeof delimiter === 'string' ? delimiter.replace(/[*+()\[\]]/g, '\\$&') : delimiter.source, ].join(''), 'y'); } export function beforeNonblankWith(delimiter: string | RegExp): RegExp { return new RegExp([ typeof delimiter === 'string' ? delimiter.replace(/[*+()\[\]]/g, '\\$&') : delimiter.source, String.raw`(?!\\?\s|&(?:${invisibleBlankHTMLEntityNames.join('|')});|)`, ].join(''), 'y'); } function afterNonblankWith(delimiter: string | RegExp): RegExp { return new RegExp([ String.raw`(?)`, typeof delimiter === 'string' ? delimiter.replace(/[*+()\[\]]/g, '\\$&') : delimiter.source, ].join(''), 'y'); } export function isNonblankFirstLine(nodes: List>): boolean { if (nodes.length === 0) return true; for (const node of nodes) { if (isNonblank(node)) return true; if (node.flags & Node.Flag.blank && typeof node.value === 'object' && node.value.tagName === 'BR') break; } return false; } export function isNonblankNodeStart(nodes: List>): boolean { if (nodes.length === 0) return true; return isNonblank(nodes.head!, 0); } function isNonblank({ value: node, flags }: Node, strpos?: number): boolean { if (flags & Node.Flag.blank) return false; if (typeof node !== 'string') return true; const str = node && strpos !== undefined ? node[strpos >= 0 ? strpos : node.length + strpos] : node; switch (str) { case '': case ' ': case '\t': case '\r': case '\n': return false; default: return !isWhitespace(str.trimStart()); } } // 終端が必要な場合は無駄な後方トリムを避けるためtrimBlankStart+trimBlankNodeEndで処理する。 export function trimBlank

>(parser: P): P; export function trimBlank(parser: Parser): Parser { return trimBlankStart(trimBlankEnd(parser)); } function trimBlankStart

(parser: P): P; function trimBlankStart(parser: Parser): Parser { return (input, output) => { const { source, position } = input; if (position === source.length) return; const reg = blank.start; reg.lastIndex = position; reg.test(source); input.position = reg.lastIndex || position; return input.position === source.length ? output.context : parser(input, output); }; } export function trimBlankEnd

>(parser: P): P; export function trimBlankEnd(parser: Parser): Parser { return fmap(parser, trimBlankNodeEnd); } export function trimBlankNodeEnd(nodes: List>): List> { const skip = nodes.last && ~nodes.last.flags & Node.Flag.blank && typeof nodes.last.value === 'object' ? nodes.last.value.className === 'indexer' : false; for (let node = skip ? nodes.last?.prev : nodes.last; node;) { if (~node.flags & Node.Flag.blank) { if (typeof node.value === 'string') { const str = node.value.trimEnd(); if (str.length > 0) { node.value = str; break; } } else { break; } } const target = node; node = node.prev; nodes.delete(target); } return nodes; }