index.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. const SINGLE_TAGS = [
  2. 'area',
  3. 'base',
  4. 'br',
  5. 'col',
  6. 'command',
  7. 'embed',
  8. 'hr',
  9. 'img',
  10. 'input',
  11. 'keygen',
  12. 'link',
  13. 'menuitem',
  14. 'meta',
  15. 'param',
  16. 'source',
  17. 'track',
  18. 'wbr'
  19. ];
  20. const ATTRIBUTE_QUOTES_REQUIRED = /[\t\n\f\r "'`=<>]/;
  21. /** Render PostHTML Tree to HTML
  22. *
  23. * @param {Array|Object} tree PostHTML Tree @param {Object} options Options
  24. *
  25. * @return {String} HTML
  26. */
  27. function render(tree, options) {
  28. /** Options
  29. *
  30. * @type {Object}
  31. *
  32. * @prop {Array<String|RegExp>} singleTags Custom single tags (selfClosing)
  33. * @prop {String} closingSingleTag Closing format for single tag @prop
  34. * @prop {Boolean} quoteAllAttributes If all attributes should be quoted.
  35. * Otherwise attributes will be unquoted when allowed.
  36. * @prop {Boolean} replaceQuote Replaces quotes in attribute values with `&quote;`
  37. *
  38. * Formats:
  39. *
  40. * ``` tag: `<br></br>` ```, slash: `<br />` ```, ```default: `<br>` ```
  41. */
  42. options = options || {};
  43. const singleTags = options.singleTags ? SINGLE_TAGS.concat(options.singleTags) : SINGLE_TAGS;
  44. const singleRegExp = singleTags.filter(tag => {
  45. return tag instanceof RegExp;
  46. });
  47. const {closingSingleTag} = options;
  48. let {quoteAllAttributes} = options;
  49. if (quoteAllAttributes === undefined) {
  50. quoteAllAttributes = true;
  51. }
  52. let {replaceQuote} = options;
  53. if (replaceQuote === undefined) {
  54. replaceQuote = true;
  55. }
  56. let {quoteStyle} = options;
  57. if (quoteStyle === undefined) {
  58. quoteStyle = 2;
  59. }
  60. return html(tree);
  61. /** @private */
  62. function isSingleTag(tag) {
  63. if (singleRegExp.length > 0) {
  64. return singleRegExp.some(reg => reg.test(tag));
  65. }
  66. if (!singleTags.includes(tag)) {
  67. return false;
  68. }
  69. return true;
  70. }
  71. /** @private */
  72. function attrs(object) {
  73. let attr = '';
  74. for (const key in object) {
  75. if (typeof object[key] === 'string') {
  76. if (quoteAllAttributes || object[key].match(ATTRIBUTE_QUOTES_REQUIRED)) {
  77. let attrValue = object[key];
  78. if (replaceQuote) {
  79. attrValue = object[key].replace(/"/g, '&quot;');
  80. }
  81. attr += makeAttr(key, attrValue, quoteStyle);
  82. } else if (object[key] === '') {
  83. attr += ' ' + key;
  84. } else {
  85. attr += ' ' + key + '=' + object[key];
  86. }
  87. } else if (object[key] === true) {
  88. attr += ' ' + key;
  89. } else if (typeof object[key] === 'number') {
  90. attr += makeAttr(key, object[key], quoteStyle);
  91. }
  92. }
  93. return attr;
  94. }
  95. /** @private */
  96. function traverse(tree, cb) {
  97. if (tree !== undefined) {
  98. for (let i = 0, {length} = tree; i < length; i++) {
  99. traverse(cb(tree[i]), cb);
  100. }
  101. }
  102. }
  103. /** @private */
  104. function makeAttr(key, attrValue, quoteStyle = 1) {
  105. if (quoteStyle === 1) {
  106. // Single Quote
  107. return ` ${key}='${attrValue}'`;
  108. }
  109. if (quoteStyle === 2) {
  110. // Double Quote
  111. return ` ${key}="${attrValue}"`;
  112. }
  113. // Smart Quote
  114. if (attrValue.includes('"')) {
  115. return ` ${key}='${attrValue}'`;
  116. }
  117. return ` ${key}="${attrValue}"`;
  118. }
  119. /**
  120. * HTML Stringifier
  121. *
  122. * @param {Array|Object} tree PostHTML Tree
  123. *
  124. * @return {String} result HTML
  125. */
  126. function html(tree) {
  127. let result = '';
  128. if (!Array.isArray(tree)) {
  129. tree = [tree];
  130. }
  131. traverse(tree, node => {
  132. // Undefined, null, '', [], NaN
  133. if (node === undefined ||
  134. node === null ||
  135. node === false ||
  136. node.length === 0 ||
  137. Number.isNaN(node)) {
  138. return;
  139. }
  140. // Treat as new root tree if node is an array
  141. if (Array.isArray(node)) {
  142. result += html(node);
  143. return;
  144. }
  145. if (typeof node === 'string' || typeof node === 'number') {
  146. result += node;
  147. return;
  148. }
  149. // Skip node
  150. if (node.tag === false) {
  151. result += html(node.content);
  152. return;
  153. }
  154. const tag = node.tag || 'div';
  155. result += '<' + tag;
  156. if (node.attrs) {
  157. result += attrs(node.attrs);
  158. }
  159. if (isSingleTag(tag)) {
  160. switch (closingSingleTag) {
  161. case 'tag':
  162. result += '></' + tag + '>';
  163. break;
  164. case 'slash':
  165. result += ' />';
  166. break;
  167. default:
  168. result += '>';
  169. }
  170. result += html(node.content);
  171. } else {
  172. result += '>' + html(node.content) + '</' + tag + '>';
  173. }
  174. });
  175. return result;
  176. }
  177. }
  178. /**
  179. * @module posthtml-render
  180. *
  181. * @version 1.1.5
  182. * @license MIT
  183. */
  184. module.exports = render;