diff --git a/fluent-langneg/package-lock.json b/fluent-langneg/package-lock.json index 25ffe7a33..e120dade1 100644 --- a/fluent-langneg/package-lock.json +++ b/fluent-langneg/package-lock.json @@ -1,5 +1,5 @@ { "name": "@fluent/langneg", - "version": "0.4.0", + "version": "0.5.0", "lockfileVersion": 1 } diff --git a/fluent-langneg/src/index.ts b/fluent-langneg/src/index.ts index 93a949e81..a6fa44ffa 100644 --- a/fluent-langneg/src/index.ts +++ b/fluent-langneg/src/index.ts @@ -7,6 +7,8 @@ * */ -export {negotiateLanguages} from "./negotiate_languages"; +export { + negotiateLanguages, NegotiateLanguagesOptions +} from "./negotiate_languages"; export {acceptedLanguages} from "./accepted_languages"; export {filterMatches} from "./matches"; diff --git a/fluent-langneg/src/negotiate_languages.ts b/fluent-langneg/src/negotiate_languages.ts index 121590ea9..2730be208 100644 --- a/fluent-langneg/src/negotiate_languages.ts +++ b/fluent-langneg/src/negotiate_languages.ts @@ -1,6 +1,6 @@ import {filterMatches} from "./matches"; -interface NegotiateLanguagesOptions { +export interface NegotiateLanguagesOptions { strategy?: "filtering" | "matching" | "lookup"; defaultLocale?: string; } @@ -49,8 +49,8 @@ interface NegotiateLanguagesOptions { * This strategy requires defaultLocale option to be set. */ export function negotiateLanguages( - requestedLocales: Array, - availableLocales: Array, + requestedLocales: Readonly>, + availableLocales: Readonly>, { strategy = "filtering", defaultLocale, diff --git a/fluent-syntax/package-lock.json b/fluent-syntax/package-lock.json index d73eeeef3..115fbdd5e 100644 --- a/fluent-syntax/package-lock.json +++ b/fluent-syntax/package-lock.json @@ -1,6 +1,6 @@ { "name": "@fluent/syntax", - "version": "0.15.0", + "version": "0.16.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/fluent-syntax/src/ast.ts b/fluent-syntax/src/ast.ts index 2099a2da2..dedba4df1 100644 --- a/fluent-syntax/src/ast.ts +++ b/fluent-syntax/src/ast.ts @@ -6,7 +6,6 @@ * */ export abstract class BaseNode { - public type = "BaseNode"; [name: string]: unknown; equals(other: BaseNode, ignoredFields: Array = ["span"]): boolean { @@ -46,7 +45,7 @@ export abstract class BaseNode { return true; } - clone(): BaseNode { + clone(): this { function visit(value: unknown): unknown { if (value instanceof BaseNode) { return value.clone(); @@ -79,7 +78,6 @@ function scalarsEqual( * Base class for AST nodes which can have Spans. */ export abstract class SyntaxNode extends BaseNode { - public type = "SyntaxNode"; public span?: Span; addSpan(start: number, end: number): void { @@ -89,21 +87,20 @@ export abstract class SyntaxNode extends BaseNode { export class Resource extends SyntaxNode { public type = "Resource" as const; - public body: Array; - constructor(body: Array = []) { + public body: Array; + constructor(body: Array = []) { super(); this.body = body; } } -/* - * An abstract base class for useful elements of Resource.body. - */ -export abstract class Entry extends SyntaxNode { - public type = "Entry"; -} +export declare type Entry = + Message | + Term | + Comments | + Junk; -export class Message extends Entry { +export class Message extends SyntaxNode { public type = "Message" as const; public id: Identifier; public value: Pattern | null; @@ -124,7 +121,7 @@ export class Message extends Entry { } } -export class Term extends Entry { +export class Term extends SyntaxNode { public type = "Term" as const; public id: Identifier; public value: Pattern; @@ -155,14 +152,9 @@ export class Pattern extends SyntaxNode { } } -/* - * An abstract base class for elements of Patterns. - */ -export abstract class PatternElement extends SyntaxNode { - public type = "PatternElement"; -} +export declare type PatternElement = TextElement | Placeable; -export class TextElement extends PatternElement { +export class TextElement extends SyntaxNode { public type = "TextElement" as const; public value: string; @@ -172,7 +164,7 @@ export class TextElement extends PatternElement { } } -export class Placeable extends PatternElement { +export class Placeable extends SyntaxNode { public type = "Placeable" as const; public expression: Expression; @@ -182,16 +174,21 @@ export class Placeable extends PatternElement { } } -/* - * An abstract base class for expressions. +/** + * A subset of expressions which can be used as outside of Placeables. */ -export abstract class Expression extends SyntaxNode { - public type = "Expression"; -} +export type InlineExpression = + StringLiteral | + NumberLiteral | + FunctionReference | + MessageReference | + TermReference | + VariableReference | + Placeable; +export declare type Expression = InlineExpression | SelectExpression; // An abstract base class for Literals. -export abstract class Literal extends Expression { - public type = "Literal"; +export abstract class BaseLiteral extends SyntaxNode { public value: string; constructor(value: string) { @@ -204,7 +201,7 @@ export abstract class Literal extends Expression { abstract parse(): { value: unknown } } -export class StringLiteral extends Literal { +export class StringLiteral extends BaseLiteral { public type = "StringLiteral" as const; parse(): { value: string } { @@ -241,7 +238,7 @@ export class StringLiteral extends Literal { } } -export class NumberLiteral extends Literal { +export class NumberLiteral extends BaseLiteral { public type = "NumberLiteral" as const; parse(): { value: number; precision: number } { @@ -254,7 +251,9 @@ export class NumberLiteral extends Literal { } } -export class MessageReference extends Expression { +export declare type Literal = StringLiteral | NumberLiteral; + +export class MessageReference extends SyntaxNode { public type = "MessageReference" as const; public id: Identifier; public attribute: Identifier | null; @@ -266,7 +265,7 @@ export class MessageReference extends Expression { } } -export class TermReference extends Expression { +export class TermReference extends SyntaxNode { public type = "TermReference" as const; public id: Identifier; public attribute: Identifier | null; @@ -284,7 +283,7 @@ export class TermReference extends Expression { } } -export class VariableReference extends Expression { +export class VariableReference extends SyntaxNode { public type = "VariableReference" as const; public id: Identifier; @@ -294,7 +293,7 @@ export class VariableReference extends Expression { } } -export class FunctionReference extends Expression { +export class FunctionReference extends SyntaxNode { public type = "FunctionReference" as const; public id: Identifier; public arguments: CallArguments; @@ -306,12 +305,12 @@ export class FunctionReference extends Expression { } } -export class SelectExpression extends Expression { +export class SelectExpression extends SyntaxNode { public type = "SelectExpression" as const; - public selector: Expression; + public selector: InlineExpression; public variants: Array; - constructor(selector: Expression, variants: Array) { + constructor(selector: InlineExpression, variants: Array) { super(); this.selector = selector; this.variants = variants; @@ -320,11 +319,11 @@ export class SelectExpression extends Expression { export class CallArguments extends SyntaxNode { public type = "CallArguments" as const; - public positional: Array; + public positional: Array; public named: Array; constructor( - positional: Array = [], + positional: Array = [], named: Array = [] ) { super(); @@ -381,8 +380,7 @@ export class Identifier extends SyntaxNode { } } -export abstract class BaseComment extends Entry { - public type = "BaseComment"; +export abstract class BaseComment extends SyntaxNode { public content: string; constructor(content: string) { super(); @@ -401,6 +399,11 @@ export class ResourceComment extends BaseComment { public type = "ResourceComment" as const; } +export declare type Comments = + Comment | + GroupComment | + ResourceComment; + export class Junk extends SyntaxNode { public type = "Junk" as const; public annotations: Array; @@ -418,7 +421,7 @@ export class Junk extends SyntaxNode { } export class Span extends BaseNode { - public type = "Span"; + public type = "Span" as const; public start: number; public end: number; @@ -430,7 +433,7 @@ export class Span extends BaseNode { } export class Annotation extends SyntaxNode { - public type = "Annotation"; + public type = "Annotation" as const; public code: string; public arguments: Array; public message: string; diff --git a/fluent-syntax/src/parser.ts b/fluent-syntax/src/parser.ts index fa6862d3e..1a6b4efb0 100644 --- a/fluent-syntax/src/parser.ts +++ b/fluent-syntax/src/parser.ts @@ -1,6 +1,8 @@ /* eslint no-magic-numbers: [0] */ import * as AST from "./ast.js"; +// eslint-disable-next-line no-duplicate-imports +import type {Resource, Entry} from "./ast.js"; import { EOF, EOL, FluentParserStream } from "./stream.js"; import { ParseError } from "./errors.js"; @@ -12,10 +14,6 @@ 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, @@ -73,11 +71,11 @@ export class FluentParser { /* eslint-enable @typescript-eslint/unbound-method */ } - parse(source: string): AST.Resource { + parse(source: string): Resource { const ps = new FluentParserStream(source); ps.skipBlankBlock(); - const entries: Array = []; + const entries: Array = []; let lastComment: AST.Comment | null = null; while (ps.currentChar()) { @@ -133,7 +131,7 @@ export class FluentParser { * Preceding comments are ignored unless they contain syntax errors * themselves, in which case Junk for the invalid comment is returned. */ - parseEntry(source: string): EntryOrJunk { + parseEntry(source: string): Entry { const ps = new FluentParserStream(source); ps.skipBlankBlock(); @@ -149,7 +147,7 @@ export class FluentParser { return this.getEntryOrJunk(ps); } - getEntryOrJunk(ps: FluentParserStream): EntryOrJunk { + getEntryOrJunk(ps: FluentParserStream): AST.Entry { const entryStartPos = ps.index; try { @@ -182,7 +180,7 @@ export class FluentParser { } } - getEntry(ps: FluentParserStream): Entry { + getEntry(ps: FluentParserStream): AST.Entry { if (ps.currentChar() === "#") { return this.getComment(ps); } @@ -198,7 +196,7 @@ export class FluentParser { throw new ParseError("E0002"); } - getComment(ps: FluentParserStream): Comment { + getComment(ps: FluentParserStream): AST.Comments { // 0 - comment // 1 - group comment // 2 - resource comment @@ -669,7 +667,9 @@ export class FluentParser { return selector; } - getInlineExpression(ps: FluentParserStream): AST.Expression | AST.Placeable { + getInlineExpression( + ps: FluentParserStream + ): AST.InlineExpression | AST.Placeable { if (ps.currentChar() === "{") { return this.getPlaceable(ps); } @@ -736,7 +736,9 @@ export class FluentParser { throw new ParseError("E0028"); } - getCallArgument(ps: FluentParserStream): AST.Expression | AST.NamedArgument { + getCallArgument( + ps: FluentParserStream + ): AST.InlineExpression | AST.NamedArgument { const exp = this.getInlineExpression(ps); ps.skipBlank(); @@ -757,7 +759,7 @@ export class FluentParser { } getCallArguments(ps: FluentParserStream): AST.CallArguments { - const positional: Array = []; + const positional: Array = []; const named: Array = []; const argumentNames: Set = new Set(); diff --git a/fluent-syntax/src/serializer.ts b/fluent-syntax/src/serializer.ts index 8bb6483ff..e3216f948 100644 --- a/fluent-syntax/src/serializer.ts +++ b/fluent-syntax/src/serializer.ts @@ -1,4 +1,6 @@ import * as AST from "./ast.js"; +// eslint-disable-next-line no-duplicate-imports +import type {Resource, Entry, Expression, Placeable} from "./ast.js"; function indentExceptFirstLine(content: string): string { return content.split("\n").join("\n "); @@ -37,7 +39,7 @@ function shouldStartOnNewLine(pattern: AST.Pattern): boolean { // Bit masks representing the state of the serializer. -export const HAS_ENTRIES = 1; +const HAS_ENTRIES = 1; export interface FluentSerializerOptions { withJunk?: boolean; @@ -50,7 +52,7 @@ export class FluentSerializer { this.withJunk = withJunk; } - serialize(resource: AST.Resource): string { + serialize(resource: Resource): string { if (!(resource instanceof AST.Resource)) { throw new Error(`Unknown resource type: ${resource}`); } @@ -70,7 +72,7 @@ export class FluentSerializer { return parts.join(""); } - serializeEntry(entry: AST.Entry | AST.Junk, state: number = 0): string { + serializeEntry(entry: Entry, state: number = 0): string { if (entry instanceof AST.Message) { return serializeMessage(entry); } @@ -202,7 +204,9 @@ function serializePlaceable(placeable: AST.Placeable): string { } -export function serializeExpression(expr: AST.Expression): string { +export function serializeExpression( + expr: Expression | Placeable +): string { if (expr instanceof AST.StringLiteral) { return `"${expr.value}"`; } diff --git a/fluent-syntax/src/visitor.ts b/fluent-syntax/src/visitor.ts index 18682185e..5acf13152 100644 --- a/fluent-syntax/src/visitor.ts +++ b/fluent-syntax/src/visitor.ts @@ -1,4 +1,4 @@ -import { BaseNode } from "./ast.js"; +import * as AST from "./ast.js"; /** * A read-only visitor. @@ -11,13 +11,13 @@ import { BaseNode } from "./ast.js"; * Visiting methods must implement the following interface: * * interface VisitingMethod { - * (this: Visitor, node: BaseNode): void; + * (this: Visitor, node: AST.BaseNode): void; * } */ export abstract class Visitor { [prop: string]: unknown; - visit(node: BaseNode): void { + visit(node: AST.BaseNode): void { let visit = this[`visit${node.type}`]; if (typeof visit === "function") { visit.call(this, node); @@ -26,43 +26,43 @@ export abstract class Visitor { } } - genericVisit(node: BaseNode): void { + genericVisit(node: AST.BaseNode): void { for (const key of Object.keys(node)) { let prop = node[key]; - if (prop instanceof BaseNode) { + if (prop instanceof AST.BaseNode) { this.visit(prop); } else if (Array.isArray(prop)) { for (let element of prop) { - this.visit(element as BaseNode); + this.visit(element as AST.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; + visitResource?(node: AST.Resource): void; + visitMessage?(node: AST.Message): void; + visitTerm?(node: AST.Term): void; + visitPattern?(node: AST.Pattern): void; + visitTextElement?(node: AST.TextElement): void; + visitPlaceable?(node: AST.Placeable): void; + visitStringLiteral?(node: AST.StringLiteral): void; + visitNumberLiteral?(node: AST.NumberLiteral): void; + visitMessageReference?(node: AST.MessageReference): void; + visitTermReference?(node: AST.TermReference): void; + visitVariableReference?(node: AST.VariableReference): void; + visitFunctionReference?(node: AST.FunctionReference): void; + visitSelectExpression?(node: AST.SelectExpression): void; + visitCallArguments?(node: AST.CallArguments): void; + visitAttribute?(node: AST.Attribute): void; + visitVariant?(node: AST.Variant): void; + visitNamedArgument?(node: AST.NamedArgument): void; + visitIdentifier?(node: AST.Identifier): void; + visitComment?(node: AST.Comment): void; + visitGroupComment?(node: AST.GroupComment): void; + visitResourceComment?(node: AST.ResourceComment): void; + visitJunk?(node: AST.Junk): void; + visitSpan?(node: AST.Span): void; + visitAnnotation?(node: AST.Annotation): void; } /** @@ -76,16 +76,16 @@ export abstract class Visitor { * Visiting methods must implement the following interface: * * interface TransformingMethod { - * (this: Transformer, node: BaseNode): BaseNode | undefined; + * (this: Transformer, node: AST.BaseNode): AST.BaseNode | undefined; * } * - * The returned node wili replace the original one in the AST. Return + * The returned node will 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 { + visit(node: AST.BaseNode): AST.BaseNode | undefined { let visit = this[`visit${node.type}`]; if (typeof visit === "function") { return visit.call(this, node); @@ -93,10 +93,10 @@ export abstract class Transformer extends Visitor { return this.genericVisit(node); } - genericVisit(node: BaseNode): BaseNode | undefined { + genericVisit(node: AST.BaseNode): AST.BaseNode { for (const key of Object.keys(node)) { let prop = node[key]; - if (prop instanceof BaseNode) { + if (prop instanceof AST.BaseNode) { let newVal = this.visit(prop); if (newVal === undefined) { delete node[key]; @@ -104,7 +104,7 @@ export abstract class Transformer extends Visitor { node[key] = newVal; } } else if (Array.isArray(prop)) { - let newVals: Array = []; + let newVals: Array = []; for (let element of prop) { let newVal = this.visit(element); if (newVal !== undefined) { @@ -117,28 +117,30 @@ export abstract class Transformer extends Visitor { 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; + visitResource?(node: AST.Resource): AST.BaseNode | undefined; + visitMessage?(node: AST.Message): AST.BaseNode | undefined; + visitTerm?(node: AST.Term): AST.BaseNode | undefined; + visitPattern?(node: AST.Pattern): AST.BaseNode | undefined; + visitTextElement?(node: AST.TextElement): AST.BaseNode | undefined; + visitPlaceable?(node: AST.Placeable): AST.BaseNode | undefined; + visitStringLiteral?(node: AST.StringLiteral): AST.BaseNode | undefined; + visitNumberLiteral?(node: AST.NumberLiteral): AST.BaseNode | undefined; + visitMessageReference?(node: AST.MessageReference): AST.BaseNode | undefined; + visitTermReference?(node: AST.TermReference): AST.BaseNode | undefined; + visitVariableReference?(node: AST.VariableReference): + AST.BaseNode | undefined; + visitFunctionReference?(node: AST.FunctionReference): + AST.BaseNode | undefined; + visitSelectExpression?(node: AST.SelectExpression): AST.BaseNode | undefined; + visitCallArguments?(node: AST.CallArguments): AST.BaseNode | undefined; + visitAttribute?(node: AST.Attribute): AST.BaseNode | undefined; + visitVariant?(node: AST.Variant): AST.BaseNode | undefined; + visitNamedArgument?(node: AST.NamedArgument): AST.BaseNode | undefined; + visitIdentifier?(node: AST.Identifier): AST.BaseNode | undefined; + visitComment?(node: AST.Comment): AST.BaseNode | undefined; + visitGroupComment?(node: AST.GroupComment): AST.BaseNode | undefined; + visitResourceComment?(node: AST.ResourceComment): AST.BaseNode | undefined; + visitJunk?(node: AST.Junk): AST.BaseNode | undefined; + visitSpan?(node: AST.Span): AST.BaseNode | undefined; + visitAnnotation?(node: AST.Annotation): AST.BaseNode | undefined; } diff --git a/fluent-syntax/test/serializer_test.js b/fluent-syntax/test/serializer_test.js index b3b0862b8..bc01a3be0 100644 --- a/fluent-syntax/test/serializer_test.js +++ b/fluent-syntax/test/serializer_test.js @@ -566,10 +566,10 @@ suite("Serialize resource", function() { }); suite("serializeExpression", function() { - let pretty; + let pretty, parser; setup(function() { - const parser = new FluentParser(); + parser = new FluentParser(); pretty = function pretty(text) { const {value: {elements: [placeable]}} = parser.parseEntry(text); @@ -650,6 +650,11 @@ suite("serializeExpression", function() { `; assert.strictEqual(pretty(input), "$num ->\n *[one] One\n"); }); + + test("Placeable", function() { + const {value: {elements: [placeable]}} = parser.parseEntry("foo = {5}"); + assert.strictEqual(serializeExpression(placeable), "{ 5 }"); + }) }); suite("Serialize padding around comments", function() {