UNPKG

bat2js

Version:
294 lines (214 loc) 7.22 kB
#!/usr/bin/env node // -- START HEADER -- // Copyright (c) benja2998. You may redistribute under the terms of the GPL-3.0-or-later. // bat2js is a Batch to JavaScript compiler. // -- END HEADER -- // -- START IMPORTS -- import * as fs from 'fs'; import * as path from 'path'; const { randomUUID } = require('crypto'); // -- END IMPORTS -- export type token = { type: string; value: string; argument: string; } export class tokenType { public static readonly COMMENT = 'COMMENT'; public static readonly COMMAND = 'COMMAND'; public static readonly STRING = 'STRING'; public static readonly VARIABLE = 'VARIABLE'; public static readonly NUMBER = 'NUMBER'; public static readonly SETVAR = 'SETVAR'; } // Define a function to tokenize the file contents export function tokenize(contents: string): token[] { // Parse the contents into an array of lines let lines = contents.split('\n'); let tokens: token[] = []; // Iterate over each word for (let i = 0; i < lines.length; i++) { let line = lines[i]; let words = line.split(' '); if (line.startsWith('@')) { continue; } if (line === '') { continue; } // Check if \r comes at the end of a word or is the only word if (words[words.length - 1].endsWith('\r')) { // Remove the \r words[words.length - 1] = words[words.length - 1].slice(0, -1); } // Do the same but for \n if (words[words.length - 1].endsWith('\n')) { // Remove the \n words[words.length - 1] = words[words.length - 1].slice(0, -1); } // Remove empty words words = words.filter(word => word !== ''); // Now that we've cleaned up, let's tokenize for (let j = 0; j < words.length; j++) { let word = words[j]; let restOfLine = line.slice(line.indexOf(word) + word.length).trim(); // Check if the word is a comment by checking if it's rem or :: if (word.startsWith('rem') || word.startsWith('::')) { // Get the comment by looking at the rest of the line let comment = line.slice(line.indexOf(word) + word.length).trim(); // Add the comment to the tokens tokens.push({ type: tokenType.COMMENT, value: comment, argument: '' // No argument for a comment }); continue; } else { // Check if the word is a command if (word === 'echo') { tokens.push({ type: tokenType.COMMAND, value: word, argument: restOfLine }); } else if (word === 'set') { // Get the rest of the line for the variable name and value let variableNameValue = restOfLine.trim(); // Remove quotes from the variable name and value variableNameValue = variableNameValue.replace(/['"]/g, ''); // Split the variable name and value by the first = let variableName = variableNameValue.split('=')[0]; let variableValue = variableNameValue.split('=')[1]; // Check if the variableValue is a string if (isNaN(Number(variableValue))) { // It's a string, so add quotes variableValue = `"${variableValue}"`; } // Add the variable to the tokens tokens.push({ type: tokenType.SETVAR, value: variableName, argument: variableValue }); } else if (word === 'pause') { let redirectTo = ''; // Get the next word let nextWord = words[j + 1]; // Check if the next word contains > if (nextWord !== undefined && nextWord.includes('>')) { // Redirection // Split nextWord by > and get the second part redirectTo = nextWord.split('>')[1]; // Check if redirectTo is empty if (redirectTo === '') { // Set redirectTo to the rest of the line redirectTo = line.slice(line.indexOf(nextWord) + nextWord.length).trim(); } } // Push the pause command to the tokens tokens.push({ type: tokenType.COMMAND, value: 'pause', argument: redirectTo }); } continue; } } } return tokens; } export function tokens_to_javascript(tokens: token[], filePath: string): string { // This is the complex part let javascript = `#!/usr/bin/env node // -- START HEADER -- // DO NOT MODIFY. // This file is automatically generated by bat2js. // To modify the contents, edit the original .bat file and run bat2js again. // Compiled from: ${filePath} // -- END HEADER -- const fs = require('fs'); const path = require('path'); const { spawn } = require('child_process'); const readline = require('readline'); // -- START FUNCTIONS -- // Make a waitForKeypress function function waitForKeypress(message) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => { rl.question(message, () => { rl.close(); resolve(); }); }); } // -- END FUNCTIONS -- // -- START COMPILED CODE -- `; for (let i = 0; i < tokens.length; i++) { let token = tokens[i]; if (token.type === tokenType.COMMENT) { javascript += `// ${token.value}\n`; } if (token.type === tokenType.COMMAND) { if (token.value === 'echo') { javascript += `console.log(\`${token.argument}\`);\n`; } if (token.value === 'pause') { // Check argument if (token.argument !== 'nul') { // Check if argument is a file if (token.argument !== undefined) { // Append "Press Enter to continue..." to the end of the file javascript += `waitForKeypress('');\n`; javascript += `fs.appendFileSync('${token.argument}', 'Press Enter to continue...');\n`; } else { javascript += `waitForKeypress('Press Enter to continue...');\n`; } } else { javascript += `waitForKeypress('');\n`; } } } if (token.type === tokenType.SETVAR) { // Check if javascript already contains "let ${token.value} = ${token.argument};" if (javascript.includes(`let ${token.value} = ${token.argument};`)) { javascript += `${token.value} = ${token.argument};\n`; } else { javascript += `let ${token.value} = ${token.argument};\n`; } } } javascript = javascript.replace(/%([^%]+)%/g, (_match, p1) => `\${${p1}}`); return javascript; } export function isPercentVariable(str: string): boolean { return /^%.*%$/.test(str); } if (require.main === module) { let filePath = process.argv[2]; if (!filePath) { console.error('Usagee: bat2js <file>');; process.exit(1); } if (!fs.existsSync(filePath)) { console.error(`File '${filePath}' does not exist.`); process.exit(1); } let fileContents = fs.readFileSync(filePath, 'utf8'); console.time('Compilation Time'); let tokens = tokenize(fileContents); let javascript = tokens_to_javascript(tokens, filePath); console.timeEnd('Compilation Time'); // Write the compiled JavaScript to a file const fileName = path.parse(filePath).name; const compiledFilePath = path.join(path.dirname(filePath), `${fileName}.js`); fs.writeFileSync(compiledFilePath, javascript); }