diff --git a/fluent-gecko/makefile b/fluent-gecko/makefile index 9c7ca97c3..9848d149c 100644 --- a/fluent-gecko/makefile +++ b/fluent-gecko/makefile @@ -23,6 +23,7 @@ Fluent.jsm: $(SOURCES) @echo -e " $(OK) $@ built" FluentSyntax.jsm: $(SOURCES) + $(MAKE) -sC ../fluent-syntax compile @rollup $(CURDIR)/src/fluent_syntax.js \ --config ./xpcom_config.js \ --no-treeshake \ diff --git a/fluent-gecko/src/fluent_syntax.js b/fluent-gecko/src/fluent_syntax.js index 8173acc07..9cf44e6d0 100644 --- a/fluent-gecko/src/fluent_syntax.js +++ b/fluent-gecko/src/fluent_syntax.js @@ -2,16 +2,17 @@ comma-dangle: "off", no-labels: "off" */ -import {FluentParser} from "../../fluent-syntax/src/parser"; -import {FluentSerializer} from "../../fluent-syntax/src/serializer"; -import * as ast from "../../fluent-syntax/src/ast"; -import * as visitor from "../../fluent-syntax/src/visitor"; +import {FluentParser} from "../../fluent-syntax/esm/parser"; +import {FluentSerializer} from "../../fluent-syntax/esm/serializer"; +import {Visitor, Transformer} from "../../fluent-syntax/esm/visitor"; +import * as ast from "../../fluent-syntax/esm/ast"; this.EXPORTED_SYMBOLS = [ + ...Object.keys(ast), ...Object.keys({ FluentParser, FluentSerializer, + Visitor, + Transformer }), - ...Object.keys(ast), - ...Object.keys(visitor), ]; diff --git a/fluent-syntax/.esdoc.json b/fluent-syntax/.esdoc.json deleted file mode 100644 index fb3d9174e..000000000 --- a/fluent-syntax/.esdoc.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "source": "./src", - "destination": "../html/syntax", - "plugins": [ - { - "name": "esdoc-standard-plugin" - }, - { - "name": "esdoc-ecmascript-proposal-plugin", - "option": { - "objectRestSpread": true, - "asyncGenerators": true - } - } - ] -} diff --git a/fluent-syntax/.gitignore b/fluent-syntax/.gitignore index 7d147bec1..99472bc4d 100644 --- a/fluent-syntax/.gitignore +++ b/fluent-syntax/.gitignore @@ -1,2 +1,3 @@ +esm/ /index.js /compat.js diff --git a/fluent-syntax/.npmignore b/fluent-syntax/.npmignore index f520b36f5..abad613ff 100644 --- a/fluent-syntax/.npmignore +++ b/fluent-syntax/.npmignore @@ -1,5 +1,7 @@ .nyc_output coverage -docs +esm/.compiled +src test makefile +tsconfig.json diff --git a/fluent-syntax/makefile b/fluent-syntax/makefile index 370709655..ca0679792 100644 --- a/fluent-syntax/makefile +++ b/fluent-syntax/makefile @@ -3,7 +3,21 @@ GLOBAL := FluentSyntax include ../common.mk -test: +lint: + @eslint --config $(ROOT)/eslint_ts.json --max-warnings 0 src/*.ts + @eslint --config $(ROOT)/eslint_test.json --max-warnings 0 test/ + @echo -e " $(OK) lint" + +.PHONY: compile +compile: esm/.compiled + +esm/.compiled: $(SOURCES) + @tsc + @touch $@ + @echo -e " $(OK) esm/ compiled" + +.PHONY: test +test: esm/.compiled @nyc --reporter=text --reporter=html mocha \ --recursive --ui tdd \ --require esm \ @@ -11,8 +25,8 @@ test: build: index.js compat.js -index.js: $(SOURCES) - @rollup $(CURDIR)/src/index.js \ +index.js: esm/.compiled + @rollup $(CURDIR)/esm/index.js \ --config $(ROOT)/bundle_config.js \ --banner "/* $(PACKAGE)@$(VERSION) */" \ --amd.id $(PACKAGE) \ @@ -20,8 +34,8 @@ index.js: $(SOURCES) --output.file $@ @echo -e " $(OK) $@ built" -compat.js: $(SOURCES) - @rollup $(CURDIR)/src/index.js \ +compat.js: esm/.compiled + @rollup $(CURDIR)/esm/index.js \ --config $(ROOT)/compat_config.js \ --banner "/* $(PACKAGE)@$(VERSION) */" \ --amd.id $(PACKAGE) \ @@ -29,9 +43,21 @@ compat.js: $(SOURCES) --output.file $@ @echo -e " $(OK) $@ built" -lint: _lint -html: _html -clean: _clean +html: + @typedoc src \ + --out ../html/syntax \ + --mode file \ + --excludeNotExported \ + --excludePrivate \ + --logger none \ + --hideGenerator + @echo -e " $(OK) html built" + +clean: + @rm -f esm/*.js esm/*.d.ts esm/.compiled + @rm -f index.js compat.js + @rm -rf .nyc_output coverage + @echo -e " $(OK) clean" STRUCTURE_FTL := $(wildcard test/fixtures_structure/*.ftl) STRUCTURE_AST := $(STRUCTURE_FTL:.ftl=.json) diff --git a/fluent-syntax/package.json b/fluent-syntax/package.json index 94445aea2..fbca581f1 100644 --- a/fluent-syntax/package.json +++ b/fluent-syntax/package.json @@ -15,11 +15,10 @@ "email": "stas@mozilla.com" } ], - "directories": { - "lib": "./src" - }, + "type": "commonjs", "main": "./index.js", - "module": "./src/index.js", + "module": "./esm/index.js", + "types": "./esm/index.d.ts", "repository": { "type": "git", "url": "https://github.com/projectfluent/fluent.js.git" diff --git a/fluent-syntax/src/ast.js b/fluent-syntax/src/ast.ts similarity index 50% rename from fluent-syntax/src/ast.js rename to fluent-syntax/src/ast.ts index 344e0598a..2099a2da2 100644 --- a/fluent-syntax/src/ast.js +++ b/fluent-syntax/src/ast.ts @@ -5,10 +5,11 @@ * Annotation. * */ -export class BaseNode { - constructor() {} +export abstract class BaseNode { + public type = "BaseNode"; + [name: string]: unknown; - equals(other, ignoredFields = ["span"]) { + equals(other: BaseNode, ignoredFields: Array = ["span"]): boolean { const thisKeys = new Set(Object.keys(this)); const otherKeys = new Set(Object.keys(other)); if (ignoredFields) { @@ -29,7 +30,7 @@ export class BaseNode { if (typeof thisVal !== typeof otherVal) { return false; } - if (thisVal instanceof Array) { + if (thisVal instanceof Array && otherVal instanceof Array) { if (thisVal.length !== otherVal.length) { return false; } @@ -45,8 +46,8 @@ export class BaseNode { return true; } - clone() { - function visit(value) { + clone(): BaseNode { + function visit(value: unknown): unknown { if (value instanceof BaseNode) { return value.clone(); } @@ -63,8 +64,12 @@ export class BaseNode { } } -function scalarsEqual(thisVal, otherVal, ignoredFields) { - if (thisVal instanceof BaseNode) { +function scalarsEqual( + thisVal: unknown, + otherVal: unknown, + ignoredFields: Array +): boolean { + if (thisVal instanceof BaseNode && otherVal instanceof BaseNode) { return thisVal.equals(otherVal, ignoredFields); } return thisVal === otherVal; @@ -73,16 +78,20 @@ function scalarsEqual(thisVal, otherVal, ignoredFields) { /* * Base class for AST nodes which can have Spans. */ -class SyntaxNode extends BaseNode { - addSpan(start, end) { +export abstract class SyntaxNode extends BaseNode { + public type = "SyntaxNode"; + public span?: Span; + + addSpan(start: number, end: number): void { this.span = new Span(start, end); } } export class Resource extends SyntaxNode { - constructor(body = []) { + public type = "Resource" as const; + public body: Array; + constructor(body: Array = []) { super(); - this.type = "Resource"; this.body = body; } } @@ -90,12 +99,24 @@ export class Resource extends SyntaxNode { /* * An abstract base class for useful elements of Resource.body. */ -export class Entry extends SyntaxNode {} +export abstract class Entry extends SyntaxNode { + public type = "Entry"; +} export class Message extends Entry { - constructor(id, value = null, attributes = [], comment = null) { + public type = "Message" as const; + public id: Identifier; + public value: Pattern | null; + public attributes: Array; + public comment: Comment | null; + + constructor( + id: Identifier, + value: Pattern | null = null, + attributes: Array = [], + comment: Comment | null = null + ) { super(); - this.type = "Message"; this.id = id; this.value = value; this.attributes = attributes; @@ -104,9 +125,19 @@ export class Message extends Entry { } export class Term extends Entry { - constructor(id, value, attributes = [], comment = null) { + public type = "Term" as const; + public id: Identifier; + public value: Pattern; + public attributes: Array; + public comment: Comment | null; + + constructor( + id: Identifier, + value: Pattern, + attributes: Array = [], + comment: Comment | null = null + ) { super(); - this.type = "Term"; this.id = id; this.value = value; this.attributes = attributes; @@ -115,9 +146,11 @@ export class Term extends Entry { } export class Pattern extends SyntaxNode { - constructor(elements) { + public type = "Pattern" as const; + public elements: Array; + + constructor(elements: Array) { super(); - this.type = "Pattern"; this.elements = elements; } } @@ -125,20 +158,26 @@ export class Pattern extends SyntaxNode { /* * An abstract base class for elements of Patterns. */ -export class PatternElement extends SyntaxNode {} +export abstract class PatternElement extends SyntaxNode { + public type = "PatternElement"; +} export class TextElement extends PatternElement { - constructor(value) { + public type = "TextElement" as const; + public value: string; + + constructor(value: string) { super(); - this.type = "TextElement"; this.value = value; } } export class Placeable extends PatternElement { - constructor(expression) { + public type = "Placeable" as const; + public expression: Expression; + + constructor(expression: Expression) { super(); - this.type = "Placeable"; this.expression = expression; } } @@ -146,40 +185,44 @@ export class Placeable extends PatternElement { /* * An abstract base class for expressions. */ -export class Expression extends SyntaxNode {} +export abstract class Expression extends SyntaxNode { + public type = "Expression"; +} // An abstract base class for Literals. -export class Literal extends Expression { - constructor(value) { +export abstract class Literal extends Expression { + public type = "Literal"; + public value: string; + + constructor(value: string) { super(); // The "value" field contains the exact contents of the literal, // character-for-character. this.value = value; } - parse() { - return {value: this.value}; - } + abstract parse(): { value: unknown } } export class StringLiteral extends Literal { - constructor(value) { - super(value); - this.type = "StringLiteral"; - } + public type = "StringLiteral" as const; - parse() { + parse(): { value: string } { // Backslash backslash, backslash double quote, uHHHH, UHHHHHH. const KNOWN_ESCAPES = /(?:\\\\|\\"|\\u([0-9a-fA-F]{4})|\\U([0-9a-fA-F]{6}))/g; - function from_escape_sequence(match, codepoint4, codepoint6) { + function fromEscapeSequence( + match: string, + codepoint4: string, + codepoint6: string + ): string { switch (match) { case "\\\\": return "\\"; case "\\\"": return "\""; - default: + default: { let codepoint = parseInt(codepoint4 || codepoint6, 16); if (codepoint <= 0xD7FF || 0xE000 <= codepoint) { // It's a Unicode scalar value. @@ -189,43 +232,52 @@ export class StringLiteral extends Literal { // well-formed but invalid in Fluent. Replace them with U+FFFD // REPLACEMENT CHARACTER. return "�"; + } } } - let value = this.value.replace(KNOWN_ESCAPES, from_escape_sequence); - return {value}; + let value = this.value.replace(KNOWN_ESCAPES, fromEscapeSequence); + return { value }; } } export class NumberLiteral extends Literal { - constructor(value) { - super(value); - this.type = "NumberLiteral"; - } + public type = "NumberLiteral" as const; - parse() { + parse(): { value: number; precision: number } { let value = parseFloat(this.value); - let decimal_position = this.value.indexOf("."); - let precision = decimal_position > 0 - ? this.value.length - decimal_position - 1 + let decimalPos = this.value.indexOf("."); + let precision = decimalPos > 0 + ? this.value.length - decimalPos - 1 : 0; - return {value, precision}; + return { value, precision }; } } export class MessageReference extends Expression { - constructor(id, attribute = null) { + public type = "MessageReference" as const; + public id: Identifier; + public attribute: Identifier | null; + + constructor(id: Identifier, attribute: Identifier | null = null) { super(); - this.type = "MessageReference"; this.id = id; this.attribute = attribute; } } export class TermReference extends Expression { - constructor(id, attribute = null, args = null) { + public type = "TermReference" as const; + public id: Identifier; + public attribute: Identifier | null; + public arguments: CallArguments | null; + + constructor( + id: Identifier, + attribute: Identifier | null = null, + args: CallArguments | null = null + ) { super(); - this.type = "TermReference"; this.id = id; this.attribute = attribute; this.arguments = args; @@ -233,53 +285,74 @@ export class TermReference extends Expression { } export class VariableReference extends Expression { - constructor(id) { + public type = "VariableReference" as const; + public id: Identifier; + + constructor(id: Identifier) { super(); - this.type = "VariableReference"; this.id = id; } } export class FunctionReference extends Expression { - constructor(id, args) { + public type = "FunctionReference" as const; + public id: Identifier; + public arguments: CallArguments; + + constructor(id: Identifier, args: CallArguments) { super(); - this.type = "FunctionReference"; this.id = id; this.arguments = args; } } export class SelectExpression extends Expression { - constructor(selector, variants) { + public type = "SelectExpression" as const; + public selector: Expression; + public variants: Array; + + constructor(selector: Expression, variants: Array) { super(); - this.type = "SelectExpression"; this.selector = selector; this.variants = variants; } } export class CallArguments extends SyntaxNode { - constructor(positional = [], named = []) { + public type = "CallArguments" as const; + public positional: Array; + public named: Array; + + constructor( + positional: Array = [], + named: Array = [] + ) { super(); - this.type = "CallArguments"; this.positional = positional; this.named = named; } } export class Attribute extends SyntaxNode { - constructor(id, value) { + public type = "Attribute" as const; + public id: Identifier; + public value: Pattern; + + constructor(id: Identifier, value: Pattern) { super(); - this.type = "Attribute"; this.id = id; this.value = value; } } export class Variant extends SyntaxNode { - constructor(key, value, def = false) { + public type = "Variant" as const; + public key: Identifier | NumberLiteral; + public value: Pattern; + public default: boolean; + + constructor(key: Identifier | NumberLiteral, value: Pattern, def: boolean) { super(); - this.type = "Variant"; this.key = key; this.value = value; this.default = def; @@ -287,76 +360,83 @@ export class Variant extends SyntaxNode { } export class NamedArgument extends SyntaxNode { - constructor(name, value) { + public type = "NamedArgument" as const; + public name: Identifier; + public value: Literal; + + constructor(name: Identifier, value: Literal) { super(); - this.type = "NamedArgument"; this.name = name; this.value = value; } } export class Identifier extends SyntaxNode { - constructor(name) { + public type = "Identifier" as const; + public name: string; + + constructor(name: string) { super(); - this.type = "Identifier"; this.name = name; } } -export class BaseComment extends Entry { - constructor(content) { +export abstract class BaseComment extends Entry { + public type = "BaseComment"; + public content: string; + constructor(content: string) { super(); - this.type = "BaseComment"; this.content = content; } } export class Comment extends BaseComment { - constructor(content) { - super(content); - this.type = "Comment"; - } + public type = "Comment" as const; } export class GroupComment extends BaseComment { - constructor(content) { - super(content); - this.type = "GroupComment"; - } + public type = "GroupComment" as const; } export class ResourceComment extends BaseComment { - constructor(content) { - super(content); - this.type = "ResourceComment"; - } + public type = "ResourceComment" as const; } export class Junk extends SyntaxNode { - constructor(content) { + public type = "Junk" as const; + public annotations: Array; + public content: string; + + constructor(content: string) { super(); - this.type = "Junk"; this.annotations = []; this.content = content; } - addAnnotation(annot) { - this.annotations.push(annot); + addAnnotation(annotation: Annotation): void { + this.annotations.push(annotation); } } export class Span extends BaseNode { - constructor(start, end) { + public type = "Span"; + public start: number; + public end: number; + + constructor(start: number, end: number) { super(); - this.type = "Span"; this.start = start; this.end = end; } } export class Annotation extends SyntaxNode { - constructor(code, args = [], message) { + public type = "Annotation"; + public code: string; + public arguments: Array; + public message: string; + + constructor(code: string, args: Array = [], message: string) { super(); - this.type = "Annotation"; this.code = code; this.arguments = args; this.message = message; diff --git a/fluent-syntax/src/errors.js b/fluent-syntax/src/errors.ts similarity index 93% rename from fluent-syntax/src/errors.js rename to fluent-syntax/src/errors.ts index 02bd686c9..556e83c11 100644 --- a/fluent-syntax/src/errors.js +++ b/fluent-syntax/src/errors.ts @@ -1,5 +1,8 @@ export class ParseError extends Error { - constructor(code, ...args) { + public code: string; + public args: Array; + + constructor(code: string, ...args: Array) { super(); this.code = code; this.args = args; @@ -8,7 +11,7 @@ export class ParseError extends Error { } /* eslint-disable complexity */ -function getErrorMessage(code, args) { +function getErrorMessage(code: string, args: Array): string { switch (code) { case "E0001": return "Generic error"; diff --git a/fluent-syntax/src/index.js b/fluent-syntax/src/index.ts similarity index 64% rename from fluent-syntax/src/index.js rename to fluent-syntax/src/index.ts index c4290b1e1..357001cd4 100644 --- a/fluent-syntax/src/index.js +++ b/fluent-syntax/src/index.ts @@ -1,27 +1,31 @@ -import {FluentParser} from "./parser"; -import {FluentSerializer} from "./serializer"; +import { Resource } from "./ast.js"; +import { FluentParser, FluentParserOptions } from "./parser.js"; +import { FluentSerializer, FluentSerializerOptions } from "./serializer.js"; export * from "./ast"; export * from "./parser"; export * from "./serializer"; export * from "./visitor"; -export function parse(source, opts) { +export function parse(source: string, opts: FluentParserOptions): Resource { const parser = new FluentParser(opts); return parser.parse(source); } -export function serialize(resource, opts) { +export function serialize( + resource: Resource, + opts: FluentSerializerOptions +): string { const serializer = new FluentSerializer(opts); return serializer.serialize(resource); } -export function lineOffset(source, pos) { +export function lineOffset(source: string, pos: number): number { // Subtract 1 to get the offset. return source.substring(0, pos).split("\n").length - 1; } -export function columnOffset(source, pos) { +export function columnOffset(source: string, pos: number): number { // Find the last line break starting backwards from the index just before // pos. This allows us to correctly handle ths case where the character at // pos is a line break as well. diff --git a/fluent-syntax/src/parser.js b/fluent-syntax/src/parser.ts similarity index 67% rename from fluent-syntax/src/parser.js rename to fluent-syntax/src/parser.ts index d0a15e20f..fa6862d3e 100644 --- a/fluent-syntax/src/parser.js +++ b/fluent-syntax/src/parser.ts @@ -1,15 +1,27 @@ /* eslint no-magic-numbers: [0] */ -import * as AST from "./ast"; -import { EOF, EOL, FluentParserStream } from "./stream"; -import { ParseError } from "./errors"; +import * as AST from "./ast.js"; +import { EOF, EOL, FluentParserStream } from "./stream.js"; +import { ParseError } from "./errors.js"; const trailingWSRe = /[ \t\n\r]+$/; -function withSpan(fn) { - return function(ps, ...args) { +type ParseFn = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this: FluentParser, ps: FluentParserStream, ...args: Array) => T; + +type Comment = AST.Comment | AST.GroupComment | AST.ResourceComment +type Entry = AST.Message | AST.Term | Comment; +type EntryOrJunk = Entry | AST.Junk; + +function withSpan(fn: ParseFn): ParseFn { + return function ( + this: FluentParser, + ps: FluentParserStream, + ...args: Array + ): T { if (!this.withSpans) { return fn.call(this, ps, ...args); } @@ -29,34 +41,46 @@ function withSpan(fn) { }; } +export interface FluentParserOptions { + withSpans?: boolean; +} -export -class FluentParser { - constructor({ - withSpans = true, - } = {}) { +export class FluentParser { + public withSpans: boolean; + + constructor({ withSpans = true }: FluentParserOptions = {}) { this.withSpans = withSpans; // Poor man's decorators. - const methodNames = [ - "getComment", "getMessage", "getTerm", "getAttribute", "getIdentifier", - "getVariant", "getNumber", "getPattern", "getTextElement", - "getPlaceable", "getExpression", "getInlineExpression", - "getCallArgument", "getCallArguments", "getString", "getLiteral", - ]; - for (const name of methodNames) { - this[name] = withSpan(this[name]); - } + /* eslint-disable @typescript-eslint/unbound-method */ + this.getComment = withSpan(this.getComment); + this.getMessage = withSpan(this.getMessage); + this.getTerm = withSpan(this.getTerm); + this.getAttribute = withSpan(this.getAttribute); + this.getIdentifier = withSpan(this.getIdentifier); + this.getVariant = withSpan(this.getVariant); + this.getNumber = withSpan(this.getNumber); + this.getPattern = withSpan(this.getPattern); + this.getTextElement = withSpan(this.getTextElement); + this.getPlaceable = withSpan(this.getPlaceable); + this.getExpression = withSpan(this.getExpression); + this.getInlineExpression = withSpan(this.getInlineExpression); + this.getCallArgument = withSpan(this.getCallArgument); + this.getCallArguments = withSpan(this.getCallArguments); + this.getString = withSpan(this.getString); + this.getLiteral = withSpan(this.getLiteral); + this.getComment = withSpan(this.getComment); + /* eslint-enable @typescript-eslint/unbound-method */ } - parse(source) { + parse(source: string): AST.Resource { const ps = new FluentParserStream(source); ps.skipBlankBlock(); - const entries = []; - let lastComment = null; + const entries: Array = []; + let lastComment: AST.Comment | null = null; - while (ps.currentChar) { + while (ps.currentChar()) { const entry = this.getEntryOrJunk(ps); const blankLines = ps.skipBlankBlock(); @@ -65,19 +89,20 @@ class FluentParser { // they should parse as standalone when they're followed by Junk. // Consequently, we only attach Comments once we know that the Message // or the Term parsed successfully. - if (entry.type === "Comment" - && blankLines.length === 0 - && ps.currentChar) { + if (entry instanceof AST.Comment + && blankLines.length === 0 + && ps.currentChar()) { // Stash the comment and decide what to do with it in the next pass. lastComment = entry; continue; } if (lastComment) { - if (entry.type === "Message" || entry.type === "Term") { + if (entry instanceof AST.Message || entry instanceof AST.Term) { entry.comment = lastComment; if (this.withSpans) { - entry.span.start = entry.comment.span.start; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + entry.span!.start = entry.comment.span!.start; } } else { entries.push(lastComment); @@ -108,13 +133,13 @@ class FluentParser { * Preceding comments are ignored unless they contain syntax errors * themselves, in which case Junk for the invalid comment is returned. */ - parseEntry(source) { + parseEntry(source: string): EntryOrJunk { const ps = new FluentParserStream(source); ps.skipBlankBlock(); - while (ps.currentChar === "#") { + while (ps.currentChar() === "#") { const skipped = this.getEntryOrJunk(ps); - if (skipped.type === "Junk") { + if (skipped instanceof AST.Junk) { // Don't skip Junk comments. return skipped; } @@ -124,7 +149,7 @@ class FluentParser { return this.getEntryOrJunk(ps); } - getEntryOrJunk(ps) { + getEntryOrJunk(ps: FluentParserStream): EntryOrJunk { const entryStartPos = ps.index; try { @@ -157,12 +182,12 @@ class FluentParser { } } - getEntry(ps) { - if (ps.currentChar === "#") { + getEntry(ps: FluentParserStream): Entry { + if (ps.currentChar() === "#") { return this.getComment(ps); } - if (ps.currentChar === "-") { + if (ps.currentChar() === "-") { return this.getTerm(ps); } @@ -173,7 +198,7 @@ class FluentParser { throw new ParseError("E0002"); } - getComment(ps) { + getComment(ps: FluentParserStream): Comment { // 0 - comment // 1 - group comment // 2 - resource comment @@ -182,7 +207,7 @@ class FluentParser { while (true) { let i = -1; - while (ps.currentChar === "#" && (i < (level === -1 ? 2 : level))) { + while (ps.currentChar() === "#" && (i < (level === -1 ? 2 : level))) { ps.next(); i++; } @@ -191,7 +216,7 @@ class FluentParser { level = i; } - if (ps.currentChar !== EOL) { + if (ps.currentChar() !== EOL) { ps.expectChar(" "); let ch; while ((ch = ps.takeChar(x => x !== EOL))) { @@ -200,7 +225,7 @@ class FluentParser { } if (ps.isNextLineComment(level)) { - content += ps.currentChar; + content += ps.currentChar(); ps.next(); } else { break; @@ -215,14 +240,13 @@ class FluentParser { case 1: Comment = AST.GroupComment; break; - case 2: + default: Comment = AST.ResourceComment; - break; } return new Comment(content); } - getMessage(ps) { + getMessage(ps: FluentParserStream): AST.Message { const id = this.getIdentifier(ps); ps.skipBlankInline(); @@ -238,7 +262,7 @@ class FluentParser { return new AST.Message(id, value, attrs); } - getTerm(ps) { + getTerm(ps: FluentParserStream): AST.Term { ps.expectChar("-"); const id = this.getIdentifier(ps); @@ -254,7 +278,7 @@ class FluentParser { return new AST.Term(id, value, attrs); } - getAttribute(ps) { + getAttribute(ps: FluentParserStream): AST.Attribute { ps.expectChar("."); const key = this.getIdentifier(ps); @@ -270,7 +294,7 @@ class FluentParser { return new AST.Attribute(key, value); } - getAttributes(ps) { + getAttributes(ps: FluentParserStream): Array { const attrs = []; ps.peekBlank(); while (ps.isAttributeStart()) { @@ -282,7 +306,7 @@ class FluentParser { return attrs; } - getIdentifier(ps) { + getIdentifier(ps: FluentParserStream): AST.Identifier { let name = ps.takeIDStart(); let ch; @@ -293,8 +317,8 @@ class FluentParser { return new AST.Identifier(name); } - getVariantKey(ps) { - const ch = ps.currentChar; + getVariantKey(ps: FluentParserStream): AST.Identifier | AST.NumberLiteral { + const ch = ps.currentChar(); if (ch === EOF) { throw new ParseError("E0013"); @@ -309,10 +333,10 @@ class FluentParser { return this.getIdentifier(ps); } - getVariant(ps, {hasDefault}) { + getVariant(ps: FluentParserStream, hasDefault: boolean = false): AST.Variant { let defaultIndex = false; - if (ps.currentChar === "*") { + if (ps.currentChar() === "*") { if (hasDefault) { throw new ParseError("E0015"); } @@ -337,13 +361,13 @@ class FluentParser { return new AST.Variant(key, value, defaultIndex); } - getVariants(ps) { - const variants = []; + getVariants(ps: FluentParserStream): Array { + const variants: Array = []; let hasDefault = false; ps.skipBlank(); while (ps.isVariantStart()) { - const variant = this.getVariant(ps, {hasDefault}); + const variant = this.getVariant(ps, hasDefault); if (variant.default) { hasDefault = true; @@ -365,7 +389,7 @@ class FluentParser { return variants; } - getDigits(ps) { + getDigits(ps: FluentParserStream): string { let num = ""; let ch; @@ -380,17 +404,17 @@ class FluentParser { return num; } - getNumber(ps) { + getNumber(ps: FluentParserStream): AST.NumberLiteral { let value = ""; - if (ps.currentChar === "-") { + if (ps.currentChar() === "-") { ps.next(); value += `-${this.getDigits(ps)}`; } else { value += this.getDigits(ps); } - if (ps.currentChar === ".") { + if (ps.currentChar() === ".") { ps.next(); value += `.${this.getDigits(ps)}`; } @@ -404,37 +428,38 @@ class FluentParser { // patterns). The distinction is important for the dedentation logic: the // indent of the first line of a block pattern must be taken into account when // calculating the maximum common indent. - maybeGetPattern(ps) { + maybeGetPattern(ps: FluentParserStream): AST.Pattern | null { ps.peekBlankInline(); if (ps.isValueStart()) { ps.skipToPeek(); - return this.getPattern(ps, {isBlock: false}); + return this.getPattern(ps, false); } ps.peekBlankBlock(); if (ps.isValueContinuation()) { ps.skipToPeek(); - return this.getPattern(ps, {isBlock: true}); + return this.getPattern(ps, true); } return null; } - getPattern(ps, {isBlock}) { - const elements = []; + getPattern(ps: FluentParserStream, isBlock: boolean): AST.Pattern { + const elements: Array = []; + let commonIndentLength; if (isBlock) { // A block pattern is a pattern which starts on a new line. Store and // measure the indent of this first line for the dedentation logic. const blankStart = ps.index; const firstIndent = ps.skipBlankInline(); elements.push(this.getIndent(ps, firstIndent, blankStart)); - var commonIndentLength = firstIndent.length; + commonIndentLength = firstIndent.length; } else { commonIndentLength = Infinity; } let ch; - elements: while ((ch = ps.currentChar)) { + elements: while ((ch = ps.currentChar())) { switch (ch) { case EOL: { const blankStart = ps.index; @@ -458,8 +483,7 @@ class FluentParser { case "}": throw new ParseError("E0027"); default: - const element = this.getTextElement(ps); - elements.push(element); + elements.push(this.getTextElement(ps)); } } @@ -470,26 +494,25 @@ class FluentParser { // Create a token representing an indent. It's not part of the AST and it will // be trimmed and merged into adjacent TextElements, or turned into a new // TextElement, if it's surrounded by two Placeables. - getIndent(ps, value, start) { - return { - type: "Indent", - span: {start, end: ps.index}, - value, - }; + getIndent(ps: FluentParserStream, value: string, start: number): Indent { + return new Indent(value, start, ps.index); } // Dedent a list of elements by removing the maximum common indent from the // beginning of text lines. The common indent is calculated in getPattern. - dedent(elements, commonIndent) { - const trimmed = []; + dedent( + elements: Array, + commonIndent: number + ): Array { + const trimmed: Array = []; for (let element of elements) { - if (element.type === "Placeable") { + if (element instanceof AST.Placeable) { trimmed.push(element); continue; } - if (element.type === "Indent") { + if (element instanceof Indent) { // Strip common indent. element.value = element.value.slice( 0, element.value.length - commonIndent); @@ -499,17 +522,18 @@ class FluentParser { } let prev = trimmed[trimmed.length - 1]; - if (prev && prev.type === "TextElement") { + if (prev && prev instanceof AST.TextElement) { // Join adjacent TextElements by replacing them with their sum. const sum = new AST.TextElement(prev.value + element.value); if (this.withSpans) { - sum.addSpan(prev.span.start, element.span.end); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + sum.addSpan(prev.span!.start, element.span!.end); } trimmed[trimmed.length - 1] = sum; continue; } - if (element.type === "Indent") { + if (element instanceof Indent) { // If the indent hasn't been merged into a preceding TextElement, // convert it into a new TextElement. const textElement = new AST.TextElement(element.value); @@ -524,7 +548,7 @@ class FluentParser { // Trim trailing whitespace from the Pattern. const lastElement = trimmed[trimmed.length - 1]; - if (lastElement.type === "TextElement") { + if (lastElement instanceof AST.TextElement) { lastElement.value = lastElement.value.replace(trailingWSRe, ""); if (lastElement.value.length === 0) { trimmed.pop(); @@ -534,11 +558,11 @@ class FluentParser { return trimmed; } - getTextElement(ps) { + getTextElement(ps: FluentParserStream): AST.TextElement { let buffer = ""; let ch; - while ((ch = ps.currentChar)) { + while ((ch = ps.currentChar())) { if (ch === "{" || ch === "}") { return new AST.TextElement(buffer); } @@ -554,8 +578,8 @@ class FluentParser { return new AST.TextElement(buffer); } - getEscapeSequence(ps) { - const next = ps.currentChar; + getEscapeSequence(ps: FluentParserStream): string { + const next = ps.currentChar(); switch (next) { case "\\": @@ -571,7 +595,11 @@ class FluentParser { } } - getUnicodeEscapeSequence(ps, u, digits) { + getUnicodeEscapeSequence( + ps: FluentParserStream, + u: string, + digits: number + ): string { ps.expectChar(u); let sequence = ""; @@ -580,7 +608,7 @@ class FluentParser { if (!ch) { throw new ParseError( - "E0026", `\\${u}${sequence}${ps.currentChar}`); + "E0026", `\\${u}${sequence}${ps.currentChar()}`); } sequence += ch; @@ -589,7 +617,7 @@ class FluentParser { return `\\${u}${sequence}`; } - getPlaceable(ps) { + getPlaceable(ps: FluentParserStream): AST.Placeable { ps.expectChar("{"); ps.skipBlank(); const expression = this.getExpression(ps); @@ -597,11 +625,11 @@ class FluentParser { return new AST.Placeable(expression); } - getExpression(ps) { + getExpression(ps: FluentParserStream): AST.Expression | AST.Placeable { const selector = this.getInlineExpression(ps); ps.skipBlank(); - if (ps.currentChar === "-") { + if (ps.currentChar() === "-") { if (ps.peek() !== ">") { ps.resetPeek(); return selector; @@ -609,24 +637,19 @@ class FluentParser { // Validate selector expression according to // abstract.js in the Fluent specification - switch (selector.type) { - case "MessageReference": - if (selector.attribute === null) { - throw new ParseError("E0016"); - } else { - throw new ParseError("E0018"); - } - case "TermReference": - if (selector.attribute === null) { - throw new ParseError("E0017"); - } - case "StringLiteral": - case "NumberLiteral": - case "VariableReference": - case "FunctionReference": - break; - default: - throw new ParseError("E0029"); + + if (selector instanceof AST.MessageReference) { + if (selector.attribute === null) { + throw new ParseError("E0016"); + } else { + throw new ParseError("E0018"); + } + } else if (selector instanceof AST.TermReference) { + if (selector.attribute === null) { + throw new ParseError("E0017"); + } + } else if (selector instanceof AST.Placeable) { + throw new ParseError("E0029"); } ps.next(); @@ -639,15 +662,15 @@ class FluentParser { return new AST.SelectExpression(selector, variants); } - if (selector.type === "TermReference" && selector.attribute !== null) { + if (selector instanceof AST.TermReference && selector.attribute !== null) { throw new ParseError("E0019"); } return selector; } - getInlineExpression(ps) { - if (ps.currentChar === "{") { + getInlineExpression(ps: FluentParserStream): AST.Expression | AST.Placeable { + if (ps.currentChar() === "{") { return this.getPlaceable(ps); } @@ -655,29 +678,29 @@ class FluentParser { return this.getNumber(ps); } - if (ps.currentChar === '"') { + if (ps.currentChar() === '"') { return this.getString(ps); } - if (ps.currentChar === "$") { + if (ps.currentChar() === "$") { ps.next(); const id = this.getIdentifier(ps); return new AST.VariableReference(id); } - if (ps.currentChar === "-") { + if (ps.currentChar() === "-") { ps.next(); const id = this.getIdentifier(ps); let attr; - if (ps.currentChar === ".") { + if (ps.currentChar() === ".") { ps.next(); attr = this.getIdentifier(ps); } let args; ps.peekBlank(); - if (ps.currentPeek === "(") { + if (ps.currentPeek() === "(") { ps.skipToPeek(); args = this.getCallArguments(ps); } @@ -689,7 +712,7 @@ class FluentParser { const id = this.getIdentifier(ps); ps.peekBlank(); - if (ps.currentPeek === "(") { + if (ps.currentPeek() === "(") { // It's a Function. Ensure it's all upper-case. if (!/^[A-Z][A-Z0-9_-]*$/.test(id.name)) { throw new ParseError("E0008"); @@ -701,7 +724,7 @@ class FluentParser { } let attr; - if (ps.currentChar === ".") { + if (ps.currentChar() === ".") { ps.next(); attr = this.getIdentifier(ps); } @@ -713,16 +736,16 @@ class FluentParser { throw new ParseError("E0028"); } - getCallArgument(ps) { + getCallArgument(ps: FluentParserStream): AST.Expression | AST.NamedArgument { const exp = this.getInlineExpression(ps); ps.skipBlank(); - if (ps.currentChar !== ":") { + if (ps.currentChar() !== ":") { return exp; } - if (exp.type === "MessageReference" && exp.attribute === null) { + if (exp instanceof AST.MessageReference && exp.attribute === null) { ps.next(); ps.skipBlank(); @@ -733,21 +756,21 @@ class FluentParser { throw new ParseError("E0009"); } - getCallArguments(ps) { - const positional = []; - const named = []; - const argumentNames = new Set(); + getCallArguments(ps: FluentParserStream): AST.CallArguments { + const positional: Array = []; + const named: Array = []; + const argumentNames: Set = new Set(); ps.expectChar("("); ps.skipBlank(); while (true) { - if (ps.currentChar === ")") { + if (ps.currentChar() === ")") { break; } const arg = this.getCallArgument(ps); - if (arg.type === "NamedArgument") { + if (arg instanceof AST.NamedArgument) { if (argumentNames.has(arg.name.name)) { throw new ParseError("E0022"); } @@ -761,7 +784,7 @@ class FluentParser { ps.skipBlank(); - if (ps.currentChar === ",") { + if (ps.currentChar() === ",") { ps.next(); ps.skipBlank(); continue; @@ -774,7 +797,7 @@ class FluentParser { return new AST.CallArguments(positional, named); } - getString(ps) { + getString(ps: FluentParserStream): AST.StringLiteral { ps.expectChar("\""); let value = ""; @@ -787,7 +810,7 @@ class FluentParser { } } - if (ps.currentChar === EOL) { + if (ps.currentChar() === EOL) { throw new ParseError("E0020"); } @@ -796,15 +819,26 @@ class FluentParser { return new AST.StringLiteral(value); } - getLiteral(ps) { + getLiteral(ps: FluentParserStream): AST.Literal { if (ps.isNumberStart()) { return this.getNumber(ps); } - if (ps.currentChar === '"') { + if (ps.currentChar() === '"') { return this.getString(ps); } throw new ParseError("E0014"); } } + +class Indent { + public type = "Indent"; + public span: AST.Span; + public value: string; + + constructor(value: string, start: number, end: number) { + this.value = value; + this.span = new AST.Span(start, end); + } +} diff --git a/fluent-syntax/src/serializer.js b/fluent-syntax/src/serializer.js deleted file mode 100644 index 5cc107f8a..000000000 --- a/fluent-syntax/src/serializer.js +++ /dev/null @@ -1,259 +0,0 @@ -import { includes } from "./util"; - -function indent(content) { - return content.split("\n").join("\n "); -} - -function includesNewLine(elem) { - return elem.type === "TextElement" && includes(elem.value, "\n"); -} - -function isSelectExpr(elem) { - return elem.type === "Placeable" - && elem.expression.type === "SelectExpression"; -} - -export -// Bit masks representing the state of the serializer. -const HAS_ENTRIES = 1; - -export -class FluentSerializer { - constructor({ withJunk = false } = {}) { - this.withJunk = withJunk; - } - - serialize(resource) { - if (resource.type !== "Resource") { - throw new Error(`Unknown resource type: ${resource.type}`); - } - - let state = 0; - const parts = []; - - for (const entry of resource.body) { - if (entry.type !== "Junk" || this.withJunk) { - parts.push(this.serializeEntry(entry, state)); - if (!(state & HAS_ENTRIES)) { - state |= HAS_ENTRIES; - } - } - } - - return parts.join(""); - } - - serializeEntry(entry, state = 0) { - switch (entry.type) { - case "Message": - return serializeMessage(entry); - case "Term": - return serializeTerm(entry); - case "Comment": - if (state & HAS_ENTRIES) { - return `\n${serializeComment(entry, "#")}\n`; - } - return `${serializeComment(entry, "#")}\n`; - case "GroupComment": - if (state & HAS_ENTRIES) { - return `\n${serializeComment(entry, "##")}\n`; - } - return `${serializeComment(entry, "##")}\n`; - case "ResourceComment": - if (state & HAS_ENTRIES) { - return `\n${serializeComment(entry, "###")}\n`; - } - return `${serializeComment(entry, "###")}\n`; - case "Junk": - return serializeJunk(entry); - default : - throw new Error(`Unknown entry type: ${entry.type}`); - } - } -} - - -function serializeComment(comment, prefix = "#") { - const prefixed = comment.content.split("\n").map( - line => line.length ? `${prefix} ${line}` : prefix - ).join("\n"); - // Add the trailing newline. - return `${prefixed}\n`; -} - - -function serializeJunk(junk) { - return junk.content; -} - - -function serializeMessage(message) { - const parts = []; - - if (message.comment) { - parts.push(serializeComment(message.comment)); - } - - parts.push(`${message.id.name} =`); - - if (message.value) { - parts.push(serializePattern(message.value)); - } - - for (const attribute of message.attributes) { - parts.push(serializeAttribute(attribute)); - } - - parts.push("\n"); - return parts.join(""); -} - - -function serializeTerm(term) { - const parts = []; - - if (term.comment) { - parts.push(serializeComment(term.comment)); - } - - parts.push(`-${term.id.name} =`); - parts.push(serializePattern(term.value)); - - for (const attribute of term.attributes) { - parts.push(serializeAttribute(attribute)); - } - - parts.push("\n"); - return parts.join(""); -} - - -function serializeAttribute(attribute) { - const value = indent(serializePattern(attribute.value)); - return `\n .${attribute.id.name} =${value}`; -} - - -function serializePattern(pattern) { - const content = pattern.elements.map(serializeElement).join(""); - const startOnNewLine = - pattern.elements.some(isSelectExpr) || - pattern.elements.some(includesNewLine); - - if (startOnNewLine) { - return `\n ${indent(content)}`; - } - - return ` ${content}`; -} - - -function serializeElement(element) { - switch (element.type) { - case "TextElement": - return element.value; - case "Placeable": - return serializePlaceable(element); - default: - throw new Error(`Unknown element type: ${element.type}`); - } -} - - -function serializePlaceable(placeable) { - const expr = placeable.expression; - switch (expr.type) { - case "Placeable": - return `{${serializePlaceable(expr)}}`; - case "SelectExpression": - // Special-case select expression to control the whitespace around the - // opening and the closing brace. - return `{ ${serializeExpression(expr)}}`; - default: - return `{ ${serializeExpression(expr)} }`; - } -} - - -export -function serializeExpression(expr) { - switch (expr.type) { - case "StringLiteral": - return `"${expr.value}"`; - case "NumberLiteral": - return expr.value; - case "VariableReference": - return `$${expr.id.name}`; - case "TermReference": { - let out = `-${expr.id.name}`; - if (expr.attribute) { - out += `.${expr.attribute.name}`; - } - if (expr.arguments) { - out += serializeCallArguments(expr.arguments); - } - return out; - } - case "MessageReference": { - let out = expr.id.name; - if (expr.attribute) { - out += `.${expr.attribute.name}`; - } - return out; - } - case "FunctionReference": - return `${expr.id.name}${serializeCallArguments(expr.arguments)}`; - case "SelectExpression": { - let out = `${serializeExpression(expr.selector)} ->`; - for (let variant of expr.variants) { - out += serializeVariant(variant); - } - return `${out}\n`; - } - case "Placeable": - return serializePlaceable(expr); - default: - throw new Error(`Unknown expression type: ${expr.type}`); - } -} - - -function serializeVariant(variant) { - const key = serializeVariantKey(variant.key); - const value = indent(serializePattern(variant.value)); - - if (variant.default) { - return `\n *[${key}]${value}`; - } - - return `\n [${key}]${value}`; -} - - -function serializeCallArguments(expr) { - const positional = expr.positional.map(serializeExpression).join(", "); - const named = expr.named.map(serializeNamedArgument).join(", "); - if (expr.positional.length > 0 && expr.named.length > 0) { - return `(${positional}, ${named})`; - } - return `(${positional || named})`; -} - - -function serializeNamedArgument(arg) { - const value = serializeExpression(arg.value); - return `${arg.name.name}: ${value}`; -} - - -export -function serializeVariantKey(key) { - switch (key.type) { - case "Identifier": - return key.name; - case "NumberLiteral": - return key.value; - default: - throw new Error(`Unknown variant key type: ${key.type}`); - } -} diff --git a/fluent-syntax/src/serializer.ts b/fluent-syntax/src/serializer.ts new file mode 100644 index 000000000..3a3c94ce7 --- /dev/null +++ b/fluent-syntax/src/serializer.ts @@ -0,0 +1,266 @@ +import * as AST from "./ast.js"; + +function indent(content: string): string { + return content.split("\n").join("\n "); +} + +function includesNewLine(elem: AST.PatternElement): boolean { + return elem instanceof AST.TextElement && elem.value.includes("\n"); +} + +function isSelectExpr(elem: AST.PatternElement): boolean { + return elem instanceof AST.Placeable + && elem.expression instanceof AST.SelectExpression; +} + +// Bit masks representing the state of the serializer. +export const HAS_ENTRIES = 1; + +export interface FluentSerializerOptions { + withJunk?: boolean; +} + +export class FluentSerializer { + public withJunk: boolean; + + constructor({ withJunk = false }: FluentSerializerOptions = {}) { + this.withJunk = withJunk; + } + + serialize(resource: AST.Resource): string { + if (!(resource instanceof AST.Resource)) { + throw new Error(`Unknown resource type: ${resource}`); + } + + let state = 0; + const parts = []; + + for (const entry of resource.body) { + if (!(entry instanceof AST.Junk) || this.withJunk) { + parts.push(this.serializeEntry(entry, state)); + if (!(state & HAS_ENTRIES)) { + state |= HAS_ENTRIES; + } + } + } + + return parts.join(""); + } + + serializeEntry(entry: AST.Entry | AST.Junk, state: number = 0): string { + if (entry instanceof AST.Message) { + return serializeMessage(entry); + } + if (entry instanceof AST.Term) { + return serializeTerm(entry); + } + if (entry instanceof AST.Comment) { + if (state & HAS_ENTRIES) { + return `\n${serializeComment(entry, "#")}\n`; + } + return `${serializeComment(entry, "#")}\n`; + } + if (entry instanceof AST.GroupComment) { + if (state & HAS_ENTRIES) { + return `\n${serializeComment(entry, "##")}\n`; + } + return `${serializeComment(entry, "##")}\n`; + } + if (entry instanceof AST.ResourceComment) { + if (state & HAS_ENTRIES) { + return `\n${serializeComment(entry, "###")}\n`; + } + return `${serializeComment(entry, "###")}\n`; + } + if (entry instanceof AST.Junk) { + return serializeJunk(entry); + } + throw new Error(`Unknown entry type: ${entry}`); + } +} + + +function serializeComment(comment: AST.BaseComment, prefix = "#"): string { + const prefixed = comment.content.split("\n").map( + line => line.length ? `${prefix} ${line}` : prefix + ).join("\n"); + // Add the trailing newline. + return `${prefixed}\n`; +} + + +function serializeJunk(junk: AST.Junk): string { + return junk.content; +} + + +function serializeMessage(message: AST.Message): string { + const parts: Array = []; + + if (message.comment) { + parts.push(serializeComment(message.comment)); + } + + parts.push(`${message.id.name} =`); + + if (message.value) { + parts.push(serializePattern(message.value)); + } + + for (const attribute of message.attributes) { + parts.push(serializeAttribute(attribute)); + } + + parts.push("\n"); + return parts.join(""); +} + + +function serializeTerm(term: AST.Term): string { + const parts: Array = []; + + if (term.comment) { + parts.push(serializeComment(term.comment)); + } + + parts.push(`-${term.id.name} =`); + parts.push(serializePattern(term.value)); + + for (const attribute of term.attributes) { + parts.push(serializeAttribute(attribute)); + } + + parts.push("\n"); + return parts.join(""); +} + + +function serializeAttribute(attribute: AST.Attribute): string { + const value = indent(serializePattern(attribute.value)); + return `\n .${attribute.id.name} =${value}`; +} + + +function serializePattern(pattern: AST.Pattern): string { + const content = pattern.elements.map(serializeElement).join(""); + const startOnNewLine = + pattern.elements.some(isSelectExpr) || + pattern.elements.some(includesNewLine); + + if (startOnNewLine) { + return `\n ${indent(content)}`; + } + + return ` ${content}`; +} + + +function serializeElement(element: AST.PatternElement): string { + if (element instanceof AST.TextElement) { + return element.value; + } + + if (element instanceof AST.Placeable) { + return serializePlaceable(element); + } + + throw new Error(`Unknown element type: ${element}`); +} + + +function serializePlaceable(placeable: AST.Placeable): string { + const expr = placeable.expression; + if (expr instanceof AST.Placeable) { + return `{${serializePlaceable(expr)}}`; + } + if (expr instanceof AST.SelectExpression) { + // Special-case select expression to control the whitespace around the + // opening and the closing brace. + return `{ ${serializeExpression(expr)}}`; + } + return `{ ${serializeExpression(expr)} }`; +} + + +export function serializeExpression(expr: AST.Expression): string { + if (expr instanceof AST.StringLiteral) { + return `"${expr.value}"`; + } + if (expr instanceof AST.NumberLiteral) { + return expr.value; + } + if (expr instanceof AST.VariableReference) { + return `$${expr.id.name}`; + } + if (expr instanceof AST.TermReference) { + let out = `-${expr.id.name}`; + if (expr.attribute) { + out += `.${expr.attribute.name}`; + } + if (expr.arguments) { + out += serializeCallArguments(expr.arguments); + } + return out; + } + if (expr instanceof AST.MessageReference) { + let out = expr.id.name; + if (expr.attribute) { + out += `.${expr.attribute.name}`; + } + return out; + } + if (expr instanceof AST.FunctionReference) { + return `${expr.id.name}${serializeCallArguments(expr.arguments)}`; + } + if (expr instanceof AST.SelectExpression) { + let out = `${serializeExpression(expr.selector)} ->`; + for (let variant of expr.variants) { + out += serializeVariant(variant); + } + return `${out}\n`; + } + if (expr instanceof AST.Placeable) { + return serializePlaceable(expr); + } + throw new Error(`Unknown expression type: ${expr}`); +} + + +function serializeVariant(variant: AST.Variant): string { + const key = serializeVariantKey(variant.key); + const value = indent(serializePattern(variant.value)); + + if (variant.default) { + return `\n *[${key}]${value}`; + } + + return `\n [${key}]${value}`; +} + + +function serializeCallArguments(expr: AST.CallArguments): string { + const positional = expr.positional.map(serializeExpression).join(", "); + const named = expr.named.map(serializeNamedArgument).join(", "); + if (expr.positional.length > 0 && expr.named.length > 0) { + return `(${positional}, ${named})`; + } + return `(${positional || named})`; +} + + +function serializeNamedArgument(arg: AST.NamedArgument): string { + const value = serializeExpression(arg.value); + return `${arg.name.name}: ${value}`; +} + +export function serializeVariantKey( + key: AST.Identifier | AST.NumberLiteral +): string { + if (key instanceof AST.Identifier) { + return key.name; + } + if (key instanceof AST.NumberLiteral) { + return key.value; + } + throw new Error(`Unknown variant key type: ${key}`); +} diff --git a/fluent-syntax/src/stream.js b/fluent-syntax/src/stream.ts similarity index 66% rename from fluent-syntax/src/stream.js rename to fluent-syntax/src/stream.ts index 2f474be99..97e715284 100644 --- a/fluent-syntax/src/stream.js +++ b/fluent-syntax/src/stream.ts @@ -1,62 +1,65 @@ /* eslint no-magic-numbers: "off" */ -import { ParseError } from "./errors"; -import { includes } from "./util"; +import { ParseError } from "./errors.js"; export class ParserStream { - constructor(string) { + public string: string; + public index: number; + public peekOffset: number; + + constructor(string: string) { this.string = string; this.index = 0; this.peekOffset = 0; } - charAt(offset) { + charAt(offset: number): string { // When the cursor is at CRLF, return LF but don't move the cursor. // The cursor still points to the EOL position, which in this case is the // beginning of the compound CRLF sequence. This ensures slices of // [inclusive, exclusive) continue to work properly. if (this.string[offset] === "\r" - && this.string[offset + 1] === "\n") { + && this.string[offset + 1] === "\n") { return "\n"; } return this.string[offset]; } - get currentChar() { + currentChar(): string { return this.charAt(this.index); } - get currentPeek() { + currentPeek(): string { return this.charAt(this.index + this.peekOffset); } - next() { + next(): string { this.peekOffset = 0; // Skip over the CRLF as if it was a single character. if (this.string[this.index] === "\r" - && this.string[this.index + 1] === "\n") { + && this.string[this.index + 1] === "\n") { this.index++; } this.index++; return this.string[this.index]; } - peek() { + peek(): string { // Skip over the CRLF as if it was a single character. if (this.string[this.index + this.peekOffset] === "\r" - && this.string[this.index + this.peekOffset + 1] === "\n") { + && this.string[this.index + this.peekOffset + 1] === "\n") { this.peekOffset++; } this.peekOffset++; return this.string[this.index + this.peekOffset]; } - resetPeek(offset = 0) { + resetPeek(offset: number = 0): void { this.peekOffset = offset; } - skipToPeek() { + skipToPeek(): void { this.index += this.peekOffset; this.peekOffset = 0; } @@ -67,31 +70,31 @@ export const EOF = undefined; const SPECIAL_LINE_START_CHARS = ["}", ".", "[", "*"]; export class FluentParserStream extends ParserStream { - peekBlankInline() { + peekBlankInline(): string { const start = this.index + this.peekOffset; - while (this.currentPeek === " ") { + while (this.currentPeek() === " ") { this.peek(); } return this.string.slice(start, this.index + this.peekOffset); } - skipBlankInline() { + skipBlankInline(): string { const blank = this.peekBlankInline(); this.skipToPeek(); return blank; } - peekBlankBlock() { + peekBlankBlock(): string { let blank = ""; while (true) { const lineStart = this.peekOffset; this.peekBlankInline(); - if (this.currentPeek === EOL) { + if (this.currentPeek() === EOL) { blank += EOL; this.peek(); continue; } - if (this.currentPeek === EOF) { + if (this.currentPeek() === EOF) { // Treat the blank line at EOF as a blank block. return blank; } @@ -101,49 +104,49 @@ export class FluentParserStream extends ParserStream { } } - skipBlankBlock() { + skipBlankBlock(): string { const blank = this.peekBlankBlock(); this.skipToPeek(); return blank; } - peekBlank() { - while (this.currentPeek === " " || this.currentPeek === EOL) { + peekBlank(): void { + while (this.currentPeek() === " " || this.currentPeek() === EOL) { this.peek(); } } - skipBlank() { + skipBlank(): void { this.peekBlank(); this.skipToPeek(); } - expectChar(ch) { - if (this.currentChar === ch) { + expectChar(ch: string): void { + if (this.currentChar() === ch) { this.next(); - return true; + return; } throw new ParseError("E0003", ch); } - expectLineEnd() { - if (this.currentChar === EOF) { + expectLineEnd(): void { + if (this.currentChar() === EOF) { // EOF is a valid line end in Fluent. - return true; + return; } - if (this.currentChar === EOL) { + if (this.currentChar() === EOL) { this.next(); - return true; + return; } // Unicode Character 'SYMBOL FOR NEWLINE' (U+2424) throw new ParseError("E0003", "\u2424"); } - takeChar(f) { - const ch = this.currentChar; + takeChar(f: (ch: string) => boolean): string | null | typeof EOF { + const ch = this.currentChar(); if (ch === EOF) { return EOF; } @@ -154,24 +157,24 @@ export class FluentParserStream extends ParserStream { return null; } - isCharIdStart(ch) { + isCharIdStart(ch: string): boolean { if (ch === EOF) { return false; } const cc = ch.charCodeAt(0); return (cc >= 97 && cc <= 122) || // a-z - (cc >= 65 && cc <= 90); // A-Z + (cc >= 65 && cc <= 90); // A-Z } - isIdentifierStart() { - return this.isCharIdStart(this.currentPeek); + isIdentifierStart(): boolean { + return this.isCharIdStart(this.currentPeek()); } - isNumberStart() { - const ch = this.currentChar === "-" + isNumberStart(): boolean { + const ch = this.currentChar() === "-" ? this.peek() - : this.currentChar; + : this.currentChar(); if (ch === EOF) { this.resetPeek(); @@ -184,25 +187,25 @@ export class FluentParserStream extends ParserStream { return isDigit; } - isCharPatternContinuation(ch) { + isCharPatternContinuation(ch: string): boolean { if (ch === EOF) { return false; } - return !includes(SPECIAL_LINE_START_CHARS, ch); + return !SPECIAL_LINE_START_CHARS.includes(ch); } - isValueStart() { + isValueStart(): boolean { // Inline Patterns may start with any char. - const ch = this.currentPeek; + const ch = this.currentPeek(); return ch !== EOL && ch !== EOF; } - isValueContinuation() { + isValueContinuation(): boolean { const column1 = this.peekOffset; this.peekBlankInline(); - if (this.currentPeek === "{") { + if (this.currentPeek() === "{") { this.resetPeek(column1); return true; } @@ -211,7 +214,7 @@ export class FluentParserStream extends ParserStream { return false; } - if (this.isCharPatternContinuation(this.currentPeek)) { + if (this.isCharPatternContinuation(this.currentPeek())) { this.resetPeek(column1); return true; } @@ -223,8 +226,8 @@ export class FluentParserStream extends ParserStream { // 0 - comment // 1 - group comment // 2 - resource comment - isNextLineComment(level = -1) { - if (this.currentChar !== EOL) { + isNextLineComment(level: number = -1): boolean { + if (this.currentChar() !== EOL) { return false; } @@ -252,12 +255,12 @@ export class FluentParserStream extends ParserStream { return false; } - isVariantStart() { + isVariantStart(): boolean { const currentPeekOffset = this.peekOffset; - if (this.currentPeek === "*") { + if (this.currentPeek() === "*") { this.peek(); } - if (this.currentPeek === "[") { + if (this.currentPeek() === "[") { this.resetPeek(currentPeekOffset); return true; } @@ -265,20 +268,20 @@ export class FluentParserStream extends ParserStream { return false; } - isAttributeStart() { - return this.currentPeek === "."; + isAttributeStart(): boolean { + return this.currentPeek() === "."; } - skipToNextEntryStart(junkStart) { + skipToNextEntryStart(junkStart: number): void { let lastNewline = this.string.lastIndexOf(EOL, this.index); if (junkStart < lastNewline) { // Last seen newline is _after_ the junk start. It's safe to rewind // without the risk of resuming at the same broken entry. this.index = lastNewline; } - while (this.currentChar) { + while (this.currentChar()) { // We're only interested in beginnings of line. - if (this.currentChar !== EOL) { + if (this.currentChar() !== EOL) { this.next(); continue; } @@ -291,9 +294,9 @@ export class FluentParserStream extends ParserStream { } } - takeIDStart() { - if (this.isCharIdStart(this.currentChar)) { - const ret = this.currentChar; + takeIDStart(): string { + if (this.isCharIdStart(this.currentChar())) { + const ret = this.currentChar(); this.next(); return ret; } @@ -301,20 +304,20 @@ export class FluentParserStream extends ParserStream { throw new ParseError("E0004", "a-zA-Z"); } - takeIDChar() { - const closure = ch => { + takeIDChar(): string | null | typeof EOF { + const closure = (ch: string): boolean => { const cc = ch.charCodeAt(0); return ((cc >= 97 && cc <= 122) || // a-z - (cc >= 65 && cc <= 90) || // A-Z - (cc >= 48 && cc <= 57) || // 0-9 - cc === 95 || cc === 45); // _- + (cc >= 65 && cc <= 90) || // A-Z + (cc >= 48 && cc <= 57) || // 0-9 + cc === 95 || cc === 45); // _- }; return this.takeChar(closure); } - takeDigit() { - const closure = ch => { + takeDigit(): string | null | typeof EOF { + const closure = (ch: string): boolean => { const cc = ch.charCodeAt(0); return (cc >= 48 && cc <= 57); // 0-9 }; @@ -322,8 +325,8 @@ export class FluentParserStream extends ParserStream { return this.takeChar(closure); } - takeHexDigit() { - const closure = ch => { + takeHexDigit(): string | null | typeof EOF { + const closure = (ch: string): boolean => { const cc = ch.charCodeAt(0); return (cc >= 48 && cc <= 57) // 0-9 || (cc >= 65 && cc <= 70) // A-F diff --git a/fluent-syntax/src/util.js b/fluent-syntax/src/util.js deleted file mode 100644 index c733bd069..000000000 --- a/fluent-syntax/src/util.js +++ /dev/null @@ -1,3 +0,0 @@ -export function includes(arr, elem) { - return arr.indexOf(elem) > -1; -} diff --git a/fluent-syntax/src/visitor.js b/fluent-syntax/src/visitor.js deleted file mode 100644 index 1b1841aac..000000000 --- a/fluent-syntax/src/visitor.js +++ /dev/null @@ -1,58 +0,0 @@ -import { BaseNode } from "./ast"; - -/* - * Abstract Visitor pattern - */ -export class Visitor { - visit(node) { - if (Array.isArray(node)) { - node.forEach(child => this.visit(child)); - return; - } - if (!(node instanceof BaseNode)) { - return; - } - const visit = this[`visit${node.type}`] || this.genericVisit; - visit.call(this, node); - } - - genericVisit(node) { - for (const propname of Object.keys(node)) { - this.visit(node[propname]); - } - } -} - -/* - * Abstract Transformer pattern - */ -export class Transformer extends Visitor { - visit(node) { - if (!(node instanceof BaseNode)) { - return node; - } - const visit = this[`visit${node.type}`] || this.genericVisit; - return visit.call(this, node); - } - - genericVisit(node) { - for (const propname of Object.keys(node)) { - const propvalue = node[propname]; - if (Array.isArray(propvalue)) { - const newvals = propvalue - .map(child => this.visit(child)) - .filter(newchild => newchild !== undefined); - node[propname] = newvals; - } - if (propvalue instanceof BaseNode) { - const new_val = this.visit(propvalue); - if (new_val === undefined) { - delete node[propname]; - } else { - node[propname] = new_val; - } - } - } - return node; - } -} diff --git a/fluent-syntax/src/visitor.ts b/fluent-syntax/src/visitor.ts new file mode 100644 index 000000000..18682185e --- /dev/null +++ b/fluent-syntax/src/visitor.ts @@ -0,0 +1,144 @@ +import { BaseNode } from "./ast.js"; + +/** + * A read-only visitor. + * + * Subclasses can be used to gather information from an AST. + * + * To handle specific node types add methods like `visitPattern`. + * Then, to descend into children call `genericVisit`. + * + * Visiting methods must implement the following interface: + * + * interface VisitingMethod { + * (this: Visitor, node: BaseNode): void; + * } + */ +export abstract class Visitor { + [prop: string]: unknown; + + visit(node: BaseNode): void { + let visit = this[`visit${node.type}`]; + if (typeof visit === "function") { + visit.call(this, node); + } else { + this.genericVisit(node); + } + } + + genericVisit(node: BaseNode): void { + for (const key of Object.keys(node)) { + let prop = node[key]; + if (prop instanceof BaseNode) { + this.visit(prop); + } else if (Array.isArray(prop)) { + for (let element of prop) { + this.visit(element as BaseNode); + } + } + } + } + + visitResource?(node: BaseNode): void; + visitMessage?(node: BaseNode): void; + visitTerm?(node: BaseNode): void; + visitPattern?(node: BaseNode): void; + visitTextElement?(node: BaseNode): void; + visitPlaceable?(node: BaseNode): void; + visitStringLiteral?(node: BaseNode): void; + visitNumberLiteral?(node: BaseNode): void; + visitMessageReference?(node: BaseNode): void; + visitTermReference?(node: BaseNode): void; + visitVariableReference?(node: BaseNode): void; + visitFunctionReference?(node: BaseNode): void; + visitSelectExpression?(node: BaseNode): void; + visitCallArguments?(node: BaseNode): void; + visitAttribute?(node: BaseNode): void; + visitVariant?(node: BaseNode): void; + visitNamedArgument?(node: BaseNode): void; + visitIdentifier?(node: BaseNode): void; + visitComment?(node: BaseNode): void; + visitGroupComment?(node: BaseNode): void; + visitResourceComment?(node: BaseNode): void; + visitJunk?(node: BaseNode): void; + visitSpan?(node: BaseNode): void; + visitAnnotation?(node: BaseNode): void; +} + +/** + * A read-and-write visitor. + * + * Subclasses can be used to modify an AST in-place. + * + * To handle specific node types add methods like `visitPattern`. + * Then, to descend into children call `genericVisit`. + * + * Visiting methods must implement the following interface: + * + * interface TransformingMethod { + * (this: Transformer, node: BaseNode): BaseNode | undefined; + * } + * + * The returned node wili replace the original one in the AST. Return + * `undefined` to remove the node instead. + */ +export abstract class Transformer extends Visitor { + [prop: string]: unknown; + + visit(node: BaseNode): BaseNode | undefined { + let visit = this[`visit${node.type}`]; + if (typeof visit === "function") { + return visit.call(this, node); + } + return this.genericVisit(node); + } + + genericVisit(node: BaseNode): BaseNode | undefined { + for (const key of Object.keys(node)) { + let prop = node[key]; + if (prop instanceof BaseNode) { + let newVal = this.visit(prop); + if (newVal === undefined) { + delete node[key]; + } else { + node[key] = newVal; + } + } else if (Array.isArray(prop)) { + let newVals: Array = []; + for (let element of prop) { + let newVal = this.visit(element); + if (newVal !== undefined) { + newVals.push(newVal); + } + } + node[key] = newVals; + } + } + return node; + } + + visitResource?(node: BaseNode): BaseNode | undefined; + visitMessage?(node: BaseNode): BaseNode | undefined; + visitTerm?(node: BaseNode): BaseNode | undefined; + visitPattern?(node: BaseNode): BaseNode | undefined; + visitTextElement?(node: BaseNode): BaseNode | undefined; + visitPlaceable?(node: BaseNode): BaseNode | undefined; + visitStringLiteral?(node: BaseNode): BaseNode | undefined; + visitNumberLiteral?(node: BaseNode): BaseNode | undefined; + visitMessageReference?(node: BaseNode): BaseNode | undefined; + visitTermReference?(node: BaseNode): BaseNode | undefined; + visitVariableReference?(node: BaseNode): BaseNode | undefined; + visitFunctionReference?(node: BaseNode): BaseNode | undefined; + visitSelectExpression?(node: BaseNode): BaseNode | undefined; + visitCallArguments?(node: BaseNode): BaseNode | undefined; + visitAttribute?(node: BaseNode): BaseNode | undefined; + visitVariant?(node: BaseNode): BaseNode | undefined; + visitNamedArgument?(node: BaseNode): BaseNode | undefined; + visitIdentifier?(node: BaseNode): BaseNode | undefined; + visitComment?(node: BaseNode): BaseNode | undefined; + visitGroupComment?(node: BaseNode): BaseNode | undefined; + visitResourceComment?(node: BaseNode): BaseNode | undefined; + visitJunk?(node: BaseNode): BaseNode | undefined; + visitSpan?(node: BaseNode): BaseNode | undefined; + visitAnnotation?(node: BaseNode): BaseNode | undefined; +} diff --git a/fluent-syntax/test/ast_test.js b/fluent-syntax/test/ast_test.js index 411ed97fa..d82695d3a 100644 --- a/fluent-syntax/test/ast_test.js +++ b/fluent-syntax/test/ast_test.js @@ -2,8 +2,8 @@ import assert from "assert"; import ftl from "@fluent/dedent"; -import { FluentParser } from "../src"; -import * as AST from "../src/ast"; +import * as AST from "../esm/ast.js"; +import { FluentParser } from "../esm/parser.js"; suite("BaseNode.equals", function() { setup(function() { diff --git a/fluent-syntax/test/entry_test.js b/fluent-syntax/test/entry_test.js index 04e7463c0..fe21a6315 100644 --- a/fluent-syntax/test/entry_test.js +++ b/fluent-syntax/test/entry_test.js @@ -1,7 +1,9 @@ import assert from "assert"; import ftl from "@fluent/dedent"; -import { FluentParser, FluentSerializer } from "../src"; +import * as AST from "../esm/ast.js"; +import { FluentParser } from "../esm/parser.js"; +import { FluentSerializer } from "../esm/serializer"; suite("Parse entry", function() { setup(function() { @@ -155,24 +157,12 @@ suite("Serialize entry", function() { }); test("simple message", function() { - const input = { - "comment": null, - "value": { - "elements": [ - { - "type": "TextElement", - "value": "Foo" - } - ], - "type": "Pattern" - }, - "attributes": [], - "type": "Message", - "id": { - "type": "Identifier", - "name": "foo" - } - }; + const input = new AST.Message( + new AST.Identifier("foo"), + new AST.Pattern([ + new AST.TextElement("Foo") + ]) + ) const output = ftl` foo = Foo diff --git a/fluent-syntax/test/fixtures_structure/escape_sequences.json b/fluent-syntax/test/fixtures_structure/escape_sequences.json index 68cacb186..06118a6fc 100644 --- a/fluent-syntax/test/fixtures_structure/escape_sequences.json +++ b/fluent-syntax/test/fixtures_structure/escape_sequences.json @@ -252,8 +252,8 @@ { "type": "Placeable", "expression": { - "value": "\\\"", "type": "StringLiteral", + "value": "\\\"", "span": { "type": "Span", "start": 262, @@ -298,8 +298,8 @@ { "type": "Placeable", "expression": { - "value": "\\\\", "type": "StringLiteral", + "value": "\\\\", "span": { "type": "Span", "start": 291, @@ -419,8 +419,8 @@ { "type": "Placeable", "expression": { - "value": "\\u0041", "type": "StringLiteral", + "value": "\\u0041", "span": { "type": "Span", "start": 443, @@ -465,8 +465,8 @@ { "type": "Placeable", "expression": { - "value": "\\\\u0041", "type": "StringLiteral", + "value": "\\\\u0041", "span": { "type": "Span", "start": 479, @@ -562,8 +562,8 @@ { "type": "Placeable", "expression": { - "value": "{", "type": "StringLiteral", + "value": "{", "span": { "type": "Span", "start": 586, @@ -626,8 +626,8 @@ { "type": "Placeable", "expression": { - "value": "}", "type": "StringLiteral", + "value": "}", "span": { "type": "Span", "start": 623, diff --git a/fluent-syntax/test/fixtures_structure/expressions_call_args.json b/fluent-syntax/test/fixtures_structure/expressions_call_args.json index 3fd9bc080..c70915db3 100644 --- a/fluent-syntax/test/fixtures_structure/expressions_call_args.json +++ b/fluent-syntax/test/fixtures_structure/expressions_call_args.json @@ -44,8 +44,8 @@ } }, "value": { - "value": "1", "type": "NumberLiteral", + "value": "1", "span": { "type": "Span", "start": 18, @@ -70,8 +70,8 @@ } }, "value": { - "value": "2", "type": "NumberLiteral", + "value": "2", "span": { "type": "Span", "start": 39, diff --git a/fluent-syntax/test/fixtures_structure/leading_dots.json b/fluent-syntax/test/fixtures_structure/leading_dots.json index 4cc4bf2b7..e11366554 100644 --- a/fluent-syntax/test/fixtures_structure/leading_dots.json +++ b/fluent-syntax/test/fixtures_structure/leading_dots.json @@ -94,8 +94,8 @@ { "type": "Placeable", "expression": { - "value": ".", "type": "StringLiteral", + "value": ".", "span": { "type": "Span", "start": 39, @@ -149,8 +149,8 @@ { "type": "Placeable", "expression": { - "value": ".", "type": "StringLiteral", + "value": ".", "span": { "type": "Span", "start": 62, @@ -213,8 +213,8 @@ { "type": "Placeable", "expression": { - "value": ".", "type": "StringLiteral", + "value": ".", "span": { "type": "Span", "start": 92, @@ -277,8 +277,8 @@ { "type": "Placeable", "expression": { - "value": ".", "type": "StringLiteral", + "value": ".", "span": { "type": "Span", "start": 127, @@ -488,8 +488,8 @@ { "type": "Placeable", "expression": { - "value": ".", "type": "StringLiteral", + "value": ".", "span": { "type": "Span", "start": 429, @@ -670,8 +670,8 @@ { "type": "Placeable", "expression": { - "value": ".", "type": "StringLiteral", + "value": ".", "span": { "type": "Span", "start": 590, @@ -733,8 +733,8 @@ "expression": { "type": "SelectExpression", "selector": { - "value": "1", "type": "NumberLiteral", + "value": "1", "span": { "type": "Span", "start": 615, @@ -796,8 +796,8 @@ { "type": "Placeable", "expression": { - "value": ".", "type": "StringLiteral", + "value": ".", "span": { "type": "Span", "start": 670, diff --git a/fluent-syntax/test/fixtures_structure/term.json b/fluent-syntax/test/fixtures_structure/term.json index b5b17d674..51f17f197 100644 --- a/fluent-syntax/test/fixtures_structure/term.json +++ b/fluent-syntax/test/fixtures_structure/term.json @@ -341,8 +341,8 @@ } }, "value": { - "value": "uppercase", "type": "StringLiteral", + "value": "uppercase", "span": { "type": "Span", "start": 149, diff --git a/fluent-syntax/test/fixtures_structure/variant_with_digit_key.json b/fluent-syntax/test/fixtures_structure/variant_with_digit_key.json index cf96f843a..286cbdb3b 100644 --- a/fluent-syntax/test/fixtures_structure/variant_with_digit_key.json +++ b/fluent-syntax/test/fixtures_structure/variant_with_digit_key.json @@ -40,8 +40,8 @@ { "type": "Variant", "key": { - "value": "-2", "type": "NumberLiteral", + "value": "-2", "span": { "type": "Span", "start": 28, diff --git a/fluent-syntax/test/fixtures_structure/variant_with_empty_pattern.json b/fluent-syntax/test/fixtures_structure/variant_with_empty_pattern.json index c46c71e8c..3c1d56d73 100644 --- a/fluent-syntax/test/fixtures_structure/variant_with_empty_pattern.json +++ b/fluent-syntax/test/fixtures_structure/variant_with_empty_pattern.json @@ -20,8 +20,8 @@ "expression": { "type": "SelectExpression", "selector": { - "value": "1", "type": "NumberLiteral", + "value": "1", "span": { "type": "Span", "start": 13, @@ -46,8 +46,8 @@ { "type": "Placeable", "expression": { - "value": "", "type": "StringLiteral", + "value": "", "span": { "type": "Span", "start": 33, diff --git a/fluent-syntax/test/fixtures_structure/whitespace_leading.json b/fluent-syntax/test/fixtures_structure/whitespace_leading.json index 7678a6030..168eee92f 100644 --- a/fluent-syntax/test/fixtures_structure/whitespace_leading.json +++ b/fluent-syntax/test/fixtures_structure/whitespace_leading.json @@ -110,8 +110,8 @@ { "type": "Placeable", "expression": { - "value": "", "type": "StringLiteral", + "value": "", "span": { "type": "Span", "start": 94, @@ -165,8 +165,8 @@ { "type": "Placeable", "expression": { - "value": " ", "type": "StringLiteral", + "value": " ", "span": { "type": "Span", "start": 120, diff --git a/fluent-syntax/test/fixtures_structure/whitespace_trailing.json b/fluent-syntax/test/fixtures_structure/whitespace_trailing.json index 592d877c6..d87f344c7 100644 --- a/fluent-syntax/test/fixtures_structure/whitespace_trailing.json +++ b/fluent-syntax/test/fixtures_structure/whitespace_trailing.json @@ -192,8 +192,8 @@ { "type": "Placeable", "expression": { - "value": " ", "type": "StringLiteral", + "value": " ", "span": { "type": "Span", "start": 150, diff --git a/fluent-syntax/test/literal_test.js b/fluent-syntax/test/literal_test.js index 62202758f..bbe948a70 100644 --- a/fluent-syntax/test/literal_test.js +++ b/fluent-syntax/test/literal_test.js @@ -1,5 +1,5 @@ import assert from "assert"; -import {FluentParser} from "../src"; +import { FluentParser } from "../esm/parser.js"; const parser = new FluentParser({withSpans: false}); const parseLiteral = input => { diff --git a/fluent-syntax/test/reference_test.js b/fluent-syntax/test/reference_test.js index 39cd7ad32..cc70387f9 100644 --- a/fluent-syntax/test/reference_test.js +++ b/fluent-syntax/test/reference_test.js @@ -2,7 +2,7 @@ import assert from "assert"; import {join} from "path"; import {readdir} from "fs"; import {readfile} from "./util"; -import {FluentParser} from "../src"; +import {FluentParser} from "../esm/parser.js"; const fixtures = join(__dirname, "fixtures_reference"); diff --git a/fluent-syntax/test/serializer_test.js b/fluent-syntax/test/serializer_test.js index a65da9740..809af6cf5 100644 --- a/fluent-syntax/test/serializer_test.js +++ b/fluent-syntax/test/serializer_test.js @@ -1,9 +1,9 @@ import assert from "assert"; import ftl from "@fluent/dedent"; -import { - FluentParser, FluentSerializer, serializeExpression, serializeVariantKey -} from "../src"; +import {FluentParser} from "../esm/parser.js"; +import {FluentSerializer, serializeExpression, serializeVariantKey} + from "../esm/serializer.js"; suite("Serialize resource", function() { @@ -23,9 +23,13 @@ suite("Serialize resource", function() { test("invalid resource", function() { const serializer = new FluentSerializer(); + assert.throws( + () => serializer.serialize(undefined), + /Unknown resource type/ + ); assert.throws( () => serializer.serialize(null), - /Cannot read property 'type'/ + /Unknown resource type/ ); assert.throws( () => serializer.serialize({}), @@ -539,9 +543,13 @@ suite("serializeExpression", function() { }); test("invalid expression", function() { + assert.throws( + () => serializeExpression(undefined), + /Unknown expression type/ + ); assert.throws( () => serializeExpression(null), - /Cannot read property 'type'/ + /Unknown expression type/ ); assert.throws( () => serializeExpression({}), @@ -686,9 +694,13 @@ suite("serializeVariantKey", function() { }); test("invalid expression", function() { + assert.throws( + () => serializeVariantKey(undefined), + /Unknown variant key type/ + ); assert.throws( () => serializeVariantKey(null), - /Cannot read property 'type'/ + /Unknown variant key type/ ); assert.throws( () => serializeVariantKey({}), diff --git a/fluent-syntax/test/stream_test.js b/fluent-syntax/test/stream_test.js index 3c4f1fb32..c8dcc0d7e 100644 --- a/fluent-syntax/test/stream_test.js +++ b/fluent-syntax/test/stream_test.js @@ -1,52 +1,52 @@ "use strict"; import assert from "assert"; -import { ParserStream } from "../src/stream"; +import { ParserStream } from "../esm/stream.js"; suite("ParserStream", function() { test("next", function() { let ps = new ParserStream("abcd"); - assert.strictEqual("a", ps.currentChar); + assert.strictEqual("a", ps.currentChar()); assert.strictEqual(0, ps.index); assert.strictEqual("b", ps.next()); - assert.strictEqual("b", ps.currentChar); + assert.strictEqual("b", ps.currentChar()); assert.strictEqual(1, ps.index); assert.strictEqual("c", ps.next()); - assert.strictEqual("c", ps.currentChar); + assert.strictEqual("c", ps.currentChar()); assert.strictEqual(2, ps.index); assert.strictEqual("d", ps.next()); - assert.strictEqual("d", ps.currentChar); + assert.strictEqual("d", ps.currentChar()); assert.strictEqual(3, ps.index); assert.strictEqual(undefined, ps.next()); - assert.strictEqual(undefined, ps.currentChar); + assert.strictEqual(undefined, ps.currentChar()); assert.strictEqual(4, ps.index); }); test("peek", function() { let ps = new ParserStream("abcd"); - assert.strictEqual("a", ps.currentPeek); + assert.strictEqual("a", ps.currentPeek()); assert.strictEqual(0, ps.peekOffset); assert.strictEqual("b", ps.peek()); - assert.strictEqual("b", ps.currentPeek); + assert.strictEqual("b", ps.currentPeek()); assert.strictEqual(1, ps.peekOffset); assert.strictEqual("c", ps.peek()); - assert.strictEqual("c", ps.currentPeek); + assert.strictEqual("c", ps.currentPeek()); assert.strictEqual(2, ps.peekOffset); assert.strictEqual("d", ps.peek()); - assert.strictEqual("d", ps.currentPeek); + assert.strictEqual("d", ps.currentPeek()); assert.strictEqual(3, ps.peekOffset); assert.strictEqual(undefined, ps.peek()); - assert.strictEqual(undefined, ps.currentPeek); + assert.strictEqual(undefined, ps.currentPeek()); assert.strictEqual(4, ps.peekOffset); }); @@ -68,8 +68,8 @@ suite("ParserStream", function() { assert.strictEqual("c", ps.next()); assert.strictEqual(0, ps.peekOffset); assert.strictEqual(2, ps.index); - assert.strictEqual("c", ps.currentChar); - assert.strictEqual("c", ps.currentPeek); + assert.strictEqual("c", ps.currentChar()); + assert.strictEqual("c", ps.currentPeek()); assert.strictEqual("d", ps.peek()); assert.strictEqual(1, ps.peekOffset); @@ -78,14 +78,14 @@ suite("ParserStream", function() { assert.strictEqual("d", ps.next()); assert.strictEqual(0, ps.peekOffset); assert.strictEqual(3, ps.index); - assert.strictEqual("d", ps.currentChar); - assert.strictEqual("d", ps.currentPeek); + assert.strictEqual("d", ps.currentChar()); + assert.strictEqual("d", ps.currentPeek()); assert.strictEqual(undefined, ps.peek()); assert.strictEqual(1, ps.peekOffset); assert.strictEqual(3, ps.index); - assert.strictEqual("d", ps.currentChar); - assert.strictEqual(undefined, ps.currentPeek); + assert.strictEqual("d", ps.currentChar()); + assert.strictEqual(undefined, ps.currentPeek()); assert.strictEqual(undefined, ps.peek()); assert.strictEqual(2, ps.peekOffset); @@ -104,22 +104,22 @@ suite("ParserStream", function() { ps.skipToPeek(); - assert.strictEqual("c", ps.currentChar); - assert.strictEqual("c", ps.currentPeek); + assert.strictEqual("c", ps.currentChar()); + assert.strictEqual("c", ps.currentPeek()); assert.strictEqual(0, ps.peekOffset); assert.strictEqual(2, ps.index); ps.peek(); - assert.strictEqual("c", ps.currentChar); - assert.strictEqual("d", ps.currentPeek); + assert.strictEqual("c", ps.currentChar()); + assert.strictEqual("d", ps.currentPeek()); assert.strictEqual(1, ps.peekOffset); assert.strictEqual(2, ps.index); ps.next(); - assert.strictEqual("d", ps.currentChar); - assert.strictEqual("d", ps.currentPeek); + assert.strictEqual("d", ps.currentChar()); + assert.strictEqual("d", ps.currentPeek()); assert.strictEqual(0, ps.peekOffset); assert.strictEqual(3, ps.index); }); @@ -132,15 +132,15 @@ suite("ParserStream", function() { ps.peek(); ps.resetPeek(); - assert.strictEqual("b", ps.currentChar); - assert.strictEqual("b", ps.currentPeek); + assert.strictEqual("b", ps.currentChar()); + assert.strictEqual("b", ps.currentPeek()); assert.strictEqual(0, ps.peekOffset); assert.strictEqual(1, ps.index); ps.peek(); - assert.strictEqual("b", ps.currentChar); - assert.strictEqual("c", ps.currentPeek); + assert.strictEqual("b", ps.currentChar()); + assert.strictEqual("c", ps.currentPeek()); assert.strictEqual(1, ps.peekOffset); assert.strictEqual(1, ps.index); @@ -149,14 +149,14 @@ suite("ParserStream", function() { ps.peek(); ps.resetPeek(); - assert.strictEqual("b", ps.currentChar); - assert.strictEqual("b", ps.currentPeek); + assert.strictEqual("b", ps.currentChar()); + assert.strictEqual("b", ps.currentPeek()); assert.strictEqual(0, ps.peekOffset); assert.strictEqual(1, ps.index); assert.strictEqual("c", ps.peek()); - assert.strictEqual("b", ps.currentChar); - assert.strictEqual("c", ps.currentPeek); + assert.strictEqual("b", ps.currentChar()); + assert.strictEqual("c", ps.currentPeek()); assert.strictEqual(1, ps.peekOffset); assert.strictEqual(1, ps.index); diff --git a/fluent-syntax/test/structure_test.js b/fluent-syntax/test/structure_test.js index 417537566..723d2e684 100644 --- a/fluent-syntax/test/structure_test.js +++ b/fluent-syntax/test/structure_test.js @@ -3,7 +3,7 @@ import { join } from "path"; import { readdir } from "fs"; import { readfile } from "./util"; -import { parse } from "../src"; +import { parse } from "../esm/index.js"; const fixtures = join(__dirname, "fixtures_structure"); diff --git a/fluent-syntax/test/visitor_test.js b/fluent-syntax/test/visitor_test.js index e75a023b2..d034cc7e3 100644 --- a/fluent-syntax/test/visitor_test.js +++ b/fluent-syntax/test/visitor_test.js @@ -2,7 +2,8 @@ import assert from "assert"; import ftl from "@fluent/dedent"; -import { FluentParser, Visitor, Transformer } from "../src"; +import { FluentParser } from "../esm/parser.js"; +import { Visitor, Transformer } from "../esm/visitor.js"; suite("Visitor", function() { setup(function() { diff --git a/fluent-syntax/tsconfig.json b/fluent-syntax/tsconfig.json new file mode 100644 index 000000000..45fefaee8 --- /dev/null +++ b/fluent-syntax/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es2019", + "module": "es2015", + "strict": true, + "allowJs": false, + "esModuleInterop": true, + "moduleResolution": "node", + "noEmitHelpers": true, + "declaration": true, + "outDir": "./esm", + }, + "include": [ + "./src/**/*.ts" + ] +} diff --git a/tools/fluentfmt.js b/tools/fluentfmt.js index 04816f67c..0e7402c26 100755 --- a/tools/fluentfmt.js +++ b/tools/fluentfmt.js @@ -6,7 +6,7 @@ const fs = require('fs'); const program = require('commander'); require = require('esm')(module); -const FluentSyntax = require('../fluent-syntax/src'); +const FluentSyntax = require('../fluent-syntax/esm/index.js'); program .version('0.0.1') diff --git a/tools/format.js b/tools/format.js index 0f5038693..2033b59aa 100755 --- a/tools/format.js +++ b/tools/format.js @@ -8,7 +8,7 @@ const program = require('commander'); require = require('esm')(module); require('intl-pluralrules'); -const Fluent = require('../fluent-bundle/src'); +const Fluent = require('../fluent-bundle/esm/index.js'); program .version('0.0.1') diff --git a/tools/fuzz.js b/tools/fuzz.js index dfbe056e9..a3edaea1a 100755 --- a/tools/fuzz.js +++ b/tools/fuzz.js @@ -32,10 +32,10 @@ function fuzz(err, data) { let parse; if (program.runtime) { - let {FluentResource} = require('../fluent-bundle/src'); + let {FluentResource} = require('../fluent-bundle/esm/index.js'); parse = source => new FluentResource(source); } else { - parse = require('../fluent-syntax/src').parse; + parse = require('../fluent-syntax/esm/index.js').parse; } const source = data.toString(); diff --git a/tools/parse.js b/tools/parse.js index 0c3be45a2..088fa2f3a 100755 --- a/tools/parse.js +++ b/tools/parse.js @@ -6,7 +6,7 @@ const fs = require('fs'); const program = require('commander'); require = require('esm')(module); -const FluentSyntax = require('../fluent-syntax/src'); +const FluentSyntax = require('../fluent-syntax/esm/index.js'); program .version('0.0.1') @@ -35,7 +35,7 @@ function print(err, data) { } function printRuntime(data) { - const FluentResource = require('../fluent-bundle/src/resource').default; + const {FluentResource} = require('../fluent-bundle/esm/index.js'); const res = new FluentResource(data.toString()); console.log(JSON.stringify(res, null, 4)); } diff --git a/tools/perf/benchmark.node.js b/tools/perf/benchmark.node.js index 258d1e7d8..3e82c795c 100644 --- a/tools/perf/benchmark.node.js +++ b/tools/perf/benchmark.node.js @@ -1,7 +1,7 @@ const fs = require('fs'); -const FluentBundle = require('../../fluent-bundle'); -const FluentSyntax = require('../../fluent-syntax'); +const FluentBundle = require('../../fluent-bundle/index.js'); +const FluentSyntax = require('../../fluent-syntax/index.js'); const { runTest } = require('./benchmark.common'); require('intl-pluralrules');