diff --git a/packages/prettier-plugin/src/index.ts b/packages/prettier-plugin/src/index.ts index d8e29e0..e583f7f 100644 --- a/packages/prettier-plugin/src/index.ts +++ b/packages/prettier-plugin/src/index.ts @@ -1,19 +1,23 @@ -import type { AstPath, Parser, Plugin, Printer } from 'prettier'; +import * as prettier from 'prettier'; +import type { AstPath, Options, Parser, ParserOptions, Plugin, Printer } from 'prettier'; import { parseTempblotRoot, type ParsedRoot, type RootBlock } from 'tempblot-parser'; interface TempblotAst { type: 'TempblotDocument'; source: string; root: ParsedRoot; + formattedBlocks: Map; } const parser: Parser = { astFormat: 'tempblot-ast', - parse(source) { + async parse(source, options) { + const root = parseTempblotRoot(source); return { type: 'TempblotDocument', source, - root: parseTempblotRoot(source), + root, + formattedBlocks: await formatBlocks(source, root.blocks, options), }; }, locStart() { @@ -64,17 +68,17 @@ function formatDocument(path: AstPath) { const trailingText = ast.source.slice(blocks[blocks.length - 1].end).trim(); const parts = [ leadingText, - ...blocks.map((block) => formatBlock(ast.source, block)), + ...blocks.map((block) => formatBlock(ast, block)), trailingText, ].filter((part) => part.length > 0); return ensureTrailingNewline(parts.join('\n\n')); } -function formatBlock(source: string, block: RootBlock) { +function formatBlock(ast: TempblotAst, block: RootBlock) { const openTag = formatOpenTag(block); const closeTag = ``; - const contents = source.slice(block.startTagEnd, block.endTagStart).trim(); + const contents = ast.formattedBlocks.get(block) ?? ast.source.slice(block.startTagEnd, block.endTagStart).trim(); if (!contents) { return `${openTag}\n${closeTag}`; @@ -94,3 +98,87 @@ function formatOpenTag(block: RootBlock) { function ensureTrailingNewline(text: string) { return `${text}\n`; } + +async function formatBlocks(source: string, blocks: RootBlock[], options: ParserOptions) { + const formattedBlocks = new Map(); + + await Promise.all(blocks.map(async (block) => { + const parserName = getBlockParser(block); + const rawContents = source.slice(block.startTagEnd, block.endTagStart).trim(); + + if (!parserName || !rawContents) { + formattedBlocks.set(block, rawContents); + return; + } + + formattedBlocks.set(block, await formatEmbedded(rawContents, parserName, options)); + })); + + return formattedBlocks; +} + +async function formatEmbedded(source: string, parserName: string, options: ParserOptions) { + try { + return (await prettier.format(source, { + ...copyPrettierOptions(options), + parser: parserName, + })).trim(); + } catch { + return source.trim(); + } +} + +function getBlockParser(block: RootBlock) { + if (block.tag === 'setup') { + return 'typescript'; + } + + if (block.tag !== 'output') { + return undefined; + } + + return getOutputParser(block.attributes.lang); +} + +function getOutputParser(lang: string | undefined) { + switch (lang) { + case undefined: + case 'json': + case 'jsonc': + return 'json'; + case 'yaml': + case 'yml': + return 'yaml'; + case 'html': + return 'html'; + case 'css': + return 'css'; + case 'javascript': + case 'js': + return 'babel'; + case 'typescript': + case 'ts': + return 'typescript'; + default: + return undefined; + } +} + +function copyPrettierOptions(options: ParserOptions): Options { + return { + arrowParens: options.arrowParens, + bracketSameLine: options.bracketSameLine, + bracketSpacing: options.bracketSpacing, + endOfLine: options.endOfLine, + htmlWhitespaceSensitivity: options.htmlWhitespaceSensitivity, + printWidth: options.printWidth, + proseWrap: options.proseWrap, + quoteProps: options.quoteProps, + semi: options.semi, + singleAttributePerLine: options.singleAttributePerLine, + singleQuote: options.singleQuote, + tabWidth: options.tabWidth, + trailingComma: options.trailingComma, + useTabs: options.useTabs, + }; +}