extwee
Version:
A story compiler tool using Twine-compatible formats
192 lines (156 loc) • 6.75 kB
JavaScript
// Import functions we need.
import {
parseTwine2HTML,
parseTwee,
parseStoryFormat,
parseTwine1HTML,
compileTwine2HTML,
compileTwine1HTML
} from '../../index.js';
// Import fs.
import { readFileSync, writeFileSync } from 'node:fs';
// Import Commander.
import { Command } from 'commander';
// Import package.json.
import Package from '../../package.json' with { type: 'json' };
// Import isFile function.
import { isFile } from './isFile.js';
/**
* Process command line arguments.
* @function CommandLineProcessing
* @description This function processes the command line arguments passed to the Extwee CLI.
* @module CLI/commandLineProcessing
* @param {Array} argv - The command line arguments passed to the CLI.
*/
export function CommandLineProcessing(argv) {
// Create a new Command.
const program = new Command();
program
.name('extwee')
.description('CLI for Extwee - A tool for compiling and decompiling Twine stories')
.version(Package.version, '-v, --version', 'Show version number')
.option('-c, --compile', 'Compile input into output')
.option('-d, --decompile', 'De-compile input into output')
.option('--twine1', 'Enable Twine 1 processing')
.option('--name <storyFormatName>', 'Name of the Twine 1 story format (needed for `code.js` inclusion)')
.option('--codejs <codeJSFile>', 'Twine 1 code.js file for use with Twine 1 HTML')
.option('--engine <engineFile>', 'Twine 1 engine.js file for use with Twine 1 HTML')
.option('--header <headerFile>', 'Twine 1 header.html file for use with Twine 1 HTML')
.option('-s <storyformat>, --storyformat <storyformat>', 'Path to story format file for Twine 2')
.option('-i <inputFile>, --input <inputFile>', 'Path to input file')
.option('-o <outputFile>, --output <outputFile>', 'Path to output file')
.addHelpText('after', `
Examples:
Compile Twee to Twine 2 HTML:
extwee -c -i story.twee -o story.html -s format.js
Compile Twee to Twine 1 HTML:
extwee --twine1 -c -i story.twee -o story.html --name "Sugarcane" --engine engine.js --header header.html --codejs code.js
Decompile Twine 2 HTML to Twee:
extwee -d -i story.html -o story.twee
Decompile Twine 1 HTML to Twee:
extwee --twine1 -d -i story.html -o story.twee
`);
// Parse the passed arguments with improved error handling
try {
program.parse(argv);
// Get parsed options
const options = program.opts();
// Validate and execute based on options
handleCommand(options);
} catch (error) {
if (error.code === 'commander.invalidArgument') {
console.error(`❌ ${error.message}`);
} else {
console.error(`❌ Error: ${error.message}`);
}
process.exit(1);
}
}
/**
* Handle the parsed command with improved validation and error messages
* @param {object} options - Parsed command options
*/
function handleCommand(options) {
try {
// Commander uses the first option name as the property, so -i becomes 'i' and --input becomes 'input'
const inputFile = options.input || options.i;
const outputFile = options.output || options.o;
const storyFormatFile = options.storyformat || options.s;
// Validate required options
if (!inputFile) {
throw new Error('Input file (-i, --input) is required');
}
if (!outputFile) {
throw new Error('Output file (-o, --output) is required');
}
if (!options.compile && !options.decompile) {
throw new Error('Either --compile (-c) or --decompile (-d) must be specified');
}
if (options.compile && options.decompile) {
throw new Error('Cannot specify both --compile and --decompile');
}
// Validate input file exists
if (!isFile(inputFile)) {
throw new Error(`Input file '${inputFile}' does not exist`);
}
const isTwine1Mode = options.twine1 === true;
const isDecompileMode = options.decompile === true;
const isCompileMode = options.compile === true;
if (isDecompileMode) {
// Decompile HTML to Twee
console.log(`🔄 Decompiling ${isTwine1Mode ? 'Twine 1' : 'Twine 2'} HTML to Twee...`);
const inputHTML = readFileSync(inputFile, 'utf-8');
let storyObject;
if (isTwine1Mode) {
storyObject = parseTwine1HTML(inputHTML);
} else {
storyObject = parseTwine2HTML(inputHTML);
}
writeFileSync(outputFile, storyObject.toTwee());
console.log(`✅ Successfully decompiled '${inputFile}' to '${outputFile}'`);
} else if (isCompileMode) {
// Compile Twee to HTML
console.log(`🔄 Compiling Twee to ${isTwine1Mode ? 'Twine 1' : 'Twine 2'} HTML...`);
const inputTwee = readFileSync(inputFile, 'utf-8');
const story = parseTwee(inputTwee);
if (isTwine1Mode) {
// Validate Twine 1 required options and files
const requiredOptions = [
{ opt: 'engine', desc: 'engine.js file' },
{ opt: 'header', desc: 'header.html file' },
{ opt: 'name', desc: 'story format name' },
{ opt: 'codejs', desc: 'code.js file' }
];
const missingOptions = requiredOptions.filter(({ opt }) => !options[opt]);
if (missingOptions.length > 0) {
throw new Error(`Twine 1 compilation requires the following options: ${missingOptions.map(({ opt }) => `--${opt}`).join(', ')}`);
}
// Validate required files exist
const requiredFiles = ['engine', 'header', 'codejs'];
for (const fileOpt of requiredFiles) {
if (!isFile(options[fileOpt])) {
throw new Error(`Twine 1 ${fileOpt} file '${options[fileOpt]}' does not exist`);
}
}
const Twine1HTML = compileTwine1HTML(story, options.engine, options.header, options.name, options.codejs);
writeFileSync(outputFile, Twine1HTML);
} else {
// Validate Twine 2 required options
if (!storyFormatFile) {
throw new Error('Twine 2 compilation requires --storyformat option');
}
if (!isFile(storyFormatFile)) {
throw new Error(`Story format file '${storyFormatFile}' does not exist`);
}
const inputStoryFormat = readFileSync(storyFormatFile, 'utf-8');
const parsedStoryFormat = parseStoryFormat(inputStoryFormat);
const Twine2HTML = compileTwine2HTML(story, parsedStoryFormat);
writeFileSync(outputFile, Twine2HTML);
}
console.log(`✅ Successfully compiled '${inputFile}' to '${outputFile}'`);
}
} catch (error) {
console.error(`❌ Operation failed: ${error.message}`);
process.exit(1);
}
}