[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:
1 parent
59720392c3
commit
8b0bdf52be
88 files changed
+2846
-191
No files matched your search
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = function(ast, newNode) {
|
||||
delete newNode.__location;
|
||||
delete newNode.sourceCodeLocation;
|
||||
|
||||
if (ast.type === "text") {
|
||||
return null;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, """), '"']);
|
||||
}
|
||||
|
||||
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 = {
|
||||
|
||||
Reference in new issue
Block a user