From 10216af48a90293b5471ff771509bcdc18bdda08 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 May 2019 10:05:33 +0900
Subject: [PATCH] Improve MisskeyPages

---
 locales/ja-JP.yml                             |  8 +++-
 src/client/app/common/scripts/aiscript.ts     | 43 +++++++++++++++----
 .../page-editor/page-editor.script-block.vue  | 18 +++++---
 3 files changed, 54 insertions(+), 15 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index cd29135bdb..adc3d18913 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2036,12 +2036,18 @@ pages:
       _numberToString:
         arg1: "数値"
       ref: "変数"
-      in: "引数"
+      in: "スロット入力"
       _in:
         arg1: "スロット番号"
       fn: "関数"
       _fn:
+        slots: "スロット"
+        slots-info: "スロットひとつひとつを改行で区切ってください"
         arg1: "出力"
+      for: "繰り返し"
+      _for:
+        arg1: "回数"
+        arg2: "処理"
     typeError: "スロット{slot}は\"{expect}\"を受け付けますが、\"{actual}\"が入れられています!"
     thereIsEmptySlot: "スロット{slot}が空です!"
     types:
diff --git a/src/client/app/common/scripts/aiscript.ts b/src/client/app/common/scripts/aiscript.ts
index 765d740e23..975531dfa4 100644
--- a/src/client/app/common/scripts/aiscript.ts
+++ b/src/client/app/common/scripts/aiscript.ts
@@ -27,18 +27,27 @@ import {
 	faDice,
 	faSortNumericUp,
 	faExchangeAlt,
+	faRecycle,
 } from '@fortawesome/free-solid-svg-icons';
 import { faFlag } from '@fortawesome/free-regular-svg-icons';
 
 import { version } from '../../config';
 
-export type Block = {
+export type Block<V = any> = {
 	id: string;
 	type: string;
 	args: Block[];
-	value: any;
+	value: V;
 };
 
+type FnBlock = Block<{
+	slots: {
+		name: string;
+		type: Type;
+	}[];
+	expression: Block;
+}>;
+
 export type Variable = Block & {
 	name: string;
 };
@@ -53,6 +62,7 @@ type TypeError = {
 
 const funcDefs = {
 	if:              { in: ['boolean', 0, 0],              out: 0,         category: 'flow',       icon: faShareAlt, },
+	for:             { in: ['number', 'function'],         out: 0,         category: 'flow',       icon: faRecycle, },
 	not:             { in: ['boolean'],                    out: 'boolean', category: 'logical',    icon: faFlag, },
 	or:              { in: ['boolean', 'boolean'],         out: 'boolean', category: 'logical',    icon: faFlag, },
 	and:             { in: ['boolean', 'boolean'],         out: 'boolean', category: 'logical',    icon: faFlag, },
@@ -99,6 +109,10 @@ const blockDefs = [
 	}))
 ];
 
+function isFnBlock(block: Block): block is FnBlock {
+	return block.type === 'fn';
+}
+
 type PageVar = { name: string; value: any; type: Type; };
 
 const envVarsDef = {
@@ -326,7 +340,7 @@ export class AiScript {
 	@autobind
 	private interpolate(str: string, values: { name: string, value: any }[]) {
 		return str.replace(/\{(.+?)\}/g, match => {
-			const v = this.getVariableValue(match.slice(1, -1).trim(), values);
+			const v = this.getVarVal(match.slice(1, -1).trim(), values);
 			return v == null ? 'NULL' : v.toString();
 		});
 	}
@@ -378,23 +392,23 @@ export class AiScript {
 		}
 
 		if (block.type === 'ref') {
-			return this.getVariableValue(block.value, values);
+			return this.getVarVal(block.value, values);
 		}
 
 		if (block.type === 'in') {
 			return slotArg[block.value];
 		}
 
-		if (block.type === 'fn') { // ユーザー関数定義
+		if (isFnBlock(block)) { // ユーザー関数定義
 			return {
-				slots: block.value.slots,
+				slots: block.value.slots.map(x => x.name),
 				exec: slotArg => this.evaluate(block.value.expression, values, slotArg)
 			};
 		}
 
 		if (block.type.startsWith('fn:')) { // ユーザー関数呼び出し
 			const fnName = block.type.split(':')[1];
-			const fn = this.getVariableValue(fnName, values);
+			const fn = this.getVarVal(fnName, values);
 			for (let i = 0; i < fn.slots.length; i++) {
 				const name = fn.slots[i];
 				slotArg[name] = this.evaluate(block.args[i], values);
@@ -418,6 +432,14 @@ export class AiScript {
 			or: (a, b) => a || b,
 			and: (a, b) => a && b,
 			if: (bool, a, b) => bool ? a : b,
+			for: (times, fn) => {
+				const result = [];
+				for (let i = 0; i < times; i++) {
+					slotArg[fn.slots[0]] = i + 1;
+					result.push(fn.exec(slotArg));
+				}
+				return result;
+			},
 			add: (a, b) => a + b,
 			subtract: (a, b) => a - b,
 			multiply: (a, b) => a * b,
@@ -449,8 +471,13 @@ export class AiScript {
 		return fn(...args);
 	}
 
+	/**
+	 * 指定した名前の変数の値を取得します
+	 * @param name 変数名
+	 * @param values ユーザー定義変数のリスト
+	 */
 	@autobind
-	private getVariableValue(name: string, values: { name: string, value: any }[]): any {
+	private getVarVal(name: string, values: { name: string, value: any }[]): any {
 		const v = values.find(v => v.name === name);
 		if (v) {
 			return v.value;
diff --git a/src/client/app/common/views/components/page-editor/page-editor.script-block.vue b/src/client/app/common/views/components/page-editor/page-editor.script-block.vue
index 9554c75d04..2f78f7de3a 100644
--- a/src/client/app/common/views/components/page-editor/page-editor.script-block.vue
+++ b/src/client/app/common/views/components/page-editor/page-editor.script-block.vue
@@ -35,15 +35,18 @@
 	</section>
 	<section v-else-if="value.type === 'in'" class="hpdwcrvs">
 		<select v-model="value.value">
-			<option v-for="v in fnSlots" :value="v">{{ v }}</option>
+			<option v-for="v in fnSlots" :value="v.name">{{ v.name }}</option>
 		</select>
 	</section>
-	<section v-else-if="value.type === 'fn'" class="" style="padding:16px;">
-		<ui-textarea v-model="slots"></ui-textarea>
+	<section v-else-if="value.type === 'fn'" class="" style="padding:0 16px 16px 16px;">
+		<ui-textarea v-model="slots">
+			<span>{{ $t('script.blocks._fn.slots') }}</span>
+			<template #desc>{{ $t('script.blocks._fn.slots-info') }}</template>
+		</ui-textarea>
 		<x-v v-if="value.value.expression" v-model="value.value.expression" :title="$t(`script.blocks._fn.arg1`)" :get-expected-type="() => null" :ai-script="aiScript" :fn-slots="value.value.slots" :name="name"/>
 	</section>
 	<section v-else-if="value.type.startsWith('fn:')" class="" style="padding:16px;">
-		<x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="aiScript.getVarByName(value.type.split(':')[1]).value.slots[i]" :get-expected-type="() => null" :ai-script="aiScript" :name="name" :key="i"/>
+		<x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="aiScript.getVarByName(value.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :ai-script="aiScript" :name="name" :key="i"/>
 	</section>
 	<section v-else class="" style="padding:16px;">
 		<x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="$t(`script.blocks._${value.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :ai-script="aiScript" :name="name" :fn-slots="fnSlots" :key="i"/>
@@ -118,7 +121,10 @@ export default Vue.extend({
 
 	watch: {
 		slots() {
-			this.value.value.slots = this.slots.split('\n');
+			this.value.value.slots = this.slots.split('\n').map(x => ({
+				name: x,
+				type: null
+			}));
 		}
 	},
 
@@ -129,7 +135,7 @@ export default Vue.extend({
 	created() {
 		if (this.value.value == null) Vue.set(this.value, 'value', null);
 
-		if (this.value.value && this.value.value.slots) this.slots = this.value.value.slots.join('\n');
+		if (this.value.value && this.value.value.slots) this.slots = this.value.value.slots.map(x => x.name).join('\n');
 
 		this.$watch('value.type', (t) => {
 			this.warn = null;