als-layout
Version:
HTML layout constructor with dynamic meta, styles, and scripts
127 lines (123 loc) • 31.1 kB
JavaScript
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
}
}