diff --git a/packages/prettier-plugin-java/src/comments.ts b/packages/prettier-plugin-java/src/comments.ts index b8ca818c..77fa2dd4 100644 --- a/packages/prettier-plugin-java/src/comments.ts +++ b/packages/prettier-plugin-java/src/comments.ts @@ -72,10 +72,13 @@ export function canAttachComment(node: JavaNode) { } switch (node.type) { case SyntaxType.EnumBodyDeclarations: + case SyntaxType.EscapeSequence: case SyntaxType.FormalParameters: case SyntaxType.Modifier: + case SyntaxType.MultilineStringFragment: case SyntaxType.ParenthesizedExpression: case SyntaxType.Program: + case SyntaxType.StringFragment: case SyntaxType.Visibility: return false; default: diff --git a/packages/prettier-plugin-java/src/printer.ts b/packages/prettier-plugin-java/src/printer.ts index 244f7e2a..04a9ba6d 100644 --- a/packages/prettier-plugin-java/src/printer.ts +++ b/packages/prettier-plugin-java/src/printer.ts @@ -8,11 +8,14 @@ import { willPrintOwnComments } from "./comments.js"; import { + embedTextBlock, + hasType, printComment, printValue, type JavaComment, type JavaNode, - type JavaNodePath + type JavaNodePath, + type JavaNodeType } from "./printers/helpers.js"; import { printerForNodeType } from "./printers/index.js"; import { SyntaxType } from "./tree-sitter-java.js"; @@ -23,6 +26,11 @@ export default { ? printerForNodeType(path.node.type)(path, print, options, args) : printValue(path); }, + embed(path: JavaNodePath) { + return hasType(path, SyntaxType.StringLiteral) + ? embedTextBlock(path) + : null; + }, hasPrettierIgnore(path) { return ( path.node.comments?.some(isPrettierIgnore) === true || @@ -44,6 +52,9 @@ export default { ownLine: handleLineComment, endOfLine: handleLineComment, remaining: handleRemainingComment + }, + getVisitorKeys() { + return ["namedChildren"]; } } satisfies Printer; diff --git a/packages/prettier-plugin-java/src/printers/helpers.ts b/packages/prettier-plugin-java/src/printers/helpers.ts index 41cfef67..55e793d8 100644 --- a/packages/prettier-plugin-java/src/printers/helpers.ts +++ b/packages/prettier-plugin-java/src/printers/helpers.ts @@ -1,5 +1,5 @@ -import type { AstPath, Doc, ParserOptions } from "prettier"; -import { builders } from "prettier/doc"; +import type { AstPath, Doc, Options, ParserOptions } from "prettier"; +import { builders, utils } from "prettier/doc"; import { SyntaxType, type NodeOfType, @@ -9,6 +9,7 @@ import { } from "../tree-sitter-java.js"; const { group, hardline, ifBreak, indent, join, line, softline } = builders; +const { mapDoc } = utils; export function hasType( path: AstPath, @@ -315,12 +316,117 @@ export function printVariableDeclaration( return declaration; } -export function findBaseIndent(lines: string[]) { - return lines.length - ? Math.min( - ...lines.map(line => line.search(/\S/)).filter(indent => indent >= 0) - ) - : 0; +export function printTextBlock( + path: JavaNodePath, + contents: Doc +) { + const parts = ['"""', hardline, contents, '"""']; + const parentType = (path.parent as JavaNode | null)?.type; + const grandparentType = (path.grandparent as JavaNode | null)?.type; + return parentType === SyntaxType.AssignmentExpression || + parentType === SyntaxType.VariableDeclarator || + (path.node.fieldName === "object" && + (grandparentType === SyntaxType.AssignmentExpression || + grandparentType === SyntaxType.VariableDeclarator)) + ? indent(parts) + : parts; +} + +export function embedTextBlock(path: JavaNodePath) { + const hasInterpolations = path.node.namedChildren.some( + ({ type }) => type === SyntaxType.StringInterpolation + ); + if (hasInterpolations || path.node.children[0].value === '"') { + return null; + } + + const language = findEmbeddedLanguage(path); + if (!language) { + return null; + } + + const text = unescapeTextBlockContents(textBlockContents(path.node)); + + return async ( + textToDoc: (text: string, options: Options) => Promise + ) => { + const doc = await textToDoc(text, { parser: language }); + return printTextBlock(path, escapeDocForTextBlock(doc)); + }; +} + +export function textBlockContents(node: JavaNode) { + const lines = node.value + .replace( + /(?<=^|[^\\])((?:\\\\)*)\\u+([0-9a-fA-F]{4})/g, + (_, backslashPairs: string, hex: string) => + backslashPairs + String.fromCharCode(parseInt(hex, 16)) + ) + .split("\n") + .slice(1); + const baseIndent = findBaseIndent(lines); + return lines + .map(line => line.slice(baseIndent)) + .join("\n") + .slice(0, -3); +} + +function findBaseIndent(lines: string[]) { + return Math.min( + ...lines.map(line => line.search(/\S/)).filter(indent => indent >= 0) + ); +} + +function findEmbeddedLanguage(path: JavaNodePath) { + return path.ancestors + .find( + ({ type, comments }) => + type === SyntaxType.Block || comments?.some(({ leading }) => leading) + ) + ?.comments?.filter(({ leading }) => leading) + .map( + ({ value }) => value.match(/^(?:\/\/|\/\*)\s*language\s*=\s*(\S+)/)?.[1] + ) + .findLast(language => language) + ?.toLowerCase(); +} + +function escapeDocForTextBlock(doc: Doc) { + return mapDoc(doc, currentDoc => + typeof currentDoc === "string" + ? currentDoc.replace(/\\|"""/g, match => `\\${match}`) + : currentDoc + ); +} + +function unescapeTextBlockContents(text: string) { + return text.replace( + /\\(?:([bstnfr"'\\])|\n|\r\n?|([0-3][0-7]{0,2}|[0-7]{1,2}))/g, + (_, single, octal) => { + if (single) { + switch (single) { + case "b": + return "\b"; + case "s": + return " "; + case "t": + return "\t"; + case "n": + return "\n"; + case "f": + return "\f"; + case "r": + return "\r"; + default: + return single; + } + } else if (octal) { + return String.fromCharCode(parseInt(octal, 8)); + } else { + return ""; + } + } + ); } export type JavaNode = diff --git a/packages/prettier-plugin-java/src/printers/lexical-structure.ts b/packages/prettier-plugin-java/src/printers/lexical-structure.ts index 1205eb2c..650441cd 100644 --- a/packages/prettier-plugin-java/src/printers/lexical-structure.ts +++ b/packages/prettier-plugin-java/src/printers/lexical-structure.ts @@ -1,9 +1,9 @@ import { builders } from "prettier/doc"; import { SyntaxType } from "../tree-sitter-java.js"; import { - findBaseIndent, + printTextBlock, printValue, - type JavaNode, + textBlockContents, type JavaNodePrinters } from "./helpers.js"; @@ -18,25 +18,10 @@ export default { return path.map(print, "children"); } - const lines = path.node.children - .map(({ value }) => value) - .join("") - .split("\n") - .slice(1); - const baseIndent = findBaseIndent(lines); - const textBlock = join(hardline, [ - '"""', - ...lines.map(line => line.slice(baseIndent)) - ]); - const parentType = (path.parent as JavaNode | null)?.type; - const grandparentType = (path.grandparent as JavaNode | null)?.type; - return parentType === SyntaxType.AssignmentExpression || - parentType === SyntaxType.VariableDeclarator || - (path.node.fieldName === "object" && - (grandparentType === SyntaxType.AssignmentExpression || - grandparentType === SyntaxType.VariableDeclarator)) - ? indent(textBlock) - : textBlock; + return printTextBlock( + path, + join(hardline, textBlockContents(path.node).split("\n")) + ); }, string_fragment: printValue, 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..72ab58f3 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,26 @@ public void print(%s object) { ); } + void json() { + // language = json + String someJson = """ + {"glossary":{"title": "example glossary"}} + """; + + // 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..3d48ffde 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,21 @@ public void print(%s object) { abc""" ); } + + void json() { + // language = json + String someJson = """ + { "glossary": { "title": "example glossary" } }"""; + + // 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 + }"""; + } }