@jarenjs/calc 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/dist/types/ast.d.ts +65 -0
- package/dist/types/compile.d.ts +52 -0
- package/dist/types/component/index.d.ts +259 -0
- package/dist/types/component/rates/binance.d.ts +34 -0
- package/dist/types/component/rates/coingecko.d.ts +47 -0
- package/dist/types/component/rates/index.d.ts +84 -0
- package/dist/types/component/rules.d.ts +180 -0
- package/dist/types/component/schema.d.ts +38 -0
- package/dist/types/env.d.ts +24 -0
- package/dist/types/errors.d.ts +18 -0
- package/dist/types/index.d.ts +42 -0
- package/dist/types/modes/converter.d.ts +46 -0
- package/dist/types/modes/financial.d.ts +59 -0
- package/dist/types/modes/index.d.ts +38 -0
- package/dist/types/modes/programmer.d.ts +50 -0
- package/dist/types/modes/scientific.d.ts +28 -0
- package/dist/types/modes/standard.d.ts +32 -0
- package/dist/types/parser/index.d.ts +32 -0
- package/dist/types/plot/plot2d.d.ts +70 -0
- package/dist/types/plot/plot3d.d.ts +68 -0
- package/dist/types/render/error.d.ts +19 -0
- package/dist/types/theme.d.ts +35 -0
- package/dist/types/to-expr.d.ts +13 -0
- package/dist/types/utils.d.ts +9 -0
- package/docs/CALC-FORMAT.md +79 -0
- package/package.json +71 -0
- package/schemas/financial-inputs.schema.json +15 -0
- package/schemas/jaren-calc-ast.schema.json +91 -0
- package/schemas/jaren-calc-state.schema.json +52 -0
- package/src/ast.js +96 -0
- package/src/compile.js +119 -0
- package/src/component/index.js +352 -0
- package/src/component/rates/binance.js +44 -0
- package/src/component/rates/coingecko.js +63 -0
- package/src/component/rates/index.js +116 -0
- package/src/component/rules.js +118 -0
- package/src/component/schema.js +24 -0
- package/src/env.js +163 -0
- package/src/errors.js +23 -0
- package/src/index.js +85 -0
- package/src/modes/converter.js +73 -0
- package/src/modes/financial.js +89 -0
- package/src/modes/index.js +24 -0
- package/src/modes/programmer.js +69 -0
- package/src/modes/scientific.js +31 -0
- package/src/modes/standard.js +39 -0
- package/src/parser/index.js +220 -0
- package/src/plot/plot2d.js +221 -0
- package/src/plot/plot3d.js +177 -0
- package/src/render/error.js +25 -0
- package/src/theme.js +76 -0
- package/src/to-expr.js +84 -0
- package/src/utils.js +11 -0
- package/styles/calc.css +128 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The error vnode. The render path is total — it emits this
|
|
4
|
+
* instead of throwing. A small standalone `<svg>` that
|
|
5
|
+
* states the parse/eval error with its line/column when known.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { svgRoot, textAt } from '@jarenjs/view/helpers';
|
|
9
|
+
import { createTheme } from '../theme.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {{ message: string, line?: number, column?: number }} error
|
|
13
|
+
* @param {{ theme?: any, width?: number, height?: number }} [options]
|
|
14
|
+
* @returns {any}
|
|
15
|
+
*/
|
|
16
|
+
export function errorToVnode(error, options = {}) {
|
|
17
|
+
const theme = createTheme(options.theme ?? 'default');
|
|
18
|
+
const width = options.width ?? 480;
|
|
19
|
+
const height = options.height ?? 80;
|
|
20
|
+
const where = error.line !== undefined ? ` (line ${error.line}, col ${error.column})` : '';
|
|
21
|
+
return svgRoot('calc-plot', width, height, theme, [
|
|
22
|
+
textAt(12, 28, 'Expression error', 13, { fill: theme.tokens.errorText, 'font-weight': 'bold', class: 'calc-error-title' }),
|
|
23
|
+
textAt(12, 48, String(error.message) + where, 11, { fill: theme.tokens.text, class: 'calc-error-msg' }),
|
|
24
|
+
], 'err:' + String(error.message));
|
|
25
|
+
}
|
package/src/theme.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Plot theme tokens (mirrors `@jarenjs/mermaid`'s theme). Resolves a
|
|
4
|
+
* name (or overrides) into concrete colors (written as SVG presentation
|
|
5
|
+
* attributes so `toSvgString()` is a valid standalone image) plus a
|
|
6
|
+
* matching `--calc-*` CSS variable set stamped inline on the root `<svg>`.
|
|
7
|
+
* The inline stamp beats every stylesheet rule, so the stamp itself is the
|
|
8
|
+
* re-theming hook: the `'host'` theme stamps linked variables as
|
|
9
|
+
* `var(--<host-token>, <concrete>)` (see `HOST_VARS`), making plots follow
|
|
10
|
+
* a host's light/dark tokens live without a re-render. Only the token
|
|
11
|
+
* tables live here; the resolution mechanics are shared
|
|
12
|
+
* (`@jarenjs/view/helpers` `resolveTheme`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { resolveTheme } from '@jarenjs/view/helpers';
|
|
16
|
+
|
|
17
|
+
/** @type {Record<string, Record<string, string>>} */
|
|
18
|
+
const THEMES = {
|
|
19
|
+
default: {
|
|
20
|
+
background: 'transparent',
|
|
21
|
+
axis: '#64748b',
|
|
22
|
+
grid: '#e2e8f0',
|
|
23
|
+
text: '#334155',
|
|
24
|
+
series1: '#2563eb',
|
|
25
|
+
series2: '#0891b2',
|
|
26
|
+
series3: '#d97706',
|
|
27
|
+
surfaceLo: '#1e3a8a',
|
|
28
|
+
surfaceHi: '#93c5fd',
|
|
29
|
+
wire: '#172554',
|
|
30
|
+
errorText: '#b91c1c',
|
|
31
|
+
fontFamily: '"trebuchet ms", verdana, arial, sans-serif',
|
|
32
|
+
},
|
|
33
|
+
dark: {
|
|
34
|
+
background: 'transparent',
|
|
35
|
+
axis: '#94a3b8',
|
|
36
|
+
grid: '#334155',
|
|
37
|
+
text: '#cbd5e1',
|
|
38
|
+
series1: '#60a5fa',
|
|
39
|
+
series2: '#22d3ee',
|
|
40
|
+
series3: '#fbbf24',
|
|
41
|
+
surfaceLo: '#1e40af',
|
|
42
|
+
surfaceHi: '#bfdbfe',
|
|
43
|
+
wire: '#0b1020',
|
|
44
|
+
errorText: '#f87171',
|
|
45
|
+
fontFamily: '"trebuchet ms", verdana, arial, sans-serif',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Host custom-property links for the `'host'` theme: token key → the host
|
|
51
|
+
* token it should follow (the site token vocabulary, docs/DESIGN.md §2). The
|
|
52
|
+
* default theme's concrete colors remain as `var()` fallbacks, so the
|
|
53
|
+
* same SVG is standalone-valid outside any host. Tokens with no host
|
|
54
|
+
* equivalent (series2, the 3-D surface shades, wire) stay concrete.
|
|
55
|
+
* @type {Record<string, string>}
|
|
56
|
+
*/
|
|
57
|
+
export const HOST_VARS = {
|
|
58
|
+
axis: '--muted',
|
|
59
|
+
grid: '--border',
|
|
60
|
+
text: '--fg',
|
|
61
|
+
series1: '--accent',
|
|
62
|
+
series3: '--warn',
|
|
63
|
+
errorText: '--fail',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve a theme name or override object. The name `'host'` resolves the
|
|
68
|
+
* default tokens linked to the host token vocabulary via {@link HOST_VARS}.
|
|
69
|
+
* @param {string | Record<string, any>} [nameOrOverrides]
|
|
70
|
+
* @returns {{ name: string, tokens: Record<string, string>, cssVars: Record<string, string> }}
|
|
71
|
+
*/
|
|
72
|
+
export function createTheme(nameOrOverrides = 'default') {
|
|
73
|
+
return resolveTheme(THEMES, 'calc', nameOrOverrides, HOST_VARS);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { THEMES };
|
package/src/to-expr.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file `toExpression(ast)` — the canonical printer. It is a **round-trip
|
|
4
|
+
* fixed point**: `parseExpression(toExpression(ast))` deep-equals
|
|
5
|
+
* `ast` for every AST the parser can produce. Parentheses are emitted
|
|
6
|
+
* from operator precedence/associativity only where removing them would
|
|
7
|
+
* change the parse; numbers print in canonical decimal (the AST never
|
|
8
|
+
* carried their original radix).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Printing precedence per node, matching the grammar in parser/index.js. */
|
|
12
|
+
const PREC = {
|
|
13
|
+
'|': 10, '&': 20, '<<': 30, '>>': 30,
|
|
14
|
+
'+': 40, '-': 40, '*': 50, '/': 50,
|
|
15
|
+
unary: 70, '^': 80, postfix: 90, atom: 100,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {any} node
|
|
20
|
+
* @returns {number}
|
|
21
|
+
*/
|
|
22
|
+
function precOf(node) {
|
|
23
|
+
switch (node.type) {
|
|
24
|
+
case 'binary': return PREC[node.op];
|
|
25
|
+
case 'unary': return PREC.unary;
|
|
26
|
+
case 'postfix': return PREC.postfix;
|
|
27
|
+
default: return PREC.atom;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Print a number canonically (finite → `String`, non-finite → the named
|
|
33
|
+
* constants the parser recognizes).
|
|
34
|
+
* @param {number} v
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
function printNum(v) {
|
|
38
|
+
if (Number.isNaN(v)) return 'nan';
|
|
39
|
+
if (v === Infinity) return 'inf';
|
|
40
|
+
if (v === -Infinity) return '-inf';
|
|
41
|
+
return String(v);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {any} node
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
export function toExpression(node) {
|
|
49
|
+
if (node === null || typeof node !== 'object') {
|
|
50
|
+
throw new TypeError('toExpression: not an AST node');
|
|
51
|
+
}
|
|
52
|
+
switch (node.type) {
|
|
53
|
+
case 'num': return printNum(node.value);
|
|
54
|
+
case 'const': return node.name;
|
|
55
|
+
case 'var': return node.name;
|
|
56
|
+
case 'call':
|
|
57
|
+
return `${node.name}(${node.args.map(toExpression).join(', ')})`;
|
|
58
|
+
case 'unary': {
|
|
59
|
+
const argP = precOf(node.arg);
|
|
60
|
+
const inner = toExpression(node.arg);
|
|
61
|
+
return `${node.op}${argP < PREC.unary ? `(${inner})` : inner}`;
|
|
62
|
+
}
|
|
63
|
+
case 'postfix': {
|
|
64
|
+
const argP = precOf(node.arg);
|
|
65
|
+
const inner = toExpression(node.arg);
|
|
66
|
+
return `${argP < PREC.postfix ? `(${inner})` : inner}${node.op}`;
|
|
67
|
+
}
|
|
68
|
+
case 'binary': {
|
|
69
|
+
const p = PREC[node.op];
|
|
70
|
+
const rightAssoc = node.op === '^';
|
|
71
|
+
const lp = precOf(node.left);
|
|
72
|
+
const rp = precOf(node.right);
|
|
73
|
+
// left needs parens when strictly lower, or equal-and-right-assoc
|
|
74
|
+
const leftParen = lp < p || (lp === p && rightAssoc);
|
|
75
|
+
// right needs parens when strictly lower, or equal-and-left-assoc
|
|
76
|
+
const rightParen = rp < p || (rp === p && !rightAssoc);
|
|
77
|
+
const l = toExpression(node.left);
|
|
78
|
+
const r = toExpression(node.right);
|
|
79
|
+
return `${leftParen ? `(${l})` : l} ${node.op} ${rightParen ? `(${r})` : r}`;
|
|
80
|
+
}
|
|
81
|
+
default:
|
|
82
|
+
throw new TypeError(`toExpression: unknown node type '${node.type}'`);
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Small shared helpers for the calc engine.
|
|
4
|
+
*
|
|
5
|
+
* The content hash is the suite's single fingerprint primitive, so calc
|
|
6
|
+
* re-exports the one implementation from `@jarenjs/core` rather than
|
|
7
|
+
* carrying its own copy: equal content hits the same O(1) fast path
|
|
8
|
+
* (vnode `key`, memo key) everywhere downstream.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { hashContent } from '@jarenjs/core/string';
|
package/styles/calc.css
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jarenjs/calc component stylesheet. `--calc-*` design tokens with a
|
|
3
|
+
* `.dark` override (mirrors mermaid.css). These tokens theme the HTML
|
|
4
|
+
* chrome (keypad, panels), which hosts may override in their own sheet
|
|
5
|
+
* (higher specificity than the `.dark .calc` block here). The plot SVG
|
|
6
|
+
* carries concrete presentation colors (valid standalone) plus inline
|
|
7
|
+
* `--calc-*` stamps that beat this stylesheet — a live plot follows a
|
|
8
|
+
* host's tokens via the `'host'` theme (`src/theme.js` `HOST_VARS`).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
.calc {
|
|
12
|
+
--calc-bg: #ffffff;
|
|
13
|
+
--calc-panel: #f8fafc;
|
|
14
|
+
--calc-border: #e2e8f0;
|
|
15
|
+
--calc-text: #1e293b;
|
|
16
|
+
--calc-muted: #64748b;
|
|
17
|
+
--calc-key: #f1f5f9;
|
|
18
|
+
--calc-key-hover: #e2e8f0;
|
|
19
|
+
--calc-op: #dbeafe;
|
|
20
|
+
--calc-fn: #e0f2fe;
|
|
21
|
+
--calc-equals: #2563eb;
|
|
22
|
+
--calc-equals-text: #ffffff;
|
|
23
|
+
--calc-clear: #fee2e2;
|
|
24
|
+
--calc-accent: #2563eb;
|
|
25
|
+
--calc-accent-text: #ffffff;
|
|
26
|
+
|
|
27
|
+
display: grid;
|
|
28
|
+
gap: 12px;
|
|
29
|
+
max-width: 720px;
|
|
30
|
+
color: var(--calc-text);
|
|
31
|
+
font-family: "trebuchet ms", verdana, arial, sans-serif;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.dark .calc,
|
|
35
|
+
.calc.dark {
|
|
36
|
+
--calc-bg: #0f172a;
|
|
37
|
+
--calc-panel: #1e293b;
|
|
38
|
+
--calc-border: #334155;
|
|
39
|
+
--calc-text: #e2e8f0;
|
|
40
|
+
--calc-muted: #94a3b8;
|
|
41
|
+
--calc-key: #1e293b;
|
|
42
|
+
--calc-key-hover: #334155;
|
|
43
|
+
--calc-op: #1e3a8a;
|
|
44
|
+
--calc-fn: #0c4a6e;
|
|
45
|
+
--calc-equals: #3b82f6;
|
|
46
|
+
--calc-equals-text: #ffffff;
|
|
47
|
+
--calc-clear: #7f1d1d;
|
|
48
|
+
--calc-accent: #60a5fa;
|
|
49
|
+
--calc-accent-text: #0b1020;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.calc-modes { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
53
|
+
/* narrow hosts: the five mode tabs become one scroll strip instead of
|
|
54
|
+
wrapping to a second row; the edge fade hints that the row scrolls */
|
|
55
|
+
@media (max-width: 480px) {
|
|
56
|
+
.calc-modes {
|
|
57
|
+
flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
|
|
58
|
+
mask-image: linear-gradient(90deg, #000 92%, transparent);
|
|
59
|
+
}
|
|
60
|
+
.calc-mode { white-space: nowrap; flex: none; }
|
|
61
|
+
}
|
|
62
|
+
.calc-mode {
|
|
63
|
+
padding: 6px 12px; border: 1px solid var(--calc-border); border-radius: 8px;
|
|
64
|
+
background: var(--calc-panel); color: var(--calc-text); cursor: pointer; font-size: 13px;
|
|
65
|
+
}
|
|
66
|
+
.calc-mode-active { background: var(--calc-accent); color: var(--calc-accent-text); border-color: var(--calc-accent); }
|
|
67
|
+
|
|
68
|
+
.calc-display {
|
|
69
|
+
background: var(--calc-panel); border: 1px solid var(--calc-border); border-radius: 10px;
|
|
70
|
+
padding: 12px 14px; text-align: right; min-height: 56px;
|
|
71
|
+
}
|
|
72
|
+
.calc-entry { color: var(--calc-muted); font-size: 15px; word-break: break-all; }
|
|
73
|
+
.calc-result { font-size: 28px; font-weight: 700; }
|
|
74
|
+
.calc-err { color: #dc2626; font-size: 13px; text-align: left; }
|
|
75
|
+
|
|
76
|
+
.calc-keypad { display: grid; gap: 6px; }
|
|
77
|
+
.calc-row { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 6px; }
|
|
78
|
+
.calc-key {
|
|
79
|
+
padding: 12px; border: 1px solid var(--calc-border); border-radius: 8px;
|
|
80
|
+
background: var(--calc-key); color: var(--calc-text); cursor: pointer; font-size: 16px;
|
|
81
|
+
}
|
|
82
|
+
.calc-key:hover { background: var(--calc-key-hover); }
|
|
83
|
+
.calc-key-op { background: var(--calc-op); }
|
|
84
|
+
.calc-key-fn { background: var(--calc-fn); font-size: 13px; }
|
|
85
|
+
.calc-key-const { background: var(--calc-fn); }
|
|
86
|
+
.calc-key-clear { background: var(--calc-clear); }
|
|
87
|
+
.calc-key-equals { background: var(--calc-equals); color: var(--calc-equals-text); font-weight: 700; }
|
|
88
|
+
.calc-key-span2 { grid-column: span 2; }
|
|
89
|
+
.calc-key-mem { font-size: 13px; }
|
|
90
|
+
|
|
91
|
+
.calc-mem { display: flex; align-items: center; gap: 8px; }
|
|
92
|
+
/* the readout keeps a breath of air off the host's right edge */
|
|
93
|
+
.calc-memval { color: var(--calc-muted); font-size: 13px; margin-left: auto; padding-right: 4px; }
|
|
94
|
+
|
|
95
|
+
.calc-bases { display: grid; gap: 4px; background: var(--calc-panel); border: 1px solid var(--calc-border); border-radius: 10px; padding: 10px; font-family: ui-monospace, monospace; }
|
|
96
|
+
.calc-baserow { display: flex; justify-content: space-between; gap: 12px; }
|
|
97
|
+
.calc-baselabel { color: var(--calc-muted); }
|
|
98
|
+
.calc-baseval { font-weight: 600; word-break: break-all; }
|
|
99
|
+
.calc-wordopts, .calc-angle { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
100
|
+
.calc-word, .calc-angle-btn {
|
|
101
|
+
padding: 4px 10px; border: 1px solid var(--calc-border); border-radius: 6px;
|
|
102
|
+
background: var(--calc-key); color: var(--calc-text); cursor: pointer; font-size: 12px;
|
|
103
|
+
}
|
|
104
|
+
.calc-word.active, .calc-angle-btn.active { background: var(--calc-accent); color: var(--calc-accent-text); }
|
|
105
|
+
|
|
106
|
+
.calc-financial, .calc-converter, .calc-plot-panel {
|
|
107
|
+
background: var(--calc-panel); border: 1px solid var(--calc-border);
|
|
108
|
+
border-radius: 10px; padding: 12px; display: grid; gap: 10px;
|
|
109
|
+
}
|
|
110
|
+
.calc-fin-result, .calc-conv-result { font-size: 18px; }
|
|
111
|
+
.calc-conv-row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
|
112
|
+
.calc-conv-input { flex: 1 1 100px; padding: 8px; border: 1px solid var(--calc-border); border-radius: 6px; background: var(--calc-bg); color: var(--calc-text); }
|
|
113
|
+
.calc-dim, .calc-conv-from, .calc-conv-to { padding: 8px; border: 1px solid var(--calc-border); border-radius: 6px; background: var(--calc-bg); color: var(--calc-text); }
|
|
114
|
+
.calc-swap, .calc-rates-refresh { padding: 8px 10px; border: 1px solid var(--calc-border); border-radius: 6px; background: var(--calc-key); cursor: pointer; }
|
|
115
|
+
.calc-rates { color: var(--calc-muted); font-size: 12px; display: flex; align-items: center; gap: 6px; }
|
|
116
|
+
.calc-rates-stale { color: #d97706; }
|
|
117
|
+
|
|
118
|
+
.calc-plot-kinds { display: flex; gap: 6px; }
|
|
119
|
+
.calc-plotkind { padding: 4px 12px; border: 1px solid var(--calc-border); border-radius: 6px; background: var(--calc-key); color: var(--calc-text); cursor: pointer; font-size: 13px; }
|
|
120
|
+
.calc-plotkind.active { background: var(--calc-accent); color: var(--calc-accent-text); }
|
|
121
|
+
.calc-plot-expr { padding: 8px; border: 1px solid var(--calc-border); border-radius: 6px; background: var(--calc-bg); color: var(--calc-text); font-family: ui-monospace, monospace; }
|
|
122
|
+
.calc-plot-svg { display: flex; justify-content: center; }
|
|
123
|
+
.calc-plot { background: var(--calc-bg); border-radius: 8px; }
|
|
124
|
+
.calc-grid { stroke: var(--calc-border); }
|
|
125
|
+
|
|
126
|
+
.calc-tape { display: grid; gap: 2px; font-family: ui-monospace, monospace; font-size: 13px; color: var(--calc-muted); max-height: 120px; overflow: auto; }
|
|
127
|
+
.calc-tape-row { display: flex; gap: 6px; }
|
|
128
|
+
.calc-tape-res { color: var(--calc-text); font-weight: 600; }
|