textadv
Version:
Text Adventures generator from Markdown files
192 lines • 7.06 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import * as fs from 'fs';
import * as YAML from 'yaml';
import path from 'path';
import { Project, Type } from './types.js';
import { removeDiacritics } from './utils.js';
import { parseMarkdown } from './md-parser.js';
function parse(ast, project, basePath) {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
let inMeta = false;
let childIndex = 0;
let node = project;
if (ast.type !== 'root')
throw new Error('expected root node as parameter');
for (const child of ast.children) {
++childIndex;
if (child.type === 'thematicBreak' && childIndex === 1) {
inMeta = true;
continue;
}
if (child.type === 'heading' && child.depth === 1) {
// project data
const { id, name } = parseTitle(child.children[0].value);
project.id = id;
project.name = name;
node = project;
}
if (child.type === 'heading' && child.depth === 2) {
if (inMeta) {
inMeta = false;
project.meta = parseMeta(child.children[0].value);
continue;
}
const { id, name, type } = parseTitle(child.children[0].value);
if (!type) {
throw new Error(`Missing heading type on "${name}"`);
}
const room = project.addChild(type, id);
room.name = name;
node = room;
}
if (child.type === 'paragraph') {
if (((_a = child.children[0]) === null || _a === void 0 ? void 0 : _a.type) === 'link') {
const linkType = ((_b = child.children[0]) === null || _b === void 0 ? void 0 : _b.children[0]).value;
switch (linkType) {
case 'extends':
const url = (_c = child.children[0]) === null || _c === void 0 ? void 0 : _c.url;
const newPath = path.join(basePath, url);
yield parseFile(newPath, project);
break;
default:
throw new Error(`Invalid link type "${linkType}"`);
}
}
// otherwise, read as text
const text = child.children[0].value;
if (text) {
node.intro.push(text);
}
}
if (child.type === 'list') {
const codes = child.children.map((item) => parseCode(item));
node.onInput.push(...codes);
}
}
return project;
});
}
function parseCode(item) {
const paragraph = item.children[0];
if (paragraph.type !== 'paragraph')
throw new Error('expected paragraph type');
if (paragraph.children[0].type !== 'text')
throw new Error('expected text type');
const itemText = paragraph.children[0].value;
const codesList = item.children[1];
if (codesList.type !== 'list')
throw new Error('expected list type');
const ops = [];
for (const codesListItem of codesList.children) {
const codeItemParagraph = codesListItem.children[0];
if (codeItemParagraph.type !== 'paragraph')
throw new Error('expected paragraph type');
if (codeItemParagraph.children[0].type !== 'text')
throw new Error('expected text type');
const codesListItemText = codeItemParagraph.children[0].value.replace(/""/g, '\\"').trim();
try {
ops.push(parseOp(codesListItemText));
}
catch (ex) {
console.error(`Error parsing codes: ${codesListItemText}`);
throw ex;
}
}
return {
on: itemText,
ops,
done: true // TODO: find a way to express "not done" on the markdown
};
}
function parseTitle(title) {
let type, m;
title = title.trim();
if (m = title.match(/^📍(.+)$/)) {
type = Type.location;
title = m[1].trim();
}
else if (m = title.match(/^📦(.+)$/)) {
type = Type.object;
title = m[1].trim();
}
if (m = title.match(/^([^\[]+)\[([^\]]+)\]$/)) {
return {
type,
name: m[1],
id: m[2],
};
}
return {
type,
name: title,
id: removeDiacritics(title).toLocaleLowerCase().replace(/[^a-z0-9]+/g, '-')
};
}
function parseOp(text) {
try {
const jsonParsed = JSON.parse(text);
if (typeof (jsonParsed) === 'string') {
return {
cmd: 'print',
params: [jsonParsed]
};
}
}
catch (err) {
// fallthrough
}
const [cmd, ...params] = text.split(/ +/g);
if (!["print", "goto", "set", "clear", "zero", "notzero", "continue", "check-room"].includes(cmd)) {
throw new Error(`Invalid command "${cmd}"`);
}
return {
cmd: cmd,
params,
};
}
export function validateProject(project) {
const errors = [];
const rooms = project.getChildrenByType(Type.location);
function validateCodes(codes) {
for (const code of codes) {
for (const op of code.ops) {
if (typeof (op) === 'string')
continue;
if (op.cmd === 'goto') {
if (!rooms[op.params[0]]) {
errors.push(`Room "${op.params[0]}" not found`);
}
}
}
}
}
validateCodes(project.onInput);
for (const room of project.children) {
validateCodes(room.onInput);
}
return errors;
}
export function parseFile(filePath, project) {
return __awaiter(this, void 0, void 0, function* () {
const basePath = path.dirname(filePath);
const src = fs.readFileSync(filePath, 'utf8');
const mdAST = parseMarkdown(src);
return {
project: yield parse(mdAST, project !== null && project !== void 0 ? project : new Project(), basePath),
mdAST
};
});
}
function parseMeta(value) {
return YAML.parse(value);
}
//# sourceMappingURL=parser.js.map