You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
164 lines
6.5 KiB
TypeScript
164 lines
6.5 KiB
TypeScript
import { tokenize } from './lexer'
|
|
import { Parser } from './parser'
|
|
import { recompile, definitions } from './compiler'
|
|
import { prettyPrint } from './ast'
|
|
|
|
export const store: Record<string, any> = {};
|
|
|
|
export const _rt = {
|
|
add: (a: number) => (b: number) => a + b,
|
|
sub: (a: number) => (b: number) => a - b,
|
|
mul: (a: number) => (b: number) => a * b,
|
|
div: (a: number) => (b: number) => a / b,
|
|
mod: (a: number) => (b: number) => a % b,
|
|
cat: (a: string) => (b: string) => a + b,
|
|
max: (a: number) => (b: number) => Math.max(a, b),
|
|
min: (a: number) => (b: number) => Math.min(a, b),
|
|
|
|
eq: (a: any) => (b: any) => ({ _tag: a === b ? 'True' : 'False' }),
|
|
neq: (a: any) => (b: any) => ({ _tag: a !== b ? 'True' : 'False' }),
|
|
gt: (a: any) => (b: any) => ({ _tag: a > b ? 'True' : 'False' }),
|
|
lt: (a: any) => (b: any) => ({ _tag: a < b ? 'True' : 'False' }),
|
|
gte: (a: any) => (b: any) => ({ _tag: a >= b ? 'True' : 'False' }),
|
|
lte: (a: any) => (b: any) => ({ _tag: a <= b ? 'True' : 'False' }),
|
|
|
|
ui: {
|
|
rect: (config: any) => ({ _kind: 'rect', ...config }),
|
|
text: (config: any) => ({ _kind: 'text', ...config }),
|
|
stack: (config: any) => ({ _kind: 'stack', ...config }),
|
|
row: (config: any) => ({ _kind: 'row', ...config }),
|
|
column: (config: any) => ({ _kind: 'column', ...config }),
|
|
padding: (config: any) => ({ _kind: 'padding', ...config }),
|
|
positioned: (config: any) => ({ _kind: 'positioned', ...config }),
|
|
clickable: (config: any) => ({ _kind: 'clickable', ...config }),
|
|
clip: (config: any) => ({ _kind: 'clip', ...config }),
|
|
opacity: (config: any) => ({ _kind: 'opacity', ...config }),
|
|
stateful: (config: any) => ({ _kind: 'stateful', ...config }),
|
|
measure: (config: any) => ({ _kind: 'measure', ...config }),
|
|
measureText: (text: string) => {
|
|
const canvas = document.createElement('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
if (ctx) {
|
|
ctx.font = '16px "SF Mono", "Monaco", "Menlo", monospace';
|
|
return Math.floor(ctx.measureText(text).width);
|
|
}
|
|
return text.length * 10; // fallback
|
|
},
|
|
},
|
|
|
|
batch: (events: any[]) => ({ _tag: 'Batch', _0: events }),
|
|
noOp: { _tag: 'NoOp' },
|
|
rerender: { _tag: 'Rerender' },
|
|
focus: (key: string) => ({ _tag: 'Focus', _0: key }),
|
|
|
|
nth: (i: number) => (xs: any[] | string) => i >= 0 && i < xs.length
|
|
? { _tag: 'Some', _0: xs[i] }
|
|
: { _tag: 'None' },
|
|
len: (xs: any[] | string) => xs.length,
|
|
str: (x: any) => String(x),
|
|
chars: (s: string) => s.split(''),
|
|
join: (delim: string) => (xs: string[]) => xs.join(delim),
|
|
split: (delim: string) => (xs: string) => xs.split(delim),
|
|
slice: (s: string | any[]) => (start: number) => (end: number) => s.slice(start, end),
|
|
debug: (label: string) => (value: any) => { console.log(label, value); return value; },
|
|
fuzzyMatch: (query: string) => (target: string) => {
|
|
const q = query.toLowerCase();
|
|
const t = target.toLowerCase();
|
|
let qi = 0;
|
|
for (let ti = 0; ti < t.length && qi < q.length; ti++) {
|
|
if (t[ti] === q[qi]) qi++;
|
|
}
|
|
return { _tag: qi === q.length ? 'True' : 'False' };
|
|
},
|
|
storeSearch: (query: string) => {
|
|
return Object.keys(store).filter(name => _rt.fuzzyMatch(query)(name)._tag === 'True');
|
|
},
|
|
getSource: (name: string) => {
|
|
const ast = definitions.get(name);
|
|
if (!ast) return "";
|
|
const printed = prettyPrint(ast);
|
|
return printed;
|
|
},
|
|
rebind: (name: string, pathOrValue: any, maybeValue?: any) => {
|
|
if (maybeValue === undefined) {
|
|
if (pathOrValue && pathOrValue.ast) {
|
|
recompile(name, pathOrValue._ast);
|
|
} else {
|
|
store[name] = pathOrValue;
|
|
}
|
|
} else {
|
|
const path = pathOrValue as string[];
|
|
let obj = store[name];
|
|
for (let i = 0; i < path.length - 1; i++) {
|
|
obj = obj[path[i]];
|
|
}
|
|
obj[path[path.length - 1]] = maybeValue;
|
|
}
|
|
},
|
|
measure: (ui: any): { width: number, height: number } => {
|
|
switch (ui._kind) {
|
|
case 'rect': return { width: ui.w, height: ui.h };
|
|
case 'text': return { width: ui.content.length * 10, height: 20 }; // TODO
|
|
case 'clip': return { width: ui.w, height: ui.h };
|
|
case 'row': {
|
|
let totalWidth = 0;
|
|
let maxHeight = 0;
|
|
for (const child of ui.children) {
|
|
const size = _rt.measure(child);
|
|
totalWidth += size.width;
|
|
maxHeight = Math.max(maxHeight, size.height);
|
|
}
|
|
totalWidth += ui.gap * (ui.children.length - 1);
|
|
return { width: totalWidth, height: maxHeight };
|
|
}
|
|
|
|
case 'column': {
|
|
let totalHeight = 0;
|
|
let maxWidth = 0;
|
|
for (const child of ui.children) {
|
|
const size = _rt.measure(child);
|
|
totalHeight += size.height;
|
|
maxWidth = Math.max(maxWidth, size.width);
|
|
}
|
|
totalHeight += ui.gap * (ui.children.length - 1);
|
|
return { width: maxWidth, height: totalHeight };
|
|
}
|
|
|
|
case 'padding': {
|
|
const childSize = _rt.measure(ui.child);
|
|
return {
|
|
width: childSize.width + ui.amount * 2,
|
|
height: childSize.height + ui.amount * 2,
|
|
}
|
|
}
|
|
|
|
case 'stack': {
|
|
let maxWidth = 0;
|
|
let maxHeight = 0;
|
|
for (const child of ui.children) {
|
|
const size = _rt.measure(child);
|
|
maxWidth = Math.max(maxWidth, size.width);
|
|
maxHeight = Math.max(maxHeight, size.height);
|
|
}
|
|
|
|
return { width: maxWidth, height: maxHeight };
|
|
}
|
|
|
|
case 'clickable':
|
|
case 'opacity':
|
|
case 'positioned':
|
|
return _rt.measure(ui.child);
|
|
|
|
default:
|
|
return { width: 0, height: 0 };
|
|
}
|
|
},
|
|
redefine: (name: string) => (code: string) => {
|
|
const tokens = tokenize(`_tmp = ${code};`);
|
|
const parser = new Parser(tokens, "");
|
|
const defs = parser.parse();
|
|
recompile(name, defs[0]. body);
|
|
return { _tag: 'Ok' };
|
|
}
|
|
}
|