Skip to content

Commit

Permalink
Add namespaces (declare + nested) (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
nahkd123 authored Jul 9, 2023
1 parent 84e1850 commit d9d203c
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 71 deletions.
34 changes: 10 additions & 24 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,18 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 19
- run: |
- name: Build Translator
run: |
npm install
npm run build
- name: Upload Artifact
- name: Run test (npm)
run: npm test
- name: Run test (sample project)
run: |
node ./dist/cli.js example/example.json
cat example/compile/en-us.json
- name: Upload Translator Artifact
uses: actions/upload-artifact@v3
with:
name: Translator
path: dist/
test-build:
name: Test 1
runs-on: ubuntu-latest
needs: [build-translator]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 19
- name: Download Translator
uses: actions/download-artifact@v3
with:
name: Translator
path: dist/
- name: Build example
run: node dist/cli.js example/example.json
- name: Upload build result
uses: actions/upload-artifact@v3
with:
name: Test Result 1
path: example/compile/
path: dist/
6 changes: 6 additions & 0 deletions example/en-us.lang
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import "./en-gb.lang";

namespace mynamespace;

// Comment
/* Comment */
example.key "Hello world!";
example.key2(a1, a2) "abc " a1 a2 " def " a1;

namespace hi {
key "mynamespace.hi.key";
}
2 changes: 1 addition & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ const compiler = new(class extends Compiler {

const fromParent = path.resolve(from, "..");
const importPath = path.isAbsolute(p)? p : path.resolve(fromParent, p);
return await this.compileFromText(await fs.promises.readFile(importPath, "utf-8"), importPath); // TODO: Read from JSON file
return await this.compileFromText(await fs.promises.readFile(importPath, "utf-8"), "", importPath); // TODO: Read from JSON file
}
})();

Expand Down
33 changes: 20 additions & 13 deletions src/compiler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Parser, Statement } from "./parser";
import { IStatement, Parser, Statement } from "./parser";
import { Token } from "./tokens";
import { TokensEmitter } from "./tokensemitter";
import { Translations } from "./translations";
Expand All @@ -9,10 +9,10 @@ export class Compiler {
throw new Error(`Missing implementation. Please extends Compiler and implement your own resolveImport().`);
}

async compileFromStatements(statements: Statement[], currentPath?: string): Promise<Translations>;
async compileFromStatements(parser: Parser, currentPath?: string): Promise<Translations>;
async compileFromStatements(a: Statement[] | Parser, currentPath = "."): Promise<Translations> {
if (a instanceof Parser) return this.compileFromStatements(a.statements, currentPath);
async compileFromStatements(statements: Statement[], keyPrefix?: string, currentPath?: string): Promise<Translations>;
async compileFromStatements(parser: Parser, keyPrefix?: string, currentPath?: string): Promise<Translations>;
async compileFromStatements(a: Statement[] | Parser, keyPrefix = "", currentPath = "."): Promise<Translations> {
if (a instanceof Parser) return this.compileFromStatements(a.statements, keyPrefix, currentPath);

let result: Translations = {};

Expand All @@ -23,26 +23,33 @@ export class Compiler {
const imported = await this.resolveImport(s.path, currentPath);
for (let key in imported) result[key] = imported[key];
} else if (s.type == "translation") {
result[s.key] = s.line;
result[keyPrefix + s.key] = s.line;
} else if (s.type == "namespace-declare") {
keyPrefix += s.name + ".";
} else if (s.type == "namespace-nested") {
const child = await this.compileFromStatements(s.children, keyPrefix + s.name + ".", currentPath);
for (let key in child) result[key] = child[key];
} else {
throw new Error(`Unknown statement: ${(s as IStatement<any>).type}`);
}
}

return result;
}

async compileFromTokens(tokens: Token[], currentPath?: string): Promise<Translations>;
async compileFromTokens(emitter: TokensEmitter, currentPath?: string): Promise<Translations>;
async compileFromTokens(a: Token[] | TokensEmitter, currentPath?: string): Promise<Translations> {
if (a instanceof TokensEmitter) return this.compileFromTokens(a.tokens, currentPath);
async compileFromTokens(tokens: Token[], keyPrefix?: string, currentPath?: string): Promise<Translations>;
async compileFromTokens(emitter: TokensEmitter, keyPrefix?: string, currentPath?: string): Promise<Translations>;
async compileFromTokens(a: Token[] | TokensEmitter, keyPrefix?: string, currentPath?: string): Promise<Translations> {
if (a instanceof TokensEmitter) return this.compileFromTokens(a.tokens, keyPrefix, currentPath);

let parser = new Parser();
parser.accept(a);
return this.compileFromStatements(parser, currentPath);
return this.compileFromStatements(parser, keyPrefix, currentPath);
}

async compileFromText(text: string, currentPath?: string): Promise<Translations> {
async compileFromText(text: string, keyPrefix?: string, currentPath?: string): Promise<Translations> {
let emitter = new TokensEmitter();
emitter.accept(text);
return this.compileFromTokens(emitter, currentPath);
return this.compileFromTokens(emitter, keyPrefix, currentPath);
}
}
47 changes: 43 additions & 4 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,38 @@ export interface TranslationStatement extends IStatement<"translation"> {
line: TranslatedLine;
}

export type Statement = ImportStatement | TranslationStatement;
export interface NamespaceDeclare extends IStatement<"namespace-declare"> {
name: string;
}

export interface NamespaceNested extends IStatement<"namespace-nested"> {
name: string;
children: Statement[];
}

export type Statement = ImportStatement | TranslationStatement | NamespaceDeclare | NamespaceNested;

export class Parser {
statements: Statement[] = [];

accept(tokens: Token[]): void;
accept(tokens: Token[], onInvaild?: (tokens: Token[]) => boolean): void;
accept(emitter: TokensEmitter): void;
accept(a: Token[] | TokensEmitter) {
accept(a: Token[] | TokensEmitter, onInvaild = (tokens: Token[]) => false) {
if (a instanceof TokensEmitter) return this.accept(a.tokens);

while (a.length > 0) {
let statement: Statement;

if (
(statement = this.#acceptImport(a)) ||
(statement = this.#acceptNamespace(a)) ||
(statement = this.#acceptTranslation(a))
) {
this.statements.push(statement);
continue;
} else {
throw new Error(`Unexpected token: ${a[0].type}`);
if (!onInvaild(a)) throw new Error(`Unexpected token: ${a[0].type}`);
return;
}
}
}
Expand All @@ -53,6 +64,34 @@ export class Parser {
}
}

#acceptNamespace(tokens: Token[]) {
if (tokens[0].type == "keyword" && tokens[0].keyword == "namespace") {
tokens.shift();
const a = tokens.shift();
if (a.type != "symbol") throw new Error(`Expected symbol but found ${a.type}`);
const name = a.name;
const b = tokens.shift();

if (b.type == "bracket" && b.bracket == "spike" && b.mode == "open") {
const parser = new Parser();
parser.accept(tokens, t => {
if (t[0].type == "bracket" && t[0].bracket == "spike" && t[0].mode == "close") {
t.shift();
return true;
}

return false;
});

return <Statement> { type: "namespace-nested", name, children: parser.statements };
} else if (b.type == "keyword" && b.keyword == "end-of-statement") {
return <Statement> { type: "namespace-declare", name };
} else {
throw new Error(`Expected ({) or (;) but found ${b.type}`);
}
}
}

#acceptTranslation(tokens: Token[]) {
if (tokens[0].type == "symbol") {
const key = tokens[0].name;
Expand Down
85 changes: 59 additions & 26 deletions src/test.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,69 @@
import { Compiler } from "./compiler";
import { Parser } from "./parser";
import { TokensEmitter } from "./tokensemitter";
import { Translations, TranslationsEngine } from "./translations";
import * as path from "node:path";
import * as fs from "node:fs";

let te = new TokensEmitter();
te.accept(`
import "./hello_world.lang";
import "./sus.lang";
// Comment
/* Comment */
example.first(playerName, arg2) "Hello " /* Comment in da middle! */ playerName " and welcome to our server!";
example.second "cool";
example.third;
`);
console.log(te);
async function compile(input: string) {
const compiler = new (class extends Compiler {
override async resolveImport(p: string, importFrom: string): Promise<Translations> {
const target = path.resolve(importFrom, "..", p);
return this.compileFromText(await fs.promises.readFile(target, "utf-8"), "", target);
}
})();
const result = await compiler.compileFromText(input, "", __filename);
return new TranslationsEngine(result);
}

let parser = new Parser();
parser.accept(te);
console.log(parser.statements);
function assert(input: string, expected: string) {
if (input != expected) {
process.stderr.write(`\x1b[91mTEST ERROR: \x1b[0m'${input}' != '${expected}'\x1b[0m\n`);
process.exit(1);
}
}

let compiler = new(class extends Compiler {
override async resolveImport(path: string): Promise<Translations> {
return {};
function exists(a: any) {
if (a == null) {
process.stderr.write(`\x1b[91mTEST ERROR: \x1b[0mNon-existent/null/undefined object\x1b[0m\n`);
process.exit(1);
}
})();
}

async function main() {
let translations = await compiler.compileFromStatements(parser);
let engine = new TranslationsEngine(translations);
console.log(engine.translate("example.first", "nahkd123"));
console.log(engine.convertToMC()["example.first"]);
let all = await Promise.all([
compile(`
test "a";
test.nothing;
test.single "a";
test.one_argument(a) "Value is " a "!";
test.two_arguments(a, b) "We got " a " then " b " and back to " a " and " a " again.";
`).then(t => {
assert(t.translate("test"), "a");
assert(t.translate("test.nothing"), "");
assert(t.translate("test.single"), "a");
assert(t.translate("test.one_argument", "12345"), "Value is 12345!");
assert(t.translate("test.two_arguments", "123", "abc"), "We got 123 then abc and back to 123 and 123 again.");
}),
compile(`
import "../example/en-us.lang";
namespace separateNamespace.abcdef;
key "separateNamespace.abcdef";
key2(a) "Value is " a "!";
key3(a, b) a " then " b " and then " a " and " a " again";
`).then(t => {
exists(t.translations["separateNamespace.abcdef.key"]);
exists(t.translations["mynamespace.example.key"]);
exists(t.translations["mynamespace.hi.key"]);

assert(t.translate("example.engb"), "12345");
assert(t.translate("mynamespace.example.key2", "A", "B"), "abc AB def A");
assert(t.translate("mynamespace.hi.key"), "mynamespace.hi.key");
})
]);

process.stdout.write(`\x1b[92mTests passed! (${all.length} tests)\x1b[0m\n`);
process.exit(0);
}

main();
4 changes: 2 additions & 2 deletions src/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface Keyword extends IToken<"keyword"> {
keyword: AllKeywords;
}

export type AllKeywords = "import" | "end-of-statement" | "comma";
export type AllKeywords = "import" | "namespace" | "end-of-statement" | "comma";

export interface Symbol extends IToken<"symbol"> {
name: string;
Expand All @@ -17,7 +17,7 @@ export interface StringToken extends IToken<"string"> {
}

export interface Bracket extends IToken<"bracket"> {
bracket: "round";
bracket: "round" | "spike";
mode: "open" | "close";
}

Expand Down
4 changes: 3 additions & 1 deletion src/tokensemitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class TokensEmitter {
}

#acceptKeyword(ts: TextStream) {
let result = ts.next(/^(import|;|,)/);
let result = ts.next(/^(import|namespace|;|,)/);
if (result) return <Token> { type: "keyword", keyword: mapping[result[0]] ?? result[0] };
return null;
}
Expand Down Expand Up @@ -76,6 +76,8 @@ export class TokensEmitter {
#acceptBracket(ts: TextStream) {
if (ts.next(/^\(/)) return <Token> { type: "bracket", bracket: "round", mode: "open" };
if (ts.next(/^\)/)) return <Token> { type: "bracket", bracket: "round", mode: "close" };
if (ts.next(/^\{/)) return <Token> { type: "bracket", bracket: "spike", mode: "open" };
if (ts.next(/^\}/)) return <Token> { type: "bracket", bracket: "spike", mode: "close" };
return null;
}
}
Expand Down

0 comments on commit d9d203c

Please sign in to comment.