prettier-plugin-ember-template-tag
Version:
A prettier plugin for formatting Ember template tags
140 lines (124 loc) • 4.03 kB
text/typescript
import { traverse } from '@babel/core';
import type {
BlockStatement,
File,
Node,
ObjectExpression,
StaticBlock,
} from '@babel/types';
import { Preprocessor } from 'content-tag';
import type { Parser } from 'prettier';
import { parsers as babelParsers } from 'prettier/plugins/babel.js';
import { PRINTER_NAME } from '../config.js';
import type { Options } from '../options.js';
import { assert } from '../utils/index.js';
import {
byteToCharIndex,
preprocessTemplateRange,
type Template,
} from './preprocess.js';
const typescript = babelParsers['babel-ts'] as Parser<Node | undefined>;
const p = new Preprocessor();
/** Converts a node into a GlimmerTemplate node */
function convertNode(
node: BlockStatement | ObjectExpression | StaticBlock,
rawTemplate: Template,
): void {
node.innerComments = [];
node.extra = Object.assign(node.extra ?? {}, {
isGlimmerTemplate: true as const,
template: rawTemplate,
});
}
/** Traverses the AST and replaces the transformed template parts with other AST */
function convertAst(ast: File, templates: Template[]): void {
const unprocessedTemplates = [...templates];
traverse(ast, {
enter(path) {
const { node } = path;
if (
node.type === 'BlockStatement' ||
node.type === 'ObjectExpression' ||
node.type === 'StaticBlock'
) {
const { range } = node;
assert('expected range', range);
const [start, end] = range;
const templateIndex = unprocessedTemplates.findIndex(
(t) =>
(t.utf16Range.start === start && t.utf16Range.end === end) ||
(node.type === 'ObjectExpression' &&
t.utf16Range.start === start - 1 &&
t.utf16Range.end === end + 1),
);
if (templateIndex > -1) {
const rawTemplate = unprocessedTemplates.splice(templateIndex, 1)[0];
if (!rawTemplate) {
throw new Error(
'expected raw template because splice index came from findIndex',
);
}
const index =
node.innerComments?.[0] &&
ast.comments?.indexOf(node.innerComments[0]);
if (ast.comments && index !== undefined && index >= 0) {
ast.comments.splice(index, 1);
}
convertNode(node, rawTemplate);
} else {
return null;
}
}
return null;
},
});
if (unprocessedTemplates.length > 0) {
throw new Error(
`failed to process all templates, ${unprocessedTemplates.length} remaining`,
);
}
}
/**
* Pre-processes the template info, parsing the template content to Glimmer AST,
* fixing the offsets and locations of all nodes also calculates the block
* params locations & ranges and adding it to the info
*/
export function preprocess(
code: string,
fileName: string,
): {
code: string;
templates: Template[];
} {
const templates = codeToGlimmerAst(code, fileName);
for (const template of templates) {
code = preprocessTemplateRange(template, code);
}
return { templates, code };
}
export const parser: Parser<Node | undefined> = {
...typescript,
astFormat: PRINTER_NAME,
async parse(code: string, options: Options): Promise<Node> {
const preprocessed = preprocess(code, options.filepath);
const ast = await typescript.parse(preprocessed.code, options);
assert('expected ast', ast);
convertAst(ast as File, preprocessed.templates);
return ast;
},
};
/** Pre-processes the template info, parsing the template content to Glimmer AST. */
export function codeToGlimmerAst(code: string, filename: string): Template[] {
const rawTemplates = p.parse(code, { filename });
const templates: Template[] = rawTemplates.map((r) => ({
type: r.type,
range: r.range,
contentRange: r.contentRange,
contents: r.contents,
utf16Range: {
start: byteToCharIndex(code, r.range.start),
end: byteToCharIndex(code, r.range.end),
},
}));
return templates;
}