Skip to content

Commit 9e9950d

Browse files
committed
feat: process embedded languages in text blocks with language identifier comments
1 parent a7d2728 commit 9e9950d

File tree

6 files changed

+174
-30
lines changed

6 files changed

+174
-30
lines changed

src/comments.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,13 @@ export function canAttachComment(node: JavaNode) {
7272
}
7373
switch (node.type) {
7474
case SyntaxType.EnumBodyDeclarations:
75+
case SyntaxType.EscapeSequence:
7576
case SyntaxType.FormalParameters:
7677
case SyntaxType.Modifier:
78+
case SyntaxType.MultilineStringFragment:
7779
case SyntaxType.ParenthesizedExpression:
7880
case SyntaxType.Program:
81+
case SyntaxType.StringFragment:
7982
case SyntaxType.Visibility:
8083
return false;
8184
default:

src/printer.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ import {
88
willPrintOwnComments
99
} from "./comments.js";
1010
import {
11+
embedTextBlock,
12+
hasType,
1113
printComment,
1214
printValue,
1315
type JavaComment,
1416
type JavaNode,
15-
type JavaNodePath
17+
type JavaNodePath,
18+
type JavaNodeType
1619
} from "./printers/helpers.js";
1720
import { printerForNodeType } from "./printers/index.js";
1821
import { SyntaxType } from "./tree-sitter-java.js";
@@ -23,6 +26,11 @@ export default {
2326
? printerForNodeType(path.node.type)(path, print, options, args)
2427
: printValue(path);
2528
},
29+
embed(path: JavaNodePath<JavaNodeType>) {
30+
return hasType(path, SyntaxType.StringLiteral)
31+
? embedTextBlock(path)
32+
: null;
33+
},
2634
hasPrettierIgnore(path) {
2735
return (
2836
path.node.comments?.some(isPrettierIgnore) === true ||
@@ -44,6 +52,9 @@ export default {
4452
ownLine: handleLineComment,
4553
endOfLine: handleLineComment,
4654
remaining: handleRemainingComment
55+
},
56+
getVisitorKeys() {
57+
return ["namedChildren"];
4758
}
4859
} satisfies Printer<JavaNode>;
4960

src/printers/helpers.ts

Lines changed: 114 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { AstPath, Doc, ParserOptions } from "prettier";
2-
import { builders } from "prettier/doc";
1+
import type { AstPath, Doc, Options, ParserOptions } from "prettier";
2+
import { builders, utils } from "prettier/doc";
33
import {
44
SyntaxType,
55
type NodeOfType,
@@ -9,6 +9,7 @@ import {
99
} from "../tree-sitter-java.js";
1010

1111
const { group, hardline, ifBreak, indent, join, line, softline } = builders;
12+
const { mapDoc } = utils;
1213

1314
export function hasType<T extends JavaTypeString>(
1415
path: AstPath<JavaNode>,
@@ -315,12 +316,117 @@ export function printVariableDeclaration(
315316
return declaration;
316317
}
317318

318-
export function findBaseIndent(lines: string[]) {
319-
return lines.length
320-
? Math.min(
321-
...lines.map(line => line.search(/\S/)).filter(indent => indent >= 0)
322-
)
323-
: 0;
319+
export function printTextBlock(
320+
path: JavaNodePath<SyntaxType.StringLiteral>,
321+
contents: Doc
322+
) {
323+
const parts = ['"""', hardline, contents, '"""'];
324+
const parentType = (path.parent as JavaNode | null)?.type;
325+
const grandparentType = (path.grandparent as JavaNode | null)?.type;
326+
return parentType === SyntaxType.AssignmentExpression ||
327+
parentType === SyntaxType.VariableDeclarator ||
328+
(path.node.fieldName === "object" &&
329+
(grandparentType === SyntaxType.AssignmentExpression ||
330+
grandparentType === SyntaxType.VariableDeclarator))
331+
? indent(parts)
332+
: parts;
333+
}
334+
335+
export function embedTextBlock(path: JavaNodePath<SyntaxType.StringLiteral>) {
336+
const hasInterpolations = path.node.namedChildren.some(
337+
({ type }) => type === SyntaxType.StringInterpolation
338+
);
339+
if (hasInterpolations || path.node.children[0].value === '"') {
340+
return null;
341+
}
342+
343+
const language = findEmbeddedLanguage(path);
344+
if (!language) {
345+
return null;
346+
}
347+
348+
const text = unescapeTextBlockContents(textBlockContents(path.node));
349+
350+
return async (
351+
textToDoc: (text: string, options: Options) => Promise<Doc>
352+
) => {
353+
const doc = await textToDoc(text, { parser: language });
354+
return printTextBlock(path, escapeDocForTextBlock(doc));
355+
};
356+
}
357+
358+
export function textBlockContents(node: JavaNode<SyntaxType.StringLiteral>) {
359+
const lines = node.value
360+
.replace(
361+
/(?<=^|[^\\])((?:\\\\)*)\\u+([0-9a-fA-F]{4})/g,
362+
(_, backslashPairs: string, hex: string) =>
363+
backslashPairs + String.fromCharCode(parseInt(hex, 16))
364+
)
365+
.split("\n")
366+
.slice(1);
367+
const baseIndent = findBaseIndent(lines);
368+
return lines
369+
.map(line => line.slice(baseIndent))
370+
.join("\n")
371+
.slice(0, -3);
372+
}
373+
374+
function findBaseIndent(lines: string[]) {
375+
return Math.min(
376+
...lines.map(line => line.search(/\S/)).filter(indent => indent >= 0)
377+
);
378+
}
379+
380+
function findEmbeddedLanguage(path: JavaNodePath) {
381+
return path.ancestors
382+
.find(
383+
({ type, comments }) =>
384+
type === SyntaxType.Block || comments?.some(({ leading }) => leading)
385+
)
386+
?.comments?.filter(({ leading }) => leading)
387+
.map(
388+
({ value }) => value.match(/^(?:\/\/|\/\*)\s*language\s*=\s*(\S+)/)?.[1]
389+
)
390+
.findLast(language => language)
391+
?.toLowerCase();
392+
}
393+
394+
function escapeDocForTextBlock(doc: Doc) {
395+
return mapDoc(doc, currentDoc =>
396+
typeof currentDoc === "string"
397+
? currentDoc.replace(/\\|"""/g, match => `\\${match}`)
398+
: currentDoc
399+
);
400+
}
401+
402+
function unescapeTextBlockContents(text: string) {
403+
return text.replace(
404+
/\\(?:([bstnfr"'\\])|\n|\r\n?|([0-3][0-7]{0,2}|[0-7]{1,2}))/g,
405+
(_, single, octal) => {
406+
if (single) {
407+
switch (single) {
408+
case "b":
409+
return "\b";
410+
case "s":
411+
return " ";
412+
case "t":
413+
return "\t";
414+
case "n":
415+
return "\n";
416+
case "f":
417+
return "\f";
418+
case "r":
419+
return "\r";
420+
default:
421+
return single;
422+
}
423+
} else if (octal) {
424+
return String.fromCharCode(parseInt(octal, 8));
425+
} else {
426+
return "";
427+
}
428+
}
429+
);
324430
}
325431

326432
export type JavaNode<T extends JavaTypeString = JavaTypeString> =

src/printers/lexical-structure.ts

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { builders } from "prettier/doc";
22
import { SyntaxType } from "../tree-sitter-java.js";
33
import {
4-
findBaseIndent,
4+
printTextBlock,
55
printValue,
6-
type JavaNode,
6+
textBlockContents,
77
type JavaNodePrinters
88
} from "./helpers.js";
99

@@ -18,25 +18,10 @@ export default {
1818
return path.map(print, "children");
1919
}
2020

21-
const lines = path.node.children
22-
.map(({ value }) => value)
23-
.join("")
24-
.split("\n")
25-
.slice(1);
26-
const baseIndent = findBaseIndent(lines);
27-
const textBlock = join(hardline, [
28-
'"""',
29-
...lines.map(line => line.slice(baseIndent))
30-
]);
31-
const parentType = (path.parent as JavaNode | null)?.type;
32-
const grandparentType = (path.grandparent as JavaNode | null)?.type;
33-
return parentType === SyntaxType.AssignmentExpression ||
34-
parentType === SyntaxType.VariableDeclarator ||
35-
(path.node.fieldName === "object" &&
36-
(grandparentType === SyntaxType.AssignmentExpression ||
37-
grandparentType === SyntaxType.VariableDeclarator))
38-
? indent(textBlock)
39-
: textBlock;
21+
return printTextBlock(
22+
path,
23+
join(hardline, textBlockContents(path.node).split("\n"))
24+
);
4025
},
4126

4227
string_fragment: printValue,

test/unit-test/text-blocks/_input.java

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

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

test/unit-test/text-blocks/_output.java

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

0 commit comments

Comments
 (0)