/** * 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 { smartypantsu } from "smartypants" import { parseWikiLink } from "@a-morphous/recital-ext-common-commands" import { FlatObject, ParagraphToken } from "@a-morphous/recital/dist/types/types" import { getNextScene } from "../utils/next-scene" import { inkSlugify } from "../utils/slugify" const parseChoice = (line: string) => { const choiceRegex = /^(?<nesting>>*)(?<modifier>[!+.]*) ?(?<logic>{.+})? ?\[\[(?<link>.+)\]\]/g const matches = choiceRegex.exec(line) if (!matches || !matches.groups) { return undefined } const link = parseWikiLink(matches.groups.link) return { nestedCount: matches.groups.nesting ? matches.groups.nesting.length : 1, modifiers: matches.groups.modifier ?? "", logic: matches.groups.logic ?? "", ...link, } } const parseGather = (line: string) => { // gathers can be on a line all by themselves, so we need to make sure the regex // accommodates that const singleRegex = /^(?<nesting><+)$/g const singleMatch = singleRegex.exec(line) if (singleMatch && singleMatch.groups) { return { nestedCount: singleMatch.groups.nesting.length, text: "", } } const regex = /^(?<nesting><+) +(?<text>.*)/g const matches = regex.exec(line) if (!matches || !matches.groups) { return undefined } return { nestedCount: matches.groups.nesting.length, text: matches.groups.text.trim(), } } // glue is a straight replacement, since we can't have <> at the beginning of a line // without it turning into a fragment. const parseGlue = (line: string) => { return line.replace(/\$\<\>/g, "<>") } const parseRedirect = (line: string) => { // -> redirect can happen at the beginning of a line, or after an equal sign // if we are setting a variable to a redirect. const regex = /(^|= *|- *)(?<redirect>->) *(?<link>.+)/g const matches = regex.exec(line) if (!matches || !matches.groups) { return undefined } return { beforeLink: line.split("->")[0] ?? "", link: matches.groups.link, } } export type ParagraphState = { lastIndent: number withinLogicBlock?: boolean // are we inside one of those { } conditionals? } export const parseParagraph = ( token: ParagraphToken, flats: FlatObject[], state: ParagraphState = { lastIndent: 0, withinLogicBlock: false } ) => { if (token.text.trim() === "END") { return `${"\t".repeat(state.lastIndent)}->END\n` } let paragraphString = "" // TODO: we should split the text and smart text individual pieces // to deal with inter-paragraph {logic}, but that's too hard for now. // check if we should be in or out of a logic block if (token.text.trim().startsWith("{")) { state.withinLogicBlock = true } if (token.text.trim().startsWith("}") || token.text.trim().endsWith("}")) { state.withinLogicBlock = false } // VAR and { } tags should not be put into smartypants let shouldSmartText = true if (/^(VAR |~).*/.test(token.text.trim())) { shouldSmartText = false } if (state.withinLogicBlock) { if (token.text.trim().startsWith("-")) { shouldSmartText = false } // the first line, for those clauses that aren't settled. if (token.text.trim().startsWith("{")) { shouldSmartText = false } } const smartText = shouldSmartText ? smartypantsu(token.text, "qie") : token.text // choices const choice = parseChoice(smartText) if (choice) { state.lastIndent = choice.nestedCount let bulletPoint = choice.modifiers.includes("+") ? "+ " : "* " paragraphString += `${"\t".repeat(choice.nestedCount - 1)}${bulletPoint.repeat( choice.nestedCount )}` if (choice.modifiers.includes(".")) { // fallback choice paragraphString += `->\n${"\t".repeat(state.lastIndent)}${choice.label}\n` return paragraphString } // add the logic if we have it if (choice.logic) { paragraphString += `${choice.logic} ` } // add the label, wrapping it with [] if we're not loud. if (choice.modifiers.includes("!")) { paragraphString += choice.label } else { paragraphString += `[${choice.label}]` } // add the redirect if we have one if (choice.target !== choice.label) { paragraphString += ` -> ${inkSlugify(choice.target)}\n` } else { paragraphString += "\n" } return paragraphString } let workingText = smartText // glue workingText = parseGlue(workingText) // gathers const gather = parseGather(workingText) if (gather) { state.lastIndent = gather.nestedCount - 1 workingText = `${"- ".repeat(gather.nestedCount)}${gather.text}\n` } // redirects const redirect = parseRedirect(workingText) if (redirect) { workingText = `${redirect.beforeLink.trim()} -> ${inkSlugify(redirect.link)}\n` } // auto-advance if (workingText.trim().endsWith("->")) { const i = flats.indexOf(token) const targetToken = i > -1 ? getNextScene(i, flats, true) : undefined if (!targetToken) { workingText = workingText.trimEnd().replace("->", "-> END") } else { workingText = workingText .trimEnd() .replace("->", `-> ${inkSlugify(targetToken.title ?? targetToken.primary ?? "ERROR")}\n`) } } // get the @ shorthand working. let shorthand = "" if (token.primary) { shorthand = "@" if (token.id) { shorthand += "#" + token.id } if (token.classes && token.classes.length) { if (token.id) { shorthand += "." } shorthand += `${token.classes.join(".")}` } shorthand += " " } // everything else paragraphString += "\t".repeat(state.lastIndent) + shorthand + workingText + "\n" return paragraphString }