@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.
Files changed (55) hide show
  1. package/README.md +60 -0
  2. package/dist/types/ast.d.ts +65 -0
  3. package/dist/types/compile.d.ts +52 -0
  4. package/dist/types/component/index.d.ts +259 -0
  5. package/dist/types/component/rates/binance.d.ts +34 -0
  6. package/dist/types/component/rates/coingecko.d.ts +47 -0
  7. package/dist/types/component/rates/index.d.ts +84 -0
  8. package/dist/types/component/rules.d.ts +180 -0
  9. package/dist/types/component/schema.d.ts +38 -0
  10. package/dist/types/env.d.ts +24 -0
  11. package/dist/types/errors.d.ts +18 -0
  12. package/dist/types/index.d.ts +42 -0
  13. package/dist/types/modes/converter.d.ts +46 -0
  14. package/dist/types/modes/financial.d.ts +59 -0
  15. package/dist/types/modes/index.d.ts +38 -0
  16. package/dist/types/modes/programmer.d.ts +50 -0
  17. package/dist/types/modes/scientific.d.ts +28 -0
  18. package/dist/types/modes/standard.d.ts +32 -0
  19. package/dist/types/parser/index.d.ts +32 -0
  20. package/dist/types/plot/plot2d.d.ts +70 -0
  21. package/dist/types/plot/plot3d.d.ts +68 -0
  22. package/dist/types/render/error.d.ts +19 -0
  23. package/dist/types/theme.d.ts +35 -0
  24. package/dist/types/to-expr.d.ts +13 -0
  25. package/dist/types/utils.d.ts +9 -0
  26. package/docs/CALC-FORMAT.md +79 -0
  27. package/package.json +71 -0
  28. package/schemas/financial-inputs.schema.json +15 -0
  29. package/schemas/jaren-calc-ast.schema.json +91 -0
  30. package/schemas/jaren-calc-state.schema.json +52 -0
  31. package/src/ast.js +96 -0
  32. package/src/compile.js +119 -0
  33. package/src/component/index.js +352 -0
  34. package/src/component/rates/binance.js +44 -0
  35. package/src/component/rates/coingecko.js +63 -0
  36. package/src/component/rates/index.js +116 -0
  37. package/src/component/rules.js +118 -0
  38. package/src/component/schema.js +24 -0
  39. package/src/env.js +163 -0
  40. package/src/errors.js +23 -0
  41. package/src/index.js +85 -0
  42. package/src/modes/converter.js +73 -0
  43. package/src/modes/financial.js +89 -0
  44. package/src/modes/index.js +24 -0
  45. package/src/modes/programmer.js +69 -0
  46. package/src/modes/scientific.js +31 -0
  47. package/src/modes/standard.js +39 -0
  48. package/src/parser/index.js +220 -0
  49. package/src/plot/plot2d.js +221 -0
  50. package/src/plot/plot3d.js +177 -0
  51. package/src/render/error.js +25 -0
  52. package/src/theme.js +76 -0
  53. package/src/to-expr.js +84 -0
  54. package/src/utils.js +11 -0
  55. 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';
@@ -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; }