|
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"; |
3 | 3 | import { |
4 | 4 | SyntaxType, |
5 | 5 | type NodeOfType, |
|
9 | 9 | } from "../tree-sitter-java.js"; |
10 | 10 |
|
11 | 11 | const { group, hardline, ifBreak, indent, join, line, softline } = builders; |
| 12 | +const { mapDoc } = utils; |
12 | 13 |
|
13 | 14 | export function hasType<T extends JavaTypeString>( |
14 | 15 | path: AstPath<JavaNode>, |
@@ -315,12 +316,117 @@ export function printVariableDeclaration( |
315 | 316 | return declaration; |
316 | 317 | } |
317 | 318 |
|
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 | + ); |
324 | 430 | } |
325 | 431 |
|
326 | 432 | export type JavaNode<T extends JavaTypeString = JavaTypeString> = |
|
0 commit comments