diff --git a/src/parse/preprocess.ts b/src/parse/preprocess.ts index bfcab46b..7a06f11d 100644 --- a/src/parse/preprocess.ts +++ b/src/parse/preprocess.ts @@ -1,57 +1,23 @@ -import { Preprocessor } from 'content-tag'; +import { + getBuffer, + parse, + type Range, + replaceContents, + sliceByteRange, +} from '../utils/content-tag.js'; export interface Template { contents: string; + range: Range; type: string; - range: { - start: number; - end: number; - }; utf16Range: { - start: number; end: number; + start: number; }; } -const BufferMap: Map = new Map(); - const PLACEHOLDER = '~'; -function getBuffer(s: string): Buffer { - let buf = BufferMap.get(s); - if (!buf) { - buf = Buffer.from(s); - BufferMap.set(s, buf); - } - return buf; -} - -/** Slice string using byte range */ -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) */ -function byteToCharIndex(s: string, byteOffset: number): number { - const buf = getBuffer(s); - return buf.subarray(0, byteOffset).toString().length; -} - -/** Calculate byte length */ -function byteLength(s: string): number { - return getBuffer(s).length; -} - -function replaceRange( - s: string, - start: number, - end: number, - substitute: string, -): string { - return sliceByteRange(s, 0, start) + substitute + sliceByteRange(s, end); -} - /** * Replace the template with a parsable placeholder that takes up the same * range. @@ -72,7 +38,7 @@ export function preprocessTemplateRange( prefix = '{/*'; suffix = '*/}'; - const nextToken = code.slice(template.range.end).toString().match(/\S+/); + const nextToken = sliceByteRange(code, template.range.end).match(/\S+/); if (nextToken && (nextToken[0] === 'as' || nextToken[0] === 'satisfies')) { // Replace with parenthesized ObjectExpression @@ -83,32 +49,38 @@ 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 contents = template.contents.replaceAll('/', PLACEHOLDER); - const tplLength = template.range.end - template.range.start; + const templateLength = template.range.end - template.range.start; const spaces = - tplLength - byteLength(content) - prefix.length - suffix.length; - const total = prefix + content + ' '.repeat(spaces) + suffix; + templateLength - getBuffer(contents).length - prefix.length - suffix.length; - return replaceRange(code, template.range.start, template.range.end, total); + return replaceContents(code, { + contents: [prefix, contents, ' '.repeat(spaces), suffix].join(''), + range: template.range, + }); } -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), - }, - })); + const contentTags = parse(code, { filename }); + + const templates: Template[] = contentTags.map((contentTag) => { + const { contentRange, contents, range, type } = contentTag; + + const utf16Range = { + end: sliceByteRange(code, 0, range.end).length, + start: sliceByteRange(code, 0, range.start).length, + }; + + return { + contentRange, + contents, + range, + type, + utf16Range, + }; + }); return templates; } @@ -131,5 +103,5 @@ export function preprocess( code = preprocessTemplateRange(template, code); } - return { templates, code }; + return { code, templates }; } diff --git a/src/types/glimmer.ts b/src/types/glimmer.ts index 02bd049a..057062a6 100644 --- a/src/types/glimmer.ts +++ b/src/types/glimmer.ts @@ -7,7 +7,8 @@ import type { StaticBlock, TSAsExpression, } from '@babel/types'; -import type { Parsed as RawGlimmerTemplate } from 'content-tag'; + +import type { ContentTag } from '../utils/content-tag.js'; type GlimmerTemplateProperties = ( | BlockStatement @@ -28,7 +29,7 @@ type GlimmerTemplateProperties = ( extra: { isGlimmerTemplate: true; - template: RawGlimmerTemplate; + template: ContentTag; [key: string]: unknown; }; }; diff --git a/src/utils/content-tag.ts b/src/utils/content-tag.ts new file mode 100644 index 00000000..96d2ac6a --- /dev/null +++ b/src/utils/content-tag.ts @@ -0,0 +1,61 @@ +/* eslint-disable @typescript-eslint/consistent-type-definitions, eslint-comments/disable-enable-pair, jsdoc/require-jsdoc, unicorn/prefer-export-from */ +import { + type Parsed as ContentTag, + Preprocessor, + type PreprocessorOptions, +} from 'content-tag'; + +type Range = { + end: number; + start: number; +}; + +const BufferMap = new Map(); + +export function getBuffer(string_: string): Buffer { + let buffer = BufferMap.get(string_); + + if (!buffer) { + buffer = Buffer.from(string_); + BufferMap.set(string_, buffer); + } + + return buffer; +} + +export function parse( + file: string, + options?: PreprocessorOptions, +): ContentTag[] { + const preprocessor = new Preprocessor(); + + return preprocessor.parse(file, options); +} + +export function replaceContents( + file: string, + options: { + contents: string; + range: Range; + }, +): string { + const { contents, range } = options; + + return [ + sliceByteRange(file, 0, range.start), + contents, + sliceByteRange(file, range.end), + ].join(''); +} + +export function sliceByteRange( + string_: string, + indexStart: number, + indexEnd?: number, +): string { + const buffer = getBuffer(string_); + + return buffer.slice(indexStart, indexEnd).toString(); +} + +export type { ContentTag, Range }; diff --git a/tests/helpers/ambiguous.ts b/tests/helpers/ambiguous.ts index 6d6ac366..84a4f21d 100644 --- a/tests/helpers/ambiguous.ts +++ b/tests/helpers/ambiguous.ts @@ -1,7 +1,7 @@ -import { Preprocessor } from 'content-tag'; import { describe, expect, test } from 'vitest'; import type { Options } from '../../src/options.js'; +import { parse } from '../../src/utils/content-tag.js'; import type { TestCase } from '../helpers/cases.js'; import { getAllCases } from '../helpers/cases.js'; import { format } from '../helpers/format.js'; @@ -13,8 +13,6 @@ import type { Config } from './make-suite.js'; */ export const AMBIGUOUS_PLACEHOLDER = '/*AMBIGUOUS*/'; -const preprocessor = new Preprocessor(); - const AMBIGUOUS_EXPRESSIONS = [ '(oops) => {}', '(oh, no) => {}', @@ -96,7 +94,7 @@ async function behavesLikeFormattedAmbiguousCase( try { const result = await format(code, formatOptions); expect(result).toMatchSnapshot(); - preprocessor.parse(result); + parse(result); } catch (error: unknown) { // Some of the ambiguous cases are Syntax Errors when parsed const isSyntaxError =