Skip to content

Commit

Permalink
feat: support sh (uses prettier-plugin-sh).
Browse files Browse the repository at this point in the history
  • Loading branch information
Sec-ant committed Dec 11, 2023
1 parent a6ddcd3 commit e534a43
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 0 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,16 @@ Formatting embedded Java Properties code requires [`prettier-plugin-properties`]

Formatting embedded Ruby code requires [`@prettier/plugin-ruby`](https://github.com/prettier/plugin-ruby) to be loaded and [its dependencies to be installed](https://github.com/prettier/plugin-ruby#getting-started) as well. And [options](https://github.com/prettier/plugin-ruby#configuration) supported by `@prettier/plugin-ruby` can therefore be used to further control the formatting behavior.

#### Shell

| Option | Default | Description |
| :---------------------: | :-------------------------------------: | --------------------------------------------------------------------------------------------------- |
| `embeddedShIdentifiers` | [`[...]`](./src/embedded/sh/options.ts) | Tag or comment identifiers that make their subsequent template literals be identified as Shell code |

Formatting embedded Shell code requires [`prettier-plugin-sh`](https://github.com/un-ts/prettier/tree/master/packages/sh#readme) to be loaded as well. And [options](https://github.com/un-ts/prettier/tree/master/packages/sh#parser-options) supported by `prettier-plugin-sh` can therefore be used to further control the formatting behavior.

Note that `prettier-plugin-sh` supports different variants of shell syntaxes and they are specified by the [`variant` option](https://github.com/un-ts/prettier/tree/master/packages/sh#parser-options). To map a subset of identifiers to another dialect, please use [`embeddedOverrides`](#embeddedoverrides).

#### SQL

| Option | Default | Description |
Expand Down
41 changes: 41 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"prettier-plugin-latex": "^2.0.1",
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-properties": "^0.3.0",
"prettier-plugin-sh": "^0.13.1",
"prettier-plugin-sql": "^0.17.1",
"prettier-plugin-toml": "^2.0.1",
"semantic-release": "^22.0.10",
Expand Down
7 changes: 7 additions & 0 deletions src/embedded/sh/embedded-language.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const embeddedLanguage = "embeddedSh";

declare module "../types.js" {
interface EmbeddedLanguagesHolder {
[embeddedLanguage]: void;
}
}
104 changes: 104 additions & 0 deletions src/embedded/sh/embedder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { Options } from "prettier";
import { builders, utils } from "prettier/doc";
import type { Embedder } from "../../types.js";
import { preparePlaceholder, printTemplateExpressions } from "../utils.js";
import { embeddedLanguage } from "./embedded-language.js";

const { hardline, group, line, softline, indent } = builders;
const { mapDoc } = utils;

export const embedder: Embedder<Options> = 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: "sh",
});

const contentDoc = mapDoc(doc, (doc) => {
if (typeof doc !== "string") {
return doc;
}
const parts = [];
const components = doc.split(placeholderRegex);
for (let i = 0; i < components.length; i++) {
let component = components[i];
if (i % 2 == 0) {
if (!component) {
continue;
}
component = component.replaceAll(/([\\`]|\${)/g, "\\$1");
component
.split(/(\n)/)
.forEach((c) => (c === "\n" ? parts.push(hardline) : parts.push(c)));
} else {
const placeholderIndex = Number(component);
parts.push(expressionDocs[placeholderIndex]);
}
}
return parts;
});

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;
}
}
3 changes: 3 additions & 0 deletions src/embedded/sh/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./embedded-language.js";
export * from "./embedder.js";
export * from "./options.js";
46 changes: 46 additions & 0 deletions src/embedded/sh/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { CoreCategoryType, SupportOptions } from "prettier";
import type { ShParserOptions } from "prettier-plugin-sh";
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#L6445C8-L6445C8
*/
const DEFAULT_IDENTIFIERS = ["sh"] as const;
type Identifiers = AutocompleteStringList<typeof DEFAULT_IDENTIFIERS>;
type DefaultIdentifiersHolder = StringListToInterfaceKey<
typeof DEFAULT_IDENTIFIERS
>;

const EMBEDDED_LANGUAGE_IDENTIFIERS =
makeIdentifiersOptionName(embeddedLanguage);

export interface PrettierPluginDepsOptions extends Partial<ShParserOptions> {}

export const options = {
[EMBEDDED_LANGUAGE_IDENTIFIERS]: {
category: "Global",
type: "string",
array: true,
default: [{ value: [...DEFAULT_IDENTIFIERS] }],
description: "Specify embedded Shell language identifiers.",
},
} satisfies SupportOptions & Record<string, { category: CoreCategoryType }>;

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 {}
}

0 comments on commit e534a43

Please sign in to comment.