diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc248b4..d628435 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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/ \ No newline at end of file + path: dist/ \ No newline at end of file diff --git a/example/en-us.lang b/example/en-us.lang index 86f8315..09decc8 100644 --- a/example/en-us.lang +++ b/example/en-us.lang @@ -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"; +} diff --git a/src/cli.ts b/src/cli.ts index 5eb1744..de02681 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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 } })(); diff --git a/src/compiler.ts b/src/compiler.ts index fddd7c8..859d7dd 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -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"; @@ -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; - async compileFromStatements(parser: Parser, currentPath?: string): Promise; - async compileFromStatements(a: Statement[] | Parser, currentPath = "."): Promise { - if (a instanceof Parser) return this.compileFromStatements(a.statements, currentPath); + async compileFromStatements(statements: Statement[], keyPrefix?: string, currentPath?: string): Promise; + async compileFromStatements(parser: Parser, keyPrefix?: string, currentPath?: string): Promise; + async compileFromStatements(a: Statement[] | Parser, keyPrefix = "", currentPath = "."): Promise { + if (a instanceof Parser) return this.compileFromStatements(a.statements, keyPrefix, currentPath); let result: Translations = {}; @@ -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).type}`); } } return result; } - async compileFromTokens(tokens: Token[], currentPath?: string): Promise; - async compileFromTokens(emitter: TokensEmitter, currentPath?: string): Promise; - async compileFromTokens(a: Token[] | TokensEmitter, currentPath?: string): Promise { - if (a instanceof TokensEmitter) return this.compileFromTokens(a.tokens, currentPath); + async compileFromTokens(tokens: Token[], keyPrefix?: string, currentPath?: string): Promise; + async compileFromTokens(emitter: TokensEmitter, keyPrefix?: string, currentPath?: string): Promise; + async compileFromTokens(a: Token[] | TokensEmitter, keyPrefix?: string, currentPath?: string): Promise { + 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 { + async compileFromText(text: string, keyPrefix?: string, currentPath?: string): Promise { let emitter = new TokensEmitter(); emitter.accept(text); - return this.compileFromTokens(emitter, currentPath); + return this.compileFromTokens(emitter, keyPrefix, currentPath); } } \ No newline at end of file diff --git a/src/parser.ts b/src/parser.ts index a1f3e7f..25f1ac6 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -15,14 +15,23 @@ 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) { @@ -30,12 +39,14 @@ export class Parser { 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; } } } @@ -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 { type: "namespace-nested", name, children: parser.statements }; + } else if (b.type == "keyword" && b.keyword == "end-of-statement") { + return { 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; diff --git a/src/test.ts b/src/test.ts index 26b9ac0..c0b2a05 100644 --- a/src/test.ts +++ b/src/test.ts @@ -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 { + 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 { - 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(); \ No newline at end of file diff --git a/src/tokens.ts b/src/tokens.ts index 1418d8a..4a20386 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -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; @@ -17,7 +17,7 @@ export interface StringToken extends IToken<"string"> { } export interface Bracket extends IToken<"bracket"> { - bracket: "round"; + bracket: "round" | "spike"; mode: "open" | "close"; } diff --git a/src/tokensemitter.ts b/src/tokensemitter.ts index 357e36a..1fb0036 100644 --- a/src/tokensemitter.ts +++ b/src/tokensemitter.ts @@ -35,7 +35,7 @@ export class TokensEmitter { } #acceptKeyword(ts: TextStream) { - let result = ts.next(/^(import|;|,)/); + let result = ts.next(/^(import|namespace|;|,)/); if (result) return { type: "keyword", keyword: mapping[result[0]] ?? result[0] }; return null; } @@ -76,6 +76,8 @@ export class TokensEmitter { #acceptBracket(ts: TextStream) { if (ts.next(/^\(/)) return { type: "bracket", bracket: "round", mode: "open" }; if (ts.next(/^\)/)) return { type: "bracket", bracket: "round", mode: "close" }; + if (ts.next(/^\{/)) return { type: "bracket", bracket: "spike", mode: "open" }; + if (ts.next(/^\}/)) return { type: "bracket", bracket: "spike", mode: "close" }; return null; } }