diff --git a/README.md b/README.md index 089324a..3be0372 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,14 @@ Formatting embedded ECMAScript code doesn't require other plugins and uses the p Formatting embedded GLSL code requires [`prettier-plugin-glsl`](https://github.com/NaridaL/glsl-language-toolkit/tree/main/packages/prettier-plugin-glsl) to be loaded as well. +#### GraphQL + +| Option | Default | Description | +| :--------------------------: | :------------------------------------------: | ----------------------------------------------------------------------------------------------------- | +| `embeddedGraphqlIdentifiers` | [`[...]`](./src/embedded/graphql/options.ts) | Tag or comment identifiers that make their subsequent template literals be identified as GraphQL code | + +Formatting embedded GraphQL code doesn't require other plugins and uses the parsers and printers provided by Prettier natively. This can override the native embedded language formatting for GraphQL code. If you want to keep the native behavior, set `embeddedGraphqlIdentifiers` to `[]` or other identifiers. + #### HTML | Option | Default | Description | diff --git a/src/embedded/graphql/embedded-language.ts b/src/embedded/graphql/embedded-language.ts new file mode 100644 index 0000000..cd10541 --- /dev/null +++ b/src/embedded/graphql/embedded-language.ts @@ -0,0 +1,7 @@ +export const embeddedLanguage = "embeddedGraphql"; + +declare module "../types.js" { + interface EmbeddedLanguagesHolder { + [embeddedLanguage]: void; + } +} diff --git a/src/embedded/graphql/embedder.ts b/src/embedded/graphql/embedder.ts new file mode 100644 index 0000000..4bd2fc1 --- /dev/null +++ b/src/embedded/graphql/embedder.ts @@ -0,0 +1,85 @@ +import type { Options } from "prettier"; +import { builders } from "prettier/doc"; +import type { Embedder } from "../../types.js"; +import { + preparePlaceholder, + printTemplateExpressions, + simpleRehydrateDoc, +} from "../utils.js"; +import { embeddedLanguage } from "./embedded-language.js"; + +const { line, group, indent, softline } = builders; + +export const embedder: Embedder = async ( + textToDoc, + print, + path, + options, + { identifier, embeddedOverrideOptions }, +) => { + options = { + ...options, + ...embeddedOverrideOptions, + }; + + const { node } = path; + + const { createPlaceholder, placeholderRegex } = preparePlaceholder(); + + const text = node.quasis + .map((quasi, index, { length }) => + index === length - 1 + ? quasi.value.cooked + : quasi.value.cooked + createPlaceholder(index), + ) + .join(""); + + const leadingWhitespaces = text.match(/^\s+/)?.[0] ?? ""; + const trailingWhitespaces = text.match(/\s+$/)?.[0] ?? ""; + + const trimmedText = text.slice( + leadingWhitespaces.length, + -trailingWhitespaces.length || undefined, + ); + + const expressionDocs = printTemplateExpressions(path, print); + + const doc = await textToDoc(trimmedText, { + ...options, + parser: "graphql", + }); + + const contentDoc = simpleRehydrateDoc(doc, placeholderRegex, expressionDocs); + + if (options.preserveEmbeddedExteriorWhitespaces?.includes(identifier)) { + // TODO: should we label the doc with { hug: false } ? + // https://github.com/prettier/prettier/blob/5cfb76ee50cf286cab267cf3cb7a26e749c995f7/src/language-js/embed/html.js#L88 + return group([ + "`", + leadingWhitespaces, + options.noEmbeddedMultiLineIndentation?.includes(identifier) + ? [group(contentDoc)] + : indent([group(contentDoc)]), + trailingWhitespaces, + "`", + ]); + } + + const leadingLineBreak = leadingWhitespaces.length ? line : softline; + const trailingLineBreak = trailingWhitespaces.length ? line : softline; + + return group([ + "`", + options.noEmbeddedMultiLineIndentation?.includes(identifier) + ? [leadingLineBreak, group(contentDoc)] + : indent([leadingLineBreak, group(contentDoc)]), + trailingLineBreak, + "`", + ]); +}; + +declare module "../types.js" { + interface EmbeddedEmbedders { + [embeddedLanguage]: typeof embedder; + } +} diff --git a/src/embedded/graphql/index.ts b/src/embedded/graphql/index.ts new file mode 100644 index 0000000..cee8055 --- /dev/null +++ b/src/embedded/graphql/index.ts @@ -0,0 +1,3 @@ +export * from "./embedded-language.js"; +export * from "./embedder.js"; +export * from "./options.js"; diff --git a/src/embedded/graphql/options.ts b/src/embedded/graphql/options.ts new file mode 100644 index 0000000..64b8867 --- /dev/null +++ b/src/embedded/graphql/options.ts @@ -0,0 +1,47 @@ +import type { CoreCategoryType, SupportOptions } from "prettier"; +import { + makeIdentifiersOptionName, + type AutocompleteStringList, + type StringListToInterfaceKey, +} from "../utils.js"; +import { embeddedLanguage } from "./embedded-language.js"; + +/** References: + * - https://github.com/github-linguist/linguist/blob/7ca3799b8b5f1acde1dd7a8dfb7ae849d3dfb4cd/lib/linguist/languages.yml#L2578 + */ +const DEFAULT_IDENTIFIERS = ["graphql", "gql"] as const; +type Identifiers = AutocompleteStringList; +type DefaultIdentifiersHolder = StringListToInterfaceKey< + typeof DEFAULT_IDENTIFIERS +>; + +const EMBEDDED_LANGUAGE_IDENTIFIERS = + makeIdentifiersOptionName(embeddedLanguage); + +export interface PrettierPluginDepsOptions { + /* prettier built-in options */ +} + +export const options = { + [EMBEDDED_LANGUAGE_IDENTIFIERS]: { + category: "Global", + type: "string", + array: true, + default: [{ value: [...DEFAULT_IDENTIFIERS] }], + description: "Specify embedded GraphQL language identifiers.", + }, +} satisfies SupportOptions & Record; + +type Options = typeof options; + +declare module "../types.js" { + interface EmbeddedOptions extends Options {} + interface EmbeddedDefaultIdentifiersHolder extends DefaultIdentifiersHolder {} + interface PrettierPluginEmbedOptions { + [EMBEDDED_LANGUAGE_IDENTIFIERS]?: Identifiers; + } +} + +declare module "prettier" { + export interface Options extends PrettierPluginDepsOptions {} +}