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 our
mailing 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) + ? /** + * 123456 + */ + "" + : 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([""]); + 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`] = `
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - - - - - - - - - - - + + + + + + + + + + + + @@ -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`] = ` John "ShotGun" Nelson ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 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.
@@ -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
@@ -199,13 +201,10 @@ exports[`form.html - html-verify 1`] = `
- + `; @@ -336,7 +335,11 @@ exports[`html5-boilerplate.html - html-verify 1`] = ` @@ -348,21 +351,21 @@ exports[`html5-boilerplate.html - html-verify 1`] = ` crossorigin="anonymous" > '); + window.jQuery || + document.write(''); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ `; @@ -71,12 +71,12 @@ exports[`simple.html - html-verify 1`] = ` Sample styled page @@ -94,10 +94,10 @@ exports[`single-script.html - html-verify 1`] = ` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ `; @@ -109,8 +109,8 @@ exports[`something-else.html - html-verify 1`] = ` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ `; @@ -196,54 +196,54 @@ exports[`typescript.html - html-verify 1`] = ` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/multiparser_html_ts/__snapshots__/jsfmt.spec.js.snap b/tests/multiparser_html_ts/__snapshots__/jsfmt.spec.js.snap index f80448aa..9168d344 100644 --- a/tests/multiparser_html_ts/__snapshots__/jsfmt.spec.js.snap +++ b/tests/multiparser_html_ts/__snapshots__/jsfmt.spec.js.snap @@ -26,11 +26,11 @@ exports[`html-with-ts-script.html - html-verify 1`] = ` diff --git a/tests_integration/__tests__/__snapshots__/early-exit.js.snap b/tests_integration/__tests__/__snapshots__/early-exit.js.snap index 4d7ab782..5ec8cf2d 100644 --- a/tests_integration/__tests__/__snapshots__/early-exit.js.snap +++ b/tests_integration/__tests__/__snapshots__/early-exit.js.snap @@ -60,6 +60,9 @@ Format options: Include parentheses around a sole arrow function parameter. Defaults to avoid. --no-bracket-spacing Do not print spaces between brackets. + --html-whitespace-sensitivity + How to handle whitespaces in HTML. + Defaults to css. --jsx-bracket-same-line Put > on the last line instead of at a new line. Defaults to false. --parser @@ -197,6 +200,9 @@ Format options: Include parentheses around a sole arrow function parameter. Defaults to avoid. --no-bracket-spacing Do not print spaces between brackets. + --html-whitespace-sensitivity + How to handle whitespaces in HTML. + Defaults to css. --jsx-bracket-same-line Put > on the last line instead of at a new line. Defaults to false. --parser diff --git a/tests_integration/__tests__/__snapshots__/help-options.js.snap b/tests_integration/__tests__/__snapshots__/help-options.js.snap index c53c84e7..bc23f915 100644 --- a/tests_integration/__tests__/__snapshots__/help-options.js.snap +++ b/tests_integration/__tests__/__snapshots__/help-options.js.snap @@ -138,6 +138,25 @@ exports[`show detailed usage with --help help (stdout) 1`] = ` exports[`show detailed usage with --help help (write) 1`] = `Array []`; +exports[`show detailed usage with --help html-whitespace-sensitivity (stderr) 1`] = `""`; + +exports[`show detailed usage with --help html-whitespace-sensitivity (stdout) 1`] = ` +"--html-whitespace-sensitivity + + How to handle whitespaces in HTML. + +Valid options: + + css Respect the default value of CSS display property. + strict Whitespaces are considered sensitive. + ignore Whitespaces are considered insensitive. + +Default: css +" +`; + +exports[`show detailed usage with --help html-whitespace-sensitivity (write) 1`] = `Array []`; + exports[`show detailed usage with --help ignore-path (stderr) 1`] = `""`; exports[`show detailed usage with --help ignore-path (stdout) 1`] = ` diff --git a/tests_integration/__tests__/__snapshots__/plugin-options.js.snap b/tests_integration/__tests__/__snapshots__/plugin-options.js.snap index 4824c3d8..ce7df73e 100644 --- a/tests_integration/__tests__/__snapshots__/plugin-options.js.snap +++ b/tests_integration/__tests__/__snapshots__/plugin-options.js.snap @@ -13,11 +13,11 @@ exports[` 1`] = ` --no-bracket-spacing Do not print spaces between brackets. + --foo-option foo description + Defaults to bar. + --html-whitespace-sensitivity + How to handle whitespaces in HTML. + Defaults to css. --jsx-bracket-same-line Put > on the last line instead of at a new line. - Defaults to false. - --parser - Which parser to use. - --print-width The line length where Prettier will try wrap." + Defaults to false." `; exports[`show detailed external option with \`--help foo-option\` (stderr) 1`] = `""`; diff --git a/tests_integration/__tests__/__snapshots__/schema.js.snap b/tests_integration/__tests__/__snapshots__/schema.js.snap index 85790723..aaac6b3a 100644 --- a/tests_integration/__tests__/__snapshots__/schema.js.snap +++ b/tests_integration/__tests__/__snapshots__/schema.js.snap @@ -48,6 +48,30 @@ This option cannot be used with --range-start and --range-end.", "description": "Specify the input filepath. This will be used to do parser inference.", "type": "string", }, + "htmlWhitespaceSensitivity": Object { + "default": "css", + "description": "How to handle whitespaces in HTML.", + "oneOf": Array [ + Object { + "description": "Respect the default value of CSS display property.", + "enum": Array [ + "css", + ], + }, + Object { + "description": "Whitespaces are considered sensitive.", + "enum": Array [ + "strict", + ], + }, + Object { + "description": "Whitespaces are considered insensitive.", + "enum": Array [ + "ignore", + ], + }, + ], + }, "insertPragma": Object { "default": false, "description": "Insert @format pragma into file's first docblock comment.", diff --git a/tests_integration/__tests__/__snapshots__/support-info.js.snap b/tests_integration/__tests__/__snapshots__/support-info.js.snap index 75901951..6af0d120 100644 --- a/tests_integration/__tests__/__snapshots__/support-info.js.snap +++ b/tests_integration/__tests__/__snapshots__/support-info.js.snap @@ -479,7 +479,27 @@ exports[`API getSupportInfo() with version 1.8.2 -> undefined 1`] = ` \\"type\\": \\"boolean\\", }, \\"cursorOffset\\": Object { -@@ -73,16 +99,30 @@ +@@ -56,10 +82,19 @@ + }, + \\"filepath\\": Object { + \\"default\\": undefined, + \\"type\\": \\"path\\", + }, ++ \\"htmlWhitespaceSensitivity\\": Object { ++ \\"choices\\": Array [ ++ \\"css\\", ++ \\"strict\\", ++ \\"ignore\\", ++ ], ++ \\"default\\": \\"css\\", ++ \\"type\\": \\"choice\\", ++ }, + \\"insertPragma\\": Object { + \\"default\\": false, + \\"type\\": \\"boolean\\", + }, + \\"jsxBracketSameLine\\": Object { +@@ -73,16 +108,30 @@ \\"typescript\\", \\"css\\", \\"less\\", @@ -511,7 +531,7 @@ exports[`API getSupportInfo() with version 1.8.2 -> undefined 1`] = ` \\"range\\": Object { \\"end\\": Infinity, \\"start\\": 0, -@@ -90,14 +130,15 @@ +@@ -90,14 +139,15 @@ }, \\"type\\": \\"int\\", }, @@ -931,6 +951,29 @@ exports[`CLI --support-info (stdout) 1`] = ` \\"since\\": \\"1.4.0\\", \\"type\\": \\"path\\" }, + { + \\"category\\": \\"HTML\\", + \\"choices\\": [ + { + \\"description\\": \\"Respect the default value of CSS display property.\\", + \\"value\\": \\"css\\" + }, + { + \\"description\\": \\"Whitespaces are considered sensitive.\\", + \\"value\\": \\"strict\\" + }, + { + \\"description\\": \\"Whitespaces are considered insensitive.\\", + \\"value\\": \\"ignore\\" + } + ], + \\"default\\": \\"css\\", + \\"description\\": \\"How to handle whitespaces in HTML.\\", + \\"name\\": \\"htmlWhitespaceSensitivity\\", + \\"pluginDefaults\\": {}, + \\"since\\": \\"1.15.0\\", + \\"type\\": \\"choice\\" + }, { \\"category\\": \\"Special\\", \\"default\\": false, diff --git a/website/playground/Playground.js b/website/playground/Playground.js index 4bc87397..54e950ac 100644 --- a/website/playground/Playground.js +++ b/website/playground/Playground.js @@ -20,6 +20,7 @@ const CATEGORIES_ORDER = [ "Common", "JavaScript", "Markdown", + "HTML", "Special" ]; const ENABLED_OPTIONS = [ @@ -34,6 +35,7 @@ const ENABLED_OPTIONS = [ "arrowParens", "trailingComma", "proseWrap", + "htmlWhitespaceSensitivity", "insertPragma", "requirePragma" ]; diff --git a/yarn.lock b/yarn.lock index a0fb68d2..5935dcfc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3184,6 +3184,10 @@ html-encoding-sniffer@^1.0.2: dependencies: whatwg-encoding "^1.0.1" +html-styles@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/html-styles/-/html-styles-1.0.0.tgz#a18061fd651f99c6b75c45c8e0549a3bc3e01a75" + html-tag-names@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/html-tag-names/-/html-tag-names-1.1.2.tgz#f65168964c5a9c82675efda882875dcb2a875c22" @@ -4392,7 +4396,7 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -lines-and-columns@^1.1.6: +lines-and-columns@1.1.6, lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=