index.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import { TAG_NAMES as $, NS, hasUnescapedText } from '../common/html.js';
  2. import { escapeText, escapeAttribute } from 'entities/lib/escape.js';
  3. import { defaultTreeAdapter } from '../tree-adapters/default.js';
  4. // Sets
  5. const VOID_ELEMENTS = new Set([
  6. $.AREA,
  7. $.BASE,
  8. $.BASEFONT,
  9. $.BGSOUND,
  10. $.BR,
  11. $.COL,
  12. $.EMBED,
  13. $.FRAME,
  14. $.HR,
  15. $.IMG,
  16. $.INPUT,
  17. $.KEYGEN,
  18. $.LINK,
  19. $.META,
  20. $.PARAM,
  21. $.SOURCE,
  22. $.TRACK,
  23. $.WBR,
  24. ]);
  25. function isVoidElement(node, options) {
  26. return (options.treeAdapter.isElementNode(node) &&
  27. options.treeAdapter.getNamespaceURI(node) === NS.HTML &&
  28. VOID_ELEMENTS.has(options.treeAdapter.getTagName(node)));
  29. }
  30. const defaultOpts = { treeAdapter: defaultTreeAdapter, scriptingEnabled: true };
  31. /**
  32. * Serializes an AST node to an HTML string.
  33. *
  34. * @example
  35. *
  36. * ```js
  37. * const parse5 = require('parse5');
  38. *
  39. * const document = parse5.parse('<!DOCTYPE html><html><head></head><body>Hi there!</body></html>');
  40. *
  41. * // Serializes a document.
  42. * const html = parse5.serialize(document);
  43. *
  44. * // Serializes the <html> element content.
  45. * const str = parse5.serialize(document.childNodes[1]);
  46. *
  47. * console.log(str); //> '<head></head><body>Hi there!</body>'
  48. * ```
  49. *
  50. * @param node Node to serialize.
  51. * @param options Serialization options.
  52. */
  53. export function serialize(node, options) {
  54. const opts = { ...defaultOpts, ...options };
  55. if (isVoidElement(node, opts)) {
  56. return '';
  57. }
  58. return serializeChildNodes(node, opts);
  59. }
  60. /**
  61. * Serializes an AST element node to an HTML string, including the element node.
  62. *
  63. * @example
  64. *
  65. * ```js
  66. * const parse5 = require('parse5');
  67. *
  68. * const document = parse5.parseFragment('<div>Hello, <b>world</b>!</div>');
  69. *
  70. * // Serializes the <div> element.
  71. * const str = parse5.serializeOuter(document.childNodes[0]);
  72. *
  73. * console.log(str); //> '<div>Hello, <b>world</b>!</div>'
  74. * ```
  75. *
  76. * @param node Node to serialize.
  77. * @param options Serialization options.
  78. */
  79. export function serializeOuter(node, options) {
  80. const opts = { ...defaultOpts, ...options };
  81. return serializeNode(node, opts);
  82. }
  83. function serializeChildNodes(parentNode, options) {
  84. let html = '';
  85. // Get container of the child nodes
  86. const container = options.treeAdapter.isElementNode(parentNode) &&
  87. options.treeAdapter.getTagName(parentNode) === $.TEMPLATE &&
  88. options.treeAdapter.getNamespaceURI(parentNode) === NS.HTML
  89. ? options.treeAdapter.getTemplateContent(parentNode)
  90. : parentNode;
  91. const childNodes = options.treeAdapter.getChildNodes(container);
  92. if (childNodes) {
  93. for (const currentNode of childNodes) {
  94. html += serializeNode(currentNode, options);
  95. }
  96. }
  97. return html;
  98. }
  99. function serializeNode(node, options) {
  100. if (options.treeAdapter.isElementNode(node)) {
  101. return serializeElement(node, options);
  102. }
  103. if (options.treeAdapter.isTextNode(node)) {
  104. return serializeTextNode(node, options);
  105. }
  106. if (options.treeAdapter.isCommentNode(node)) {
  107. return serializeCommentNode(node, options);
  108. }
  109. if (options.treeAdapter.isDocumentTypeNode(node)) {
  110. return serializeDocumentTypeNode(node, options);
  111. }
  112. // Return an empty string for unknown nodes
  113. return '';
  114. }
  115. function serializeElement(node, options) {
  116. const tn = options.treeAdapter.getTagName(node);
  117. return `<${tn}${serializeAttributes(node, options)}>${isVoidElement(node, options) ? '' : `${serializeChildNodes(node, options)}</${tn}>`}`;
  118. }
  119. function serializeAttributes(node, { treeAdapter }) {
  120. let html = '';
  121. for (const attr of treeAdapter.getAttrList(node)) {
  122. html += ' ';
  123. if (attr.namespace) {
  124. switch (attr.namespace) {
  125. case NS.XML: {
  126. html += `xml:${attr.name}`;
  127. break;
  128. }
  129. case NS.XMLNS: {
  130. if (attr.name !== 'xmlns') {
  131. html += 'xmlns:';
  132. }
  133. html += attr.name;
  134. break;
  135. }
  136. case NS.XLINK: {
  137. html += `xlink:${attr.name}`;
  138. break;
  139. }
  140. default: {
  141. html += `${attr.prefix}:${attr.name}`;
  142. }
  143. }
  144. }
  145. else {
  146. html += attr.name;
  147. }
  148. html += `="${escapeAttribute(attr.value)}"`;
  149. }
  150. return html;
  151. }
  152. function serializeTextNode(node, options) {
  153. const { treeAdapter } = options;
  154. const content = treeAdapter.getTextNodeContent(node);
  155. const parent = treeAdapter.getParentNode(node);
  156. const parentTn = parent && treeAdapter.isElementNode(parent) && treeAdapter.getTagName(parent);
  157. return parentTn &&
  158. treeAdapter.getNamespaceURI(parent) === NS.HTML &&
  159. hasUnescapedText(parentTn, options.scriptingEnabled)
  160. ? content
  161. : escapeText(content);
  162. }
  163. function serializeCommentNode(node, { treeAdapter }) {
  164. return `<!--${treeAdapter.getCommentNodeContent(node)}-->`;
  165. }
  166. function serializeDocumentTypeNode(node, { treeAdapter }) {
  167. return `<!DOCTYPE ${treeAdapter.getDocumentTypeNodeName(node)}>`;
  168. }