import { escapeHtml } from "../inline/render.js";
import { isFalsyAttrValue, renderAttrs as renderAttrsRaw } from "./attribute.js";
import type { BlockHandler } from "./handler.type.js";
const FORM_BOOLEAN_ATTRS: ReadonlySet = new Set([
"checked",
"disabled",
"readonly",
"required",
"multiple",
"autofocus",
]);
function renderAttrs(attrs?: Record): string {
return renderAttrsRaw(attrs, FORM_BOOLEAN_ATTRS);
}
const ALLOWED_TAGS = new Set([
"input",
"select",
"textarea",
"output",
"button",
"meter",
"progress",
]);
/**
* Parse an Emmet-style abbreviation into tag name and type attribute.
*
* Examples:
* "input:date" → { tag: "input", type: "date" }
* "input:text" → { tag: "input", type: "text" }
* "select" → { tag: "select", type: undefined }
* "textarea" → { tag: "textarea", type: undefined }
*/
function parseEmmet(abbr: string): { tag: string; type?: string } {
const colon = abbr.indexOf(":");
if (colon === -1) return { tag: abbr };
return { tag: abbr.slice(0, colon), type: abbr.slice(colon + 1) };
}
/**
* Factory: create a BlockHandler for `html:` fenced code blocks.
*
* Only allows form-related elements: input, select, textarea,
* output, button, meter, progress.
* Throws on unknown/disallowed elements.
*/
export function createHtmlHandler(emmetAbbr: string): BlockHandler {
const { tag, type } = parseEmmet(emmetAbbr);
if (!ALLOWED_TAGS.has(tag)) {
throw new Error(`html: block — disallowed element "${tag}"`);
}
return {
render(_raw: string, attrs?: Record): string {
switch (tag) {
case "select":
return renderSelect(attrs);
case "textarea":
return renderTextarea(attrs);
case "output":
return renderOutput(attrs);
case "button":
return renderButton(type, attrs);
case "meter":
return renderMeter(attrs);
case "progress":
return renderProgress(attrs);
default:
return renderInput(type, attrs);
}
},
serialize(raw: string, attrs?: Record): string {
const body =
raw || (attrs && Object.keys(attrs).length > 0 ? JSON.stringify(attrs, null, 2) : "");
return `\`\`\`html:${emmetAbbr}\n${body}\n\`\`\``;
},
};
}
function renderInput(type: string | undefined, attrs?: Record): string {
// The Emmet type wins over a `type` key in the body, which would otherwise
// emit a second, invalid `type` attribute.
const { type: attrType, ...rest } = attrs ?? {};
const resolvedType = type ?? attrType;
const parts = ["");
return parts.join("");
}
function renderSelect(attrs?: Record): string {
const { options: optionsJson, value, ...rest } = attrs ?? {};
const selected = selectedValues(value, !isFalsyAttrValue(rest.multiple));
const parts = ["");
return parts.join("");
}
function selectedValues(value: string | undefined, multiple: boolean): ReadonlySet {
if (value === undefined) return new Set();
if (multiple) {
try {
const parsed: unknown = JSON.parse(value);
if (Array.isArray(parsed)) return new Set(parsed.map(String));
} catch {}
}
return new Set([value]);
}
function renderTextarea(attrs?: Record): string {
const { value, ...rest } = attrs ?? {};
const parts = ["");
return parts.join("");
}
function renderOutput(attrs?: Record): string {
const { value, ...rest } = attrs ?? {};
const parts = ["");
return parts.join("");
}
function renderButton(type: string | undefined, attrs?: Record): string {
const { label, type: attrType, ...rest } = attrs ?? {};
const resolvedType = type ?? attrType;
const parts = ["");
return parts.join("");
}
function renderMeter(attrs?: Record): string {
const { label, ...rest } = attrs ?? {};
const parts = ["");
if (label) parts.push(escapeHtml(label));
parts.push("");
return parts.join("");
}
function renderProgress(attrs?: Record): string {
const { label, ...rest } = attrs ?? {};
const parts = ["");
return parts.join("");
}