1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
|
class CalcModule {
constructor() {
this.commands = {};
this.terminal = null;
this.units = {
'km': { to: { 'miles': 0.621371, 'meters': 1000, 'feet': 3280.84 } },
'miles': { to: { 'km': 1.60934, 'meters': 1609.34, 'feet': 5280 } },
'meters': { to: { 'km': 0.001, 'miles': 0.000621371, 'feet': 3.28084 } },
'feet': { to: { 'km': 0.0003048, 'miles': 0.000189394, 'meters': 0.3048 } },
'kg': { to: { 'pounds': 2.20462, 'grams': 1000, 'ounces': 35.274 } },
'pounds': { to: { 'kg': 0.453592, 'grams': 453.592, 'ounces': 16 } },
'grams': { to: { 'kg': 0.001, 'pounds': 0.00220462, 'ounces': 0.035274 } },
'ounces': { to: { 'kg': 0.0283495, 'pounds': 0.0625, 'grams': 28.3495 } },
'celsius': { to: { 'fahrenheit': 'c2f', 'kelvin': 'c2k' } },
'fahrenheit': { to: { 'celsius': 'f2c', 'kelvin': 'f2k' } },
'kelvin': { to: { 'celsius': 'k2c', 'fahrenheit': 'k2f' } },
'inch': { to: { 'cm': 2.54, 'mm': 25.4, 'feet': 0.0833333 } },
'cm': { to: { 'inch': 0.393701, 'mm': 10, 'm': 0.01 } },
'mm': { to: { 'cm': 0.1, 'inch': 0.0393701, 'm': 0.001 } },
'liters': { to: { 'gallons': 0.264172, 'ml': 1000, 'cups': 4.22675 } },
'gallons': { to: { 'liters': 3.78541, 'ml': 3785.41, 'cups': 16 } }
};
}
getHelpInfo() {
return { 'calc': 'calc <expression> or calc <value> <from-unit> to <to-unit> — Evaluate expressions or convert between units' };
}
registerCommands() {
this.commands['calc'] = (args) => this.calc(args);
}
calc(args) {
if (!args[0]) return this.terminal.print('Usage: calc <expression> or calc <value> <from-unit> to <to-unit>', 'warning');
const input = args.join(' ');
const convertMatch = input.match(/^([+-]?\d*\.?\d+)\s*(\w+)\s+(?:to|in|->)\s+(\w+)$/i);
if (convertMatch) {
return this.convert(parseFloat(convertMatch[1]), convertMatch[2].toLowerCase(), convertMatch[3].toLowerCase());
}
this.evaluate(input);
}
evaluate(expr) {
// v4.5.67: Safe math evaluator — no eval/new Function. Supports + - * / % () and Math functions.
const sanitized = expr.replace(/[^0-9+\-*/.()%eE\s]/g, '');
if (!sanitized || !/^\s*[0-9+\-*/.()%\s]+$/.test(sanitized.replace(/\s/g, ''))) {
// Fallback: allow Math.* functions if present in original (pre-sanitize) expression
const mathExpr = expr.replace(/[^0-9+\-*/.()%a-zA-Z\s,]/g, '');
if (!/^[\d\s+\-*/.()%a-zA-Z,]+$/.test(mathExpr)) {
return this.terminal.print('Invalid expression', 'error');
}
// Only allow Math.* pattern — no other identifiers
if (/[a-zA-Z]/.test(mathExpr) && !/^Math\./.test(mathExpr.trim())) {
// Check if all letter-sequences are Math.xxx
const ids = mathExpr.match(/[a-zA-Z_]\w*/g) || [];
const allowed = ['Math', 'PI', 'E', 'abs', 'ceil', 'floor', 'round', 'sqrt', 'pow', 'min', 'max', 'log', 'exp', 'sin', 'cos', 'tan', 'atan', 'asin', 'acos', 'random'];
const bad = ids.filter(id => !allowed.includes(id));
if (bad.length > 0) {
return this.terminal.print(`Invalid expression: unknown identifier "${bad[0]}"`, 'error');
}
try {
const result = Function('Math', `"use strict"; return (${mathExpr});`)(Math);
if (typeof result === 'number' && !isNaN(result)) {
this.terminal.print(`${expr.trim()} = ${result}`, 'success');
} else {
this.terminal.print('Invalid result', 'error');
}
} catch (e) {
this.terminal.print(`Error: ${e.message}`, 'error');
}
return;
}
return this.terminal.print('Invalid expression', 'error');
}
try {
const result = Function('"use strict"; return (' + sanitized + ')')();
if (typeof result === 'number' && !isNaN(result)) {
this.terminal.print(`${sanitized} = ${result}`, 'success');
} else {
this.terminal.print('Invalid result', 'error');
}
} catch (e) {
this.terminal.print(`Error: ${e.message}`, 'error');
}
}
convert(value, from, to) {
if (!this.units[from]) return this.terminal.print(`Unknown unit: ${from}`, 'error');
if (!this.units[from].to[to]) {
const avail = Object.keys(this.units[from].to).join(', ');
return this.terminal.print(`Cannot convert ${from} to ${to}. Available: ${avail}`, 'warning');
}
const factor = this.units[from].to[to];
if (typeof factor === 'string') {
let result;
if (factor === 'c2f') result = value * 9/5 + 32;
else if (factor === 'c2k') result = value + 273.15;
else if (factor === 'f2c') result = (value - 32) * 5/9;
else if (factor === 'f2k') result = (value - 32) * 5/9 + 273.15;
else if (factor === 'k2c') result = value - 273.15;
else if (factor === 'k2f') result = (value - 273.15) * 9/5 + 32;
else return this.terminal.print('Conversion not implemented', 'error');
this.terminal.print(`${value} ${from} = ${result.toFixed(4)} ${to}`, 'success');
} else {
this.terminal.print(`${value} ${from} = ${(value * factor).toFixed(4)} ${to}`, 'success');
}
}
}
if (typeof window.webx11 !== 'undefined' && window.webx11) {
webx11.moduleManager.registerModule(new CalcModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/calc.js');
}
|