[WIP] feat: html support (#4753)

* feat: html support

* fix: support empty lines between children

* refactor: isScriptTagNode util

* refactor: rename `glimmer` and `handlebars` tests

* fix don't add newline after `directive` node

* fix: support `pre` tag

* fix: don't trim comments content

* fix: `script` and `style` tags output

* feat: indent `head` and `body` inside `html`

* fix: better handle quotes around attribute value

* feat: better handle embed

* tests: svg into html

* fix: better handle boolean and empty attributes

* chore(deps): update `parse5` to latest version

* fix: better handle empty attribute

* tests: fix

* fix: extra newline in multiline node

* fix: print `pre` and `textarea` in right way

* tests: xhtml doctype

* fix: use `>` instead `/>`

* tests: case

* refactor: remove `toLowerCase()`

* feat: support `prettier-ignore`

* tests: empty `script` and `style` tags

* fix: clean

* tests: refactor and more
This commit is contained in:
Evilebot Tnawi authored and Suchipi committed 2018-09-13 07:06:49 -06:00
1 parent 59720392c3
commit 8b0bdf52be
88 files changed
+2846 -191

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
"use strict";
module.exports = function(ast, newNode) {
delete newNode.__location;
delete newNode.sourceCodeLocation;
if (ast.type === "text") {
return null;
+3 -3
View File
@@ -21,7 +21,7 @@ function embed(path, print, textToDoc, options) {
) {
const parser = options.parser === "flow" ? "flow" : "babylon";
const doc = textToDoc(getText(options, node), { parser });
return concat([hardline, doc]);
return concat([hardline, stripTrailingHardline(doc)]);
}
// Inline TypeScript
@@ -35,7 +35,7 @@ function embed(path, print, textToDoc, options) {
{ parser: "typescript" },
options
);
return concat([hardline, doc]);
return concat([hardline, stripTrailingHardline(doc)]);
}
// Inline Styles
@@ -49,7 +49,7 @@ function embed(path, print, textToDoc, options) {
case "attribute": {
/*
* Vue binding sytax: JS expressions
* Vue binding syntax: JS expressions
* :class="{ 'some-key': value }"
* v-bind:id="'list-' + id"
* v-if="foo && !bar"
+7 -4
View File
@@ -3,12 +3,15 @@
function parse(text /*, parsers, opts*/) {
// Inline the require to avoid loading all the JS if we don't use it
const parse5 = require("parse5");
const htmlparser2TreeAdapter = require("parse5-htmlparser2-tree-adapter");
try {
const isFragment = !/^\s*<(!doctype|html|head|body|!--)/i.test(text);
const ast = (isFragment ? parse5.parseFragment : parse5.parse)(text, {
treeAdapter: parse5.treeAdapters.htmlparser2,
locationInfo: true
treeAdapter: htmlparser2TreeAdapter,
sourceCodeLocationInfo: true
});
return normalize(extendAst(ast));
} catch (error) {
throw error;
@@ -64,10 +67,10 @@ module.exports = {
parse,
astFormat: "htmlparser2",
locStart(node) {
return node.__location && node.__location.startOffset;
return node.sourceCodeLocation && node.sourceCodeLocation.startOffset;
},
locEnd(node) {
return node.__location && node.__location.endOffset;
return node.sourceCodeLocation && node.sourceCodeLocation.endOffset;
}
}
}
+299 -65
View File
@@ -2,34 +2,21 @@
const embed = require("./embed");
const clean = require("./clean");
const { hasNewlineInRange, hasIgnoreComment } = require("../common/util");
const { getLast, hasIgnoreComment } = require("../common/util");
const { isNextLineEmpty } = require("../common/util-shared");
const {
concat,
join,
hardline,
line,
softline,
group,
indent
} = require("../doc").builders;
// 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
};
builders: {
concat,
line,
hardline,
softline,
group,
indent,
conditionalGroup,
dedentToRoot
},
utils: { willBreak, isLineNext, isEmpty }
} = require("../doc");
function genericPrint(path, options, print) {
const n = path.getValue();
@@ -43,50 +30,170 @@ function genericPrint(path, options, print) {
switch (n.type) {
case "root": {
return printChildren(path, print);
return concat(printChildren(path, print, options));
}
case "directive": {
return concat(["<", n.data, ">", hardline]);
return concat([
"<",
n.data.replace('!DOCTYPE html ""', "!DOCTYPE html"),
">",
hardline
]);
}
case "text": {
const parentNode = path.getParentNode();
if (
parentNode &&
(isPreTagNode(parentNode) || isTextAreaTagNode(parentNode))
) {
return n.data;
}
return n.data.replace(/\s+/g, " ").trim();
}
case "script":
case "style":
case "tag": {
const selfClose = voidTags[n.name] ? ">" : " />";
const children = printChildren(path, print);
const isVoid = isVoidTagNode(n);
const openingPrinted = printOpeningPart(path, print);
const hasNewline = hasNewlineInRange(
options.originalText,
options.locStart(n),
options.locEnd(n)
);
// Print self closing tag
if (isVoid) {
return concat([openingPrinted]);
}
return group(
const closingPrinted = printClosingPart(path, print);
const hasChildren = n.children.length > 0;
// Print tags without children
if (!hasChildren) {
return concat([openingPrinted, closingPrinted]);
}
const children = printChildren(path, print, options);
// NOTE: If the next token is a U+000A LINE FEED (LF) character token, then ignore that token and move
// on to the next one. (Newlines at the start of textarea elements are ignored as an authoring convenience.)
if (isPreTagNode(n) || isTextAreaTagNode(n)) {
const originalTagContent = options.originalText.slice(
n.sourceCodeLocation.startTag.endOffset,
n.sourceCodeLocation.endTag.startOffset
);
const hasNewlineAfterTag = /^(\r\n|\r|\n)/.test(originalTagContent);
return dedentToRoot(
group(
concat([
openingPrinted,
hasNewlineAfterTag ? hardline : "",
concat(children),
closingPrinted
])
)
);
}
const isScriptTag = isScriptTagNode(n);
if (isScriptTag) {
return group(
concat([openingPrinted, concat(children), closingPrinted])
);
}
const containsTag =
n.children.filter(
node => ["script", "style", "tag"].indexOf(node.type) !== -1
).length > 0;
const containsMultipleAttributes = n.attributes.length > 1;
let forcedBreak =
willBreak(openingPrinted) || containsTag || containsMultipleAttributes;
// Trim trailing lines (or empty strings)
while (
children.length &&
(isLineNext(getLast(children)) || isEmpty(getLast(children)))
) {
children.pop();
}
// Trim leading lines (or empty strings)
while (
children.length &&
(isLineNext(children[0]) || isEmpty(children[0])) &&
(isLineNext(children[1]) || isEmpty(children[1]))
) {
children.shift();
children.shift();
}
// Detect whether we will force this element to output over multiple lines.
const multilineChildren = [];
children.forEach(child => {
multilineChildren.push(child);
if (willBreak(child)) {
forcedBreak = true;
}
});
const containsOnlyEmptyTextNodes = n.children.every(node => {
return node.type === "text" && /^\s+$/.test(node.data);
});
const printedMultilineChildren = concat([
!isScriptTag && !containsOnlyEmptyTextNodes ? hardline : "",
group(concat(multilineChildren), { shouldBreak: true })
]);
const multiLineElem = group(
concat([
hasNewline ? hardline : "",
"<",
n.name,
printAttributes(path, print),
n.children.length ? ">" : selfClose,
n.name.toLowerCase() === "html"
? concat([hardline, children])
: indent(children),
n.children.length ? concat([softline, "</", n.name, ">"]) : hardline
openingPrinted,
indent(printedMultilineChildren),
hardline,
closingPrinted
])
);
if (forcedBreak) {
return multiLineElem;
}
return conditionalGroup([
group(concat([openingPrinted, concat(children), closingPrinted])),
multiLineElem
]);
}
case "comment": {
return concat(["<!-- ", n.data.trim(), " -->"]);
return concat(["<!--", n.data, "-->"]);
}
case "attribute": {
if (!n.value) {
return n.key;
if (isBooleanAttributeNode(n)) {
return n.key;
}
const parentNode = path.getParentNode();
if (!parentNode || !parentNode.sourceCodeLocation) {
return n.key;
}
const attributeSourceCodeLocation =
parentNode.sourceCodeLocation.attrs[n.key];
const originalAttributeSourceCode = options.originalText.slice(
attributeSourceCodeLocation.startOffset,
attributeSourceCodeLocation.endOffset
);
const hasEqualSign = originalAttributeSourceCode.indexOf("=") !== -1;
return hasEqualSign ? concat([n.key, '=""']) : n.key;
}
return concat([n.key, '="', n.value, '"']);
return concat([n.key, '="', n.value.replace(/"/g, "&quot;"), '"']);
}
default:
@@ -95,25 +202,152 @@ function genericPrint(path, options, print) {
}
}
function printAttributes(path, print) {
const node = path.getValue();
return concat([
node.attributes.length ? " " : "",
indent(join(line, path.map(print, "attributes")))
]);
function isBooleanAttributeNode(node) {
return (
node.type === "attribute" &&
[
"allowfullscreen",
"allowpaymentrequest",
"async",
"autofocus",
"autoplay",
"checked",
"controls",
"default",
"defer",
"disabled",
"formnovalidate",
"hidden",
"ismap",
"itemscope",
"loop",
"multiple",
"muted",
"nomodule",
"novalidate",
"open",
"readonly",
"required",
"reversed",
"selected",
"typemustmatch"
].indexOf(node.key) !== -1
);
}
function printChildren(path, print) {
const children = [];
path.each(childPath => {
// http://w3c.github.io/html/single-page.html#void-elements
function isVoidTagNode(node) {
return (
node.type === "tag" &&
[
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"param",
"source",
"track",
"wbr"
].indexOf(node.name) !== -1
);
}
function isPreTagNode(node) {
return node.type === "tag" && node.name === "pre";
}
function isTextAreaTagNode(node) {
return node.type === "tag" && node.name === "textarea";
}
function isScriptTagNode(node) {
return (
(node.type === "script" || node.type === "style") &&
["script", "style"].indexOf(node.name) !== -1
);
}
function printOpeningPart(path, print) {
const n = path.getValue();
const isVoid = isVoidTagNode(n);
// Don't break self-closing elements with no attributes
if (isVoid && !n.attributes.length) {
return concat(["<", n.name, ">"]);
}
// Don't break up opening elements with a single long text attribute
if (n.attributes && n.attributes.length === 1 && n.attributes[0].value) {
return group(
concat([
"<",
path.call(print, "name"),
" ",
concat(path.map(print, "attributes")),
">"
])
);
}
return group(
concat([
"<",
n.name,
concat([
indent(
concat(path.map(attr => concat([line, print(attr)]), "attributes"))
),
softline
]),
">"
])
);
}
function printClosingPart(path, print) {
return concat(["</", path.call(print, "name"), ">"]);
}
function printChildren(path, print, options) {
const parts = [];
path.map((childPath, i) => {
const parentNode = childPath.getParentNode();
const child = childPath.getValue();
if (child.type !== "text") {
children.push(hardline);
const printedChild = print(childPath);
if (
parentNode &&
parentNode.children[i - 2] &&
parentNode.children[i - 2].type === "comment" &&
parentNode.children[i - 2].data.trim() === "prettier-ignore"
) {
parts.push(
options.originalText.slice(
options.locStart(child),
options.locEnd(child)
)
);
} else {
parts.push(printedChild);
}
if (child.type !== "text" && child.type !== "directive") {
parts.push(hardline);
}
if (isNextLineEmpty(options.originalText, childPath.getValue(), options)) {
parts.push(hardline);
}
children.push(childPath.call(print));
}, "children");
return concat(children);
return parts;
}
module.exports = {