Vue: Support custom blocks (#8023)

* 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 f65c7cd7d814d3149aadbeb3cfd00c5372887c33.

* 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>
This commit is contained in:
Sosuke Suzukiandfisker Cheung authored and GitHub committed 2020-04-17 07:54:24 +09:00
1 parent 7b7ca476d2
commit 915327d61d
19 files changed
+1862 -90

No files matched your search

+27
View File
@@ -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
View File
@@ -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]/;
@@ -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,
@@ -828,6 +845,7 @@ module.exports = {
getPrecedence,
shouldFlatten,
isBitwiseOperator,
getParserName,
getPenultimate,
getLast,
getNextNonSpaceNonCommentCharacterIndexWithStartIndex,
+17 -3
View File
@@ -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 {
+9 -3
View File
@@ -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;
@@ -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) => ({
+22 -1
View File
@@ -30,6 +30,7 @@ const {
getPrettierIgnoreAttributeCommentData,
hasPrettierIgnore,
inferScriptParser,
isVueCustomBlock,
isScriptLikeTag,
isTextLikeNode,
normalizeParts,
@@ -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;
}
@@ -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
+80 -65
View File
@@ -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");
@@ -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) &&
@@ -87,6 +71,14 @@ function shouldPreserveContent(node, options) {
return true;
}
if (
isVueCustomBlock(node, options) &&
(options.embeddedLanguageFormatting === "off" ||
!inferScriptParser(node, options))
) {
return true;
}
return false;
}
@@ -161,7 +153,7 @@ function isIndentationSensitiveNode(node) {
return getNodeCssStyleWhiteSpace(node).startsWith("pre");
}
function isLeadingSpaceSensitiveNode(node) {
function isLeadingSpaceSensitiveNode(node, options) {
const isLeadingSpaceSensitive = _isLeadingSpaceSensitiveNode();
if (
@@ -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;
@@ -218,7 +211,7 @@ function isLeadingSpaceSensitiveNode(node) {
}
}
function isTrailingSpaceSensitiveNode(node) {
function isTrailingSpaceSensitiveNode(node, options) {
if (isFrontMatterNode(node)) {
return false;
}
@@ -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;
@@ -361,59 +355,68 @@ function hasNonTextChild(node) {
return node.children && node.children.some((child) => child.type !== "text");
}
function inferScriptParser(node) {
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 (type === "application/x-typescript" || lang === "ts" || lang === "tsx") {
return "typescript";
}
if (type === "text/markdown") {
return "markdown";
}
if (type && (type.endsWith("json") || type.endsWith("importmap"))) {
return "json";
}
if (type === "text/x-handlebars-template") {
return "glimmer";
}
}
function inferStyleParser(node) {
const { lang } = node.attrMap;
if (lang === "postcss" || lang === "css") {
return "css";
}
if (lang === "scss") {
return "scss";
}
if (lang === "less") {
return "less";
}
}
function inferScriptParser(node, options) {
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"
) {
if (!node.attrMap.lang && !node.attrMap.type) {
return "babel";
}
if (
node.attrMap.type === "application/x-typescript" ||
node.attrMap.lang === "ts" ||
node.attrMap.lang === "tsx"
) {
return "typescript";
}
if (node.attrMap.type === "text/markdown") {
return "markdown";
}
if (
node.attrMap.type.endsWith("json") ||
node.attrMap.type.endsWith("importmap")
) {
return "json";
}
if (node.attrMap.type === "text/x-handlebars-template") {
return "glimmer";
}
return _inferScriptParser(node);
}
if (node.name === "style") {
if (
!node.attrMap.lang ||
node.attrMap.lang === "postcss" ||
node.attrMap.lang === "css"
) {
return "css";
}
return inferStyleParser(node) || "css";
}
if (node.attrMap.lang === "scss") {
return "scss";
}
if (node.attrMap.lang === "less") {
return "less";
}
if (options && isVueCustomBlock(node, options)) {
return (
_inferScriptParser(node) ||
inferStyleParser(node) ||
getParserName(node.attrMap.lang, options)
);
}
return null;
@@ -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,
@@ -642,6 +656,7 @@ module.exports = {
hasPrettierIgnore,
identity,
inferScriptParser,
isVueCustomBlock,
isDanglingSpaceSensitiveNode,
isFrontMatterNode,
isIndentationSensitiveNode,
+1 -18
View File
@@ -1,7 +1,6 @@
"use strict";
const util = require("../common/util");
const support = require("../main/support");
const {
builders: { hardline, literalline, concat, markAsRoot },
utils: { mapDoc },
@@ -15,7 +14,7 @@ function embed(path, print, textToDoc, options) {
// only look for the first string so as to support [markdown-preview-enhanced](https://shd101wyy.github.io/markdown-preview-enhanced/#/code-chunk)
const langMatch = node.lang.match(/^[\w-]+/);
const lang = langMatch ? langMatch[0] : "";
const parser = getParserName(lang);
const parser = util.getParserName(lang, options);
if (parser) {
const styleUnit = options.__inJsTemplate ? "~" : "`";
const style = styleUnit.repeat(
@@ -65,22 +64,6 @@ function embed(path, print, textToDoc, options) {
return null;
function getParserName(lang) {
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;
}
function replaceNewlinesWithLiterallines(doc) {
return mapDoc(doc, (currentDoc) =>
typeof currentDoc === "string" && currentDoc.includes("\n")
@@ -0,0 +1,55 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`custom_block.vue error test 1`] = `
"Unexpected character \\"\\"\\" (2:16)
1 | <custom lang=\\"js\\">
> 2 | const foo = \\"</\\";
| ^
3 | </custom>
4 | "
`;
exports[`custom_block.vue error test 2`] = `
"Unexpected character \\"\\"\\" (2:16)
1 | <custom lang=\\"js\\">
> 2 | const foo = \\"</\\";
| ^
3 | </custom>
4 | "
`;
exports[`custom_block.vue error test 3`] = `
"Unexpected character \\"\\"\\" (2:16)
1 | <custom lang=\\"js\\">
> 2 | const foo = \\"</\\";
| ^
3 | </custom>
4 | "
`;
exports[`custom_block.vue error test 4`] = `
"Unexpected character \\"\\"\\" (2:16)
1 | <custom lang=\\"js\\">
> 2 | const foo = \\"</\\";
| ^
3 | </custom>
4 | "
`;
exports[`custom_block.vue error test 5`] = `
"Unexpected character \\"\\"\\" (2:16)
1 | <custom lang=\\"js\\">
> 2 | const foo = \\"</\\";
| ^
3 | </custom>
4 | "
`;
exports[`custom_block.vue error test 6`] = `
"Unexpected character \\"\\"\\" (2:16)
1 | <custom lang=\\"js\\">
> 2 | const foo = \\"</\\";
| ^
3 | </custom>
4 | "
`;
+3
View File
@@ -0,0 +1,3 @@
<custom lang="js">
const foo = "</";
</custom>
+6
View File
@@ -0,0 +1,6 @@
run_spec(__dirname, ["vue"]);
run_spec(__dirname, ["vue"], { trailingComma: "none" });
run_spec(__dirname, ["vue"]);
run_spec(__dirname, ["vue"], { semi: false });
run_spec(__dirname, ["vue"], { vueIndentScriptAndStyle: true });
run_spec(__dirname, ["vue"], { embeddedLanguageFormatting: "off" });
File diff suppressed because it is too large. Load diff
+11
View File
@@ -0,0 +1,11 @@
<page-query lang="graphql">
query { posts: allWordPressPost {
edges {
node {
id
title
}
}
}
}
</page-query>
@@ -0,0 +1,3 @@
<custom type="text/x-handlebars-template">
Handlebars <b>{{ doesWhat}}</b>
</custom>
@@ -0,0 +1,5 @@
run_spec(__dirname, ["vue"], { trailingComma: "none" });
run_spec(__dirname, ["vue"]);
run_spec(__dirname, ["vue"], { semi: false });
run_spec(__dirname, ["vue"], { vueIndentScriptAndStyle: true });
run_spec(__dirname, ["vue"], { embeddedLanguageFormatting: "off" });
+34
View File
@@ -0,0 +1,34 @@
<i18n lang="json">
{ "en": {
"hello": "hello world!"
}, "ja": {
"hello": "こんにちは、世界!"
}
}
</i18n>
<i18n type="application/json">
{ "en": {
"hello": "hello world!"
}, "ja": {
"hello": "こんにちは、世界!"
}
}
</i18n>
<custom lang="json">{
"a": 1
}</custom>
<custom lang="json">{
"a": 1
}
</custom>
@@ -0,0 +1,4 @@
<custom lang="js">
const foo
= 'foo'
</custom>
+23
View File
@@ -0,0 +1,23 @@
<docs lang="markdown">
# Foo
- bar
- baz
| Age | Time | Food | Gold | Requirement |
| ------------ | ----- | ---- | ---- | ----------------------- |
| Feudal Age | 02:10 | 500 | 0 | Dark Age building x 2 |
| Castle Age | 02:40 | 800 | 200 |- |
| Imperial Age | 03:30 | 1000 | 800 | Castle Age building x 2 |
</docs>
<docs type="text/markdown">
# Foo
- bar
- baz
| Age | Time | Food | Gold | Requirement |
| ------------ | ----- | ---- | ---- | ----------------------- |
| Feudal Age | 02:10 | 500 | 0 | Dark Age building x 2 |
| Castle Age | 02:40 | 800 | 200 |- |
| Imperial Age | 03:30 | 1000 | 800 | Castle Age building x 2 |
</docs>
+25
View File
@@ -0,0 +1,25 @@
<i18n lang="foooo">
{"en": {
"hello": "hello world!"
}, "ja": {
"hello": "こんにちは、世界!"
}
}
</i18n>
<custom>
const foo =
"foo";
</custom>
<custom lang="zzz">
const foo = "foo";
const foo = "foo";</custom>
<custom lang="zzz">123</custom>
<custom>{
foo: "bar"
}</custom>
+6
View File
@@ -0,0 +1,6 @@
<i18n lang="yaml">
en:
hello: "hello world!"
ja:
hello: "こんにちは、世界!"
</i18n>