import autobind from 'autobind-decorator'; import { PageVar, envVarsDef, Fn, HpmlScope, HpmlError } from '.'; import { version } from '@/config'; import { AiScript, utils, values } from '@syuilo/aiscript'; import { createAiScriptEnv } from '../aiscript/api'; import { collectPageVars } from '../collect-page-vars'; import { initHpmlLib, initAiLib } from './lib'; import * as os from '@/os'; import { markRaw, ref, Ref, unref } from 'vue'; import { Expr, isLiteralValue, Variable } from './expr'; /** * Hpml evaluator */ export class Hpml { private variables: Variable[]; private pageVars: PageVar[]; private envVars: Record; public aiscript?: AiScript; public pageVarUpdatedCallback?: values.VFn; public canvases: Record = {}; public vars: Ref> = ref({}); public page: Record; private opts: { randomSeed: string; visitor?: any; url?: string; enableAiScript: boolean; }; constructor(page: Hpml['page'], opts: Hpml['opts']) { this.page = page; this.variables = this.page.variables; this.pageVars = collectPageVars(this.page.content); this.opts = opts; if (this.opts.enableAiScript) { this.aiscript = markRaw(new AiScript({ ...createAiScriptEnv({ storageKey: 'pages:' + this.page.id, }), ...initAiLib(this) }, { in: (q) => { return new Promise(ok => { os.inputText({ title: q, }).then(({ canceled, result: a }) => { ok(a); }); }); }, out: (value) => { console.log(value); }, log: (type, params) => { }, })); this.aiscript.scope.opts.onUpdated = (name, value) => { this.eval(); }; } const date = new Date(); this.envVars = { AI: 'kawaii', VERSION: version, URL: this.page ? `${opts.url}/@${this.page.user.username}/pages/${this.page.name}` : '', LOGIN: opts.visitor != null, NAME: opts.visitor ? opts.visitor.name || opts.visitor.username : '', USERNAME: opts.visitor ? opts.visitor.username : '', USERID: opts.visitor ? opts.visitor.id : '', NOTES_COUNT: opts.visitor ? opts.visitor.notesCount : 0, FOLLOWERS_COUNT: opts.visitor ? opts.visitor.followersCount : 0, FOLLOWING_COUNT: opts.visitor ? opts.visitor.followingCount : 0, IS_CAT: opts.visitor ? opts.visitor.isCat : false, SEED: opts.randomSeed ? opts.randomSeed : '', YMD: `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`, AISCRIPT_DISABLED: !this.opts.enableAiScript, NULL: null, }; this.eval(); } @autobind public eval() { try { this.vars.value = this.evaluateVars(); } catch (err) { //this.onError(e); } } @autobind public interpolate(str: string) { if (str == null) return null; return str.replace(/{(.+?)}/g, match => { const v = unref(this.vars)[match.slice(1, -1).trim()]; return v == null ? 'NULL' : v.toString(); }); } @autobind public callAiScript(fn: string) { try { if (this.aiscript) this.aiscript.execFn(this.aiscript.scope.get(fn), []); } catch (err) {} } @autobind public registerCanvas(id: string, canvas: any) { this.canvases[id] = canvas; } @autobind public updatePageVar(name: string, value: any) { const pageVar = this.pageVars.find(v => v.name === name); if (pageVar !== undefined) { pageVar.value = value; if (this.pageVarUpdatedCallback) { if (this.aiscript) this.aiscript.execFn(this.pageVarUpdatedCallback, [values.STR(name), utils.jsToVal(value)]); } } else { throw new HpmlError(`No such page var '${name}'`); } } @autobind public updateRandomSeed(seed: string) { this.opts.randomSeed = seed; this.envVars.SEED = seed; } @autobind private _interpolateScope(str: string, scope: HpmlScope) { return str.replace(/{(.+?)}/g, match => { const v = scope.getState(match.slice(1, -1).trim()); return v == null ? 'NULL' : v.toString(); }); } @autobind public evaluateVars(): Record { const values: Record = {}; for (const [k, v] of Object.entries(this.envVars)) { values[k] = v; } for (const v of this.pageVars) { values[v.name] = v.value; } for (const v of this.variables) { values[v.name] = this.evaluate(v, new HpmlScope([values])); } return values; } @autobind private evaluate(expr: Expr, scope: HpmlScope): any { if (isLiteralValue(expr)) { if (expr.type === null) { return null; } if (expr.type === 'number') { return parseInt((expr.value as any), 10); } if (expr.type === 'text' || expr.type === 'multiLineText') { return this._interpolateScope(expr.value || '', scope); } if (expr.type === 'textList') { return this._interpolateScope(expr.value || '', scope).trim().split('\n'); } if (expr.type === 'ref') { return scope.getState(expr.value); } if (expr.type === 'aiScriptVar') { if (this.aiscript) { try { return utils.valToJs(this.aiscript.scope.get(expr.value)); } catch (err) { return null; } } else { return null; } } // Define user function if (expr.type === 'fn') { return { slots: expr.value.slots.map(x => x.name), exec: (slotArg: Record) => { return this.evaluate(expr.value.expression, scope.createChildScope(slotArg, expr.id)); }, } as Fn; } return; } // Call user function if (expr.type.startsWith('fn:')) { const fnName = expr.type.split(':')[1]; const fn = scope.getState(fnName); const args = {} as Record; for (let i = 0; i < fn.slots.length; i++) { const name = fn.slots[i]; args[name] = this.evaluate(expr.args[i], scope); } return fn.exec(args); } if (expr.args === undefined) return null; const funcs = initHpmlLib(expr, scope, this.opts.randomSeed, this.opts.visitor); // Call function const fnName = expr.type; const fn = (funcs as any)[fnName]; if (fn == null) { throw new HpmlError(`No such function '${fnName}'`); } else { return fn(...expr.args.map(x => this.evaluate(x, scope))); } } }