diff --git a/src/parse/index.ts b/src/parse/index.ts index 1a1abb27..c8ef90cc 100644 --- a/src/parse/index.ts +++ b/src/parse/index.ts @@ -6,21 +6,15 @@ import type { 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'; +import { assert } from '../utils/assert.js'; +import { preprocess, type Template } from './preprocess.js'; const typescript = babelParsers['babel-ts'] as Parser; -const p = new Preprocessor(); /** Converts a node into a GlimmerTemplate node */ function convertNode( @@ -36,43 +30,53 @@ function convertNode( /** 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]; + + // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check + switch (node.type) { + case 'BlockStatement': + case 'ObjectExpression': + case 'StaticBlock': { + assert('expected range', node.range); + const [start, end] = node.range; + + const templateIndex = templates.findIndex((template) => { + const { utf16Range } = template; + + if (utf16Range.start === start && utf16Range.end === end) { + return true; + } + + return ( + node.type === 'ObjectExpression' && + utf16Range.start === start - 1 && + utf16Range.end === end + 1 + ); + }); + + if (templateIndex === -1) { + return null; + } + + const rawTemplate = templates.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; } } @@ -80,60 +84,24 @@ function convertAst(ast: File, templates: Template[]): void { }, }); - if (unprocessedTemplates.length > 0) { + if (templates.length > 0) { throw new Error( - `failed to process all templates, ${unprocessedTemplates.length} remaining`, + `failed to process all templates, ${templates.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 = { ...typescript, astFormat: PRINTER_NAME, async parse(code: string, options: Options): Promise { 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; -} diff --git a/src/parse/preprocess.ts b/src/parse/preprocess.ts index 0dd434a8..bfcab46b 100644 --- a/src/parse/preprocess.ts +++ b/src/parse/preprocess.ts @@ -1,3 +1,5 @@ +import { Preprocessor } from 'content-tag'; + export interface Template { contents: string; type: string; @@ -13,7 +15,7 @@ export interface Template { const BufferMap: Map = new Map(); -export const PLACEHOLDER = '~'; +const PLACEHOLDER = '~'; function getBuffer(s: string): Buffer { let buf = BufferMap.get(s); @@ -25,19 +27,19 @@ function getBuffer(s: string): Buffer { } /** Slice string using byte range */ -export function sliceByteRange(s: string, a: number, b?: number): string { +function sliceByteRange(s: string, a: number, b?: number): string { const buf = getBuffer(s); return buf.subarray(a, b).toString(); } /** Converts byte index to js char index (utf16) */ -export function byteToCharIndex(s: string, byteOffset: number): number { +function byteToCharIndex(s: string, byteOffset: number): number { const buf = getBuffer(s); return buf.subarray(0, byteOffset).toString().length; } /** Calculate byte length */ -export function byteLength(s: string): number { +function byteLength(s: string): number { return getBuffer(s).length; } @@ -71,6 +73,7 @@ export function preprocessTemplateRange( suffix = '*/}'; const nextToken = code.slice(template.range.end).toString().match(/\S+/); + if (nextToken && (nextToken[0] === 'as' || nextToken[0] === 'satisfies')) { // Replace with parenthesized ObjectExpression prefix = '(' + prefix; @@ -81,9 +84,52 @@ export function preprocessTemplateRange( // We need to replace forward slash with _something else_, because // forward slash breaks the parsed templates. const content = template.contents.replaceAll('/', PLACEHOLDER); + const tplLength = template.range.end - template.range.start; const spaces = tplLength - byteLength(content) - prefix.length - suffix.length; const total = prefix + content + ' '.repeat(spaces) + suffix; + return replaceRange(code, template.range.start, template.range.end, total); } + +const p = new Preprocessor(); + +/** 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; +} + +/** + * 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 }; +} diff --git a/src/print/index.ts b/src/print/index.ts index 110fd038..30b39901 100644 --- a/src/print/index.ts +++ b/src/print/index.ts @@ -13,7 +13,7 @@ import { isGlimmerTemplate, isGlimmerTemplateParent, } from '../types/glimmer.js'; -import { assert } from '../utils/index.js'; +import { assert } from '../utils/assert.js'; import { fixPreviousPrint, saveCurrentPrintOnSiblingNode, diff --git a/src/utils/index.ts b/src/utils/assert.ts similarity index 100% rename from src/utils/index.ts rename to src/utils/assert.ts diff --git a/tests/unit-tests/preprocess.test.ts b/tests/unit-tests/preprocess.test.ts index 05bec1a8..f516c030 100644 --- a/tests/unit-tests/preprocess.test.ts +++ b/tests/unit-tests/preprocess.test.ts @@ -1,8 +1,7 @@ import { describe, expect, test } from 'vitest'; -import { codeToGlimmerAst } from '../../src/parse/index.js'; import { - PLACEHOLDER, + codeToGlimmerAst, preprocessTemplateRange, } from '../../src/parse/preprocess.js'; @@ -13,21 +12,19 @@ const TEST_CASES = [ }, { code: '', - expected: [`{/*${PLACEHOLDER}* hi *${PLACEHOLDER} */}`], + expected: [`{/*~* hi *~ */}`], }, { code: '', - expected: [`{/*
hi<${PLACEHOLDER}div> */}`], + expected: [`{/*
hi<~div> */}`], }, { code: '', - expected: [`{/*{{#if true}}hi{{${PLACEHOLDER}if}} */}`], + expected: [`{/*{{#if true}}hi{{~if}} */}`], }, { code: '', - expected: [ - `{/*${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER} */}`, - ], + expected: [`{/*~~~~~~~~~~~~~~~~ */}`], }, { code: '',