Skip to content

Commit c4e8261

Browse files
committed
feat(mf2): Support {#markup}, {/markup}, and {#markup /}
1 parent 1d9e73f commit c4e8261

26 files changed

+609
-371
lines changed

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,14 @@ function asExpression(exp: Fluent.Expression): Expression {
6767
return {
6868
type: 'expression',
6969
arg: asValue(exp),
70-
annotation: { type: 'function', kind: 'value', name: 'number' }
70+
annotation: { type: 'function', name: 'number' }
7171
};
7272
case 'StringLiteral':
7373
case 'VariableReference':
7474
return { type: 'expression', arg: asValue(exp) };
7575
case 'FunctionReference': {
7676
const annotation: FunctionAnnotation = {
7777
type: 'function',
78-
kind: 'value',
7978
name: exp.id.name.toLowerCase()
8079
};
8180
const { positional, named } = exp.arguments;
@@ -105,13 +104,12 @@ function asExpression(exp: Fluent.Expression): Expression {
105104
return {
106105
type: 'expression',
107106
arg: { type: 'literal', value: msgId },
108-
annotation: { type: 'function', kind: 'value', name: 'message' }
107+
annotation: { type: 'function', name: 'message' }
109108
};
110109
}
111110
case 'TermReference': {
112111
const annotation: FunctionAnnotation = {
113112
type: 'function',
114-
kind: 'value',
115113
name: 'message'
116114
};
117115
const msgId = exp.attribute

packages/mf2-fluent/src/fluent.test.ts

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -738,15 +738,12 @@ describe('messagetoFluent', () => {
738738
type: 'select',
739739
declarations: [
740740
{
741+
type: 'local',
741742
name: 'local',
742743
value: {
743744
type: 'expression',
744745
arg: { type: 'variable', name: 'num' },
745-
annotation: {
746-
type: 'function',
747-
name: 'number',
748-
kind: 'value'
749-
}
746+
annotation: { type: 'function', name: 'number' }
750747
}
751748
}
752749
],
@@ -804,6 +801,7 @@ describe('messagetoFluent', () => {
804801
type: 'message',
805802
declarations: [
806803
{
804+
type: 'local',
807805
name: 'local',
808806
value: {
809807
type: 'expression',
@@ -816,20 +814,12 @@ describe('messagetoFluent', () => {
816814
{
817815
type: 'expression',
818816
arg: { type: 'literal', value: 'msg' },
819-
annotation: {
820-
type: 'function',
821-
name: 'message',
822-
kind: 'value'
823-
}
817+
annotation: { type: 'function', name: 'message' }
824818
},
825819
{
826820
type: 'expression',
827821
arg: { type: 'variable', name: 'local' },
828-
annotation: {
829-
type: 'function',
830-
name: 'message',
831-
kind: 'value'
832-
}
822+
annotation: { type: 'function', name: 'message' }
833823
}
834824
]
835825
}
@@ -868,11 +858,7 @@ describe('messagetoFluent', () => {
868858
{
869859
type: 'expression',
870860
arg: { type: 'variable', name: 'input' },
871-
annotation: {
872-
type: 'function',
873-
name: 'message',
874-
kind: 'value'
875-
}
861+
annotation: { type: 'function', name: 'message' }
876862
}
877863
]
878864
}

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,13 @@ function keysMatch(a: (Literal | CatchallKey)[], b: (Literal | CatchallKey)[]) {
146146
}
147147

148148
function patternToFluent(ctx: MsgContext, pattern: Pattern) {
149-
const elements = pattern.body.map(el =>
150-
typeof el === 'string'
151-
? new Fluent.TextElement(el)
152-
: new Fluent.Placeable(expressionToFluent(ctx, el))
153-
);
149+
const elements = pattern.body.map(el => {
150+
if (typeof el === 'string') return new Fluent.TextElement(el);
151+
if (el.type === 'expression') {
152+
return new Fluent.Placeable(expressionToFluent(ctx, el));
153+
}
154+
throw new Error(`Conversion of ${el.type} to Fluent is not supported`);
155+
});
154156
return new Fluent.Pattern(elements);
155157
}
156158

@@ -230,7 +232,7 @@ function expressionToFluent(
230232
return functionRefToFluent(ctx, fluentArg, annotation);
231233
} else {
232234
throw new Error(
233-
`Conversion of "${annotation.type}" expression to Fluent is not supported`
235+
`Conversion of ${annotation.type} annotation to Fluent is not supported`
234236
);
235237
}
236238
}

packages/mf2-icu-mf1/src/mf1-to-message-data.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ function tokenToPart(
6666
case 'function': {
6767
const annotation: FunctionAnnotation = {
6868
type: 'function',
69-
kind: 'value',
7069
name: token.key
7170
};
7271
if (token.param && token.param.length > 0) {
@@ -89,7 +88,6 @@ function tokenToPart(
8988
if (!pluralArg) return '#';
9089
const annotation: FunctionAnnotation = {
9190
type: 'function',
92-
kind: 'value',
9391
name: 'number'
9492
};
9593
if (pluralOffset)
@@ -121,7 +119,7 @@ function argToExpression({
121119
return {
122120
type: 'expression',
123121
arg,
124-
annotation: { type: 'function', kind: 'value', name: 'string' }
122+
annotation: { type: 'function', name: 'string' }
125123
};
126124

127125
const options: Option[] = [];
@@ -138,11 +136,7 @@ function argToExpression({
138136
});
139137
}
140138

141-
const annotation: FunctionAnnotation = {
142-
type: 'function',
143-
kind: 'value',
144-
name: 'number'
145-
};
139+
const annotation: FunctionAnnotation = { type: 'function', name: 'number' };
146140
if (options.length) annotation.options = options;
147141

148142
return { type: 'expression', arg, annotation };

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

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[
22
{ "src": "hello", "exp": "hello" },
3+
{ "src": "hello {world}", "exp": "hello world" },
34
{ "src": "hello {|world|}", "exp": "hello world" },
45
{ "src": "hello {||}", "exp": "hello " },
56
{
@@ -248,71 +249,63 @@
248249
"errors": [{ "type": "unresolved-var" }]
249250
},
250251
{
251-
"src": "{+tag}",
252+
"src": "{#tag}",
252253
"exp": "",
253-
"parts": [{ "type": "open", "name": "tag" }]
254+
"parts": [{ "type": "markup", "kind": "open", "name": "tag" }]
254255
},
255256
{
256-
"src": "{+tag}content",
257+
"src": "{#tag}content",
257258
"exp": "content",
258259
"parts": [
259-
{ "type": "open", "name": "tag" },
260+
{ "type": "markup", "kind": "open", "name": "tag" },
260261
{ "type": "literal", "value": "content" }
261262
]
262263
},
263264
{
264-
"src": "{+tag}content{-tag}",
265+
"src": "{#tag}content{/tag}",
265266
"exp": "content",
266267
"parts": [
267-
{ "type": "open", "name": "tag" },
268+
{ "type": "markup", "kind": "open", "name": "tag" },
268269
{ "type": "literal", "value": "content" },
269-
{ "type": "close", "name": "tag" }
270+
{ "type": "markup", "kind": "close", "name": "tag" }
270271
]
271272
},
272273
{
273-
"src": "{-tag}content",
274+
"src": "{/tag}content",
274275
"exp": "content",
275276
"parts": [
276-
{ "type": "close", "name": "tag" },
277+
{ "type": "markup", "kind": "close", "name": "tag" },
277278
{ "type": "literal", "value": "content" }
278279
]
279280
},
280281
{
281-
"src": "{+tag foo=bar}",
282+
"src": "{#tag foo=bar}",
282283
"exp": "",
283-
"parts": [{ "type": "open", "name": "tag", "options": { "foo": "bar" } }]
284+
"parts": [
285+
{
286+
"type": "markup",
287+
"kind": "open",
288+
"name": "tag",
289+
"options": { "foo": "bar" }
290+
}
291+
]
284292
},
285293
{
286-
"src": "{+tag foo=|foo| bar=$bar}",
294+
"src": "{#tag foo=|foo| bar=$bar}",
287295
"params": { "bar": "b a r" },
288296
"exp": "",
289297
"parts": [
290298
{
291-
"type": "open",
299+
"type": "markup",
300+
"kind": "open",
292301
"name": "tag",
293302
"options": { "foo": "foo", "bar": "b a r" }
294303
}
295304
]
296305
},
297-
{
298-
"src": "{|foo| +markup}",
299-
"exp": "",
300-
"parts": [{ "type": "open", "name": "markup", "value": "foo" }]
301-
},
302-
{
303-
"src": "{-tag foo=bar}",
304-
"exp": "",
305-
"parts": [{ "type": "close", "name": "tag", "options": { "foo": "bar" } }]
306-
},
307-
{
308-
"src": "unquoted {literal}",
309-
"exp": "unquoted literal"
310-
},
311-
{
312-
"src": ".match {+foo} * {{foo}}",
313-
"exp": "foo",
314-
"errors": [{ "type": "not-selectable" }]
315-
},
306+
{ "src": "{|foo| #markup}", "syntaxError": true },
307+
{ "src": "{/tag foo=bar}", "syntaxError": true },
308+
{ "src": ".match {#foo} * {{foo}}", "syntaxError": true },
316309
{ "src": "{{missing end brace}", "syntaxError": true },
317310
{ "src": "{{missing end braces", "syntaxError": true },
318311
{ "src": "{{missing end {$braces", "syntaxError": true },

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

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function asDataModel(msg: CST.Message): Model.Message {
1616
return {
1717
type: 'select',
1818
declarations,
19-
selectors: msg.selectors.map(asExpression),
19+
selectors: msg.selectors.map(sel => asExpression(sel, false)),
2020
variants: msg.variants.map(cst => ({
2121
keys: cst.keys.map(key =>
2222
key.type === '*' ? { type: '*' } : asValue(key)
@@ -36,7 +36,7 @@ export function asDataModel(msg: CST.Message): Model.Message {
3636
function asDeclaration(decl: CST.Declaration): Model.Declaration {
3737
switch (decl.type) {
3838
case 'input': {
39-
const value = asExpression(decl.value);
39+
const value = asExpression(decl.value, false);
4040
if (value.arg?.type !== 'variable') {
4141
const { start, end } = decl.value;
4242
throw new MessageSyntaxError('parse-error', start, end);
@@ -51,38 +51,69 @@ function asDeclaration(decl: CST.Declaration): Model.Declaration {
5151
return {
5252
type: 'local',
5353
name: asValue(decl.target).name,
54-
value: asExpression(decl.value)
54+
value: asExpression(decl.value, false)
5555
};
5656
default:
5757
return {
5858
type: 'unsupported-statement',
5959
keyword: (decl.keyword?.value ?? '').substring(1),
6060
body: decl.body?.value || undefined,
61-
expressions: decl.values?.map(asExpression) ?? []
61+
expressions: decl.values?.map(dv => asExpression(dv, true)) ?? []
6262
};
6363
}
6464
}
6565

6666
function asPattern(cst: CST.Pattern): Model.Pattern {
6767
const body: Model.Pattern['body'] = cst.body.map(el =>
68-
el.type === 'text' ? el.value : asExpression(el)
68+
el.type === 'text' ? el.value : asExpression(el, true)
6969
);
7070
return { body };
7171
}
7272

73-
function asExpression(cst: CST.Expression | CST.Junk): Model.Expression {
73+
function asExpression(
74+
cst: CST.Expression | CST.Junk,
75+
allowMarkup: false
76+
): Model.Expression;
77+
function asExpression(
78+
cst: CST.Expression | CST.Junk,
79+
allowMarkup: true
80+
): Model.Expression | Model.Markup;
81+
function asExpression(
82+
cst: CST.Expression | CST.Junk,
83+
allowMarkup: boolean
84+
): Model.Expression | Model.Markup {
7485
if (cst.type === 'expression') {
86+
if (allowMarkup && cst.markup) {
87+
const cm = cst.markup;
88+
if (cm.type === 'markup-close') {
89+
return { type: 'markup', kind: 'close', name: cm.name };
90+
}
91+
const markup: Model.MarkupOpen | Model.MarkupStandalone = {
92+
type: 'markup',
93+
kind: cm.close ? 'standalone' : 'open',
94+
name: cm.name
95+
};
96+
if (cm.options.length > 0) {
97+
markup.options = cm.options.map(opt => ({
98+
name: opt.name,
99+
value: asValue(opt.value)
100+
}));
101+
}
102+
return markup;
103+
}
104+
75105
const arg = cst.arg ? asValue(cst.arg) : undefined;
76106
let annotation:
77107
| Model.FunctionAnnotation
78108
| Model.UnsupportedAnnotation
79109
| undefined;
110+
80111
const ca = cst.annotation;
81112
if (ca) {
82113
switch (ca.type) {
83114
case 'function':
84-
annotation = { type: 'function', kind: ca.kind, name: ca.name };
85-
if (ca.options && ca.options.length > 0) {
115+
annotation = { type: 'function', name: ca.name };
116+
if (ca.options.length > 0) {
86117
annotation.options = ca.options.map(opt => ({
87118
name: opt.name,
88119
value: asValue(opt.value)

0 commit comments

Comments
 (0)