import { ExtensionParser } from '../../block'; import { Segment } from '../../context'; import { List, Node } from '../../../combinator/parser'; import { union, inits, sequence, some, scope, block, line, fence, rewrite, close, match, fallback, fmap } from '../../../combinator'; import { str, contentline, emptyline } from '../../source'; import { label, test as test_label } from '../../inline/extension/label'; import { ulist } from '../ulist'; import { olist } from '../olist'; import { table as styled_table } from '../table'; import { codeblock, segment_ as seg_code } from '../codeblock'; import { mathblock, segment_ as seg_math } from '../mathblock'; import { example } from './example'; import { table } from './table'; import { placeholder, segment_ as seg_placeholder } from './placeholder'; import { blockquote, segment as seg_blockquote } from '../blockquote'; import { inline, media, lineshortmedia } from '../../inline'; import { visualize, trimBlank } from '../../visibility'; import { unwrap, invalid } from '../../util'; import { memoize } from 'spica/memoize'; import { html, defrag } from 'typed-dom/dom'; import FigureParser = ExtensionParser.FigureParser; export const segment: FigureParser.SegmentParser = block(match( /(~{3,})(?:figure )?(?=\[?\$)/y, memoize( ([, fence], closer = new RegExp(String.raw`${fence}[^\S\r\n]*(?:$|\r?\n)`, 'y')) => close( sequence([ contentline, inits([ // All parsers which can include closing terms. union([ seg_code, seg_math, seg_placeholder, seg_blockquote, some(contentline, closer), ]), emptyline, union([ emptyline, some(contentline, closer), ]), ]), ]), closer), ([, fence]) => fence.length - 1, [], 2 ** 4 - 1)), true, Segment.figure); export const figure: FigureParser = block(fallback(rewrite(segment, fmap( scope(({ source }) => source.slice(source.match(/^~+(?:\w+\s+)?/)![0].length, source.trimEnd().lastIndexOf('\n')), sequence([ line(sequence([label, str(/(?!\S)[^\r\n]*\r?\n/y)]), false), inits([ block(union([ ulist, olist, styled_table, codeblock, mathblock, example, table, placeholder, blockquote, line(media, false), line(lineshortmedia, false), ])), emptyline, block(visualize(trimBlank(some(inline)))), ]), ]), false), nodes => { const [label, param, content, ...caption] = unwrap(nodes) as [HTMLAnchorElement, string, ...HTMLElement[]]; return new List([ new Node(html('figure', attributes(label.getAttribute('data-label')!, param, content, caption), [ html('figcaption', [ html('span', { class: 'figindex' }), html('span', { class: 'figtext' }, defrag(caption)), ]), html('div', [content]), ])) ]); })), inits([ fence(/(~{3,})(?:figure(?=$|[ \r\n])|\[?\$)[^\r\n]*(?:$|\r?\n)/y, true), (_, output) => { const [body, overflow, closer, opener, delim] = unwrap(output.pop()) as string[]; const violation = !closer && [ 'fence', `Missing the closing delimiter "${delim}"`, ] || overflow && [ 'fence', `Invalid trailing line after the closing delimiter "${delim}"`, ] || !test_label(opener.match(/^~+(?:figure )?(\[?\$\S+)/)?.[1] ?? '') && [ 'label', 'Invalid label', ] || /^~+(?:figure )?(\[?\$\S+)[^\S\r\n]+\S/.test(opener) && [ 'argument', 'Invalid argument', ] || [ 'content', 'Invalid content', ]; return output.append( new Node(html('pre', { class: 'invalid', translate: 'no', ...invalid('figure', violation[0], violation[1]), }, `${opener}${body}${closer}${overflow}`))); }, ]))); function attributes(label: string, param: string, content: HTMLElement, caption: readonly HTMLElement[]): Record { const group = label.split('-', 1)[0]; let type = content.className.split(/\s/)[0]; switch (type || content.tagName) { case 'UL': case 'OL': case 'checklist': type = 'list'; break; case 'TABLE': type = 'table'; break; case 'BLOCKQUOTE': type = 'quote'; break; case 'A': type = 'media'; break; case 'text': case 'code': case 'math': case 'example': case 'invalid': break; default: assert(false); } const violation = param.trimStart() !== '' && [ 'argument', 'Invalid argument', ] || /^[^-]+-(?:[0-9]+\.)*0$/.test(label) && [ 'label', 'The last part of the fixed label numbers must not be 0', ] || group === '$' && (type !== 'math' || caption.length > 0) && [ 'label', '"$" label group must be used to math formulas with no caption', ] || type === 'media' && [ ] || ['fig', 'figure'].includes(group) && [ 'label', '"fig" and "figure" label groups must be used to media', ] || group === 'table' && type !== group && [ 'label', '"table" label group must be used to tables', ] || group === 'list' && type !== group && [ 'label', '"list" label group must be used to lists', ] || group === 'quote' && type !== group && [ 'label', '"quote" label group must be used to blockquotes', ] || group === 'text' && type !== group && [ 'label', '"text" label group must be used to codeblocks with no language', ] || group === 'code' && type !== group && [ 'label', '"code" label group must be used to codeblocks with any language', ] || group === 'example' && type !== group && [ 'label', '"example" label group must be used to examples', ] || undefined; return { 'data-type': type, 'data-label': label, 'data-group': group, ...violation?.[0] && { class: 'invalid', ...invalid('figure', violation[0], violation[1]), }, }; }