From fc4db14df9f0445f3960f3ca7d2426b0b5c8c95c Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Fri, 22 Dec 2023 17:54:43 +0200 Subject: [PATCH 01/44] feat(mf2): In Pattern, replace Text with a primitive string --- packages/mf2-fluent/src/fluent-to-message.ts | 13 +++++----- packages/mf2-fluent/src/fluent.test.ts | 24 +++++++++---------- packages/mf2-fluent/src/message-to-fluent.ts | 5 ++-- .../mf2-icu-mf1/src/mf1-to-message-data.ts | 14 +++++------ .../src/cst-parser/as-data-model.ts | 2 +- packages/mf2-messageformat/src/data-model.ts | 6 ++--- .../mf2-messageformat/src/messageformat.ts | 8 +++---- .../mf2-messageformat/src/pattern/index.ts | 2 +- .../mf2-messageformat/src/pattern/literal.ts | 19 --------------- .../src/stringifier/message.ts | 3 +-- packages/mf2-xliff/src/mf2xliff.ts | 7 +++--- packages/mf2-xliff/src/xliff2mf.ts | 2 +- 12 files changed, 40 insertions(+), 65 deletions(-) diff --git a/packages/mf2-fluent/src/fluent-to-message.ts b/packages/mf2-fluent/src/fluent-to-message.ts index 696ee6d2..eb711dea 100644 --- a/packages/mf2-fluent/src/fluent-to-message.ts +++ b/packages/mf2-fluent/src/fluent-to-message.ts @@ -4,12 +4,10 @@ import { Expression, FunctionRef, isFunctionRef, - isText, Literal, Option, PatternMessage, SelectMessage, - Text, VariableRef, Variant } from 'messageformat'; @@ -140,9 +138,9 @@ function expressionToPart( } } -const elementToPart = (el: Fluent.PatternElement): Expression | Text => +const elementToPart = (el: Fluent.PatternElement): string | Expression => el.type === 'TextElement' - ? { type: 'text', value: el.value } + ? el.value : { type: 'expression', body: expressionToPart(el.expression) }; function asFluentSelect( @@ -238,10 +236,11 @@ export function fluentToMessage( : String(value) === vi.value; }) ) { - const last = vp[vp.length - 1]; + const i = vp.length - 1; const part = elementToPart(el); - if (isText(last) && isText(part)) last.value += part.value; - else vp.push(part); + if (typeof vp[i] === 'string' && typeof part === 'string') { + vp[i] += part; + } else vp.push(part); } } } diff --git a/packages/mf2-fluent/src/fluent.test.ts b/packages/mf2-fluent/src/fluent.test.ts index 3ff1c5b0..f3e06d0c 100644 --- a/packages/mf2-fluent/src/fluent.test.ts +++ b/packages/mf2-fluent/src/fluent.test.ts @@ -499,7 +499,7 @@ describe('formatToParts', () => { declarations: [], pattern: { body: [ - { type: 'text', value: 'Foo ' }, + 'Foo ', { type: 'expression', body: { type: 'variable', name: 'num' } @@ -519,7 +519,7 @@ describe('formatToParts', () => { type: 'message', comment: 'Group 1', declarations: [], - pattern: { body: [{ type: 'text', value: 'Bar' }] } + pattern: { body: ['Bar'] } } ] ] @@ -533,7 +533,7 @@ describe('formatToParts', () => { type: 'message', comment: 'Group 2\n\nOther message', declarations: [], - pattern: { body: [{ type: 'text', value: 'Qux' }] } + pattern: { body: ['Qux'] } } ] ] @@ -658,20 +658,20 @@ describe('fluentToResourceData', () => { test('Merge adjacent text elements for one selector', () => { const msg = data.get('single')?.get('') as SelectMessage; expect(msg.variants.map(v => v.value.body)).toMatchObject([ - [{ type: 'text', value: 'One Zero selector.' }], - [{ type: 'text', value: 'One Other selector.' }] + ['One Zero selector.'], + ['One Other selector.'] ]); }); test('Merge adjacent text elements for multiple selectors', () => { const msg = data.get('multi')?.get('') as SelectMessage; expect(msg.variants.map(v => v.value.body)).toMatchObject([ - [{ type: 'text', value: 'Combine Zero multiple F selectors.' }], - [{ type: 'text', value: 'Combine Zero multiple M selectors.' }], - [{ type: 'text', value: 'Combine Zero multiple N selectors.' }], - [{ type: 'text', value: 'Combine Other multiple F selectors.' }], - [{ type: 'text', value: 'Combine Other multiple M selectors.' }], - [{ type: 'text', value: 'Combine Other multiple N selectors.' }] + ['Combine Zero multiple F selectors.'], + ['Combine Zero multiple M selectors.'], + ['Combine Zero multiple N selectors.'], + ['Combine Other multiple F selectors.'], + ['Combine Other multiple M selectors.'], + ['Combine Other multiple N selectors.'] ]); }); @@ -756,7 +756,7 @@ describe('messagetoFluent', () => { variants: [ { keys: [{ type: '*' }], - value: { body: [{ type: 'text', value: 'X' }] } + value: { body: ['X'] } } ] }; diff --git a/packages/mf2-fluent/src/message-to-fluent.ts b/packages/mf2-fluent/src/message-to-fluent.ts index 67645eab..18a2a2d7 100644 --- a/packages/mf2-fluent/src/message-to-fluent.ts +++ b/packages/mf2-fluent/src/message-to-fluent.ts @@ -8,7 +8,6 @@ import { isLiteral, isPatternMessage, isSelectMessage, - isText, Literal, Message, Pattern, @@ -148,8 +147,8 @@ function keysMatch(a: (Literal | CatchallKey)[], b: (Literal | CatchallKey)[]) { function patternToFluent(ctx: MsgContext, pattern: Pattern) { const elements = pattern.body.map(el => - isText(el) - ? new Fluent.TextElement(el.value) + typeof el === 'string' + ? new Fluent.TextElement(el) : new Fluent.Placeable(expressionToFluent(ctx, el)) ); return new Fluent.Pattern(elements); diff --git a/packages/mf2-icu-mf1/src/mf1-to-message-data.ts b/packages/mf2-icu-mf1/src/mf1-to-message-data.ts index 0faae844..d4246f7e 100644 --- a/packages/mf2-icu-mf1/src/mf1-to-message-data.ts +++ b/packages/mf2-icu-mf1/src/mf1-to-message-data.ts @@ -2,10 +2,8 @@ import type * as AST from '@messageformat/parser'; import { Expression, FunctionRef, - isText, Message, Option, - Text, VariableRef, Variant } from 'messageformat'; @@ -56,10 +54,10 @@ function tokenToPart( token: AST.Token, pluralArg: string | null, pluralOffset: number | null -): Text | Expression { +): string | Expression { switch (token.type) { case 'content': - return { type: 'text', value: token.value }; + return token.value; case 'argument': return { type: 'expression', @@ -83,7 +81,7 @@ function tokenToPart( return { type: 'expression', body }; } case 'octothorpe': { - if (!pluralArg) return { type: 'text', value: '#' }; + if (!pluralArg) return '#'; const body: FunctionRef = { type: 'function', kind: 'value', @@ -217,10 +215,10 @@ export function mf1ToMessageData(ast: AST.Token[]): Message { : String(value) === vi.value; }) ) { - const last = vp[vp.length - 1]; + const i = vp.length - 1; const part = tokenToPart(token, pluralArg, pluralOffset); - if (isText(last) && isText(part)) { - last.value += part.value; + if (typeof vp[i] === 'string' && typeof part === 'string') { + vp[i] += part; } else vp.push(part); } } diff --git a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts index 8f125196..ff408699 100644 --- a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts +++ b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts @@ -39,7 +39,7 @@ export function asDataModel(msg: CST.Message): Model.Message { function asPattern(cst: CST.Pattern): Model.Pattern { const body: Model.Pattern['body'] = cst.body.map(el => - el.type === 'text' ? { type: 'text', value: el.value } : asExpression(el) + el.type === 'text' ? el.value : asExpression(el) ); return { body }; } diff --git a/packages/mf2-messageformat/src/data-model.ts b/packages/mf2-messageformat/src/data-model.ts index b276aa01..e5f2730e 100644 --- a/packages/mf2-messageformat/src/data-model.ts +++ b/packages/mf2-messageformat/src/data-model.ts @@ -1,7 +1,7 @@ -import type { Expression, Literal, Text, VariableRef } from './pattern'; +import type { Expression, Literal, VariableRef } from './pattern'; export type { FunctionRef, Option, Reserved } from './pattern'; -export type { Expression, Literal, Text, VariableRef }; +export type { Expression, Literal, VariableRef }; /** * The representation of a single message. @@ -45,7 +45,7 @@ export interface Declaration { * @beta */ export interface Pattern { - body: Array; + body: Array; } /** diff --git a/packages/mf2-messageformat/src/messageformat.ts b/packages/mf2-messageformat/src/messageformat.ts index bf8001fe..21b98e64 100644 --- a/packages/mf2-messageformat/src/messageformat.ts +++ b/packages/mf2-messageformat/src/messageformat.ts @@ -67,8 +67,8 @@ export class MessageFormat { const ctx = this.createContext(msgParams, onError); let res = ''; for (const elem of selectPattern(ctx, this.#message)) { - if (elem.type === 'text') { - res += elem.value; + if (typeof elem === 'string') { + res += elem; } else { let mv: MessageValue | undefined; try { @@ -95,8 +95,8 @@ export class MessageFormat { const ctx = this.createContext(msgParams, onError); const parts: MessagePart[] = []; for (const elem of selectPattern(ctx, this.#message)) { - if (elem.type === 'text') { - parts.push({ type: 'literal', value: elem.value }); + if (typeof elem === 'string') { + parts.push({ type: 'literal', value: elem }); } else { let mv: MessageValue | undefined; try { diff --git a/packages/mf2-messageformat/src/pattern/index.ts b/packages/mf2-messageformat/src/pattern/index.ts index ab8bdb26..1b2ba72d 100644 --- a/packages/mf2-messageformat/src/pattern/index.ts +++ b/packages/mf2-messageformat/src/pattern/index.ts @@ -7,7 +7,7 @@ import { Reserved, resolveReserved } from './reserved'; import { resolveVariableRef, VariableRef } from './variable-ref'; export { isFunctionRef, FunctionRef, Option } from './function-ref'; -export { isLiteral, isText, Literal, Text } from './literal'; +export { isLiteral, Literal } from './literal'; export { isReserved, Reserved } from './reserved'; export { getMessageValue, diff --git a/packages/mf2-messageformat/src/pattern/literal.ts b/packages/mf2-messageformat/src/pattern/literal.ts index 0d460f89..2942551c 100644 --- a/packages/mf2-messageformat/src/pattern/literal.ts +++ b/packages/mf2-messageformat/src/pattern/literal.ts @@ -16,16 +16,6 @@ export interface Literal { value: string; } -/** - * Top-level literal content. - * - * @beta - */ -export interface Text { - type: 'text'; - value: string; -} - /** * Type guard for {@link Literal} pattern elements * @@ -35,15 +25,6 @@ export interface Text { export const isLiteral = (part: any): part is Literal => !!part && typeof part === 'object' && part.type === 'literal'; -/** - * Type guard for {@link Text} pattern elements - * - * @beta - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const isText = (part: any): part is Text => - !!part && typeof part === 'object' && part.type === 'text'; - export function resolveLiteral(ctx: Context, lit: Literal) { const msgCtx = new MessageFunctionContext(ctx, `|${lit.value}|`); return ctx.functions.string(msgCtx, {}, lit.value); diff --git a/packages/mf2-messageformat/src/stringifier/message.ts b/packages/mf2-messageformat/src/stringifier/message.ts index 5f5b5d5d..b88e3768 100644 --- a/packages/mf2-messageformat/src/stringifier/message.ts +++ b/packages/mf2-messageformat/src/stringifier/message.ts @@ -11,7 +11,6 @@ import { Expression, FunctionRef, isLiteral, - isText, isVariableRef, Literal, Option, @@ -81,7 +80,7 @@ function stringifyOption({ name, value }: Option) { function stringifyPattern({ body }: Pattern) { let res = ''; for (const el of body) { - res += isText(el) ? el.value : stringifyExpression(el); + res += typeof el === 'string' ? el : stringifyExpression(el); } return `{${res}}`; } diff --git a/packages/mf2-xliff/src/mf2xliff.ts b/packages/mf2-xliff/src/mf2xliff.ts index ee4f6d93..38f3a921 100644 --- a/packages/mf2-xliff/src/mf2xliff.ts +++ b/packages/mf2-xliff/src/mf2xliff.ts @@ -4,7 +4,6 @@ import { isLiteral, isMessage, isSelectMessage, - isText, isVariableRef, MessageFormat } from 'messageformat'; @@ -56,7 +55,7 @@ const msgAttributes = (pre: 'g' | 'u', key: string[]) => ({ // TODO Add escapes const asText = (value: unknown): X.Text => ({ type: 'text', - text: String(isText(value) || isLiteral(value) ? value.value : value) + text: String(isLiteral(value) ? value.value : value) }); const mismatch = (key: string[]) => @@ -231,8 +230,8 @@ function resolvePattern( trgPattern: MF.Pattern | undefined ): X.Unit { const parts: X.MessagePart[] = []; - const handlePart = (p: MF.Text | MF.Expression): X.Text | X.InlineElement => { - if (isText(p)) return asText(p); + const handlePart = (p: string | MF.Expression): X.Text | X.InlineElement => { + if (typeof p === 'string') return asText(p); const id = nextId(); const part = resolveExpression(id, p); parts.push(part); diff --git a/packages/mf2-xliff/src/xliff2mf.ts b/packages/mf2-xliff/src/xliff2mf.ts index ea08c2ed..70c4f1ca 100644 --- a/packages/mf2-xliff/src/xliff2mf.ts +++ b/packages/mf2-xliff/src/xliff2mf.ts @@ -185,7 +185,7 @@ function resolvePattern( for (const part of resolveContents(stel.elements, mf)) { body.push( part.type === 'literal' - ? { type: 'text', value: part.value } + ? part.value : { type: 'expression', body: part } ); } From a19397b57d755a7fa8fc43021699ddd82bab73f1 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sat, 23 Dec 2023 15:55:12 +0200 Subject: [PATCH 02/44] fix(mf2): Rename Reserved -> UnsupportedAnnotation --- .../src/cst-parser/as-data-model.ts | 12 +++-- packages/mf2-messageformat/src/data-model.ts | 2 +- packages/mf2-messageformat/src/errors.ts | 6 ++- .../mf2-messageformat/src/pattern/index.ts | 16 +++++-- .../mf2-messageformat/src/pattern/reserved.ts | 42 ----------------- ...test.ts => unsupported-annotation.test.ts} | 8 ++-- .../src/pattern/unsupported-annotation.ts | 46 +++++++++++++++++++ 7 files changed, 75 insertions(+), 57 deletions(-) delete mode 100644 packages/mf2-messageformat/src/pattern/reserved.ts rename packages/mf2-messageformat/src/pattern/{reserved.test.ts => unsupported-annotation.test.ts} (82%) create mode 100644 packages/mf2-messageformat/src/pattern/unsupported-annotation.ts diff --git a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts index ff408699..2a855e9e 100644 --- a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts +++ b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts @@ -52,7 +52,7 @@ function asExpression(cst: CST.Expression | CST.Junk): Model.Expression { | Model.Literal | Model.VariableRef | Model.FunctionRef - | Model.Reserved; + | Model.UnsupportedAnnotation; switch (cst.body.type) { case 'literal': case 'variable': @@ -62,7 +62,7 @@ function asExpression(cst: CST.Expression | CST.Junk): Model.Expression { body = asFunctionRef(cst.body); break; case 'reserved': - body = asReserved(cst.body); + body = asUnsupportedAnnotation(cst.body); break; default: throw new MessageSyntaxError('parse-error', cst.start, cst.end); @@ -86,9 +86,11 @@ function asFunctionRef(cst: CST.FunctionRef): Model.FunctionRef { return fn; } -function asReserved(cst: CST.Reserved): Model.Reserved { - const res: Model.Reserved = { - type: 'reserved', +function asUnsupportedAnnotation( + cst: CST.Reserved +): Model.UnsupportedAnnotation { + const res: Model.UnsupportedAnnotation = { + type: 'unsupported-annotation', sigil: cst.sigil, source: cst.source }; diff --git a/packages/mf2-messageformat/src/data-model.ts b/packages/mf2-messageformat/src/data-model.ts index e5f2730e..5c50cc95 100644 --- a/packages/mf2-messageformat/src/data-model.ts +++ b/packages/mf2-messageformat/src/data-model.ts @@ -1,6 +1,6 @@ import type { Expression, Literal, VariableRef } from './pattern'; -export type { FunctionRef, Option, Reserved } from './pattern'; +export type { FunctionRef, Option, UnsupportedAnnotation } from './pattern'; export type { Expression, Literal, VariableRef }; /** diff --git a/packages/mf2-messageformat/src/errors.ts b/packages/mf2-messageformat/src/errors.ts index a0f21b4d..e4a25742 100644 --- a/packages/mf2-messageformat/src/errors.ts +++ b/packages/mf2-messageformat/src/errors.ts @@ -71,7 +71,11 @@ export class MessageDataModelError extends MessageSyntaxError { } export class MessageResolutionError extends MessageError { - declare type: 'bad-input' | 'bad-option' | 'reserved' | 'unresolved-var'; + declare type: + | 'bad-input' + | 'bad-option' + | 'unresolved-var' + | 'unsupported-annotation'; source: string; constructor( type: typeof MessageResolutionError.prototype.type, diff --git a/packages/mf2-messageformat/src/pattern/index.ts b/packages/mf2-messageformat/src/pattern/index.ts index 1b2ba72d..be157f54 100644 --- a/packages/mf2-messageformat/src/pattern/index.ts +++ b/packages/mf2-messageformat/src/pattern/index.ts @@ -3,12 +3,18 @@ import type { MessageValue } from '../runtime'; import { FunctionRef, resolveFunctionRef } from './function-ref'; import { Literal, resolveLiteral } from './literal'; -import { Reserved, resolveReserved } from './reserved'; +import { + resolveunsupportedAnnotation, + UnsupportedAnnotation +} from './unsupported-annotation'; import { resolveVariableRef, VariableRef } from './variable-ref'; export { isFunctionRef, FunctionRef, Option } from './function-ref'; export { isLiteral, Literal } from './literal'; -export { isReserved, Reserved } from './reserved'; +export { + isUnsupportedAnnotation, + UnsupportedAnnotation +} from './unsupported-annotation'; export { getMessageValue, isVariableRef, @@ -23,7 +29,7 @@ export { */ export interface Expression { type: 'expression'; - body: Literal | VariableRef | FunctionRef | Reserved; + body: Literal | VariableRef | FunctionRef | UnsupportedAnnotation; } /** @@ -47,8 +53,8 @@ export function resolveExpression( return resolveVariableRef(ctx, body); case 'function': return resolveFunctionRef(ctx, body); - case 'reserved': - return resolveReserved(ctx, body); + case 'unsupported-annotation': + return resolveunsupportedAnnotation(ctx, body); default: // @ts-expect-error - should never happen throw new Error(`Unsupported expression: ${body.type}`); diff --git a/packages/mf2-messageformat/src/pattern/reserved.ts b/packages/mf2-messageformat/src/pattern/reserved.ts deleted file mode 100644 index 3e665dda..00000000 --- a/packages/mf2-messageformat/src/pattern/reserved.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { MessageResolutionError } from '../errors.js'; -import type { Context } from '../format-context.js'; -import { fallback } from '../runtime/index.js'; -import type { Literal } from './literal.js'; -import { getValueSource } from './value.js'; -import type { VariableRef } from './variable-ref.js'; - -/** - * When the parser encounters an expression with syntax reserved for later use, - * it emits a Reserved pattern element to represent it. - * - * @remarks - * As the meaning of this syntax is not yet defined or supported, - * it will always resolve with a fallback representation and emit an error. - * - * @beta - */ -export interface Reserved { - type: 'reserved'; - sigil: '!' | '@' | '#' | '%' | '^' | '&' | '*' | '<' | '>' | '?' | '~'; - source: string; - name?: never; - operand?: Literal | VariableRef; -} - -/** - * Type guard for {@link Reserved} pattern elements - * - * @beta - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const isReserved = (part: any): part is Reserved => - !!part && typeof part === 'object' && part.type === 'reserved'; - -export function resolveReserved( - ctx: Context, - { operand, sigil, source }: Reserved -) { - const msg = `Reserved ${sigil} expression is not supported`; - ctx.onError(new MessageResolutionError('reserved', msg, source)); - return fallback(getValueSource(operand) ?? source); -} diff --git a/packages/mf2-messageformat/src/pattern/reserved.test.ts b/packages/mf2-messageformat/src/pattern/unsupported-annotation.test.ts similarity index 82% rename from packages/mf2-messageformat/src/pattern/reserved.test.ts rename to packages/mf2-messageformat/src/pattern/unsupported-annotation.test.ts index f1db3afc..80593658 100644 --- a/packages/mf2-messageformat/src/pattern/reserved.test.ts +++ b/packages/mf2-messageformat/src/pattern/unsupported-annotation.test.ts @@ -18,18 +18,20 @@ function resolve( describe('Reserved syntax', () => { test('empty', () => { - const msg = resolve('{{!}}', {}, [{ type: 'reserved' }]); + const msg = resolve('{{!}}', {}, [{ type: 'unsupported-annotation' }]); expect(msg).toMatchObject([{ type: 'fallback', source: '!' }]); }); test('argument', () => { - const msg = resolve('{{$foo @bar}}', { foo: 42 }, [{ type: 'reserved' }]); + const msg = resolve('{{$foo @bar}}', { foo: 42 }, [ + { type: 'unsupported-annotation' } + ]); expect(msg).toMatchObject([{ type: 'fallback', source: '$foo' }]); }); test('whitespace', () => { const msg = resolve('{{ # one\ntwo\rthree four }}', {}, [ - { type: 'reserved' } + { type: 'unsupported-annotation' } ]); expect(msg).toMatchObject([ { type: 'fallback', source: '# one\ntwo\rthree four' } diff --git a/packages/mf2-messageformat/src/pattern/unsupported-annotation.ts b/packages/mf2-messageformat/src/pattern/unsupported-annotation.ts new file mode 100644 index 00000000..651e93e1 --- /dev/null +++ b/packages/mf2-messageformat/src/pattern/unsupported-annotation.ts @@ -0,0 +1,46 @@ +import { MessageResolutionError } from '../errors.js'; +import type { Context } from '../format-context.js'; +import { fallback } from '../runtime/index.js'; +import type { Literal } from './literal.js'; +import { getValueSource } from './value.js'; +import type { VariableRef } from './variable-ref.js'; + +/** + * When the parser encounters an expression with reserved syntax, + * it emits an UnsupportedAnnotation to represent it. + * + * @remarks + * As the meaning of this syntax is not supported, + * it will always resolve with a fallback representation and emit an error. + * + * @beta + */ +export interface UnsupportedAnnotation { + type: 'unsupported-annotation'; + sigil: '!' | '@' | '#' | '%' | '^' | '&' | '*' | '<' | '>' | '?' | '~'; + source: string; + name?: never; + operand?: Literal | VariableRef; +} + +/** + * Type guard for {@link UnsupportedAnnotation} pattern elements + * + * @beta + */ +export const isUnsupportedAnnotation = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + part: any +): part is UnsupportedAnnotation => + !!part && typeof part === 'object' && part.type === 'unsupported-annotation'; + +export function resolveunsupportedAnnotation( + ctx: Context, + { operand, sigil, source }: UnsupportedAnnotation +) { + const msg = `Reserved ${sigil} annotation is not supported`; + ctx.onError( + new MessageResolutionError('unsupported-annotation', msg, source) + ); + return fallback(getValueSource(operand) ?? source); +} From 8aef2531256df40ecffd75d90ec17ab48aff7af8 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 24 Dec 2023 12:40:10 +0200 Subject: [PATCH 03/44] fix(mf2): Rename FunctionRef -> FunctionAnnotation --- packages/mf2-fluent/src/fluent-to-message.ts | 8 ++++---- packages/mf2-fluent/src/message-to-fluent.ts | 4 ++-- .../mf2-icu-mf1/src/mf1-to-message-data.ts | 6 +++--- .../src/cst-parser/as-data-model.ts | 8 ++++---- packages/mf2-messageformat/src/data-model.ts | 6 +++++- .../mf2-messageformat/src/extra/validate.ts | 4 ++-- ...ef.test.ts => function-annotation.test.ts} | 0 ...function-ref.ts => function-annotation.ts} | 19 +++++++++++-------- .../mf2-messageformat/src/pattern/index.ts | 19 +++++++++++++------ .../mf2-messageformat/src/runtime/index.ts | 2 +- .../src/stringifier/message.ts | 13 +++++++++---- packages/mf2-xliff/src/mf2xliff.ts | 4 ++-- packages/mf2-xliff/src/xliff2mf.ts | 16 ++++++++-------- 13 files changed, 64 insertions(+), 45 deletions(-) rename packages/mf2-messageformat/src/pattern/{function-ref.test.ts => function-annotation.test.ts} (100%) rename packages/mf2-messageformat/src/pattern/{function-ref.ts => function-annotation.ts} (83%) diff --git a/packages/mf2-fluent/src/fluent-to-message.ts b/packages/mf2-fluent/src/fluent-to-message.ts index eb711dea..3e09ba58 100644 --- a/packages/mf2-fluent/src/fluent-to-message.ts +++ b/packages/mf2-fluent/src/fluent-to-message.ts @@ -2,8 +2,8 @@ import * as Fluent from '@fluent/syntax'; import deepEqual from 'fast-deep-equal'; import { Expression, - FunctionRef, - isFunctionRef, + FunctionAnnotation, + isFunctionAnnotation, Literal, Option, PatternMessage, @@ -52,7 +52,7 @@ function findSelectArgs(pattern: Fluent.Pattern): SelectArg[] { function expressionToPart( exp: Fluent.Expression -): Literal | VariableRef | FunctionRef { +): Literal | VariableRef | FunctionAnnotation { switch (exp.type) { case 'NumberLiteral': return { @@ -70,7 +70,7 @@ function expressionToPart( const { positional, named } = exp.arguments; const args = positional.map(exp => { const part = expressionToPart(exp); - if (isFunctionRef(part)) + if (isFunctionAnnotation(part)) throw new Error(`A Fluent ${exp.type} is not supported here.`); return part; }); diff --git a/packages/mf2-fluent/src/message-to-fluent.ts b/packages/mf2-fluent/src/message-to-fluent.ts index 18a2a2d7..b700b19b 100644 --- a/packages/mf2-fluent/src/message-to-fluent.ts +++ b/packages/mf2-fluent/src/message-to-fluent.ts @@ -3,7 +3,7 @@ import { CatchallKey, Declaration, Expression, - FunctionRef, + FunctionAnnotation, isCatchallKey, isLiteral, isPatternMessage, @@ -156,7 +156,7 @@ function patternToFluent(ctx: MsgContext, pattern: Pattern) { function functionRefToFluent( ctx: MsgContext, - { name, operand, options }: FunctionRef + { name, operand, options }: FunctionAnnotation ): Fluent.InlineExpression { const args = new Fluent.CallArguments(); if (operand) args.positional[0] = valueToFluent(ctx, operand); diff --git a/packages/mf2-icu-mf1/src/mf1-to-message-data.ts b/packages/mf2-icu-mf1/src/mf1-to-message-data.ts index d4246f7e..58851c34 100644 --- a/packages/mf2-icu-mf1/src/mf1-to-message-data.ts +++ b/packages/mf2-icu-mf1/src/mf1-to-message-data.ts @@ -1,7 +1,7 @@ import type * as AST from '@messageformat/parser'; import { Expression, - FunctionRef, + FunctionAnnotation, Message, Option, VariableRef, @@ -64,7 +64,7 @@ function tokenToPart( body: { type: 'variable', name: token.arg } }; case 'function': { - const body: FunctionRef = { + const body: FunctionAnnotation = { type: 'function', kind: 'value', name: token.key, @@ -82,7 +82,7 @@ function tokenToPart( } case 'octothorpe': { if (!pluralArg) return '#'; - const body: FunctionRef = { + const body: FunctionAnnotation = { type: 'function', kind: 'value', name: 'number', diff --git a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts index 2a855e9e..deb8d9b3 100644 --- a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts +++ b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts @@ -51,7 +51,7 @@ function asExpression(cst: CST.Expression | CST.Junk): Model.Expression { let body: | Model.Literal | Model.VariableRef - | Model.FunctionRef + | Model.FunctionAnnotation | Model.UnsupportedAnnotation; switch (cst.body.type) { case 'literal': @@ -59,7 +59,7 @@ function asExpression(cst: CST.Expression | CST.Junk): Model.Expression { body = asValue(cst.body); break; case 'function': - body = asFunctionRef(cst.body); + body = asFunctionAnnotation(cst.body); break; case 'reserved': body = asUnsupportedAnnotation(cst.body); @@ -70,8 +70,8 @@ function asExpression(cst: CST.Expression | CST.Junk): Model.Expression { return { type: 'expression', body }; } -function asFunctionRef(cst: CST.FunctionRef): Model.FunctionRef { - const fn: Model.FunctionRef = { +function asFunctionAnnotation(cst: CST.FunctionRef): Model.FunctionAnnotation { + const fn: Model.FunctionAnnotation = { type: 'function', kind: cst.kind, name: cst.name diff --git a/packages/mf2-messageformat/src/data-model.ts b/packages/mf2-messageformat/src/data-model.ts index 5c50cc95..55c9e839 100644 --- a/packages/mf2-messageformat/src/data-model.ts +++ b/packages/mf2-messageformat/src/data-model.ts @@ -1,6 +1,10 @@ import type { Expression, Literal, VariableRef } from './pattern'; -export type { FunctionRef, Option, UnsupportedAnnotation } from './pattern'; +export type { + FunctionAnnotation, + Option, + UnsupportedAnnotation +} from './pattern'; export type { Expression, Literal, VariableRef }; /** diff --git a/packages/mf2-messageformat/src/extra/validate.ts b/packages/mf2-messageformat/src/extra/validate.ts index c07f28c4..2f8bcf2a 100644 --- a/packages/mf2-messageformat/src/extra/validate.ts +++ b/packages/mf2-messageformat/src/extra/validate.ts @@ -1,12 +1,12 @@ import type { Message, Pattern } from '../data-model'; -import { isExpression, isFunctionRef } from '../pattern'; +import { isExpression, isFunctionAnnotation } from '../pattern'; import type { MessageFunctions } from '../runtime'; function validateParts(parts: Pattern['body'], functions: MessageFunctions) { for (const part of parts) { if ( isExpression(part) && - isFunctionRef(part.body) && + isFunctionAnnotation(part.body) && part.body.kind === 'value' ) { if (typeof functions[part.body.name] !== 'function') { diff --git a/packages/mf2-messageformat/src/pattern/function-ref.test.ts b/packages/mf2-messageformat/src/pattern/function-annotation.test.ts similarity index 100% rename from packages/mf2-messageformat/src/pattern/function-ref.test.ts rename to packages/mf2-messageformat/src/pattern/function-annotation.test.ts diff --git a/packages/mf2-messageformat/src/pattern/function-ref.ts b/packages/mf2-messageformat/src/pattern/function-annotation.ts similarity index 83% rename from packages/mf2-messageformat/src/pattern/function-ref.ts rename to packages/mf2-messageformat/src/pattern/function-annotation.ts index 0978ec79..4bfe37bf 100644 --- a/packages/mf2-messageformat/src/pattern/function-ref.ts +++ b/packages/mf2-messageformat/src/pattern/function-annotation.ts @@ -6,7 +6,7 @@ import { getValueSource, resolveValue } from './value.js'; import type { VariableRef } from './variable-ref.js'; /** - * To resolve a FunctionRef, an externally defined function is called. + * To resolve a FunctionAnnotation, an externally defined function is called. * * @remarks * The `name` identifies a function that takes in the arguments `args`, the @@ -17,7 +17,7 @@ import type { VariableRef } from './variable-ref.js'; * * @beta */ -export interface FunctionRef { +export interface FunctionAnnotation { type: 'function'; kind: 'open' | 'close' | 'value'; name: string; @@ -26,7 +26,7 @@ export interface FunctionRef { } /** - * {@link FunctionRef} options are expressed as + * {@link FunctionAnnotation} options are expressed as * `key`/`value` pairs to allow their order to be maintained. * * @beta @@ -37,15 +37,18 @@ export interface Option { } /** - * Type guard for {@link FunctionRef} pattern elements + * Type guard for {@link FunctionAnnotation} pattern elements * * @beta */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export const isFunctionRef = (part: any): part is FunctionRef => +export const isFunctionAnnotation = (part: any): part is FunctionAnnotation => !!part && typeof part === 'object' && part.type === 'function'; -export function functionRefSource(kind: FunctionRef['kind'], name: string) { +export function functionRefSource( + kind: FunctionAnnotation['kind'], + name: string +) { switch (kind) { case 'open': return `+${name}`; @@ -56,9 +59,9 @@ export function functionRefSource(kind: FunctionRef['kind'], name: string) { } } -export function resolveFunctionRef( +export function resolveFunctionAnnotation( ctx: Context, - { kind, operand, name, options }: FunctionRef + { kind, operand, name, options }: FunctionAnnotation ) { let source: string | undefined; try { diff --git a/packages/mf2-messageformat/src/pattern/index.ts b/packages/mf2-messageformat/src/pattern/index.ts index be157f54..25b4f06b 100644 --- a/packages/mf2-messageformat/src/pattern/index.ts +++ b/packages/mf2-messageformat/src/pattern/index.ts @@ -1,7 +1,10 @@ import type { Context } from '../format-context'; import type { MessageValue } from '../runtime'; -import { FunctionRef, resolveFunctionRef } from './function-ref'; +import { + resolveFunctionAnnotation, + FunctionAnnotation +} from './function-annotation'; import { Literal, resolveLiteral } from './literal'; import { resolveunsupportedAnnotation, @@ -9,7 +12,11 @@ import { } from './unsupported-annotation'; import { resolveVariableRef, VariableRef } from './variable-ref'; -export { isFunctionRef, FunctionRef, Option } from './function-ref'; +export { + isFunctionAnnotation, + FunctionAnnotation, + Option +} from './function-annotation'; export { isLiteral, Literal } from './literal'; export { isUnsupportedAnnotation, @@ -27,10 +34,10 @@ export { * * @beta */ -export interface Expression { +export type Expression = { type: 'expression'; - body: Literal | VariableRef | FunctionRef | UnsupportedAnnotation; -} + body: Literal | VariableRef | FunctionAnnotation | UnsupportedAnnotation; +}; /** * Type guard for {@link Expression} pattern elements @@ -52,7 +59,7 @@ export function resolveExpression( case 'variable': return resolveVariableRef(ctx, body); case 'function': - return resolveFunctionRef(ctx, body); + return resolveFunctionAnnotation(ctx, body); case 'unsupported-annotation': return resolveunsupportedAnnotation(ctx, body); default: diff --git a/packages/mf2-messageformat/src/runtime/index.ts b/packages/mf2-messageformat/src/runtime/index.ts index 2571a54c..c0ca2ea9 100644 --- a/packages/mf2-messageformat/src/runtime/index.ts +++ b/packages/mf2-messageformat/src/runtime/index.ts @@ -68,7 +68,7 @@ export class MessageFunctionContext { } /** - * The runtime function registry available when resolving {@link FunctionRef} elements. + * The runtime function registry available when resolving {@link FunctionAnnotation} elements. * * @beta */ diff --git a/packages/mf2-messageformat/src/stringifier/message.ts b/packages/mf2-messageformat/src/stringifier/message.ts index b88e3768..a3af5e79 100644 --- a/packages/mf2-messageformat/src/stringifier/message.ts +++ b/packages/mf2-messageformat/src/stringifier/message.ts @@ -9,14 +9,14 @@ import { import { MessageFormat } from '../messageformat'; import { Expression, - FunctionRef, + FunctionAnnotation, isLiteral, isVariableRef, Literal, Option, VariableRef } from '../pattern'; -import { functionRefSource } from '../pattern/function-ref'; +import { functionRefSource } from '../pattern/function-annotation.js'; /** * Stringify a message using its syntax representation. @@ -47,7 +47,12 @@ function stringifyDeclaration({ name, value }: Declaration) { return `let $${name} = ${stringifyExpression(value)}\n`; } -function stringifyFunctionRef({ kind, name, operand, options }: FunctionRef) { +function stringifyFunctionAnnotation({ + kind, + name, + operand, + options +}: FunctionAnnotation) { let res: string; switch (operand?.type) { case 'literal': @@ -89,7 +94,7 @@ function stringifyExpression({ body }: Expression) { let res: string; switch (body.type) { case 'function': - res = stringifyFunctionRef(body); + res = stringifyFunctionAnnotation(body); break; case 'literal': res = stringifyLiteral(body); diff --git a/packages/mf2-xliff/src/mf2xliff.ts b/packages/mf2-xliff/src/mf2xliff.ts index 38f3a921..33bcb436 100644 --- a/packages/mf2-xliff/src/mf2xliff.ts +++ b/packages/mf2-xliff/src/mf2xliff.ts @@ -1,6 +1,6 @@ import deepEqual from 'fast-deep-equal'; import { - isFunctionRef, + isFunctionAnnotation, isLiteral, isMessage, isSelectMessage, @@ -265,7 +265,7 @@ function resolveExpression( ): X.MessagePart { if (isLiteral(body) || isVariableRef(body)) return resolveArgument(id, body); - if (isFunctionRef(body)) { + if (isFunctionAnnotation(body)) { const elements: X.MessageFunction['elements'] = []; if (body.options) { for (const { name, value } of body.options) { diff --git a/packages/mf2-xliff/src/xliff2mf.ts b/packages/mf2-xliff/src/xliff2mf.ts index 70c4f1ca..3e80791d 100644 --- a/packages/mf2-xliff/src/xliff2mf.ts +++ b/packages/mf2-xliff/src/xliff2mf.ts @@ -2,7 +2,7 @@ import type * as MF from 'messageformat'; import type { MessageFormatInfo, MessageResourceData } from './index'; import type * as X from './xliff-spec'; -import { isFunctionRef, isLiteral } from 'messageformat'; +import { isFunctionAnnotation, isLiteral } from 'messageformat'; import { parse } from './xliff'; // TODO: Support declarations @@ -201,7 +201,7 @@ function resolveContents( contents: (X.Text | X.InlineElement)[], mf: X.MessageFormat | null ) { - const res: (MF.Literal | MF.VariableRef | MF.FunctionRef)[] = []; + const res: (MF.Literal | MF.VariableRef | MF.FunctionAnnotation)[] = []; for (const ie of contents) { const last = res[res.length - 1]; const part = resolveInlineElement(ie, mf); @@ -217,7 +217,7 @@ const resolveCharCode = (cc: X.CharCode) => function resolveInlineElement( ie: X.Text | X.InlineElement, mf: X.MessageFormat | null -): MF.Literal | MF.VariableRef | MF.FunctionRef { +): MF.Literal | MF.VariableRef | MF.FunctionAnnotation { switch (ie.type) { case 'text': case 'cdata': @@ -230,7 +230,7 @@ function resolveInlineElement( return resolveRef(ie.name, ie.attributes['mf:ref'], mf); case 'pc': { const part = resolveRef(ie.name, ie.attributes['mf:ref'], mf); - if (!isFunctionRef(part)) + if (!isFunctionAnnotation(part)) throw new Error(` is only valid for function values`); const arg = resolveContents(ie.elements, mf); if (arg.length > 1) @@ -238,7 +238,7 @@ function resolveInlineElement( 'Forming function arguments by concatenation is not supported' ); const operand = arg[0]; - if (isFunctionRef(operand)) + if (isFunctionAnnotation(operand)) throw new Error(`A ${operand.type} is not supported here`); part.operand = operand; return part; @@ -255,7 +255,7 @@ function resolveRef( name: string, ref: string | undefined, mf: X.MessageFormat | null -): MF.Literal | MF.VariableRef | MF.FunctionRef { +): MF.Literal | MF.VariableRef | MF.FunctionAnnotation { if (!ref) throw new Error(`Unsupported <${name}> without mf:ref attribute`); if (!mf) throw new Error( @@ -274,14 +274,14 @@ const resolveText = (text: (X.Text | X.CharCode)[]) => function resolvePart( part: X.MessagePart -): MF.Literal | MF.VariableRef | MF.FunctionRef { +): MF.Literal | MF.VariableRef | MF.FunctionAnnotation { switch (part.name) { case 'mf:literal': case 'mf:variable': return resolveArgument(part); case 'mf:function': { - let operand: MF.FunctionRef['operand'] = undefined; + let operand: MF.FunctionAnnotation['operand'] = undefined; const options: MF.Option[] = []; for (const el of part.elements) { if (el.name === 'mf:option') { From 0444f0ab3a489549c9a5dea29fe37ed4e6f47151 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 24 Dec 2023 12:48:17 +0200 Subject: [PATCH 04/44] fix(mf2): Rename pattern/ -> expression/ --- packages/mf2-messageformat/src/data-model.ts | 4 ++-- .../{pattern => expression}/function-annotation.test.ts | 0 .../src/{pattern => expression}/function-annotation.ts | 6 +++--- .../src/{pattern => expression}/index.ts | 2 ++ .../src/{pattern => expression}/literal.test.ts | 0 .../src/{pattern => expression}/literal.ts | 0 .../unsupported-annotation.test.ts | 0 .../src/{pattern => expression}/unsupported-annotation.ts | 0 .../src/{pattern => expression}/value.ts | 0 .../src/{pattern => expression}/variable-ref.ts | 0 .../src/{pattern => expression}/variable.test.ts | 0 packages/mf2-messageformat/src/extra/validate.ts | 2 +- packages/mf2-messageformat/src/format-context.ts | 2 +- packages/mf2-messageformat/src/index.ts | 2 +- packages/mf2-messageformat/src/messageformat.ts | 7 +++++-- packages/mf2-messageformat/src/stringifier/message.ts | 8 ++++---- 16 files changed, 19 insertions(+), 14 deletions(-) rename packages/mf2-messageformat/src/{pattern => expression}/function-annotation.test.ts (100%) rename packages/mf2-messageformat/src/{pattern => expression}/function-annotation.ts (94%) rename packages/mf2-messageformat/src/{pattern => expression}/index.ts (96%) rename packages/mf2-messageformat/src/{pattern => expression}/literal.test.ts (100%) rename packages/mf2-messageformat/src/{pattern => expression}/literal.ts (100%) rename packages/mf2-messageformat/src/{pattern => expression}/unsupported-annotation.test.ts (100%) rename packages/mf2-messageformat/src/{pattern => expression}/unsupported-annotation.ts (100%) rename packages/mf2-messageformat/src/{pattern => expression}/value.ts (100%) rename packages/mf2-messageformat/src/{pattern => expression}/variable-ref.ts (100%) rename packages/mf2-messageformat/src/{pattern => expression}/variable.test.ts (100%) diff --git a/packages/mf2-messageformat/src/data-model.ts b/packages/mf2-messageformat/src/data-model.ts index 55c9e839..6d7c6f3e 100644 --- a/packages/mf2-messageformat/src/data-model.ts +++ b/packages/mf2-messageformat/src/data-model.ts @@ -1,10 +1,10 @@ -import type { Expression, Literal, VariableRef } from './pattern'; +import type { Expression, Literal, VariableRef } from './expression/index.js'; export type { FunctionAnnotation, Option, UnsupportedAnnotation -} from './pattern'; +} from './expression'; export type { Expression, Literal, VariableRef }; /** diff --git a/packages/mf2-messageformat/src/pattern/function-annotation.test.ts b/packages/mf2-messageformat/src/expression/function-annotation.test.ts similarity index 100% rename from packages/mf2-messageformat/src/pattern/function-annotation.test.ts rename to packages/mf2-messageformat/src/expression/function-annotation.test.ts diff --git a/packages/mf2-messageformat/src/pattern/function-annotation.ts b/packages/mf2-messageformat/src/expression/function-annotation.ts similarity index 94% rename from packages/mf2-messageformat/src/pattern/function-annotation.ts rename to packages/mf2-messageformat/src/expression/function-annotation.ts index 4bfe37bf..7d4d8f20 100644 --- a/packages/mf2-messageformat/src/pattern/function-annotation.ts +++ b/packages/mf2-messageformat/src/expression/function-annotation.ts @@ -45,7 +45,7 @@ export interface Option { export const isFunctionAnnotation = (part: any): part is FunctionAnnotation => !!part && typeof part === 'object' && part.type === 'function'; -export function functionRefSource( +export function functionAnnotationSource( kind: FunctionAnnotation['kind'], name: string ) { @@ -72,7 +72,7 @@ export function resolveFunctionAnnotation( } else { fnInput = []; } - source ??= functionRefSource(kind, name); + source ??= functionAnnotationSource(kind, name); switch (kind) { case 'open': @@ -92,7 +92,7 @@ export function resolveFunctionAnnotation( } } catch (error) { ctx.onError(error); - source ??= getValueSource(operand) ?? functionRefSource(kind, name); + source ??= getValueSource(operand) ?? functionAnnotationSource(kind, name); return fallback(source); } } diff --git a/packages/mf2-messageformat/src/pattern/index.ts b/packages/mf2-messageformat/src/expression/index.ts similarity index 96% rename from packages/mf2-messageformat/src/pattern/index.ts rename to packages/mf2-messageformat/src/expression/index.ts index 25b4f06b..1619bffd 100644 --- a/packages/mf2-messageformat/src/pattern/index.ts +++ b/packages/mf2-messageformat/src/expression/index.ts @@ -13,6 +13,7 @@ import { import { resolveVariableRef, VariableRef } from './variable-ref'; export { + functionAnnotationSource, isFunctionAnnotation, FunctionAnnotation, Option @@ -22,6 +23,7 @@ export { isUnsupportedAnnotation, UnsupportedAnnotation } from './unsupported-annotation'; +export { resolveValue } from './value.js'; export { getMessageValue, isVariableRef, diff --git a/packages/mf2-messageformat/src/pattern/literal.test.ts b/packages/mf2-messageformat/src/expression/literal.test.ts similarity index 100% rename from packages/mf2-messageformat/src/pattern/literal.test.ts rename to packages/mf2-messageformat/src/expression/literal.test.ts diff --git a/packages/mf2-messageformat/src/pattern/literal.ts b/packages/mf2-messageformat/src/expression/literal.ts similarity index 100% rename from packages/mf2-messageformat/src/pattern/literal.ts rename to packages/mf2-messageformat/src/expression/literal.ts diff --git a/packages/mf2-messageformat/src/pattern/unsupported-annotation.test.ts b/packages/mf2-messageformat/src/expression/unsupported-annotation.test.ts similarity index 100% rename from packages/mf2-messageformat/src/pattern/unsupported-annotation.test.ts rename to packages/mf2-messageformat/src/expression/unsupported-annotation.test.ts diff --git a/packages/mf2-messageformat/src/pattern/unsupported-annotation.ts b/packages/mf2-messageformat/src/expression/unsupported-annotation.ts similarity index 100% rename from packages/mf2-messageformat/src/pattern/unsupported-annotation.ts rename to packages/mf2-messageformat/src/expression/unsupported-annotation.ts diff --git a/packages/mf2-messageformat/src/pattern/value.ts b/packages/mf2-messageformat/src/expression/value.ts similarity index 100% rename from packages/mf2-messageformat/src/pattern/value.ts rename to packages/mf2-messageformat/src/expression/value.ts diff --git a/packages/mf2-messageformat/src/pattern/variable-ref.ts b/packages/mf2-messageformat/src/expression/variable-ref.ts similarity index 100% rename from packages/mf2-messageformat/src/pattern/variable-ref.ts rename to packages/mf2-messageformat/src/expression/variable-ref.ts diff --git a/packages/mf2-messageformat/src/pattern/variable.test.ts b/packages/mf2-messageformat/src/expression/variable.test.ts similarity index 100% rename from packages/mf2-messageformat/src/pattern/variable.test.ts rename to packages/mf2-messageformat/src/expression/variable.test.ts diff --git a/packages/mf2-messageformat/src/extra/validate.ts b/packages/mf2-messageformat/src/extra/validate.ts index 2f8bcf2a..3486c83a 100644 --- a/packages/mf2-messageformat/src/extra/validate.ts +++ b/packages/mf2-messageformat/src/extra/validate.ts @@ -1,5 +1,5 @@ import type { Message, Pattern } from '../data-model'; -import { isExpression, isFunctionAnnotation } from '../pattern'; +import { isExpression, isFunctionAnnotation } from '../expression/index.js'; import type { MessageFunctions } from '../runtime'; function validateParts(parts: Pattern['body'], functions: MessageFunctions) { diff --git a/packages/mf2-messageformat/src/format-context.ts b/packages/mf2-messageformat/src/format-context.ts index bf373cb8..0d92171e 100644 --- a/packages/mf2-messageformat/src/format-context.ts +++ b/packages/mf2-messageformat/src/format-context.ts @@ -1,4 +1,4 @@ -import type { Expression, Literal, VariableRef } from './pattern'; +import type { Expression, Literal, VariableRef } from './expression'; import type { MessageFunctions, MessageValue } from './runtime'; export interface Context { diff --git a/packages/mf2-messageformat/src/index.ts b/packages/mf2-messageformat/src/index.ts index 48807968..aa6ce1c7 100644 --- a/packages/mf2-messageformat/src/index.ts +++ b/packages/mf2-messageformat/src/index.ts @@ -6,8 +6,8 @@ export { MessageResolutionError, MessageSyntaxError } from './errors.js'; +export * from './expression/index.js'; export { MessageFormat, MessageFormatOptions } from './messageformat.js'; export { stringifyMessage } from './stringifier/message.js'; -export * from './pattern/index.js'; export * from './runtime/index.js'; export { validate } from './extra/validate.js'; // must be after ./messageformat -- but why!? diff --git a/packages/mf2-messageformat/src/messageformat.ts b/packages/mf2-messageformat/src/messageformat.ts index 21b98e64..99988a48 100644 --- a/packages/mf2-messageformat/src/messageformat.ts +++ b/packages/mf2-messageformat/src/messageformat.ts @@ -2,8 +2,11 @@ import { asDataModel, parseMessage } from './cst-parser/index.js'; import { Message } from './data-model'; import type { Context } from './format-context'; import { selectPattern } from './select-pattern.js'; -import { resolveExpression, UnresolvedExpression } from './pattern'; -import { resolveValue } from './pattern/value.js'; +import { + resolveExpression, + resolveValue, + UnresolvedExpression +} from './expression/index.js'; import { defaultFunctions, MessageFunctions, diff --git a/packages/mf2-messageformat/src/stringifier/message.ts b/packages/mf2-messageformat/src/stringifier/message.ts index a3af5e79..f66bbd86 100644 --- a/packages/mf2-messageformat/src/stringifier/message.ts +++ b/packages/mf2-messageformat/src/stringifier/message.ts @@ -6,17 +6,17 @@ import { Message, Pattern } from '../data-model'; -import { MessageFormat } from '../messageformat'; import { Expression, FunctionAnnotation, + functionAnnotationSource, isLiteral, isVariableRef, Literal, Option, VariableRef -} from '../pattern'; -import { functionRefSource } from '../pattern/function-annotation.js'; +} from '../expression/index.js'; +import { MessageFormat } from '../messageformat.js'; /** * Stringify a message using its syntax representation. @@ -64,7 +64,7 @@ function stringifyFunctionAnnotation({ default: res = ''; } - res += functionRefSource(kind, name); + res += functionAnnotationSource(kind, name); if (options) for (const opt of options) res += ' ' + stringifyOption(opt); return res; } From c98b911688f97c94a4703a4afa698f064551f849 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 26 Dec 2023 16:32:16 +0200 Subject: [PATCH 05/44] feat(mf2): Refactor Expression to have arg+annotation rather than body --- packages/mf2-fluent/src/fluent-to-message.ts | 120 ++++++------ packages/mf2-fluent/src/fluent.test.ts | 30 +-- packages/mf2-fluent/src/message-to-fluent.ts | 25 +-- .../mf2-icu-mf1/src/mf1-to-message-data.ts | 57 +++--- .../src/cst-parser/as-data-model.ts | 37 ++-- .../src/expression/function-annotation.ts | 4 +- .../mf2-messageformat/src/expression/index.ts | 36 ++-- .../src/expression/unsupported-annotation.ts | 10 +- .../src/stringifier/message.ts | 42 ++--- packages/mf2-xliff/src/mf2xliff.test.ts | 33 +++- packages/mf2-xliff/src/mf2xliff.ts | 101 +++++----- packages/mf2-xliff/src/xliff-spec.ts | 43 ++++- packages/mf2-xliff/src/xliff2mf.ts | 175 +++++++----------- 13 files changed, 364 insertions(+), 349 deletions(-) diff --git a/packages/mf2-fluent/src/fluent-to-message.ts b/packages/mf2-fluent/src/fluent-to-message.ts index 3e09ba58..7ae13ff8 100644 --- a/packages/mf2-fluent/src/fluent-to-message.ts +++ b/packages/mf2-fluent/src/fluent-to-message.ts @@ -3,9 +3,7 @@ import deepEqual from 'fast-deep-equal'; import { Expression, FunctionAnnotation, - isFunctionAnnotation, Literal, - Option, PatternMessage, SelectMessage, VariableRef, @@ -50,87 +48,96 @@ function findSelectArgs(pattern: Fluent.Pattern): SelectArg[] { return args; } -function expressionToPart( - exp: Fluent.Expression -): Literal | VariableRef | FunctionAnnotation { +function asValue(exp: Fluent.InlineExpression): Literal | VariableRef { switch (exp.type) { case 'NumberLiteral': - return { - type: 'function', - kind: 'value', - name: 'number', - operand: { type: 'literal', value: exp.value } - }; + return { type: 'literal', value: exp.value }; case 'StringLiteral': return { type: 'literal', value: exp.parse().value }; case 'VariableReference': return { type: 'variable', name: exp.id.name }; + default: + throw new Error(`A Fluent ${exp.type} is not supported here.`); + } +} + +function asExpression(exp: Fluent.Expression): Expression { + switch (exp.type) { + case 'NumberLiteral': + return { + type: 'expression', + arg: asValue(exp), + annotation: { type: 'function', kind: 'value', name: 'number' } + }; + case 'StringLiteral': + case 'VariableReference': + return { type: 'expression', arg: asValue(exp) }; case 'FunctionReference': { - const func = exp.id.name.toLowerCase(); + const annotation: FunctionAnnotation = { + type: 'function', + kind: 'value', + name: exp.id.name.toLowerCase() + }; const { positional, named } = exp.arguments; - const args = positional.map(exp => { - const part = expressionToPart(exp); - if (isFunctionAnnotation(part)) - throw new Error(`A Fluent ${exp.type} is not supported here.`); - return part; - }); + const args = positional.map(asValue); if (args.length > 1) { throw new Error(`More than one positional argument is not supported.`); } - const operand = args[0]; - if (named.length === 0) - return { type: 'function', kind: 'value', name: func, operand }; - const options: Option[] = []; - for (const { name, value } of named) { - const quoted = value.type !== 'NumberLiteral'; - const litValue = quoted ? value.parse().value : value.value; - options.push({ - name: name.name, - value: { type: 'literal', value: litValue } - }); + if (named.length > 0) { + annotation.options = []; + for (const { name, value } of named) { + const quoted = value.type !== 'NumberLiteral'; + const litValue = quoted ? value.parse().value : value.value; + annotation.options.push({ + name: name.name, + value: { type: 'literal', value: litValue } + }); + } } - return { type: 'function', kind: 'value', name: func, operand, options }; + return args.length > 0 + ? { type: 'expression', arg: args[0], annotation } + : { type: 'expression', annotation }; } case 'MessageReference': { const msgId = exp.attribute ? `${exp.id.name}.${exp.attribute.name}` : exp.id.name; return { - type: 'function', - kind: 'value', - name: 'message', - operand: { type: 'literal', value: msgId } + type: 'expression', + arg: { type: 'literal', value: msgId }, + annotation: { type: 'function', kind: 'value', name: 'message' } }; } case 'TermReference': { + const annotation: FunctionAnnotation = { + type: 'function', + kind: 'value', + name: 'message' + }; const msgId = exp.attribute ? `-${exp.id.name}.${exp.attribute.name}` : `-${exp.id.name}`; - const operand: Literal = { type: 'literal', value: msgId }; - if (!exp.arguments) - return { type: 'function', kind: 'value', name: 'message', operand }; - - const options: Option[] = []; - for (const { name, value } of exp.arguments.named) { - const quoted = value.type !== 'NumberLiteral'; - const litValue = quoted ? value.parse().value : value.value; - options.push({ - name: name.name, - value: { type: 'literal', value: litValue } - }); + if (exp.arguments?.named.length) { + annotation.options = []; + for (const { name, value } of exp.arguments.named) { + const quoted = value.type !== 'NumberLiteral'; + const litValue = quoted ? value.parse().value : value.value; + annotation.options.push({ + name: name.name, + value: { type: 'literal', value: litValue } + }); + } } return { - type: 'function', - kind: 'value', - name: 'message', - operand, - options + type: 'expression', + arg: { type: 'literal', value: msgId }, + annotation }; } /* istanbul ignore next - never happens */ case 'Placeable': - return expressionToPart(exp.expression); + return asExpression(exp.expression); /* istanbul ignore next - never happens */ default: @@ -139,9 +146,7 @@ function expressionToPart( } const elementToPart = (el: Fluent.PatternElement): string | Expression => - el.type === 'TextElement' - ? el.value - : { type: 'expression', body: expressionToPart(el.expression) }; + el.type === 'TextElement' ? el.value : asExpression(el.expression); function asFluentSelect( el: Fluent.PatternElement @@ -251,10 +256,7 @@ export function fluentToMessage( return { type: 'select', declarations: [], - selectors: args.map(arg => ({ - type: 'expression', - body: expressionToPart(arg.selector) - })), + selectors: args.map(arg => asExpression(arg.selector)), variants }; } diff --git a/packages/mf2-fluent/src/fluent.test.ts b/packages/mf2-fluent/src/fluent.test.ts index f3e06d0c..5e0ac1fd 100644 --- a/packages/mf2-fluent/src/fluent.test.ts +++ b/packages/mf2-fluent/src/fluent.test.ts @@ -502,7 +502,7 @@ describe('formatToParts', () => { 'Foo ', { type: 'expression', - body: { type: 'variable', name: 'num' } + arg: { type: 'variable', name: 'num' } } ] } @@ -741,17 +741,17 @@ describe('messagetoFluent', () => { name: 'local', value: { type: 'expression', - body: { + arg: { type: 'variable', name: 'num' }, + annotation: { type: 'function', name: 'number', - kind: 'value', - operand: { type: 'variable', name: 'num' } + kind: 'value' } } } ], selectors: [ - { type: 'expression', body: { type: 'variable', name: 'local' } } + { type: 'expression', arg: { type: 'variable', name: 'local' } } ], variants: [ { @@ -807,7 +807,7 @@ describe('messagetoFluent', () => { name: 'local', value: { type: 'expression', - body: { type: 'literal', value: '-term' } + arg: { type: 'literal', value: '-term' } } } ], @@ -815,20 +815,20 @@ describe('messagetoFluent', () => { body: [ { type: 'expression', - body: { + arg: { type: 'literal', value: 'msg' }, + annotation: { type: 'function', name: 'message', - kind: 'value', - operand: { type: 'literal', value: 'msg' } + kind: 'value' } }, { type: 'expression', - body: { + arg: { type: 'variable', name: 'local' }, + annotation: { type: 'function', name: 'message', - kind: 'value', - operand: { type: 'variable', name: 'local' } + kind: 'value' } } ] @@ -867,11 +867,11 @@ describe('messagetoFluent', () => { body: [ { type: 'expression', - body: { + arg: { type: 'variable', name: 'input' }, + annotation: { type: 'function', name: 'message', - kind: 'value', - operand: { type: 'variable', name: 'input' } + kind: 'value' } } ] diff --git a/packages/mf2-fluent/src/message-to-fluent.ts b/packages/mf2-fluent/src/message-to-fluent.ts index b700b19b..2cb6df75 100644 --- a/packages/mf2-fluent/src/message-to-fluent.ts +++ b/packages/mf2-fluent/src/message-to-fluent.ts @@ -156,10 +156,11 @@ function patternToFluent(ctx: MsgContext, pattern: Pattern) { function functionRefToFluent( ctx: MsgContext, - { name, operand, options }: FunctionAnnotation + arg: Fluent.InlineExpression | null, + { name, options }: FunctionAnnotation ): Fluent.InlineExpression { const args = new Fluent.CallArguments(); - if (operand) args.positional[0] = valueToFluent(ctx, operand); + if (arg) args.positional[0] = arg; if (options) { args.named = options.map(opt => { const va = valueToFluent(ctx, opt.value); @@ -221,20 +222,20 @@ function literalToFluent({ value }: Literal) { function expressionToFluent( ctx: MsgContext, - { body }: Expression + { arg, annotation }: Expression ): Fluent.InlineExpression { - switch (body.type) { - case 'function': - return functionRefToFluent(ctx, body); - case 'literal': - return new Fluent.StringLiteral(body.value); - case 'variable': - return variableRefToFluent(ctx, body); - default: + const fluentArg = arg ? valueToFluent(ctx, arg) : null; + if (annotation) { + if (annotation.type === 'function') { + return functionRefToFluent(ctx, fluentArg, annotation); + } else { throw new Error( - `Conversion of "${body.type}" expression to Fluent is not supported` + `Conversion of "${annotation.type}" expression to Fluent is not supported` ); + } } + if (fluentArg) return fluentArg; + throw new Error('Invalid empty expression'); } function valueToFluent( diff --git a/packages/mf2-icu-mf1/src/mf1-to-message-data.ts b/packages/mf2-icu-mf1/src/mf1-to-message-data.ts index 58851c34..2506aa7a 100644 --- a/packages/mf2-icu-mf1/src/mf1-to-message-data.ts +++ b/packages/mf2-icu-mf1/src/mf1-to-message-data.ts @@ -61,14 +61,13 @@ function tokenToPart( case 'argument': return { type: 'expression', - body: { type: 'variable', name: token.arg } + arg: { type: 'variable', name: token.arg } }; case 'function': { - const body: FunctionAnnotation = { + const annotation: FunctionAnnotation = { type: 'function', kind: 'value', - name: token.key, - operand: { type: 'variable', name: token.arg } + name: token.key }; if (token.param && token.param.length > 0) { let value = ''; @@ -76,26 +75,35 @@ function tokenToPart( if (pt.type === 'content') value += pt.value; else throw new Error(`Unsupported param type: ${pt.type}`); } - body.options = [{ name: 'param', value: { type: 'literal', value } }]; + annotation.options = [ + { name: 'param', value: { type: 'literal', value } } + ]; } - return { type: 'expression', body }; + return { + type: 'expression', + arg: { type: 'variable', name: token.arg }, + annotation + }; } case 'octothorpe': { if (!pluralArg) return '#'; - const body: FunctionAnnotation = { + const annotation: FunctionAnnotation = { type: 'function', kind: 'value', - name: 'number', - operand: { type: 'variable', name: pluralArg } + name: 'number' }; if (pluralOffset) - body.options = [ + annotation.options = [ { name: 'pluralOffset', value: { type: 'literal', value: String(pluralOffset) } } ]; - return { type: 'expression', body }; + return { + type: 'expression', + arg: { type: 'variable', name: pluralArg }, + annotation + }; } /* istanbul ignore next - never happens */ default: @@ -103,12 +111,17 @@ function tokenToPart( } } -function argToExpression({ arg, pluralOffset, type }: SelectArg): Expression { - const argVar: VariableRef = { type: 'variable', name: arg }; +function argToExpression({ + arg: selName, + pluralOffset, + type +}: SelectArg): Expression { + const arg: VariableRef = { type: 'variable', name: selName }; if (type === 'select') return { type: 'expression', - body: { type: 'function', kind: 'value', name: 'string', operand: argVar } + arg, + annotation: { type: 'function', kind: 'value', name: 'string' } }; const options: Option[] = []; @@ -125,16 +138,14 @@ function argToExpression({ arg, pluralOffset, type }: SelectArg): Expression { }); } - return { - type: 'expression', - body: { - type: 'function', - kind: 'value', - name: 'number', - operand: argVar, - options - } + const annotation: FunctionAnnotation = { + type: 'function', + kind: 'value', + name: 'number' }; + if (options.length) annotation.options = options; + + return { type: 'expression', arg, annotation }; } /** diff --git a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts index deb8d9b3..0c75b763 100644 --- a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts +++ b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts @@ -48,54 +48,45 @@ function asExpression(cst: CST.Expression | CST.Junk): Model.Expression { if (cst.type !== 'expression') { throw new MessageSyntaxError('parse-error', cst.start, cst.end); } - let body: - | Model.Literal - | Model.VariableRef - | Model.FunctionAnnotation - | Model.UnsupportedAnnotation; switch (cst.body.type) { case 'literal': case 'variable': - body = asValue(cst.body); - break; + return { type: 'expression', arg: asValue(cst.body) }; case 'function': - body = asFunctionAnnotation(cst.body); - break; + return asFunctionExpression(cst.body); case 'reserved': - body = asUnsupportedAnnotation(cst.body); - break; + return asUnsupportedExpression(cst.body); default: throw new MessageSyntaxError('parse-error', cst.start, cst.end); } - return { type: 'expression', body }; } -function asFunctionAnnotation(cst: CST.FunctionRef): Model.FunctionAnnotation { - const fn: Model.FunctionAnnotation = { +function asFunctionExpression(cst: CST.FunctionRef): Model.Expression { + const annotation: Model.FunctionAnnotation = { type: 'function', kind: cst.kind, name: cst.name }; - if (cst.operand) fn.operand = asValue(cst.operand); if (cst.options && cst.options.length > 0) { - fn.options = cst.options.map(opt => ({ + annotation.options = cst.options.map(opt => ({ name: opt.name, value: asValue(opt.value) })); } - return fn; + return cst.operand + ? { type: 'expression', arg: asValue(cst.operand), annotation } + : { type: 'expression', annotation }; } -function asUnsupportedAnnotation( - cst: CST.Reserved -): Model.UnsupportedAnnotation { - const res: Model.UnsupportedAnnotation = { +function asUnsupportedExpression(cst: CST.Reserved): Model.Expression { + const annotation: Model.UnsupportedAnnotation = { type: 'unsupported-annotation', sigil: cst.sigil, source: cst.source }; - if (cst.operand) res.operand = asValue(cst.operand); - return res; + return cst.operand + ? { type: 'expression', arg: asValue(cst.operand), annotation } + : { type: 'expression', annotation }; } function asValue(cst: CST.Literal | CST.Junk): Model.Literal; diff --git a/packages/mf2-messageformat/src/expression/function-annotation.ts b/packages/mf2-messageformat/src/expression/function-annotation.ts index 7d4d8f20..5baaf98f 100644 --- a/packages/mf2-messageformat/src/expression/function-annotation.ts +++ b/packages/mf2-messageformat/src/expression/function-annotation.ts @@ -21,7 +21,6 @@ export interface FunctionAnnotation { type: 'function'; kind: 'open' | 'close' | 'value'; name: string; - operand?: Literal | VariableRef; options?: Option[]; } @@ -61,7 +60,8 @@ export function functionAnnotationSource( export function resolveFunctionAnnotation( ctx: Context, - { kind, operand, name, options }: FunctionAnnotation + operand: Literal | VariableRef | undefined, + { kind, name, options }: FunctionAnnotation ) { let source: string | undefined; try { diff --git a/packages/mf2-messageformat/src/expression/index.ts b/packages/mf2-messageformat/src/expression/index.ts index 1619bffd..07573fb4 100644 --- a/packages/mf2-messageformat/src/expression/index.ts +++ b/packages/mf2-messageformat/src/expression/index.ts @@ -7,7 +7,7 @@ import { } from './function-annotation'; import { Literal, resolveLiteral } from './literal'; import { - resolveunsupportedAnnotation, + resolveUnsupportedAnnotation, UnsupportedAnnotation } from './unsupported-annotation'; import { resolveVariableRef, VariableRef } from './variable-ref'; @@ -36,10 +36,17 @@ export { * * @beta */ -export type Expression = { - type: 'expression'; - body: Literal | VariableRef | FunctionAnnotation | UnsupportedAnnotation; -}; +export type Expression = + | { + type: 'expression'; + arg: Literal | VariableRef; + annotation?: FunctionAnnotation | UnsupportedAnnotation; + } + | { + type: 'expression'; + arg?: never; + annotation: FunctionAnnotation | UnsupportedAnnotation; + }; /** * Type guard for {@link Expression} pattern elements @@ -53,19 +60,20 @@ export const isExpression = (part: any): part is Expression => /** @internal */ export function resolveExpression( ctx: Context, - { body }: Expression + { arg, annotation }: Expression ): MessageValue { - switch (body.type) { + if (annotation) { + return annotation.type === 'function' + ? resolveFunctionAnnotation(ctx, arg, annotation) + : resolveUnsupportedAnnotation(ctx, arg, annotation); + } + switch (arg?.type) { case 'literal': - return resolveLiteral(ctx, body); + return resolveLiteral(ctx, arg); case 'variable': - return resolveVariableRef(ctx, body); - case 'function': - return resolveFunctionAnnotation(ctx, body); - case 'unsupported-annotation': - return resolveunsupportedAnnotation(ctx, body); + return resolveVariableRef(ctx, arg); default: // @ts-expect-error - should never happen - throw new Error(`Unsupported expression: ${body.type}`); + throw new Error(`Unsupported expression: ${arg?.type}`); } } diff --git a/packages/mf2-messageformat/src/expression/unsupported-annotation.ts b/packages/mf2-messageformat/src/expression/unsupported-annotation.ts index 651e93e1..40b3e22c 100644 --- a/packages/mf2-messageformat/src/expression/unsupported-annotation.ts +++ b/packages/mf2-messageformat/src/expression/unsupported-annotation.ts @@ -17,10 +17,9 @@ import type { VariableRef } from './variable-ref.js'; */ export interface UnsupportedAnnotation { type: 'unsupported-annotation'; - sigil: '!' | '@' | '#' | '%' | '^' | '&' | '*' | '<' | '>' | '?' | '~'; + sigil: '!' | '@' | '#' | '%' | '^' | '&' | '*' | '<' | '>' | '?' | '~' | '�'; source: string; name?: never; - operand?: Literal | VariableRef; } /** @@ -34,11 +33,12 @@ export const isUnsupportedAnnotation = ( ): part is UnsupportedAnnotation => !!part && typeof part === 'object' && part.type === 'unsupported-annotation'; -export function resolveunsupportedAnnotation( +export function resolveUnsupportedAnnotation( ctx: Context, - { operand, sigil, source }: UnsupportedAnnotation + operand: Literal | VariableRef | undefined, + { sigil, source = '�' }: UnsupportedAnnotation ) { - const msg = `Reserved ${sigil} annotation is not supported`; + const msg = `Reserved ${sigil ?? '�'} annotation is not supported`; ctx.onError( new MessageResolutionError('unsupported-annotation', msg, source) ); diff --git a/packages/mf2-messageformat/src/stringifier/message.ts b/packages/mf2-messageformat/src/stringifier/message.ts index f66bbd86..b86a7d3d 100644 --- a/packages/mf2-messageformat/src/stringifier/message.ts +++ b/packages/mf2-messageformat/src/stringifier/message.ts @@ -14,6 +14,7 @@ import { isVariableRef, Literal, Option, + UnsupportedAnnotation, VariableRef } from '../expression/index.js'; import { MessageFormat } from '../messageformat.js'; @@ -50,21 +51,9 @@ function stringifyDeclaration({ name, value }: Declaration) { function stringifyFunctionAnnotation({ kind, name, - operand, options }: FunctionAnnotation) { - let res: string; - switch (operand?.type) { - case 'literal': - res = stringifyLiteral(operand) + ' '; - break; - case 'variable': - res = stringifyVariableRef(operand) + ' '; - break; - default: - res = ''; - } - res += functionAnnotationSource(kind, name); + let res = functionAnnotationSource(kind, name); if (options) for (const opt of options) res += ' ' + stringifyOption(opt); return res; } @@ -90,24 +79,35 @@ function stringifyPattern({ body }: Pattern) { return `{${res}}`; } -function stringifyExpression({ body }: Expression) { +function stringifyExpression({ arg, annotation }: Expression) { let res: string; - switch (body.type) { - case 'function': - res = stringifyFunctionAnnotation(body); - break; + switch (arg?.type) { case 'literal': - res = stringifyLiteral(body); + res = stringifyLiteral(arg); break; case 'variable': - res = stringifyVariableRef(body); + res = stringifyVariableRef(arg); break; default: - res = ''; // bad expression + res = ''; + } + if (annotation) { + if (res) res += ' '; + res += + annotation.type === 'function' + ? stringifyFunctionAnnotation(annotation) + : stringifyUnsupportedAnnotation(annotation); } return `{${res}}`; } +function stringifyUnsupportedAnnotation({ + sigil, + source = '�' +}: UnsupportedAnnotation) { + return (sigil ?? '�') + source; +} + function stringifyVariableRef(ref: VariableRef) { return '$' + ref.name; } diff --git a/packages/mf2-xliff/src/mf2xliff.test.ts b/packages/mf2-xliff/src/mf2xliff.test.ts index 6290db4d..fae38f4b 100644 --- a/packages/mf2-xliff/src/mf2xliff.test.ts +++ b/packages/mf2-xliff/src/mf2xliff.test.ts @@ -27,7 +27,9 @@ test('source only', () => { - + + + Foo· @@ -39,9 +41,10 @@ test('source only', () => { - + msg - + + This is the· @@ -53,7 +56,9 @@ test('source only', () => { - + + + @@ -112,8 +117,12 @@ test('combine source & target', () => { - - + + + + + + Foo· @@ -128,7 +137,9 @@ test('combine source & target', () => { - + + + @@ -178,8 +189,12 @@ test('selector mismatch between source & target languages', () => { - - + + + + + + diff --git a/packages/mf2-xliff/src/mf2xliff.ts b/packages/mf2-xliff/src/mf2xliff.ts index 33bcb436..e23ee176 100644 --- a/packages/mf2-xliff/src/mf2xliff.ts +++ b/packages/mf2-xliff/src/mf2xliff.ts @@ -119,14 +119,14 @@ function resolveSelect( } const select: { id: string; keys: string[] }[] = []; - const parts: X.MessagePart[] = srcSel.selectors.map(sel => { + const expressions: X.MessageExpression[] = srcSel.selectors.map(sel => { const id = nextId(); select.push({ id, keys: [] }); return resolveExpression(id, sel); }); const elements: (X.MessageFormat | X.Unit)[] = [ - { type: 'element', name: 'mf:messageformat', elements: parts } + { type: 'element', name: 'mf:messageformat', elements: expressions } ]; if (!trgSel) { @@ -147,7 +147,7 @@ function resolveSelect( const id = nextId(); select.push({ id, keys: [] }); trgSelMap.push(select.length - 1); - parts.push(resolveExpression(id, sel)); + expressions.push(resolveExpression(id, sel)); } } @@ -229,12 +229,12 @@ function resolvePattern( srcPattern: MF.Pattern, trgPattern: MF.Pattern | undefined ): X.Unit { - const parts: X.MessagePart[] = []; + const expressions: X.MessageExpression[] = []; const handlePart = (p: string | MF.Expression): X.Text | X.InlineElement => { if (typeof p === 'string') return asText(p); const id = nextId(); - const part = resolveExpression(id, p); - parts.push(part); + const exp = resolveExpression(id, p); + expressions.push(exp); const attributes = { id: id.substring(1), 'mf:ref': id }; return { type: 'element', name: 'ph', attributes }; }; @@ -251,62 +251,73 @@ function resolvePattern( const attributes = msgAttributes('u', key); let elements: X.Unit['elements']; - if (parts.length > 0) { + if (expressions.length > 0) { const name = 'mf:messageformat'; - const mf: X.MessageFormat = { type: 'element', name, elements: parts }; + const mf: X.MessageFormat = { + type: 'element', + name, + elements: expressions + }; elements = [mf, segment]; } else elements = [segment]; return { type: 'element', name: 'unit', attributes, elements }; } function resolveExpression( - id: string | null, - { body }: MF.Expression -): X.MessagePart { - if (isLiteral(body) || isVariableRef(body)) return resolveArgument(id, body); - - if (isFunctionAnnotation(body)) { - const elements: X.MessageFunction['elements'] = []; - if (body.options) { - for (const { name, value } of body.options) { - elements.push({ - type: 'element', - name: 'mf:option', - attributes: { name }, - elements: [resolveArgument(null, value)] - }); + id: string, + { arg, annotation }: MF.Expression +): X.MessageExpression { + let resFunc: X.MessageFunction | X.MessageUnsupported | undefined; + if (annotation) { + if (isFunctionAnnotation(annotation)) { + const elements: X.MessageFunction['elements'] = []; + if (annotation.options) { + for (const { name, value } of annotation.options) { + elements.push({ + type: 'element', + name: 'mf:option', + attributes: { name }, + elements: [resolveArgument(value)] + }); + } } + const attributes = { name: annotation.name }; + resFunc = { type: 'element', name: 'mf:function', attributes, elements }; + } else { + resFunc = { + type: 'element', + name: 'mf:unsupported', + attributes: { sigil: annotation.sigil ?? '�' }, + elements: [asText(annotation.source ?? '�')] + }; } - if (body.operand) elements.push(resolveArgument(null, body.operand)); - const attributes = { id: id ?? undefined, name: body.name }; - return { type: 'element', name: 'mf:function', attributes, elements }; } - /* istanbul ignore next - never happens */ - throw new Error(`Unsupported part: ${JSON.stringify(body)}`); + let elements: X.MessageExpression['elements']; + if (arg) { + const resArg = resolveArgument(arg); + elements = resFunc ? [resArg, resFunc] : [resArg]; + } else if (resFunc) { + elements = [resFunc]; + } else { + throw new Error('Invalid empty expression'); + } + + return { + type: 'element', + name: 'mf:expression', + attributes: { id }, + elements + }; } function resolveArgument( - id: string | null, - part: MF.Literal | string | number | boolean -): X.MessageLiteral; -function resolveArgument( - id: string | null, - part: MF.VariableRef -): X.MessageVariable; -function resolveArgument( - id: string | null, - part: MF.Literal | MF.VariableRef | string | number | boolean -): X.MessageLiteral | X.MessageVariable; -function resolveArgument( - id: string | null, - part: MF.Literal | MF.VariableRef | string | number | boolean + part: MF.Literal | MF.VariableRef ): X.MessageLiteral | X.MessageVariable { - if (isLiteral(part) || typeof part !== 'object') { + if (isLiteral(part)) { return { type: 'element', name: 'mf:literal', - attributes: { id: id ?? undefined }, elements: [asText(part)] }; } @@ -315,7 +326,7 @@ function resolveArgument( return { type: 'element', name: 'mf:variable', - attributes: { id: id ?? undefined, name: part.name } + attributes: { name: part.name } }; } diff --git a/packages/mf2-xliff/src/xliff-spec.ts b/packages/mf2-xliff/src/xliff-spec.ts index 76e4b628..574e7ada 100644 --- a/packages/mf2-xliff/src/xliff-spec.ts +++ b/packages/mf2-xliff/src/xliff-spec.ts @@ -1170,18 +1170,25 @@ export interface ValidationRule extends Element { * MessageFormat Module */ -export type MessagePart = MessageLiteral | MessageFunction | MessageVariable; - export interface MessageFormat extends Element { name: 'mf:messageformat'; - elements: MessagePart[]; + elements: MessageExpression[]; +} + +export interface MessageExpression extends Element { + name: 'mf:expression'; + attributes: { + id: string; + }; + elements: + | [MessageLiteral | MessageVariable] + | [MessageLiteral | MessageVariable, MessageFunction | MessageUnsupported] + | [MessageFunction | MessageUnsupported]; } export interface MessageLiteral extends Element { name: 'mf:literal'; attributes?: { - id?: string; - /** * Directionality of content: `ltr` (Left-To-Right), `rtl` (Right-To-Left), * or `auto` (determined heuristically, based on the first strong @@ -1204,10 +1211,9 @@ export interface MessageLiteral extends Element { export interface MessageFunction extends Element { name: 'mf:function'; attributes: { - id?: string; name: string; }; - elements: (MessageOption | MessageLiteral | MessageVariable)[]; + elements: MessageOption[]; } export interface MessageOption extends Element { @@ -1216,13 +1222,32 @@ export interface MessageOption extends Element { name: string; values?: string; }; - elements: (MessageLiteral | MessageVariable)[]; + elements: [MessageLiteral | MessageVariable]; +} + +export interface MessageUnsupported extends Element { + name: 'mf:unsupported'; + attributes: { + sigil: + | '!' + | '@' + | '#' + | '%' + | '^' + | '&' + | '*' + | '<' + | '>' + | '?' + | '~' + | '�'; + }; + elements: (Text | CharCode)[]; } export interface MessageVariable extends Element { name: 'mf:variable'; attributes: { - id?: string; name: string; }; } diff --git a/packages/mf2-xliff/src/xliff2mf.ts b/packages/mf2-xliff/src/xliff2mf.ts index 3e80791d..07f75be7 100644 --- a/packages/mf2-xliff/src/xliff2mf.ts +++ b/packages/mf2-xliff/src/xliff2mf.ts @@ -2,7 +2,6 @@ import type * as MF from 'messageformat'; import type { MessageFormatInfo, MessageResourceData } from './index'; import type * as X from './xliff-spec'; -import { isFunctionAnnotation, isLiteral } from 'messageformat'; import { parse } from './xliff'; // TODO: Support declarations @@ -150,12 +149,12 @@ function resolveSelect( throw new Error(` not found in ${el}`); } const selectors = idList(attributes['mf:select']).map(selId => { - const part = mf?.elements.find(part => part.attributes?.id === selId); - if (!part) { + const exp = mf?.elements.find(exp => exp.attributes?.id === selId); + if (!exp) { const el = prettyElement('group', attributes.id); throw new Error(`Selector ${selId} not found in ${el}`); } - return { type: 'expression' as const, body: resolvePart(part) }; + return resolveExpression(exp); }); return { type: 'select', declarations: [], selectors, variants }; } @@ -182,12 +181,14 @@ function resolvePattern( ); } if (stel) { - for (const part of resolveContents(stel.elements, mf)) { - body.push( - part.type === 'literal' - ? part.value - : { type: 'expression', body: part } - ); + for (const ie of stel.elements) { + const last = body[body.length - 1]; + const next = resolveInlineElement(ie, mf); + if (typeof last === 'string' && typeof next === 'string') { + body[body.length - 1] += next; + } else { + body.push(next); + } } } break; @@ -197,52 +198,24 @@ function resolvePattern( return { body }; } -function resolveContents( - contents: (X.Text | X.InlineElement)[], - mf: X.MessageFormat | null -) { - const res: (MF.Literal | MF.VariableRef | MF.FunctionAnnotation)[] = []; - for (const ie of contents) { - const last = res[res.length - 1]; - const part = resolveInlineElement(ie, mf); - if (isLiteral(last) && isLiteral(part)) last.value += part; - else res.push(part); - } - return res; -} - const resolveCharCode = (cc: X.CharCode) => String.fromCodePoint(Number(cc.attributes.hex)); function resolveInlineElement( ie: X.Text | X.InlineElement, mf: X.MessageFormat | null -): MF.Literal | MF.VariableRef | MF.FunctionAnnotation { +): string | MF.Expression { switch (ie.type) { case 'text': case 'cdata': - return { type: 'literal', value: ie.text }; + return ie.text; case 'element': switch (ie.name) { case 'cp': - return { type: 'literal', value: resolveCharCode(ie) }; + return resolveCharCode(ie); case 'ph': - return resolveRef(ie.name, ie.attributes['mf:ref'], mf); - case 'pc': { - const part = resolveRef(ie.name, ie.attributes['mf:ref'], mf); - if (!isFunctionAnnotation(part)) - throw new Error(` is only valid for function values`); - const arg = resolveContents(ie.elements, mf); - if (arg.length > 1) - throw new Error( - 'Forming function arguments by concatenation is not supported' - ); - const operand = arg[0]; - if (isFunctionAnnotation(operand)) - throw new Error(`A ${operand.type} is not supported here`); - part.operand = operand; - return part; - } + return resolveRef(mf, ie.attributes['mf:ref']); + case 'pc': case 'sc': case 'ec': // TODO @@ -252,98 +225,76 @@ function resolveInlineElement( } function resolveRef( - name: string, - ref: string | undefined, - mf: X.MessageFormat | null -): MF.Literal | MF.VariableRef | MF.FunctionAnnotation { - if (!ref) throw new Error(`Unsupported <${name}> without mf:ref attribute`); + mf: X.MessageFormat | null, + ref: string | undefined +): MF.Expression { + if (!ref) throw new Error('Unsupported without mf:ref attribute'); if (!mf) throw new Error( - `Inline <${name}> requires a preceding in the same ` + 'Inline requires a preceding in the same ' ); const res = mf.elements.find(el => el.attributes?.id === ref); if (!res) - throw new Error( - `MessageFormat value not found for <${name} mf:ref="${ref}">` - ); - return resolvePart(res); + throw new Error(`MessageFormat value not found for `); + return resolveExpression(res); } const resolveText = (text: (X.Text | X.CharCode)[]) => text.map(t => (t.type === 'element' ? resolveCharCode(t) : t.text)).join(''); -function resolvePart( - part: X.MessagePart -): MF.Literal | MF.VariableRef | MF.FunctionAnnotation { - switch (part.name) { +function resolveExpression(part: X.MessageExpression): MF.Expression { + const xArg = part.elements[0]; + let xFunc; + let arg: MF.Literal | MF.VariableRef | undefined; + switch (xArg.name) { case 'mf:literal': case 'mf:variable': - return resolveArgument(part); - - case 'mf:function': { - let operand: MF.FunctionAnnotation['operand'] = undefined; - const options: MF.Option[] = []; - for (const el of part.elements) { - if (el.name === 'mf:option') { - options.push({ name: el.attributes.name, value: resolveOption(el) }); - } else if (operand) { - throw new Error( - `More than one positional argument is not supported.` - ); - } else { - operand = resolveArgument(el); - } - } + arg = resolveValue(xArg); + xFunc = part.elements[1]; + if (!xFunc) return { type: 'expression', arg }; + break; + default: + xFunc = xArg; + if (!xFunc) throw new Error('Invalid empty expression'); + } - return { - type: 'function', - kind: 'value', - name: part.attributes.name, - operand, - options - }; + let annotation: MF.FunctionAnnotation | MF.UnsupportedAnnotation; + if (xFunc.name === 'mf:function') { + annotation = { + type: 'function', + kind: 'value', + name: xFunc.attributes.name + }; + if (xFunc.elements.length) { + annotation.options = xFunc.elements.map(el => ({ + name: el.attributes.name, + value: resolveValue(el.elements[0]) + })); } + } else { + annotation = { + type: 'unsupported-annotation', + sigil: xFunc.attributes.sigil ?? '�', + source: resolveText(xFunc.elements) + }; } - /* istanbul ignore next - never happens */ - throw new Error( - `Unsupported part ${(part as X.MessagePart).type} <${ - (part as X.MessagePart).name - }>` - ); + return arg + ? { type: 'expression', arg, annotation } + : { type: 'expression', annotation }; } -function resolveArgument(part: X.MessageLiteral): MF.Literal; -function resolveArgument(part: X.MessageVariable): MF.VariableRef; -function resolveArgument(part: X.MessagePart): MF.Literal | MF.VariableRef; -function resolveArgument(part: X.MessagePart): MF.Literal | MF.VariableRef { +function resolveValue( + part: X.MessageLiteral | X.MessageVariable +): MF.Literal | MF.VariableRef { switch (part.name) { case 'mf:literal': - return { - type: 'literal', - value: resolveText(part.elements) - }; - + return { type: 'literal', value: resolveText(part.elements) }; case 'mf:variable': return { type: 'variable', name: part.attributes.name }; } /* istanbul ignore next - never happens */ - throw new Error( - `Unsupported argument ${(part as X.MessagePart).type} <${ - (part as X.MessagePart).name - }>` - ); -} - -function resolveOption(opt: X.MessageOption): MF.Literal | MF.VariableRef { - const sv = opt.elements.map(resolveArgument); - switch (sv.length) { - case 0: - return { type: 'literal', value: '' }; - case 1: - return sv[0]; - default: - throw new Error('Options may only have one value'); - } + // @ts-expect-error - Guard against unexpected contents + throw new Error(`Unsupported value <${part.name}>`); } From ac2cc2fdec81d70e811896feec9ff7bb9ffaab30 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Thu, 28 Dec 2023 13:08:17 +0100 Subject: [PATCH 06/44] feat(mf2): Update syntax to .let & .match, with {{doubled pattern braces}} --- .../src/__fixtures/test-messages.json | 167 +++++++++--------- .../src/cst-parser/as-data-model.ts | 49 +++-- .../src/cst-parser/cst-parser.test.ts | 49 +++-- .../src/cst-parser/cst-types.ts | 30 ++-- .../src/cst-parser/declarations.ts | 16 +- .../src/cst-parser/expression.ts | 6 +- .../src/cst-parser/message.ts | 87 +++++---- .../src/cst-parser/values.ts | 2 +- packages/mf2-messageformat/src/errors.ts | 22 +-- .../expression/function-annotation.test.ts | 29 ++- .../src/expression/literal.test.ts | 8 +- .../expression/unsupported-annotation.test.ts | 10 +- .../src/expression/variable.test.ts | 4 +- .../mf2-messageformat/src/extra/validate.ts | 11 +- .../src/mf2-features.test.ts | 30 ++-- .../src/stringifier/message.ts | 17 +- 16 files changed, 281 insertions(+), 256 deletions(-) diff --git a/packages/mf2-messageformat/src/__fixtures/test-messages.json b/packages/mf2-messageformat/src/__fixtures/test-messages.json index 2d9bcbd1..1a88fb06 100644 --- a/packages/mf2-messageformat/src/__fixtures/test-messages.json +++ b/packages/mf2-messageformat/src/__fixtures/test-messages.json @@ -1,233 +1,233 @@ [ - { "src": "{hello}", "exp": "hello" }, - { "src": "{hello {|world|}}", "exp": "hello world" }, - { "src": "{hello {||}}", "exp": "hello " }, + { "src": "hello", "exp": "hello" }, + { "src": "hello {|world|}", "exp": "hello world" }, + { "src": "hello {||}", "exp": "hello " }, { - "src": "{hello {$place}}", + "src": "hello {$place}", "params": { "place": "world" }, "exp": "hello world" }, { - "src": "{hello {$place:-.}}", + "src": "hello {$place:-.}", "params": { "place:-.": "world" }, "exp": "hello world" }, { - "src": "{hello {$place}}", + "src": "hello {$place}", "errors": [{ "type": "unresolved-var" }], "exp": "hello {$place}" }, { - "src": "{{$one} and {$two}}", + "src": "{$one} and {$two}", "params": { "one": 1.3, "two": 4.2 }, "exp": "1.3 and 4.2" }, { - "src": "{{$one} et {$two}}", + "src": "{$one} et {$two}", "locale": "fr", "params": { "one": 1.3, "two": 4.2 }, "exp": "1,3 et 4,2" }, - { "src": "{hello {|4.2| :number}}", "exp": "hello 4.2" }, + { "src": "hello {|4.2| :number}", "exp": "hello 4.2" }, { - "src": "{hello {foo :number}}", + "src": "hello {foo :number}", "exp": "hello {|foo|}", "errors": [{ "type": "bad-input" }] }, { - "src": "{hello {:number}}", + "src": "hello {:number}", "exp": "hello {:number}", "errors": [{ "type": "bad-input" }] }, { - "src": "{hello {|4.2| :number minimumFractionDigits=2}}", + "src": "hello {|4.2| :number minimumFractionDigits=2}", "exp": "hello 4.20" }, { - "src": "{hello {|4.2| :number minimumFractionDigits=|2|}}", + "src": "hello {|4.2| :number minimumFractionDigits=|2|}", "exp": "hello 4.20" }, { - "src": "{hello {|4.2| :number minimumFractionDigits=$foo}}", + "src": "hello {|4.2| :number minimumFractionDigits=$foo}", "params": { "foo": 2 }, "exp": "hello 4.20" }, { - "src": "{hello {|4.2| :number minimumFractionDigits=$foo}}", + "src": "hello {|4.2| :number minimumFractionDigits=$foo}", "params": { "foo": "2" }, "exp": "hello 4.20" }, { - "src": "let $foo = {bar} {bar {$foo}}", + "src": ".let $foo = {bar} {{bar {$foo}}}", "exp": "bar bar" }, { - "src": "let $foo = {|bar|} {bar {$foo}}", + "src": ".let $foo = {|bar|} {{bar {$foo}}}", "exp": "bar bar" }, { - "src": "let $foo = {|bar|} {bar {$foo}}", + "src": ".let $foo = {|bar|} {{bar {$foo}}}", "params": { "foo": "foo" }, "exp": "bar bar" }, { - "src": "let $foo = {$bar} {bar {$foo}}", + "src": ".let $foo = {$bar} {{bar {$foo}}}", "params": { "bar": "foo" }, "exp": "bar foo" }, { - "src": "let $foo = {$bar :number} {bar {$foo}}", + "src": ".let $foo = {$bar :number} {{bar {$foo}}}", "params": { "bar": 4.2 }, "exp": "bar 4.2" }, { - "src": "let $foo = {$bar :number minimumFractionDigits=2} {bar {$foo}}", + "src": ".let $foo = {$bar :number minimumFractionDigits=2} {{bar {$foo}}}", "params": { "bar": 4.2 }, "exp": "bar 4.20" }, { - "src": "let $foo = {$bar :number minimumFractionDigits=foo} {bar {$foo}}", + "src": ".let $foo = {$bar :number minimumFractionDigits=foo} {{bar {$foo}}}", "params": { "bar": 4.2 }, "exp": "bar {$bar}", "errors": [{ "type": "bad-option" }] }, { - "src": "let $foo = {$bar :number} {bar {$foo}}", + "src": ".let $foo = {$bar :number} {{bar {$foo}}}", "params": { "bar": "foo" }, "exp": "bar {$bar}", "errors": [{ "type": "bad-input" }] }, { - "src": "let $foo = {$baz} let $bar = {$foo} {bar {$bar}}", + "src": ".let $foo = {$baz} .let $bar = {$foo} {{bar {$bar}}}", "params": { "baz": "foo" }, "exp": "bar foo" }, { - "src": "let $foo = {$foo} {bar {$foo}}", + "src": ".let $foo = {$foo} {{bar {$foo}}}", "params": { "foo": "foo" }, "exp": "bar foo" }, { - "src": "let $foo = {$foo} let $foo = {42} {bar {$foo}}", + "src": ".let $foo = {$foo} .let $foo = {42} {{bar {$foo}}}", "params": { "foo": "foo" }, "exp": "bar 42" }, { - "src": "let $foo = {42} let $foo = {$foo} {bar {$foo}}", + "src": ".let $foo = {42} .let $foo = {$foo} {{bar {$foo}}}", "params": { "foo": "foo" }, "exp": "bar 42" }, { - "src": "let $foo = {$foo} let $foo = {42} {bar {$foo}}", + "src": ".let $foo = {$foo} .let $foo = {42} {{bar {$foo}}}", "exp": "bar 42" }, { - "src": "let $foo = {:unknown} let $foo = {42} {bar {$foo}}", + "src": ".let $foo = {:unknown} .let $foo = {42} {{bar {$foo}}}", "exp": "bar 42" }, { - "src": "let $x = {42} let $y = {$x} let $x = {13} {{$x} {$y}}", + "src": ".let $x = {42} .let $y = {$x} .let $x = {13} {{{$x} {$y}}}", "exp": "13 42" }, { - "src": "match {$foo} when |1| {one} when * {other}", + "src": ".match {$foo} |1| {{one}} * {{other}}", "params": { "foo": "1" }, "exp": "one" }, { - "src": "match {$foo} when 1 {one} when * {other}", + "src": ".match {$foo} 1 {{one}} * {{other}}", "params": { "foo": 1 }, "exp": "one" }, { - "src": "match {$foo} when 1 {one} when * {other}", + "src": ".match {$foo} 1 {{one}} * {{other}}", "params": { "foo": null }, "exp": "other", "errors": [{ "type": "not-selectable" }] }, { - "src": "match {$foo} when 1 {one} when * {other}", + "src": ".match {$foo} 1 {{one}} * {{other}}", "exp": "other", "errors": [{ "type": "unresolved-var" }, { "type": "not-selectable" }] }, { - "src": "match {$foo} when one {one} when * {other}", + "src": ".match {$foo} one {{one}} * {{other}}", "params": { "foo": 1 }, "exp": "one" }, { - "src": "match {$foo} when 1 {=1} when one {one} when * {other}", + "src": ".match {$foo} 1 {{=1}} one {{one}} * {{other}}", "params": { "foo": 1 }, "exp": "=1" }, { - "src": "match {$foo} when one {one} when 1 {=1} when * {other}", + "src": ".match {$foo} one {{one}} 1 {{=1}} * {{other}}", "params": { "foo": 1 }, "exp": "=1" }, { - "src": "match {$foo} {$bar} when one one {one one} when one * {one other} when * * {other}", + "src": ".match {$foo} {$bar} one one {{one one}} one * {{one other}} * * {{other}}", "params": { "foo": 1, "bar": 1 }, "exp": "one one" }, { - "src": "match {$foo} {$bar} when one one {one one} when one * {one other} when * * {other}", + "src": ".match {$foo} {$bar} one one {{one one}} one * {{one other}} * * {{other}}", "params": { "foo": 1, "bar": 2 }, "exp": "one other" }, { - "src": "match {$foo} {$bar} when one one {one one} when one * {one other} when * * {other}", + "src": ".match {$foo} {$bar} one one {{one one}} one * {{one other}} * * {{other}}", "params": { "foo": 2, "bar": 2 }, "exp": "other" }, { - "src": "let $foo = {$bar :number} match {$foo} when one {one} when * {other}", + "src": ".let $foo = {$bar :number} .match {$foo} one {{one}} * {{other}}", "params": { "bar": 1 }, "exp": "one" }, { - "src": "let $foo = {$bar :number} match {$foo} when one {one} when * {other}", + "src": ".let $foo = {$bar :number} .match {$foo} one {{one}} * {{other}}", "params": { "bar": 2 }, "exp": "other" }, { - "src": "let $foo = {$bar} match {$foo :number} when one {one} when * {other}", + "src": ".let $foo = {$bar} .match {$foo :number} one {{one}} * {{other}}", "params": { "bar": 1 }, "exp": "one" }, { - "src": "let $foo = {$bar} match {$foo :number} when one {one} when * {other}", + "src": ".let $foo = {$bar} .match {$foo :number} one {{one}} * {{other}}", "params": { "bar": 2 }, "exp": "other" }, { - "src": "let $foo = {$bar} match {$foo} when one {one} when * {other}", + "src": ".let $foo = {$bar} .match {$foo} one {{one}} * {{other}}", "params": { "bar": 1 }, "exp": "one" }, { - "src": "let $foo = {$bar} match {$foo} when one {one} when * {other}", + "src": ".let $foo = {$bar} .match {$foo} one {{one}} * {{other}}", "params": { "bar": 2 }, "exp": "other" }, { - "src": "let $bar = {$none} match {$foo} when one {one} when * {{$bar}}", + "src": ".let $bar = {$none} .match {$foo} one {{one}} * {{{$bar}}}", "params": { "foo": 1 }, "exp": "one" }, { - "src": "let $bar = {$none} match {$foo} when one {one} when * {{$bar}}", + "src": ".let $bar = {$none} .match {$foo} one {{one}} * {{{$bar}}}", "params": { "foo": 2 }, "exp": "{$none}", "errors": [{ "type": "unresolved-var" }] }, { - "src": "{{+tag}}", + "src": "{+tag}", "exp": "", "parts": [{ "type": "open", "name": "tag" }] }, { - "src": "{{+tag}content}", + "src": "{+tag}content", "exp": "content", "parts": [ { "type": "open", "name": "tag" }, @@ -235,7 +235,7 @@ ] }, { - "src": "{{+tag}content{-tag}}", + "src": "{+tag}content{-tag}", "exp": "content", "parts": [ { "type": "open", "name": "tag" }, @@ -244,7 +244,7 @@ ] }, { - "src": "{{-tag}content}", + "src": "{-tag}content", "exp": "content", "parts": [ { "type": "close", "name": "tag" }, @@ -252,12 +252,12 @@ ] }, { - "src": "{{+tag foo=bar}}", + "src": "{+tag foo=bar}", "exp": "", "parts": [{ "type": "open", "name": "tag", "options": { "foo": "bar" } }] }, { - "src": "{{+tag foo=|foo| bar=$bar}}", + "src": "{+tag foo=|foo| bar=$bar}", "params": { "bar": "b a r" }, "exp": "", "parts": [ @@ -269,54 +269,53 @@ ] }, { - "src": "{{|foo| +markup}}", + "src": "{|foo| +markup}", "exp": "", "parts": [{ "type": "open", "name": "markup", "value": "foo" }] }, { - "src": "{{-tag foo=bar}}", + "src": "{-tag foo=bar}", "exp": "", "parts": [{ "type": "close", "name": "tag", "options": { "foo": "bar" } }] }, { - "src": "{unquoted {literal}}", + "src": "unquoted {literal}", "exp": "unquoted literal" }, { - "src": "match {+foo} when * {foo}", + "src": ".match {+foo} * {{foo}}", "exp": "foo", "errors": [{ "type": "not-selectable" }] }, - { "src": "no braces", "syntaxError": true }, - { "src": "no braces {$foo}", "syntaxError": true }, - { "src": "{missing end brace", "syntaxError": true }, - { "src": "{missing end {$brace", "syntaxError": true }, - { "src": "{extra} content", "syntaxError": true }, - { "src": "let bar = {|foo|} {{$bar}}", "syntaxError": true }, - { "src": "let $bar {|foo|} {{$bar}}", "syntaxError": true }, - { "src": "let $bar = |foo| {{$bar}}", "syntaxError": true }, + { "src": "{{missing end brace}", "syntaxError": true }, + { "src": "{{missing end braces", "syntaxError": true }, + { "src": "{{missing end {$braces", "syntaxError": true }, + { "src": "{{extra}} content", "syntaxError": true }, + { "src": ".let bar = {|foo|} {{{$bar}}}", "syntaxError": true }, + { "src": ".let $bar {|foo|} {{{$bar}}}", "syntaxError": true }, + { "src": ".let $bar = |foo| {{{$bar}}}", "syntaxError": true }, { - "src": "let $foo = {$bar} let $bar = {$baz} {bar {$foo}}", + "src": ".let $foo = {$bar} .let $bar = {$baz} {{bar {$foo}}}", "syntaxError": true }, - { "src": "{empty { }}", "syntaxError": true }, - { "src": "{bad {:}}", "syntaxError": true }, - { "src": "{bad {\u0000placeholder}}", "syntaxError": true }, + { "src": "empty { }", "syntaxError": true }, + { "src": "bad {:}", "syntaxError": true }, + { "src": "bad {\u0000placeholder}", "syntaxError": true }, { - "src": "{no-equal {|42| :number minimumFractionDigits 2}}", + "src": "no-equal {|42| :number minimumFractionDigits 2}", "syntaxError": true }, - { "src": "{bad {:placeholder option=}}", "syntaxError": true }, - { "src": "{bad {:placeholder option value}}", "syntaxError": true }, - { "src": "{bad {:placeholder option}}", "syntaxError": true }, - { "src": "{bad {$placeholder option}}", "syntaxError": true }, - { "src": "{no {$placeholder end}", "syntaxError": true }, - { "src": "match {} when * {foo}", "syntaxError": true }, - { "src": "match {|foo|} when*{foo}", "syntaxError": true }, - { "src": "match when * {foo}", "syntaxError": true }, - { "src": "match {|x|} when * foo", "syntaxError": true }, - { "src": "match {|x|} when * {foo} extra", "syntaxError": true }, - { "src": "match |x| when * {foo}", "syntaxError": true }, - { "src": "match {$foo} when * * {foo}", "syntaxError": true }, - { "src": "match {$foo} {$bar} when * {foo}", "syntaxError": true } + { "src": "bad {:placeholder option=}", "syntaxError": true }, + { "src": "bad {:placeholder option value}", "syntaxError": true }, + { "src": "bad {:placeholder option}", "syntaxError": true }, + { "src": "bad {$placeholder option}", "syntaxError": true }, + { "src": "no {$placeholder end", "syntaxError": true }, + { "src": ".match {} * {{foo}}", "syntaxError": true }, + { "src": ".match {|foo|} {|bar|} ** {{foo}}", "syntaxError": true }, + { "src": ".match * {{foo}}", "syntaxError": true }, + { "src": ".match {|x|} * foo", "syntaxError": true }, + { "src": ".match {|x|} * {{foo}} extra", "syntaxError": true }, + { "src": ".match |x| * {{foo}}", "syntaxError": true }, + { "src": ".match {$foo} * * {{foo}}", "syntaxError": true }, + { "src": ".match {$foo} {$bar} * {{foo}}", "syntaxError": true } ] diff --git a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts index 0c75b763..1d3c6ee3 100644 --- a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts +++ b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts @@ -9,31 +9,30 @@ import type * as CST from './cst-types.js'; */ export function asDataModel(msg: CST.Message): Model.Message { for (const error of msg.errors) throw error; - const declarations: Model.Declaration[] = msg.declarations.map(decl => ({ - name: asValue(decl.target).name, - value: asExpression(decl.value) - })); - switch (msg.type) { - case 'message': - return { - type: 'message', - declarations, - pattern: asPattern(msg.pattern) - }; - case 'select': - return { - type: 'select', - declarations, - selectors: msg.selectors.map(asExpression), - variants: msg.variants.map(cst => ({ - keys: cst.keys.map(key => - key.type === '*' ? { type: '*' } : asValue(key) - ), - value: asPattern(cst.value) - })) - }; - default: - throw new MessageSyntaxError('parse-error', 0, msg.source?.length ?? 0); + const declarations: Model.Declaration[] = (msg.declarations ?? []).map( + decl => ({ + name: asValue(decl.target).name, + value: asExpression(decl.value) + }) + ); + if (msg.type === 'select') { + return { + type: 'select', + declarations, + selectors: msg.selectors.map(asExpression), + variants: msg.variants.map(cst => ({ + keys: cst.keys.map(key => + key.type === '*' ? { type: '*' } : asValue(key) + ), + value: asPattern(cst.value) + })) + }; + } else { + return { + type: 'message', + declarations, + pattern: asPattern(msg.pattern) + }; } } diff --git a/packages/mf2-messageformat/src/cst-parser/cst-parser.test.ts b/packages/mf2-messageformat/src/cst-parser/cst-parser.test.ts index b69661c5..2674b42a 100644 --- a/packages/mf2-messageformat/src/cst-parser/cst-parser.test.ts +++ b/packages/mf2-messageformat/src/cst-parser/cst-parser.test.ts @@ -3,13 +3,12 @@ import { parseMessage } from './index.js'; describe('messages in resources', () => { test('text character escapes', () => { - const src = '{\\\t\\ \\n\\r\\t\\x01\\u0002\\U000003}'; + const src = '\\\t\\ \\n\\r\\t\\x01\\u0002\\U000003'; const noRes = parseMessage(src, { resource: false }); expect(noRes.errors).toHaveLength(8); const msg = parseMessage(src, { resource: true }); expect(msg).toMatchObject({ - type: 'message', - declarations: [], + type: 'simple', errors: [], pattern: { start: 0, @@ -17,8 +16,8 @@ describe('messages in resources', () => { body: [ { type: 'text', - start: 1, - end: src.length - 1, + start: 0, + end: src.length, value: '\t \n\r\t\x01\x02\x03' } ] @@ -27,13 +26,12 @@ describe('messages in resources', () => { }); test('quoted literal character escapes', () => { - const src = '{{|\\\t\\ \\n\\r\\t\\x01\\u0002\\U000003|}}'; + const src = '{|\\\t\\ \\n\\r\\t\\x01\\u0002\\U000003|}'; const noRes = parseMessage(src, { resource: false }); expect(noRes.errors).toHaveLength(8); const msg = parseMessage(src, { resource: true }); expect(msg).toMatchObject({ - type: 'message', - declarations: [], + type: 'simple', errors: [], pattern: { start: 0, @@ -41,13 +39,13 @@ describe('messages in resources', () => { body: [ { type: 'expression', - start: 1, - end: src.length - 1, + start: 0, + end: src.length, body: { type: 'literal', quoted: true, - start: 2, - end: src.length - 2, + start: 1, + end: src.length - 1, value: '\t \n\r\t\x01\x02\x03' } } @@ -56,24 +54,46 @@ describe('messages in resources', () => { }); }); + test('complex pattern with leading .', () => { + const src = '{{.let}}'; + const msg = parseMessage(src, { resource: false }); + expect(msg).toMatchObject({ + type: 'complex', + errors: [], + declarations: [], + pattern: { + quotes: [ + { start: 0, end: 2, value: '{{' }, + { start: 6, end: 8, value: '}}' } + ], + start: 0, + end: src.length, + body: [{ type: 'text', start: 2, end: src.length - 2, value: '.let' }] + } + }); + }); + test('newlines in text', () => { - const src = '{1\n \t2 \n \\ 3\n\\t}'; + const src = '1\n \t2 \n \\ 3\n\\t'; const noRes = parseMessage(src, { resource: false }); expect(noRes).toMatchObject({ + type: 'simple', errors: [{ type: 'bad-escape' }, { type: 'bad-escape' }], pattern: { body: [{ type: 'text', value: '1\n \t2 \n \\ 3\n\\t' }] } }); const msg = parseMessage(src, { resource: true }); expect(msg).toMatchObject({ + type: 'simple', errors: [], pattern: { body: [{ type: 'text', value: '1\n2 \n 3\n\t' }] } }); }); test('newlines in quoted literal', () => { - const src = '{{|1\n \t2 \n \\ 3\n\\t|}}'; + const src = '{|1\n \t2 \n \\ 3\n\\t|}'; const noRes = parseMessage(src, { resource: false }); expect(noRes).toMatchObject({ + type: 'simple', errors: [{ type: 'bad-escape' }, { type: 'bad-escape' }], pattern: { body: [ @@ -86,6 +106,7 @@ describe('messages in resources', () => { }); const msg = parseMessage(src, { resource: true }); expect(msg).toMatchObject({ + type: 'simple', errors: [], pattern: { body: [ diff --git a/packages/mf2-messageformat/src/cst-parser/cst-types.ts b/packages/mf2-messageformat/src/cst-parser/cst-types.ts index 228d75a7..7213458f 100644 --- a/packages/mf2-messageformat/src/cst-parser/cst-types.ts +++ b/packages/mf2-messageformat/src/cst-parser/cst-types.ts @@ -1,39 +1,39 @@ import type { MessageSyntaxError } from '../errors'; /** @beta */ -export type Message = PatternMessage | SelectMessage | JunkMessage; +export type Message = SimpleMessage | ComplexMessage | SelectMessage; /** @beta */ -export interface PatternMessage { - type: 'message'; - declarations: Declaration[]; +export interface SimpleMessage { + type: 'simple'; + declarations?: never; pattern: Pattern; errors: MessageSyntaxError[]; } /** @beta */ -export interface SelectMessage { - type: 'select'; +export interface ComplexMessage { + type: 'complex'; declarations: Declaration[]; - match: Syntax<'match'>; - selectors: Expression[]; - variants: Variant[]; + pattern: Pattern; errors: MessageSyntaxError[]; } /** @beta */ -export interface JunkMessage { - type: 'junk'; +export interface SelectMessage { + type: 'select'; declarations: Declaration[]; + match: Syntax<'.match'>; + selectors: Expression[]; + variants: Variant[]; errors: MessageSyntaxError[]; - source: string; } /** @beta */ export interface Declaration { start: number; end: number; - let: Syntax<'let'>; + let: Syntax<'.let'>; target: VariableRef | Junk; equals: Syntax<'=' | ''>; value: Expression | Junk; @@ -43,7 +43,6 @@ export interface Declaration { export interface Variant { start: number; end: number; - when: Syntax<'when'>; keys: Array; value: Pattern; } @@ -58,11 +57,10 @@ export interface CatchallKey { /** @beta */ export interface Pattern { - /** position of the `{` */ start: number; - /** position one past the `}` */ end: number; body: Array; + quotes?: [Syntax<'{{'>] | [Syntax<'{{'>, Syntax<'}}'>]; } /** @beta */ diff --git a/packages/mf2-messageformat/src/cst-parser/declarations.ts b/packages/mf2-messageformat/src/cst-parser/declarations.ts index b4ceefbd..a90b888a 100644 --- a/packages/mf2-messageformat/src/cst-parser/declarations.ts +++ b/packages/mf2-messageformat/src/cst-parser/declarations.ts @@ -10,7 +10,7 @@ export function parseDeclarations(ctx: ParseContext): { } { let pos = whitespaces(ctx.source, 0); const declarations: CST.Declaration[] = []; - while (ctx.source.startsWith('let', pos)) { + while (ctx.source.startsWith('.let', pos)) { const decl = parseDeclaration(ctx, pos); declarations.push(decl); pos = decl.end; @@ -20,15 +20,13 @@ export function parseDeclarations(ctx: ParseContext): { return { declarations, end: pos }; } -// declaration = let s variable [s] "=" [s] expression -// let = %x6C.65.74 ; "let" function parseDeclaration(ctx: ParseContext, start: number): CST.Declaration { - let pos = start + 3; // 'let' - const let_: CST.Syntax<'let'> = { start, end: pos, value: 'let' }; + let pos = start + 4; // '.let' + const let_: CST.Syntax<'.let'> = { start, end: pos, value: '.let' }; const ws = whitespaces(ctx.source, pos); pos += ws; - if (ws === 0) ctx.onError('missing-char', pos, ' '); + if (ws === 0) ctx.onError('missing-syntax', pos, ' '); let target: CST.VariableRef | CST.Junk; if (ctx.source[pos] === '$') { @@ -44,7 +42,7 @@ function parseDeclaration(ctx: ParseContext, start: number): CST.Declaration { end: pos, source: ctx.source.substring(junkStart, pos) }; - ctx.onError('missing-char', junkStart, '$'); + ctx.onError('missing-syntax', junkStart, '$'); } pos += whitespaces(ctx.source, pos); @@ -54,7 +52,7 @@ function parseDeclaration(ctx: ParseContext, start: number): CST.Declaration { pos += 1; } else { equals = { start: pos, end: pos, value: '' }; - ctx.onError('missing-char', pos, '='); + ctx.onError('missing-syntax', pos, '='); } let value: CST.Expression | CST.Junk; @@ -74,7 +72,7 @@ function parseDeclaration(ctx: ParseContext, start: number): CST.Declaration { end: pos, source: ctx.source.substring(junkStart, pos) }; - ctx.onError('missing-char', junkStart, '{'); + ctx.onError('missing-syntax', junkStart, '{'); } return { start, end: pos, let: let_, target, equals, value }; diff --git a/packages/mf2-messageformat/src/cst-parser/expression.ts b/packages/mf2-messageformat/src/cst-parser/expression.ts index 7a7ab899..22e90c20 100644 --- a/packages/mf2-messageformat/src/cst-parser/expression.ts +++ b/packages/mf2-messageformat/src/cst-parser/expression.ts @@ -61,7 +61,7 @@ export function parseExpression( pos += whitespaces(ctx.source, pos); if (pos >= ctx.source.length) { - ctx.onError('missing-char', pos, '}'); + ctx.onError('missing-syntax', pos, '}'); } else if (ctx.source[pos] !== '}') { const errStart = pos; while (pos < ctx.source.length && ctx.source[pos] !== '}') pos += 1; @@ -95,7 +95,7 @@ function parseFunctionRef( while (pos < ctx.source.length) { const ws = whitespaces(ctx.source, pos); if (ctx.source[pos + ws] === '}') break; - if (ws === 0) ctx.onError('missing-char', pos, ' '); + if (ws === 0) ctx.onError('missing-syntax', pos, ' '); pos += ws; const opt = parseOption(ctx, pos); if (opt.end === pos) break; // error @@ -112,7 +112,7 @@ function parseOption(ctx: ParseContext, start: number): CST.Option { let pos = start + name.length; pos += whitespaces(ctx.source, pos); if (ctx.source[pos] === '=') pos += 1; - else ctx.onError('missing-char', pos, '='); + else ctx.onError('missing-syntax', pos, '='); pos += whitespaces(ctx.source, pos); const value = ctx.source[pos] === '$' diff --git a/packages/mf2-messageformat/src/cst-parser/message.ts b/packages/mf2-messageformat/src/cst-parser/message.ts index 884c6290..0e15ef20 100644 --- a/packages/mf2-messageformat/src/cst-parser/message.ts +++ b/packages/mf2-messageformat/src/cst-parser/message.ts @@ -1,7 +1,7 @@ import { MessageDataModelError, MessageSyntaxError, - MissingCharError + MissingSyntaxError } from '../errors.js'; import type * as CST from './cst-types.js'; import { parseDeclarations } from './declarations.js'; @@ -20,11 +20,11 @@ export class ParseContext { } onError( - type: Exclude, + type: Exclude, start: number, end: number ): void; - onError(type: 'missing-char', start: number, char: string): void; + onError(type: 'missing-syntax', start: number, char: string): void; onError( type: typeof MessageSyntaxError.prototype.type, start: number, @@ -36,8 +36,8 @@ export class ParseContext { case 'missing-fallback': err = new MessageDataModelError(type, start, Number(end)); break; - case 'missing-char': - err = new MissingCharError(start, String(end)); + case 'missing-syntax': + err = new MissingSyntaxError(start, String(end)); break; default: err = new MessageSyntaxError(type, start, Number(end)); @@ -46,9 +46,6 @@ export class ParseContext { } } -// message = [s] *(declaration [s]) body [s] -// body = pattern / (selectors 1*([s] variant)) -// selectors = match 1*([s] expression) /** * Parse the string syntax representation of a message into * its corresponding CST representation. @@ -60,24 +57,24 @@ export function parseMessage( opt?: { resource?: boolean } ): CST.Message { const ctx = new ParseContext(source, opt); - const { declarations, end: pos } = parseDeclarations(ctx); - if (source.startsWith('match', pos)) { - return parseSelectMessage(ctx, pos, declarations); - } else if (source[pos] === '{') { - return parsePatternMessage(ctx, pos, declarations); + if (source.startsWith('.')) { + const { declarations, end: pos } = parseDeclarations(ctx); + return source.startsWith('.match', pos) + ? parseSelectMessage(ctx, pos, declarations) + : parsePatternMessage(ctx, pos, declarations, true); } else { - ctx.onError('parse-error', pos, source.length); - return { type: 'junk', declarations, errors: ctx.errors, source }; + return parsePatternMessage(ctx, 0, [], source.startsWith('{{')); } } function parsePatternMessage( ctx: ParseContext, start: number, - declarations: CST.Declaration[] -): CST.PatternMessage { - const pattern = parsePattern(ctx, start); + declarations: CST.Declaration[], + complex: boolean +): CST.SimpleMessage | CST.ComplexMessage { + const pattern = parsePattern(ctx, start, complex); let pos = pattern.end; pos += whitespaces(ctx.source, pos); @@ -85,7 +82,9 @@ function parsePatternMessage( ctx.onError('extra-content', pos, ctx.source.length); } - return { type: 'message', declarations, pattern, errors: ctx.errors }; + return complex + ? { type: 'complex', declarations, pattern, errors: ctx.errors } + : { type: 'simple', pattern, errors: ctx.errors }; } function parseSelectMessage( @@ -93,8 +92,8 @@ function parseSelectMessage( start: number, declarations: CST.Declaration[] ): CST.SelectMessage { - let pos = start + 5; // 'match' - const match: CST.Syntax<'match'> = { start, end: pos, value: 'match' }; + let pos = start + 6; // '.match' + const match: CST.Syntax<'.match'> = { start, end: pos, value: '.match' }; pos += whitespaces(ctx.source, pos); const selectors: CST.Expression[] = []; @@ -120,7 +119,7 @@ function parseSelectMessage( const variants: CST.Variant[] = []; pos += whitespaces(ctx.source, pos); - while (ctx.source.startsWith('when', pos)) { + while (pos < ctx.source.length) { const variant = parseVariant(ctx, pos, selectors.length); variants.push(variant); pos = variant.end; @@ -141,15 +140,12 @@ function parseSelectMessage( }; } -// variant = when 1*(s key) [s] pattern -// key = literal / "*" function parseVariant( ctx: ParseContext, start: number, selCount: number ): CST.Variant { - let pos = start + 4; // 'when' - const when: CST.Syntax<'when'> = { start, end: pos, value: 'when' }; + let pos = start; const keys: Array = []; while (pos < ctx.source.length) { const ws = whitespaces(ctx.source, pos); @@ -157,7 +153,7 @@ function parseVariant( const ch = ctx.source[pos]; if (ch === '{') break; - if (ws === 0) ctx.onError('missing-char', pos, ' '); + if (pos > start && ws === 0) ctx.onError('missing-syntax', pos, "' '"); const key = ch === '*' @@ -173,18 +169,25 @@ function parseVariant( ctx.onError('key-mismatch', start, end); } - const value = parsePattern(ctx, pos); - return { start, end: value.end, when, keys, value }; + const value = parsePattern(ctx, pos, true); + return { start, end: value.end, keys, value }; } -// pattern = "{" *(text / expression) "}" -function parsePattern(ctx: ParseContext, start: number): CST.Pattern { - if (ctx.source[start] !== '{') { - ctx.onError('missing-char', start, '{'); - return { start, end: start, body: [] }; +function parsePattern( + ctx: ParseContext, + start: number, + quoted: boolean +): CST.Pattern { + let pos = start; + if (quoted) { + if (ctx.source.startsWith('{{', pos)) { + pos += 2; + } else { + ctx.onError('missing-syntax', start, '{{'); + return { start, end: start, body: [] }; + } } - let pos = start + 1; const body: Array = []; loop: while (pos < ctx.source.length) { switch (ctx.source[pos]) { @@ -204,8 +207,18 @@ function parsePattern(ctx: ParseContext, start: number): CST.Pattern { } } - if (ctx.source[pos] === '}') pos += 1; - else ctx.onError('missing-char', pos, '}'); + if (quoted) { + const q0: CST.Syntax<'{{'> = { start, end: start + 2, value: '{{' }; + let quotes: CST.Pattern['quotes']; + if (ctx.source.startsWith('}}', pos)) { + quotes = [q0, { start: pos, end: pos + 2, value: '}}' }]; + pos += 2; + } else { + quotes = [q0]; + ctx.onError('missing-syntax', pos, '}}'); + } + return { quotes, start, end: pos, body }; + } return { start, end: pos, body }; } diff --git a/packages/mf2-messageformat/src/cst-parser/values.ts b/packages/mf2-messageformat/src/cst-parser/values.ts index 2202d12d..64a2ef76 100644 --- a/packages/mf2-messageformat/src/cst-parser/values.ts +++ b/packages/mf2-messageformat/src/cst-parser/values.ts @@ -114,7 +114,7 @@ export function parseQuotedLiteral( } } value += ctx.source.substring(pos); - ctx.onError('missing-char', ctx.source.length, '|'); + ctx.onError('missing-syntax', ctx.source.length, '|'); return { type: 'literal', quoted: true, diff --git a/packages/mf2-messageformat/src/errors.ts b/packages/mf2-messageformat/src/errors.ts index e4a25742..83a3fea0 100644 --- a/packages/mf2-messageformat/src/errors.ts +++ b/packages/mf2-messageformat/src/errors.ts @@ -21,17 +21,17 @@ export class MessageSyntaxError extends MessageError { | 'extra-content' | 'key-mismatch' | 'parse-error' - | 'missing-char' - | 'missing-fallback'; + | 'missing-fallback' + | 'missing-syntax'; start: number; end: number; - char?: string; + expected?: string; constructor( type: typeof MessageSyntaxError.prototype.type, start: number, end: number, - char?: string + expected?: string ) { let message: string; switch (type) { @@ -43,8 +43,8 @@ export class MessageSyntaxError extends MessageError { case 'parse-error': message = `Syntax parse error: ${type} at ${start}`; break; - case 'missing-char': - message = `Syntax parse error: Missing character ${char} at ${start}`; + case 'missing-syntax': + message = `Syntax parse error: Missing ${expected} at ${start}`; break; case 'key-mismatch': case 'missing-fallback': @@ -57,12 +57,12 @@ export class MessageSyntaxError extends MessageError { } } -export class MissingCharError extends MessageSyntaxError { - char: string; +export class MissingSyntaxError extends MessageSyntaxError { + expected: string; - constructor(pos: number, char: string) { - super('missing-char', pos, pos + 1, char); - this.char = char; + constructor(pos: number, expected: string) { + super('missing-syntax', pos, pos + expected.length, expected); + this.expected = expected; } } diff --git a/packages/mf2-messageformat/src/expression/function-annotation.test.ts b/packages/mf2-messageformat/src/expression/function-annotation.test.ts index 3560b2b3..b3a0606e 100644 --- a/packages/mf2-messageformat/src/expression/function-annotation.test.ts +++ b/packages/mf2-messageformat/src/expression/function-annotation.test.ts @@ -12,7 +12,7 @@ test('Custom function', () => { toString: () => `str:${input}` }) } satisfies MessageFunctions; - const mf = new MessageFormat('{{$var :custom}}', 'en', { functions }); + const mf = new MessageFormat('{$var :custom}', 'en', { functions }); expect(mf.format({ var: 42 })).toEqual('str:42'); expect(mf.formatToParts({ var: 42 })).toEqual([ { type: 'custom', source: '$var', locale: 'en', value: 'part:42' } @@ -22,8 +22,8 @@ test('Custom function', () => { describe('inputs with options', () => { test('local variable with :number expression', () => { const mf = new MessageFormat( - `let $val = {12345678 :number useGrouping=false} - {{$val :number minimumFractionDigits=2}}`, + `.let $val = {12345678 :number useGrouping=false} + {{{$val :number minimumFractionDigits=2}}}`, 'en' ); //const val = new MessageNumber(null, BigInt(12345678), { options: { useGrouping: false } }); @@ -39,7 +39,7 @@ describe('inputs with options', () => { test('value with options', () => { const mf = new MessageFormat( - '{{$val :number minimumFractionDigits=2}}', + '{$val :number minimumFractionDigits=2}', 'en' ); const val = Object.assign(new Number(12345678), { @@ -57,7 +57,7 @@ describe('inputs with options', () => { test('MessageValue locales take precedence', () => { const mf = new MessageFormat( - '{{$val :number minimumFractionDigits=2}}', + '{$val :number minimumFractionDigits=2}', 'en' ); const val = Object.assign(new Number(12345), { locale: 'fi' }); @@ -71,18 +71,15 @@ describe('inputs with options', () => { describe('Type casts based on runtime', () => { test('boolean function option with literal value', () => { - const mfTrue = new MessageFormat('{{$var :number useGrouping=true}}', 'en'); + const mfTrue = new MessageFormat('{$var :number useGrouping=true}', 'en'); expect(mfTrue.format({ var: 1234 })).toBe('1,234'); - const mfFalse = new MessageFormat( - '{{$var :number useGrouping=false}}', - 'en' - ); + const mfFalse = new MessageFormat('{$var :number useGrouping=false}', 'en'); expect(mfFalse.format({ var: 1234 })).toBe('1234'); }); test('boolean function option with variable value', () => { const mf = new MessageFormat( - '{{$var :number useGrouping=$useGrouping}}', + '{$var :number useGrouping=$useGrouping}', 'en' ); expect(mf.format({ var: 1234, useGrouping: 'false' })).toBe('1234'); @@ -92,7 +89,7 @@ describe('Type casts based on runtime', () => { describe('Simple open/close', () => { test('no options, literal body', () => { - const mf = new MessageFormat('{{+b}foo{-b}}'); + const mf = new MessageFormat('{+b}foo{-b}'); expect(mf.formatToParts()).toEqual([ { type: 'open', source: '+b', name: 'b' }, { type: 'literal', value: 'foo' }, @@ -103,7 +100,7 @@ describe('Simple open/close', () => { test('arguments, options, variables', () => { const mf = new MessageFormat( - '{{|x| +b foo=42 bar=$foo}foo{$foo}{|y| -b foo=13}}', + '{|x| +b foo=42 bar=$foo}foo{$foo}{|y| -b foo=13}', 'en' ); const msg = mf.formatToParts({ foo: 'foo bar' }); @@ -131,7 +128,7 @@ describe('Simple open/close', () => { describe('Multiple open/close', () => { test('adjacent', () => { - const mf = new MessageFormat('{{+b}foo{-b}{+a}bar{-a}}'); + const mf = new MessageFormat('{+b}foo{-b}{+a}bar{-a}'); expect(mf.formatToParts()).toEqual([ { type: 'open', source: '+b', name: 'b' }, { type: 'literal', value: 'foo' }, @@ -144,7 +141,7 @@ describe('Multiple open/close', () => { }); test('nested', () => { - const mf = new MessageFormat('{{+b}foo{+a}bar{-a}{-b}}'); + const mf = new MessageFormat('{+b}foo{+a}bar{-a}{-b}'); expect(mf.formatToParts()).toEqual([ { type: 'open', source: '+b', name: 'b' }, { type: 'literal', value: 'foo' }, @@ -157,7 +154,7 @@ describe('Multiple open/close', () => { }); test('overlapping', () => { - const mf = new MessageFormat('{{+b}foo{+a}bar{-b}baz{-a}}'); + const mf = new MessageFormat('{+b}foo{+a}bar{-b}baz{-a}'); expect(mf.formatToParts()).toEqual([ { type: 'open', source: '+b', name: 'b' }, { type: 'literal', value: 'foo' }, diff --git a/packages/mf2-messageformat/src/expression/literal.test.ts b/packages/mf2-messageformat/src/expression/literal.test.ts index 0cf031d9..62824061 100644 --- a/packages/mf2-messageformat/src/expression/literal.test.ts +++ b/packages/mf2-messageformat/src/expression/literal.test.ts @@ -14,19 +14,19 @@ function resolve(source: string, errors: any[] = []) { describe('quoted literals', () => { test('simple', () => { - const res = resolve('{{|quoted literal|}}'); + const res = resolve('{|quoted literal|}'); expect(res).toMatchObject([{ type: 'string', value: 'quoted literal' }]); }); test('spaces, newlines and escapes', () => { - const res = resolve('{{| quoted \n \\\\\\|literal\\\\\\||}}'); + const res = resolve('{| quoted \n \\\\\\|literal\\\\\\||}'); expect(res).toMatchObject([ { type: 'string', value: ' quoted \n \\|literal\\|' } ]); }); test('invalid escapes', () => { - expect(() => new MessageFormat('{{|quoted \\}iteral|}}')).toThrow(); - expect(() => new MessageFormat('{{|quoted \\{iteral|}}')).toThrow(); + expect(() => new MessageFormat('{|quoted \\}iteral|}')).toThrow(); + expect(() => new MessageFormat('{|quoted \\{iteral|}')).toThrow(); }); }); diff --git a/packages/mf2-messageformat/src/expression/unsupported-annotation.test.ts b/packages/mf2-messageformat/src/expression/unsupported-annotation.test.ts index 80593658..08be3d3b 100644 --- a/packages/mf2-messageformat/src/expression/unsupported-annotation.test.ts +++ b/packages/mf2-messageformat/src/expression/unsupported-annotation.test.ts @@ -18,19 +18,19 @@ function resolve( describe('Reserved syntax', () => { test('empty', () => { - const msg = resolve('{{!}}', {}, [{ type: 'unsupported-annotation' }]); + const msg = resolve('{!}', {}, [{ type: 'unsupported-annotation' }]); expect(msg).toMatchObject([{ type: 'fallback', source: '!' }]); }); test('argument', () => { - const msg = resolve('{{$foo @bar}}', { foo: 42 }, [ + const msg = resolve('{$foo @bar}', { foo: 42 }, [ { type: 'unsupported-annotation' } ]); expect(msg).toMatchObject([{ type: 'fallback', source: '$foo' }]); }); test('whitespace', () => { - const msg = resolve('{{ # one\ntwo\rthree four }}', {}, [ + const msg = resolve('{ # one\ntwo\rthree four }', {}, [ { type: 'unsupported-annotation' } ]); expect(msg).toMatchObject([ @@ -39,8 +39,6 @@ describe('Reserved syntax', () => { }); test('surrogates', () => { - expect( - () => new MessageFormat('{{ %invalid \ud900 surrogate }}') - ).toThrow(); + expect(() => new MessageFormat('{ %invalid \ud900 surrogate }')).toThrow(); }); }); diff --git a/packages/mf2-messageformat/src/expression/variable.test.ts b/packages/mf2-messageformat/src/expression/variable.test.ts index 38415d2b..0b487398 100644 --- a/packages/mf2-messageformat/src/expression/variable.test.ts +++ b/packages/mf2-messageformat/src/expression/variable.test.ts @@ -3,7 +3,7 @@ import { MessageFormat } from '../index'; describe('variables', () => { let mf: MessageFormat; beforeEach(() => { - mf = new MessageFormat('{{$val}}', 'en'); + mf = new MessageFormat('{$val}', 'en'); }); test('number', () => { @@ -78,7 +78,7 @@ describe('variables', () => { describe('Variable paths', () => { let mf: MessageFormat; beforeEach(() => { - mf = new MessageFormat('{{$user.name}}', 'en'); + mf = new MessageFormat('{$user.name}', 'en'); }); test('top-level match', () => { diff --git a/packages/mf2-messageformat/src/extra/validate.ts b/packages/mf2-messageformat/src/extra/validate.ts index 3486c83a..99c7eca6 100644 --- a/packages/mf2-messageformat/src/extra/validate.ts +++ b/packages/mf2-messageformat/src/extra/validate.ts @@ -6,13 +6,12 @@ function validateParts(parts: Pattern['body'], functions: MessageFunctions) { for (const part of parts) { if ( isExpression(part) && - isFunctionAnnotation(part.body) && - part.body.kind === 'value' + isFunctionAnnotation(part.annotation) && + part.annotation.kind === 'value' ) { - if (typeof functions[part.body.name] !== 'function') { - throw new ReferenceError( - `Runtime function not available: ${part.body.name}` - ); + const { name } = part.annotation; + if (typeof functions[name] !== 'function') { + throw new ReferenceError(`Runtime function not available: ${name}`); } // TODO: Once runtime arg requirements are defined, test against them } diff --git a/packages/mf2-messageformat/src/mf2-features.test.ts b/packages/mf2-messageformat/src/mf2-features.test.ts index 77755416..8220de29 100644 --- a/packages/mf2-messageformat/src/mf2-features.test.ts +++ b/packages/mf2-messageformat/src/mf2-features.test.ts @@ -40,10 +40,10 @@ describe('Plural Range Selectors & Range Formatters (unicode-org/message-format- test('input as { start, end } object', () => { const mf = new MessageFormat( source` - let $range = {$range :range} - match {$range} - when one {{$range} dag} - when * {{$range} dagen} + .let $range = {$range :range} + .match {$range} + one {{{$range} dag}} + * {{{$range} dagen}} `, 'nl', { functions: { range } } @@ -158,7 +158,7 @@ describe('Multi-selector messages (unicode-org/message-format-wg#119)', () => { ); }); - test('Mozilla activity (@stasm)', () => { + test('Mozilla Common Voice activity (@stasm)', () => { const src = source` activity-needed-calculation-plural = { NUMBER($totalHours) -> [one] {$totalHours} hour @@ -233,18 +233,18 @@ maybe('List formatting', () => { const list = ['Motorcycle', 'Bus', 'Car']; const opt = { functions: { list: listFn() } }; - const mf1 = new MessageFormat('{{$list :list}}', 'en', opt); + const mf1 = new MessageFormat('{$list :list}', 'en', opt); expect(mf1.format({ list })).toBe('Motorcycle, Bus, and Car'); const mf2 = new MessageFormat( - '{{$list :list style=short type=conjunction}}', + '{$list :list style=short type=conjunction}', 'en', opt ); expect(mf2.format({ list })).toBe('Motorcycle, Bus, & Car'); const mf3 = new MessageFormat( - '{{$list :list style=long type=disjunction}}', + '{$list :list style=long type=disjunction}', 'en', opt ); @@ -264,9 +264,9 @@ maybe('List formatting', () => { const mf = new MessageFormat( source` - match {$count :number} - when one {I-am dat cadouri {$list :list each=dative}.} - when * {Le-am dat cadouri {$list :list each=dative}.} + .match {$count :number} + one {{I-am dat cadouri {$list :list each=dative}.}} + * {{Le-am dat cadouri {$list :list each=dative}.}} `, 'ro', { functions: { list: listFn({ dative }) } } @@ -309,7 +309,7 @@ describe('Neighbouring text transformations (unicode-org/message-format-wg#160)' } test('Match, no change', () => { - const mf = new MessageFormat('{A {$foo} and an {$other}}', 'en'); + const mf = new MessageFormat('A {$foo} and an {$other}', 'en'); const parts = mf.formatToParts({ foo: 'foo', other: 'other' }); hackyFixArticles(['en'], parts); expect(parts).toEqual([ @@ -321,7 +321,7 @@ describe('Neighbouring text transformations (unicode-org/message-format-wg#160)' }); test('Match, changed', () => { - const mf = new MessageFormat('{A {$foo} and an {$other}}', 'en'); + const mf = new MessageFormat('A {$foo} and an {$other}', 'en'); const parts = mf.formatToParts({ foo: 'other', other: 'foo' }); hackyFixArticles(['en'], parts); expect(parts).toEqual([ @@ -333,7 +333,7 @@ describe('Neighbouring text transformations (unicode-org/message-format-wg#160)' }); test('No match, no change', () => { - const mf = new MessageFormat('{The {$foo} and lotsa {$other}}', 'en'); + const mf = new MessageFormat('The {$foo} and lotsa {$other}', 'en'); const parts = mf.formatToParts({ foo: 'foo', other: 'other' }); hackyFixArticles(['en'], parts); expect(parts).toEqual([ @@ -345,7 +345,7 @@ describe('Neighbouring text transformations (unicode-org/message-format-wg#160)' }); test('Articles across non-wordy content', () => { - const mf = new MessageFormat('{{$foo} foo and a {|...|} {$other}}', 'en'); + const mf = new MessageFormat('{$foo} foo and a {|...|} {$other}', 'en'); const parts = mf.formatToParts({ foo: 'An', other: 'other' }); hackyFixArticles(['en'], parts); expect(parts).toEqual([ diff --git a/packages/mf2-messageformat/src/stringifier/message.ts b/packages/mf2-messageformat/src/stringifier/message.ts index b86a7d3d..2951ebd5 100644 --- a/packages/mf2-messageformat/src/stringifier/message.ts +++ b/packages/mf2-messageformat/src/stringifier/message.ts @@ -29,23 +29,23 @@ export function stringifyMessage(msg: Message | MessageFormat) { let res = ''; for (const decl of msg.declarations) res += stringifyDeclaration(decl); if (isPatternMessage(msg)) { - res += stringifyPattern(msg.pattern); + res += stringifyPattern(msg.pattern, !!res); } else if (isSelectMessage(msg)) { - res += 'match'; + res += '.match'; for (const sel of msg.selectors) res += ' ' + stringifyExpression(sel); for (const { keys, value } of msg.variants) { - res += '\nwhen '; + res += '\n'; for (const key of keys) { res += (isLiteral(key) ? stringifyLiteral(key) : '*') + ' '; } - res += stringifyPattern(value); + res += stringifyPattern(value, true); } } return res; } function stringifyDeclaration({ name, value }: Declaration) { - return `let $${name} = ${stringifyExpression(value)}\n`; + return `.let $${name} = ${stringifyExpression(value)}\n`; } function stringifyFunctionAnnotation({ @@ -71,12 +71,15 @@ function stringifyOption({ name, value }: Option) { return `${name}=${valueStr}`; } -function stringifyPattern({ body }: Pattern) { +function stringifyPattern({ body }: Pattern, quoted: boolean) { let res = ''; + if (!quoted && typeof body[0] === 'string' && body[0][0] === '.') { + quoted = true; + } for (const el of body) { res += typeof el === 'string' ? el : stringifyExpression(el); } - return `{${res}}`; + return quoted ? `{{${res}}}` : res; } function stringifyExpression({ arg, annotation }: Expression) { From ad608e1451f0bedd369290d4e7fa833513962bdd Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Thu, 28 Dec 2023 19:01:08 +0100 Subject: [PATCH 07/44] feat(mf2): Replace .let with .input and .local --- .../src/__fixtures/test-messages.json | 94 ++++++--- .../src/cst-parser/as-data-model.ts | 27 ++- .../src/cst-parser/cst-parser.test.ts | 6 +- .../src/cst-parser/cst-types.ts | 17 +- .../src/cst-parser/declarations.ts | 182 ++++++++++++------ .../src/cst-parser/message.ts | 2 + packages/mf2-messageformat/src/data-model.ts | 16 +- packages/mf2-messageformat/src/errors.ts | 14 +- .../expression/function-annotation.test.ts | 2 +- .../mf2-messageformat/src/expression/index.ts | 13 +- .../src/messageformat.test.ts | 6 +- .../src/mf2-features.test.ts | 2 +- .../src/stringifier/message.ts | 10 +- 13 files changed, 274 insertions(+), 117 deletions(-) diff --git a/packages/mf2-messageformat/src/__fixtures/test-messages.json b/packages/mf2-messageformat/src/__fixtures/test-messages.json index 1a88fb06..e2a23bb6 100644 --- a/packages/mf2-messageformat/src/__fixtures/test-messages.json +++ b/packages/mf2-messageformat/src/__fixtures/test-messages.json @@ -58,76 +58,102 @@ "exp": "hello 4.20" }, { - "src": ".let $foo = {bar} {{bar {$foo}}}", + "src": ".local $foo = {bar} {{bar {$foo}}}", "exp": "bar bar" }, { - "src": ".let $foo = {|bar|} {{bar {$foo}}}", + "src": ".local $foo = {|bar|} {{bar {$foo}}}", "exp": "bar bar" }, { - "src": ".let $foo = {|bar|} {{bar {$foo}}}", + "src": ".local $foo = {|bar|} {{bar {$foo}}}", "params": { "foo": "foo" }, "exp": "bar bar" }, { - "src": ".let $foo = {$bar} {{bar {$foo}}}", + "src": ".local $foo = {$bar} {{bar {$foo}}}", "params": { "bar": "foo" }, "exp": "bar foo" }, { - "src": ".let $foo = {$bar :number} {{bar {$foo}}}", + "src": ".local $foo = {$bar :number} {{bar {$foo}}}", "params": { "bar": 4.2 }, "exp": "bar 4.2" }, { - "src": ".let $foo = {$bar :number minimumFractionDigits=2} {{bar {$foo}}}", + "src": ".local $foo = {$bar :number minimumFractionDigits=2} {{bar {$foo}}}", "params": { "bar": 4.2 }, "exp": "bar 4.20" }, { - "src": ".let $foo = {$bar :number minimumFractionDigits=foo} {{bar {$foo}}}", + "src": ".local $foo = {$bar :number minimumFractionDigits=foo} {{bar {$foo}}}", "params": { "bar": 4.2 }, "exp": "bar {$bar}", "errors": [{ "type": "bad-option" }] }, { - "src": ".let $foo = {$bar :number} {{bar {$foo}}}", + "src": ".local $foo = {$bar :number} {{bar {$foo}}}", "params": { "bar": "foo" }, "exp": "bar {$bar}", "errors": [{ "type": "bad-input" }] }, { - "src": ".let $foo = {$baz} .let $bar = {$foo} {{bar {$bar}}}", + "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}", "params": { "baz": "foo" }, "exp": "bar foo" }, { - "src": ".let $foo = {$foo} {{bar {$foo}}}", + "src": ".input {$foo} {{bar {$foo}}}", "params": { "foo": "foo" }, "exp": "bar foo" }, { - "src": ".let $foo = {$foo} .let $foo = {42} {{bar {$foo}}}", + "src": ".input {$foo :number} {{bar {$foo}}}", + "params": { "foo": 4.2 }, + "exp": "bar 4.2" + }, + { + "src": ".input {$foo :number minimumFractionDigits=2} {{bar {$foo}}}", + "params": { "foo": 4.2 }, + "exp": "bar 4.20" + }, + { + "src": ".input {$foo :number minimumFractionDigits=foo} {{bar {$foo}}}", + "params": { "foo": 4.2 }, + "exp": "bar {$foo}", + "errors": [{ "type": "bad-option" }] + }, + { + "src": ".input {$foo :number} {{bar {$foo}}}", "params": { "foo": "foo" }, - "exp": "bar 42" + "exp": "bar {$foo}", + "errors": [{ "type": "bad-input" }] }, { - "src": ".let $foo = {42} .let $foo = {$foo} {{bar {$foo}}}", + "src": ".input {$foo} .local $bar = {$foo} {{bar {$bar}}}", "params": { "foo": "foo" }, - "exp": "bar 42" + "exp": "bar foo" }, { - "src": ".let $foo = {$foo} .let $foo = {42} {{bar {$foo}}}", - "exp": "bar 42" + "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}", + "params": { "baz": "foo" }, + "exp": "bar foo" + }, + { + "src": ".input {$foo} .local $foo = {42} {{bar {$foo}}}", + "syntaxError": true }, { - "src": ".let $foo = {:unknown} .let $foo = {42} {{bar {$foo}}}", - "exp": "bar 42" + "src": ".local $foo = {42} .input {$foo} {{bar {$foo}}}", + "syntaxError": true }, { - "src": ".let $x = {42} .let $y = {$x} .let $x = {13} {{{$x} {$y}}}", - "exp": "13 42" + "src": ".local $foo = {:unknown} .local $foo = {42} {{bar {$foo}}}", + "syntaxError": true + }, + { + "src": ".local $x = {42} .local $y = {$x} {{{$x} {$y}}}", + "exp": "42 42" }, { "src": ".match {$foo} |1| {{one}} * {{other}}", @@ -181,42 +207,42 @@ "exp": "other" }, { - "src": ".let $foo = {$bar :number} .match {$foo} one {{one}} * {{other}}", + "src": ".local $foo = {$bar :number} .match {$foo} one {{one}} * {{other}}", "params": { "bar": 1 }, "exp": "one" }, { - "src": ".let $foo = {$bar :number} .match {$foo} one {{one}} * {{other}}", + "src": ".local $foo = {$bar :number} .match {$foo} one {{one}} * {{other}}", "params": { "bar": 2 }, "exp": "other" }, { - "src": ".let $foo = {$bar} .match {$foo :number} one {{one}} * {{other}}", + "src": ".local $foo = {$bar} .match {$foo :number} one {{one}} * {{other}}", "params": { "bar": 1 }, "exp": "one" }, { - "src": ".let $foo = {$bar} .match {$foo :number} one {{one}} * {{other}}", + "src": ".local $foo = {$bar} .match {$foo :number} one {{one}} * {{other}}", "params": { "bar": 2 }, "exp": "other" }, { - "src": ".let $foo = {$bar} .match {$foo} one {{one}} * {{other}}", + "src": ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}", "params": { "bar": 1 }, "exp": "one" }, { - "src": ".let $foo = {$bar} .match {$foo} one {{one}} * {{other}}", + "src": ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}", "params": { "bar": 2 }, "exp": "other" }, { - "src": ".let $bar = {$none} .match {$foo} one {{one}} * {{{$bar}}}", + "src": ".local $bar = {$none} .match {$foo} one {{one}} * {{{$bar}}}", "params": { "foo": 1 }, "exp": "one" }, { - "src": ".let $bar = {$none} .match {$foo} one {{one}} * {{{$bar}}}", + "src": ".local $bar = {$none} .match {$foo} one {{one}} * {{{$bar}}}", "params": { "foo": 2 }, "exp": "{$none}", "errors": [{ "type": "unresolved-var" }] @@ -291,11 +317,15 @@ { "src": "{{missing end braces", "syntaxError": true }, { "src": "{{missing end {$braces", "syntaxError": true }, { "src": "{{extra}} content", "syntaxError": true }, - { "src": ".let bar = {|foo|} {{{$bar}}}", "syntaxError": true }, - { "src": ".let $bar {|foo|} {{{$bar}}}", "syntaxError": true }, - { "src": ".let $bar = |foo| {{{$bar}}}", "syntaxError": true }, + { "src": ".local bar = {|foo|} {{{$bar}}}", "syntaxError": true }, + { "src": ".local $bar {|foo|} {{{$bar}}}", "syntaxError": true }, + { "src": ".local $bar = |foo| {{{$bar}}}", "syntaxError": true }, + { + "src": ".local $foo = {$bar} .local $bar = {$baz} {{bar {$foo}}}", + "syntaxError": true + }, { - "src": ".let $foo = {$bar} .let $bar = {$baz} {{bar {$foo}}}", + "src": ".local $foo = {$bar :func} .local $bar = {$baz} {{bar {$foo}}}", "syntaxError": true }, { "src": "empty { }", "syntaxError": true }, diff --git a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts index 1d3c6ee3..fd666e48 100644 --- a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts +++ b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts @@ -10,10 +10,29 @@ import type * as CST from './cst-types.js'; export function asDataModel(msg: CST.Message): Model.Message { for (const error of msg.errors) throw error; const declarations: Model.Declaration[] = (msg.declarations ?? []).map( - decl => ({ - name: asValue(decl.target).name, - value: asExpression(decl.value) - }) + decl => { + switch (decl.type) { + case 'input': { + const value = asExpression(decl.value); + if (value.arg?.type === 'variable') { + return { + type: 'input', + name: value.arg.name, + value: value as Model.Expression + }; + } + break; + } + case 'local': + return { + type: 'local', + name: asValue(decl.target).name, + value: asExpression(decl.value) + }; + } + const { start, end } = decl.value; + throw new MessageSyntaxError('parse-error', start, end); + } ); if (msg.type === 'select') { return { diff --git a/packages/mf2-messageformat/src/cst-parser/cst-parser.test.ts b/packages/mf2-messageformat/src/cst-parser/cst-parser.test.ts index 2674b42a..d40bd2dd 100644 --- a/packages/mf2-messageformat/src/cst-parser/cst-parser.test.ts +++ b/packages/mf2-messageformat/src/cst-parser/cst-parser.test.ts @@ -55,7 +55,7 @@ describe('messages in resources', () => { }); test('complex pattern with leading .', () => { - const src = '{{.let}}'; + const src = '{{.local}}'; const msg = parseMessage(src, { resource: false }); expect(msg).toMatchObject({ type: 'complex', @@ -64,11 +64,11 @@ describe('messages in resources', () => { pattern: { quotes: [ { start: 0, end: 2, value: '{{' }, - { start: 6, end: 8, value: '}}' } + { start: 8, end: 10, value: '}}' } ], start: 0, end: src.length, - body: [{ type: 'text', start: 2, end: src.length - 2, value: '.let' }] + body: [{ type: 'text', start: 2, end: src.length - 2, value: '.local' }] } }); }); diff --git a/packages/mf2-messageformat/src/cst-parser/cst-types.ts b/packages/mf2-messageformat/src/cst-parser/cst-types.ts index 7213458f..a6bc4b30 100644 --- a/packages/mf2-messageformat/src/cst-parser/cst-types.ts +++ b/packages/mf2-messageformat/src/cst-parser/cst-types.ts @@ -30,10 +30,23 @@ export interface SelectMessage { } /** @beta */ -export interface Declaration { +export type Declaration = InputDeclaration | LocalDeclaration; + +/** @beta */ +export interface InputDeclaration { + type: 'input'; + start: number; + end: number; + keyword: Syntax<'.input'>; + value: Expression | Junk; +} + +/** @beta */ +export interface LocalDeclaration { + type: 'local'; start: number; end: number; - let: Syntax<'.let'>; + keyword: Syntax<'.local'>; target: VariableRef | Junk; equals: Syntax<'=' | ''>; value: Expression | Junk; diff --git a/packages/mf2-messageformat/src/cst-parser/declarations.ts b/packages/mf2-messageformat/src/cst-parser/declarations.ts index a90b888a..cfd6e1c4 100644 --- a/packages/mf2-messageformat/src/cst-parser/declarations.ts +++ b/packages/mf2-messageformat/src/cst-parser/declarations.ts @@ -8,46 +8,82 @@ export function parseDeclarations(ctx: ParseContext): { declarations: CST.Declaration[]; end: number; } { - let pos = whitespaces(ctx.source, 0); + const { source } = ctx; + let pos = whitespaces(source, 0); const declarations: CST.Declaration[] = []; - while (ctx.source.startsWith('.let', pos)) { - const decl = parseDeclaration(ctx, pos); + while (pos < source.length) { + const decl = source.startsWith('.input', pos) + ? parseInputDeclaration(ctx, pos) + : source.startsWith('.local', pos) + ? parseLocalDeclaration(ctx, pos) + : null; + if (!decl) break; declarations.push(decl); pos = decl.end; - pos += whitespaces(ctx.source, pos); + pos += whitespaces(source, pos); } - checkLocalVarReferences(ctx, declarations); + checkDeclarations(ctx, declarations); return { declarations, end: pos }; } -function parseDeclaration(ctx: ParseContext, start: number): CST.Declaration { - let pos = start + 4; // '.let' - const let_: CST.Syntax<'.let'> = { start, end: pos, value: '.let' }; - const ws = whitespaces(ctx.source, pos); +function parseInputDeclaration( + ctx: ParseContext, + start: number +): CST.Declaration { + // + let pos = start + 6; // '.input' + const keyword: CST.Syntax<'.input'> = { start, end: pos, value: '.input' }; + pos += whitespaces(ctx.source, pos); + + const value = parseDeclarationValue(ctx, pos); + if (value.type === 'expression') { + const { body } = value; + if ( + !( + body.type === 'variable' || + (body.type === 'function' && body.operand?.type === 'variable') + ) + ) { + ctx.onError('bad-input-expression', value.start, value.end); + } + } + + return { type: 'input', start, end: value.end, keyword, value }; +} + +function parseLocalDeclaration( + ctx: ParseContext, + start: number +): CST.LocalDeclaration { + const { source } = ctx; + + let pos = start + 6; // '.local' + const keyword: CST.Syntax<'.local'> = { start, end: pos, value: '.local' }; + const ws = whitespaces(source, pos); pos += ws; if (ws === 0) ctx.onError('missing-syntax', pos, ' '); let target: CST.VariableRef | CST.Junk; - if (ctx.source[pos] === '$') { + if (source[pos] === '$') { target = parseVariable(ctx, pos); pos = target.end; } else { const junkStart = pos; - const junkEndOffset = ctx.source.substring(pos).search(/[\t\n\r ={}]/); - pos = junkEndOffset === -1 ? ctx.source.length : pos + junkEndOffset; + const junkEndOffset = source.substring(pos).search(/[\t\n\r ={}]/); + pos = junkEndOffset === -1 ? source.length : pos + junkEndOffset; target = { type: 'junk', start: junkStart, end: pos, - source: ctx.source.substring(junkStart, pos) + source: source.substring(junkStart, pos) }; ctx.onError('missing-syntax', junkStart, '$'); } - pos += whitespaces(ctx.source, pos); + pos += whitespaces(source, pos); let equals: CST.Syntax<'=' | ''>; - if (ctx.source[pos] === '=') { + if (source[pos] === '=') { equals = { start: pos, end: pos + 1, value: '=' }; pos += 1; } else { @@ -55,59 +91,85 @@ function parseDeclaration(ctx: ParseContext, start: number): CST.Declaration { ctx.onError('missing-syntax', pos, '='); } - let value: CST.Expression | CST.Junk; - pos += whitespaces(ctx.source, pos); - if (ctx.source[pos] === '{') { - value = parseExpression(ctx, pos); - pos = value.end; - } else { - const junkStart = pos; - const junkEndOffset = ctx.source - .substring(pos) - .search(/\blet|\bmatch|\bwhen|[${}]/); - pos = junkEndOffset === -1 ? ctx.source.length : pos + junkEndOffset; - value = { - type: 'junk', - start: junkStart, - end: pos, - source: ctx.source.substring(junkStart, pos) - }; - ctx.onError('missing-syntax', junkStart, '{'); - } + pos += whitespaces(source, pos); + const value = parseDeclarationValue(ctx, pos); - return { start, end: pos, let: let_, target, equals, value }; + return { + type: 'local', + start, + end: value.end, + keyword, + target, + equals, + value + }; } -/** Local variable declarations can't refer to later ones */ -function checkLocalVarReferences( +function parseDeclarationValue( ctx: ParseContext, - declarations: CST.Declaration[] -) { - const check = (name: string, ref: CST.VariableRef) => { - if (ref.name === name) { - ctx.onError('bad-local-var', ref.start, ref.end); + start: number +): CST.Expression | CST.Junk { + const { source } = ctx; + if (source[start] === '{') return parseExpression(ctx, start); + + const junkEndOffset = source.substring(start).search(/\.[a-z]|{{/); + const end = junkEndOffset === -1 ? source.length : start + junkEndOffset; + ctx.onError('missing-syntax', start, '{'); + return { type: 'junk', start, end, source: source.substring(start, end) }; +} + +/** Variables can only be declared once & declarations can't refer to later ones */ +function checkDeclarations(ctx: ParseContext, declarations: CST.Declaration[]) { + const targets = new Set(); + const refs = new Map(); + const addRef = (ref: CST.Literal | CST.VariableRef | undefined) => { + if (ref?.type === 'variable') { + const prev = refs.get(ref.name); + if (prev) prev.push(ref); + else refs.set(ref.name, [ref]); } }; - for (let i = 1; i < declarations.length; ++i) { - const { name } = declarations[i].target; - if (!name) continue; - for (let j = 0; j < i; ++j) { - const { target, value } = declarations[j]; - if (target.name === name) break; - if (value.type === 'expression') { - const exp = value.body; - switch (exp.type) { - case 'function': - if (exp.operand?.type === 'variable') check(name, exp.operand); - for (const opt of exp.options) { - if (opt.value.type === 'variable') check(name, opt.value); - } - break; - case 'variable': - check(name, exp); - break; + for (const decl of declarations) { + let target: CST.VariableRef | undefined; + switch (decl.type) { + case 'input': + if (decl.value.type === 'expression') { + const db = decl.value.body; + switch (db.type) { + case 'variable': + target = db; + break; + case 'function': + if (db.operand?.type === 'variable') target = db.operand; + } } + break; + case 'local': + if (decl.target.type === 'variable') target = decl.target; + break; + } + if (target) { + if (targets.has(target.name)) { + ctx.onError('duplicate-declaration', target.start, target.end); + } else { + targets.add(target.name); + } + for (const { start, end } of refs.get(target.name) ?? []) { + ctx.onError('forward-reference', start, end); + } + } + + if (decl.value.type === 'expression') { + const { body } = decl.value; + switch (body.type) { + case 'variable': + if (decl.type !== 'input') addRef(body); + break; + case 'function': + if (decl.type !== 'input') addRef(body.operand); + for (const opt of body.options) addRef(opt.value); + break; } } } diff --git a/packages/mf2-messageformat/src/cst-parser/message.ts b/packages/mf2-messageformat/src/cst-parser/message.ts index 0e15ef20..b3d4ee82 100644 --- a/packages/mf2-messageformat/src/cst-parser/message.ts +++ b/packages/mf2-messageformat/src/cst-parser/message.ts @@ -32,6 +32,8 @@ export class ParseContext { ) { let err: MessageSyntaxError; switch (type) { + case 'duplicate-declaration': + case 'forward-reference': case 'key-mismatch': case 'missing-fallback': err = new MessageDataModelError(type, start, Number(end)); diff --git a/packages/mf2-messageformat/src/data-model.ts b/packages/mf2-messageformat/src/data-model.ts index 6d7c6f3e..d1adf775 100644 --- a/packages/mf2-messageformat/src/data-model.ts +++ b/packages/mf2-messageformat/src/data-model.ts @@ -29,14 +29,22 @@ export interface PatternMessage { } /** - * A message may declare any number of local variables or aliases, + * A message may declare any number of input and local variables, * each with a value defined by an expression. - * The order of the declarations is not relevant, - * but a valid message may not include a dependency loop amond them. + * The variable name for each declaration must be unique. * * @beta */ -export interface Declaration { +export type Declaration = InputDeclaration | LocalDeclaration; + +export interface InputDeclaration { + type: 'input'; + name: string; + value: Expression; +} + +export interface LocalDeclaration { + type: 'local'; name: string; value: Expression; } diff --git a/packages/mf2-messageformat/src/errors.ts b/packages/mf2-messageformat/src/errors.ts index 83a3fea0..773b0a3f 100644 --- a/packages/mf2-messageformat/src/errors.ts +++ b/packages/mf2-messageformat/src/errors.ts @@ -16,9 +16,11 @@ export class MessageSyntaxError extends MessageError { declare type: | 'empty-token' | 'bad-escape' - | 'bad-local-var' + | 'bad-input-expression' | 'bad-selector' + | 'duplicate-declaration' | 'extra-content' + | 'forward-reference' | 'key-mismatch' | 'parse-error' | 'missing-fallback' @@ -37,7 +39,7 @@ export class MessageSyntaxError extends MessageError { switch (type) { case 'empty-token': case 'bad-escape': - case 'bad-local-var': + case 'bad-input-expression': case 'bad-selector': case 'extra-content': case 'parse-error': @@ -46,6 +48,8 @@ export class MessageSyntaxError extends MessageError { case 'missing-syntax': message = `Syntax parse error: Missing ${expected} at ${start}`; break; + case 'duplicate-declaration': + case 'forward-reference': case 'key-mismatch': case 'missing-fallback': message = `Data model error: ${type}`; @@ -67,7 +71,11 @@ export class MissingSyntaxError extends MessageSyntaxError { } export class MessageDataModelError extends MessageSyntaxError { - declare type: 'key-mismatch' | 'missing-fallback'; + declare type: + | 'duplicate-declaration' + | 'forward-reference' + | 'key-mismatch' + | 'missing-fallback'; } export class MessageResolutionError extends MessageError { diff --git a/packages/mf2-messageformat/src/expression/function-annotation.test.ts b/packages/mf2-messageformat/src/expression/function-annotation.test.ts index b3a0606e..cb3feb2c 100644 --- a/packages/mf2-messageformat/src/expression/function-annotation.test.ts +++ b/packages/mf2-messageformat/src/expression/function-annotation.test.ts @@ -22,7 +22,7 @@ test('Custom function', () => { describe('inputs with options', () => { test('local variable with :number expression', () => { const mf = new MessageFormat( - `.let $val = {12345678 :number useGrouping=false} + `.local $val = {12345678 :number useGrouping=false} {{{$val :number minimumFractionDigits=2}}}`, 'en' ); diff --git a/packages/mf2-messageformat/src/expression/index.ts b/packages/mf2-messageformat/src/expression/index.ts index 07573fb4..96a650e9 100644 --- a/packages/mf2-messageformat/src/expression/index.ts +++ b/packages/mf2-messageformat/src/expression/index.ts @@ -36,13 +36,18 @@ export { * * @beta */ -export type Expression = - | { +export type Expression< + A extends Literal | VariableRef | undefined = + | Literal + | VariableRef + | undefined +> = A extends Literal | VariableRef + ? { type: 'expression'; - arg: Literal | VariableRef; + arg: A; annotation?: FunctionAnnotation | UnsupportedAnnotation; } - | { + : { type: 'expression'; arg?: never; annotation: FunctionAnnotation | UnsupportedAnnotation; diff --git a/packages/mf2-messageformat/src/messageformat.test.ts b/packages/mf2-messageformat/src/messageformat.test.ts index 1aee3119..1db86405 100644 --- a/packages/mf2-messageformat/src/messageformat.test.ts +++ b/packages/mf2-messageformat/src/messageformat.test.ts @@ -19,7 +19,11 @@ export type TestMessage = describe('Format messages', () => { for (const testMsg of testMessages as TestMessage[]) { if (testMsg.syntaxError) { - expect(() => new MessageFormat(testMsg.src)).toThrow(MessageSyntaxError); + test(testName(testMsg), () => { + expect(() => new MessageFormat(testMsg.src)).toThrow( + MessageSyntaxError + ); + }); } else { const { src, diff --git a/packages/mf2-messageformat/src/mf2-features.test.ts b/packages/mf2-messageformat/src/mf2-features.test.ts index 8220de29..23b45619 100644 --- a/packages/mf2-messageformat/src/mf2-features.test.ts +++ b/packages/mf2-messageformat/src/mf2-features.test.ts @@ -40,7 +40,7 @@ describe('Plural Range Selectors & Range Formatters (unicode-org/message-format- test('input as { start, end } object', () => { const mf = new MessageFormat( source` - .let $range = {$range :range} + .input {$range :range} .match {$range} one {{{$range} dag}} * {{{$range} dagen}} diff --git a/packages/mf2-messageformat/src/stringifier/message.ts b/packages/mf2-messageformat/src/stringifier/message.ts index 2951ebd5..7c5d02d5 100644 --- a/packages/mf2-messageformat/src/stringifier/message.ts +++ b/packages/mf2-messageformat/src/stringifier/message.ts @@ -44,8 +44,14 @@ export function stringifyMessage(msg: Message | MessageFormat) { return res; } -function stringifyDeclaration({ name, value }: Declaration) { - return `.let $${name} = ${stringifyExpression(value)}\n`; +function stringifyDeclaration({ type, name, value }: Declaration) { + switch (type) { + case 'input': + return `.input ${stringifyExpression(value)}\n`; + case 'local': + return `.local $${name} = ${stringifyExpression(value)}\n`; + } + throw new Error(`Unsupported ${type} declaration`); } function stringifyFunctionAnnotation({ From c0dda5188d12a86461ea103b71ac41bf23d79509 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Thu, 28 Dec 2023 21:12:25 +0100 Subject: [PATCH 08/44] feat(mf2): Add ReservedStatement --- packages/mf2-fluent/src/message-to-fluent.ts | 2 +- .../src/cst-parser/as-data-model.ts | 66 +++++++++++-------- .../src/cst-parser/cst-types.ts | 23 +++++-- .../src/cst-parser/declarations.ts | 62 ++++++++++++++--- .../src/cst-parser/expression.ts | 30 ++++++--- packages/mf2-messageformat/src/data-model.ts | 14 +++- packages/mf2-messageformat/src/errors.ts | 3 +- .../expression/unsupported-annotation.test.ts | 4 +- .../src/expression/unsupported-annotation.ts | 10 ++- .../mf2-messageformat/src/messageformat.ts | 19 ++++-- .../src/stringifier/message.ts | 17 +++-- 11 files changed, 177 insertions(+), 73 deletions(-) diff --git a/packages/mf2-fluent/src/message-to-fluent.ts b/packages/mf2-fluent/src/message-to-fluent.ts index 2cb6df75..5bad5b52 100644 --- a/packages/mf2-fluent/src/message-to-fluent.ts +++ b/packages/mf2-fluent/src/message-to-fluent.ts @@ -259,7 +259,7 @@ function variableRefToFluent( { name }: VariableRef ): Fluent.InlineExpression { const local = ctx.declarations.find(decl => decl.name === name); - return local + return local?.value ? expressionToFluent(ctx, local.value) : new Fluent.VariableReference(new Fluent.Identifier(name)); } diff --git a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts index fd666e48..da647712 100644 --- a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts +++ b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts @@ -9,31 +9,9 @@ import type * as CST from './cst-types.js'; */ export function asDataModel(msg: CST.Message): Model.Message { for (const error of msg.errors) throw error; - const declarations: Model.Declaration[] = (msg.declarations ?? []).map( - decl => { - switch (decl.type) { - case 'input': { - const value = asExpression(decl.value); - if (value.arg?.type === 'variable') { - return { - type: 'input', - name: value.arg.name, - value: value as Model.Expression - }; - } - break; - } - case 'local': - return { - type: 'local', - name: asValue(decl.target).name, - value: asExpression(decl.value) - }; - } - const { start, end } = decl.value; - throw new MessageSyntaxError('parse-error', start, end); - } - ); + const declarations: Model.Declaration[] = msg.declarations + ? msg.declarations.map(asDeclaration) + : []; if (msg.type === 'select') { return { type: 'select', @@ -55,6 +33,36 @@ export function asDataModel(msg: CST.Message): Model.Message { } } +function asDeclaration(decl: CST.Declaration): Model.Declaration { + switch (decl.type) { + case 'input': { + const value = asExpression(decl.value); + if (value.arg?.type !== 'variable') { + const { start, end } = decl.value; + throw new MessageSyntaxError('parse-error', start, end); + } + return { + type: 'input', + name: value.arg.name, + value: value as Model.Expression + }; + } + case 'local': + return { + type: 'local', + name: asValue(decl.target).name, + value: asExpression(decl.value) + }; + default: + return { + type: 'unsupported-statement', + keyword: (decl.keyword?.value ?? '').substring(1), + body: decl.body?.value || undefined, + expressions: decl.values?.map(asExpression) ?? [] + }; + } +} + function asPattern(cst: CST.Pattern): Model.Pattern { const body: Model.Pattern['body'] = cst.body.map(el => el.type === 'text' ? el.value : asExpression(el) @@ -72,7 +80,7 @@ function asExpression(cst: CST.Expression | CST.Junk): Model.Expression { return { type: 'expression', arg: asValue(cst.body) }; case 'function': return asFunctionExpression(cst.body); - case 'reserved': + case 'reserved-annotation': return asUnsupportedExpression(cst.body); default: throw new MessageSyntaxError('parse-error', cst.start, cst.end); @@ -96,11 +104,13 @@ function asFunctionExpression(cst: CST.FunctionRef): Model.Expression { : { type: 'expression', annotation }; } -function asUnsupportedExpression(cst: CST.Reserved): Model.Expression { +function asUnsupportedExpression( + cst: CST.ReservedAnnotation +): Model.Expression { const annotation: Model.UnsupportedAnnotation = { type: 'unsupported-annotation', sigil: cst.sigil, - source: cst.source + source: cst.source.value }; return cst.operand ? { type: 'expression', arg: asValue(cst.operand), annotation } diff --git a/packages/mf2-messageformat/src/cst-parser/cst-types.ts b/packages/mf2-messageformat/src/cst-parser/cst-types.ts index a6bc4b30..75b925aa 100644 --- a/packages/mf2-messageformat/src/cst-parser/cst-types.ts +++ b/packages/mf2-messageformat/src/cst-parser/cst-types.ts @@ -30,7 +30,10 @@ export interface SelectMessage { } /** @beta */ -export type Declaration = InputDeclaration | LocalDeclaration; +export type Declaration = + | InputDeclaration + | LocalDeclaration + | ReservedStatement; /** @beta */ export interface InputDeclaration { @@ -52,6 +55,16 @@ export interface LocalDeclaration { value: Expression | Junk; } +/** @beta */ +export interface ReservedStatement { + type: 'reserved-statement'; + start: number; + end: number; + keyword: Syntax; + body: Syntax; + values: Expression[]; +} + /** @beta */ export interface Variant { start: number; @@ -91,7 +104,7 @@ export interface Expression { start: number; /** position one past the `}` */ end: number; - body: Literal | VariableRef | FunctionRef | Reserved | Junk; + body: Literal | VariableRef | FunctionRef | ReservedAnnotation | Junk; } /** @beta */ @@ -136,11 +149,11 @@ export interface FunctionRef { } /** @beta */ -export interface Reserved { - type: 'reserved'; +export interface ReservedAnnotation { + type: 'reserved-annotation'; sigil: '!' | '@' | '#' | '%' | '^' | '&' | '*' | '<' | '>' | '?' | '~'; operand: Literal | VariableRef | undefined; - source: string; + source: Syntax; /** position of the sigil, so `operand.start` may be earlier */ start: number; end: number; diff --git a/packages/mf2-messageformat/src/cst-parser/declarations.ts b/packages/mf2-messageformat/src/cst-parser/declarations.ts index cfd6e1c4..f10a3dfa 100644 --- a/packages/mf2-messageformat/src/cst-parser/declarations.ts +++ b/packages/mf2-messageformat/src/cst-parser/declarations.ts @@ -1,8 +1,9 @@ import type * as CST from './cst-types.js'; import type { ParseContext } from './message.js'; -import { parseExpression } from './expression.js'; +import { parseExpression, parseReservedBody } from './expression.js'; import { whitespaces } from './util.js'; import { parseVariable } from './values.js'; +import { parseNameValue } from './names.js'; export function parseDeclarations(ctx: ParseContext): { declarations: CST.Declaration[]; @@ -11,13 +12,22 @@ export function parseDeclarations(ctx: ParseContext): { const { source } = ctx; let pos = whitespaces(source, 0); const declarations: CST.Declaration[] = []; - while (pos < source.length) { - const decl = source.startsWith('.input', pos) - ? parseInputDeclaration(ctx, pos) - : source.startsWith('.local', pos) - ? parseLocalDeclaration(ctx, pos) - : null; - if (!decl) break; + loop: while (source[pos] === '.') { + const keyword = parseNameValue(source, pos + 1); + let decl; + switch (keyword) { + case '': + case 'match': + break loop; + case 'input': + decl = parseInputDeclaration(ctx, pos); + break; + case 'local': + decl = parseLocalDeclaration(ctx, pos); + break; + default: + decl = parseReservedStatement(ctx, pos, '.' + keyword); + } declarations.push(decl); pos = decl.end; pos += whitespaces(source, pos); @@ -29,7 +39,7 @@ export function parseDeclarations(ctx: ParseContext): { function parseInputDeclaration( ctx: ParseContext, start: number -): CST.Declaration { +): CST.InputDeclaration { // let pos = start + 6; // '.input' const keyword: CST.Syntax<'.input'> = { start, end: pos, value: '.input' }; @@ -105,6 +115,36 @@ function parseLocalDeclaration( }; } +function parseReservedStatement( + ctx: ParseContext, + start: number, + keyword: string +): CST.ReservedStatement { + let pos = start + keyword.length; + pos += whitespaces(ctx.source, pos); + + const body = parseReservedBody(ctx, pos); + let end = body.end; + pos = end + whitespaces(ctx.source, end); + + const values: CST.Expression[] = []; + while (ctx.source[pos] === '{') { + const value = parseExpression(ctx, pos); + end = value.end; + pos = end + whitespaces(ctx.source, end); + } + if (values.length === 0) ctx.onError('missing-syntax', end, '{'); + + return { + type: 'reserved-statement', + start, + end, + keyword: { start, end: keyword.length, value: keyword }, + body, + values + }; +} + function parseDeclarationValue( ctx: ParseContext, start: number @@ -130,7 +170,7 @@ function checkDeclarations(ctx: ParseContext, declarations: CST.Declaration[]) { } }; - for (const decl of declarations) { + loop: for (const decl of declarations) { let target: CST.VariableRef | undefined; switch (decl.type) { case 'input': @@ -148,6 +188,8 @@ function checkDeclarations(ctx: ParseContext, declarations: CST.Declaration[]) { case 'local': if (decl.target.type === 'variable') target = decl.target; break; + default: + continue loop; } if (target) { if (targets.has(target.name)) { diff --git a/packages/mf2-messageformat/src/cst-parser/expression.ts b/packages/mf2-messageformat/src/cst-parser/expression.ts index 22e90c20..5536fb82 100644 --- a/packages/mf2-messageformat/src/cst-parser/expression.ts +++ b/packages/mf2-messageformat/src/cst-parser/expression.ts @@ -23,7 +23,7 @@ export function parseExpression( | CST.Literal | CST.VariableRef | CST.FunctionRef - | CST.Reserved + | CST.ReservedAnnotation | CST.Junk; let junkError: MessageSyntaxError | undefined; pos += whitespaces(ctx.source, pos); @@ -45,7 +45,7 @@ export function parseExpression( case '>': case '?': case '~': - body = parseReserved(ctx, pos, arg); + body = parseReservedAnnotation(ctx, pos, arg); pos = body.end; break; default: @@ -131,13 +131,28 @@ function parseOption(ctx: ParseContext, start: number): CST.Option { // / %x5D-7A ; omit { | } // / %x7E-D7FF ; omit surrogates // / %xE000-10FFFF -function parseReserved( +function parseReservedAnnotation( ctx: ParseContext, start: number, operand: CST.Literal | CST.VariableRef | undefined -): CST.Reserved { - const sigil = ctx.source[start] as CST.Reserved['sigil']; - let pos = start + 1; // sigil +): CST.ReservedAnnotation { + const sigil = ctx.source[start] as CST.ReservedAnnotation['sigil']; + const source = parseReservedBody(ctx, start + 1); // skip sigil + return { + type: 'reserved-annotation', + start, + end: source.end, + operand, + sigil, + source + }; +} + +export function parseReservedBody( + ctx: ParseContext, + start: number +): CST.Syntax { + let pos = start; loop: while (pos < ctx.source.length) { const ch = ctx.source[pos]; switch (ch) { @@ -174,6 +189,5 @@ function parseReserved( pos -= 1; prev = ctx.source[pos - 1]; } - const source = ctx.source.substring(start, pos); - return { type: 'reserved', operand, sigil, source, start, end: pos }; + return { start, end: pos, value: ctx.source.substring(start, pos) }; } diff --git a/packages/mf2-messageformat/src/data-model.ts b/packages/mf2-messageformat/src/data-model.ts index d1adf775..fd39e03d 100644 --- a/packages/mf2-messageformat/src/data-model.ts +++ b/packages/mf2-messageformat/src/data-model.ts @@ -35,7 +35,10 @@ export interface PatternMessage { * * @beta */ -export type Declaration = InputDeclaration | LocalDeclaration; +export type Declaration = + | InputDeclaration + | LocalDeclaration + | UnsupportedStatement; export interface InputDeclaration { type: 'input'; @@ -49,6 +52,15 @@ export interface LocalDeclaration { value: Expression; } +export interface UnsupportedStatement { + type: 'unsupported-statement'; + keyword: string; + name?: never; + value?: never; + body?: string; + expressions: Expression[]; +} + /** * The body of each message is composed of a sequence of parts, some of them * fixed (Text), others placeholders for values depending on additional diff --git a/packages/mf2-messageformat/src/errors.ts b/packages/mf2-messageformat/src/errors.ts index 773b0a3f..12af8a3e 100644 --- a/packages/mf2-messageformat/src/errors.ts +++ b/packages/mf2-messageformat/src/errors.ts @@ -83,7 +83,8 @@ export class MessageResolutionError extends MessageError { | 'bad-input' | 'bad-option' | 'unresolved-var' - | 'unsupported-annotation'; + | 'unsupported-annotation' + | 'unsupported-statement'; source: string; constructor( type: typeof MessageResolutionError.prototype.type, diff --git a/packages/mf2-messageformat/src/expression/unsupported-annotation.test.ts b/packages/mf2-messageformat/src/expression/unsupported-annotation.test.ts index 08be3d3b..8a48c91a 100644 --- a/packages/mf2-messageformat/src/expression/unsupported-annotation.test.ts +++ b/packages/mf2-messageformat/src/expression/unsupported-annotation.test.ts @@ -33,9 +33,7 @@ describe('Reserved syntax', () => { const msg = resolve('{ # one\ntwo\rthree four }', {}, [ { type: 'unsupported-annotation' } ]); - expect(msg).toMatchObject([ - { type: 'fallback', source: '# one\ntwo\rthree four' } - ]); + expect(msg).toMatchObject([{ type: 'fallback', source: '#' }]); }); test('surrogates', () => { diff --git a/packages/mf2-messageformat/src/expression/unsupported-annotation.ts b/packages/mf2-messageformat/src/expression/unsupported-annotation.ts index 40b3e22c..5780be3e 100644 --- a/packages/mf2-messageformat/src/expression/unsupported-annotation.ts +++ b/packages/mf2-messageformat/src/expression/unsupported-annotation.ts @@ -36,11 +36,9 @@ export const isUnsupportedAnnotation = ( export function resolveUnsupportedAnnotation( ctx: Context, operand: Literal | VariableRef | undefined, - { sigil, source = '�' }: UnsupportedAnnotation + { sigil = '�' }: UnsupportedAnnotation ) { - const msg = `Reserved ${sigil ?? '�'} annotation is not supported`; - ctx.onError( - new MessageResolutionError('unsupported-annotation', msg, source) - ); - return fallback(getValueSource(operand) ?? source); + const msg = `Reserved ${sigil} annotation is not supported`; + ctx.onError(new MessageResolutionError('unsupported-annotation', msg, sigil)); + return fallback(getValueSource(operand) ?? sigil); } diff --git a/packages/mf2-messageformat/src/messageformat.ts b/packages/mf2-messageformat/src/messageformat.ts index 99988a48..76ef38ec 100644 --- a/packages/mf2-messageformat/src/messageformat.ts +++ b/packages/mf2-messageformat/src/messageformat.ts @@ -13,7 +13,7 @@ import { MessagePart, MessageValue } from './runtime'; -import { MessageError } from './errors.js'; +import { MessageError, MessageResolutionError } from './errors.js'; /** @beta */ export interface MessageFormatOptions { @@ -140,10 +140,19 @@ export class MessageFormat { } ) { let scope = { ...msgParams }; - for (const { name, value } of this.#message.declarations) { - const ue = new UnresolvedExpression(value, scope); - if (name in scope) scope = { ...scope, [name]: ue }; - else scope[name] = ue; + for (const decl of this.#message.declarations) { + if (decl.type === 'unsupported-statement') { + const source = decl.keyword ?? '�'; + const msg = `Reserved ${source} annotation is not supported`; + onError( + new MessageResolutionError('unsupported-statement', msg, source) + ); + } else { + const { name, value } = decl; + const ue = new UnresolvedExpression(value, scope); + if (name in scope) scope = { ...scope, [name]: ue }; + else scope[name] = ue; + } } const ctx: Context = { onError, diff --git a/packages/mf2-messageformat/src/stringifier/message.ts b/packages/mf2-messageformat/src/stringifier/message.ts index 7c5d02d5..5612b9a1 100644 --- a/packages/mf2-messageformat/src/stringifier/message.ts +++ b/packages/mf2-messageformat/src/stringifier/message.ts @@ -44,14 +44,21 @@ export function stringifyMessage(msg: Message | MessageFormat) { return res; } -function stringifyDeclaration({ type, name, value }: Declaration) { - switch (type) { +function stringifyDeclaration(decl: Declaration) { + switch (decl.type) { case 'input': - return `.input ${stringifyExpression(value)}\n`; + return `.input ${stringifyExpression(decl.value)}\n`; case 'local': - return `.local $${name} = ${stringifyExpression(value)}\n`; + return `.local $${decl.name} = ${stringifyExpression(decl.value)}\n`; + case 'unsupported-statement': { + const parts = [`.${decl.keyword}`]; + if (decl.body) parts.push(decl.body); + for (const exp of decl.expressions) parts.push(stringifyExpression(exp)); + return parts.join(' '); + } } - throw new Error(`Unsupported ${type} declaration`); + // @ts-expect-error Guard against non-TS users with bad data + throw new Error(`Unsupported ${decl.type} declaration`); } function stringifyFunctionAnnotation({ From 1d9e73f9d74b4fac0c545b7e25e4ceeb4dd1226f Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Thu, 28 Dec 2023 21:53:47 +0100 Subject: [PATCH 09/44] fix(mf2): Refactor CST Expression structure --- .../src/cst-parser/as-data-model.ts | 80 +++++++++---------- .../src/cst-parser/cst-parser.test.ts | 15 ++-- .../src/cst-parser/cst-types.ts | 14 ++-- .../src/cst-parser/declarations.ts | 36 +++------ .../src/cst-parser/expression.ts | 70 ++++++++-------- .../src/cst-parser/message.ts | 26 +++--- 6 files changed, 110 insertions(+), 131 deletions(-) diff --git a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts index da647712..322bf708 100644 --- a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts +++ b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts @@ -71,50 +71,44 @@ function asPattern(cst: CST.Pattern): Model.Pattern { } function asExpression(cst: CST.Expression | CST.Junk): Model.Expression { - if (cst.type !== 'expression') { - throw new MessageSyntaxError('parse-error', cst.start, cst.end); - } - switch (cst.body.type) { - case 'literal': - case 'variable': - return { type: 'expression', arg: asValue(cst.body) }; - case 'function': - return asFunctionExpression(cst.body); - case 'reserved-annotation': - return asUnsupportedExpression(cst.body); - default: - throw new MessageSyntaxError('parse-error', cst.start, cst.end); - } -} - -function asFunctionExpression(cst: CST.FunctionRef): Model.Expression { - const annotation: Model.FunctionAnnotation = { - type: 'function', - kind: cst.kind, - name: cst.name - }; - if (cst.options && cst.options.length > 0) { - annotation.options = cst.options.map(opt => ({ - name: opt.name, - value: asValue(opt.value) - })); + if (cst.type === 'expression') { + const arg = cst.arg ? asValue(cst.arg) : undefined; + let annotation: + | Model.FunctionAnnotation + | Model.UnsupportedAnnotation + | undefined; + const ca = cst.annotation; + if (ca) { + switch (ca.type) { + case 'function': + annotation = { type: 'function', kind: ca.kind, name: ca.name }; + if (ca.options && ca.options.length > 0) { + annotation.options = ca.options.map(opt => ({ + name: opt.name, + value: asValue(opt.value) + })); + } + break; + case 'reserved-annotation': + annotation = { + type: 'unsupported-annotation', + sigil: ca.sigil, + source: ca.source.value + }; + break; + default: + throw new MessageSyntaxError('parse-error', cst.start, cst.end); + } + } + if (arg) { + return annotation + ? { type: 'expression', arg, annotation } + : { type: 'expression', arg }; + } else if (annotation) { + return { type: 'expression', annotation }; + } } - return cst.operand - ? { type: 'expression', arg: asValue(cst.operand), annotation } - : { type: 'expression', annotation }; -} - -function asUnsupportedExpression( - cst: CST.ReservedAnnotation -): Model.Expression { - const annotation: Model.UnsupportedAnnotation = { - type: 'unsupported-annotation', - sigil: cst.sigil, - source: cst.source.value - }; - return cst.operand - ? { type: 'expression', arg: asValue(cst.operand), annotation } - : { type: 'expression', annotation }; + throw new MessageSyntaxError('parse-error', cst.start, cst.end); } function asValue(cst: CST.Literal | CST.Junk): Model.Literal; diff --git a/packages/mf2-messageformat/src/cst-parser/cst-parser.test.ts b/packages/mf2-messageformat/src/cst-parser/cst-parser.test.ts index d40bd2dd..f7eead25 100644 --- a/packages/mf2-messageformat/src/cst-parser/cst-parser.test.ts +++ b/packages/mf2-messageformat/src/cst-parser/cst-parser.test.ts @@ -41,13 +41,18 @@ describe('messages in resources', () => { type: 'expression', start: 0, end: src.length, - body: { + braces: [ + { start: 0, end: 1, value: '{' }, + { start: src.length - 1, end: src.length, value: '}' } + ], + arg: { type: 'literal', quoted: true, start: 1, end: src.length - 1, value: '\t \n\r\t\x01\x02\x03' - } + }, + annotation: undefined } ] } @@ -62,7 +67,7 @@ describe('messages in resources', () => { errors: [], declarations: [], pattern: { - quotes: [ + braces: [ { start: 0, end: 2, value: '{{' }, { start: 8, end: 10, value: '}}' } ], @@ -99,7 +104,7 @@ describe('messages in resources', () => { body: [ { type: 'expression', - body: { type: 'literal', value: '1\n \t2 \n \\ 3\n\\t' } + arg: { type: 'literal', value: '1\n \t2 \n \\ 3\n\\t' } } ] } @@ -112,7 +117,7 @@ describe('messages in resources', () => { body: [ { type: 'expression', - body: { type: 'literal', value: '1\n2 \n 3\n\t' } + arg: { type: 'literal', value: '1\n2 \n 3\n\t' } } ] } diff --git a/packages/mf2-messageformat/src/cst-parser/cst-types.ts b/packages/mf2-messageformat/src/cst-parser/cst-types.ts index 75b925aa..00bdd757 100644 --- a/packages/mf2-messageformat/src/cst-parser/cst-types.ts +++ b/packages/mf2-messageformat/src/cst-parser/cst-types.ts @@ -86,7 +86,7 @@ export interface Pattern { start: number; end: number; body: Array; - quotes?: [Syntax<'{{'>] | [Syntax<'{{'>, Syntax<'}}'>]; + braces?: [Syntax<'{{'>] | [Syntax<'{{'>, Syntax<'}}'>]; } /** @beta */ @@ -100,11 +100,11 @@ export interface Text { /** @beta */ export interface Expression { type: 'expression'; - /** position of the `{` */ start: number; - /** position one past the `}` */ end: number; - body: Literal | VariableRef | FunctionRef | ReservedAnnotation | Junk; + braces: [Syntax<'{'>] | [Syntax<'{'>, Syntax<'}'>]; + arg: Literal | VariableRef | undefined; + annotation: FunctionRef | ReservedAnnotation | Junk | undefined; } /** @beta */ @@ -140,8 +140,7 @@ export interface VariableRef { export interface FunctionRef { type: 'function'; kind: 'open' | 'close' | 'value'; - operand: Literal | VariableRef | undefined; - /** position of the `:`/`+`/`-`, so `operand.start` may be earlier */ + /** position of the `:`/`+`/`-` */ start: number; end: number; name: string; @@ -152,9 +151,8 @@ export interface FunctionRef { export interface ReservedAnnotation { type: 'reserved-annotation'; sigil: '!' | '@' | '#' | '%' | '^' | '&' | '*' | '<' | '>' | '?' | '~'; - operand: Literal | VariableRef | undefined; source: Syntax; - /** position of the sigil, so `operand.start` may be earlier */ + /** position of the sigil */ start: number; end: number; } diff --git a/packages/mf2-messageformat/src/cst-parser/declarations.ts b/packages/mf2-messageformat/src/cst-parser/declarations.ts index f10a3dfa..7836ac1b 100644 --- a/packages/mf2-messageformat/src/cst-parser/declarations.ts +++ b/packages/mf2-messageformat/src/cst-parser/declarations.ts @@ -47,13 +47,7 @@ function parseInputDeclaration( const value = parseDeclarationValue(ctx, pos); if (value.type === 'expression') { - const { body } = value; - if ( - !( - body.type === 'variable' || - (body.type === 'function' && body.operand?.type === 'variable') - ) - ) { + if (value.arg?.type !== 'variable') { ctx.onError('bad-input-expression', value.start, value.end); } } @@ -173,18 +167,13 @@ function checkDeclarations(ctx: ParseContext, declarations: CST.Declaration[]) { loop: for (const decl of declarations) { let target: CST.VariableRef | undefined; switch (decl.type) { - case 'input': - if (decl.value.type === 'expression') { - const db = decl.value.body; - switch (db.type) { - case 'variable': - target = db; - break; - case 'function': - if (db.operand?.type === 'variable') target = db.operand; - } + case 'input': { + const dv = decl.value; + if (dv.type === 'expression' && dv.arg?.type === 'variable') { + target = dv.arg; } break; + } case 'local': if (decl.target.type === 'variable') target = decl.target; break; @@ -203,15 +192,10 @@ function checkDeclarations(ctx: ParseContext, declarations: CST.Declaration[]) { } if (decl.value.type === 'expression') { - const { body } = decl.value; - switch (body.type) { - case 'variable': - if (decl.type !== 'input') addRef(body); - break; - case 'function': - if (decl.type !== 'input') addRef(body.operand); - for (const opt of body.options) addRef(opt.value); - break; + const { arg, annotation } = decl.value; + if (arg?.type === 'variable') addRef(arg); + if (annotation?.type === 'function') { + for (const opt of annotation.options) addRef(opt.value); } } } diff --git a/packages/mf2-messageformat/src/cst-parser/expression.ts b/packages/mf2-messageformat/src/cst-parser/expression.ts index 5536fb82..d15a8656 100644 --- a/packages/mf2-messageformat/src/cst-parser/expression.ts +++ b/packages/mf2-messageformat/src/cst-parser/expression.ts @@ -19,20 +19,19 @@ export function parseExpression( : parseLiteral(ctx, pos, false); if (arg) pos = arg.end; - let body: - | CST.Literal - | CST.VariableRef + let annotation: | CST.FunctionRef | CST.ReservedAnnotation - | CST.Junk; + | CST.Junk + | undefined; let junkError: MessageSyntaxError | undefined; pos += whitespaces(ctx.source, pos); switch (ctx.source[pos]) { case ':': case '+': case '-': - body = parseFunctionRef(ctx, pos, arg); - pos = body.end; + annotation = parseFunctionRef(ctx, pos); + pos = annotation.end; break; case '!': case '@': @@ -45,47 +44,54 @@ export function parseExpression( case '>': case '?': case '~': - body = parseReservedAnnotation(ctx, pos, arg); - pos = body.end; + annotation = parseReservedAnnotation(ctx, pos); + pos = annotation.end; break; default: - if (arg) { - body = arg; - } else { + if (!arg) { const source = ctx.source.substring(pos, pos + 1); - body = { type: 'junk', start: pos, end: pos, source }; + annotation = { type: 'junk', start: pos, end: pos, source }; junkError = new MessageSyntaxError('parse-error', pos, pos + 1); ctx.errors.push(junkError); } } pos += whitespaces(ctx.source, pos); + const open: CST.Syntax<'{'> = { start, end: start + 1, value: '{' }; + let close: CST.Syntax<'}'> | undefined; if (pos >= ctx.source.length) { ctx.onError('missing-syntax', pos, '}'); - } else if (ctx.source[pos] !== '}') { - const errStart = pos; - while (pos < ctx.source.length && ctx.source[pos] !== '}') pos += 1; - if (body.type === 'junk') { - body.end = pos; - body.source = ctx.source.substring(body.start, pos); - if (junkError) junkError.end = pos; - } else { - ctx.onError('extra-content', errStart, pos); - } } else { - pos += 1; + if (ctx.source[pos] !== '}') { + const errStart = pos; + while (pos < ctx.source.length && ctx.source[pos] !== '}') pos += 1; + if (annotation?.type === 'junk') { + annotation.end = pos; + annotation.source = ctx.source.substring(annotation.start, pos); + if (junkError) junkError.end = pos; + } else { + ctx.onError('extra-content', errStart, pos); + } + } + if (ctx.source[pos] === '}') { + close = { start: pos, end: pos + 1, value: '}' }; + pos += 1; + } } - return { type: 'expression', start, end: pos, body }; + return { + type: 'expression', + start, + end: pos, + braces: close ? [open, close] : [open], + arg, + annotation + }; } // annotation = (function *(s option)) / reserved // function = (":" / "+" / "-") name -function parseFunctionRef( - ctx: ParseContext, - start: number, - operand: CST.Literal | CST.VariableRef | undefined -): CST.FunctionRef { +function parseFunctionRef(ctx: ParseContext, start: number): CST.FunctionRef { const sigil = ctx.source[start]; let pos = start + 1; // ':' | '+' | '-' const name = parseNameValue(ctx.source, pos); @@ -103,7 +109,7 @@ function parseFunctionRef( pos = opt.end; } const kind = sigil === '+' ? 'open' : sigil === '-' ? 'close' : 'value'; - return { type: 'function', kind, operand, start, end: pos, name, options }; + return { type: 'function', kind, start, end: pos, name, options }; } // option = name [s] "=" [s] (literal / variable) @@ -133,8 +139,7 @@ function parseOption(ctx: ParseContext, start: number): CST.Option { // / %xE000-10FFFF function parseReservedAnnotation( ctx: ParseContext, - start: number, - operand: CST.Literal | CST.VariableRef | undefined + start: number ): CST.ReservedAnnotation { const sigil = ctx.source[start] as CST.ReservedAnnotation['sigil']; const source = parseReservedBody(ctx, start + 1); // skip sigil @@ -142,7 +147,6 @@ function parseReservedAnnotation( type: 'reserved-annotation', start, end: source.end, - operand, sigil, source }; diff --git a/packages/mf2-messageformat/src/cst-parser/message.ts b/packages/mf2-messageformat/src/cst-parser/message.ts index b3d4ee82..4b358b83 100644 --- a/packages/mf2-messageformat/src/cst-parser/message.ts +++ b/packages/mf2-messageformat/src/cst-parser/message.ts @@ -100,19 +100,13 @@ function parseSelectMessage( const selectors: CST.Expression[] = []; while (ctx.source[pos] === '{') { - const ph = parseExpression(ctx, pos); - switch (ph.body.type) { - case 'function': - case 'literal': - case 'variable': - break; - default: { - const { start, end } = ph.body; - ctx.onError('bad-selector', start, end); - } + const sel = parseExpression(ctx, pos); + if (sel.annotation && sel.annotation.type !== 'function') { + const { start, end } = sel.annotation; + ctx.onError('bad-selector', start, end); } - selectors.push(ph); - pos = ph.end; + selectors.push(sel); + pos = sel.end; pos += whitespaces(ctx.source, pos); } if (selectors.length === 0) { @@ -211,15 +205,15 @@ function parsePattern( if (quoted) { const q0: CST.Syntax<'{{'> = { start, end: start + 2, value: '{{' }; - let quotes: CST.Pattern['quotes']; + let braces: CST.Pattern['braces']; if (ctx.source.startsWith('}}', pos)) { - quotes = [q0, { start: pos, end: pos + 2, value: '}}' }]; + braces = [q0, { start: pos, end: pos + 2, value: '}}' }]; pos += 2; } else { - quotes = [q0]; + braces = [q0]; ctx.onError('missing-syntax', pos, '}}'); } - return { quotes, start, end: pos, body }; + return { braces, start, end: pos, body }; } return { start, end: pos, body }; From c4e8261677e499c39ac3fcc16579910037d6498f Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Mon, 1 Jan 2024 12:45:20 +0200 Subject: [PATCH 10/44] feat(mf2): Support {#markup}, {/markup}, and {#markup /} --- packages/mf2-fluent/src/fluent-to-message.ts | 6 +- packages/mf2-fluent/src/fluent.test.ts | 26 +--- packages/mf2-fluent/src/message-to-fluent.ts | 14 +- .../mf2-icu-mf1/src/mf1-to-message-data.ts | 10 +- .../src/__fixtures/test-messages.json | 57 ++++----- .../src/cst-parser/as-data-model.ts | 47 +++++-- .../src/cst-parser/cst-types.ts | 27 +++- .../src/cst-parser/declarations.ts | 2 +- .../src/cst-parser/expression.ts | 78 ++++++++---- .../src/cst-parser/message.ts | 6 +- packages/mf2-messageformat/src/data-model.ts | 14 +- .../expression/function-annotation.test.ts | 81 ------------ .../src/expression/function-annotation.ts | 44 ++----- .../mf2-messageformat/src/expression/index.ts | 1 - .../expression/unsupported-annotation.test.ts | 9 +- .../mf2-messageformat/src/extra/validate.ts | 6 +- packages/mf2-messageformat/src/index.ts | 1 + .../mf2-messageformat/src/markup/index.ts | 75 +++++++++++ .../src/markup/markup.test.ts | 102 +++++++++++++++ .../mf2-messageformat/src/messageformat.ts | 21 +-- .../mf2-messageformat/src/runtime/index.ts | 23 +++- .../mf2-messageformat/src/runtime/markup.ts | 65 ---------- .../src/stringifier/message.ts | 46 ++++--- packages/mf2-xliff/src/mf2xliff.ts | 88 ++++++++++++- packages/mf2-xliff/src/xliff-spec.ts | 11 +- packages/mf2-xliff/src/xliff2mf.ts | 120 +++++++++++++----- 26 files changed, 609 insertions(+), 371 deletions(-) create mode 100644 packages/mf2-messageformat/src/markup/index.ts create mode 100644 packages/mf2-messageformat/src/markup/markup.test.ts delete mode 100644 packages/mf2-messageformat/src/runtime/markup.ts diff --git a/packages/mf2-fluent/src/fluent-to-message.ts b/packages/mf2-fluent/src/fluent-to-message.ts index 7ae13ff8..68abc65f 100644 --- a/packages/mf2-fluent/src/fluent-to-message.ts +++ b/packages/mf2-fluent/src/fluent-to-message.ts @@ -67,7 +67,7 @@ function asExpression(exp: Fluent.Expression): Expression { return { type: 'expression', arg: asValue(exp), - annotation: { type: 'function', kind: 'value', name: 'number' } + annotation: { type: 'function', name: 'number' } }; case 'StringLiteral': case 'VariableReference': @@ -75,7 +75,6 @@ function asExpression(exp: Fluent.Expression): Expression { case 'FunctionReference': { const annotation: FunctionAnnotation = { type: 'function', - kind: 'value', name: exp.id.name.toLowerCase() }; const { positional, named } = exp.arguments; @@ -105,13 +104,12 @@ function asExpression(exp: Fluent.Expression): Expression { return { type: 'expression', arg: { type: 'literal', value: msgId }, - annotation: { type: 'function', kind: 'value', name: 'message' } + annotation: { type: 'function', name: 'message' } }; } case 'TermReference': { const annotation: FunctionAnnotation = { type: 'function', - kind: 'value', name: 'message' }; const msgId = exp.attribute diff --git a/packages/mf2-fluent/src/fluent.test.ts b/packages/mf2-fluent/src/fluent.test.ts index 5e0ac1fd..5f0ce2dd 100644 --- a/packages/mf2-fluent/src/fluent.test.ts +++ b/packages/mf2-fluent/src/fluent.test.ts @@ -738,15 +738,12 @@ describe('messagetoFluent', () => { type: 'select', declarations: [ { + type: 'local', name: 'local', value: { type: 'expression', arg: { type: 'variable', name: 'num' }, - annotation: { - type: 'function', - name: 'number', - kind: 'value' - } + annotation: { type: 'function', name: 'number' } } } ], @@ -804,6 +801,7 @@ describe('messagetoFluent', () => { type: 'message', declarations: [ { + type: 'local', name: 'local', value: { type: 'expression', @@ -816,20 +814,12 @@ describe('messagetoFluent', () => { { type: 'expression', arg: { type: 'literal', value: 'msg' }, - annotation: { - type: 'function', - name: 'message', - kind: 'value' - } + annotation: { type: 'function', name: 'message' } }, { type: 'expression', arg: { type: 'variable', name: 'local' }, - annotation: { - type: 'function', - name: 'message', - kind: 'value' - } + annotation: { type: 'function', name: 'message' } } ] } @@ -868,11 +858,7 @@ describe('messagetoFluent', () => { { type: 'expression', arg: { type: 'variable', name: 'input' }, - annotation: { - type: 'function', - name: 'message', - kind: 'value' - } + annotation: { type: 'function', name: 'message' } } ] } diff --git a/packages/mf2-fluent/src/message-to-fluent.ts b/packages/mf2-fluent/src/message-to-fluent.ts index 5bad5b52..6e5ee239 100644 --- a/packages/mf2-fluent/src/message-to-fluent.ts +++ b/packages/mf2-fluent/src/message-to-fluent.ts @@ -146,11 +146,13 @@ function keysMatch(a: (Literal | CatchallKey)[], b: (Literal | CatchallKey)[]) { } function patternToFluent(ctx: MsgContext, pattern: Pattern) { - const elements = pattern.body.map(el => - typeof el === 'string' - ? new Fluent.TextElement(el) - : new Fluent.Placeable(expressionToFluent(ctx, el)) - ); + const elements = pattern.body.map(el => { + if (typeof el === 'string') return new Fluent.TextElement(el); + if (el.type === 'expression') { + return new Fluent.Placeable(expressionToFluent(ctx, el)); + } + throw new Error(`Conversion of ${el.type} to Fluent is not supported`); + }); return new Fluent.Pattern(elements); } @@ -230,7 +232,7 @@ function expressionToFluent( return functionRefToFluent(ctx, fluentArg, annotation); } else { throw new Error( - `Conversion of "${annotation.type}" expression to Fluent is not supported` + `Conversion of ${annotation.type} annotation to Fluent is not supported` ); } } diff --git a/packages/mf2-icu-mf1/src/mf1-to-message-data.ts b/packages/mf2-icu-mf1/src/mf1-to-message-data.ts index 2506aa7a..9d58b78a 100644 --- a/packages/mf2-icu-mf1/src/mf1-to-message-data.ts +++ b/packages/mf2-icu-mf1/src/mf1-to-message-data.ts @@ -66,7 +66,6 @@ function tokenToPart( case 'function': { const annotation: FunctionAnnotation = { type: 'function', - kind: 'value', name: token.key }; if (token.param && token.param.length > 0) { @@ -89,7 +88,6 @@ function tokenToPart( if (!pluralArg) return '#'; const annotation: FunctionAnnotation = { type: 'function', - kind: 'value', name: 'number' }; if (pluralOffset) @@ -121,7 +119,7 @@ function argToExpression({ return { type: 'expression', arg, - annotation: { type: 'function', kind: 'value', name: 'string' } + annotation: { type: 'function', name: 'string' } }; const options: Option[] = []; @@ -138,11 +136,7 @@ function argToExpression({ }); } - const annotation: FunctionAnnotation = { - type: 'function', - kind: 'value', - name: 'number' - }; + const annotation: FunctionAnnotation = { type: 'function', name: 'number' }; if (options.length) annotation.options = options; return { type: 'expression', arg, annotation }; diff --git a/packages/mf2-messageformat/src/__fixtures/test-messages.json b/packages/mf2-messageformat/src/__fixtures/test-messages.json index e2a23bb6..24dfe562 100644 --- a/packages/mf2-messageformat/src/__fixtures/test-messages.json +++ b/packages/mf2-messageformat/src/__fixtures/test-messages.json @@ -1,5 +1,6 @@ [ { "src": "hello", "exp": "hello" }, + { "src": "hello {world}", "exp": "hello world" }, { "src": "hello {|world|}", "exp": "hello world" }, { "src": "hello {||}", "exp": "hello " }, { @@ -248,71 +249,63 @@ "errors": [{ "type": "unresolved-var" }] }, { - "src": "{+tag}", + "src": "{#tag}", "exp": "", - "parts": [{ "type": "open", "name": "tag" }] + "parts": [{ "type": "markup", "kind": "open", "name": "tag" }] }, { - "src": "{+tag}content", + "src": "{#tag}content", "exp": "content", "parts": [ - { "type": "open", "name": "tag" }, + { "type": "markup", "kind": "open", "name": "tag" }, { "type": "literal", "value": "content" } ] }, { - "src": "{+tag}content{-tag}", + "src": "{#tag}content{/tag}", "exp": "content", "parts": [ - { "type": "open", "name": "tag" }, + { "type": "markup", "kind": "open", "name": "tag" }, { "type": "literal", "value": "content" }, - { "type": "close", "name": "tag" } + { "type": "markup", "kind": "close", "name": "tag" } ] }, { - "src": "{-tag}content", + "src": "{/tag}content", "exp": "content", "parts": [ - { "type": "close", "name": "tag" }, + { "type": "markup", "kind": "close", "name": "tag" }, { "type": "literal", "value": "content" } ] }, { - "src": "{+tag foo=bar}", + "src": "{#tag foo=bar}", "exp": "", - "parts": [{ "type": "open", "name": "tag", "options": { "foo": "bar" } }] + "parts": [ + { + "type": "markup", + "kind": "open", + "name": "tag", + "options": { "foo": "bar" } + } + ] }, { - "src": "{+tag foo=|foo| bar=$bar}", + "src": "{#tag foo=|foo| bar=$bar}", "params": { "bar": "b a r" }, "exp": "", "parts": [ { - "type": "open", + "type": "markup", + "kind": "open", "name": "tag", "options": { "foo": "foo", "bar": "b a r" } } ] }, - { - "src": "{|foo| +markup}", - "exp": "", - "parts": [{ "type": "open", "name": "markup", "value": "foo" }] - }, - { - "src": "{-tag foo=bar}", - "exp": "", - "parts": [{ "type": "close", "name": "tag", "options": { "foo": "bar" } }] - }, - { - "src": "unquoted {literal}", - "exp": "unquoted literal" - }, - { - "src": ".match {+foo} * {{foo}}", - "exp": "foo", - "errors": [{ "type": "not-selectable" }] - }, + { "src": "{|foo| #markup}", "syntaxError": true }, + { "src": "{/tag foo=bar}", "syntaxError": true }, + { "src": ".match {#foo} * {{foo}}", "syntaxError": true }, { "src": "{{missing end brace}", "syntaxError": true }, { "src": "{{missing end braces", "syntaxError": true }, { "src": "{{missing end {$braces", "syntaxError": true }, diff --git a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts index 322bf708..3e914508 100644 --- a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts +++ b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts @@ -16,7 +16,7 @@ export function asDataModel(msg: CST.Message): Model.Message { return { type: 'select', declarations, - selectors: msg.selectors.map(asExpression), + selectors: msg.selectors.map(sel => asExpression(sel, false)), variants: msg.variants.map(cst => ({ keys: cst.keys.map(key => key.type === '*' ? { type: '*' } : asValue(key) @@ -36,7 +36,7 @@ export function asDataModel(msg: CST.Message): Model.Message { function asDeclaration(decl: CST.Declaration): Model.Declaration { switch (decl.type) { case 'input': { - const value = asExpression(decl.value); + const value = asExpression(decl.value, false); if (value.arg?.type !== 'variable') { const { start, end } = decl.value; throw new MessageSyntaxError('parse-error', start, end); @@ -51,38 +51,69 @@ function asDeclaration(decl: CST.Declaration): Model.Declaration { return { type: 'local', name: asValue(decl.target).name, - value: asExpression(decl.value) + value: asExpression(decl.value, false) }; default: return { type: 'unsupported-statement', keyword: (decl.keyword?.value ?? '').substring(1), body: decl.body?.value || undefined, - expressions: decl.values?.map(asExpression) ?? [] + expressions: decl.values?.map(dv => asExpression(dv, true)) ?? [] }; } } function asPattern(cst: CST.Pattern): Model.Pattern { const body: Model.Pattern['body'] = cst.body.map(el => - el.type === 'text' ? el.value : asExpression(el) + el.type === 'text' ? el.value : asExpression(el, true) ); return { body }; } -function asExpression(cst: CST.Expression | CST.Junk): Model.Expression { +function asExpression( + cst: CST.Expression | CST.Junk, + allowMarkup: false +): Model.Expression; +function asExpression( + cst: CST.Expression | CST.Junk, + allowMarkup: true +): Model.Expression | Model.Markup; +function asExpression( + cst: CST.Expression | CST.Junk, + allowMarkup: boolean +): Model.Expression | Model.Markup { if (cst.type === 'expression') { + if (allowMarkup && cst.markup) { + const cm = cst.markup; + if (cm.type === 'markup-close') { + return { type: 'markup', kind: 'close', name: cm.name }; + } + const markup: Model.MarkupOpen | Model.MarkupStandalone = { + type: 'markup', + kind: cm.close ? 'standalone' : 'open', + name: cm.name + }; + if (cm.options.length > 0) { + markup.options = cm.options.map(opt => ({ + name: opt.name, + value: asValue(opt.value) + })); + } + return markup; + } + const arg = cst.arg ? asValue(cst.arg) : undefined; let annotation: | Model.FunctionAnnotation | Model.UnsupportedAnnotation | undefined; + const ca = cst.annotation; if (ca) { switch (ca.type) { case 'function': - annotation = { type: 'function', kind: ca.kind, name: ca.name }; - if (ca.options && ca.options.length > 0) { + annotation = { type: 'function', name: ca.name }; + if (ca.options.length > 0) { annotation.options = ca.options.map(opt => ({ name: opt.name, value: asValue(opt.value) diff --git a/packages/mf2-messageformat/src/cst-parser/cst-types.ts b/packages/mf2-messageformat/src/cst-parser/cst-types.ts index 00bdd757..8352722b 100644 --- a/packages/mf2-messageformat/src/cst-parser/cst-types.ts +++ b/packages/mf2-messageformat/src/cst-parser/cst-types.ts @@ -103,8 +103,9 @@ export interface Expression { start: number; end: number; braces: [Syntax<'{'>] | [Syntax<'{'>, Syntax<'}'>]; - arg: Literal | VariableRef | undefined; - annotation: FunctionRef | ReservedAnnotation | Junk | undefined; + arg?: Literal | VariableRef; + annotation?: FunctionRef | ReservedAnnotation | Junk; + markup?: Markup | MarkupClose; } /** @beta */ @@ -139,8 +140,6 @@ export interface VariableRef { /** @beta */ export interface FunctionRef { type: 'function'; - kind: 'open' | 'close' | 'value'; - /** position of the `:`/`+`/`-` */ start: number; end: number; name: string; @@ -157,6 +156,26 @@ export interface ReservedAnnotation { end: number; } +/** @beta */ +export interface Markup { + type: 'markup'; + /** position of the sigil */ + start: number; + end: number; + close: Syntax<'/'> | undefined; + name: string; + options: Option[]; +} + +/** @beta */ +export interface MarkupClose { + type: 'markup-close'; + /** position of the sigil */ + start: number; + end: number; + name: string; +} + /** @beta */ export interface Option { /** position at the start of the name */ diff --git a/packages/mf2-messageformat/src/cst-parser/declarations.ts b/packages/mf2-messageformat/src/cst-parser/declarations.ts index 7836ac1b..36c5c1a3 100644 --- a/packages/mf2-messageformat/src/cst-parser/declarations.ts +++ b/packages/mf2-messageformat/src/cst-parser/declarations.ts @@ -47,7 +47,7 @@ function parseInputDeclaration( const value = parseDeclarationValue(ctx, pos); if (value.type === 'expression') { - if (value.arg?.type !== 'variable') { + if (value.markup || value.arg?.type !== 'variable') { ctx.onError('bad-input-expression', value.start, value.end); } } diff --git a/packages/mf2-messageformat/src/cst-parser/expression.ts b/packages/mf2-messageformat/src/cst-parser/expression.ts index d15a8656..5e584820 100644 --- a/packages/mf2-messageformat/src/cst-parser/expression.ts +++ b/packages/mf2-messageformat/src/cst-parser/expression.ts @@ -5,7 +5,6 @@ import { parseNameValue } from './names.js'; import { whitespaces } from './util.js'; import { parseLiteral, parseQuotedLiteral, parseVariable } from './values.js'; -// expression = "{" [s] (((literal / variable) [s annotation]) / annotation) [s] "}" export function parseExpression( ctx: ParseContext, start: number @@ -24,22 +23,31 @@ export function parseExpression( | CST.ReservedAnnotation | CST.Junk | undefined; + let markup: CST.Markup | CST.MarkupClose | undefined; let junkError: MessageSyntaxError | undefined; pos += whitespaces(ctx.source, pos); switch (ctx.source[pos]) { case ':': - case '+': - case '-': - annotation = parseFunctionRef(ctx, pos); + annotation = parseFunctionRefOrMarkup(ctx, pos, 'function'); pos = annotation.end; break; + case '#': + if (arg) ctx.onError('extra-content', arg.start, arg.end); + markup = parseFunctionRefOrMarkup(ctx, pos, 'markup'); + pos = markup.end; + break; + case '/': + if (arg) ctx.onError('extra-content', arg.start, arg.end); + markup = parseMarkupClose(ctx, pos); + pos = markup.end; + break; case '!': case '@': - case '#': case '%': case '^': case '&': case '*': + case '+': case '<': case '>': case '?': @@ -78,29 +86,44 @@ export function parseExpression( pos += 1; } } - - return { - type: 'expression', - start, - end: pos, - braces: close ? [open, close] : [open], - arg, - annotation - }; + const braces: CST.Expression['braces'] = close ? [open, close] : [open]; + return markup + ? { type: 'expression', start, end: pos, braces, markup } + : { type: 'expression', start, end: pos, braces, arg, annotation }; } -// annotation = (function *(s option)) / reserved -// function = (":" / "+" / "-") name -function parseFunctionRef(ctx: ParseContext, start: number): CST.FunctionRef { - const sigil = ctx.source[start]; - let pos = start + 1; // ':' | '+' | '-' +function parseFunctionRefOrMarkup( + ctx: ParseContext, + start: number, + type: 'function' +): CST.FunctionRef; +function parseFunctionRefOrMarkup( + ctx: ParseContext, + start: number, + type: 'markup' +): CST.Markup; +function parseFunctionRefOrMarkup( + ctx: ParseContext, + start: number, + type: 'function' | 'markup' +): CST.FunctionRef | CST.Markup { + let pos = start + 1; // ':' | '#' const name = parseNameValue(ctx.source, pos); if (!name) ctx.onError('empty-token', pos, pos + 1); const options: CST.Option[] = []; pos += name.length; + let close: CST.Syntax<'/'> | undefined; while (pos < ctx.source.length) { - const ws = whitespaces(ctx.source, pos); - if (ctx.source[pos + ws] === '}') break; + let ws = whitespaces(ctx.source, pos); + const next = ctx.source[pos + ws]; + if (next === '}') break; + if (type === 'markup' && next === '/') { + pos += ws + 1; + close = { start: pos - 1, end: pos, value: '/' }; + ws = whitespaces(ctx.source, pos); + if (ws > 0) ctx.onError('extra-content', pos, pos + ws); + break; + } if (ws === 0) ctx.onError('missing-syntax', pos, ' '); pos += ws; const opt = parseOption(ctx, pos); @@ -108,8 +131,17 @@ function parseFunctionRef(ctx: ParseContext, start: number): CST.FunctionRef { options.push(opt); pos = opt.end; } - const kind = sigil === '+' ? 'open' : sigil === '-' ? 'close' : 'value'; - return { type: 'function', kind, start, end: pos, name, options }; + return type === 'function' + ? { type, start, end: pos, name, options } + : { type, start, end: pos, name, options, close }; +} + +function parseMarkupClose(ctx: ParseContext, start: number): CST.MarkupClose { + let pos = start + 1; // '/' + const name = parseNameValue(ctx.source, pos); + if (name) pos += name.length; + else ctx.onError('empty-token', pos, pos + 1); + return { type: 'markup-close', start, end: pos, name }; } // option = name [s] "=" [s] (literal / variable) diff --git a/packages/mf2-messageformat/src/cst-parser/message.ts b/packages/mf2-messageformat/src/cst-parser/message.ts index 4b358b83..f92b7ef4 100644 --- a/packages/mf2-messageformat/src/cst-parser/message.ts +++ b/packages/mf2-messageformat/src/cst-parser/message.ts @@ -101,9 +101,9 @@ function parseSelectMessage( const selectors: CST.Expression[] = []; while (ctx.source[pos] === '{') { const sel = parseExpression(ctx, pos); - if (sel.annotation && sel.annotation.type !== 'function') { - const { start, end } = sel.annotation; - ctx.onError('bad-selector', start, end); + const body = sel.markup ?? sel.annotation; + if (body && body.type !== 'function') { + ctx.onError('bad-selector', body.start, body.end); } selectors.push(sel); pos = sel.end; diff --git a/packages/mf2-messageformat/src/data-model.ts b/packages/mf2-messageformat/src/data-model.ts index fd39e03d..8ff4b090 100644 --- a/packages/mf2-messageformat/src/data-model.ts +++ b/packages/mf2-messageformat/src/data-model.ts @@ -1,11 +1,17 @@ import type { Expression, Literal, VariableRef } from './expression/index.js'; +import type { Markup } from './markup/index.js'; export type { FunctionAnnotation, Option, UnsupportedAnnotation -} from './expression'; -export type { Expression, Literal, VariableRef }; +} from './expression/index.js'; +export type { + MarkupClose, + MarkupOpen, + MarkupStandalone +} from './markup/index.js'; +export type { Expression, Literal, Markup, VariableRef }; /** * The representation of a single message. @@ -58,7 +64,7 @@ export interface UnsupportedStatement { name?: never; value?: never; body?: string; - expressions: Expression[]; + expressions: (Expression | Markup)[]; } /** @@ -69,7 +75,7 @@ export interface UnsupportedStatement { * @beta */ export interface Pattern { - body: Array; + body: Array; } /** diff --git a/packages/mf2-messageformat/src/expression/function-annotation.test.ts b/packages/mf2-messageformat/src/expression/function-annotation.test.ts index cb3feb2c..36f49337 100644 --- a/packages/mf2-messageformat/src/expression/function-annotation.test.ts +++ b/packages/mf2-messageformat/src/expression/function-annotation.test.ts @@ -86,84 +86,3 @@ describe('Type casts based on runtime', () => { expect(mf.format({ var: 1234, useGrouping: false })).toBe('1234'); }); }); - -describe('Simple open/close', () => { - test('no options, literal body', () => { - const mf = new MessageFormat('{+b}foo{-b}'); - expect(mf.formatToParts()).toEqual([ - { type: 'open', source: '+b', name: 'b' }, - { type: 'literal', value: 'foo' }, - { type: 'close', source: '-b', name: 'b' } - ]); - expect(mf.format()).toBe('foo'); - }); - - test('arguments, options, variables', () => { - const mf = new MessageFormat( - '{|x| +b foo=42 bar=$foo}foo{$foo}{|y| -b foo=13}', - 'en' - ); - const msg = mf.formatToParts({ foo: 'foo bar' }); - expect(msg).toEqual([ - { - type: 'open', - options: { foo: '42', bar: 'foo bar' }, - source: '|x|', - name: 'b', - value: 'x' - }, - { type: 'literal', value: 'foo' }, - { type: 'string', locale: 'en', source: '$foo', value: 'foo bar' }, - { - type: 'close', - options: { foo: '13' }, - source: '|y|', - name: 'b', - value: 'y' - } - ]); - expect(mf.format({ foo: 'foo bar' })).toBe('foofoo bar'); - }); -}); - -describe('Multiple open/close', () => { - test('adjacent', () => { - const mf = new MessageFormat('{+b}foo{-b}{+a}bar{-a}'); - expect(mf.formatToParts()).toEqual([ - { type: 'open', source: '+b', name: 'b' }, - { type: 'literal', value: 'foo' }, - { type: 'close', source: '-b', name: 'b' }, - { type: 'open', source: '+a', name: 'a' }, - { type: 'literal', value: 'bar' }, - { type: 'close', source: '-a', name: 'a' } - ]); - expect(mf.format()).toBe('foobar'); - }); - - test('nested', () => { - const mf = new MessageFormat('{+b}foo{+a}bar{-a}{-b}'); - expect(mf.formatToParts()).toEqual([ - { type: 'open', source: '+b', name: 'b' }, - { type: 'literal', value: 'foo' }, - { type: 'open', source: '+a', name: 'a' }, - { type: 'literal', value: 'bar' }, - { type: 'close', source: '-a', name: 'a' }, - { type: 'close', source: '-b', name: 'b' } - ]); - expect(mf.format()).toBe('foobar'); - }); - - test('overlapping', () => { - const mf = new MessageFormat('{+b}foo{+a}bar{-b}baz{-a}'); - expect(mf.formatToParts()).toEqual([ - { type: 'open', source: '+b', name: 'b' }, - { type: 'literal', value: 'foo' }, - { type: 'open', source: '+a', name: 'a' }, - { type: 'literal', value: 'bar' }, - { type: 'close', source: '-b', name: 'b' }, - { type: 'literal', value: 'baz' }, - { type: 'close', source: '-a', name: 'a' } - ]); - expect(mf.format()).toBe('foobarbaz'); - }); -}); diff --git a/packages/mf2-messageformat/src/expression/function-annotation.ts b/packages/mf2-messageformat/src/expression/function-annotation.ts index 5baaf98f..d11e77ba 100644 --- a/packages/mf2-messageformat/src/expression/function-annotation.ts +++ b/packages/mf2-messageformat/src/expression/function-annotation.ts @@ -1,6 +1,6 @@ import { MessageError } from '../errors.js'; import type { Context } from '../format-context.js'; -import { MessageFunctionContext, fallback, markup } from '../runtime/index.js'; +import { MessageFunctionContext, fallback } from '../runtime/index.js'; import type { Literal } from './literal.js'; import { getValueSource, resolveValue } from './value.js'; import type { VariableRef } from './variable-ref.js'; @@ -19,7 +19,6 @@ import type { VariableRef } from './variable-ref.js'; */ export interface FunctionAnnotation { type: 'function'; - kind: 'open' | 'close' | 'value'; name: string; options?: Option[]; } @@ -44,24 +43,10 @@ export interface Option { export const isFunctionAnnotation = (part: any): part is FunctionAnnotation => !!part && typeof part === 'object' && part.type === 'function'; -export function functionAnnotationSource( - kind: FunctionAnnotation['kind'], - name: string -) { - switch (kind) { - case 'open': - return `+${name}`; - case 'close': - return `-${name}`; - default: - return `:${name}`; - } -} - export function resolveFunctionAnnotation( ctx: Context, operand: Literal | VariableRef | undefined, - { kind, name, options }: FunctionAnnotation + { name, options }: FunctionAnnotation ) { let source: string | undefined; try { @@ -72,27 +57,18 @@ export function resolveFunctionAnnotation( } else { fnInput = []; } - source ??= functionAnnotationSource(kind, name); + source ??= `:${name}`; - switch (kind) { - case 'open': - case 'close': { - const opt = options?.length ? resolveOptions(ctx, options) : undefined; - return markup(source, kind, name, opt, fnInput[0]); - } - default: { - const rf = ctx.functions[name]; - if (!rf) { - throw new MessageError('missing-func', `Unknown function ${name}`); - } - const msgCtx = new MessageFunctionContext(ctx, source); - const opt = resolveOptions(ctx, options); - return rf(msgCtx, opt, ...fnInput); - } + const rf = ctx.functions[name]; + if (!rf) { + throw new MessageError('missing-func', `Unknown function ${name}`); } + const msgCtx = new MessageFunctionContext(ctx, source); + const opt = resolveOptions(ctx, options); + return rf(msgCtx, opt, ...fnInput); } catch (error) { ctx.onError(error); - source ??= getValueSource(operand) ?? functionAnnotationSource(kind, name); + source ??= getValueSource(operand) ?? `:${name}`; return fallback(source); } } diff --git a/packages/mf2-messageformat/src/expression/index.ts b/packages/mf2-messageformat/src/expression/index.ts index 96a650e9..f617156a 100644 --- a/packages/mf2-messageformat/src/expression/index.ts +++ b/packages/mf2-messageformat/src/expression/index.ts @@ -13,7 +13,6 @@ import { import { resolveVariableRef, VariableRef } from './variable-ref'; export { - functionAnnotationSource, isFunctionAnnotation, FunctionAnnotation, Option diff --git a/packages/mf2-messageformat/src/expression/unsupported-annotation.test.ts b/packages/mf2-messageformat/src/expression/unsupported-annotation.test.ts index 8a48c91a..631aa6aa 100644 --- a/packages/mf2-messageformat/src/expression/unsupported-annotation.test.ts +++ b/packages/mf2-messageformat/src/expression/unsupported-annotation.test.ts @@ -29,11 +29,16 @@ describe('Reserved syntax', () => { expect(msg).toMatchObject([{ type: 'fallback', source: '$foo' }]); }); + test('old markup syntax', () => { + const msg = resolve('{+open}', {}, [{ type: 'unsupported-annotation' }]); + expect(msg).toMatchObject([{ type: 'fallback', source: '+' }]); + }); + test('whitespace', () => { - const msg = resolve('{ # one\ntwo\rthree four }', {}, [ + const msg = resolve('{ + one\ntwo\rthree four }', {}, [ { type: 'unsupported-annotation' } ]); - expect(msg).toMatchObject([{ type: 'fallback', source: '#' }]); + expect(msg).toMatchObject([{ type: 'fallback', source: '+' }]); }); test('surrogates', () => { diff --git a/packages/mf2-messageformat/src/extra/validate.ts b/packages/mf2-messageformat/src/extra/validate.ts index 99c7eca6..a053ed51 100644 --- a/packages/mf2-messageformat/src/extra/validate.ts +++ b/packages/mf2-messageformat/src/extra/validate.ts @@ -4,11 +4,7 @@ import type { MessageFunctions } from '../runtime'; function validateParts(parts: Pattern['body'], functions: MessageFunctions) { for (const part of parts) { - if ( - isExpression(part) && - isFunctionAnnotation(part.annotation) && - part.annotation.kind === 'value' - ) { + if (isExpression(part) && isFunctionAnnotation(part.annotation)) { const { name } = part.annotation; if (typeof functions[name] !== 'function') { throw new ReferenceError(`Runtime function not available: ${name}`); diff --git a/packages/mf2-messageformat/src/index.ts b/packages/mf2-messageformat/src/index.ts index aa6ce1c7..2fdd2ed4 100644 --- a/packages/mf2-messageformat/src/index.ts +++ b/packages/mf2-messageformat/src/index.ts @@ -7,6 +7,7 @@ export { MessageSyntaxError } from './errors.js'; export * from './expression/index.js'; +export { isMarkup } from './markup/index.js'; export { MessageFormat, MessageFormatOptions } from './messageformat.js'; export { stringifyMessage } from './stringifier/message.js'; export * from './runtime/index.js'; diff --git a/packages/mf2-messageformat/src/markup/index.ts b/packages/mf2-messageformat/src/markup/index.ts new file mode 100644 index 00000000..4e03a379 --- /dev/null +++ b/packages/mf2-messageformat/src/markup/index.ts @@ -0,0 +1,75 @@ +import type { Option } from '../data-model.js'; +import { resolveValue } from '../expression/value.js'; +import type { Context } from '../format-context.js'; +import type { + MessageMarkupClosePart, + MessageMarkupPart +} from '../runtime/index.js'; + +/** + * Markup placeholders can span ranges of other pattern elements, + * or represent other inline elements. + * + * @remarks + * The `name` identifies the markup part, + * which will be included in the result along with any `options`. + * + * When formatted to string, all markup will format as an empty string. + * To use markup, format to parts and post-process the formatted results. + * + * @beta + */ +export type Markup = MarkupOpen | MarkupStandalone | MarkupClose; + +export interface MarkupOpen { + type: 'markup'; + kind: 'open'; + name: string; + options?: Option[]; +} + +export interface MarkupStandalone { + type: 'markup'; + kind: 'standalone'; + name: string; + options?: Option[]; +} + +export interface MarkupClose { + type: 'markup'; + kind: 'close'; + name: string; + options?: never; +} + +/** + * Type guard for {@link Markup} pattern elements + * + * @beta + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const isMarkup = (part: any): part is Markup => + !!part && typeof part === 'object' && part.type === 'markup'; + +export function formatMarkup( + ctx: Context, + { kind, name, options }: Markup +): MessageMarkupPart | MessageMarkupClosePart { + if (kind === 'close') { + return { type: 'markup', kind, source: `/${name}`, name }; + } + const source = kind === 'open' ? `#${name}` : `#${name}/`; + const part: MessageMarkupPart = { type: 'markup', kind, source, name }; + if (options?.length) { + part.options = {}; + for (const { name, value } of options) { + let rv = resolveValue(ctx, value); + if (typeof rv === 'object' && typeof rv?.valueOf === 'function') { + const vv = rv.valueOf(); + if (vv !== rv) rv = vv; + } + part.options[name] = rv; + } + } + return part; +} diff --git a/packages/mf2-messageformat/src/markup/markup.test.ts b/packages/mf2-messageformat/src/markup/markup.test.ts new file mode 100644 index 00000000..10238e71 --- /dev/null +++ b/packages/mf2-messageformat/src/markup/markup.test.ts @@ -0,0 +1,102 @@ +import { MessageFormat, parseMessage } from '../index'; + +describe('Simple open/close', () => { + test('no options, literal body', () => { + const mf = new MessageFormat('{#b}foo{/b}'); + expect(mf.formatToParts()).toEqual([ + { type: 'markup', kind: 'open', source: '#b', name: 'b' }, + { type: 'literal', value: 'foo' }, + { type: 'markup', kind: 'close', source: '/b', name: 'b' } + ]); + expect(mf.format()).toBe('foo'); + }); + + test('options', () => { + const mf = new MessageFormat('{#b foo=42 bar=$foo}foo{$foo}{/b}', 'en'); + const msg = mf.formatToParts({ foo: 'foo bar' }); + expect(msg).toEqual([ + { + type: 'markup', + kind: 'open', + options: { foo: '42', bar: 'foo bar' }, + source: '#b', + name: 'b' + }, + { type: 'literal', value: 'foo' }, + { type: 'string', locale: 'en', source: '$foo', value: 'foo bar' }, + { type: 'markup', kind: 'close', source: '/b', name: 'b' } + ]); + expect(mf.format({ foo: 'foo bar' })).toBe('foofoo bar'); + }); + + test('do not allow operands', () => { + const src = '{x #b}'; + expect(() => new MessageFormat(src, 'en')).toThrow(); + const cst = parseMessage(src); + expect(cst).toMatchObject({ + type: 'simple', + errors: [{ type: 'extra-content' }], + pattern: { + body: [{ type: 'expression', markup: { type: 'markup', name: 'b' } }] + } + }); + }); + + test('do not allow options on close', () => { + const src = '{/b foo=13}'; + expect(() => new MessageFormat(src, 'en')).toThrow(); + const cst = parseMessage(src); + expect(cst).toMatchObject({ + type: 'simple', + errors: [{ type: 'extra-content' }], + pattern: { + body: [ + { type: 'expression', markup: { type: 'markup-close', name: 'b' } } + ] + } + }); + expect(cst).not.toHaveProperty('pattern.body.0.markup.options'); + }); +}); + +describe('Multiple open/close', () => { + test('adjacent', () => { + const mf = new MessageFormat('{#b}foo{/b}{#a}bar{/a}'); + expect(mf.formatToParts()).toEqual([ + { type: 'markup', kind: 'open', source: '#b', name: 'b' }, + { type: 'literal', value: 'foo' }, + { type: 'markup', kind: 'close', source: '/b', name: 'b' }, + { type: 'markup', kind: 'open', source: '#a', name: 'a' }, + { type: 'literal', value: 'bar' }, + { type: 'markup', kind: 'close', source: '/a', name: 'a' } + ]); + expect(mf.format()).toBe('foobar'); + }); + + test('nested', () => { + const mf = new MessageFormat('{#b}foo{#a}bar{/a}{/b}'); + expect(mf.formatToParts()).toEqual([ + { type: 'markup', kind: 'open', source: '#b', name: 'b' }, + { type: 'literal', value: 'foo' }, + { type: 'markup', kind: 'open', source: '#a', name: 'a' }, + { type: 'literal', value: 'bar' }, + { type: 'markup', kind: 'close', source: '/a', name: 'a' }, + { type: 'markup', kind: 'close', source: '/b', name: 'b' } + ]); + expect(mf.format()).toBe('foobar'); + }); + + test('overlapping', () => { + const mf = new MessageFormat('{#b}foo{#a}bar{/b}baz{/a}'); + expect(mf.formatToParts()).toEqual([ + { type: 'markup', kind: 'open', source: '#b', name: 'b' }, + { type: 'literal', value: 'foo' }, + { type: 'markup', kind: 'open', source: '#a', name: 'a' }, + { type: 'literal', value: 'bar' }, + { type: 'markup', kind: 'close', source: '/b', name: 'b' }, + { type: 'literal', value: 'baz' }, + { type: 'markup', kind: 'close', source: '/a', name: 'a' } + ]); + expect(mf.format()).toBe('foobarbaz'); + }); +}); diff --git a/packages/mf2-messageformat/src/messageformat.ts b/packages/mf2-messageformat/src/messageformat.ts index 76ef38ec..9dcda58a 100644 --- a/packages/mf2-messageformat/src/messageformat.ts +++ b/packages/mf2-messageformat/src/messageformat.ts @@ -1,19 +1,20 @@ import { asDataModel, parseMessage } from './cst-parser/index.js'; -import { Message } from './data-model'; +import type { Message } from './data-model'; +import { MessageError, MessageResolutionError } from './errors.js'; import type { Context } from './format-context'; -import { selectPattern } from './select-pattern.js'; import { + UnresolvedExpression, resolveExpression, - resolveValue, - UnresolvedExpression + resolveValue } from './expression/index.js'; +import { formatMarkup } from './markup/index.js'; import { - defaultFunctions, MessageFunctions, MessagePart, - MessageValue -} from './runtime'; -import { MessageError, MessageResolutionError } from './errors.js'; + MessageValue, + defaultFunctions +} from './runtime/index.js'; +import { selectPattern } from './select-pattern.js'; /** @beta */ export interface MessageFormatOptions { @@ -72,7 +73,7 @@ export class MessageFormat { for (const elem of selectPattern(ctx, this.#message)) { if (typeof elem === 'string') { res += elem; - } else { + } else if (elem.type !== 'markup') { let mv: MessageValue | undefined; try { mv = ctx.resolveExpression(elem); @@ -100,6 +101,8 @@ export class MessageFormat { for (const elem of selectPattern(ctx, this.#message)) { if (typeof elem === 'string') { parts.push({ type: 'literal', value: elem }); + } else if (elem.type === 'markup') { + parts.push(formatMarkup(ctx, elem)); } else { let mv: MessageValue | undefined; try { diff --git a/packages/mf2-messageformat/src/runtime/index.ts b/packages/mf2-messageformat/src/runtime/index.ts index c0ca2ea9..9a8309b7 100644 --- a/packages/mf2-messageformat/src/runtime/index.ts +++ b/packages/mf2-messageformat/src/runtime/index.ts @@ -5,7 +5,6 @@ import { string } from './string.js'; export { MessageDateTime, MessageDateTimePart, datetime } from './datetime.js'; export { MessageFallback, MessageFallbackPart, fallback } from './fallback.js'; -export { MessageMarkup, MessageMarkupPart, markup } from './markup.js'; export { MessageNumber, MessageNumberPart, number } from './number.js'; export { MessageString, MessageStringPart, string } from './string.js'; export { MessageUnknownPart, MessageUnknownValue, unknown } from './unknown.js'; @@ -40,7 +39,27 @@ export interface MessageLiteralPart { value: string; } -export type MessagePart = MessageExpressionPart | MessageLiteralPart; +export interface MessageMarkupPart { + type: 'markup'; + kind: 'open' | 'standalone'; + source: string; + name: string; + options?: { [key: string]: unknown }; +} + +export interface MessageMarkupClosePart { + type: 'markup'; + kind: 'close'; + source: string; + name: string; + options?: never; +} + +export type MessagePart = + | MessageExpressionPart + | MessageLiteralPart + | MessageMarkupClosePart + | MessageMarkupPart; /** * The default Runtime includes three functions, `datetime`, `number` and `string`. diff --git a/packages/mf2-messageformat/src/runtime/markup.ts b/packages/mf2-messageformat/src/runtime/markup.ts deleted file mode 100644 index 4816c163..00000000 --- a/packages/mf2-messageformat/src/runtime/markup.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { MessageExpressionPart, MessageValue } from './index.js'; - -export interface MessageMarkup extends MessageValue { - readonly type: 'open' | 'close'; - readonly source: string; - readonly locale: 'und'; - readonly name: string; - readonly options: Record; - toParts(): [MessageMarkupPart]; - toString(): ''; - valueOf?: () => unknown; -} - -export interface MessageMarkupPart extends MessageExpressionPart { - type: 'open' | 'close'; - source: string; - name: string; - value?: unknown; - options?: { [key: string]: unknown }; -} - -function getValue(value: unknown): unknown { - if (typeof value === 'object' && typeof value?.valueOf === 'function') { - const vv = value.valueOf(); - if (vv !== value) return vv; - } - return value; -} - -export function markup( - source: string, - type: 'open' | 'close', - name: string, - options?: Record, - input?: unknown -): MessageMarkup { - const value = getValue(input); - - let opt: Record | undefined; - if (options) { - opt = {}; - for (const [name, value] of Object.entries(options)) { - opt[name] = getValue(value); - } - } - - const markup: MessageMarkup = { - type: type, - source, - locale: 'und', - name, - get options() { - return { ...opt }; - }, - toParts() { - const part: MessageMarkupPart = { type, source, name }; - if (input !== undefined) part.value = value; - if (opt) part.options = opt; - return [part]; - }, - toString: () => '' - }; - if (input !== undefined) markup.valueOf = () => value; - return markup; -} diff --git a/packages/mf2-messageformat/src/stringifier/message.ts b/packages/mf2-messageformat/src/stringifier/message.ts index 5612b9a1..0bc9341c 100644 --- a/packages/mf2-messageformat/src/stringifier/message.ts +++ b/packages/mf2-messageformat/src/stringifier/message.ts @@ -1,22 +1,19 @@ import { isValidUnquotedLiteral } from '../cst-parser/index.js'; import { Declaration, - isPatternMessage, - isSelectMessage, - Message, - Pattern -} from '../data-model'; -import { Expression, FunctionAnnotation, - functionAnnotationSource, - isLiteral, - isVariableRef, Literal, + Markup, + Message, Option, + Pattern, UnsupportedAnnotation, - VariableRef -} from '../expression/index.js'; + VariableRef, + isPatternMessage, + isSelectMessage +} from '../data-model.js'; +import { isLiteral, isVariableRef } from '../expression/index.js'; import { MessageFormat } from '../messageformat.js'; /** @@ -53,7 +50,12 @@ function stringifyDeclaration(decl: Declaration) { case 'unsupported-statement': { const parts = [`.${decl.keyword}`]; if (decl.body) parts.push(decl.body); - for (const exp of decl.expressions) parts.push(stringifyExpression(exp)); + for (const exp of decl.expressions) + parts.push( + exp.type === 'expression' + ? stringifyExpression(exp) + : stringifyMarkup(exp) + ); return parts.join(' '); } } @@ -61,13 +63,17 @@ function stringifyDeclaration(decl: Declaration) { throw new Error(`Unsupported ${decl.type} declaration`); } -function stringifyFunctionAnnotation({ - kind, - name, - options -}: FunctionAnnotation) { - let res = functionAnnotationSource(kind, name); +function stringifyFunctionAnnotation({ name, options }: FunctionAnnotation) { + let res = `:${name}`; + if (options) for (const opt of options) res += ' ' + stringifyOption(opt); + return res; +} + +function stringifyMarkup({ kind, name, options }: Markup) { + if (kind === 'close') return `{/${name}}`; + let res = `{#${name}`; if (options) for (const opt of options) res += ' ' + stringifyOption(opt); + res += kind === 'standalone' ? ' /}' : '}'; return res; } @@ -90,7 +96,9 @@ function stringifyPattern({ body }: Pattern, quoted: boolean) { quoted = true; } for (const el of body) { - res += typeof el === 'string' ? el : stringifyExpression(el); + if (typeof el === 'string') res += el; + else if (el.type === 'markup') res += stringifyMarkup(el); + else res += stringifyExpression(el); } return quoted ? `{{${res}}}` : res; } diff --git a/packages/mf2-xliff/src/mf2xliff.ts b/packages/mf2-xliff/src/mf2xliff.ts index e23ee176..50067560 100644 --- a/packages/mf2-xliff/src/mf2xliff.ts +++ b/packages/mf2-xliff/src/mf2xliff.ts @@ -229,34 +229,85 @@ function resolvePattern( srcPattern: MF.Pattern, trgPattern: MF.Pattern | undefined ): X.Unit { - const expressions: X.MessageExpression[] = []; - const handlePart = (p: string | MF.Expression): X.Text | X.InlineElement => { + const refs: (X.MessageExpression | X.MessageMarkup)[] = []; + const openMarkup: X.MessageMarkup[] = []; + const handlePart = ( + p: string | MF.Expression | MF.Markup + ): X.Text | X.InlineElement => { if (typeof p === 'string') return asText(p); const id = nextId(); + const attributes: X.CodeSpanStart['attributes'] = { + id: id.substring(1), + 'mf:ref': id + }; + if (p.type === 'markup') { + if (p.kind === 'close') { + const oi = openMarkup.findIndex(xm => xm.attributes.name === p.name); + if (oi === -1) { + attributes.isolated = 'yes'; + } else { + const [om] = openMarkup.splice(oi, 1); + const { id } = om.attributes; + return { + type: 'element', + name: 'ec', + attributes: { startRef: id.substring(1), 'mf:ref': id } + }; + } + } + const markup = resolveMarkup(id, p); + refs.push(markup); + openMarkup.unshift(markup); + const name = p.kind === 'open' ? 'sc' : p.kind === 'close' ? 'ec' : 'ph'; + return { type: 'element', name, attributes }; + } const exp = resolveExpression(id, p); - expressions.push(exp); - const attributes = { id: id.substring(1), 'mf:ref': id }; + refs.push(exp); return { type: 'element', name: 'ph', attributes }; }; + const cleanMarkupSpans = (elements: (X.Text | X.InlineElement)[]) => { + for (let i = 0; i < elements.length; ++i) { + const sc = elements[i]; + if (sc.type !== 'element' || sc.name !== 'sc') continue; + for (let j = i + 1; j < elements.length; ++j) { + const ec = elements[j]; + if ( + ec.type === 'element' && + ec.name === 'ec' && + ec.attributes?.startRef === sc.attributes.id + ) { + const body = elements.splice(i + 1, j - i); + body.pop(); + cleanMarkupSpans(body); + Object.assign(sc, { name: 'pc', elements: body }); + break; + } + } + } + }; const se = srcPattern.body.map(handlePart); + cleanMarkupSpans(se); const source: X.Source = { type: 'element', name: 'source', elements: se }; let ge: X.Segment['elements']; if (trgPattern) { const te = trgPattern.body.map(handlePart); + cleanMarkupSpans(te); const target: X.Target = { type: 'element', name: 'target', elements: te }; ge = [source, target]; - } else ge = [source]; + } else { + ge = [source]; + } const segment: X.Segment = { type: 'element', name: 'segment', elements: ge }; const attributes = msgAttributes('u', key); let elements: X.Unit['elements']; - if (expressions.length > 0) { + if (refs.length > 0) { const name = 'mf:messageformat'; const mf: X.MessageFormat = { type: 'element', name, - elements: expressions + elements: refs }; elements = [mf, segment]; } else elements = [segment]; @@ -311,6 +362,29 @@ function resolveExpression( }; } +function resolveMarkup( + id: string, + { name, options }: MF.Markup +): X.MessageMarkup { + const elements: X.MessageOption[] = []; + if (options) { + for (const { name, value } of options) { + elements.push({ + type: 'element', + name: 'mf:option', + attributes: { name }, + elements: [resolveArgument(value)] + }); + } + } + return { + type: 'element', + name: 'mf:markup', + attributes: { id, name }, + elements + }; +} + function resolveArgument( part: MF.Literal | MF.VariableRef ): X.MessageLiteral | X.MessageVariable { diff --git a/packages/mf2-xliff/src/xliff-spec.ts b/packages/mf2-xliff/src/xliff-spec.ts index 574e7ada..ce70b751 100644 --- a/packages/mf2-xliff/src/xliff-spec.ts +++ b/packages/mf2-xliff/src/xliff-spec.ts @@ -1172,7 +1172,7 @@ export interface ValidationRule extends Element { export interface MessageFormat extends Element { name: 'mf:messageformat'; - elements: MessageExpression[]; + elements: (MessageExpression | MessageMarkup)[]; } export interface MessageExpression extends Element { @@ -1186,6 +1186,15 @@ export interface MessageExpression extends Element { | [MessageFunction | MessageUnsupported]; } +export interface MessageMarkup extends Element { + name: 'mf:markup'; + attributes: { + id: string; + name: string; + }; + elements: MessageOption[]; +} + export interface MessageLiteral extends Element { name: 'mf:literal'; attributes?: { diff --git a/packages/mf2-xliff/src/xliff2mf.ts b/packages/mf2-xliff/src/xliff2mf.ts index 07f75be7..7e42d3c3 100644 --- a/packages/mf2-xliff/src/xliff2mf.ts +++ b/packages/mf2-xliff/src/xliff2mf.ts @@ -150,9 +150,9 @@ function resolveSelect( } const selectors = idList(attributes['mf:select']).map(selId => { const exp = mf?.elements.find(exp => exp.attributes?.id === selId); - if (!exp) { + if (exp?.name !== 'mf:expression') { const el = prettyElement('group', attributes.id); - throw new Error(`Selector ${selId} not found in ${el}`); + throw new Error(`Selector expression ${selId} not found in ${el}`); } return resolveExpression(exp); }); @@ -180,17 +180,7 @@ function resolvePattern( `Expected to find a <${st}> inside <${el.name}> of ${pe}` ); } - if (stel) { - for (const ie of stel.elements) { - const last = body[body.length - 1]; - const next = resolveInlineElement(ie, mf); - if (typeof last === 'string' && typeof next === 'string') { - body[body.length - 1] += next; - } else { - body.push(next); - } - } - } + if (stel) body.push(...resolvePatternElements(mf, stel.elements)); break; } } @@ -198,13 +188,31 @@ function resolvePattern( return { body }; } +function resolvePatternElements( + mf: X.MessageFormat | null, + elements: (X.Text | X.InlineElement)[] +) { + const body: MF.Pattern['body'] = []; + for (const ie of elements) { + const last = body[body.length - 1]; + const next = resolveInlineElement(mf, ie); + if (typeof next === 'string') { + if (typeof last === 'string') body[body.length - 1] += next; + else body.push(next); + } else { + body.push(...next); + } + } + return body; +} + const resolveCharCode = (cc: X.CharCode) => String.fromCodePoint(Number(cc.attributes.hex)); function resolveInlineElement( - ie: X.Text | X.InlineElement, - mf: X.MessageFormat | null -): string | MF.Expression { + mf: X.MessageFormat | null, + ie: X.Text | X.InlineElement +): string | Array { switch (ie.type) { case 'text': case 'cdata': @@ -214,29 +222,63 @@ function resolveInlineElement( case 'cp': return resolveCharCode(ie); case 'ph': - return resolveRef(mf, ie.attributes['mf:ref']); - case 'pc': case 'sc': case 'ec': - // TODO + return [resolvePlaceholder(mf, ie)]; + case 'pc': + return resolveSpanningCode(mf, ie); } } throw new Error(`Unsupported inline ${ie.type} <${ie.name}>`); } -function resolveRef( +function resolveSpanningCode( + mf: X.MessageFormat | null, + pc: X.CodeSpan +): Array { + const open = resolvePlaceholder(mf, pc); + return [ + open, + ...resolvePatternElements(mf, pc.elements), + { type: 'markup', kind: 'close', name: open.name } + ]; +} + +function resolvePlaceholder( + mf: X.MessageFormat | null, + el: X.CodeSpan | X.CodeSpanStart | X.CodeSpanEnd +): MF.Markup; +function resolvePlaceholder( mf: X.MessageFormat | null, - ref: string | undefined -): MF.Expression { - if (!ref) throw new Error('Unsupported without mf:ref attribute'); + el: X.Placeholder | X.CodeSpan | X.CodeSpanStart | X.CodeSpanEnd +): MF.Expression | MF.Markup; +function resolvePlaceholder( + mf: X.MessageFormat | null, + el: X.Placeholder | X.CodeSpan | X.CodeSpanStart | X.CodeSpanEnd +): MF.Expression | MF.Markup { + const { name } = el; + const ref = el.attributes?.['mf:ref']; + if (!ref) throw new Error(`Unsupported <${name}> without mf:ref attribute`); if (!mf) throw new Error( - 'Inline requires a preceding in the same ' + `Inline <${name}> requires a preceding in the same ` ); const res = mf.elements.find(el => el.attributes?.id === ref); - if (!res) - throw new Error(`MessageFormat value not found for `); - return resolveExpression(res); + switch (res?.name) { + case 'mf:expression': + if (name !== 'ph') { + throw new Error('Only elements may refer to expression values'); + } + return resolveExpression(res); + case 'mf:markup': { + const kind = + name === 'ph' ? 'standalone' : name === 'ec' ? 'close' : 'open'; + return resolveMarkup(res, kind); + } + } + throw new Error( + `MessageFormat value not found for <${name} mf:ref="${ref}">` + ); } const resolveText = (text: (X.Text | X.CharCode)[]) => @@ -260,11 +302,7 @@ function resolveExpression(part: X.MessageExpression): MF.Expression { let annotation: MF.FunctionAnnotation | MF.UnsupportedAnnotation; if (xFunc.name === 'mf:function') { - annotation = { - type: 'function', - kind: 'value', - name: xFunc.attributes.name - }; + annotation = { type: 'function', name: xFunc.attributes.name }; if (xFunc.elements.length) { annotation.options = xFunc.elements.map(el => ({ name: el.attributes.name, @@ -284,6 +322,24 @@ function resolveExpression(part: X.MessageExpression): MF.Expression { : { type: 'expression', annotation }; } +function resolveMarkup( + part: X.MessageMarkup, + kind: 'open' | 'standalone' | 'close' +) { + const markup: MF.Markup = { + type: 'markup', + kind, + name: part.attributes.name + }; + if (kind !== 'close' && part.elements.length) { + markup.options = part.elements.map(el => ({ + name: el.attributes.name, + value: resolveValue(el.elements[0]) + })); + } + return markup; +} + function resolveValue( part: X.MessageLiteral | X.MessageVariable ): MF.Literal | MF.VariableRef { From 7ae0e1057e9181d1c4d584fa580b1797f14722d0 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Mon, 1 Jan 2024 13:36:51 +0200 Subject: [PATCH 11/44] feat(mf2): Allow number-literal as unquoted value --- .../src/__fixtures/test-messages.json | 12 +++-- .../mf2-messageformat/src/cst-parser/index.ts | 1 - .../mf2-messageformat/src/cst-parser/names.ts | 47 ++++++++++--------- .../src/cst-parser/values.ts | 7 --- .../src/expression/literal.test.ts | 18 +++++++ .../src/stringifier/message.ts | 2 +- 6 files changed, 52 insertions(+), 35 deletions(-) diff --git a/packages/mf2-messageformat/src/__fixtures/test-messages.json b/packages/mf2-messageformat/src/__fixtures/test-messages.json index 24dfe562..4f57160a 100644 --- a/packages/mf2-messageformat/src/__fixtures/test-messages.json +++ b/packages/mf2-messageformat/src/__fixtures/test-messages.json @@ -9,8 +9,8 @@ "exp": "hello world" }, { - "src": "hello {$place:-.}", - "params": { "place:-.": "world" }, + "src": "hello {$place-.}", + "params": { "place-.": "world" }, "exp": "hello world" }, { @@ -29,7 +29,9 @@ "params": { "one": 1.3, "two": 4.2 }, "exp": "1,3 et 4,2" }, - { "src": "hello {|4.2| :number}", "exp": "hello 4.2" }, + { "src": "hello {4.2 :number}", "exp": "hello 4.2" }, + { "src": "hello {-4.20 :number}", "exp": "hello -4.2" }, + { "src": "hello {0.42e+1 :number}", "exp": "hello 4.2" }, { "src": "hello {foo :number}", "exp": "hello {|foo|}", @@ -41,7 +43,7 @@ "errors": [{ "type": "bad-input" }] }, { - "src": "hello {|4.2| :number minimumFractionDigits=2}", + "src": "hello {4.2 :number minimumFractionDigits=2}", "exp": "hello 4.20" }, { @@ -49,7 +51,7 @@ "exp": "hello 4.20" }, { - "src": "hello {|4.2| :number minimumFractionDigits=$foo}", + "src": "hello {4.2 :number minimumFractionDigits=$foo}", "params": { "foo": 2 }, "exp": "hello 4.20" }, diff --git a/packages/mf2-messageformat/src/cst-parser/index.ts b/packages/mf2-messageformat/src/cst-parser/index.ts index ae1bf8ab..9f3e9fe8 100644 --- a/packages/mf2-messageformat/src/cst-parser/index.ts +++ b/packages/mf2-messageformat/src/cst-parser/index.ts @@ -2,4 +2,3 @@ import type * as CST from './cst-types.js'; export { CST }; export { asDataModel } from './as-data-model.js'; export { parseMessage } from './message.js'; -export { isValidUnquotedLiteral } from './names.js'; diff --git a/packages/mf2-messageformat/src/cst-parser/names.ts b/packages/mf2-messageformat/src/cst-parser/names.ts index b7e5e6b1..2c8fca26 100644 --- a/packages/mf2-messageformat/src/cst-parser/names.ts +++ b/packages/mf2-messageformat/src/cst-parser/names.ts @@ -22,10 +22,11 @@ const isNameStartCode = (cc: number) => (cc >= 0xfdf0 && cc <= 0xfffd) || (cc >= 0x10000 && cc <= 0xeffff); -// unquoted-start = name-start / DIGIT / "." -// / %xB7 / %x300-36F / %x203F-2040 -const isUnquotedStartCharCode = (cc: number) => +// name-char = name-start / DIGIT / "-" / "." +// / %xB7 / %x300-36F / %x203F-2040 +const isNameCharCode = (cc: number) => isNameStartCode(cc) || + cc === 0x2d || // - cc === 0x2e || // . (cc >= 0x30 && cc <= 0x39) || // 0-9 cc === 0xb7 || // · @@ -33,13 +34,22 @@ const isUnquotedStartCharCode = (cc: number) => cc === 0x203f || // ‿ cc === 0x2040; // ⁀ -// name-char = name-start / DIGIT / "-" / "." / ":" -// / %xB7 / %x300-36F / %x203F-2040 -const isNameCharCode = (cc: number) => - isUnquotedStartCharCode(cc) || cc === 0x2d || cc === 0x3a; // - : +// This is sticky so that parsing doesn't need to substring the source +const numberLiteral = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][-+]?\d+)?/y; + +export function parseNameValue(src: string, start: number): string { + if (!isNameStartCode(src.charCodeAt(start))) return ''; + let pos = start + 1; + while (isNameCharCode(src.charCodeAt(pos))) pos += 1; + return src.substring(start, pos); +} export function isValidUnquotedLiteral(str: string): boolean { - if (!isUnquotedStartCharCode(str.charCodeAt(0))) return false; + numberLiteral.lastIndex = 0; + const num = numberLiteral.exec(str); + if (num && num[0].length === str.length) return true; + + if (!isNameStartCode(str.charCodeAt(0))) return false; for (let i = 1; i < str.length; ++i) { const cc = str.charCodeAt(i); if (!isNameCharCode(cc)) return false; @@ -47,23 +57,18 @@ export function isValidUnquotedLiteral(str: string): boolean { return str.length > 0; } -// name = name-start *name-char -export function parseNameValue(src: string, start: number): string { - if (!isNameStartCode(src.charCodeAt(start))) return ''; - let pos = start + 1; - while (isNameCharCode(src.charCodeAt(pos))) pos += 1; - return src.substring(start, pos); -} - -// unquoted = unquoted-start *name-char export function parseUnquotedLiteralValue( - ctx: ParseContext, + { source }: ParseContext, start: number ): string { + numberLiteral.lastIndex = start; + const num = numberLiteral.exec(source); + if (num) return num[0]; + let pos = start; - if (isUnquotedStartCharCode(ctx.source.charCodeAt(pos))) { + if (isNameStartCode(source.charCodeAt(pos))) { pos += 1; - while (isNameCharCode(ctx.source.charCodeAt(pos))) pos += 1; + while (isNameCharCode(source.charCodeAt(pos))) pos += 1; } - return ctx.source.substring(start, pos); + return source.substring(start, pos); } diff --git a/packages/mf2-messageformat/src/cst-parser/values.ts b/packages/mf2-messageformat/src/cst-parser/values.ts index 64a2ef76..8f199baa 100644 --- a/packages/mf2-messageformat/src/cst-parser/values.ts +++ b/packages/mf2-messageformat/src/cst-parser/values.ts @@ -70,13 +70,6 @@ export function parseLiteral( return { type: 'literal', quoted: false, start, end, value }; } -// quoted = "|" *(quoted-char / quoted-escape) "|" -// quoted-char = %x0-5B ; omit \ -// / %x5D-7B ; omit | -// / %x7D-D7FF ; omit surrogates -// / %xE000-10FFFF -// quoted-escape = backslash ( backslash / "|" ) -// backslash = %x5C ; U+005C REVERSE SOLIDUS "\" export function parseQuotedLiteral( ctx: ParseContext, start: number diff --git a/packages/mf2-messageformat/src/expression/literal.test.ts b/packages/mf2-messageformat/src/expression/literal.test.ts index 62824061..3a7c2771 100644 --- a/packages/mf2-messageformat/src/expression/literal.test.ts +++ b/packages/mf2-messageformat/src/expression/literal.test.ts @@ -30,3 +30,21 @@ describe('quoted literals', () => { expect(() => new MessageFormat('{|quoted \\{iteral|}')).toThrow(); }); }); + +describe('unquoted numbers', () => { + for (const value of [ + '0', + '42', + '2.5', + '-1', + '-0.999', + '1e3', + '0.4E+5', + '11.1e-1' + ]) { + test(value, () => { + const res = resolve(`{${value}}`); + expect(res).toMatchObject([{ type: 'string', value }]); + }); + } +}); diff --git a/packages/mf2-messageformat/src/stringifier/message.ts b/packages/mf2-messageformat/src/stringifier/message.ts index 0bc9341c..3b77ad57 100644 --- a/packages/mf2-messageformat/src/stringifier/message.ts +++ b/packages/mf2-messageformat/src/stringifier/message.ts @@ -1,4 +1,4 @@ -import { isValidUnquotedLiteral } from '../cst-parser/index.js'; +import { isValidUnquotedLiteral } from '../cst-parser/names.js'; import { Declaration, Expression, From c9e081f4f8ee5f623a100738536344970a5e7877 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Mon, 1 Jan 2024 14:45:46 +0200 Subject: [PATCH 12/44] feat(mf2): Add CST Identifier, with ns:name support --- .../src/__fixtures/test-messages.json | 17 +++-- .../src/cst-parser/as-data-model.ts | 39 ++++++---- .../src/cst-parser/cst-types.ts | 16 +++-- .../src/cst-parser/expression.ts | 71 +++++++++++-------- .../src/markup/markup.test.ts | 12 +++- 5 files changed, 101 insertions(+), 54 deletions(-) diff --git a/packages/mf2-messageformat/src/__fixtures/test-messages.json b/packages/mf2-messageformat/src/__fixtures/test-messages.json index 4f57160a..cc0e8b8a 100644 --- a/packages/mf2-messageformat/src/__fixtures/test-messages.json +++ b/packages/mf2-messageformat/src/__fixtures/test-messages.json @@ -264,12 +264,12 @@ ] }, { - "src": "{#tag}content{/tag}", + "src": "{#ns:tag}content{/ns:tag}", "exp": "content", "parts": [ - { "type": "markup", "kind": "open", "name": "tag" }, + { "type": "markup", "kind": "open", "name": "ns:tag" }, { "type": "literal", "value": "content" }, - { "type": "markup", "kind": "close", "name": "tag" } + { "type": "markup", "kind": "close", "name": "ns:tag" } ] }, { @@ -293,7 +293,7 @@ ] }, { - "src": "{#tag foo=|foo| bar=$bar}", + "src": "{#tag a:foo=|foo| b:bar=$bar}", "params": { "bar": "b a r" }, "exp": "", "parts": [ @@ -301,7 +301,7 @@ "type": "markup", "kind": "open", "name": "tag", - "options": { "foo": "foo", "bar": "b a r" } + "options": { "a:foo": "foo", "b:bar": "b a r" } } ] }, @@ -332,7 +332,14 @@ }, { "src": "bad {:placeholder option=}", "syntaxError": true }, { "src": "bad {:placeholder option value}", "syntaxError": true }, + { "src": "bad {:placeholder option:value}", "syntaxError": true }, { "src": "bad {:placeholder option}", "syntaxError": true }, + { "src": "bad {:placeholder:}", "syntaxError": true }, + { "src": "bad {::placeholder}", "syntaxError": true }, + { "src": "bad {:placeholder::foo}", "syntaxError": true }, + { "src": "bad {:placeholder option:=x}", "syntaxError": true }, + { "src": "bad {:placeholder :option=x}", "syntaxError": true }, + { "src": "bad {:placeholder option::x=y}", "syntaxError": true }, { "src": "bad {$placeholder option}", "syntaxError": true }, { "src": "no {$placeholder end", "syntaxError": true }, { "src": ".match {} * {{foo}}", "syntaxError": true }, diff --git a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts index 3e914508..daf1310c 100644 --- a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts +++ b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts @@ -85,20 +85,16 @@ function asExpression( if (cst.type === 'expression') { if (allowMarkup && cst.markup) { const cm = cst.markup; + const name = asName(cm.name); if (cm.type === 'markup-close') { - return { type: 'markup', kind: 'close', name: cm.name }; + return { type: 'markup', kind: 'close', name }; } const markup: Model.MarkupOpen | Model.MarkupStandalone = { type: 'markup', kind: cm.close ? 'standalone' : 'open', - name: cm.name + name }; - if (cm.options.length > 0) { - markup.options = cm.options.map(opt => ({ - name: opt.name, - value: asValue(opt.value) - })); - } + if (cm.options.length > 0) markup.options = cm.options.map(asOption); return markup; } @@ -112,12 +108,9 @@ function asExpression( if (ca) { switch (ca.type) { case 'function': - annotation = { type: 'function', name: ca.name }; + annotation = { type: 'function', name: asName(ca.name) }; if (ca.options.length > 0) { - annotation.options = ca.options.map(opt => ({ - name: opt.name, - value: asValue(opt.value) - })); + annotation.options = ca.options.map(asOption); } break; case 'reserved-annotation': @@ -142,6 +135,26 @@ function asExpression( throw new MessageSyntaxError('parse-error', cst.start, cst.end); } +const asOption = (cst: CST.Option): Model.Option => ({ + name: asName(cst.name), + value: asValue(cst.value) +}); + +function asName(cst: CST.Identifier): string { + switch (cst.length) { + case 1: + return cst[0].value; + case 3: + return `${cst[0].value}:${cst[2].value}`; + default: + throw new MessageSyntaxError( + 'parse-error', + cst[0]?.start ?? -1, + cst.at(-1)?.end ?? -1 + ); + } +} + function asValue(cst: CST.Literal | CST.Junk): Model.Literal; function asValue(cst: CST.VariableRef | CST.Junk): Model.VariableRef; function asValue( diff --git a/packages/mf2-messageformat/src/cst-parser/cst-types.ts b/packages/mf2-messageformat/src/cst-parser/cst-types.ts index 8352722b..86cf6da1 100644 --- a/packages/mf2-messageformat/src/cst-parser/cst-types.ts +++ b/packages/mf2-messageformat/src/cst-parser/cst-types.ts @@ -142,7 +142,7 @@ export interface FunctionRef { type: 'function'; start: number; end: number; - name: string; + name: Identifier; options: Option[]; } @@ -162,9 +162,9 @@ export interface Markup { /** position of the sigil */ start: number; end: number; - close: Syntax<'/'> | undefined; - name: string; + name: Identifier; options: Option[]; + close: Syntax<'/'> | undefined; } /** @beta */ @@ -173,7 +173,7 @@ export interface MarkupClose { /** position of the sigil */ start: number; end: number; - name: string; + name: Identifier; } /** @beta */ @@ -181,10 +181,16 @@ export interface Option { /** position at the start of the name */ start: number; end: number; - name: string; + name: Identifier; value: Literal | VariableRef; } +/** @beta */ +export type Identifier = + | [name: Syntax] + | [namespace: Syntax, separator: Syntax<':'>] + | [namespace: Syntax, separator: Syntax<':'>, name: Syntax]; + /** @beta */ export interface Syntax { start: number; diff --git a/packages/mf2-messageformat/src/cst-parser/expression.ts b/packages/mf2-messageformat/src/cst-parser/expression.ts index 5e584820..d98c9a6a 100644 --- a/packages/mf2-messageformat/src/cst-parser/expression.ts +++ b/packages/mf2-messageformat/src/cst-parser/expression.ts @@ -107,20 +107,19 @@ function parseFunctionRefOrMarkup( start: number, type: 'function' | 'markup' ): CST.FunctionRef | CST.Markup { - let pos = start + 1; // ':' | '#' - const name = parseNameValue(ctx.source, pos); - if (!name) ctx.onError('empty-token', pos, pos + 1); + const { source } = ctx; + const id = parseIdentifier(ctx, start + 1); + let pos = id.end; const options: CST.Option[] = []; - pos += name.length; let close: CST.Syntax<'/'> | undefined; - while (pos < ctx.source.length) { - let ws = whitespaces(ctx.source, pos); - const next = ctx.source[pos + ws]; + while (pos < source.length) { + let ws = whitespaces(source, pos); + const next = source[pos + ws]; if (next === '}') break; if (type === 'markup' && next === '/') { pos += ws + 1; close = { start: pos - 1, end: pos, value: '/' }; - ws = whitespaces(ctx.source, pos); + ws = whitespaces(source, pos); if (ws > 0) ctx.onError('extra-content', pos, pos + ws); break; } @@ -132,22 +131,18 @@ function parseFunctionRefOrMarkup( pos = opt.end; } return type === 'function' - ? { type, start, end: pos, name, options } - : { type, start, end: pos, name, options, close }; + ? { type, start, end: pos, name: id.parts, options } + : { type, start, end: pos, name: id.parts, options, close }; } function parseMarkupClose(ctx: ParseContext, start: number): CST.MarkupClose { - let pos = start + 1; // '/' - const name = parseNameValue(ctx.source, pos); - if (name) pos += name.length; - else ctx.onError('empty-token', pos, pos + 1); - return { type: 'markup-close', start, end: pos, name }; + const id = parseIdentifier(ctx, start + 1); + return { type: 'markup-close', start, end: id.end, name: id.parts }; } -// option = name [s] "=" [s] (literal / variable) function parseOption(ctx: ParseContext, start: number): CST.Option { - const name = parseNameValue(ctx.source, start); - let pos = start + name.length; + const id = parseIdentifier(ctx, start); + let pos = id.end; pos += whitespaces(ctx.source, pos); if (ctx.source[pos] === '=') pos += 1; else ctx.onError('missing-syntax', pos, '='); @@ -156,19 +151,37 @@ function parseOption(ctx: ParseContext, start: number): CST.Option { ctx.source[pos] === '$' ? parseVariable(ctx, pos) : parseLiteral(ctx, pos, true); - return { start, end: value.end, name, value }; + return { start, end: value.end, name: id.parts, value }; +} + +function parseIdentifier( + ctx: ParseContext, + start: number +): { parts: CST.Identifier; end: number } { + const { source } = ctx; + const str0 = parseNameValue(source, start); + if (!str0) { + ctx.onError('empty-token', start, start + 1); + return { parts: [{ start, end: start, value: '' }], end: start }; + } + let pos = start + str0.length; + const id0 = { start, end: pos, value: str0 }; + if (source[pos] !== ':') return { parts: [id0], end: pos }; + + const sep = { start: pos, end: pos + 1, value: ':' as const }; + pos += 1; + + const str1 = parseNameValue(source, pos); + if (str1) { + const end = pos + str1.length; + const id1 = { start: pos, end, value: str1 }; + return { parts: [id0, sep, id1], end }; + } else { + ctx.onError('empty-token', pos, pos + 1); + return { parts: [id0, sep], end: pos }; + } } -// reserved = reserved-start reserved-body -// reserved-start = "!" / "@" / "#" / "%" / "^" / "&" / "*" / "<" / ">" / "?" / "~" -// reserved-body = *( [s] 1*(reserved-char / reserved-escape / literal)) -// reserved-char = %x00-08 ; omit HTAB and LF -// / %x0B-0C ; omit CR -// / %x0E-19 ; omit SP -// / %x21-5B ; omit \ -// / %x5D-7A ; omit { | } -// / %x7E-D7FF ; omit surrogates -// / %xE000-10FFFF function parseReservedAnnotation( ctx: ParseContext, start: number diff --git a/packages/mf2-messageformat/src/markup/markup.test.ts b/packages/mf2-messageformat/src/markup/markup.test.ts index 10238e71..0d62bc19 100644 --- a/packages/mf2-messageformat/src/markup/markup.test.ts +++ b/packages/mf2-messageformat/src/markup/markup.test.ts @@ -37,7 +37,12 @@ describe('Simple open/close', () => { type: 'simple', errors: [{ type: 'extra-content' }], pattern: { - body: [{ type: 'expression', markup: { type: 'markup', name: 'b' } }] + body: [ + { + type: 'expression', + markup: { type: 'markup', name: [{ value: 'b' }] } + } + ] } }); }); @@ -51,7 +56,10 @@ describe('Simple open/close', () => { errors: [{ type: 'extra-content' }], pattern: { body: [ - { type: 'expression', markup: { type: 'markup-close', name: 'b' } } + { + type: 'expression', + markup: { type: 'markup-close', name: [{ value: 'b' }] } + } ] } }); From e22e4b7b581350c13e8f7581598614b490473f15 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 2 Jan 2024 00:17:12 +0200 Subject: [PATCH 13/44] fix(mf2): Simplify formatting Context, dropping resolveExpression & resolveValue wrappers --- .../mf2-messageformat/src/expression/index.ts | 1 - .../src/expression/variable-ref.ts | 9 ++-- .../mf2-messageformat/src/format-context.ts | 3 -- .../mf2-messageformat/src/messageformat.ts | 43 ++++++++----------- .../mf2-messageformat/src/select-pattern.ts | 3 +- 5 files changed, 25 insertions(+), 34 deletions(-) diff --git a/packages/mf2-messageformat/src/expression/index.ts b/packages/mf2-messageformat/src/expression/index.ts index f617156a..4137d448 100644 --- a/packages/mf2-messageformat/src/expression/index.ts +++ b/packages/mf2-messageformat/src/expression/index.ts @@ -22,7 +22,6 @@ export { isUnsupportedAnnotation, UnsupportedAnnotation } from './unsupported-annotation'; -export { resolveValue } from './value.js'; export { getMessageValue, isVariableRef, diff --git a/packages/mf2-messageformat/src/expression/variable-ref.ts b/packages/mf2-messageformat/src/expression/variable-ref.ts index efcf411c..bfa7181c 100644 --- a/packages/mf2-messageformat/src/expression/variable-ref.ts +++ b/packages/mf2-messageformat/src/expression/variable-ref.ts @@ -6,7 +6,7 @@ import { fallback, unknown } from '../runtime/index.js'; -import type { Expression } from './index.js'; +import { resolveExpression, type Expression } from './index.js'; /** * The value of a VariableRef is defined by the current Scope. @@ -35,8 +35,8 @@ export interface VariableRef { */ export class UnresolvedExpression { expression: Expression; - scope: Context['scope']; - constructor(expression: Expression, scope: Context['scope']) { + scope: Record | undefined; + constructor(expression: Expression, scope?: Record) { this.expression = expression; this.scope = scope; } @@ -78,7 +78,8 @@ function getValue(scope: unknown, name: string): unknown { export function lookupVariableRef(ctx: Context, { name }: VariableRef) { const value = getValue(ctx.scope, name); if (value instanceof UnresolvedExpression) { - const local = { ...ctx, scope: value.scope }.resolveExpression( + const local = resolveExpression( + value.scope ? { ...ctx, scope: value.scope } : ctx, value.expression ); ctx.scope[name] = local; diff --git a/packages/mf2-messageformat/src/format-context.ts b/packages/mf2-messageformat/src/format-context.ts index 0d92171e..c4a2983c 100644 --- a/packages/mf2-messageformat/src/format-context.ts +++ b/packages/mf2-messageformat/src/format-context.ts @@ -1,11 +1,8 @@ -import type { Expression, Literal, VariableRef } from './expression'; import type { MessageFunctions, MessageValue } from './runtime'; export interface Context { functions: MessageFunctions; onError(error: unknown): void; - resolveExpression(expr: Expression): MessageValue; - resolveValue(value: Literal | VariableRef): unknown; localeMatcher: 'best fit' | 'lookup'; locales: string[]; /** Cache for local variables */ diff --git a/packages/mf2-messageformat/src/messageformat.ts b/packages/mf2-messageformat/src/messageformat.ts index 9dcda58a..67da075d 100644 --- a/packages/mf2-messageformat/src/messageformat.ts +++ b/packages/mf2-messageformat/src/messageformat.ts @@ -2,11 +2,7 @@ import { asDataModel, parseMessage } from './cst-parser/index.js'; import type { Message } from './data-model'; import { MessageError, MessageResolutionError } from './errors.js'; import type { Context } from './format-context'; -import { - UnresolvedExpression, - resolveExpression, - resolveValue -} from './expression/index.js'; +import { UnresolvedExpression, resolveExpression } from './expression/index.js'; import { formatMarkup } from './markup/index.js'; import { MessageFunctions, @@ -76,7 +72,7 @@ export class MessageFormat { } else if (elem.type !== 'markup') { let mv: MessageValue | undefined; try { - mv = ctx.resolveExpression(elem); + mv = resolveExpression(ctx, elem); if (typeof mv.toString === 'function') { res += mv.toString(); } else { @@ -106,7 +102,7 @@ export class MessageFormat { } else { let mv: MessageValue | undefined; try { - mv = ctx.resolveExpression(elem); + mv = resolveExpression(ctx, elem); if (typeof mv.toParts === 'function') { parts.push(...mv.toParts()); } else { @@ -142,29 +138,26 @@ export class MessageFormat { } } ) { - let scope = { ...msgParams }; + const scope = { ...msgParams }; for (const decl of this.#message.declarations) { - if (decl.type === 'unsupported-statement') { - const source = decl.keyword ?? '�'; - const msg = `Reserved ${source} annotation is not supported`; - onError( - new MessageResolutionError('unsupported-statement', msg, source) - ); - } else { - const { name, value } = decl; - const ue = new UnresolvedExpression(value, scope); - if (name in scope) scope = { ...scope, [name]: ue }; - else scope[name] = ue; + switch (decl.type) { + case 'input': + scope[decl.name] = new UnresolvedExpression(decl.value, msgParams); + break; + case 'local': + scope[decl.name] = new UnresolvedExpression(decl.value); + break; + default: { + const source = decl.keyword ?? '�'; + const msg = `Reserved ${source} annotation is not supported`; + onError( + new MessageResolutionError('unsupported-statement', msg, source) + ); + } } } const ctx: Context = { onError, - resolveExpression(elem) { - return resolveExpression(this, elem); - }, - resolveValue(value) { - return resolveValue(this, value); - }, localeMatcher: this.#localeMatcher, locales: this.#locales, localVars: new WeakSet(), diff --git a/packages/mf2-messageformat/src/select-pattern.ts b/packages/mf2-messageformat/src/select-pattern.ts index d531aec3..57adf7fe 100644 --- a/packages/mf2-messageformat/src/select-pattern.ts +++ b/packages/mf2-messageformat/src/select-pattern.ts @@ -1,5 +1,6 @@ import type { Message, Pattern } from './data-model'; import { MessageSelectionError } from './errors'; +import { resolveExpression } from './expression/index.js'; import type { Context } from './format-context'; export function selectPattern( @@ -12,7 +13,7 @@ export function selectPattern( case 'select': { const ctx = message.selectors.map(sel => { - const selector = context.resolveExpression(sel); + const selector = resolveExpression(context, sel); let selectKey; if (typeof selector.selectKey === 'function') { selectKey = selector.selectKey.bind(selector); From d821d05990aebe2275bafef9851068489f39903e Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 2 Jan 2024 01:00:20 +0200 Subject: [PATCH 14/44] fix(mf2): Treat duplicate option identifiers as a data model error --- .../src/__fixtures/test-messages.json | 7 +++++++ .../mf2-messageformat/src/cst-parser/expression.ts | 12 ++++++++++++ packages/mf2-messageformat/src/errors.ts | 3 +++ 3 files changed, 22 insertions(+) diff --git a/packages/mf2-messageformat/src/__fixtures/test-messages.json b/packages/mf2-messageformat/src/__fixtures/test-messages.json index cc0e8b8a..92a7e682 100644 --- a/packages/mf2-messageformat/src/__fixtures/test-messages.json +++ b/packages/mf2-messageformat/src/__fixtures/test-messages.json @@ -330,6 +330,8 @@ "src": "no-equal {|42| :number minimumFractionDigits 2}", "syntaxError": true }, + { "src": "bad {:placeholder option=x option=x}", "syntaxError": true }, + { "src": "bad {:placeholder ns:option=x ns:option=y}", "syntaxError": true }, { "src": "bad {:placeholder option=}", "syntaxError": true }, { "src": "bad {:placeholder option value}", "syntaxError": true }, { "src": "bad {:placeholder option:value}", "syntaxError": true }, @@ -341,7 +343,12 @@ { "src": "bad {:placeholder :option=x}", "syntaxError": true }, { "src": "bad {:placeholder option::x=y}", "syntaxError": true }, { "src": "bad {$placeholder option}", "syntaxError": true }, + { "src": "no {placeholder end", "syntaxError": true }, { "src": "no {$placeholder end", "syntaxError": true }, + { "src": "no {:placeholder end", "syntaxError": true }, + { "src": "no {|placeholder| end", "syntaxError": true }, + { "src": "no {|literal} end", "syntaxError": true }, + { "src": "no {|literal or placeholder end", "syntaxError": true }, { "src": ".match {} * {{foo}}", "syntaxError": true }, { "src": ".match {|foo|} {|bar|} ** {{foo}}", "syntaxError": true }, { "src": ".match * {{foo}}", "syntaxError": true }, diff --git a/packages/mf2-messageformat/src/cst-parser/expression.ts b/packages/mf2-messageformat/src/cst-parser/expression.ts index d98c9a6a..6095fc79 100644 --- a/packages/mf2-messageformat/src/cst-parser/expression.ts +++ b/packages/mf2-messageformat/src/cst-parser/expression.ts @@ -127,6 +127,13 @@ function parseFunctionRefOrMarkup( pos += ws; const opt = parseOption(ctx, pos); if (opt.end === pos) break; // error + if (options.some(prev => sameIdentifier(prev.name, opt.name))) { + ctx.onError( + 'duplicate-option', + opt.name[0].start, + opt.name.at(-1)?.end ?? -1 + ); + } options.push(opt); pos = opt.end; } @@ -182,6 +189,11 @@ function parseIdentifier( } } +const sameIdentifier = (a: CST.Identifier, b: CST.Identifier) => + a.length === b.length && + a[0].value === b[0].value && + a[2]?.value === b[2]?.value; + function parseReservedAnnotation( ctx: ParseContext, start: number diff --git a/packages/mf2-messageformat/src/errors.ts b/packages/mf2-messageformat/src/errors.ts index 12af8a3e..415bd56c 100644 --- a/packages/mf2-messageformat/src/errors.ts +++ b/packages/mf2-messageformat/src/errors.ts @@ -19,6 +19,7 @@ export class MessageSyntaxError extends MessageError { | 'bad-input-expression' | 'bad-selector' | 'duplicate-declaration' + | 'duplicate-option' | 'extra-content' | 'forward-reference' | 'key-mismatch' @@ -49,6 +50,7 @@ export class MessageSyntaxError extends MessageError { message = `Syntax parse error: Missing ${expected} at ${start}`; break; case 'duplicate-declaration': + case 'duplicate-option': case 'forward-reference': case 'key-mismatch': case 'missing-fallback': @@ -73,6 +75,7 @@ export class MissingSyntaxError extends MessageSyntaxError { export class MessageDataModelError extends MessageSyntaxError { declare type: | 'duplicate-declaration' + | 'duplicate-option' | 'forward-reference' | 'key-mismatch' | 'missing-fallback'; From ae72b4c4c5b263f86dd95eaf43f58ae1923d2edc Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 2 Jan 2024 13:09:08 +0200 Subject: [PATCH 15/44] style: Add ESLint rules curly, no-useless-rename & sort-imports --- .eslintrc.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.eslintrc.yaml b/.eslintrc.yaml index c510f356..080723f9 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -13,11 +13,14 @@ rules: array-callback-return: error camelcase: off consistent-return: error + curly: [warn, multi-line, consistent] eqeqeq: [error, smart] no-constant-condition: [error, checkLoops: false] no-implicit-globals: error no-unused-vars: [warn, argsIgnorePattern: '^_'] + no-useless-rename: error prefer-const: [error, destructuring: all] + sort-imports: [warn, ignoreDeclarationSort: true] overrides: - files: From 289d640546e9360be12f8929c85464b778510967 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 2 Jan 2024 13:11:28 +0200 Subject: [PATCH 16/44] style: Apply updated ESLint rules --- packages/cli/lib/options.js | 3 +- packages/cli/lib/read-input.js | 4 +- packages/cli/lib/simplify-input.js | 9 ++-- packages/convert/index.js | 3 +- packages/convert/lib/get-plural-message.js | 6 ++- packages/core/src/compile-module.ts | 3 +- packages/core/src/compiler.ts | 34 +++++++----- packages/core/src/messageformat.test.ts | 12 +++-- packages/core/src/messageformat.ts | 6 +-- packages/core/src/plurals.ts | 3 +- .../src/get-date-formatter.test.ts | 4 +- .../date-skeleton/src/get-date-formatter.ts | 4 +- packages/date-skeleton/src/options.ts | 5 +- packages/mf2-fluent/src/fluent-to-message.ts | 19 ++++--- packages/mf2-fluent/src/fluent-to-resource.ts | 3 +- packages/mf2-fluent/src/message-to-fluent.ts | 10 ++-- packages/mf2-icu-mf1/src/functions.ts | 4 +- .../mf2-icu-mf1/src/mf1-to-message-data.ts | 28 ++++++---- packages/mf2-icu-mf1/src/mf1-to-message.ts | 2 +- packages/mf2-icu-mf1/src/mf1.test.ts | 12 +++-- .../src/__fixtures/test-name.ts | 3 +- .../mf2-messageformat/src/expression/index.ts | 10 ++-- .../mf2-messageformat/src/expression/value.ts | 2 +- .../src/expression/variable-ref.ts | 2 +- .../src/mf2-features.test.ts | 6 +-- .../src/stringifier/message.ts | 3 +- packages/mf2-xliff/src/mf2xliff.ts | 53 ++++++++++++------- packages/mf2-xliff/src/xliff.ts | 5 +- packages/mf2-xliff/src/xliff2mf.ts | 6 ++- .../src/get-pattern-formatter.test.ts | 7 ++- .../src/get-skeleton-formatter.test.ts | 4 +- .../src/numberformat/options.test.ts | 4 +- .../src/numberformat/options.ts | 3 +- .../src/pattern-parser/number-as-skeleton.ts | 11 ++-- .../src/skeleton-parser/options.ts | 2 +- .../parse-precision-blueprint.ts | 6 ++- .../skeleton-parser/skeleton-parser.test.ts | 3 +- .../src/skeleton-parser/token-parser.ts | 39 +++++++++----- packages/parser/src/parser.test.ts | 8 +-- packages/parser/src/parser.ts | 23 +++++--- packages/react/src/examples.test.tsx | 4 +- packages/react/src/message-provider.ts | 7 +-- packages/react/src/message.ts | 6 ++- packages/react/src/use-message-getter.ts | 2 +- packages/rollup-plugin/src/index.ts | 7 +-- packages/runtime/src/runtime.ts | 6 ++- packages/test-utils/src/index.ts | 3 +- packages/webpack-loader/index.js | 6 ++- scripts/set-api-docs-frontmatter.js | 3 +- test/browser/browserstack-fixtures.js | 12 +++-- test/browser/src/browser-tests.ts | 12 +++-- 51 files changed, 283 insertions(+), 159 deletions(-) diff --git a/packages/cli/lib/options.js b/packages/cli/lib/options.js index c0ffedc8..f60d0265 100644 --- a/packages/cli/lib/options.js +++ b/packages/cli/lib/options.js @@ -102,8 +102,9 @@ messageformat.rc.{js,json,yaml}.`; return opt; }, _(input) { - if (input.length === 0) + if (input.length === 0) { throw new Error('At least one input file or directory is required.'); + } return input.map(fn => path.resolve(fn)); } }) diff --git a/packages/cli/lib/read-input.js b/packages/cli/lib/read-input.js index dbf50385..7664568d 100644 --- a/packages/cli/lib/read-input.js +++ b/packages/cli/lib/read-input.js @@ -76,7 +76,9 @@ module.exports = function readInput(include, extensions, sep) { const child = input[keys[0]]; if (!child || typeof child !== 'object') break; input = child; - } else break; + } else { + break; + } } return input; }; diff --git a/packages/cli/lib/simplify-input.js b/packages/cli/lib/simplify-input.js index de5010e4..7298ed48 100644 --- a/packages/cli/lib/simplify-input.js +++ b/packages/cli/lib/simplify-input.js @@ -1,11 +1,14 @@ function getAllObjects(obj, level) { if (typeof obj !== 'object') throw new Error('non-object value'); - if (level === 0) return [obj]; - else if (level === 1) return Object.keys(obj).map(k => obj[k]); - else + if (level === 0) { + return [obj]; + } else if (level === 1) { + return Object.keys(obj).map(k => obj[k]); + } else { return Object.keys(obj) .map(k => getAllObjects(obj[k], level - 1)) .reduce((a, b) => a.concat(b), []); + } } module.exports = function simplifyInput(input) { diff --git a/packages/convert/index.js b/packages/convert/index.js index 16c98133..04305b40 100644 --- a/packages/convert/index.js +++ b/packages/convert/index.js @@ -12,8 +12,9 @@ const defaultOptions = { const convertData = (data, locale, options) => { if (!data) return ''; - if (Array.isArray(data)) + if (Array.isArray(data)) { return data.map(d => convertData(d, locale, options)); + } if (typeof data !== 'object') return applyReplacements(data, options); const pluralMsg = options.pluralVariable && getPluralMessage(data, locale, options); diff --git a/packages/convert/lib/get-plural-message.js b/packages/convert/lib/get-plural-message.js index 839eef16..4a871658 100644 --- a/packages/convert/lib/get-plural-message.js +++ b/packages/convert/lib/get-plural-message.js @@ -15,8 +15,9 @@ function isPluralObject(data, locale, { verbose }) { pluralKeys.includes(key) && (!data[key] || typeof data[key] !== 'object') ) - ) + ) { return true; + } if (verbose && keys.every(key => cldrPluralCategories.includes(key))) { const msg = `Possible plural with keys not supported by ${locale}`; console.warn(`messageformat-convert: ${msg}: ${JSON.stringify(data)}`); @@ -30,8 +31,9 @@ function isPluralObject(data, locale, { verbose }) { * `options.pluralVariable` is empty) returns `null`. */ module.exports = function getPluralMessage(data, locale, options) { - if (!options.pluralVariable || !isPluralObject(data, locale, options)) + if (!options.pluralVariable || !isPluralObject(data, locale, options)) { return null; + } const keys = Object.keys(data); const messages = keys.map(key => applyReplacements(data[key], options)); const c0 = commonLength(messages, false); diff --git a/packages/core/src/compile-module.ts b/packages/core/src/compile-module.ts index 996f2c8a..835c612b 100644 --- a/packages/core/src/compile-module.ts +++ b/packages/core/src/compile-module.ts @@ -112,8 +112,9 @@ export default function compileModule( ) { const { plurals } = messageformat; const cp: { [key: string]: PluralObject } = {}; - if (plurals.length > 1) + if (plurals.length > 1) { for (const pl of plurals) cp[pl.lc] = cp[pl.locale] = pl; + } const compiler = new Compiler(messageformat.options); const msgObj = compiler.compile(messages, plurals[0], cp); const msgStr = stringifyObject(msgObj); diff --git a/packages/core/src/compiler.ts b/packages/core/src/compiler.ts index 98fdf0ab..f6a9c9bc 100644 --- a/packages/core/src/compiler.ts +++ b/packages/core/src/compiler.ts @@ -6,7 +6,7 @@ import { getNumberFormatter, getNumberFormatterSource } from '@messageformat/number-skeleton'; -import { parse, FunctionArg, Select, Token } from '@messageformat/parser'; +import { FunctionArg, Select, Token, parse } from '@messageformat/parser'; import * as Runtime from '@messageformat/runtime'; import * as Formatters from '@messageformat/runtime/lib/formatters'; import { identifier, property } from 'safe-identifier'; @@ -122,8 +122,9 @@ export default class Compiler { type === 'select' || (type === 'plural' && cardinals.includes('other')) || (type === 'selectordinal' && ordinals.includes('other')) - ) + ) { throw new Error(`No 'other' form found in ${JSON.stringify(token)}`); + } } return `{ ${r.join(', ')} }`; } @@ -143,7 +144,9 @@ export default class Compiler { if ('arg' in token) { this.arguments.push(token.arg); args = [property('d', token.arg)]; - } else args = []; + } else { + args = []; + } switch (token.type) { case 'argument': return this.options.biDiSupport @@ -224,8 +227,9 @@ export default class Compiler { } runtimeIncludes(key: string, type: RuntimeType) { - if (identifier(key) !== key) + if (identifier(key) !== key) { throw new SyntaxError(`Reserved word used as ${type} identifier: ${key}`); + } const prev = this.runtime[key]; if (!prev || prev.type === type) return prev; throw new TypeError( @@ -279,23 +283,29 @@ export default class Compiler { if (argShape === 'options') { let value = ''; for (const tok of param) { - if (tok.type === 'content') value += tok.value; - else + if (tok.type === 'content') { + value += tok.value; + } else { throw new SyntaxError( `Expected literal options for ${key} formatter` ); + } } const options: Record = {}; for (const pair of value.split(',')) { const keyEnd = pair.indexOf(':'); - if (keyEnd === -1) options[pair.trim()] = null; - else { + if (keyEnd === -1) { + options[pair.trim()] = null; + } else { const k = pair.substring(0, keyEnd).trim(); const v = pair.substring(keyEnd + 1).trim(); - if (v === 'true') options[k] = true; - else if (v === 'false') options[k] = false; - else if (v === 'null') options[k] = null; - else { + if (v === 'true') { + options[k] = true; + } else if (v === 'false') { + options[k] = false; + } else if (v === 'null') { + options[k] = null; + } else { const n = Number(v); options[k] = Number.isFinite(n) ? n : v; } diff --git a/packages/core/src/messageformat.test.ts b/packages/core/src/messageformat.test.ts index f6817d96..03ba167e 100644 --- a/packages/core/src/messageformat.test.ts +++ b/packages/core/src/messageformat.test.ts @@ -183,17 +183,21 @@ for (const [title, cases] of Object.entries( let name = src; if (locale || options) { const opt = [locale || 'en']; - for (const [key, value] of Object.entries(options || {})) + for (const [key, value] of Object.entries(options || {})) { opt.push(`${key}: ${value}`); + } name = `[${opt.join(', ')}] ${src}`; } desc(name, () => { for (const [param, res] of exp) { const strParam = []; - if (param && typeof param === 'object') - for (const [key, value] of Object.entries(param)) + if (param && typeof param === 'object') { + for (const [key, value] of Object.entries(param)) { strParam.push(`${key}: ${value}`); - else strParam.push(String(param)); + } + } else { + strParam.push(String(param)); + } test(strParam.join(', '), () => { const mf = new MessageFormat(locale || 'en', options); const msg = mf.compile(src); diff --git a/packages/core/src/messageformat.ts b/packages/core/src/messageformat.ts index e2f04e9f..5c46246b 100644 --- a/packages/core/src/messageformat.ts +++ b/packages/core/src/messageformat.ts @@ -1,10 +1,10 @@ import Compiler from './compiler'; import { + PluralFunction, + PluralObject, getAllPlurals, getPlural, - hasPlural, - PluralFunction, - PluralObject + hasPlural } from './plurals'; export { PluralFunction }; diff --git a/packages/core/src/plurals.ts b/packages/core/src/plurals.ts index 6a86cfd0..d2016c77 100644 --- a/packages/core/src/plurals.ts +++ b/packages/core/src/plurals.ts @@ -4,8 +4,9 @@ import * as Plurals from 'make-plural/plurals'; import { identifier } from 'safe-identifier'; function normalize(locale: string) { - if (typeof locale !== 'string' || locale.length < 2) + if (typeof locale !== 'string' || locale.length < 2) { throw new RangeError(`Invalid language tag: ${locale}`); + } // The only locale for which anything but the primary subtag matters is // Portuguese as spoken in Portugal. diff --git a/packages/date-skeleton/src/get-date-formatter.test.ts b/packages/date-skeleton/src/get-date-formatter.test.ts index f8677377..4ea57f74 100644 --- a/packages/date-skeleton/src/get-date-formatter.test.ts +++ b/packages/date-skeleton/src/get-date-formatter.test.ts @@ -80,7 +80,9 @@ describe('Examples', () => { if (errors) { const messages = onError.mock.calls.map(args => args[0].message); expect(messages).toMatchObject(errors); - } else expect(onError).not.toHaveBeenCalled(); + } else { + expect(onError).not.toHaveBeenCalled(); + } // function from tokens const tokens = parseDateTokens(src); diff --git a/packages/date-skeleton/src/get-date-formatter.ts b/packages/date-skeleton/src/get-date-formatter.ts index 467994cf..a099c7a4 100644 --- a/packages/date-skeleton/src/get-date-formatter.ts +++ b/packages/date-skeleton/src/get-date-formatter.ts @@ -1,5 +1,5 @@ -import { getDateFormatOptions, DateFormatError } from './options.js'; -import { parseDateTokens, DateToken } from './tokens.js'; +import { DateFormatError, getDateFormatOptions } from './options.js'; +import { DateToken, parseDateTokens } from './tokens.js'; /** * Returns a date formatter function for the given locales and date skeleton diff --git a/packages/date-skeleton/src/options.ts b/packages/date-skeleton/src/options.ts index 536bd0f1..6d553021 100644 --- a/packages/date-skeleton/src/options.ts +++ b/packages/date-skeleton/src/options.ts @@ -72,8 +72,9 @@ function monthStyle(token: DateToken, onError: ErrorHandler) { function dayStyle(token: DateToken, onError: ErrorHandler) { const { char, desc, width } = token; - if (char === 'd') return numeric(width); - else { + if (char === 'd') { + return numeric(width); + } else { onError(`${desc} is not supported`); return undefined; } diff --git a/packages/mf2-fluent/src/fluent-to-message.ts b/packages/mf2-fluent/src/fluent-to-message.ts index 68abc65f..e4c861a3 100644 --- a/packages/mf2-fluent/src/fluent-to-message.ts +++ b/packages/mf2-fluent/src/fluent-to-message.ts @@ -39,12 +39,14 @@ function findSelectArgs(pattern: Fluent.Pattern): SelectArg[] { if (prev) for (const key of arg.keys) prev.keys.push(key); else args.push(arg); }; - for (const el of pattern.elements) + for (const el of pattern.elements) { if (el.type === 'Placeable' && el.expression.type === 'SelectExpression') { add(asSelectArg(el.expression)); - for (const v of el.expression.variants) + for (const v of el.expression.variants) { for (const arg of findSelectArgs(v.value)) add(arg); + } } + } return args; } @@ -193,10 +195,13 @@ export function fluentToMessage( if (typeof b === 'number') return 1; return 0; }); - if (i === 0) keys = kk.map(key => [key]); - else - for (let i = keys.length - 1; i >= 0; --i) + if (i === 0) { + keys = kk.map(key => [key]); + } else { + for (let i = keys.length - 1; i >= 0; --i) { keys.splice(i, 1, ...kk.map(key => [...keys[i], key])); + } + } } const variants: Variant[] = keys.map(key => ({ keys: key.map((k, i) => @@ -243,7 +248,9 @@ export function fluentToMessage( const part = elementToPart(el); if (typeof vp[i] === 'string' && typeof part === 'string') { vp[i] += part; - } else vp.push(part); + } else { + vp.push(part); + } } } } diff --git a/packages/mf2-fluent/src/fluent-to-resource.ts b/packages/mf2-fluent/src/fluent-to-resource.ts index 036a2d35..01b4a06f 100644 --- a/packages/mf2-fluent/src/fluent-to-resource.ts +++ b/packages/mf2-fluent/src/fluent-to-resource.ts @@ -85,8 +85,9 @@ export function fluentToResourceData(source: string | Fluent.Resource): { } group.set('', entry); } - for (const attr of msg.attributes) + for (const attr of msg.attributes) { group.set(attr.id.name, fluentToMessage(attr.value)); + } data.set(id, group); break; } diff --git a/packages/mf2-fluent/src/message-to-fluent.ts b/packages/mf2-fluent/src/message-to-fluent.ts index 6e5ee239..86013867 100644 --- a/packages/mf2-fluent/src/message-to-fluent.ts +++ b/packages/mf2-fluent/src/message-to-fluent.ts @@ -4,15 +4,15 @@ import { Declaration, Expression, FunctionAnnotation, - isCatchallKey, - isLiteral, - isPatternMessage, - isSelectMessage, Literal, Message, Pattern, VariableRef, - Variant + Variant, + isCatchallKey, + isLiteral, + isPatternMessage, + isSelectMessage } from 'messageformat'; /** diff --git a/packages/mf2-icu-mf1/src/functions.ts b/packages/mf2-icu-mf1/src/functions.ts index c19b2326..2da6d494 100644 --- a/packages/mf2-icu-mf1/src/functions.ts +++ b/packages/mf2-icu-mf1/src/functions.ts @@ -1,9 +1,9 @@ import { - datetime, type MessageDateTime, type MessageFunctionContext, type MessageNumber, - type MessageValue + type MessageValue, + datetime } from 'messageformat'; function getParam(options: Record) { diff --git a/packages/mf2-icu-mf1/src/mf1-to-message-data.ts b/packages/mf2-icu-mf1/src/mf1-to-message-data.ts index 9d58b78a..2e88045d 100644 --- a/packages/mf2-icu-mf1/src/mf1-to-message-data.ts +++ b/packages/mf2-icu-mf1/src/mf1-to-message-data.ts @@ -41,12 +41,14 @@ function findSelectArgs(tokens: AST.Token[]): SelectArg[] { if (prev) for (const key of arg.keys) prev.keys.push(key); else args.push(arg); }; - for (const token of tokens) + for (const token of tokens) { if (isAstSelect(token)) { add(asSelectArg(token)); - for (const c of token.cases) + for (const c of token.cases) { for (const arg of findSelectArgs(c.tokens)) add(arg); + } } + } return args; } @@ -90,13 +92,14 @@ function tokenToPart( type: 'function', name: 'number' }; - if (pluralOffset) + if (pluralOffset) { annotation.options = [ { name: 'pluralOffset', value: { type: 'literal', value: String(pluralOffset) } } ]; + } return { type: 'expression', arg: { type: 'variable', name: pluralArg }, @@ -115,12 +118,13 @@ function argToExpression({ type }: SelectArg): Expression { const arg: VariableRef = { type: 'variable', name: selName }; - if (type === 'select') + if (type === 'select') { return { type: 'expression', arg, annotation: { type: 'function', name: 'string' } }; + } const options: Option[] = []; if (pluralOffset) { @@ -157,12 +161,13 @@ function argToExpression({ */ export function mf1ToMessageData(ast: AST.Token[]): Message { const args = findSelectArgs(ast); - if (args.length === 0) + if (args.length === 0) { return { type: 'message', declarations: [], pattern: { body: ast.map(token => tokenToPart(token, null, null)) } }; + } // First determine the keys for all cases, with empty values let keys: (string | number)[][] = []; @@ -173,10 +178,13 @@ export function mf1ToMessageData(ast: AST.Token[]): Message { if (typeof b === 'number' || a === 'other') return 1; return 0; }); - if (i === 0) keys = kk.map(key => [key]); - else - for (let i = keys.length - 1; i >= 0; --i) + if (i === 0) { + keys = kk.map(key => [key]); + } else { + for (let i = keys.length - 1; i >= 0; --i) { keys.splice(i, 1, ...kk.map(key => [...keys[i], key])); + } + } } const variants: Variant[] = keys.map(key => ({ keys: key.map(k => @@ -224,7 +232,9 @@ export function mf1ToMessageData(ast: AST.Token[]): Message { const part = tokenToPart(token, pluralArg, pluralOffset); if (typeof vp[i] === 'string' && typeof part === 'string') { vp[i] += part; - } else vp.push(part); + } else { + vp.push(part); + } } } } diff --git a/packages/mf2-icu-mf1/src/mf1-to-message.ts b/packages/mf2-icu-mf1/src/mf1-to-message.ts index 48033cba..ffa6f324 100644 --- a/packages/mf2-icu-mf1/src/mf1-to-message.ts +++ b/packages/mf2-icu-mf1/src/mf1-to-message.ts @@ -1,4 +1,4 @@ -import { parse, Token } from '@messageformat/parser'; +import { Token, parse } from '@messageformat/parser'; import { Message, MessageFormat, MessageFormatOptions } from 'messageformat'; import { mf1ToMessageData } from './mf1-to-message-data'; import { getMF1Functions } from './functions'; diff --git a/packages/mf2-icu-mf1/src/mf1.test.ts b/packages/mf2-icu-mf1/src/mf1.test.ts index a9a4fce5..780f8af9 100644 --- a/packages/mf2-icu-mf1/src/mf1.test.ts +++ b/packages/mf2-icu-mf1/src/mf1.test.ts @@ -357,8 +357,9 @@ for (const [title, cases] of Object.entries(testCases)) { let name = src; if (locale || options) { const opt = [locale]; - for (const [key, value] of Object.entries(options || {})) + for (const [key, value] of Object.entries(options || {})) { opt.push(`${key}: ${value}`); + } name = `[${opt.join(', ')}] ${src}`; } @@ -366,10 +367,13 @@ for (const [title, cases] of Object.entries(testCases)) { _describe(name, () => { for (const [param, res] of exp) { const strParam = []; - if (param && typeof param === 'object') - for (const [key, value] of Object.entries(param)) + if (param && typeof param === 'object') { + for (const [key, value] of Object.entries(param)) { strParam.push(`${key}: ${value}`); - else strParam.push(String(param)); + } + } else { + strParam.push(String(param)); + } test(strParam.join(', '), () => { const data = mf1ToMessageData(parse(src)); diff --git a/packages/mf2-messageformat/src/__fixtures/test-name.ts b/packages/mf2-messageformat/src/__fixtures/test-name.ts index ec4c35b0..c63adba4 100644 --- a/packages/mf2-messageformat/src/__fixtures/test-name.ts +++ b/packages/mf2-messageformat/src/__fixtures/test-name.ts @@ -9,9 +9,10 @@ export function testName({ }) { let name = src; if (locale) name += ` [${locale}]`; - if (params) + if (params) { name += ` / {${Object.entries(params) .map(p => ` ${p[0]}: ${p[1]}`) .join()} }`; + } return name.replace(/ *\n */g, ' '); } diff --git a/packages/mf2-messageformat/src/expression/index.ts b/packages/mf2-messageformat/src/expression/index.ts index 4137d448..822a3a0c 100644 --- a/packages/mf2-messageformat/src/expression/index.ts +++ b/packages/mf2-messageformat/src/expression/index.ts @@ -2,15 +2,15 @@ import type { Context } from '../format-context'; import type { MessageValue } from '../runtime'; import { - resolveFunctionAnnotation, - FunctionAnnotation + FunctionAnnotation, + resolveFunctionAnnotation } from './function-annotation'; import { Literal, resolveLiteral } from './literal'; import { - resolveUnsupportedAnnotation, - UnsupportedAnnotation + UnsupportedAnnotation, + resolveUnsupportedAnnotation } from './unsupported-annotation'; -import { resolveVariableRef, VariableRef } from './variable-ref'; +import { VariableRef, resolveVariableRef } from './variable-ref'; export { isFunctionAnnotation, diff --git a/packages/mf2-messageformat/src/expression/value.ts b/packages/mf2-messageformat/src/expression/value.ts index 3a2246ed..774ba7ce 100644 --- a/packages/mf2-messageformat/src/expression/value.ts +++ b/packages/mf2-messageformat/src/expression/value.ts @@ -1,6 +1,6 @@ import type { Context } from '../format-context'; import type { Literal } from './literal'; -import { lookupVariableRef, VariableRef } from './variable-ref'; +import { VariableRef, lookupVariableRef } from './variable-ref'; /** @internal */ export function resolveValue( diff --git a/packages/mf2-messageformat/src/expression/variable-ref.ts b/packages/mf2-messageformat/src/expression/variable-ref.ts index bfa7181c..b2dafdef 100644 --- a/packages/mf2-messageformat/src/expression/variable-ref.ts +++ b/packages/mf2-messageformat/src/expression/variable-ref.ts @@ -6,7 +6,7 @@ import { fallback, unknown } from '../runtime/index.js'; -import { resolveExpression, type Expression } from './index.js'; +import { type Expression, resolveExpression } from './index.js'; /** * The value of a VariableRef is defined by the current Scope. diff --git a/packages/mf2-messageformat/src/mf2-features.test.ts b/packages/mf2-messageformat/src/mf2-features.test.ts index 23b45619..a18ba39e 100644 --- a/packages/mf2-messageformat/src/mf2-features.test.ts +++ b/packages/mf2-messageformat/src/mf2-features.test.ts @@ -7,11 +7,11 @@ import { parse } from '@messageformat/parser'; import { source } from '@messageformat/test-utils'; import { - MessageValue, MessageFormat, MessageFunctionContext, - SelectMessage, - MessagePart + MessagePart, + MessageValue, + SelectMessage } from './index'; describe('Plural Range Selectors & Range Formatters (unicode-org/message-format-wg#125)', () => { diff --git a/packages/mf2-messageformat/src/stringifier/message.ts b/packages/mf2-messageformat/src/stringifier/message.ts index 3b77ad57..8484d49a 100644 --- a/packages/mf2-messageformat/src/stringifier/message.ts +++ b/packages/mf2-messageformat/src/stringifier/message.ts @@ -50,12 +50,13 @@ function stringifyDeclaration(decl: Declaration) { case 'unsupported-statement': { const parts = [`.${decl.keyword}`]; if (decl.body) parts.push(decl.body); - for (const exp of decl.expressions) + for (const exp of decl.expressions) { parts.push( exp.type === 'expression' ? stringifyExpression(exp) : stringifyMarkup(exp) ); + } return parts.join(' '); } } diff --git a/packages/mf2-xliff/src/mf2xliff.ts b/packages/mf2-xliff/src/mf2xliff.ts index 50067560..52c2d18c 100644 --- a/packages/mf2-xliff/src/mf2xliff.ts +++ b/packages/mf2-xliff/src/mf2xliff.ts @@ -1,11 +1,11 @@ import deepEqual from 'fast-deep-equal'; import { + MessageFormat, isFunctionAnnotation, isLiteral, isMessage, isSelectMessage, - isVariableRef, - MessageFormat + isVariableRef } from 'messageformat'; import type * as MF from 'messageformat'; import type { MessageFormatInfo, MessageResourceData } from './index'; @@ -27,9 +27,11 @@ export function mf2xliff( 'xmlns:mf': 'http://www.unicode.org/ns/2021/messageformat/2.0/not-real-yet' // FIXME }; attributes.srcLang = source.locale; - if (target instanceof MessageFormat) + if (target instanceof MessageFormat) { throw new Error('source and target must be of the same type'); - else if (target) attributes.trgLang = target.locale; + } else if (target) { + attributes.trgLang = target.locale; + } const elements: (X.Unit | X.Group)[] = []; for (const [key, srcMsg] of source.data) { @@ -69,9 +71,11 @@ function resolveEntry( if (isMessage(srcMsg)) { if (trgMsg) { if (!isMessage(trgMsg)) throw new Error(mismatch(key)); - if (isSelectMessage(srcMsg) || isSelectMessage(trgMsg)) + if (isSelectMessage(srcMsg) || isSelectMessage(trgMsg)) { return resolveSelect(key, srcMsg, trgMsg); - else return resolvePattern(key, srcMsg.pattern, trgMsg.pattern); + } else { + return resolvePattern(key, srcMsg.pattern, trgMsg.pattern); + } } else { return isSelectMessage(srcMsg) ? resolveSelect(key, srcMsg, undefined) @@ -106,10 +110,11 @@ function resolveSelect( }; } } else { - if (!trgSel || !isSelectMessage(trgSel)) + if (!trgSel || !isSelectMessage(trgSel)) { throw new Error( `At least one of source & target at ${key.join('.')} must be a select` ); + } srcSel = { type: 'select', declarations: [], @@ -142,8 +147,9 @@ function resolveSelect( const trgSelMap: number[] = []; for (const sel of trgSel.selectors) { const prevIdx = srcSel.selectors.findIndex(prev => deepEqual(sel, prev)); - if (prevIdx !== -1) trgSelMap.push(prevIdx); - else { + if (prevIdx !== -1) { + trgSelMap.push(prevIdx); + } else { const id = nextId(); select.push({ id, keys: [] }); trgSelMap.push(select.length - 1); @@ -156,11 +162,13 @@ function resolveSelect( const { keys } = select[i]; const sk = key.type === '*' ? '*' : key.value; if (keys.includes(sk)) return; - if (sk === '*') keys.push(sk); - else if (Number.isFinite(Number(sk))) { + if (sk === '*') { + keys.push(sk); + } else if (Number.isFinite(Number(sk))) { let pos = 0; - while (keys[pos] !== '*' && Number.isFinite(Number(keys[pos]))) + while (keys[pos] !== '*' && Number.isFinite(Number(keys[pos]))) { pos += 1; + } keys.splice(pos, 0, sk); } else { let pos = keys.length; @@ -168,11 +176,14 @@ function resolveSelect( keys.splice(pos, 0, sk); } }; - for (const c of srcSel.variants) + for (const c of srcSel.variants) { for (let i = 0; i < c.keys.length; ++i) addSorted(i, c.keys[i]); - for (const c of trgSel.variants) - for (let i = 0; i < c.keys.length; ++i) + } + for (const c of trgSel.variants) { + for (let i = 0; i < c.keys.length; ++i) { addSorted(trgSelMap[i], c.keys[i]); + } + } // Add a separate entry for each combined case // TODO: Collapse duplicates to default value only, where possible @@ -186,8 +197,9 @@ function resolveSelect( return k.type === '*' || k.value === sk[ti]; }) ); - if (!srcCase || !trgCase) + if (!srcCase || !trgCase) { throw new Error(`Case ${sk} not found‽ src:${srcCase} trg:${trgCase}`); + } elements.push( resolvePattern([...key, sk.join(' ')], srcCase.value, trgCase.value) ); @@ -208,8 +220,9 @@ function everyKey(select: { keys: string[] }[]): Iterable { let ptr: number[] | null = null; const max = select.map(s => s.keys.length - 1); function next(): IteratorResult { - if (!ptr) ptr = new Array(select.length).fill(0); - else { + if (!ptr) { + ptr = new Array(select.length).fill(0); + } else { for (let i = ptr.length - 1; i >= 0; --i) { if (ptr[i] < max[i]) { ptr[i] += 1; @@ -310,7 +323,9 @@ function resolvePattern( elements: refs }; elements = [mf, segment]; - } else elements = [segment]; + } else { + elements = [segment]; + } return { type: 'element', name: 'unit', attributes, elements }; } diff --git a/packages/mf2-xliff/src/xliff.ts b/packages/mf2-xliff/src/xliff.ts index 6823b6df..451c617f 100644 --- a/packages/mf2-xliff/src/xliff.ts +++ b/packages/mf2-xliff/src/xliff.ts @@ -1,4 +1,4 @@ -import { js2xml, Options, xml2js } from 'xml-js'; +import { Options, js2xml, xml2js } from 'xml-js'; import type { Xliff, XliffDoc } from './xliff-spec'; export type ParseOptions = Options.XML2JS; @@ -14,8 +14,9 @@ export function parse(src: string, options?: ParseOptions) { !doc.elements || doc.elements.length !== 1 || doc.elements[0].name !== 'xliff' - ) + ) { throw new Error('Could not find element in XML'); + } return doc as XliffDoc; } diff --git a/packages/mf2-xliff/src/xliff2mf.ts b/packages/mf2-xliff/src/xliff2mf.ts index 7e42d3c3..5dbcf75e 100644 --- a/packages/mf2-xliff/src/xliff2mf.ts +++ b/packages/mf2-xliff/src/xliff2mf.ts @@ -110,10 +110,11 @@ function resolveSelect( { attributes, elements }: X.Group, st: 'source' | 'target' ): MF.Message { - if (!elements || elements.length === 0) + if (!elements || elements.length === 0) { throw new Error( `Select ${prettyElement('group', attributes.id)} cannot be empty` ); + } let mf: X.MessageFormat | null = null; const variants: MF.Variant[] = []; for (const el of elements) { @@ -259,10 +260,11 @@ function resolvePlaceholder( const { name } = el; const ref = el.attributes?.['mf:ref']; if (!ref) throw new Error(`Unsupported <${name}> without mf:ref attribute`); - if (!mf) + if (!mf) { throw new Error( `Inline <${name}> requires a preceding in the same ` ); + } const res = mf.elements.find(el => el.attributes?.id === ref); switch (res?.name) { case 'mf:expression': diff --git a/packages/number-skeleton/src/get-pattern-formatter.test.ts b/packages/number-skeleton/src/get-pattern-formatter.test.ts index 08240939..d093c3d6 100644 --- a/packages/number-skeleton/src/get-pattern-formatter.test.ts +++ b/packages/number-skeleton/src/get-pattern-formatter.test.ts @@ -76,10 +76,9 @@ const tests: { for (const [testSet, cases] of Object.entries(tests)) { describe(testSet, () => { - for (const [ - src, - { value, lc, cur: cur, exp, errors = [] } - ] of Object.entries(cases)) { + for (const [src, { value, lc, cur, exp, errors = [] }] of Object.entries( + cases + )) { test(src, () => { const cb = jest.fn(); diff --git a/packages/number-skeleton/src/get-skeleton-formatter.test.ts b/packages/number-skeleton/src/get-skeleton-formatter.test.ts index 16f764be..a90f40f1 100644 --- a/packages/number-skeleton/src/get-skeleton-formatter.test.ts +++ b/packages/number-skeleton/src/get-skeleton-formatter.test.ts @@ -123,7 +123,9 @@ for (const [testSet, cases] of Object.entries(tests)) { expect(cb.mock.calls).toMatchObject( errors.map(error => [{ ...base, ...error }]) ); - } else expect(cb).not.toHaveBeenCalled(); + } else { + expect(cb).not.toHaveBeenCalled(); + } expect(fmt(value)).toBe(expected); // function from skeleton diff --git a/packages/number-skeleton/src/numberformat/options.test.ts b/packages/number-skeleton/src/numberformat/options.test.ts index 3a8c645e..325d520b 100644 --- a/packages/number-skeleton/src/numberformat/options.test.ts +++ b/packages/number-skeleton/src/numberformat/options.test.ts @@ -303,7 +303,9 @@ for (const [testSet, cases] of Object.entries(tests)) { new UnsupportedError(stem, source) ]); expect(cb.mock.calls).toEqual(errors); - } else expect(cb).not.toHaveBeenCalled(); + } else { + expect(cb).not.toHaveBeenCalled(); + } }); } }); diff --git a/packages/number-skeleton/src/numberformat/options.ts b/packages/number-skeleton/src/numberformat/options.ts index 7a6f4e98..990073f1 100644 --- a/packages/number-skeleton/src/numberformat/options.ts +++ b/packages/number-skeleton/src/numberformat/options.ts @@ -195,8 +195,9 @@ export function getNumberFormatOptions( if ( (expDigits && expDigits > 1) || (expSign && expSign !== 'sign-auto') - ) + ) { fail(style, source); + } break; } } diff --git a/packages/number-skeleton/src/pattern-parser/number-as-skeleton.ts b/packages/number-skeleton/src/pattern-parser/number-as-skeleton.ts index e674b783..5bc0febb 100644 --- a/packages/number-skeleton/src/pattern-parser/number-as-skeleton.ts +++ b/packages/number-skeleton/src/pattern-parser/number-as-skeleton.ts @@ -1,6 +1,6 @@ import { - NumberFormatError, MaskedValueError, + NumberFormatError, PatternError } from '../errors.js'; import { Skeleton } from '../types/skeleton.js'; @@ -49,8 +49,9 @@ export function parseNumberAsSkeleton( } case '@': { - if (res.precision) + if (res.precision) { onError(new MaskedValueError('precision', res.precision)); + } res.precision = { style: 'precision-fraction', minSignificant: token.min, @@ -72,8 +73,9 @@ export function parseNumberAsSkeleton( break; case 'E': { - if (hasExponent) + if (hasExponent) { onError(new MaskedValueError('exponent', res.notation)); + } if (hasGroups) { const msg = 'Exponential patterns may not contain grouping separators'; @@ -114,8 +116,9 @@ export function parseNumberAsSkeleton( } else { const dc = intDigits.length + fracDigits.length; if (decimalPos === -1) { - if (dc > 0) + if (dc > 0) { res.precision = { style: 'precision-fraction', maxSignificant: dc }; + } } else { res.precision = { style: 'precision-fraction', diff --git a/packages/number-skeleton/src/skeleton-parser/options.ts b/packages/number-skeleton/src/skeleton-parser/options.ts index 84609603..3d20f1a8 100644 --- a/packages/number-skeleton/src/skeleton-parser/options.ts +++ b/packages/number-skeleton/src/skeleton-parser/options.ts @@ -1,7 +1,7 @@ import { - NumberFormatError, BadOptionError, MissingOptionError, + NumberFormatError, TooManyOptionsError } from '../errors.js'; diff --git a/packages/number-skeleton/src/skeleton-parser/parse-precision-blueprint.ts b/packages/number-skeleton/src/skeleton-parser/parse-precision-blueprint.ts index 72dcfe8b..d523db97 100644 --- a/packages/number-skeleton/src/skeleton-parser/parse-precision-blueprint.ts +++ b/packages/number-skeleton/src/skeleton-parser/parse-precision-blueprint.ts @@ -1,6 +1,6 @@ import { - NumberFormatError, BadOptionError, + NumberFormatError, TooManyOptionsError } from '../errors.js'; import { Skeleton } from '../types/skeleton.js'; @@ -44,7 +44,9 @@ export function parsePrecisionBlueprint( res.source = `${stem}/${option}`; res.minSignificant = sd.min; if (sd.max != null) res.maxSignificant = sd.max; - } else if (option) onError(new BadOptionError(stem, option)); + } else if (option) { + onError(new BadOptionError(stem, option)); + } return res; } diff --git a/packages/number-skeleton/src/skeleton-parser/skeleton-parser.test.ts b/packages/number-skeleton/src/skeleton-parser/skeleton-parser.test.ts index 1242a4eb..5a03ae25 100644 --- a/packages/number-skeleton/src/skeleton-parser/skeleton-parser.test.ts +++ b/packages/number-skeleton/src/skeleton-parser/skeleton-parser.test.ts @@ -230,8 +230,9 @@ describe('errors', () => { 'precision-increment', 'scale', '.00' - ]) + ]) { cases[`${stem}/foo`] = { code: 'BAD_OPTION', stem, option: 'foo' }; + } for (const [src, expected] of Object.entries(cases)) { test(src, () => { diff --git a/packages/number-skeleton/src/skeleton-parser/token-parser.ts b/packages/number-skeleton/src/skeleton-parser/token-parser.ts index dbbda8f9..66893db5 100644 --- a/packages/number-skeleton/src/skeleton-parser/token-parser.ts +++ b/packages/number-skeleton/src/skeleton-parser/token-parser.ts @@ -1,10 +1,10 @@ import { - NumberFormatError, BadOptionError, BadStemError, - MaskedValueError + MaskedValueError, + NumberFormatError } from '../errors.js'; -import { isNumberingSystem, Skeleton } from '../types/skeleton.js'; +import { Skeleton, isNumberingSystem } from '../types/skeleton.js'; import { isUnit } from '../types/unit.js'; import { validOptions } from './options.js'; import { parsePrecisionBlueprint } from './parse-precision-blueprint.js'; @@ -57,8 +57,9 @@ export class TokenParser { expSign = opt; break; default: - if (/^\+e+$/.test(opt)) expDigits = opt.length - 1; - else { + if (/^\+e+$/.test(opt)) { + expDigits = opt.length - 1; + } else { this.badOption(stem, opt); } } @@ -87,13 +88,17 @@ export class TokenParser { if (/^[A-Z]{3}$/.test(option)) { this.assertEmpty('unit'); res.unit = { style: stem, currency: option }; - } else this.badOption(stem, option); + } else { + this.badOption(stem, option); + } break; case 'measure-unit': { if (isUnit(option)) { this.assertEmpty('unit'); res.unit = { style: stem, unit: option }; - } else this.badOption(stem, option); + } else { + this.badOption(stem, option); + } break; } @@ -102,7 +107,9 @@ export class TokenParser { if (isUnit(option)) { this.assertEmpty('unitPer'); res.unitPer = option; - } else this.badOption(stem, option); + } else { + this.badOption(stem, option); + } break; } @@ -136,7 +143,9 @@ export class TokenParser { if (increment > 0) { this.assertEmpty('precision'); res.precision = { style: stem, increment }; - } else this.badOption(stem, option); + } else { + this.badOption(stem, option); + } break; } @@ -170,7 +179,9 @@ export class TokenParser { min: m[1].length, max: m[0].length }; - } else this.badOption(stem, option); + } else { + this.badOption(stem, option); + } } break; } @@ -181,7 +192,9 @@ export class TokenParser { if (scale > 0) { this.assertEmpty('scale'); res.scale = scale; - } else this.badOption(stem, option); + } else { + this.badOption(stem, option); + } break; } @@ -204,7 +217,9 @@ export class TokenParser { if (isNumberingSystem(option)) { this.assertEmpty('numberingSystem'); res.numberingSystem = option; - } else this.badOption(stem, option); + } else { + this.badOption(stem, option); + } break; } diff --git a/packages/parser/src/parser.test.ts b/packages/parser/src/parser.test.ts index 8fc9fd6a..967e9dbf 100644 --- a/packages/parser/src/parser.test.ts +++ b/packages/parser/src/parser.test.ts @@ -1,6 +1,6 @@ /* eslint-env jest */ -import { parse, ParseOptions, Select } from './parser.js'; +import { ParseOptions, Select, parse } from './parser.js'; function run(shape: Record) { for (const [src, value] of Object.entries(shape)) { @@ -234,8 +234,9 @@ describe('Plurals', () => { '{NUM,plural,one{1}other{2}}', '{\nNUM, \nplural,\n one\n\n{1}\n other {2}\n\n\n}', '{\tNUM\t,\t\t\r plural\t\n, \tone\n{1} other\t\n{2}\n\n\n}' - ]) + ]) { expect(parse(src)).toMatchObject(exp); + } }); describe('Plural offsets', () => { @@ -360,10 +361,11 @@ describe('Ordinals', () => { describe('Functions', function () { it('should allow upper-case type, except for built-ins', function () { - for (const date of ['date', 'Date', '9ate']) + for (const date of ['date', 'Date', '9ate']) { expect(parse(`{var,${date}}`)).toMatchObject([ { type: 'function', arg: 'var', key: date } ]); + } expect(() => parse('{var,Select}')).toThrow( /Invalid type identifier: Select/ ); diff --git a/packages/parser/src/parser.ts b/packages/parser/src/parser.ts index 4ac9de57..2acbac85 100644 --- a/packages/parser/src/parser.ts +++ b/packages/parser/src/parser.ts @@ -267,8 +267,9 @@ class Parser { checkSelectKey(lt: LexerToken, type: Select['type'], key: string) { if (key[0] === '=') { - if (type === 'select') + if (type === 'select') { throw new ParseError(lt, `The case ${key} is not valid with select`); + } } else if (type !== 'select') { const keys = type === 'plural' ? this.cardinalKeys : this.ordinalKeys; if (this.strictPluralKeys && keys.length > 0 && !keys.includes(key)) { @@ -290,10 +291,12 @@ class Parser { for (const lt of this.lexer) { switch (lt.type) { case 'offset': - if (type === 'select') + if (type === 'select') { throw new ParseError(lt, 'Unexpected plural offset for select'); - if (sel.cases.length > 0) + } + if (sel.cases.length > 0) { throw new ParseError(lt, 'Plural offset must be set before cases'); + } sel.pluralOffset = Number(lt.value); ctx.text += lt.text; ctx.lineBreaks += lt.lineBreaks; @@ -341,14 +344,16 @@ class Parser { const end = this.lexer.next(); if (!end) throw new ParseError(null, 'Unexpected message end'); /* istanbul ignore if: never happens */ - if (end.type !== 'end') + if (end.type !== 'end') { throw new ParseError(end, `Unexpected lexer token: ${end.type}`); + } ctx.text += end.text; - if (isSelectType(argType.value.toLowerCase())) + if (isSelectType(argType.value.toLowerCase())) { throw new ParseError( argType, `Invalid type identifier: ${argType.value}` ); + } return { type: 'function', arg: lt.value, @@ -362,8 +367,9 @@ class Parser { throw new ParseError(argType, msg); } let param = this.parseBody(this.strict ? false : inPlural); - if (this.strict && param.length > 0) + if (this.strict && param.length > 0) { param = strictArgStyleParam(lt, param); + } return { type: 'function', arg: lt.value, @@ -374,13 +380,14 @@ class Parser { } case 'select': /* istanbul ignore else: never happens */ - if (isSelectType(argType.value)) + if (isSelectType(argType.value)) { return this.parseSelect(lt, inPlural, ctx, argType.value); - else + } else { throw new ParseError( argType, `Unexpected select type ${argType.value}` ); + } /* istanbul ignore next: never happens */ default: throw new ParseError( diff --git a/packages/react/src/examples.test.tsx b/packages/react/src/examples.test.tsx index d917769a..85bde434 100644 --- a/packages/react/src/examples.test.tsx +++ b/packages/react/src/examples.test.tsx @@ -2,11 +2,11 @@ import React, { Component } from 'react'; import renderer from 'react-test-renderer'; import { - getMessage, - getMessageGetter, Message, MessageContext, MessageProvider, + getMessage, + getMessageGetter, useLocales, useMessage, useMessageGetter diff --git a/packages/react/src/message-provider.ts b/packages/react/src/message-provider.ts index 9942a8dd..8b675da2 100644 --- a/packages/react/src/message-provider.ts +++ b/packages/react/src/message-provider.ts @@ -1,6 +1,6 @@ -import { createElement, ReactNode, useContext, useMemo } from 'react'; +import { ReactNode, createElement, useContext, useMemo } from 'react'; import { MessageContext, MessageObject, defaultValue } from './message-context'; -import { MessageError, ErrorCode, errorMessages } from './message-error'; +import { ErrorCode, MessageError, errorMessages } from './message-error'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - https://github.com/microsoft/rushstack/issues/1050 @@ -71,9 +71,10 @@ function getOnError( if (onError === undefined) { // debug is deprecated, will be removed later - if (typeof debug === 'function') + if (typeof debug === 'function') { return (path: string[], code: ErrorCode) => debug(`${errorMessages[code]}: ${asId(path)}`); + } onError = debug; } diff --git a/packages/react/src/message.ts b/packages/react/src/message.ts index 70abf363..931c4c71 100644 --- a/packages/react/src/message.ts +++ b/packages/react/src/message.ts @@ -71,8 +71,9 @@ export function Message(props: MessageProps) { const msgParams = rest(props, ['children', 'id', 'locale', 'params']); let context = useContext(MessageContext); let fallback = false; - if (children && typeof children !== 'function') + if (children && typeof children !== 'function') { context = Object.assign({}, context, { onError: () => (fallback = true) }); + } const msg = getMessage(context, id, locale); if (fallback) return children; if (typeof children === 'function') return children(msg); @@ -82,8 +83,9 @@ export function Message(props: MessageProps) { case 'boolean': return String(msg); case 'object': - if (msg && !Array.isArray(msg)) + if (msg && !Array.isArray(msg)) { return context.onError ? context.onError(getPath(id), 'EBADMSG') : null; + } } return msg || null; } diff --git a/packages/react/src/use-message-getter.ts b/packages/react/src/use-message-getter.ts index d5ba5f50..91d7015d 100644 --- a/packages/react/src/use-message-getter.ts +++ b/packages/react/src/use-message-getter.ts @@ -1,5 +1,5 @@ import { useContext } from 'react'; -import { getMessageGetter, MessageGetterOptions } from './get-message'; +import { MessageGetterOptions, getMessageGetter } from './get-message'; import { MessageContext } from './message-context'; /** diff --git a/packages/rollup-plugin/src/index.ts b/packages/rollup-plugin/src/index.ts index eac3520f..1b138ea8 100644 --- a/packages/rollup-plugin/src/index.ts +++ b/packages/rollup-plugin/src/index.ts @@ -1,5 +1,5 @@ import { LoadResult } from 'rollup'; -import { createFilter, FilterPattern } from '@rollup/pluginutils'; +import { FilterPattern, createFilter } from '@rollup/pluginutils'; import { parse } from 'dot-properties'; import { readFile } from 'fs'; import MessageFormat, { MessageFormatOptions } from '@messageformat/core'; @@ -83,8 +83,9 @@ export function messageformat({ if (!id.endsWith('.properties') || !filter(id)) return null; return new Promise((resolve, reject) => readFile(id, (err, buffer) => { - if (err) reject(err); - else { + if (err) { + reject(err); + } else { const encoding = uv(buffer) ? 'utf8' : 'latin1'; resolve({ code: buffer.toString(encoding) }); } diff --git a/packages/runtime/src/runtime.ts b/packages/runtime/src/runtime.ts index 75c22db0..e2819b01 100644 --- a/packages/runtime/src/runtime.ts +++ b/packages/runtime/src/runtime.ts @@ -87,7 +87,9 @@ export function select(value: string, data: { [key: string]: unknown }) { * @param data The data object being checked */ export function reqArgs(keys: string[], data: { [key: string]: unknown }) { - for (let i = 0; i < keys.length; ++i) - if (!data || data[keys[i]] === undefined) + for (let i = 0; i < keys.length; ++i) { + if (!data || data[keys[i]] === undefined) { throw new Error(`Message requires argument '${keys[i]}'`); + } + } } diff --git a/packages/test-utils/src/index.ts b/packages/test-utils/src/index.ts index 1fee14e9..55756337 100644 --- a/packages/test-utils/src/index.ts +++ b/packages/test-utils/src/index.ts @@ -5,8 +5,9 @@ export function source( ) { // concatenate let res = strings[0]; - for (let i = 1; i < strings.length; ++i) + for (let i = 1; i < strings.length; ++i) { res += String(expressions[i]) + strings[i + 1]; + } // remove trailing whitespace + initial newline res = res.replace(/[^\S\n]+$/gm, '').replace(/^\n/, ''); diff --git a/packages/webpack-loader/index.js b/packages/webpack-loader/index.js index d3210104..d3234fb1 100644 --- a/packages/webpack-loader/index.js +++ b/packages/webpack-loader/index.js @@ -30,8 +30,9 @@ module.exports = function loadMessages(content) { ...mfOpt } = loaderUtils.getOptions(this) || {}; - if (!encoding || encoding === 'auto') + if (!encoding || encoding === 'auto') { encoding = uv(content) ? 'utf8' : 'latin1'; + } const input = content.toString(encoding); let messages = this.resourcePath.endsWith('.properties') @@ -44,8 +45,9 @@ module.exports = function loadMessages(content) { messages = cm.translations; } - if (typeof locale === 'string' && locale.indexOf(',') !== -1) + if (typeof locale === 'string' && locale.indexOf(',') !== -1) { locale = locale.split(','); + } if (Array.isArray(locale) && locale.length > 1) { const relPath = relative(process.cwd(), this.resourcePath); locale = localeFromResourcePath(relPath, locale); diff --git a/scripts/set-api-docs-frontmatter.js b/scripts/set-api-docs-frontmatter.js index ff791aa4..a5f17047 100644 --- a/scripts/set-api-docs-frontmatter.js +++ b/scripts/set-api-docs-frontmatter.js @@ -15,8 +15,9 @@ async function main() { const bc0 = src.indexOf('[Home](./index.md)'); const bc1 = src.indexOf('\n', bc0); const bc = src.substring(bc0, bc1).split(/ > /); - if (bc0 === -1 || bc1 === -1 || bc1 < bc0 || bc.length === 0) + if (bc0 === -1 || bc1 === -1 || bc1 < bc0 || bc.length === 0) { throw new Error(`Failed to parse breadcrumbs for ${fn}`); + } const navpath = bc.map(md => md.match(/^\[(.*)][^\]]*$/)[1]); diff --git a/test/browser/browserstack-fixtures.js b/test/browser/browserstack-fixtures.js index 534e8457..4adfc6a9 100644 --- a/test/browser/browserstack-fixtures.js +++ b/test/browser/browserstack-fixtures.js @@ -17,8 +17,12 @@ exports.mochaGlobalSetup = async function () { if (error.name === 'LocalError') { console.info(`Skipping BrowserStack Local start: ${error.message}`); resolve(); - } else reject(error); - } else resolve(); + } else { + reject(error); + } + } else { + resolve(); + } }); }); console.log('Starting browser tests...'); @@ -31,6 +35,8 @@ exports.mochaGlobalTeardown = async function () { if (this.bsl && this.bsl.isRunning()) { console.log('\nStopping BrowserStack Local...'); this.bsl.stop(resolve); - } else resolve(); + } else { + resolve(); + } }); }; diff --git a/test/browser/src/browser-tests.ts b/test/browser/src/browser-tests.ts index 5e7c1d86..30c1d57d 100644 --- a/test/browser/src/browser-tests.ts +++ b/test/browser/src/browser-tests.ts @@ -161,17 +161,21 @@ for (const [title, cases] of Object.entries( let name = src; if (locale || options) { const opt = [locale || 'en']; - for (const [key, value] of Object.entries(options || {})) + for (const [key, value] of Object.entries(options || {})) { opt.push(`${key}: ${value}`); + } name = `[${opt.join(', ')}] ${src}`; } describe(name, () => { for (const [param, res] of exp) { const strParam = []; - if (param && typeof param === 'object') - for (const [key, value] of Object.entries(param)) + if (param && typeof param === 'object') { + for (const [key, value] of Object.entries(param)) { strParam.push(`${key}: ${value}`); - else strParam.push(String(param)); + } + } else { + strParam.push(String(param)); + } it(strParam.join(', '), () => { const mf = new MessageFormat(locale || 'en', options); const msg = mf.compile(src); From 710e63d48dc43b17cd1fd919e5f9656d3757ccea Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 3 Jan 2024 02:10:16 +0200 Subject: [PATCH 17/44] chore: Drop husky & commitlint -- more trouble than they're worth --- commitlint.config.js | 10 - config/husky/commit-msg | 4 - package-lock.json | 1413 +-------------------------------------- package.json | 6 +- 4 files changed, 8 insertions(+), 1425 deletions(-) delete mode 100644 commitlint.config.js delete mode 100755 config/husky/commit-msg diff --git a/commitlint.config.js b/commitlint.config.js deleted file mode 100644 index 3d53cf24..00000000 --- a/commitlint.config.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - extends: ['@commitlint/config-conventional'], - rules: { - 'subject-case': [ - 2, - 'never', - ['upper-case', 'camel-case', 'kebab-case', 'snake-case'] - ] - } -}; diff --git a/config/husky/commit-msg b/config/husky/commit-msg deleted file mode 100755 index 7b9584f8..00000000 --- a/config/husky/commit-msg +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -npx --no-install -- commitlint --edit "$1" diff --git a/package-lock.json b/package-lock.json index ef3aaf98..52b9e080 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,8 +31,6 @@ "@babel/preset-env": "^7.12.7", "@babel/preset-react": "^7.12.7", "@babel/preset-typescript": "^7.12.7", - "@commitlint/cli": "^18.2.0", - "@commitlint/config-conventional": "^18.1.0", "@microsoft/api-documenter": "^7.11.0", "@microsoft/api-extractor": "^7.12.0", "@rollup/plugin-alias": "^5.0.1", @@ -54,7 +52,6 @@ "chai": "^4.2.0", "eslint": "^8.6.0", "eslint-config-prettier": "^9.0.0", - "husky": "^8.0.1", "intl-list-format": "^1.0.3", "jest": "^29.2.1", "jest-ts-webcompat-resolver": "^1.0.0", @@ -1970,493 +1967,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@commitlint/cli": { - "version": "18.4.1", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-18.4.1.tgz", - "integrity": "sha512-4+jljfd29Udw9RDDyigavLO9LvdbmB8O9xjDzVZ0R3lJuG7nCeyHgnKWIVpFaN590isZMV/cMeQK0gH7hRF40A==", - "dev": true, - "dependencies": { - "@commitlint/format": "^18.4.0", - "@commitlint/lint": "^18.4.0", - "@commitlint/load": "^18.4.1", - "@commitlint/read": "^18.4.0", - "@commitlint/types": "^18.4.0", - "execa": "^5.0.0", - "lodash.isfunction": "^3.0.9", - "resolve-from": "5.0.0", - "resolve-global": "1.0.0", - "yargs": "^17.0.0" - }, - "bin": { - "commitlint": "cli.js" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/config-conventional": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-18.4.0.tgz", - "integrity": "sha512-vArwCZopsZs0FnGsh9AR7uUTPZ5oVGk8+qnEZWq2KTsMjrE0k80b+oZ32GSQmXQT2iMKVrDC8pKX5uKNkCe9Sw==", - "dev": true, - "dependencies": { - "conventional-changelog-conventionalcommits": "^7.0.2" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/config-validator": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-18.4.0.tgz", - "integrity": "sha512-1y6qHMU3o4cYQSK+Y9EnmH6H1GRiwQGjnLIUOIKlekrmfc8MrMk1ByNmb8od4vK3qHJAaL/77/5n+1uyyIF5dA==", - "dev": true, - "dependencies": { - "@commitlint/types": "^18.4.0", - "ajv": "^8.11.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/ensure": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-18.4.0.tgz", - "integrity": "sha512-N5cJo/n61ULSwz3W5Iz/IZJ0I9H/PaHc+OMcF2XcRVbLa6B3YwzEW66XGCRKVULlsBNSrIH6tk5un9ayXAXIdw==", - "dev": true, - "dependencies": { - "@commitlint/types": "^18.4.0", - "lodash.camelcase": "^4.3.0", - "lodash.kebabcase": "^4.1.1", - "lodash.snakecase": "^4.1.1", - "lodash.startcase": "^4.4.0", - "lodash.upperfirst": "^4.3.1" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/execute-rule": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-18.4.0.tgz", - "integrity": "sha512-g013SWki6ZWhURBLOSXTaVQGWHdA0QlPJGiW4a+YpThezmJOemvc4LiKVpn13AjSKQ40QnmBqpBrxujOaSo+3A==", - "dev": true, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/format": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-18.4.0.tgz", - "integrity": "sha512-MiAe4D5/ahty38CzULdQbpRa3ReKZtx0kyigOWcntq+N5uqez+Ac4/MO7H+3j1kC4G7nfJVfBu6TqcXeyNvhCQ==", - "dev": true, - "dependencies": { - "@commitlint/types": "^18.4.0", - "chalk": "^4.1.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/format/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@commitlint/format/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@commitlint/format/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@commitlint/format/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@commitlint/format/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@commitlint/format/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@commitlint/is-ignored": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-18.4.0.tgz", - "integrity": "sha512-vyBKBj3Q4N3Xe4ZQcJXW9ef6gVrDL9Fl2HXnnC3F0Qt/F6E4runhJkEuUh5DB3WCXTJUHIJkByKPqrnz4RNrZw==", - "dev": true, - "dependencies": { - "@commitlint/types": "^18.4.0", - "semver": "7.5.4" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/is-ignored/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@commitlint/is-ignored/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@commitlint/is-ignored/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/@commitlint/lint": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-18.4.0.tgz", - "integrity": "sha512-Wkkf1DPVeLdHYGqtzMBfWoMbUtCojvlzDR89OKVic1rid41iZbb0FzTcwgMYs/1TNWNxoIq9PVVwY7ovLX1aJQ==", - "dev": true, - "dependencies": { - "@commitlint/is-ignored": "^18.4.0", - "@commitlint/parse": "^18.4.0", - "@commitlint/rules": "^18.4.0", - "@commitlint/types": "^18.4.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/load": { - "version": "18.4.1", - "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-18.4.1.tgz", - "integrity": "sha512-o/plBiPJQgbSq/4ipDpsq4HCmURjBAEjr1EO/p2falr3VhwV0WGXTvb8NlihgI8xtSyO6lHvtycrE535GMLQbA==", - "dev": true, - "dependencies": { - "@commitlint/config-validator": "^18.4.0", - "@commitlint/execute-rule": "^18.4.0", - "@commitlint/resolve-extends": "^18.4.0", - "@commitlint/types": "^18.4.0", - "@types/node": "^18.11.9", - "chalk": "^4.1.0", - "cosmiconfig": "^8.3.6", - "cosmiconfig-typescript-loader": "^5.0.0", - "lodash.isplainobject": "^4.0.6", - "lodash.merge": "^4.6.2", - "lodash.uniq": "^4.5.0", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/load/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@commitlint/load/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@commitlint/load/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@commitlint/load/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@commitlint/load/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@commitlint/load/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@commitlint/message": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-18.4.0.tgz", - "integrity": "sha512-3kg6NQO6pJ+VdBTWi51KInT8ngkxPJaW+iI7URtUALjKcO9K4XY3gf80ZPmS1hDessrjb7qCr1lau8eWMINAQw==", - "dev": true, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/parse": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-18.4.0.tgz", - "integrity": "sha512-SxTCSUZH8CJNYWOlFg18YUQ2RLz8ubXKbpHUIiSNwCbiQx7UDCydp1JnhoB4sOYOxgV8d3nuDwYluRU5KnEY4A==", - "dev": true, - "dependencies": { - "@commitlint/types": "^18.4.0", - "conventional-changelog-angular": "^6.0.0", - "conventional-commits-parser": "^5.0.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/read": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-18.4.0.tgz", - "integrity": "sha512-IpnABCbDeOw5npZ09SZZGLfd3T7cFtsxUYm6wT3aGmIB2fXKE3fMeuj3jxXjMibiGIyA3Z5voCMuOcKWpkNySA==", - "dev": true, - "dependencies": { - "@commitlint/top-level": "^18.4.0", - "@commitlint/types": "^18.4.0", - "fs-extra": "^11.0.0", - "git-raw-commits": "^2.0.11", - "minimist": "^1.2.6" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/resolve-extends": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-18.4.0.tgz", - "integrity": "sha512-qhgU6ach+S6sJMD9NjCYiEycOObGhxzWQLQzqlScJCv9zkPs15Bg0ffLXTQ3z7ipXv46XEKYMnSJzjLRw2Tlkg==", - "dev": true, - "dependencies": { - "@commitlint/config-validator": "^18.4.0", - "@commitlint/types": "^18.4.0", - "import-fresh": "^3.0.0", - "lodash.mergewith": "^4.6.2", - "resolve-from": "^5.0.0", - "resolve-global": "^1.0.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/rules": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-18.4.0.tgz", - "integrity": "sha512-T3ChRxQZ6g0iNCpVLc6KeQId0/86TnyQA8PFkng+dWElO2DAA5km/yirgKZV1Xlc+gF7Rf6d+a0ottxdKpOY+w==", - "dev": true, - "dependencies": { - "@commitlint/ensure": "^18.4.0", - "@commitlint/message": "^18.4.0", - "@commitlint/to-lines": "^18.4.0", - "@commitlint/types": "^18.4.0", - "execa": "^5.0.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/to-lines": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-18.4.0.tgz", - "integrity": "sha512-bZXuCtfBPjNgtEnG3gwJrveIgfKK2UdhIhFvKpMTrQl/gAwoto/3mzmE7qGAHwmuP4eZ2U8X7iwMnqIlWmv2Tw==", - "dev": true, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/top-level": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-18.4.0.tgz", - "integrity": "sha512-TfulcA8UHF7MZ6tm4Ci3aqZgMBZa1OoCg4prccWHvwG/hsHujZ7+0FKbeKqDbcSli/YWm4NJwEjl4uh5itIJeA==", - "dev": true, - "dependencies": { - "find-up": "^5.0.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/types": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-18.4.0.tgz", - "integrity": "sha512-MKeaFxt0I9fhqUb2E+YIzX/gZtmkuodJET/XKiZIMvXUff8Ee4Ih86eLg+yAm2jf1pwGBmU02uNOp0y094w2Uw==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/types/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@commitlint/types/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@commitlint/types/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@commitlint/types/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@commitlint/types/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@commitlint/types/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -4310,12 +3820,6 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, - "node_modules/@types/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", - "dev": true - }, "node_modules/@types/moo": { "version": "0.5.9", "resolved": "https://registry.npmjs.org/@types/moo/-/moo-0.5.9.tgz", @@ -4331,12 +3835,6 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true - }, "node_modules/@types/prop-types": { "version": "15.7.10", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.10.tgz", @@ -4748,22 +4246,6 @@ "node": ">= 6.0.0" } }, - "node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/ansi-align": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", @@ -4885,12 +4367,6 @@ "sprintf-js": "~1.0.2" } }, - "node_modules/array-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", - "dev": true - }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -4900,15 +4376,6 @@ "node": ">=8" } }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -5378,23 +4845,6 @@ "node": ">=6" } }, - "node_modules/camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, - "dependencies": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001562", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001562.tgz", @@ -5762,16 +5212,6 @@ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true }, - "node_modules/compare-func": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", - "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", - "dev": true, - "dependencies": { - "array-ify": "^1.0.0", - "dot-prop": "^5.1.0" - } - }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -5825,53 +5265,11 @@ }, "node_modules/content-disposition": { "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/conventional-changelog-angular": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-6.0.0.tgz", - "integrity": "sha512-6qLgrBF4gueoC7AFVHu51nHL9pF9FRjXrH+ceVf7WmAfH3gs+gEYOkvxhjMPjZu57I4AGUGoNTY8V7Hrgf1uqg==", - "dev": true, - "dependencies": { - "compare-func": "^2.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/conventional-changelog-conventionalcommits": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", - "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", - "dev": true, - "dependencies": { - "compare-func": "^2.0.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/conventional-commits-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", - "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", - "dev": true, - "dependencies": { - "is-text-path": "^2.0.0", - "JSONStream": "^1.3.5", - "meow": "^12.0.1", - "split2": "^4.0.0" - }, - "bin": { - "conventional-commits-parser": "cli.mjs" - }, + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "dev": true, "engines": { - "node": ">=16" + "node": ">= 0.6" } }, "node_modules/convert-source-map": { @@ -5899,67 +5297,6 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, - "node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", - "dev": true, - "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cosmiconfig-typescript-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-5.0.0.tgz", - "integrity": "sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==", - "dev": true, - "dependencies": { - "jiti": "^1.19.1" - }, - "engines": { - "node": ">=v16" - }, - "peerDependencies": { - "@types/node": "*", - "cosmiconfig": ">=8.2", - "typescript": ">=4" - } - }, - "node_modules/cosmiconfig/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -6071,15 +5408,6 @@ "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", "dev": true }, - "node_modules/dargs": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", - "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -6109,40 +5437,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/decamelize-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "dev": true, - "dependencies": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decamelize-keys/node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/dedent": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", @@ -6244,18 +5538,6 @@ "node": ">=6.0.0" } }, - "node_modules/dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dot-properties": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dot-properties/-/dot-properties-1.0.1.tgz", @@ -6869,20 +6151,6 @@ "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", "dev": true }, - "node_modules/fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/fs-readdir-recursive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", @@ -6964,85 +6232,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/git-raw-commits": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", - "integrity": "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==", - "dev": true, - "dependencies": { - "dargs": "^7.0.0", - "lodash": "^4.17.15", - "meow": "^8.0.0", - "split2": "^3.0.0", - "through2": "^4.0.0" - }, - "bin": { - "git-raw-commits": "cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/git-raw-commits/node_modules/meow": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", - "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", - "dev": true, - "dependencies": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/git-raw-commits/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/git-raw-commits/node_modules/split2": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", - "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", - "dev": true, - "dependencies": { - "readable-stream": "^3.0.0" - } - }, - "node_modules/git-raw-commits/node_modules/type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -7075,18 +6264,6 @@ "node": ">= 6" } }, - "node_modules/global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", - "dev": true, - "dependencies": { - "ini": "^1.3.4" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -7137,15 +6314,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -7176,36 +6344,6 @@ "he": "bin/he" } }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/hosted-git-info/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -7234,21 +6372,6 @@ "node": ">=10.17.0" } }, - "node_modules/husky": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", - "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", - "dev": true, - "bin": { - "husky": "lib/bin.js" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, "node_modules/hyperdyperid": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", @@ -7335,15 +6458,6 @@ "node": ">=0.8.19" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -7486,15 +6600,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -7552,18 +6657,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-text-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", - "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==", - "dev": true, - "dependencies": { - "text-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -9366,15 +8459,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", - "dev": true, - "bin": { - "jiti": "bin/jiti.js" - } - }, "node_modules/jju": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", @@ -9477,43 +8561,6 @@ "node": ">=6" } }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", - "dev": true, - "engines": [ - "node >= 0.2.0" - ] - }, - "node_modules/JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "dev": true, - "dependencies": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - }, - "bin": { - "JSONStream": "bin.js" - }, - "engines": { - "node": "*" - } - }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -9535,15 +8582,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -9624,12 +8662,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "dev": true - }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -9655,58 +8687,10 @@ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "dev": true }, - "node_modules/lodash.isfunction": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", - "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", - "dev": true - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true - }, - "node_modules/lodash.kebabcase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", - "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", - "dev": true - }, "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", - "dev": true - }, - "node_modules/lodash.snakecase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", - "dev": true - }, - "node_modules/lodash.startcase": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", - "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", - "dev": true - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "dev": true - }, - "node_modules/lodash.upperfirst": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", - "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, "node_modules/log-symbols": { @@ -9872,18 +8856,6 @@ "tmpl": "1.0.5" } }, - "node_modules/map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/map-stream": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", @@ -9910,18 +8882,6 @@ "tslib": "2" } }, - "node_modules/meow": { - "version": "12.1.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", - "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", - "dev": true, - "engines": { - "node": ">=16.10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -9984,15 +8944,6 @@ "node": ">=6" } }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -10014,29 +8965,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dev": true, - "dependencies": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/minimist-options/node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/mocha": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", @@ -10356,54 +9284,6 @@ "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", "dev": true }, - "node_modules/normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/normalize-package-data/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/normalize-package-data/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -10855,15 +9735,6 @@ } ] }, - "node_modules/quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/quill-delta": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", @@ -10978,135 +9849,6 @@ "react": "^18.2.0" } }, - "node_modules/read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "dependencies": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg/node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "node_modules/read-pkg/node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -11134,19 +9876,6 @@ "node": ">=8.10.0" } }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -11295,18 +10024,6 @@ "node": ">=8" } }, - "node_modules/resolve-global": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz", - "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==", - "dev": true, - "dependencies": { - "global-dirs": "^0.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -11620,38 +10337,6 @@ "source-map": "^0.6.0" } }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", - "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", - "dev": true - }, "node_modules/split": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", @@ -11664,15 +10349,6 @@ "node": "*" } }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "dev": true, - "engines": { - "node": ">= 10.x" - } - }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -11813,18 +10489,6 @@ "node": ">=6" } }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -11899,18 +10563,6 @@ "node": ">=8" } }, - "node_modules/text-extensions": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", - "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -11935,29 +10587,6 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, - "node_modules/through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", - "dev": true, - "dependencies": { - "readable-stream": "3" - } - }, - "node_modules/through2/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -12006,15 +10635,6 @@ "node": ">=8.0" } }, - "node_modules/trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -12125,15 +10745,6 @@ "node": ">=4" } }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -12220,16 +10831,6 @@ "node": ">=10.12.0" } }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "node_modules/validator": { "version": "13.11.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", diff --git a/package.json b/package.json index 9db49edb..33b11997 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "packages/webpack-loader" ], "scripts": { - "clean": "git clean -fdx -e node_modules -e config/husky/_", + "clean": "git clean -fdx -e node_modules", "build": "npm run build --workspaces --if-present", "build:mf2": "npm run build -w packages/mf2-messageformat -w packages/mf2-fluent -w packages/mf2-icu-mf1 -w packages/mf2-resource -w packages/mf2-xliff", "predocs": "npm run extract-api --workspaces --if-present", @@ -37,7 +37,6 @@ "lint": "eslint '**/*.{js,ts,tsx}'", "prettier": "prettier --write .", "start": "node -i -r ./scripts/start.js", - "prepare": "husky install config/husky", "pretest:browsers": "cd test && rollup -c", "test:browsers": "mocha --parallel --jobs 5 --require test/browser/browserstack-fixtures.js --timeout 120000 test/browser/tests/", "test:chrome": "npm run pretest:browsers && mocha-selenium-bridge -b chrome test/browser/test.html", @@ -57,8 +56,6 @@ "@babel/preset-env": "^7.12.7", "@babel/preset-react": "^7.12.7", "@babel/preset-typescript": "^7.12.7", - "@commitlint/cli": "^18.2.0", - "@commitlint/config-conventional": "^18.1.0", "@microsoft/api-documenter": "^7.11.0", "@microsoft/api-extractor": "^7.12.0", "@rollup/plugin-alias": "^5.0.1", @@ -80,7 +77,6 @@ "chai": "^4.2.0", "eslint": "^8.6.0", "eslint-config-prettier": "^9.0.0", - "husky": "^8.0.1", "intl-list-format": "^1.0.3", "jest": "^29.2.1", "jest-ts-webcompat-resolver": "^1.0.0", From 78cb096d50fe810f75dd994bc472a9dc28d4b0fa Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 2 Jan 2024 16:14:51 +0200 Subject: [PATCH 18/44] refactor(mf2): Collect files in a new src/data-model/ directory --- .../mf2-messageformat/src/cst-parser/as-data-model.ts | 2 +- .../expression/function-annotation.test.ts | 6 +++++- .../{ => data-model}/expression/function-annotation.ts | 6 +++--- .../src/{ => data-model}/expression/index.ts | 4 ++-- .../src/{ => data-model}/expression/literal.test.ts | 2 +- .../src/{ => data-model}/expression/literal.ts | 4 ++-- .../expression/unsupported-annotation.test.ts | 2 +- .../expression/unsupported-annotation.ts | 6 +++--- .../src/{ => data-model}/expression/value.ts | 2 +- .../src/{ => data-model}/expression/variable-ref.ts | 6 +++--- .../src/{ => data-model}/expression/variable.test.ts | 2 +- .../src/{data-model.ts => data-model/index.ts} | 9 +++------ .../src/{markup => data-model}/markup.test.ts | 0 .../src/{markup/index.ts => data-model/markup.ts} | 4 ++-- packages/mf2-messageformat/src/index.ts | 5 ++--- packages/mf2-messageformat/src/messageformat.ts | 9 ++++++--- packages/mf2-messageformat/src/select-pattern.ts | 4 ++-- packages/mf2-messageformat/src/stringifier/message.ts | 4 ++-- 18 files changed, 40 insertions(+), 37 deletions(-) rename packages/mf2-messageformat/src/{ => data-model}/expression/function-annotation.test.ts (97%) rename packages/mf2-messageformat/src/{ => data-model}/expression/function-annotation.ts (92%) rename packages/mf2-messageformat/src/{ => data-model}/expression/index.ts (94%) rename packages/mf2-messageformat/src/{ => data-model}/expression/literal.test.ts (96%) rename packages/mf2-messageformat/src/{ => data-model}/expression/literal.ts (86%) rename packages/mf2-messageformat/src/{ => data-model}/expression/unsupported-annotation.test.ts (96%) rename packages/mf2-messageformat/src/{ => data-model}/expression/unsupported-annotation.ts (88%) rename packages/mf2-messageformat/src/{ => data-model}/expression/value.ts (93%) rename packages/mf2-messageformat/src/{ => data-model}/expression/variable-ref.ts (96%) rename packages/mf2-messageformat/src/{ => data-model}/expression/variable.test.ts (98%) rename packages/mf2-messageformat/src/{data-model.ts => data-model/index.ts} (95%) rename packages/mf2-messageformat/src/{markup => data-model}/markup.test.ts (100%) rename packages/mf2-messageformat/src/{markup/index.ts => data-model/markup.ts} (94%) diff --git a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts index daf1310c..425f5cee 100644 --- a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts +++ b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts @@ -1,4 +1,4 @@ -import type * as Model from '../data-model.js'; +import type * as Model from '../data-model/index.js'; import { MessageSyntaxError } from '../errors.js'; import type * as CST from './cst-types.js'; diff --git a/packages/mf2-messageformat/src/expression/function-annotation.test.ts b/packages/mf2-messageformat/src/data-model/expression/function-annotation.test.ts similarity index 97% rename from packages/mf2-messageformat/src/expression/function-annotation.test.ts rename to packages/mf2-messageformat/src/data-model/expression/function-annotation.test.ts index 36f49337..84607410 100644 --- a/packages/mf2-messageformat/src/expression/function-annotation.test.ts +++ b/packages/mf2-messageformat/src/data-model/expression/function-annotation.test.ts @@ -1,4 +1,8 @@ -import { MessageFormat, MessageFunctions, MessageNumberPart } from '../index'; +import { + MessageFormat, + MessageFunctions, + MessageNumberPart +} from '../../index'; test('Custom function', () => { const functions = { diff --git a/packages/mf2-messageformat/src/expression/function-annotation.ts b/packages/mf2-messageformat/src/data-model/expression/function-annotation.ts similarity index 92% rename from packages/mf2-messageformat/src/expression/function-annotation.ts rename to packages/mf2-messageformat/src/data-model/expression/function-annotation.ts index d11e77ba..ffe163f8 100644 --- a/packages/mf2-messageformat/src/expression/function-annotation.ts +++ b/packages/mf2-messageformat/src/data-model/expression/function-annotation.ts @@ -1,6 +1,6 @@ -import { MessageError } from '../errors.js'; -import type { Context } from '../format-context.js'; -import { MessageFunctionContext, fallback } from '../runtime/index.js'; +import { MessageError } from '../../errors.js'; +import type { Context } from '../../format-context.js'; +import { MessageFunctionContext, fallback } from '../../runtime/index.js'; import type { Literal } from './literal.js'; import { getValueSource, resolveValue } from './value.js'; import type { VariableRef } from './variable-ref.js'; diff --git a/packages/mf2-messageformat/src/expression/index.ts b/packages/mf2-messageformat/src/data-model/expression/index.ts similarity index 94% rename from packages/mf2-messageformat/src/expression/index.ts rename to packages/mf2-messageformat/src/data-model/expression/index.ts index 822a3a0c..2db771a0 100644 --- a/packages/mf2-messageformat/src/expression/index.ts +++ b/packages/mf2-messageformat/src/data-model/expression/index.ts @@ -1,5 +1,5 @@ -import type { Context } from '../format-context'; -import type { MessageValue } from '../runtime'; +import type { Context } from '../../format-context'; +import type { MessageValue } from '../../runtime'; import { FunctionAnnotation, diff --git a/packages/mf2-messageformat/src/expression/literal.test.ts b/packages/mf2-messageformat/src/data-model/expression/literal.test.ts similarity index 96% rename from packages/mf2-messageformat/src/expression/literal.test.ts rename to packages/mf2-messageformat/src/data-model/expression/literal.test.ts index 3a7c2771..f6ddeb5d 100644 --- a/packages/mf2-messageformat/src/expression/literal.test.ts +++ b/packages/mf2-messageformat/src/data-model/expression/literal.test.ts @@ -1,4 +1,4 @@ -import { MessageFormat } from '../index'; +import { MessageFormat } from '../../index'; function resolve(source: string, errors: any[] = []) { const mf = new MessageFormat(source); diff --git a/packages/mf2-messageformat/src/expression/literal.ts b/packages/mf2-messageformat/src/data-model/expression/literal.ts similarity index 86% rename from packages/mf2-messageformat/src/expression/literal.ts rename to packages/mf2-messageformat/src/data-model/expression/literal.ts index 2942551c..21cca123 100644 --- a/packages/mf2-messageformat/src/expression/literal.ts +++ b/packages/mf2-messageformat/src/data-model/expression/literal.ts @@ -1,5 +1,5 @@ -import type { Context } from '../format-context.js'; -import { MessageFunctionContext } from '../runtime/index.js'; +import type { Context } from '../../format-context.js'; +import { MessageFunctionContext } from '../../runtime/index.js'; /** * An immediately defined value. diff --git a/packages/mf2-messageformat/src/expression/unsupported-annotation.test.ts b/packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.test.ts similarity index 96% rename from packages/mf2-messageformat/src/expression/unsupported-annotation.test.ts rename to packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.test.ts index 631aa6aa..29a8dbec 100644 --- a/packages/mf2-messageformat/src/expression/unsupported-annotation.test.ts +++ b/packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.test.ts @@ -1,4 +1,4 @@ -import { MessageFormat } from '../index'; +import { MessageFormat } from '../../index'; function resolve( source: string, diff --git a/packages/mf2-messageformat/src/expression/unsupported-annotation.ts b/packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.ts similarity index 88% rename from packages/mf2-messageformat/src/expression/unsupported-annotation.ts rename to packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.ts index 5780be3e..7f033ece 100644 --- a/packages/mf2-messageformat/src/expression/unsupported-annotation.ts +++ b/packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.ts @@ -1,6 +1,6 @@ -import { MessageResolutionError } from '../errors.js'; -import type { Context } from '../format-context.js'; -import { fallback } from '../runtime/index.js'; +import { MessageResolutionError } from '../../errors.js'; +import type { Context } from '../../format-context.js'; +import { fallback } from '../../runtime/index.js'; import type { Literal } from './literal.js'; import { getValueSource } from './value.js'; import type { VariableRef } from './variable-ref.js'; diff --git a/packages/mf2-messageformat/src/expression/value.ts b/packages/mf2-messageformat/src/data-model/expression/value.ts similarity index 93% rename from packages/mf2-messageformat/src/expression/value.ts rename to packages/mf2-messageformat/src/data-model/expression/value.ts index 774ba7ce..d55b30c1 100644 --- a/packages/mf2-messageformat/src/expression/value.ts +++ b/packages/mf2-messageformat/src/data-model/expression/value.ts @@ -1,4 +1,4 @@ -import type { Context } from '../format-context'; +import type { Context } from '../../format-context'; import type { Literal } from './literal'; import { VariableRef, lookupVariableRef } from './variable-ref'; diff --git a/packages/mf2-messageformat/src/expression/variable-ref.ts b/packages/mf2-messageformat/src/data-model/expression/variable-ref.ts similarity index 96% rename from packages/mf2-messageformat/src/expression/variable-ref.ts rename to packages/mf2-messageformat/src/data-model/expression/variable-ref.ts index b2dafdef..6d5d2b65 100644 --- a/packages/mf2-messageformat/src/expression/variable-ref.ts +++ b/packages/mf2-messageformat/src/data-model/expression/variable-ref.ts @@ -1,11 +1,11 @@ -import { MessageResolutionError } from '../errors.js'; -import type { Context } from '../format-context.js'; +import { MessageResolutionError } from '../../errors.js'; +import type { Context } from '../../format-context.js'; import { MessageFunctionContext, MessageValue, fallback, unknown -} from '../runtime/index.js'; +} from '../../runtime/index.js'; import { type Expression, resolveExpression } from './index.js'; /** diff --git a/packages/mf2-messageformat/src/expression/variable.test.ts b/packages/mf2-messageformat/src/data-model/expression/variable.test.ts similarity index 98% rename from packages/mf2-messageformat/src/expression/variable.test.ts rename to packages/mf2-messageformat/src/data-model/expression/variable.test.ts index 0b487398..64f7a6ad 100644 --- a/packages/mf2-messageformat/src/expression/variable.test.ts +++ b/packages/mf2-messageformat/src/data-model/expression/variable.test.ts @@ -1,4 +1,4 @@ -import { MessageFormat } from '../index'; +import { MessageFormat } from '../../index'; describe('variables', () => { let mf: MessageFormat; diff --git a/packages/mf2-messageformat/src/data-model.ts b/packages/mf2-messageformat/src/data-model/index.ts similarity index 95% rename from packages/mf2-messageformat/src/data-model.ts rename to packages/mf2-messageformat/src/data-model/index.ts index 8ff4b090..2c31429e 100644 --- a/packages/mf2-messageformat/src/data-model.ts +++ b/packages/mf2-messageformat/src/data-model/index.ts @@ -1,16 +1,13 @@ import type { Expression, Literal, VariableRef } from './expression/index.js'; -import type { Markup } from './markup/index.js'; +import type { Markup } from './markup.js'; export type { FunctionAnnotation, Option, UnsupportedAnnotation } from './expression/index.js'; -export type { - MarkupClose, - MarkupOpen, - MarkupStandalone -} from './markup/index.js'; +export type { MarkupClose, MarkupOpen, MarkupStandalone } from './markup.js'; +export { isMarkup } from './markup.js'; export type { Expression, Literal, Markup, VariableRef }; /** diff --git a/packages/mf2-messageformat/src/markup/markup.test.ts b/packages/mf2-messageformat/src/data-model/markup.test.ts similarity index 100% rename from packages/mf2-messageformat/src/markup/markup.test.ts rename to packages/mf2-messageformat/src/data-model/markup.test.ts diff --git a/packages/mf2-messageformat/src/markup/index.ts b/packages/mf2-messageformat/src/data-model/markup.ts similarity index 94% rename from packages/mf2-messageformat/src/markup/index.ts rename to packages/mf2-messageformat/src/data-model/markup.ts index 4e03a379..d985a09e 100644 --- a/packages/mf2-messageformat/src/markup/index.ts +++ b/packages/mf2-messageformat/src/data-model/markup.ts @@ -1,10 +1,10 @@ -import type { Option } from '../data-model.js'; -import { resolveValue } from '../expression/value.js'; +import { resolveValue } from './expression/value.js'; import type { Context } from '../format-context.js'; import type { MessageMarkupClosePart, MessageMarkupPart } from '../runtime/index.js'; +import type { Option } from './index.js'; /** * Markup placeholders can span ranges of other pattern elements, diff --git a/packages/mf2-messageformat/src/index.ts b/packages/mf2-messageformat/src/index.ts index 2fdd2ed4..2d803b40 100644 --- a/packages/mf2-messageformat/src/index.ts +++ b/packages/mf2-messageformat/src/index.ts @@ -1,13 +1,12 @@ export { asDataModel, parseMessage, type CST } from './cst-parser/index.js'; -export * from './data-model.js'; +export * from './data-model/index.js'; +export * from './data-model/expression/index.js'; export { MessageDataModelError, MessageError, MessageResolutionError, MessageSyntaxError } from './errors.js'; -export * from './expression/index.js'; -export { isMarkup } from './markup/index.js'; export { MessageFormat, MessageFormatOptions } from './messageformat.js'; export { stringifyMessage } from './stringifier/message.js'; export * from './runtime/index.js'; diff --git a/packages/mf2-messageformat/src/messageformat.ts b/packages/mf2-messageformat/src/messageformat.ts index 67da075d..5af216aa 100644 --- a/packages/mf2-messageformat/src/messageformat.ts +++ b/packages/mf2-messageformat/src/messageformat.ts @@ -1,9 +1,12 @@ import { asDataModel, parseMessage } from './cst-parser/index.js'; -import type { Message } from './data-model'; +import type { Message } from './data-model/index.js'; import { MessageError, MessageResolutionError } from './errors.js'; import type { Context } from './format-context'; -import { UnresolvedExpression, resolveExpression } from './expression/index.js'; -import { formatMarkup } from './markup/index.js'; +import { + UnresolvedExpression, + resolveExpression +} from './data-model/expression/index.js'; +import { formatMarkup } from './data-model/markup.js'; import { MessageFunctions, MessagePart, diff --git a/packages/mf2-messageformat/src/select-pattern.ts b/packages/mf2-messageformat/src/select-pattern.ts index 57adf7fe..b3887dcd 100644 --- a/packages/mf2-messageformat/src/select-pattern.ts +++ b/packages/mf2-messageformat/src/select-pattern.ts @@ -1,6 +1,6 @@ -import type { Message, Pattern } from './data-model'; +import type { Message, Pattern } from './data-model/index.js'; import { MessageSelectionError } from './errors'; -import { resolveExpression } from './expression/index.js'; +import { resolveExpression } from './data-model/expression/index.js'; import type { Context } from './format-context'; export function selectPattern( diff --git a/packages/mf2-messageformat/src/stringifier/message.ts b/packages/mf2-messageformat/src/stringifier/message.ts index 8484d49a..835ad98e 100644 --- a/packages/mf2-messageformat/src/stringifier/message.ts +++ b/packages/mf2-messageformat/src/stringifier/message.ts @@ -12,8 +12,8 @@ import { VariableRef, isPatternMessage, isSelectMessage -} from '../data-model.js'; -import { isLiteral, isVariableRef } from '../expression/index.js'; +} from '../data-model/index.js'; +import { isLiteral, isVariableRef } from '../data-model/expression/index.js'; import { MessageFormat } from '../messageformat.js'; /** From 487744fac8911ec4b0c85c355b1b6e4c8448f0b8 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 2 Jan 2024 16:31:31 +0200 Subject: [PATCH 19/44] refactor(mf2): Collect all type guards into src/data-model/type-guards.ts --- .../expression/function-annotation.ts | 9 --- .../src/data-model/expression/index.ts | 23 +------- .../src/data-model/expression/literal.ts | 9 --- .../expression/unsupported-annotation.ts | 11 ---- .../src/data-model/expression/variable-ref.ts | 9 --- .../mf2-messageformat/src/data-model/index.ts | 37 ------------ .../src/data-model/markup.ts | 9 --- .../src/data-model/type-guards.ts | 58 +++++++++++++++++++ packages/mf2-messageformat/src/index.ts | 1 + .../src/stringifier/message.ts | 11 ++-- 10 files changed, 69 insertions(+), 108 deletions(-) create mode 100644 packages/mf2-messageformat/src/data-model/type-guards.ts diff --git a/packages/mf2-messageformat/src/data-model/expression/function-annotation.ts b/packages/mf2-messageformat/src/data-model/expression/function-annotation.ts index ffe163f8..7e430f2a 100644 --- a/packages/mf2-messageformat/src/data-model/expression/function-annotation.ts +++ b/packages/mf2-messageformat/src/data-model/expression/function-annotation.ts @@ -34,15 +34,6 @@ export interface Option { value: Literal | VariableRef; } -/** - * Type guard for {@link FunctionAnnotation} pattern elements - * - * @beta - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const isFunctionAnnotation = (part: any): part is FunctionAnnotation => - !!part && typeof part === 'object' && part.type === 'function'; - export function resolveFunctionAnnotation( ctx: Context, operand: Literal | VariableRef | undefined, diff --git a/packages/mf2-messageformat/src/data-model/expression/index.ts b/packages/mf2-messageformat/src/data-model/expression/index.ts index 2db771a0..48c9fd47 100644 --- a/packages/mf2-messageformat/src/data-model/expression/index.ts +++ b/packages/mf2-messageformat/src/data-model/expression/index.ts @@ -12,19 +12,11 @@ import { } from './unsupported-annotation'; import { VariableRef, resolveVariableRef } from './variable-ref'; -export { - isFunctionAnnotation, - FunctionAnnotation, - Option -} from './function-annotation'; -export { isLiteral, Literal } from './literal'; -export { - isUnsupportedAnnotation, - UnsupportedAnnotation -} from './unsupported-annotation'; +export type { FunctionAnnotation, Option } from './function-annotation'; +export type { Literal } from './literal'; +export type { UnsupportedAnnotation } from './unsupported-annotation'; export { getMessageValue, - isVariableRef, UnresolvedExpression, VariableRef } from './variable-ref'; @@ -51,15 +43,6 @@ export type Expression< annotation: FunctionAnnotation | UnsupportedAnnotation; }; -/** - * Type guard for {@link Expression} pattern elements - * - * @beta - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const isExpression = (part: any): part is Expression => - !!part && typeof part === 'object' && part.type === 'expression'; - /** @internal */ export function resolveExpression( ctx: Context, diff --git a/packages/mf2-messageformat/src/data-model/expression/literal.ts b/packages/mf2-messageformat/src/data-model/expression/literal.ts index 21cca123..c22e4099 100644 --- a/packages/mf2-messageformat/src/data-model/expression/literal.ts +++ b/packages/mf2-messageformat/src/data-model/expression/literal.ts @@ -16,15 +16,6 @@ export interface Literal { value: string; } -/** - * Type guard for {@link Literal} pattern elements - * - * @beta - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const isLiteral = (part: any): part is Literal => - !!part && typeof part === 'object' && part.type === 'literal'; - export function resolveLiteral(ctx: Context, lit: Literal) { const msgCtx = new MessageFunctionContext(ctx, `|${lit.value}|`); return ctx.functions.string(msgCtx, {}, lit.value); diff --git a/packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.ts b/packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.ts index 7f033ece..00886841 100644 --- a/packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.ts +++ b/packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.ts @@ -22,17 +22,6 @@ export interface UnsupportedAnnotation { name?: never; } -/** - * Type guard for {@link UnsupportedAnnotation} pattern elements - * - * @beta - */ -export const isUnsupportedAnnotation = ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - part: any -): part is UnsupportedAnnotation => - !!part && typeof part === 'object' && part.type === 'unsupported-annotation'; - export function resolveUnsupportedAnnotation( ctx: Context, operand: Literal | VariableRef | undefined, diff --git a/packages/mf2-messageformat/src/data-model/expression/variable-ref.ts b/packages/mf2-messageformat/src/data-model/expression/variable-ref.ts index 6d5d2b65..49a22d91 100644 --- a/packages/mf2-messageformat/src/data-model/expression/variable-ref.ts +++ b/packages/mf2-messageformat/src/data-model/expression/variable-ref.ts @@ -42,15 +42,6 @@ export class UnresolvedExpression { } } -/** - * Type guard for {@link VariableRef} pattern elements - * - * @beta - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const isVariableRef = (part: any): part is VariableRef => - !!part && typeof part === 'object' && part.type === 'variable'; - const isScope = (scope: unknown): scope is Record => scope instanceof Object; diff --git a/packages/mf2-messageformat/src/data-model/index.ts b/packages/mf2-messageformat/src/data-model/index.ts index 2c31429e..702a9143 100644 --- a/packages/mf2-messageformat/src/data-model/index.ts +++ b/packages/mf2-messageformat/src/data-model/index.ts @@ -7,7 +7,6 @@ export type { UnsupportedAnnotation } from './expression/index.js'; export type { MarkupClose, MarkupOpen, MarkupStandalone } from './markup.js'; -export { isMarkup } from './markup.js'; export type { Expression, Literal, Markup, VariableRef }; /** @@ -110,39 +109,3 @@ export interface CatchallKey { type: '*'; value?: string; } - -/** - * A type guard for {@link CatchallKey} values - * - * @beta - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const isCatchallKey = (key: any): key is CatchallKey => - !!key && typeof key === 'object' && key.type === '*'; - -/** - * A type guard for {@link Message} values - * - * @beta - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const isMessage = (msg: any): msg is Message => - !!msg && - typeof msg === 'object' && - (msg.type === 'message' || msg.type === 'select'); - -/** - * A type guard for {@link PatternMessage} values - * - * @beta - */ -export const isPatternMessage = (msg: Message): msg is PatternMessage => - msg.type === 'message'; - -/** - * A type guard for {@link SelectMessage} values - * - * @beta - */ -export const isSelectMessage = (msg: Message): msg is SelectMessage => - msg.type === 'select'; diff --git a/packages/mf2-messageformat/src/data-model/markup.ts b/packages/mf2-messageformat/src/data-model/markup.ts index d985a09e..5f8d0d27 100644 --- a/packages/mf2-messageformat/src/data-model/markup.ts +++ b/packages/mf2-messageformat/src/data-model/markup.ts @@ -42,15 +42,6 @@ export interface MarkupClose { options?: never; } -/** - * Type guard for {@link Markup} pattern elements - * - * @beta - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const isMarkup = (part: any): part is Markup => - !!part && typeof part === 'object' && part.type === 'markup'; - export function formatMarkup( ctx: Context, { kind, name, options }: Markup diff --git a/packages/mf2-messageformat/src/data-model/type-guards.ts b/packages/mf2-messageformat/src/data-model/type-guards.ts new file mode 100644 index 00000000..9d69b281 --- /dev/null +++ b/packages/mf2-messageformat/src/data-model/type-guards.ts @@ -0,0 +1,58 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { FunctionAnnotation } from './expression/function-annotation.js'; +import type { Expression } from './expression/index.js'; +import type { Literal } from './expression/literal.js'; +import type { UnsupportedAnnotation } from './expression/unsupported-annotation.js'; +import type { VariableRef } from './expression/variable-ref.js'; +import type { + CatchallKey, + Message, + PatternMessage, + SelectMessage +} from './index.js'; +import type { Markup } from './markup.js'; + +/** @beta */ +export const isCatchallKey = (key: any): key is CatchallKey => + !!key && typeof key === 'object' && key.type === '*'; + +/** @beta */ +export const isExpression = (part: any): part is Expression => + !!part && typeof part === 'object' && part.type === 'expression'; + +/** @beta */ +export const isFunctionAnnotation = (part: any): part is FunctionAnnotation => + !!part && typeof part === 'object' && part.type === 'function'; + +/** @beta */ +export const isLiteral = (part: any): part is Literal => + !!part && typeof part === 'object' && part.type === 'literal'; + +/** @beta */ +export const isMarkup = (part: any): part is Markup => + !!part && typeof part === 'object' && part.type === 'markup'; + +/** @beta */ +export const isMessage = (msg: any): msg is Message => + !!msg && + typeof msg === 'object' && + (msg.type === 'message' || msg.type === 'select'); + +/** @beta */ +export const isPatternMessage = (msg: Message): msg is PatternMessage => + msg.type === 'message'; + +/** @beta */ +export const isSelectMessage = (msg: Message): msg is SelectMessage => + msg.type === 'select'; + +/** @beta */ +export const isUnsupportedAnnotation = ( + part: any +): part is UnsupportedAnnotation => + !!part && typeof part === 'object' && part.type === 'unsupported-annotation'; + +/** @beta */ +export const isVariableRef = (part: any): part is VariableRef => + !!part && typeof part === 'object' && part.type === 'variable'; diff --git a/packages/mf2-messageformat/src/index.ts b/packages/mf2-messageformat/src/index.ts index 2d803b40..639b5636 100644 --- a/packages/mf2-messageformat/src/index.ts +++ b/packages/mf2-messageformat/src/index.ts @@ -1,6 +1,7 @@ export { asDataModel, parseMessage, type CST } from './cst-parser/index.js'; export * from './data-model/index.js'; export * from './data-model/expression/index.js'; +export * from './data-model/type-guards.js'; export { MessageDataModelError, MessageError, diff --git a/packages/mf2-messageformat/src/stringifier/message.ts b/packages/mf2-messageformat/src/stringifier/message.ts index 835ad98e..906fcc2b 100644 --- a/packages/mf2-messageformat/src/stringifier/message.ts +++ b/packages/mf2-messageformat/src/stringifier/message.ts @@ -9,11 +9,14 @@ import { Option, Pattern, UnsupportedAnnotation, - VariableRef, - isPatternMessage, - isSelectMessage + VariableRef } from '../data-model/index.js'; -import { isLiteral, isVariableRef } from '../data-model/expression/index.js'; +import { + isLiteral, + isPatternMessage, + isSelectMessage, + isVariableRef +} from '../data-model/type-guards.js'; import { MessageFormat } from '../messageformat.js'; /** From c69b00e6003ec2615283754612fbfc43dbc5fccc Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 2 Jan 2024 17:00:02 +0200 Subject: [PATCH 20/44] refactor(mf2): Collect all data model types into src/data-model/types.ts --- .../src/cst-parser/as-data-model.ts | 2 +- .../expression/function-annotation.ts | 37 +-- .../src/data-model/expression/index.ts | 48 +--- .../src/data-model/expression/literal.ts | 16 +- .../expression/unsupported-annotation.ts | 20 +- .../src/data-model/expression/value.ts | 4 +- .../src/data-model/expression/variable-ref.ts | 21 +- .../mf2-messageformat/src/data-model/index.ts | 111 -------- .../src/data-model/markup.ts | 38 +-- .../src/data-model/type-guards.ts | 14 +- .../mf2-messageformat/src/data-model/types.ts | 238 ++++++++++++++++++ .../mf2-messageformat/src/extra/validate.ts | 4 +- packages/mf2-messageformat/src/index.ts | 4 +- .../mf2-messageformat/src/messageformat.ts | 10 +- .../mf2-messageformat/src/select-pattern.ts | 4 +- .../src/stringifier/message.ts | 14 +- 16 files changed, 281 insertions(+), 304 deletions(-) delete mode 100644 packages/mf2-messageformat/src/data-model/index.ts create mode 100644 packages/mf2-messageformat/src/data-model/types.ts diff --git a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts index 425f5cee..6ee3ab3a 100644 --- a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts +++ b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts @@ -1,4 +1,4 @@ -import type * as Model from '../data-model/index.js'; +import type * as Model from '../data-model/types.js'; import { MessageSyntaxError } from '../errors.js'; import type * as CST from './cst-types.js'; diff --git a/packages/mf2-messageformat/src/data-model/expression/function-annotation.ts b/packages/mf2-messageformat/src/data-model/expression/function-annotation.ts index 7e430f2a..89f3afe2 100644 --- a/packages/mf2-messageformat/src/data-model/expression/function-annotation.ts +++ b/packages/mf2-messageformat/src/data-model/expression/function-annotation.ts @@ -1,38 +1,13 @@ import { MessageError } from '../../errors.js'; import type { Context } from '../../format-context.js'; import { MessageFunctionContext, fallback } from '../../runtime/index.js'; -import type { Literal } from './literal.js'; +import type { + FunctionAnnotation, + Literal, + Option, + VariableRef +} from '../types.js'; import { getValueSource, resolveValue } from './value.js'; -import type { VariableRef } from './variable-ref.js'; - -/** - * To resolve a FunctionAnnotation, an externally defined function is called. - * - * @remarks - * The `name` identifies a function that takes in the arguments `args`, the - * current locale, as well as any `options`, and returns some corresponding - * output. Likely functions available by default would include `'plural'` for - * determining the plural category of a numeric value, as well as `'number'` - * and `'date'` for formatting values. - * - * @beta - */ -export interface FunctionAnnotation { - type: 'function'; - name: string; - options?: Option[]; -} - -/** - * {@link FunctionAnnotation} options are expressed as - * `key`/`value` pairs to allow their order to be maintained. - * - * @beta - */ -export interface Option { - name: string; - value: Literal | VariableRef; -} export function resolveFunctionAnnotation( ctx: Context, diff --git a/packages/mf2-messageformat/src/data-model/expression/index.ts b/packages/mf2-messageformat/src/data-model/expression/index.ts index 48c9fd47..8bdc654f 100644 --- a/packages/mf2-messageformat/src/data-model/expression/index.ts +++ b/packages/mf2-messageformat/src/data-model/expression/index.ts @@ -1,49 +1,11 @@ import type { Context } from '../../format-context'; import type { MessageValue } from '../../runtime'; +import type { Expression } from '../types'; +import { resolveFunctionAnnotation } from './function-annotation'; +import { resolveLiteral } from './literal'; +import { resolveUnsupportedAnnotation } from './unsupported-annotation'; +import { resolveVariableRef } from './variable-ref'; -import { - FunctionAnnotation, - resolveFunctionAnnotation -} from './function-annotation'; -import { Literal, resolveLiteral } from './literal'; -import { - UnsupportedAnnotation, - resolveUnsupportedAnnotation -} from './unsupported-annotation'; -import { VariableRef, resolveVariableRef } from './variable-ref'; - -export type { FunctionAnnotation, Option } from './function-annotation'; -export type { Literal } from './literal'; -export type { UnsupportedAnnotation } from './unsupported-annotation'; -export { - getMessageValue, - UnresolvedExpression, - VariableRef -} from './variable-ref'; - -/** - * Wrapper for selectors and placeholders. - * - * @beta - */ -export type Expression< - A extends Literal | VariableRef | undefined = - | Literal - | VariableRef - | undefined -> = A extends Literal | VariableRef - ? { - type: 'expression'; - arg: A; - annotation?: FunctionAnnotation | UnsupportedAnnotation; - } - : { - type: 'expression'; - arg?: never; - annotation: FunctionAnnotation | UnsupportedAnnotation; - }; - -/** @internal */ export function resolveExpression( ctx: Context, { arg, annotation }: Expression diff --git a/packages/mf2-messageformat/src/data-model/expression/literal.ts b/packages/mf2-messageformat/src/data-model/expression/literal.ts index c22e4099..04b03b4d 100644 --- a/packages/mf2-messageformat/src/data-model/expression/literal.ts +++ b/packages/mf2-messageformat/src/data-model/expression/literal.ts @@ -1,20 +1,6 @@ import type { Context } from '../../format-context.js'; import { MessageFunctionContext } from '../../runtime/index.js'; - -/** - * An immediately defined value. - * - * @remarks - * Always contains a string value. In Function arguments and options, - * the expeted type of the value may result in the value being - * further parsed as a boolean or a number. - * - * @beta - */ -export interface Literal { - type: 'literal'; - value: string; -} +import type { Literal } from '../types.js'; export function resolveLiteral(ctx: Context, lit: Literal) { const msgCtx = new MessageFunctionContext(ctx, `|${lit.value}|`); diff --git a/packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.ts b/packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.ts index 00886841..9e623d8d 100644 --- a/packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.ts +++ b/packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.ts @@ -1,26 +1,8 @@ import { MessageResolutionError } from '../../errors.js'; import type { Context } from '../../format-context.js'; import { fallback } from '../../runtime/index.js'; -import type { Literal } from './literal.js'; +import type { Literal, UnsupportedAnnotation, VariableRef } from '../types.js'; import { getValueSource } from './value.js'; -import type { VariableRef } from './variable-ref.js'; - -/** - * When the parser encounters an expression with reserved syntax, - * it emits an UnsupportedAnnotation to represent it. - * - * @remarks - * As the meaning of this syntax is not supported, - * it will always resolve with a fallback representation and emit an error. - * - * @beta - */ -export interface UnsupportedAnnotation { - type: 'unsupported-annotation'; - sigil: '!' | '@' | '#' | '%' | '^' | '&' | '*' | '<' | '>' | '?' | '~' | '�'; - source: string; - name?: never; -} export function resolveUnsupportedAnnotation( ctx: Context, diff --git a/packages/mf2-messageformat/src/data-model/expression/value.ts b/packages/mf2-messageformat/src/data-model/expression/value.ts index d55b30c1..2e08b707 100644 --- a/packages/mf2-messageformat/src/data-model/expression/value.ts +++ b/packages/mf2-messageformat/src/data-model/expression/value.ts @@ -1,6 +1,6 @@ import type { Context } from '../../format-context'; -import type { Literal } from './literal'; -import { VariableRef, lookupVariableRef } from './variable-ref'; +import type { Literal, VariableRef } from '../types'; +import { lookupVariableRef } from './variable-ref'; /** @internal */ export function resolveValue( diff --git a/packages/mf2-messageformat/src/data-model/expression/variable-ref.ts b/packages/mf2-messageformat/src/data-model/expression/variable-ref.ts index 49a22d91..85daafe6 100644 --- a/packages/mf2-messageformat/src/data-model/expression/variable-ref.ts +++ b/packages/mf2-messageformat/src/data-model/expression/variable-ref.ts @@ -6,25 +6,8 @@ import { fallback, unknown } from '../../runtime/index.js'; -import { type Expression, resolveExpression } from './index.js'; - -/** - * The value of a VariableRef is defined by the current Scope. - * - * @remarks - * To refer to an inner property of an object value, use `.` as a separator; - * in case of conflict, the longest starting substring wins. - * For example, `'user.name'` would be first matched by an exactly matching top-level key, - * and in case that fails, with the `'name'` property of the `'user'` object: - * The runtime scopes `{ 'user.name': 'Kat' }` and `{ user: { name: 'Kat' } }` - * would both resolve a `'user.name'` VariableRef as the string `'Kat'`. - * - * @beta - */ -export interface VariableRef { - type: 'variable'; - name: string; -} +import type { Expression, VariableRef } from '../types.js'; +import { resolveExpression } from './index.js'; /** * Declarations aren't resolved until they're requierd, diff --git a/packages/mf2-messageformat/src/data-model/index.ts b/packages/mf2-messageformat/src/data-model/index.ts deleted file mode 100644 index 702a9143..00000000 --- a/packages/mf2-messageformat/src/data-model/index.ts +++ /dev/null @@ -1,111 +0,0 @@ -import type { Expression, Literal, VariableRef } from './expression/index.js'; -import type { Markup } from './markup.js'; - -export type { - FunctionAnnotation, - Option, - UnsupportedAnnotation -} from './expression/index.js'; -export type { MarkupClose, MarkupOpen, MarkupStandalone } from './markup.js'; -export type { Expression, Literal, Markup, VariableRef }; - -/** - * The representation of a single message. - * The shape of the message is an implementation detail, - * and may vary for the same message in different languages. - * - * @beta - */ -export type Message = PatternMessage | SelectMessage; - -/** - * A single message with no variants. - * - * @beta - */ -export interface PatternMessage { - type: 'message'; - declarations: Declaration[]; - pattern: Pattern; - comment?: string; -} - -/** - * A message may declare any number of input and local variables, - * each with a value defined by an expression. - * The variable name for each declaration must be unique. - * - * @beta - */ -export type Declaration = - | InputDeclaration - | LocalDeclaration - | UnsupportedStatement; - -export interface InputDeclaration { - type: 'input'; - name: string; - value: Expression; -} - -export interface LocalDeclaration { - type: 'local'; - name: string; - value: Expression; -} - -export interface UnsupportedStatement { - type: 'unsupported-statement'; - keyword: string; - name?: never; - value?: never; - body?: string; - expressions: (Expression | Markup)[]; -} - -/** - * The body of each message is composed of a sequence of parts, some of them - * fixed (Text), others placeholders for values depending on additional - * data. - * - * @beta - */ -export interface Pattern { - body: Array; -} - -/** - * SelectMessage generalises the plural, selectordinal and select - * argument types of MessageFormat 1. - * Each case is defined by a key of one or more string identifiers, - * and selection between them is made according to - * the values of a corresponding number of expressions. - * Selection iterates among the `variants` in order, - * and terminates when all of the Variant keys match. - * The result of the selection is always a single Pattern. - * - * @beta - */ -export interface SelectMessage { - type: 'select'; - declarations: Declaration[]; - selectors: Expression[]; - variants: Variant[]; - comment?: string; -} - -/** @beta */ -export interface Variant { - keys: Array; - value: Pattern; -} - -/** - * The catch-all key matches all values. - * - * @beta - */ -export interface CatchallKey { - type: '*'; - value?: string; -} diff --git a/packages/mf2-messageformat/src/data-model/markup.ts b/packages/mf2-messageformat/src/data-model/markup.ts index 5f8d0d27..92300328 100644 --- a/packages/mf2-messageformat/src/data-model/markup.ts +++ b/packages/mf2-messageformat/src/data-model/markup.ts @@ -4,43 +4,7 @@ import type { MessageMarkupClosePart, MessageMarkupPart } from '../runtime/index.js'; -import type { Option } from './index.js'; - -/** - * Markup placeholders can span ranges of other pattern elements, - * or represent other inline elements. - * - * @remarks - * The `name` identifies the markup part, - * which will be included in the result along with any `options`. - * - * When formatted to string, all markup will format as an empty string. - * To use markup, format to parts and post-process the formatted results. - * - * @beta - */ -export type Markup = MarkupOpen | MarkupStandalone | MarkupClose; - -export interface MarkupOpen { - type: 'markup'; - kind: 'open'; - name: string; - options?: Option[]; -} - -export interface MarkupStandalone { - type: 'markup'; - kind: 'standalone'; - name: string; - options?: Option[]; -} - -export interface MarkupClose { - type: 'markup'; - kind: 'close'; - name: string; - options?: never; -} +import type { Markup } from './types.js'; export function formatMarkup( ctx: Context, diff --git a/packages/mf2-messageformat/src/data-model/type-guards.ts b/packages/mf2-messageformat/src/data-model/type-guards.ts index 9d69b281..b5a58a4d 100644 --- a/packages/mf2-messageformat/src/data-model/type-guards.ts +++ b/packages/mf2-messageformat/src/data-model/type-guards.ts @@ -1,17 +1,17 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { FunctionAnnotation } from './expression/function-annotation.js'; -import type { Expression } from './expression/index.js'; -import type { Literal } from './expression/literal.js'; -import type { UnsupportedAnnotation } from './expression/unsupported-annotation.js'; -import type { VariableRef } from './expression/variable-ref.js'; +import type { FunctionAnnotation } from './types.js'; +import type { Expression } from './types.js'; +import type { Literal } from './types.js'; +import type { UnsupportedAnnotation } from './types.js'; +import type { VariableRef } from './types.js'; import type { CatchallKey, Message, PatternMessage, SelectMessage -} from './index.js'; -import type { Markup } from './markup.js'; +} from './types.js'; +import type { Markup } from './types.js'; /** @beta */ export const isCatchallKey = (key: any): key is CatchallKey => diff --git a/packages/mf2-messageformat/src/data-model/types.ts b/packages/mf2-messageformat/src/data-model/types.ts new file mode 100644 index 00000000..e865e994 --- /dev/null +++ b/packages/mf2-messageformat/src/data-model/types.ts @@ -0,0 +1,238 @@ +/** + * The representation of a single message. + * The shape of the message is an implementation detail, + * and may vary for the same message in different languages. + * + * @beta + */ +export type Message = PatternMessage | SelectMessage; + +/** + * A single message with no variants. + * + * @beta + */ +export interface PatternMessage { + type: 'message'; + declarations: Declaration[]; + pattern: Pattern; + comment?: string; +} + +/** + * A message may declare any number of input and local variables, + * each with a value defined by an expression. + * The variable name for each declaration must be unique. + * + * @beta + */ +export type Declaration = + | InputDeclaration + | LocalDeclaration + | UnsupportedStatement; + +export interface InputDeclaration { + type: 'input'; + name: string; + value: Expression; +} + +export interface LocalDeclaration { + type: 'local'; + name: string; + value: Expression; +} + +export interface UnsupportedStatement { + type: 'unsupported-statement'; + keyword: string; + name?: never; + value?: never; + body?: string; + expressions: (Expression | Markup)[]; +} + +/** + * SelectMessage generalises the plural, selectordinal and select + * argument types of MessageFormat 1. + * Each case is defined by a key of one or more string identifiers, + * and selection between them is made according to + * the values of a corresponding number of expressions. + * Selection iterates among the `variants` in order, + * and terminates when all of the Variant keys match. + * The result of the selection is always a single Pattern. + * + * @beta + */ +export interface SelectMessage { + type: 'select'; + declarations: Declaration[]; + selectors: Expression[]; + variants: Variant[]; + comment?: string; +} + +/** @beta */ +export interface Variant { + keys: Array; + value: Pattern; +} + +/** + * The catch-all key matches all values. + * + * @beta + */ +export interface CatchallKey { + type: '*'; + value?: string; +} + +/** + * The body of each message is composed of a sequence of parts, some of them + * fixed (Text), others placeholders for values depending on additional + * data. + * + * @beta + */ +export interface Pattern { + body: Array; +} + +/** + * Expressions are used in declarations, as selectors, and as placeholders. + * Each must include at least an `arg` or an `annotation`, or both. + * + * @beta + */ +export type Expression< + A extends Literal | VariableRef | undefined = + | Literal + | VariableRef + | undefined +> = A extends Literal | VariableRef + ? { + type: 'expression'; + arg: A; + annotation?: FunctionAnnotation | UnsupportedAnnotation; + } + : { + type: 'expression'; + arg?: never; + annotation: FunctionAnnotation | UnsupportedAnnotation; + }; + +/** + * An immediately defined value. + * + * @remarks + * Always contains a string value. In Function arguments and options, + * the expeted type of the value may result in the value being + * further parsed as a boolean or a number. + * + * @beta + */ +export interface Literal { + type: 'literal'; + value: string; +} + +/** + * The value of a VariableRef is defined by the current Scope. + * + * @remarks + * To refer to an inner property of an object value, use `.` as a separator; + * in case of conflict, the longest starting substring wins. + * For example, `'user.name'` would be first matched by an exactly matching top-level key, + * and in case that fails, with the `'name'` property of the `'user'` object: + * The runtime scopes `{ 'user.name': 'Kat' }` and `{ user: { name: 'Kat' } }` + * would both resolve a `'user.name'` VariableRef as the string `'Kat'`. + * + * @beta + */ +export interface VariableRef { + type: 'variable'; + name: string; +} + +/** + * To resolve a FunctionAnnotation, an externally defined function is called. + * + * @remarks + * The `name` identifies a function that takes in the arguments `args`, the + * current locale, as well as any `options`, and returns some corresponding + * output. Likely functions available by default would include `'plural'` for + * determining the plural category of a numeric value, as well as `'number'` + * and `'date'` for formatting values. + * + * @beta + */ +export interface FunctionAnnotation { + type: 'function'; + name: string; + options?: Option[]; +} + +/** + * When the parser encounters an expression with reserved syntax, + * it emits an UnsupportedAnnotation to represent it. + * + * @remarks + * As the meaning of this syntax is not supported, + * it will always resolve with a fallback representation and emit an error. + * + * @beta + */ +export interface UnsupportedAnnotation { + type: 'unsupported-annotation'; + sigil: '!' | '@' | '#' | '%' | '^' | '&' | '*' | '<' | '>' | '?' | '~' | '�'; + source: string; + name?: never; +} + +/** + * Markup placeholders can span ranges of other pattern elements, + * or represent other inline elements. + * + * @remarks + * The `name` identifies the markup part, + * which will be included in the result along with any `options`. + * + * When formatted to string, all markup will format as an empty string. + * To use markup, format to parts and post-process the formatted results. + * + * @beta + */ +export type Markup = MarkupOpen | MarkupStandalone | MarkupClose; + +export interface MarkupOpen { + type: 'markup'; + kind: 'open'; + name: string; + options?: Option[]; +} + +export interface MarkupStandalone { + type: 'markup'; + kind: 'standalone'; + name: string; + options?: Option[]; +} + +export interface MarkupClose { + type: 'markup'; + kind: 'close'; + name: string; + options?: never; +} + +/** + * The options of {@link FunctionAnnotation} and {@link Markup} + * are expressed as `key`/`value` pairs to allow their order to be maintained. + * + * @beta + */ +export interface Option { + name: string; + value: Literal | VariableRef; +} diff --git a/packages/mf2-messageformat/src/extra/validate.ts b/packages/mf2-messageformat/src/extra/validate.ts index a053ed51..11bd9579 100644 --- a/packages/mf2-messageformat/src/extra/validate.ts +++ b/packages/mf2-messageformat/src/extra/validate.ts @@ -1,5 +1,5 @@ -import type { Message, Pattern } from '../data-model'; -import { isExpression, isFunctionAnnotation } from '../expression/index.js'; +import type { Message, Pattern } from '../data-model/types.js'; +import { isExpression, isFunctionAnnotation } from '../data-model/type-guards.js'; import type { MessageFunctions } from '../runtime'; function validateParts(parts: Pattern['body'], functions: MessageFunctions) { diff --git a/packages/mf2-messageformat/src/index.ts b/packages/mf2-messageformat/src/index.ts index 639b5636..4858ffb2 100644 --- a/packages/mf2-messageformat/src/index.ts +++ b/packages/mf2-messageformat/src/index.ts @@ -1,6 +1,6 @@ export { asDataModel, parseMessage, type CST } from './cst-parser/index.js'; -export * from './data-model/index.js'; -export * from './data-model/expression/index.js'; +export { getMessageValue } from './data-model/expression/variable-ref'; +export type * from './data-model/types.js'; export * from './data-model/type-guards.js'; export { MessageDataModelError, diff --git a/packages/mf2-messageformat/src/messageformat.ts b/packages/mf2-messageformat/src/messageformat.ts index 5af216aa..bdaa917f 100644 --- a/packages/mf2-messageformat/src/messageformat.ts +++ b/packages/mf2-messageformat/src/messageformat.ts @@ -1,12 +1,10 @@ import { asDataModel, parseMessage } from './cst-parser/index.js'; -import type { Message } from './data-model/index.js'; +import { resolveExpression } from './data-model/expression/index.js'; +import { UnresolvedExpression } from './data-model/expression/variable-ref.js'; +import { formatMarkup } from './data-model/markup.js'; +import type { Message } from './data-model/types.js'; import { MessageError, MessageResolutionError } from './errors.js'; import type { Context } from './format-context'; -import { - UnresolvedExpression, - resolveExpression -} from './data-model/expression/index.js'; -import { formatMarkup } from './data-model/markup.js'; import { MessageFunctions, MessagePart, diff --git a/packages/mf2-messageformat/src/select-pattern.ts b/packages/mf2-messageformat/src/select-pattern.ts index b3887dcd..f3896850 100644 --- a/packages/mf2-messageformat/src/select-pattern.ts +++ b/packages/mf2-messageformat/src/select-pattern.ts @@ -1,6 +1,6 @@ -import type { Message, Pattern } from './data-model/index.js'; -import { MessageSelectionError } from './errors'; import { resolveExpression } from './data-model/expression/index.js'; +import type { Message, Pattern } from './data-model/types.js'; +import { MessageSelectionError } from './errors'; import type { Context } from './format-context'; export function selectPattern( diff --git a/packages/mf2-messageformat/src/stringifier/message.ts b/packages/mf2-messageformat/src/stringifier/message.ts index 906fcc2b..015818b8 100644 --- a/packages/mf2-messageformat/src/stringifier/message.ts +++ b/packages/mf2-messageformat/src/stringifier/message.ts @@ -1,5 +1,11 @@ import { isValidUnquotedLiteral } from '../cst-parser/names.js'; import { + isLiteral, + isPatternMessage, + isSelectMessage, + isVariableRef +} from '../data-model/type-guards.js'; +import type { Declaration, Expression, FunctionAnnotation, @@ -10,13 +16,7 @@ import { Pattern, UnsupportedAnnotation, VariableRef -} from '../data-model/index.js'; -import { - isLiteral, - isPatternMessage, - isSelectMessage, - isVariableRef -} from '../data-model/type-guards.js'; +} from '../data-model/types.js'; import { MessageFormat } from '../messageformat.js'; /** From af3c1bb6dcb03dad7916778aebd722d532140ad6 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 2 Jan 2024 18:03:12 +0200 Subject: [PATCH 21/44] feat(mf2): Require annotations on all selectors --- packages/mf2-icu-mf1/src/mf1.test.ts | 11 ++- .../src/__fixtures/test-messages.json | 73 ++++++++++++------- .../src/cst-parser/message.ts | 37 ++++++++++ .../src/data-model/expression/variable-ref.ts | 12 +-- packages/mf2-messageformat/src/errors.ts | 5 +- 5 files changed, 100 insertions(+), 38 deletions(-) diff --git a/packages/mf2-icu-mf1/src/mf1.test.ts b/packages/mf2-icu-mf1/src/mf1.test.ts index 780f8af9..cf739edb 100644 --- a/packages/mf2-icu-mf1/src/mf1.test.ts +++ b/packages/mf2-icu-mf1/src/mf1.test.ts @@ -169,8 +169,7 @@ export const testCases: Record = { exp: [ [{ FEELING: 'a' }, 'I am happy.'], [{ FEELING: 'b' }, 'I am sad.'], - [{ FEELING: 'q' }, 'I am indifferent.'], - [{}, 'I am indifferent.'] + [{ FEELING: 'q' }, 'I am indifferent.'] ] }, { @@ -249,19 +248,19 @@ export const testCases: Record = { 'Kit added himself and 2 other people to his group.' ], [ - { PLURAL_NUM_PEOPLE: 0, PERSON: 'Kot' }, + { PLURAL_NUM_PEOPLE: 0, PERSON: 'Kot', GENDER: 'other' }, 'Kot added no one to their group.' ], [ - { PLURAL_NUM_PEOPLE: 1, PERSON: 'Kot' }, + { PLURAL_NUM_PEOPLE: 1, PERSON: 'Kot', GENDER: 'other' }, 'Kot added just themself to their group.' ], [ - { PLURAL_NUM_PEOPLE: 2, PERSON: 'Kot' }, + { PLURAL_NUM_PEOPLE: 2, PERSON: 'Kot', GENDER: 'other' }, 'Kot added themself and one other person to their group.' ], [ - { PLURAL_NUM_PEOPLE: 3, PERSON: 'Kot' }, + { PLURAL_NUM_PEOPLE: 3, PERSON: 'Kot', GENDER: 'other' }, 'Kot added themself and 2 other people to their group.' ] ] diff --git a/packages/mf2-messageformat/src/__fixtures/test-messages.json b/packages/mf2-messageformat/src/__fixtures/test-messages.json index 92a7e682..96d8fa91 100644 --- a/packages/mf2-messageformat/src/__fixtures/test-messages.json +++ b/packages/mf2-messageformat/src/__fixtures/test-messages.json @@ -159,93 +159,107 @@ "exp": "42 42" }, { - "src": ".match {$foo} |1| {{one}} * {{other}}", + "src": ".match {$foo :string} |1| {{one}} * {{other}}", "params": { "foo": "1" }, "exp": "one" }, { - "src": ".match {$foo} 1 {{one}} * {{other}}", + "src": ".match {$foo :string} 1 {{one}} * {{other}}", "params": { "foo": 1 }, "exp": "one" }, { - "src": ".match {$foo} 1 {{one}} * {{other}}", + "src": ".match {$foo :string} 1 {{one}} * {{other}}", "params": { "foo": null }, - "exp": "other", - "errors": [{ "type": "not-selectable" }] + "exp": "other" }, { - "src": ".match {$foo} 1 {{one}} * {{other}}", + "src": ".match {$foo :string} 1 {{one}} * {{other}}", "exp": "other", - "errors": [{ "type": "unresolved-var" }, { "type": "not-selectable" }] + "errors": [{ "type": "unresolved-var" }] }, { - "src": ".match {$foo} one {{one}} * {{other}}", + "src": ".match {$foo :number} one {{one}} * {{other}}", "params": { "foo": 1 }, "exp": "one" }, { - "src": ".match {$foo} 1 {{=1}} one {{one}} * {{other}}", + "src": ".match {$foo :number} 1 {{=1}} one {{one}} * {{other}}", "params": { "foo": 1 }, "exp": "=1" }, { - "src": ".match {$foo} one {{one}} 1 {{=1}} * {{other}}", + "src": ".match {$foo :number} one {{one}} 1 {{=1}} * {{other}}", "params": { "foo": 1 }, "exp": "=1" }, { - "src": ".match {$foo} {$bar} one one {{one one}} one * {{one other}} * * {{other}}", + "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", "params": { "foo": 1, "bar": 1 }, "exp": "one one" }, { - "src": ".match {$foo} {$bar} one one {{one one}} one * {{one other}} * * {{other}}", + "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", "params": { "foo": 1, "bar": 2 }, "exp": "one other" }, { - "src": ".match {$foo} {$bar} one one {{one one}} one * {{one other}} * * {{other}}", + "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", "params": { "foo": 2, "bar": 2 }, "exp": "other" }, + { + "src": ".input {$foo :number} .match {$foo} one {{one}} * {{other}}", + "params": { "foo": 1 }, + "exp": "one" + }, { "src": ".local $foo = {$bar :number} .match {$foo} one {{one}} * {{other}}", "params": { "bar": 1 }, "exp": "one" }, { - "src": ".local $foo = {$bar :number} .match {$foo} one {{one}} * {{other}}", + "src": ".input {$foo :number } .local $bar = {$foo} .match {$bar} one {{one}} * {{other}}", + "params": { "foo": 1 }, + "exp": "one" + }, + { + "src": ".input {$bar :number} .match {$bar} one {{one}} * {{other}}", "params": { "bar": 2 }, "exp": "other" }, { - "src": ".local $foo = {$bar} .match {$foo :number} one {{one}} * {{other}}", + "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", "params": { "bar": 1 }, "exp": "one" }, { - "src": ".local $foo = {$bar} .match {$foo :number} one {{one}} * {{other}}", + "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", "params": { "bar": 2 }, "exp": "other" }, { - "src": ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}", + "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", "params": { "bar": 1 }, "exp": "one" }, { - "src": ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}", + "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", "params": { "bar": 2 }, "exp": "other" }, { - "src": ".local $bar = {$none} .match {$foo} one {{one}} * {{{$bar}}}", + "src": ".input {$none} .match {$foo :number} one {{one}} * {{{$none}}}", + "params": { "foo": 1 }, + "exp": "one" + }, + { + "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}", "params": { "foo": 1 }, "exp": "one" }, { - "src": ".local $bar = {$none} .match {$foo} one {{one}} * {{{$bar}}}", + "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}", "params": { "foo": 2 }, "exp": "{$none}", "errors": [{ "type": "unresolved-var" }] @@ -350,11 +364,20 @@ { "src": "no {|literal} end", "syntaxError": true }, { "src": "no {|literal or placeholder end", "syntaxError": true }, { "src": ".match {} * {{foo}}", "syntaxError": true }, - { "src": ".match {|foo|} {|bar|} ** {{foo}}", "syntaxError": true }, + { "src": ".match {|foo| :x} {|bar| :x} ** {{foo}}", "syntaxError": true }, { "src": ".match * {{foo}}", "syntaxError": true }, - { "src": ".match {|x|} * foo", "syntaxError": true }, - { "src": ".match {|x|} * {{foo}} extra", "syntaxError": true }, + { "src": ".match {|x| :x} * foo", "syntaxError": true }, + { "src": ".match {|x| :x} * {{foo}} extra", "syntaxError": true }, { "src": ".match |x| * {{foo}}", "syntaxError": true }, - { "src": ".match {$foo} * * {{foo}}", "syntaxError": true }, - { "src": ".match {$foo} {$bar} * {{foo}}", "syntaxError": true } + { "src": ".match {$foo :x} * * {{foo}}", "syntaxError": true }, + { "src": ".match {$foo :x} {$bar :x} * {{foo}}", "syntaxError": true }, + { "src": ".match {$foo} one {{one}} * {{other}}", "syntaxError": true }, + { + "src": ".input {$foo} .match {$foo} one {{one}} * {{other}}", + "syntaxError": true + }, + { + "src": ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}", + "syntaxError": true + } ] diff --git a/packages/mf2-messageformat/src/cst-parser/message.ts b/packages/mf2-messageformat/src/cst-parser/message.ts index f92b7ef4..9ca0b230 100644 --- a/packages/mf2-messageformat/src/cst-parser/message.ts +++ b/packages/mf2-messageformat/src/cst-parser/message.ts @@ -105,6 +105,43 @@ function parseSelectMessage( if (body && body.type !== 'function') { ctx.onError('bad-selector', body.start, body.end); } + if (!sel.annotation) { + let hasAnnotation = false; + let arg = sel.arg; + loop: while (arg?.type === 'variable') { + for (const decl of declarations) { + switch (decl.type) { + case 'input': + if ( + decl.value.type === 'expression' && + decl.value.arg?.type === 'variable' && + decl.value.arg.name === arg.name + ) { + hasAnnotation = Boolean(decl.value.annotation); + break loop; + } + break; + case 'local': + if ( + decl.target.name === arg.name && + decl.value.type === 'expression' + ) { + if (decl.value.annotation) { + hasAnnotation = true; + break loop; + } + arg = decl.value.arg; + continue loop; + } + break; + } + } + break loop; + } + if (!hasAnnotation) { + ctx.onError('missing-selector-annotation', sel.start, sel.end); + } + } selectors.push(sel); pos = sel.end; pos += whitespaces(ctx.source, pos); diff --git a/packages/mf2-messageformat/src/data-model/expression/variable-ref.ts b/packages/mf2-messageformat/src/data-model/expression/variable-ref.ts index 85daafe6..ca991321 100644 --- a/packages/mf2-messageformat/src/data-model/expression/variable-ref.ts +++ b/packages/mf2-messageformat/src/data-model/expression/variable-ref.ts @@ -51,7 +51,11 @@ function getValue(scope: unknown, name: string): unknown { export function lookupVariableRef(ctx: Context, { name }: VariableRef) { const value = getValue(ctx.scope, name); - if (value instanceof UnresolvedExpression) { + if (value === undefined) { + const source = '$' + name; + const msg = `Variable not available: ${source}`; + ctx.onError(new MessageResolutionError('unresolved-var', msg, source)); + } else if (value instanceof UnresolvedExpression) { const local = resolveExpression( value.scope ? { ...ctx, scope: value.scope } : ctx, value.expression @@ -93,9 +97,5 @@ export function getMessageValue( export function resolveVariableRef(ctx: Context, ref: VariableRef) { const source = '$' + ref.name; const value = lookupVariableRef(ctx, ref); - const mv = getMessageValue(ctx, source, value); - if (mv !== undefined) return mv; - const msg = `Variable not available: ${source}`; - ctx.onError(new MessageResolutionError('unresolved-var', msg, source)); - return fallback(source); + return getMessageValue(ctx, source, value) ?? fallback(source); } diff --git a/packages/mf2-messageformat/src/errors.ts b/packages/mf2-messageformat/src/errors.ts index 415bd56c..f79ec825 100644 --- a/packages/mf2-messageformat/src/errors.ts +++ b/packages/mf2-messageformat/src/errors.ts @@ -25,6 +25,7 @@ export class MessageSyntaxError extends MessageError { | 'key-mismatch' | 'parse-error' | 'missing-fallback' + | 'missing-selector-annotation' | 'missing-syntax'; start: number; end: number; @@ -54,6 +55,7 @@ export class MessageSyntaxError extends MessageError { case 'forward-reference': case 'key-mismatch': case 'missing-fallback': + case 'missing-selector-annotation': message = `Data model error: ${type}`; if (start >= 0) message += ` at ${start}`; } @@ -78,7 +80,8 @@ export class MessageDataModelError extends MessageSyntaxError { | 'duplicate-option' | 'forward-reference' | 'key-mismatch' - | 'missing-fallback'; + | 'missing-fallback' + | 'missing-selector-annotation'; } export class MessageResolutionError extends MessageError { From 9780f6f55fcf007e780bd774bd97c598d59140f2 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 2 Jan 2024 18:20:53 +0200 Subject: [PATCH 22/44] refactor(mf2): Rename src/data-model/ files, drop getMessageValue export --- .../{markup.ts => format-markup.ts} | 2 +- .../function-annotation.test.ts | 6 +--- .../{expression => }/literal.test.ts | 2 +- .../index.ts => resolve-expression.ts} | 14 ++++---- ...tion.ts => resolve-function-annotation.ts} | 10 +++--- .../literal.ts => resolve-literal.ts} | 6 ++-- ...n.ts => resolve-unsupported-annotation.ts} | 10 +++--- .../{expression/value.ts => resolve-value.ts} | 6 ++-- .../variable-ref.ts => resolve-variable.ts} | 36 ++++++++++--------- .../unsupported-annotation.test.ts | 2 +- .../{expression => }/variable.test.ts | 2 +- packages/mf2-messageformat/src/index.ts | 1 - .../mf2-messageformat/src/messageformat.ts | 8 ++--- .../mf2-messageformat/src/select-pattern.ts | 2 +- 14 files changed, 52 insertions(+), 55 deletions(-) rename packages/mf2-messageformat/src/data-model/{markup.ts => format-markup.ts} (94%) rename packages/mf2-messageformat/src/data-model/{expression => }/function-annotation.test.ts (97%) rename packages/mf2-messageformat/src/data-model/{expression => }/literal.test.ts (96%) rename packages/mf2-messageformat/src/data-model/{expression/index.ts => resolve-expression.ts} (57%) rename packages/mf2-messageformat/src/data-model/{expression/function-annotation.ts => resolve-function-annotation.ts} (81%) rename packages/mf2-messageformat/src/data-model/{expression/literal.ts => resolve-literal.ts} (52%) rename packages/mf2-messageformat/src/data-model/{expression/unsupported-annotation.ts => resolve-unsupported-annotation.ts} (64%) rename packages/mf2-messageformat/src/data-model/{expression/value.ts => resolve-value.ts} (79%) rename packages/mf2-messageformat/src/data-model/{expression/variable-ref.ts => resolve-variable.ts} (82%) rename packages/mf2-messageformat/src/data-model/{expression => }/unsupported-annotation.test.ts (96%) rename packages/mf2-messageformat/src/data-model/{expression => }/variable.test.ts (98%) diff --git a/packages/mf2-messageformat/src/data-model/markup.ts b/packages/mf2-messageformat/src/data-model/format-markup.ts similarity index 94% rename from packages/mf2-messageformat/src/data-model/markup.ts rename to packages/mf2-messageformat/src/data-model/format-markup.ts index 92300328..1fea4e0a 100644 --- a/packages/mf2-messageformat/src/data-model/markup.ts +++ b/packages/mf2-messageformat/src/data-model/format-markup.ts @@ -1,4 +1,4 @@ -import { resolveValue } from './expression/value.js'; +import { resolveValue } from './resolve-value.js'; import type { Context } from '../format-context.js'; import type { MessageMarkupClosePart, diff --git a/packages/mf2-messageformat/src/data-model/expression/function-annotation.test.ts b/packages/mf2-messageformat/src/data-model/function-annotation.test.ts similarity index 97% rename from packages/mf2-messageformat/src/data-model/expression/function-annotation.test.ts rename to packages/mf2-messageformat/src/data-model/function-annotation.test.ts index 84607410..36f49337 100644 --- a/packages/mf2-messageformat/src/data-model/expression/function-annotation.test.ts +++ b/packages/mf2-messageformat/src/data-model/function-annotation.test.ts @@ -1,8 +1,4 @@ -import { - MessageFormat, - MessageFunctions, - MessageNumberPart -} from '../../index'; +import { MessageFormat, MessageFunctions, MessageNumberPart } from '../index'; test('Custom function', () => { const functions = { diff --git a/packages/mf2-messageformat/src/data-model/expression/literal.test.ts b/packages/mf2-messageformat/src/data-model/literal.test.ts similarity index 96% rename from packages/mf2-messageformat/src/data-model/expression/literal.test.ts rename to packages/mf2-messageformat/src/data-model/literal.test.ts index f6ddeb5d..3a7c2771 100644 --- a/packages/mf2-messageformat/src/data-model/expression/literal.test.ts +++ b/packages/mf2-messageformat/src/data-model/literal.test.ts @@ -1,4 +1,4 @@ -import { MessageFormat } from '../../index'; +import { MessageFormat } from '../index'; function resolve(source: string, errors: any[] = []) { const mf = new MessageFormat(source); diff --git a/packages/mf2-messageformat/src/data-model/expression/index.ts b/packages/mf2-messageformat/src/data-model/resolve-expression.ts similarity index 57% rename from packages/mf2-messageformat/src/data-model/expression/index.ts rename to packages/mf2-messageformat/src/data-model/resolve-expression.ts index 8bdc654f..57d2b591 100644 --- a/packages/mf2-messageformat/src/data-model/expression/index.ts +++ b/packages/mf2-messageformat/src/data-model/resolve-expression.ts @@ -1,10 +1,10 @@ -import type { Context } from '../../format-context'; -import type { MessageValue } from '../../runtime'; -import type { Expression } from '../types'; -import { resolveFunctionAnnotation } from './function-annotation'; -import { resolveLiteral } from './literal'; -import { resolveUnsupportedAnnotation } from './unsupported-annotation'; -import { resolveVariableRef } from './variable-ref'; +import type { Context } from '../format-context'; +import type { MessageValue } from '../runtime'; +import type { Expression } from './types'; +import { resolveFunctionAnnotation } from './resolve-function-annotation.js'; +import { resolveLiteral } from './resolve-literal.js'; +import { resolveUnsupportedAnnotation } from './resolve-unsupported-annotation.js'; +import { resolveVariableRef } from './resolve-variable.js'; export function resolveExpression( ctx: Context, diff --git a/packages/mf2-messageformat/src/data-model/expression/function-annotation.ts b/packages/mf2-messageformat/src/data-model/resolve-function-annotation.ts similarity index 81% rename from packages/mf2-messageformat/src/data-model/expression/function-annotation.ts rename to packages/mf2-messageformat/src/data-model/resolve-function-annotation.ts index 89f3afe2..949140de 100644 --- a/packages/mf2-messageformat/src/data-model/expression/function-annotation.ts +++ b/packages/mf2-messageformat/src/data-model/resolve-function-annotation.ts @@ -1,13 +1,13 @@ -import { MessageError } from '../../errors.js'; -import type { Context } from '../../format-context.js'; -import { MessageFunctionContext, fallback } from '../../runtime/index.js'; +import { MessageError } from '../errors.js'; +import type { Context } from '../format-context.js'; +import { MessageFunctionContext, fallback } from '../runtime/index.js'; import type { FunctionAnnotation, Literal, Option, VariableRef -} from '../types.js'; -import { getValueSource, resolveValue } from './value.js'; +} from './types.js'; +import { getValueSource, resolveValue } from './resolve-value.js'; export function resolveFunctionAnnotation( ctx: Context, diff --git a/packages/mf2-messageformat/src/data-model/expression/literal.ts b/packages/mf2-messageformat/src/data-model/resolve-literal.ts similarity index 52% rename from packages/mf2-messageformat/src/data-model/expression/literal.ts rename to packages/mf2-messageformat/src/data-model/resolve-literal.ts index 04b03b4d..45e028a4 100644 --- a/packages/mf2-messageformat/src/data-model/expression/literal.ts +++ b/packages/mf2-messageformat/src/data-model/resolve-literal.ts @@ -1,6 +1,6 @@ -import type { Context } from '../../format-context.js'; -import { MessageFunctionContext } from '../../runtime/index.js'; -import type { Literal } from '../types.js'; +import type { Context } from '../format-context.js'; +import { MessageFunctionContext } from '../runtime/index.js'; +import type { Literal } from './types.js'; export function resolveLiteral(ctx: Context, lit: Literal) { const msgCtx = new MessageFunctionContext(ctx, `|${lit.value}|`); diff --git a/packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.ts b/packages/mf2-messageformat/src/data-model/resolve-unsupported-annotation.ts similarity index 64% rename from packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.ts rename to packages/mf2-messageformat/src/data-model/resolve-unsupported-annotation.ts index 9e623d8d..55ae87e7 100644 --- a/packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.ts +++ b/packages/mf2-messageformat/src/data-model/resolve-unsupported-annotation.ts @@ -1,8 +1,8 @@ -import { MessageResolutionError } from '../../errors.js'; -import type { Context } from '../../format-context.js'; -import { fallback } from '../../runtime/index.js'; -import type { Literal, UnsupportedAnnotation, VariableRef } from '../types.js'; -import { getValueSource } from './value.js'; +import { MessageResolutionError } from '../errors.js'; +import type { Context } from '../format-context.js'; +import { fallback } from '../runtime/index.js'; +import type { Literal, UnsupportedAnnotation, VariableRef } from './types.js'; +import { getValueSource } from './resolve-value.js'; export function resolveUnsupportedAnnotation( ctx: Context, diff --git a/packages/mf2-messageformat/src/data-model/expression/value.ts b/packages/mf2-messageformat/src/data-model/resolve-value.ts similarity index 79% rename from packages/mf2-messageformat/src/data-model/expression/value.ts rename to packages/mf2-messageformat/src/data-model/resolve-value.ts index 2e08b707..7d13ac26 100644 --- a/packages/mf2-messageformat/src/data-model/expression/value.ts +++ b/packages/mf2-messageformat/src/data-model/resolve-value.ts @@ -1,6 +1,6 @@ -import type { Context } from '../../format-context'; -import type { Literal, VariableRef } from '../types'; -import { lookupVariableRef } from './variable-ref'; +import type { Context } from '../format-context'; +import type { Literal, VariableRef } from './types'; +import { lookupVariableRef } from './resolve-variable.js'; /** @internal */ export function resolveValue( diff --git a/packages/mf2-messageformat/src/data-model/expression/variable-ref.ts b/packages/mf2-messageformat/src/data-model/resolve-variable.ts similarity index 82% rename from packages/mf2-messageformat/src/data-model/expression/variable-ref.ts rename to packages/mf2-messageformat/src/data-model/resolve-variable.ts index ca991321..274c1f66 100644 --- a/packages/mf2-messageformat/src/data-model/expression/variable-ref.ts +++ b/packages/mf2-messageformat/src/data-model/resolve-variable.ts @@ -1,13 +1,13 @@ -import { MessageResolutionError } from '../../errors.js'; -import type { Context } from '../../format-context.js'; +import { MessageResolutionError } from '../errors.js'; +import type { Context } from '../format-context.js'; import { MessageFunctionContext, MessageValue, fallback, unknown -} from '../../runtime/index.js'; -import type { Expression, VariableRef } from '../types.js'; -import { resolveExpression } from './index.js'; +} from '../runtime/index.js'; +import type { Expression, VariableRef } from './types.js'; +import { resolveExpression } from './resolve-expression.js'; /** * Declarations aren't resolved until they're requierd, @@ -49,6 +49,14 @@ function getValue(scope: unknown, name: string): unknown { return undefined; } +/** + * Get the raw value of a variable. + * Resolves declarations as necessary + * + * @internal + * @returns `unknown` or `any` for input values; + * `MessageValue` for `.input` and `.local` declaration values. + */ export function lookupVariableRef(ctx: Context, { name }: VariableRef) { const value = getValue(ctx.scope, name); if (value === undefined) { @@ -67,11 +75,10 @@ export function lookupVariableRef(ctx: Context, { name }: VariableRef) { return value; } -export function getMessageValue( - ctx: Context, - source: string, - value: unknown -): MessageValue | undefined { +export function resolveVariableRef(ctx: Context, ref: VariableRef) { + const source = '$' + ref.name; + const value = lookupVariableRef(ctx, ref); + let type = typeof value; if (type === 'object') { const mv = value as MessageValue; @@ -79,6 +86,7 @@ export function getMessageValue( if (value instanceof Number) type = 'number'; else if (value instanceof String) type = 'string'; } + switch (type) { case 'bigint': case 'number': { @@ -89,13 +97,7 @@ export function getMessageValue( const msgCtx = new MessageFunctionContext(ctx, source); return ctx.functions.string(msgCtx, {}, value); } - default: - return value === undefined ? undefined : unknown(source, value); } -} -export function resolveVariableRef(ctx: Context, ref: VariableRef) { - const source = '$' + ref.name; - const value = lookupVariableRef(ctx, ref); - return getMessageValue(ctx, source, value) ?? fallback(source); + return value === undefined ? fallback(source) : unknown(source, value); } diff --git a/packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.test.ts b/packages/mf2-messageformat/src/data-model/unsupported-annotation.test.ts similarity index 96% rename from packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.test.ts rename to packages/mf2-messageformat/src/data-model/unsupported-annotation.test.ts index 29a8dbec..631aa6aa 100644 --- a/packages/mf2-messageformat/src/data-model/expression/unsupported-annotation.test.ts +++ b/packages/mf2-messageformat/src/data-model/unsupported-annotation.test.ts @@ -1,4 +1,4 @@ -import { MessageFormat } from '../../index'; +import { MessageFormat } from '../index'; function resolve( source: string, diff --git a/packages/mf2-messageformat/src/data-model/expression/variable.test.ts b/packages/mf2-messageformat/src/data-model/variable.test.ts similarity index 98% rename from packages/mf2-messageformat/src/data-model/expression/variable.test.ts rename to packages/mf2-messageformat/src/data-model/variable.test.ts index 64f7a6ad..0b487398 100644 --- a/packages/mf2-messageformat/src/data-model/expression/variable.test.ts +++ b/packages/mf2-messageformat/src/data-model/variable.test.ts @@ -1,4 +1,4 @@ -import { MessageFormat } from '../../index'; +import { MessageFormat } from '../index'; describe('variables', () => { let mf: MessageFormat; diff --git a/packages/mf2-messageformat/src/index.ts b/packages/mf2-messageformat/src/index.ts index 4858ffb2..3394a398 100644 --- a/packages/mf2-messageformat/src/index.ts +++ b/packages/mf2-messageformat/src/index.ts @@ -1,5 +1,4 @@ export { asDataModel, parseMessage, type CST } from './cst-parser/index.js'; -export { getMessageValue } from './data-model/expression/variable-ref'; export type * from './data-model/types.js'; export * from './data-model/type-guards.js'; export { diff --git a/packages/mf2-messageformat/src/messageformat.ts b/packages/mf2-messageformat/src/messageformat.ts index bdaa917f..1a5c8042 100644 --- a/packages/mf2-messageformat/src/messageformat.ts +++ b/packages/mf2-messageformat/src/messageformat.ts @@ -1,10 +1,10 @@ import { asDataModel, parseMessage } from './cst-parser/index.js'; -import { resolveExpression } from './data-model/expression/index.js'; -import { UnresolvedExpression } from './data-model/expression/variable-ref.js'; -import { formatMarkup } from './data-model/markup.js'; +import { formatMarkup } from './data-model/format-markup.js'; +import { resolveExpression } from './data-model/resolve-expression.js'; +import { UnresolvedExpression } from './data-model/resolve-variable.js'; import type { Message } from './data-model/types.js'; import { MessageError, MessageResolutionError } from './errors.js'; -import type { Context } from './format-context'; +import type { Context } from './format-context.js'; import { MessageFunctions, MessagePart, diff --git a/packages/mf2-messageformat/src/select-pattern.ts b/packages/mf2-messageformat/src/select-pattern.ts index f3896850..630f1b70 100644 --- a/packages/mf2-messageformat/src/select-pattern.ts +++ b/packages/mf2-messageformat/src/select-pattern.ts @@ -1,4 +1,4 @@ -import { resolveExpression } from './data-model/expression/index.js'; +import { resolveExpression } from './data-model/resolve-expression.js'; import type { Message, Pattern } from './data-model/types.js'; import { MessageSelectionError } from './errors'; import type { Context } from './format-context'; From 34b019815fd9ea0abd4416e5007eb1a6814982f5 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 2 Jan 2024 20:24:39 +0200 Subject: [PATCH 23/44] test(mf2): Split test data into three files; leave out coverage from bare `npm test` --- .github/workflows/nodejs.yml | 2 +- package.json | 2 +- .../src/__fixtures/syntax-errors.json | 58 +++ .../src/__fixtures/test-core.json | 120 ++++++ .../src/__fixtures/test-functions.json | 187 +++++++++ .../src/__fixtures/test-messages.json | 383 ------------------ .../{test-name.ts => util-test-name.ts} | 0 .../src/messageformat.test.ts | 54 +-- .../src/stringifier/message.test.ts | 36 +- 9 files changed, 419 insertions(+), 423 deletions(-) create mode 100644 packages/mf2-messageformat/src/__fixtures/syntax-errors.json create mode 100644 packages/mf2-messageformat/src/__fixtures/test-core.json create mode 100644 packages/mf2-messageformat/src/__fixtures/test-functions.json delete mode 100644 packages/mf2-messageformat/src/__fixtures/test-messages.json rename packages/mf2-messageformat/src/__fixtures/{test-name.ts => util-test-name.ts} (100%) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 1ee3da60..66c83526 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -18,7 +18,7 @@ jobs: with: node-version: ${{ matrix.node-version }} - run: npm ci - - run: npm test + - run: npm test -- --coverage - run: npm run build lint: diff --git a/package.json b/package.json index 33b11997..88549079 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "test:chrome": "npm run pretest:browsers && mocha-selenium-bridge -b chrome test/browser/test.html", "test:firefox": "npm run pretest:browsers && mocha-selenium-bridge -b firefox test/browser/test.html", "test:serve": "npm run pretest:browsers && serve", - "test": "jest --coverage" + "test": "jest" }, "prettier": { "arrowParens": "avoid", diff --git a/packages/mf2-messageformat/src/__fixtures/syntax-errors.json b/packages/mf2-messageformat/src/__fixtures/syntax-errors.json new file mode 100644 index 00000000..ec390577 --- /dev/null +++ b/packages/mf2-messageformat/src/__fixtures/syntax-errors.json @@ -0,0 +1,58 @@ +[ + ".", + "{", + "}", + "{}", + "{{", + "{{}", + "{{}}}", + "{|foo| #markup}", + "{/tag foo=bar}", + "{{missing end brace}", + "{{missing end braces", + "{{missing end {$braces", + "{{extra}} content", + "empty { } placeholder", + "bad {:} placeholder", + "bad {\\u0000placeholder}", + "no-equal {|42| :number minimumFractionDigits 2}", + "bad {:placeholder option=x option=x}", + "bad {:placeholder ns:option=x ns:option=y}", + "bad {:placeholder option=}", + "bad {:placeholder option value}", + "bad {:placeholder option:value}", + "bad {:placeholder option}", + "bad {:placeholder:}", + "bad {::placeholder}", + "bad {:placeholder::foo}", + "bad {:placeholder option:=x}", + "bad {:placeholder :option=x}", + "bad {:placeholder option::x=y}", + "bad {$placeholder option}", + "no {placeholder end", + "no {$placeholder end", + "no {:placeholder end", + "no {|placeholder| end", + "no {|literal} end", + "no {|literal or placeholder end", + ".input {$foo} .local $foo = {42} {{bar {$foo}}}", + ".local $foo = {42} .input {$foo} {{bar {$foo}}}", + ".local $foo = {:unknown} .local $foo = {42} {{bar {$foo}}}", + ".local bar = {|foo|} {{{$bar}}}", + ".local $bar {|foo|} {{{$bar}}}", + ".local $bar = |foo| {{{$bar}}}", + ".local $foo = {$bar} .local $bar = {$baz} {{bar {$foo}}}", + ".local $foo = {$bar :func} .local $bar = {$baz} {{bar {$foo}}}", + ".match {#foo} * {{foo}}", + ".match {} * {{foo}}", + ".match {|foo| :x} {|bar| :x} ** {{foo}}", + ".match * {{foo}}", + ".match {|x| :x} * foo", + ".match {|x| :x} * {{foo}} extra", + ".match |x| * {{foo}}", + ".match {$foo :x} * * {{foo}}", + ".match {$foo :x} {$bar :x} * {{foo}}", + ".match {$foo} one {{one}} * {{other}}", + ".input {$foo} .match {$foo} one {{one}} * {{other}}", + ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}" +] diff --git a/packages/mf2-messageformat/src/__fixtures/test-core.json b/packages/mf2-messageformat/src/__fixtures/test-core.json new file mode 100644 index 00000000..bca145b3 --- /dev/null +++ b/packages/mf2-messageformat/src/__fixtures/test-core.json @@ -0,0 +1,120 @@ +[ + { "src": "hello", "exp": "hello" }, + { "src": "hello {world}", "exp": "hello world" }, + { "src": "hello {|world|}", "exp": "hello world" }, + { "src": "hello {||}", "exp": "hello " }, + { + "src": "hello {$place}", + "params": { "place": "world" }, + "exp": "hello world" + }, + { + "src": "hello {$place-.}", + "params": { "place-.": "world" }, + "exp": "hello world" + }, + { + "src": "hello {$place}", + "errors": [{ "type": "unresolved-var" }], + "exp": "hello {$place}" + }, + { + "src": "{$one} and {$two}", + "params": { "one": 1.3, "two": 4.2 }, + "exp": "1.3 and 4.2" + }, + { + "src": "{$one} et {$two}", + "locale": "fr", + "params": { "one": 1.3, "two": 4.2 }, + "exp": "1,3 et 4,2" + }, + { "src": ".local $foo = {bar} {{bar {$foo}}}", "exp": "bar bar" }, + { "src": ".local $foo = {|bar|} {{bar {$foo}}}", "exp": "bar bar" }, + { + "src": ".local $foo = {|bar|} {{bar {$foo}}}", + "params": { "foo": "foo" }, + "exp": "bar bar" + }, + { + "src": ".local $foo = {$bar} {{bar {$foo}}}", + "params": { "bar": "foo" }, + "exp": "bar foo" + }, + { + "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}", + "params": { "baz": "foo" }, + "exp": "bar foo" + }, + { + "src": ".input {$foo} {{bar {$foo}}}", + "params": { "foo": "foo" }, + "exp": "bar foo" + }, + { + "src": ".input {$foo} .local $bar = {$foo} {{bar {$bar}}}", + "params": { "foo": "foo" }, + "exp": "bar foo" + }, + { + "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}", + "params": { "baz": "foo" }, + "exp": "bar foo" + }, + { "src": ".local $x = {42} .local $y = {$x} {{{$x} {$y}}}", "exp": "42 42" }, + { + "src": "{#tag}", + "exp": "", + "parts": [{ "type": "markup", "kind": "open", "name": "tag" }] + }, + { + "src": "{#tag}content", + "exp": "content", + "parts": [ + { "type": "markup", "kind": "open", "name": "tag" }, + { "type": "literal", "value": "content" } + ] + }, + { + "src": "{#ns:tag}content{/ns:tag}", + "exp": "content", + "parts": [ + { "type": "markup", "kind": "open", "name": "ns:tag" }, + { "type": "literal", "value": "content" }, + { "type": "markup", "kind": "close", "name": "ns:tag" } + ] + }, + { + "src": "{/tag}content", + "exp": "content", + "parts": [ + { "type": "markup", "kind": "close", "name": "tag" }, + { "type": "literal", "value": "content" } + ] + }, + { + "src": "{#tag foo=bar}", + "exp": "", + "parts": [ + { + "type": "markup", + "kind": "open", + "name": "tag", + "options": { "foo": "bar" } + } + ] + }, + { + "src": "{#tag a:foo=|foo| b:bar=$bar}", + "params": { "bar": "b a r" }, + "exp": "", + "parts": [ + { + "type": "markup", + "kind": "open", + "name": "tag", + "options": { "a:foo": "foo", "b:bar": "b a r" } + } + ] + } +] diff --git a/packages/mf2-messageformat/src/__fixtures/test-functions.json b/packages/mf2-messageformat/src/__fixtures/test-functions.json new file mode 100644 index 00000000..2b4d8dce --- /dev/null +++ b/packages/mf2-messageformat/src/__fixtures/test-functions.json @@ -0,0 +1,187 @@ +{ + "number": [ + { "src": "hello {4.2 :number}", "exp": "hello 4.2" }, + { "src": "hello {-4.20 :number}", "exp": "hello -4.2" }, + { "src": "hello {0.42e+1 :number}", "exp": "hello 4.2" }, + { + "src": "hello {foo :number}", + "exp": "hello {|foo|}", + "errors": [{ "type": "bad-input" }] + }, + { + "src": "hello {:number}", + "exp": "hello {:number}", + "errors": [{ "type": "bad-input" }] + }, + { + "src": "hello {4.2 :number minimumFractionDigits=2}", + "exp": "hello 4.20" + }, + { + "src": "hello {|4.2| :number minimumFractionDigits=|2|}", + "exp": "hello 4.20" + }, + { + "src": "hello {4.2 :number minimumFractionDigits=$foo}", + "params": { "foo": 2 }, + "exp": "hello 4.20" + }, + { + "src": "hello {|4.2| :number minimumFractionDigits=$foo}", + "params": { "foo": "2" }, + "exp": "hello 4.20" + }, + { + "src": ".local $foo = {$bar :number} {{bar {$foo}}}", + "params": { "bar": 4.2 }, + "exp": "bar 4.2" + }, + { + "src": ".local $foo = {$bar :number minimumFractionDigits=2} {{bar {$foo}}}", + "params": { "bar": 4.2 }, + "exp": "bar 4.20" + }, + { + "src": ".local $foo = {$bar :number minimumFractionDigits=foo} {{bar {$foo}}}", + "params": { "bar": 4.2 }, + "exp": "bar {$bar}", + "errors": [{ "type": "bad-option" }] + }, + { + "src": ".local $foo = {$bar :number} {{bar {$foo}}}", + "params": { "bar": "foo" }, + "exp": "bar {$bar}", + "errors": [{ "type": "bad-input" }] + }, + { + "src": ".input {$foo :number} {{bar {$foo}}}", + "params": { "foo": 4.2 }, + "exp": "bar 4.2" + }, + { + "src": ".input {$foo :number minimumFractionDigits=2} {{bar {$foo}}}", + "params": { "foo": 4.2 }, + "exp": "bar 4.20" + }, + { + "src": ".input {$foo :number minimumFractionDigits=foo} {{bar {$foo}}}", + "params": { "foo": 4.2 }, + "exp": "bar {$foo}", + "errors": [{ "type": "bad-option" }] + }, + { + "src": ".input {$foo :number} {{bar {$foo}}}", + "params": { "foo": "foo" }, + "exp": "bar {$foo}", + "errors": [{ "type": "bad-input" }] + }, + { + "src": ".match {$foo :number} one {{one}} * {{other}}", + "params": { "foo": 1 }, + "exp": "one" + }, + { + "src": ".match {$foo :number} 1 {{=1}} one {{one}} * {{other}}", + "params": { "foo": 1 }, + "exp": "=1" + }, + { + "src": ".match {$foo :number} one {{one}} 1 {{=1}} * {{other}}", + "params": { "foo": 1 }, + "exp": "=1" + }, + { + "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", + "params": { "foo": 1, "bar": 1 }, + "exp": "one one" + }, + { + "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", + "params": { "foo": 1, "bar": 2 }, + "exp": "one other" + }, + { + "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", + "params": { "foo": 2, "bar": 2 }, + "exp": "other" + }, + { + "src": ".input {$foo :number} .match {$foo} one {{one}} * {{other}}", + "params": { "foo": 1 }, + "exp": "one" + }, + { + "src": ".local $foo = {$bar :number} .match {$foo} one {{one}} * {{other}}", + "params": { "bar": 1 }, + "exp": "one" + }, + { + "src": ".input {$foo :number } .local $bar = {$foo} .match {$bar} one {{one}} * {{other}}", + "params": { "foo": 1 }, + "exp": "one" + }, + { + "src": ".input {$bar :number} .match {$bar} one {{one}} * {{other}}", + "params": { "bar": 2 }, + "exp": "other" + }, + { + "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", + "params": { "bar": 1 }, + "exp": "one" + }, + { + "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", + "params": { "bar": 2 }, + "exp": "other" + }, + { + "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", + "params": { "bar": 1 }, + "exp": "one" + }, + { + "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", + "params": { "bar": 2 }, + "exp": "other" + }, + { + "src": ".input {$none} .match {$foo :number} one {{one}} * {{{$none}}}", + "params": { "foo": 1 }, + "exp": "one" + }, + { + "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}", + "params": { "foo": 1 }, + "exp": "one" + }, + { + "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}", + "params": { "foo": 2 }, + "exp": "{$none}", + "errors": [{ "type": "unresolved-var" }] + } + ], + "string": [ + { + "src": ".match {$foo :string} |1| {{one}} * {{other}}", + "params": { "foo": "1" }, + "exp": "one" + }, + { + "src": ".match {$foo :string} 1 {{one}} * {{other}}", + "params": { "foo": 1 }, + "exp": "one" + }, + { + "src": ".match {$foo :string} 1 {{one}} * {{other}}", + "params": { "foo": null }, + "exp": "other" + }, + { + "src": ".match {$foo :string} 1 {{one}} * {{other}}", + "exp": "other", + "errors": [{ "type": "unresolved-var" }] + } + ] +} diff --git a/packages/mf2-messageformat/src/__fixtures/test-messages.json b/packages/mf2-messageformat/src/__fixtures/test-messages.json deleted file mode 100644 index 96d8fa91..00000000 --- a/packages/mf2-messageformat/src/__fixtures/test-messages.json +++ /dev/null @@ -1,383 +0,0 @@ -[ - { "src": "hello", "exp": "hello" }, - { "src": "hello {world}", "exp": "hello world" }, - { "src": "hello {|world|}", "exp": "hello world" }, - { "src": "hello {||}", "exp": "hello " }, - { - "src": "hello {$place}", - "params": { "place": "world" }, - "exp": "hello world" - }, - { - "src": "hello {$place-.}", - "params": { "place-.": "world" }, - "exp": "hello world" - }, - { - "src": "hello {$place}", - "errors": [{ "type": "unresolved-var" }], - "exp": "hello {$place}" - }, - { - "src": "{$one} and {$two}", - "params": { "one": 1.3, "two": 4.2 }, - "exp": "1.3 and 4.2" - }, - { - "src": "{$one} et {$two}", - "locale": "fr", - "params": { "one": 1.3, "two": 4.2 }, - "exp": "1,3 et 4,2" - }, - { "src": "hello {4.2 :number}", "exp": "hello 4.2" }, - { "src": "hello {-4.20 :number}", "exp": "hello -4.2" }, - { "src": "hello {0.42e+1 :number}", "exp": "hello 4.2" }, - { - "src": "hello {foo :number}", - "exp": "hello {|foo|}", - "errors": [{ "type": "bad-input" }] - }, - { - "src": "hello {:number}", - "exp": "hello {:number}", - "errors": [{ "type": "bad-input" }] - }, - { - "src": "hello {4.2 :number minimumFractionDigits=2}", - "exp": "hello 4.20" - }, - { - "src": "hello {|4.2| :number minimumFractionDigits=|2|}", - "exp": "hello 4.20" - }, - { - "src": "hello {4.2 :number minimumFractionDigits=$foo}", - "params": { "foo": 2 }, - "exp": "hello 4.20" - }, - { - "src": "hello {|4.2| :number minimumFractionDigits=$foo}", - "params": { "foo": "2" }, - "exp": "hello 4.20" - }, - { - "src": ".local $foo = {bar} {{bar {$foo}}}", - "exp": "bar bar" - }, - { - "src": ".local $foo = {|bar|} {{bar {$foo}}}", - "exp": "bar bar" - }, - { - "src": ".local $foo = {|bar|} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "exp": "bar bar" - }, - { - "src": ".local $foo = {$bar} {{bar {$foo}}}", - "params": { "bar": "foo" }, - "exp": "bar foo" - }, - { - "src": ".local $foo = {$bar :number} {{bar {$foo}}}", - "params": { "bar": 4.2 }, - "exp": "bar 4.2" - }, - { - "src": ".local $foo = {$bar :number minimumFractionDigits=2} {{bar {$foo}}}", - "params": { "bar": 4.2 }, - "exp": "bar 4.20" - }, - { - "src": ".local $foo = {$bar :number minimumFractionDigits=foo} {{bar {$foo}}}", - "params": { "bar": 4.2 }, - "exp": "bar {$bar}", - "errors": [{ "type": "bad-option" }] - }, - { - "src": ".local $foo = {$bar :number} {{bar {$foo}}}", - "params": { "bar": "foo" }, - "exp": "bar {$bar}", - "errors": [{ "type": "bad-input" }] - }, - { - "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}", - "params": { "baz": "foo" }, - "exp": "bar foo" - }, - { - "src": ".input {$foo} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "exp": "bar foo" - }, - { - "src": ".input {$foo :number} {{bar {$foo}}}", - "params": { "foo": 4.2 }, - "exp": "bar 4.2" - }, - { - "src": ".input {$foo :number minimumFractionDigits=2} {{bar {$foo}}}", - "params": { "foo": 4.2 }, - "exp": "bar 4.20" - }, - { - "src": ".input {$foo :number minimumFractionDigits=foo} {{bar {$foo}}}", - "params": { "foo": 4.2 }, - "exp": "bar {$foo}", - "errors": [{ "type": "bad-option" }] - }, - { - "src": ".input {$foo :number} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "exp": "bar {$foo}", - "errors": [{ "type": "bad-input" }] - }, - { - "src": ".input {$foo} .local $bar = {$foo} {{bar {$bar}}}", - "params": { "foo": "foo" }, - "exp": "bar foo" - }, - { - "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}", - "params": { "baz": "foo" }, - "exp": "bar foo" - }, - { - "src": ".input {$foo} .local $foo = {42} {{bar {$foo}}}", - "syntaxError": true - }, - { - "src": ".local $foo = {42} .input {$foo} {{bar {$foo}}}", - "syntaxError": true - }, - { - "src": ".local $foo = {:unknown} .local $foo = {42} {{bar {$foo}}}", - "syntaxError": true - }, - { - "src": ".local $x = {42} .local $y = {$x} {{{$x} {$y}}}", - "exp": "42 42" - }, - { - "src": ".match {$foo :string} |1| {{one}} * {{other}}", - "params": { "foo": "1" }, - "exp": "one" - }, - { - "src": ".match {$foo :string} 1 {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "one" - }, - { - "src": ".match {$foo :string} 1 {{one}} * {{other}}", - "params": { "foo": null }, - "exp": "other" - }, - { - "src": ".match {$foo :string} 1 {{one}} * {{other}}", - "exp": "other", - "errors": [{ "type": "unresolved-var" }] - }, - { - "src": ".match {$foo :number} one {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "one" - }, - { - "src": ".match {$foo :number} 1 {{=1}} one {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "=1" - }, - { - "src": ".match {$foo :number} one {{one}} 1 {{=1}} * {{other}}", - "params": { "foo": 1 }, - "exp": "=1" - }, - { - "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", - "params": { "foo": 1, "bar": 1 }, - "exp": "one one" - }, - { - "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", - "params": { "foo": 1, "bar": 2 }, - "exp": "one other" - }, - { - "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", - "params": { "foo": 2, "bar": 2 }, - "exp": "other" - }, - { - "src": ".input {$foo :number} .match {$foo} one {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "one" - }, - { - "src": ".local $foo = {$bar :number} .match {$foo} one {{one}} * {{other}}", - "params": { "bar": 1 }, - "exp": "one" - }, - { - "src": ".input {$foo :number } .local $bar = {$foo} .match {$bar} one {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "one" - }, - { - "src": ".input {$bar :number} .match {$bar} one {{one}} * {{other}}", - "params": { "bar": 2 }, - "exp": "other" - }, - { - "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", - "params": { "bar": 1 }, - "exp": "one" - }, - { - "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", - "params": { "bar": 2 }, - "exp": "other" - }, - { - "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", - "params": { "bar": 1 }, - "exp": "one" - }, - { - "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", - "params": { "bar": 2 }, - "exp": "other" - }, - { - "src": ".input {$none} .match {$foo :number} one {{one}} * {{{$none}}}", - "params": { "foo": 1 }, - "exp": "one" - }, - { - "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}", - "params": { "foo": 1 }, - "exp": "one" - }, - { - "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}", - "params": { "foo": 2 }, - "exp": "{$none}", - "errors": [{ "type": "unresolved-var" }] - }, - { - "src": "{#tag}", - "exp": "", - "parts": [{ "type": "markup", "kind": "open", "name": "tag" }] - }, - { - "src": "{#tag}content", - "exp": "content", - "parts": [ - { "type": "markup", "kind": "open", "name": "tag" }, - { "type": "literal", "value": "content" } - ] - }, - { - "src": "{#ns:tag}content{/ns:tag}", - "exp": "content", - "parts": [ - { "type": "markup", "kind": "open", "name": "ns:tag" }, - { "type": "literal", "value": "content" }, - { "type": "markup", "kind": "close", "name": "ns:tag" } - ] - }, - { - "src": "{/tag}content", - "exp": "content", - "parts": [ - { "type": "markup", "kind": "close", "name": "tag" }, - { "type": "literal", "value": "content" } - ] - }, - { - "src": "{#tag foo=bar}", - "exp": "", - "parts": [ - { - "type": "markup", - "kind": "open", - "name": "tag", - "options": { "foo": "bar" } - } - ] - }, - { - "src": "{#tag a:foo=|foo| b:bar=$bar}", - "params": { "bar": "b a r" }, - "exp": "", - "parts": [ - { - "type": "markup", - "kind": "open", - "name": "tag", - "options": { "a:foo": "foo", "b:bar": "b a r" } - } - ] - }, - { "src": "{|foo| #markup}", "syntaxError": true }, - { "src": "{/tag foo=bar}", "syntaxError": true }, - { "src": ".match {#foo} * {{foo}}", "syntaxError": true }, - { "src": "{{missing end brace}", "syntaxError": true }, - { "src": "{{missing end braces", "syntaxError": true }, - { "src": "{{missing end {$braces", "syntaxError": true }, - { "src": "{{extra}} content", "syntaxError": true }, - { "src": ".local bar = {|foo|} {{{$bar}}}", "syntaxError": true }, - { "src": ".local $bar {|foo|} {{{$bar}}}", "syntaxError": true }, - { "src": ".local $bar = |foo| {{{$bar}}}", "syntaxError": true }, - { - "src": ".local $foo = {$bar} .local $bar = {$baz} {{bar {$foo}}}", - "syntaxError": true - }, - { - "src": ".local $foo = {$bar :func} .local $bar = {$baz} {{bar {$foo}}}", - "syntaxError": true - }, - { "src": "empty { }", "syntaxError": true }, - { "src": "bad {:}", "syntaxError": true }, - { "src": "bad {\u0000placeholder}", "syntaxError": true }, - { - "src": "no-equal {|42| :number minimumFractionDigits 2}", - "syntaxError": true - }, - { "src": "bad {:placeholder option=x option=x}", "syntaxError": true }, - { "src": "bad {:placeholder ns:option=x ns:option=y}", "syntaxError": true }, - { "src": "bad {:placeholder option=}", "syntaxError": true }, - { "src": "bad {:placeholder option value}", "syntaxError": true }, - { "src": "bad {:placeholder option:value}", "syntaxError": true }, - { "src": "bad {:placeholder option}", "syntaxError": true }, - { "src": "bad {:placeholder:}", "syntaxError": true }, - { "src": "bad {::placeholder}", "syntaxError": true }, - { "src": "bad {:placeholder::foo}", "syntaxError": true }, - { "src": "bad {:placeholder option:=x}", "syntaxError": true }, - { "src": "bad {:placeholder :option=x}", "syntaxError": true }, - { "src": "bad {:placeholder option::x=y}", "syntaxError": true }, - { "src": "bad {$placeholder option}", "syntaxError": true }, - { "src": "no {placeholder end", "syntaxError": true }, - { "src": "no {$placeholder end", "syntaxError": true }, - { "src": "no {:placeholder end", "syntaxError": true }, - { "src": "no {|placeholder| end", "syntaxError": true }, - { "src": "no {|literal} end", "syntaxError": true }, - { "src": "no {|literal or placeholder end", "syntaxError": true }, - { "src": ".match {} * {{foo}}", "syntaxError": true }, - { "src": ".match {|foo| :x} {|bar| :x} ** {{foo}}", "syntaxError": true }, - { "src": ".match * {{foo}}", "syntaxError": true }, - { "src": ".match {|x| :x} * foo", "syntaxError": true }, - { "src": ".match {|x| :x} * {{foo}} extra", "syntaxError": true }, - { "src": ".match |x| * {{foo}}", "syntaxError": true }, - { "src": ".match {$foo :x} * * {{foo}}", "syntaxError": true }, - { "src": ".match {$foo :x} {$bar :x} * {{foo}}", "syntaxError": true }, - { "src": ".match {$foo} one {{one}} * {{other}}", "syntaxError": true }, - { - "src": ".input {$foo} .match {$foo} one {{one}} * {{other}}", - "syntaxError": true - }, - { - "src": ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}", - "syntaxError": true - } -] diff --git a/packages/mf2-messageformat/src/__fixtures/test-name.ts b/packages/mf2-messageformat/src/__fixtures/util-test-name.ts similarity index 100% rename from packages/mf2-messageformat/src/__fixtures/test-name.ts rename to packages/mf2-messageformat/src/__fixtures/util-test-name.ts diff --git a/packages/mf2-messageformat/src/messageformat.test.ts b/packages/mf2-messageformat/src/messageformat.test.ts index 1db86405..f9ced9dc 100644 --- a/packages/mf2-messageformat/src/messageformat.test.ts +++ b/packages/mf2-messageformat/src/messageformat.test.ts @@ -1,30 +1,34 @@ -import testMessages from './__fixtures/test-messages.json'; -import { testName } from './__fixtures/test-name'; +import syntaxErrors from './__fixtures/syntax-errors.json'; +import testCore from './__fixtures/test-core.json'; +import testFunctions from './__fixtures/test-functions.json'; +import { testName } from './__fixtures/util-test-name'; import { MessageSyntaxError } from './errors'; import { MessageFormat } from './messageformat'; -export type TestMessage = - | { - src: string; - only?: boolean; - locale?: string; - params?: Record; - exp: string; - parts?: Array; - errors?: Array<{ type: string }>; - syntaxError?: never; - } - | { src: string; syntaxError: true }; +export type TestMessage = { + src: string; + only?: boolean; + locale?: string; + params?: Record; + exp: string; + parts?: Array; + errors?: Array<{ type: string }>; +}; -describe('Format messages', () => { - for (const testMsg of testMessages as TestMessage[]) { - if (testMsg.syntaxError) { - test(testName(testMsg), () => { - expect(() => new MessageFormat(testMsg.src)).toThrow( - MessageSyntaxError - ); - }); - } else { +describe('Syntax errors', () => { + for (const src of syntaxErrors) { + test(src, () => { + expect(() => new MessageFormat(src)).toThrow(MessageSyntaxError); + }); + } +}); + +for (const [title, messages] of [ + ['Core message syntax', testCore], + ...Object.entries(testFunctions).map(x => [`Messages using :${x[0]}`, x[1]]) +] as Array<[string, TestMessage[]]>) { + describe(title, () => { + for (const testMsg of messages) { const { src, exp, @@ -48,5 +52,5 @@ describe('Format messages', () => { } }); } - } -}); + }); +} diff --git a/packages/mf2-messageformat/src/stringifier/message.test.ts b/packages/mf2-messageformat/src/stringifier/message.test.ts index ac2853cf..4d7145c3 100644 --- a/packages/mf2-messageformat/src/stringifier/message.test.ts +++ b/packages/mf2-messageformat/src/stringifier/message.test.ts @@ -1,17 +1,27 @@ -import testMessages from '../__fixtures/test-messages.json'; -import { testName } from '../__fixtures/test-name'; +import testCore from '../__fixtures/test-core.json'; +import testFunctions from '../__fixtures/test-functions.json'; +import { testName } from '../__fixtures/util-test-name'; import { asDataModel, parseMessage } from '../cst-parser/index.js'; +import type { TestMessage } from '../messageformat.test'; import { stringifyMessage } from './message'; -describe('Stringify messages', () => { - for (const testMsg of testMessages) { - if (!testMsg.syntaxError && !testMsg.errors?.length) { - test(testName(testMsg), () => { - const msg0 = asDataModel(parseMessage(testMsg.src)); - const src1 = stringifyMessage(msg0); - const msg1 = asDataModel(parseMessage(src1)); - expect(msg1).toEqual(msg0); - }); +for (const [title, messages] of [ + ['Stringify core messages', testCore], + ...Object.entries(testFunctions).map(x => [ + `Stringify :${x[0]} messages`, + x[1] + ]) +] as Array<[string, TestMessage[]]>) { + describe(title, () => { + for (const testMsg of messages) { + if (!testMsg.errors?.length) { + test(testName(testMsg), () => { + const msg0 = asDataModel(parseMessage(testMsg.src)); + const src1 = stringifyMessage(msg0); + const msg1 = asDataModel(parseMessage(src1)); + expect(msg1).toEqual(msg0); + }); + } } - } -}); + }); +} From 00d742b397d6fc49867d472cad93ef38e1a401af Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 2 Jan 2024 20:50:35 +0200 Subject: [PATCH 24/44] feat(mf2): Add :integer, :ordinal and :plural aliases --- .../src/__fixtures/test-functions.json | 65 +++++++++++ .../mf2-messageformat/src/runtime/index.ts | 23 +++- .../mf2-messageformat/src/runtime/number.ts | 103 +++++++++++++++--- 3 files changed, 171 insertions(+), 20 deletions(-) diff --git a/packages/mf2-messageformat/src/__fixtures/test-functions.json b/packages/mf2-messageformat/src/__fixtures/test-functions.json index 2b4d8dce..52305a15 100644 --- a/packages/mf2-messageformat/src/__fixtures/test-functions.json +++ b/packages/mf2-messageformat/src/__fixtures/test-functions.json @@ -1,4 +1,25 @@ { + "integer": [ + { "src": "hello {4.2 :integer}", "exp": "hello 4" }, + { "src": "hello {-4.20 :integer}", "exp": "hello -4" }, + { "src": "hello {0.42e+1 :integer}", "exp": "hello 4" }, + { + "src": ".match {$foo :integer} one {{one}} * {{other}}", + "params": { "foo": 1.2 }, + "exp": "one" + }, + { + "src": ".match {$foo :integer} 1.2 {{=1.2}} one {{one}} * {{other}}", + "params": { "foo": 1.2 }, + "exp": "=1.2" + }, + { + "src": ".match {$foo :integer} 1.2 {{=1.2}} |1,2| {{=1,2}} * {{other}}", + "locale": "fr", + "params": { "foo": 1.2 }, + "exp": "=1.2" + } + ], "number": [ { "src": "hello {4.2 :number}", "exp": "hello 4.2" }, { "src": "hello {-4.20 :number}", "exp": "hello -4.2" }, @@ -162,6 +183,50 @@ "errors": [{ "type": "unresolved-var" }] } ], + "ordinal": [ + { + "src": ".match {$foo :ordinal} one {{st}} two {{nd}} few {{rd}} * {{th}}", + "params": { "foo": 1 }, + "exp": "st" + }, + { + "src": ".match {$foo :ordinal} one {{st}} two {{nd}} few {{rd}} * {{th}}", + "params": { "foo": 2 }, + "exp": "nd" + }, + { + "src": ".match {$foo :ordinal} one {{st}} two {{nd}} few {{rd}} * {{th}}", + "params": { "foo": 3 }, + "exp": "rd" + }, + { + "src": ".match {$foo :ordinal} one {{st}} two {{nd}} few {{rd}} * {{th}}", + "params": { "foo": 4 }, + "exp": "th" + }, + { + "src": "hello {42 :ordinal}", + "exp": "hello {|42|}", + "errors": [{ "type": "not-formattable" }] + } + ], + "plural": [ + { + "src": ".match {$foo :plural} one {{one}} * {{other}}", + "params": { "foo": 1 }, + "exp": "one" + }, + { + "src": ".match {$foo :plural} one {{one}} * {{other}}", + "params": { "foo": 1.1 }, + "exp": "other" + }, + { + "src": "hello {42 :plural}", + "exp": "hello {|42|}", + "errors": [{ "type": "not-formattable" }] + } + ], "string": [ { "src": ".match {$foo :string} |1| {{one}} * {{other}}", diff --git a/packages/mf2-messageformat/src/runtime/index.ts b/packages/mf2-messageformat/src/runtime/index.ts index 9a8309b7..d9a0cbc0 100644 --- a/packages/mf2-messageformat/src/runtime/index.ts +++ b/packages/mf2-messageformat/src/runtime/index.ts @@ -1,11 +1,18 @@ import { Context } from '../format-context.js'; import { datetime } from './datetime.js'; -import { number } from './number.js'; +import { integer, number, ordinal, plural } from './number.js'; import { string } from './string.js'; export { MessageDateTime, MessageDateTimePart, datetime } from './datetime.js'; export { MessageFallback, MessageFallbackPart, fallback } from './fallback.js'; -export { MessageNumber, MessageNumberPart, number } from './number.js'; +export { + MessageNumber, + MessageNumberPart, + integer, + number, + ordinal, + plural +} from './number.js'; export { MessageString, MessageStringPart, string } from './string.js'; export { MessageUnknownPart, MessageUnknownValue, unknown } from './unknown.js'; export { @@ -62,11 +69,19 @@ export type MessagePart = | MessageMarkupPart; /** - * The default Runtime includes three functions, `datetime`, `number` and `string`. + * The default Runtime includes three functions, `datetime`, `number` and `string`, + * as well as the `integer`, `ordinal`, and `plural` aliases for `number`. * * @beta */ -export const defaultFunctions = { datetime, number, string }; +export const defaultFunctions = { + datetime, + integer, + number, + ordinal, + plural, + string +}; export class MessageFunctionContext { #ctx: Context; diff --git a/packages/mf2-messageformat/src/runtime/number.ts b/packages/mf2-messageformat/src/runtime/number.ts index 0074a703..c62b0e37 100644 --- a/packages/mf2-messageformat/src/runtime/number.ts +++ b/packages/mf2-messageformat/src/runtime/number.ts @@ -29,8 +29,8 @@ export interface MessageNumber extends MessageValue { * so a key `zero` will never be matched for that locale. */ selectKey(keys: Set): string | null; - toParts(): [MessageNumberPart]; - toString(): string; + toParts?: () => [MessageNumberPart]; + toString?: () => string; valueOf(): number | bigint; } @@ -41,6 +41,8 @@ export interface MessageNumberPart extends MessageExpressionPart { parts: Intl.NumberFormatPart[]; } +const NotFormattable = Symbol('not-formattable'); + /** * `number` accepts a number, BigInt or string representing a JSON number as input * and formats it with the same options as @@ -52,11 +54,12 @@ export interface MessageNumberPart extends MessageExpressionPart { */ export function number( { localeMatcher, locales, source }: MessageFunctionContext, - options: Record, + options: Record, input?: unknown ): MessageNumber { let value: unknown; - const opt: Intl.NumberFormatOptions & Intl.PluralRulesOptions = { + const opt: Intl.NumberFormatOptions & + Intl.PluralRulesOptions & { select?: 'exact' | 'cardinal' | 'ordinal' } = { localeMatcher }; switch (typeof input) { @@ -88,6 +91,7 @@ export function number( try { switch (name) { case 'locale': + case 'type': // used internally by Intl.PluralRules, but called 'select' here break; case 'minimumIntegerDigits': case 'minimumFractionDigits': @@ -112,6 +116,7 @@ export function number( } } + const formattable = !options[NotFormattable]; const lc = mergeLocales(locales, input, options); const num = value; let locale: string | undefined; @@ -130,21 +135,87 @@ export function number( selectKey(keys) { const str = String(num); if (keys.has(str)) return str; + if (opt.select === 'exact') return null; + const pluralOpt = opt.select + ? { ...opt, select: undefined, type: opt.select } + : opt; // Intl.PluralRules needs a number, not bigint - cat ??= new Intl.PluralRules(lc, opt).select(Number(num)); + cat ??= new Intl.PluralRules(lc, pluralOpt).select(Number(num)); return keys.has(cat) ? cat : null; }, - toParts() { - nf ??= new Intl.NumberFormat(lc, opt); - const parts = nf.formatToParts(num); - locale ??= nf.resolvedOptions().locale; - return [{ type: 'number', source, locale, parts }]; - }, - toString() { - nf ??= new Intl.NumberFormat(lc, opt); - str ??= nf.format(num); - return str; - }, + toParts: formattable + ? () => { + nf ??= new Intl.NumberFormat(lc, opt); + const parts = nf.formatToParts(num); + locale ??= nf.resolvedOptions().locale; + return [{ type: 'number', source, locale, parts }]; + } + : undefined, + toString: formattable + ? () => { + nf ??= new Intl.NumberFormat(lc, opt); + str ??= nf.format(num); + return str; + } + : undefined, valueOf: () => num }; } + +/** + * `integer` accepts a number, BigInt or string representing a JSON number as input + * and formats it with the same options as + * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat | Intl.NumberFormat}. + * It also supports plural category selection via + * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules | Intl.PluralRules}. + * + * The `maximumFractionDigits=0` and `style='decimal'` options are fixed for `:integer`. + * + * @beta + */ +export const integer = ( + ctx: MessageFunctionContext, + options: Record, + input?: unknown +) => + number( + ctx, + { ...options, maximumFractionDigits: 0, style: 'decimal' }, + input + ); + +/** + * `ordinal` accepts a number, BigInt or string representing a JSON number as input + * and acts as a ordinal category selector on the value, using + * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules | Intl.PluralRules}. + * + * `ordinal` cannot be used as a formatter. + * + * @beta + */ +export const ordinal = ( + ctx: MessageFunctionContext, + options: Record, + input?: unknown +) => + number(ctx, { ...options, select: 'ordinal', [NotFormattable]: true }, input); + +/** + * `plural` accepts a number, BigInt or string representing a JSON number as input + * and acts as a cardinal plural selector on the value, using + * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules | Intl.PluralRules}. + * + * `plural` cannot be used as a formatter. + * + * @beta + */ +export const plural = ( + ctx: MessageFunctionContext, + options: Record, + input?: unknown +) => + number( + ctx, + { ...options, select: 'cardinal', [NotFormattable]: true }, + input + ); From 3de36095e3c6e98123cf97cb36f00b8184920ca5 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 2 Jan 2024 22:32:05 +0200 Subject: [PATCH 25/44] feat(mf2): Add a data model validator & visitor --- packages/mf2-icu-mf1/src/mf1.test.ts | 10 +- .../mf2-messageformat/src/data-model/types.ts | 1 + .../src/data-model/validate.ts | 153 ++++++++++++++++++ .../mf2-messageformat/src/data-model/visit.ts | 74 +++++++++ .../mf2-messageformat/src/extra/validate.ts | 45 ------ packages/mf2-messageformat/src/index.ts | 3 +- 6 files changed, 239 insertions(+), 47 deletions(-) create mode 100644 packages/mf2-messageformat/src/data-model/validate.ts create mode 100644 packages/mf2-messageformat/src/data-model/visit.ts delete mode 100644 packages/mf2-messageformat/src/extra/validate.ts diff --git a/packages/mf2-icu-mf1/src/mf1.test.ts b/packages/mf2-icu-mf1/src/mf1.test.ts index cf739edb..0fd3ef66 100644 --- a/packages/mf2-icu-mf1/src/mf1.test.ts +++ b/packages/mf2-icu-mf1/src/mf1.test.ts @@ -377,7 +377,15 @@ for (const [title, cases] of Object.entries(testCases)) { test(strParam.join(', '), () => { const data = mf1ToMessageData(parse(src)); const mf = mf1ToMessage(data, locale); - validate(data, mf.resolvedOptions().functions); + const req = validate(data, type => { + throw new Error(`Validation failed: ${type}`); + }); + const { functions } = mf.resolvedOptions(); + for (const fn of req.functions) { + if (typeof functions[fn] !== 'function') { + throw new Error(`Unknown message function: ${fn}`); + } + } const msg = mf.format( param as Record ); diff --git a/packages/mf2-messageformat/src/data-model/types.ts b/packages/mf2-messageformat/src/data-model/types.ts index e865e994..147673a3 100644 --- a/packages/mf2-messageformat/src/data-model/types.ts +++ b/packages/mf2-messageformat/src/data-model/types.ts @@ -188,6 +188,7 @@ export interface UnsupportedAnnotation { sigil: '!' | '@' | '#' | '%' | '^' | '&' | '*' | '<' | '>' | '?' | '~' | '�'; source: string; name?: never; + options?: never; } /** diff --git a/packages/mf2-messageformat/src/data-model/validate.ts b/packages/mf2-messageformat/src/data-model/validate.ts new file mode 100644 index 00000000..a897ecef --- /dev/null +++ b/packages/mf2-messageformat/src/data-model/validate.ts @@ -0,0 +1,153 @@ +import type { MessageDataModelError } from '../errors.js'; +import type { + Declaration, + Expression, + Literal, + Message, + Option, + VariableRef, + Variant +} from './types.js'; +import { visit } from './visit.js'; + +/** + * Ensure that the `msg` data model is _valid_, calling `onError` on errors. + * + * @remarks + * Detects the following errors: + * + * - **Variant Key Mismatch**: + * The number of keys on a _variant_ does not equal the number of _selectors_. + * + * - **Missing Fallback Variant**: + * The message does not include a _variant_ with only catch-all keys. + * + * - **Missing Selector Annotation**: + * A _selector_ does not have an _annotation_, + * or contains a _variable_ that does not directly or indirectly + * reference a _declaration_ with an _annotation_. + * + * - **Duplicate Declaration**: + * A _variable_ appears in two _declarations_. + * + * - **Invalid Forward Reference**: + * A _declaration_ _expression_ refers to a _variable_ defined by a later _declaration_. + * + * - **Duplicate Option Name**: + * The same _identifier_ appears as the name of more than one _option_ in the same _expression_. + * + * @returns The sets of runtime `functions` and `variables` used by the message. + * @beta + */ +export function validate( + msg: Message, + onError: ( + type: MessageDataModelError['type'], + node: Message | Declaration | Expression | Option | VariableRef | Variant + ) => void +) { + const annotated = new Set(); + const declared = new Set(); + let selectorCount = 0; + let missingFallback = false; + + const declRefs = new Map(); + const addDeclarationRef = (ref: Literal | VariableRef | undefined) => { + if (ref?.type === 'variable') { + const prev = declRefs.get(ref.name); + if (prev) prev.push(ref); + else declRefs.set(ref.name, [ref]); + } + }; + + const functions = new Set(); + const localVars = new Set(); + const variables = new Set(); + + const visitOptions = ( + options: Option[] | undefined, + context: 'declaration' | 'selector' | 'placeholder' + ) => { + if (options) { + for (let i = 0; i < options.length; ++i) { + const { name, value } = options[i]; + for (let j = i + 1; j < options.length; ++j) { + if (options[j].name === name) onError('duplicate-option', options[j]); + } + if (value.type === 'variable') { + variables.add(value.name); + if (context === 'declaration') addDeclarationRef(value); + } + } + } + }; + + visit(msg, { + declaration(decl) { + if (decl.name) { + if (declared.has(decl.name)) onError('duplicate-declaration', decl); + else declared.add(decl.name); + + for (const ref of declRefs.get(decl.name) ?? []) { + if (decl.type !== 'input' || ref !== decl.value.arg) { + onError('forward-reference', ref); + } + } + + if ( + decl.value.annotation || + (decl.type === 'local' && + decl.value.arg?.type === 'variable' && + annotated.has(decl.value.arg.name)) + ) { + annotated.add(decl.name); + } + + if (decl.type === 'local') localVars.add(decl.name); + } + }, + + expression(expression, context) { + const { arg, annotation } = expression; + visitOptions(annotation?.options, context); + + if (annotation?.type === 'function') functions.add(annotation.name); + + switch (context) { + case 'declaration': { + addDeclarationRef(arg); + break; + } + + case 'selector': + selectorCount += 1; + missingFallback = true; + if ( + !annotation && + (arg?.type !== 'variable' || !annotated.has(arg.name)) + ) { + onError('missing-selector-annotation', expression); + } + break; + } + }, + + markup({ options }, context) { + visitOptions(options, context); + }, + + variant(variant) { + const { keys } = variant; + if (keys.length !== selectorCount) onError('key-mismatch', variant); + if (missingFallback && keys.every(key => key.type === '*')) { + missingFallback = false; + } + } + }); + + if (missingFallback) onError('missing-fallback', msg); + + for (const lv of localVars) variables.delete(lv); + + return { functions, variables }; +} diff --git a/packages/mf2-messageformat/src/data-model/visit.ts b/packages/mf2-messageformat/src/data-model/visit.ts new file mode 100644 index 00000000..d804777e --- /dev/null +++ b/packages/mf2-messageformat/src/data-model/visit.ts @@ -0,0 +1,74 @@ +import type { + Declaration, + Expression, + Markup, + Message, + Pattern, + Variant +} from './types.js'; + +/** + * Apply visitor functions to a message. + * + * @remarks + * The visitors are applied in data model order, starting from leaf nodes. + * + * @beta + */ +export function visit( + msg: Message, + visitors: { + declaration?: (declaration: Declaration) => void; + expression?: ( + expression: Expression, + context: 'declaration' | 'selector' | 'placeholder' + ) => void; + markup?: (markup: Markup, context: 'declaration' | 'placeholder') => void; + pattern?: (pattern: Pattern) => void; + variant?: (variant: Variant) => void; + } +) { + const { declaration, expression, markup, pattern, variant } = visitors; + + const handleElement = ( + exp: string | Expression | Markup, + context: 'declaration' | 'placeholder' + ) => { + if (typeof exp === 'object') { + switch (exp.type) { + case 'expression': + expression?.(exp, context); + break; + case 'markup': + markup?.(exp, context); + break; + } + } + }; + + const handlePattern = (pat: Pattern) => { + for (const el of pat.body) handleElement(el, 'placeholder'); + pattern?.(pat); + }; + + for (const decl of msg.declarations) { + if (decl.value) { + expression?.(decl.value, 'declaration'); + } else { + for (const exp of decl.expressions) handleElement(exp, 'declaration'); + } + declaration?.(decl); + } + + if (msg.type === 'message') { + handlePattern(msg.pattern); + } else { + if (expression) { + for (const sel of msg.selectors) expression(sel, 'selector'); + } + for (const vari of msg.variants) { + handlePattern(vari.value); + variant?.(vari); + } + } +} diff --git a/packages/mf2-messageformat/src/extra/validate.ts b/packages/mf2-messageformat/src/extra/validate.ts deleted file mode 100644 index 11bd9579..00000000 --- a/packages/mf2-messageformat/src/extra/validate.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { Message, Pattern } from '../data-model/types.js'; -import { isExpression, isFunctionAnnotation } from '../data-model/type-guards.js'; -import type { MessageFunctions } from '../runtime'; - -function validateParts(parts: Pattern['body'], functions: MessageFunctions) { - for (const part of parts) { - if (isExpression(part) && isFunctionAnnotation(part.annotation)) { - const { name } = part.annotation; - if (typeof functions[name] !== 'function') { - throw new ReferenceError(`Runtime function not available: ${name}`); - } - // TODO: Once runtime arg requirements are defined, test against them - } - } -} - -/** - * Validate a message. - * - * @remarks - * Throws if `msg` is not a pattern or select message, - * or if it references runtime functions that are not available in the `functions`. - * - * Formatting a message that passes validation may still fail, - * as it may depend on parameters that are not passed in, - * or its runtime function calls may fail. - * - * @beta - */ -export function validate(msg: Readonly, functions: MessageFunctions) { - switch (msg.type) { - case 'message': - validateParts(msg.pattern.body, functions); - break; - case 'select': - validateParts(msg.selectors, functions); - for (const { value } of msg.variants) { - validateParts(value.body, functions); - } - break; - default: - // @ts-expect-error With TS, this condition should never be reached. - throw new Error(`Invalid message type: ${msg.type}`); - } -} diff --git a/packages/mf2-messageformat/src/index.ts b/packages/mf2-messageformat/src/index.ts index 3394a398..73f6c6cf 100644 --- a/packages/mf2-messageformat/src/index.ts +++ b/packages/mf2-messageformat/src/index.ts @@ -1,6 +1,8 @@ export { asDataModel, parseMessage, type CST } from './cst-parser/index.js'; export type * from './data-model/types.js'; export * from './data-model/type-guards.js'; +export { validate } from './data-model/validate.js'; +export { visit } from './data-model/visit.js'; export { MessageDataModelError, MessageError, @@ -10,4 +12,3 @@ export { export { MessageFormat, MessageFormatOptions } from './messageformat.js'; export { stringifyMessage } from './stringifier/message.js'; export * from './runtime/index.js'; -export { validate } from './extra/validate.js'; // must be after ./messageformat -- but why!? From 6d31d55fec5dfc51e553629eb75f305a09b0cd76 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 2 Jan 2024 22:33:41 +0200 Subject: [PATCH 26/44] feat(mf2-fluent): Add default-true detectNumberSelection option --- packages/mf2-fluent/src/fluent-to-message.ts | 76 ++++++++++++---- packages/mf2-fluent/src/fluent-to-resource.ts | 19 ++-- packages/mf2-fluent/src/fluent.test.ts | 89 ++++++++++++++----- packages/mf2-fluent/src/message-to-fluent.ts | 14 ++- packages/mf2-xliff/src/mf2xliff.test.ts | 4 + 5 files changed, 150 insertions(+), 52 deletions(-) diff --git a/packages/mf2-fluent/src/fluent-to-message.ts b/packages/mf2-fluent/src/fluent-to-message.ts index e4c861a3..e5ca2d55 100644 --- a/packages/mf2-fluent/src/fluent-to-message.ts +++ b/packages/mf2-fluent/src/fluent-to-message.ts @@ -18,20 +18,6 @@ interface SelectArg { keys: (string | number | typeof CATCHALL)[]; } -function asSelectArg(sel: Fluent.SelectExpression): SelectArg { - let defaultName = ''; - const keys = sel.variants.map(v => { - const name = v.key.type === 'Identifier' ? v.key.name : v.key.parse().value; - if (v.default) { - defaultName = String(name); - return CATCHALL; - } else { - return name; - } - }); - return { selector: sel.selector, defaultName, keys }; -} - function findSelectArgs(pattern: Fluent.Pattern): SelectArg[] { const args: SelectArg[] = []; const add = (arg: SelectArg) => { @@ -39,10 +25,23 @@ function findSelectArgs(pattern: Fluent.Pattern): SelectArg[] { if (prev) for (const key of arg.keys) prev.keys.push(key); else args.push(arg); }; + for (const el of pattern.elements) { if (el.type === 'Placeable' && el.expression.type === 'SelectExpression') { - add(asSelectArg(el.expression)); - for (const v of el.expression.variants) { + const { selector, variants } = el.expression; + let defaultName = ''; + const keys = variants.map(v => { + const name = + v.key.type === 'Identifier' ? v.key.name : v.key.parse().value; + if (v.default) { + defaultName = String(name); + return CATCHALL; + } else { + return name; + } + }); + add({ selector, defaultName, keys }); + for (const v of variants) { for (const arg of findSelectArgs(v.value)) add(arg); } } @@ -50,6 +49,40 @@ function findSelectArgs(pattern: Fluent.Pattern): SelectArg[] { return args; } +function asSelectExpression( + { selector, defaultName, keys }: SelectArg, + detectNumberSelection: boolean = true +): Expression { + switch (selector.type) { + case 'StringLiteral': + return { + type: 'expression', + arg: asValue(selector), + annotation: { type: 'function', name: 'string' } + }; + case 'VariableReference': { + let name = detectNumberSelection ? 'number' : 'string'; + if (name === 'number') { + for (const key of [...keys, defaultName]) { + if ( + typeof key === 'string' && + !['zero', 'one', 'two', 'few', 'many', 'other'].includes(key) + ) { + name = 'string'; + break; + } + } + } + return { + type: 'expression', + arg: asValue(selector), + annotation: { type: 'function', name } + }; + } + } + return asExpression(selector); +} + function asValue(exp: Fluent.InlineExpression): Literal | VariableRef { switch (exp.type) { case 'NumberLiteral': @@ -72,8 +105,9 @@ function asExpression(exp: Fluent.Expression): Expression { annotation: { type: 'function', name: 'number' } }; case 'StringLiteral': - case 'VariableReference': + case 'VariableReference': { return { type: 'expression', arg: asValue(exp) }; + } case 'FunctionReference': { const annotation: FunctionAnnotation = { type: 'function', @@ -171,9 +205,11 @@ function asFluentSelect( * {@link messageformat#Message} data object. * * @beta + * @param options.detectNumberSelection - Set `false` to disable number selector detection based on keys. */ export function fluentToMessage( - ast: Fluent.Pattern + ast: Fluent.Pattern, + options?: { detectNumberSelection?: boolean } ): PatternMessage | SelectMessage { const args = findSelectArgs(ast); if (args.length === 0) { @@ -261,7 +297,9 @@ export function fluentToMessage( return { type: 'select', declarations: [], - selectors: args.map(arg => asExpression(arg.selector)), + selectors: args.map(arg => + asSelectExpression(arg, options?.detectNumberSelection) + ), variants }; } diff --git a/packages/mf2-fluent/src/fluent-to-resource.ts b/packages/mf2-fluent/src/fluent-to-resource.ts index 01b4a06f..844aa0c3 100644 --- a/packages/mf2-fluent/src/fluent-to-resource.ts +++ b/packages/mf2-fluent/src/fluent-to-resource.ts @@ -18,20 +18,21 @@ import { getFluentFunctions } from './functions'; * or in the shape output by {@link fluentToResourceData} as `data`. * @param locales - The locale code or codes to use for all of the resource's messages. * @param options - The MessageFormat constructor options to use for all of the resource's messages. + * @param options.detectNumberSelection - Set `false` to disable number selector detection based on keys. */ export function fluentToResource( source: string | Fluent.Resource | FluentMessageResourceData, locales?: string | string[], - options?: MessageFormatOptions + options?: MessageFormatOptions & { detectNumberSelection?: boolean } ): FluentMessageResource { const res: FluentMessageResource = new Map(); - const functions = Object.assign(getFluentFunctions(res), options?.functions); - const opt = { ...options, functions }; + const { detectNumberSelection, ...opt } = options ?? {}; + opt.functions = Object.assign(getFluentFunctions(res), options?.functions); const data = typeof source === 'string' || source instanceof Fluent.Resource - ? fluentToResourceData(source).data + ? fluentToResourceData(source, { detectNumberSelection }).data : source; for (const [id, group] of data) { let rg = res.get(id); @@ -55,10 +56,14 @@ export function fluentToResource( * @param source - A Fluent resource, * as the string contents of an FTL file or * as a {@link https://projectfluent.org/fluent.js/syntax/classes/resource.html | Fluent.Resource} + * @param options.detectNumberSelection - Set `false` to disable number selector detection based on keys. * @returns An object containing the messages as `data` and any resource-level * `comments` of the resource. */ -export function fluentToResourceData(source: string | Fluent.Resource): { +export function fluentToResourceData( + source: string | Fluent.Resource, + options?: { detectNumberSelection?: boolean } +): { data: FluentMessageResourceData; comments: string; } { @@ -76,7 +81,7 @@ export function fluentToResourceData(source: string | Fluent.Resource): { const id = msg.type === 'Term' ? `-${msg.id.name}` : msg.id.name; const group: Map = new Map(); if (msg.value) { - const entry = fluentToMessage(msg.value); + const entry = fluentToMessage(msg.value, options); if (msg.comment) entry.comment = msg.comment.content; if (groupComment) { entry.comment = entry.comment @@ -86,7 +91,7 @@ export function fluentToResourceData(source: string | Fluent.Resource): { group.set('', entry); } for (const attr of msg.attributes) { - group.set(attr.id.name, fluentToMessage(attr.value)); + group.set(attr.id.name, fluentToMessage(attr.value, options)); } data.set(id, group); break; diff --git a/packages/mf2-fluent/src/fluent.test.ts b/packages/mf2-fluent/src/fluent.test.ts index 5f0ce2dd..cae83036 100644 --- a/packages/mf2-fluent/src/fluent.test.ts +++ b/packages/mf2-fluent/src/fluent.test.ts @@ -94,7 +94,7 @@ const testCases: Record = { msg: 'num-fraction-bad', scope: { arg: 1234 }, exp: '{$arg}', - errors: ['oops'] + errors: ['bad-option'] }, { msg: 'num-style', scope: { arg: 1234 }, exp: '123,400%' }, { msg: 'num-currency', scope: { arg: 1234 }, exp: '€1,234.00' }, @@ -163,23 +163,23 @@ const testCases: Record = { } `, tests: [ - { msg: 'ref-attr', exp: 'It', errors: ['$style', 'not-selectable'] }, + { msg: 'ref-attr', exp: 'It', errors: ['$style'] }, { msg: 'ref-attr', scope: { style: 'chicago' }, exp: 'It', - errors: ['$style', 'not-selectable'] + errors: ['$style'] }, { msg: 'call-attr-no-args', exp: 'It', - errors: ['$style', 'not-selectable'] + errors: ['$style'] }, { msg: 'call-attr-no-args', scope: { style: 'chicago' }, exp: 'It', - errors: ['$style', 'not-selectable'] + errors: ['$style'] }, { msg: 'call-attr-with-expected-arg', exp: 'She' }, { @@ -190,13 +190,13 @@ const testCases: Record = { { msg: 'call-attr-with-other-arg', exp: 'It', - errors: ['$style', 'not-selectable'] + errors: ['$style'] }, { msg: 'call-attr-with-other-arg', scope: { style: 'chicago' }, exp: 'It', - errors: ['$style', 'not-selectable'] + errors: ['$style'] } ] }, @@ -254,7 +254,7 @@ const testCases: Record = { msg: 'select', scope: {}, exp: 'B', - errors: ['$selector', 'not-selectable'] + errors: ['$selector'] }, { msg: 'select', scope: { selector: 'a' }, exp: 'A' }, { msg: 'select', scope: { selector: 'b' }, exp: 'B' }, @@ -263,7 +263,7 @@ const testCases: Record = { msg: 'number', scope: {}, exp: 'B', - errors: ['$selector', 'not-selectable'] + errors: ['$selector'] }, { msg: 'number', scope: { selector: 0 }, exp: 'A' }, { msg: 'number', scope: { selector: 1 }, exp: 'B' }, @@ -272,18 +272,23 @@ const testCases: Record = { msg: 'plural', scope: {}, exp: 'B', - errors: ['$selector', 'not-selectable'] + errors: ['$selector', 'bad-input', 'not-selectable'] }, { msg: 'plural', scope: { selector: 1 }, exp: 'A' }, { msg: 'plural', scope: { selector: 2 }, exp: 'B' }, - { msg: 'plural', scope: { selector: 'one' }, exp: 'A' }, + { + msg: 'plural', + scope: { selector: 'one' }, + exp: 'B', + errors: ['bad-input', 'not-selectable'] + }, { msg: 'default', scope: {}, exp: 'D', - errors: ['$selector', 'not-selectable'] + errors: ['$selector'] }, - { msg: 'default', scope: { selector: 1 }, exp: 'A' }, + { msg: 'default', scope: { selector: 1 }, exp: 'D' }, { msg: 'default', scope: { selector: 2 }, exp: 'D' }, { msg: 'default', scope: { selector: 'one' }, exp: 'A' } ] @@ -335,7 +340,14 @@ for (const [title, { locale = 'en', src, tests }] of Object.entries( for (const [attr, mf] of group) { const { functions } = mf.resolvedOptions(); // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - validate(data.get(id)?.get(attr ?? '')!, functions); + const req = validate(data.get(id)?.get(attr ?? '')!, type => { + throw new Error(`Validation failed: ${type}`); + }); + for (const fn of req.functions) { + if (typeof functions[fn] !== 'function') { + throw new Error(`Unknown message function: ${fn}`); + } + } } } }); @@ -352,17 +364,22 @@ for (const [title, { locale = 'en', src, tests }] of Object.entries( const test_ = only ? test.only : test; test_(name, () => { const onError = jest.fn(); - const str = res - .get(msg) - ?.get(attr ?? '') - ?.format(scope, onError); + const mf = res.get(msg)!.get(attr ?? ''); + const str = mf!.format(scope, onError); if (exp instanceof RegExp) expect(str).toMatch(exp); else expect(str).toBe(exp); if (errors?.length) { + //console.dir(mf?.resolvedOptions().message,{depth:null}); + //console.dir(onError.mock.calls, { depth: null }); expect(onError).toHaveBeenCalledTimes(errors.length); for (let i = 0; i < errors.length; ++i) { const [err] = onError.mock.calls[i]; - expect(err.message).toMatch(errors[i]); + const expErr = errors[i]; + if (typeof expErr === 'string' && !expErr.startsWith('$')) { + expect(err.type).toBe(expErr); + } else { + expect(err.message).toMatch(expErr); + } } } else { expect(onError).not.toHaveBeenCalled(); @@ -374,6 +391,20 @@ for (const [title, { locale = 'en', src, tests }] of Object.entries( const template = Fluent.parse(src, { withSpans: false }); const res = resourceToFluent(data, template); + class FixResult extends Fluent.Transformer { + // When converting to MF2, number wrappers are added + visitSelectExpression(node: Fluent.SelectExpression) { + if ( + node.selector.type === 'FunctionReference' && + node.selector.id.name === 'NUMBER' + ) { + node.selector = node.selector.arguments.positional[0]; + } + return this.genericVisit(node); + } + } + new FixResult().visit(res); + class FixExpected extends Fluent.Transformer { // MF2 uses first-match selection, so default variants will be last visitSelectExpression(node: Fluent.SelectExpression) { @@ -456,7 +487,7 @@ describe('formatToParts', () => { const onError = jest.fn(); const sel = res.get('sel')?.get('')?.formatToParts(undefined, onError); expect(sel).toEqual([{ type: 'literal', value: 'B' }]); - expect(onError).toHaveBeenCalledTimes(2); + expect(onError).toHaveBeenCalledTimes(1); }); }); @@ -612,7 +643,9 @@ describe('formatToParts', () => { const onError = jest.fn(); const msg = res.get('gender')?.get('')?.formatToParts(undefined, onError); expect(msg).toEqual([{ type: 'literal', value: 'N' }]); - expect(onError).toHaveBeenCalledTimes(2); + expect(onError.mock.calls.map(args => args[0].type)).toEqual([ + 'unresolved-var' + ]); }); test('plural with match', () => { @@ -631,8 +664,16 @@ describe('formatToParts', () => { }); test('plural with non-plural input', () => { - const msg = res.get('plural')?.get('')?.formatToParts({ num: 'NaN' }); + const onError = jest.fn(); + const msg = res + .get('plural') + ?.get('') + ?.formatToParts({ num: 'NaN' }, onError); expect(msg).toEqual([{ type: 'literal', value: 'Other' }]); + expect(onError.mock.calls.map(args => args[0].type)).toEqual([ + 'bad-input', + 'not-selectable' + ]); }); }); }); @@ -709,12 +750,12 @@ describe('fluentToResourceData', () => { const res = resourceToFluent(data); expect(Fluent.serialize(res, {})).toBe(source` single = - { $num -> + { NUMBER($num) -> [0] One Zero selector. *[other] One Other selector. } multi = - { $num -> + { NUMBER($num) -> [0] { $gender -> [feminine] Combine Zero multiple F selectors. diff --git a/packages/mf2-fluent/src/message-to-fluent.ts b/packages/mf2-fluent/src/message-to-fluent.ts index 86013867..2bd87348 100644 --- a/packages/mf2-fluent/src/message-to-fluent.ts +++ b/packages/mf2-fluent/src/message-to-fluent.ts @@ -26,7 +26,7 @@ type MsgContext = { functionMap: FunctionMap; }; -export type FunctionMap = Record; +export type FunctionMap = Record; /** * Default value for the {@link messageToFluent} `functionMap` option. @@ -35,7 +35,9 @@ export type FunctionMap = Record; export const defaultFunctionMap: FunctionMap = { datetime: 'DATETIME', message: FluentMessageRef, - number: 'NUMBER' + number: 'NUMBER', + plural: 'NUMBER', + string: null }; const isIdentifier = (value: string) => /^[a-zA-Z][\w-]*$/.test(value); @@ -177,6 +179,14 @@ function functionRefToFluent( } const id = ctx.functionMap[name]; + if (id === null) { + if (args.named.length > 0) { + throw new Error( + `The function :${name} is dropped in Fluent, so cannot have options.` + ); + } + return args.positional[0]; + } if ( id === 'NUMBER' && args.positional[0] instanceof Fluent.NumberLiteral && diff --git a/packages/mf2-xliff/src/mf2xliff.test.ts b/packages/mf2-xliff/src/mf2xliff.test.ts index fae38f4b..f742f413 100644 --- a/packages/mf2-xliff/src/mf2xliff.test.ts +++ b/packages/mf2-xliff/src/mf2xliff.test.ts @@ -58,6 +58,7 @@ test('source only', () => { + @@ -139,6 +140,7 @@ test('combine source & target', () => { + @@ -191,9 +193,11 @@ test('selector mismatch between source & target languages', () => { + + From ddc286dbe0d1c800e51778cf9015cc2ac0afd591 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 2 Jan 2024 23:50:41 +0200 Subject: [PATCH 27/44] refactor(mf2): Rename src/runtime/ as src/functions/, add src/formatted-parts.ts --- packages/mf2-icu-mf1/src/functions.ts | 6 +- .../src/data-model/format-markup.ts | 4 +- .../src/data-model/resolve-expression.ts | 6 +- .../data-model/resolve-function-annotation.ts | 5 +- .../src/data-model/resolve-literal.ts | 2 +- .../resolve-unsupported-annotation.ts | 4 +- .../src/data-model/resolve-variable.ts | 9 +-- .../mf2-messageformat/src/format-context.ts | 2 +- .../mf2-messageformat/src/formatted-parts.ts | 40 ++++++++++ .../src/{runtime => functions}/datetime.ts | 7 +- .../src/{runtime => functions}/fallback.ts | 3 +- .../src/{runtime => functions}/index.ts | 73 +++++-------------- .../src/{runtime => functions}/number.ts | 7 +- .../src/{runtime => functions}/string.ts | 7 +- .../src/{runtime => functions}/unknown.ts | 3 +- .../src/{runtime => functions}/utils.ts | 0 packages/mf2-messageformat/src/index.ts | 13 +++- .../mf2-messageformat/src/messageformat.ts | 4 +- 18 files changed, 99 insertions(+), 96 deletions(-) create mode 100644 packages/mf2-messageformat/src/formatted-parts.ts rename packages/mf2-messageformat/src/{runtime => functions}/datetime.ts (95%) rename packages/mf2-messageformat/src/{runtime => functions}/fallback.ts (83%) rename packages/mf2-messageformat/src/{runtime => functions}/index.ts (52%) rename packages/mf2-messageformat/src/{runtime => functions}/number.ts (97%) rename packages/mf2-messageformat/src/{runtime => functions}/string.ts (87%) rename packages/mf2-messageformat/src/{runtime => functions}/unknown.ts (83%) rename packages/mf2-messageformat/src/{runtime => functions}/utils.ts (100%) diff --git a/packages/mf2-icu-mf1/src/functions.ts b/packages/mf2-icu-mf1/src/functions.ts index 2da6d494..c312c3a2 100644 --- a/packages/mf2-icu-mf1/src/functions.ts +++ b/packages/mf2-icu-mf1/src/functions.ts @@ -3,7 +3,7 @@ import { type MessageFunctionContext, type MessageNumber, type MessageValue, - datetime + defaultFunctions } from 'messageformat'; function getParam(options: Record) { @@ -33,7 +33,7 @@ function date( : 'short', year: 'numeric' }; - return datetime(msgCtx, opt, input); + return defaultFunctions.datetime(msgCtx, opt, input); } function time( @@ -48,7 +48,7 @@ function time( hour: 'numeric', timeZoneName: size === 'full' || size === 'long' ? 'short' : undefined }; - return datetime(msgCtx, opt, input); + return defaultFunctions.datetime(msgCtx, opt, input); } /** diff --git a/packages/mf2-messageformat/src/data-model/format-markup.ts b/packages/mf2-messageformat/src/data-model/format-markup.ts index 1fea4e0a..852465cd 100644 --- a/packages/mf2-messageformat/src/data-model/format-markup.ts +++ b/packages/mf2-messageformat/src/data-model/format-markup.ts @@ -1,9 +1,9 @@ -import { resolveValue } from './resolve-value.js'; import type { Context } from '../format-context.js'; import type { MessageMarkupClosePart, MessageMarkupPart -} from '../runtime/index.js'; +} from '../formatted-parts.js'; +import { resolveValue } from './resolve-value.js'; import type { Markup } from './types.js'; export function formatMarkup( diff --git a/packages/mf2-messageformat/src/data-model/resolve-expression.ts b/packages/mf2-messageformat/src/data-model/resolve-expression.ts index 57d2b591..d94f2a7f 100644 --- a/packages/mf2-messageformat/src/data-model/resolve-expression.ts +++ b/packages/mf2-messageformat/src/data-model/resolve-expression.ts @@ -1,10 +1,10 @@ -import type { Context } from '../format-context'; -import type { MessageValue } from '../runtime'; -import type { Expression } from './types'; +import type { Context } from '../format-context.js'; +import type { MessageValue } from '../functions/index.js'; import { resolveFunctionAnnotation } from './resolve-function-annotation.js'; import { resolveLiteral } from './resolve-literal.js'; import { resolveUnsupportedAnnotation } from './resolve-unsupported-annotation.js'; import { resolveVariableRef } from './resolve-variable.js'; +import type { Expression } from './types.js'; export function resolveExpression( ctx: Context, diff --git a/packages/mf2-messageformat/src/data-model/resolve-function-annotation.ts b/packages/mf2-messageformat/src/data-model/resolve-function-annotation.ts index 949140de..6f7d1d23 100644 --- a/packages/mf2-messageformat/src/data-model/resolve-function-annotation.ts +++ b/packages/mf2-messageformat/src/data-model/resolve-function-annotation.ts @@ -1,13 +1,14 @@ import { MessageError } from '../errors.js'; import type { Context } from '../format-context.js'; -import { MessageFunctionContext, fallback } from '../runtime/index.js'; +import { fallback } from '../functions/fallback.js'; +import { MessageFunctionContext } from '../functions/index.js'; +import { getValueSource, resolveValue } from './resolve-value.js'; import type { FunctionAnnotation, Literal, Option, VariableRef } from './types.js'; -import { getValueSource, resolveValue } from './resolve-value.js'; export function resolveFunctionAnnotation( ctx: Context, diff --git a/packages/mf2-messageformat/src/data-model/resolve-literal.ts b/packages/mf2-messageformat/src/data-model/resolve-literal.ts index 45e028a4..7b3471d7 100644 --- a/packages/mf2-messageformat/src/data-model/resolve-literal.ts +++ b/packages/mf2-messageformat/src/data-model/resolve-literal.ts @@ -1,5 +1,5 @@ import type { Context } from '../format-context.js'; -import { MessageFunctionContext } from '../runtime/index.js'; +import { MessageFunctionContext } from '../functions/index.js'; import type { Literal } from './types.js'; export function resolveLiteral(ctx: Context, lit: Literal) { diff --git a/packages/mf2-messageformat/src/data-model/resolve-unsupported-annotation.ts b/packages/mf2-messageformat/src/data-model/resolve-unsupported-annotation.ts index 55ae87e7..a9046d9b 100644 --- a/packages/mf2-messageformat/src/data-model/resolve-unsupported-annotation.ts +++ b/packages/mf2-messageformat/src/data-model/resolve-unsupported-annotation.ts @@ -1,8 +1,8 @@ import { MessageResolutionError } from '../errors.js'; import type { Context } from '../format-context.js'; -import { fallback } from '../runtime/index.js'; -import type { Literal, UnsupportedAnnotation, VariableRef } from './types.js'; +import { fallback } from '../functions/fallback.js'; import { getValueSource } from './resolve-value.js'; +import type { Literal, UnsupportedAnnotation, VariableRef } from './types.js'; export function resolveUnsupportedAnnotation( ctx: Context, diff --git a/packages/mf2-messageformat/src/data-model/resolve-variable.ts b/packages/mf2-messageformat/src/data-model/resolve-variable.ts index 274c1f66..54493712 100644 --- a/packages/mf2-messageformat/src/data-model/resolve-variable.ts +++ b/packages/mf2-messageformat/src/data-model/resolve-variable.ts @@ -1,11 +1,8 @@ import { MessageResolutionError } from '../errors.js'; import type { Context } from '../format-context.js'; -import { - MessageFunctionContext, - MessageValue, - fallback, - unknown -} from '../runtime/index.js'; +import { fallback } from '../functions/fallback.js'; +import { MessageFunctionContext, MessageValue } from '../functions/index.js'; +import { unknown } from '../functions/unknown.js'; import type { Expression, VariableRef } from './types.js'; import { resolveExpression } from './resolve-expression.js'; diff --git a/packages/mf2-messageformat/src/format-context.ts b/packages/mf2-messageformat/src/format-context.ts index c4a2983c..832851a1 100644 --- a/packages/mf2-messageformat/src/format-context.ts +++ b/packages/mf2-messageformat/src/format-context.ts @@ -1,4 +1,4 @@ -import type { MessageFunctions, MessageValue } from './runtime'; +import type { MessageFunctions, MessageValue } from './functions'; export interface Context { functions: MessageFunctions; diff --git a/packages/mf2-messageformat/src/formatted-parts.ts b/packages/mf2-messageformat/src/formatted-parts.ts new file mode 100644 index 00000000..bc6a8ddd --- /dev/null +++ b/packages/mf2-messageformat/src/formatted-parts.ts @@ -0,0 +1,40 @@ +export type { MessageDateTimePart } from './functions/datetime.js'; +export type { MessageFallbackPart } from './functions/fallback.js'; +export type { MessageNumberPart } from './functions/number.js'; +export type { MessageStringPart } from './functions/string.js'; +export type { MessageUnknownPart } from './functions/unknown.js'; + +export interface MessageExpressionPart { + type: string; + source: string; + locale?: string; + parts?: Array<{ type: string; source?: string; value?: unknown }>; + value?: unknown; +} + +export interface MessageLiteralPart { + type: 'literal'; + value: string; +} + +export interface MessageMarkupPart { + type: 'markup'; + kind: 'open' | 'standalone'; + source: string; + name: string; + options?: { [key: string]: unknown }; +} + +export interface MessageMarkupClosePart { + type: 'markup'; + kind: 'close'; + source: string; + name: string; + options?: never; +} + +export type MessagePart = + | MessageExpressionPart + | MessageLiteralPart + | MessageMarkupClosePart + | MessageMarkupPart; diff --git a/packages/mf2-messageformat/src/runtime/datetime.ts b/packages/mf2-messageformat/src/functions/datetime.ts similarity index 95% rename from packages/mf2-messageformat/src/runtime/datetime.ts rename to packages/mf2-messageformat/src/functions/datetime.ts index ad47b1c6..46c00e38 100644 --- a/packages/mf2-messageformat/src/runtime/datetime.ts +++ b/packages/mf2-messageformat/src/functions/datetime.ts @@ -1,9 +1,6 @@ import { MessageResolutionError } from '../errors.js'; -import type { - MessageExpressionPart, - MessageFunctionContext, - MessageValue -} from './index.js'; +import type { MessageFunctionContext, MessageValue } from './index.js'; +import type { MessageExpressionPart } from '../formatted-parts.js'; import { asBoolean, asPositiveInteger, diff --git a/packages/mf2-messageformat/src/runtime/fallback.ts b/packages/mf2-messageformat/src/functions/fallback.ts similarity index 83% rename from packages/mf2-messageformat/src/runtime/fallback.ts rename to packages/mf2-messageformat/src/functions/fallback.ts index e3370547..dade07df 100644 --- a/packages/mf2-messageformat/src/runtime/fallback.ts +++ b/packages/mf2-messageformat/src/functions/fallback.ts @@ -1,4 +1,5 @@ -import type { MessageExpressionPart, MessageValue } from './index.js'; +import type { MessageValue } from './index.js'; +import type { MessageExpressionPart } from '../formatted-parts.js'; /** * Used to represent runtime/formatting errors. diff --git a/packages/mf2-messageformat/src/runtime/index.ts b/packages/mf2-messageformat/src/functions/index.ts similarity index 52% rename from packages/mf2-messageformat/src/runtime/index.ts rename to packages/mf2-messageformat/src/functions/index.ts index d9a0cbc0..f18ae3b7 100644 --- a/packages/mf2-messageformat/src/runtime/index.ts +++ b/packages/mf2-messageformat/src/functions/index.ts @@ -1,26 +1,16 @@ import { Context } from '../format-context.js'; +import { MessageExpressionPart } from '../formatted-parts.js'; import { datetime } from './datetime.js'; +import { fallback } from './fallback.js'; import { integer, number, ordinal, plural } from './number.js'; import { string } from './string.js'; +import { unknown } from './unknown.js'; -export { MessageDateTime, MessageDateTimePart, datetime } from './datetime.js'; -export { MessageFallback, MessageFallbackPart, fallback } from './fallback.js'; -export { - MessageNumber, - MessageNumberPart, - integer, - number, - ordinal, - plural -} from './number.js'; -export { MessageString, MessageStringPart, string } from './string.js'; -export { MessageUnknownPart, MessageUnknownValue, unknown } from './unknown.js'; -export { - asBoolean, - asPositiveInteger, - asString, - mergeLocales -} from './utils.js'; +export type { MessageDateTime } from './datetime.js'; +export type { MessageFallback } from './fallback.js'; +export type { MessageNumber } from './number.js'; +export type { MessageString } from './string.js'; +export type { MessageUnknownValue } from './unknown.js'; export interface MessageValue { readonly type: string; @@ -33,55 +23,28 @@ export interface MessageValue { valueOf?: () => unknown; } -export interface MessageExpressionPart { - type: string; - source: string; - locale?: string; - parts?: Array<{ type: string; source?: string; value?: unknown }>; - value?: unknown; -} - -export interface MessageLiteralPart { - type: 'literal'; - value: string; -} - -export interface MessageMarkupPart { - type: 'markup'; - kind: 'open' | 'standalone'; - source: string; - name: string; - options?: { [key: string]: unknown }; -} - -export interface MessageMarkupClosePart { - type: 'markup'; - kind: 'close'; - source: string; - name: string; - options?: never; -} - -export type MessagePart = - | MessageExpressionPart - | MessageLiteralPart - | MessageMarkupClosePart - | MessageMarkupPart; - /** * The default Runtime includes three functions, `datetime`, `number` and `string`, * as well as the `integer`, `ordinal`, and `plural` aliases for `number`. * * @beta */ -export const defaultFunctions = { +export const defaultFunctions = Object.freeze({ datetime, integer, number, ordinal, plural, string -}; +}); + +/** + * Utility functions not available directly as named functions, + * but used to handle fallback values and input variables of unknown type. + * + * @beta + */ +export const utilFunctions = Object.freeze({ fallback, unknown }); export class MessageFunctionContext { #ctx: Context; diff --git a/packages/mf2-messageformat/src/runtime/number.ts b/packages/mf2-messageformat/src/functions/number.ts similarity index 97% rename from packages/mf2-messageformat/src/runtime/number.ts rename to packages/mf2-messageformat/src/functions/number.ts index c62b0e37..5508f395 100644 --- a/packages/mf2-messageformat/src/runtime/number.ts +++ b/packages/mf2-messageformat/src/functions/number.ts @@ -5,11 +5,8 @@ import { asString, mergeLocales } from './utils.js'; -import type { - MessageExpressionPart, - MessageFunctionContext, - MessageValue -} from './index.js'; +import type { MessageFunctionContext, MessageValue } from './index.js'; +import type { MessageExpressionPart } from '../formatted-parts.js'; export interface MessageNumber extends MessageValue { readonly type: 'number'; diff --git a/packages/mf2-messageformat/src/runtime/string.ts b/packages/mf2-messageformat/src/functions/string.ts similarity index 87% rename from packages/mf2-messageformat/src/runtime/string.ts rename to packages/mf2-messageformat/src/functions/string.ts index 9db97049..2cfff62e 100644 --- a/packages/mf2-messageformat/src/runtime/string.ts +++ b/packages/mf2-messageformat/src/functions/string.ts @@ -1,8 +1,5 @@ -import type { - MessageExpressionPart, - MessageFunctionContext, - MessageValue -} from './index.js'; +import type { MessageFunctionContext, MessageValue } from './index.js'; +import type { MessageExpressionPart } from '../formatted-parts.js'; import { mergeLocales } from './utils.js'; export interface MessageString extends MessageValue { diff --git a/packages/mf2-messageformat/src/runtime/unknown.ts b/packages/mf2-messageformat/src/functions/unknown.ts similarity index 83% rename from packages/mf2-messageformat/src/runtime/unknown.ts rename to packages/mf2-messageformat/src/functions/unknown.ts index 7958dcb0..17925d76 100644 --- a/packages/mf2-messageformat/src/runtime/unknown.ts +++ b/packages/mf2-messageformat/src/functions/unknown.ts @@ -1,4 +1,5 @@ -import type { MessageExpressionPart, MessageValue } from './index.js'; +import type { MessageValue } from './index.js'; +import type { MessageExpressionPart } from '../formatted-parts.js'; export interface MessageUnknownValue extends MessageValue { readonly type: 'unknown'; diff --git a/packages/mf2-messageformat/src/runtime/utils.ts b/packages/mf2-messageformat/src/functions/utils.ts similarity index 100% rename from packages/mf2-messageformat/src/runtime/utils.ts rename to packages/mf2-messageformat/src/functions/utils.ts diff --git a/packages/mf2-messageformat/src/index.ts b/packages/mf2-messageformat/src/index.ts index 73f6c6cf..f1f75b60 100644 --- a/packages/mf2-messageformat/src/index.ts +++ b/packages/mf2-messageformat/src/index.ts @@ -1,5 +1,8 @@ -export { asDataModel, parseMessage, type CST } from './cst-parser/index.js'; export type * from './data-model/types.js'; +export type * from './formatted-parts.js'; +export type * from './functions/index.js'; + +export { asDataModel, parseMessage, type CST } from './cst-parser/index.js'; export * from './data-model/type-guards.js'; export { validate } from './data-model/validate.js'; export { visit } from './data-model/visit.js'; @@ -9,6 +12,12 @@ export { MessageResolutionError, MessageSyntaxError } from './errors.js'; +export { defaultFunctions, utilFunctions } from './functions/index.js'; +export { + asBoolean, + asPositiveInteger, + asString, + mergeLocales +} from './functions/utils.js'; export { MessageFormat, MessageFormatOptions } from './messageformat.js'; export { stringifyMessage } from './stringifier/message.js'; -export * from './runtime/index.js'; diff --git a/packages/mf2-messageformat/src/messageformat.ts b/packages/mf2-messageformat/src/messageformat.ts index 1a5c8042..fda9e476 100644 --- a/packages/mf2-messageformat/src/messageformat.ts +++ b/packages/mf2-messageformat/src/messageformat.ts @@ -5,12 +5,12 @@ import { UnresolvedExpression } from './data-model/resolve-variable.js'; import type { Message } from './data-model/types.js'; import { MessageError, MessageResolutionError } from './errors.js'; import type { Context } from './format-context.js'; +import type { MessagePart } from './formatted-parts.js'; import { MessageFunctions, - MessagePart, MessageValue, defaultFunctions -} from './runtime/index.js'; +} from './functions/index.js'; import { selectPattern } from './select-pattern.js'; /** @beta */ From 7e195c88b0ffdd5e6e0d19c1bf8c499fa305b7a9 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 3 Jan 2024 01:09:09 +0200 Subject: [PATCH 28/44] refactor(mf2): Rearrange exports, adding messageformat/functions & messageformat/functions/utils --- packages/mf2-icu-mf1/src/functions.ts | 16 ++-- packages/mf2-messageformat/package.json | 12 +++ .../src/data-model/function-context.ts | 19 +++++ .../data-model/resolve-function-annotation.ts | 2 +- .../src/data-model/resolve-literal.ts | 2 +- .../src/data-model/resolve-variable.ts | 3 +- .../mf2-messageformat/src/format-context.ts | 3 +- .../src/functions/datetime.ts | 2 +- .../mf2-messageformat/src/functions/index.ts | 73 ++----------------- .../mf2-messageformat/src/functions/number.ts | 6 +- .../mf2-messageformat/src/functions/string.ts | 2 +- .../src/functions/unknown.ts | 2 +- packages/mf2-messageformat/src/index.ts | 25 +++++-- .../mf2-messageformat/src/messageformat.ts | 33 ++++++++- tsconfig.json | 9 ++- 15 files changed, 113 insertions(+), 96 deletions(-) create mode 100644 packages/mf2-messageformat/src/data-model/function-context.ts diff --git a/packages/mf2-icu-mf1/src/functions.ts b/packages/mf2-icu-mf1/src/functions.ts index c312c3a2..e08a8262 100644 --- a/packages/mf2-icu-mf1/src/functions.ts +++ b/packages/mf2-icu-mf1/src/functions.ts @@ -1,10 +1,10 @@ -import { - type MessageDateTime, - type MessageFunctionContext, - type MessageNumber, - type MessageValue, - defaultFunctions +import type { + MessageDateTime, + MessageFunctionContext, + MessageNumber, + MessageValue } from 'messageformat'; +import { datetime } from 'messageformat/functions'; function getParam(options: Record) { if (options.param) { @@ -33,7 +33,7 @@ function date( : 'short', year: 'numeric' }; - return defaultFunctions.datetime(msgCtx, opt, input); + return datetime(msgCtx, opt, input); } function time( @@ -48,7 +48,7 @@ function time( hour: 'numeric', timeZoneName: size === 'full' || size === 'long' ? 'short' : undefined }; - return defaultFunctions.datetime(msgCtx, opt, input); + return datetime(msgCtx, opt, input); } /** diff --git a/packages/mf2-messageformat/package.json b/packages/mf2-messageformat/package.json index 79e8aeb6..6c6073a9 100644 --- a/packages/mf2-messageformat/package.json +++ b/packages/mf2-messageformat/package.json @@ -25,6 +25,18 @@ "files": [ "lib/" ], + "exports": { + ".": { + "default": "./lib/index.js" + }, + "./functions": { + "default": "./lib/functions/index.js" + }, + "./functions/utils": { + "default": "./lib/functions/utils.js" + }, + "./package.json": "./package.json" + }, "scripts": { "build": "tsc --project tsconfig.build.json", "extract-api": "api-extractor run --local --verbose" diff --git a/packages/mf2-messageformat/src/data-model/function-context.ts b/packages/mf2-messageformat/src/data-model/function-context.ts new file mode 100644 index 00000000..116f7086 --- /dev/null +++ b/packages/mf2-messageformat/src/data-model/function-context.ts @@ -0,0 +1,19 @@ +import type { Context } from '../format-context.js'; + +export class MessageFunctionContext { + #ctx: Context; + readonly source: string; + constructor(ctx: Context, source: string) { + this.#ctx = ctx; + this.source = source; + } + get localeMatcher() { + return this.#ctx.localeMatcher; + } + get locales() { + return this.#ctx.locales.slice(); + } + get onError() { + return this.#ctx.onError; + } +} diff --git a/packages/mf2-messageformat/src/data-model/resolve-function-annotation.ts b/packages/mf2-messageformat/src/data-model/resolve-function-annotation.ts index 6f7d1d23..767a3392 100644 --- a/packages/mf2-messageformat/src/data-model/resolve-function-annotation.ts +++ b/packages/mf2-messageformat/src/data-model/resolve-function-annotation.ts @@ -1,7 +1,7 @@ import { MessageError } from '../errors.js'; import type { Context } from '../format-context.js'; import { fallback } from '../functions/fallback.js'; -import { MessageFunctionContext } from '../functions/index.js'; +import { MessageFunctionContext } from './function-context.js'; import { getValueSource, resolveValue } from './resolve-value.js'; import type { FunctionAnnotation, diff --git a/packages/mf2-messageformat/src/data-model/resolve-literal.ts b/packages/mf2-messageformat/src/data-model/resolve-literal.ts index 7b3471d7..547e998f 100644 --- a/packages/mf2-messageformat/src/data-model/resolve-literal.ts +++ b/packages/mf2-messageformat/src/data-model/resolve-literal.ts @@ -1,5 +1,5 @@ import type { Context } from '../format-context.js'; -import { MessageFunctionContext } from '../functions/index.js'; +import { MessageFunctionContext } from './function-context.js'; import type { Literal } from './types.js'; export function resolveLiteral(ctx: Context, lit: Literal) { diff --git a/packages/mf2-messageformat/src/data-model/resolve-variable.ts b/packages/mf2-messageformat/src/data-model/resolve-variable.ts index 54493712..c9fcb12a 100644 --- a/packages/mf2-messageformat/src/data-model/resolve-variable.ts +++ b/packages/mf2-messageformat/src/data-model/resolve-variable.ts @@ -1,8 +1,9 @@ import { MessageResolutionError } from '../errors.js'; import type { Context } from '../format-context.js'; import { fallback } from '../functions/fallback.js'; -import { MessageFunctionContext, MessageValue } from '../functions/index.js'; +import { MessageValue } from '../functions/index.js'; import { unknown } from '../functions/unknown.js'; +import { MessageFunctionContext } from './function-context.js'; import type { Expression, VariableRef } from './types.js'; import { resolveExpression } from './resolve-expression.js'; diff --git a/packages/mf2-messageformat/src/format-context.ts b/packages/mf2-messageformat/src/format-context.ts index 832851a1..cd2bcd28 100644 --- a/packages/mf2-messageformat/src/format-context.ts +++ b/packages/mf2-messageformat/src/format-context.ts @@ -1,4 +1,5 @@ -import type { MessageFunctions, MessageValue } from './functions'; +import type { MessageValue } from './functions/index.js'; +import type { MessageFunctions } from './messageformat.js'; export interface Context { functions: MessageFunctions; diff --git a/packages/mf2-messageformat/src/functions/datetime.ts b/packages/mf2-messageformat/src/functions/datetime.ts index 46c00e38..ce2bbfd8 100644 --- a/packages/mf2-messageformat/src/functions/datetime.ts +++ b/packages/mf2-messageformat/src/functions/datetime.ts @@ -1,6 +1,6 @@ import { MessageResolutionError } from '../errors.js'; -import type { MessageFunctionContext, MessageValue } from './index.js'; import type { MessageExpressionPart } from '../formatted-parts.js'; +import type { MessageFunctionContext, MessageValue } from './index.js'; import { asBoolean, asPositiveInteger, diff --git a/packages/mf2-messageformat/src/functions/index.ts b/packages/mf2-messageformat/src/functions/index.ts index f18ae3b7..5a519cac 100644 --- a/packages/mf2-messageformat/src/functions/index.ts +++ b/packages/mf2-messageformat/src/functions/index.ts @@ -1,16 +1,11 @@ -import { Context } from '../format-context.js'; -import { MessageExpressionPart } from '../formatted-parts.js'; -import { datetime } from './datetime.js'; -import { fallback } from './fallback.js'; -import { integer, number, ordinal, plural } from './number.js'; -import { string } from './string.js'; -import { unknown } from './unknown.js'; +import type { MessageExpressionPart } from '../formatted-parts.js'; -export type { MessageDateTime } from './datetime.js'; -export type { MessageFallback } from './fallback.js'; -export type { MessageNumber } from './number.js'; -export type { MessageString } from './string.js'; -export type { MessageUnknownValue } from './unknown.js'; +export type { MessageFunctionContext } from '../data-model/function-context.js'; +export { MessageDateTime, datetime } from './datetime.js'; +export { MessageFallback, fallback } from './fallback.js'; +export { MessageNumber, integer, number, ordinal, plural } from './number.js'; +export { MessageString, string } from './string.js'; +export { MessageUnknownValue, unknown } from './unknown.js'; export interface MessageValue { readonly type: string; @@ -22,57 +17,3 @@ export interface MessageValue { toString?: () => string; valueOf?: () => unknown; } - -/** - * The default Runtime includes three functions, `datetime`, `number` and `string`, - * as well as the `integer`, `ordinal`, and `plural` aliases for `number`. - * - * @beta - */ -export const defaultFunctions = Object.freeze({ - datetime, - integer, - number, - ordinal, - plural, - string -}); - -/** - * Utility functions not available directly as named functions, - * but used to handle fallback values and input variables of unknown type. - * - * @beta - */ -export const utilFunctions = Object.freeze({ fallback, unknown }); - -export class MessageFunctionContext { - #ctx: Context; - readonly source: string; - constructor(ctx: Context, source: string) { - this.#ctx = ctx; - this.source = source; - } - get localeMatcher() { - return this.#ctx.localeMatcher; - } - get locales() { - return this.#ctx.locales.slice(); - } - get onError() { - return this.#ctx.onError; - } -} - -/** - * The runtime function registry available when resolving {@link FunctionAnnotation} elements. - * - * @beta - */ -export interface MessageFunctions { - [key: string]: ( - context: MessageFunctionContext, - options: Record, - input?: unknown - ) => MessageValue; -} diff --git a/packages/mf2-messageformat/src/functions/number.ts b/packages/mf2-messageformat/src/functions/number.ts index 5508f395..8900d7eb 100644 --- a/packages/mf2-messageformat/src/functions/number.ts +++ b/packages/mf2-messageformat/src/functions/number.ts @@ -1,12 +1,12 @@ import { MessageResolutionError } from '../errors.js'; +import type { MessageExpressionPart } from '../formatted-parts.js'; +import type { MessageFunctionContext, MessageValue } from './index.js'; import { asBoolean, asPositiveInteger, asString, mergeLocales } from './utils.js'; -import type { MessageFunctionContext, MessageValue } from './index.js'; -import type { MessageExpressionPart } from '../formatted-parts.js'; export interface MessageNumber extends MessageValue { readonly type: 'number'; @@ -17,7 +17,7 @@ export interface MessageNumber extends MessageValue { >; /** * In addition to matching exact values, - * numerical values will also match keys with the same plural rule category, + * numerical values may also match keys with the same plural rule category, * i.e. one of `zero`, `one`, `two`, `few`, `many`, and `other`. * * @remarks diff --git a/packages/mf2-messageformat/src/functions/string.ts b/packages/mf2-messageformat/src/functions/string.ts index 2cfff62e..22cecf71 100644 --- a/packages/mf2-messageformat/src/functions/string.ts +++ b/packages/mf2-messageformat/src/functions/string.ts @@ -1,5 +1,5 @@ -import type { MessageFunctionContext, MessageValue } from './index.js'; import type { MessageExpressionPart } from '../formatted-parts.js'; +import type { MessageFunctionContext, MessageValue } from './index.js'; import { mergeLocales } from './utils.js'; export interface MessageString extends MessageValue { diff --git a/packages/mf2-messageformat/src/functions/unknown.ts b/packages/mf2-messageformat/src/functions/unknown.ts index 17925d76..85b01131 100644 --- a/packages/mf2-messageformat/src/functions/unknown.ts +++ b/packages/mf2-messageformat/src/functions/unknown.ts @@ -1,5 +1,5 @@ -import type { MessageValue } from './index.js'; import type { MessageExpressionPart } from '../formatted-parts.js'; +import type { MessageValue } from './index.js'; export interface MessageUnknownValue extends MessageValue { readonly type: 'unknown'; diff --git a/packages/mf2-messageformat/src/index.ts b/packages/mf2-messageformat/src/index.ts index f1f75b60..98df3e35 100644 --- a/packages/mf2-messageformat/src/index.ts +++ b/packages/mf2-messageformat/src/index.ts @@ -3,21 +3,30 @@ export type * from './formatted-parts.js'; export type * from './functions/index.js'; export { asDataModel, parseMessage, type CST } from './cst-parser/index.js'; -export * from './data-model/type-guards.js'; +export { + isCatchallKey, + isExpression, + isFunctionAnnotation, + isLiteral, + isMarkup, + isMessage, + isPatternMessage, + isSelectMessage, + isUnsupportedAnnotation, + isVariableRef +} from './data-model/type-guards.js'; export { validate } from './data-model/validate.js'; export { visit } from './data-model/visit.js'; export { MessageDataModelError, MessageError, MessageResolutionError, + MessageSelectionError, MessageSyntaxError } from './errors.js'; -export { defaultFunctions, utilFunctions } from './functions/index.js'; export { - asBoolean, - asPositiveInteger, - asString, - mergeLocales -} from './functions/utils.js'; -export { MessageFormat, MessageFormatOptions } from './messageformat.js'; + MessageFormat, + MessageFormatOptions, + MessageFunctions +} from './messageformat.js'; export { stringifyMessage } from './stringifier/message.js'; diff --git a/packages/mf2-messageformat/src/messageformat.ts b/packages/mf2-messageformat/src/messageformat.ts index fda9e476..aad8f016 100644 --- a/packages/mf2-messageformat/src/messageformat.ts +++ b/packages/mf2-messageformat/src/messageformat.ts @@ -7,12 +7,39 @@ import { MessageError, MessageResolutionError } from './errors.js'; import type { Context } from './format-context.js'; import type { MessagePart } from './formatted-parts.js'; import { - MessageFunctions, MessageValue, - defaultFunctions + datetime, + integer, + number, + ordinal, + plural, + string } from './functions/index.js'; +import type { MessageFunctionContext } from './data-model/function-context.js'; import { selectPattern } from './select-pattern.js'; +const defaultFunctions = Object.freeze({ + datetime, + integer, + number, + ordinal, + plural, + string +}); + +/** + * The runtime function registry available when resolving {@link FunctionAnnotation} elements. + * + * @beta + */ +export interface MessageFunctions { + [key: string]: ( + context: MessageFunctionContext, + options: Record, + input?: unknown + ) => MessageValue; +} + /** @beta */ export interface MessageFormatOptions { /** @@ -27,7 +54,7 @@ export interface MessageFormatOptions { /** * The set of custom functions available during message resolution. - * Extends {@link defaultFunctions}. + * Extends the default set of functions. */ functions?: MessageFunctions; } diff --git a/tsconfig.json b/tsconfig.json index 85503c04..0f7ee4ce 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,14 @@ "@messageformat/react": ["packages/react/src/index"], "@messageformat/runtime": ["packages/runtime/src/runtime"], "@messageformat/runtime/lib/*": ["packages/runtime/src/*"], - "messageformat": ["packages/mf2-messageformat/src/index.ts"] + "@messageformat/test-utils": ["packages/test-utils/src/index.ts"], + "messageformat": ["packages/mf2-messageformat/src/index.ts"], + "messageformat/functions": [ + "packages/mf2-messageformat/src/functions/index.ts" + ], + "messageformat/functions/utils": [ + "packages/mf2-messageformat/src/functions/utils.ts" + ] } } } From 40fb6063064beba93fde6d5998f49e1c46c0a1b1 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 3 Jan 2024 01:11:26 +0200 Subject: [PATCH 29/44] chore(mf2): Use NodeNext module & moduleResolution in tsconfigs --- config/tsconfig.mf2-base.json | 2 ++ packages/mf2-fluent/tsconfig.build.json | 1 - packages/mf2-icu-mf1/tsconfig.build.json | 1 - packages/mf2-messageformat/tsconfig.build.json | 1 - packages/mf2-resource/tsconfig.build.json | 1 - packages/mf2-xliff/tsconfig.build.json | 1 - 6 files changed, 2 insertions(+), 5 deletions(-) diff --git a/config/tsconfig.mf2-base.json b/config/tsconfig.mf2-base.json index 07f42d9e..8a223b06 100644 --- a/config/tsconfig.mf2-base.json +++ b/config/tsconfig.mf2-base.json @@ -2,6 +2,8 @@ "extends": "./tsconfig.base.json", "compilerOptions": { "lib": ["ES2020"], + "module": "NodeNext", + "moduleResolution": "NodeNext", "target": "ES2020", "types": [] } diff --git a/packages/mf2-fluent/tsconfig.build.json b/packages/mf2-fluent/tsconfig.build.json index 754a5b81..57d77aae 100644 --- a/packages/mf2-fluent/tsconfig.build.json +++ b/packages/mf2-fluent/tsconfig.build.json @@ -2,7 +2,6 @@ "extends": "../../config/tsconfig.mf2-base.json", "compilerOptions": { "declaration": true, - "module": "CommonJS", "outDir": "lib" }, "include": ["src/*"], diff --git a/packages/mf2-icu-mf1/tsconfig.build.json b/packages/mf2-icu-mf1/tsconfig.build.json index 754a5b81..57d77aae 100644 --- a/packages/mf2-icu-mf1/tsconfig.build.json +++ b/packages/mf2-icu-mf1/tsconfig.build.json @@ -2,7 +2,6 @@ "extends": "../../config/tsconfig.mf2-base.json", "compilerOptions": { "declaration": true, - "module": "CommonJS", "outDir": "lib" }, "include": ["src/*"], diff --git a/packages/mf2-messageformat/tsconfig.build.json b/packages/mf2-messageformat/tsconfig.build.json index 11d110a3..75e6e315 100644 --- a/packages/mf2-messageformat/tsconfig.build.json +++ b/packages/mf2-messageformat/tsconfig.build.json @@ -2,7 +2,6 @@ "extends": "../../config/tsconfig.mf2-base.json", "compilerOptions": { "declaration": true, - "module": "CommonJS", "outDir": "lib", "types": ["node"] }, diff --git a/packages/mf2-resource/tsconfig.build.json b/packages/mf2-resource/tsconfig.build.json index 8c3abbf7..0b7554b2 100644 --- a/packages/mf2-resource/tsconfig.build.json +++ b/packages/mf2-resource/tsconfig.build.json @@ -3,7 +3,6 @@ "compilerOptions": { "declaration": true, "lib": ["ES2022"], - "module": "CommonJS", "outDir": "lib", "target": "ES2022" }, diff --git a/packages/mf2-xliff/tsconfig.build.json b/packages/mf2-xliff/tsconfig.build.json index 754a5b81..57d77aae 100644 --- a/packages/mf2-xliff/tsconfig.build.json +++ b/packages/mf2-xliff/tsconfig.build.json @@ -2,7 +2,6 @@ "extends": "../../config/tsconfig.mf2-base.json", "compilerOptions": { "declaration": true, - "module": "CommonJS", "outDir": "lib" }, "include": ["src/*"], From f9f9c9899029a58677a5ed5f9a43c70bf2b1b65b Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 3 Jan 2024 01:54:19 +0200 Subject: [PATCH 30/44] style(mf2): Always use file extensions in imports --- packages/mf2-fluent/src/fluent-to-resource.ts | 9 ++++++--- packages/mf2-fluent/src/fluent.test.ts | 6 +++--- packages/mf2-fluent/src/functions.ts | 4 ++-- packages/mf2-fluent/src/index.ts | 13 ++++++++----- packages/mf2-fluent/src/resource-to-fluent.ts | 4 ++-- packages/mf2-icu-mf1/src/index.ts | 6 +++--- packages/mf2-icu-mf1/src/mf1-to-message-data.ts | 2 +- packages/mf2-icu-mf1/src/mf1-to-message.ts | 4 ++-- packages/mf2-icu-mf1/src/mf1.test.ts | 2 +- .../mf2-messageformat/src/cst-parser/cst-types.ts | 2 +- .../src/cst-parser/declarations.ts | 4 ++-- .../src/data-model/function-annotation.test.ts | 6 +++++- .../src/data-model/literal.test.ts | 2 +- .../src/data-model/markup.test.ts | 2 +- .../src/data-model/resolve-value.ts | 2 +- .../src/data-model/resolve-variable.ts | 2 +- .../src/data-model/type-guards.ts | 14 +++++++------- .../src/data-model/unsupported-annotation.test.ts | 2 +- .../src/data-model/variable.test.ts | 2 +- .../mf2-messageformat/src/functions/fallback.ts | 2 +- .../mf2-messageformat/src/messageformat.test.ts | 6 +++--- packages/mf2-messageformat/src/messageformat.ts | 2 +- .../mf2-messageformat/src/mf2-features.test.ts | 2 +- packages/mf2-messageformat/src/select-pattern.ts | 4 ++-- .../src/stringifier/message.test.ts | 4 ++-- 25 files changed, 59 insertions(+), 49 deletions(-) diff --git a/packages/mf2-fluent/src/fluent-to-resource.ts b/packages/mf2-fluent/src/fluent-to-resource.ts index 844aa0c3..d77adbe4 100644 --- a/packages/mf2-fluent/src/fluent-to-resource.ts +++ b/packages/mf2-fluent/src/fluent-to-resource.ts @@ -1,8 +1,11 @@ import * as Fluent from '@fluent/syntax'; import { Message, MessageFormat, MessageFormatOptions } from 'messageformat'; -import type { FluentMessageResource, FluentMessageResourceData } from '.'; -import { fluentToMessage } from './fluent-to-message'; -import { getFluentFunctions } from './functions'; +import { fluentToMessage } from './fluent-to-message.js'; +import { getFluentFunctions } from './functions.js'; +import type { + FluentMessageResource, + FluentMessageResourceData +} from './index.js'; /** * Compile a Fluent resource (i.e. an FTL file) into a Map of diff --git a/packages/mf2-fluent/src/fluent.test.ts b/packages/mf2-fluent/src/fluent.test.ts index cae83036..e78b0149 100644 --- a/packages/mf2-fluent/src/fluent.test.ts +++ b/packages/mf2-fluent/src/fluent.test.ts @@ -26,9 +26,9 @@ import * as Fluent from '@fluent/syntax'; import { source } from '@messageformat/test-utils'; import { PatternMessage, SelectMessage, validate } from 'messageformat'; -import { fluentToResource, fluentToResourceData } from './index'; -import { messageToFluent } from './message-to-fluent'; -import { resourceToFluent } from './resource-to-fluent'; +import { fluentToResource, fluentToResourceData } from './index.js'; +import { messageToFluent } from './message-to-fluent.js'; +import { resourceToFluent } from './resource-to-fluent.js'; type TestCase = { locale?: string; diff --git a/packages/mf2-fluent/src/functions.ts b/packages/mf2-fluent/src/functions.ts index 443fd844..36047d02 100644 --- a/packages/mf2-fluent/src/functions.ts +++ b/packages/mf2-fluent/src/functions.ts @@ -1,6 +1,6 @@ import type { MessageFunctionContext, MessageValue } from 'messageformat'; -import type { FluentMessageResource } from '.'; -import { valueToMessageRef } from './message-to-fluent'; +import type { FluentMessageResource } from './index.js'; +import { valueToMessageRef } from './message-to-fluent.js'; /** * Build a {@link messageformat#MessageFormat} runtime to use with Fluent messages. diff --git a/packages/mf2-fluent/src/index.ts b/packages/mf2-fluent/src/index.ts index 2e26eb9c..bfe9e1ae 100644 --- a/packages/mf2-fluent/src/index.ts +++ b/packages/mf2-fluent/src/index.ts @@ -22,12 +22,15 @@ export type FluentMessageResource = Map>; */ export type FluentMessageResourceData = Map>; -export { fluentToMessage } from './fluent-to-message'; -export { fluentToResource, fluentToResourceData } from './fluent-to-resource'; +export { fluentToMessage } from './fluent-to-message.js'; +export { + fluentToResource, + fluentToResourceData +} from './fluent-to-resource.js'; export { defaultFunctionMap, FluentMessageRef, messageToFluent -} from './message-to-fluent'; -export { resourceToFluent } from './resource-to-fluent'; -export { getFluentFunctions } from './functions'; +} from './message-to-fluent.js'; +export { resourceToFluent } from './resource-to-fluent.js'; +export { getFluentFunctions } from './functions.js'; diff --git a/packages/mf2-fluent/src/resource-to-fluent.ts b/packages/mf2-fluent/src/resource-to-fluent.ts index 5e893b83..d46c9064 100644 --- a/packages/mf2-fluent/src/resource-to-fluent.ts +++ b/packages/mf2-fluent/src/resource-to-fluent.ts @@ -1,7 +1,7 @@ import * as Fluent from '@fluent/syntax'; import type { Message } from 'messageformat'; -import type { FluentMessageResourceData } from '.'; -import { FunctionMap, messageToFluent } from './message-to-fluent'; +import type { FluentMessageResourceData } from './index.js'; +import { FunctionMap, messageToFluent } from './message-to-fluent.js'; /** * Convert a Map of {@link messageformat#Message} data objects into a diff --git a/packages/mf2-icu-mf1/src/index.ts b/packages/mf2-icu-mf1/src/index.ts index 44ab7283..eb55d336 100644 --- a/packages/mf2-icu-mf1/src/index.ts +++ b/packages/mf2-icu-mf1/src/index.ts @@ -1,3 +1,3 @@ -export { MF1Options, mf1ToMessage } from './mf1-to-message'; -export { mf1ToMessageData } from './mf1-to-message-data'; -export { getMF1Functions } from './functions'; +export { getMF1Functions } from './functions.js'; +export { MF1Options, mf1ToMessage } from './mf1-to-message.js'; +export { mf1ToMessageData } from './mf1-to-message-data.js'; diff --git a/packages/mf2-icu-mf1/src/mf1-to-message-data.ts b/packages/mf2-icu-mf1/src/mf1-to-message-data.ts index 2e88045d..0cb7f8c9 100644 --- a/packages/mf2-icu-mf1/src/mf1-to-message-data.ts +++ b/packages/mf2-icu-mf1/src/mf1-to-message-data.ts @@ -1,5 +1,5 @@ import type * as AST from '@messageformat/parser'; -import { +import type { Expression, FunctionAnnotation, Message, diff --git a/packages/mf2-icu-mf1/src/mf1-to-message.ts b/packages/mf2-icu-mf1/src/mf1-to-message.ts index ffa6f324..3bb28552 100644 --- a/packages/mf2-icu-mf1/src/mf1-to-message.ts +++ b/packages/mf2-icu-mf1/src/mf1-to-message.ts @@ -1,7 +1,7 @@ import { Token, parse } from '@messageformat/parser'; import { Message, MessageFormat, MessageFormatOptions } from 'messageformat'; -import { mf1ToMessageData } from './mf1-to-message-data'; -import { getMF1Functions } from './functions'; +import { getMF1Functions } from './functions.js'; +import { mf1ToMessageData } from './mf1-to-message-data.js'; /** @beta */ export type MF1Options = { diff --git a/packages/mf2-icu-mf1/src/mf1.test.ts b/packages/mf2-icu-mf1/src/mf1.test.ts index 0fd3ef66..f6df2d2d 100644 --- a/packages/mf2-icu-mf1/src/mf1.test.ts +++ b/packages/mf2-icu-mf1/src/mf1.test.ts @@ -1,6 +1,6 @@ import { parse } from '@messageformat/parser'; import { validate } from 'messageformat'; -import { mf1ToMessage, mf1ToMessageData } from './index'; +import { mf1ToMessage, mf1ToMessageData } from './index.js'; export type TestCase = { locale?: string; diff --git a/packages/mf2-messageformat/src/cst-parser/cst-types.ts b/packages/mf2-messageformat/src/cst-parser/cst-types.ts index 86cf6da1..23488206 100644 --- a/packages/mf2-messageformat/src/cst-parser/cst-types.ts +++ b/packages/mf2-messageformat/src/cst-parser/cst-types.ts @@ -1,4 +1,4 @@ -import type { MessageSyntaxError } from '../errors'; +import type { MessageSyntaxError } from '../errors.js'; /** @beta */ export type Message = SimpleMessage | ComplexMessage | SelectMessage; diff --git a/packages/mf2-messageformat/src/cst-parser/declarations.ts b/packages/mf2-messageformat/src/cst-parser/declarations.ts index 36c5c1a3..e95a4482 100644 --- a/packages/mf2-messageformat/src/cst-parser/declarations.ts +++ b/packages/mf2-messageformat/src/cst-parser/declarations.ts @@ -1,9 +1,9 @@ import type * as CST from './cst-types.js'; -import type { ParseContext } from './message.js'; import { parseExpression, parseReservedBody } from './expression.js'; +import type { ParseContext } from './message.js'; +import { parseNameValue } from './names.js'; import { whitespaces } from './util.js'; import { parseVariable } from './values.js'; -import { parseNameValue } from './names.js'; export function parseDeclarations(ctx: ParseContext): { declarations: CST.Declaration[]; diff --git a/packages/mf2-messageformat/src/data-model/function-annotation.test.ts b/packages/mf2-messageformat/src/data-model/function-annotation.test.ts index 36f49337..e8d2155c 100644 --- a/packages/mf2-messageformat/src/data-model/function-annotation.test.ts +++ b/packages/mf2-messageformat/src/data-model/function-annotation.test.ts @@ -1,4 +1,8 @@ -import { MessageFormat, MessageFunctions, MessageNumberPart } from '../index'; +import { + MessageFormat, + MessageFunctions, + MessageNumberPart +} from '../index.js'; test('Custom function', () => { const functions = { diff --git a/packages/mf2-messageformat/src/data-model/literal.test.ts b/packages/mf2-messageformat/src/data-model/literal.test.ts index 3a7c2771..1e5ef43b 100644 --- a/packages/mf2-messageformat/src/data-model/literal.test.ts +++ b/packages/mf2-messageformat/src/data-model/literal.test.ts @@ -1,4 +1,4 @@ -import { MessageFormat } from '../index'; +import { MessageFormat } from '../index.js'; function resolve(source: string, errors: any[] = []) { const mf = new MessageFormat(source); diff --git a/packages/mf2-messageformat/src/data-model/markup.test.ts b/packages/mf2-messageformat/src/data-model/markup.test.ts index 0d62bc19..29d6c2b8 100644 --- a/packages/mf2-messageformat/src/data-model/markup.test.ts +++ b/packages/mf2-messageformat/src/data-model/markup.test.ts @@ -1,4 +1,4 @@ -import { MessageFormat, parseMessage } from '../index'; +import { MessageFormat, parseMessage } from '../index.js'; describe('Simple open/close', () => { test('no options, literal body', () => { diff --git a/packages/mf2-messageformat/src/data-model/resolve-value.ts b/packages/mf2-messageformat/src/data-model/resolve-value.ts index 7d13ac26..599dc9f6 100644 --- a/packages/mf2-messageformat/src/data-model/resolve-value.ts +++ b/packages/mf2-messageformat/src/data-model/resolve-value.ts @@ -1,6 +1,6 @@ import type { Context } from '../format-context'; -import type { Literal, VariableRef } from './types'; import { lookupVariableRef } from './resolve-variable.js'; +import type { Literal, VariableRef } from './types.js'; /** @internal */ export function resolveValue( diff --git a/packages/mf2-messageformat/src/data-model/resolve-variable.ts b/packages/mf2-messageformat/src/data-model/resolve-variable.ts index c9fcb12a..a35e6f16 100644 --- a/packages/mf2-messageformat/src/data-model/resolve-variable.ts +++ b/packages/mf2-messageformat/src/data-model/resolve-variable.ts @@ -4,8 +4,8 @@ import { fallback } from '../functions/fallback.js'; import { MessageValue } from '../functions/index.js'; import { unknown } from '../functions/unknown.js'; import { MessageFunctionContext } from './function-context.js'; -import type { Expression, VariableRef } from './types.js'; import { resolveExpression } from './resolve-expression.js'; +import type { Expression, VariableRef } from './types.js'; /** * Declarations aren't resolved until they're requierd, diff --git a/packages/mf2-messageformat/src/data-model/type-guards.ts b/packages/mf2-messageformat/src/data-model/type-guards.ts index b5a58a4d..8234f1d6 100644 --- a/packages/mf2-messageformat/src/data-model/type-guards.ts +++ b/packages/mf2-messageformat/src/data-model/type-guards.ts @@ -1,17 +1,17 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { FunctionAnnotation } from './types.js'; -import type { Expression } from './types.js'; -import type { Literal } from './types.js'; -import type { UnsupportedAnnotation } from './types.js'; -import type { VariableRef } from './types.js'; import type { CatchallKey, + Expression, + FunctionAnnotation, + Literal, + Markup, Message, PatternMessage, - SelectMessage + SelectMessage, + UnsupportedAnnotation, + VariableRef } from './types.js'; -import type { Markup } from './types.js'; /** @beta */ export const isCatchallKey = (key: any): key is CatchallKey => diff --git a/packages/mf2-messageformat/src/data-model/unsupported-annotation.test.ts b/packages/mf2-messageformat/src/data-model/unsupported-annotation.test.ts index 631aa6aa..7fb4cfcc 100644 --- a/packages/mf2-messageformat/src/data-model/unsupported-annotation.test.ts +++ b/packages/mf2-messageformat/src/data-model/unsupported-annotation.test.ts @@ -1,4 +1,4 @@ -import { MessageFormat } from '../index'; +import { MessageFormat } from '../index.js'; function resolve( source: string, diff --git a/packages/mf2-messageformat/src/data-model/variable.test.ts b/packages/mf2-messageformat/src/data-model/variable.test.ts index 0b487398..e3cbc4ac 100644 --- a/packages/mf2-messageformat/src/data-model/variable.test.ts +++ b/packages/mf2-messageformat/src/data-model/variable.test.ts @@ -1,4 +1,4 @@ -import { MessageFormat } from '../index'; +import { MessageFormat } from '../index.js'; describe('variables', () => { let mf: MessageFormat; diff --git a/packages/mf2-messageformat/src/functions/fallback.ts b/packages/mf2-messageformat/src/functions/fallback.ts index dade07df..0c5949b2 100644 --- a/packages/mf2-messageformat/src/functions/fallback.ts +++ b/packages/mf2-messageformat/src/functions/fallback.ts @@ -1,5 +1,5 @@ -import type { MessageValue } from './index.js'; import type { MessageExpressionPart } from '../formatted-parts.js'; +import type { MessageValue } from './index.js'; /** * Used to represent runtime/formatting errors. diff --git a/packages/mf2-messageformat/src/messageformat.test.ts b/packages/mf2-messageformat/src/messageformat.test.ts index f9ced9dc..d5b1ea7f 100644 --- a/packages/mf2-messageformat/src/messageformat.test.ts +++ b/packages/mf2-messageformat/src/messageformat.test.ts @@ -1,9 +1,9 @@ import syntaxErrors from './__fixtures/syntax-errors.json'; import testCore from './__fixtures/test-core.json'; import testFunctions from './__fixtures/test-functions.json'; -import { testName } from './__fixtures/util-test-name'; -import { MessageSyntaxError } from './errors'; -import { MessageFormat } from './messageformat'; +import { testName } from './__fixtures/util-test-name.js'; +import { MessageSyntaxError } from './errors.js'; +import { MessageFormat } from './messageformat.js'; export type TestMessage = { src: string; diff --git a/packages/mf2-messageformat/src/messageformat.ts b/packages/mf2-messageformat/src/messageformat.ts index aad8f016..a072f193 100644 --- a/packages/mf2-messageformat/src/messageformat.ts +++ b/packages/mf2-messageformat/src/messageformat.ts @@ -1,4 +1,5 @@ import { asDataModel, parseMessage } from './cst-parser/index.js'; +import type { MessageFunctionContext } from './data-model/function-context.js'; import { formatMarkup } from './data-model/format-markup.js'; import { resolveExpression } from './data-model/resolve-expression.js'; import { UnresolvedExpression } from './data-model/resolve-variable.js'; @@ -15,7 +16,6 @@ import { plural, string } from './functions/index.js'; -import type { MessageFunctionContext } from './data-model/function-context.js'; import { selectPattern } from './select-pattern.js'; const defaultFunctions = Object.freeze({ diff --git a/packages/mf2-messageformat/src/mf2-features.test.ts b/packages/mf2-messageformat/src/mf2-features.test.ts index a18ba39e..541f3dbe 100644 --- a/packages/mf2-messageformat/src/mf2-features.test.ts +++ b/packages/mf2-messageformat/src/mf2-features.test.ts @@ -12,7 +12,7 @@ import { MessagePart, MessageValue, SelectMessage -} from './index'; +} from './index.js'; describe('Plural Range Selectors & Range Formatters (unicode-org/message-format-wg#125)', () => { function range( diff --git a/packages/mf2-messageformat/src/select-pattern.ts b/packages/mf2-messageformat/src/select-pattern.ts index 630f1b70..db2b6750 100644 --- a/packages/mf2-messageformat/src/select-pattern.ts +++ b/packages/mf2-messageformat/src/select-pattern.ts @@ -1,7 +1,7 @@ import { resolveExpression } from './data-model/resolve-expression.js'; import type { Message, Pattern } from './data-model/types.js'; -import { MessageSelectionError } from './errors'; -import type { Context } from './format-context'; +import { MessageSelectionError } from './errors.js'; +import type { Context } from './format-context.js'; export function selectPattern( context: Context, diff --git a/packages/mf2-messageformat/src/stringifier/message.test.ts b/packages/mf2-messageformat/src/stringifier/message.test.ts index 4d7145c3..3caecb3e 100644 --- a/packages/mf2-messageformat/src/stringifier/message.test.ts +++ b/packages/mf2-messageformat/src/stringifier/message.test.ts @@ -2,8 +2,8 @@ import testCore from '../__fixtures/test-core.json'; import testFunctions from '../__fixtures/test-functions.json'; import { testName } from '../__fixtures/util-test-name'; import { asDataModel, parseMessage } from '../cst-parser/index.js'; -import type { TestMessage } from '../messageformat.test'; -import { stringifyMessage } from './message'; +import type { TestMessage } from '../messageformat.test.js'; +import { stringifyMessage } from './message.js'; for (const [title, messages] of [ ['Stringify core messages', testCore], From f7b0866da76ecde672ec8325de08528a9044c7d0 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 3 Jan 2024 02:12:17 +0200 Subject: [PATCH 31/44] refactor(mf2-fluent)!: Change module type to ES only --- packages/mf2-fluent/README.md | 2 ++ packages/mf2-fluent/package.json | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/packages/mf2-fluent/README.md b/packages/mf2-fluent/README.md index eaa95d21..10dc5c81 100644 --- a/packages/mf2-fluent/README.md +++ b/packages/mf2-fluent/README.md @@ -6,6 +6,8 @@ with the [Unicode MessageFormat 2.0] -based [ECMA-402 Intl.MessageFormat proposa The Fluent message representation relies on [`@fluent/syntax`], while the MessageFormat 2 representation uses [`messageformat@next`]. +This package is distributed only as an ES module. + [fluent]: https://projectfluent.org/ [ecma-402 intl.messageformat proposal]: https://github.com/tc39/proposal-intl-messageformat [unicode messageformat 2.0]: https://github.com/unicode-org/message-format-wg diff --git a/packages/mf2-fluent/package.json b/packages/mf2-fluent/package.json index 5144786c..fcae80c8 100644 --- a/packages/mf2-fluent/package.json +++ b/packages/mf2-fluent/package.json @@ -22,9 +22,14 @@ "directory": "packages/mf2-fluent" }, "main": "lib/index.js", + "type": "module", "files": [ "lib/" ], + "exports": { + ".": "./lib/index.js", + "./package.json": "./package.json" + }, "dependencies": { "@fluent/syntax": "^0.19.0", "fast-deep-equal": "^3.1.3", From 880402288319f79a7e2697572e371596a84ad28a Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 3 Jan 2024 02:26:28 +0200 Subject: [PATCH 32/44] fix(mf2): Consider U+3000 as whitespace --- packages/mf2-messageformat/src/__fixtures/test-core.json | 2 ++ packages/mf2-messageformat/src/cst-parser/expression.ts | 4 ++-- packages/mf2-messageformat/src/cst-parser/util.ts | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/mf2-messageformat/src/__fixtures/test-core.json b/packages/mf2-messageformat/src/__fixtures/test-core.json index bca145b3..eda430b5 100644 --- a/packages/mf2-messageformat/src/__fixtures/test-core.json +++ b/packages/mf2-messageformat/src/__fixtures/test-core.json @@ -1,6 +1,8 @@ [ { "src": "hello", "exp": "hello" }, { "src": "hello {world}", "exp": "hello world" }, + { "src": "hello { world\t\n}", "exp": "hello world" }, + { "src": "hello {\u3000world\r}", "exp": "hello world" }, { "src": "hello {|world|}", "exp": "hello world" }, { "src": "hello {||}", "exp": "hello " }, { diff --git a/packages/mf2-messageformat/src/cst-parser/expression.ts b/packages/mf2-messageformat/src/cst-parser/expression.ts index 6095fc79..cf109e77 100644 --- a/packages/mf2-messageformat/src/cst-parser/expression.ts +++ b/packages/mf2-messageformat/src/cst-parser/expression.ts @@ -2,7 +2,7 @@ import { MessageSyntaxError } from '../errors.js'; import type * as CST from './cst-types.js'; import type { ParseContext } from './message.js'; import { parseNameValue } from './names.js'; -import { whitespaces } from './util.js'; +import { whitespaceChars, whitespaces } from './util.js'; import { parseLiteral, parseQuotedLiteral, parseVariable } from './values.js'; export function parseExpression( @@ -246,7 +246,7 @@ export function parseReservedBody( } } let prev = ctx.source[pos - 1]; - while (prev === '\r' || prev === '\n' || prev === '\t' || prev === ' ') { + while (whitespaceChars.includes(prev)) { pos -= 1; prev = ctx.source[pos - 1]; } diff --git a/packages/mf2-messageformat/src/cst-parser/util.ts b/packages/mf2-messageformat/src/cst-parser/util.ts index 16ae43e4..0a7e0854 100644 --- a/packages/mf2-messageformat/src/cst-parser/util.ts +++ b/packages/mf2-messageformat/src/cst-parser/util.ts @@ -1,8 +1,9 @@ -// WhiteSpace ::= #x9 | #xD | #xA | #x20 /* ws: definition */ +export const whitespaceChars = ['\t', '\n', '\r', ' ', '\u3000']; + export function whitespaces(src: string, start: number): number { let length = 0; let ch = src[start]; - while (ch === '\t' || ch === '\n' || ch === '\r' || ch === ' ') { + while (whitespaceChars.includes(ch)) { length += 1; ch = src[start + length]; } From e6991fd9a939609a8724dee8efcc6770c18c04b2 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 3 Jan 2024 03:13:34 +0200 Subject: [PATCH 33/44] test(mf2): Add missing jest moduleNameMapper config for new messageformat endpoints --- jest.config.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jest.config.js b/jest.config.js index 0f0b2bbd..72c2e123 100644 --- a/jest.config.js +++ b/jest.config.js @@ -11,6 +11,10 @@ module.exports = { moduleFileExtensions: ['mjs', ...defaults.moduleFileExtensions], moduleNameMapper: { '^messageformat$': '/packages/mf2-messageformat/src/index.ts', + '^messageformat/functions$': + '/packages/mf2-messageformat/src/functions/index.ts', + '^messageformat/functions/utils$': + '/packages/mf2-messageformat/src/functions/utils.ts', '^@messageformat/core$': '/packages/core/src/messageformat.ts', '^@messageformat/core/compile-module$': '/packages/core/src/compile-module.ts', From e6566cf68a02f3de155ed22726696fc1ad5bb2f8 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 3 Jan 2024 18:25:06 +0200 Subject: [PATCH 34/44] feat(mf2): Improve visitor, add MessageNode type --- .../mf2-messageformat/src/data-model/types.ts | 15 +++ .../src/data-model/validate.ts | 122 ++++++++---------- .../mf2-messageformat/src/data-model/visit.ts | 121 +++++++++++++---- 3 files changed, 164 insertions(+), 94 deletions(-) diff --git a/packages/mf2-messageformat/src/data-model/types.ts b/packages/mf2-messageformat/src/data-model/types.ts index 147673a3..9b1cb92c 100644 --- a/packages/mf2-messageformat/src/data-model/types.ts +++ b/packages/mf2-messageformat/src/data-model/types.ts @@ -1,3 +1,16 @@ +/** A node in a message data model */ +export type MessageNode = + | Declaration + | Variant + | CatchallKey + | Expression + | Literal + | VariableRef + | FunctionAnnotation + | UnsupportedAnnotation + | Markup + | Option; + /** * The representation of a single message. * The shape of the message is an implementation detail, @@ -74,6 +87,7 @@ export interface SelectMessage { /** @beta */ export interface Variant { + type?: never; keys: Array; value: Pattern; } @@ -234,6 +248,7 @@ export interface MarkupClose { * @beta */ export interface Option { + type?: never; name: string; value: Literal | VariableRef; } diff --git a/packages/mf2-messageformat/src/data-model/validate.ts b/packages/mf2-messageformat/src/data-model/validate.ts index a897ecef..d6717d24 100644 --- a/packages/mf2-messageformat/src/data-model/validate.ts +++ b/packages/mf2-messageformat/src/data-model/validate.ts @@ -1,10 +1,8 @@ import type { MessageDataModelError } from '../errors.js'; import type { - Declaration, Expression, - Literal, Message, - Option, + MessageNode, VariableRef, Variant } from './types.js'; @@ -41,111 +39,93 @@ import { visit } from './visit.js'; */ export function validate( msg: Message, - onError: ( - type: MessageDataModelError['type'], - node: Message | Declaration | Expression | Option | VariableRef | Variant - ) => void + onError: (type: MessageDataModelError['type'], node: MessageNode) => void ) { + let selectorCount = 0; + let missingFallback: Expression | Variant | null = null; + + /** Tracks directly & indirectly annotated variables for `missing-selector-annotation` */ const annotated = new Set(); + + /** Tracks declared variables for `duplicate-declaration` */ const declared = new Set(); - let selectorCount = 0; - let missingFallback = false; const declRefs = new Map(); - const addDeclarationRef = (ref: Literal | VariableRef | undefined) => { - if (ref?.type === 'variable') { - const prev = declRefs.get(ref.name); - if (prev) prev.push(ref); - else declRefs.set(ref.name, [ref]); - } - }; - const functions = new Set(); const localVars = new Set(); const variables = new Set(); - const visitOptions = ( - options: Option[] | undefined, - context: 'declaration' | 'selector' | 'placeholder' - ) => { - if (options) { - for (let i = 0; i < options.length; ++i) { - const { name, value } = options[i]; - for (let j = i + 1; j < options.length; ++j) { - if (options[j].name === name) onError('duplicate-option', options[j]); - } - if (value.type === 'variable') { - variables.add(value.name); - if (context === 'declaration') addDeclarationRef(value); - } - } - } - }; - visit(msg, { declaration(decl) { - if (decl.name) { - if (declared.has(decl.name)) onError('duplicate-declaration', decl); - else declared.add(decl.name); + // Skip all ReservedStatement + if (!decl.name) return undefined; + + if (declared.has(decl.name)) onError('duplicate-declaration', decl); + else declared.add(decl.name); + + if ( + decl.value.annotation || + (decl.type === 'local' && + decl.value.arg?.type === 'variable' && + annotated.has(decl.value.arg.name)) + ) { + annotated.add(decl.name); + } + if (decl.type === 'local') localVars.add(decl.name); + + return () => { for (const ref of declRefs.get(decl.name) ?? []) { if (decl.type !== 'input' || ref !== decl.value.arg) { onError('forward-reference', ref); } } + }; + }, + expression(expression, context) { + const { arg, annotation } = expression; + if (annotation?.type === 'function') functions.add(annotation.name); + if (context === 'selector') { + selectorCount += 1; + missingFallback = expression; if ( - decl.value.annotation || - (decl.type === 'local' && - decl.value.arg?.type === 'variable' && - annotated.has(decl.value.arg.name)) + !annotation && + (arg?.type !== 'variable' || !annotated.has(arg.name)) ) { - annotated.add(decl.name); + onError('missing-selector-annotation', expression); } - - if (decl.type === 'local') localVars.add(decl.name); } }, - expression(expression, context) { - const { arg, annotation } = expression; - visitOptions(annotation?.options, context); - - if (annotation?.type === 'function') functions.add(annotation.name); - - switch (context) { - case 'declaration': { - addDeclarationRef(arg); + option({ name }, index, options) { + for (let j = index + 1; j < options.length; ++j) { + if (options[j].name === name) { + onError('duplicate-option', options[j]); break; } - - case 'selector': - selectorCount += 1; - missingFallback = true; - if ( - !annotation && - (arg?.type !== 'variable' || !annotated.has(arg.name)) - ) { - onError('missing-selector-annotation', expression); - } - break; } }, - markup({ options }, context) { - visitOptions(options, context); + value(value, context) { + if (value.type === 'variable') { + variables.add(value.name); + if (context === 'declaration') { + const prev = declRefs.get(value.name); + if (prev) prev.push(value); + else declRefs.set(value.name, [value]); + } + } }, variant(variant) { const { keys } = variant; if (keys.length !== selectorCount) onError('key-mismatch', variant); - if (missingFallback && keys.every(key => key.type === '*')) { - missingFallback = false; - } + missingFallback &&= keys.every(key => key.type === '*') ? null : variant; } }); - if (missingFallback) onError('missing-fallback', msg); + if (missingFallback) onError('missing-fallback', missingFallback); for (const lv of localVars) variables.delete(lv); diff --git a/packages/mf2-messageformat/src/data-model/visit.ts b/packages/mf2-messageformat/src/data-model/visit.ts index d804777e..49903ba0 100644 --- a/packages/mf2-messageformat/src/data-model/visit.ts +++ b/packages/mf2-messageformat/src/data-model/visit.ts @@ -1,74 +1,149 @@ import type { + CatchallKey, Declaration, Expression, + FunctionAnnotation, + Literal, Markup, Message, + MessageNode, + Option, Pattern, + UnsupportedAnnotation, + VariableRef, Variant } from './types.js'; /** - * Apply visitor functions to a message. + * Apply visitor functions to message nodes. * * @remarks - * The visitors are applied in data model order, starting from leaf nodes. + * The visitors are applied in source order, starting from the root. + * Visitors for nodes that contain other nodes may return a callback function + * that will be called with no arguments when exiting the node. + * + * If set, the `node` visitor is called for all {@link MessageNode} values + * for which an explicit visitor is not defined. + * + * Many visitors will be called with additional arguments + * identifying some of the context for the visited node. * * @beta */ export function visit( msg: Message, visitors: { - declaration?: (declaration: Declaration) => void; + annotation?: ( + annotation: FunctionAnnotation | UnsupportedAnnotation, + context: 'declaration' | 'selector' | 'placeholder', + argument: Literal | VariableRef | undefined + ) => (() => void) | void; + declaration?: (declaration: Declaration) => (() => void) | void; expression?: ( expression: Expression, context: 'declaration' | 'selector' | 'placeholder' + ) => (() => void) | void; + key?: ( + key: Literal | CatchallKey, + index: number, + keys: (Literal | CatchallKey)[] + ) => void; + markup?: ( + markup: Markup, + context: 'declaration' | 'placeholder' + ) => (() => void) | void; + node?: (node: MessageNode, ...rest: unknown[]) => void; + option?: ( + option: Option, + index: number, + options: Option[], + context: 'declaration' | 'selector' | 'placeholder' + ) => (() => void) | void; + pattern?: (pattern: Pattern) => (() => void) | void; + value?: ( + value: Literal | VariableRef, + context: 'declaration' | 'selector' | 'placeholder', + position: 'arg' | 'option' ) => void; - markup?: (markup: Markup, context: 'declaration' | 'placeholder') => void; - pattern?: (pattern: Pattern) => void; - variant?: (variant: Variant) => void; + variant?: (variant: Variant) => (() => void) | void; } ) { - const { declaration, expression, markup, pattern, variant } = visitors; + const { node, pattern } = visitors; + const { + annotation = node, + declaration = node, + expression = node, + key = node, + markup = node, + option = node, + value = node, + variant = node + } = visitors; + + const handleOptions = ( + options: Option[] | undefined, + context: 'declaration' | 'selector' | 'placeholder' + ) => { + if (options) { + for (let i = 0; i < options.length; ++i) { + const opt = options[i]; + const end = option?.(opt, i, options, context); + value?.(opt.value, context, 'option'); + end?.(); + } + } + }; const handleElement = ( exp: string | Expression | Markup, - context: 'declaration' | 'placeholder' + context: 'declaration' | 'selector' | 'placeholder' ) => { if (typeof exp === 'object') { switch (exp.type) { - case 'expression': - expression?.(exp, context); + case 'expression': { + const endE = expression?.(exp, context); + if (exp.arg) value?.(exp.arg, context, 'arg'); + if (exp.annotation) { + const endA = annotation?.(exp.annotation, context, exp.arg); + handleOptions(exp.annotation.options, context); + endA?.(); + } + endE?.(); break; - case 'markup': - markup?.(exp, context); + } + case 'markup': { + const end = + context !== 'selector' ? markup?.(exp, context) : undefined; + handleOptions(exp.options, context); + end?.(); break; + } } } }; const handlePattern = (pat: Pattern) => { + const end = pattern?.(pat); for (const el of pat.body) handleElement(el, 'placeholder'); - pattern?.(pat); + end?.(); }; for (const decl of msg.declarations) { - if (decl.value) { - expression?.(decl.value, 'declaration'); - } else { - for (const exp of decl.expressions) handleElement(exp, 'declaration'); - } - declaration?.(decl); + const end = declaration?.(decl); + if (decl.value) handleElement(decl.value, 'declaration'); + else for (const exp of decl.expressions) handleElement(exp, 'declaration'); + end?.(); } if (msg.type === 'message') { handlePattern(msg.pattern); } else { - if (expression) { - for (const sel of msg.selectors) expression(sel, 'selector'); - } + for (const sel of msg.selectors) handleElement(sel, 'selector'); for (const vari of msg.variants) { + const end = variant?.(vari); + if (key) vari.keys.forEach(key); handlePattern(vari.value); - variant?.(vari); + end?.(); } } } From 28c2e995d958c5c5159e090a9aab638a84d23138 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 3 Jan 2024 18:29:47 +0200 Subject: [PATCH 35/44] feat(mf2): Include CST reference in parsed data model, using `cst` Symbol key --- .../src/cst-parser/as-data-model.ts | 102 ++++++++++-------- .../mf2-messageformat/src/cst-parser/index.ts | 2 +- .../mf2-messageformat/src/data-model/types.ts | 20 ++++ packages/mf2-messageformat/src/index.ts | 7 +- .../src/stringifier/message.test.ts | 14 +++ 5 files changed, 98 insertions(+), 47 deletions(-) diff --git a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts index 6ee3ab3a..770cd119 100644 --- a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts +++ b/packages/mf2-messageformat/src/cst-parser/as-data-model.ts @@ -2,6 +2,8 @@ import type * as Model from '../data-model/types.js'; import { MessageSyntaxError } from '../errors.js'; import type * as CST from './cst-types.js'; +export const cst = Symbol.for('CST'); + /** * Convert a CST message structure into its data model representation. * @@ -17,18 +19,21 @@ export function asDataModel(msg: CST.Message): Model.Message { type: 'select', declarations, selectors: msg.selectors.map(sel => asExpression(sel, false)), - variants: msg.variants.map(cst => ({ - keys: cst.keys.map(key => - key.type === '*' ? { type: '*' } : asValue(key) + variants: msg.variants.map(variant => ({ + keys: variant.keys.map(key => + key.type === '*' ? { type: '*', [cst]: key } : asValue(key) ), - value: asPattern(cst.value) - })) + value: asPattern(variant.value), + [cst]: variant + })), + [cst]: msg }; } else { return { type: 'message', declarations, - pattern: asPattern(msg.pattern) + pattern: asPattern(msg.pattern), + [cst]: msg }; } } @@ -44,131 +49,138 @@ function asDeclaration(decl: CST.Declaration): Model.Declaration { return { type: 'input', name: value.arg.name, - value: value as Model.Expression + value: value as Model.Expression, + [cst]: decl }; } case 'local': return { type: 'local', name: asValue(decl.target).name, - value: asExpression(decl.value, false) + value: asExpression(decl.value, false), + [cst]: decl }; default: return { type: 'unsupported-statement', keyword: (decl.keyword?.value ?? '').substring(1), body: decl.body?.value || undefined, - expressions: decl.values?.map(dv => asExpression(dv, true)) ?? [] + expressions: decl.values?.map(dv => asExpression(dv, true)) ?? [], + [cst]: decl }; } } -function asPattern(cst: CST.Pattern): Model.Pattern { - const body: Model.Pattern['body'] = cst.body.map(el => +function asPattern(pattern: CST.Pattern): Model.Pattern { + const body: Model.Pattern['body'] = pattern.body.map(el => el.type === 'text' ? el.value : asExpression(el, true) ); return { body }; } function asExpression( - cst: CST.Expression | CST.Junk, + exp: CST.Expression | CST.Junk, allowMarkup: false ): Model.Expression; function asExpression( - cst: CST.Expression | CST.Junk, + exp: CST.Expression | CST.Junk, allowMarkup: true ): Model.Expression | Model.Markup; function asExpression( - cst: CST.Expression | CST.Junk, + exp: CST.Expression | CST.Junk, allowMarkup: boolean ): Model.Expression | Model.Markup { - if (cst.type === 'expression') { - if (allowMarkup && cst.markup) { - const cm = cst.markup; + if (exp.type === 'expression') { + if (allowMarkup && exp.markup) { + const cm = exp.markup; const name = asName(cm.name); if (cm.type === 'markup-close') { - return { type: 'markup', kind: 'close', name }; + return { type: 'markup', kind: 'close', name, [cst]: exp }; } const markup: Model.MarkupOpen | Model.MarkupStandalone = { type: 'markup', kind: cm.close ? 'standalone' : 'open', - name + name, + [cst]: exp }; if (cm.options.length > 0) markup.options = cm.options.map(asOption); return markup; } - const arg = cst.arg ? asValue(cst.arg) : undefined; + const arg = exp.arg ? asValue(exp.arg) : undefined; let annotation: | Model.FunctionAnnotation | Model.UnsupportedAnnotation | undefined; - const ca = cst.annotation; + const ca = exp.annotation; if (ca) { switch (ca.type) { case 'function': - annotation = { type: 'function', name: asName(ca.name) }; + annotation = { type: 'function', name: asName(ca.name), [cst]: ca }; if (ca.options.length > 0) { annotation.options = ca.options.map(asOption); } + break; case 'reserved-annotation': annotation = { type: 'unsupported-annotation', sigil: ca.sigil, - source: ca.source.value + source: ca.source.value, + [cst]: ca }; break; default: - throw new MessageSyntaxError('parse-error', cst.start, cst.end); + throw new MessageSyntaxError('parse-error', exp.start, exp.end); } } if (arg) { return annotation - ? { type: 'expression', arg, annotation } - : { type: 'expression', arg }; + ? { type: 'expression', arg, annotation, [cst]: exp } + : { type: 'expression', arg, [cst]: exp }; } else if (annotation) { - return { type: 'expression', annotation }; + return { type: 'expression', annotation, [cst]: exp }; } } - throw new MessageSyntaxError('parse-error', cst.start, cst.end); + throw new MessageSyntaxError('parse-error', exp.start, exp.end); } -const asOption = (cst: CST.Option): Model.Option => ({ - name: asName(cst.name), - value: asValue(cst.value) +const asOption = (option: CST.Option): Model.Option => ({ + name: asName(option.name), + value: asValue(option.value), + [cst]: option }); -function asName(cst: CST.Identifier): string { - switch (cst.length) { +function asName(id: CST.Identifier): string { + switch (id.length) { case 1: - return cst[0].value; + return id[0].value; case 3: - return `${cst[0].value}:${cst[2].value}`; + return `${id[0].value}:${id[2].value}`; default: throw new MessageSyntaxError( 'parse-error', - cst[0]?.start ?? -1, - cst.at(-1)?.end ?? -1 + id[0]?.start ?? -1, + id.at(-1)?.end ?? -1 ); } } -function asValue(cst: CST.Literal | CST.Junk): Model.Literal; -function asValue(cst: CST.VariableRef | CST.Junk): Model.VariableRef; +function asValue(value: CST.Literal | CST.Junk): Model.Literal; +function asValue(value: CST.VariableRef | CST.Junk): Model.VariableRef; function asValue( - cst: CST.Literal | CST.VariableRef | CST.Junk + value: CST.Literal | CST.VariableRef | CST.Junk ): Model.Literal | Model.VariableRef; function asValue( - cst: CST.Literal | CST.VariableRef | CST.Junk + value: CST.Literal | CST.VariableRef | CST.Junk ): Model.Literal | Model.VariableRef { - switch (cst.type) { + switch (value.type) { case 'literal': - return { type: 'literal', value: cst.value }; + return { type: 'literal', value: value.value, [cst]: value }; case 'variable': - return { type: 'variable', name: cst.name }; + return { type: 'variable', name: value.name, [cst]: value }; default: - throw new MessageSyntaxError('parse-error', cst.start, cst.end); + throw new MessageSyntaxError('parse-error', value.start, value.end); } } diff --git a/packages/mf2-messageformat/src/cst-parser/index.ts b/packages/mf2-messageformat/src/cst-parser/index.ts index 9f3e9fe8..1a056864 100644 --- a/packages/mf2-messageformat/src/cst-parser/index.ts +++ b/packages/mf2-messageformat/src/cst-parser/index.ts @@ -1,4 +1,4 @@ import type * as CST from './cst-types.js'; export { CST }; -export { asDataModel } from './as-data-model.js'; +export { asDataModel, cst } from './as-data-model.js'; export { parseMessage } from './message.js'; diff --git a/packages/mf2-messageformat/src/data-model/types.ts b/packages/mf2-messageformat/src/data-model/types.ts index 9b1cb92c..80d333e9 100644 --- a/packages/mf2-messageformat/src/data-model/types.ts +++ b/packages/mf2-messageformat/src/data-model/types.ts @@ -1,3 +1,6 @@ +import { cst } from '../cst-parser/as-data-model.js'; +import type * as CST from '../cst-parser/cst-types.js'; + /** A node in a message data model */ export type MessageNode = | Declaration @@ -30,6 +33,7 @@ export interface PatternMessage { declarations: Declaration[]; pattern: Pattern; comment?: string; + [cst]?: CST.SimpleMessage | CST.ComplexMessage; } /** @@ -48,12 +52,14 @@ export interface InputDeclaration { type: 'input'; name: string; value: Expression; + [cst]?: CST.Declaration; } export interface LocalDeclaration { type: 'local'; name: string; value: Expression; + [cst]?: CST.Declaration; } export interface UnsupportedStatement { @@ -63,6 +69,7 @@ export interface UnsupportedStatement { value?: never; body?: string; expressions: (Expression | Markup)[]; + [cst]?: CST.Declaration; } /** @@ -83,6 +90,7 @@ export interface SelectMessage { selectors: Expression[]; variants: Variant[]; comment?: string; + [cst]?: CST.SelectMessage; } /** @beta */ @@ -90,6 +98,7 @@ export interface Variant { type?: never; keys: Array; value: Pattern; + [cst]?: CST.Variant; } /** @@ -100,6 +109,7 @@ export interface Variant { export interface CatchallKey { type: '*'; value?: string; + [cst]?: CST.CatchallKey; } /** @@ -129,11 +139,13 @@ export type Expression< type: 'expression'; arg: A; annotation?: FunctionAnnotation | UnsupportedAnnotation; + [cst]?: CST.Expression; } : { type: 'expression'; arg?: never; annotation: FunctionAnnotation | UnsupportedAnnotation; + [cst]?: CST.Expression; }; /** @@ -149,6 +161,7 @@ export type Expression< export interface Literal { type: 'literal'; value: string; + [cst]?: CST.Literal; } /** @@ -167,6 +180,7 @@ export interface Literal { export interface VariableRef { type: 'variable'; name: string; + [cst]?: CST.VariableRef; } /** @@ -185,6 +199,7 @@ export interface FunctionAnnotation { type: 'function'; name: string; options?: Option[]; + [cst]?: CST.FunctionRef; } /** @@ -203,6 +218,7 @@ export interface UnsupportedAnnotation { source: string; name?: never; options?: never; + [cst]?: CST.ReservedAnnotation; } /** @@ -225,6 +241,7 @@ export interface MarkupOpen { kind: 'open'; name: string; options?: Option[]; + [cst]?: CST.Expression; } export interface MarkupStandalone { @@ -232,6 +249,7 @@ export interface MarkupStandalone { kind: 'standalone'; name: string; options?: Option[]; + [cst]?: CST.Expression; } export interface MarkupClose { @@ -239,6 +257,7 @@ export interface MarkupClose { kind: 'close'; name: string; options?: never; + [cst]?: CST.Expression; } /** @@ -251,4 +270,5 @@ export interface Option { type?: never; name: string; value: Literal | VariableRef; + [cst]?: CST.Option; } diff --git a/packages/mf2-messageformat/src/index.ts b/packages/mf2-messageformat/src/index.ts index 98df3e35..864179a7 100644 --- a/packages/mf2-messageformat/src/index.ts +++ b/packages/mf2-messageformat/src/index.ts @@ -2,7 +2,12 @@ export type * from './data-model/types.js'; export type * from './formatted-parts.js'; export type * from './functions/index.js'; -export { asDataModel, parseMessage, type CST } from './cst-parser/index.js'; +export { + type CST, + asDataModel, + cst, + parseMessage +} from './cst-parser/index.js'; export { isCatchallKey, isExpression, diff --git a/packages/mf2-messageformat/src/stringifier/message.test.ts b/packages/mf2-messageformat/src/stringifier/message.test.ts index 3caecb3e..1725b15b 100644 --- a/packages/mf2-messageformat/src/stringifier/message.test.ts +++ b/packages/mf2-messageformat/src/stringifier/message.test.ts @@ -2,9 +2,21 @@ import testCore from '../__fixtures/test-core.json'; import testFunctions from '../__fixtures/test-functions.json'; import { testName } from '../__fixtures/util-test-name'; import { asDataModel, parseMessage } from '../cst-parser/index.js'; +import { Message } from '../data-model/types'; +import { cst } from '../cst-parser/as-data-model'; +import { visit } from '../data-model/visit'; import type { TestMessage } from '../messageformat.test.js'; import { stringifyMessage } from './message.js'; +function trimCST(msg: Message) { + delete msg[cst]; + visit(msg, { + node(node) { + delete node[cst]; + } + }); +} + for (const [title, messages] of [ ['Stringify core messages', testCore], ...Object.entries(testFunctions).map(x => [ @@ -17,8 +29,10 @@ for (const [title, messages] of [ if (!testMsg.errors?.length) { test(testName(testMsg), () => { const msg0 = asDataModel(parseMessage(testMsg.src)); + trimCST(msg0); const src1 = stringifyMessage(msg0); const msg1 = asDataModel(parseMessage(src1)); + trimCST(msg1); expect(msg1).toEqual(msg0); }); } From ab0f80715fc592d1b021c61b2520342dbb716b7d Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 3 Jan 2024 18:48:06 +0200 Subject: [PATCH 36/44] fix(mf2): Use validate() for all data model error detection --- .../src/__fixtures/syntax-errors.json | 25 +++++--- .../src/cst-parser/declarations.ts | 50 --------------- .../src/cst-parser/expression.ts | 12 ---- .../src/cst-parser/message.ts | 61 ++----------------- packages/mf2-messageformat/src/errors.ts | 10 +++ .../mf2-messageformat/src/messageformat.ts | 10 ++- 6 files changed, 42 insertions(+), 126 deletions(-) diff --git a/packages/mf2-messageformat/src/__fixtures/syntax-errors.json b/packages/mf2-messageformat/src/__fixtures/syntax-errors.json index ec390577..aca57744 100644 --- a/packages/mf2-messageformat/src/__fixtures/syntax-errors.json +++ b/packages/mf2-messageformat/src/__fixtures/syntax-errors.json @@ -35,14 +35,20 @@ "no {|placeholder| end", "no {|literal} end", "no {|literal or placeholder end", - ".input {$foo} .local $foo = {42} {{bar {$foo}}}", - ".local $foo = {42} .input {$foo} {{bar {$foo}}}", - ".local $foo = {:unknown} .local $foo = {42} {{bar {$foo}}}", - ".local bar = {|foo|} {{{$bar}}}", - ".local $bar {|foo|} {{{$bar}}}", - ".local $bar = |foo| {{{$bar}}}", - ".local $foo = {$bar} .local $bar = {$baz} {{bar {$foo}}}", - ".local $foo = {$bar :func} .local $bar = {$baz} {{bar {$foo}}}", + ".input {$foo} .input {$foo} {{_}}", + ".input {$foo} .local $foo = {42} {{_}}", + ".local $foo = {42} .input {$foo} {{_}}", + ".local $foo = {:unknown} .local $foo = {42} {{_}}", + ".local bar = {|foo|} {{_}}", + ".local #bar = {|foo|} {{_}}", + ".local $bar {|foo|} {{_}}", + ".local $bar = |foo| {{_}}", + ".local $foo = {$bar} .local $bar = {42} {{_}}", + ".local $foo = {$foo} {{_}}", + ".local $foo = {$bar} .local $bar = {$baz} {{_}}", + ".local $foo = {$bar :func} .local $bar = {$baz} {{_}}", + ".local $foo = {42 :func opt=$foo} {{_}}", + ".local $foo = {42 :func opt=$bar} .local $bar = {42} {{_}}", ".match {#foo} * {{foo}}", ".match {} * {{foo}}", ".match {|foo| :x} {|bar| :x} ** {{foo}}", @@ -50,6 +56,9 @@ ".match {|x| :x} * foo", ".match {|x| :x} * {{foo}} extra", ".match |x| * {{foo}}", + ".match {:foo} 1 {{_}}", + ".match {:foo} other {{_}}", + ".match {:foo} {:bar} * 1 {{_}} 1 * {{_}}", ".match {$foo :x} * * {{foo}}", ".match {$foo :x} {$bar :x} * {{foo}}", ".match {$foo} one {{one}} * {{other}}", diff --git a/packages/mf2-messageformat/src/cst-parser/declarations.ts b/packages/mf2-messageformat/src/cst-parser/declarations.ts index e95a4482..8a73beec 100644 --- a/packages/mf2-messageformat/src/cst-parser/declarations.ts +++ b/packages/mf2-messageformat/src/cst-parser/declarations.ts @@ -32,7 +32,6 @@ export function parseDeclarations(ctx: ParseContext): { pos = decl.end; pos += whitespaces(source, pos); } - checkDeclarations(ctx, declarations); return { declarations, end: pos }; } @@ -151,52 +150,3 @@ function parseDeclarationValue( ctx.onError('missing-syntax', start, '{'); return { type: 'junk', start, end, source: source.substring(start, end) }; } - -/** Variables can only be declared once & declarations can't refer to later ones */ -function checkDeclarations(ctx: ParseContext, declarations: CST.Declaration[]) { - const targets = new Set(); - const refs = new Map(); - const addRef = (ref: CST.Literal | CST.VariableRef | undefined) => { - if (ref?.type === 'variable') { - const prev = refs.get(ref.name); - if (prev) prev.push(ref); - else refs.set(ref.name, [ref]); - } - }; - - loop: for (const decl of declarations) { - let target: CST.VariableRef | undefined; - switch (decl.type) { - case 'input': { - const dv = decl.value; - if (dv.type === 'expression' && dv.arg?.type === 'variable') { - target = dv.arg; - } - break; - } - case 'local': - if (decl.target.type === 'variable') target = decl.target; - break; - default: - continue loop; - } - if (target) { - if (targets.has(target.name)) { - ctx.onError('duplicate-declaration', target.start, target.end); - } else { - targets.add(target.name); - } - for (const { start, end } of refs.get(target.name) ?? []) { - ctx.onError('forward-reference', start, end); - } - } - - if (decl.value.type === 'expression') { - const { arg, annotation } = decl.value; - if (arg?.type === 'variable') addRef(arg); - if (annotation?.type === 'function') { - for (const opt of annotation.options) addRef(opt.value); - } - } - } -} diff --git a/packages/mf2-messageformat/src/cst-parser/expression.ts b/packages/mf2-messageformat/src/cst-parser/expression.ts index cf109e77..0f61572a 100644 --- a/packages/mf2-messageformat/src/cst-parser/expression.ts +++ b/packages/mf2-messageformat/src/cst-parser/expression.ts @@ -127,13 +127,6 @@ function parseFunctionRefOrMarkup( pos += ws; const opt = parseOption(ctx, pos); if (opt.end === pos) break; // error - if (options.some(prev => sameIdentifier(prev.name, opt.name))) { - ctx.onError( - 'duplicate-option', - opt.name[0].start, - opt.name.at(-1)?.end ?? -1 - ); - } options.push(opt); pos = opt.end; } @@ -189,11 +182,6 @@ function parseIdentifier( } } -const sameIdentifier = (a: CST.Identifier, b: CST.Identifier) => - a.length === b.length && - a[0].value === b[0].value && - a[2]?.value === b[2]?.value; - function parseReservedAnnotation( ctx: ParseContext, start: number diff --git a/packages/mf2-messageformat/src/cst-parser/message.ts b/packages/mf2-messageformat/src/cst-parser/message.ts index 9ca0b230..5be32bd5 100644 --- a/packages/mf2-messageformat/src/cst-parser/message.ts +++ b/packages/mf2-messageformat/src/cst-parser/message.ts @@ -20,7 +20,10 @@ export class ParseContext { } onError( - type: Exclude, + type: Exclude< + typeof MessageSyntaxError.prototype.type, + 'missing-syntax' | typeof MessageDataModelError.prototype.type + >, start: number, end: number ): void; @@ -32,12 +35,6 @@ export class ParseContext { ) { let err: MessageSyntaxError; switch (type) { - case 'duplicate-declaration': - case 'forward-reference': - case 'key-mismatch': - case 'missing-fallback': - err = new MessageDataModelError(type, start, Number(end)); - break; case 'missing-syntax': err = new MissingSyntaxError(start, String(end)); break; @@ -105,43 +102,6 @@ function parseSelectMessage( if (body && body.type !== 'function') { ctx.onError('bad-selector', body.start, body.end); } - if (!sel.annotation) { - let hasAnnotation = false; - let arg = sel.arg; - loop: while (arg?.type === 'variable') { - for (const decl of declarations) { - switch (decl.type) { - case 'input': - if ( - decl.value.type === 'expression' && - decl.value.arg?.type === 'variable' && - decl.value.arg.name === arg.name - ) { - hasAnnotation = Boolean(decl.value.annotation); - break loop; - } - break; - case 'local': - if ( - decl.target.name === arg.name && - decl.value.type === 'expression' - ) { - if (decl.value.annotation) { - hasAnnotation = true; - break loop; - } - arg = decl.value.arg; - continue loop; - } - break; - } - } - break loop; - } - if (!hasAnnotation) { - ctx.onError('missing-selector-annotation', sel.start, sel.end); - } - } selectors.push(sel); pos = sel.end; pos += whitespaces(ctx.source, pos); @@ -153,7 +113,7 @@ function parseSelectMessage( const variants: CST.Variant[] = []; pos += whitespaces(ctx.source, pos); while (pos < ctx.source.length) { - const variant = parseVariant(ctx, pos, selectors.length); + const variant = parseVariant(ctx, pos); variants.push(variant); pos = variant.end; pos += whitespaces(ctx.source, pos); @@ -173,11 +133,7 @@ function parseSelectMessage( }; } -function parseVariant( - ctx: ParseContext, - start: number, - selCount: number -): CST.Variant { +function parseVariant(ctx: ParseContext, start: number): CST.Variant { let pos = start; const keys: Array = []; while (pos < ctx.source.length) { @@ -197,11 +153,6 @@ function parseVariant( pos = key.end; } - if (selCount > 0 && keys.length !== selCount) { - const end = keys.length === 0 ? pos : keys[keys.length - 1].end; - ctx.onError('key-mismatch', start, end); - } - const value = parsePattern(ctx, pos, true); return { start, end: value.end, keys, value }; } diff --git a/packages/mf2-messageformat/src/errors.ts b/packages/mf2-messageformat/src/errors.ts index f79ec825..590a01ca 100644 --- a/packages/mf2-messageformat/src/errors.ts +++ b/packages/mf2-messageformat/src/errors.ts @@ -1,3 +1,6 @@ +import { type MessageNode } from './data-model/types.js'; +import { cst } from './cst-parser/as-data-model.js'; + export class MessageError extends Error { type: | 'missing-func' @@ -82,6 +85,13 @@ export class MessageDataModelError extends MessageSyntaxError { | 'key-mismatch' | 'missing-fallback' | 'missing-selector-annotation'; + constructor( + type: typeof MessageDataModelError.prototype.type, + node: MessageNode + ) { + const { start, end } = node[cst] ?? { start: -1, end: -1 }; + super(type, start, end); + } } export class MessageResolutionError extends MessageError { diff --git a/packages/mf2-messageformat/src/messageformat.ts b/packages/mf2-messageformat/src/messageformat.ts index a072f193..75a37088 100644 --- a/packages/mf2-messageformat/src/messageformat.ts +++ b/packages/mf2-messageformat/src/messageformat.ts @@ -4,7 +4,11 @@ import { formatMarkup } from './data-model/format-markup.js'; import { resolveExpression } from './data-model/resolve-expression.js'; import { UnresolvedExpression } from './data-model/resolve-variable.js'; import type { Message } from './data-model/types.js'; -import { MessageError, MessageResolutionError } from './errors.js'; +import { + MessageDataModelError, + MessageError, + MessageResolutionError +} from './errors.js'; import type { Context } from './format-context.js'; import type { MessagePart } from './formatted-parts.js'; import { @@ -17,6 +21,7 @@ import { string } from './functions/index.js'; import { selectPattern } from './select-pattern.js'; +import { validate } from './data-model/validate.js'; const defaultFunctions = Object.freeze({ datetime, @@ -83,6 +88,9 @@ export class MessageFormat { : []; this.#message = typeof source === 'string' ? asDataModel(parseMessage(source)) : source; + validate(this.#message, (type, node) => { + throw new MessageDataModelError(type, node); + }); this.#functions = options?.functions ? { ...defaultFunctions, ...options.functions } : defaultFunctions; From 74549bbe3590821321503c714d893c7371b62c4b Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 3 Jan 2024 19:37:35 +0200 Subject: [PATCH 37/44] refactor(mf2): Move & rename parsers, now use messageFromCST(parseCST(src)) instead of asDataModel(parseMessage(src)) --- .../src/cst-parser/cst-parser.test.ts | 21 +++++++++---------- .../src/cst-parser/declarations.ts | 2 +- .../src/cst-parser/expression.ts | 2 +- .../mf2-messageformat/src/cst-parser/index.ts | 4 ---- .../mf2-messageformat/src/cst-parser/names.ts | 2 +- .../cst-parser/{message.ts => parse-cst.ts} | 2 +- .../src/cst-parser/values.ts | 2 +- .../from-cst.ts} | 6 +++--- .../src/data-model/markup.test.ts | 6 +++--- .../mf2-messageformat/src/data-model/types.ts | 2 +- packages/mf2-messageformat/src/errors.ts | 2 +- packages/mf2-messageformat/src/index.ts | 10 ++++----- .../mf2-messageformat/src/messageformat.ts | 7 ++++--- .../src/stringifier/message.test.ts | 19 ++++++++++------- 14 files changed, 42 insertions(+), 45 deletions(-) delete mode 100644 packages/mf2-messageformat/src/cst-parser/index.ts rename packages/mf2-messageformat/src/cst-parser/{message.ts => parse-cst.ts} (99%) rename packages/mf2-messageformat/src/{cst-parser/as-data-model.ts => data-model/from-cst.ts} (96%) diff --git a/packages/mf2-messageformat/src/cst-parser/cst-parser.test.ts b/packages/mf2-messageformat/src/cst-parser/cst-parser.test.ts index f7eead25..168f96eb 100644 --- a/packages/mf2-messageformat/src/cst-parser/cst-parser.test.ts +++ b/packages/mf2-messageformat/src/cst-parser/cst-parser.test.ts @@ -1,12 +1,11 @@ -import type * as CST from './cst-types.js'; -import { parseMessage } from './index.js'; +import { type CST, parseCST } from '../index.js'; describe('messages in resources', () => { test('text character escapes', () => { const src = '\\\t\\ \\n\\r\\t\\x01\\u0002\\U000003'; - const noRes = parseMessage(src, { resource: false }); + const noRes = parseCST(src, { resource: false }); expect(noRes.errors).toHaveLength(8); - const msg = parseMessage(src, { resource: true }); + const msg = parseCST(src, { resource: true }); expect(msg).toMatchObject({ type: 'simple', errors: [], @@ -27,9 +26,9 @@ describe('messages in resources', () => { test('quoted literal character escapes', () => { const src = '{|\\\t\\ \\n\\r\\t\\x01\\u0002\\U000003|}'; - const noRes = parseMessage(src, { resource: false }); + const noRes = parseCST(src, { resource: false }); expect(noRes.errors).toHaveLength(8); - const msg = parseMessage(src, { resource: true }); + const msg = parseCST(src, { resource: true }); expect(msg).toMatchObject({ type: 'simple', errors: [], @@ -61,7 +60,7 @@ describe('messages in resources', () => { test('complex pattern with leading .', () => { const src = '{{.local}}'; - const msg = parseMessage(src, { resource: false }); + const msg = parseCST(src, { resource: false }); expect(msg).toMatchObject({ type: 'complex', errors: [], @@ -80,13 +79,13 @@ describe('messages in resources', () => { test('newlines in text', () => { const src = '1\n \t2 \n \\ 3\n\\t'; - const noRes = parseMessage(src, { resource: false }); + const noRes = parseCST(src, { resource: false }); expect(noRes).toMatchObject({ type: 'simple', errors: [{ type: 'bad-escape' }, { type: 'bad-escape' }], pattern: { body: [{ type: 'text', value: '1\n \t2 \n \\ 3\n\\t' }] } }); - const msg = parseMessage(src, { resource: true }); + const msg = parseCST(src, { resource: true }); expect(msg).toMatchObject({ type: 'simple', errors: [], @@ -96,7 +95,7 @@ describe('messages in resources', () => { test('newlines in quoted literal', () => { const src = '{|1\n \t2 \n \\ 3\n\\t|}'; - const noRes = parseMessage(src, { resource: false }); + const noRes = parseCST(src, { resource: false }); expect(noRes).toMatchObject({ type: 'simple', errors: [{ type: 'bad-escape' }, { type: 'bad-escape' }], @@ -109,7 +108,7 @@ describe('messages in resources', () => { ] } }); - const msg = parseMessage(src, { resource: true }); + const msg = parseCST(src, { resource: true }); expect(msg).toMatchObject({ type: 'simple', errors: [], diff --git a/packages/mf2-messageformat/src/cst-parser/declarations.ts b/packages/mf2-messageformat/src/cst-parser/declarations.ts index 8a73beec..e807ebaa 100644 --- a/packages/mf2-messageformat/src/cst-parser/declarations.ts +++ b/packages/mf2-messageformat/src/cst-parser/declarations.ts @@ -1,7 +1,7 @@ import type * as CST from './cst-types.js'; import { parseExpression, parseReservedBody } from './expression.js'; -import type { ParseContext } from './message.js'; import { parseNameValue } from './names.js'; +import type { ParseContext } from './parse-cst.js'; import { whitespaces } from './util.js'; import { parseVariable } from './values.js'; diff --git a/packages/mf2-messageformat/src/cst-parser/expression.ts b/packages/mf2-messageformat/src/cst-parser/expression.ts index 0f61572a..b49c8b56 100644 --- a/packages/mf2-messageformat/src/cst-parser/expression.ts +++ b/packages/mf2-messageformat/src/cst-parser/expression.ts @@ -1,6 +1,6 @@ import { MessageSyntaxError } from '../errors.js'; import type * as CST from './cst-types.js'; -import type { ParseContext } from './message.js'; +import type { ParseContext } from './parse-cst.js'; import { parseNameValue } from './names.js'; import { whitespaceChars, whitespaces } from './util.js'; import { parseLiteral, parseQuotedLiteral, parseVariable } from './values.js'; diff --git a/packages/mf2-messageformat/src/cst-parser/index.ts b/packages/mf2-messageformat/src/cst-parser/index.ts deleted file mode 100644 index 1a056864..00000000 --- a/packages/mf2-messageformat/src/cst-parser/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type * as CST from './cst-types.js'; -export { CST }; -export { asDataModel, cst } from './as-data-model.js'; -export { parseMessage } from './message.js'; diff --git a/packages/mf2-messageformat/src/cst-parser/names.ts b/packages/mf2-messageformat/src/cst-parser/names.ts index 2c8fca26..daf12c46 100644 --- a/packages/mf2-messageformat/src/cst-parser/names.ts +++ b/packages/mf2-messageformat/src/cst-parser/names.ts @@ -1,4 +1,4 @@ -import type { ParseContext } from './message.js'; +import type { ParseContext } from './parse-cst.js'; // name-start = ALPHA / "_" // / %xC0-D6 / %xD8-F6 / %xF8-2FF diff --git a/packages/mf2-messageformat/src/cst-parser/message.ts b/packages/mf2-messageformat/src/cst-parser/parse-cst.ts similarity index 99% rename from packages/mf2-messageformat/src/cst-parser/message.ts rename to packages/mf2-messageformat/src/cst-parser/parse-cst.ts index 5be32bd5..d5e87097 100644 --- a/packages/mf2-messageformat/src/cst-parser/message.ts +++ b/packages/mf2-messageformat/src/cst-parser/parse-cst.ts @@ -51,7 +51,7 @@ export class ParseContext { * * @beta */ -export function parseMessage( +export function parseCST( source: string, opt?: { resource?: boolean } ): CST.Message { diff --git a/packages/mf2-messageformat/src/cst-parser/values.ts b/packages/mf2-messageformat/src/cst-parser/values.ts index 8f199baa..502da54d 100644 --- a/packages/mf2-messageformat/src/cst-parser/values.ts +++ b/packages/mf2-messageformat/src/cst-parser/values.ts @@ -1,6 +1,6 @@ import type * as CST from './cst-types.js'; -import type { ParseContext } from './message.js'; import { parseNameValue, parseUnquotedLiteralValue } from './names.js'; +import type { ParseContext } from './parse-cst.js'; // Text ::= (TextChar | TextEscape)+ // TextChar ::= AnyChar - ('{' | '}' | Esc) diff --git a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts b/packages/mf2-messageformat/src/data-model/from-cst.ts similarity index 96% rename from packages/mf2-messageformat/src/cst-parser/as-data-model.ts rename to packages/mf2-messageformat/src/data-model/from-cst.ts index 770cd119..9193c344 100644 --- a/packages/mf2-messageformat/src/cst-parser/as-data-model.ts +++ b/packages/mf2-messageformat/src/data-model/from-cst.ts @@ -1,6 +1,6 @@ -import type * as Model from '../data-model/types.js'; +import type * as Model from './types.js'; import { MessageSyntaxError } from '../errors.js'; -import type * as CST from './cst-types.js'; +import type * as CST from '../cst-parser/cst-types.js'; export const cst = Symbol.for('CST'); @@ -9,7 +9,7 @@ export const cst = Symbol.for('CST'); * * @beta */ -export function asDataModel(msg: CST.Message): Model.Message { +export function messageFromCST(msg: CST.Message): Model.Message { for (const error of msg.errors) throw error; const declarations: Model.Declaration[] = msg.declarations ? msg.declarations.map(asDeclaration) diff --git a/packages/mf2-messageformat/src/data-model/markup.test.ts b/packages/mf2-messageformat/src/data-model/markup.test.ts index 29d6c2b8..7a2ebab6 100644 --- a/packages/mf2-messageformat/src/data-model/markup.test.ts +++ b/packages/mf2-messageformat/src/data-model/markup.test.ts @@ -1,4 +1,4 @@ -import { MessageFormat, parseMessage } from '../index.js'; +import { MessageFormat, parseCST } from '../index.js'; describe('Simple open/close', () => { test('no options, literal body', () => { @@ -32,7 +32,7 @@ describe('Simple open/close', () => { test('do not allow operands', () => { const src = '{x #b}'; expect(() => new MessageFormat(src, 'en')).toThrow(); - const cst = parseMessage(src); + const cst = parseCST(src); expect(cst).toMatchObject({ type: 'simple', errors: [{ type: 'extra-content' }], @@ -50,7 +50,7 @@ describe('Simple open/close', () => { test('do not allow options on close', () => { const src = '{/b foo=13}'; expect(() => new MessageFormat(src, 'en')).toThrow(); - const cst = parseMessage(src); + const cst = parseCST(src); expect(cst).toMatchObject({ type: 'simple', errors: [{ type: 'extra-content' }], diff --git a/packages/mf2-messageformat/src/data-model/types.ts b/packages/mf2-messageformat/src/data-model/types.ts index 80d333e9..7092af6f 100644 --- a/packages/mf2-messageformat/src/data-model/types.ts +++ b/packages/mf2-messageformat/src/data-model/types.ts @@ -1,5 +1,5 @@ -import { cst } from '../cst-parser/as-data-model.js'; import type * as CST from '../cst-parser/cst-types.js'; +import { cst } from './from-cst.js'; /** A node in a message data model */ export type MessageNode = diff --git a/packages/mf2-messageformat/src/errors.ts b/packages/mf2-messageformat/src/errors.ts index 590a01ca..f9c7c25f 100644 --- a/packages/mf2-messageformat/src/errors.ts +++ b/packages/mf2-messageformat/src/errors.ts @@ -1,5 +1,5 @@ import { type MessageNode } from './data-model/types.js'; -import { cst } from './cst-parser/as-data-model.js'; +import { cst } from './data-model/from-cst.js'; export class MessageError extends Error { type: diff --git a/packages/mf2-messageformat/src/index.ts b/packages/mf2-messageformat/src/index.ts index 864179a7..dbe58471 100644 --- a/packages/mf2-messageformat/src/index.ts +++ b/packages/mf2-messageformat/src/index.ts @@ -1,13 +1,11 @@ +import type * as CST from './cst-parser/cst-types.js'; +export type { CST }; export type * from './data-model/types.js'; export type * from './formatted-parts.js'; export type * from './functions/index.js'; -export { - type CST, - asDataModel, - cst, - parseMessage -} from './cst-parser/index.js'; +export { parseCST } from './cst-parser/parse-cst.js'; +export { messageFromCST, cst } from './data-model/from-cst.js'; export { isCatchallKey, isExpression, diff --git a/packages/mf2-messageformat/src/messageformat.ts b/packages/mf2-messageformat/src/messageformat.ts index 75a37088..dbc2f098 100644 --- a/packages/mf2-messageformat/src/messageformat.ts +++ b/packages/mf2-messageformat/src/messageformat.ts @@ -1,9 +1,11 @@ -import { asDataModel, parseMessage } from './cst-parser/index.js'; +import { parseCST } from './cst-parser/parse-cst.js'; +import { messageFromCST } from './data-model/from-cst.js'; import type { MessageFunctionContext } from './data-model/function-context.js'; import { formatMarkup } from './data-model/format-markup.js'; import { resolveExpression } from './data-model/resolve-expression.js'; import { UnresolvedExpression } from './data-model/resolve-variable.js'; import type { Message } from './data-model/types.js'; +import { validate } from './data-model/validate.js'; import { MessageDataModelError, MessageError, @@ -21,7 +23,6 @@ import { string } from './functions/index.js'; import { selectPattern } from './select-pattern.js'; -import { validate } from './data-model/validate.js'; const defaultFunctions = Object.freeze({ datetime, @@ -87,7 +88,7 @@ export class MessageFormat { ? [locales] : []; this.#message = - typeof source === 'string' ? asDataModel(parseMessage(source)) : source; + typeof source === 'string' ? messageFromCST(parseCST(source)) : source; validate(this.#message, (type, node) => { throw new MessageDataModelError(type, node); }); diff --git a/packages/mf2-messageformat/src/stringifier/message.test.ts b/packages/mf2-messageformat/src/stringifier/message.test.ts index 1725b15b..74e0fce9 100644 --- a/packages/mf2-messageformat/src/stringifier/message.test.ts +++ b/packages/mf2-messageformat/src/stringifier/message.test.ts @@ -1,12 +1,15 @@ import testCore from '../__fixtures/test-core.json'; import testFunctions from '../__fixtures/test-functions.json'; -import { testName } from '../__fixtures/util-test-name'; -import { asDataModel, parseMessage } from '../cst-parser/index.js'; -import { Message } from '../data-model/types'; -import { cst } from '../cst-parser/as-data-model'; -import { visit } from '../data-model/visit'; +import { testName } from '../__fixtures/util-test-name.js'; +import { + type Message, + cst, + messageFromCST, + parseCST, + stringifyMessage, + visit +} from '../index.js'; import type { TestMessage } from '../messageformat.test.js'; -import { stringifyMessage } from './message.js'; function trimCST(msg: Message) { delete msg[cst]; @@ -28,10 +31,10 @@ for (const [title, messages] of [ for (const testMsg of messages) { if (!testMsg.errors?.length) { test(testName(testMsg), () => { - const msg0 = asDataModel(parseMessage(testMsg.src)); + const msg0 = messageFromCST(parseCST(testMsg.src)); trimCST(msg0); const src1 = stringifyMessage(msg0); - const msg1 = asDataModel(parseMessage(src1)); + const msg1 = messageFromCST(parseCST(src1)); trimCST(msg1); expect(msg1).toEqual(msg0); }); From 44d70da59baba97f1725034878636dfa794f4d05 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 3 Jan 2024 19:58:35 +0200 Subject: [PATCH 38/44] refactor(mf2): Rename src/cst-parser/ -> src/cst/ --- .../mf2-messageformat/src/{cst-parser => cst}/declarations.ts | 2 +- .../mf2-messageformat/src/{cst-parser => cst}/expression.ts | 2 +- packages/mf2-messageformat/src/{cst-parser => cst}/names.ts | 0 .../mf2-messageformat/src/{cst-parser => cst}/parse-cst.ts | 2 +- .../cst-parser.test.ts => cst/resource-option.test.ts} | 0 .../src/{cst-parser/cst-types.ts => cst/types.ts} | 0 packages/mf2-messageformat/src/{cst-parser => cst}/util.ts | 0 packages/mf2-messageformat/src/{cst-parser => cst}/values.ts | 2 +- packages/mf2-messageformat/src/data-model/from-cst.ts | 2 +- packages/mf2-messageformat/src/data-model/types.ts | 2 +- packages/mf2-messageformat/src/index.ts | 4 ++-- packages/mf2-messageformat/src/messageformat.ts | 2 +- packages/mf2-messageformat/src/stringifier/message.ts | 2 +- 13 files changed, 10 insertions(+), 10 deletions(-) rename packages/mf2-messageformat/src/{cst-parser => cst}/declarations.ts (98%) rename packages/mf2-messageformat/src/{cst-parser => cst}/expression.ts (99%) rename packages/mf2-messageformat/src/{cst-parser => cst}/names.ts (100%) rename packages/mf2-messageformat/src/{cst-parser => cst}/parse-cst.ts (99%) rename packages/mf2-messageformat/src/{cst-parser/cst-parser.test.ts => cst/resource-option.test.ts} (100%) rename packages/mf2-messageformat/src/{cst-parser/cst-types.ts => cst/types.ts} (100%) rename packages/mf2-messageformat/src/{cst-parser => cst}/util.ts (100%) rename packages/mf2-messageformat/src/{cst-parser => cst}/values.ts (99%) diff --git a/packages/mf2-messageformat/src/cst-parser/declarations.ts b/packages/mf2-messageformat/src/cst/declarations.ts similarity index 98% rename from packages/mf2-messageformat/src/cst-parser/declarations.ts rename to packages/mf2-messageformat/src/cst/declarations.ts index e807ebaa..41399910 100644 --- a/packages/mf2-messageformat/src/cst-parser/declarations.ts +++ b/packages/mf2-messageformat/src/cst/declarations.ts @@ -1,7 +1,7 @@ -import type * as CST from './cst-types.js'; import { parseExpression, parseReservedBody } from './expression.js'; import { parseNameValue } from './names.js'; import type { ParseContext } from './parse-cst.js'; +import type * as CST from './types.js'; import { whitespaces } from './util.js'; import { parseVariable } from './values.js'; diff --git a/packages/mf2-messageformat/src/cst-parser/expression.ts b/packages/mf2-messageformat/src/cst/expression.ts similarity index 99% rename from packages/mf2-messageformat/src/cst-parser/expression.ts rename to packages/mf2-messageformat/src/cst/expression.ts index b49c8b56..22a90caf 100644 --- a/packages/mf2-messageformat/src/cst-parser/expression.ts +++ b/packages/mf2-messageformat/src/cst/expression.ts @@ -1,7 +1,7 @@ import { MessageSyntaxError } from '../errors.js'; -import type * as CST from './cst-types.js'; import type { ParseContext } from './parse-cst.js'; import { parseNameValue } from './names.js'; +import type * as CST from './types.js'; import { whitespaceChars, whitespaces } from './util.js'; import { parseLiteral, parseQuotedLiteral, parseVariable } from './values.js'; diff --git a/packages/mf2-messageformat/src/cst-parser/names.ts b/packages/mf2-messageformat/src/cst/names.ts similarity index 100% rename from packages/mf2-messageformat/src/cst-parser/names.ts rename to packages/mf2-messageformat/src/cst/names.ts diff --git a/packages/mf2-messageformat/src/cst-parser/parse-cst.ts b/packages/mf2-messageformat/src/cst/parse-cst.ts similarity index 99% rename from packages/mf2-messageformat/src/cst-parser/parse-cst.ts rename to packages/mf2-messageformat/src/cst/parse-cst.ts index d5e87097..263f6f3c 100644 --- a/packages/mf2-messageformat/src/cst-parser/parse-cst.ts +++ b/packages/mf2-messageformat/src/cst/parse-cst.ts @@ -3,9 +3,9 @@ import { MessageSyntaxError, MissingSyntaxError } from '../errors.js'; -import type * as CST from './cst-types.js'; import { parseDeclarations } from './declarations.js'; import { parseExpression } from './expression.js'; +import type * as CST from './types.js'; import { whitespaces } from './util.js'; import { parseLiteral, parseText } from './values.js'; diff --git a/packages/mf2-messageformat/src/cst-parser/cst-parser.test.ts b/packages/mf2-messageformat/src/cst/resource-option.test.ts similarity index 100% rename from packages/mf2-messageformat/src/cst-parser/cst-parser.test.ts rename to packages/mf2-messageformat/src/cst/resource-option.test.ts diff --git a/packages/mf2-messageformat/src/cst-parser/cst-types.ts b/packages/mf2-messageformat/src/cst/types.ts similarity index 100% rename from packages/mf2-messageformat/src/cst-parser/cst-types.ts rename to packages/mf2-messageformat/src/cst/types.ts diff --git a/packages/mf2-messageformat/src/cst-parser/util.ts b/packages/mf2-messageformat/src/cst/util.ts similarity index 100% rename from packages/mf2-messageformat/src/cst-parser/util.ts rename to packages/mf2-messageformat/src/cst/util.ts diff --git a/packages/mf2-messageformat/src/cst-parser/values.ts b/packages/mf2-messageformat/src/cst/values.ts similarity index 99% rename from packages/mf2-messageformat/src/cst-parser/values.ts rename to packages/mf2-messageformat/src/cst/values.ts index 502da54d..b8792b6e 100644 --- a/packages/mf2-messageformat/src/cst-parser/values.ts +++ b/packages/mf2-messageformat/src/cst/values.ts @@ -1,6 +1,6 @@ -import type * as CST from './cst-types.js'; import { parseNameValue, parseUnquotedLiteralValue } from './names.js'; import type { ParseContext } from './parse-cst.js'; +import type * as CST from './types.js'; // Text ::= (TextChar | TextEscape)+ // TextChar ::= AnyChar - ('{' | '}' | Esc) diff --git a/packages/mf2-messageformat/src/data-model/from-cst.ts b/packages/mf2-messageformat/src/data-model/from-cst.ts index 9193c344..0a81613e 100644 --- a/packages/mf2-messageformat/src/data-model/from-cst.ts +++ b/packages/mf2-messageformat/src/data-model/from-cst.ts @@ -1,6 +1,6 @@ import type * as Model from './types.js'; import { MessageSyntaxError } from '../errors.js'; -import type * as CST from '../cst-parser/cst-types.js'; +import type * as CST from '../cst/types.js'; export const cst = Symbol.for('CST'); diff --git a/packages/mf2-messageformat/src/data-model/types.ts b/packages/mf2-messageformat/src/data-model/types.ts index 7092af6f..21f14848 100644 --- a/packages/mf2-messageformat/src/data-model/types.ts +++ b/packages/mf2-messageformat/src/data-model/types.ts @@ -1,4 +1,4 @@ -import type * as CST from '../cst-parser/cst-types.js'; +import type * as CST from '../cst/types.js'; import { cst } from './from-cst.js'; /** A node in a message data model */ diff --git a/packages/mf2-messageformat/src/index.ts b/packages/mf2-messageformat/src/index.ts index dbe58471..431daf3f 100644 --- a/packages/mf2-messageformat/src/index.ts +++ b/packages/mf2-messageformat/src/index.ts @@ -1,10 +1,10 @@ -import type * as CST from './cst-parser/cst-types.js'; +import type * as CST from './cst/types.js'; export type { CST }; export type * from './data-model/types.js'; export type * from './formatted-parts.js'; export type * from './functions/index.js'; -export { parseCST } from './cst-parser/parse-cst.js'; +export { parseCST } from './cst/parse-cst.js'; export { messageFromCST, cst } from './data-model/from-cst.js'; export { isCatchallKey, diff --git a/packages/mf2-messageformat/src/messageformat.ts b/packages/mf2-messageformat/src/messageformat.ts index dbc2f098..b4b0099c 100644 --- a/packages/mf2-messageformat/src/messageformat.ts +++ b/packages/mf2-messageformat/src/messageformat.ts @@ -1,4 +1,4 @@ -import { parseCST } from './cst-parser/parse-cst.js'; +import { parseCST } from './cst/parse-cst.js'; import { messageFromCST } from './data-model/from-cst.js'; import type { MessageFunctionContext } from './data-model/function-context.js'; import { formatMarkup } from './data-model/format-markup.js'; diff --git a/packages/mf2-messageformat/src/stringifier/message.ts b/packages/mf2-messageformat/src/stringifier/message.ts index 015818b8..ce1a59ec 100644 --- a/packages/mf2-messageformat/src/stringifier/message.ts +++ b/packages/mf2-messageformat/src/stringifier/message.ts @@ -1,4 +1,4 @@ -import { isValidUnquotedLiteral } from '../cst-parser/names.js'; +import { isValidUnquotedLiteral } from '../cst/names.js'; import { isLiteral, isPatternMessage, From 825c18902dfd9dfe69b5d8509f7984e472e9cc37 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 3 Jan 2024 20:28:51 +0200 Subject: [PATCH 39/44] fix(mf2): Include all sigils in CST --- .../mf2-messageformat/src/cst/declarations.ts | 3 +- .../mf2-messageformat/src/cst/expression.ts | 32 +++++++++++++------ packages/mf2-messageformat/src/cst/types.ts | 19 +++++------ packages/mf2-messageformat/src/cst/values.ts | 19 ++++++++--- .../src/data-model/from-cst.ts | 3 +- 5 files changed, 50 insertions(+), 26 deletions(-) diff --git a/packages/mf2-messageformat/src/cst/declarations.ts b/packages/mf2-messageformat/src/cst/declarations.ts index 41399910..51279927 100644 --- a/packages/mf2-messageformat/src/cst/declarations.ts +++ b/packages/mf2-messageformat/src/cst/declarations.ts @@ -85,12 +85,11 @@ function parseLocalDeclaration( } pos += whitespaces(source, pos); - let equals: CST.Syntax<'=' | ''>; + let equals: CST.Syntax<'='> | undefined; if (source[pos] === '=') { equals = { start: pos, end: pos + 1, value: '=' }; pos += 1; } else { - equals = { start: pos, end: pos, value: '' }; ctx.onError('missing-syntax', pos, '='); } diff --git a/packages/mf2-messageformat/src/cst/expression.ts b/packages/mf2-messageformat/src/cst/expression.ts index 22a90caf..5d4c4545 100644 --- a/packages/mf2-messageformat/src/cst/expression.ts +++ b/packages/mf2-messageformat/src/cst/expression.ts @@ -130,28 +130,38 @@ function parseFunctionRefOrMarkup( options.push(opt); pos = opt.end; } - return type === 'function' - ? { type, start, end: pos, name: id.parts, options } - : { type, start, end: pos, name: id.parts, options, close }; + if (type === 'function') { + const open = { start, end: start + 1, value: ':' as const }; + return { type, start, end: pos, open, name: id.parts, options }; + } else { + const open = { start, end: start + 1, value: '#' as const }; + return { type, start, end: pos, open, name: id.parts, options, close }; + } } function parseMarkupClose(ctx: ParseContext, start: number): CST.MarkupClose { const id = parseIdentifier(ctx, start + 1); - return { type: 'markup-close', start, end: id.end, name: id.parts }; + const open = { start, end: start + 1, value: '/' as const }; + return { type: 'markup-close', start, end: id.end, open, name: id.parts }; } function parseOption(ctx: ParseContext, start: number): CST.Option { const id = parseIdentifier(ctx, start); let pos = id.end; pos += whitespaces(ctx.source, pos); - if (ctx.source[pos] === '=') pos += 1; - else ctx.onError('missing-syntax', pos, '='); + let equals: CST.Syntax<'='> | undefined; + if (ctx.source[pos] === '=') { + equals = { start: pos, end: pos + 1, value: '=' }; + pos += 1; + } else { + ctx.onError('missing-syntax', pos, '='); + } pos += whitespaces(ctx.source, pos); const value = ctx.source[pos] === '$' ? parseVariable(ctx, pos) : parseLiteral(ctx, pos, true); - return { start, end: value.end, name: id.parts, value }; + return { start, end: value.end, name: id.parts, equals, value }; } function parseIdentifier( @@ -186,13 +196,17 @@ function parseReservedAnnotation( ctx: ParseContext, start: number ): CST.ReservedAnnotation { - const sigil = ctx.source[start] as CST.ReservedAnnotation['sigil']; + const open = { + start, + end: start + 1, + value: ctx.source[start] + } as CST.ReservedAnnotation['open']; const source = parseReservedBody(ctx, start + 1); // skip sigil return { type: 'reserved-annotation', start, end: source.end, - sigil, + open, source }; } diff --git a/packages/mf2-messageformat/src/cst/types.ts b/packages/mf2-messageformat/src/cst/types.ts index 23488206..fe86dad3 100644 --- a/packages/mf2-messageformat/src/cst/types.ts +++ b/packages/mf2-messageformat/src/cst/types.ts @@ -51,7 +51,7 @@ export interface LocalDeclaration { end: number; keyword: Syntax<'.local'>; target: VariableRef | Junk; - equals: Syntax<'=' | ''>; + equals?: Syntax<'='>; value: Expression | Junk; } @@ -121,19 +121,19 @@ export interface Junk { export interface Literal { type: 'literal'; quoted: boolean; - /** position of the initial `|` */ start: number; - /** position one past the terminal `|` */ end: number; + open?: Syntax<'|'>; value: string; + close?: Syntax<'|'>; } /** @beta */ export interface VariableRef { type: 'variable'; - /** position of the `$` */ start: number; end: number; + open: Syntax<'$'>; name: string; } @@ -142,6 +142,7 @@ export interface FunctionRef { type: 'function'; start: number; end: number; + open: Syntax<':'>; name: Identifier; options: Option[]; } @@ -149,9 +150,8 @@ export interface FunctionRef { /** @beta */ export interface ReservedAnnotation { type: 'reserved-annotation'; - sigil: '!' | '@' | '#' | '%' | '^' | '&' | '*' | '<' | '>' | '?' | '~'; + open: Syntax<'!' | '@' | '#' | '%' | '^' | '&' | '*' | '<' | '>' | '?' | '~'>; source: Syntax; - /** position of the sigil */ start: number; end: number; } @@ -159,20 +159,20 @@ export interface ReservedAnnotation { /** @beta */ export interface Markup { type: 'markup'; - /** position of the sigil */ start: number; end: number; + open: Syntax<'#'>; name: Identifier; options: Option[]; - close: Syntax<'/'> | undefined; + close?: Syntax<'/'>; } /** @beta */ export interface MarkupClose { type: 'markup-close'; - /** position of the sigil */ start: number; end: number; + open: Syntax<'/'>; name: Identifier; } @@ -182,6 +182,7 @@ export interface Option { start: number; end: number; name: Identifier; + equals?: Syntax<'='>; value: Literal | VariableRef; } diff --git a/packages/mf2-messageformat/src/cst/values.ts b/packages/mf2-messageformat/src/cst/values.ts index b8792b6e..941ea30e 100644 --- a/packages/mf2-messageformat/src/cst/values.ts +++ b/packages/mf2-messageformat/src/cst/values.ts @@ -76,6 +76,7 @@ export function parseQuotedLiteral( ): CST.Literal { let value = ''; let pos = start + 1; + const open = { start, end: pos, value: '|' as const }; for (let i = pos; i < ctx.source.length; ++i) { switch (ctx.source[i]) { case '\\': { @@ -89,7 +90,15 @@ export function parseQuotedLiteral( } case '|': value += ctx.source.substring(pos, i); - return { type: 'literal', quoted: true, start, end: i + 1, value }; + return { + type: 'literal', + quoted: true, + start, + end: i + 1, + open, + value, + close: { start: i, end: i + 1, value: '|' } + }; case '\n': if (ctx.resource) { const nl = i; @@ -113,20 +122,22 @@ export function parseQuotedLiteral( quoted: true, start, end: ctx.source.length, - value + open, + value, + close: undefined }; } -// Variable ::= '$' Name /* ws: explicit */ export function parseVariable( ctx: ParseContext, start: number ): CST.VariableRef { const pos = start + 1; + const open = { start, end: pos, value: '$' as const }; const name = parseNameValue(ctx.source, pos); const end = pos + name.length; if (!name) ctx.onError('empty-token', pos, pos + 1); - return { type: 'variable', start, end, name }; + return { type: 'variable', start, end, open, name }; } function parseEscape( diff --git a/packages/mf2-messageformat/src/data-model/from-cst.ts b/packages/mf2-messageformat/src/data-model/from-cst.ts index 0a81613e..c6d59879 100644 --- a/packages/mf2-messageformat/src/data-model/from-cst.ts +++ b/packages/mf2-messageformat/src/data-model/from-cst.ts @@ -121,12 +121,11 @@ function asExpression( if (ca.options.length > 0) { annotation.options = ca.options.map(asOption); } - break; case 'reserved-annotation': annotation = { type: 'unsupported-annotation', - sigil: ca.sigil, + sigil: ca.open.value, source: ca.source.value, [cst]: ca }; From 867c57549099282e7bd6da95ca6191b341dd502f Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 3 Jan 2024 21:56:29 +0200 Subject: [PATCH 40/44] feat(mf2): Add CST stringifier --- .../src/__fixtures/test-core.json | 12 +- .../src/__fixtures/test-functions.json | 2 +- .../mf2-messageformat/src/cst/cst.test.ts | 24 ++++ .../src/cst/stringify-cst.ts | 125 ++++++++++++++++++ packages/mf2-messageformat/src/index.ts | 1 + .../src/messageformat.test.ts | 1 + 6 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 packages/mf2-messageformat/src/cst/cst.test.ts create mode 100644 packages/mf2-messageformat/src/cst/stringify-cst.ts diff --git a/packages/mf2-messageformat/src/__fixtures/test-core.json b/packages/mf2-messageformat/src/__fixtures/test-core.json index eda430b5..498f0246 100644 --- a/packages/mf2-messageformat/src/__fixtures/test-core.json +++ b/packages/mf2-messageformat/src/__fixtures/test-core.json @@ -1,8 +1,16 @@ [ { "src": "hello", "exp": "hello" }, { "src": "hello {world}", "exp": "hello world" }, - { "src": "hello { world\t\n}", "exp": "hello world" }, - { "src": "hello {\u3000world\r}", "exp": "hello world" }, + { + "src": "hello { world\t\n}", + "exp": "hello world", + "cleanSrc": "hello {world}" + }, + { + "src": "hello {\u3000world\r}", + "exp": "hello world", + "cleanSrc": "hello {world}" + }, { "src": "hello {|world|}", "exp": "hello world" }, { "src": "hello {||}", "exp": "hello " }, { diff --git a/packages/mf2-messageformat/src/__fixtures/test-functions.json b/packages/mf2-messageformat/src/__fixtures/test-functions.json index 52305a15..d50c09f1 100644 --- a/packages/mf2-messageformat/src/__fixtures/test-functions.json +++ b/packages/mf2-messageformat/src/__fixtures/test-functions.json @@ -137,7 +137,7 @@ "exp": "one" }, { - "src": ".input {$foo :number } .local $bar = {$foo} .match {$bar} one {{one}} * {{other}}", + "src": ".input {$foo :number} .local $bar = {$foo} .match {$bar} one {{one}} * {{other}}", "params": { "foo": 1 }, "exp": "one" }, diff --git a/packages/mf2-messageformat/src/cst/cst.test.ts b/packages/mf2-messageformat/src/cst/cst.test.ts new file mode 100644 index 00000000..4a7f989d --- /dev/null +++ b/packages/mf2-messageformat/src/cst/cst.test.ts @@ -0,0 +1,24 @@ +import testCore from '../__fixtures/test-core.json'; +import testFunctions from '../__fixtures/test-functions.json'; +import { testName } from '../__fixtures/util-test-name.js'; +import type { TestMessage } from '../messageformat.test.js'; +import { parseCST, stringifyCST } from '../index.js'; + +for (const [title, messages] of [ + ['Parse & stringify CST of core messages', testCore], + ...Object.entries(testFunctions).map(x => [ + `Parse & stringify CST of :${x[0]} messages`, + x[1] + ]) +] as Array<[string, TestMessage[]]>) { + describe(title, () => { + for (const testMsg of messages) { + test(testName(testMsg), () => { + const cst = parseCST(testMsg.src); + expect(cst.errors).toHaveLength(0); + const src = stringifyCST(cst); + expect(src.replace(/\n/g, ' ')).toBe(testMsg.cleanSrc ?? testMsg.src); + }); + } + }); +} diff --git a/packages/mf2-messageformat/src/cst/stringify-cst.ts b/packages/mf2-messageformat/src/cst/stringify-cst.ts new file mode 100644 index 00000000..a773eacb --- /dev/null +++ b/packages/mf2-messageformat/src/cst/stringify-cst.ts @@ -0,0 +1,125 @@ +import type * as CST from './types.js'; + +export function stringifyCST(cst: CST.Message): string { + let str = ''; + + if (cst.declarations) { + for (const decl of cst.declarations) { + const kw = decl.keyword.value; + switch (decl.type) { + case 'input': { + const val = stringifyExpression(decl.value); + str += `${kw} ${val}\n`; + break; + } + case 'local': { + const tgt = stringifyValue(decl.target); + const eq = decl.equals?.value ?? '='; + const val = stringifyExpression(decl.value); + str += `${kw} ${tgt} ${eq} ${val}\n`; + break; + } + case 'reserved-statement': { + str += kw; + if (decl.body) str += ' ' + decl.body; + for (const exp of decl.values) str += ' ' + stringifyExpression(exp); + str += '\n'; + break; + } + } + } + } + + if (cst.type === 'select') { + str += cst.match.value; + for (const sel of cst.selectors) str += ' ' + stringifyExpression(sel); + for (const { keys, value } of cst.variants) { + str += '\n'; + for (const key of keys) str += stringifyValue(key) + ' '; + str += stringifyPattern(value, true); + } + } else { + str += stringifyPattern( + cst.pattern, + cst.type !== 'simple' || !!cst.declarations + ); + } + + return str; +} + +function stringifyPattern( + { body, braces }: CST.Pattern, + braced: boolean +): string { + let str = braced ? braces?.[0]?.value ?? '{{' : ''; + for (const el of body) { + str += + el.type === 'text' + ? el.value.replace(/[\\{}]/g, '\\$&') + : stringifyExpression(el); + } + if (braced) str += braces?.[1]?.value ?? '}}'; + return str; +} + +function stringifyExpression( + exp: CST.Expression | CST.Junk | undefined +): string { + if (exp?.type !== 'expression') return exp?.source ?? ''; + const { braces, arg, annotation, markup } = exp; + let str = braces[0]?.value ?? '{'; + if (markup) { + str += markup.open.value + stringifyIdentifier(markup.name); + if (markup.type === 'markup') { + for (const opt of markup.options) str += stringifyOption(opt); + if (markup.close) str += ' ' + markup.close.value; + } + } else { + if (arg) { + str += stringifyValue(arg); + if (annotation) str += ' '; + } + switch (annotation?.type) { + case 'function': + str += annotation.open.value + stringifyIdentifier(annotation.name); + for (const opt of annotation.options) str += stringifyOption(opt); + break; + case 'reserved-annotation': + str += annotation.open.value + annotation.source.value; + break; + case 'junk': + str += annotation.source; + break; + } + } + return str + (braces[1]?.value ?? '}'); +} + +const stringifyOption = ({ name, equals, value }: CST.Option) => + ' ' + + stringifyIdentifier(name) + + (equals?.value ?? '=') + + stringifyValue(value); + +const stringifyIdentifier = (id: CST.Identifier) => + id.map(part => part.value).join(''); + +function stringifyValue( + value: CST.Literal | CST.VariableRef | CST.CatchallKey | CST.Junk +): string { + switch (value.type) { + case 'variable': + return value.open.value + value.name; + case 'literal': + if (!value.quoted) return value.value; + return ( + (value.open?.value ?? '|') + + value.value.replace(/[\\|]/g, '\\$&') + + (value.close?.value ?? '|') + ); + case '*': + return '*'; + } + return value.source ?? ''; +} diff --git a/packages/mf2-messageformat/src/index.ts b/packages/mf2-messageformat/src/index.ts index 431daf3f..461e9fa5 100644 --- a/packages/mf2-messageformat/src/index.ts +++ b/packages/mf2-messageformat/src/index.ts @@ -5,6 +5,7 @@ export type * from './formatted-parts.js'; export type * from './functions/index.js'; export { parseCST } from './cst/parse-cst.js'; +export { stringifyCST } from './cst/stringify-cst.js'; export { messageFromCST, cst } from './data-model/from-cst.js'; export { isCatchallKey, diff --git a/packages/mf2-messageformat/src/messageformat.test.ts b/packages/mf2-messageformat/src/messageformat.test.ts index d5b1ea7f..d2664ae5 100644 --- a/packages/mf2-messageformat/src/messageformat.test.ts +++ b/packages/mf2-messageformat/src/messageformat.test.ts @@ -12,6 +12,7 @@ export type TestMessage = { params?: Record; exp: string; parts?: Array; + cleanSrc?: string; errors?: Array<{ type: string }>; }; From 915b6ed12037885bbaec119f391a9f9de3976214 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 3 Jan 2024 22:24:43 +0200 Subject: [PATCH 41/44] refactor(mf2): Move src/stringifier/message.ts -> src/data-model/stringify.ts --- .../message.test.ts => data-model/stringify.test.ts} | 0 .../src/{stringifier/message.ts => data-model/stringify.ts} | 6 +++--- packages/mf2-messageformat/src/index.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename packages/mf2-messageformat/src/{stringifier/message.test.ts => data-model/stringify.test.ts} (100%) rename packages/mf2-messageformat/src/{stringifier/message.ts => data-model/stringify.ts} (98%) diff --git a/packages/mf2-messageformat/src/stringifier/message.test.ts b/packages/mf2-messageformat/src/data-model/stringify.test.ts similarity index 100% rename from packages/mf2-messageformat/src/stringifier/message.test.ts rename to packages/mf2-messageformat/src/data-model/stringify.test.ts diff --git a/packages/mf2-messageformat/src/stringifier/message.ts b/packages/mf2-messageformat/src/data-model/stringify.ts similarity index 98% rename from packages/mf2-messageformat/src/stringifier/message.ts rename to packages/mf2-messageformat/src/data-model/stringify.ts index ce1a59ec..d0cdc043 100644 --- a/packages/mf2-messageformat/src/stringifier/message.ts +++ b/packages/mf2-messageformat/src/data-model/stringify.ts @@ -1,10 +1,11 @@ import { isValidUnquotedLiteral } from '../cst/names.js'; +import { MessageFormat } from '../messageformat.js'; import { isLiteral, isPatternMessage, isSelectMessage, isVariableRef -} from '../data-model/type-guards.js'; +} from './type-guards.js'; import type { Declaration, Expression, @@ -16,8 +17,7 @@ import type { Pattern, UnsupportedAnnotation, VariableRef -} from '../data-model/types.js'; -import { MessageFormat } from '../messageformat.js'; +} from './types.js'; /** * Stringify a message using its syntax representation. diff --git a/packages/mf2-messageformat/src/index.ts b/packages/mf2-messageformat/src/index.ts index 461e9fa5..ccb3112e 100644 --- a/packages/mf2-messageformat/src/index.ts +++ b/packages/mf2-messageformat/src/index.ts @@ -7,6 +7,7 @@ export type * from './functions/index.js'; export { parseCST } from './cst/parse-cst.js'; export { stringifyCST } from './cst/stringify-cst.js'; export { messageFromCST, cst } from './data-model/from-cst.js'; +export { stringifyMessage } from './data-model/stringify.js'; export { isCatchallKey, isExpression, @@ -33,4 +34,3 @@ export { MessageFormatOptions, MessageFunctions } from './messageformat.js'; -export { stringifyMessage } from './stringifier/message.js'; From 207476f9c3adaead437fc7c7c84ac42a3b26c1c8 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 3 Jan 2024 22:30:38 +0200 Subject: [PATCH 42/44] fix(mf2): Use only 'messageformat/functions' for function type exports --- packages/mf2-fluent/src/functions.ts | 5 ++++- packages/mf2-icu-mf1/src/functions.ts | 14 +++++++------- .../mf2-messageformat/src/functions/index.ts | 16 +++++++++++----- packages/mf2-messageformat/src/index.ts | 1 - 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/packages/mf2-fluent/src/functions.ts b/packages/mf2-fluent/src/functions.ts index 36047d02..26a793d0 100644 --- a/packages/mf2-fluent/src/functions.ts +++ b/packages/mf2-fluent/src/functions.ts @@ -1,4 +1,7 @@ -import type { MessageFunctionContext, MessageValue } from 'messageformat'; +import type { + MessageFunctionContext, + MessageValue +} from 'messageformat/functions'; import type { FluentMessageResource } from './index.js'; import { valueToMessageRef } from './message-to-fluent.js'; diff --git a/packages/mf2-icu-mf1/src/functions.ts b/packages/mf2-icu-mf1/src/functions.ts index e08a8262..f29e26c1 100644 --- a/packages/mf2-icu-mf1/src/functions.ts +++ b/packages/mf2-icu-mf1/src/functions.ts @@ -1,10 +1,10 @@ -import type { - MessageDateTime, - MessageFunctionContext, - MessageNumber, - MessageValue -} from 'messageformat'; -import { datetime } from 'messageformat/functions'; +import { + type MessageDateTime, + type MessageFunctionContext, + type MessageNumber, + type MessageValue, + datetime +} from 'messageformat/functions'; function getParam(options: Record) { if (options.param) { diff --git a/packages/mf2-messageformat/src/functions/index.ts b/packages/mf2-messageformat/src/functions/index.ts index 5a519cac..74b8f643 100644 --- a/packages/mf2-messageformat/src/functions/index.ts +++ b/packages/mf2-messageformat/src/functions/index.ts @@ -1,11 +1,17 @@ import type { MessageExpressionPart } from '../formatted-parts.js'; export type { MessageFunctionContext } from '../data-model/function-context.js'; -export { MessageDateTime, datetime } from './datetime.js'; -export { MessageFallback, fallback } from './fallback.js'; -export { MessageNumber, integer, number, ordinal, plural } from './number.js'; -export { MessageString, string } from './string.js'; -export { MessageUnknownValue, unknown } from './unknown.js'; +export { type MessageDateTime, datetime } from './datetime.js'; +export { type MessageFallback, fallback } from './fallback.js'; +export { + type MessageNumber, + integer, + number, + ordinal, + plural +} from './number.js'; +export { type MessageString, string } from './string.js'; +export { type MessageUnknownValue, unknown } from './unknown.js'; export interface MessageValue { readonly type: string; diff --git a/packages/mf2-messageformat/src/index.ts b/packages/mf2-messageformat/src/index.ts index ccb3112e..51345f37 100644 --- a/packages/mf2-messageformat/src/index.ts +++ b/packages/mf2-messageformat/src/index.ts @@ -2,7 +2,6 @@ import type * as CST from './cst/types.js'; export type { CST }; export type * from './data-model/types.js'; export type * from './formatted-parts.js'; -export type * from './functions/index.js'; export { parseCST } from './cst/parse-cst.js'; export { stringifyCST } from './cst/stringify-cst.js'; From 579501eff1daf3491880c7848840aa48335ae4a9 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 3 Jan 2024 22:46:02 +0200 Subject: [PATCH 43/44] fix(mf2): Add default onError handler (throws) for validate() --- packages/mf2-messageformat/src/data-model/validate.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/mf2-messageformat/src/data-model/validate.ts b/packages/mf2-messageformat/src/data-model/validate.ts index d6717d24..f76d30f0 100644 --- a/packages/mf2-messageformat/src/data-model/validate.ts +++ b/packages/mf2-messageformat/src/data-model/validate.ts @@ -1,4 +1,4 @@ -import type { MessageDataModelError } from '../errors.js'; +import { MessageDataModelError } from '../errors.js'; import type { Expression, Message, @@ -39,7 +39,12 @@ import { visit } from './visit.js'; */ export function validate( msg: Message, - onError: (type: MessageDataModelError['type'], node: MessageNode) => void + onError: (type: MessageDataModelError['type'], node: MessageNode) => void = ( + type, + node + ) => { + throw new MessageDataModelError(type, node); + } ) { let selectorCount = 0; let missingFallback: Expression | Variant | null = null; From e27726379ee2dfde65ac6dc0594022ddc6ab8d3a Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 7 Jan 2024 19:05:36 +0200 Subject: [PATCH 44/44] docs(mf2): Satisfy api-extractor --- packages/mf2-fluent/src/fluent-to-message.ts | 13 ++++++---- packages/mf2-fluent/src/fluent-to-resource.ts | 11 ++++---- packages/mf2-fluent/src/index.ts | 5 +++- packages/mf2-icu-mf1/src/functions.ts | 2 +- .../src/cst/stringify-cst.ts | 6 +++++ .../src/data-model/from-cst.ts | 6 +++++ .../mf2-messageformat/src/data-model/types.ts | 12 ++++++++- packages/mf2-messageformat/src/errors.ts | 26 +++++++++++++++++++ .../mf2-messageformat/src/formatted-parts.ts | 5 ++++ .../src/functions/datetime.ts | 2 ++ .../src/functions/fallback.ts | 1 + .../mf2-messageformat/src/functions/number.ts | 2 ++ .../mf2-messageformat/src/functions/string.ts | 8 ++++++ .../src/functions/unknown.ts | 3 +++ 14 files changed, 89 insertions(+), 13 deletions(-) diff --git a/packages/mf2-fluent/src/fluent-to-message.ts b/packages/mf2-fluent/src/fluent-to-message.ts index e5ca2d55..c79ed265 100644 --- a/packages/mf2-fluent/src/fluent-to-message.ts +++ b/packages/mf2-fluent/src/fluent-to-message.ts @@ -199,17 +199,22 @@ function asFluentSelect( } } +/** @beta */ +export type FluentToMessageOptions = { + /** Set `false` to disable number selector detection based on keys. */ + detectNumberSelection?: boolean; +}; + /** * Compile a {@link https://projectfluent.org/fluent.js/syntax/classes/pattern.html | Fluent.Pattern} * (i.e. the value of a Fluent message or an attribute) into a * {@link messageformat#Message} data object. * * @beta - * @param options.detectNumberSelection - Set `false` to disable number selector detection based on keys. */ export function fluentToMessage( ast: Fluent.Pattern, - options?: { detectNumberSelection?: boolean } + { detectNumberSelection }: FluentToMessageOptions = {} ): PatternMessage | SelectMessage { const args = findSelectArgs(ast); if (args.length === 0) { @@ -297,9 +302,7 @@ export function fluentToMessage( return { type: 'select', declarations: [], - selectors: args.map(arg => - asSelectExpression(arg, options?.detectNumberSelection) - ), + selectors: args.map(arg => asSelectExpression(arg, detectNumberSelection)), variants }; } diff --git a/packages/mf2-fluent/src/fluent-to-resource.ts b/packages/mf2-fluent/src/fluent-to-resource.ts index d77adbe4..ff38333e 100644 --- a/packages/mf2-fluent/src/fluent-to-resource.ts +++ b/packages/mf2-fluent/src/fluent-to-resource.ts @@ -1,6 +1,9 @@ import * as Fluent from '@fluent/syntax'; import { Message, MessageFormat, MessageFormatOptions } from 'messageformat'; -import { fluentToMessage } from './fluent-to-message.js'; +import { + FluentToMessageOptions, + fluentToMessage +} from './fluent-to-message.js'; import { getFluentFunctions } from './functions.js'; import type { FluentMessageResource, @@ -21,12 +24,11 @@ import type { * or in the shape output by {@link fluentToResourceData} as `data`. * @param locales - The locale code or codes to use for all of the resource's messages. * @param options - The MessageFormat constructor options to use for all of the resource's messages. - * @param options.detectNumberSelection - Set `false` to disable number selector detection based on keys. */ export function fluentToResource( source: string | Fluent.Resource | FluentMessageResourceData, locales?: string | string[], - options?: MessageFormatOptions & { detectNumberSelection?: boolean } + options?: MessageFormatOptions & FluentToMessageOptions ): FluentMessageResource { const res: FluentMessageResource = new Map(); @@ -59,13 +61,12 @@ export function fluentToResource( * @param source - A Fluent resource, * as the string contents of an FTL file or * as a {@link https://projectfluent.org/fluent.js/syntax/classes/resource.html | Fluent.Resource} - * @param options.detectNumberSelection - Set `false` to disable number selector detection based on keys. * @returns An object containing the messages as `data` and any resource-level * `comments` of the resource. */ export function fluentToResourceData( source: string | Fluent.Resource, - options?: { detectNumberSelection?: boolean } + options?: FluentToMessageOptions ): { data: FluentMessageResourceData; comments: string; diff --git a/packages/mf2-fluent/src/index.ts b/packages/mf2-fluent/src/index.ts index bfe9e1ae..3c273ab3 100644 --- a/packages/mf2-fluent/src/index.ts +++ b/packages/mf2-fluent/src/index.ts @@ -22,7 +22,10 @@ export type FluentMessageResource = Map>; */ export type FluentMessageResourceData = Map>; -export { fluentToMessage } from './fluent-to-message.js'; +export { + type FluentToMessageOptions, + fluentToMessage +} from './fluent-to-message.js'; export { fluentToResource, fluentToResourceData diff --git a/packages/mf2-icu-mf1/src/functions.ts b/packages/mf2-icu-mf1/src/functions.ts index f29e26c1..d03191ed 100644 --- a/packages/mf2-icu-mf1/src/functions.ts +++ b/packages/mf2-icu-mf1/src/functions.ts @@ -54,7 +54,7 @@ function time( /** * Represent a duration in seconds as a string * - * @return Includes one or two `:` separators, and matches the pattern + * @returns Includes one or two `:` separators, and matches the pattern * `hhhh:mm:ss`, possibly with a leading `-` for negative values and a * trailing `.sss` part for non-integer input */ diff --git a/packages/mf2-messageformat/src/cst/stringify-cst.ts b/packages/mf2-messageformat/src/cst/stringify-cst.ts index a773eacb..b30c70e2 100644 --- a/packages/mf2-messageformat/src/cst/stringify-cst.ts +++ b/packages/mf2-messageformat/src/cst/stringify-cst.ts @@ -1,5 +1,11 @@ import type * as CST from './types.js'; +/** + * Stringify a message CST. + * Does not perform any error checking or validation. + * + * @beta + */ export function stringifyCST(cst: CST.Message): string { let str = ''; diff --git a/packages/mf2-messageformat/src/data-model/from-cst.ts b/packages/mf2-messageformat/src/data-model/from-cst.ts index c6d59879..449ab699 100644 --- a/packages/mf2-messageformat/src/data-model/from-cst.ts +++ b/packages/mf2-messageformat/src/data-model/from-cst.ts @@ -2,6 +2,12 @@ import type * as Model from './types.js'; import { MessageSyntaxError } from '../errors.js'; import type * as CST from '../cst/types.js'; +/** + * Shared symbol used as a key on message data model nodes + * to reference their CST source. + * + * @beta + */ export const cst = Symbol.for('CST'); /** diff --git a/packages/mf2-messageformat/src/data-model/types.ts b/packages/mf2-messageformat/src/data-model/types.ts index 21f14848..2894ed35 100644 --- a/packages/mf2-messageformat/src/data-model/types.ts +++ b/packages/mf2-messageformat/src/data-model/types.ts @@ -1,7 +1,11 @@ import type * as CST from '../cst/types.js'; import { cst } from './from-cst.js'; -/** A node in a message data model */ +/** + * A node in a message data model + * + * @beta + */ export type MessageNode = | Declaration | Variant @@ -48,6 +52,7 @@ export type Declaration = | LocalDeclaration | UnsupportedStatement; +/** @beta */ export interface InputDeclaration { type: 'input'; name: string; @@ -55,6 +60,7 @@ export interface InputDeclaration { [cst]?: CST.Declaration; } +/** @beta */ export interface LocalDeclaration { type: 'local'; name: string; @@ -62,6 +68,7 @@ export interface LocalDeclaration { [cst]?: CST.Declaration; } +/** @beta */ export interface UnsupportedStatement { type: 'unsupported-statement'; keyword: string; @@ -236,6 +243,7 @@ export interface UnsupportedAnnotation { */ export type Markup = MarkupOpen | MarkupStandalone | MarkupClose; +/** @beta */ export interface MarkupOpen { type: 'markup'; kind: 'open'; @@ -244,6 +252,7 @@ export interface MarkupOpen { [cst]?: CST.Expression; } +/** @beta */ export interface MarkupStandalone { type: 'markup'; kind: 'standalone'; @@ -252,6 +261,7 @@ export interface MarkupStandalone { [cst]?: CST.Expression; } +/** @beta */ export interface MarkupClose { type: 'markup'; kind: 'close'; diff --git a/packages/mf2-messageformat/src/errors.ts b/packages/mf2-messageformat/src/errors.ts index f9c7c25f..f884444d 100644 --- a/packages/mf2-messageformat/src/errors.ts +++ b/packages/mf2-messageformat/src/errors.ts @@ -1,6 +1,11 @@ import { type MessageNode } from './data-model/types.js'; import { cst } from './data-model/from-cst.js'; +/** + * Base error class used by MessageFormat + * + * @beta + */ export class MessageError extends Error { type: | 'missing-func' @@ -15,6 +20,11 @@ export class MessageError extends Error { } } +/** + * Errors in the message syntax. + * + * @beta + */ export class MessageSyntaxError extends MessageError { declare type: | 'empty-token' @@ -68,6 +78,7 @@ export class MessageSyntaxError extends MessageError { } } +/** @beta */ export class MissingSyntaxError extends MessageSyntaxError { expected: string; @@ -77,6 +88,11 @@ export class MissingSyntaxError extends MessageSyntaxError { } } +/** + * Errors in the message data model. + * + * @beta + */ export class MessageDataModelError extends MessageSyntaxError { declare type: | 'duplicate-declaration' @@ -94,6 +110,11 @@ export class MessageDataModelError extends MessageSyntaxError { } } +/** + * Message runtime resolution errors + * + * @beta + */ export class MessageResolutionError extends MessageError { declare type: | 'bad-input' @@ -112,6 +133,11 @@ export class MessageResolutionError extends MessageError { } } +/** + * Errors in message selection. + * + * @beta + */ export class MessageSelectionError extends MessageError { declare type: 'no-match' | 'not-selectable'; constructor(type: typeof MessageSelectionError.prototype.type) { diff --git a/packages/mf2-messageformat/src/formatted-parts.ts b/packages/mf2-messageformat/src/formatted-parts.ts index bc6a8ddd..c7bc33c8 100644 --- a/packages/mf2-messageformat/src/formatted-parts.ts +++ b/packages/mf2-messageformat/src/formatted-parts.ts @@ -4,6 +4,7 @@ export type { MessageNumberPart } from './functions/number.js'; export type { MessageStringPart } from './functions/string.js'; export type { MessageUnknownPart } from './functions/unknown.js'; +/** @beta */ export interface MessageExpressionPart { type: string; source: string; @@ -12,11 +13,13 @@ export interface MessageExpressionPart { value?: unknown; } +/** @beta */ export interface MessageLiteralPart { type: 'literal'; value: string; } +/** @beta */ export interface MessageMarkupPart { type: 'markup'; kind: 'open' | 'standalone'; @@ -25,6 +28,7 @@ export interface MessageMarkupPart { options?: { [key: string]: unknown }; } +/** @beta */ export interface MessageMarkupClosePart { type: 'markup'; kind: 'close'; @@ -33,6 +37,7 @@ export interface MessageMarkupClosePart { options?: never; } +/** @beta */ export type MessagePart = | MessageExpressionPart | MessageLiteralPart diff --git a/packages/mf2-messageformat/src/functions/datetime.ts b/packages/mf2-messageformat/src/functions/datetime.ts index ce2bbfd8..d9539d8a 100644 --- a/packages/mf2-messageformat/src/functions/datetime.ts +++ b/packages/mf2-messageformat/src/functions/datetime.ts @@ -8,6 +8,7 @@ import { mergeLocales } from './utils.js'; +/** @beta */ export interface MessageDateTime extends MessageValue { readonly type: 'datetime'; readonly source: string; @@ -18,6 +19,7 @@ export interface MessageDateTime extends MessageValue { valueOf(): Date; } +/** @beta */ export interface MessageDateTimePart extends MessageExpressionPart { type: 'datetime'; source: string; diff --git a/packages/mf2-messageformat/src/functions/fallback.ts b/packages/mf2-messageformat/src/functions/fallback.ts index 0c5949b2..24a7d239 100644 --- a/packages/mf2-messageformat/src/functions/fallback.ts +++ b/packages/mf2-messageformat/src/functions/fallback.ts @@ -14,6 +14,7 @@ export interface MessageFallback extends MessageValue { toString(): string; } +/** @beta */ export interface MessageFallbackPart extends MessageExpressionPart { type: 'fallback'; source: string; diff --git a/packages/mf2-messageformat/src/functions/number.ts b/packages/mf2-messageformat/src/functions/number.ts index 8900d7eb..0676c631 100644 --- a/packages/mf2-messageformat/src/functions/number.ts +++ b/packages/mf2-messageformat/src/functions/number.ts @@ -8,6 +8,7 @@ import { mergeLocales } from './utils.js'; +/** @beta */ export interface MessageNumber extends MessageValue { readonly type: 'number'; readonly source: string; @@ -31,6 +32,7 @@ export interface MessageNumber extends MessageValue { valueOf(): number | bigint; } +/** @beta */ export interface MessageNumberPart extends MessageExpressionPart { type: 'number'; source: string; diff --git a/packages/mf2-messageformat/src/functions/string.ts b/packages/mf2-messageformat/src/functions/string.ts index 22cecf71..f65bb6f6 100644 --- a/packages/mf2-messageformat/src/functions/string.ts +++ b/packages/mf2-messageformat/src/functions/string.ts @@ -2,6 +2,7 @@ import type { MessageExpressionPart } from '../formatted-parts.js'; import type { MessageFunctionContext, MessageValue } from './index.js'; import { mergeLocales } from './utils.js'; +/** @beta */ export interface MessageString extends MessageValue { readonly type: 'string'; readonly source: string; @@ -12,6 +13,7 @@ export interface MessageString extends MessageValue { valueOf(): string; } +/** @beta */ export interface MessageStringPart extends MessageExpressionPart { type: 'string'; source: string; @@ -19,6 +21,12 @@ export interface MessageStringPart extends MessageExpressionPart { value: string; } +/** + * Accepts any input, and parses any non-string value using `String()`. + * For no input, resolves its value to an empty string. + * On error, resolves to a fallback value. + * + * @beta */ export function string( { locales, source }: Pick, options: Record, diff --git a/packages/mf2-messageformat/src/functions/unknown.ts b/packages/mf2-messageformat/src/functions/unknown.ts index 85b01131..a0d8ab6b 100644 --- a/packages/mf2-messageformat/src/functions/unknown.ts +++ b/packages/mf2-messageformat/src/functions/unknown.ts @@ -1,6 +1,7 @@ import type { MessageExpressionPart } from '../formatted-parts.js'; import type { MessageValue } from './index.js'; +/** @beta */ export interface MessageUnknownValue extends MessageValue { readonly type: 'unknown'; readonly source: string; @@ -10,12 +11,14 @@ export interface MessageUnknownValue extends MessageValue { valueOf(): unknown; } +/** @beta */ export interface MessageUnknownPart extends MessageExpressionPart { type: 'unknown'; source: string; value: unknown; } +/** @beta */ export const unknown = ( source: string, input: unknown