/** * Copyright (c) 2022 Amorphous * * 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 http://mozilla.org/MPL/2.0/. */ import { parseFlat } from '@a-morphous/recital' import { enforceIds } from '@a-morphous/recital-ext-common-commands' import { FlatObject, SceneObject } from '@a-morphous/recital/dist/types/types' import { convertMetaIntoInkTags } from './lib/meta' import { ParagraphState, parseParagraph } from './lib/paragraph' import { StageToInkConfig, StageToInkConfigWithState } from './types' import { getNextScene } from './utils/next-scene' import { inkSlugify } from './utils/slugify' const handleRegularInkToken = ( token: FlatObject, flats: FlatObject[], state: ParagraphState, config: StageToInkConfigWithState = { _globals: {} } ) => { let finalString = '' switch (token.type) { case 'fragment': finalString += `= ${inkSlugify(token.title ?? token.primary ?? 'ERROR')}\n` state.lastIndent = 0 finalString += convertMetaIntoInkTags(token, config) break case 'command': // glue command if (token.text.startsWith('<>')) { finalString += token.text + '\n' break } // all other commands handled as-is finalString += '$' + token.text + '\n' break case 'paragraph': // add variable definitions to global state. if (token.text.trimStart().startsWith('VAR')) { if (!config._globals) { config._globals = {} } if (!config._globals.variables) { config._globals.variables = new Set() } const variableName = token.text.split('=')[0].replace('VAR', '').trim() config._globals.variables.add(variableName) } finalString += parseParagraph(token, flats, state) } return finalString } const HUB_REPLACEMENT = 'HUB_REPLACEMENT' const handleStoryletSection = ( startToken: SceneObject, flats: FlatObject[], state: ParagraphState, config: StageToInkConfigWithState = { _globals: {} } ) => { const index = flats.indexOf(startToken) let storyletString = '' // figure out when the storylet section ends. let endIndex for (let i = index; i < flats.length; i++) { if (flats[i].type === 'endScene') { endIndex = i break } } let isInHub = false let hubText = '' let beforeFirstStorylet = true for (let i = index; i < endIndex; i++) { const token = flats[i] switch (token.type) { case 'scene': // we assume that the default scene creation stuff has happened, including metadata. // so we just move on. break case 'fragment': if (token.title && token.title.toLocaleLowerCase() === 'hub') { isInHub = true break } if (beforeFirstStorylet) { beforeFirstStorylet = false storyletString += '-> _hub\n\n' + HUB_REPLACEMENT + '\n' } isInHub = false storyletString += `= ${inkSlugify(token.title ?? token.primary ?? 'ERROR')}\n` state.lastIndent = 0 storyletString += convertMetaIntoInkTags(token, config) // we might need to create some variables // if they're called for in the enter tag of the storylet, but haven't been defined yet // in the story proper. if (token.meta?.enter) { const operations = token.meta.enter for (let operation of operations) { const variableName = operation.split(/(\+|\-|\*|\/|%|)?=/g)[0]?.trim() if (config._globals.variables && config._globals.variables.has(variableName)) { continue } if (!config._globals.storyletVariables) { config._globals.storyletVariables = new Set() } config._globals.storyletVariables.add(variableName) } } break case 'endFragment': if (isInHub) { isInHub = false break } // redirect to hub storyletString += '-> _hub\n\n' break case 'command': if (isInHub) { hubText += token.text + '\n' break } storyletString += token.text + '\n' break case 'paragraph': if (isInHub) { hubText += parseParagraph(token, flats, state) break } storyletString += parseParagraph(token, flats, state) } } let hubString = '' // hub stuff const sceneTitle = inkSlugify(startToken.title ?? startToken.primary ?? 'ERROR') hubString += '= _hub\n' hubString += hubText + '\n' hubString += `$HUB ${sceneTitle}\n` const i = flats.indexOf(startToken) const targetToken = i > -1 ? getNextScene(i, flats) : undefined if (!targetToken) { hubString += `-> END\n` } else { hubString += `-> ${inkSlugify(targetToken.title ?? targetToken.primary ?? 'ERROR')}\n` } let finalString = storyletString.replace(HUB_REPLACEMENT, hubString) return { inkText: finalString, newIndex: endIndex, } } export const stageToInk = (recitalText: string, config: StageToInkConfig = {}): string => { const flats = enforceIds(parseFlat(recitalText)) let finalString = '' const statefulConfig: StageToInkConfigWithState = { ...config, _globals: {} } let state: ParagraphState = { lastIndent: 0, } let firstScene = inkSlugify(statefulConfig.sceneToStartOn ?? '') let totalNumScenes = 0 // first pass: collecting metadata for (let i = 0; i < flats.length; i++) { const token = flats[i] if (token.type === 'scene') { totalNumScenes += 1 } } let currentScene = 0 for (let i = 0; i < flats.length; i++) { const token = flats[i] switch (token.type) { case 'scene': const sceneTitle = inkSlugify(token.title ?? token.primary ?? 'ERROR') finalString += `=== ${sceneTitle} ===\n` state.lastIndent = 0 if (!firstScene) { firstScene = sceneTitle } const additionalToken = { ...token } if (statefulConfig.addStats) { if (!additionalToken.meta) { additionalToken.meta = {} } additionalToken.meta.__sceneNumber = currentScene additionalToken.meta.__totalScenes = totalNumScenes currentScene += 1 } finalString += convertMetaIntoInkTags(additionalToken, statefulConfig) // handle all the storylet info at once if (token.meta?.storylet) { const storyletData = handleStoryletSection(token, flats, state, statefulConfig) i = storyletData.newIndex finalString += storyletData.inkText } break } finalString += handleRegularInkToken(token, flats, state, statefulConfig) } // finally process those globals! let varString = '' // define any variables that need to be defined if (statefulConfig._globals.storyletVariables) { statefulConfig._globals.storyletVariables.forEach((variableName) => { varString += `VAR ${variableName} = false\n` }) } // we need to start with the first string. finalString = `${varString}\n-> ${firstScene}\n${finalString}` return finalString.trim() + '\n' }