@yotoplay/twee2json
Version:
Convert Twine/Twee files to JSON format
146 lines • 5.05 kB
JavaScript
export function convertTweeToJson(tweeContent) {
const passages = [];
const metadata = {
title: undefined,
init: null,
};
let variables = {};
const passagePattern = /:: ([^\n[]+)\s*((?:\[[^\]]+\]\s*)*)({.*})?\n([\s\S]*?)(?=\n::|$)/g;
let match;
while ((match = passagePattern.exec(tweeContent)) !== null) {
const [, name, tagsString, metadataString, content] = match;
const passageName = name.replace(/\{[^}]*\}/, "").trim();
const tags = extractTags(tagsString);
const { cleanedContent, choices, comments } = processContent(content);
const passageVariables = extractVariables(content);
if (passageName === "StoryTitle") {
metadata.title = cleanedContent.replace(":", "").trim();
}
else if (passageName === "StoryInit") {
variables = passageVariables;
}
else if (passageName === "StoryData") {
metadata.data = JSON.parse(cleanedContent.trim());
}
else {
passages.push({
name: passageName,
metadata: metadataString ? JSON.parse(metadataString) : null,
content: cleanedContent,
choices,
comments,
variables: passageVariables,
tags,
});
}
}
if (metadata.data?.start) {
moveStartNodeToFront(passages, metadata.data.start);
}
return { metadata, variables, passages };
}
function extractTags(tagsString) {
return [...tagsString.matchAll(/\[([^\]]+)\]/g)]
.map((tag) => tag[1].trim().split(" "))
.reduce((x, y) => x.concat(y), []);
}
function processContent(content) {
const htmlCommentPattern = /<!--(.*?)-->/g;
const setVariablePattern = /\(set:\s*\$(\w+)\s*to\s*(.*)\)/g;
const printPattern = /\(print:.*?\)/g;
const comments = [];
const choices = [];
const cleanedContent = content
.replace(htmlCommentPattern, (comment, commentText) => {
comments.push(commentText.trim());
return "";
})
.replace(setVariablePattern, "")
.replace(printPattern, "")
.trim();
const finalContent = extractChoices(cleanedContent, choices);
return { cleanedContent: finalContent, choices, comments };
}
function extractChoices(content, choices) {
const lines = content.split("\n").map((line) => line.trim());
let finalContent = content;
lines.forEach((line) => {
if (line === "[[]]") {
choices.push({
text: "",
link: "",
});
finalContent = finalContent.replace(line, "").trim();
}
else {
const choiceRegex = /\[\[(.*?)(?:\s*(\||->)\s*(.*?))?\]\]/g;
const choiceMatch = choiceRegex.exec(line);
if (choiceMatch) {
const [content, text, delimeter, link] = choiceMatch;
if (delimeter) {
choices.push({
text: text.trim(),
link: link.trim(),
});
}
else {
choices.push({
text: "",
link: text.trim(),
});
}
finalContent = finalContent.replace(content, "").trim();
}
}
});
return finalContent;
}
function extractVariables(content) {
const setVariablePattern = /\(set:\s*\$(\w+)\s*to\s*(.*)\)/g;
const passageVariables = {};
content.replace(setVariablePattern, (setExpr, varName, value) => {
passageVariables[varName] = parseValue(value.trim());
return "";
});
return passageVariables;
}
function parseValue(value) {
value = value.trim();
if (value.startsWith('"') && value.endsWith('"')) {
return value.slice(1, -1); // Remove quotes
}
if (!isNaN(Number(value))) {
return Number(value);
}
const datamapPattern = /\(datamap:\s*(.*?)\)/;
if (datamapPattern.test(value)) {
const datamapContent = value.match(datamapPattern)?.[1];
if (datamapContent) {
return parseDatamap(datamapContent);
}
}
if (value === "true")
return true;
if (value === "false")
return false;
return value;
}
function parseDatamap(content) {
const pairs = content.split(",").map((pair) => pair.trim());
const datamap = {};
for (let i = 0; i < pairs.length; i += 2) {
const key = pairs[i].trim().replace(/"/g, "");
const value = parseValue(pairs[i + 1].trim());
datamap[key] = value;
}
return datamap;
}
function moveStartNodeToFront(passages, startNodeName) {
const startIndex = passages.findIndex((passage) => passage.name === startNodeName);
if (startIndex > 0) {
const [startNode] = passages.splice(startIndex, 1);
passages.unshift(startNode);
}
}
export * from "./types.js";
//# sourceMappingURL=index.js.map