Skip to content

Commit ac2cc2f

Browse files
committed
feat(mf2): Update syntax to .let & .match, with {{doubled pattern braces}}
1 parent c98b911 commit ac2cc2f

File tree

16 files changed

+281
-256
lines changed

16 files changed

+281
-256
lines changed

packages/mf2-messageformat/src/__fixtures/test-messages.json

Lines changed: 83 additions & 84 deletions
Large diffs are not rendered by default.

packages/mf2-messageformat/src/cst-parser/as-data-model.ts

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,30 @@ import type * as CST from './cst-types.js';
99
*/
1010
export function asDataModel(msg: CST.Message): Model.Message {
1111
for (const error of msg.errors) throw error;
12-
const declarations: Model.Declaration[] = msg.declarations.map(decl => ({
13-
name: asValue(decl.target).name,
14-
value: asExpression(decl.value)
15-
}));
16-
switch (msg.type) {
17-
case 'message':
18-
return {
19-
type: 'message',
20-
declarations,
21-
pattern: asPattern(msg.pattern)
22-
};
23-
case 'select':
24-
return {
25-
type: 'select',
26-
declarations,
27-
selectors: msg.selectors.map(asExpression),
28-
variants: msg.variants.map(cst => ({
29-
keys: cst.keys.map(key =>
30-
key.type === '*' ? { type: '*' } : asValue(key)
31-
),
32-
value: asPattern(cst.value)
33-
}))
34-
};
35-
default:
36-
throw new MessageSyntaxError('parse-error', 0, msg.source?.length ?? 0);
12+
const declarations: Model.Declaration[] = (msg.declarations ?? []).map(
13+
decl => ({
14+
name: asValue(decl.target).name,
15+
value: asExpression(decl.value)
16+
})
17+
);
18+
if (msg.type === 'select') {
19+
return {
20+
type: 'select',
21+
declarations,
22+
selectors: msg.selectors.map(asExpression),
23+
variants: msg.variants.map(cst => ({
24+
keys: cst.keys.map(key =>
25+
key.type === '*' ? { type: '*' } : asValue(key)
26+
),
27+
value: asPattern(cst.value)
28+
}))
29+
};
30+
} else {
31+
return {
32+
type: 'message',
33+
declarations,
34+
pattern: asPattern(msg.pattern)
35+
};
3736
}
3837
}
3938

packages/mf2-messageformat/src/cst-parser/cst-parser.test.ts

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,21 @@ import { parseMessage } from './index.js';
33

44
describe('messages in resources', () => {
55
test('text character escapes', () => {
6-
const src = '{\\\t\\ \\n\\r\\t\\x01\\u0002\\U000003}';
6+
const src = '\\\t\\ \\n\\r\\t\\x01\\u0002\\U000003';
77
const noRes = parseMessage(src, { resource: false });
88
expect(noRes.errors).toHaveLength(8);
99
const msg = parseMessage(src, { resource: true });
1010
expect(msg).toMatchObject<CST.Message>({
11-
type: 'message',
12-
declarations: [],
11+
type: 'simple',
1312
errors: [],
1413
pattern: {
1514
start: 0,
1615
end: src.length,
1716
body: [
1817
{
1918
type: 'text',
20-
start: 1,
21-
end: src.length - 1,
19+
start: 0,
20+
end: src.length,
2221
value: '\t \n\r\t\x01\x02\x03'
2322
}
2423
]
@@ -27,27 +26,26 @@ describe('messages in resources', () => {
2726
});
2827

2928
test('quoted literal character escapes', () => {
30-
const src = '{{|\\\t\\ \\n\\r\\t\\x01\\u0002\\U000003|}}';
29+
const src = '{|\\\t\\ \\n\\r\\t\\x01\\u0002\\U000003|}';
3130
const noRes = parseMessage(src, { resource: false });
3231
expect(noRes.errors).toHaveLength(8);
3332
const msg = parseMessage(src, { resource: true });
3433
expect(msg).toMatchObject<CST.Message>({
35-
type: 'message',
36-
declarations: [],
34+
type: 'simple',
3735
errors: [],
3836
pattern: {
3937
start: 0,
4038
end: src.length,
4139
body: [
4240
{
4341
type: 'expression',
44-
start: 1,
45-
end: src.length - 1,
42+
start: 0,
43+
end: src.length,
4644
body: {
4745
type: 'literal',
4846
quoted: true,
49-
start: 2,
50-
end: src.length - 2,
47+
start: 1,
48+
end: src.length - 1,
5149
value: '\t \n\r\t\x01\x02\x03'
5250
}
5351
}
@@ -56,24 +54,46 @@ describe('messages in resources', () => {
5654
});
5755
});
5856

57+
test('complex pattern with leading .', () => {
58+
const src = '{{.let}}';
59+
const msg = parseMessage(src, { resource: false });
60+
expect(msg).toMatchObject<CST.Message>({
61+
type: 'complex',
62+
errors: [],
63+
declarations: [],
64+
pattern: {
65+
quotes: [
66+
{ start: 0, end: 2, value: '{{' },
67+
{ start: 6, end: 8, value: '}}' }
68+
],
69+
start: 0,
70+
end: src.length,
71+
body: [{ type: 'text', start: 2, end: src.length - 2, value: '.let' }]
72+
}
73+
});
74+
});
75+
5976
test('newlines in text', () => {
60-
const src = '{1\n \t2 \n \\ 3\n\\t}';
77+
const src = '1\n \t2 \n \\ 3\n\\t';
6178
const noRes = parseMessage(src, { resource: false });
6279
expect(noRes).toMatchObject({
80+
type: 'simple',
6381
errors: [{ type: 'bad-escape' }, { type: 'bad-escape' }],
6482
pattern: { body: [{ type: 'text', value: '1\n \t2 \n \\ 3\n\\t' }] }
6583
});
6684
const msg = parseMessage(src, { resource: true });
6785
expect(msg).toMatchObject({
86+
type: 'simple',
6887
errors: [],
6988
pattern: { body: [{ type: 'text', value: '1\n2 \n 3\n\t' }] }
7089
});
7190
});
7291

7392
test('newlines in quoted literal', () => {
74-
const src = '{{|1\n \t2 \n \\ 3\n\\t|}}';
93+
const src = '{|1\n \t2 \n \\ 3\n\\t|}';
7594
const noRes = parseMessage(src, { resource: false });
7695
expect(noRes).toMatchObject({
96+
type: 'simple',
7797
errors: [{ type: 'bad-escape' }, { type: 'bad-escape' }],
7898
pattern: {
7999
body: [
@@ -86,6 +106,7 @@ describe('messages in resources', () => {
86106
});
87107
const msg = parseMessage(src, { resource: true });
88108
expect(msg).toMatchObject({
109+
type: 'simple',
89110
errors: [],
90111
pattern: {
91112
body: [

packages/mf2-messageformat/src/cst-parser/cst-types.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,39 @@
11
import type { MessageSyntaxError } from '../errors';
22

33
/** @beta */
4-
export type Message = PatternMessage | SelectMessage | JunkMessage;
4+
export type Message = SimpleMessage | ComplexMessage | SelectMessage;
55

66
/** @beta */
7-
export interface PatternMessage {
8-
type: 'message';
9-
declarations: Declaration[];
7+
export interface SimpleMessage {
8+
type: 'simple';
9+
declarations?: never;
1010
pattern: Pattern;
1111
errors: MessageSyntaxError[];
1212
}
1313

1414
/** @beta */
15-
export interface SelectMessage {
16-
type: 'select';
15+
export interface ComplexMessage {
16+
type: 'complex';
1717
declarations: Declaration[];
18-
match: Syntax<'match'>;
19-
selectors: Expression[];
20-
variants: Variant[];
18+
pattern: Pattern;
2119
errors: MessageSyntaxError[];
2220
}
2321

2422
/** @beta */
25-
export interface JunkMessage {
26-
type: 'junk';
23+
export interface SelectMessage {
24+
type: 'select';
2725
declarations: Declaration[];
26+
match: Syntax<'.match'>;
27+
selectors: Expression[];
28+
variants: Variant[];
2829
errors: MessageSyntaxError[];
29-
source: string;
3030
}
3131

3232
/** @beta */
3333
export interface Declaration {
3434
start: number;
3535
end: number;
36-
let: Syntax<'let'>;
36+
let: Syntax<'.let'>;
3737
target: VariableRef | Junk;
3838
equals: Syntax<'=' | ''>;
3939
value: Expression | Junk;
@@ -43,7 +43,6 @@ export interface Declaration {
4343
export interface Variant {
4444
start: number;
4545
end: number;
46-
when: Syntax<'when'>;
4746
keys: Array<Literal | CatchallKey>;
4847
value: Pattern;
4948
}
@@ -58,11 +57,10 @@ export interface CatchallKey {
5857

5958
/** @beta */
6059
export interface Pattern {
61-
/** position of the `{` */
6260
start: number;
63-
/** position one past the `}` */
6461
end: number;
6562
body: Array<Text | Expression>;
63+
quotes?: [Syntax<'{{'>] | [Syntax<'{{'>, Syntax<'}}'>];
6664
}
6765

6866
/** @beta */

packages/mf2-messageformat/src/cst-parser/declarations.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export function parseDeclarations(ctx: ParseContext): {
1010
} {
1111
let pos = whitespaces(ctx.source, 0);
1212
const declarations: CST.Declaration[] = [];
13-
while (ctx.source.startsWith('let', pos)) {
13+
while (ctx.source.startsWith('.let', pos)) {
1414
const decl = parseDeclaration(ctx, pos);
1515
declarations.push(decl);
1616
pos = decl.end;
@@ -20,15 +20,13 @@ export function parseDeclarations(ctx: ParseContext): {
2020
return { declarations, end: pos };
2121
}
2222

23-
// declaration = let s variable [s] "=" [s] expression
24-
// let = %x6C.65.74 ; "let"
2523
function parseDeclaration(ctx: ParseContext, start: number): CST.Declaration {
26-
let pos = start + 3; // 'let'
27-
const let_: CST.Syntax<'let'> = { start, end: pos, value: 'let' };
24+
let pos = start + 4; // '.let'
25+
const let_: CST.Syntax<'.let'> = { start, end: pos, value: '.let' };
2826
const ws = whitespaces(ctx.source, pos);
2927
pos += ws;
3028

31-
if (ws === 0) ctx.onError('missing-char', pos, ' ');
29+
if (ws === 0) ctx.onError('missing-syntax', pos, ' ');
3230

3331
let target: CST.VariableRef | CST.Junk;
3432
if (ctx.source[pos] === '$') {
@@ -44,7 +42,7 @@ function parseDeclaration(ctx: ParseContext, start: number): CST.Declaration {
4442
end: pos,
4543
source: ctx.source.substring(junkStart, pos)
4644
};
47-
ctx.onError('missing-char', junkStart, '$');
45+
ctx.onError('missing-syntax', junkStart, '$');
4846
}
4947

5048
pos += whitespaces(ctx.source, pos);
@@ -54,7 +52,7 @@ function parseDeclaration(ctx: ParseContext, start: number): CST.Declaration {
5452
pos += 1;
5553
} else {
5654
equals = { start: pos, end: pos, value: '' };
57-
ctx.onError('missing-char', pos, '=');
55+
ctx.onError('missing-syntax', pos, '=');
5856
}
5957

6058
let value: CST.Expression | CST.Junk;
@@ -74,7 +72,7 @@ function parseDeclaration(ctx: ParseContext, start: number): CST.Declaration {
7472
end: pos,
7573
source: ctx.source.substring(junkStart, pos)
7674
};
77-
ctx.onError('missing-char', junkStart, '{');
75+
ctx.onError('missing-syntax', junkStart, '{');
7876
}
7977

8078
return { start, end: pos, let: let_, target, equals, value };

packages/mf2-messageformat/src/cst-parser/expression.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export function parseExpression(
6161
pos += whitespaces(ctx.source, pos);
6262

6363
if (pos >= ctx.source.length) {
64-
ctx.onError('missing-char', pos, '}');
64+
ctx.onError('missing-syntax', pos, '}');
6565
} else if (ctx.source[pos] !== '}') {
6666
const errStart = pos;
6767
while (pos < ctx.source.length && ctx.source[pos] !== '}') pos += 1;
@@ -95,7 +95,7 @@ function parseFunctionRef(
9595
while (pos < ctx.source.length) {
9696
const ws = whitespaces(ctx.source, pos);
9797
if (ctx.source[pos + ws] === '}') break;
98-
if (ws === 0) ctx.onError('missing-char', pos, ' ');
98+
if (ws === 0) ctx.onError('missing-syntax', pos, ' ');
9999
pos += ws;
100100
const opt = parseOption(ctx, pos);
101101
if (opt.end === pos) break; // error
@@ -112,7 +112,7 @@ function parseOption(ctx: ParseContext, start: number): CST.Option {
112112
let pos = start + name.length;
113113
pos += whitespaces(ctx.source, pos);
114114
if (ctx.source[pos] === '=') pos += 1;
115-
else ctx.onError('missing-char', pos, '=');
115+
else ctx.onError('missing-syntax', pos, '=');
116116
pos += whitespaces(ctx.source, pos);
117117
const value =
118118
ctx.source[pos] === '$'

0 commit comments

Comments
 (0)