Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 36 additions & 64 deletions src/parse/preprocess.ts
Original file line number Diff line number Diff line change
@@ -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<string, Buffer> = 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.
Expand All @@ -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
Expand All @@ -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;
}
Expand All @@ -131,5 +103,5 @@ export function preprocess(
code = preprocessTemplateRange(template, code);
}

return { templates, code };
return { code, templates };
}
5 changes: 3 additions & 2 deletions src/types/glimmer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,7 +29,7 @@ type GlimmerTemplateProperties = (

extra: {
isGlimmerTemplate: true;
template: RawGlimmerTemplate;
template: ContentTag;
[key: string]: unknown;
};
};
Expand Down
61 changes: 61 additions & 0 deletions src/utils/content-tag.ts
Original file line number Diff line number Diff line change
@@ -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<string, Buffer>();

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 };
6 changes: 2 additions & 4 deletions tests/helpers/ambiguous.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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) => {}',
Expand Down Expand Up @@ -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 =
Expand Down