prettier-plugin-ember-template-tag
Version:
A prettier plugin for formatting Ember template tags
174 lines (147 loc) • 4.76 kB
text/typescript
import type { Node } from '@babel/types';
import type {
AstPath,
doc,
Options as PrettierOptions,
Printer,
} from 'prettier';
import { printers as estreePrinters } from 'prettier/plugins/estree.js';
import { TEMPLATE_TAG_CLOSE, TEMPLATE_TAG_OPEN } from '../config.js';
import type { Options } from '../options.js';
import {
isGlimmerTemplate,
isGlimmerTemplateParent,
} from '../types/glimmer.js';
import { assert } from '../utils/index.js';
import {
fixPreviousPrint,
saveCurrentPrintOnSiblingNode,
} from './ambiguity.js';
import { printTemplateContent, printTemplateTag } from './template.js';
const estreePrinter = estreePrinters['estree'] as Printer<Node | undefined>;
export const printer: Printer<Node | undefined> = {
...estreePrinter,
getVisitorKeys(node, nonTraversableKeys) {
if (node && isGlimmerTemplate(node)) {
return [];
}
return estreePrinter.getVisitorKeys?.(node, nonTraversableKeys) || [];
},
print(
path: AstPath<Node | undefined>,
options: Options,
print: (path: AstPath<Node | undefined>) => doc.builders.Doc,
args: unknown,
) {
const { node } = path;
if (isGlimmerTemplateParent(node)) {
if (checkPrettierIgnore(path)) {
return printRawText(path, options);
} else {
let printed = estreePrinter.print(path, options, print, args);
assert('Expected Glimmer doc to be an array', Array.isArray(printed));
trimPrinted(printed);
// Remove semicolons so we can manage them ourselves
if (docMatchesString(printed[0], ';')) {
printed.shift();
}
if (docMatchesString(printed.at(-1), ';')) {
printed.pop();
}
trimPrinted(printed);
// Always remove export default so we start with a blank slate
if (
docMatchesString(printed[0], 'export') &&
docMatchesString(printed[1], 'default')
) {
printed = printed.slice(2);
trimPrinted(printed);
}
if (options.templateExportDefault) {
printed.unshift('export ', 'default ');
}
saveCurrentPrintOnSiblingNode(path, printed);
return printed;
}
}
if (options.semi && node?.extra?.['prevTemplatePrinted']) {
fixPreviousPrint(
node.extra['prevTemplatePrinted'] as doc.builders.Doc[],
path,
options,
print,
args,
);
}
return estreePrinter.print(path, options, print, args);
},
/** Prints embedded GlimmerExpressions/GlimmerTemplates. */
embed(path: AstPath<Node | undefined>, embedOptions: PrettierOptions) {
const { node } = path;
return async (textToDoc) => {
if (node && isGlimmerTemplate(node)) {
if (checkPrettierIgnore(path)) {
return printRawText(path, embedOptions as Options);
}
try {
const content = await printTemplateContent(
node.extra.template.contents,
textToDoc,
embedOptions as Options,
);
const printed = printTemplateTag(content);
saveCurrentPrintOnSiblingNode(path, printed);
return printed;
} catch (error) {
console.error(error);
const printed = [printRawText(path, embedOptions as Options)];
saveCurrentPrintOnSiblingNode(path, printed);
return printed;
}
}
// Nothing to embed, so move on to the regular printer.
return;
};
},
};
/** Remove the empty strings that Prettier added so we can manage them. */
function trimPrinted(printed: doc.builders.Doc[]): void {
while (docMatchesString(printed[0], '')) {
printed.shift();
}
while (docMatchesString(printed.at(-1), '')) {
printed.pop();
}
}
function printRawText(
{ node }: AstPath<Node | undefined>,
options: Options,
): string {
if (!node) {
return '';
}
if (isGlimmerTemplate(node)) {
return (
TEMPLATE_TAG_OPEN + node.extra.template.contents + TEMPLATE_TAG_CLOSE
);
}
assert('expected start', typeof node.start == 'number');
assert('expected end', typeof node.end == 'number');
return options.originalText.slice(node.start, node.end);
}
function hasPrettierIgnore(path: AstPath<Node | undefined>): boolean {
return path.node?.leadingComments?.at(-1)?.value.trim() === 'prettier-ignore';
}
function checkPrettierIgnore(path: AstPath<Node | undefined>): boolean {
return (
hasPrettierIgnore(path) ||
(!!path.getParentNode() &&
path.callParent((parent) => checkPrettierIgnore(parent)))
);
}
function docMatchesString(
doc: doc.builders.Doc | undefined,
string: string,
): doc is string {
return typeof doc === 'string' && doc.trim() === string;
}