UNPKG

als-layout

Version:

HTML layout constructor with dynamic meta, styles, and scripts

127 lines (123 loc) 31.1 kB
const alsDocument = (function(){ class Query{ static get(query){ let q=new Query(query) return q.selectors } constructor(query){ this.query=query this.selectors=[] this.stringValues=[]; this.parseSelectors(query.split(',')) } parseSelectors(selectors){ selectors.forEach(selector=>{ let originalSelector=selector.trim() selector=this.removeSpaces(selector) this.stringValues=[] selector=selector.replace(/\[.*?\]/g,(value)=>{ this.stringValues.push(value) return `[${this.stringValues.length-1}]` }) let [element,ancestors]=this.splitAndCutLast(selector,' ') element=this.getFamily(element) if (ancestors.length>0) element.ancestors=ancestors.map(ancestor=>this.getFamily(ancestor)) element.group=originalSelector this.selectors.push(element) }); } splitAndCutLast(string,splitBy){ const array=string.split(splitBy); const last=array.pop(); return [last,array]; } getFamily(group,element,prev,prevAny,sign){ if (group.match(/\~|\+/)!==null){ let [last,prevBrothers]=this.splitAndCutLast(group,/\~|\+/) let signs=group.replace(last,'') prevBrothers.forEach(el=>signs=signs.replace(el,'')) signs=signs.match(/\~|\+/g) if (signs.length==1){ sign=signs[0] } else if (signs.length>1){ sign=signs.splice(signs.length-1,signs.length-1)[0] prevBrothers[0]=prevBrothers.map((b,i)=>{ if (i< prevBrothers.length-1) b+=signs[i] return b }).join('') prevBrothers[0]=this.getFamily(prevBrothers[0]) } if (sign=='~') prevAny=prevBrothers[0] else if (sign=='+') prev=prevBrothers[0] element=last } else element=group let family if (prev || prevAny){ family=this.getParents(element) if (prev) family.prev=this.getParents(prev) if (prevAny) family.prevAny=this.getParents(prevAny) } else family=this.getParents(element) if (family.query!==group) family.group=group return family } getParents(selector){ if (typeof selector=='string'){ let [element,parents]=this.splitAndCutLast(selector,'>') element=this.buildElement(element) parents=parents.map(parent=>this.buildElement(parent)) if (parents.length>0) element.parents=parents return element } else return selector } buildElement(element,id=null,tag=null,classList=[]){ let query=element element=element.replace(/\#(\w-?)*/,$id=>{ id=$id.replace(/^\#/,''); return '' }) element=element.replace(/\.(\w-?)*/,$class=>{ classList.push($class.replace(/^\./,'')); return '' }) element=element.replace(/(\w\:?-?)*/,$tag=>{ tag=$tag=='' ? null : $tag; return '' }) let attribs=this.getAttributes(element) element={ query } if (id) element.id=id if (tag) element.tag=tag if (classList.length>0) element.classList=classList if (attribs.length>0) element.attribs=attribs return element } getAttributes(element){ let attribs=this.stringValues.filter((value,index)=>{ let searchValue=`[${index}]` if (element.match(searchValue)) return true else return false }) attribs=attribs.map(attrib=>{ let query=attrib attrib=attrib.replace('[','').replace(']','') let [name,...values]=attrib.split('=') const value=values.join('=').trim().replace(/^\"/,'').replace(/\"$/,'') let sign attrib={query,name} if(value){ sign='=' attrib.name=attrib.name.replace(/[\~\|\^\$\*]$/,(match=>{ sign=match+sign return '' })) attrib.value=value } if (sign){ attrib.sign=sign attrib.check=this.getAttribFn(sign).bind(attrib) } return attrib }); return attribs } getAttribFn(sign){ if (sign=='=') return function (value){ return value===this.value } if (sign=='*=') return function (value){ return value.includes(this.value) } if (sign=='^=') return function (value){ return value.startsWith(this.value) } if (sign=='$=') return function (value){ return value.endsWith(this.value) } if (sign=='|=') return function (value){ return value.trim().split(' ').length==1 && (value.startsWith(this.value) || value.startsWith(this.value+'-')) ? true : false } if (sign=='~=') return function (value){ return this.value.trim().split(' ').length==1 && value.includes(this.value) ? true : false } } removeSpaces(selector){ selector=selector.replace(/\s{2}/g,' ') selector=selector.replace(/\s?\^?\$?\|?\~?\*?\=\s*/g,(m)=>m.trim()) selector=selector.replace(/\s?(\+|\~|\>)\s?/g,(m)=>m.trim()) return selector } } function checkElement(el,selector){ if(selector==undefined) return true if(el==null) return false let{tag,classList,attribs:attributes,id,prev,ancestors,parents,prevAny}=selector if(typeof el==='string') return false if(id!==undefined && el.id===null) return false if(id && id!==el.id) return false if(tag && el._tagName===undefined) return false else if(tag && tag!==el._tagName) return false const clas=el.attributes.class if(classList!==undefined && (clas===undefined || clas==='')) return false else if(classList!==undefined){ if(classList.every(e=>el.classList.contains(e))===false) return false } if(checkattributes(attributes,el)===false) return false if(checkElement(el.prev,prev)===false) return false if(checkAncestors(el.ancestors,ancestors)===false) return false if(checkParents(el.ancestors,parents)===false) return false if(el.parent){ if(checkPrevAny(el.parent.children,el.childIndex,prevAny)==false) return false } return true } function checkattributes(attributes=[],el){ let elattributes=el.attributes let names=Object.keys(elattributes) let passedTests=0 if(attributes) for(let i=0; i<attributes.length; i++){ let{name,value,check}=attributes[i] if(name=='inner' && value!==undefined && check && el.inner){ if(check(el.inner)) passedTests++ } if(!names.includes(name)) continue else if(value==undefined) passedTests++ else if(value && elattributes[name]){ if(check(elattributes[name])==false) continue else passedTests++ } } if(passedTests==attributes.length) return true else return false } function checkPrevAny(children=[],index,prevAny){ let size=children.length if((size==0 || index==0) && prevAny) return false for(let i=index; i>=0; i--){ if(checkElement(children[i],prevAny)) return true } return false } function checkAncestors(ancestors=[],selectorAncestors=[]){ let count=0 if(selectorAncestors.length==0) return true let endIndex=ancestors.length-1 let selectorIndex=selectorAncestors.length-1 while(selectorIndex>=0){ for(let i=endIndex; i>=0; i--){ endIndex=i-1 if(checkElement(ancestors[i],selectorAncestors[selectorIndex])==true){ count++ break } } selectorIndex-- } if(count==selectorAncestors.length) return true else return false } function checkParents(ancestors=[],selectorParents=[]){ if(selectorParents.length===0) return true if(ancestors.length< selectorParents.length) return false let index=ancestors.length-1 for(let i=selectorParents.length-1; i>=0; i--){ if(checkElement(ancestors[index],selectorParents[i])===false) return false index-- } return true } const getDataName=prop=>'data-'+prop.toLowerCase() function getDataset(element){ return new Proxy(element.attributes,{ get: (target,prop)=>{return target[getDataName(prop)]},set: (target,prop,value)=>{target[getDataName(prop)]=value; return true},deleteProperty: (target,prop)=>{ const dataAttr=getDataName(prop) if (dataAttr in target){ delete target[dataAttr]; return true; } return false; } }); } function find(selectors,element,collection,first=false,firstTime=true){ if(!firstTime) for(let selector of selectors){ if(checkElement(element,selector)) collection.add(element) } if(element.children) element.children.forEach(child=>{ if(first && collection.size>0) return find(selectors,child,collection,first,false) }) return firstTime ? [...collection] : collection } class TextNode{ constructor(data){ this.nodeName='#text'; this.parent=null; this.textContent=data; } get nodeValue(){return this.textContent} get parentNode(){return this.parent} } function buildStyle(attributes){ const styles=attributes.style || ""; const camelToKebab=str=>str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g,'$1-$2').toLowerCase(); const kebabToCamel=str=>str.replace(/-([a-z])/g,g=>g[1].toUpperCase()); const baseStyleObj=styles.split(";").reduce((acc,style)=>{ const [key,value]=style.split(":").map(s=>s.trim()); if (key && value) acc[kebabToCamel(key)]=value; return acc; },{}); return new Proxy(baseStyleObj,{ get: (obj,prop)=>obj[camelToKebab(prop)] || obj[prop],set: (obj,prop,value)=>{ obj[camelToKebab(prop)]=value; attributes.style=Object.entries(obj).map(([k,v])=>`${camelToKebab(k)}: ${v}`).join("; "); return true; },deleteProperty: (obj,prop)=>{ delete obj[camelToKebab(prop)]; attributes.style=Object.entries(obj).map(([k,v])=>`${camelToKebab(k)}: ${v}`).join("; "); return true; } }); } class NodeClassList{ constructor(node){ this.node=node } get classes(){ return (this.node.attributes.class || "").split(" ").filter(Boolean) } set classes(val){ this.node.attributes.class=val.join(" ") } contains(className){ return this.classes.includes(className) } add(className){ const currentClasses=this.classes; if (!currentClasses.includes(className)) this.classes=[...currentClasses,className]; } remove(className){ this.classes=this.classes.filter(cls=>cls!==className); } toggle(className){ if (this.classes.includes(className)) this.remove(className); else this.add(className); } replace(oldClass,newClass){ if (this.classes.includes(oldClass)){ this.remove(oldClass); this.add(newClass); } } } function insertBefore(arr,index,newItem){ const existingIndex=arr.indexOf(newItem); if (existingIndex!==-1) arr.splice(existingIndex,1); arr.splice(index,0,newItem); } class Node{ constructor(tagName,attributes={},parent=null){ this.isSingle=false; this._tagName=tagName.toLowerCase(); this.tagName=tagName.toUpperCase(); this.attributes=attributes; this.childNodes=[]; if (parent!==null) parent.childNodes.push(this) this.parent=parent; this._classList=null; this.__style=null; this._dataset=null } get id(){ return this.attributes.id ? this.attributes.id : null; } set id(newValue){ this.attributes.id=newValue; } get className(){ return this.attributes.class || null } get parentNode(){ return this.parent } get ancestors(){ if (!this.parent) return [] const ancestors=[] let element=this.parent while (element.tagName!=='ROOT'){ ancestors.push(element) element=element.parent } return ancestors.reverse() } get childNodeIndex(){ if (!this.parent) return null return this.parent.childNodes ? this.parent.childNodes.indexOf(this) : null } get childIndex(){ if (!this.parent) return null return this.parent.children ? this.parent.children.indexOf(this) : null } get previousElementSibling(){ return this.prev } get prev(){ if (this.childIndex===null) return null return this.parent.children[this.childIndex-1] } get nextElementSibling(){ return this.next } get next(){ if (this.childIndex===null) return null return this.parent.children[this.childIndex+1] || null } get dataset(){ if (!this._dataset) this._dataset=getDataset(this); return this._dataset; } get classList(){ if (!this._classList) this._classList=new NodeClassList(this); return this._classList; } get style(){ if (!this.__style) this.__style=buildStyle(this.attributes) return this.__style } get outerHTML(){ const attrs=Object.entries(this.attributes).map(([key,val])=>val.length ? `${key}="${val}"` : key).join(" "); return `<${this._tagName}${attrs ? ' '+attrs : ''}>${this.innerHTML}</${this._tagName}>`; } getAttribute(attrName){ return this.attributes[attrName]!==undefined ? this.attributes[attrName] : null } setAttribute(attrName,value=''){ this.attributes[attrName]=value } removeAttribute(attrName){ delete this.attributes[attrName] } remove(){ if (!this.parent) return const index=this.childNodeIndex; if (index!==null) this.parent.childNodes.splice(index,1); } get innerHTML(){ return this.childNodes.map(child=>{ if (child instanceof Node || child instanceof SingleNode) return child.outerHTML; else if (child instanceof TextNode) return child.textContent; else return child }).join(""); } get innerText(){ return this.childNodes.map(child=>{ if (child instanceof Node || child instanceof SingleNode) return child.innerText; else if (child instanceof TextNode) return child.textContent; else return child }).join(""); } set innerText(value){ this.childNodes=[new TextNode(value)] return value } $$(query){ return this.querySelectorAll(query) } querySelectorAll(query){ const selectors=Query.get(query) return find(selectors,this,new Set()) } $(query){ return this.querySelector(query) } querySelector(query){ const selectors=Query.get(query) return find(selectors,this,new Set(),true)[0] || null } getElementsByClassName(query){ return this.querySelectorAll('.'+query) } getElementsByTagName(query){ return this.querySelectorAll(query) } getElementById(query){ return this.querySelector('#'+query) } get children(){ return this.childNodes.filter(child=>{ if (!(child instanceof Node)) return false if (child._tagName==='#comment') return false return true }); } insertAdjacentElement(position,newElement){ if (newElement.tagName==='ROOT' && newElement.childNodes.length>0) newElement=newElement.childNodes[0] const pos=position.toLowerCase(); if(newElement.parentNode){ newElement.parentNode.childNodes=newElement.parentNode.childNodes.filter(el=>el!==newElement) } if (pos==='afterbegin' || pos==='beforeend'){ if (pos==="afterbegin") this.childNodes.unshift(newElement); else if (pos==="beforeend") this.childNodes.push(newElement); newElement.parent=this return newElement } if (!this.parent) throw new Error("Can't insert element to element without parent") if (pos==="beforebegin") insertBefore(this.parent.childNodes,this.childNodeIndex,newElement) else if (pos==="afterend") this.parent.childNodes.splice(this.childNodeIndex+1,0,newElement); newElement.parent=this.parent return newElement } insertAdjacentHTML(position,html){ const newNode=parseHTML(html); newNode.childNodes.forEach(node=>{ this.insertAdjacentElement(position,node); }); return newNode } insertAdjacentText(position,text){ return this.insertAdjacentElement(position,new TextNode(text)); } insert(position,element){ const positions=['beforebegin','afterbegin','beforeend','afterend'] if (positions[position]) position=positions[position] if (typeof element==='string'){ element=element.trim() if (element.startsWith('<') && element.endsWith('>')){ return this.insertAdjacentHTML(position,element) } return this.insertAdjacentText(position,element) } return this.insertAdjacentElement(position,element) } set innerHTML(html){ this.childNodes=html.trim().startsWith('<') ? parseHTML(html).childNodes : [html] this.children.forEach(child=>child.parent=this); } set outerHTML(html){ const parsed=parseHTML(html); if (!this.parent) throw new Error('element has no parent node') const index=this.childIndex if (index!==null) this.parent.childNodes.splice(index,1,...parsed.childNodes); } appendChild(newChild){ if (newChild instanceof Node || newChild instanceof TextNode || newChild instanceof SingleNode){ if (newChild.parent) newChild.parent.childNodes=newChild.parent.childNodes.filter(child=>child!==newChild); } else if (typeof newChild==='string') newChild=new TextNode(newChild) else return newChild this.childNodes.push(newChild); newChild.parent=this; return newChild; } get textContent(){ if (this.childNodes.length===0) return this.nodeName==='#text' ? this.nodeValue : ''; return this.childNodes.map(child=>{ if (child instanceof SingleNode) return '' if (child instanceof TextNode) return child.nodeValue if (child instanceof Node) return child.textContent; else return child; }).join(''); } set textContent(value){ this.childNodes=[]; if (value!==null && value!==undefined){ this.childNodes.push(value.toString()); } } } class SingleNode extends Node{ constructor(tagName,attributes={},parent=null){ if(attributes['?'] && tagName==='?xml') delete attributes['?'] super(tagName,attributes,parent); this.isSingle=true } get outerHTML(){ if (this._tagName==="#cdata-section") return `<![CDATA[${this.nodeValue}]]>`; const attrs=Object.entries(this.attributes).map(([key,val])=>val.length ? `${key}="${val}"` : key).join(" "); return `<${this._tagName} ${attrs}${this._tagName==='?xml' ? '?' : ''}>`; } get innerHTML(){ return ""; } set innerHTML(_){ } $(_){return null} $$(_){return []} querySelectorAll(_){ return []; } querySelector(_){ return null; } getElementsByClassName(_){ return []; } getElementsByTagName(_){ return []; } getElementById(_){ return null; } get children(){ return []; } insertAdjacentElement(position,newElement){ if(position==='afterbegin') position='beforebegin' if(position==='beforeend') position='afterend' return super.insertAdjacentElement(position,newElement) } insertAdjacentHTML(position,html){ if(position==='afterbegin') position='beforebegin' if(position==='beforeend') position='afterend' return super.insertAdjacentHTML(position,html) } insertAdjacentText(position,text){ if(position==='afterbegin') position='beforebegin' if(position==='beforeend') position='afterend' return super.insertAdjacentText(position,text) } insert(position,element){ if(position===1) position=0 if(position===2) position=3 return super.insert(position,element) } appendChild(_){ } get textContent(){ return ""; } set textContent(_){ } } class Root extends Node{ constructor(){ super('ROOT',{},null); this.isSingle=false } } class Document extends Node{ constructor(html,url){ super('ROOT',{},null); this.isSingle=false this.URL=url if(html instanceof Document) this.childNodes=[...buildFromCache(cacheDoc(html)).childNodes] else if(typeof html==='string') this.innerHTML=html else this.html } get documentElement(){return this.html} get html(){ const html=this.$('html') if(html===null) this.innerHTML=/*html*/`<html lang="en"><head><meta charset="UTF-8"><title></title></head><body></body></html>` return this.$('html') } get innerHTML(){ const inner=super.innerHTML return '<!DOCTYPE html>'+inner } set innerHTML(html){return super.innerHTML=html} get head(){ const head=this.html.$('head') if(head===null) this.html.insert(1,'<head></head>') return this.$('head') } get body(){ const body=this.html.$('body') if(body===null) this.head.insert(3,'<body></body>') return this.$('body') } get title(){ const title=this.head.$('title') if(title===null) this.head.insert(1,'<title></title>') return this.$('title').innerText } get charset(){return this.$('meta[charset]')} set title(title){return this.head.$('title').innerText=title} get clone(){return new Document(this)} } function parseAttributes(str){ const attrs={}; let key=""; let value=""; let isKey=true; let quoteChar=null; for (let i=0; i< str.length; i++){ const char=str[i]; if (isKey && (char==='=' || char===' ')){ if (char==='=') isKey=false; else if (key.trim()){ attrs[key.trim()]=true; key=""; } continue; } if (!quoteChar && (char==='"' || char==="'")){ quoteChar=char; continue; } else if (quoteChar && char===quoteChar){ quoteChar=null; attrs[key.trim()]=value.trim(); key=""; value=""; isKey=true; continue; } if (isKey) key+=char; else value+=char; } if (key.trim() &&!value) attrs[key.trim()]=''; return attrs; } const VOID_TAGS=new Set(["area","base","br","col","command","embed","hr","img","input","keygen","link","meta","param","source","track","wbr","!doctype",'?xml']); function parseHTML(html){ const root=new Root(); const stack=[root]; let currentText="",i=0; let max=0 function parseScript(){ if (!html.startsWith("<script",i)) return false; const openTagEnd=html.indexOf(">",i); if (openTagEnd===-1) return false; const attributesString=html.substring(i+7,openTagEnd).trim(); const attributes=parseAttributes(attributesString); let closeTagStart=html.indexOf("</script>",openTagEnd); if (closeTagStart===-1) return false; const content=html.substring(openTagEnd+1,closeTagStart); const scriptNode=new Node('script',attributes,stack[stack.length-1]); if(content.length>0) scriptNode.childNodes.push(content); i=closeTagStart+9; return true; } function parseSpecial(startStr,endStr,n1,n2,tag){ if (!html.startsWith(startStr,i)) return false if(currentText.length) stack[stack.length-1].childNodes.push(new TextNode(currentText)); const end=html.indexOf(endStr,i+n1); const strNode=new Node(tag,{},stack[stack.length-1]); strNode.childNodes.push(html.substring(i+n1,end)); i=end+n2; return true } while (i< html.length){ if(i>=max) max=i; else break; if (parseScript()) continue if (parseSpecial("<!--","-->",4,3,'#comment')) continue if (parseSpecial("<style","</style>",7,8,'style')) continue if (html.startsWith("<![CDATA[",i)){ const end=html.indexOf("]]>",i+9); if (end===-1) break; const content=html.substring(i+9,end); const cdataNode=new SingleNode("#cdata-section",{},stack[stack.length-1]); cdataNode.nodeValue=content; i=end+3; continue; } if (html.startsWith("<",i)){ if (currentText && stack[stack.length-1]){ const textNode=new TextNode(currentText) stack[stack.length-1].childNodes.push(textNode); textNode.parent=stack[stack.length-1] currentText=""; } let tagEnd=i+1; let insideQuotes=false; let quoteChar=null; while (tagEnd< html.length){ const char=html[tagEnd]; if (!insideQuotes && (char==='"' || char==="'")){ insideQuotes=true; quoteChar=char; } else if (insideQuotes && char===quoteChar){ insideQuotes=false; quoteChar=null; } if (!insideQuotes && char==='>') break; tagEnd++; } const tagContent=html.substring(i+1,tagEnd); if (tagContent.startsWith("/")) stack.pop(); else{ let isSelfClosing=tagContent.endsWith('/'); const tagNameEnd=tagContent.search(/\s|>|\//); const tagName=tagContent.substring(0,tagNameEnd>0 ? tagNameEnd : tagEnd-i-1); const attributesString=tagContent.substring(tagName.length,isSelfClosing ? tagContent.length-1 : tagContent.length).trim(); const attributes=parseAttributes(attributesString); if (VOID_TAGS.has(tagName.toLowerCase()) || isSelfClosing) new SingleNode(tagName,attributes,stack[stack.length-1]) else stack.push(new Node(tagName,attributes,stack[stack.length-1])); } i=tagEnd+1; } else{ currentText+=html[i]; i++; } } if (currentText.trim() && stack[stack.length-1]) stack[stack.length-1].childNodes.push(new TextNode(currentText)); return root; } function buildFromCache(cached){ function buildNode(cache,parent=null){ if(typeof cache==='string') return parent.childNodes.push(cache) const{isSingle,tagName,attributes,childNodes,textContent}=cache if(textContent) return parent.childNodes.push(new TextNode(textContent)) if(isSingle && parent) return parent.childNodes.push(new SingleNode(tagName,attributes)) const newDoc=tagName==='ROOT' ? new Root() : new Node(tagName,attributes,parent) childNodes.forEach(childNode=>{ buildNode(childNode,newDoc) }); return newDoc } return buildNode(cached) } function cacheDoc(doc){ const props=['isSingle','tagName','attributes'] function addToCache(element,cache={}){ if(typeof element==='string') return element if(element.nodeName==='#text') return{textContent:element.textContent} props.forEach(prop=>{ if(element[prop]){ cache[prop]=typeof element[prop]==='object' ?{...element[prop]} : element[prop] } }); if(!element.childNodes) return cache cache.childNodes=[] element.childNodes.forEach(childNode=>{ cache.childNodes.push(addToCache(childNode)) }); return cache } return addToCache(doc) } return { parseHTML,Node,Query,TextNode,SingleNode,buildFromCache,cacheDoc,Root,Document } })() const { Document, SingleNode, Node } = alsDocument class Layout extends Document { constructor(html, host) { super(html, host); this.root = this.html; } get rawHtml() { return this.innerHTML } get clone() { return new this.constructor(new Document(this, this.URL), this.host) } lang(lang) { this.html.setAttribute('lang', lang); return this } link(href, attributes = { rel: "stylesheet", type: "text/css" }) { if (!href || typeof href !== 'string') throw new Error(`href attribute must be a string`) if (!this.root.querySelector(`link[rel=stylesheet][href^="${href}"]`)) this.head.insert(2, new SingleNode('link', { href, ...attributes })) return this } keywords(keywords = []) { let el = this.root.$('meta[name=keywords]') || new SingleNode('meta', { name: 'keywords' }) keywords = new Set([...(el.getAttribute('content') || '').split(','),...keywords.map(k => k.trim())].filter(Boolean)) if (keywords.size) el.setAttribute('content', Array.from(keywords).join(',')) if(keywords.size && !el.parent) this.head.insert(2, el) return this } style(styles) { if (typeof styles !== 'string') throw 'styles parameter should be string'; let el = this.root.$('style') || this.head.insert(2, new Node('style')) el.innerHTML = el.innerHTML + '\n' + styles; return this } url(url, host = this.URL) { try { url = (host ? new URL(url, host) : new URL(url)).href.replace(/\/$/, '') this.meta({ property: 'og:url', content: url }) const el = this.root.$('link[rel="canonical"]') || this.head.insert(2, new SingleNode('link', { rel: 'canonical', href: url })) el.setAttribute('href', url) } catch (error) { error.info = `url "${url}" with host "${host}" is not valid url`; throw error; } return this } meta(props) { const entries = Object.entries(props) const [name, value] = entries[0] const metaElement = this.root.querySelector(`meta[${name}="${value}"]`) || this.head.insert(2, new SingleNode('meta', props)) entries.forEach(([name, v]) => metaElement.setAttribute(name, props[name])) return this } script(attrs = {}, innerHTML = '', head = true) { if (typeof attrs !== 'object' || attrs === null || Array.isArray(attrs)) attrs = {} if (attrs.src && this.root.querySelector(`script[src="${attrs.src}"]`)) return this if (Object.keys(attrs).length || innerHTML) { const script = new Node('script', attrs) if (innerHTML) script.innerHTML = innerHTML if (head) this.head.insert(2, script) else this.html.insert(2, script) } return this } description(description) { this.meta({ name: 'description', content: description }) this.meta({ property: 'og:description', content: description }) this.meta({ property: 'twitter:description', content: description }) return this } favicon(href) { const el = this.root.$('link[rel=icon][type=image/x-icon]') if (el) el.setAttribute('href', href) else this.head.insert(2, new SingleNode('link', { rel: 'icon', href, type: 'image/x-icon' })) return this } viewport(viewport = 'width=device-width, initial-scale=1.0') { const el = this.root.$('meta[name="viewport"]') if (el) el.setAttribute('content', viewport) else this.head.insert(2, new SingleNode('meta', { name: 'viewport', content: viewport })) return this } image(image) { this.meta({ property: 'og:image', content: image }) this.meta({ name: 'twitter:image', content: image }) this.meta({ name: 'twitter:card', content: 'summary_large_image' }) return this } title(title) { super.title // create title tag if not exists super.title = title this.meta({ property: 'og:title', content: title }) return this } }