String
Render PostHTML Tree to HTML
-String
-Render PostHTML Tree to HTML
-
-**Kind**: global function
-**Returns**: String
- HTML
-
-| Param | Type | Description |
-| --- | --- | --- |
-| tree | Array
\| Object
| PostHTML Tree @param {Object} options Options |
-
-
-* [render(tree)](#render) ⇒ String
- * [~options](#render..options) : Object
- * [~html(tree)](#render..html) ⇒ String
-
-
-
-### render~options : Object
-Options
-
-**Kind**: inner property of [render
](#render)
-**Properties**
-
-| Name | Type | Description |
-| --- | --- | --- |
-| singleTags | Array.<(String\|RegExp)>
| Custom single tags (selfClosing) |
-| closingSingleTag | String
| Closing format for single tag @prop |
-| quoteAllAttributes | Boolean
| If all attributes should be quoted. Otherwise attributes will be unquoted when allowed. |
-| replaceQuote | Boolean
| Replaces quotes in attribute values with `"e;` Formats: ``` tag: `String
-HTML Stringifier
-
-**Kind**: inner method of [render
](#render)
-**Returns**: String
- result HTML
-
-| Param | Type | Description |
-| --- | --- | --- |
-| tree | Array
\| Object
| PostHTML Tree |
-
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..192c89c
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,223 @@
+import {Attributes, NodeText, NodeTag} from 'posthtml-parser';
+import {closingSingleTagOptionEnum, closingSingleTagTypeEnum, Options, quoteStyleEnum} from '../types/index.d';
+
+type Node = NodeText | NodeTag & {
+ closeAs?: closingSingleTagTypeEnum;
+};
+
+const SINGLE_TAGS = [
+ 'area',
+ 'base',
+ 'br',
+ 'col',
+ 'command',
+ 'embed',
+ 'hr',
+ 'img',
+ 'input',
+ 'keygen',
+ 'link',
+ 'menuitem',
+ 'meta',
+ 'param',
+ 'source',
+ 'track',
+ 'wbr'
+];
+
+const ATTRIBUTE_QUOTES_REQUIRED = /[\t\n\f\r "'`=<>]/;
+
+const defaultOptions = {
+ singleTags: SINGLE_TAGS,
+ closingSingleTag: undefined,
+ quoteAllAttributes: true,
+ replaceQuote: true,
+ quoteStyle: quoteStyleEnum.Double
+};
+
+function render(tree?: Node | Node[], options: Options = {}): string {
+ options = {
+ ...defaultOptions,
+ ...options
+ };
+
+ const {
+ singleTags,
+ closingSingleTag,
+ quoteAllAttributes,
+ replaceQuote,
+ quoteStyle
+ } = options;
+
+ const singleRegExp: RegExp[] = singleTags
+ ?.filter((tag): tag is RegExp => tag instanceof RegExp) ??
+ [];
+
+ if (!Array.isArray(tree)) {
+ if (!tree) {
+ tree = '';
+ }
+
+ tree = [tree];
+ }
+
+ return html(tree);
+
+ function html(tree: Node[] | Node[][]) {
+ let result = '';
+
+ for (const node of tree) {
+ // Undefined, null, '', [], NaN
+ if (
+ node === undefined ||
+ node === null ||
+ (typeof node === 'string' && node.length === 0) ||
+ Number.isNaN(node)) {
+ break;
+ }
+
+ // Treat as new root tree if node is an array
+ if (Array.isArray(node)) {
+ result += html(node);
+
+ break;
+ }
+
+ if (typeof node === 'string' || typeof node === 'number') {
+ result += node;
+
+ continue;
+ }
+
+ if (!Array.isArray(node.content)) {
+ if (!node.content) {
+ node.content = '';
+ }
+
+ node.content = [node.content];
+ }
+
+ if (node.tag === false) {
+ result += html(node.content);
+
+ break;
+ }
+
+ const tag = typeof node.tag === 'string' ? node.tag : 'div';
+
+ result += `<${tag}`;
+
+ if (node.attrs) {
+ result += attrs(node.attrs);
+ }
+
+ const closeAs = {
+ [closingSingleTagTypeEnum.tag]: `>${tag}>`,
+ [closingSingleTagTypeEnum.slash]: ' />',
+ [closingSingleTagTypeEnum.default]: '>'
+ };
+
+ if (isSingleTag(tag)) {
+ switch (closingSingleTag) {
+ case closingSingleTagOptionEnum.tag:
+ result += closeAs[closingSingleTagTypeEnum.tag];
+
+ break;
+ case closingSingleTagOptionEnum.slash:
+ result += closeAs[closingSingleTagTypeEnum.slash];
+
+ break;
+ case closingSingleTagOptionEnum.closeAs:
+ result += closeAs[node.closeAs ?
+ closingSingleTagTypeEnum[node.closeAs] :
+ closingSingleTagTypeEnum.default];
+
+ break;
+ default:
+ result += closeAs[closingSingleTagTypeEnum.default];
+ }
+
+ if (node.content) {
+ result += html(node.content);
+ }
+ } else if (closingSingleTag === closingSingleTagOptionEnum.closeAs && node.closeAs) {
+ const type = node.closeAs ?
+ closingSingleTagTypeEnum[node.closeAs] :
+ closingSingleTagTypeEnum.default;
+ result += `${closeAs[type]}${html(node.content)}`;
+ } else {
+ result += `>${html(node.content)}${tag}>`;
+ }
+ }
+
+ return result;
+ }
+
+ function isSingleTag(tag: string) {
+ if (singleRegExp.length > 0) {
+ return singleRegExp.some(reg => reg.test(tag));
+ }
+
+ if (!singleTags?.includes(tag.toLowerCase())) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function attrs(object: Attributes) {
+ let attr = '';
+
+ for (const [key, value] of Object.entries(object)) {
+ if (typeof value === 'string') {
+ let json;
+ try {
+ json = JSON.parse(value);
+ } catch {}
+
+ if (json) {
+ attr += ` ${key}='${value}'`;
+ } else if (quoteAllAttributes || ATTRIBUTE_QUOTES_REQUIRED.test(value)) {
+ let attrValue = value;
+
+ if (replaceQuote) {
+ attrValue = value.replace(/"/g, '"');
+ }
+
+ attr += makeAttr(key, attrValue, quoteStyle);
+ } else if (value === '') {
+ attr += ` ${key}`;
+ } else {
+ attr += ` ${key}=${value}`;
+ }
+ } else if (value === true) {
+ attr += ` ${key}`;
+ } else if (typeof value === 'number') {
+ attr += makeAttr(key, value, quoteStyle);
+ }
+ }
+
+ return attr;
+ }
+
+ function makeAttr(key: string, attrValue: string | number | boolean, quoteStyle = 1): string {
+ if (quoteStyle === 1) {
+ // Single Quote
+ return ` ${key}='${attrValue}'`;
+ }
+
+ if (quoteStyle === 2) {
+ // Double Quote
+ return ` ${key}="${attrValue}"`;
+ }
+
+ // Smart Quote
+ if (typeof attrValue === 'string' && attrValue.includes('"')) {
+ return ` ${key}='${attrValue}'`;
+ }
+
+ return ` ${key}="${attrValue}"`;
+ }
+}
+
+export default render;
diff --git a/test.html b/test.html
new file mode 100644
index 0000000..3c0ff56
--- /dev/null
+++ b/test.html
@@ -0,0 +1 @@
+