1'use strict'; 2 3const defaultTreeAdapter = require('../tree-adapters/default'); 4const mergeOptions = require('../utils/merge-options'); 5const doctype = require('../common/doctype'); 6const HTML = require('../common/html'); 7 8//Aliases 9const $ = HTML.TAG_NAMES; 10const NS = HTML.NAMESPACES; 11 12//Default serializer options 13const DEFAULT_OPTIONS = { 14 treeAdapter: defaultTreeAdapter 15}; 16 17//Escaping regexes 18const AMP_REGEX = /&/g; 19const NBSP_REGEX = /\u00a0/g; 20const DOUBLE_QUOTE_REGEX = /"/g; 21const LT_REGEX = /</g; 22const GT_REGEX = />/g; 23 24//Serializer 25class Serializer { 26 constructor(node, options) { 27 this.options = mergeOptions(DEFAULT_OPTIONS, options); 28 this.treeAdapter = this.options.treeAdapter; 29 30 this.html = ''; 31 this.startNode = node; 32 } 33 34 //API 35 serialize() { 36 this._serializeChildNodes(this.startNode); 37 38 return this.html; 39 } 40 41 //Internals 42 _serializeChildNodes(parentNode) { 43 const childNodes = this.treeAdapter.getChildNodes(parentNode); 44 45 if (childNodes) { 46 for (let i = 0, cnLength = childNodes.length; i < cnLength; i++) { 47 const currentNode = childNodes[i]; 48 49 if (this.treeAdapter.isElementNode(currentNode)) { 50 this._serializeElement(currentNode); 51 } else if (this.treeAdapter.isTextNode(currentNode)) { 52 this._serializeTextNode(currentNode); 53 } else if (this.treeAdapter.isCommentNode(currentNode)) { 54 this._serializeCommentNode(currentNode); 55 } else if (this.treeAdapter.isDocumentTypeNode(currentNode)) { 56 this._serializeDocumentTypeNode(currentNode); 57 } 58 } 59 } 60 } 61 62 _serializeElement(node) { 63 const tn = this.treeAdapter.getTagName(node); 64 const ns = this.treeAdapter.getNamespaceURI(node); 65 66 this.html += '<' + tn; 67 this._serializeAttributes(node); 68 this.html += '>'; 69 70 if ( 71 tn !== $.AREA && 72 tn !== $.BASE && 73 tn !== $.BASEFONT && 74 tn !== $.BGSOUND && 75 tn !== $.BR && 76 tn !== $.COL && 77 tn !== $.EMBED && 78 tn !== $.FRAME && 79 tn !== $.HR && 80 tn !== $.IMG && 81 tn !== $.INPUT && 82 tn !== $.KEYGEN && 83 tn !== $.LINK && 84 tn !== $.META && 85 tn !== $.PARAM && 86 tn !== $.SOURCE && 87 tn !== $.TRACK && 88 tn !== $.WBR 89 ) { 90 const childNodesHolder = 91 tn === $.TEMPLATE && ns === NS.HTML ? this.treeAdapter.getTemplateContent(node) : node; 92 93 this._serializeChildNodes(childNodesHolder); 94 this.html += '</' + tn + '>'; 95 } 96 } 97 98 _serializeAttributes(node) { 99 const attrs = this.treeAdapter.getAttrList(node); 100 101 for (let i = 0, attrsLength = attrs.length; i < attrsLength; i++) { 102 const attr = attrs[i]; 103 const value = Serializer.escapeString(attr.value, true); 104 105 this.html += ' '; 106 107 if (!attr.namespace) { 108 this.html += attr.name; 109 } else if (attr.namespace === NS.XML) { 110 this.html += 'xml:' + attr.name; 111 } else if (attr.namespace === NS.XMLNS) { 112 if (attr.name !== 'xmlns') { 113 this.html += 'xmlns:'; 114 } 115 116 this.html += attr.name; 117 } else if (attr.namespace === NS.XLINK) { 118 this.html += 'xlink:' + attr.name; 119 } else { 120 this.html += attr.prefix + ':' + attr.name; 121 } 122 123 this.html += '="' + value + '"'; 124 } 125 } 126 127 _serializeTextNode(node) { 128 const content = this.treeAdapter.getTextNodeContent(node); 129 const parent = this.treeAdapter.getParentNode(node); 130 let parentTn = void 0; 131 132 if (parent && this.treeAdapter.isElementNode(parent)) { 133 parentTn = this.treeAdapter.getTagName(parent); 134 } 135 136 if ( 137 parentTn === $.STYLE || 138 parentTn === $.SCRIPT || 139 parentTn === $.XMP || 140 parentTn === $.IFRAME || 141 parentTn === $.NOEMBED || 142 parentTn === $.NOFRAMES || 143 parentTn === $.PLAINTEXT || 144 parentTn === $.NOSCRIPT 145 ) { 146 this.html += content; 147 } else { 148 this.html += Serializer.escapeString(content, false); 149 } 150 } 151 152 _serializeCommentNode(node) { 153 this.html += '<!--' + this.treeAdapter.getCommentNodeContent(node) + '-->'; 154 } 155 156 _serializeDocumentTypeNode(node) { 157 const name = this.treeAdapter.getDocumentTypeNodeName(node); 158 159 this.html += '<' + doctype.serializeContent(name, null, null) + '>'; 160 } 161} 162 163// NOTE: used in tests and by rewriting stream 164Serializer.escapeString = function(str, attrMode) { 165 str = str.replace(AMP_REGEX, '&').replace(NBSP_REGEX, ' '); 166 167 if (attrMode) { 168 str = str.replace(DOUBLE_QUOTE_REGEX, '"'); 169 } else { 170 str = str.replace(LT_REGEX, '<').replace(GT_REGEX, '>'); 171 } 172 173 return str; 174}; 175 176module.exports = Serializer; 177