Skip to content

Commit e54254f

Browse files
committed
feat(mf2): Allow options on close (unicode-org/message-format-wg#649)
1 parent 86f4046 commit e54254f

File tree

12 files changed

+45
-104
lines changed

12 files changed

+45
-104
lines changed

packages/mf2-messageformat/src/__fixtures/syntax-errors.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
"{{}",
88
"{{}}}",
99
"{|foo| #markup}",
10-
"{/tag foo=bar}",
1110
"{{missing end brace}",
1211
"{{missing end braces",
1312
"{{missing end {$braces",

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,18 @@
140140
}
141141
]
142142
},
143+
{
144+
"src": "{/tag foo=bar}",
145+
"exp": "",
146+
"parts": [
147+
{
148+
"type": "markup",
149+
"kind": "close",
150+
"name": "tag",
151+
"options": { "foo": "bar" }
152+
}
153+
]
154+
},
143155
{
144156
"src": "{42 @foo @bar=13}",
145157
"exp": "42",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ function parseReservedStatement(
123123
while (ctx.source[pos] === '{') {
124124
if (ctx.source.startsWith('{{', pos)) break;
125125
const value = parseExpression(ctx, pos);
126-
values.push(value)
126+
values.push(value);
127127
end = value.end;
128128
pos = end + whitespaces(ctx.source, end);
129129
}

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

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,17 @@ export function parseExpression(
3131
| CST.ReservedAnnotation
3232
| CST.Junk
3333
| undefined;
34-
let markup: CST.Markup | CST.MarkupClose | undefined;
34+
let markup: CST.Markup | undefined;
3535
let junkError: MessageSyntaxError | undefined;
3636
switch (source[pos]) {
3737
case ':':
3838
annotation = parseFunctionRefOrMarkup(ctx, pos, 'function');
3939
pos = annotation.end;
4040
break;
4141
case '#':
42-
if (arg) ctx.onError('extra-content', arg.start, arg.end);
43-
markup = parseFunctionRefOrMarkup(ctx, pos, 'markup');
44-
pos = markup.end;
45-
break;
4642
case '/':
4743
if (arg) ctx.onError('extra-content', arg.start, arg.end);
48-
markup = parseMarkupClose(ctx, pos);
44+
markup = parseFunctionRefOrMarkup(ctx, pos, 'markup');
4945
pos = markup.end;
5046
break;
5147
case '!':
@@ -140,7 +136,7 @@ function parseFunctionRefOrMarkup(
140136
let ws = whitespaces(source, pos);
141137
const next = source[pos + ws];
142138
if (next === '@' || next === '}') break;
143-
if (type === 'markup' && next === '/') {
139+
if (next === '/' && source[start] === '#') {
144140
pos += ws + 1;
145141
close = { start: pos - 1, end: pos, value: '/' };
146142
ws = whitespaces(source, pos);
@@ -158,17 +154,11 @@ function parseFunctionRefOrMarkup(
158154
const open = { start, end: start + 1, value: ':' as const };
159155
return { type, start, end: pos, open, name: id.parts, options };
160156
} else {
161-
const open = { start, end: start + 1, value: '#' as const };
157+
const open = { start, end: start + 1, value: source[start] as '#' | '/' };
162158
return { type, start, end: pos, open, name: id.parts, options, close };
163159
}
164160
}
165161

166-
function parseMarkupClose(ctx: ParseContext, start: number): CST.MarkupClose {
167-
const id = parseIdentifier(ctx, start + 1);
168-
const open = { start, end: start + 1, value: '/' as const };
169-
return { type: 'markup-close', start, end: id.end, open, name: id.parts };
170-
}
171-
172162
function parseOption(ctx: ParseContext, start: number): CST.Option {
173163
const id = parseIdentifier(ctx, start);
174164
let pos = id.end;

packages/mf2-messageformat/src/cst/resource-option.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ describe('messages in resources', () => {
5151
end: src.length - 1,
5252
value: '\t \n\r\t\x01\x02\x03'
5353
},
54-
annotation: undefined
54+
annotation: undefined,
55+
attributes: []
5556
}
5657
]
5758
}

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

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export interface Expression {
105105
braces: [Syntax<'{'>] | [Syntax<'{'>, Syntax<'}'>];
106106
arg?: Literal | VariableRef;
107107
annotation?: FunctionRef | ReservedAnnotation | Junk;
108-
markup?: Markup | MarkupClose;
108+
markup?: Markup;
109109
attributes: Attribute[];
110110
}
111111

@@ -162,21 +162,12 @@ export interface Markup {
162162
type: 'markup';
163163
start: number;
164164
end: number;
165-
open: Syntax<'#'>;
165+
open: Syntax<'#' | '/'>;
166166
name: Identifier;
167167
options: Option[];
168168
close?: Syntax<'/'>;
169169
}
170170

171-
/** @beta */
172-
export interface MarkupClose {
173-
type: 'markup-close';
174-
start: number;
175-
end: number;
176-
open: Syntax<'/'>;
177-
name: Identifier;
178-
}
179-
180171
/** @beta */
181172
export interface Option {
182173
/** position at the start of the name */

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

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
import type { Context } from '../format-context.js';
2-
import type {
3-
MessageMarkupClosePart,
4-
MessageMarkupPart
5-
} from '../formatted-parts.js';
2+
import type { MessageMarkupPart } from '../formatted-parts.js';
63
import { resolveValue } from './resolve-value.js';
74
import type { Markup } from './types.js';
85

96
export function formatMarkup(
107
ctx: Context,
118
{ kind, name, options }: Markup
12-
): MessageMarkupPart | MessageMarkupClosePart {
13-
if (kind === 'close') {
14-
return { type: 'markup', kind, source: `/${name}`, name };
15-
}
16-
const source = kind === 'open' ? `#${name}` : `#${name}/`;
9+
): MessageMarkupPart {
10+
const source =
11+
kind === 'close' ? `/${name}` : kind === 'open' ? `#${name}` : `#${name}/`;
1712
const part: MessageMarkupPart = { type: 'markup', kind, source, name };
1813
if (options?.length) {
1914
part.options = {};

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

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,10 @@ function asExpression(
101101
if (allowMarkup && exp.markup) {
102102
const cm = exp.markup;
103103
const name = asName(cm.name);
104-
let markup: Model.Markup;
105-
if (cm.type === 'markup-close') {
106-
markup = { type: 'markup', kind: 'close', name };
107-
} else {
108-
const kind = cm.close ? 'standalone' : 'open';
109-
markup = { type: 'markup', kind, name };
110-
if (cm.options.length) markup.options = cm.options.map(asOption);
111-
}
104+
const kind =
105+
cm.open.value === '/' ? 'close' : cm.close ? 'standalone' : 'open';
106+
const markup: Model.Markup = { type: 'markup', kind, name };
107+
if (cm.options.length) markup.options = cm.options.map(asOption);
112108
if (attributes) markup.attributes = attributes;
113109
markup[cst] = exp;
114110
return markup;

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

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ describe('Simple open/close', () => {
1212
});
1313

1414
test('options', () => {
15-
const mf = new MessageFormat('{#b foo=42 bar=$foo}foo{$foo}{/b}', 'en');
15+
const mf = new MessageFormat(
16+
'{#b foo=42 bar=$foo}foo{$foo}{/b foo=| bar 13 |}',
17+
'en'
18+
);
1619
const msg = mf.formatToParts({ foo: 'foo bar' });
1720
expect(msg).toEqual([
1821
{
@@ -24,7 +27,13 @@ describe('Simple open/close', () => {
2427
},
2528
{ type: 'literal', value: 'foo' },
2629
{ type: 'string', locale: 'en', source: '$foo', value: 'foo bar' },
27-
{ type: 'markup', kind: 'close', source: '/b', name: 'b' }
30+
{
31+
type: 'markup',
32+
kind: 'close',
33+
source: '/b',
34+
name: 'b',
35+
options: { foo: ' bar 13 ' }
36+
}
2837
]);
2938
expect(mf.format({ foo: 'foo bar' })).toBe('foofoo bar');
3039
});
@@ -46,25 +55,6 @@ describe('Simple open/close', () => {
4655
}
4756
});
4857
});
49-
50-
test('do not allow options on close', () => {
51-
const src = '{/b foo=13}';
52-
expect(() => new MessageFormat(src, 'en')).toThrow();
53-
const cst = parseCST(src);
54-
expect(cst).toMatchObject({
55-
type: 'simple',
56-
errors: [{ type: 'extra-content' }],
57-
pattern: {
58-
body: [
59-
{
60-
type: 'expression',
61-
markup: { type: 'markup-close', name: [{ value: 'b' }] }
62-
}
63-
]
64-
}
65-
});
66-
expect(cst).not.toHaveProperty('pattern.body.0.markup.options');
67-
});
6858
});
6959

7060
describe('Multiple open/close', () => {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ function stringifyFunctionAnnotation({ name, options }: FunctionAnnotation) {
7575
}
7676

7777
function stringifyMarkup({ kind, name, options, attributes }: Markup) {
78-
if (kind === 'close') return `{/${name}}`;
79-
let res = `{#${name}`;
78+
let res = kind === 'close' ? '{/' : '{#';
79+
res += name;
8080
if (options) for (const opt of options) res += ' ' + stringifyOption(opt);
8181
if (attributes) {
8282
for (const attr of attributes) res += ' ' + stringifyAttribute(attr);

0 commit comments

Comments
 (0)