Refactor ftl parsing to use fluent-syntax/compat. (#1888)

* Refactor ftl parsing to use fluent-syntax.

This now got rebased to fluent-syntax 0.6.5 which re-adds node 6 LTS
compatibility.

Fixes #1789

* Use upstream lineOffset and columnOffset
This commit is contained in:
Christopher Grebs 2018-03-13 19:05:32 +01:00 коммит произвёл GitHub
Родитель 053bb7a515
Коммит 0c7870c6af
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
4 изменённых файлов: 108 добавлений и 120 удалений

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

@ -95,7 +95,7 @@
"eslint-plugin-no-unsafe-innerhtml": "1.0.16",
"esprima": "3.1.3",
"first-chunk-stream": "2.0.0",
"fluent": "0.4.1",
"fluent-syntax": "^0.6.5",
"glob": "7.1.2",
"jed": "1.1.1",
"os-locale": "2.1.0",

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

@ -1,4 +1,8 @@
import { _parse as parseFluent } from 'fluent';
import {
FluentParser as FluentSyntaxParser,
lineOffset,
columnOffset,
} from 'fluent-syntax';
import * as messages from 'messages';
@ -19,29 +23,33 @@ export default class FluentParser {
}
parse() {
const [entries, errors] = parseFluent(this._sourceString);
const parser = new FluentSyntaxParser();
const resource = parser.parse(this._sourceString);
this.parsedData = {};
Object.keys(entries).forEach((id) => {
this.parsedData[id] = entries[id];
});
resource.body.forEach((entry) => {
if (entry.type === 'Junk') {
this.isValid = false;
if (errors.length) {
this.isValid = false;
// There is always just one annotation for a junk entry
const annotation = entry.annotations[0];
const matchedLine = lineOffset(this._sourceString, annotation.span.end) + 1;
const matchedColumn = columnOffset(this._sourceString, annotation.span.end);
errors.forEach((error) => {
// We only have the message being passed down from fluent, unfortunately
// it doesn't log any line numbers or columns.
const errorData = {
...messages.FLUENT_INVALID,
file: this.filename,
// normalize newlines and flatten the message a bit.
description: error.message.replace(/(?:\n(?:\s*))+/g, ' '),
description: entry.annotations[0].message,
column: matchedColumn,
line: matchedLine,
};
this.collector.addError(errorData);
});
}
} else if (entry.id !== undefined) {
this.parsedData[entry.id.name] = entry;
}
});
if (this.isValid !== false) {
this.isValid = true;

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

@ -20,33 +20,6 @@ choose-download-folder-title =
parser.parse();
expect(parser.isValid).toEqual(true);
expect(parser.parsedData).toEqual({
'choose-download-folder-title': {
val: [
{
def: 0,
exp: null,
type: 'sel',
vars: [
{
key: {
name: 'nominative',
type: 'sym',
},
val: 'Foo',
},
{
key: {
name: 'accusative',
type: 'sym',
},
val: 'Foo2',
},
],
},
],
},
});
});
it('support key assignments', () => {
@ -65,29 +38,26 @@ key69
parser.parse();
expect(parser.isValid).toEqual(true);
expect(parser.parsedData).toEqual({
key67: {
attrs: {
accesskey: 'Y',
label: 'Sign In To &syncBrand.shortName.label;…',
},
val: undefined,
},
key68: {
attrs: {
accesskey: 'S',
label: 'Sync Now',
},
val: undefined,
},
key69: {
attrs: {
accesskey: 'R',
label: 'Reconnect to &syncBrand.shortName.label;…',
},
val: undefined,
},
});
expect(parser.parsedData.key67.attributes[0].value.elements[0].value).toEqual(
'Sign In To &syncBrand.shortName.label;…'
);
expect(parser.parsedData.key67.attributes[1].value.elements[0].value).toEqual(
'Y'
);
expect(parser.parsedData.key68.attributes[0].value.elements[0].value).toEqual(
'Sync Now'
);
expect(parser.parsedData.key68.attributes[1].value.elements[0].value).toEqual(
'S'
);
expect(parser.parsedData.key69.attributes[0].value.elements[0].value).toEqual(
'Reconnect to &syncBrand.shortName.label;…'
);
expect(parser.parsedData.key69.attributes[1].value.elements[0].value).toEqual(
'R'
);
});
it('supports placeable', () => {
@ -103,56 +73,6 @@ shared-photos =
parser.parse();
expect(parser.isValid).toEqual(true);
expect(parser.parsedData).toEqual({
'shared-photos': {
val: [
{
name: 'user_name',
type: 'ext',
},
' ',
{
def: 2,
exp: {
name: 'photo_count',
type: 'ext',
},
type: 'sel',
vars: [
{
key: {
type: 'num',
val: '0',
},
val: 'hasn\'t added any photos yet',
},
{
key: {
name: 'one',
type: 'sym',
},
val: 'added a new photo',
},
{
key: {
name: 'other',
type: 'sym',
},
val: [
'added ',
{
name: 'photo_count',
type: 'ext',
},
' new photos',
],
},
],
},
'.',
],
},
});
});
it('catches syntax errors and throws warnings', () => {
@ -166,8 +86,68 @@ shared-photos =
code: messages.FLUENT_INVALID.code,
message: 'Your FTL is not valid.',
description: oneLine`
Expected a value (like: " = value") or an attribute
(like: ".key = value")`,
Expected message "shared-photos" to have a value or attributes`,
line: 1,
column: 15,
});
});
it('supports firefox 60 beta en-gb file', () => {
const addonLinter = new Linter({ _: ['bar'] });
const parser = new FluentParser(`
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
do-not-track-description = Send web sites a Do Not Track signal that you dont want to be tracked
do-not-track-learn-more = Learn more
do-not-track-option-default =
.label = Only when using Tracking Protection
do-not-track-option-always =
.label = Always
pref-page =
.title = { PLATFORM() ->
[windows] Options
*[other] Preferences
}
# This is used to determine the width of the search field in about:preferences,
# in order to make the entire placeholder string visible
#
# Notice: The value of the .style attribute is a CSS string, and the width
# is the name of the CSS property. It is intended only to adjust the element's width.
# Do not translate.
search-input =
.style = width: 15.4em
pane-general-title = General
category-general =
.tooltiptext = { pane-general-title }
pane-search-title = Search
category-search =
.tooltiptext = { pane-search-title }
pane-privacy-title = Privacy & Security
category-privacy =
.tooltiptext = { pane-privacy-title }
# The word "account" can be translated, do not translate or transliterate "Firefox".
pane-sync-title = Firefox Account
category-sync =
.tooltiptext = { pane-sync-title }
help-button-label = { -brand-short-name } Support
focus-search =
.key = f
close-button =
.aria-label = Close
## Browser Restart Dialog
feature-enable-requires-restart = { -brand-short-name } must restart to enable this feature.
feature-disable-requires-restart = { -brand-short-name } must restart to disable this feature.
should-restart-title = Restart { -brand-short-name }
should-restart-ok = Restart { -brand-short-name } now
revert-no-restart-button = Revert
restart-later = Restart Later`, addonLinter.collector);
parser.parse();
expect(parser.isValid).toEqual(true);
});
});

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

@ -2816,9 +2816,9 @@ flatstr@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.5.tgz#5b451b08cbd48e2eac54a2bbe0bf46165aa14be3"
fluent@0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/fluent/-/fluent-0.4.1.tgz#cd3c4cfb9974d51e603b1dead28c73dda2cf4c1e"
fluent-syntax@^0.6.5:
version "0.6.5"
resolved "https://registry.yarnpkg.com/fluent-syntax/-/fluent-syntax-0.6.5.tgz#e40a76fa41ce55ba6b8ab29d63b0e4c5e5b01e99"
for-in@^0.1.3:
version "0.1.8"