From d00956d51da38faa5c88d900ec5780cb1789b472 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sat, 10 Jun 2017 12:22:59 +1000 Subject: [PATCH] feat(html): add parse5/htmlparser2 printer (#2083) --- bin/prettier.js | 1 + package.json | 1 + src/options.js | 2 + src/parser-parse5.js | 25 +++++ src/parser.js | 31 +++--- src/printer-htmlparser2.js | 100 ++++++++++++++++++ src/printer.js | 2 + .../__snapshots__/jsfmt.spec.js.snap | 29 +++++ tests/html_basics/hello-world.html | 14 +++ tests/html_basics/jsfmt.spec.js | 1 + tests_config/run_spec.js | 2 +- yarn.lock | 10 ++ 12 files changed, 203 insertions(+), 15 deletions(-) create mode 100644 src/parser-parse5.js create mode 100644 src/printer-htmlparser2.js create mode 100644 tests/html_basics/__snapshots__/jsfmt.spec.js.snap create mode 100644 tests/html_basics/hello-world.html create mode 100644 tests/html_basics/jsfmt.spec.js diff --git a/bin/prettier.js b/bin/prettier.js index b32ce10c..741cee7d 100755 --- a/bin/prettier.js +++ b/bin/prettier.js @@ -98,6 +98,7 @@ function getParserOption() { value === "babylon" || value === "typescript" || value === "postcss" || + value === "parse5" || value === "graphql" ) { return value; diff --git a/package.json b/package.json index 9fd4cb8e..a3ec2bb4 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "jest-validate": "20.0.3", "minimist": "1.2.0", "mkdirp": "^0.5.1", + "parse5": "3.0.2", "postcss": "^6.0.1", "postcss-less": "^1.0.0", "postcss-media-query-parser": "0.2.3", diff --git a/src/options.js b/src/options.js index 73d40426..3f5de295 100644 --- a/src/options.js +++ b/src/options.js @@ -31,6 +31,8 @@ function normalize(options) { if (/\.(css|less|scss)$/.test(filepath)) { normalized.parser = "postcss"; + } else if (/\.html$/.test(filepath)) { + normalized.parser = "parse5"; } else if (/\.(ts|tsx)$/.test(filepath)) { normalized.parser = "typescript"; } diff --git a/src/parser-parse5.js b/src/parser-parse5.js new file mode 100644 index 00000000..09f8c9d4 --- /dev/null +++ b/src/parser-parse5.js @@ -0,0 +1,25 @@ +"use strict"; + +// const createError = require("./parser-create-error"); + +function parse(text) { + // Inline the require to avoid loading all the JS if we don't use it + const parse5 = require("parse5"); + try { + const ast = parse5.parse(text, { + treeAdapter: parse5.treeAdapters.htmlparser2 + }); + return ast; + } catch (error) { + // throw createError(error.message, { + // start: { + // line: error.locations[0].line, + // column: error.locations[0].column + // } + // } else { + throw error; + // } + } +} + +module.exports = parse; diff --git a/src/parser.js b/src/parser.js index c1789b41..83d17d1f 100644 --- a/src/parser.js +++ b/src/parser.js @@ -1,22 +1,25 @@ "use strict"; -function parse(text, opts) { - let parseFunction; - - if (opts.parser === "flow") { - parseFunction = eval("require")("./parser-flow"); - } else if (opts.parser === "graphql") { - parseFunction = eval("require")("./parser-graphql"); - } else if (opts.parser === "typescript") { - parseFunction = eval("require")("./parser-typescript"); - } else if (opts.parser === "postcss") { - parseFunction = eval("require")("./parser-postcss"); - } else { - parseFunction = eval("require")("./parser-babylon"); +function getParseFunction(opts) { + switch (opts.parser) { + case "flow": + return eval("require")("./parser-flow"); + case "graphql": + return eval("require")("./parser-graphql"); + case "parse5": + return eval("require")("./parser-parse5"); + case "postcss": + return eval("require")("./parser-postcss"); + case "typescript": + return eval("require")("./parser-typescript"); + default: + return eval("require")("./parser-babylon"); } +} +function parse(text, opts) { try { - return parseFunction(text); + return getParseFunction(opts)(text); } catch (error) { const loc = error.loc; diff --git a/src/printer-htmlparser2.js b/src/printer-htmlparser2.js new file mode 100644 index 00000000..ae8d7f63 --- /dev/null +++ b/src/printer-htmlparser2.js @@ -0,0 +1,100 @@ +"use strict"; + +const docBuilders = require("./doc-builders"); +const concat = docBuilders.concat; +const join = docBuilders.join; +const hardline = docBuilders.hardline; +// const line = docBuilders.line; +const softline = docBuilders.softline; +const group = docBuilders.group; +const indent = docBuilders.indent; +// const ifBreak = docBuilders.ifBreak; + +// http://w3c.github.io/html/single-page.html#void-elements +const voidTags = { + area: true, + base: true, + br: true, + col: true, + embed: true, + hr: true, + img: true, + input: true, + link: true, + meta: true, + param: true, + source: true, + track: true, + wbr: true +}; + +function genericPrint(path, options, print) { + const n = path.getValue(); + if (!n) { + return ""; + } + + if (typeof n === "string") { + return n; + } + + switch (n.type) { + case "root": { + return concat(path.map(print, "children")); + } + case "directive": { + return concat(["<", n.data, ">", hardline]); + } + case "text": { + return n.data.replace(/\s+/g, " ").trim(); + } + case "tag": { + const selfClose = voidTags[n.name] ? ">" : " />"; + + const children = []; + path.each(childPath => { + const child = childPath.getValue(); + if (child.type !== "text") { + children.push(softline); + } + children.push(childPath.call(print)); + }, "children"); + + return group( + concat([ + "<", + n.name, + printAttributes(n.attribs), + + n.children.length ? ">" : selfClose, + + indent(concat(children)), + n.children.length ? concat([softline, ""]) : "" + ]) + ); + } + case "comment": { + return concat([""]); + } + default: + throw new Error("unknown htmlparser2 type: " + n.type); + } +} + +function printAttributes(attribs) { + const attributeKeys = Object.keys(attribs); + return concat([ + attributeKeys.length ? " " : "", + join( + " ", + attributeKeys.map(name => { + if (attribs[name] === "") { + return name; + } + return concat([name, '="', attribs[name], '"']); + }) + ) + ]); +} + +module.exports = genericPrint; diff --git a/src/printer.js b/src/printer.js index b285df7c..3b6a5a9e 100644 --- a/src/printer.js +++ b/src/printer.js @@ -52,6 +52,8 @@ function getPrintFunction(options) { switch (options.parser) { case "graphql": return require("./printer-graphql"); + case "parse5": + return require("./printer-htmlparser2"); case "postcss": return require("./printer-postcss"); default: diff --git a/tests/html_basics/__snapshots__/jsfmt.spec.js.snap b/tests/html_basics/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 00000000..2bcd2edb --- /dev/null +++ b/tests/html_basics/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`hello-world.html 1`] = ` + + + + + + + Document + + + +

Hello World

+ + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + + + + + Document + +

Hello World

+ +`; diff --git a/tests/html_basics/hello-world.html b/tests/html_basics/hello-world.html new file mode 100644 index 00000000..12ed3767 --- /dev/null +++ b/tests/html_basics/hello-world.html @@ -0,0 +1,14 @@ + + + + + + + Document + + + +

Hello World

+ + + diff --git a/tests/html_basics/jsfmt.spec.js b/tests/html_basics/jsfmt.spec.js new file mode 100644 index 00000000..04b24727 --- /dev/null +++ b/tests/html_basics/jsfmt.spec.js @@ -0,0 +1 @@ +run_spec(__dirname, { parser: "parse5" }); diff --git a/tests_config/run_spec.js b/tests_config/run_spec.js index 72578dae..ca3f9f2c 100644 --- a/tests_config/run_spec.js +++ b/tests_config/run_spec.js @@ -16,7 +16,7 @@ function run_spec(dirname, options, additionalParsers) { fs.readdirSync(dirname).forEach(filename => { const extension = extname(filename); if ( - /^\.([jt]sx?|css|graphql)$/.test(extension) && + /^\.([jt]sx?|css|graphql|html)$/.test(extension) && filename !== "jsfmt.spec.js" ) { const path = dirname + "/" + filename; diff --git a/yarn.lock b/yarn.lock index b37111de..a7ef2c4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,10 @@ # yarn lockfile v1 +"@types/node@^6.0.46": + version "6.0.78" + resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.78.tgz#5d4a3f579c1524e01ee21bf474e6fba09198f470" + abab@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d" @@ -2746,6 +2750,12 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" +parse5@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.2.tgz#05eff57f0ef4577fb144a79f8b9a967a6cc44510" + dependencies: + "@types/node" "^6.0.46" + parse5@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94"