import { Parser, List, Node } from '../../../combinator/parser'; import { Input } from '../../context'; import { fmap } from '../../../combinator'; import { define } from 'typed-dom/dom'; export function indexee

>(parser: P): P; export function indexee(parser: Parser): Parser { return fmap(parser, (ns, { id }) => ns.length === 1 ? new List([new Node(define(ns.head!.value, { id: identity('index', id, ns.head!.value), 'data-index': null }))]) : ns); } const table = [ ...[...Array(36)].map((_, i) => i.toString(36)), ...[...Array(36)].map((_, i) => i.toString(36).toUpperCase()).slice(-26), '-', '=', ].join(''); assert(table.length === 64); const MAX = 64 - '='.length - Math.ceil(Math.log(~0 >>> 0) / Math.log(table.length)); assert(MAX === 57); const ELLIPSIS = '...'; const PART = (MAX - ELLIPSIS.length) / 2 | 0; const REM = MAX - PART * 2 - ELLIPSIS.length; assert(PART * 2 + REM + ELLIPSIS.length === MAX); export function identity( type: 'index' | 'mark' | '', id: string | undefined, text: string | HTMLElement, ): string | undefined { assert(id?.match(/^[0-9a-z/-]*$/i) ?? true); if (id === '') return undefined; if (typeof text !== 'string') { const index = text.getAttribute('data-index') ?? undefined; if (index === '' && text.tagName === 'LI') return undefined; return index ? `${type}:${id ?? ''}:${index}` : identity(type, id, signature(text.cloneNode(true))); } text = text.trim(); if (text === '') return undefined; const str = text.replace(/\s/g, '_'); const cs = [...str]; if (type === '' || cs.length <= MAX) { return `${type}:${id ?? ''}:${str}${/_|[^\S ]|=[0-9A-Za-z]{1,6}$/.test(text) ? `=${hash(text)}` : ''}`; } const s1 = cs.slice(0, PART + REM).join(''); const s2 = cs.slice(-PART).join(''); assert([...`${s1}${ELLIPSIS}${s2}`].length === MAX); return `${type}:${id ?? ''}:${s1}${ELLIPSIS}${s2}=${hash(text)}`; } assert.deepStrictEqual( identity('index', undefined, ' 0 '), identity('index', undefined, ' 0 '.trim())); assert.notDeepStrictEqual( identity('index', undefined, '0 0'), identity('index', undefined, '0 0')); assert.notDeepStrictEqual( identity('index', undefined, '0 0'), identity('index', undefined, '0_0')); assert.notDeepStrictEqual( identity('index', undefined, '0 0'), identity('index', undefined, '0\t0')); assert.notDeepStrictEqual( identity('index', undefined, '0_0'), identity('index', undefined, identity('index', undefined, '0_0')!.slice(7).replace('_', ' '))); assert.deepStrictEqual( identity('index', undefined, `${'0'.repeat(MAX - 1)}1`)!.slice(7), `${'0'.repeat(MAX - 1)}1`); assert.deepStrictEqual( identity('index', undefined, `0${'1'.repeat(MAX / 2)}${'2'.repeat(MAX / 2)}3`)!.slice(7), `0${'1'.repeat(PART + REM - 1)}${ELLIPSIS}${'2'.repeat(PART - 1)}3=E0deO`); assert.deepStrictEqual( identity('index', undefined, `0${'1'.repeat(MAX * 2)}${'2'.repeat(MAX * 2)}3`)!.slice(7), `0${'1'.repeat(PART + REM - 1)}${ELLIPSIS}${'2'.repeat(PART - 1)}3=2RHwOS`); function hash(source: string): string { let x = 0; for (let i = 0; i < source.length; ++i) { const c = source.charCodeAt(i); x = x ^ c << 1 || ~x ^ c << 1; // 16+1bit assert(x !== 0); x ^= x << 13; // shift <= 32-17bit x ^= x >>> 17; x ^= x << 15; } return baseR(x >>> 0, 62); } assert(hash('\x00') !== '0'); assert(hash('\x01') !== '0'); assert(hash('\x00') !== hash(String.fromCharCode(1 << 15))); // 62も64も最大6桁 assert(Math.ceil(Math.log(~0 >>> 0) / Math.log(62)) === 6); assert(Math.ceil(Math.log(~0 >>> 0) / Math.log(64)) === 6); function baseR(n: number, r: number): string { assert(n >= 0); assert(Math.floor(n) === n); assert(r <= 64); let acc = ''; do { const mod = n % r; n = (n - mod) / r; assert(Math.floor(n) === n); acc = table[mod] + acc; } while (n > 0) assert(acc !== ''); return acc; } assert(baseR(0, 36) === (0).toString(36)); assert(baseR(~0 >>> 0, 36) === (~0 >>> 0).toString(36)); assert(baseR(0, 62) === '0'); assert(baseR(61, 62) === 'Z'); assert(baseR(62, 62) === '10'); export function signature(target: Element | DocumentFragment): string { assert(!target.parentNode); assert(!target.querySelector('br:not(:has(+ :is(ul, ol)))') || target.nodeName === 'MARK'); for (let es = target.querySelectorAll('code[data-src], .math[data-src], .remark, rt, rp, br, .annotation, .reference, :is(.annotation, .reference) > a, .checkbox, ul, ol, .label[data-label]'), i = es.length; i--;) { const el = es[i]; switch (el.className) { case 'math': el.replaceWith(el.getAttribute('data-src')!); continue; case 'label': el.replaceWith(`[$${el.getAttribute('data-label')!.replace('$', '')}]`); continue; case 'annotation': el.replaceWith(`((${el.textContent}))`); continue; case 'reference': const abbr = el.getAttribute('data-abbr'); el.replaceWith(`[[${abbr ? `^${abbr}` : el.textContent}]]`); continue; case 'checkbox': case 'remark': el.remove(); continue; } switch (el.tagName) { case 'CODE': el.replaceWith(el.getAttribute('data-src')!); continue; case 'UL': case 'OL': case 'RT': case 'RP': case 'A': el.remove(); continue; case 'BR': el.replaceWith('\n'); continue; } } return target.textContent!.trim(); } export function text(target: Element | DocumentFragment): string { assert(!target.parentNode); assert(!target.querySelector('br:not(:has(+ :is(ul, ol)))')); for (let es = target.querySelectorAll('code[data-src], .math[data-src], .remark, rt, rp, br, .annotation, .reference, :is(.annotation, .reference) > a, .checkbox, ul, ol'), i = es.length; i--;) { const el = es[i]; switch (el.className) { case 'math': el.replaceWith(el.getAttribute('data-src')!); continue; case 'annotation': el.replaceWith(`((${el.textContent}))`); continue; case 'reference': const abbr = el.getAttribute('data-abbr'); el.replaceWith(`[[${abbr ? `^${abbr}` : el.textContent}]]`); continue; case 'checkbox': case 'remark': el.remove(); continue; } switch (el.tagName) { case 'CODE': el.replaceWith(el.getAttribute('data-src')!); continue; case 'UL': case 'OL': case 'RT': case 'RP': case 'A': case 'BR': el.remove(); continue; } } return target.textContent!; }