@fusedashlabs/ui9000-workspace
Version:
UI9000 workspace MCP. One tool, show_workspace, draws a chart from your table. Set TYPESAFE_API_KEY to let Jev choose the chart.
246 lines (245 loc) • 7.36 kB
JavaScript
import { DATA_PROFILE_KEYS } from '../spec/data-profile.js';
const PROFILE_KEY_SET = new Set(DATA_PROFILE_KEYS);
const CMP_OPS = ['===', '!==', '<=', '>=', '<', '>'];
function isDataProfileKey(key) {
return PROFILE_KEY_SET.has(key);
}
function tokenize(when) {
const tokens = [];
let i = 0;
const src = when;
const skipWs = () => {
while (i < src.length && /\s/.test(src[i]))
i += 1;
};
while (i < src.length) {
skipWs();
if (i >= src.length)
break;
if (src.startsWith('profile.', i)) {
i += 'profile.'.length;
const start = i;
if (!/[A-Za-z]/.test(src[i] ?? '')) {
throw new Error(`when: expected DataProfile key after profile. at ${start}`);
}
i += 1;
while (i < src.length && /[A-Za-z0-9]/.test(src[i]))
i += 1;
const key = src.slice(start, i);
if (!isDataProfileKey(key)) {
throw new Error(`when references unknown DataProfile key "${key}"`);
}
skipWs();
if (src[i] === '(') {
throw new Error('when does not allow calls');
}
tokens.push({ kind: 'profile', key });
continue;
}
if (src.startsWith('&&', i)) {
tokens.push({ kind: 'op', op: '&&' });
i += 2;
continue;
}
if (src.startsWith('||', i)) {
tokens.push({ kind: 'op', op: '||' });
i += 2;
continue;
}
if (src.startsWith('===', i)) {
tokens.push({ kind: 'op', op: '===' });
i += 3;
continue;
}
if (src.startsWith('!==', i)) {
tokens.push({ kind: 'op', op: '!==' });
i += 3;
continue;
}
if (src.startsWith('<=', i)) {
tokens.push({ kind: 'op', op: '<=' });
i += 2;
continue;
}
if (src.startsWith('>=', i)) {
tokens.push({ kind: 'op', op: '>=' });
i += 2;
continue;
}
if (src[i] === '<' || src[i] === '>') {
tokens.push({ kind: 'op', op: src[i] });
i += 1;
continue;
}
if (src[i] === '!') {
tokens.push({ kind: 'op', op: '!' });
i += 1;
continue;
}
if (src[i] === '(') {
tokens.push({ kind: 'lparen' });
i += 1;
continue;
}
if (src[i] === ')') {
tokens.push({ kind: 'rparen' });
i += 1;
continue;
}
if (src[i] === '-' || /[0-9]/.test(src[i])) {
const start = i;
if (src[i] === '-')
i += 1;
if (!/[0-9]/.test(src[i] ?? '')) {
throw new Error(`when uses disallowed characters near "${src.slice(start)}"`);
}
while (i < src.length && /[0-9]/.test(src[i]))
i += 1;
if (src[i] === '.') {
i += 1;
if (!/[0-9]/.test(src[i] ?? '')) {
throw new Error('when: number has a trailing dot');
}
while (i < src.length && /[0-9]/.test(src[i]))
i += 1;
}
tokens.push({ kind: 'number', value: Number(src.slice(start, i)) });
continue;
}
throw new Error(`when uses disallowed characters: ${src.slice(i)}`);
}
return tokens;
}
class Parser {
tokens;
profile;
pos = 0;
constructor(tokens, profile) {
this.tokens = tokens;
this.profile = profile;
}
peek() {
return this.tokens[this.pos];
}
peekOp(op) {
const token = this.peek();
return token?.kind === 'op' && token.op === op;
}
take() {
const token = this.tokens[this.pos];
if (!token)
throw new Error('when: unexpected end of expression');
this.pos += 1;
return token;
}
expectEnd() {
if (this.pos !== this.tokens.length) {
throw new Error('when: unexpected trailing tokens');
}
}
parseExpr() {
return this.parseOr();
}
parseOr() {
let left = this.parseAnd();
while (this.peekOp('||')) {
this.take();
const right = this.parseAnd();
left = left || right;
}
return left;
}
parseAnd() {
let left = this.parseComparison();
while (this.peekOp('&&')) {
this.take();
const right = this.parseComparison();
left = left && right;
}
return left;
}
parseComparison() {
const left = this.parseUnary();
const next = this.peek();
if (next?.kind === 'op' && CMP_OPS.includes(next.op)) {
const op = next.op;
this.take();
const right = this.parseUnary();
return compare(op, left, right);
}
return left;
}
parseUnary() {
const next = this.peek();
if (next?.kind === 'op' && next.op === '!') {
this.take();
return !this.parseUnary();
}
return this.parsePrimary();
}
parsePrimary() {
const token = this.take();
if (token.kind === 'profile') {
return this.profile[token.key];
}
if (token.kind === 'number') {
return token.value;
}
if (token.kind === 'lparen') {
const inner = this.parseExpr();
const close = this.take();
if (close.kind !== 'rparen') {
throw new Error('when: expected closing parenthesis');
}
return inner;
}
throw new Error('when: expected profile key, number, or grouped expression');
}
}
function compare(op, left, right) {
switch (op) {
case '===':
return left === right;
case '!==':
return left !== right;
case '<=':
return left <= right;
case '>=':
return left >= right;
case '<':
return left < right;
case '>':
return left > right;
}
}
/** Pull `profile.<key>` identifiers out of a `when` string. */
export function profileKeysInWhen(when) {
return tokenize(when)
.filter((token) => token.kind === 'profile')
.map((token) => token.key);
}
export function assertWhenUsesClosedProfile(when) {
if (!when.trim()) {
throw new Error('empty when expression');
}
tokenize(when);
}
export function assertProfileUsesClosedKeys(profile) {
for (const key of Object.keys(profile)) {
if (!PROFILE_KEY_SET.has(key)) {
throw new Error(`evalCases.profile has unknown key "${key}"`);
}
}
}
/**
* Evaluate a catalog `when` string against a profile.
* Only `profile.<DataProfileKey>`, numbers, && || ! comparisons, and grouping.
*/
export function evalWhen(when, profile) {
assertWhenUsesClosedProfile(when);
const parser = new Parser(tokenize(when), profile);
const value = parser.parseExpr();
parser.expectEnd();
return Boolean(value);
}
export { isDataProfileKey };