import { unescapeHtml } from './util' import { StringReader } from './string_reader'
const SELF_CLOSING_TAGS = [
'area', 'base', 'br', 'embed', 'hr', 'iframe', 'img', 'input', 'link', 'meta', 'param', 'source', 'track'
]
const TEXT_ONLY_TAGS = [
'script', 'style'
]
class CloseTag {
constructor(type){ this.type = type }
}
export class VirtualNode {
static fromString(html){ const out = new this() out.appendHtml(html) out.normalize() return out } constructor(parent = null, type = '#fragment', attributes = {}){ this.parent = parent this.type = type this.attributes = attributes this.children = [] } appendNode(type, attributes = {}){ const out = new this.constructor(this, type, attributes) this.children.push(out) return out } appendHtml(html){ if(!(html instanceof StringReader)){ html = new StringReader(html) while(html.length > 0){ try { this.appendHtml(html) } catch(e){ if(e instanceof CloseTag){ //do nothing } else { throw e } } } return this } while(html.length > 0){ let matches; if(matches = html.match(/^[^<]+/)){ this.appendNode('#text', {value: matches[0]}) } else if(matches = html.match(/^<!DOCTYPE[^>]*>/i)){ if(!this.parent){ this.appendNode('#doctype') } } else if(matches = html.match(/^<!--([\s\S]*?)-->/i)){ this.appendNode('#comment', {value: matches[1]}) } else if(matches = html.match(/^<([^>\s]+)/)){ const type = matches[1].toLowerCase() const attributes = {} while(html.length > 0){ if(matches = html.match(/^\s*([\w-]+)\s*=\s*\"([^\">]*)\"/)){ attributes[matches[1]] = unescapeHtml(matches[2]) } else if(matches = html.match(/^\s*([\w-]+)\s*=\s*\'([^\'>]*)\'/)){ attributes[matches[1]] = unescapeHtml(matches[2]) } else if(matches = html.match(/^\s*([\w-]+)\s*=\s*([^\s>]+)/)){ attributes[matches[1]] = unescapeHtml(matches[2]) } else if(matches = html.match(/^\s*([\w-]+)/)){ attributes[matches[1]] = null } else { html.match(/^[^>]*>/) break } } if(matches = type.match(/^\/(.*)/)){ throw new CloseTag(matches[1]) } const child = this.appendNode(type, attributes) if(SELF_CLOSING_TAGS.includes(type)){ // do nothing } else if(TEXT_ONLY_TAGS.includes(type) && (matches = html.match(new RegExp(`^([\\s\\S]*?)<\\/${type}[^>]*>`)))){ child.appendNode('#text', {value: matches[1]}) } else if(TEXT_ONLY_TAGS.includes(type) && (matches = html.match(/^([\s\S]+)/))){ child.appendNode('#text', {value: matches[1]}) } else { try { child.appendHtml(html) } catch(e){ if(e instanceof CloseTag && e.type == type){ //do nothing } else { throw e } } } } else if(matches = html.match(/^[\s\S]/)) { this.appendNode('#text', {value: matches[0]}) } else { break; } } } normalize(){ if(!this.parent && this.children.some(child => child.type == 'html')){ this.children = [ new this.constructor(this, '#doctype'), ...this.children.filter(child => child.type == 'html') ] } if(this.type == '#text'){ this.attributes.value = this.attributes.value.replace(/\r\n/g, '\n').replace(/\r/g, '\n') } if(this.parent && this.parent.type == 'textarea' && this.type == '#text'){ this.attributes.value = this.attributes.value.replace(/^\n/, '') } this.children.forEach(child => child.normalize()) }
}