// HSML tag head format "tag#id.class1.class2~handler" export type HTagHeadName = keyof HTMLElementTagNameMap | `${string}${"-"}${string}`; export type HTagHeadAttr = "." | "#" | "~"; export type HTagHead = `${HTagHeadName}` | `${HTagHeadName}${HTagHeadAttr}${T}`; export type HAttrClasses = Array; // export type HAttrStyles = { [key: string]: string }; export type HAttrStyles = Partial; export type HDataValue = | string | String | number | Number | boolean | Boolean | Date | Array | Object; export type HAttrData = { [key: string]: HDataValue; }; export type HAttrOnDataFnc = (e: Event) => any; export type HAttrOnData = | HDataValue | HAttrOnDataFnc | null; export type HAttrOnCb = [keyof HTMLElementEventMap, EventListener]; export type HAttrOnAct = [ keyof HTMLElementEventMap | string, HAttrOnActType, HAttrOnData? ]; export type HAttrOn = HAttrOnCb | HAttrOnAct | Array>; export interface HTagAttrs { readonly _id?: string; readonly _classes?: string[]; readonly _ref?: string; readonly _hObj?: HObj; readonly key?: string; readonly skip?: boolean; readonly id?: string; readonly ref?: string; readonly classes?: HAttrClasses; readonly class?: string; readonly data?: HAttrData; readonly styles?: HAttrStyles; readonly style?: string; /** Event mapping to action, on: [event, action_type, action_data] */ readonly on?: HAttrOn; /** Custom validation error message that is displayed when a form is submitted. */ readonly validation?: { badInput?: string; patternMismatch?: string; rangeOverflow?: string; rangeUnderflow?: string; stepMismatch?: string; tooLong?: string; tooShort?: string; typeMismatch?: string; valueMissing?: string; }; // HTMLElement readonly title?: string; readonly lang?: string; readonly accessKey?: string; readonly href?: string; readonly target?: "_blank" | "_self" | "_parent" | "_top"; // HTMLFormElement readonly novalidate?: boolean | Boolean; // HTMLInputElement readonly type?: // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types | "text" | "hidden" | "password" | "email" | "number" | "search" | "url" | "tel" | "color" | "date" | "datetime-local" | "month" | "range" | "time" | "week" | "submit" | "button" | "radio" | "checkbox" | "file" | "image" | "reset" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types | "text/plain" | "text/html" | "text/css" | "text/javascript" // (.js)" | "text/csv" | "text/xml" | "image/png" | "image/jpeg" // (.jpg, .jpeg, .jfif, .pjpeg, .pjp)" | "image/svg+xml" // (.svg)" | "audio/mp3" | "audio/ogg" | "video/mp4" | "video/ogg" | "application/json" // | "application/ld+json" // (JSON-LD) // | "application/msword" // (.doc) | "application/pdf" // | "application/sql" // | "application/vnd.api+json" // | "application/vnd.ms-excel" // (.xls) // | "application/vnd.ms-powerpoint" // (.ppt)" // | "application/vnd.oasis.opendocument.text" // (.odt)" // | "application/vnd.oasis.opendocument.spreadsheet" // (.ods)" // | "application/vnd.oasis.opendocument.presentation" // (.odp)" // | "application/vnd.openxmlformats-officedocument.presentationml.presentation" // (.pptx)" // | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" // (.xlsx)" // | "application/vnd.openxmlformats-officedocument.wordprocessingml.document" // (.docx)" | "application/x-www-form-urlencoded" | "application/xml" // | "application/zip" // | "application/gzip" | "multipart/form-data" // | `text/${string}` // | `image/${string}` // | `audio/${string}` // | `video/${string}` // | `application/${string}` // | `message/${string}` // | `multipart/${string}` // | `${string}/${string}` // | string ; readonly name?: string; readonly value?: string | number | Number | String | null; readonly autocomplete?: string; readonly checked?: boolean | Boolean; readonly disabled?: boolean | Boolean; readonly list?: string; readonly max?: number | string; readonly maxlength?: number | string; readonly min?: number | string; readonly minlength?: number | string; readonly multiple?: boolean | Boolean; readonly pattern?: string; readonly placeholder?: string; readonly readonly?: boolean | Boolean; readonly required?: boolean | Boolean; readonly size?: number | string; readonly step?: number | string; // HTMLImageElement readonly src?: string; readonly alt?: string; readonly height?: number | string; readonly width?: number | string; // HTMLInputElement type file custom attributes readonly maxsize?: number; readonly convert?: "text" | "json" | "base64" | "object" | "dataurl" | "arraybuffer" | "stream"; // Generic attributes readonly [key: string]: | string | String | string[] | String[] | number | Number | boolean | Boolean | Date | HAttrClasses | HAttrStyles // | HsmlAttrData | HAttrOn | EventListener | HObj | null | undefined; } export type HFnc = (e: Element) => boolean | void; export interface HObj { toHsml?(): HElement; } export interface HElements extends Array> {} export type HTagChildren = | HElements | HFnc | HObj | string | String | boolean | Boolean | number | Number | Date | undefined | null; export type HTagNoAttr = [HTagHead, HTagChildren?]; export type HTagWithAttr = [HTagHead, HTagAttrs, HTagChildren?]; export type HTag = HTagNoAttr | HTagWithAttr; export type HElement = | HFnc | HObj | HTag | string | String | boolean | Boolean | number | Number | Date | undefined | null; export interface HHandlerCtx extends HObj { refs: { [name: string]: Element }; actionCb(action: HAttrOnActType, data: HAttrOnData, e: Event): void; } export interface HHandler> { open(tag: HTagHeadName, attrs: HTagAttrs, children: HElements, ctx?: C): boolean; close(tag: HTagHeadName, children: HElements, ctx?: C): void; text(text: string, ctx?: C): void; fnc(fnc: HFnc, ctx?: C): void; obj(obj: HObj, ctx?: C): void; } /** NO-BREAK SPACE */ export const NBSP = "\u00A0"; /** NARROW NO-BREAK SPACE */ export const NNBSP = "\u202F"; /** THIN SPACE (&thinsp) */ export const THSP = "\u2009"; /** NUM/FIGURE SPACE (&numsp) */ export const NUMSP = "\u2007"; const headRegex = /^([^#.~]*)(?:#([^.~]*))?(?:\.([^~]*))?(?:~(.*))?$/; export function hsml>( hml: HElement, handler: HHandler, ctx?: C): void { // console.log("hsml", hsml); if (hml === undefined || hml === null) { return; } if (Array.isArray(hml)) { // const tag = hml as HTag; // if ( // ( // tag.length === 1 && // tag[0].constructor === String // ) || // ( // tag.length === 2 && // ( // tag[0].constructor === String && // (tag[1]!.constructor === Array || tag[1]!.constructor === Function) // ) || // ( // tag[0].constructor === String && // tag[1]!.constructor === Object // ) // ) || // ( // tag.length === 3 && // tag[0].constructor === String && // tag[1].constructor === Object && // tag[2]!.constructor === Array // ) // ) { // hsmlTag(hml as HTag, handler, ctx); // } else { // console.error("hsml parse error:", hml); // // console.error("hsml parse error:", JSON.stringify(hml, null, 4)); // // throw Error(`hsml parse error: ${JSON.stringify(hml)}`); // } hsmlTag(hml as HTag, handler, ctx); } else if (typeof hml === "function") { handler.fnc(hml as HFnc, ctx); } else if (typeof hml === "string") { handler.text(hml as string, ctx); } else if (typeof hml === "boolean") { handler.text("" + hml, ctx); } else if (typeof hml === "number") { const n = hml as number; const ns = n.toLocaleString ? n.toLocaleString() : n.toString(); handler.text(ns, ctx); } else if (hml instanceof Date) { const d = hml as Date; const ds = d.toLocaleString ? d.toLocaleString() : d.toString(); handler.text(ds, ctx); // } else { // HObj // handler.obj(hml as HObj, ctx); // } } else if (typeof hml === "object") { // HObj handler.obj(hml as HObj, ctx); } else { console.warn("hsml: unexpected element type:", hml); } function hsmlTag(hmlTag: HTag, handler: HHandler, ctx?: C): void { // console.log("hsml tag", hmlTag); if (typeof hmlTag[0] !== "string") { console.error("hsml: tag head is not a string:", hmlTag); return; } const head = hmlTag[0] as HTagHead; const attrsObj = hmlTag[1] as any; const hasAttrs = attrsObj !== null && attrsObj !== undefined && !Array.isArray(attrsObj) && typeof attrsObj === "object" && !(attrsObj instanceof Date); const childIdx = hasAttrs ? 2 : 1; let children: HElements = []; let hFnc: HFnc | undefined; let hObj: HObj | undefined; const htc = hmlTag[childIdx]; if (htc != null) { if (Array.isArray(htc)) { children = htc as HElements; } else if (typeof htc === "function") { hFnc = htc as HFnc; } else if ( typeof htc === "string" || typeof htc === "boolean" || typeof htc === "number" || htc instanceof Date ) { children = [htc as string | boolean | number | Date]; } else { // HObj hObj = htc as HObj; } } // const refSplit = head.split("~"); // const ref = refSplit[1]; // const dotSplit = refSplit[0].split("."); // const hashSplit = dotSplit[0].split("#"); // const tag = (hashSplit[0] ?? "div") as HTagHeadName; // const id = hashSplit[1]; // const classes = dotSplit.slice(1); // let attrs: HTagAttrs; // if (hasAttrs) { // attrs = attrsObj as HTagAttrs; // } else { // attrs = {} as HTagAttrs; // } // if (id) { // (attrs as any)._id = id; // } // if (classes.length) { // (attrs as any)._classes = classes; // } // if (ref) { // (attrs as any)._ref = ref; // } // if (hObj) { // (attrs as any)._hObj = hObj; // } /** * The regex matches the following parts of the head string: * 1. Tag name: The first part of the string before any of the special characters (#, ., ~). If not specified, it defaults to "div". * 2. ID: The part of the string after the # character and before any . or ~ characters. This is optional. * 3. Classes: The part of the string after the . character and before any ~ character. Multiple classes can be specified by separating them with dots (e.g., .class1.class2). * 4. Ref: The part of the string after the ~ character. This is optional and can be used to reference the element in code. */ const match = head.match(headRegex); const tag = (match![1] || "div") as HTagHeadName; const id = match![2]; const classes = match![3] ? match![3].split(".") : []; const ref = match![4]; const attrs: HTagAttrs = { ...(hasAttrs ? attrsObj as HTagAttrs : {}), ...(id && { _id: id }), ...(classes.length && { _classes: classes }), ...(ref && { _ref: ref }), ...(hObj && { _hObj: hObj }), } as HTagAttrs; const skip = handler.open(tag, attrs, children, ctx); if (hFnc) { handler.fnc(hFnc, ctx); } if (!skip) { children.forEach(jml => hsml(jml, handler, ctx)); } handler.close(tag, children, ctx); } } export function hjoin(hsmls: HElements, sep: string | HElement): HElements { if (hsmls.length === 0) { return [] as HElements; } const r = hsmls.reduce>( (p, c) => (p.push(c, sep), p), [] as HElements ); r.splice(-1); return r; }