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
108 changes: 38 additions & 70 deletions src/parse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Node | undefined>;
const p = new Preprocessor();

/** Converts a node into a GlimmerTemplate node */
function convertNode(
Expand All @@ -36,104 +30,78 @@ 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;
}
}

return null;
},
});

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<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;
}
54 changes: 50 additions & 4 deletions src/parse/preprocess.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Preprocessor } from 'content-tag';

export interface Template {
contents: string;
type: string;
Expand All @@ -13,7 +15,7 @@ export interface Template {

const BufferMap: Map<string, Buffer> = new Map();

export const PLACEHOLDER = '~';
const PLACEHOLDER = '~';

function getBuffer(s: string): Buffer {
let buf = BufferMap.get(s);
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
Expand All @@ -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 };
}
2 changes: 1 addition & 1 deletion src/print/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
File renamed without changes.
13 changes: 5 additions & 8 deletions tests/unit-tests/preprocess.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -13,21 +12,19 @@ const TEST_CASES = [
},
{
code: '<template>/* hi */</template>',
expected: [`{/*${PLACEHOLDER}* hi *${PLACEHOLDER} */}`],
expected: [`{/*~* hi *~ */}`],
},
{
code: '<template><div>hi</div></template>',
expected: [`{/*<div>hi<${PLACEHOLDER}div> */}`],
expected: [`{/*<div>hi<~div> */}`],
},
{
code: '<template>{{#if true}}hi{{/if}}</template>',
expected: [`{/*{{#if true}}hi{{${PLACEHOLDER}if}} */}`],
expected: [`{/*{{#if true}}hi{{~if}} */}`],
},
{
code: '<template>////////////////</template>',
expected: [
`{/*${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER}${PLACEHOLDER} */}`,
],
expected: [`{/*~~~~~~~~~~~~~~~~ */}`],
},
{
code: '<template>💩</template>',
Expand Down