Skip to content

Commit 60dc6a8

Browse files
committed
feat: process embedded languages in text blocks with language identifier comments
1 parent daefe4c commit 60dc6a8

File tree

6 files changed

+139
-29
lines changed

6 files changed

+139
-29
lines changed

packages/prettier-plugin-java/src/comments.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ export function determineFormatterOffOnRanges(cst: JavaNonTerminal) {
4242

4343
export function isFullyBetweenFormatterOffOn(path: AstPath<JavaNode>) {
4444
const { node, root } = path;
45+
if (isNonTerminal(node) && node.location === undefined) {
46+
return false;
47+
}
4548
const start = parser.locStart(node);
4649
const end = parser.locEnd(node);
4750
return (

packages/prettier-plugin-java/src/printer.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,29 @@ import {
66
isFullyBetweenFormatterOffOn
77
} from "./comments.js";
88
import {
9+
embedTextBlock,
910
isNonTerminal,
1011
isTerminal,
1112
printComment,
13+
printTextBlock,
1214
type JavaNode,
1315
type JavaTerminal
1416
} from "./printers/helpers.js";
1517
import { printerForNodeType } from "./printers/index.js";
1618

1719
export default {
1820
print(path: DistributedAstPath<JavaNode>, options, print, args) {
19-
return hasTerminal(path)
20-
? path.node.image
21-
: printerForNodeType(path.node.name)(path, print, options, args);
21+
if (hasTerminal(path)) {
22+
return path.node.tokenType.name === "TextBlock"
23+
? printTextBlock(path)
24+
: path.node.image;
25+
}
26+
return printerForNodeType(path.node.name)(path, print, options, args);
27+
},
28+
embed(path: DistributedAstPath<JavaNode>) {
29+
return hasTerminal(path) && path.node.tokenType.name === "TextBlock"
30+
? embedTextBlock(path)
31+
: null;
2232
},
2333
hasPrettierIgnore(path) {
2434
const { node } = path;

packages/prettier-plugin-java/src/printers/helpers.ts

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type {
99
IToken,
1010
StatementCstNode
1111
} from "java-parser";
12-
import type { AstPath, Doc, ParserOptions } from "prettier";
12+
import type { AstPath, Doc, Options, ParserOptions } from "prettier";
1313
import { builders } from "prettier/doc";
1414
import type { JavaComment } from "../comments.js";
1515
import parser from "../parser.js";
@@ -328,6 +328,94 @@ export function printClassType(
328328
});
329329
}
330330

331+
export function printTextBlock(path: AstPath<JavaTerminal>) {
332+
const [open, ...lines] = path.node.image.split("\n");
333+
const baseIndent = findBaseIndent(lines);
334+
const textBlock = join(hardline, [
335+
open,
336+
...lines.map(line => line.slice(baseIndent))
337+
]);
338+
const ancestor = path.getNode(16) as JavaNonTerminal | null;
339+
return ancestor?.name === "variableInitializer" ||
340+
(ancestor?.name === "binaryExpression" &&
341+
ancestor.children.AssignmentOperator)
342+
? indent(textBlock)
343+
: textBlock;
344+
}
345+
346+
export function embedTextBlock(path: AstPath<JavaTerminal>) {
347+
const language = findEmbeddedLanguage(path);
348+
if (!language) {
349+
return null;
350+
}
351+
const text = path.node.image
352+
.replace(/^"""\n/, "")
353+
.replace(/"""$/, "")
354+
.replace(/\\u+([0-9a-fA-F]{4})/g, (_, hex) =>
355+
String.fromCharCode(parseInt(hex, 16))
356+
);
357+
const unindentedText = stripIndent(text);
358+
const decodedText = translateEscapes(unindentedText);
359+
360+
return async (
361+
textToDoc: (text: string, options: Options) => Promise<Doc>
362+
) => {
363+
const doc = await textToDoc(decodedText, { parser: language });
364+
return group(indent(['"""', hardline, doc, hardline, '"""']));
365+
};
366+
}
367+
368+
function findEmbeddedLanguage(path: AstPath<JavaNode>) {
369+
return path.ancestors
370+
.find(
371+
node =>
372+
(isNonTerminal(node) && node.name === "blockStatement") ||
373+
node.comments?.some(({ leading }) => leading)
374+
)
375+
?.comments?.filter(({ leading }) => leading)
376+
.reverse()
377+
.map(
378+
({ image }) =>
379+
image.match(/^(?:\/\/|\/\*)\s*language\s*=\s*([^\s]+)/)?.[1]
380+
)
381+
.find(language => language)
382+
?.toLowerCase();
383+
}
384+
385+
function stripIndent(text: string) {
386+
const lines = text.split("\n");
387+
const indent = findBaseIndent(lines);
388+
return lines.map(line => line.slice(indent)).join("\n");
389+
}
390+
391+
function translateEscapes(text: string) {
392+
return text.replace(
393+
/\\(?:([bfntr"'\\])|([0-7]{1,3})|\n)/g,
394+
(_, single, octal) => {
395+
if (single) {
396+
switch (single) {
397+
case "b":
398+
return "\b";
399+
case "f":
400+
return "\f";
401+
case "n":
402+
return "\n";
403+
case "t":
404+
return "\t";
405+
case "r":
406+
return "\r";
407+
default:
408+
return single;
409+
}
410+
} else if (octal) {
411+
return String.fromCharCode(parseInt(octal, 8));
412+
} else {
413+
return "";
414+
}
415+
}
416+
);
417+
}
418+
331419
export function isBinaryExpression(expression: ExpressionCstNode) {
332420
const conditionalExpression =
333421
expression.children.conditionalExpression?.[0].children;

packages/prettier-plugin-java/src/printers/lexical-structure.ts

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,12 @@
1-
import { builders } from "prettier/doc";
21
import {
3-
findBaseIndent,
42
map,
53
onlyDefinedKey,
64
printSingle,
7-
type JavaNodePrinters,
8-
type JavaNonTerminal
5+
type JavaNodePrinters
96
} from "./helpers.js";
107

11-
const { hardline, indent, join } = builders;
12-
138
export default {
14-
literal(path, print) {
15-
const { TextBlock } = path.node.children;
16-
if (!TextBlock) {
17-
return printSingle(path, print);
18-
}
19-
const [open, ...lines] = TextBlock[0].image.split("\n");
20-
const baseIndent = findBaseIndent(lines);
21-
const textBlock = join(hardline, [
22-
open,
23-
...lines.map(line => line.slice(baseIndent))
24-
]);
25-
const ancestor = path.getNode(14) as JavaNonTerminal | null;
26-
return ancestor?.name === "variableInitializer" ||
27-
(ancestor?.name === "binaryExpression" &&
28-
ancestor.children.AssignmentOperator)
29-
? indent(textBlock)
30-
: textBlock;
31-
},
32-
9+
literal: printSingle,
3310
integerLiteral: printSingle,
3411
floatingPointLiteral: printSingle,
3512
booleanLiteral: printSingle,

packages/prettier-plugin-java/test/unit-test/text-blocks/_input.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,21 @@ public void print(%s object) {
5555
);
5656
}
5757

58+
void json() {
59+
// language=json
60+
String config = """
61+
{ "name":"example",
62+
"enabled" :true,
63+
"timeout":30}
64+
""";
65+
66+
/* language = JSON */
67+
String query = """
68+
{
69+
"sql":"SELECT * FROM users \
70+
WHERE active=1 \
71+
AND deleted=0",
72+
"limit":10}
73+
""";
74+
}
5875
}

packages/prettier-plugin-java/test/unit-test/text-blocks/_output.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,19 @@ public void print(%s object) {
5252
abc"""
5353
);
5454
}
55+
56+
void json() {
57+
// language=json
58+
String config = """
59+
{ "name": "example", "enabled": true, "timeout": 30 }
60+
""";
61+
62+
/* language = JSON */
63+
String query = """
64+
{
65+
"sql": "SELECT * FROM users WHERE active=1 AND deleted=0",
66+
"limit": 10
67+
}
68+
""";
69+
}
5570
}

0 commit comments

Comments
 (0)