Bug 1480881 - Upgrade Gecko to Fluent 0.6. r=stas

Upgrade Gecko to Fluent 0.6.

Differential Revision: https://phabricator.services.mozilla.com/D2740

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Zibi Braniecki 2018-08-07 00:08:29 +00:00
parent 7b8f8f1076
commit 6ddc6d1828
6 changed files with 683 additions and 635 deletions

View File

@ -16,7 +16,7 @@
*/
/* fluent-dom@aa95b1f (July 10, 2018) */
/* fluent-dom@cab517f (July 31, 2018) */
const { Localization } =
ChromeUtils.import("resource://gre/modules/Localization.jsm", {});
@ -60,10 +60,10 @@ const LOCALIZABLE_ATTRIBUTES = {
th: ["abbr"]
},
"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul": {
description: ["value"],
global: [
"accesskey", "aria-label", "aria-valuetext", "aria-moz-hint", "label"
],
description: ["value"],
key: ["key", "keycode"],
label: ["value"],
textbox: ["placeholder"],
@ -523,7 +523,7 @@ class DOMLocalization extends Localization {
if (this.windowElement) {
if (this.windowElement !== newRoot.ownerGlobal) {
throw new Error(`Cannot connect a root:
DOMLocalization already has a root from a different window`);
DOMLocalization already has a root from a different window.`);
}
} else {
this.windowElement = newRoot.ownerGlobal;

View File

@ -16,7 +16,7 @@
*/
/* fluent-dom@aa95b1f (July 10, 2018) */
/* fluent-dom@cab517f (July 31, 2018) */
/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
/* global console */
@ -25,13 +25,25 @@ const { L10nRegistry } = ChromeUtils.import("resource://gre/modules/L10nRegistry
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm", {});
const { AppConstants } = ChromeUtils.import("resource://gre/modules/AppConstants.jsm", {});
/*
* CachedAsyncIterable caches the elements yielded by an iterable.
*
* It can be used to iterate over an iterable many times without depleting the
* iterable.
*/
class CachedAsyncIterable {
class CachedIterable extends Array {
/**
* Create a `CachedIterable` instance from an iterable or, if another
* instance of `CachedIterable` is passed, return it without any
* modifications.
*
* @param {Iterable} iterable
* @returns {CachedIterable}
*/
static from(iterable) {
if (iterable instanceof this) {
return iterable;
}
return new this(iterable);
}
}
class CachedAsyncIterable extends CachedIterable {
/**
* Create an `CachedAsyncIterable` instance.
*
@ -39,6 +51,8 @@ class CachedAsyncIterable {
* @returns {CachedAsyncIterable}
*/
constructor(iterable) {
super();
if (Symbol.asyncIterator in Object(iterable)) {
this.iterator = iterable[Symbol.asyncIterator]();
} else if (Symbol.iterator in Object(iterable)) {
@ -46,20 +60,46 @@ class CachedAsyncIterable {
} else {
throw new TypeError("Argument must implement the iteration protocol.");
}
this.seen = [];
}
/**
* Synchronous iterator over the cached elements.
*
* Return a generator object implementing the iterator protocol over the
* cached elements of the original (async or sync) iterable.
*/
[Symbol.iterator]() {
const cached = this;
let cur = 0;
return {
next() {
if (cached.length === cur) {
return {value: undefined, done: true};
}
return cached[cur++];
}
};
}
/**
* Asynchronous iterator caching the yielded elements.
*
* Elements yielded by the original iterable will be cached and available
* synchronously. Returns an async generator object implementing the
* iterator protocol over the elements of the original (async or sync)
* iterable.
*/
[Symbol.asyncIterator]() {
const { seen, iterator } = this;
const cached = this;
let cur = 0;
return {
async next() {
if (seen.length <= cur) {
seen.push(await iterator.next());
if (cached.length <= cur) {
cached.push(await cached.iterator.next());
}
return seen[cur++];
return cached[cur++];
}
};
}
@ -71,13 +111,17 @@ class CachedAsyncIterable {
* @param {number} count - number of elements to consume
*/
async touchNext(count = 1) {
const { seen, iterator } = this;
let idx = 0;
while (idx++ < count) {
if (seen.length === 0 || seen[seen.length - 1].done === false) {
seen.push(await iterator.next());
const last = this[this.length - 1];
if (last && last.done) {
break;
}
this.push(await this.iterator.next());
}
// Return the last cached {value, done} object to allow the calling
// code to decide if it needs to call touchNext again.
return this[this.length - 1];
}
}
@ -112,8 +156,8 @@ class Localization {
constructor(resourceIds = [], generateMessages = defaultGenerateMessages) {
this.resourceIds = resourceIds;
this.generateMessages = generateMessages;
this.ctxs =
new CachedAsyncIterable(this.generateMessages(this.resourceIds));
this.ctxs = CachedAsyncIterable.from(
this.generateMessages(this.resourceIds));
}
addResourceIds(resourceIds) {
@ -276,8 +320,8 @@ class Localization {
* that language negotiation or available resources changed.
*/
onChange() {
this.ctxs =
new CachedAsyncIterable(this.generateMessages(this.resourceIds));
this.ctxs = CachedAsyncIterable.from(
this.generateMessages(this.resourceIds));
this.ctxs.touchNext(2);
}
}

View File

@ -16,7 +16,7 @@
*/
/* fluent@aa95b1f (July 10, 2018) */
/* fluent@0.7.0 */
/* eslint no-magic-numbers: [0] */
@ -25,6 +25,9 @@ const MAX_PLACEABLES = 100;
const entryIdentifierRe = /-?[a-zA-Z][a-zA-Z0-9_-]*/y;
const identifierRe = /[a-zA-Z][a-zA-Z0-9_-]*/y;
const functionIdentifierRe = /^[A-Z][A-Z_?-]*$/;
const unicodeEscapeRe = /^[a-fA-F0-9]{4}$/;
const trailingWSRe = /[ \t\n\r]+$/;
/**
* The `Parser` class is responsible for parsing FTL resources.
@ -94,46 +97,15 @@ class RuntimeParser {
const ch = this._source[this._index];
// We don't care about comments or sections at runtime
if (ch === "/" ||
(ch === "#" &&
[" ", "#", "\n"].includes(this._source[this._index + 1]))) {
if (ch === "#" &&
[" ", "#", "\n"].includes(this._source[this._index + 1])) {
this.skipComment();
return;
}
if (ch === "[") {
this.skipSection();
return;
}
this.getMessage();
}
/**
* Skip the section entry from the current index.
*
* @private
*/
skipSection() {
this._index += 1;
if (this._source[this._index] !== "[") {
throw this.error('Expected "[[" to open a section');
}
this._index += 1;
this.skipInlineWS();
this.getVariantName();
this.skipInlineWS();
if (this._source[this._index] !== "]" ||
this._source[this._index + 1] !== "]") {
throw this.error('Expected "]]" to close a section');
}
this._index += 2;
}
/**
* Parse the source string from the current index as an FTL message
* and add it to the entries property on the Parser.
@ -147,6 +119,8 @@ class RuntimeParser {
if (this._source[this._index] === "=") {
this._index++;
} else {
throw this.error("Expected \"=\" after the identifier");
}
this.skipInlineWS();
@ -236,7 +210,7 @@ class RuntimeParser {
* Get identifier using the provided regex.
*
* By default this will get identifiers of public messages, attributes and
* external arguments (without the $).
* variables (without the $).
*
* @returns {String}
* @private
@ -311,21 +285,30 @@ class RuntimeParser {
* @private
*/
getString() {
const start = this._index + 1;
let value = "";
this._index++;
while (++this._index < this._length) {
while (this._index < this._length) {
const ch = this._source[this._index];
if (ch === '"') {
this._index++;
break;
}
if (ch === "\n") {
throw this.error("Unterminated string expression");
}
if (ch === "\\") {
value += this.getEscapedCharacter(["{", "\\", "\""]);
} else {
this._index++;
value += ch;
}
}
return this._source.substring(start, this._index++);
return value;
}
/**
@ -349,10 +332,17 @@ class RuntimeParser {
eol = this._length;
}
const firstLineContent = start !== eol ?
this._source.slice(start, eol) : null;
// If there's any text between the = and the EOL, store it for now. The next
// non-empty line will decide what to do with it.
const firstLineContent = start !== eol
// Trim the trailing whitespace in case this is a single-line pattern.
// Multiline patterns are parsed anew by getComplexPattern.
? this._source.slice(start, eol).replace(trailingWSRe, "")
: null;
if (firstLineContent && firstLineContent.includes("{")) {
if (firstLineContent
&& (firstLineContent.includes("{")
|| firstLineContent.includes("\\"))) {
return this.getComplexPattern();
}
@ -439,13 +429,19 @@ class RuntimeParser {
}
ch = this._source[this._index];
continue;
} else if (ch === "\\") {
const ch2 = this._source[this._index + 1];
if (ch2 === '"' || ch2 === "{" || ch2 === "\\") {
ch = ch2;
this._index++;
}
} else if (ch === "{") {
}
if (ch === undefined) {
break;
}
if (ch === "\\") {
buffer += this.getEscapedCharacter();
ch = this._source[this._index];
continue;
}
if (ch === "{") {
// Push the buffer to content array right before placeable
if (buffer.length) {
content.push(buffer);
@ -457,18 +453,13 @@ class RuntimeParser {
buffer = "";
content.push(this.getPlaceable());
this._index++;
ch = this._source[this._index];
ch = this._source[++this._index];
placeables++;
continue;
}
if (ch) {
buffer += ch;
}
this._index++;
ch = this._source[this._index];
buffer += ch;
ch = this._source[++this._index];
}
if (content.length === 0) {
@ -476,13 +467,42 @@ class RuntimeParser {
}
if (buffer.length) {
content.push(buffer);
// Trim trailing whitespace, too.
content.push(buffer.replace(trailingWSRe, ""));
}
return content;
}
/* eslint-enable complexity */
/**
* Parse an escape sequence and return the unescaped character.
*
* @returns {string}
* @private
*/
getEscapedCharacter(specials = ["{", "\\"]) {
this._index++;
const next = this._source[this._index];
if (specials.includes(next)) {
this._index++;
return next;
}
if (next === "u") {
const sequence = this._source.slice(this._index + 1, this._index + 5);
if (unicodeEscapeRe.test(sequence)) {
this._index += 5;
return String.fromCodePoint(parseInt(sequence, 16));
}
throw this.error(`Invalid Unicode escape sequence: \\u${sequence}`);
}
throw this.error(`Unknown escape sequence: \\${next}`);
}
/**
* Parses a single placeable in a Message pattern and returns its
* expression.
@ -519,7 +539,7 @@ class RuntimeParser {
const ch = this._source[this._index];
if (ch === "}") {
if (selector.type === "attr" && selector.id.name.startsWith("-")) {
if (selector.type === "getattr" && selector.id.name.startsWith("-")) {
throw this.error(
"Attributes of private messages cannot be interpolated."
);
@ -536,11 +556,11 @@ class RuntimeParser {
throw this.error("Message references cannot be used as selectors.");
}
if (selector.type === "var") {
if (selector.type === "getvar") {
throw this.error("Variants cannot be used as selectors.");
}
if (selector.type === "attr" && !selector.id.name.startsWith("-")) {
if (selector.type === "getattr" && !selector.id.name.startsWith("-")) {
throw this.error(
"Attributes of public messages cannot be used as selectors."
);
@ -578,6 +598,10 @@ class RuntimeParser {
* @private
*/
getSelectorExpression() {
if (this._source[this._index] === "{") {
return this.getPlaceable();
}
const literal = this.getLiteral();
if (literal.type !== "ref") {
@ -590,7 +614,7 @@ class RuntimeParser {
const name = this.getIdentifier();
this._index++;
return {
type: "attr",
type: "getattr",
id: literal,
name
};
@ -602,7 +626,7 @@ class RuntimeParser {
const key = this.getVariantKey();
this._index++;
return {
type: "var",
type: "getvar",
id: literal,
key
};
@ -640,7 +664,7 @@ class RuntimeParser {
const args = [];
while (this._index < this._length) {
this.skipInlineWS();
this.skipWS();
if (this._source[this._index] === ")") {
return args;
@ -657,7 +681,7 @@ class RuntimeParser {
if (this._source[this._index] === ":") {
this._index++;
this.skipInlineWS();
this.skipWS();
const val = this.getSelectorExpression();
@ -685,7 +709,7 @@ class RuntimeParser {
}
}
this.skipInlineWS();
this.skipWS();
if (this._source[this._index] === ")") {
break;
@ -885,7 +909,7 @@ class RuntimeParser {
if (cc0 === 36) { // $
this._index++;
return {
type: "ext",
type: "var",
name: this.getIdentifier()
};
}
@ -925,12 +949,11 @@ class RuntimeParser {
// to parse them properly and skip their content.
let eol = this._source.indexOf("\n", this._index);
while (eol !== -1 &&
((this._source[eol + 1] === "/" && this._source[eol + 2] === "/") ||
(this._source[eol + 1] === "#" &&
[" ", "#"].includes(this._source[eol + 2])))) {
this._index = eol + 3;
while (eol !== -1
&& this._source[eol + 1] === "#"
&& [" ", "#"].includes(this._source[eol + 2])) {
this._index = eol + 3;
eol = this._source.indexOf("\n", this._index);
if (eol === -1) {
@ -972,7 +995,7 @@ class RuntimeParser {
if ((cc >= 97 && cc <= 122) || // a-z
(cc >= 65 && cc <= 90) || // A-Z
cc === 47 || cc === 91) { // /[
cc === 45) { // -
this._index = start;
return;
}
@ -1169,11 +1192,11 @@ function values(opts) {
* The role of the Fluent resolver is to format a translation object to an
* instance of `FluentType` or an array of instances.
*
* Translations can contain references to other messages or external arguments,
* Translations can contain references to other messages or variables,
* conditional logic in form of select expressions, traits which describe their
* grammatical features, and can use Fluent builtins which make use of the
* `Intl` formatters to format numbers, dates, lists and more into the
* context's language. See the documentation of the Fluent syntax for more
* context's language. See the documentation of the Fluent syntax for more
* information.
*
* In case of errors the resolver will try to salvage as much of the
@ -1436,8 +1459,8 @@ function Type(env, expr) {
return new FluentSymbol(expr.name);
case "num":
return new FluentNumber(expr.val);
case "ext":
return ExternalArgument(env, expr);
case "var":
return VariableReference(env, expr);
case "fun":
return FunctionReference(env, expr);
case "call":
@ -1446,11 +1469,11 @@ function Type(env, expr) {
const message = MessageReference(env, expr);
return Type(env, message);
}
case "attr": {
case "getattr": {
const attr = AttributeExpression(env, expr);
return Type(env, attr);
}
case "var": {
case "getvar": {
const variant = VariantExpression(env, expr);
return Type(env, variant);
}
@ -1474,7 +1497,7 @@ function Type(env, expr) {
}
/**
* Resolve a reference to an external argument.
* Resolve a reference to a variable.
*
* @param {Object} env
* Resolver environment object.
@ -1485,11 +1508,11 @@ function Type(env, expr) {
* @returns {FluentType}
* @private
*/
function ExternalArgument(env, {name}) {
function VariableReference(env, {name}) {
const { args, errors } = env;
if (!args || !args.hasOwnProperty(name)) {
errors.push(new ReferenceError(`Unknown external: ${name}`));
errors.push(new ReferenceError(`Unknown variable: ${name}`));
return new FluentNone(name);
}
@ -1512,7 +1535,7 @@ function ExternalArgument(env, {name}) {
}
default:
errors.push(
new TypeError(`Unsupported external type: ${name}, ${typeof arg}`)
new TypeError(`Unsupported variable type: ${name}, ${typeof arg}`)
);
return new FluentNone(name);
}
@ -1691,13 +1714,13 @@ class FluentResource extends Map {
* responsible for parsing translation resources in the Fluent syntax and can
* format translation units (entities) to strings.
*
* Always use `MessageContext.format` to retrieve translation units from
* a context. Translations can contain references to other entities or
* external arguments, conditional logic in form of select expressions, traits
* which describe their grammatical features, and can use Fluent builtins which
* make use of the `Intl` formatters to format numbers, dates, lists and more
* into the context's language. See the documentation of the Fluent syntax for
* more information.
* Always use `MessageContext.format` to retrieve translation units from a
* context. Translations can contain references to other entities or variables,
* conditional logic in form of select expressions, traits which describe their
* grammatical features, and can use Fluent builtins which make use of the
* `Intl` formatters to format numbers, dates, lists and more into the
* context's language. See the documentation of the Fluent syntax for more
* information.
*/
class MessageContext {
@ -1849,8 +1872,8 @@ class MessageContext {
* Format a message to a string or null.
*
* Format a raw `message` from the context into a string (or a null if it has
* a null value). `args` will be used to resolve references to external
* arguments inside of the translation.
* a null value). `args` will be used to resolve references to variables
* passed as arguments to the translation.
*
* In case of errors `format` will try to salvage as much of the translation
* as possible and will still return a string. For performance reasons, the
@ -1868,7 +1891,7 @@ class MessageContext {
*
* // Returns 'Hello, name!' and `errors` is now:
*
* [<ReferenceError: Unknown external: name>]
* [<ReferenceError: Unknown variable: name>]
*
* @param {Object | string} message
* @param {Object | undefined} args

File diff suppressed because it is too large Load Diff

View File

@ -19,10 +19,10 @@
async function * generateMessages(locales, resourceIds) {
const mc = new MessageContext(locales);
mc.addMessages(`
file-menu
file-menu =
.label = File
.accesskey = F
new-tab
new-tab =
.label = New Tab
.accesskey = N
`);

View File

@ -15,7 +15,7 @@
async function* mockGenerateMessages(locales, resourceIds) {
const mc = new MessageContext(locales);
mc.addMessages("title = Hello World");
mc.addMessages("link\n .title = Click me");
mc.addMessages("link =\n .title = Click me");
yield mc;
}