Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Formatting Specification Stub #278

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6b3d575
Bundle.formatPattern
stasm Jul 31, 2019
921e214
Remove VariableReference, SelectExpression
stasm Jul 31, 2019
20639d7
Move the Syntax spec to spec/syntax
stasm Jul 31, 2019
1e58e20
bin/validate
stasm Jul 31, 2019
f614bcd
string_literal.md
stasm Jul 31, 2019
699e0e8
Move everything related to Syntax to syntax/
stasm Jul 31, 2019
34d0198
Move everything related to Formatting to format/
stasm Jul 31, 2019
29899d9
Remove test/harness
stasm Jul 31, 2019
d4015be
Accept a directory of md files
stasm Jul 31, 2019
7cd8fc0
npm run test:format
stasm Jul 31, 2019
bd79e5a
Build format/ before running tests
stasm Jul 31, 2019
df9ee87
Remove Scope.variables
stasm Jul 31, 2019
c903692
Replace resolver/ast.ts with parser/ast.d.ts
stasm Jul 31, 2019
89f835e
Re-add .eslintrc
stasm Jul 31, 2019
726b34b
Update the README a bit
stasm Jul 31, 2019
cf448a8
Fix npm generate
stasm Jul 31, 2019
68e837d
Fix npm bench
stasm Jul 31, 2019
5e47cfb
Update dependencies
stasm Dec 11, 2019
01ed74d
Target es2019
stasm Dec 11, 2019
78faa44
Base Value.value is unknown
stasm Dec 11, 2019
c0bc25a
Expand the specification of escape sequences
stasm Dec 12, 2019
c08063c
Fix paths in build/
stasm Dec 12, 2019
013e1b1
Add npm run watch to README
stasm Dec 12, 2019
2436ca3
Compile to JS files next to TS source
stasm Jul 16, 2020
0f22ecf
Compile to ES2015 modules
stasm Jul 16, 2020
e470d98
Require Node 14; remove esm dependency
stasm Jul 16, 2020
bb20bfe
Hide node_modules and JS files under format/ in VS Code
stasm Jul 16, 2020
19c67af
Move tsconfig. inot format/
stasm Jul 17, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
*.ftl eol=lf
test/fixtures/crlf.ftl eol=crlf
test/fixtures/cr.ftl eol=cr
**.ftl eol=lf
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
build
/format/**/*.js
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"bracketSpacing": false,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 4
}
6 changes: 5 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
sudo: false
language: node_js
before_script: npm run build:impls
script: npm run ci
node_js: 9
node_js:
- "8"
- "10"
- "12"
cache:
directories: node_modules
notifications:
Expand Down
19 changes: 19 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"editor.formatOnSave": false,
"[javascript]": {
"editor.formatOnSave": false
},
"[json]": {
"editor.formatOnSave": true
},
"[typescript]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
},
"files.exclude": {
"node_modules/": true,
"format/**/*.js": true
}
}
13 changes: 13 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": [
"$tsc-watch"
],
"isBackground": true
}
]
}
47 changes: 34 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,56 @@ the natural language.
This repository contains the specification, the reference implementation of the
parser and the documentation for Fluent.

## Fluent Syntax (FTL)
## Fluent Syntax

FTL is the syntax for describing translation resources in Project Fluent. FTL
stands for *Fluent Translation List*. Read the [Fluent Syntax Guide][] to get
started learning Fluent.

The `syntax/` directory contains the reference implementation of the syntax as
a _LL(infinity)_ parser.
The `syntax/spec` directory contains the formal EBNF grammar, autogenerated
from the reference implementation.

The `syntax/parser` directory contains the reference implementation of the
syntax as a _LL(infinity)_ parser.

## Fluent Format

The `format/spec` directory contains the specification of formatting patterns
into strings, including resolving all expressions in placeables.

The `format/resolver` directory contains the reference implementation of
`Bundle.formatPattern` and the resolver.

The `spec/` directory contains the formal EBNF grammar, autogenerated from the
reference implementation.

## Development

While working on the reference parser, use the following commands to test and
validate your work:
The reference resolver is written in TypeScript and must be compiled before
tests are run. The reference parser is currently still written in JavaScript.

While working on the reference implementations, use the following commands to
test and validate your work:

npm run watch # Run the TypeScript compiler watcher.
npm test # Test the parser against JSON AST fixtures,
# and test the resolver using the examples
# from the spec (the resolver must be compiled).

npm test # Test the parser against JSON AST fixtures.
npm run lint # Lint the parser code.
npm run lint # Lint the parser and the resolver code.
npm run pretty # Prettify the resolver code.

npm run generate:ebnf # Generate the EBNF from syntax/grammar.js.
npm run generate:fixtures # Generate test fixtures (FTL → JSON AST).

npm run build:impls # Compile the resolver.
npm run build:guide # Build the HTML version of the Guide.

npm run bench # Run the performance benchmark on large FTL.

## Other Implementations

This repository contains the reference implementation of the parser. Other implementations exist which should be preferred for use in production and in tooling.
This repository contains the reference implementation of Fluent. Other
implementations exist which should be preferred for use in production and in
tooling.

- The JavaScript implementation at [`fluent.js`](https://github.com/projectfluent/fluent.js), including the [React bindings](https://github.com/projectfluent/fluent.js/tree/master/fluent-react).
- The Python implementation at [`python-fluent`](https://github.com/projectfluent/python-fluent).
Expand All @@ -49,8 +69,9 @@ We also know about the following community-driven implementations:

## Learn More and Discuss

Find out more about Project Fluent at [projectfluent.org][] and discuss the future of Fluent at [Mozilla Discourse][].
Find out more about Project Fluent at [projectfluent.org][] and discuss the
future of Fluent at [Mozilla Discourse][].

[Fluent Syntax Guide]: http://projectfluent.org/fluent/guide
[projectfluent.org]: http://projectfluent.org
[Fluent Syntax Guide]: https://projectfluent.org/fluent/guide
[projectfluent.org]: https://projectfluent.org
[Mozilla Discourse]: https://discourse.mozilla.org/c/fluent
36 changes: 36 additions & 0 deletions format/lib/input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import fs from "fs";
import path from "path";
import readline from "readline";

export function fromStdin(callback: (value: string) => void) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: "fluent>",
});

let lines: Array<string> = [];

rl.on("line", line => lines.push(line));
rl.on("close", () => callback(lines.join("\n") + "\n"));
}

export function fromFile(filePath: string) {
return fs.readFileSync(filePath, "utf8");
}

export function* files(destination: string, ext: string) {
let files;
if (destination.endsWith(ext)) {
files = [destination];
} else {
files = fs
.readdirSync(destination)
.filter(filename => filename.endsWith(ext))
.map(filename => path.join(destination, filename));
}

for (let file of files) {
yield file;
}
}
42 changes: 42 additions & 0 deletions format/lib/tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {Bundle} from "../resolver/bundle.js";
import {ErrorKind} from "../resolver/error.js";
import {Resource} from "../resolver/resource.js";
import {Value} from "../resolver/value.js";

type Variables = Map<string, Value>;

export function formatResource(resource: Resource, variables: Variables) {
let bundle = new Bundle();
bundle.addResource(resource);

let results = [];
for (let entry of resource.body) {
if (entry.type !== "Message") {
continue;
}
let message = bundle.getMessage(entry.id.name);
if (message) {
if (message.value) {
let {value, errors} = bundle.formatPattern(message.value, variables);
results.push({
value,
errors: errors.map(error => ({
kind: ErrorKind[error.kind],
arg: error.arg,
})),
});
}
}
}
return results;
}

export function formatMessage(resource: Resource, variables: Variables, id: string) {
let bundle = new Bundle();
bundle.addResource(resource);

let message = bundle.getMessage(id);
if (message && message.value) {
return bundle.formatPattern(message.value, variables);
}
}
3 changes: 3 additions & 0 deletions format/lib/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module "json-diff" {
export function diffString(actual: string, expected: string): string;
}
35 changes: 35 additions & 0 deletions format/lib/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {deepStrictEqual} from "assert";
import commonmark from "commonmark";
import jsonDiff from "json-diff";

type formatFn = (stdin: string) => string;

export function validate(fn: formatFn, spec: string, source: string) {
// https://github.com/commonmark/commonmark.js/blob/master/README.md#usage
let reader = new commonmark.Parser();
let parsed = reader.parse(source);
let walker = parsed.walker();

let counter = 1;
let actual = null;
let current;
while ((current = walker.next())) {
let {entering, node} = current;
if (entering && node.type === "code_block" && node.literal !== null) {
if (node.info === "properties") {
actual = fn(node.literal);
} else if (node.info === "json" && actual) {
let expected = node.literal;

try {
deepStrictEqual(JSON.parse(actual), JSON.parse(expected));
console.log(`${spec} Example ${counter++} PASS`);
} catch (err) {
console.log(`${spec} Example ${counter++} FAIL`);
console.log(jsonDiff.diffString(err.actual, err.expected));
}
actual = null;
}
}
}
}
45 changes: 45 additions & 0 deletions format/resolver/bundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {Pattern} from "../../syntax/parser/ast.js";
import {ScopeError} from "./error.js";
import {Message} from "./message.js";
import {Resource} from "./resource.js";
import {Scope} from "./scope.js";
import {Value} from "./value.js";

export interface FormatResult {
readonly value: string;
readonly errors: Array<ScopeError>;
}

export class Bundle {
public readonly messages: Map<string, Message> = new Map();

addResource(resource: Resource) {
for (let message of resource.body) {
if (message.type === "Message") {
let attributes: Record<string, Pattern> = {};
for (let attribute of message.attributes) {
attributes[attribute.id.name] = attribute.value;
}
this.messages.set(message.id.name, <Message>{
id: message.id.name,
value: message.value,
attributes,
});
}
}
}

hasMessage(id: string) {
return this.messages.has(id);
}

getMessage(id: string) {
return this.messages.get(id);
}

formatPattern(pattern: Pattern, variables: Map<string, Value>): FormatResult {
let scope = new Scope(this.messages, variables);
let value = scope.resolvePattern(pattern).format(scope);
return {value, errors: scope.errors};
}
}
15 changes: 15 additions & 0 deletions format/resolver/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export enum ErrorKind {
UnknownMessage,
MissingValue,
}

export class ScopeError extends Error {
public kind: ErrorKind;
public arg: string;

constructor(kind: ErrorKind, arg: string) {
super();
this.kind = kind;
this.arg = arg;
}
}
7 changes: 7 additions & 0 deletions format/resolver/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {Pattern} from "../../syntax/parser/ast.js";

export interface Message {
readonly id: string;
readonly value: Pattern | null;
readonly attributes: Record<string, Pattern>;
}
19 changes: 19 additions & 0 deletions format/resolver/resource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {Entry} from "../../syntax/parser/ast.js";
import {Resource as ResourceParser} from "../../syntax/parser/grammar.js";

export class Resource {
public readonly body: Array<Entry>;

constructor(source: string) {
this.body = this.parse(source).body;
}

private parse(source: string) {
return ResourceParser.run(source).fold(
resource => resource,
err => {
throw err;
}
);
}
}
Loading