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 |
||||
import { scriptFile } from "./ScriptFile"; |
||||
import { numberValue } from "./ScriptValue"; |
||||
import { versionStatement } from "./TopLevelStatement"; |
||||
import { battlerDeclaration, battlerAttributeType, battlerAttribute } from "./BattlerStatement"; |
||||
import { numberValue, identifier } from "./ScriptValue"; |
||||
}} |
||||
|
||||
{ |
||||
// per-parse initializer |
||||
} |
||||
|
||||
// peggy-loader:startRule ScriptFile |
||||
// ********************************************************************************************************************* |
||||
// * Entry Points |
||||
// ********************************************************************************************************************* |
||||
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; } |
||||
LineComment "line comment" = "//" [^\n]* { return null; } |
||||
BlockCommentTail = "*/" / ( "*" !"/" [^*]* BlockCommentTail ) { return null; } |
||||
BlockComment "block comment" = "/*" [^*]* BlockCommentTail { return null; } |
||||
// ********************************************************************************************************************* |
||||
// * Top-level Statements |
||||
// ********************************************************************************************************************* |
||||
TopLevelStatementList = (__ head:TopLevelStatement tail:(StatementEnd __ @TopLevelStatement)* FileEnd { return [head, ...tail]; }) / (FileEnd { return []; }) |
||||
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; } |
||||
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 |
||||
TopLevelStatementList = (StatementEnd* _? head:TopLevelStatement tail:(StatementEnd+ _? @TopLevelStatement)* StatementEnd* _? { return [head, ...tail]; }) / (StatementEnd* _? { return []; }) |
||||
WhiteSpace "whitespace" = [ \r\v\t]+ { return null; } |
||||
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 { |
||||
readonly statements: readonly TopLevelStatement[] |
||||
readonly statements: TopLevelStatementList |
||||
} |
||||
export function scriptFile(statements: readonly TopLevelStatement[]): ScriptFile { |
||||
export function scriptFile(statements: TopLevelStatementList): ScriptFile { |
||||
return { |
||||
statements |
||||
} |
||||
|
@ -1,26 +1,41 @@ |
||||
export enum ScriptValueTypes { |
||||
NUMBER = "number" |
||||
export enum ScriptValueType { |
||||
NUMBER = "number", |
||||
IDENTIFIER = "identifier", |
||||
} |
||||
|
||||
export interface NumberValue { |
||||
readonly type: ScriptValueTypes.NUMBER, |
||||
readonly type: ScriptValueType.NUMBER, |
||||
readonly value: number, |
||||
} |
||||
export function numberValue(value: number|string) { |
||||
export function numberValue(value: number|string): NumberValue { |
||||
if (typeof value === "string") { |
||||
return { |
||||
type: ScriptValueTypes.NUMBER, |
||||
type: ScriptValueType.NUMBER, |
||||
value: parseInt(value, 10), |
||||
} |
||||
} else { |
||||
return { |
||||
type: ScriptValueTypes.NUMBER, |
||||
type: ScriptValueType.NUMBER, |
||||
value, |
||||
} |
||||
} |
||||
} |
||||
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", |
||||
BATTLER = "battler", |
||||
} |
||||
|
||||
export interface VersionStatement { |
||||
readonly type: TopLevelStatementTypes.VERSION, |
||||
readonly type: TopLevelStatementType.VERSION, |
||||
readonly version: number, |
||||
} |
||||
export function versionStatement(version: number): VersionStatement { |
||||
return { |
||||
type: TopLevelStatementTypes.VERSION, |
||||
type: TopLevelStatementType.VERSION, |
||||
version, |
||||
} |
||||
} |
||||
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