| import { TAG_NAMES as $, NS, hasUnescapedText } from '../common/html.js'; |
| import { escapeText, escapeAttribute } from 'entities/escape'; |
| import { defaultTreeAdapter } from '../tree-adapters/default.js'; |
| // Sets |
| const VOID_ELEMENTS = new Set([ |
| $.AREA, |
| $.BASE, |
| $.BASEFONT, |
| $.BGSOUND, |
| $.BR, |
| $.COL, |
| $.EMBED, |
| $.FRAME, |
| $.HR, |
| $.IMG, |
| $.INPUT, |
| $.KEYGEN, |
| $.LINK, |
| $.META, |
| $.PARAM, |
| $.SOURCE, |
| $.TRACK, |
| $.WBR, |
| ]); |
| function isVoidElement(node, options) { |
| return (options.treeAdapter.isElementNode(node) && |
| options.treeAdapter.getNamespaceURI(node) === NS.HTML && |
| VOID_ELEMENTS.has(options.treeAdapter.getTagName(node))); |
| } |
| const defaultOpts = { treeAdapter: defaultTreeAdapter, scriptingEnabled: true }; |
| /** |
| * Serializes an AST node to an HTML string. |
| * |
| * @example |
| * |
| * ```js |
| * const parse5 = require('parse5'); |
| * |
| * const document = parse5.parse('<!DOCTYPE html><html><head></head><body>Hi there!</body></html>'); |
| * |
| * // Serializes a document. |
| * const html = parse5.serialize(document); |
| * |
| * // Serializes the <html> element content. |
| * const str = parse5.serialize(document.childNodes[1]); |
| * |
| * console.log(str); //> '<head></head><body>Hi there!</body>' |
| * ``` |
| * |
| * @param node Node to serialize. |
| * @param options Serialization options. |
| */ |
| export function serialize(node, options) { |
| const opts = { ...defaultOpts, ...options }; |
| if (isVoidElement(node, opts)) { |
| return ''; |
| } |
| return serializeChildNodes(node, opts); |
| } |
| /** |
| * Serializes an AST element node to an HTML string, including the element node. |
| * |
| * @example |
| * |
| * ```js |
| * const parse5 = require('parse5'); |
| * |
| * const document = parse5.parseFragment('<div>Hello, <b>world</b>!</div>'); |
| * |
| * // Serializes the <div> element. |
| * const str = parse5.serializeOuter(document.childNodes[0]); |
| * |
| * console.log(str); //> '<div>Hello, <b>world</b>!</div>' |
| * ``` |
| * |
| * @param node Node to serialize. |
| * @param options Serialization options. |
| */ |
| export function serializeOuter(node, options) { |
| const opts = { ...defaultOpts, ...options }; |
| return serializeNode(node, opts); |
| } |
| function serializeChildNodes(parentNode, options) { |
| let html = ''; |
| // Get container of the child nodes |
| const container = options.treeAdapter.isElementNode(parentNode) && |
| options.treeAdapter.getTagName(parentNode) === $.TEMPLATE && |
| options.treeAdapter.getNamespaceURI(parentNode) === NS.HTML |
| ? options.treeAdapter.getTemplateContent(parentNode) |
| : parentNode; |
| const childNodes = options.treeAdapter.getChildNodes(container); |
| if (childNodes) { |
| for (const currentNode of childNodes) { |
| html += serializeNode(currentNode, options); |
| } |
| } |
| return html; |
| } |
| function serializeNode(node, options) { |
| if (options.treeAdapter.isElementNode(node)) { |
| return serializeElement(node, options); |
| } |
| if (options.treeAdapter.isTextNode(node)) { |
| return serializeTextNode(node, options); |
| } |
| if (options.treeAdapter.isCommentNode(node)) { |
| return serializeCommentNode(node, options); |
| } |
| if (options.treeAdapter.isDocumentTypeNode(node)) { |
| return serializeDocumentTypeNode(node, options); |
| } |
| // Return an empty string for unknown nodes |
| return ''; |
| } |
| function serializeElement(node, options) { |
| const tn = options.treeAdapter.getTagName(node); |
| return `<${tn}${serializeAttributes(node, options)}>${isVoidElement(node, options) ? '' : `${serializeChildNodes(node, options)}</${tn}>`}`; |
| } |
| function serializeAttributes(node, { treeAdapter }) { |
| let html = ''; |
| for (const attr of treeAdapter.getAttrList(node)) { |
| html += ' '; |
| if (attr.namespace) { |
| switch (attr.namespace) { |
| case NS.XML: { |
| html += `xml:${attr.name}`; |
| break; |
| } |
| case NS.XMLNS: { |
| if (attr.name !== 'xmlns') { |
| html += 'xmlns:'; |
| } |
| html += attr.name; |
| break; |
| } |
| case NS.XLINK: { |
| html += `xlink:${attr.name}`; |
| break; |
| } |
| default: { |
| html += `${attr.prefix}:${attr.name}`; |
| } |
| } |
| } |
| else { |
| html += attr.name; |
| } |
| html += `="${escapeAttribute(attr.value)}"`; |
| } |
| return html; |
| } |
| function serializeTextNode(node, options) { |
| const { treeAdapter } = options; |
| const content = treeAdapter.getTextNodeContent(node); |
| const parent = treeAdapter.getParentNode(node); |
| const parentTn = parent && treeAdapter.isElementNode(parent) && treeAdapter.getTagName(parent); |
| return parentTn && |
| treeAdapter.getNamespaceURI(parent) === NS.HTML && |
| hasUnescapedText(parentTn, options.scriptingEnabled) |
| ? content |
| : escapeText(content); |
| } |
| function serializeCommentNode(node, { treeAdapter }) { |
| return `<!--${treeAdapter.getCommentNodeContent(node)}-->`; |
| } |
| function serializeDocumentTypeNode(node, { treeAdapter }) { |
| return `<!DOCTYPE ${treeAdapter.getDocumentTypeNodeName(node)}>`; |
| } |