Skip to content

Commit 13c3313

Browse files
authored
Merge pull request #194 from gitKrystan/fix-191
2 parents 755af73 + 15885a5 commit 13c3313

File tree

22 files changed

+1121
-2239
lines changed

22 files changed

+1121
-2239
lines changed

examples/bin/test.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const run = async () => {
4040
'node',
4141
[
4242
'./node_modules/prettier/bin/prettier.cjs',
43-
'.',
43+
'./input',
4444
'--write',
4545
debug ? '--log-level' : null,
4646
debug ? 'debug' : null,

examples/expected-output/example.gts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ export interface Signature {
1111
module top level component. Explicit default export module top level
1212
component. Explicit default export module top level component. Explicit
1313
default export module top level component.
14-
</template> as TemplateOnlyComponent<Signature>;
14+
</template> as TemplateOnlyComponent<Signature>

src/options.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import type {
88
export interface Options extends ParserOptions<Node | undefined> {
99
templateExportDefault?: boolean;
1010
templateSingleQuote?: boolean;
11-
__inputWasPreprocessed?: boolean;
1211
}
1312

1413
const templateExportDefault: BooleanSupportOption = {
@@ -38,15 +37,7 @@ const templateSingleQuote: BooleanSupportOption = {
3837
'Use single quotes instead of double quotes within template tags. Since 0.0.3.',
3938
};
4039

41-
const __inputWasPreprocessed: BooleanSupportOption = {
42-
category: 'Format',
43-
type: 'boolean',
44-
description:
45-
'Internal: If true, the template was preprocessed before being run through Prettier. Since 0.1.0.',
46-
};
47-
4840
export const options: SupportOptions = {
4941
templateExportDefault,
5042
templateSingleQuote,
51-
__inputWasPreprocessed,
5243
};

src/parse/index.ts

Lines changed: 27 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,12 @@
1-
import type { NodePath } from '@babel/core';
21
import { traverse } from '@babel/core';
3-
import type {
4-
BlockStatement,
5-
Node,
6-
ObjectExpression,
7-
StaticBlock,
8-
} from '@babel/types';
2+
import type { Node, ObjectExpression, StaticBlock } from '@babel/types';
93
import type { Parsed as RawGlimmerTemplate } from 'content-tag';
104
import { Preprocessor } from 'content-tag';
115
import type { Parser } from 'prettier';
126
import { parsers as babelParsers } from 'prettier/plugins/babel.js';
137

148
import { PRINTER_NAME } from '../config.js';
159
import type { Options } from '../options.js';
16-
import type { GlimmerTemplate } from '../types/glimmer.js';
17-
import { isDefaultTemplate } from '../types/glimmer.js';
1810
import { assert } from '../utils/index.js';
1911
import { preprocessTemplateRange } from './preprocess.js';
2012

@@ -23,60 +15,53 @@ const p = new Preprocessor();
2315

2416
/** Converts a node into a GlimmerTemplate node */
2517
function convertNode(
26-
path: NodePath,
27-
node: BlockStatement | ObjectExpression | StaticBlock,
18+
node: ObjectExpression | StaticBlock,
2819
rawTemplate: RawGlimmerTemplate,
2920
): void {
30-
const cast = node as unknown as GlimmerTemplate;
31-
// HACK: Changing the node type here isn't recommended by babel
32-
cast.type = 'FunctionDeclaration';
33-
cast.range = [rawTemplate.range.start, rawTemplate.range.end];
34-
cast.start = rawTemplate.range.start;
35-
cast.end = rawTemplate.range.end;
36-
cast.extra = Object.assign(node.extra ?? {}, {
21+
node.extra = Object.assign(node.extra ?? {}, {
3722
isGlimmerTemplate: true as const,
38-
isDefaultTemplate: isDefaultTemplate(path),
3923
template: rawTemplate,
4024
});
4125
}
4226

4327
/** Traverses the AST and replaces the transformed template parts with other AST */
4428
function convertAst(ast: Node, rawTemplates: RawGlimmerTemplate[]): void {
45-
let counter = 0;
29+
const unprocessedTemplates = [...rawTemplates];
4630

4731
traverse(ast, {
4832
enter(path) {
4933
const { node } = path;
50-
if (
51-
node.type === 'ObjectExpression' ||
52-
node.type === 'BlockStatement' ||
53-
node.type === 'StaticBlock'
54-
) {
34+
if (node.type === 'ObjectExpression' || node.type === 'StaticBlock') {
5535
const { range } = node;
5636
assert('expected range', range);
5737
const [start, end] = range;
5838

59-
const rawTemplate = rawTemplates.find(
39+
const templateIndex = unprocessedTemplates.findIndex(
6040
(t) =>
61-
(t.range.start === start && t.range.end === end) ||
62-
(t.range.start === start - 1 && t.range.end === end + 1) ||
63-
(t.range.start === start && t.range.end === end + 1),
41+
(node.type === 'StaticBlock' &&
42+
t.range.start === start &&
43+
t.range.end === end) ||
44+
(node.type === 'ObjectExpression' &&
45+
t.range.start === start - 1 &&
46+
t.range.end === end + 1),
6447
);
48+
const rawTemplate = unprocessedTemplates.splice(templateIndex, 1)[0];
6549

6650
if (!rawTemplate) {
6751
return null;
6852
}
6953

70-
convertNode(path, node, rawTemplate);
71-
72-
counter++;
54+
convertNode(node, rawTemplate);
7355
}
56+
7457
return null;
7558
},
7659
});
7760

78-
if (counter !== rawTemplates.length) {
79-
throw new Error('failed to process all templates');
61+
if (unprocessedTemplates.length > 0) {
62+
throw new Error(
63+
`failed to process all templates, ${unprocessedTemplates.length} remaining`,
64+
);
8065
}
8166
}
8267

@@ -85,26 +70,28 @@ function convertAst(ast: Node, rawTemplates: RawGlimmerTemplate[]): void {
8570
* fixing the offsets and locations of all nodes also calculates the block
8671
* params locations & ranges and adding it to the info
8772
*/
88-
function preprocess(code: string): {
73+
function preprocess(
74+
code: string,
75+
fileName: string,
76+
): {
8977
code: string;
9078
rawTemplates: RawGlimmerTemplate[];
9179
} {
92-
const rawTemplates = p.parse(code);
80+
const rawTemplates = p.parse(code, fileName);
9381

94-
let output = code;
9582
for (const rawTemplate of rawTemplates) {
96-
output = preprocessTemplateRange(rawTemplate, code, output);
83+
code = preprocessTemplateRange(rawTemplate, code);
9784
}
9885

99-
return { rawTemplates, code: output };
86+
return { rawTemplates, code };
10087
}
10188

10289
export const parser: Parser<Node | undefined> = {
10390
...typescript,
10491
astFormat: PRINTER_NAME,
10592

10693
async parse(code: string, options: Options): Promise<Node> {
107-
const preprocessed = preprocess(code);
94+
const preprocessed = preprocess(code, options.filepath);
10895
const ast = await typescript.parse(preprocessed.code, options);
10996
assert('expected ast', ast);
11097
convertAst(ast, preprocessed.rawTemplates);

src/parse/preprocess.ts

Lines changed: 68 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,65 @@
11
import type { Parsed as RawGlimmerTemplate } from 'content-tag';
22

3-
function replaceRange(
3+
const EMPTY_SPACE = ' ';
4+
5+
/**
6+
* Given a string (`original`), replaces the bytes in the given `range` with
7+
* equivalent bytes of empty space (' ') surrounded by the given prefix and
8+
* suffix. The total byte length will not change.
9+
*
10+
* Returns the resulting string.
11+
*/
12+
function replaceByteRange(
413
original: string,
514
range: { start: number; end: number },
6-
substitute: string,
15+
options: { prefix: string; suffix: string },
716
): string {
8-
return (
9-
original.slice(0, range.start) + substitute + original.slice(range.end)
10-
);
17+
// Convert the original string and the prefix, suffix to buffers for byte manipulation
18+
const originalBuffer = Buffer.from(original);
19+
const prefixBuffer = Buffer.from(options.prefix);
20+
const suffixBuffer = Buffer.from(options.suffix);
21+
22+
// Validate range
23+
if (
24+
range.start < 0 ||
25+
range.end > originalBuffer.length ||
26+
range.start > range.end ||
27+
prefixBuffer.length + suffixBuffer.length > range.end - range.start
28+
) {
29+
throw new Error(
30+
`Invalid byte range:\n\tstart=${range.start}\n\tend=${range.end}\n\tprefix=${options.prefix}\n\tsuffix=${options.suffix}\n\tstring=\n\t${original}`,
31+
);
32+
}
33+
34+
// Adjust the space length to account for the prefix and suffix lengths
35+
const totalReplacementLength = range.end - range.start;
36+
const spaceLength =
37+
totalReplacementLength - prefixBuffer.length - suffixBuffer.length;
38+
39+
// Create a buffer for the replacement
40+
const spaceBuffer = Buffer.alloc(spaceLength, EMPTY_SPACE);
41+
42+
// Concatenate prefix, space, and suffix buffers
43+
const replacementBuffer = Buffer.concat([
44+
prefixBuffer,
45+
spaceBuffer,
46+
suffixBuffer,
47+
]);
48+
49+
// Create buffers for before and after the range using subarray
50+
const beforeRange = originalBuffer.subarray(0, range.start);
51+
const afterRange = originalBuffer.subarray(range.end);
52+
53+
// Concatenate all parts and convert back to a string
54+
const result = Buffer.concat([beforeRange, replacementBuffer, afterRange]);
55+
56+
if (result.length !== originalBuffer.length) {
57+
throw new Error(
58+
`Result length (${result.length}) does not match original length (${originalBuffer.length})`,
59+
);
60+
}
61+
62+
return result.toString('utf8');
1163
}
1264

1365
/**
@@ -16,34 +68,23 @@ function replaceRange(
1668
*/
1769
export function preprocessTemplateRange(
1870
rawTemplate: RawGlimmerTemplate,
19-
originalCode: string,
20-
currentCode: string,
71+
code: string,
2172
): string {
2273
let prefix: string;
2374
let suffix: string;
2475

2576
if (rawTemplate.type === 'class-member') {
26-
prefix = 'static{`';
27-
suffix = '`}';
28-
} else {
29-
prefix = '{';
77+
// Replace with StaticBlock
78+
prefix = 'static{';
3079
suffix = '}';
31-
const nextWord = originalCode.slice(rawTemplate.range.end).match(/\S+/);
32-
if (nextWord && nextWord[0] === 'as') {
33-
prefix = '(' + prefix;
34-
suffix = suffix + ')';
35-
} else if (!nextWord || ![',', ')'].includes(nextWord[0][0] || '')) {
36-
suffix += ';';
37-
}
80+
} else {
81+
// Replace with ObjectExpression
82+
prefix = '({';
83+
suffix = '})';
3884
}
3985

40-
const totalLength = rawTemplate.range.end - rawTemplate.range.start;
41-
const placeholderLength = totalLength - prefix.length - suffix.length;
42-
const placeholder = ' '.repeat(placeholderLength);
43-
44-
return replaceRange(
45-
currentCode,
46-
rawTemplate.range,
47-
`${prefix}${placeholder}${suffix}`,
48-
);
86+
return replaceByteRange(code, rawTemplate.range, {
87+
prefix,
88+
suffix,
89+
});
4990
}

src/print/ambiguity.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,10 @@ import type { AstPath, doc, Printer } from 'prettier';
33
import { printers as estreePrinters } from 'prettier/plugins/estree.js';
44

55
import type { Options } from '../options.js';
6+
import { flattenDoc } from '../utils/doc.js';
67

78
const estreePrinter = estreePrinters['estree'] as Printer<Node | undefined>;
89

9-
/** NOTE: This is highly specialized for use in `fixPreviousPrint` */
10-
function flattenDoc(doc: doc.builders.Doc): string[] {
11-
if (Array.isArray(doc)) {
12-
return doc.flatMap(flattenDoc);
13-
} else if (typeof doc === 'string') {
14-
return [doc];
15-
} else if ('contents' in doc) {
16-
return flattenDoc(doc.contents);
17-
} else {
18-
return [];
19-
}
20-
}
21-
2210
/**
2311
* Search next non EmptyStatement node and set current print, so we can fix it
2412
* later if its ambiguous

0 commit comments

Comments
 (0)