parent
6094cc8891
commit
06fffb7b2b
@ -0,0 +1,103 @@ |
|||||||
|
import { |
||||||
|
battlerAttribute, |
||||||
|
BattlerAttributeType, |
||||||
|
BattlerDeclaration, |
||||||
|
battlerDeclaration, |
||||||
|
BattlerStatement, |
||||||
|
BattlerStatementType, |
||||||
|
isBattlerAttribute, |
||||||
|
isBattlerDeclaration |
||||||
|
} from "./BattlerStatement"; |
||||||
|
import {TopLevelStatementType} from "./TopLevelStatement"; |
||||||
|
import {ScriptFile, scriptFile} from "./ScriptFile"; |
||||||
|
import {parse as parseInternal, SyntaxError} from "./NomScript.peggy"; |
||||||
|
import {numberValue} from "./ScriptValue"; |
||||||
|
|
||||||
|
function parse(text: string): ScriptFile { |
||||||
|
return parseInternal(text, {start: "ScriptFile", grammarSource: "testData"}) |
||||||
|
} |
||||||
|
|
||||||
|
describe("BattlerDeclaration", () => { |
||||||
|
describe("constructor", () => { |
||||||
|
test("sets id property from input", () => { |
||||||
|
expect(battlerDeclaration("myId", []).id).toEqual("myId") |
||||||
|
}) |
||||||
|
}); |
||||||
|
describe("typecheck", () => { |
||||||
|
test("returns true on constructor output", () => { |
||||||
|
expect(isBattlerDeclaration(battlerDeclaration("testId", []))).toBeTruthy() |
||||||
|
}) |
||||||
|
test("returns true on hand-crafted instance", () => { |
||||||
|
expect(isBattlerDeclaration({ |
||||||
|
type: TopLevelStatementType.BATTLER, |
||||||
|
id: "someId", |
||||||
|
contents: [] |
||||||
|
})).toBeTruthy() |
||||||
|
}) |
||||||
|
}); |
||||||
|
describe("parsing", () => { |
||||||
|
function success(name: string, text: string, ...result: BattlerDeclaration[]) { |
||||||
|
test(`succeeds for ${name}`, () => { |
||||||
|
expect(parse(text)).toEqual(scriptFile(result)) |
||||||
|
}) |
||||||
|
} |
||||||
|
success("basic empty instance", "battler nomi\nend battler", battlerDeclaration("nomi", [])) |
||||||
|
success("comment after opening", "battler nomi // of the nomi crew\nend battler", battlerDeclaration("nomi", [])) |
||||||
|
success("comments and empty lines inside the block", "battler nomi\n\n// hungry gal...\n\nend battler", battlerDeclaration("nomi", [])) |
||||||
|
success("nonempty instance", "battler nomi\nhealth 20\nconfidence 90\nend battler", |
||||||
|
battlerDeclaration("nomi", [ |
||||||
|
battlerAttribute(BattlerAttributeType.HEALTH, numberValue(20)), |
||||||
|
battlerAttribute(BattlerAttributeType.CONFIDENCE, numberValue(90))])) |
||||||
|
|
||||||
|
function failure(name: string, text: string) { |
||||||
|
test(`fails for ${name}`, () => { |
||||||
|
expect(() => parse(text)).toThrow(SyntaxError) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
failure("opening statement only", "battler open") |
||||||
|
failure("ending statement only", "end battler") |
||||||
|
failure("multi identifier", "battler cool girl\nend battler") |
||||||
|
failure("containing top level statement", "battler genius\nscript version 3\nend battler") |
||||||
|
failure("containing other battler declaration", "battler coolio\nbattler even_cooler\nend battler\nend battler") |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe("BattlerAttribute", () => { |
||||||
|
describe("constructor", () => { |
||||||
|
test("forwards the elements to the fields", () => { |
||||||
|
const attr = battlerAttribute(BattlerAttributeType.HEALTH, numberValue(20)) |
||||||
|
|
||||||
|
expect(attr.attribute).toEqual(BattlerAttributeType.HEALTH) |
||||||
|
expect(attr.value).toEqual(numberValue(20)) |
||||||
|
}) |
||||||
|
}) |
||||||
|
describe("typecheck", () => { |
||||||
|
test("returns true on constructor input", () => { |
||||||
|
expect(isBattlerAttribute(battlerAttribute(BattlerAttributeType.HEALTH, numberValue(20)))).toBeTruthy() |
||||||
|
}) |
||||||
|
test("returns true on hand-crafted instance", () => { |
||||||
|
expect(isBattlerAttribute({ |
||||||
|
type: BattlerStatementType.ATTRIBUTE, |
||||||
|
attribute: BattlerAttributeType.STAMINA, |
||||||
|
value: numberValue(20) |
||||||
|
})).toBeTruthy() |
||||||
|
}) |
||||||
|
}) |
||||||
|
describe("parsing", () => { |
||||||
|
function success(name: string, text: string, ...result: readonly BattlerStatement[]) { |
||||||
|
test(`succeeds for ${name}`, () => { |
||||||
|
expect(parse(`battler battler\n${text}\nend battler`)).toEqual(scriptFile([battlerDeclaration("battler", result)])) |
||||||
|
}) |
||||||
|
} |
||||||
|
success("basic empty instance", "health 20", battlerAttribute(BattlerAttributeType.HEALTH, numberValue(20))) |
||||||
|
success("comment between the attribute and the number", "stamina/* 200 */99", battlerAttribute(BattlerAttributeType.STAMINA, numberValue(99))) |
||||||
|
|
||||||
|
function failure(name: string, text: string) { |
||||||
|
test(`fails for ${name}`, () => { |
||||||
|
expect(() => parse(text)).toThrow(SyntaxError) |
||||||
|
}) |
||||||
|
} |
||||||
|
failure("no value after the attribute", "stamina // 99") |
||||||
|
}) |
||||||
|
}); |
@ -0,0 +1,58 @@ |
|||||||
|
import {TopLevelStatement, TopLevelStatementType} from "./TopLevelStatement"; |
||||||
|
import {ScriptExpression} from "./ScriptExpression"; |
||||||
|
|
||||||
|
export interface BattlerDeclaration { |
||||||
|
readonly type: TopLevelStatementType.BATTLER |
||||||
|
readonly id: string |
||||||
|
readonly contents: readonly BattlerStatement[] |
||||||
|
} |
||||||
|
export function battlerDeclaration(id: string, contents: readonly BattlerStatement[]): BattlerDeclaration { |
||||||
|
return { |
||||||
|
type: TopLevelStatementType.BATTLER, |
||||||
|
id, |
||||||
|
contents, |
||||||
|
} |
||||||
|
} |
||||||
|
export function isBattlerDeclaration(statement: TopLevelStatement): statement is BattlerDeclaration { |
||||||
|
return statement.type === TopLevelStatementType.BATTLER |
||||||
|
} |
||||||
|
|
||||||
|
export enum BattlerStatementType { |
||||||
|
ATTRIBUTE = "attribute", |
||||||
|
} |
||||||
|
|
||||||
|
export enum BattlerAttributeType { |
||||||
|
HEALTH = "health", |
||||||
|
CONFIDENCE = "confidence", |
||||||
|
STAMINA = "stamina", |
||||||
|
} |
||||||
|
export function battlerAttributeType(text: string): BattlerAttributeType { |
||||||
|
switch (text) { |
||||||
|
case "health": |
||||||
|
return BattlerAttributeType.HEALTH |
||||||
|
case "confidence": |
||||||
|
return BattlerAttributeType.CONFIDENCE |
||||||
|
case "stamina": |
||||||
|
return BattlerAttributeType.STAMINA |
||||||
|
default: |
||||||
|
throw Error(`Unrecognized attribute for battler: ${text}`) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export interface BattlerAttribute { |
||||||
|
readonly type: BattlerStatementType.ATTRIBUTE |
||||||
|
readonly attribute: BattlerAttributeType |
||||||
|
readonly value: ScriptExpression |
||||||
|
} |
||||||
|
export function battlerAttribute(attribute: BattlerAttributeType, value: ScriptExpression): BattlerAttribute { |
||||||
|
return { |
||||||
|
type: BattlerStatementType.ATTRIBUTE, |
||||||
|
attribute, |
||||||
|
value |
||||||
|
} |
||||||
|
} |
||||||
|
export function isBattlerAttribute(statement: BattlerStatement): statement is BattlerAttribute { |
||||||
|
return statement.type === BattlerStatementType.ATTRIBUTE |
||||||
|
} |
||||||
|
|
||||||
|
export type BattlerStatement = BattlerAttribute |
@ -0,0 +1,36 @@ |
|||||||
|
import {parse} from "./NomScript.peggy"; |
||||||
|
import {scriptFile} from "./ScriptFile"; |
||||||
|
import {versionStatement} from "./TopLevelStatement"; |
||||||
|
import {battlerAttribute, BattlerAttributeType, battlerDeclaration} from "./BattlerStatement"; |
||||||
|
import {numberValue} from "./ScriptValue"; |
||||||
|
|
||||||
|
test("integration test", () => { |
||||||
|
const result = parse(` |
||||||
|
script version 1 |
||||||
|
|
||||||
|
battler reya |
||||||
|
health 90 |
||||||
|
confidence 2000 |
||||||
|
stamina 50 |
||||||
|
end battler |
||||||
|
|
||||||
|
battler kun_chan |
||||||
|
health 500 |
||||||
|
confidence 1000 |
||||||
|
stamina 200 |
||||||
|
end battler |
||||||
|
`, {grammarSource: "testData", start: "ScriptFile"})
|
||||||
|
expect(result).toEqual(scriptFile([ |
||||||
|
versionStatement(1), |
||||||
|
battlerDeclaration("reya", [ |
||||||
|
battlerAttribute(BattlerAttributeType.HEALTH, numberValue(90)), |
||||||
|
battlerAttribute(BattlerAttributeType.CONFIDENCE, numberValue(2000)), |
||||||
|
battlerAttribute(BattlerAttributeType.STAMINA, numberValue(50)) |
||||||
|
]), |
||||||
|
battlerDeclaration("kun_chan", [ |
||||||
|
battlerAttribute(BattlerAttributeType.HEALTH, numberValue(500)), |
||||||
|
battlerAttribute(BattlerAttributeType.CONFIDENCE, numberValue(1000)), |
||||||
|
battlerAttribute(BattlerAttributeType.STAMINA, numberValue(200)) |
||||||
|
]), |
||||||
|
])) |
||||||
|
}) |
@ -1,30 +1,60 @@ |
|||||||
{{ |
{{ |
||||||
// global (one-time) initializer |
// global (one-time) initializer |
||||||
import { scriptFile } from "./ScriptFile"; |
import { scriptFile } from "./ScriptFile"; |
||||||
import { numberValue } from "./ScriptValue"; |
|
||||||
import { versionStatement } from "./TopLevelStatement"; |
import { versionStatement } from "./TopLevelStatement"; |
||||||
|
import { battlerDeclaration, battlerAttributeType, battlerAttribute } from "./BattlerStatement"; |
||||||
|
import { numberValue, identifier } from "./ScriptValue"; |
||||||
}} |
}} |
||||||
|
|
||||||
{ |
{ |
||||||
// per-parse initializer |
// per-parse initializer |
||||||
} |
} |
||||||
|
|
||||||
// peggy-loader:startRule ScriptFile |
// ********************************************************************************************************************* |
||||||
|
// * Entry Points |
||||||
|
// ********************************************************************************************************************* |
||||||
ScriptFile = statements:TopLevelStatementList { return scriptFile(statements); } |
ScriptFile = statements:TopLevelStatementList { return scriptFile(statements); } |
||||||
|
// peggy-loader:startRule ScriptFile |
||||||
|
TopLevelExpression = _? @Expression FileEnd |
||||||
|
// peggy-loader:startRule TopLevelExpression |
||||||
|
|
||||||
NewLine "newline" = "\n" { return null; } |
// ********************************************************************************************************************* |
||||||
WhiteSpace "whitespace" = [ \r\v\t]+ { return null; } |
// * Top-level Statements |
||||||
LineComment "line comment" = "//" [^\n]* { return null; } |
// ********************************************************************************************************************* |
||||||
BlockCommentTail = "*/" / ( "*" !"/" [^*]* BlockCommentTail ) { return null; } |
TopLevelStatementList = (__ head:TopLevelStatement tail:(StatementEnd __ @TopLevelStatement)* FileEnd { return [head, ...tail]; }) / (FileEnd { return []; }) |
||||||
BlockComment "block comment" = "/*" [^*]* BlockCommentTail { return null; } |
TopLevelStatement "top-level statement" = VersionStatement / BattlerDeclaration |
||||||
|
|
||||||
|
VersionStatement "version statement" = "script" _ "version" _ version:WholeNumberLiteral { return versionStatement(version.value); } |
||||||
|
|
||||||
IntegerLiteral "integer literal" = digits:$[0-9]+ { return numberValue(digits); } |
// ********************************************************************************************************************* |
||||||
|
// * Battler Declaration + Statements |
||||||
|
// ********************************************************************************************************************* |
||||||
|
BattlerDeclaration "battler declaration" = "battler" _ id:Identifier contents:(StatementEnd __ @BattlerStatement)* StatementEnd __ "end" _ "battler" { return battlerDeclaration(id.value, contents) } |
||||||
|
BattlerStatement = BattlerAttribute |
||||||
|
|
||||||
NonToken = (LineComment / BlockComment / WhiteSpace) { return null; } |
BattlerAttribute = attr:BattlerAttributeType _ expr:Expression { return battlerAttribute(attr, expr); } |
||||||
|
BattlerAttributeType "attribute name" = attr:("health" / "confidence" / "stamina") { return battlerAttributeType(attr); } |
||||||
|
|
||||||
|
// ********************************************************************************************************************* |
||||||
|
// * Expressions |
||||||
|
// ********************************************************************************************************************* |
||||||
|
Expression "expression" = WholeNumberLiteral / Identifier |
||||||
|
|
||||||
|
Identifier "identifier" = id:$([a-zA-Z_$] [a-zA-Z_$0-9]*) { return identifier(id); } |
||||||
|
WholeNumberLiteral "whole number literal" = digits:$[0-9]+ { return numberValue(digits); } |
||||||
|
|
||||||
|
// ********************************************************************************************************************* |
||||||
|
// * Whitespace and Comments |
||||||
|
// ********************************************************************************************************************* |
||||||
|
FileEnd = __ LineComment? |
||||||
|
__ = StatementEnd* _? { return null; } |
||||||
|
StatementEnd = _? LineComment? NewLine { return null; } |
||||||
_ = (NonToken+) { return null; } |
_ = (NonToken+) { return null; } |
||||||
StatementEnd = _? NewLine { return null; } |
NonToken = (BlockComment / WhiteSpace) { return null; } |
||||||
|
|
||||||
VersionStatement "version statement" = "script" _ "version" _ version:IntegerLiteral { return versionStatement(version.value); } |
LineComment "line comment" = "//" [^\n]* { return null; } |
||||||
|
BlockComment "block comment" = "/*" [^*]* BlockCommentTail { return null; } |
||||||
|
BlockCommentTail = "*/" / ( "*" !"/" [^*]* BlockCommentTail ) { return null; } |
||||||
|
|
||||||
TopLevelStatement "top-level statement" = VersionStatement |
WhiteSpace "whitespace" = [ \r\v\t]+ { return null; } |
||||||
TopLevelStatementList = (StatementEnd* _? head:TopLevelStatement tail:(StatementEnd+ _? @TopLevelStatement)* StatementEnd* _? { return [head, ...tail]; }) / (StatementEnd* _? { return []; }) |
NewLine "newline" = "\n" { return null; } |
@ -0,0 +1,24 @@ |
|||||||
|
import {parse, SyntaxError} from "./NomScript.peggy"; |
||||||
|
|
||||||
|
describe("ScriptExpression", () => { |
||||||
|
describe("parse", () => { |
||||||
|
test("fails with an empty input", () => { |
||||||
|
expect(() => parse("", {grammarSource: "testData", startRule: "TopLevelExpression"})).toThrow(SyntaxError) |
||||||
|
}); |
||||||
|
test("fails with empty lines", () => { |
||||||
|
expect(() => parse("\n\n\n\n\n", {grammarSource: "testData", startRule: "TopLevelExpression"})).toThrow(SyntaxError) |
||||||
|
}); |
||||||
|
test("fails with lines filled only with whitespace", () => { |
||||||
|
expect(() => parse("\n\n \n\t\t \t\n\n", {grammarSource: "testData", startRule: "TopLevelExpression"})).toThrow(SyntaxError) |
||||||
|
}); |
||||||
|
test("fails with lines filled only with comments", () => { |
||||||
|
expect(() => parse("\n\n /* */ \n// test \n /* */\n", {grammarSource: "testData", startRule: "TopLevelExpression"})).toThrow(SyntaxError) |
||||||
|
}); |
||||||
|
test("works for a simple expression", () => { |
||||||
|
expect(() => parse("73", {grammarSource: "testData", startRule: "TopLevelExpression"})).not.toThrow() |
||||||
|
}); |
||||||
|
test("succeeds with end of line comments", () => { |
||||||
|
expect(() => parse("90 // you know", {grammarSource: "testData", startRule: "TopLevelExpression"})).not.toThrow() |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,3 @@ |
|||||||
|
import {ScriptValue} from "./ScriptValue"; |
||||||
|
|
||||||
|
export type ScriptExpression = ScriptValue |
@ -0,0 +1,27 @@ |
|||||||
|
import {parse} from "./NomScript.peggy"; |
||||||
|
import {scriptFile} from "./ScriptFile"; |
||||||
|
import {versionStatement} from "./TopLevelStatement"; |
||||||
|
|
||||||
|
describe("ScriptFile", () => { |
||||||
|
describe("constructor", () => { |
||||||
|
test("puts the statements into the list", () => { |
||||||
|
expect(scriptFile([versionStatement(3)])).toEqual({ |
||||||
|
statements: [versionStatement(3)] |
||||||
|
}) |
||||||
|
}); |
||||||
|
}); |
||||||
|
describe("parse", () => { |
||||||
|
test("works with an empty input", () => { |
||||||
|
expect(() => parse("", {grammarSource: "testData", startRule: "ScriptFile"})).not.toThrow() |
||||||
|
}); |
||||||
|
test("works with empty lines", () => { |
||||||
|
expect(() => parse("\n\n\n\n\n", {grammarSource: "testData", startRule: "ScriptFile"})).not.toThrow() |
||||||
|
}); |
||||||
|
test("works with lines filled only with whitespace", () => { |
||||||
|
expect(() => parse("\n\n \n\t\t \t\n\n", {grammarSource: "testData", startRule: "ScriptFile"})).not.toThrow() |
||||||
|
}); |
||||||
|
test("works with lines filled only with comments", () => { |
||||||
|
expect(() => parse("\n\n /* */ \n// test \n /* */\n", {grammarSource: "testData", startRule: "ScriptFile"})).not.toThrow() |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -1,9 +1,9 @@ |
|||||||
import {TopLevelStatement} from "./TopLevelStatement.js"; |
import {TopLevelStatementList} from "./TopLevelStatement.js"; |
||||||
|
|
||||||
export interface ScriptFile { |
export interface ScriptFile { |
||||||
readonly statements: readonly TopLevelStatement[] |
readonly statements: TopLevelStatementList |
||||||
} |
} |
||||||
export function scriptFile(statements: readonly TopLevelStatement[]): ScriptFile { |
export function scriptFile(statements: TopLevelStatementList): ScriptFile { |
||||||
return { |
return { |
||||||
statements |
statements |
||||||
} |
} |
||||||
|
@ -1,26 +1,41 @@ |
|||||||
export enum ScriptValueTypes { |
export enum ScriptValueType { |
||||||
NUMBER = "number" |
NUMBER = "number", |
||||||
|
IDENTIFIER = "identifier", |
||||||
} |
} |
||||||
|
|
||||||
export interface NumberValue { |
export interface NumberValue { |
||||||
readonly type: ScriptValueTypes.NUMBER, |
readonly type: ScriptValueType.NUMBER, |
||||||
readonly value: number, |
readonly value: number, |
||||||
} |
} |
||||||
export function numberValue(value: number|string) { |
export function numberValue(value: number|string): NumberValue { |
||||||
if (typeof value === "string") { |
if (typeof value === "string") { |
||||||
return { |
return { |
||||||
type: ScriptValueTypes.NUMBER, |
type: ScriptValueType.NUMBER, |
||||||
value: parseInt(value, 10), |
value: parseInt(value, 10), |
||||||
} |
} |
||||||
} else { |
} else { |
||||||
return { |
return { |
||||||
type: ScriptValueTypes.NUMBER, |
type: ScriptValueType.NUMBER, |
||||||
value, |
value, |
||||||
} |
} |
||||||
} |
} |
||||||
} |
} |
||||||
export function isNumberValue(value: ScriptValue): value is NumberValue { |
export function isNumberValue(value: ScriptValue): value is NumberValue { |
||||||
return value.type === ScriptValueTypes.NUMBER |
return value.type === ScriptValueType.NUMBER |
||||||
} |
} |
||||||
|
|
||||||
export type ScriptValue = NumberValue |
export interface ScriptIdentifier { |
||||||
|
readonly type: ScriptValueType.IDENTIFIER |
||||||
|
readonly value: string |
||||||
|
} |
||||||
|
export function identifier(value: string): ScriptIdentifier { |
||||||
|
return { |
||||||
|
type: ScriptValueType.IDENTIFIER, |
||||||
|
value |
||||||
|
} |
||||||
|
} |
||||||
|
export function isIdentifier(value: ScriptValue): value is ScriptIdentifier { |
||||||
|
return value.type === ScriptValueType.IDENTIFIER |
||||||
|
} |
||||||
|
|
||||||
|
export type ScriptValue = NumberValue|ScriptIdentifier |
@ -1,19 +1,23 @@ |
|||||||
export enum TopLevelStatementTypes { |
import {BattlerDeclaration} from "./BattlerStatement"; |
||||||
|
|
||||||
|
export enum TopLevelStatementType { |
||||||
VERSION = "version", |
VERSION = "version", |
||||||
|
BATTLER = "battler", |
||||||
} |
} |
||||||
|
|
||||||
export interface VersionStatement { |
export interface VersionStatement { |
||||||
readonly type: TopLevelStatementTypes.VERSION, |
readonly type: TopLevelStatementType.VERSION, |
||||||
readonly version: number, |
readonly version: number, |
||||||
} |
} |
||||||
export function versionStatement(version: number): VersionStatement { |
export function versionStatement(version: number): VersionStatement { |
||||||
return { |
return { |
||||||
type: TopLevelStatementTypes.VERSION, |
type: TopLevelStatementType.VERSION, |
||||||
version, |
version, |
||||||
} |
} |
||||||
} |
} |
||||||
export function isVersionStatement(statement: TopLevelStatement): statement is VersionStatement { |
export function isVersionStatement(statement: TopLevelStatement): statement is VersionStatement { |
||||||
return statement.type === TopLevelStatementTypes.VERSION |
return statement.type === TopLevelStatementType.VERSION |
||||||
} |
} |
||||||
|
|
||||||
export type TopLevelStatement = VersionStatement |
export type TopLevelStatement = VersionStatement | BattlerDeclaration |
||||||
|
export type TopLevelStatementList = readonly TopLevelStatement[] |
Loading…
Reference in new issue