Skip to content

Commit c0dda51

Browse files
committed
feat(mf2): Add ReservedStatement
1 parent ad608e1 commit c0dda51

File tree

11 files changed

+177
-73
lines changed

11 files changed

+177
-73
lines changed

packages/mf2-fluent/src/message-to-fluent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ function variableRefToFluent(
259259
{ name }: VariableRef
260260
): Fluent.InlineExpression {
261261
const local = ctx.declarations.find(decl => decl.name === name);
262-
return local
262+
return local?.value
263263
? expressionToFluent(ctx, local.value)
264264
: new Fluent.VariableReference(new Fluent.Identifier(name));
265265
}

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

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,9 @@ 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(
13-
decl => {
14-
switch (decl.type) {
15-
case 'input': {
16-
const value = asExpression(decl.value);
17-
if (value.arg?.type === 'variable') {
18-
return {
19-
type: 'input',
20-
name: value.arg.name,
21-
value: value as Model.Expression<Model.VariableRef>
22-
};
23-
}
24-
break;
25-
}
26-
case 'local':
27-
return {
28-
type: 'local',
29-
name: asValue(decl.target).name,
30-
value: asExpression(decl.value)
31-
};
32-
}
33-
const { start, end } = decl.value;
34-
throw new MessageSyntaxError('parse-error', start, end);
35-
}
36-
);
12+
const declarations: Model.Declaration[] = msg.declarations
13+
? msg.declarations.map(asDeclaration)
14+
: [];
3715
if (msg.type === 'select') {
3816
return {
3917
type: 'select',
@@ -55,6 +33,36 @@ export function asDataModel(msg: CST.Message): Model.Message {
5533
}
5634
}
5735

36+
function asDeclaration(decl: CST.Declaration): Model.Declaration {
37+
switch (decl.type) {
38+
case 'input': {
39+
const value = asExpression(decl.value);
40+
if (value.arg?.type !== 'variable') {
41+
const { start, end } = decl.value;
42+
throw new MessageSyntaxError('parse-error', start, end);
43+
}
44+
return {
45+
type: 'input',
46+
name: value.arg.name,
47+
value: value as Model.Expression<Model.VariableRef>
48+
};
49+
}
50+
case 'local':
51+
return {
52+
type: 'local',
53+
name: asValue(decl.target).name,
54+
value: asExpression(decl.value)
55+
};
56+
default:
57+
return {
58+
type: 'unsupported-statement',
59+
keyword: (decl.keyword?.value ?? '').substring(1),
60+
body: decl.body?.value || undefined,
61+
expressions: decl.values?.map(asExpression) ?? []
62+
};
63+
}
64+
}
65+
5866
function asPattern(cst: CST.Pattern): Model.Pattern {
5967
const body: Model.Pattern['body'] = cst.body.map(el =>
6068
el.type === 'text' ? el.value : asExpression(el)
@@ -72,7 +80,7 @@ function asExpression(cst: CST.Expression | CST.Junk): Model.Expression {
7280
return { type: 'expression', arg: asValue(cst.body) };
7381
case 'function':
7482
return asFunctionExpression(cst.body);
75-
case 'reserved':
83+
case 'reserved-annotation':
7684
return asUnsupportedExpression(cst.body);
7785
default:
7886
throw new MessageSyntaxError('parse-error', cst.start, cst.end);
@@ -96,11 +104,13 @@ function asFunctionExpression(cst: CST.FunctionRef): Model.Expression {
96104
: { type: 'expression', annotation };
97105
}
98106

99-
function asUnsupportedExpression(cst: CST.Reserved): Model.Expression {
107+
function asUnsupportedExpression(
108+
cst: CST.ReservedAnnotation
109+
): Model.Expression {
100110
const annotation: Model.UnsupportedAnnotation = {
101111
type: 'unsupported-annotation',
102112
sigil: cst.sigil,
103-
source: cst.source
113+
source: cst.source.value
104114
};
105115
return cst.operand
106116
? { type: 'expression', arg: asValue(cst.operand), annotation }

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

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ export interface SelectMessage {
3030
}
3131

3232
/** @beta */
33-
export type Declaration = InputDeclaration | LocalDeclaration;
33+
export type Declaration =
34+
| InputDeclaration
35+
| LocalDeclaration
36+
| ReservedStatement;
3437

3538
/** @beta */
3639
export interface InputDeclaration {
@@ -52,6 +55,16 @@ export interface LocalDeclaration {
5255
value: Expression | Junk;
5356
}
5457

58+
/** @beta */
59+
export interface ReservedStatement {
60+
type: 'reserved-statement';
61+
start: number;
62+
end: number;
63+
keyword: Syntax<string>;
64+
body: Syntax<string>;
65+
values: Expression[];
66+
}
67+
5568
/** @beta */
5669
export interface Variant {
5770
start: number;
@@ -91,7 +104,7 @@ export interface Expression {
91104
start: number;
92105
/** position one past the `}` */
93106
end: number;
94-
body: Literal | VariableRef | FunctionRef | Reserved | Junk;
107+
body: Literal | VariableRef | FunctionRef | ReservedAnnotation | Junk;
95108
}
96109

97110
/** @beta */
@@ -136,11 +149,11 @@ export interface FunctionRef {
136149
}
137150

138151
/** @beta */
139-
export interface Reserved {
140-
type: 'reserved';
152+
export interface ReservedAnnotation {
153+
type: 'reserved-annotation';
141154
sigil: '!' | '@' | '#' | '%' | '^' | '&' | '*' | '<' | '>' | '?' | '~';
142155
operand: Literal | VariableRef | undefined;
143-
source: string;
156+
source: Syntax<string>;
144157
/** position of the sigil, so `operand.start` may be earlier */
145158
start: number;
146159
end: number;

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

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type * as CST from './cst-types.js';
22
import type { ParseContext } from './message.js';
3-
import { parseExpression } from './expression.js';
3+
import { parseExpression, parseReservedBody } from './expression.js';
44
import { whitespaces } from './util.js';
55
import { parseVariable } from './values.js';
6+
import { parseNameValue } from './names.js';
67

78
export function parseDeclarations(ctx: ParseContext): {
89
declarations: CST.Declaration[];
@@ -11,13 +12,22 @@ export function parseDeclarations(ctx: ParseContext): {
1112
const { source } = ctx;
1213
let pos = whitespaces(source, 0);
1314
const declarations: CST.Declaration[] = [];
14-
while (pos < source.length) {
15-
const decl = source.startsWith('.input', pos)
16-
? parseInputDeclaration(ctx, pos)
17-
: source.startsWith('.local', pos)
18-
? parseLocalDeclaration(ctx, pos)
19-
: null;
20-
if (!decl) break;
15+
loop: while (source[pos] === '.') {
16+
const keyword = parseNameValue(source, pos + 1);
17+
let decl;
18+
switch (keyword) {
19+
case '':
20+
case 'match':
21+
break loop;
22+
case 'input':
23+
decl = parseInputDeclaration(ctx, pos);
24+
break;
25+
case 'local':
26+
decl = parseLocalDeclaration(ctx, pos);
27+
break;
28+
default:
29+
decl = parseReservedStatement(ctx, pos, '.' + keyword);
30+
}
2131
declarations.push(decl);
2232
pos = decl.end;
2333
pos += whitespaces(source, pos);
@@ -29,7 +39,7 @@ export function parseDeclarations(ctx: ParseContext): {
2939
function parseInputDeclaration(
3040
ctx: ParseContext,
3141
start: number
32-
): CST.Declaration {
42+
): CST.InputDeclaration {
3343
//
3444
let pos = start + 6; // '.input'
3545
const keyword: CST.Syntax<'.input'> = { start, end: pos, value: '.input' };
@@ -105,6 +115,36 @@ function parseLocalDeclaration(
105115
};
106116
}
107117

118+
function parseReservedStatement(
119+
ctx: ParseContext,
120+
start: number,
121+
keyword: string
122+
): CST.ReservedStatement {
123+
let pos = start + keyword.length;
124+
pos += whitespaces(ctx.source, pos);
125+
126+
const body = parseReservedBody(ctx, pos);
127+
let end = body.end;
128+
pos = end + whitespaces(ctx.source, end);
129+
130+
const values: CST.Expression[] = [];
131+
while (ctx.source[pos] === '{') {
132+
const value = parseExpression(ctx, pos);
133+
end = value.end;
134+
pos = end + whitespaces(ctx.source, end);
135+
}
136+
if (values.length === 0) ctx.onError('missing-syntax', end, '{');
137+
138+
return {
139+
type: 'reserved-statement',
140+
start,
141+
end,
142+
keyword: { start, end: keyword.length, value: keyword },
143+
body,
144+
values
145+
};
146+
}
147+
108148
function parseDeclarationValue(
109149
ctx: ParseContext,
110150
start: number
@@ -130,7 +170,7 @@ function checkDeclarations(ctx: ParseContext, declarations: CST.Declaration[]) {
130170
}
131171
};
132172

133-
for (const decl of declarations) {
173+
loop: for (const decl of declarations) {
134174
let target: CST.VariableRef | undefined;
135175
switch (decl.type) {
136176
case 'input':
@@ -148,6 +188,8 @@ function checkDeclarations(ctx: ParseContext, declarations: CST.Declaration[]) {
148188
case 'local':
149189
if (decl.target.type === 'variable') target = decl.target;
150190
break;
191+
default:
192+
continue loop;
151193
}
152194
if (target) {
153195
if (targets.has(target.name)) {

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

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function parseExpression(
2323
| CST.Literal
2424
| CST.VariableRef
2525
| CST.FunctionRef
26-
| CST.Reserved
26+
| CST.ReservedAnnotation
2727
| CST.Junk;
2828
let junkError: MessageSyntaxError | undefined;
2929
pos += whitespaces(ctx.source, pos);
@@ -45,7 +45,7 @@ export function parseExpression(
4545
case '>':
4646
case '?':
4747
case '~':
48-
body = parseReserved(ctx, pos, arg);
48+
body = parseReservedAnnotation(ctx, pos, arg);
4949
pos = body.end;
5050
break;
5151
default:
@@ -131,13 +131,28 @@ function parseOption(ctx: ParseContext, start: number): CST.Option {
131131
// / %x5D-7A ; omit { | }
132132
// / %x7E-D7FF ; omit surrogates
133133
// / %xE000-10FFFF
134-
function parseReserved(
134+
function parseReservedAnnotation(
135135
ctx: ParseContext,
136136
start: number,
137137
operand: CST.Literal | CST.VariableRef | undefined
138-
): CST.Reserved {
139-
const sigil = ctx.source[start] as CST.Reserved['sigil'];
140-
let pos = start + 1; // sigil
138+
): CST.ReservedAnnotation {
139+
const sigil = ctx.source[start] as CST.ReservedAnnotation['sigil'];
140+
const source = parseReservedBody(ctx, start + 1); // skip sigil
141+
return {
142+
type: 'reserved-annotation',
143+
start,
144+
end: source.end,
145+
operand,
146+
sigil,
147+
source
148+
};
149+
}
150+
151+
export function parseReservedBody(
152+
ctx: ParseContext,
153+
start: number
154+
): CST.Syntax<string> {
155+
let pos = start;
141156
loop: while (pos < ctx.source.length) {
142157
const ch = ctx.source[pos];
143158
switch (ch) {
@@ -174,6 +189,5 @@ function parseReserved(
174189
pos -= 1;
175190
prev = ctx.source[pos - 1];
176191
}
177-
const source = ctx.source.substring(start, pos);
178-
return { type: 'reserved', operand, sigil, source, start, end: pos };
192+
return { start, end: pos, value: ctx.source.substring(start, pos) };
179193
}

packages/mf2-messageformat/src/data-model.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ export interface PatternMessage {
3535
*
3636
* @beta
3737
*/
38-
export type Declaration = InputDeclaration | LocalDeclaration;
38+
export type Declaration =
39+
| InputDeclaration
40+
| LocalDeclaration
41+
| UnsupportedStatement;
3942

4043
export interface InputDeclaration {
4144
type: 'input';
@@ -49,6 +52,15 @@ export interface LocalDeclaration {
4952
value: Expression;
5053
}
5154

55+
export interface UnsupportedStatement {
56+
type: 'unsupported-statement';
57+
keyword: string;
58+
name?: never;
59+
value?: never;
60+
body?: string;
61+
expressions: Expression[];
62+
}
63+
5264
/**
5365
* The body of each message is composed of a sequence of parts, some of them
5466
* fixed (Text), others placeholders for values depending on additional

packages/mf2-messageformat/src/errors.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ export class MessageResolutionError extends MessageError {
8383
| 'bad-input'
8484
| 'bad-option'
8585
| 'unresolved-var'
86-
| 'unsupported-annotation';
86+
| 'unsupported-annotation'
87+
| 'unsupported-statement';
8788
source: string;
8889
constructor(
8990
type: typeof MessageResolutionError.prototype.type,

packages/mf2-messageformat/src/expression/unsupported-annotation.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,7 @@ describe('Reserved syntax', () => {
3333
const msg = resolve('{ # one\ntwo\rthree four }', {}, [
3434
{ type: 'unsupported-annotation' }
3535
]);
36-
expect(msg).toMatchObject([
37-
{ type: 'fallback', source: '# one\ntwo\rthree four' }
38-
]);
36+
expect(msg).toMatchObject([{ type: 'fallback', source: '#' }]);
3937
});
4038

4139
test('surrogates', () => {

packages/mf2-messageformat/src/expression/unsupported-annotation.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,9 @@ export const isUnsupportedAnnotation = (
3636
export function resolveUnsupportedAnnotation(
3737
ctx: Context,
3838
operand: Literal | VariableRef | undefined,
39-
{ sigil, source = '�' }: UnsupportedAnnotation
39+
{ sigil = '�' }: UnsupportedAnnotation
4040
) {
41-
const msg = `Reserved ${sigil ?? '�'} annotation is not supported`;
42-
ctx.onError(
43-
new MessageResolutionError('unsupported-annotation', msg, source)
44-
);
45-
return fallback(getValueSource(operand) ?? source);
41+
const msg = `Reserved ${sigil} annotation is not supported`;
42+
ctx.onError(new MessageResolutionError('unsupported-annotation', msg, sigil));
43+
return fallback(getValueSource(operand) ?? sigil);
4644
}

0 commit comments

Comments
 (0)