index.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. /*
  2. Module dependencies
  3. */
  4. var ElementType = require('domelementtype');
  5. var entities = require('entities');
  6. /* mixed-case SVG and MathML tags & attributes
  7. recognized by the HTML parser, see
  8. https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inforeign
  9. */
  10. var foreignNames = require('./foreignNames.json');
  11. foreignNames.elementNames.__proto__ = null; /* use as a simple dictionary */
  12. foreignNames.attributeNames.__proto__ = null;
  13. var unencodedElements = {
  14. __proto__: null,
  15. style: true,
  16. script: true,
  17. xmp: true,
  18. iframe: true,
  19. noembed: true,
  20. noframes: true,
  21. plaintext: true,
  22. noscript: true
  23. };
  24. /*
  25. Format attributes
  26. */
  27. function formatAttrs(attributes, opts) {
  28. if (!attributes) return;
  29. var output = '';
  30. var value;
  31. // Loop through the attributes
  32. for (var key in attributes) {
  33. value = attributes[key];
  34. if (output) {
  35. output += ' ';
  36. }
  37. if (opts.xmlMode === 'foreign') {
  38. /* fix up mixed-case attribute names */
  39. key = foreignNames.attributeNames[key] || key;
  40. }
  41. output += key;
  42. if ((value !== null && value !== '') || opts.xmlMode) {
  43. output +=
  44. '="' +
  45. (opts.decodeEntities
  46. ? entities.encodeXML(value)
  47. : value.replace(/\"/g, '"')) +
  48. '"';
  49. }
  50. }
  51. return output;
  52. }
  53. /*
  54. Self-enclosing tags (stolen from node-htmlparser)
  55. */
  56. var singleTag = {
  57. __proto__: null,
  58. area: true,
  59. base: true,
  60. basefont: true,
  61. br: true,
  62. col: true,
  63. command: true,
  64. embed: true,
  65. frame: true,
  66. hr: true,
  67. img: true,
  68. input: true,
  69. isindex: true,
  70. keygen: true,
  71. link: true,
  72. meta: true,
  73. param: true,
  74. source: true,
  75. track: true,
  76. wbr: true
  77. };
  78. var render = (module.exports = function(dom, opts) {
  79. if (!Array.isArray(dom) && !dom.cheerio) dom = [dom];
  80. opts = opts || {};
  81. var output = '';
  82. for (var i = 0; i < dom.length; i++) {
  83. var elem = dom[i];
  84. if (elem.type === 'root') output += render(elem.children, opts);
  85. else if (ElementType.isTag(elem)) output += renderTag(elem, opts);
  86. else if (elem.type === ElementType.Directive)
  87. output += renderDirective(elem);
  88. else if (elem.type === ElementType.Comment) output += renderComment(elem);
  89. else if (elem.type === ElementType.CDATA) output += renderCdata(elem);
  90. else output += renderText(elem, opts);
  91. }
  92. return output;
  93. });
  94. var foreignModeIntegrationPoints = [
  95. 'mi',
  96. 'mo',
  97. 'mn',
  98. 'ms',
  99. 'mtext',
  100. 'annotation-xml',
  101. 'foreignObject',
  102. 'desc',
  103. 'title'
  104. ];
  105. function renderTag(elem, opts) {
  106. // Handle SVG / MathML in HTML
  107. if (opts.xmlMode === 'foreign') {
  108. /* fix up mixed-case element names */
  109. elem.name = foreignNames.elementNames[elem.name] || elem.name;
  110. /* exit foreign mode at integration points */
  111. if (
  112. elem.parent &&
  113. foreignModeIntegrationPoints.indexOf(elem.parent.name) >= 0
  114. )
  115. opts = Object.assign({}, opts, { xmlMode: false });
  116. }
  117. if (!opts.xmlMode && ['svg', 'math'].indexOf(elem.name) >= 0) {
  118. opts = Object.assign({}, opts, { xmlMode: 'foreign' });
  119. }
  120. var tag = '<' + elem.name;
  121. var attribs = formatAttrs(elem.attribs, opts);
  122. if (attribs) {
  123. tag += ' ' + attribs;
  124. }
  125. if (opts.xmlMode && (!elem.children || elem.children.length === 0)) {
  126. tag += '/>';
  127. } else {
  128. tag += '>';
  129. if (elem.children) {
  130. tag += render(elem.children, opts);
  131. }
  132. if (!singleTag[elem.name] || opts.xmlMode) {
  133. tag += '</' + elem.name + '>';
  134. }
  135. }
  136. return tag;
  137. }
  138. function renderDirective(elem) {
  139. return '<' + elem.data + '>';
  140. }
  141. function renderText(elem, opts) {
  142. var data = elem.data || '';
  143. // if entities weren't decoded, no need to encode them back
  144. if (
  145. opts.decodeEntities &&
  146. !(elem.parent && elem.parent.name in unencodedElements)
  147. ) {
  148. data = entities.encodeXML(data);
  149. }
  150. return data;
  151. }
  152. function renderCdata(elem) {
  153. return '<![CDATA[' + elem.children[0].data + ']]>';
  154. }
  155. function renderComment(elem) {
  156. return '<!--' + elem.data + '-->';
  157. }