diff --git a/package.json b/package.json
index c9615c01..8c6e57dc 100644
--- a/package.json
+++ b/package.json
@@ -38,12 +38,14 @@
"globby": "6.1.0",
"graphql": "0.13.2",
"html-element-attributes": "2.0.0",
+ "html-styles": "1.0.0",
"html-tag-names": "1.1.2",
"htmlparser2": "3.9.2",
"ignore": "3.3.7",
"jest-docblock": "23.2.0",
"json-stable-stringify": "1.0.1",
"leven": "2.1.0",
+ "lines-and-columns": "1.1.6",
"linguist-languages": "6.2.1-dev.20180706",
"lodash.uniqby": "4.7.0",
"mem": "1.1.0",
diff --git a/src/language-html/clean.js b/src/language-html/clean.js
index 81eac82c..6c16c1db 100644
--- a/src/language-html/clean.js
+++ b/src/language-html/clean.js
@@ -5,12 +5,12 @@ module.exports = function(ast, newNode) {
delete newNode.endIndex;
delete newNode.attribs;
- if (ast.type === "text") {
+ if (ast.type === "text" || ast.type === "comment") {
return null;
}
// may be formatted by multiparser
- if (ast.type === "yaml") {
+ if (ast.type === "yaml" || ast.type === "toml") {
return null;
}
diff --git a/src/language-html/constants.evaluate.js b/src/language-html/constants.evaluate.js
new file mode 100644
index 00000000..c56fc7f0
--- /dev/null
+++ b/src/language-html/constants.evaluate.js
@@ -0,0 +1,30 @@
+"use strict";
+
+const htmlStyles = require("html-styles");
+
+const getCssStyleTags = property =>
+ htmlStyles
+ .filter(htmlStyle => htmlStyle.style[property])
+ .map(htmlStyle =>
+ htmlStyle.selectorText
+ .split(",")
+ .map(selector => selector.trim())
+ .filter(selector => /^[a-zA-Z0-9]+$/.test(selector))
+ .reduce((reduced, tagName) => {
+ reduced[tagName] = htmlStyle.style[property];
+ return reduced;
+ }, {})
+ )
+ .reduce((reduced, value) => Object.assign(reduced, value), {});
+
+const CSS_DISPLAY_TAGS = getCssStyleTags("display");
+const CSS_DISPLAY_DEFAULT = "inline";
+const CSS_WHITE_SPACE_TAGS = getCssStyleTags("white-space");
+const CSS_WHITE_SPACE_DEFAULT = "normal";
+
+module.exports = {
+ CSS_DISPLAY_TAGS,
+ CSS_DISPLAY_DEFAULT,
+ CSS_WHITE_SPACE_TAGS,
+ CSS_WHITE_SPACE_DEFAULT
+};
diff --git a/src/language-html/embed.js b/src/language-html/embed.js
deleted file mode 100644
index d255e4d8..00000000
--- a/src/language-html/embed.js
+++ /dev/null
@@ -1,106 +0,0 @@
-"use strict";
-
-const { hasNewlineInRange } = require("../common/util");
-const {
- builders: { hardline, concat, markAsRoot, literalline },
- utils: { removeLines, mapDoc }
-} = require("../doc");
-
-function embed(path, print, textToDoc, options) {
- const node = path.getValue();
-
- switch (node.type) {
- case "text": {
- const parent = path.getParentNode();
- // Inline JavaScript
- if (
- parent.type === "script" &&
- ((!parent.attribs.lang && !parent.attribs.type) ||
- parent.attribs.type === "text/javascript" ||
- parent.attribs.type === "text/babel" ||
- parent.attribs.type === "application/javascript")
- ) {
- const parser = options.parser === "flow" ? "flow" : "babylon";
- const doc = textToDoc(node.data, { parser });
- return concat([hardline, doc]);
- }
-
- // Inline TypeScript
- if (
- parent.type === "script" &&
- (parent.attribs.type === "application/x-typescript" ||
- parent.attribs.lang === "ts")
- ) {
- const doc = textToDoc(node.data, { parser: "typescript" }, options);
- return concat([hardline, doc]);
- }
-
- // Inline Styles
- if (parent.type === "style") {
- const doc = textToDoc(node.data, { parser: "css" });
- return concat([hardline, doc]);
- }
-
- break;
- }
-
- case "attribute": {
- /*
- * Vue binding syntax: JS expressions
- * :class="{ 'some-key': value }"
- * v-bind:id="'list-' + id"
- * v-if="foo && !bar"
- * @click="someFunction()"
- */
- if (/(^@)|(^v-)|:/.test(node.key) && !/^\w+$/.test(node.value)) {
- const doc = textToDoc(node.value, {
- parser: "__js_expression",
- // Use singleQuote since HTML attributes use double-quotes.
- // TODO(azz): We still need to do an entity escape on the attribute.
- singleQuote: true
- });
- return concat([
- node.key,
- '="',
- hasNewlineInRange(node.value, 0, node.value.length)
- ? doc
- : removeLines(doc),
- '"'
- ]);
- }
-
- break;
- }
-
- case "yaml":
- return markAsRoot(
- concat([
- "---",
- hardline,
- node.value.trim()
- ? replaceNewlinesWithLiterallines(
- textToDoc(node.value, { parser: "yaml" })
- )
- : "",
- "---",
- hardline
- ])
- );
- }
-}
-
-function replaceNewlinesWithLiterallines(doc) {
- return mapDoc(
- doc,
- currentDoc =>
- typeof currentDoc === "string" && currentDoc.includes("\n")
- ? concat(
- currentDoc
- .split(/(\n)/g)
- .map((v, i) => (i % 2 === 0 ? v : literalline))
- )
- : currentDoc
- );
-}
-
-module.exports = embed;
diff --git a/src/language-html/index.js b/src/language-html/index.js
index 3a7e9a78..3a9be510 100644
--- a/src/language-html/index.js
+++ b/src/language-html/index.js
@@ -2,6 +2,7 @@
const printer = require("./printer-htmlparser2");
const createLanguage = require("../utils/create-language");
+const options = require("./options");
const languages = [
createLanguage(require("linguist-languages/data/html"), {
@@ -19,5 +20,6 @@ const printers = {
module.exports = {
languages,
- printers
+ printers,
+ options
};
diff --git a/src/language-html/options.js b/src/language-html/options.js
new file mode 100644
index 00000000..ee8d6c05
--- /dev/null
+++ b/src/language-html/options.js
@@ -0,0 +1,28 @@
+"use strict";
+
+const CATEGORY_HTML = "HTML";
+
+// format based on https://github.com/prettier/prettier/blob/master/src/main/core-options.js
+module.exports = {
+ htmlWhitespaceSensitivity: {
+ since: "1.15.0",
+ category: CATEGORY_HTML,
+ type: "choice",
+ default: "css",
+ description: "How to handle whitespaces in HTML.",
+ choices: [
+ {
+ value: "css",
+ description: "Respect the default value of CSS display property."
+ },
+ {
+ value: "strict",
+ description: "Whitespaces are considered sensitive."
+ },
+ {
+ value: "ignore",
+ description: "Whitespaces are considered insensitive."
+ }
+ ]
+ }
+};
diff --git a/src/language-html/parser-html.js b/src/language-html/parser-html.js
index 6c8cec27..1e346964 100644
--- a/src/language-html/parser-html.js
+++ b/src/language-html/parser-html.js
@@ -1,10 +1,12 @@
"use strict";
const parseFrontMatter = require("../utils/front-matter");
-const { HTML_TAGS, HTML_ELEMENT_ATTRIBUTES } = require("./utils");
+const { HTML_ELEMENT_ATTRIBUTES, HTML_TAGS, mapNode } = require("./utils");
-function parse(text /*, parsers, opts*/) {
- const { frontMatter, content } = parseFrontMatter(text);
+function parse(text, parsers, options, { shouldParseFrontMatter = true } = {}) {
+ const { frontMatter, content } = shouldParseFrontMatter
+ ? parseFrontMatter(text)
+ : { frontMatter: null, content: text };
// Inline the require to avoid loading all the JS if we don't use it
const Parser = require("htmlparser2/lib/Parser");
@@ -27,7 +29,13 @@ function parse(text /*, parsers, opts*/) {
super.onattribdata(value);
}
onattribend() {
- super.onattribend();
+ if (this._cbs.onattribute) {
+ this._cbs.onattribute(this._attribname, this._attribvalue);
+ }
+ if (this._attribs) {
+ this._attribs.push([this._attribname, this._attribvalue]);
+ }
+ this._attribname = "";
this._attribvalue = null;
}
onselfclosingtag() {
@@ -43,15 +51,36 @@ function parse(text /*, parsers, opts*/) {
this.onopentagend();
}
}
+ onopentagname(name) {
+ super.onopentagname(name);
+ if (this._cbs.onopentag) {
+ this._attribs = [];
+ }
+ }
}
/**
* modifications:
- * - add `selfClosing` field
+ * - add `isSelfClosing` field
+ * - correct `endIndex` for whitespaces before closing tag end marker (e.g., ` `)
*/
class CustomDomHandler extends DomHandler {
onselfclosingtag() {
- this._tagStack[this._tagStack.length - 1].selfClosing = true;
+ this._tagStack[this._tagStack.length - 1].isSelfClosing = true;
+ }
+ onclosetag() {
+ const elem = this._tagStack.pop();
+ if (this._options.withEndIndices && elem) {
+ const buffer = this._parser._tokenizer._buffer;
+ let endIndex = this._parser.endIndex;
+ while (buffer[endIndex] && buffer[endIndex] !== ">") {
+ endIndex++;
+ }
+ elem.endIndex = buffer[endIndex] ? endIndex : this._parser.endIndex;
+ }
+ if (this._elementCB) {
+ this._elementCB(elem);
+ }
}
}
@@ -66,13 +95,59 @@ function parse(text /*, parsers, opts*/) {
recognizeSelfClosing: true
}).end(content);
- const ast = normalize({ type: "root", children: handler.dom }, text);
+ const ast = normalize(
+ {
+ type: "root",
+ children: handler.dom,
+ startIndex: 0,
+ endIndex: text.length
+ },
+ text
+ );
if (frontMatter) {
ast.children.unshift(frontMatter);
}
- return ast;
+ const parseHtml = data =>
+ parse(data, parsers, options, {
+ shouldParseFrontMatter: false
+ });
+
+ return mapNode(ast, node => {
+ const ieConditionalComment = parseIeConditionalComment(node, parseHtml);
+ return ieConditionalComment ? ieConditionalComment : node;
+ });
+}
+
+function parseIeConditionalComment(node, parseHtml) {
+ if (node.type !== "comment") {
+ return null;
+ }
+
+ const match = node.data.match(/^(\[if([^\]]*?)\]>)([\s\S]*?)
+ Object.assign({}, currentNode, {
+ startIndex: baseIndex + currentNode.startIndex,
+ endIndex: baseIndex + currentNode.endIndex
+ })
+ ),
+ {
+ type: "ieConditionalComment",
+ condition: condition.trim().replace(/\s+/g, " ")
+ }
+ );
}
function normalize(node, text) {
@@ -90,7 +165,7 @@ function normalize(node, text) {
if (node.attribs) {
const CURRENT_HTML_ELEMENT_ATTRIBUTES =
HTML_ELEMENT_ATTRIBUTES[node.name] || Object.create(null);
- const attributes = Object.keys(node.attribs).map(attributeKey => {
+ const attributes = node.attribs.map(([attributeKey, attributeValue]) => {
const lowerCaseAttributeKey = attributeKey.toLowerCase();
return {
type: "attribute",
@@ -99,7 +174,7 @@ function normalize(node, text) {
lowerCaseAttributeKey in CURRENT_HTML_ELEMENT_ATTRIBUTES
? lowerCaseAttributeKey
: attributeKey,
- value: node.attribs[attributeKey]
+ value: attributeValue
};
});
diff --git a/src/language-html/preprocess.js b/src/language-html/preprocess.js
new file mode 100644
index 00000000..a5adcf24
--- /dev/null
+++ b/src/language-html/preprocess.js
@@ -0,0 +1,285 @@
+"use strict";
+
+const {
+ VOID_TAGS,
+ getNodeCssStyleDisplay,
+ getNodeCssStyleWhiteSpace,
+ getPrevNode,
+ isDanglingSpaceSensitiveNode,
+ isLeadingSpaceSensitiveNode,
+ isScriptLikeTag,
+ isTrailingSpaceSensitiveNode,
+ mapNode
+} = require("./utils");
+const LineAndColumn = (m => m.default || m)(require("lines-and-columns"));
+
+const PREPROCESS_PIPELINE = [
+ renameScriptAndStyleWithTag,
+ processDirectives,
+ addIsSelfClosing,
+ extractWhitespaces,
+ addCssDisplay,
+ addIsSpaceSensitive,
+ addStartAndEndLocation,
+ addShortcuts
+];
+
+function preprocess(ast, options) {
+ for (const fn of PREPROCESS_PIPELINE) {
+ ast = fn(ast, options);
+ }
+ return ast;
+}
+
+/** add `startLocation` and `endLocation` field */
+function addStartAndEndLocation(ast, options) {
+ const locator = new LineAndColumn(options.originalText);
+ return mapNode(ast, node => {
+ const startLocation = locator.locationForIndex(options.locStart(node));
+ const endLocation = locator.locationForIndex(options.locEnd(node) - 1);
+ return Object.assign({}, node, { startLocation, endLocation });
+ });
+}
+
+/** rename `script` and `style` with `tag` */
+function renameScriptAndStyleWithTag(ast /*, options */) {
+ return mapNode(ast, node => {
+ return node.type === "script" || node.type === "style"
+ ? Object.assign({}, node, { type: "tag" })
+ : node;
+ });
+}
+
+/** add `isSelfClosing` for void tags, directives, and comments */
+function addIsSelfClosing(ast /*, options */) {
+ return mapNode(ast, node => {
+ if (
+ (node.type === "tag" && node.name in VOID_TAGS) ||
+ node.type === "directive" ||
+ node.type === "comment"
+ ) {
+ return Object.assign({}, node, { isSelfClosing: true });
+ }
+ return node;
+ });
+}
+
+function processDirectives(ast /*, options */) {
+ return mapNode(ast, node => {
+ if (node.type !== "directive") {
+ return node;
+ }
+
+ const isDoctype = /^!doctype$/i.test(node.name);
+ const data = node.data.slice(node.name.length).replace(/\s+/g, " ");
+
+ return Object.assign({}, node, {
+ name: isDoctype ? "!DOCTYPE" : node.name,
+ data: isDoctype ? data.replace(/^\s+html/i, " html") : data,
+ // workaround for htmlparser2 bug
+ endIndex:
+ node.startIndex +
+ "<".length +
+ node.name.length +
+ node.data.length +
+ ">".length
+ });
+ });
+}
+
+/**
+ * - add `hasLeadingSpaces` field
+ * - add `hasTrailingSpaces` field
+ * - add `hasDanglingSpaces` field for parent nodes
+ * - add `isWhiteSpaceSensitive`, `isIndentationSensitive` field for text nodes
+ * - remove insensitive whitespaces
+ */
+function extractWhitespaces(ast /*, options*/) {
+ const TYPE_WHITESPACE = "whitespace";
+ return mapNode(ast, node => {
+ if (!node.children) {
+ return node;
+ }
+
+ if (
+ node.children.length === 0 ||
+ (node.children.length === 1 &&
+ node.children[0].type === "text" &&
+ node.children[0].data.trim().length === 0)
+ ) {
+ return Object.assign({}, node, {
+ children: [],
+ hasDanglingSpaces: node.children.length !== 0
+ });
+ }
+
+ const cssStyleWhiteSpace = getNodeCssStyleWhiteSpace(node);
+ const isCssStyleWhiteSpacePre = cssStyleWhiteSpace.startsWith("pre");
+ const isScriptLike = isScriptLikeTag(node);
+
+ return Object.assign({}, node, {
+ children: node.children
+ // extract whitespace nodes
+ .reduce((newChildren, child) => {
+ if (child.type !== "text") {
+ return newChildren.concat(child);
+ }
+
+ if (isCssStyleWhiteSpacePre || isScriptLike) {
+ return newChildren.concat(
+ Object.assign({}, child, {
+ isWhiteSpaceSensitive: true,
+ isIndentationSensitive: isCssStyleWhiteSpacePre
+ })
+ );
+ }
+
+ const localChildren = [];
+
+ const [, leadingSpaces, text, trailingSpaces] = child.data.match(
+ /^(\s*)([\s\S]*?)(\s*)$/
+ );
+
+ if (leadingSpaces) {
+ localChildren.push({ type: TYPE_WHITESPACE });
+ }
+
+ if (text) {
+ localChildren.push({
+ type: "text",
+ data: text,
+ startIndex: child.startIndex + leadingSpaces.length,
+ endIndex: child.endIndex - trailingSpaces.length
+ });
+ }
+
+ if (trailingSpaces) {
+ localChildren.push({ type: TYPE_WHITESPACE });
+ }
+
+ return newChildren.concat(localChildren);
+ }, [])
+ // set hasLeadingSpaces/hasTrailingSpaces and filter whitespace nodes
+ .reduce((newChildren, child, i, children) => {
+ if (child.type === TYPE_WHITESPACE) {
+ return newChildren;
+ }
+
+ const hasLeadingSpaces =
+ i !== 0 && children[i - 1].type === TYPE_WHITESPACE;
+ const hasTrailingSpaces =
+ i !== children.length - 1 &&
+ children[i + 1].type === TYPE_WHITESPACE;
+
+ return newChildren.concat(
+ Object.assign({}, child, {
+ hasLeadingSpaces,
+ hasTrailingSpaces
+ })
+ );
+ }, [])
+ });
+ });
+}
+
+function addCssDisplay(ast, options) {
+ return mapNode(ast, (node, stack) => {
+ const prevNode = getPrevNode(stack);
+ return Object.assign({}, node, {
+ cssDisplay: getNodeCssStyleDisplay(node, prevNode, options)
+ });
+ });
+}
+
+/**
+ * - add `isLeadingSpaceSensitive` field
+ * - add `isTrailingSpaceSensitive` field
+ * - add `isDanglingSpaceSensitive` field for parent nodes
+ */
+function addIsSpaceSensitive(ast /*, options */) {
+ return mapNode(ast, node => {
+ if (!node.children) {
+ return node;
+ }
+
+ if (node.children.length === 0) {
+ return Object.assign({}, node, {
+ isDanglingSpaceSensitive: isDanglingSpaceSensitiveNode(node)
+ });
+ }
+
+ return Object.assign({}, node, {
+ children: node.children
+ // set isLeadingSpaceSensitive
+ .map((child, i, children) => {
+ const prevChild = i === 0 ? null : children[i - 1];
+ const nextChild = i === children.length - 1 ? null : children[i + 1];
+ return Object.assign({}, child, {
+ isLeadingSpaceSensitive: isLeadingSpaceSensitiveNode(child, {
+ parent: node,
+ prev: prevChild,
+ next: nextChild
+ })
+ });
+ })
+ // set isTrailingSpaceSensitive and update isLeadingSpaceSensitive if necessary
+ .reduce((newChildren, child, i, children) => {
+ const prevChild = i === 0 ? null : newChildren[i - 1];
+ const nextChild = i === children.length - 1 ? null : children[i + 1];
+ const isTrailingSpaceSensitive =
+ nextChild && !nextChild.isLeadingSpaceSensitive
+ ? false
+ : isTrailingSpaceSensitiveNode(child, {
+ parent: node,
+ prev: prevChild,
+ next: nextChild
+ });
+ return newChildren.concat(
+ Object.assign(
+ {},
+ child,
+ { isTrailingSpaceSensitive },
+ prevChild &&
+ !prevChild.isTrailingSpaceSensitive &&
+ child.isLeadingSpaceSensitive
+ ? { isLeadingSpaceSensitive: false }
+ : null
+ )
+ );
+ }, [])
+ });
+ });
+}
+
+function addShortcuts(ast /*, options */) {
+ function _addShortcuts(node, parent, index) {
+ const prev = index === -1 ? null : parent.children[index - 1];
+ const next = index === -1 ? null : parent.children[index + 1];
+
+ const hasChildren = node.children && node.children.length !== 0;
+
+ const firstChild = !hasChildren ? null : node.children[0];
+ const lastChild = !hasChildren
+ ? null
+ : node.children[node.children.length - 1];
+
+ Object.defineProperties(node, {
+ parent: { value: parent, enumerable: false },
+ prev: { value: prev, enumerable: false },
+ next: { value: next, enumerable: false },
+ firstChild: { value: firstChild, enumerable: false },
+ lastChild: { value: lastChild, enumerable: false }
+ });
+
+ if (node.children) {
+ node.children.forEach((child, childIndex) =>
+ _addShortcuts(child, node, childIndex)
+ );
+ }
+ }
+
+ _addShortcuts(ast, null, -1);
+ return ast;
+}
+
+module.exports = preprocess;
diff --git a/src/language-html/printer-htmlparser2.js b/src/language-html/printer-htmlparser2.js
index df5ed050..279431e5 100644
--- a/src/language-html/printer-htmlparser2.js
+++ b/src/language-html/printer-htmlparser2.js
@@ -1,236 +1,582 @@
"use strict";
-const embed = require("./embed");
const clean = require("./clean");
-const { getLast } = require("../common/util");
-const { isNextLineEmpty } = require("../common/util-shared");
const {
- builders: {
- concat,
- line,
- hardline,
- softline,
- group,
- indent,
- conditionalGroup,
- dedentToRoot
- },
- utils: { willBreak, isLineNext, isEmpty }
+ builders,
+ utils: { removeLines, stripTrailingHardline }
} = require("../doc");
const {
- VOID_TAGS,
+ breakParent,
+ group,
+ hardline,
+ indent,
+ join,
+ line,
+ literalline,
+ markAsRoot,
+ softline
+} = builders;
+const { hasNewlineInRange } = require("../common/util");
+const {
+ normalizeParts,
+ dedentString,
+ forceBreakChildren,
+ forceBreakContent,
+ forceNextEmptyLine,
+ getCommentData,
+ getLastDescendant,
hasPrettierIgnore,
- isPreTagNode,
- isScriptTagNode,
- isTextAreaTagNode,
- isWhitespaceOnlyText
+ inferScriptParser,
+ isScriptLikeTag,
+ preferHardlineAsLeadingSpaces,
+ replaceDocNewlines,
+ replaceNewlines
} = require("./utils");
+const preprocess = require("./preprocess");
+const assert = require("assert");
-function genericPrint(path, options, print) {
- const n = path.getValue();
+function concat(parts) {
+ const newParts = normalizeParts(parts);
+ return newParts.length === 0
+ ? ""
+ : newParts.length === 1
+ ? newParts[0]
+ : builders.concat(newParts);
+}
- switch (n.type) {
- case "root": {
- return concat(printChildren(path, print, options));
- }
- case "directive": {
- return concat([
- "<",
- n.name === "!doctype"
- ? n.data
- .replace(/\s+/g, " ")
- .replace(
- /^(!doctype)(( html)?)/i,
- (_, doctype, doctypeHtml) =>
- doctype.toUpperCase() + doctypeHtml.toLowerCase()
- )
- : n.data,
- ">",
- hardline
- ]);
+function fill(parts) {
+ const newParts = [];
+
+ let hasSeparator = true;
+ for (const part of normalizeParts(parts)) {
+ switch (part) {
+ case line:
+ case hardline:
+ case literalline:
+ case softline:
+ newParts.push(part);
+ hasSeparator = true;
+ break;
+ default:
+ if (!hasSeparator) {
+ // `fill` needs a separator between each two parts
+ newParts.push("");
+ }
+ newParts.push(part);
+ hasSeparator = false;
+ break;
}
+ }
+
+ return builders.fill(newParts);
+}
+
+function embed(path, print, textToDoc /*, options */) {
+ const node = path.getValue();
+ switch (node.type) {
case "text": {
- const parentNode = path.getParentNode();
-
- if (
- isPreTagNode(parentNode) ||
- isTextAreaTagNode(parentNode) ||
- isScriptTagNode(parentNode)
- ) {
- return concat(
- n.data.split(/(\n)/g).map((x, i) => (i % 2 === 1 ? hardline : x))
- );
+ if (isScriptLikeTag(node.parent)) {
+ const parser = inferScriptParser(node.parent);
+ if (parser) {
+ return builders.concat([
+ concat([
+ breakParent,
+ printOpeningTagPrefix(node),
+ markAsRoot(
+ stripTrailingHardline(textToDoc(node.data, { parser }))
+ ),
+ printClosingTagSuffix(node)
+ ])
+ ]);
+ }
}
-
- return n.data.replace(/\s+/g, " ").trim();
- }
- case "script":
- case "style":
- case "tag": {
- const isVoid = n.name in VOID_TAGS;
- const openingPrinted = printOpeningTag(path, print, isVoid);
-
- // Print self closing tag
- if (isVoid || n.selfClosing) {
- return openingPrinted;
- }
-
- const closingPrinted = printClosingTag(n);
-
- // Print tags without children
- if (n.children.length === 0) {
- return concat([openingPrinted, closingPrinted]);
- }
-
- const children = printChildren(path, print, options);
-
- if (isPreTagNode(n) || isTextAreaTagNode(n)) {
- return dedentToRoot(
- group(concat([openingPrinted, concat(children), closingPrinted]))
- );
- }
-
- const isScriptTag = isScriptTagNode(n);
-
- if (isScriptTag) {
- return group(
- concat([openingPrinted, concat(children), closingPrinted])
- );
- }
-
- const containsTag = n.children.some(
- child => ["script", "style", "tag"].indexOf(child.type) !== -1
- );
-
- let forcedBreak =
- willBreak(openingPrinted) || containsTag || n.attributes.length > 1;
-
- // 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]))
- ) {
- children.shift();
- }
-
- // Detect whether we will force this element to output over multiple lines.
- if (children.some(doc => willBreak(doc))) {
- forcedBreak = true;
- }
-
- const containsOnlyEmptyTextNodes = n.children.every(isWhitespaceOnlyText);
-
- const printedMultilineChildren = concat([
- !isScriptTag && !containsOnlyEmptyTextNodes ? hardline : "",
- group(concat(children), { shouldBreak: true })
- ]);
-
- const multiLineElem = group(
- concat([
- openingPrinted,
- indent(printedMultilineChildren),
- hardline,
- closingPrinted
- ])
- );
-
- if (forcedBreak) {
- return multiLineElem;
- }
-
- return conditionalGroup([
- group(concat([openingPrinted, concat(children), closingPrinted])),
- multiLineElem
- ]);
- }
- case "comment": {
- return concat([""]);
+ break;
}
case "attribute": {
- if (n.value === null) {
- return n.key;
+ /*
+ * Vue binding syntax: JS expressions
+ * :class="{ 'some-key': value }"
+ * v-bind:id="'list-' + id"
+ * v-if="foo && !bar"
+ * @click="someFunction()"
+ */
+ if (/(^@)|(^v-)|:/.test(node.key) && !/^\w+$/.test(node.value)) {
+ const doc = textToDoc(node.value, {
+ parser: "__js_expression",
+ // Use singleQuote since HTML attributes use double-quotes.
+ // TODO(azz): We still need to do an entity escape on the attribute.
+ singleQuote: true
+ });
+ return concat([
+ node.key,
+ '="',
+ hasNewlineInRange(node.value, 0, node.value.length)
+ ? doc
+ : removeLines(doc),
+ '"'
+ ]);
}
-
- return concat([n.key, '="', n.value.replace(/"/g, """), '"']);
+ break;
}
- // front matter
case "yaml":
- case "toml":
- return concat([n.raw, hardline]);
- default:
- /* istanbul ignore next */
- throw new Error("unknown htmlparser2 type: " + n.type);
+ return markAsRoot(
+ concat([
+ "---",
+ hardline,
+ node.value.trim().length === 0
+ ? ""
+ : replaceDocNewlines(
+ textToDoc(node.value, { parser: "yaml" }),
+ literalline
+ ),
+ "---"
+ ])
+ );
}
}
-function printOpeningTag(path, print, isVoid) {
- const n = path.getValue();
+function genericPrint(path, options, print) {
+ const node = path.getValue();
+ switch (node.type) {
+ case "root":
+ return concat([group(printChildren(path, options, print)), hardline]);
+ case "tag":
+ case "ieConditionalComment":
+ return concat([
+ group(
+ concat([
+ printOpeningTag(path, options, print),
+ node.children.length === 0
+ ? node.hasDanglingSpaces && node.isDanglingSpaceSensitive
+ ? line
+ : ""
+ : concat([
+ forceBreakContent(node) ? breakParent : "",
+ indent(
+ concat([
+ node.firstChild.type === "text" &&
+ node.firstChild.isWhiteSpaceSensitive &&
+ node.firstChild.isIndentationSensitive
+ ? literalline
+ : node.firstChild.hasLeadingSpaces &&
+ node.firstChild.isLeadingSpaceSensitive
+ ? line
+ : softline,
+ printChildren(path, options, print)
+ ])
+ ),
+ (node.next
+ ? needsToBorrowPrevClosingTagEndMarker(node.next)
+ : needsToBorrowLastChildClosingTagEndMarker(node.parent))
+ ? ""
+ : node.lastChild.hasTrailingSpaces &&
+ node.lastChild.isTrailingSpaceSensitive
+ ? line
+ : softline
+ ])
+ ])
+ ),
+ printClosingTag(node)
+ ]);
+ case "text":
+ return fill(
+ [].concat(
+ printOpeningTagPrefix(node),
+ node.isWhiteSpaceSensitive
+ ? node.isIndentationSensitive
+ ? replaceNewlines(
+ node.data.replace(/^\s*?\n|\n\s*?$/g, ""),
+ literalline
+ )
+ : replaceNewlines(
+ dedentString(node.data.replace(/^\s*?\n|\n\s*?$/g, "")),
+ hardline
+ )
+ : join(line, node.data.split(/\s+/)).parts,
+ printClosingTagSuffix(node)
+ )
+ );
+ case "comment":
+ case "directive": {
+ const data = getCommentData(node);
+ return concat([
+ group(
+ concat([
+ printOpeningTagStart(node),
+ data.trim().length === 0
+ ? ""
+ : concat([
+ indent(
+ concat([
+ node.prev &&
+ needsToBorrowNextOpeningTagStartMarker(node.prev)
+ ? breakParent
+ : "",
+ node.type === "directive" ? " " : line,
+ concat(replaceNewlines(data, hardline))
+ ])
+ ),
+ node.type === "directive"
+ ? ""
+ : (node.next
+ ? needsToBorrowPrevClosingTagEndMarker(node.next)
+ : needsToBorrowLastChildClosingTagEndMarker(node.parent))
+ ? " "
+ : line
+ ])
+ ])
+ ),
+ printClosingTagEnd(node)
+ ]);
+ }
+ case "attribute":
+ return concat([
+ node.key,
+ node.value === null
+ ? ""
+ : concat([
+ '="',
+ concat(
+ replaceNewlines(node.value.replace(/"/g, """), literalline)
+ ),
+ '"'
+ ])
+ ]);
+ case "yaml":
+ case "toml":
+ return node.raw;
+ default:
+ throw new Error(`Unexpected node type ${node.type}`);
+ }
+}
- const selfClosing = isVoid || n.selfClosing;
+function printChildren(path, options, print) {
+ const node = path.getValue();
- // Don't break self-closing elements with no attributes
- if (selfClosing && !n.attributes.length) {
- return concat(["<", n.name, " />"]);
+ if (forceBreakChildren(node)) {
+ return concat([
+ breakParent,
+ concat(
+ path.map(childPath => {
+ const childNode = childPath.getValue();
+ const prevBetweenLine = !childNode.prev
+ ? ""
+ : printBetweenLine(childNode.prev, childNode);
+ return concat([
+ !prevBetweenLine
+ ? ""
+ : concat([
+ prevBetweenLine,
+ forceNextEmptyLine(childNode.prev) ||
+ childNode.prev.endLocation.line + 1 <
+ childNode.startLocation.line
+ ? hardline
+ : ""
+ ]),
+ print(childPath)
+ ]);
+ }, "children")
+ )
+ ]);
}
- // 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([
- "<",
- n.name,
- " ",
- concat(path.map(print, "attributes")),
- selfClosing ? " />" : ">"
- ])
+ const parts = [];
+
+ path.map((childPath, childIndex) => {
+ const childNode = childPath.getValue();
+
+ if (childIndex !== 0) {
+ const prevBetweenLine = printBetweenLine(childNode.prev, childNode);
+ if (prevBetweenLine) {
+ if (
+ forceNextEmptyLine(childNode.prev) ||
+ childNode.prev.endLocation.line + 1 < childNode.startLocation.line
+ ) {
+ parts.push(hardline, hardline);
+ } else {
+ parts.push(prevBetweenLine);
+ }
+ }
+ }
+
+ Array.prototype.push.apply(
+ parts,
+ childNode.type === "text" ? print(childPath).parts : [print(childPath)]
);
- }
+ }, "children");
- return group(
- concat([
- "<",
- n.name,
- indent(
- concat(path.map(attr => concat([line, print(attr)]), "attributes"))
- ),
- selfClosing ? concat([line, "/>"]) : concat([softline, ">"])
- ])
- );
+ return fill(parts);
+
+ function printBetweenLine(prevNode, nextNode) {
+ return (needsToBorrowNextOpeningTagStartMarker(prevNode) &&
+ /**
+ * 123
+ */
+ (nextNode.firstChild ||
+ /**
+ * 123
+ * ~
+ */
+ (nextNode.type === "tag" &&
+ nextNode.isSelfClosing &&
+ nextNode.attributes.length === 0))) ||
+ /**
+ * 123
+ */
+ (prevNode.type === "tag" &&
+ prevNode.isSelfClosing &&
+ needsToBorrowPrevClosingTagEndMarker(nextNode))
+ ? ""
+ : !nextNode.isLeadingSpaceSensitive ||
+ preferHardlineAsLeadingSpaces(nextNode) ||
+ /**
+ * Want to write us a letter? Use ourmailing address .
+ */
+ (needsToBorrowPrevClosingTagEndMarker(nextNode) &&
+ prevNode.lastChild &&
+ needsToBorrowParentClosingTagStartMarker(prevNode.lastChild) &&
+ prevNode.lastChild.lastChild &&
+ needsToBorrowParentClosingTagStartMarker(
+ prevNode.lastChild.lastChild
+ ))
+ ? hardline
+ : nextNode.hasLeadingSpaces
+ ? line
+ : softline;
+ }
+}
+
+function printOpeningTag(path, options, print) {
+ const node = path.getValue();
+ return concat([
+ printOpeningTagStart(node),
+ !node.attributes || node.attributes.length === 0
+ ? node.isSelfClosing
+ ? /**
+ *
+ * ^
+ */
+ " "
+ : ""
+ : group(
+ concat([
+ node.prev && needsToBorrowNextOpeningTagStartMarker(node.prev)
+ ? /**
+ * 123
+ */
+ breakParent
+ : "",
+ indent(concat([line, join(line, path.map(print, "attributes"))])),
+ node.firstChild &&
+ needsToBorrowParentOpeningTagEndMarker(node.firstChild)
+ ? /**
+ * 123 456
+ */
+ ""
+ : node.isSelfClosing
+ ? line
+ : softline
+ ])
+ ),
+ node.isSelfClosing ? "" : printOpeningTagEnd(node)
+ ]);
+}
+
+function printOpeningTagStart(node) {
+ return node.prev && needsToBorrowNextOpeningTagStartMarker(node.prev)
+ ? ""
+ : concat([printOpeningTagPrefix(node), printOpeningTagStartMarker(node)]);
+}
+
+function printOpeningTagEnd(node) {
+ return node.firstChild &&
+ needsToBorrowParentOpeningTagEndMarker(node.firstChild)
+ ? ""
+ : printOpeningTagEndMarker(node);
}
function printClosingTag(node) {
- return concat(["", node.name, ">"]);
+ return concat([
+ node.isSelfClosing ? "" : printClosingTagStart(node),
+ printClosingTagEnd(node)
+ ]);
}
-function printChildren(path, print, options) {
- const parts = [];
+function printClosingTagStart(node) {
+ return node.lastChild &&
+ needsToBorrowParentClosingTagStartMarker(node.lastChild)
+ ? ""
+ : concat([printClosingTagPrefix(node), printClosingTagStartMarker(node)]);
+}
- path.map(childPath => {
- const child = childPath.getValue();
+function printClosingTagEnd(node) {
+ return (node.next
+ ? needsToBorrowPrevClosingTagEndMarker(node.next)
+ : needsToBorrowLastChildClosingTagEndMarker(node.parent))
+ ? ""
+ : concat([printClosingTagEndMarker(node), printClosingTagSuffix(node)]);
+}
- parts.push(print(childPath));
+function needsToBorrowNextOpeningTagStartMarker(node) {
+ /**
+ * 123
+ */
+ return (
+ node.next &&
+ node.type === "text" &&
+ node.isTrailingSpaceSensitive &&
+ !node.hasTrailingSpaces
+ );
+}
- if (child.type !== "text" && child.type !== "directive") {
- parts.push(hardline);
- }
+function needsToBorrowParentOpeningTagEndMarker(node) {
+ /**
+ *
123
+ * ^
+ *
+ *
123
+ * ^
+ *
+ *
+ */
+ return (
+ node.lastChild &&
+ node.lastChild.isTrailingSpaceSensitive &&
+ !node.lastChild.hasTrailingSpaces &&
+ getLastDescendant(node.lastChild).type !== "text"
+ );
+}
+
+function needsToBorrowParentClosingTagStartMarker(node) {
+ /**
+ *
+ * 123
+ *
+ * 123
+ */
+ return (
+ !node.next &&
+ !node.hasTrailingSpaces &&
+ node.isTrailingSpaceSensitive &&
+ getLastDescendant(node).type === "text"
+ );
+}
+
+function printOpeningTagPrefix(node) {
+ return needsToBorrowParentOpeningTagEndMarker(node)
+ ? printOpeningTagEndMarker(node.parent)
+ : needsToBorrowPrevClosingTagEndMarker(node)
+ ? printClosingTagEndMarker(node.prev)
+ : "";
+}
+
+function printClosingTagPrefix(node) {
+ return needsToBorrowLastChildClosingTagEndMarker(node)
+ ? printClosingTagEndMarker(node.lastChild)
+ : "";
+}
+
+function printClosingTagSuffix(node) {
+ return needsToBorrowParentClosingTagStartMarker(node)
+ ? printClosingTagStartMarker(node.parent)
+ : needsToBorrowNextOpeningTagStartMarker(node)
+ ? printOpeningTagStartMarker(node.next)
+ : "";
+}
+
+function printOpeningTagStartMarker(node) {
+ switch (node.type) {
+ case "comment":
+ return "";
+ case "ieConditionalComment":
+ return `[endif]-->`;
+ case "tag":
+ if (node.isSelfClosing) {
+ return "/>";
+ }
+ // fall through
+ default:
+ return ">";
+ }
}
module.exports = {
+ preprocess,
print: genericPrint,
massageAstNode: clean,
embed,
diff --git a/src/language-html/utils.js b/src/language-html/utils.js
index 25a74295..1b8eed5f 100644
--- a/src/language-html/utils.js
+++ b/src/language-html/utils.js
@@ -1,5 +1,17 @@
"use strict";
+const {
+ builders: { concat },
+ utils: { mapDoc }
+} = require("../doc");
+
+const {
+ CSS_DISPLAY_TAGS,
+ CSS_DISPLAY_DEFAULT,
+ CSS_WHITE_SPACE_TAGS,
+ CSS_WHITE_SPACE_DEFAULT
+} = require("./constants.evaluate");
+
const htmlTagNames = require("html-tag-names");
const htmlElementAttributes = require("html-element-attributes");
@@ -58,64 +70,402 @@ function mapObject(object, fn) {
function hasPrettierIgnore(path) {
const node = path.getValue();
-
- if (isWhitespaceOnlyText(node) || node.type === "attribute") {
+ if (node.type === "attribute") {
return false;
}
const parentNode = path.getParentNode();
-
if (!parentNode) {
return false;
}
const index = path.getName();
-
if (typeof index !== "number" || index === 0) {
return false;
}
const prevNode = parentNode.children[index - 1];
-
- if (isPrettierIgnore(prevNode)) {
- return true;
- }
-
- if (!isWhitespaceOnlyText(prevNode)) {
- return false;
- }
-
- const prevPrevNode = parentNode.children[index - 2];
- return prevPrevNode && isPrettierIgnore(prevPrevNode);
+ return isPrettierIgnore(prevNode);
}
function isPrettierIgnore(node) {
return node.type === "comment" && node.data.trim() === "prettier-ignore";
}
-function isWhitespaceOnlyText(node) {
- return node.type === "text" && node.data.trim().length === 0;
+function isTag(node) {
+ return node.type === "tag";
}
-function isPreTagNode(node) {
- return node.type === "tag" && node.name === "pre";
+function isScriptLikeTag(node) {
+ return isTag(node) && (node.name === "script" || node.name === "style");
}
-function isTextAreaTagNode(node) {
- return node.type === "tag" && node.name === "textarea";
+function isFrontMatterNode(node) {
+ return node.type === "yaml" || node.type === "toml";
}
-function isScriptTagNode(node) {
- return node.type === "script" || node.type === "style";
+function isLeadingSpaceSensitiveNode(node, { prev, parent }) {
+ if (isFrontMatterNode(node)) {
+ return false;
+ }
+
+ if (!parent || parent.cssDisplay === "none") {
+ return false;
+ }
+
+ if (
+ !prev &&
+ (parent.type === "root" ||
+ isScriptLikeTag(parent) ||
+ isBlockLikeCssDisplay(parent.cssDisplay))
+ ) {
+ return false;
+ }
+
+ if (prev && isBlockLikeCssDisplay(prev.cssDisplay)) {
+ return false;
+ }
+
+ return true;
+}
+
+function isTrailingSpaceSensitiveNode(node, { next, parent }) {
+ if (isFrontMatterNode(node)) {
+ return false;
+ }
+
+ if (!parent || parent.cssDisplay === "none") {
+ return false;
+ }
+
+ if (
+ !next &&
+ (parent.type === "root" ||
+ isScriptLikeTag(parent) ||
+ isBlockLikeCssDisplay(parent.cssDisplay))
+ ) {
+ return false;
+ }
+
+ if (next && isBlockLikeCssDisplay(next.cssDisplay)) {
+ return false;
+ }
+
+ return true;
+}
+
+function isDanglingSpaceSensitiveNode(node) {
+ return !isBlockLikeCssDisplay(node.cssDisplay);
+}
+
+/**
+ * @param {unknown} node
+ * @param {(node: unknown, stack: Array)} fn
+ * @param {unknown=} parent
+ */
+function mapNode(node, fn, stack = []) {
+ const newNode = Object.assign({}, node);
+
+ if (newNode.children) {
+ newNode.children = newNode.children.map((child, childIndex) =>
+ mapNode(child, fn, [childIndex, node].concat(stack))
+ );
+ }
+
+ return fn(newNode, stack);
+}
+
+function getPrevNode(stack) {
+ const [index, parent] = stack;
+
+ if (typeof index !== "number" || index === 0) {
+ return null;
+ }
+
+ return parent.children[index - 1];
+}
+
+function replaceNewlines(text, replacement) {
+ return text
+ .split(/(\n)/g)
+ .map((data, index) => (index % 2 === 1 ? replacement : data));
+}
+
+function replaceDocNewlines(doc, replacement) {
+ return mapDoc(
+ doc,
+ currentDoc =>
+ typeof currentDoc === "string" && currentDoc.includes("\n")
+ ? concat(replaceNewlines(currentDoc, replacement))
+ : currentDoc
+ );
+}
+
+function forceNextEmptyLine(node) {
+ return isFrontMatterNode(node);
+}
+
+/** firstChild leadingSpaces and lastChild trailingSpaces */
+function forceBreakContent(node) {
+ return (
+ forceBreakChildren(node) ||
+ (isTag(node) &&
+ node.children.length !== 0 &&
+ (["body", "template"].indexOf(node.name) !== -1 ||
+ node.children.some(child => hasNonTextChild(child))))
+ );
+}
+
+/** spaces between children */
+function forceBreakChildren(node) {
+ return (
+ isTag(node) &&
+ node.children.length !== 0 &&
+ (["html", "head", "ul", "ol", "select"].indexOf(node.name) !== -1 ||
+ (node.cssDisplay.startsWith("table") && node.cssDisplay !== "table-cell"))
+ );
+}
+
+function preferHardlineAsLeadingSpaces(node) {
+ return (
+ preferHardlineAsSurroundingSpaces(node) ||
+ (node.prev && preferHardlineAsTrailingSpaces(node.prev))
+ );
+}
+
+function preferHardlineAsTrailingSpaces(node) {
+ return (
+ preferHardlineAsSurroundingSpaces(node) ||
+ (isTag(node) && node.name === "br")
+ );
+}
+
+function preferHardlineAsSurroundingSpaces(node) {
+ switch (node.type) {
+ case "ieConditionalComment":
+ case "comment":
+ case "directive":
+ return true;
+ case "tag":
+ return ["script", "select"].indexOf(node.name) !== -1;
+ }
+ return false;
+}
+
+function getLastDescendant(node) {
+ return node.lastChild ? getLastDescendant(node.lastChild) : node;
+}
+
+function hasNonTextChild(node) {
+ return node.children && node.children.some(child => child.type !== "text");
+}
+
+function inferScriptParser(node) {
+ if (
+ node.name === "script" &&
+ ((!node.attribs.lang && !node.attribs.type) ||
+ node.attribs.type === "text/javascript" ||
+ node.attribs.type === "text/babel" ||
+ node.attribs.type === "application/javascript")
+ ) {
+ return "babylon";
+ }
+
+ if (
+ node.name === "script" &&
+ (node.attribs.type === "application/x-typescript" ||
+ node.attribs.lang === "ts")
+ ) {
+ return "typescript";
+ }
+
+ if (node.name === "style") {
+ return "css";
+ }
+
+ return null;
+}
+
+/**
+ * firstChild leadingSpaces, lastChild trailingSpaces, and danglingSpaces are insensitive
+ */
+function isBlockLikeCssDisplay(cssDisplay) {
+ return cssDisplay === "block" || cssDisplay.startsWith("table");
+}
+
+function getNodeCssStyleDisplay(node, prevNode, options) {
+ switch (getNodeCssStyleWhiteSpace(node)) {
+ case "pre":
+ case "pre-wrap":
+ // textarea-like
+ return "block";
+ }
+
+ if (prevNode && prevNode.type === "comment") {
+ //
+ const match = prevNode.data.match(/^\s*display:\s*([a-z]+)\s*$/);
+ if (match) {
+ return match[1];
+ }
+ }
+
+ switch (options.htmlWhitespaceSensitivity) {
+ case "strict":
+ return "inline";
+ case "ignore":
+ return "block";
+ default:
+ return (
+ (isTag(node) && CSS_DISPLAY_TAGS[node.name]) || CSS_DISPLAY_DEFAULT
+ );
+ }
+}
+
+function getNodeCssStyleWhiteSpace(node) {
+ return (
+ (isTag(node) && CSS_WHITE_SPACE_TAGS[node.name]) || CSS_WHITE_SPACE_DEFAULT
+ );
+}
+
+function getCommentData(node) {
+ const rightTrimmedData = node.data.trimRight();
+
+ const hasLeadingEmptyLine = /^[^\S\n]*?\n/.test(node.data);
+ if (hasLeadingEmptyLine) {
+ /**
+ *
+ */
+ return dedentString(rightTrimmedData.replace(/^\s*\n/, ""));
+ }
+
+ /**
+ *
+ *
+ *
+ *
+ *
+ */
+ if (!rightTrimmedData.includes("\n")) {
+ return rightTrimmedData.trimLeft();
+ }
+
+ const firstNewlineIndex = rightTrimmedData.indexOf("\n");
+ const dataWithoutLeadingLine = rightTrimmedData.slice(firstNewlineIndex + 1);
+ const minIndentationForDataWithoutLeadingLine = getMinIndentation(
+ dataWithoutLeadingLine
+ );
+
+ const commentDataStartColumn = node.startLocation.column + "
+ */
+ if (minIndentationForDataWithoutLeadingLine >= commentDataStartColumn) {
+ return dedentString(
+ " ".repeat(commentDataStartColumn) + "\n" + rightTrimmedData
+ );
+ }
+
+ const leadingLineData = rightTrimmedData.slice(0, firstNewlineIndex);
+ /**
+ *
+ */
+ return (
+ leadingLineData.trim() +
+ "\n" +
+ dedentString(
+ dataWithoutLeadingLine,
+ minIndentationForDataWithoutLeadingLine
+ )
+ );
+}
+
+function getMinIndentation(text) {
+ let minIndentation = Infinity;
+
+ for (const lineText of text.split("\n")) {
+ if (/\S/.test(lineText[0])) {
+ return 0;
+ }
+
+ const indentation = lineText.match(/^\s*/)[0].length;
+
+ if (lineText.length === indentation) {
+ continue;
+ }
+
+ if (indentation < minIndentation) {
+ minIndentation = indentation;
+ }
+ }
+
+ return minIndentation === Infinity ? 0 : minIndentation;
+}
+
+function dedentString(text, minIndent = getMinIndentation(text)) {
+ return minIndent === 0
+ ? text
+ : text
+ .split("\n")
+ .map(lineText => lineText.slice(minIndent))
+ .join("\n");
+}
+
+function normalizeParts(parts) {
+ const newParts = [];
+
+ for (const part of parts) {
+ if (!part) {
+ continue;
+ }
+
+ if (
+ newParts.length !== 0 &&
+ typeof newParts[newParts.length - 1] === "string" &&
+ typeof part === "string"
+ ) {
+ newParts.push(newParts.pop() + part);
+ continue;
+ }
+
+ newParts.push(part);
+ }
+
+ return newParts;
}
module.exports = {
HTML_ELEMENT_ATTRIBUTES,
HTML_TAGS,
VOID_TAGS,
+ dedentString,
+ forceBreakChildren,
+ forceBreakContent,
+ forceNextEmptyLine,
+ getCommentData,
+ getLastDescendant,
+ getNodeCssStyleDisplay,
+ getNodeCssStyleWhiteSpace,
+ getPrevNode,
hasPrettierIgnore,
- isPreTagNode,
- isScriptTagNode,
- isTextAreaTagNode,
- isWhitespaceOnlyText
+ inferScriptParser,
+ isDanglingSpaceSensitiveNode,
+ isFrontMatterNode,
+ isLeadingSpaceSensitiveNode,
+ isScriptLikeTag,
+ isTrailingSpaceSensitiveNode,
+ mapNode,
+ normalizeParts,
+ preferHardlineAsLeadingSpaces,
+ preferHardlineAsTrailingSpaces,
+ replaceDocNewlines,
+ replaceNewlines
};
diff --git a/tests/html_attributes/__snapshots__/jsfmt.spec.js.snap b/tests/html_attributes/__snapshots__/jsfmt.spec.js.snap
index a7c82b11..dbdf774e 100644
--- a/tests/html_attributes/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/html_attributes/__snapshots__/jsfmt.spec.js.snap
@@ -61,6 +61,22 @@ and HTML5 Apps. It also documents Mozilla products, like Firefox OS.">
+
+
+
+
+
+
+
+
+
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -96,8 +112,7 @@ and HTML5 Apps. It also documents Mozilla products, like Firefox OS."
data-columns="3"
data-index-number="12314"
data-parent="cars"
->
-
+>
-
+>
-
+>
+
+
`;
@@ -151,42 +172,36 @@ exports[`boolean.html - html-verify 1`] = `
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is valid.
-
- This is valid.
-
-
- This is valid.
-
-
- This is valid.
-
-
- This is valid. This will be disabled.
-
-
- This is valid. This will be disabled.
-
-
- This is valid. This will be disabled.
-
-
- This is valid. This will be disabled.
-
-
- This is valid. This will be disabled.
-
-
- This is valid. This will be disabled.
-
-
- This is valid. This will be disabled.
-
-
- This is valid. This will be disabled.
-
-
- This is valid. This will be disabled.
-
+This is valid.
+This is valid.
+This is valid.
+This is valid. This will be disabled.
+This is valid. This will be disabled.
+This is valid. This will be disabled.
+This is valid. This will be disabled.
+This is valid. This will be disabled.
+This is valid. This will be disabled.
+This is valid. This will be disabled.
+This is valid. This will be disabled.
+This is valid. This will be disabled.
@@ -208,6 +223,13 @@ exports[`dobule-quotes.html - html-verify 1`] = `
`;
+exports[`duplicate.html - html-verify 1`] = `
+123
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+123
+
+`;
+
exports[`single-quotes.html - html-verify 1`] = `
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/html_attributes/attributes.html b/tests/html_attributes/attributes.html
index a1950c3f..a30d5157 100644
--- a/tests/html_attributes/attributes.html
+++ b/tests/html_attributes/attributes.html
@@ -58,3 +58,19 @@ and HTML5 Apps. It also documents Mozilla products, like Firefox OS.">
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/html_attributes/duplicate.html b/tests/html_attributes/duplicate.html
new file mode 100644
index 00000000..a45ebe3c
--- /dev/null
+++ b/tests/html_attributes/duplicate.html
@@ -0,0 +1 @@
+123
diff --git a/tests/html_basics/__snapshots__/jsfmt.spec.js.snap b/tests/html_basics/__snapshots__/jsfmt.spec.js.snap
index cb98eaec..cfbe45c3 100644
--- a/tests/html_basics/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/html_basics/__snapshots__/jsfmt.spec.js.snap
@@ -3,7 +3,7 @@
exports[`comment.html - html-verify 1`] = `
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
+
`;
@@ -107,9 +107,9 @@ exports[`form.html - html-verify 1`] = `
aria-describedby="emailHelp"
placeholder="Enter email"
/>
-
- We'll never share your email with anyone else.
-
+ We'll never share your email with anyone else.
Password
@@ -152,9 +152,10 @@ exports[`form.html - html-verify 1`] = `
id="exampleInputFile"
aria-describedby="fileHelp"
/>
-
- This is some placeholder block-level help text for the above input. It's a bit lighter and easily wraps to a new line.
-
+ This is some placeholder block-level help text for the above input. It's
+ a bit lighter and easily wraps to a new line.
Radio buttons
@@ -180,7 +181,8 @@ exports[`form.html - html-verify 1`] = `
id="optionsRadios2"
value="option2"
/>
- Option two can be something else and selecting it will deselect option one
+ Option two can be something else and selecting it will deselect option
+ one