Skip to content

Commit

Permalink
Support private messages in MessageContext
Browse files Browse the repository at this point in the history
  • Loading branch information
stasm committed Jan 23, 2018
1 parent 83d647b commit 99e9f26
Show file tree
Hide file tree
Showing 8 changed files with 336 additions and 68 deletions.
19 changes: 13 additions & 6 deletions fluent/src/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,20 @@ export class MessageContext {
constructor(locales, { functions = {}, useIsolating = true } = {}) {
this.locales = Array.isArray(locales) ? locales : [locales];

this._messages = new Map();
this._privateMessages = new Map();
this._publicMessages = new Map();
this._functions = functions;
this._useIsolating = useIsolating;
this._intls = new WeakMap();
}

/*
* Return an iterator over `[id, message]` pairs.
* Return an iterator over public `[id, message]` pairs.
*
* @returns {Iterator}
*/
get messages() {
return this._messages[Symbol.iterator]();
return this._publicMessages[Symbol.iterator]();
}

/*
Expand All @@ -72,7 +73,7 @@ export class MessageContext {
* @returns {bool}
*/
hasMessage(id) {
return this._messages.has(id);
return this._publicMessages.has(id);
}

/*
Expand All @@ -85,7 +86,7 @@ export class MessageContext {
* @returns {Any}
*/
getMessage(id) {
return this._messages.get(id);
return this._publicMessages.get(id);
}

/**
Expand All @@ -109,7 +110,13 @@ export class MessageContext {
addMessages(source) {
const [entries, errors] = parse(source);
for (const id in entries) {
this._messages.set(id, entries[id]);
if (id.startsWith('-')) {
// Identifiers starting with a dash (-) are considered private and
// cannot be retrieved from MessageContext.
this._privateMessages.set(id, entries[id]);
} else {
this._publicMessages.set(id, entries[id]);
}
}

return errors;
Expand Down
108 changes: 85 additions & 23 deletions fluent/src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

const MAX_PLACEABLES = 100;

const identifierRe = new RegExp('[a-zA-Z_][a-zA-Z0-9_-]*', 'y');
const privateIdentifierRe = new RegExp('-?[a-zA-Z][a-zA-Z0-9_-]*', 'y');
const publicIdentifierRe = new RegExp('[a-zA-Z][a-zA-Z0-9_-]*', 'y');
const functionIdentifierRe = /^[A-Z][A-Z_?-]*$/;

/**
* The `Parser` class is responsible for parsing FTL resources.
Expand Down Expand Up @@ -118,7 +120,7 @@ class RuntimeParser {
* @private
*/
getMessage() {
const id = this.getIdentifier();
const id = this.getPrivateIdentifier();
let attrs = null;

this.skipInlineWS();
Expand Down Expand Up @@ -224,25 +226,44 @@ class RuntimeParser {
}

/**
* Get Message identifier.
* Get identifier of a Message, Attribute or External Attribute.
*
* @returns {String}
* @private
*/
getIdentifier() {
identifierRe.lastIndex = this._index;

const result = identifierRe.exec(this._source);
getIdentifier(re) {
re.lastIndex = this._index;
const result = re.exec(this._source);

if (result === null) {
this._index += 1;
throw this.error('Expected an identifier (starting with [a-zA-Z_])');
throw this.error(`Expected an identifier [${re.toString()}]`);
}

this._index = identifierRe.lastIndex;
this._index = re.lastIndex;
return result[0];
}

/**
* Get a potentially private identifier (staring with a dash).
*
* @returns {String}
* @private
*/
getPrivateIdentifier() {
return this.getIdentifier(privateIdentifierRe);
}

/**
* Get a public identifier.
*
* @returns {String}
* @private
*/
getPublicIdentifier() {
return this.getIdentifier(publicIdentifierRe);
}

/**
* Get Variant name.
*
Expand Down Expand Up @@ -479,13 +500,34 @@ class RuntimeParser {
const ch = this._source[this._index];

if (ch === '}') {
if (selector.type === 'attr' && selector.id.name.startsWith('-')) {
throw this.error(
'Attributes of private messages cannot be interpolated.'
);
}

return selector;
}

if (ch !== '-' || this._source[this._index + 1] !== '>') {
throw this.error('Expected "}" or "->"');
}

if (selector.type === 'ref') {
throw this.error('Message references cannot be used as selectors.');
}

if (selector.type === 'var') {
throw this.error('Variants cannot be used as selectors.');
}

if (selector.type === 'attr' && !selector.id.name.startsWith('-')) {
throw this.error(
'Attributes of public messages cannot be used as selectors.'
);
}


this._index += 2; // ->

this.skipInlineWS();
Expand Down Expand Up @@ -526,7 +568,7 @@ class RuntimeParser {
if (this._source[this._index] === '.') {
this._index++;

const name = this.getIdentifier();
const name = this.getPublicIdentifier();
this._index++;
return {
type: 'attr',
Expand All @@ -551,6 +593,10 @@ class RuntimeParser {
this._index++;
const args = this.getCallArgs();

if (!functionIdentifierRe.test(literal.name)) {
throw this.error('Function names must be all upper-case');
}

this._index++;

literal.type = 'fun';
Expand Down Expand Up @@ -705,7 +751,7 @@ class RuntimeParser {
}
this._index++;

const key = this.getIdentifier();
const key = this.getPublicIdentifier();

this.skipInlineWS();

Expand Down Expand Up @@ -810,23 +856,39 @@ class RuntimeParser {
* @private
*/
getLiteral() {
const cc = this._source.charCodeAt(this._index);
if ((cc >= 48 && cc <= 57) || cc === 45) {
return this.getNumber();
} else if (cc === 34) { // "
return this.getString();
} else if (cc === 36) { // $
const cc0 = this._source.charCodeAt(this._index);

if (cc0 === 36) { // $
this._index++;
return {
type: 'ext',
name: this.getIdentifier()
name: this.getPublicIdentifier()
};
}

return {
type: 'ref',
name: this.getIdentifier()
};
const cc1 = cc0 === 45 // -
// Peek at the next character after the dash.
? this._source.charCodeAt(this._index + 1)
// Or keep using the character at the current index.
: cc0;

if ((cc1 >= 97 && cc1 <= 122) || // a-z
(cc1 >= 65 && cc1 <= 90)) { // A-Z
return {
type: 'ref',
name: this.getPrivateIdentifier()
};
}

if ((cc1 >= 48 && cc1 <= 57)) { // 0-9
return this.getNumber();
}

if (cc0 === 34) { // "
return this.getString();
}

throw this.error('Expected literal');
}

/**
Expand Down Expand Up @@ -886,7 +948,7 @@ class RuntimeParser {

if ((cc >= 97 && cc <= 122) || // a-z
(cc >= 65 && cc <= 90) || // A-Z
cc === 95 || cc === 47 || cc === 91) { // _/[
cc === 47 || cc === 91) { // /[
this._index = start;
return;
}
Expand Down
4 changes: 3 additions & 1 deletion fluent/src/resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ function DefaultMember(env, members, def) {
*/
function MessageReference(env, {name}) {
const { ctx, errors } = env;
const message = ctx.getMessage(name);
const message = name.startsWith('-')
? ctx._privateMessages.get(name)
: ctx._publicMessages.get(name);

if (!message) {
errors.push(new ReferenceError(`Unknown message: ${name}`));
Expand Down
77 changes: 69 additions & 8 deletions fluent/test/context_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,48 @@ suite('Context', function() {
ctx = new MessageContext('en-US', { useIsolating: false });
ctx.addMessages(ftl`
foo = Foo
bar = Bar
-bar = Private Bar
`);
});

test('adds messages', function() {
assert.equal(ctx._publicMessages.has('foo'), true);
assert.equal(ctx._privateMessages.has('foo'), false);
assert.equal(ctx._publicMessages.has('-bar'), false);
assert.equal(ctx._privateMessages.has('-bar'), true);
});

test('preserves existing messages when new are added', function() {
ctx.addMessages(ftl`
baz = Baz
`);
assert(ctx.hasMessage('foo'));
assert(ctx.hasMessage('bar'));
assert(ctx.hasMessage('baz'));

assert.equal(ctx._publicMessages.has('foo'), true);
assert.equal(ctx._privateMessages.has('foo'), false);
assert.equal(ctx._publicMessages.has('-bar'), false);
assert.equal(ctx._privateMessages.has('-bar'), true);

assert.equal(ctx._publicMessages.has('baz'), true);
assert.equal(ctx._privateMessages.has('baz'), false);
});

test('public and private can share the same name', function() {
ctx.addMessages(ftl`
-foo = Private Foo
`);
assert.equal(ctx._publicMessages.has('foo'), true);
assert.equal(ctx._privateMessages.has('foo'), false);
assert.equal(ctx._publicMessages.has('-foo'), false);
assert.equal(ctx._privateMessages.has('-foo'), true);
});


test('overwrites existing messages if the ids are the same', function() {
ctx.addMessages(ftl`
foo = New Foo
`);
assert(ctx.hasMessage('foo'));
assert(ctx.hasMessage('bar'));
assert(ctx.hasMessage('baz'));
assert.equal(ctx._messages.size, 3);

assert.equal(ctx._publicMessages.size, 2);

const msg = ctx.getMessage('foo');
const val = ctx.format(msg, args, errs);
Expand All @@ -46,4 +67,44 @@ suite('Context', function() {
});
});

suite('hasMessage', function(){
suiteSetup(function() {
ctx = new MessageContext('en-US', { useIsolating: false });
ctx.addMessages(ftl`
foo = Foo
-bar = Private Bar
`);
});

test('returns true only for public messages', function() {
assert.equal(ctx.hasMessage('foo'), true);
});

test('returns false for private and missing messages', function() {
assert.equal(ctx.hasMessage('-bar'), false);
assert.equal(ctx.hasMessage('baz'), false);
assert.equal(ctx.hasMessage('-baz'), false);
});
});

suite('getMessage', function(){
suiteSetup(function() {
ctx = new MessageContext('en-US', { useIsolating: false });
ctx.addMessages(ftl`
foo = Foo
-bar = Private Bar
`);
});

test('returns public messages', function() {
assert.equal(ctx.getMessage('foo'), 'Foo');
});

test('returns null for private and missing messages', function() {
assert.equal(ctx.getMessage('-bar'), null);
assert.equal(ctx.getMessage('baz'), null);
assert.equal(ctx.getMessage('-baz'), null);
});
});

});
Loading

0 comments on commit 99e9f26

Please sign in to comment.