Skip to content

Commit

Permalink
Vue: Support custom blocks (#8023)
Browse files Browse the repository at this point in the history
* Support Vue Custom Block

* Add changelog

* Udpate snapshots

* Support "type" for json

* Remove guessing languages

* Modify to reuse inferScriptParser logic

* Implement withInnerParts arg for stripTrailingHardline

* Rename isVueCustomBlock

* Set `verbose: true`

* Use getParserName

* Ignore vueIndentScriptAndStyle

* tmp

* Fix formatting

* Add tests

* Fix by lint

* Revert "Set `verbose: true`"

This reverts commit f65c7cd.

* loop with i--

* Modify to preserve unknown langauge

* Support embeddedLanguageFormatting

* Fix by lint

* Fix mistake

* Modify to preserve unknown lang

* Remove unnecesary condition

* Add error tests

Co-authored-by: fisker Cheung <lionkay@gmail.com>
  • Loading branch information
sosukesuzuki and fisker committed Apr 16, 2020
1 parent 7b7ca47 commit 915327d
Show file tree
Hide file tree
Showing 19 changed files with 1,860 additions and 88 deletions.
27 changes: 27 additions & 0 deletions changelog_unreleased/vue/pr-8023.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#### Support custom blocks ([#8023](https://github.com/prettier/prettier/pull/8023) by [@sosukesuzuki](https://github.com/sosukesuzuki))

Support [vue-loader custom blocks](https://vue-loader.vuejs.org/guide/custom-blocks.html) in SFC with `lang` attribute.

<!-- prettier-ignore -->
```html
<!-- Input -->
<custom lang="json">
{
"foo":
"bar"}
</custom>

<!-- Prettier stable -->
<custom lang="json">
{
"foo":
"bar"}
</custom>

<!-- Prettier master -->
<custom lang="json">
{
"foo": "bar"
}
</custom>
```
18 changes: 18 additions & 0 deletions src/common/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const stringWidth = require("string-width");
const escapeStringRegexp = require("escape-string-regexp");
const getLast = require("../utils/get-last");
const support = require("../main/support");

// eslint-disable-next-line no-control-regex
const notAsciiRegex = /[^\x20-\x7F]/;
Expand Down Expand Up @@ -820,6 +821,22 @@ function replaceEndOfLineWith(text, replacement) {
return parts;
}

function getParserName(lang, options) {
const supportInfo = support.getSupportInfo({ plugins: options.plugins });
const language = supportInfo.languages.find(
(language) =>
language.name.toLowerCase() === lang ||
(language.aliases && language.aliases.includes(lang)) ||
(language.extensions &&
language.extensions.find((ext) => ext === `.${lang}`))
);
if (language) {
return language.parsers[0];
}

return null;
}

module.exports = {
replaceEndOfLineWith,
getStringWidth,
Expand All @@ -828,6 +845,7 @@ module.exports = {
getPrecedence,
shouldFlatten,
isBitwiseOperator,
getParserName,
getPenultimate,
getLast,
getNextNonSpaceNonCommentCharacterIndexWithStartIndex,
Expand Down
20 changes: 17 additions & 3 deletions src/document/doc-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,17 +182,31 @@ function removeLines(doc) {
return mapDoc(doc, removeLinesFn);
}

function stripTrailingHardline(doc) {
function getInnerParts(doc) {
let { parts } = doc;
let lastPart;
// Avoid a falsy element like ""
for (let i = doc.parts.length; i > 0 && !lastPart; i--) {
lastPart = parts[i - 1];
}
if (lastPart.type === "group") {
parts = lastPart.contents.parts;
}
return parts;
}

function stripTrailingHardline(doc, withInnerParts = false) {
// HACK remove ending hardline, original PR: #1984
if (doc.type === "concat" && doc.parts.length !== 0) {
const lastPart = doc.parts[doc.parts.length - 1];
const parts = withInnerParts ? getInnerParts(doc) : doc.parts;
const lastPart = parts[parts.length - 1];
if (lastPart.type === "concat") {
if (
lastPart.parts.length === 2 &&
lastPart.parts[0].hard &&
lastPart.parts[1].type === "break-parent"
) {
return { type: "concat", parts: doc.parts.slice(0, -1) };
return { type: "concat", parts: parts.slice(0, -1) };
}

return {
Expand Down
12 changes: 9 additions & 3 deletions src/language-html/preprocess.js
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ function addCssDisplay(ast, options) {
* - add `isTrailingSpaceSensitive` field
* - add `isDanglingSpaceSensitive` field for parent nodes
*/
function addIsSpaceSensitive(ast /*, options */) {
function addIsSpaceSensitive(ast, options) {
return ast.map((node) => {
if (!node.children) {
return node;
Expand All @@ -443,8 +443,14 @@ function addIsSpaceSensitive(ast /*, options */) {
.map((child) => {
return {
...child,
isLeadingSpaceSensitive: isLeadingSpaceSensitiveNode(child),
isTrailingSpaceSensitive: isTrailingSpaceSensitiveNode(child),
isLeadingSpaceSensitive: isLeadingSpaceSensitiveNode(
child,
options
),
isTrailingSpaceSensitive: isTrailingSpaceSensitiveNode(
child,
options
),
};
})
.map((child, index, children) => ({
Expand Down
23 changes: 22 additions & 1 deletion src/language-html/printer-html.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const {
getPrettierIgnoreAttributeCommentData,
hasPrettierIgnore,
inferScriptParser,
isVueCustomBlock,
isScriptLikeTag,
isTextLikeNode,
normalizeParts,
Expand Down Expand Up @@ -98,6 +99,25 @@ function embed(path, print, textToDoc, options) {
? " "
: line,
]);
} else if (isVueCustomBlock(node.parent, options)) {
const parser = inferScriptParser(node.parent, options);
let printed;
if (parser) {
try {
printed = textToDoc(node.value, { parser });
} catch (error) {
// Do nothing
}
}
if (printed == null) {
printed = node.value;
}
return concat([
parser ? breakParent : "",
printOpeningTagPrefix(node),
stripTrailingHardline(printed, true),
printClosingTagSuffix(node),
]);
}
break;
}
Expand Down Expand Up @@ -225,7 +245,8 @@ function genericPrint(path, options, print) {
? ifBreak(indent(childrenDoc), childrenDoc, {
groupId: attrGroupId,
})
: isScriptLikeTag(node) &&
: (isScriptLikeTag(node) ||
isVueCustomBlock(node, options)) &&
node.parent.type === "root" &&
options.parser === "vue" &&
!options.vueIndentScriptAndStyle
Expand Down
141 changes: 78 additions & 63 deletions src/language-html/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const {
CSS_WHITE_SPACE_TAGS,
CSS_WHITE_SPACE_DEFAULT,
} = require("./constants.evaluate");
const { getParserName } = require("../common/util");

const htmlTagNames = require("html-tag-names");
const htmlElementAttributes = require("html-element-attributes");
Expand Down Expand Up @@ -60,23 +61,6 @@ function shouldPreserveContent(node, options) {
return true;
}

// top-level elements (excluding <template>, <style> and <script>) in Vue SFC are considered custom block
// custom blocks can be written in other languages so we should preserve them to not break the code
if (
options.parser === "vue" &&
node.type === "element" &&
node.parent.type === "root" &&
![
"template",
"style",
"script",
// vue parser can be used for vue dom template as well, so we should still format top-level <html>
"html",
].includes(node.fullName)
) {
return true;
}

// TODO: handle non-text children in <pre>
if (
isPreLikeNode(node) &&
Expand All @@ -87,6 +71,14 @@ function shouldPreserveContent(node, options) {
return true;
}

if (
isVueCustomBlock(node, options) &&
(options.embeddedLanguageFormatting === "off" ||
!inferScriptParser(node, options))
) {
return true;
}

return false;
}

Expand Down Expand Up @@ -161,7 +153,7 @@ function isIndentationSensitiveNode(node) {
return getNodeCssStyleWhiteSpace(node).startsWith("pre");
}

function isLeadingSpaceSensitiveNode(node) {
function isLeadingSpaceSensitiveNode(node, options) {
const isLeadingSpaceSensitive = _isLeadingSpaceSensitiveNode();

if (
Expand Down Expand Up @@ -202,6 +194,7 @@ function isLeadingSpaceSensitiveNode(node) {
(node.parent.type === "root" ||
(isPreLikeNode(node) && node.parent) ||
isScriptLikeTag(node.parent) ||
isVueCustomBlock(node.parent, options) ||
!isFirstChildLeadingSpaceSensitiveCssDisplay(node.parent.cssDisplay))
) {
return false;
Expand All @@ -218,7 +211,7 @@ function isLeadingSpaceSensitiveNode(node) {
}
}

function isTrailingSpaceSensitiveNode(node) {
function isTrailingSpaceSensitiveNode(node, options) {
if (isFrontMatterNode(node)) {
return false;
}
Expand All @@ -244,6 +237,7 @@ function isTrailingSpaceSensitiveNode(node) {
(node.parent.type === "root" ||
(isPreLikeNode(node) && node.parent) ||
isScriptLikeTag(node.parent) ||
isVueCustomBlock(node.parent, options) ||
!isLastChildTrailingSpaceSensitiveCssDisplay(node.parent.cssDisplay))
) {
return false;
Expand Down Expand Up @@ -361,59 +355,68 @@ function hasNonTextChild(node) {
return node.children && node.children.some((child) => child.type !== "text");
}

function inferScriptParser(node) {
if (node.name === "script" && !node.attrMap.src) {
if (
(!node.attrMap.lang && !node.attrMap.type) ||
node.attrMap.type === "module" ||
node.attrMap.type === "text/javascript" ||
node.attrMap.type === "text/babel" ||
node.attrMap.type === "application/javascript" ||
node.attrMap.lang === "jsx"
) {
return "babel";
}
function _inferScriptParser(node) {
const { type, lang } = node.attrMap;
if (
type === "module" ||
type === "text/javascript" ||
type === "text/babel" ||
type === "application/javascript" ||
lang === "jsx"
) {
return "babel";
}

if (
node.attrMap.type === "application/x-typescript" ||
node.attrMap.lang === "ts" ||
node.attrMap.lang === "tsx"
) {
return "typescript";
}
if (type === "application/x-typescript" || lang === "ts" || lang === "tsx") {
return "typescript";
}

if (node.attrMap.type === "text/markdown") {
return "markdown";
}
if (type === "text/markdown") {
return "markdown";
}

if (
node.attrMap.type.endsWith("json") ||
node.attrMap.type.endsWith("importmap")
) {
return "json";
}
if (type && (type.endsWith("json") || type.endsWith("importmap"))) {
return "json";
}

if (node.attrMap.type === "text/x-handlebars-template") {
return "glimmer";
}
if (type === "text/x-handlebars-template") {
return "glimmer";
}
}

if (node.name === "style") {
if (
!node.attrMap.lang ||
node.attrMap.lang === "postcss" ||
node.attrMap.lang === "css"
) {
return "css";
}
function inferStyleParser(node) {
const { lang } = node.attrMap;
if (lang === "postcss" || lang === "css") {
return "css";
}

if (node.attrMap.lang === "scss") {
return "scss";
}
if (lang === "scss") {
return "scss";
}

if (lang === "less") {
return "less";
}
}

if (node.attrMap.lang === "less") {
return "less";
function inferScriptParser(node, options) {
if (node.name === "script" && !node.attrMap.src) {
if (!node.attrMap.lang && !node.attrMap.type) {
return "babel";
}
return _inferScriptParser(node);
}

if (node.name === "style") {
return inferStyleParser(node) || "css";
}

if (options && isVueCustomBlock(node, options)) {
return (
_inferScriptParser(node) ||
inferStyleParser(node) ||
getParserName(node.attrMap.lang, options)
);
}

return null;
Expand Down Expand Up @@ -625,6 +628,17 @@ function unescapeQuoteEntities(text) {
return text.replace(/&apos;/g, "'").replace(/&quot;/g, '"');
}

// top-level elements (excluding <template>, <style> and <script>) in Vue SFC are considered custom block
const rootElementsSet = new Set(["template", "style", "script", "html"]);
function isVueCustomBlock(node, options) {
return (
options.parser === "vue" &&
node.type === "element" &&
node.parent.type === "root" &&
!rootElementsSet.has(node.fullName)
);
}

module.exports = {
HTML_ELEMENT_ATTRIBUTES,
HTML_TAGS,
Expand All @@ -642,6 +656,7 @@ module.exports = {
hasPrettierIgnore,
identity,
inferScriptParser,
isVueCustomBlock,
isDanglingSpaceSensitiveNode,
isFrontMatterNode,
isIndentationSensitiveNode,
Expand Down

0 comments on commit 915327d

Please sign in to comment.