UNPKG

quickmathjs

Version:

A simple web-based plaintext calculator harnessing the power of math.js for intuitive free-form calculations. Now with a command-line interface (CLI) for testing purposes.

359 lines (303 loc) 11.3 kB
#!/usr/bin/env node /* QuickMathsJS-WebCalc An intuitive web-based calculator using math.js. Features inline results and supports free-form calculations. Copyright (C) 2023 Brian Khuu This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. */ const fs = require('fs'); const mathjs = require('mathjs'); const webcalc = require('./calculator.js')(mathjs); const open = require('opn'); const express = require('express'); const zlib = require('zlib'); const path = require('path'); const { version } = require('./package.json'); require('colors'); const Diff = require('diff'); function calculateFileContent(content, useSections = false) { if (useSections) { return webcalc.calculateWithMathSections(content); } else { return webcalc.calculate(content); } } function processFile(filePath, useSections, callback) { fs.readFile(filePath, 'utf8', (err, data) => { if (err) { console.error("Error reading the file:", err); return; } const updatedContent = calculateFileContent(data, useSections); callback(filePath, updatedContent); }); } /****************************************************************************** * TESTING */ function processTests(tests, testCaseFilePath) { let failures = 0; tests.forEach((test, index) => { try { const result = calculateFileContent(test.given); const passfail = (result === test.expect) ? "PASS"['green'] : "FAIL"['red'] console.log(`Test ${index + 1} (${test.name}): ${passfail}`); if (result !== test.expect) { const diff = Diff.diffChars(test.expect, result); let diff_list = ''; diff.forEach((part) => { diff_list += part.added ? part.value.underline.green: part.removed ? part.value.strikethrough.red: part.value.grey; }); console.log(`Given:\n${test.given}`); console.log(`Diff:\n${diff_list}`); failures++; } } catch (error) { // Extract the undefined symbol name from the error message console.log(`\nTest ${index + 1} (${test.name}): ${"FAIL - ERROR"['red']}`); console.log("ERR:", error.message); console.log(error.stack.toString()); console.log('\n'); failures++; } }); return failures; } // This has before and after cases (good for checking replacement test examples) function runFullTestCase(testCaseFilePath) { const fullPath = path.join(__dirname, testCaseFilePath); const content = fs.readFileSync(fullPath, 'utf-8'); const regex = /^#+ (.*?)\n(?:[^#]*?)\*\*(?:For Example|Input|Given):\*\*\n```(?:.*?)\n([\s\S]+?)\n```\n\n\*\*(?:Result|Output|Expect):\*\*\n```(?:.*?)\n([\s\S]+?)\n```$/gm; const tests = []; console.log(`FULL TEST CASE FILE: ${testCaseFilePath}`); let match; while ((match = regex.exec(content)) !== null) { tests.push({ name: match[1], given: match[2], expect: match[3] }); tests.push({ name: match[1] + "- STATIC TESTING", given: match[3], expect: match[3] }); } return processTests(tests, testCaseFilePath); } // This has simple cases (e.g. results already present examples) function runMathBlockTestCase(testCaseFilePath) { const fullPath = path.join(__dirname, testCaseFilePath); const content = fs.readFileSync(fullPath, 'utf-8'); const regex = /^#+ (.*?)\n(?:[^#]*?)^```calc(?:.*?)\n([\s\S]+?)\n^```$/gm; const tests = []; console.log(`BLOCK TEST CASE FILE: ${testCaseFilePath}`); let match; while ((match = regex.exec(content)) !== null) { tests.push({ name: match[1], given: match[2], expect: match[2] }); } return processTests(tests, testCaseFilePath); } function runDelimTests() { // Test for the math delimiter feature let failures = 0; /** * Tagged template function to remove common indentation from template literals. * This allows for cleaner code formatting without affecting the string's actual content. * * @param {Array<string>} strings - The static parts of the template literal. * @param {...any} values - The interpolated values within the template literal. * @return {string} - The processed string with common indentation removed. */ function stripIndent(strings, ...values) { // Combine the strings and values back into the full string const fullString = strings.reduce((acc, str, i) => `${acc}${str}${values[i] || ''}`, ''); // Find the common indentation at the beginning of each line const match = fullString.match(/^[ \t]*(?=\S)/gm); if (!match) return fullString; // Calculate the amount of indentation to remove const indent = Math.min(...match.map(el => el.length)); // Create a regular expression to remove the common indentation const regexp = new RegExp(`^[ \\t]{${indent}}`, 'gm'); // Return the string with the common indentation removed return indent > 0 ? fullString.replace(regexp, '') : fullString; } const mockContent = stripIndent` This is a sample content. \`\`\`calc 1 + 1 = ? \`\`\` \`\`\`calc 1 + 1 = ? 1 + 1 = ? \`\`\` \`\`\`calc {id = "testid"} 1 + 1 = ? 1 + 1 = ? \`\`\` \`\`\`calc 1 + 1 = ? 1 + 1 = ? \`\`\` This should remain unchanged. \`\`\`calc 1 + 1 = \`\`\` This should also remain unchanged.`; const expectedContent = stripIndent` This is a sample content. \`\`\`calc 1 + 1 = 2 \`\`\` \`\`\`calc 1 + 1 = 2 1 + 1 = 2 \`\`\` \`\`\`calc {id = "testid"} 1 + 1 = 2 1 + 1 = 2 \`\`\` \`\`\`calc 1 + 1 = 2 1 + 1 = 2 \`\`\` This should remain unchanged. \`\`\`calc 1 + 1 = \`\`\` This should also remain unchanged.`; const mathDelimiterResult = calculateFileContent(mockContent, true); if (mathDelimiterResult.trim() === expectedContent.trim()) { console.log(`Math delimiter test (Using LF): ${"PASS"['green']}`); } else { console.log(`Math delimiter test (Using LF): ${"FAIL"['red']}`); console.log('Expected:\n', expectedContent); console.log('Got:\n', mathDelimiterResult); failures++; } // Windows Style Newline Handling const mockContent_crlf = expectedContent.replace(/\n/g, "\r\n"); const expectedContent_crlf = expectedContent.replace(/\n/g, "\r\n"); const mathDelimiterResult_crlf = calculateFileContent(mockContent_crlf, true); if (mathDelimiterResult_crlf.trim() === expectedContent_crlf.trim()) { console.log(`Math delimiter test (Using CRLF): ${"PASS"['green']}`); } else { console.log(`Math delimiter test (Using CRLF): ${"FAIL"['red']}`); console.log('Expected:\n', expectedContent_crlf); console.log('Got:\n', mathDelimiterResult_crlf); failures++; } return failures; } function runTests() { Error.stackTraceLimit = Infinity; let failures = runDelimTests(); failures += failures > 0 ? 0 : runMathBlockTestCase('readme.md'); failures += failures > 0 ? 0 : runFullTestCase('readme.md'); failures += failures > 0 ? 0 : runMathBlockTestCase('userexamples.md'); failures += failures > 0 ? 0 : runFullTestCase('userexamples.md'); failures += failures > 0 ? 0 : runFullTestCase('testcases_basics.md'); failures += failures > 0 ? 0 : runFullTestCase('testcases_advance.md'); failures += failures > 0 ? 0 : runFullTestCase('testcases_constants.md'); failures += failures > 0 ? 0 : runFullTestCase('testcases_errors_and_edgecases.md'); if (failures > 0) { console.error(`Failed ${failures} test(s). Exiting.`); process.exit(1); } else { console.error(`All test passed. Exiting.`); } } /****************************************************************************** * CLI */ // Initialise webcalc internals webcalc.initialise(); // Check for command line flags const useSectionsFlag = '--sections'; const testFlag = '--test'; const helpFlag = '--help'; const webFlag = '--web'; const versionFlag = '--version'; function displayHelp() { console.log(` Usage: quickmath [OPTIONS] [FILE] Options: --help Show this help message and exit. --version Display the current version of QuickMathsJS. --sections Evaluate only sections surrounded by the \`\`\`calc delimiter. --web Launch the web interface. --test Run predefined test cases. Examples: quickmath --help Show this help message and exit. quickmath --version Display the current version. quickmath path/to/your/file.txt Evaluate entire file. quickmath --sections path/to/your/file.txt Evaluate only math sections in file. quickmath --web path/to/your/file.txt Launch the web interface and load entire file. quickmath --test Run predefined test cases. `); } function displayVersion() { console.log(`QuickMathsJS version: ${version}`); } if (process.argv.includes(helpFlag)) { displayHelp(); } else if (process.argv.includes(versionFlag)) { displayVersion(); } else if (process.argv.includes(testFlag)) { runTests(); } else if (process.argv.includes(webFlag)) { const app = express(); const port = 3000; // Serve static files from the root directory app.use(express.static(__dirname)); const launchURL = async () => { let url = `http://localhost:${port}`; if (process.argv.length > 3) { const filePath = process.argv[3]; try { const fileContent = await fs.promises.readFile(filePath, 'utf8'); const compressed = zlib.gzipSync(fileContent).toString('base64'); url += `#${compressed}`; } catch (err) { console.error("Error reading the file:", err); process.exit(1); } } open(url); }; app.listen(port, () => { console.log(`QuickMathsJS-WebCalc is running at http://localhost:${port}`); launchURL(); }); } else { if (process.argv.length < 3) { console.error("Please provide a path to the input file or use --help for more options."); displayHelp(); process.exit(1); } const filePath = process.argv[2]; const useSections = process.argv.includes(useSectionsFlag); processFile(filePath, useSections, (path, updatedContent) => { fs.writeFile(path, updatedContent, (err) => { if (err) { console.error("Error writing to the file:", err); return; } console.log("File updated successfully!"); }); }); }