зеркало из 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 */
|
||||
|
||||
|
@ -195,20 +195,7 @@ const FSI = "\u2068";
|
|||
const PDI = "\u2069";
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
// Helper: match a variant key to the given selector.
|
||||
function match(bundle, selector, key) {
|
||||
if (key === selector) {
|
||||
// Both are strings.
|
||||
|
@ -233,23 +220,10 @@ function match(bundle, selector, key) {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for choosing the default value from a set of members.
|
||||
*
|
||||
* Used in SelectExpressions and Type.
|
||||
*
|
||||
* @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];
|
||||
// Helper: resolve the default variant from a list of variants.
|
||||
function getDefault(env, variants, star) {
|
||||
if (variants[star]) {
|
||||
return Type(env, variants[star]);
|
||||
}
|
||||
|
||||
const { errors } = env;
|
||||
|
@ -257,176 +231,34 @@ function DefaultMember(env, members, star) {
|
|||
return new FluentNone();
|
||||
}
|
||||
|
||||
// Helper: resolve arguments to a call expression.
|
||||
function getArguments(env, args) {
|
||||
const positional = [];
|
||||
const named = {};
|
||||
|
||||
/**
|
||||
* Resolve a reference to another message.
|
||||
*
|
||||
* @param {Object} env
|
||||
* Resolver environment object.
|
||||
* @param {Object} id
|
||||
* 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;
|
||||
if (args) {
|
||||
for (const arg of args) {
|
||||
if (arg.type === "narg") {
|
||||
named[arg.name] = Type(env, arg.value);
|
||||
} else {
|
||||
positional.push(Type(env, arg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errors.push(
|
||||
new ReferenceError(`Unknown variant: ${sel.toString(bundle)}`));
|
||||
return Type(env, message);
|
||||
return [positional, named];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
// Resolve an expression to a Fluent type.
|
||||
function Type(env, expr) {
|
||||
// A fast-path for strings which are the most common case, and for
|
||||
// `FluentNone` which doesn't require any additional logic.
|
||||
// A fast-path for strings which are the most common case. Since they
|
||||
// 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") {
|
||||
return env.bundle._transform(expr);
|
||||
}
|
||||
|
||||
// A fast-path for `FluentNone` which doesn't require any additional logic.
|
||||
if (expr instanceof FluentNone) {
|
||||
return expr;
|
||||
}
|
||||
|
@ -437,32 +269,21 @@ function Type(env, expr) {
|
|||
return Pattern(env, expr);
|
||||
}
|
||||
|
||||
|
||||
switch (expr.type) {
|
||||
case "str":
|
||||
return expr.value;
|
||||
case "num":
|
||||
return new FluentNumber(expr.value);
|
||||
case "var":
|
||||
return VariableReference(env, expr);
|
||||
case "func":
|
||||
return FunctionReference(env, expr);
|
||||
case "call":
|
||||
return CallExpression(env, expr);
|
||||
case "ref": {
|
||||
const message = MessageReference(env, expr);
|
||||
return Type(env, message);
|
||||
}
|
||||
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 "term":
|
||||
return TermReference({...env, args: {}}, expr);
|
||||
case "ref":
|
||||
return expr.args
|
||||
? FunctionReference(env, expr)
|
||||
: MessageReference(env, expr);
|
||||
case "select":
|
||||
return SelectExpression(env, expr);
|
||||
case undefined: {
|
||||
// If it's a node with a value, resolve the value.
|
||||
if (expr.value !== null && expr.value !== undefined) {
|
||||
|
@ -478,24 +299,13 @@ function Type(env, expr) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
// Resolve a reference to a variable.
|
||||
function VariableReference(env, {name}) {
|
||||
const { args, errors } = env;
|
||||
|
||||
if (!args || !args.hasOwnProperty(name)) {
|
||||
errors.push(new ReferenceError(`Unknown variable: ${name}`));
|
||||
return new FluentNone(name);
|
||||
return new FluentNone(`$${name}`);
|
||||
}
|
||||
|
||||
const arg = args[name];
|
||||
|
@ -519,26 +329,80 @@ function VariableReference(env, {name}) {
|
|||
errors.push(
|
||||
new TypeError(`Unsupported variable type: ${name}, ${typeof arg}`)
|
||||
);
|
||||
return new FluentNone(name);
|
||||
return new FluentNone(`$${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a reference to a function.
|
||||
*
|
||||
* @param {Object} env
|
||||
* Resolver environment object.
|
||||
* @param {Object} expr
|
||||
* An expression to be resolved.
|
||||
* @param {String} expr.name
|
||||
* Name of the function to be returned.
|
||||
* @returns {Function}
|
||||
* @private
|
||||
*/
|
||||
function FunctionReference(env, {name}) {
|
||||
// Some functions are built-in. Others may be provided by the runtime via
|
||||
// Resolve a reference to another message.
|
||||
function MessageReference(env, {name, attr}) {
|
||||
const {bundle, errors} = env;
|
||||
const message = bundle._messages.get(name);
|
||||
if (!message) {
|
||||
const err = new ReferenceError(`Unknown message: ${name}`);
|
||||
errors.push(err);
|
||||
return new FluentNone(name);
|
||||
}
|
||||
|
||||
if (attr) {
|
||||
const attribute = message.attrs && message.attrs[attr];
|
||||
if (attribute) {
|
||||
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.
|
||||
const { bundle: { _functions }, errors } = env;
|
||||
const {bundle: {_functions}, errors} = env;
|
||||
const func = _functions[name] || builtins[name];
|
||||
|
||||
if (!func) {
|
||||
|
@ -551,59 +415,39 @@ function FunctionReference(env, {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 {
|
||||
return func(posargs, keyargs);
|
||||
return func(...getArguments(env, args));
|
||||
} catch (e) {
|
||||
// XXX Report errors.
|
||||
return new FluentNone();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a pattern (a complex string with placeables).
|
||||
*
|
||||
* @param {Object} env
|
||||
* Resolver environment object.
|
||||
* @param {Array} ptn
|
||||
* Array of pattern elements.
|
||||
* @returns {Array}
|
||||
* @private
|
||||
*/
|
||||
// Resolve a select expression to the member object.
|
||||
function SelectExpression(env, {selector, variants, star}) {
|
||||
if (selector === null) {
|
||||
return getDefault(env, variants, star);
|
||||
}
|
||||
|
||||
let sel = Type(env, selector);
|
||||
if (sel instanceof FluentNone) {
|
||||
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) {
|
||||
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.
|
||||
// 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
|
||||
// used to break out of them.
|
||||
const RE_ATTRIBUTE_START = /\.([a-zA-Z][a-zA-Z0-9_-]*) *= */y;
|
||||
// [^] matches all characters, including newlines.
|
||||
// XXX Use /s (dotall) when it's widely supported.
|
||||
const RE_VARIANT_START = /\*?\[[^]*?] */y;
|
||||
const RE_ATTRIBUTE_START = /\.([a-zA-Z][\w-]*) *= */y;
|
||||
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_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
|
||||
// require any special handling. For TextElements such special characters are:
|
||||
// { (starts a placeable), \ (starts an escape sequence), and line breaks which
|
||||
// require additional logic to check if the next line is indented. For
|
||||
// StringLiterals they are: \ (starts an escape sequence), " (ends the
|
||||
// literal), and line breaks which are not allowed in StringLiterals. Also note
|
||||
// that string runs may be empty, but text runs may not.
|
||||
const RE_TEXT_RUN = /([^\\{\n\r]+)/y;
|
||||
// require any special handling. For TextElements such special characters are: {
|
||||
// (starts a placeable), and line breaks which require additional logic to check
|
||||
// if the next line is indented. For StringLiterals they are: \ (starts an
|
||||
// escape sequence), " (ends the literal), and line breaks which are not allowed
|
||||
// in StringLiterals. Note that string runs may be empty; text runs may not.
|
||||
const RE_TEXT_RUN = /([^{}\n\r]+)/y;
|
||||
const RE_STRING_RUN = /([^\\"\n\r]*)/y;
|
||||
|
||||
// Escape sequences.
|
||||
const RE_UNICODE_ESCAPE = /\\u([a-fA-F0-9]{4})/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
|
||||
// the end of every line.
|
||||
const RE_TRAILING_SPACES = / +$/mg;
|
||||
// CRLFs are normalized to LF.
|
||||
const RE_CRLF = /\r\n/g;
|
||||
// Used for trimming TextElements and indents.
|
||||
const RE_LEADING_NEWLINES = /^\n+/;
|
||||
const RE_TRAILING_SPACES = / +$/;
|
||||
// Used in makeIndent to strip spaces from blank lines and normalize CRLF to LF.
|
||||
const RE_BLANK_LINES = / *\r?\n/g;
|
||||
// Used in makeIndent to measure the indentation.
|
||||
const RE_INDENT = /( *)$/;
|
||||
|
||||
// Common tokens.
|
||||
const TOKEN_BRACE_OPEN = /{\s*/y;
|
||||
const TOKEN_BRACE_CLOSE = /\s*}/y;
|
||||
const TOKEN_BRACKET_OPEN = /\[\s*/y;
|
||||
const TOKEN_BRACKET_CLOSE = /\s*]/y;
|
||||
const TOKEN_PAREN_OPEN = /\(\s*/y;
|
||||
const TOKEN_BRACKET_CLOSE = /\s*] */y;
|
||||
const TOKEN_PAREN_OPEN = /\s*\(\s*/y;
|
||||
const TOKEN_ARROW = /\s*->\s*/y;
|
||||
const TOKEN_COLON = /\s*:\s*/y;
|
||||
// Note the optional comma. As a deviation from the Fluent EBNF, the parser
|
||||
|
@ -811,7 +654,7 @@ class FluentResource extends Map {
|
|||
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) {
|
||||
re.lastIndex = cursor;
|
||||
let result = re.exec(source);
|
||||
|
@ -819,7 +662,12 @@ class FluentResource extends Map {
|
|||
throw new FluentError(`Expected ${re.toString()}`);
|
||||
}
|
||||
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() {
|
||||
|
@ -827,6 +675,9 @@ class FluentResource extends Map {
|
|||
let attrs = parseAttributes();
|
||||
|
||||
if (attrs === null) {
|
||||
if (value === null) {
|
||||
throw new FluentError("Expected message value or attributes");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
@ -835,67 +686,62 @@ class FluentResource extends Map {
|
|||
|
||||
function parseAttributes() {
|
||||
let attrs = {};
|
||||
let hasAttributes = false;
|
||||
|
||||
while (test(RE_ATTRIBUTE_START)) {
|
||||
if (!hasAttributes) {
|
||||
hasAttributes = true;
|
||||
let name = match1(RE_ATTRIBUTE_START);
|
||||
let value = parsePattern();
|
||||
if (value === null) {
|
||||
throw new FluentError("Expected attribute value");
|
||||
}
|
||||
|
||||
let name = match(RE_ATTRIBUTE_START);
|
||||
attrs[name] = parsePattern();
|
||||
attrs[name] = value;
|
||||
}
|
||||
|
||||
return hasAttributes ? attrs : null;
|
||||
return Object.keys(attrs).length > 0 ? attrs : null;
|
||||
}
|
||||
|
||||
function parsePattern() {
|
||||
// First try to parse any simple text on the same line as the id.
|
||||
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
|
||||
// back to parsing a complex pattern.
|
||||
switch (source[cursor]) {
|
||||
case "{":
|
||||
case "\\":
|
||||
return first
|
||||
// Re-use the text parsed above, if possible.
|
||||
? parsePatternElements(first)
|
||||
: parsePatternElements();
|
||||
// If there's a placeable on the first line, parse a complex pattern.
|
||||
if (source[cursor] === "{" || source[cursor] === "}") {
|
||||
// Re-use the text parsed above, if possible.
|
||||
return parsePatternElements(first ? [first] : [], Infinity);
|
||||
}
|
||||
|
||||
// RE_TEXT_VALUE stops at newlines. Only continue parsing the pattern if
|
||||
// what comes after the newline is indented.
|
||||
let indent = parseIndent();
|
||||
if (indent) {
|
||||
return first
|
||||
if (first) {
|
||||
// If there's text on the first line, the blank block is part of the
|
||||
// translation content.
|
||||
? parsePatternElements(first, trim(indent))
|
||||
// Otherwise, we're dealing with a block pattern. The blank block is
|
||||
// the leading whitespace; discard it.
|
||||
: parsePatternElements();
|
||||
// translation content in its entirety.
|
||||
return parsePatternElements([first, indent], indent.length);
|
||||
}
|
||||
// Otherwise, we're dealing with a block pattern, i.e. a pattern which
|
||||
// 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) {
|
||||
// It was just a simple inline text after all.
|
||||
return trim(first);
|
||||
return trim(first, RE_TRAILING_SPACES);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse a complex pattern as an array of elements.
|
||||
function parsePatternElements(...elements) {
|
||||
function parsePatternElements(elements = [], commonIndent) {
|
||||
let placeableCount = 0;
|
||||
let needsTrimming = false;
|
||||
|
||||
while (true) {
|
||||
if (test(RE_TEXT_RUN)) {
|
||||
elements.push(match(RE_TEXT_RUN));
|
||||
needsTrimming = true;
|
||||
elements.push(match1(RE_TEXT_RUN));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -904,35 +750,43 @@ class FluentResource extends Map {
|
|||
throw new FluentError("Too many placeables");
|
||||
}
|
||||
elements.push(parsePlaceable());
|
||||
needsTrimming = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source[cursor] === "}") {
|
||||
throw new FluentError("Unbalanced closing brace");
|
||||
}
|
||||
|
||||
let indent = parseIndent();
|
||||
if (indent) {
|
||||
elements.push(trim(indent));
|
||||
needsTrimming = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source[cursor] === "\\") {
|
||||
elements.push(parseEscapeSequence(RE_TEXT_ESCAPE));
|
||||
needsTrimming = false;
|
||||
elements.push(indent);
|
||||
commonIndent = Math.min(commonIndent, indent.length);
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (needsTrimming) {
|
||||
// Trim the trailing whitespace of the last element if it's a
|
||||
// TextElement. Use a flag rather than a typeof check to tell
|
||||
// TextElements and StringLiterals apart (both are strings).
|
||||
let lastIndex = elements.length - 1;
|
||||
elements[lastIndex] = trim(elements[lastIndex]);
|
||||
let lastIndex = elements.length - 1;
|
||||
// Trim the trailing spaces in the last element if it's a TextElement.
|
||||
if (typeof elements[lastIndex] === "string") {
|
||||
elements[lastIndex] = trim(elements[lastIndex], RE_TRAILING_SPACES);
|
||||
}
|
||||
|
||||
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() {
|
||||
|
@ -965,28 +819,20 @@ class FluentResource extends Map {
|
|||
return parsePlaceable();
|
||||
}
|
||||
|
||||
if (consumeChar("$")) {
|
||||
return {type: "var", name: match(RE_IDENTIFIER)};
|
||||
}
|
||||
|
||||
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 (test(RE_REFERENCE)) {
|
||||
let [, sigil, name, attr = null] = match(RE_REFERENCE);
|
||||
let type = {"$": "var", "-": "term"}[sigil] || "ref";
|
||||
|
||||
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)) {
|
||||
let callee = {...ref, type: "func"};
|
||||
return {type: "call", callee, args: parseArguments()};
|
||||
return {type, name, attr, args: parseArguments()};
|
||||
}
|
||||
|
||||
return ref;
|
||||
return {type, name, attr, args: null};
|
||||
}
|
||||
|
||||
return parseLiteral();
|
||||
|
@ -1035,18 +881,29 @@ class FluentResource extends Map {
|
|||
}
|
||||
|
||||
let key = parseVariantKey();
|
||||
cursor = RE_VARIANT_START.lastIndex;
|
||||
variants[count++] = {key, value: parsePattern()};
|
||||
let 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() {
|
||||
consumeToken(TOKEN_BRACKET_OPEN, FluentError);
|
||||
let key = test(RE_NUMBER_LITERAL)
|
||||
? parseNumberLiteral()
|
||||
: match(RE_IDENTIFIER);
|
||||
: match1(RE_IDENTIFIER);
|
||||
consumeToken(TOKEN_BRACKET_CLOSE, FluentError);
|
||||
return key;
|
||||
}
|
||||
|
@ -1064,22 +921,22 @@ class FluentResource extends Map {
|
|||
}
|
||||
|
||||
function parseNumberLiteral() {
|
||||
return {type: "num", value: match(RE_NUMBER_LITERAL)};
|
||||
return {type: "num", value: match1(RE_NUMBER_LITERAL)};
|
||||
}
|
||||
|
||||
function parseStringLiteral() {
|
||||
consumeChar("\"", FluentError);
|
||||
let value = "";
|
||||
while (true) {
|
||||
value += match(RE_STRING_RUN);
|
||||
value += match1(RE_STRING_RUN);
|
||||
|
||||
if (source[cursor] === "\\") {
|
||||
value += parseEscapeSequence(RE_STRING_ESCAPE);
|
||||
value += parseEscapeSequence();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (consumeChar("\"")) {
|
||||
return value;
|
||||
return {type: "str", value};
|
||||
}
|
||||
|
||||
// We've reached an EOL of EOF.
|
||||
|
@ -1088,14 +945,20 @@ class FluentResource extends Map {
|
|||
}
|
||||
|
||||
// Unescape known escape sequences.
|
||||
function parseEscapeSequence(reSpecialized) {
|
||||
if (test(RE_UNICODE_ESCAPE)) {
|
||||
let sequence = match(RE_UNICODE_ESCAPE);
|
||||
return String.fromCodePoint(parseInt(sequence, 16));
|
||||
function parseEscapeSequence() {
|
||||
if (test(RE_STRING_ESCAPE)) {
|
||||
return match1(RE_STRING_ESCAPE);
|
||||
}
|
||||
|
||||
if (test(reSpecialized)) {
|
||||
return match(reSpecialized);
|
||||
if (test(RE_UNICODE_ESCAPE)) {
|
||||
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");
|
||||
|
@ -1119,7 +982,7 @@ class FluentResource extends Map {
|
|||
case "{":
|
||||
// Placeables don't require indentation (in EBNF: block-placeable).
|
||||
// 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
|
||||
|
@ -1128,7 +991,7 @@ class FluentResource extends Map {
|
|||
if (source[cursor - 1] === " ") {
|
||||
// It's an indented text character (in EBNF: indented-char). Continue
|
||||
// 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
|
||||
|
@ -1136,9 +999,16 @@ class FluentResource extends Map {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Trim spaces trailing on every line of text.
|
||||
function trim(text) {
|
||||
return text.replace(RE_TRAILING_SPACES, "");
|
||||
// Trim blanks in text according to the given regex.
|
||||
function trim(text, re) {
|
||||
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};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Загрузка…
Ссылка в новой задаче