diff --git a/src/compiler/Compiler.ts b/src/compiler/Compiler.ts index d1ae2388..da2bb348 100644 --- a/src/compiler/Compiler.ts +++ b/src/compiler/Compiler.ts @@ -7,6 +7,7 @@ import { Story as ParsedStory } from "./Parser/ParsedHierarchy/Story"; import { DebugMetadata } from "../engine/DebugMetadata"; import { StringValue } from "../engine/Value"; import { asOrNull } from "../engine/TypeAssertion"; +import { GenerateStoryStats, Stats } from "./Stats"; export { CompilerOptions } from "./CompilerOptions"; export { InkParser } from "./Parser/InkParser"; @@ -113,6 +114,13 @@ export class Compiler { } }; + public readonly GenerateStats = (): Stats | null => { + if (this._parsedStory === null) { + return null; + } + return GenerateStoryStats(this._parsedStory); + }; + public readonly DebugMetadataForContentAtOffset = ( offset: number ): DebugMetadata | null => { diff --git a/src/compiler/Stats.ts b/src/compiler/Stats.ts new file mode 100644 index 00000000..4ca3bf66 --- /dev/null +++ b/src/compiler/Stats.ts @@ -0,0 +1,52 @@ +import { Choice } from "./Parser/ParsedHierarchy/Choice"; +import { Divert } from "./Parser/ParsedHierarchy/Divert/Divert"; +import { Gather } from "./Parser/ParsedHierarchy/Gather/Gather"; +import { Knot } from "./Parser/ParsedHierarchy/Knot"; +import { Stitch } from "./Parser/ParsedHierarchy/Stitch"; +import { Story } from "./Parser/ParsedHierarchy/Story"; +import { Text } from "./Parser/ParsedHierarchy/Text"; + +export interface Stats { + words: number; + knots: number; + stitches: number; + functions: number; + choices: number; + gathers: number; + diverts: number; +} + +export function GenerateStoryStats(story: Story): Stats { + let allText = story.FindAll(Text)(); + let words = 0; + for (const text of allText) { + let wordsInThisStr = 0; + let wasWhiteSpace = true; + for (const c of text.text) { + if (c == " " || c == "\t" || c == "\n" || c == "\r") { + wasWhiteSpace = true; + } else if (wasWhiteSpace) { + wordsInThisStr++; + wasWhiteSpace = false; + } + } + + words += wordsInThisStr; + } + + const knots = story.FindAll(Knot)(); + const stitches = story.FindAll(Stitch)(); + const choices = story.FindAll(Choice)(); + const gathers = story.FindAll(Gather)((g) => g.debugMetadata != null); + const diverts = story.FindAll(Divert)(); + + return { + words, + knots: knots.length, + functions: knots.filter((k) => k.isFunction).length, + stitches: stitches.length, + gathers: gathers.length, + diverts: diverts.length - 1, + choices: choices.length, + }; +} diff --git a/src/tests/specs/inkjs/compiler/Stats.spec.ts b/src/tests/specs/inkjs/compiler/Stats.spec.ts new file mode 100644 index 00000000..28e65a9b --- /dev/null +++ b/src/tests/specs/inkjs/compiler/Stats.spec.ts @@ -0,0 +1,43 @@ +import { Stats } from "../../../../compiler/Stats"; +import { Compiler } from "../../../../ink"; + +function getStats(inkSource: string): Stats { + const compiler = new Compiler(inkSource); + compiler.Compile(); + const stats = compiler.GenerateStats(); + expect(stats).not.toBeNull(); + return stats!; +} + +describe("Stat Generation", () => { + it("basic word count", () => { + const stats = getStats("this is an ink story."); + expect(stats.words).toBe(5); + }); + it("word count doesn't include divert or variable names", () => { + const stats = getStats( + "VAR MyVariable = 3\n->start\n=== start\nthis is an ink story.\n->END" + ); + expect(stats.words).toBe(5); + }); + it("count functions, knots, and stitches", () => { + const stats = getStats( + "->start\n=== function myFunc()\n~ return 0\n=== start\n= stitch\nHello world!" + ); + expect(stats.functions).toBe(1); + expect(stats.knots).toBe(2); + expect(stats.stitches).toBe(1); + }); + it("count diverts", () => { + const stats = getStats("->go\n- (go)\n->next\n-(next)"); + expect(stats.diverts).toBe(2); + }); + it("end counts as a divert", () => { + const stats = getStats("->go\n- (go)\n->next\n-(next)\n->END"); + expect(stats.diverts).toBe(3); + }); + it("count gathers", () => { + const stats = getStats("->go\n- (go)\n->next\n-(next)"); + expect(stats.gathers).toBe(2); + }); +});