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:
Zibi Braniecki 2018-12-16 02:23:27 +00:00
Родитель 566c8a0523
Коммит 65756bf552
2 изменённых файлов: 1696 добавлений и 1048 удалений

Просмотреть файл

@ -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};
} }
} }
} }

Разница между файлами не показана из-за своего большого размера Загрузить разницу