Skip to content

Commit c9e081f

Browse files
committed
feat(mf2): Add CST Identifier, with ns:name support
1 parent 7ae0e10 commit c9e081f

File tree

5 files changed

+101
-54
lines changed

5 files changed

+101
-54
lines changed

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -264,12 +264,12 @@
264264
]
265265
},
266266
{
267-
"src": "{#tag}content{/tag}",
267+
"src": "{#ns:tag}content{/ns:tag}",
268268
"exp": "content",
269269
"parts": [
270-
{ "type": "markup", "kind": "open", "name": "tag" },
270+
{ "type": "markup", "kind": "open", "name": "ns:tag" },
271271
{ "type": "literal", "value": "content" },
272-
{ "type": "markup", "kind": "close", "name": "tag" }
272+
{ "type": "markup", "kind": "close", "name": "ns:tag" }
273273
]
274274
},
275275
{
@@ -293,15 +293,15 @@
293293
]
294294
},
295295
{
296-
"src": "{#tag foo=|foo| bar=$bar}",
296+
"src": "{#tag a:foo=|foo| b:bar=$bar}",
297297
"params": { "bar": "b a r" },
298298
"exp": "",
299299
"parts": [
300300
{
301301
"type": "markup",
302302
"kind": "open",
303303
"name": "tag",
304-
"options": { "foo": "foo", "bar": "b a r" }
304+
"options": { "a:foo": "foo", "b:bar": "b a r" }
305305
}
306306
]
307307
},
@@ -332,7 +332,14 @@
332332
},
333333
{ "src": "bad {:placeholder option=}", "syntaxError": true },
334334
{ "src": "bad {:placeholder option value}", "syntaxError": true },
335+
{ "src": "bad {:placeholder option:value}", "syntaxError": true },
335336
{ "src": "bad {:placeholder option}", "syntaxError": true },
337+
{ "src": "bad {:placeholder:}", "syntaxError": true },
338+
{ "src": "bad {::placeholder}", "syntaxError": true },
339+
{ "src": "bad {:placeholder::foo}", "syntaxError": true },
340+
{ "src": "bad {:placeholder option:=x}", "syntaxError": true },
341+
{ "src": "bad {:placeholder :option=x}", "syntaxError": true },
342+
{ "src": "bad {:placeholder option::x=y}", "syntaxError": true },
336343
{ "src": "bad {$placeholder option}", "syntaxError": true },
337344
{ "src": "no {$placeholder end", "syntaxError": true },
338345
{ "src": ".match {} * {{foo}}", "syntaxError": true },

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

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -85,20 +85,16 @@ function asExpression(
8585
if (cst.type === 'expression') {
8686
if (allowMarkup && cst.markup) {
8787
const cm = cst.markup;
88+
const name = asName(cm.name);
8889
if (cm.type === 'markup-close') {
89-
return { type: 'markup', kind: 'close', name: cm.name };
90+
return { type: 'markup', kind: 'close', name };
9091
}
9192
const markup: Model.MarkupOpen | Model.MarkupStandalone = {
9293
type: 'markup',
9394
kind: cm.close ? 'standalone' : 'open',
94-
name: cm.name
95+
name
9596
};
96-
if (cm.options.length > 0) {
97-
markup.options = cm.options.map(opt => ({
98-
name: opt.name,
99-
value: asValue(opt.value)
100-
}));
101-
}
97+
if (cm.options.length > 0) markup.options = cm.options.map(asOption);
10298
return markup;
10399
}
104100

@@ -112,12 +108,9 @@ function asExpression(
112108
if (ca) {
113109
switch (ca.type) {
114110
case 'function':
115-
annotation = { type: 'function', name: ca.name };
111+
annotation = { type: 'function', name: asName(ca.name) };
116112
if (ca.options.length > 0) {
117-
annotation.options = ca.options.map(opt => ({
118-
name: opt.name,
119-
value: asValue(opt.value)
120-
}));
113+
annotation.options = ca.options.map(asOption);
121114
}
122115
break;
123116
case 'reserved-annotation':
@@ -142,6 +135,26 @@ function asExpression(
142135
throw new MessageSyntaxError('parse-error', cst.start, cst.end);
143136
}
144137

138+
const asOption = (cst: CST.Option): Model.Option => ({
139+
name: asName(cst.name),
140+
value: asValue(cst.value)
141+
});
142+
143+
function asName(cst: CST.Identifier): string {
144+
switch (cst.length) {
145+
case 1:
146+
return cst[0].value;
147+
case 3:
148+
return `${cst[0].value}:${cst[2].value}`;
149+
default:
150+
throw new MessageSyntaxError(
151+
'parse-error',
152+
cst[0]?.start ?? -1,
153+
cst.at(-1)?.end ?? -1
154+
);
155+
}
156+
}
157+
145158
function asValue(cst: CST.Literal | CST.Junk): Model.Literal;
146159
function asValue(cst: CST.VariableRef | CST.Junk): Model.VariableRef;
147160
function asValue(

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export interface FunctionRef {
142142
type: 'function';
143143
start: number;
144144
end: number;
145-
name: string;
145+
name: Identifier;
146146
options: Option[];
147147
}
148148

@@ -162,9 +162,9 @@ export interface Markup {
162162
/** position of the sigil */
163163
start: number;
164164
end: number;
165-
close: Syntax<'/'> | undefined;
166-
name: string;
165+
name: Identifier;
167166
options: Option[];
167+
close: Syntax<'/'> | undefined;
168168
}
169169

170170
/** @beta */
@@ -173,18 +173,24 @@ export interface MarkupClose {
173173
/** position of the sigil */
174174
start: number;
175175
end: number;
176-
name: string;
176+
name: Identifier;
177177
}
178178

179179
/** @beta */
180180
export interface Option {
181181
/** position at the start of the name */
182182
start: number;
183183
end: number;
184-
name: string;
184+
name: Identifier;
185185
value: Literal | VariableRef;
186186
}
187187

188+
/** @beta */
189+
export type Identifier =
190+
| [name: Syntax<string>]
191+
| [namespace: Syntax<string>, separator: Syntax<':'>]
192+
| [namespace: Syntax<string>, separator: Syntax<':'>, name: Syntax<string>];
193+
188194
/** @beta */
189195
export interface Syntax<T extends string> {
190196
start: number;

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

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -107,20 +107,19 @@ function parseFunctionRefOrMarkup(
107107
start: number,
108108
type: 'function' | 'markup'
109109
): CST.FunctionRef | CST.Markup {
110-
let pos = start + 1; // ':' | '#'
111-
const name = parseNameValue(ctx.source, pos);
112-
if (!name) ctx.onError('empty-token', pos, pos + 1);
110+
const { source } = ctx;
111+
const id = parseIdentifier(ctx, start + 1);
112+
let pos = id.end;
113113
const options: CST.Option[] = [];
114-
pos += name.length;
115114
let close: CST.Syntax<'/'> | undefined;
116-
while (pos < ctx.source.length) {
117-
let ws = whitespaces(ctx.source, pos);
118-
const next = ctx.source[pos + ws];
115+
while (pos < source.length) {
116+
let ws = whitespaces(source, pos);
117+
const next = source[pos + ws];
119118
if (next === '}') break;
120119
if (type === 'markup' && next === '/') {
121120
pos += ws + 1;
122121
close = { start: pos - 1, end: pos, value: '/' };
123-
ws = whitespaces(ctx.source, pos);
122+
ws = whitespaces(source, pos);
124123
if (ws > 0) ctx.onError('extra-content', pos, pos + ws);
125124
break;
126125
}
@@ -132,22 +131,18 @@ function parseFunctionRefOrMarkup(
132131
pos = opt.end;
133132
}
134133
return type === 'function'
135-
? { type, start, end: pos, name, options }
136-
: { type, start, end: pos, name, options, close };
134+
? { type, start, end: pos, name: id.parts, options }
135+
: { type, start, end: pos, name: id.parts, options, close };
137136
}
138137

139138
function parseMarkupClose(ctx: ParseContext, start: number): CST.MarkupClose {
140-
let pos = start + 1; // '/'
141-
const name = parseNameValue(ctx.source, pos);
142-
if (name) pos += name.length;
143-
else ctx.onError('empty-token', pos, pos + 1);
144-
return { type: 'markup-close', start, end: pos, name };
139+
const id = parseIdentifier(ctx, start + 1);
140+
return { type: 'markup-close', start, end: id.end, name: id.parts };
145141
}
146142

147-
// option = name [s] "=" [s] (literal / variable)
148143
function parseOption(ctx: ParseContext, start: number): CST.Option {
149-
const name = parseNameValue(ctx.source, start);
150-
let pos = start + name.length;
144+
const id = parseIdentifier(ctx, start);
145+
let pos = id.end;
151146
pos += whitespaces(ctx.source, pos);
152147
if (ctx.source[pos] === '=') pos += 1;
153148
else ctx.onError('missing-syntax', pos, '=');
@@ -156,19 +151,37 @@ function parseOption(ctx: ParseContext, start: number): CST.Option {
156151
ctx.source[pos] === '$'
157152
? parseVariable(ctx, pos)
158153
: parseLiteral(ctx, pos, true);
159-
return { start, end: value.end, name, value };
154+
return { start, end: value.end, name: id.parts, value };
155+
}
156+
157+
function parseIdentifier(
158+
ctx: ParseContext,
159+
start: number
160+
): { parts: CST.Identifier; end: number } {
161+
const { source } = ctx;
162+
const str0 = parseNameValue(source, start);
163+
if (!str0) {
164+
ctx.onError('empty-token', start, start + 1);
165+
return { parts: [{ start, end: start, value: '' }], end: start };
166+
}
167+
let pos = start + str0.length;
168+
const id0 = { start, end: pos, value: str0 };
169+
if (source[pos] !== ':') return { parts: [id0], end: pos };
170+
171+
const sep = { start: pos, end: pos + 1, value: ':' as const };
172+
pos += 1;
173+
174+
const str1 = parseNameValue(source, pos);
175+
if (str1) {
176+
const end = pos + str1.length;
177+
const id1 = { start: pos, end, value: str1 };
178+
return { parts: [id0, sep, id1], end };
179+
} else {
180+
ctx.onError('empty-token', pos, pos + 1);
181+
return { parts: [id0, sep], end: pos };
182+
}
160183
}
161184

162-
// reserved = reserved-start reserved-body
163-
// reserved-start = "!" / "@" / "#" / "%" / "^" / "&" / "*" / "<" / ">" / "?" / "~"
164-
// reserved-body = *( [s] 1*(reserved-char / reserved-escape / literal))
165-
// reserved-char = %x00-08 ; omit HTAB and LF
166-
// / %x0B-0C ; omit CR
167-
// / %x0E-19 ; omit SP
168-
// / %x21-5B ; omit \
169-
// / %x5D-7A ; omit { | }
170-
// / %x7E-D7FF ; omit surrogates
171-
// / %xE000-10FFFF
172185
function parseReservedAnnotation(
173186
ctx: ParseContext,
174187
start: number

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,12 @@ describe('Simple open/close', () => {
3737
type: 'simple',
3838
errors: [{ type: 'extra-content' }],
3939
pattern: {
40-
body: [{ type: 'expression', markup: { type: 'markup', name: 'b' } }]
40+
body: [
41+
{
42+
type: 'expression',
43+
markup: { type: 'markup', name: [{ value: 'b' }] }
44+
}
45+
]
4146
}
4247
});
4348
});
@@ -51,7 +56,10 @@ describe('Simple open/close', () => {
5156
errors: [{ type: 'extra-content' }],
5257
pattern: {
5358
body: [
54-
{ type: 'expression', markup: { type: 'markup-close', name: 'b' } }
59+
{
60+
type: 'expression',
61+
markup: { type: 'markup-close', name: [{ value: 'b' }] }
62+
}
5563
]
5664
}
5765
});

0 commit comments

Comments
 (0)