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:
1 parent
7b7ca476d2
commit
915327d61d
19 files changed
+1862
-90
No files matched your search
@@ -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>
|
||||
```
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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
@@ -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(/'/g, "'").replace(/"/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,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 | "
|
||||
`;
|
||||
@@ -0,0 +1,3 @@
|
||||
<custom lang="js">
|
||||
const foo = "</";
|
||||
</custom>
|
||||
@@ -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
@@ -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" });
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,6 @@
|
||||
<i18n lang="yaml">
|
||||
en:
|
||||
hello: "hello world!"
|
||||
ja:
|
||||
hello: "こんにちは、世界!"
|
||||
</i18n>
|
||||
Reference in new issue
Block a user