// a hiccup-like HTML domain specific language// inspired by https://gist.github.com/hns/654226import{isArray}from"utils/array";import{component}from"./components";importtype{PageSyntax,HtmlAttributes,HtmlTag,HtmlNode,Dependency,}from"../types/html";import{isHtmlAttributes}from"./parseDSL";/** * Convert HTML to a string. */functionhtml(...args: PageSyntax[]){letbuffer: string[]=[];letdependencies: Dependency[]=[];build(args,buffer,dependencies);return{dependsOn: dependencies,body: buffer.join(""),};}/** * Render a PageSyntax node to an HTML page string. * Include front matter that configures the document as a whole. */functionhtmlPage(...args: PageSyntax[]){const{ dependsOn, body }=html(...args);return{
dependsOn,body: `<!DOCTYPE html>${body}`,};}/** * Build the HTML attributes between the tag. * @param attrs the attributes to build into the JS. * @param buffer buffer to queue the changes to. */functionbuildAttributes(attrs: HtmlAttributes,dependencies: Dependency[]){if(!attrs||Object.keys(attrs).length===0){return"";}// The buffer starts with an empty string so the `.join`// operation prefixes the attributes with a space if they exist!constbuffer: string[]=[""];for(varkeyinattrs){buffer.push(`${key}="${attrs[key]?.toString()}"`);}if(attrs.href){dependencies.push({src: attrs.href});}if(attrs.src){dependencies.push({src: attrs.src});}returnbuffer.join(" ");}/** * Build a custom component. * @param name the name of the component * @param args the arguments to the component */functionbuildComponent(name: string,args: object,buffer: string[],dependencies: Dependency[]){const{ dependsOn, body }=component(name,args);build(body,buffer,dependencies);dependencies.push(...dependsOn);}/** * Build a single HTML tag. * The callback proceeds to build the rest of the page sequentially. */functionbuildTag(tagName: string,attributes: HtmlAttributes,buffer: string[],dependencies: Dependency[],list: HtmlNode,index: number){constisComponent=tagName[0]===tagName[0].toUpperCase();if(isComponent){buildComponent(tagName,{ ...attributes,children: list?.slice(index)},buffer,dependencies);return;}// Create the start of the tag: <tag { .. attrs .. }>buffer.push(`<${tagName}${buildAttributes(attributes,dependencies)}>`);// Build the contents of the tag - an arbitrary array of elements.buildRest(list,index,buffer,dependencies);// Close the tag: </ tag >buffer.push(`</${tagName}>`);}/** * Build an HTML configuration from the list of nodes, * adding the stringified representation to the buffer. */functionbuild(list: HtmlNode,buffer: string[],dependencies: Dependency[]){letindex=0;letnextElement=list?.[index];// if our next element is the start of an HTML tag:if(typeofnextElement==="string"){// split the tag to get potential `id` or `class` syntax from the tag name.const[tagName,attr]=splitTag(nextElement);index+=1;letattributesToUse=attr;nextElement=list?.[index];// If, after the tag, we have more attributes,// merge them with the attributes we found when splitting the tag.if(isHtmlAttributes(nextElement)){attributesToUse=mergeAttributes(attr,nextElement);index+=1;}buildTag(tagName,attributesToUse,buffer,dependencies,list,index);}else{// if we don't have a tag, we know we have an array of tags. Process those.buildRest(list,index,buffer,dependencies);}}/** * Build an arbitrary HTML node. * @param list the HTML node(s) to process. * @param index our current index into the HTML node list. * @param buffer our output string buffer. */functionbuildRest(list: HtmlNode,index: number,buffer: string[],dependencies: Dependency[]){constlength=list?.length??0;while(index<length){varitem=list?.[index++];if(isArray(item)){build(item,buffer,dependencies);}else{// NOTE: The information is not encompassed in the type (yet)// but it's possible for anything in the tree to be undefined.// This makes it easier for us to use conditionals and return falsy// elements if the item isn't truthy for some reason.item ? buffer.push(item.toString()) : undefined;}}}/** * Merge the `class` properties of HTML attributes objects. * @param attr1 the attributes to merge into. * @param attr2 the attributes to merge into attr1. * @returns a compbined set of HTML attributes */functionmergeAttributes(attr1: HtmlAttributes,attr2: HtmlAttributes){for(varkeyinattr2){if(!attr1.hasOwnProperty(key)){attr1[key]=attr2[key];}elseif(key==="class"){attr1[key]+=" "+attr2[key];}}returnattr1;}/** * Split a tag name into the tag followed by some attributes. * This allows us to support Hiccup-like configuration such as: * 'div.className#htmlId' -> <div class="className" id="htmlId"> ... </div> * * @param tag the tag name to split out * @returns the tag name and corresponding HTML attributes to set -- if any. */functionsplitTag(tag: string): [HtmlTag,HtmlAttributes]{letattr: HtmlAttributes={};letmatch=tag.match(/([^\s\.#]+)(?:#([^\s\.#]+))?(?:\.([^\s#]+))?/);if(match?.[2])attr.id=match[2];if(match?.[3])attr.class=match[3].replace(/\./g," ");return[match?.[1]asHtmlTag,attr];}export{html,htmlPage};