UNPKG

substance

Version:

Substance is a JavaScript library for web-based content editing. It provides building blocks for realizing custom text editors and web-based publishing systems.

485 lines (409 loc) 10.8 kB
import ElementType from 'domelementtype'; const _encodeXMLContent = ((obj) => { let invObj = getInverseObj(obj); let replacer = getInverseReplacer(invObj); return getInverse(invObj, replacer) })({ amp: "&", gt: ">", lt: "<", }); const _encodeXMLAttr = ((obj) => { let invObj = getInverseObj(obj); let replacer = getInverseReplacer(invObj); return getInverse(invObj, replacer) })({ quot: "\"", }); function getInverseObj(obj){ return Object.keys(obj).sort().reduce(function(inverse, name){ inverse[obj[name]] = "&" + name + ";"; return inverse; }, {}); } function getInverseReplacer(inverse){ var single = [], multiple = []; Object.keys(inverse).forEach(function(k){ if(k.length === 1){ single.push("\\" + k); } else { multiple.push(k); } }); multiple.unshift("[" + single.join("") + "]"); return new RegExp(multiple.join("|"), "g"); } var re_nonASCII = /[^\0-\x7F]/g; var re_astralSymbols = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; function singleCharReplacer(c){ return "&#x" + c.charCodeAt(0).toString(16).toUpperCase() + ";"; } function astralReplacer(c){ var high = c.charCodeAt(0); var low = c.charCodeAt(1); var codePoint = (high - 0xD800) * 0x400 + low - 0xDC00 + 0x10000; return "&#x" + codePoint.toString(16).toUpperCase() + ";"; } function getInverse(inverse, re){ function func(name){ return inverse[name]; } return function(data){ return data .replace(re, func) .replace(re_astralSymbols, astralReplacer) .replace(re_nonASCII, singleCharReplacer); }; } const booleanAttributes = { __proto__: null, allowfullscreen: true, async: true, autofocus: true, autoplay: true, checked: true, controls: true, default: true, defer: true, disabled: true, hidden: true, ismap: true, loop: true, multiple: true, muted: true, open: true, readonly: true, required: true, reversed: true, scoped: true, seamless: true, selected: true, typemustmatch: true }; const unencodedElements = { __proto__: null, style: true, script: true, xmp: true, iframe: true, noembed: true, noframes: true, plaintext: true, noscript: true }; const singleTag = { __proto__: null, area: true, base: true, basefont: true, br: true, col: true, command: true, embed: true, frame: true, hr: true, img: true, input: true, isindex: true, keygen: true, link: true, meta: true, param: true, source: true, track: true, wbr: true, }; class DomUtils { isTag(elem) { return ElementType.isTag(elem) } removeElement(elem){ if(elem.prev) elem.prev.next = elem.next; if(elem.next) elem.next.prev = elem.prev; if(elem.parent){ var childs = elem.parent.childNodes; let pos = childs.lastIndexOf(elem); if (pos < 0) throw new Error('Invalid state') childs.splice(pos, 1); elem.parent = null; } } replaceElement(elem, replacement){ if (replacement.parent) this.removeElement(replacement); var prev = replacement.prev = elem.prev; if(prev){ prev.next = replacement; } var next = replacement.next = elem.next; if(next){ next.prev = replacement; } var parent = replacement.parent = elem.parent; if(parent){ var childs = parent.childNodes; let pos = childs.lastIndexOf(elem); if (pos < 0) throw new Error('Invalid state') childs[pos] = replacement; } } appendChild(elem, child){ if (child.parent) this.removeElement(child); child.parent = elem; if(elem.childNodes.push(child) !== 1){ var sibling = elem.childNodes[elem.childNodes.length - 2]; sibling.next = child; child.prev = sibling; child.next = null; } } append(elem, next){ if (next.parent) this.removeElement(next); var parent = elem.parent, currNext = elem.next; next.next = currNext; next.prev = elem; elem.next = next; next.parent = parent; if(currNext){ currNext.prev = next; if(parent){ var childs = parent.childNodes; let pos = childs.lastIndexOf(currNext); if (pos < 0) throw new Error('Invalid state') childs.splice(pos, 0, next); } } else if(parent){ parent.childNodes.push(next); } } prepend(elem, prev){ if (prev.parent) this.removeElement(prev); var parent = elem.parent; if(parent){ var childs = parent.childNodes; let pos = childs.lastIndexOf(elem); if (pos < 0) throw new Error('Invalid state') childs.splice(pos, 0, prev); } if(elem.prev){ elem.prev.next = prev; } prev.parent = parent; prev.prev = elem.prev; prev.next = elem; elem.prev = prev; } filter(test, element, recurse, limit){ if(!Array.isArray(element)) element = [element]; if(typeof limit !== "number" || !isFinite(limit)){ limit = Infinity; } return this.find(test, element, recurse !== false, limit); } find(test, elems, recurse, limit){ var result = [], childs; for(var i = 0, j = elems.length; i < j; i++){ if(test(elems[i])){ result.push(elems[i]); if(--limit <= 0) break; } childs = this.getChildren(elems[i]); if(recurse && childs && childs.length > 0){ childs = this.find(test, childs, recurse, limit); result = result.concat(childs); limit -= childs.length; if(limit <= 0) break; } } return result; } findOneChild(test, elems){ for(var i = 0, l = elems.length; i < l; i++){ if(test(elems[i])) return elems[i]; } return null; } findOne(test, elems){ var elem = null; for(var i = 0, l = elems.length; i < l && !elem; i++){ const child = elems[i]; if(!this.isTag(child)){ continue; } else if(test(child)){ elem = child; } else { const childNodes = this.getChildren(child); if (childNodes.length > 0) { elem = this.findOne(test, childNodes); } } } return elem; } existsOne(test, elems){ for(var i = 0, l = elems.length; i < l; i++){ const elem = elems[i]; if (!this.isTag(elem)) continue if (test(elem)) return true const childNodes = this.getChildren(elem); if (childNodes.length > 0 && this.existsOne(test, childNodes)) return true } return false; } findAll(test, elems){ var result = []; for(var i = 0, j = elems.length; i < j; i++){ const elem = elems[i]; if(!this.isTag(elem)) continue; if(test(elem)) result.push(elem); const childNodes = this.getChildren(elem); if(childNodes.length > 0){ result = result.concat(this.findAll(test, childNodes)); } } return result; } getAttributes(el) { let attribs = el.getAttributes(); if (attribs instanceof Map) { return Array.from(attribs) } else if (attribs && attribs.forEach) { let res = []; attribs.forEach((val, key) => { res.push([key, val]); }); return res } else { return [] } } formatAttribs(el, opts = {}) { let output = []; const attributes = this.getAttributes(el); attributes.forEach(([key, value]) => { if (!value && booleanAttributes[key]) { output.push(key); } else { output.push(key + '="' + (opts.decodeEntities ? _encodeXMLAttr(value) : value) + '"'); } }); return output.join(' ') } render(dom, opts) { if (!Array.isArray(dom)) dom = [dom]; opts = opts || {}; let output = []; for(var i = 0; i < dom.length; i++){ let elem = dom[i]; if (elem.type === 'root' || elem.type === 'document') { output.push(this.render(this.getChildren(elem), opts)); } else if (ElementType.isTag(elem)) { output.push(this.renderTag(elem, opts)); } else if (elem.type === ElementType.Directive) { output.push(this.renderDirective(elem)); } else if (elem.type === ElementType.Comment) { output.push(this.renderComment(elem)); } else if (elem.type === ElementType.CDATA) { output.push(this.renderCdata(elem)); } else { output.push(this.renderText(elem, opts)); } } return output.join('') } renderTag(elem, opts) { const name = this.getName(elem); if (name === "svg") opts = {decodeEntities: opts.decodeEntities, xmlMode: true}; let tag = '<' + name; let attribs = this.formatAttribs(elem, opts); if (attribs) { tag += ' ' + attribs; } const childNodes = this.getChildren(elem); if (opts.xmlMode && childNodes.length === 0) { tag += '/>'; } else { tag += '>'; if (childNodes.length > 0) { tag += this.render(childNodes, opts); } if (!singleTag[name] || opts.xmlMode) { tag += '</' + name + '>'; } } return tag } renderDirective(elem) { return '<' + this.getData(elem) + '>' } renderText(elem, opts) { let text = this.getText(elem); if (opts.decodeEntities) { const parent = this.getParent(elem); if (!(parent && this.getName(parent) in unencodedElements)) { text = _encodeXMLContent(text); } } return text } renderCdata(elem) { const childNodes = this.getChildren(elem); return '<![CDATA[' + this.getData(childNodes[0]) + ']]>' } renderComment(elem) { return '<!--' + this.getData(elem) + '-->' } getInnerHTML(elem, opts){ const childNodes = this.getChildren(elem); return childNodes.map((child) => { return this.render(child, opts); }).join("") } getOuterHTML(elem, opts) { return this.render(elem, opts) } getData(elem) { return elem.data } getText(elem){ if(Array.isArray(elem)) return elem.map(e => this.getText(e)).join(""); switch(elem.type) { case ElementType.Tag: case ElementType.Script: case ElementType.Style: return this.getText(this.getChildren(elem)) case ElementType.Text: case ElementType.Comment: case ElementType.CDATA: return elem.data default: return "" } } getChildren(elem) { return elem.childNodes; } getParent(elem){ return elem.parent; } getSiblings(elem){ var parent = this.getParent(elem); return parent ? this.getChildren(parent) : [elem]; } getAttributeValue(elem, name){ return elem.getAttribute(name); } hasAttrib(elem, name){ return elem.hasAttribute(name); } getName(elem){ return elem.name } getNameWithoutNS(elem){ return elem.nameWithoutNS } } const domUtils = new DomUtils(); domUtils.DomUtils = DomUtils; export default domUtils;