Something went wrong. Try again.
yet another localization library
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106import type { Model } from 'messageformat';import { MessageSyntaxError, validate, visit } from 'messageformat';import { cstKey, messageFromCST, parseCST } from 'messageformat/cst';
import { CompileError } from '../diagnostic.ts';import type { Span } from '../span.ts';
export type { Model as Mf2 } from 'messageformat';
/** thrown for syntactically or structurally invalid MF2. */export class MessageParseError extends CompileError { /** * @param error the error reported by `messageformat` * @param source the parser input */ constructor(error: MessageSyntaxError, source: string) { const { label, message } = describe(error, source); super(message, { cause: error, label, span: error.start >= 0 ? { end: error.end, start: error.start } : undefined, }); this.name = 'MessageParseError'; }}
// `messageformat` uses error codes as messages.const DESCRIPTIONS: Record<MessageSyntaxError['type'], string> = { 'bad-escape': 'invalid escape sequence', 'bad-input-expression': '.input must declare a variable', 'duplicate-attribute': 'markup attribute given more than once', 'duplicate-declaration': 'variable declared more than once, or declared after its first use', 'duplicate-option-name': 'option given more than once', 'duplicate-variant': 'variant keys repeat an earlier variant', 'empty-token': 'expected a token', 'extra-content': 'unexpected content after the message', 'key-mismatch': 'variant key count differs from the selector count', 'missing-fallback': 'no catch-all variant with only * keys', 'missing-selector-annotation': 'selector has no function annotation', 'missing-syntax': 'missing required syntax', 'parse-error': 'syntax error',};
const describe = (error: MessageSyntaxError, source: string): { label?: string; message: string } => { if (error.type === 'missing-syntax') { // the expected token is only exposed through the error message. const token = /^Missing (.+?)(?: at -?\d+)?$/s.exec(error.message)?.[1]; if (token !== undefined) { const whitespace = token.trim() === '' || token === "' '"; const expected = whitespace ? 'expected whitespace' : `expected \`${token}\``; // at EOF, the expected token may be just the first alternative the parser tried. if (error.start >= source.length) { return { label: whitespace ? undefined : expected, message: 'unexpected end of message' }; } return { message: expected }; } } return { message: DESCRIPTIONS[error.type] };};
/** a data model node that may carry its source position. */export type Spanned = { readonly [cstKey]?: Span };
/** * locate a data model node within its message source. * @param node a node from a message returned by {@link parseMessage} * @returns the node's span, or `undefined` when it was not parsed from source */export const spanOf = (node: Spanned): Span | undefined => { const cst = node[cstKey]; return cst === undefined ? undefined : { end: cst.end, start: cst.start };};
const withoutUnicodeOptions = (options: Model.Options | undefined): Model.Options | undefined => options === undefined ? undefined : Object.fromEntries(Object.entries(options).filter(([name]) => !name.startsWith('u:')));
/** * parse and validate an MF2 message. * @param source the message source * @returns the data model with unsupported reserved `u:` options removed; use {@link spanOf} * for node locations * @throws MessageParseError when the message is invalid, with a span relative to `source` */export const parseMessage = (source: string): Model.Message => { try { const message = messageFromCST(parseCST(source)); validate(message); visit(message, { functionRef(fn) { fn.options = withoutUnicodeOptions(fn.options); }, markup(markup) { markup.options = withoutUnicodeOptions(markup.options); }, }); return message; } catch (error) { if (error instanceof MessageSyntaxError) { throw new MessageParseError(error, source); } throw error; }};