From 4c2def653a81a8f73839600cc4a84047bc6a0ee8 Mon Sep 17 00:00:00 2001 From: Chris Andrejewski Date: Sat, 11 Nov 2017 21:18:16 -0500 Subject: [PATCH 1/3] Version 1 --- README.md | 126 ++------- bin/himalaya.js | 70 ----- docs/dist/himalaya.js | 169 +++++------ docs/dist/himalaya.js.map | 2 +- package.json | 6 - src/{formats/v1.js => format.js} | 18 +- src/formats/util.js | 15 - src/formats/v0.js | 70 ----- src/index.js | 10 +- src/{translate/v1.js => stringify.js} | 9 +- src/translate/v0.js | 194 ------------- test/{formats/v1-integration.js => format.js} | 41 +-- test/formats/v0-integration.js | 191 ------------- test/formats/v0.js | 51 ---- test/formats/v1.js | 15 - test/{translate/v1.js => stringify.js} | 17 +- test/translate/v0.js | 266 ------------------ text/translation.md | 55 ---- translate.js | 1 - 19 files changed, 147 insertions(+), 1179 deletions(-) delete mode 100644 bin/himalaya.js rename src/{formats/v1.js => format.js} (59%) delete mode 100644 src/formats/util.js delete mode 100644 src/formats/v0.js rename src/{translate/v1.js => stringify.js} (78%) delete mode 100644 src/translate/v0.js rename test/{formats/v1-integration.js => format.js} (81%) delete mode 100644 test/formats/v0-integration.js delete mode 100644 test/formats/v0.js delete mode 100644 test/formats/v1.js rename test/{translate/v1.js => stringify.js} (82%) delete mode 100644 test/translate/v0.js delete mode 100644 text/translation.md delete mode 100644 translate.js diff --git a/README.md b/README.md index aeb6f1c..b6e590f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ [![Greenkeeper badge](https://badges.greenkeeper.io/andrejewski/himalaya.svg)](https://greenkeeper.io/) [Try online 🚀](http://andrejewski.github.io/himalaya) +| +[Read the specification 📖](https://github.com/andrejewski/himalaya/blob/master/text/ast-spec-v1.md) ## Usage @@ -17,9 +19,10 @@ npm install himalaya ``` ```js -var himalaya = require('himalaya') -var html = require('fs').readFileSync('/webpage.html', {encoding: 'utf8'}) -var json = himalaya.parse(html) +import fs from 'fs' +import {parse} from 'himalaya' +const html = fs.readFileSync('/webpage.html', {encoding: 'utf8'}) +const json = parse(html) console.log('👉', json) ``` @@ -27,91 +30,23 @@ console.log('👉', json) Download [himalaya.js](https://github.com/andrejewski/himalaya/blob/master/docs/dist/himalaya.js) and put it in a `" - const xml = "" - const jsonHTML = parse(html) - const jsonXML = parse(xml) - - t.is(toHTML(jsonHTML), html) - t.is(toHTML(jsonHTML, { - doctype: 'xml' - }), xml) - - t.is(toHTML(jsonXML), html) - t.is(toHTML(jsonXML, { - doctype: 'xml' - }), xml) -}) - -test('toPug() should handle plain text', t => { - const html = 'This is text.' - const jade = '| This is text.' - t.is(toPug(parse(html)), jade) -}) - -test('toPug() should handle multi-line plain text', t => { - const html = 'This is multiline text.\nLook newlines.' - const jade = '| This is multiline text.\n| Look newlines.' - t.is(toPug(parse(html)), jade) -}) - -test('toPug() should handle inline comments', t => { - const html = '' - const jade = '// Comment ' - t.is(toPug(parse(html)), jade) -}) - -test('toPug() should handle multi-line comments', t => { - const html = [ - '' - ].join('\n') - const jade = [ - '//', - ' This is a multiline comment.', - ' Look newlines.' - ].join('\n') - t.is(toPug(parse(html)), jade) -}) - -test('toPug() should write short-hand tag ids', t => { - const html = "
" - const jade = 'article#story' - t.is(toPug(parse(html)), jade) -}) - -test('toPug() should write short-hand tag classes', t => { - const html = "
" - const jade = 'article.story.story--main' - t.is(toPug(parse(html)), jade) -}) - -test('toPug() should ignore `div` if an id or class(es) are provided', t => { - const htmlId = "
" - const jadeId = '#block' - t.is(toPug(parse(htmlId)), jadeId) - - const htmlClass = "
" - const jadeClass = '.block' - t.is(toPug(parse(htmlClass)), jadeClass) - - const htmlClasses = "
" - const jadeClasses = '.block.block--jumbo' - t.is(toPug(parse(htmlClasses)), jadeClasses) -}) - -test('toPug() should write attributes', t => { - const html = "" - const jade = "canvas(width='500', height='400')" - t.is(toPug(parse(html)), jade) -}) - -test('toPug() should write data-* attributes', t => { - const html = "
" - const jade = "div(data-one='5', data-two='five')" - t.is(toPug(parse(html)), jade) -}) - -test('toPug() should do basic escaping if a value contains either single or double quotes', t => { - const html = '
' - const jade = 'div(data-val="cake is \'good\'")' - t.is(toPug(parse(html)), jade) -}) - -test('toPug() should write the style attribute', t => { - const html = "Word" - const jade = "b(style='font-weight: bold; font-style: italics') Word" - t.is(toPug(parse(html)), jade) -}) - -test('toPug() should appropriate place tag inner text', t => { - const htmlInline = '

Hello

' - const jadeInline = 'h1 Hello' - t.is(toPug(parse(htmlInline)), jadeInline) - - const htmlMultiline = '

Hello\nWorld

' - const jadeMultiline = 'h1.\n Hello\n World' - t.is(toPug(parse(htmlMultiline)), jadeMultiline) -}) - -test('toPug() should use tabs for indentation if configured', t => { - const html = '

Hello\nWorld

' - const jade = 'h1.\n\t\tHello\n\t\tWorld' - const jadeOptions = {indentation: '\t\t'} - t.is(toPug(parse(html), jadeOptions), jade) -}) - -test('toPug() should work for script and style tags', t => { - const htmlScript = "" - const jadeScript = "script(type='text/javascript').\n console.log('yes');\n console.log('no');" - t.is(toPug(parse(htmlScript)), jadeScript) - - const htmlStyle = '' - const jadeStyle = 'style.\n h1 {color: #fff;}\n .text {font-size: 12px;}' - t.is(toPug(parse(htmlStyle)), jadeStyle) -}) - -test('toPug() should handle receiving a single root node', t => { - const html = '

Words are words.

' - const jade = [ - 'div', - ' p Words are words.' - ].join('\n') - t.is(toPug(parse(html)[0]), jade) -}) - -test('toPug() should work for void tags', t => { - const html = '' - const jade = "img(src='cake.png')" - t.is(toPug(parse(html)), jade) -}) - -test('toPug() should prioritize using a configured doctype', t => { - const html = '' - const jade = 'doctype foobar' - const jadeOptions = {doctype: 'foobar', indentation: ' '} - t.is(toPug(parse(html), jadeOptions), jade) -}) - -test('toPug() should produce proper shorthand doctypes', t => { - const cases = [ - [ - '', - 'doctype transitional' - ], - [ - '', - 'doctype frameset' - ], - [ - '', - 'doctype strict' - ], - [ - '', - 'doctype basic' - ], - [ - '', - 'doctype 1.1' - ], - [ - '', - 'doctype mobile' - ], - ['', 'doctype html'] - ] - - cases.forEach(([html, jade]) => { - t.is(toPug(parse(html)), jade) - }) -}) diff --git a/text/translation.md b/text/translation.md deleted file mode 100644 index 4a367d7..0000000 --- a/text/translation.md +++ /dev/null @@ -1,55 +0,0 @@ -# Himalaya Translations - -This document describes the functions included with the Himalaya parser for transforming the [abstract syntax tree](https://github.com/andrejewski/himalaya/tree/master/docs/ast-spec.md) into different HTML-based languages. - -## Version 1 Translations - -```js -import himalaya from 'himalaya' -import {toHTML} from 'himalaya/lib/translate/v1' - -const source = "" -const json = himalaya.parse(source) - -const html = toHTML(json) -// => "" -``` - -*Note: Version 1 does not include the `toPug` transform.* - -## Version 0 Translations - -The language translations included are for HTML and [Pug](https://pugjs.org/api/getting-started.html). - -```js -import himalaya from 'himalaya' -import {toHTML, toPug} from 'himalaya/lib/translate/v0' - -const source = "" -const json = himalaya.parse(source) - -const html = toHTML(json) -// => "" - -const pug = toPug(json) -// => "img(src='bar.png' async)" - -const xml = toHTML(json, {doctype: 'xml'}) -// => "" - -const pugXml = toPug(json, {doctype: 'xml'}) -// => "img(src='bar.png', async='async')" -``` - -### toHTML(ast, [options]) HTML -The HTML translator takes a given **ast** and an optional **options** object containing: - -- `doctype` which if set to `"xml"` will render boolean attributes and void tags in XML format - -### toPug(ast, [options]) Pug -The Pug translator takes a given **ast** and an optional **options** object containing: - -- `doctype` which if set to `"xml"` will render boolean attributes in XML format -- `indentation` defaults to two spaces - -The Pug translator is not a one-to-one translation with HTML. HTML transformed into Pug and then into HTML will lose some of the original's whitespace. diff --git a/translate.js b/translate.js deleted file mode 100644 index 37c24fa..0000000 --- a/translate.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./lib/translate/v0').default From 28a0f9dc69bde7c550287be4ab3cc91d385e8fb1 Mon Sep 17 00:00:00 2001 From: Chris Andrejewski Date: Sun, 12 Nov 2017 12:24:24 -0500 Subject: [PATCH 2/3] Bring code coverage back up to 100% --- docs/dist/himalaya.js | 1 - docs/dist/himalaya.js.map | 2 +- src/lexer.js | 1 - test/lexer.js | 49 ++++++++++++++++++++++++++++++++++++ test/parser.js | 36 ++++++++++++++++++++++++++ test/stringify.js | 53 +++++++++++++++++++++++---------------- 6 files changed, 117 insertions(+), 25 deletions(-) diff --git a/docs/dist/himalaya.js b/docs/dist/himalaya.js index 357ad42..0b9d5c1 100644 --- a/docs/dist/himalaya.js +++ b/docs/dist/himalaya.js @@ -360,7 +360,6 @@ function lexTagAttributes(state) { var type = 'attribute'; for (var i = 0; i < wLen; i++) { var word = words[i]; - if (!(word && word.length)) continue; var isNotPair = word.indexOf('=') === -1; if (isNotPair) { var secondWord = words[i + 1]; diff --git a/docs/dist/himalaya.js.map b/docs/dist/himalaya.js.map index ade1207..139ad7a 100644 --- a/docs/dist/himalaya.js.map +++ b/docs/dist/himalaya.js.map @@ -1 +1 @@ -{"version":3,"names":[],"mappings":"","sources":["himalaya.js"],"sourcesContent":["(function(f){if(typeof exports===\"object\"&&typeof module!==\"undefined\"){module.exports=f()}else if(typeof define===\"function\"&&define.amd){define([],f)}else{var g;if(typeof window!==\"undefined\"){g=window}else if(typeof global!==\"undefined\"){g=global}else if(typeof self!==\"undefined\"){g=self}else{g=this}g.himalaya = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require==\"function\"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error(\"Cannot find module '\"+o+\"'\");throw f.code=\"MODULE_NOT_FOUND\",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require==\"function\"&&require;for(var o=0;o= 0 ? lookupIndex : len + lookupIndex;\n while (searchIndex < len) {\n var element = array[searchIndex++];\n if (element === searchElement) return true;\n if (isNaNElement && isRealNaN(element)) return true;\n }\n\n return false;\n}\n\n},{}],2:[function(require,module,exports){\n'use strict';\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.splitHead = splitHead;\nexports.unquote = unquote;\nexports.format = format;\nexports.formatAttributes = formatAttributes;\nfunction splitHead(str, sep) {\n var idx = str.indexOf(sep);\n if (idx === -1) return [str];\n return [str.slice(0, idx), str.slice(idx + sep.length)];\n}\n\nfunction unquote(str) {\n var car = str.charAt(0);\n var end = str.length - 1;\n var isQuoteStart = car === '\"' || car === \"'\";\n if (isQuoteStart && car === str.charAt(end)) {\n return str.slice(1, end);\n }\n return str;\n}\n\nfunction format(nodes) {\n return nodes.map(function (node) {\n var type = node.type;\n if (type === 'element') {\n var tagName = node.tagName.toLowerCase();\n var attributes = formatAttributes(node.attributes);\n var children = format(node.children);\n return { type: type, tagName: tagName, attributes: attributes, children: children };\n }\n\n return { type: type, content: node.content };\n });\n}\n\nfunction formatAttributes(attributes) {\n return attributes.map(function (attribute) {\n var parts = splitHead(attribute.trim(), '=');\n var key = parts[0];\n var value = typeof parts[1] === 'string' ? unquote(parts[1]) : null;\n return { key: key, value: value };\n });\n}\n\n},{}],3:[function(require,module,exports){\n'use strict';\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.parseDefaults = undefined;\nexports.parse = parse;\nexports.stringify = stringify;\n\nvar _lexer = require('./lexer');\n\nvar _lexer2 = _interopRequireDefault(_lexer);\n\nvar _parser = require('./parser');\n\nvar _parser2 = _interopRequireDefault(_parser);\n\nvar _format = require('./format');\n\nvar _stringify = require('./stringify');\n\nvar _tags = require('./tags');\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar parseDefaults = exports.parseDefaults = {\n voidTags: _tags.voidTags,\n closingTags: _tags.closingTags,\n childlessTags: _tags.childlessTags,\n closingTagAncestorBreakers: _tags.closingTagAncestorBreakers\n};\n\nfunction parse(str) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : parseDefaults;\n\n var tokens = (0, _lexer2.default)(str, options);\n var nodes = (0, _parser2.default)(tokens, options);\n return (0, _format.format)(nodes, options);\n}\n\nfunction stringify(ast) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : parseDefaults;\n\n return (0, _stringify.toHTML)(ast, options);\n}\n\n},{\"./format\":2,\"./lexer\":4,\"./parser\":5,\"./stringify\":6,\"./tags\":7}],4:[function(require,module,exports){\n'use strict';\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = lexer;\nexports.lex = lex;\nexports.findTextEnd = findTextEnd;\nexports.lexText = lexText;\nexports.lexComment = lexComment;\nexports.lexTag = lexTag;\nexports.isWhitespaceChar = isWhitespaceChar;\nexports.lexTagName = lexTagName;\nexports.lexTagAttributes = lexTagAttributes;\nexports.lexSkipTag = lexSkipTag;\n\nvar _compat = require('./compat');\n\nfunction _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }\n\nfunction lexer(str, options) {\n var state = { str: str, options: options, cursor: 0, tokens: [] };\n lex(state);\n return state.tokens;\n}\n\nfunction lex(state) {\n var str = state.str;\n\n var len = str.length;\n while (state.cursor < len) {\n var start = state.cursor;\n lexText(state);\n if (state.cursor === start) {\n var isComment = (0, _compat.startsWith)(str, '!--', state.cursor + 1);\n if (isComment) {\n lexComment(state);\n } else {\n var tagName = lexTag(state);\n var safeTag = tagName.toLowerCase();\n var childlessTags = state.options.childlessTags;\n\n if ((0, _compat.arrayIncludes)(childlessTags, safeTag)) {\n lexSkipTag(tagName, state);\n }\n }\n }\n }\n}\n\nvar alphanumeric = /[A-Za-z0-9]/;\nfunction findTextEnd(str, index) {\n while (true) {\n var textEnd = str.indexOf('<', index);\n if (textEnd === -1) {\n return textEnd;\n }\n var char = str.charAt(textEnd + 1);\n if (char === '/' || char === '!' || alphanumeric.test(char)) {\n return textEnd;\n }\n index = textEnd + 1;\n }\n}\n\nfunction lexText(state) {\n var type = 'text';\n var str = state.str,\n cursor = state.cursor;\n\n var textEnd = findTextEnd(str, cursor);\n if (textEnd === -1) {\n // there is only text left\n var _content = str.slice(cursor);\n state.cursor = str.length;\n state.tokens.push({ type: type, content: _content });\n return;\n }\n\n if (textEnd === cursor) return;\n\n var content = str.slice(cursor, textEnd);\n state.cursor = textEnd;\n state.tokens.push({ type: type, content: content });\n}\n\nfunction lexComment(state) {\n state.cursor += 4; // \"', cursor);\n var type = 'comment';\n if (commentEnd === -1) {\n // there is only the comment left\n var _content2 = str.slice(cursor);\n state.cursor = str.length;\n state.tokens.push({ type: type, content: _content2 });\n return;\n }\n\n var content = str.slice(cursor, commentEnd);\n state.cursor = commentEnd + 3; // \"-->\".length\n state.tokens.push({ type: type, content: content });\n}\n\nfunction lexTag(state) {\n var str = state.str;\n\n {\n var secondChar = str.charAt(state.cursor + 1);\n var close = secondChar === '/';\n state.tokens.push({ type: 'tag-start', close: close });\n state.cursor += close ? 2 : 1;\n }\n var tagName = lexTagName(state);\n lexTagAttributes(state);\n {\n var firstChar = str.charAt(state.cursor);\n var _close = firstChar === '/';\n state.tokens.push({ type: 'tag-end', close: _close });\n state.cursor += _close ? 2 : 1;\n }\n return tagName;\n}\n\n// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#special-white-space\nvar whitespace = /\\s/;\nfunction isWhitespaceChar(char) {\n return whitespace.test(char);\n}\n\nfunction lexTagName(state) {\n var str = state.str,\n cursor = state.cursor;\n\n var len = str.length;\n var start = cursor;\n while (start < len) {\n var char = str.charAt(start);\n var isTagChar = !(isWhitespaceChar(char) || char === '/' || char === '>');\n if (isTagChar) break;\n start++;\n }\n\n var end = start + 1;\n while (end < len) {\n var _char = str.charAt(end);\n var _isTagChar = !(isWhitespaceChar(_char) || _char === '/' || _char === '>');\n if (!_isTagChar) break;\n end++;\n }\n\n state.cursor = end;\n var tagName = str.slice(start, end);\n state.tokens.push({ type: 'tag', content: tagName });\n return tagName;\n}\n\nfunction lexTagAttributes(state) {\n var str = state.str,\n tokens = state.tokens;\n\n var cursor = state.cursor;\n var quote = null; // null, single-, or double-quote\n var wordBegin = cursor; // index of word start\n var words = []; // \"key\", \"key=value\", \"key='value'\", etc\n var len = str.length;\n while (cursor < len) {\n var char = str.charAt(cursor);\n if (quote) {\n var isQuoteEnd = char === quote;\n if (isQuoteEnd) {\n quote = null;\n }\n cursor++;\n continue;\n }\n\n var isTagEnd = char === '/' || char === '>';\n if (isTagEnd) {\n if (cursor !== wordBegin) {\n words.push(str.slice(wordBegin, cursor));\n }\n break;\n }\n\n var isWordEnd = isWhitespaceChar(char);\n if (isWordEnd) {\n if (cursor !== wordBegin) {\n words.push(str.slice(wordBegin, cursor));\n }\n wordBegin = cursor + 1;\n cursor++;\n continue;\n }\n\n var isQuoteStart = char === '\\'' || char === '\"';\n if (isQuoteStart) {\n quote = char;\n cursor++;\n continue;\n }\n\n cursor++;\n }\n state.cursor = cursor;\n\n var wLen = words.length;\n var type = 'attribute';\n for (var i = 0; i < wLen; i++) {\n var word = words[i];\n if (!(word && word.length)) continue;\n var isNotPair = word.indexOf('=') === -1;\n if (isNotPair) {\n var secondWord = words[i + 1];\n if (secondWord && (0, _compat.startsWith)(secondWord, '=')) {\n if (secondWord.length > 1) {\n var newWord = word + secondWord;\n tokens.push({ type: type, content: newWord });\n i += 1;\n continue;\n }\n var thirdWord = words[i + 2];\n i += 1;\n if (thirdWord) {\n var _newWord = word + '=' + thirdWord;\n tokens.push({ type: type, content: _newWord });\n i += 1;\n continue;\n }\n }\n }\n if ((0, _compat.endsWith)(word, '=')) {\n var _secondWord = words[i + 1];\n if (_secondWord && !(0, _compat.stringIncludes)(_secondWord, '=')) {\n var _newWord3 = word + _secondWord;\n tokens.push({ type: type, content: _newWord3 });\n i += 1;\n continue;\n }\n\n var _newWord2 = word.slice(0, -1);\n tokens.push({ type: type, content: _newWord2 });\n continue;\n }\n\n tokens.push({ type: type, content: word });\n }\n}\n\nfunction lexSkipTag(tagName, state) {\n var str = state.str,\n cursor = state.cursor,\n tokens = state.tokens;\n\n var len = str.length;\n var index = cursor;\n while (index < len) {\n var nextTag = str.indexOf('= 0) {\n var parentTagName = stack[currentIndex].tagName;\n if (parentTagName === tagName) {\n break;\n }\n if ((0, _compat.arrayIncludes)(tagParents, parentTagName)) {\n return true;\n }\n currentIndex--;\n }\n }\n return false;\n}\n\nfunction parse(state) {\n var tokens = state.tokens,\n options = state.options;\n var stack = state.stack;\n\n var nodes = stack[stack.length - 1].children;\n var len = tokens.length;\n var cursor = state.cursor;\n\n while (cursor < len) {\n var token = tokens[cursor];\n if (token.type !== 'tag-start') {\n nodes.push(token);\n cursor++;\n continue;\n }\n\n var tagToken = tokens[++cursor];\n cursor++;\n var tagName = tagToken.content.toLowerCase();\n if (token.close) {\n var item = void 0;\n while (item = stack.pop()) {\n if (tagName === item.tagName) break;\n }\n while (cursor < len) {\n var endToken = tokens[cursor];\n if (endToken.type !== 'tag-end') break;\n cursor++;\n }\n break;\n }\n\n var isClosingTag = (0, _compat.arrayIncludes)(options.closingTags, tagName);\n var shouldRewindToAutoClose = isClosingTag;\n if (shouldRewindToAutoClose) {\n var terminals = options.closingTagAncestorBreakers;\n\n shouldRewindToAutoClose = !hasTerminalParent(tagName, stack, terminals);\n }\n\n if (shouldRewindToAutoClose) {\n // rewind the stack to just above the previous\n // closing tag of the same name\n var currentIndex = stack.length - 1;\n while (currentIndex > 0) {\n if (tagName === stack[currentIndex].tagName) {\n stack = stack.slice(0, currentIndex);\n var previousIndex = currentIndex - 1;\n nodes = stack[previousIndex].children;\n break;\n }\n currentIndex = currentIndex - 1;\n }\n }\n\n var attributes = [];\n var attrToken = void 0;\n while (cursor < len) {\n attrToken = tokens[cursor];\n if (attrToken.type === 'tag-end') break;\n attributes.push(attrToken.content);\n cursor++;\n }\n\n cursor++;\n var children = [];\n nodes.push({\n type: 'element',\n tagName: tagToken.content,\n attributes: attributes,\n children: children\n });\n\n var hasChildren = !(attrToken.close || (0, _compat.arrayIncludes)(options.voidTags, tagName));\n if (hasChildren) {\n stack.push({ tagName: tagName, children: children });\n var innerState = { tokens: tokens, options: options, cursor: cursor, stack: stack };\n parse(innerState);\n cursor = innerState.cursor;\n }\n }\n state.cursor = cursor;\n}\n\n},{\"./compat\":1}],6:[function(require,module,exports){\n'use strict';\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.formatAttributes = formatAttributes;\nexports.toHTML = toHTML;\n\nvar _compat = require('./compat');\n\nfunction formatAttributes(attributes) {\n return attributes.reduce(function (attrs, attribute) {\n var key = attribute.key,\n value = attribute.value;\n\n if (value === null) {\n return attrs + ' ' + key;\n }\n var quoteEscape = value.indexOf('\\'') !== -1;\n var quote = quoteEscape ? '\"' : '\\'';\n return attrs + ' ' + key + '=' + quote + value + quote;\n }, '');\n}\n\nfunction toHTML(tree, options) {\n return tree.map(function (node) {\n if (node.type === 'text') {\n return node.content;\n }\n if (node.type === 'comment') {\n return '';\n }\n var tagName = node.tagName,\n attributes = node.attributes,\n children = node.children;\n\n var isSelfClosing = (0, _compat.arrayIncludes)(options.voidTags, tagName.toLowerCase());\n return isSelfClosing ? '<' + tagName + formatAttributes(attributes) + '>' : '<' + tagName + formatAttributes(attributes) + '>' + toHTML(children, options) + '';\n }).join('');\n}\n\nexports.default = { toHTML: toHTML };\n\n},{\"./compat\":1}],7:[function(require,module,exports){\n'use strict';\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n/*\n Tags which contain arbitary non-parsed content\n For example: