зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1513958 - Update Fluent.jsm to version 0.10.0. r=stas
Differential Revision: https://phabricator.services.mozilla.com/D14612 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
566c8a0523
Коммит
65756bf552
|
@ -16,7 +16,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/* fluent@fa25466f (October 12, 2018) */
|
/* fluent@0.10.0 */
|
||||||
|
|
||||||
/* global Intl */
|
/* global Intl */
|
||||||
|
|
||||||
|
@ -195,20 +195,7 @@ const FSI = "\u2068";
|
||||||
const PDI = "\u2069";
|
const PDI = "\u2069";
|
||||||
|
|
||||||
|
|
||||||
/**
|
// Helper: match a variant key to the given selector.
|
||||||
* Helper for matching a variant key to the given selector.
|
|
||||||
*
|
|
||||||
* Used in SelectExpressions and VariantExpressions.
|
|
||||||
*
|
|
||||||
* @param {FluentBundle} bundle
|
|
||||||
* Resolver environment object.
|
|
||||||
* @param {FluentType} key
|
|
||||||
* The key of the currently considered variant.
|
|
||||||
* @param {FluentType} selector
|
|
||||||
* The selector based om which the correct variant should be chosen.
|
|
||||||
* @returns {FluentType}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function match(bundle, selector, key) {
|
function match(bundle, selector, key) {
|
||||||
if (key === selector) {
|
if (key === selector) {
|
||||||
// Both are strings.
|
// Both are strings.
|
||||||
|
@ -233,23 +220,10 @@ function match(bundle, selector, key) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Helper: resolve the default variant from a list of variants.
|
||||||
* Helper for choosing the default value from a set of members.
|
function getDefault(env, variants, star) {
|
||||||
*
|
if (variants[star]) {
|
||||||
* Used in SelectExpressions and Type.
|
return Type(env, variants[star]);
|
||||||
*
|
|
||||||
* @param {Object} env
|
|
||||||
* Resolver environment object.
|
|
||||||
* @param {Object} members
|
|
||||||
* Hash map of variants from which the default value is to be selected.
|
|
||||||
* @param {Number} star
|
|
||||||
* The index of the default variant.
|
|
||||||
* @returns {FluentType}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function DefaultMember(env, members, star) {
|
|
||||||
if (members[star]) {
|
|
||||||
return members[star];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { errors } = env;
|
const { errors } = env;
|
||||||
|
@ -257,176 +231,34 @@ function DefaultMember(env, members, star) {
|
||||||
return new FluentNone();
|
return new FluentNone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper: resolve arguments to a call expression.
|
||||||
|
function getArguments(env, args) {
|
||||||
|
const positional = [];
|
||||||
|
const named = {};
|
||||||
|
|
||||||
/**
|
if (args) {
|
||||||
* Resolve a reference to another message.
|
for (const arg of args) {
|
||||||
*
|
if (arg.type === "narg") {
|
||||||
* @param {Object} env
|
named[arg.name] = Type(env, arg.value);
|
||||||
* Resolver environment object.
|
} else {
|
||||||
* @param {Object} id
|
positional.push(Type(env, arg));
|
||||||
* The identifier of the message to be resolved.
|
|
||||||
* @param {String} id.name
|
|
||||||
* The name of the identifier.
|
|
||||||
* @returns {FluentType}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function MessageReference(env, {name}) {
|
|
||||||
const { bundle, errors } = env;
|
|
||||||
const message = name.startsWith("-")
|
|
||||||
? bundle._terms.get(name)
|
|
||||||
: bundle._messages.get(name);
|
|
||||||
|
|
||||||
if (!message) {
|
|
||||||
const err = name.startsWith("-")
|
|
||||||
? new ReferenceError(`Unknown term: ${name}`)
|
|
||||||
: new ReferenceError(`Unknown message: ${name}`);
|
|
||||||
errors.push(err);
|
|
||||||
return new FluentNone(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a variant expression to the variant object.
|
|
||||||
*
|
|
||||||
* @param {Object} env
|
|
||||||
* Resolver environment object.
|
|
||||||
* @param {Object} expr
|
|
||||||
* An expression to be resolved.
|
|
||||||
* @param {Object} expr.ref
|
|
||||||
* An Identifier of a message for which the variant is resolved.
|
|
||||||
* @param {Object} expr.id.name
|
|
||||||
* Name a message for which the variant is resolved.
|
|
||||||
* @param {Object} expr.key
|
|
||||||
* Variant key to be resolved.
|
|
||||||
* @returns {FluentType}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function VariantExpression(env, {ref, selector}) {
|
|
||||||
const message = MessageReference(env, ref);
|
|
||||||
if (message instanceof FluentNone) {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { bundle, errors } = env;
|
|
||||||
const sel = Type(env, selector);
|
|
||||||
const value = message.value || message;
|
|
||||||
|
|
||||||
function isVariantList(node) {
|
|
||||||
return Array.isArray(node) &&
|
|
||||||
node[0].type === "select" &&
|
|
||||||
node[0].selector === null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isVariantList(value)) {
|
|
||||||
// Match the specified key against keys of each variant, in order.
|
|
||||||
for (const variant of value[0].variants) {
|
|
||||||
const key = Type(env, variant.key);
|
|
||||||
if (match(env.bundle, sel, key)) {
|
|
||||||
return variant;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
errors.push(
|
return [positional, named];
|
||||||
new ReferenceError(`Unknown variant: ${sel.toString(bundle)}`));
|
|
||||||
return Type(env, message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve an expression to a Fluent type.
|
||||||
/**
|
|
||||||
* Resolve an attribute expression to the attribute object.
|
|
||||||
*
|
|
||||||
* @param {Object} env
|
|
||||||
* Resolver environment object.
|
|
||||||
* @param {Object} expr
|
|
||||||
* An expression to be resolved.
|
|
||||||
* @param {String} expr.ref
|
|
||||||
* An ID of a message for which the attribute is resolved.
|
|
||||||
* @param {String} expr.name
|
|
||||||
* Name of the attribute to be resolved.
|
|
||||||
* @returns {FluentType}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function AttributeExpression(env, {ref, name}) {
|
|
||||||
const message = MessageReference(env, ref);
|
|
||||||
if (message instanceof FluentNone) {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.attrs) {
|
|
||||||
// Match the specified name against keys of each attribute.
|
|
||||||
for (const attrName in message.attrs) {
|
|
||||||
if (name === attrName) {
|
|
||||||
return message.attrs[name];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { errors } = env;
|
|
||||||
errors.push(new ReferenceError(`Unknown attribute: ${name}`));
|
|
||||||
return Type(env, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a select expression to the member object.
|
|
||||||
*
|
|
||||||
* @param {Object} env
|
|
||||||
* Resolver environment object.
|
|
||||||
* @param {Object} expr
|
|
||||||
* An expression to be resolved.
|
|
||||||
* @param {String} expr.selector
|
|
||||||
* Selector expression
|
|
||||||
* @param {Array} expr.variants
|
|
||||||
* List of variants for the select expression.
|
|
||||||
* @param {Number} expr.star
|
|
||||||
* Index of the default variant.
|
|
||||||
* @returns {FluentType}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function SelectExpression(env, {selector, variants, star}) {
|
|
||||||
if (selector === null) {
|
|
||||||
return DefaultMember(env, variants, star);
|
|
||||||
}
|
|
||||||
|
|
||||||
let sel = Type(env, selector);
|
|
||||||
if (sel instanceof FluentNone) {
|
|
||||||
return DefaultMember(env, variants, star);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match the selector against keys of each variant, in order.
|
|
||||||
for (const variant of variants) {
|
|
||||||
const key = Type(env, variant.key);
|
|
||||||
if (match(env.bundle, sel, key)) {
|
|
||||||
return variant;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return DefaultMember(env, variants, star);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve expression to a Fluent type.
|
|
||||||
*
|
|
||||||
* JavaScript strings are a special case. Since they natively have the
|
|
||||||
* `toString` method they can be used as if they were a Fluent type without
|
|
||||||
* paying the cost of creating a instance of one.
|
|
||||||
*
|
|
||||||
* @param {Object} env
|
|
||||||
* Resolver environment object.
|
|
||||||
* @param {Object} expr
|
|
||||||
* An expression object to be resolved into a Fluent type.
|
|
||||||
* @returns {FluentType}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function Type(env, expr) {
|
function Type(env, expr) {
|
||||||
// A fast-path for strings which are the most common case, and for
|
// A fast-path for strings which are the most common case. Since they
|
||||||
// `FluentNone` which doesn't require any additional logic.
|
// natively have the `toString` method they can be used as if they were
|
||||||
|
// a FluentType instance without incurring the cost of creating one.
|
||||||
if (typeof expr === "string") {
|
if (typeof expr === "string") {
|
||||||
return env.bundle._transform(expr);
|
return env.bundle._transform(expr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A fast-path for `FluentNone` which doesn't require any additional logic.
|
||||||
if (expr instanceof FluentNone) {
|
if (expr instanceof FluentNone) {
|
||||||
return expr;
|
return expr;
|
||||||
}
|
}
|
||||||
|
@ -437,32 +269,21 @@ function Type(env, expr) {
|
||||||
return Pattern(env, expr);
|
return Pattern(env, expr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
switch (expr.type) {
|
switch (expr.type) {
|
||||||
|
case "str":
|
||||||
|
return expr.value;
|
||||||
case "num":
|
case "num":
|
||||||
return new FluentNumber(expr.value);
|
return new FluentNumber(expr.value);
|
||||||
case "var":
|
case "var":
|
||||||
return VariableReference(env, expr);
|
return VariableReference(env, expr);
|
||||||
case "func":
|
case "term":
|
||||||
return FunctionReference(env, expr);
|
return TermReference({...env, args: {}}, expr);
|
||||||
case "call":
|
case "ref":
|
||||||
return CallExpression(env, expr);
|
return expr.args
|
||||||
case "ref": {
|
? FunctionReference(env, expr)
|
||||||
const message = MessageReference(env, expr);
|
: MessageReference(env, expr);
|
||||||
return Type(env, message);
|
case "select":
|
||||||
}
|
return SelectExpression(env, expr);
|
||||||
case "getattr": {
|
|
||||||
const attr = AttributeExpression(env, expr);
|
|
||||||
return Type(env, attr);
|
|
||||||
}
|
|
||||||
case "getvar": {
|
|
||||||
const variant = VariantExpression(env, expr);
|
|
||||||
return Type(env, variant);
|
|
||||||
}
|
|
||||||
case "select": {
|
|
||||||
const member = SelectExpression(env, expr);
|
|
||||||
return Type(env, member);
|
|
||||||
}
|
|
||||||
case undefined: {
|
case undefined: {
|
||||||
// If it's a node with a value, resolve the value.
|
// If it's a node with a value, resolve the value.
|
||||||
if (expr.value !== null && expr.value !== undefined) {
|
if (expr.value !== null && expr.value !== undefined) {
|
||||||
|
@ -478,24 +299,13 @@ function Type(env, expr) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Resolve a reference to a variable.
|
||||||
* Resolve a reference to a variable.
|
|
||||||
*
|
|
||||||
* @param {Object} env
|
|
||||||
* Resolver environment object.
|
|
||||||
* @param {Object} expr
|
|
||||||
* An expression to be resolved.
|
|
||||||
* @param {String} expr.name
|
|
||||||
* Name of an argument to be returned.
|
|
||||||
* @returns {FluentType}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function VariableReference(env, {name}) {
|
function VariableReference(env, {name}) {
|
||||||
const { args, errors } = env;
|
const { args, errors } = env;
|
||||||
|
|
||||||
if (!args || !args.hasOwnProperty(name)) {
|
if (!args || !args.hasOwnProperty(name)) {
|
||||||
errors.push(new ReferenceError(`Unknown variable: ${name}`));
|
errors.push(new ReferenceError(`Unknown variable: ${name}`));
|
||||||
return new FluentNone(name);
|
return new FluentNone(`$${name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const arg = args[name];
|
const arg = args[name];
|
||||||
|
@ -519,26 +329,80 @@ function VariableReference(env, {name}) {
|
||||||
errors.push(
|
errors.push(
|
||||||
new TypeError(`Unsupported variable type: ${name}, ${typeof arg}`)
|
new TypeError(`Unsupported variable type: ${name}, ${typeof arg}`)
|
||||||
);
|
);
|
||||||
return new FluentNone(name);
|
return new FluentNone(`$${name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Resolve a reference to another message.
|
||||||
* Resolve a reference to a function.
|
function MessageReference(env, {name, attr}) {
|
||||||
*
|
const {bundle, errors} = env;
|
||||||
* @param {Object} env
|
const message = bundle._messages.get(name);
|
||||||
* Resolver environment object.
|
if (!message) {
|
||||||
* @param {Object} expr
|
const err = new ReferenceError(`Unknown message: ${name}`);
|
||||||
* An expression to be resolved.
|
errors.push(err);
|
||||||
* @param {String} expr.name
|
return new FluentNone(name);
|
||||||
* Name of the function to be returned.
|
}
|
||||||
* @returns {Function}
|
|
||||||
* @private
|
if (attr) {
|
||||||
*/
|
const attribute = message.attrs && message.attrs[attr];
|
||||||
function FunctionReference(env, {name}) {
|
if (attribute) {
|
||||||
// Some functions are built-in. Others may be provided by the runtime via
|
return Type(env, attribute);
|
||||||
|
}
|
||||||
|
errors.push(new ReferenceError(`Unknown attribute: ${attr}`));
|
||||||
|
return Type(env, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Type(env, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve a call to a Term with key-value arguments.
|
||||||
|
function TermReference(env, {name, attr, selector, args}) {
|
||||||
|
const {bundle, errors} = env;
|
||||||
|
|
||||||
|
const id = `-${name}`;
|
||||||
|
const term = bundle._terms.get(id);
|
||||||
|
if (!term) {
|
||||||
|
const err = new ReferenceError(`Unknown term: ${id}`);
|
||||||
|
errors.push(err);
|
||||||
|
return new FluentNone(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every TermReference has its own args.
|
||||||
|
const [, keyargs] = getArguments(env, args);
|
||||||
|
const local = {...env, args: keyargs};
|
||||||
|
|
||||||
|
if (attr) {
|
||||||
|
const attribute = term.attrs && term.attrs[attr];
|
||||||
|
if (attribute) {
|
||||||
|
return Type(local, attribute);
|
||||||
|
}
|
||||||
|
errors.push(new ReferenceError(`Unknown attribute: ${attr}`));
|
||||||
|
return Type(local, term);
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantList = getVariantList(term);
|
||||||
|
if (selector && variantList) {
|
||||||
|
return SelectExpression(local, {...variantList, selector});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Type(local, term);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: convert a value into a variant list, if possible.
|
||||||
|
function getVariantList(term) {
|
||||||
|
const value = term.value || term;
|
||||||
|
return Array.isArray(value)
|
||||||
|
&& value[0].type === "select"
|
||||||
|
&& value[0].selector === null
|
||||||
|
? value[0]
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve a call to a Function with positional and key-value arguments.
|
||||||
|
function FunctionReference(env, {name, args}) {
|
||||||
|
// Some functions are built-in. Others may be provided by the runtime via
|
||||||
// the `FluentBundle` constructor.
|
// the `FluentBundle` constructor.
|
||||||
const { bundle: { _functions }, errors } = env;
|
const {bundle: {_functions}, errors} = env;
|
||||||
const func = _functions[name] || builtins[name];
|
const func = _functions[name] || builtins[name];
|
||||||
|
|
||||||
if (!func) {
|
if (!func) {
|
||||||
|
@ -551,59 +415,39 @@ function FunctionReference(env, {name}) {
|
||||||
return new FluentNone(`${name}()`);
|
return new FluentNone(`${name}()`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return func;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a call to a Function with positional and key-value arguments.
|
|
||||||
*
|
|
||||||
* @param {Object} env
|
|
||||||
* Resolver environment object.
|
|
||||||
* @param {Object} expr
|
|
||||||
* An expression to be resolved.
|
|
||||||
* @param {Object} expr.callee
|
|
||||||
* FTL Function object.
|
|
||||||
* @param {Array} expr.args
|
|
||||||
* FTL Function argument list.
|
|
||||||
* @returns {FluentType}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function CallExpression(env, {callee, args}) {
|
|
||||||
const func = FunctionReference(env, callee);
|
|
||||||
|
|
||||||
if (func instanceof FluentNone) {
|
|
||||||
return func;
|
|
||||||
}
|
|
||||||
|
|
||||||
const posargs = [];
|
|
||||||
const keyargs = {};
|
|
||||||
|
|
||||||
for (const arg of args) {
|
|
||||||
if (arg.type === "narg") {
|
|
||||||
keyargs[arg.name] = Type(env, arg.value);
|
|
||||||
} else {
|
|
||||||
posargs.push(Type(env, arg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return func(posargs, keyargs);
|
return func(...getArguments(env, args));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// XXX Report errors.
|
// XXX Report errors.
|
||||||
return new FluentNone();
|
return new FluentNone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Resolve a select expression to the member object.
|
||||||
* Resolve a pattern (a complex string with placeables).
|
function SelectExpression(env, {selector, variants, star}) {
|
||||||
*
|
if (selector === null) {
|
||||||
* @param {Object} env
|
return getDefault(env, variants, star);
|
||||||
* Resolver environment object.
|
}
|
||||||
* @param {Array} ptn
|
|
||||||
* Array of pattern elements.
|
let sel = Type(env, selector);
|
||||||
* @returns {Array}
|
if (sel instanceof FluentNone) {
|
||||||
* @private
|
const variant = getDefault(env, variants, star);
|
||||||
*/
|
return Type(env, variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match the selector against keys of each variant, in order.
|
||||||
|
for (const variant of variants) {
|
||||||
|
const key = Type(env, variant.key);
|
||||||
|
if (match(env.bundle, sel, key)) {
|
||||||
|
return Type(env, variant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const variant = getDefault(env, variants, star);
|
||||||
|
return Type(env, variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve a pattern (a complex string with placeables).
|
||||||
function Pattern(env, ptn) {
|
function Pattern(env, ptn) {
|
||||||
const { bundle, dirty, errors } = env;
|
const { bundle, dirty, errors } = env;
|
||||||
|
|
||||||
|
@ -679,45 +523,44 @@ class FluentError extends Error {}
|
||||||
|
|
||||||
// This regex is used to iterate through the beginnings of messages and terms.
|
// This regex is used to iterate through the beginnings of messages and terms.
|
||||||
// With the /m flag, the ^ matches at the beginning of every line.
|
// With the /m flag, the ^ matches at the beginning of every line.
|
||||||
const RE_MESSAGE_START = /^(-?[a-zA-Z][a-zA-Z0-9_-]*) *= */mg;
|
const RE_MESSAGE_START = /^(-?[a-zA-Z][\w-]*) *= */mg;
|
||||||
|
|
||||||
// Both Attributes and Variants are parsed in while loops. These regexes are
|
// Both Attributes and Variants are parsed in while loops. These regexes are
|
||||||
// used to break out of them.
|
// used to break out of them.
|
||||||
const RE_ATTRIBUTE_START = /\.([a-zA-Z][a-zA-Z0-9_-]*) *= */y;
|
const RE_ATTRIBUTE_START = /\.([a-zA-Z][\w-]*) *= */y;
|
||||||
// [^] matches all characters, including newlines.
|
const RE_VARIANT_START = /\*?\[/y;
|
||||||
// XXX Use /s (dotall) when it's widely supported.
|
|
||||||
const RE_VARIANT_START = /\*?\[[^]*?] */y;
|
|
||||||
|
|
||||||
const RE_IDENTIFIER = /(-?[a-zA-Z][a-zA-Z0-9_-]*)/y;
|
|
||||||
const RE_NUMBER_LITERAL = /(-?[0-9]+(\.[0-9]+)?)/y;
|
const RE_NUMBER_LITERAL = /(-?[0-9]+(\.[0-9]+)?)/y;
|
||||||
|
const RE_IDENTIFIER = /([a-zA-Z][\w-]*)/y;
|
||||||
|
const RE_REFERENCE = /([$-])?([a-zA-Z][\w-]*)(?:\.([a-zA-Z][\w-]*))?/y;
|
||||||
|
|
||||||
// A "run" is a sequence of text or string literal characters which don't
|
// A "run" is a sequence of text or string literal characters which don't
|
||||||
// require any special handling. For TextElements such special characters are:
|
// require any special handling. For TextElements such special characters are: {
|
||||||
// { (starts a placeable), \ (starts an escape sequence), and line breaks which
|
// (starts a placeable), and line breaks which require additional logic to check
|
||||||
// require additional logic to check if the next line is indented. For
|
// if the next line is indented. For StringLiterals they are: \ (starts an
|
||||||
// StringLiterals they are: \ (starts an escape sequence), " (ends the
|
// escape sequence), " (ends the literal), and line breaks which are not allowed
|
||||||
// literal), and line breaks which are not allowed in StringLiterals. Also note
|
// in StringLiterals. Note that string runs may be empty; text runs may not.
|
||||||
// that string runs may be empty, but text runs may not.
|
const RE_TEXT_RUN = /([^{}\n\r]+)/y;
|
||||||
const RE_TEXT_RUN = /([^\\{\n\r]+)/y;
|
|
||||||
const RE_STRING_RUN = /([^\\"\n\r]*)/y;
|
const RE_STRING_RUN = /([^\\"\n\r]*)/y;
|
||||||
|
|
||||||
// Escape sequences.
|
// Escape sequences.
|
||||||
const RE_UNICODE_ESCAPE = /\\u([a-fA-F0-9]{4})/y;
|
|
||||||
const RE_STRING_ESCAPE = /\\([\\"])/y;
|
const RE_STRING_ESCAPE = /\\([\\"])/y;
|
||||||
const RE_TEXT_ESCAPE = /\\([\\{])/y;
|
const RE_UNICODE_ESCAPE = /\\u([a-fA-F0-9]{4})|\\U([a-fA-F0-9]{6})/y;
|
||||||
|
|
||||||
// Used for trimming TextElements and indents. With the /m flag, the $ matches
|
// Used for trimming TextElements and indents.
|
||||||
// the end of every line.
|
const RE_LEADING_NEWLINES = /^\n+/;
|
||||||
const RE_TRAILING_SPACES = / +$/mg;
|
const RE_TRAILING_SPACES = / +$/;
|
||||||
// CRLFs are normalized to LF.
|
// Used in makeIndent to strip spaces from blank lines and normalize CRLF to LF.
|
||||||
const RE_CRLF = /\r\n/g;
|
const RE_BLANK_LINES = / *\r?\n/g;
|
||||||
|
// Used in makeIndent to measure the indentation.
|
||||||
|
const RE_INDENT = /( *)$/;
|
||||||
|
|
||||||
// Common tokens.
|
// Common tokens.
|
||||||
const TOKEN_BRACE_OPEN = /{\s*/y;
|
const TOKEN_BRACE_OPEN = /{\s*/y;
|
||||||
const TOKEN_BRACE_CLOSE = /\s*}/y;
|
const TOKEN_BRACE_CLOSE = /\s*}/y;
|
||||||
const TOKEN_BRACKET_OPEN = /\[\s*/y;
|
const TOKEN_BRACKET_OPEN = /\[\s*/y;
|
||||||
const TOKEN_BRACKET_CLOSE = /\s*]/y;
|
const TOKEN_BRACKET_CLOSE = /\s*] */y;
|
||||||
const TOKEN_PAREN_OPEN = /\(\s*/y;
|
const TOKEN_PAREN_OPEN = /\s*\(\s*/y;
|
||||||
const TOKEN_ARROW = /\s*->\s*/y;
|
const TOKEN_ARROW = /\s*->\s*/y;
|
||||||
const TOKEN_COLON = /\s*:\s*/y;
|
const TOKEN_COLON = /\s*:\s*/y;
|
||||||
// Note the optional comma. As a deviation from the Fluent EBNF, the parser
|
// Note the optional comma. As a deviation from the Fluent EBNF, the parser
|
||||||
|
@ -811,7 +654,7 @@ class FluentResource extends Map {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute a regex, advance the cursor, and return the capture group.
|
// Execute a regex, advance the cursor, and return all capture groups.
|
||||||
function match(re) {
|
function match(re) {
|
||||||
re.lastIndex = cursor;
|
re.lastIndex = cursor;
|
||||||
let result = re.exec(source);
|
let result = re.exec(source);
|
||||||
|
@ -819,7 +662,12 @@ class FluentResource extends Map {
|
||||||
throw new FluentError(`Expected ${re.toString()}`);
|
throw new FluentError(`Expected ${re.toString()}`);
|
||||||
}
|
}
|
||||||
cursor = re.lastIndex;
|
cursor = re.lastIndex;
|
||||||
return result[1];
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute a regex, advance the cursor, and return the capture group.
|
||||||
|
function match1(re) {
|
||||||
|
return match(re)[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseMessage() {
|
function parseMessage() {
|
||||||
|
@ -827,6 +675,9 @@ class FluentResource extends Map {
|
||||||
let attrs = parseAttributes();
|
let attrs = parseAttributes();
|
||||||
|
|
||||||
if (attrs === null) {
|
if (attrs === null) {
|
||||||
|
if (value === null) {
|
||||||
|
throw new FluentError("Expected message value or attributes");
|
||||||
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -835,67 +686,62 @@ class FluentResource extends Map {
|
||||||
|
|
||||||
function parseAttributes() {
|
function parseAttributes() {
|
||||||
let attrs = {};
|
let attrs = {};
|
||||||
let hasAttributes = false;
|
|
||||||
|
|
||||||
while (test(RE_ATTRIBUTE_START)) {
|
while (test(RE_ATTRIBUTE_START)) {
|
||||||
if (!hasAttributes) {
|
let name = match1(RE_ATTRIBUTE_START);
|
||||||
hasAttributes = true;
|
let value = parsePattern();
|
||||||
|
if (value === null) {
|
||||||
|
throw new FluentError("Expected attribute value");
|
||||||
}
|
}
|
||||||
|
attrs[name] = value;
|
||||||
let name = match(RE_ATTRIBUTE_START);
|
|
||||||
attrs[name] = parsePattern();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasAttributes ? attrs : null;
|
return Object.keys(attrs).length > 0 ? attrs : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePattern() {
|
function parsePattern() {
|
||||||
// First try to parse any simple text on the same line as the id.
|
// First try to parse any simple text on the same line as the id.
|
||||||
if (test(RE_TEXT_RUN)) {
|
if (test(RE_TEXT_RUN)) {
|
||||||
var first = match(RE_TEXT_RUN);
|
var first = match1(RE_TEXT_RUN);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's a backslash escape or a placeable on the first line, fall
|
// If there's a placeable on the first line, parse a complex pattern.
|
||||||
// back to parsing a complex pattern.
|
if (source[cursor] === "{" || source[cursor] === "}") {
|
||||||
switch (source[cursor]) {
|
// Re-use the text parsed above, if possible.
|
||||||
case "{":
|
return parsePatternElements(first ? [first] : [], Infinity);
|
||||||
case "\\":
|
|
||||||
return first
|
|
||||||
// Re-use the text parsed above, if possible.
|
|
||||||
? parsePatternElements(first)
|
|
||||||
: parsePatternElements();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RE_TEXT_VALUE stops at newlines. Only continue parsing the pattern if
|
// RE_TEXT_VALUE stops at newlines. Only continue parsing the pattern if
|
||||||
// what comes after the newline is indented.
|
// what comes after the newline is indented.
|
||||||
let indent = parseIndent();
|
let indent = parseIndent();
|
||||||
if (indent) {
|
if (indent) {
|
||||||
return first
|
if (first) {
|
||||||
// If there's text on the first line, the blank block is part of the
|
// If there's text on the first line, the blank block is part of the
|
||||||
// translation content.
|
// translation content in its entirety.
|
||||||
? parsePatternElements(first, trim(indent))
|
return parsePatternElements([first, indent], indent.length);
|
||||||
// Otherwise, we're dealing with a block pattern. The blank block is
|
}
|
||||||
// the leading whitespace; discard it.
|
// Otherwise, we're dealing with a block pattern, i.e. a pattern which
|
||||||
: parsePatternElements();
|
// starts on a new line. Discrad the leading newlines but keep the
|
||||||
|
// inline indent; it will be used by the dedentation logic.
|
||||||
|
indent.value = trim(indent.value, RE_LEADING_NEWLINES);
|
||||||
|
return parsePatternElements([indent], indent.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (first) {
|
if (first) {
|
||||||
// It was just a simple inline text after all.
|
// It was just a simple inline text after all.
|
||||||
return trim(first);
|
return trim(first, RE_TRAILING_SPACES);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse a complex pattern as an array of elements.
|
// Parse a complex pattern as an array of elements.
|
||||||
function parsePatternElements(...elements) {
|
function parsePatternElements(elements = [], commonIndent) {
|
||||||
let placeableCount = 0;
|
let placeableCount = 0;
|
||||||
let needsTrimming = false;
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (test(RE_TEXT_RUN)) {
|
if (test(RE_TEXT_RUN)) {
|
||||||
elements.push(match(RE_TEXT_RUN));
|
elements.push(match1(RE_TEXT_RUN));
|
||||||
needsTrimming = true;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -904,35 +750,43 @@ class FluentResource extends Map {
|
||||||
throw new FluentError("Too many placeables");
|
throw new FluentError("Too many placeables");
|
||||||
}
|
}
|
||||||
elements.push(parsePlaceable());
|
elements.push(parsePlaceable());
|
||||||
needsTrimming = false;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (source[cursor] === "}") {
|
||||||
|
throw new FluentError("Unbalanced closing brace");
|
||||||
|
}
|
||||||
|
|
||||||
let indent = parseIndent();
|
let indent = parseIndent();
|
||||||
if (indent) {
|
if (indent) {
|
||||||
elements.push(trim(indent));
|
elements.push(indent);
|
||||||
needsTrimming = false;
|
commonIndent = Math.min(commonIndent, indent.length);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source[cursor] === "\\") {
|
|
||||||
elements.push(parseEscapeSequence(RE_TEXT_ESCAPE));
|
|
||||||
needsTrimming = false;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsTrimming) {
|
let lastIndex = elements.length - 1;
|
||||||
// Trim the trailing whitespace of the last element if it's a
|
// Trim the trailing spaces in the last element if it's a TextElement.
|
||||||
// TextElement. Use a flag rather than a typeof check to tell
|
if (typeof elements[lastIndex] === "string") {
|
||||||
// TextElements and StringLiterals apart (both are strings).
|
elements[lastIndex] = trim(elements[lastIndex], RE_TRAILING_SPACES);
|
||||||
let lastIndex = elements.length - 1;
|
|
||||||
elements[lastIndex] = trim(elements[lastIndex]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return elements;
|
let baked = [];
|
||||||
|
for (let element of elements) {
|
||||||
|
if (element.type === "indent") {
|
||||||
|
// Dedent indented lines by the maximum common indent.
|
||||||
|
element = element.value.slice(0, element.value.length - commonIndent);
|
||||||
|
} else if (element.type === "str") {
|
||||||
|
// Optimize StringLiterals into their value.
|
||||||
|
element = element.value;
|
||||||
|
}
|
||||||
|
if (element) {
|
||||||
|
baked.push(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return baked;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePlaceable() {
|
function parsePlaceable() {
|
||||||
|
@ -965,28 +819,20 @@ class FluentResource extends Map {
|
||||||
return parsePlaceable();
|
return parsePlaceable();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (consumeChar("$")) {
|
if (test(RE_REFERENCE)) {
|
||||||
return {type: "var", name: match(RE_IDENTIFIER)};
|
let [, sigil, name, attr = null] = match(RE_REFERENCE);
|
||||||
}
|
let type = {"$": "var", "-": "term"}[sigil] || "ref";
|
||||||
|
|
||||||
if (test(RE_IDENTIFIER)) {
|
|
||||||
let ref = {type: "ref", name: match(RE_IDENTIFIER)};
|
|
||||||
|
|
||||||
if (consumeChar(".")) {
|
|
||||||
let name = match(RE_IDENTIFIER);
|
|
||||||
return {type: "getattr", ref, name};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source[cursor] === "[") {
|
if (source[cursor] === "[") {
|
||||||
return {type: "getvar", ref, selector: parseVariantKey()};
|
// DEPRECATED VariantExpressions will be removed before 1.0.
|
||||||
|
return {type, name, selector: parseVariantKey()};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (consumeToken(TOKEN_PAREN_OPEN)) {
|
if (consumeToken(TOKEN_PAREN_OPEN)) {
|
||||||
let callee = {...ref, type: "func"};
|
return {type, name, attr, args: parseArguments()};
|
||||||
return {type: "call", callee, args: parseArguments()};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ref;
|
return {type, name, attr, args: null};
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseLiteral();
|
return parseLiteral();
|
||||||
|
@ -1035,18 +881,29 @@ class FluentResource extends Map {
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = parseVariantKey();
|
let key = parseVariantKey();
|
||||||
cursor = RE_VARIANT_START.lastIndex;
|
let value = parsePattern();
|
||||||
variants[count++] = {key, value: parsePattern()};
|
if (value === null) {
|
||||||
|
throw new FluentError("Expected variant value");
|
||||||
|
}
|
||||||
|
variants[count++] = {key, value};
|
||||||
}
|
}
|
||||||
|
|
||||||
return count > 0 ? {variants, star} : null;
|
if (count === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (star === undefined) {
|
||||||
|
throw new FluentError("Expected default variant");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {variants, star};
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseVariantKey() {
|
function parseVariantKey() {
|
||||||
consumeToken(TOKEN_BRACKET_OPEN, FluentError);
|
consumeToken(TOKEN_BRACKET_OPEN, FluentError);
|
||||||
let key = test(RE_NUMBER_LITERAL)
|
let key = test(RE_NUMBER_LITERAL)
|
||||||
? parseNumberLiteral()
|
? parseNumberLiteral()
|
||||||
: match(RE_IDENTIFIER);
|
: match1(RE_IDENTIFIER);
|
||||||
consumeToken(TOKEN_BRACKET_CLOSE, FluentError);
|
consumeToken(TOKEN_BRACKET_CLOSE, FluentError);
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
@ -1064,22 +921,22 @@ class FluentResource extends Map {
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseNumberLiteral() {
|
function parseNumberLiteral() {
|
||||||
return {type: "num", value: match(RE_NUMBER_LITERAL)};
|
return {type: "num", value: match1(RE_NUMBER_LITERAL)};
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStringLiteral() {
|
function parseStringLiteral() {
|
||||||
consumeChar("\"", FluentError);
|
consumeChar("\"", FluentError);
|
||||||
let value = "";
|
let value = "";
|
||||||
while (true) {
|
while (true) {
|
||||||
value += match(RE_STRING_RUN);
|
value += match1(RE_STRING_RUN);
|
||||||
|
|
||||||
if (source[cursor] === "\\") {
|
if (source[cursor] === "\\") {
|
||||||
value += parseEscapeSequence(RE_STRING_ESCAPE);
|
value += parseEscapeSequence();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (consumeChar("\"")) {
|
if (consumeChar("\"")) {
|
||||||
return value;
|
return {type: "str", value};
|
||||||
}
|
}
|
||||||
|
|
||||||
// We've reached an EOL of EOF.
|
// We've reached an EOL of EOF.
|
||||||
|
@ -1088,14 +945,20 @@ class FluentResource extends Map {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unescape known escape sequences.
|
// Unescape known escape sequences.
|
||||||
function parseEscapeSequence(reSpecialized) {
|
function parseEscapeSequence() {
|
||||||
if (test(RE_UNICODE_ESCAPE)) {
|
if (test(RE_STRING_ESCAPE)) {
|
||||||
let sequence = match(RE_UNICODE_ESCAPE);
|
return match1(RE_STRING_ESCAPE);
|
||||||
return String.fromCodePoint(parseInt(sequence, 16));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (test(reSpecialized)) {
|
if (test(RE_UNICODE_ESCAPE)) {
|
||||||
return match(reSpecialized);
|
let [, codepoint4, codepoint6] = match(RE_UNICODE_ESCAPE);
|
||||||
|
let codepoint = parseInt(codepoint4 || codepoint6, 16);
|
||||||
|
return codepoint <= 0xD7FF || 0xE000 <= codepoint
|
||||||
|
// It's a Unicode scalar value.
|
||||||
|
? String.fromCodePoint(codepoint)
|
||||||
|
// Lonely surrogates can cause trouble when the parsing result is
|
||||||
|
// saved using UTF-8. Use U+FFFD REPLACEMENT CHARACTER instead.
|
||||||
|
: "<22>";
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new FluentError("Unknown escape sequence");
|
throw new FluentError("Unknown escape sequence");
|
||||||
|
@ -1119,7 +982,7 @@ class FluentResource extends Map {
|
||||||
case "{":
|
case "{":
|
||||||
// Placeables don't require indentation (in EBNF: block-placeable).
|
// Placeables don't require indentation (in EBNF: block-placeable).
|
||||||
// Continue the Pattern.
|
// Continue the Pattern.
|
||||||
return source.slice(start, cursor).replace(RE_CRLF, "\n");
|
return makeIndent(source.slice(start, cursor));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the first character on the line is not one of the special characters
|
// If the first character on the line is not one of the special characters
|
||||||
|
@ -1128,7 +991,7 @@ class FluentResource extends Map {
|
||||||
if (source[cursor - 1] === " ") {
|
if (source[cursor - 1] === " ") {
|
||||||
// It's an indented text character (in EBNF: indented-char). Continue
|
// It's an indented text character (in EBNF: indented-char). Continue
|
||||||
// the Pattern.
|
// the Pattern.
|
||||||
return source.slice(start, cursor).replace(RE_CRLF, "\n");
|
return makeIndent(source.slice(start, cursor));
|
||||||
}
|
}
|
||||||
|
|
||||||
// A not-indented text character is likely the identifier of the next
|
// A not-indented text character is likely the identifier of the next
|
||||||
|
@ -1136,9 +999,16 @@ class FluentResource extends Map {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trim spaces trailing on every line of text.
|
// Trim blanks in text according to the given regex.
|
||||||
function trim(text) {
|
function trim(text, re) {
|
||||||
return text.replace(RE_TRAILING_SPACES, "");
|
return text.replace(re, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize a blank block and extract the indent details.
|
||||||
|
function makeIndent(blank) {
|
||||||
|
let value = blank.replace(RE_BLANK_LINES, "\n");
|
||||||
|
let length = RE_INDENT.exec(blank)[1].length;
|
||||||
|
return {type: "indent", value, length};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Загрузка…
Ссылка в новой задаче