frontispiece / packages / ink-utils / src / index.ts
index.ts
Raw
/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

import { Story } from 'inkjs/engine/Story';
import { Container } from 'inkjs/engine/Container';

/**
 * Returns metadata for a section at the given path.
 * Metadata are listed as tags, which can be created by Recital's ink parser, and are attached to
 * knots and stitches.
 * @param story 
 * @param path the path to the knot / stitch in the format `knot.stitch`. If you just want to go to a knot, path should be `knot`.
 * @param recursive set to true if you want the parents' meta to percolate down to this section.
 * @returns 
 */
export const getMetaForInkStoryPath = (
	story: Story,
	path: string,
	recursive: boolean = false
): Record<string, any> => {
	const tags: string[] = []
	if (recursive) {
		const splits = path.split(".")
		let pointer = ""
		for (let i = 0; i < splits.length - 1; i++) {
			pointer += splits[i]
			tags.push(...(story.TagsForContentAtPath(pointer) || []))
			pointer += "."
		}
	}
	tags.push(...(story.TagsForContentAtPath(path) || []))
	const meta: Record<string, any> = {}
	for (let tag of tags) {
		if (!tag.includes(":")) {
			continue
		}
		const splits = tag.split(":")
		try {
			meta[splits[0]] = JSON.parse(splits[1])
		} catch (e) {
			if (!meta._) {
				meta._ = []
			}
			meta._.push(tag);
		}
	}
	return meta
}

export const getContainerAtPathString = function (story: Story, path: string) {
	let container = story.mainContentContainer

	for (var name of path.split(".")) {
		if (!container.namedContent.has(name)) return null
		container = container.namedContent.get(name) as Container
	}
	return container
}

export const getAllKnots = (story: Story): string[] => {
	const list: string[] = []
	const container = story.mainContentContainer
	const knotKeys = container.namedContent.keys()
	for (let knot of knotKeys) {
		list.push(knot)
	}

	return list
}

/**
 * Given a top-level knot, returns all the stitches **as paths** in the form `knot.stitch`
 * @param story
 * @param knotName
 * @returns The list of stitches, as their paths.
 */
export const getAllStitchesForKnot = (story: Story, knotName: string): string[] => {
	let list: string[] = []
	let container = story.KnotContainerWithName(knotName)
	if (!container) {
		return list;
	}
	let stitchKeys = container.namedContent.keys()
	for (let stitch of stitchKeys) {
		list.push(`${knotName}.${stitch}`)
	}

	return list
}

export const arePrereqsMetForStorylet = (story: Story, storyletPath: string) => {
	const meta = getMetaForInkStoryPath(story, storyletPath)
	let prereqs = meta.PREREQS ?? []

	for (let prereq of prereqs) {
		// this will cover all of the comparison operators.
		// see https://www.programiz.com/javascript/comparison-logical
		const variableName = prereq.split(/(\!|\=|\<|\>)/g)[0]?.trim()

		// TODO: it's really easy to introduce bugs with string prereqs since you need to write them as
		// "var === 'value'", with the single quote inside double quotes.
		// Maybe detect variable value, and see if we should automatically fix quotes?
		// or use JSON parse and call it a day.

		let variableValue = story.variablesState.$(variableName)
		const evaluate = Function(`"use strict";

		let ${variableName} = ${variableValue ? JSON.stringify(variableValue) : "undefined"}
		return ${prereq}`)
		let result = evaluate()
		if (!result) {
			return false
		}
	}

	return true
}

/**
 * Sets variables in an ink story using thet Javascript parser.
 * @param story
 * @param operations A list of operations of the form `variable = <xyz>` in string form.
 */
export const performVarOperationsOnStory = (story: Story, operations: string[]) => {
	for (let operation of operations) {
		const variableName = operation.split(/(\+|\-|\*|\/|%|)?=/g)[0]?.trim()
		let variableValue = story.variablesState.$(variableName)
		const evaluate = Function(`"use strict";

		let ${variableName} = ${variableValue ? JSON.stringify(variableValue) : "undefined"}
		${operation}
		return ${variableName}`)
		let result = evaluate()
		story.variablesState[variableName] = result
	}
}