diff --git a/src/client/scripts/hpml/evaluator.ts b/src/client/scripts/hpml/evaluator.ts index dc6a6d5a4..4fa95e89c 100644 --- a/src/client/scripts/hpml/evaluator.ts +++ b/src/client/scripts/hpml/evaluator.ts @@ -1,19 +1,13 @@ import autobind from 'autobind-decorator'; -import * as seedrandom from 'seedrandom'; -import { Variable, PageVar, envVarsDef, funcDefs, Block, isFnBlock } from '.'; +import { Variable, PageVar, envVarsDef, Block, isFnBlock, 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 { initLib } from './lib'; +import { initHpmlLib, initAiLib } from './lib'; import * as os from '@/os'; import { markRaw, ref, Ref } from 'vue'; -type Fn = { - slots: string[]; - exec: (args: Record) => ReturnType; -}; - /** * Hpml evaluator */ @@ -41,7 +35,7 @@ export class Hpml { if (this.opts.enableAiScript) { this.aiscript = markRaw(new AiScript({ ...createAiScriptEnv({ storageKey: 'pages:' + this.page.id - }), ...initLib(this)}, { + }), ...initAiLib(this)}, { in: (q) => { return new Promise(ok => { os.dialog({ @@ -137,7 +131,7 @@ export class Hpml { } @autobind - private _interpolate(str: string, scope: Scope) { + 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(); @@ -157,14 +151,14 @@ export class Hpml { } for (const v of this.variables) { - values[v.name] = this.evaluate(v, new Scope([values])); + values[v.name] = this.evaluate(v, new HpmlScope([values])); } return values; } @autobind - private evaluate(block: Block, scope: Scope): any { + private evaluate(block: Block, scope: HpmlScope): any { if (block.type === null) { return null; } @@ -174,11 +168,11 @@ export class Hpml { } if (block.type === 'text' || block.type === 'multiLineText') { - return this._interpolate(block.value || '', scope); + return this._interpolateScope(block.value || '', scope); } if (block.type === 'textList') { - return this._interpolate(block.value || '', scope).trim().split('\n'); + return this._interpolateScope(block.value || '', scope).trim().split('\n'); } if (block.type === 'ref') { @@ -197,7 +191,8 @@ export class Hpml { } } - if (isFnBlock(block)) { // ユーザー関数定義 + // Define user function + if (isFnBlock(block)) { return { slots: block.value.slots.map(x => x.name), exec: (slotArg: Record) => { @@ -206,7 +201,8 @@ export class Hpml { } as Fn; } - if (block.type.startsWith('fn:')) { // ユーザー関数呼び出し + // Call user function + if (block.type.startsWith('fn:')) { const fnName = block.type.split(':')[1]; const fn = scope.getState(fnName); const args = {} as Record; @@ -219,77 +215,9 @@ export class Hpml { if (block.args === undefined) return null; - const date = new Date(); - const day = `${this.opts.visitor ? this.opts.visitor.id : ''} ${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; - - const funcs: { [p in keyof typeof funcDefs]: Function } = { - not: (a: boolean) => !a, - or: (a: boolean, b: boolean) => a || b, - and: (a: boolean, b: boolean) => a && b, - eq: (a: any, b: any) => a === b, - notEq: (a: any, b: any) => a !== b, - gt: (a: number, b: number) => a > b, - lt: (a: number, b: number) => a < b, - gtEq: (a: number, b: number) => a >= b, - ltEq: (a: number, b: number) => a <= b, - if: (bool: boolean, a: any, b: any) => bool ? a : b, - for: (times: number, fn: Fn) => { - const result = []; - for (let i = 0; i < times; i++) { - result.push(fn.exec({ - [fn.slots[0]]: i + 1 - })); - } - return result; - }, - add: (a: number, b: number) => a + b, - subtract: (a: number, b: number) => a - b, - multiply: (a: number, b: number) => a * b, - divide: (a: number, b: number) => a / b, - mod: (a: number, b: number) => a % b, - round: (a: number) => Math.round(a), - strLen: (a: string) => a.length, - strPick: (a: string, b: number) => a[b - 1], - strReplace: (a: string, b: string, c: string) => a.split(b).join(c), - strReverse: (a: string) => a.split('').reverse().join(''), - join: (texts: string[], separator: string) => texts.join(separator || ''), - stringToNumber: (a: string) => parseInt(a), - numberToString: (a: number) => a.toString(), - splitStrByLine: (a: string) => a.split('\n'), - pick: (list: any[], i: number) => list[i - 1], - listLen: (list: any[]) => list.length, - random: (probability: number) => Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * 100) < probability, - rannum: (min: number, max: number) => min + Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * (max - min + 1)), - randomPick: (list: any[]) => list[Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * list.length)], - dailyRandom: (probability: number) => Math.floor(seedrandom(`${day}:${block.id}`)() * 100) < probability, - dailyRannum: (min: number, max: number) => min + Math.floor(seedrandom(`${day}:${block.id}`)() * (max - min + 1)), - dailyRandomPick: (list: any[]) => list[Math.floor(seedrandom(`${day}:${block.id}`)() * list.length)], - seedRandom: (seed: any, probability: number) => Math.floor(seedrandom(seed)() * 100) < probability, - seedRannum: (seed: any, min: number, max: number) => min + Math.floor(seedrandom(seed)() * (max - min + 1)), - seedRandomPick: (seed: any, list: any[]) => list[Math.floor(seedrandom(seed)() * list.length)], - DRPWPM: (list: string[]) => { - const xs = []; - let totalFactor = 0; - for (const x of list) { - const parts = x.split(' '); - const factor = parseInt(parts.pop()!, 10); - const text = parts.join(' '); - totalFactor += factor; - xs.push({ factor, text }); - } - const r = seedrandom(`${day}:${block.id}`)() * totalFactor; - let stackedFactor = 0; - for (const x of xs) { - if (r >= stackedFactor && r <= stackedFactor + x.factor) { - return x.text; - } else { - stackedFactor += x.factor; - } - } - return xs[0].text; - }, - }; + const funcs = initHpmlLib(block, scope, this.opts.randomSeed, this.opts.visitor); + // Call function const fnName = block.type; const fn = (funcs as any)[fnName]; if (fn == null) { @@ -299,53 +227,3 @@ export class Hpml { } } } - -class HpmlError extends Error { - public info?: any; - - constructor(message: string, info?: any) { - super(message); - - this.info = info; - - // Maintains proper stack trace for where our error was thrown (only available on V8) - if (Error.captureStackTrace) { - Error.captureStackTrace(this, HpmlError); - } - } -} - -class Scope { - private layerdStates: Record[]; - public name: string; - - constructor(layerdStates: Scope['layerdStates'], name?: Scope['name']) { - this.layerdStates = layerdStates; - this.name = name || 'anonymous'; - } - - @autobind - public createChildScope(states: Record, name?: Scope['name']): Scope { - const layer = [states, ...this.layerdStates]; - return new Scope(layer, name); - } - - /** - * 指定した名前の変数の値を取得します - * @param name 変数名 - */ - @autobind - public getState(name: string): any { - for (const later of this.layerdStates) { - const state = later[name]; - if (state !== undefined) { - return state; - } - } - - throw new HpmlError( - `No such variable '${name}' in scope '${this.name}'`, { - scope: this.layerdStates - }); - } -} diff --git a/src/client/scripts/hpml/index.ts b/src/client/scripts/hpml/index.ts index c87d5b998..fa34b25d8 100644 --- a/src/client/scripts/hpml/index.ts +++ b/src/client/scripts/hpml/index.ts @@ -2,6 +2,8 @@ * Hpml */ +import autobind from 'autobind-decorator'; + import { faMagic, faSquareRootAlt, @@ -27,6 +29,7 @@ import { faCalculator, } from '@fortawesome/free-solid-svg-icons'; import { faFlag } from '@fortawesome/free-regular-svg-icons'; +import { Hpml } from './evaluator'; export type Block = { id: string; @@ -47,6 +50,11 @@ export type Variable = Block & { name: string; }; +export type Fn = { + slots: string[]; + exec: (args: Record) => ReturnType; +}; + export type Type = 'string' | 'number' | 'boolean' | 'stringArray' | null; export const funcDefs: Record = { @@ -137,3 +145,53 @@ export function isLiteralBlock(v: Block) { if (literalDefs[v.type]) return true; return false; } + +export class HpmlScope { + private layerdStates: Record[]; + public name: string; + + constructor(layerdStates: HpmlScope['layerdStates'], name?: HpmlScope['name']) { + this.layerdStates = layerdStates; + this.name = name || 'anonymous'; + } + + @autobind + public createChildScope(states: Record, name?: HpmlScope['name']): HpmlScope { + const layer = [states, ...this.layerdStates]; + return new HpmlScope(layer, name); + } + + /** + * 指定した名前の変数の値を取得します + * @param name 変数名 + */ + @autobind + public getState(name: string): any { + for (const later of this.layerdStates) { + const state = later[name]; + if (state !== undefined) { + return state; + } + } + + throw new HpmlError( + `No such variable '${name}' in scope '${this.name}'`, { + scope: this.layerdStates + }); + } +} + +export class HpmlError extends Error { + public info?: any; + + constructor(message: string, info?: any) { + super(message); + + this.info = info; + + // Maintains proper stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, HpmlError); + } + } +} diff --git a/src/client/scripts/hpml/lib.ts b/src/client/scripts/hpml/lib.ts index 5ec507841..11e4f2fc3 100644 --- a/src/client/scripts/hpml/lib.ts +++ b/src/client/scripts/hpml/lib.ts @@ -2,6 +2,8 @@ import * as tinycolor from 'tinycolor2'; import Chart from 'chart.js'; import { Hpml } from './evaluator'; import { values, utils } from '@syuilo/aiscript'; +import { Block, Fn, HpmlScope } from '.'; +import * as seedrandom from 'seedrandom'; // https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs Chart.pluginService.register({ @@ -16,7 +18,7 @@ Chart.pluginService.register({ } }); -export function initLib(hpml: Hpml) { +export function initAiLib(hpml: Hpml) { return { 'MkPages:updated': values.FN_NATIVE(([callback]) => { hpml.pageVarUpdatedCallback = (callback as values.VFn); @@ -122,3 +124,79 @@ export function initLib(hpml: Hpml) { }) }; } + +export function initHpmlLib(block: Block, scope: HpmlScope, randomSeed: string, visitor?: any) { + + const date = new Date(); + const day = `${visitor ? visitor.id : ''} ${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; + + const funcs: Record = { + not: (a: boolean) => !a, + or: (a: boolean, b: boolean) => a || b, + and: (a: boolean, b: boolean) => a && b, + eq: (a: any, b: any) => a === b, + notEq: (a: any, b: any) => a !== b, + gt: (a: number, b: number) => a > b, + lt: (a: number, b: number) => a < b, + gtEq: (a: number, b: number) => a >= b, + ltEq: (a: number, b: number) => a <= b, + if: (bool: boolean, a: any, b: any) => bool ? a : b, + for: (times: number, fn: Fn) => { + const result: any[] = []; + for (let i = 0; i < times; i++) { + result.push(fn.exec({ + [fn.slots[0]]: i + 1 + })); + } + return result; + }, + add: (a: number, b: number) => a + b, + subtract: (a: number, b: number) => a - b, + multiply: (a: number, b: number) => a * b, + divide: (a: number, b: number) => a / b, + mod: (a: number, b: number) => a % b, + round: (a: number) => Math.round(a), + strLen: (a: string) => a.length, + strPick: (a: string, b: number) => a[b - 1], + strReplace: (a: string, b: string, c: string) => a.split(b).join(c), + strReverse: (a: string) => a.split('').reverse().join(''), + join: (texts: string[], separator: string) => texts.join(separator || ''), + stringToNumber: (a: string) => parseInt(a), + numberToString: (a: number) => a.toString(), + splitStrByLine: (a: string) => a.split('\n'), + pick: (list: any[], i: number) => list[i - 1], + listLen: (list: any[]) => list.length, + random: (probability: number) => Math.floor(seedrandom(`${randomSeed}:${block.id}`)() * 100) < probability, + rannum: (min: number, max: number) => min + Math.floor(seedrandom(`${randomSeed}:${block.id}`)() * (max - min + 1)), + randomPick: (list: any[]) => list[Math.floor(seedrandom(`${randomSeed}:${block.id}`)() * list.length)], + dailyRandom: (probability: number) => Math.floor(seedrandom(`${day}:${block.id}`)() * 100) < probability, + dailyRannum: (min: number, max: number) => min + Math.floor(seedrandom(`${day}:${block.id}`)() * (max - min + 1)), + dailyRandomPick: (list: any[]) => list[Math.floor(seedrandom(`${day}:${block.id}`)() * list.length)], + seedRandom: (seed: any, probability: number) => Math.floor(seedrandom(seed)() * 100) < probability, + seedRannum: (seed: any, min: number, max: number) => min + Math.floor(seedrandom(seed)() * (max - min + 1)), + seedRandomPick: (seed: any, list: any[]) => list[Math.floor(seedrandom(seed)() * list.length)], + DRPWPM: (list: string[]) => { + const xs: any[] = []; + let totalFactor = 0; + for (const x of list) { + const parts = x.split(' '); + const factor = parseInt(parts.pop()!, 10); + const text = parts.join(' '); + totalFactor += factor; + xs.push({ factor, text }); + } + const r = seedrandom(`${day}:${block.id}`)() * totalFactor; + let stackedFactor = 0; + for (const x of xs) { + if (r >= stackedFactor && r <= stackedFactor + x.factor) { + return x.text; + } else { + stackedFactor += x.factor; + } + } + return xs[0].text; + }, + }; + + return funcs; +}