import { HTMLParser } from '../inline';
import { Input, Recursion } from '../context';
import { List, Node } from '../../combinator/parser';
import { union, some, recursion, precedence, surround, open, match, lazy } from '../../combinator';
import { inline } from '../inline';
import { str } from '../source';
import { isNonblankFirstLine, blankWith } from '../visibility';
import { invalid, unwrap } from '../util';
import { memoize } from 'spica/memoize';
import { html as h, defrag } from 'typed-dom/dom';
const tags: readonly string[] = ['wbr', 'bdo', 'bdi'];
const attrspecs = {
wbr: {},
bdo: {
dir: Object.freeze(['ltr', 'rtl']),
},
} as const;
Object.setPrototypeOf(attrspecs, null);
Object.values(attrspecs).forEach(o => Object.setPrototypeOf(o, null));
export const html: HTMLParser = lazy(() => union([
surround(
// https://html.spec.whatwg.org/multipage/syntax.html#void-elements
str(/<(?:area|base|br|col|embed|hr|img|input|link|meta|source|track|wbr)(?=[ >])/y),
precedence(9, some(union([attribute]))),
open(str(/ ?/y), str('>'), true),
true, [],
([as, bs = new List(), cs], input, output) =>
output.append(new Node(elem(as.head!.value.slice(1), false, [...unwrap(as.import(bs).import(cs))], new List(), new List(), input), as.head!.position, as.head!.value === '
output.append(new Node(elem(as.head!.value.slice(1), false, [...unwrap(as.import(bs))], new List(), new List(), input)))),
match(
new RegExp(String.raw`<(${TAGS.join('|')})(?=[ >])`, 'y'),
memoize(
([, tag]) =>
surround(
surround(
str(`<${tag}`),
precedence(9, some(attribute)),
open(str(/ ?/y), str('>'), true),
true, [],
([as, bs = new List(), cs], _, output) => output.import(as.import(bs).import(cs)),
([as, bs = new List()], _, output) => output.import(as.import(bs))),
// 不可視のHTML構造が可視構造を変化させるべきでない。
// 可視のHTMLは優先度変更を検討する。
// このため`<>`記号は将来的に共通構造を変化させる可能性があり
// 共通構造を変化させない非構造文字列としては依然としてエスケープを要する。
precedence(0, recursion(Recursion.inline,
some(union([
some(inline, blankWith('\n', `${tag}>`)),
open('\n', some(inline, `${tag}>`), true),
])))),
str(`${tag}>`),
true, [],
([as, bs = new List(), cs], input, output) =>
output.append(new Node(elem(tag, true, [...unwrap(as)], bs, cs, input))),
([as, bs = new List()], input, output) =>
output.append(new Node(elem(tag, true, [...unwrap(as)], bs, new List(), input)))),
([, tag]) => tag2index(tag),
Array(TAGS.length))),
surround(
// https://html.spec.whatwg.org/multipage/syntax.html#void-elements
str(/<[a-z]+(?=[ >])/yi),
precedence(9, some(union([attribute]))),
open(str(/ ?/y), str('>'), true),
true, [],
([as, bs = new List(), cs], input, output) =>
output.append(new Node(elem(as.head!.value.slice(1), false, [...unwrap(as.import(bs).import(cs))], new List(), new List(), input))),
([as, bs = new List()], input, output) =>
output.append(new Node(elem(as.head!.value.slice(1), false, [...unwrap(as.import(bs))], new List(), new List(), input)))),
]));
export const attribute: HTMLParser.AttributeParser = union([
str(/ [a-z]+(?:-[a-z]+)*(?:="(?:\\[^\r\n]|[^\\\r\n"])*")?(?=[ >])/yi),
str(/ [^\s<>]+/y),
]);
function elem(tag: string, content: boolean, as: readonly string[], bs: List>, cs: List>, input: Input): HTMLElement {
assert(as.length > 0);
assert(as[0][0] === '<');
if (!tags.includes(tag)) return ielem('tag', `Invalid HTML tag name "${tag}"`, input);
if (content) {
if (cs.length === 0) return ielem('tag', `Missing the closing HTML tag "${tag}>"`, input);
if (bs.length === 0) return ielem('content', `Missing the content`, input);
if (!isNonblankFirstLine(bs)) return ielem('content', `Missing the visible content in the same line`, input);
}
const [attrs] = attributes('html', attrspecs[tag], as.slice(1, as.at(-1) === '>' ? -1 : as.length));
if (/(?') return ielem('tag', `Missing the closing symbol ">"`, input);
return h(tag as 'span', attrs, defrag(unwrap(bs)));
}
function ielem(type: string, message: string, input: Input): HTMLElement {
return h('span',
{ class: 'invalid', ...invalid('html', type, message) },
input.source.slice(input.position - input.range, input.position));
}
const requiredAttributes = memoize(
(spec: Readonly>) =>
Object.entries(spec).flatMap(([k, v]) => v && Object.isFrozen(v) ? [k] : []),
new WeakMap());
export function attributes(
syntax: string,
spec: Readonly> | undefined,
params: Iterable,
): [Record, string[]] {
assert(spec instanceof Object === false);
assert(!spec?.['__proto__']);
assert(!spec?.toString);
const remains = [];
let invalidation = false;
const attrs: Record = {};
for (const param of params) {
const attr = param.trimStart();
if (attr === '') continue;
const name = attr.split('=', 1)[0];
const value = attr !== name
? attr.slice(name.length + 2, -1).replace(/\\(.?)/g, '$1')
: undefined;
invalidation ||= name === '' || !spec || name in attrs;
if (name === '') continue;
if (spec && name in spec && !spec[name]) {
remains.push(param);
continue;
}
if (spec?.[name]?.includes(value) || spec?.[name]?.length === 0 && value !== undefined) {
attrs[name] = value ?? ''
}
else {
invalidation ||= !!spec;
}
assert(!(name in {} && attrs.hasOwnProperty(name)));
}
invalidation ||= !!spec && !requiredAttributes(spec).every(name => name in attrs);
if (invalidation) {
attrs['class'] = 'invalid';
Object.assign(attrs, invalid(syntax, 'argument', 'Invalid argument'));
}
return [attrs, remains];
}
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element
// [...document.querySelectorAll('tbody > tr > td:first-child')].map(el => el.textContent.slice(1, -1))
export const TAGS: readonly string[] = [
"html",
"base",
"head",
"link",
"meta",
"style",
"title",
"body",
"address",
"article",
"aside",
"footer",
"header",
"h1", "h2", "h3", "h4", "h5", "h6",
"hgroup",
"main",
"nav",
"section",
"blockquote",
"dd",
"div",
"dl",
"dt",
"figcaption",
"figure",
"hr",
"li",
"menu",
"ol",
"p",
"pre",
"ul",
"a",
"abbr",
"b",
"bdi",
"bdo",
"br",
"cite",
"code",
"data",
"dfn",
"em",
"i",
"kbd",
"mark",
"q",
"rp",
"rt",
"ruby",
"s",
"samp",
"small",
"span",
"strong",
"sub",
"sup",
"time",
"u",
"var",
"wbr",
"area",
"audio",
"img",
"map",
"track",
"video",
"embed",
"iframe",
"object",
"picture",
"portal",
"source",
"svg",
"math",
"canvas",
"noscript",
"script",
"del",
"ins",
"caption",
"col",
"colgroup",
"table",
"tbody",
"td",
"tfoot",
"th",
"thead",
"tr",
"button",
"datalist",
"fieldset",
"form",
"input",
"label",
"legend",
"meter",
"optgroup",
"option",
"output",
"progress",
"select",
"textarea",
"details",
"dialog",
"summary",
"slot",
"template",
"acronym",
"applet",
"bgsound",
"big",
"blink",
"center",
"content",
"dir",
"font",
"frame",
"frameset",
"image",
"keygen",
"marquee",
"menuitem",
"nobr",
"noembed",
"noframes",
"param",
"plaintext",
"rb",
"rtc",
"shadow",
"spacer",
"strike",
"tt",
"xmp",
];
const tag2index: (tag: string) => number = eval([
'tag => {',
'switch(tag){',
TAGS.map((tag, i) => `case '${tag}':return ${i};`).join(''),
'default: throw new Error();',
'}',
'}',
].join(''));