diff --git a/packages/prettier-plugin-java/src/comments.ts b/packages/prettier-plugin-java/src/comments.ts index a8376ea0..ec13e104 100644 --- a/packages/prettier-plugin-java/src/comments.ts +++ b/packages/prettier-plugin-java/src/comments.ts @@ -42,6 +42,9 @@ export function determineFormatterOffOnRanges(cst: JavaNonTerminal) { export function isFullyBetweenFormatterOffOn(path: AstPath) { const { node, root } = path; + if (isNonTerminal(node) && node.location === undefined) { + return false; + } const start = parser.locStart(node); const end = parser.locEnd(node); return ( diff --git a/packages/prettier-plugin-java/src/printer.ts b/packages/prettier-plugin-java/src/printer.ts index 24198ab4..415ea137 100644 --- a/packages/prettier-plugin-java/src/printer.ts +++ b/packages/prettier-plugin-java/src/printer.ts @@ -6,9 +6,11 @@ import { isFullyBetweenFormatterOffOn } from "./comments.js"; import { + embedTextBlock, isNonTerminal, isTerminal, printComment, + printTextBlock, type JavaNode, type JavaTerminal } from "./printers/helpers.js"; @@ -16,9 +18,17 @@ import { printerForNodeType } from "./printers/index.js"; export default { print(path: DistributedAstPath, options, print, args) { - return hasTerminal(path) - ? path.node.image - : printerForNodeType(path.node.name)(path, print, options, args); + if (hasTerminal(path)) { + return path.node.tokenType.name === "TextBlock" + ? printTextBlock(path) + : path.node.image; + } + return printerForNodeType(path.node.name)(path, print, options, args); + }, + embed(path: DistributedAstPath) { + return hasTerminal(path) && path.node.tokenType.name === "TextBlock" + ? embedTextBlock(path) + : null; }, hasPrettierIgnore(path) { const { node } = path; diff --git a/packages/prettier-plugin-java/src/printers/helpers.ts b/packages/prettier-plugin-java/src/printers/helpers.ts index f91f7ed5..12d780db 100644 --- a/packages/prettier-plugin-java/src/printers/helpers.ts +++ b/packages/prettier-plugin-java/src/printers/helpers.ts @@ -9,7 +9,7 @@ import type { IToken, StatementCstNode } from "java-parser"; -import type { AstPath, Doc, ParserOptions } from "prettier"; +import type { AstPath, Doc, Options, ParserOptions } from "prettier"; import { builders } from "prettier/doc"; import type { JavaComment } from "../comments.js"; import parser from "../parser.js"; @@ -328,6 +328,94 @@ export function printClassType( }); } +export function printTextBlock(path: AstPath) { + const [open, ...lines] = path.node.image.split("\n"); + const baseIndent = findBaseIndent(lines); + const textBlock = join(hardline, [ + open, + ...lines.map(line => line.slice(baseIndent)) + ]); + const ancestor = path.getNode(16) as JavaNonTerminal | null; + return ancestor?.name === "variableInitializer" || + (ancestor?.name === "binaryExpression" && + ancestor.children.AssignmentOperator) + ? indent(textBlock) + : textBlock; +} + +export function embedTextBlock(path: AstPath) { + const language = findEmbeddedLanguage(path); + if (!language) { + return null; + } + const text = path.node.image + .replace(/^"""\n/, "") + .replace(/"""$/, "") + .replace(/\\u+([0-9a-fA-F]{4})/g, (_, hex) => + String.fromCharCode(parseInt(hex, 16)) + ); + const unindentedText = stripIndent(text); + const decodedText = translateEscapes(unindentedText); + + return async ( + textToDoc: (text: string, options: Options) => Promise + ) => { + const doc = await textToDoc(decodedText, { parser: language }); + return group(indent(['"""', hardline, doc, hardline, '"""'])); + }; +} + +function findEmbeddedLanguage(path: AstPath) { + return path.ancestors + .find( + node => + (isNonTerminal(node) && node.name === "blockStatement") || + node.comments?.some(({ leading }) => leading) + ) + ?.comments?.filter(({ leading }) => leading) + .reverse() + .map( + ({ image }) => + image.match(/^(?:\/\/|\/\*)\s*language\s*=\s*([^\s]+)/)?.[1] + ) + .find(language => language) + ?.toLowerCase(); +} + +function stripIndent(text: string) { + const lines = text.split("\n"); + const indent = findBaseIndent(lines); + return lines.map(line => line.slice(indent)).join("\n"); +} + +function translateEscapes(text: string) { + return text.replace( + /\\(?:([bfntr"'\\])|([0-7]{1,3})|\n)/g, + (_, single, octal) => { + if (single) { + switch (single) { + case "b": + return "\b"; + case "f": + return "\f"; + case "n": + return "\n"; + case "t": + return "\t"; + case "r": + return "\r"; + default: + return single; + } + } else if (octal) { + return String.fromCharCode(parseInt(octal, 8)); + } else { + return ""; + } + } + ); +} + export function isBinaryExpression(expression: ExpressionCstNode) { const conditionalExpression = expression.children.conditionalExpression?.[0].children; diff --git a/packages/prettier-plugin-java/src/printers/lexical-structure.ts b/packages/prettier-plugin-java/src/printers/lexical-structure.ts index 2f67321c..50b7f8cd 100644 --- a/packages/prettier-plugin-java/src/printers/lexical-structure.ts +++ b/packages/prettier-plugin-java/src/printers/lexical-structure.ts @@ -1,35 +1,12 @@ -import { builders } from "prettier/doc"; import { - findBaseIndent, map, onlyDefinedKey, printSingle, - type JavaNodePrinters, - type JavaNonTerminal + type JavaNodePrinters } from "./helpers.js"; -const { hardline, indent, join } = builders; - export default { - literal(path, print) { - const { TextBlock } = path.node.children; - if (!TextBlock) { - return printSingle(path, print); - } - const [open, ...lines] = TextBlock[0].image.split("\n"); - const baseIndent = findBaseIndent(lines); - const textBlock = join(hardline, [ - open, - ...lines.map(line => line.slice(baseIndent)) - ]); - const ancestor = path.getNode(14) as JavaNonTerminal | null; - return ancestor?.name === "variableInitializer" || - (ancestor?.name === "binaryExpression" && - ancestor.children.AssignmentOperator) - ? indent(textBlock) - : textBlock; - }, - + literal: printSingle, integerLiteral: printSingle, floatingPointLiteral: printSingle, booleanLiteral: printSingle, diff --git a/packages/prettier-plugin-java/test/unit-test/text-blocks/_input.java b/packages/prettier-plugin-java/test/unit-test/text-blocks/_input.java index 0780d47e..ef607ea6 100644 --- a/packages/prettier-plugin-java/test/unit-test/text-blocks/_input.java +++ b/packages/prettier-plugin-java/test/unit-test/text-blocks/_input.java @@ -55,4 +55,21 @@ public void print(%s object) { ); } + void json() { + // language=json + String config = """ + { "name":"example", + "enabled" :true, + "timeout":30} + """; + + /* language = JSON */ + String query = """ + { + "sql":"SELECT * FROM users \ + WHERE active=1 \ + AND deleted=0", + "limit":10} + """; + } } diff --git a/packages/prettier-plugin-java/test/unit-test/text-blocks/_output.java b/packages/prettier-plugin-java/test/unit-test/text-blocks/_output.java index 566eeff1..072fab42 100644 --- a/packages/prettier-plugin-java/test/unit-test/text-blocks/_output.java +++ b/packages/prettier-plugin-java/test/unit-test/text-blocks/_output.java @@ -52,4 +52,19 @@ public void print(%s object) { abc""" ); } + + void json() { + // language=json + String config = """ + { "name": "example", "enabled": true, "timeout": 30 } + """; + + /* language = JSON */ + String query = """ + { + "sql": "SELECT * FROM users WHERE active=1 AND deleted=0", + "limit": 10 + } + """; + } }