mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-24 00:39:32 +09:00
hpml refactoring (#7047)
This commit is contained in:
parent
393ac6c203
commit
9e3610d513
@ -1,19 +1,13 @@
|
|||||||
import autobind from 'autobind-decorator';
|
import autobind from 'autobind-decorator';
|
||||||
import * as seedrandom from 'seedrandom';
|
import { Variable, PageVar, envVarsDef, Block, isFnBlock, Fn, HpmlScope, HpmlError } from '.';
|
||||||
import { Variable, PageVar, envVarsDef, funcDefs, Block, isFnBlock } from '.';
|
|
||||||
import { version } from '@/config';
|
import { version } from '@/config';
|
||||||
import { AiScript, utils, values } from '@syuilo/aiscript';
|
import { AiScript, utils, values } from '@syuilo/aiscript';
|
||||||
import { createAiScriptEnv } from '../aiscript/api';
|
import { createAiScriptEnv } from '../aiscript/api';
|
||||||
import { collectPageVars } from '../collect-page-vars';
|
import { collectPageVars } from '../collect-page-vars';
|
||||||
import { initLib } from './lib';
|
import { initHpmlLib, initAiLib } from './lib';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { markRaw, ref, Ref } from 'vue';
|
import { markRaw, ref, Ref } from 'vue';
|
||||||
|
|
||||||
type Fn = {
|
|
||||||
slots: string[];
|
|
||||||
exec: (args: Record<string, any>) => ReturnType<Hpml['evaluate']>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hpml evaluator
|
* Hpml evaluator
|
||||||
*/
|
*/
|
||||||
@ -41,7 +35,7 @@ export class Hpml {
|
|||||||
if (this.opts.enableAiScript) {
|
if (this.opts.enableAiScript) {
|
||||||
this.aiscript = markRaw(new AiScript({ ...createAiScriptEnv({
|
this.aiscript = markRaw(new AiScript({ ...createAiScriptEnv({
|
||||||
storageKey: 'pages:' + this.page.id
|
storageKey: 'pages:' + this.page.id
|
||||||
}), ...initLib(this)}, {
|
}), ...initAiLib(this)}, {
|
||||||
in: (q) => {
|
in: (q) => {
|
||||||
return new Promise(ok => {
|
return new Promise(ok => {
|
||||||
os.dialog({
|
os.dialog({
|
||||||
@ -137,7 +131,7 @@ export class Hpml {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
private _interpolate(str: string, scope: Scope) {
|
private _interpolateScope(str: string, scope: HpmlScope) {
|
||||||
return str.replace(/{(.+?)}/g, match => {
|
return str.replace(/{(.+?)}/g, match => {
|
||||||
const v = scope.getState(match.slice(1, -1).trim());
|
const v = scope.getState(match.slice(1, -1).trim());
|
||||||
return v == null ? 'NULL' : v.toString();
|
return v == null ? 'NULL' : v.toString();
|
||||||
@ -157,14 +151,14 @@ export class Hpml {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const v of this.variables) {
|
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;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
private evaluate(block: Block, scope: Scope): any {
|
private evaluate(block: Block, scope: HpmlScope): any {
|
||||||
if (block.type === null) {
|
if (block.type === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -174,11 +168,11 @@ export class Hpml {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (block.type === 'text' || block.type === 'multiLineText') {
|
if (block.type === 'text' || block.type === 'multiLineText') {
|
||||||
return this._interpolate(block.value || '', scope);
|
return this._interpolateScope(block.value || '', scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (block.type === 'textList') {
|
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') {
|
if (block.type === 'ref') {
|
||||||
@ -197,7 +191,8 @@ export class Hpml {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFnBlock(block)) { // ユーザー関数定義
|
// Define user function
|
||||||
|
if (isFnBlock(block)) {
|
||||||
return {
|
return {
|
||||||
slots: block.value.slots.map(x => x.name),
|
slots: block.value.slots.map(x => x.name),
|
||||||
exec: (slotArg: Record<string, any>) => {
|
exec: (slotArg: Record<string, any>) => {
|
||||||
@ -206,7 +201,8 @@ export class Hpml {
|
|||||||
} as Fn;
|
} as Fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (block.type.startsWith('fn:')) { // ユーザー関数呼び出し
|
// Call user function
|
||||||
|
if (block.type.startsWith('fn:')) {
|
||||||
const fnName = block.type.split(':')[1];
|
const fnName = block.type.split(':')[1];
|
||||||
const fn = scope.getState(fnName);
|
const fn = scope.getState(fnName);
|
||||||
const args = {} as Record<string, any>;
|
const args = {} as Record<string, any>;
|
||||||
@ -219,77 +215,9 @@ export class Hpml {
|
|||||||
|
|
||||||
if (block.args === undefined) return null;
|
if (block.args === undefined) return null;
|
||||||
|
|
||||||
const date = new Date();
|
const funcs = initHpmlLib(block, scope, this.opts.randomSeed, this.opts.visitor);
|
||||||
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;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Call function
|
||||||
const fnName = block.type;
|
const fnName = block.type;
|
||||||
const fn = (funcs as any)[fnName];
|
const fn = (funcs as any)[fnName];
|
||||||
if (fn == null) {
|
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<string, any>[];
|
|
||||||
public name: string;
|
|
||||||
|
|
||||||
constructor(layerdStates: Scope['layerdStates'], name?: Scope['name']) {
|
|
||||||
this.layerdStates = layerdStates;
|
|
||||||
this.name = name || 'anonymous';
|
|
||||||
}
|
|
||||||
|
|
||||||
@autobind
|
|
||||||
public createChildScope(states: Record<string, any>, 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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
* Hpml
|
* Hpml
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import autobind from 'autobind-decorator';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
faMagic,
|
faMagic,
|
||||||
faSquareRootAlt,
|
faSquareRootAlt,
|
||||||
@ -27,6 +29,7 @@ import {
|
|||||||
faCalculator,
|
faCalculator,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faFlag } from '@fortawesome/free-regular-svg-icons';
|
import { faFlag } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
import { Hpml } from './evaluator';
|
||||||
|
|
||||||
export type Block<V = any> = {
|
export type Block<V = any> = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -47,6 +50,11 @@ export type Variable = Block & {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Fn = {
|
||||||
|
slots: string[];
|
||||||
|
exec: (args: Record<string, any>) => ReturnType<Hpml['evaluate']>;
|
||||||
|
};
|
||||||
|
|
||||||
export type Type = 'string' | 'number' | 'boolean' | 'stringArray' | null;
|
export type Type = 'string' | 'number' | 'boolean' | 'stringArray' | null;
|
||||||
|
|
||||||
export const funcDefs: Record<string, { in: any[]; out: any; category: string; icon: any; }> = {
|
export const funcDefs: Record<string, { in: any[]; out: any; category: string; icon: any; }> = {
|
||||||
@ -137,3 +145,53 @@ export function isLiteralBlock(v: Block) {
|
|||||||
if (literalDefs[v.type]) return true;
|
if (literalDefs[v.type]) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class HpmlScope {
|
||||||
|
private layerdStates: Record<string, any>[];
|
||||||
|
public name: string;
|
||||||
|
|
||||||
|
constructor(layerdStates: HpmlScope['layerdStates'], name?: HpmlScope['name']) {
|
||||||
|
this.layerdStates = layerdStates;
|
||||||
|
this.name = name || 'anonymous';
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
public createChildScope(states: Record<string, any>, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,6 +2,8 @@ import * as tinycolor from 'tinycolor2';
|
|||||||
import Chart from 'chart.js';
|
import Chart from 'chart.js';
|
||||||
import { Hpml } from './evaluator';
|
import { Hpml } from './evaluator';
|
||||||
import { values, utils } from '@syuilo/aiscript';
|
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
|
// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs
|
||||||
Chart.pluginService.register({
|
Chart.pluginService.register({
|
||||||
@ -16,7 +18,7 @@ Chart.pluginService.register({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export function initLib(hpml: Hpml) {
|
export function initAiLib(hpml: Hpml) {
|
||||||
return {
|
return {
|
||||||
'MkPages:updated': values.FN_NATIVE(([callback]) => {
|
'MkPages:updated': values.FN_NATIVE(([callback]) => {
|
||||||
hpml.pageVarUpdatedCallback = (callback as values.VFn);
|
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<string, 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: 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;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user