Browse Source

feat(xtintin): 新函数库和一个文档注释方案

dzp 3 years ago
parent
commit
5fad2a4638

+ 2 - 0
README.md

@@ -38,6 +38,8 @@ PaoTin++ 主要由以下四部分组成:
 
 * ✅同步发布 Docker 镜像
 * ✅模块化编程框架
+* ✅内联的文档化注释
+* ✅丰富的基础函数库,以及丰富的文档
 * ✅支持模块源码重定位
 * ✅支持多 MUDLIB
 * ✅完善的日志记录

+ 45 - 15
framework/module-loader.tin

@@ -81,7 +81,7 @@ class.open module-loader;
         };
     };
 
-     errLog 模块 $moduleName 加载失败。;
+    errLog 模块 $moduleName 加载失败。;
     #return;
 };
 
@@ -177,6 +177,8 @@ class.open module-loader;
     };
     #var {xtt-modules} {$modules};
 
+    #unvar xtt.module-doc[$moduleName];
+
     #local prefix {$moduleName};
     #replace prefix {/} {_};
     #unvar {${prefix}-loaded};
@@ -279,15 +281,11 @@ class.open module-loader;
         #local hasConfig {有};
         #local hasEvents {无};
 
-        #if { "$cnName" != "" } {
-            #local cnName {(<160>$cnName<070>)};
-        };
-
         #if { "$config" == "" } {
             #local hasConfig {无};
         };
 
-        #local event {@filter{{$gValidEvent};{"VALUE[module]"=="$name"}}};
+        #local event {@fp.Filter{{$gValidEvent};{"VALUE[module]"=="$name"}}};
         #if { &event[] > 0 } {
             #local hasEvents {<160>&event[]个<070>};
         };
@@ -303,13 +301,13 @@ class.open module-loader;
             #local enable {<110>禁用<070>};
         };
 
-        #echo {$format} {white} {$name $cnName} {$author} {$type} {$enable} {$hasEvents} {$hasConfig} {$desc};
+        #echo {$format} {white} {@genModuleLink{$name;MOD}} {$author} {$type} {$enable} {$hasEvents} {$hasConfig} {$desc};
     };
     #echo {%c%h} {cyan} { 共列出 $count 项模块信息 };
 };
 
 #nop 列出所有已加载模块以及它们的开关状态、配置参数;
-#alias {MOD} {look-module};
+#alias {MOD} {look-module %0; #buffer end};
 #alias {look-module} {
     #local moduleName   {%1};
     #local metaInfo     {${xtt-modules[$moduleName]}};
@@ -339,12 +337,9 @@ class.open module-loader;
         #local enable {<120>开启<070>};
     };
 
-    #if { "$cnName" != "" } {
-        #local cnName { (<160>$cnName<070>)<060>};
-    };
-
-    #echo {%c%h}    {cyan} { $moduleName$cnName };
-    #echo {%s}      {    <060>名称:<070> $moduleName$cnName <060>类型:<070> $type <060>状态:<070> $enable <060>作者:<070> $author};
+    #local moduleLink {@genModuleLink{$moduleName;HELP}};
+    #echo {%c%h}    {cyan} { $moduleLink<060> };
+    #echo {%s}      {    <060>名称:<070> $moduleLink <060>类型:<070> $type <060>状态:<070> $enable <060>作者:<070> $author};
     #echo {%s}      {    <060>脚本路径:<070> $path};
 
     #if { "$type" == "弱模块" } {
@@ -361,7 +356,7 @@ class.open module-loader;
 
     #local format {    %c%-20s %-20s %s};
 
-    #local event {@filter{{$gValidEvent};{"VALUE[module]"=="$moduleName"}}};
+    #local event {@fp.Filter{{$gValidEvent};{"VALUE[module]"=="$moduleName"}}};
     #if { &event[] > 0 } {
         #local name {};
         #echo {%s}      {    <060>提供事件列表:<070>};
@@ -453,4 +448,39 @@ class.open module-loader;
     load-lib {%1} {%2};
 };
 
+#nop 给出一个用于屏幕显示的模块名称。会自动根据是否有文档点亮 MSLP 链接;
+#func {genModuleLink} {
+    #local {name}   {%1};
+    #local {cmd}    {%2};
+
+    #if { "$name" == "" } {
+        #return {};
+    };
+
+    #if { &xtt-modules[$name][] == 0 } {
+        #return {$name};
+    };
+
+    #local cnName {$xtt-modules[$name][NAME]};
+
+    #if { "$cnName" != "" } {
+        #local cnName { (<160>$cnName<070>)};
+    };
+
+    #if { "$cmd" != "{MOD|HELP}" } {
+        #return {$name$cnName};
+    };
+
+    #if { "$cmd" == "HELP" && &xtt.module-doc[$name][] > 0 } {
+        #local name {@mslp.Help{$name;$name}};
+        #local name {<140>$name<270>$cnName};
+    };
+    #else {
+        #local name {@mslp.Module{$name;$name}};
+        #local name {<140>$name<270>$cnName};
+    };
+
+    #return {$name};
+};
+
 class.close module-loader;

+ 9 - 7
plugins/lib/event.tin

@@ -3,7 +3,7 @@
 /*
 本文件属于 PaoTin++ 的一部分
 ===========
-PaoTin++ © 2020~2022 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利
+PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利
 你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
 ===========
 */
@@ -70,8 +70,8 @@ PaoTin++ © 2020~2022 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
 
     #echo {%h} { 已经定义的事件列表 };
 
-    #echo {%-20s %-5s %-20s %s} {事件/已注册的钩子} {类型} {模块} {说明/代码};
-    #echo {%-20s %-5s %-20s %s} {--------------------} {----} {----------} {------------};
+    #echo {%-20s %-5s %-30s %s} {事件/已注册的钩子} {类型} {模块} {说明/代码};
+    #echo {%-20s %-5s %-30s %s} {@str.Repeat{20;-}} {----} {@str.Repeat{30;-}} {------------};
 
     #foreach {*gValidEvent[]} {event} {
         #local type {有参};
@@ -79,7 +79,9 @@ PaoTin++ © 2020~2022 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
             #local type {无参};
         };
 
-        #echo {%-20s %-5s %-20s %s} {$event} {$type} {$gValidEvent[$event][module]} {$gValidEvent[$event][desc]};
+        #echo {%-20s %-5s %-30s %s}{$event} {$type}
+            {@genModuleLink{$gValidEvent[$event][module];MOD}}
+            {$gValidEvent[$event][desc]};
         #local hook {};
         #local count {0};
         #foreach {*gEventHandlers[$event][]} {hook} {
@@ -91,9 +93,9 @@ PaoTin++ © 2020~2022 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
             #if { $count == &gEventHandlers[$event][] } {
                 #Local lead {╰};
             };
-            #echo { $lead@repeat{$len;─} %s        %-20s %s}{$hook}
-                            {$gEventHandlers[$event][$hook][module]}
-                            {$gEventHandlers[$event][$hook][code]};
+            #echo { $lead@str.Repeat{$len;─} %s        %-30s %s}{$hook}
+                {@genModuleLink{$gEventHandlers[$event][$hook][module];MOD}}
+                {$gEventHandlers[$event][$hook][code]};
         };
     };
 

+ 81 - 2
plugins/lib/xtintin/__init__.tin

@@ -8,15 +8,94 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
 
 #nop xtintin 是为了方便 TinTin++ 使用而增加的一些语法扩展。;
 
+load-file plugins/lib/xtintin/doc.tin;
 load-file plugins/lib/xtintin/number.tin;
 load-file plugins/lib/xtintin/bool.tin;
 load-file plugins/lib/xtintin/string.tin;
+load-file plugins/lib/xtintin/algo.tin;
 load-file plugins/lib/xtintin/list.tin;
+load-file plugins/lib/xtintin/slist.tin;
+load-file plugins/lib/xtintin/set.tin;
 load-file plugins/lib/xtintin/time.tin;
 load-file plugins/lib/xtintin/path.tin;
-load-file plugins/lib/xtintin/cmds.tin;
 load-file plugins/lib/xtintin/debug.tin;
 load-file plugins/lib/xtintin/fp.tin;
 load-file plugins/lib/xtintin/default.tin;
-load-file plugins/lib/xtintin/diff.tin;
+load-file plugins/lib/xtintin/cmds.tin;
 load-file plugins/lib/xtintin/mslp.tin;
+
+///=== {
+///// 以上文档采用 PaoTin++ 的文档化注释工具生成。
+///// 下面是关于文档化注释工具的简单介绍。
+/////
+// ## xtt.Doc
+//    解析文档化注释,生成文档。
+//
+//    文档化注释是 PaoTin++ 非常有特色的创举,是对 tt++ 能力的一大增强,可以实现
+//    以下八合一效果:
+//        - 代码注释: 文档化注释既是文档,也是注释。它和被说明的源代码紧密结合
+//                    在一起,既方便查看,也方便维护,可以有效避免文档过时的问题。
+//        - 联机帮助: 文档化注释可以在 PaoTin++ 运行时,即使是在游戏界面,也可以
+//                    随时查看,支持模糊查询。也和 PaoTin++ 其它部分做紧密结合。
+//        - 错误提示: 文档化注释可以给别名当成是参数输入错误时提示用户正确用法的
+//                    作用。具体参见 <130>HELP xtt.Usage<070>。
+//        - Markdown: 文档化注释用类似于 Markdown 的标记语言书写,可以轻松输出为
+//                    Markdown 文档。
+//        - HTML文档: 借助于 TinTin++ 的 HTML 输出功能,文档化注释也可以输出成彩
+//                    色 HTML 页面。
+//        - GH-Pages: 生成的 HTML 页面可以部署在 GitHub Pages 上,作为项目的文档
+//                    页面使用。
+//        - 示例代码: 文档化注释里可以写示例代码,帮助用户更好地理解文档。示例
+//                    代码有别于普通的文本,它会被识别并高亮显示给用户。
+//        - 单元测试: 符合规范的示例代码本身也可以作为单元测试来使用,方便在长期
+//                    维护过程中,保持代码的健壮性和可靠性。
+//
+//    先给大家看一个直观的例子。
+//    用户只需要按照下面这个格式,就可以为模块和函数(或别名)书写文档。
+//    <110>注意:<070>在下面的模版中,「格式非常重要」,一定要按照格式书写,才会
+//    被正确识别。
+//
+//    ///=== {
+//    \/\/\/\/\/ 模块名称,和模块简要说明
+//    \/\/\/\/\/
+//    \/\/\/\/\/ 模块的整体说明性的文档,建议放在文件的一开头。
+//    \/\/\/\/\/
+//    \/\/\/\/\/ 五个斜线(<160>\/\/\/\/\/<070>)开头的文本将会被识别为模块文档。
+//    \/\/\/\/\/ 模块文档仅在 HELP <模块名> 时出现,如果只查看函数或者别名,
+//    \/\/\/\/\/ 是不会显示的。
+//    \/\/\/\/\/
+//    \/\/\/\/\/ 这里继续写模块文档。
+//    \/\/\/\/\/
+//    \/\/\/\/\/ 下面开始写别名和函数的文档:
+//    \/\/\/\/\/ 两个斜线(<160>\/\/<070>)开头的文本会被识别为别名和函数的文档。
+//    \/\/\/\/\/
+//    \/\/\/\/\/ 建议放在被说明的别名或者函数的紧挨着的前面。
+//    \/\/\/\/\/
+//    \/\/\/\/\/ 实际运用中,文档和代码一般都是穿插书写,最终所有文档会按照书写
+//    \/\/\/\/\/ 时的顺序提取成一篇完整的文档,因此书写顺序非常重要。请自行体会。
+//    \/\/
+//    \/\/ <160>\#\#<070> foo.Bar <参数1> <参数2>
+//    \/\/    函数(或别名)的一句话介绍。
+//    \/\/    函数(或别名)的更多介绍。
+//    \/\/    函数(或别名)的参数说明:
+//    \/\/        - 参数1: 参数说明
+//    \/\/        - 参数2: 参数说明
+//    \/\/ };
+//    #alias {foo.Bar} {
+//       ....;
+//    };
+//
+//    上面的例子中,如果是为别名写文档,则应该用 <160>\#\#<070> 开头,如果为函数写文档,
+//    则应当以 <160>\#\@<070> 开头。
+//
+//    参数格式说明:
+//        - 必选参数: 用 <参数名> 表示
+//        - 可选参数: 用 [<参数名>] 表示
+//        - 重复参数: 用 ... 或 [...] 表示
+//
+//    参数名一般用具有实际意义的中文名称来表示,或者用参数类型表示。
+//    有关参数类型的更多内容请查看 <130>HELP DataType<070>。
+//
+// ## xtt.Usage <关键字> [<错误提示>]
+//    给出关键字对应的帮助信息,以提示用户了解如何正确使用该别名(或函数)。
+// };

+ 107 - 0
plugins/lib/xtintin/algo.tin

@@ -0,0 +1,107 @@
+#nop vim: set filetype=tt:;
+
+/*
+本文件属于 PaoTin++ 的一部分。
+PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利
+你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
+*/
+
+#nop 本文件是 xtintin 的一部分,实现了一些常用算法;
+
+///=== {
+// #@ algo.Diff <字符串1> <字符串2>
+//    如果两个字符串参数中,其中一个是另一个插入了一个子串的结果,则本函数可计算并返回该子串。
+// };
+#func {algo.Diff} {
+    #local str1 {%1};
+    #local str2 {%2};
+
+    #list str1 {tokenize} {$str1};
+    #list str2 {tokenize} {$str2};
+
+    #local size1 {&str1[]};
+    #local size2 {&str2[]};
+
+    #if { $size1 < $size2 } {
+        #local tmp      {$str1};
+        #local str1     {$str2};
+        #local str2     {$tmp};
+        #local tmp      {$size1};
+        #local size1    {$size2};
+        #local size2    {$tmp};
+    };
+
+    #local idx {};
+    #loop {1} {$size2} {idx} {
+        #local ch1 {$str1[$idx]};
+        #local ch2 {$str2[$idx]};
+        #if { "$ch1" != "$ch2" } {
+            #break;
+        };
+    };
+
+    #local begin    {$idx};
+    #local end      {};
+    #math  end      {$size1 - $size2 + $begin - 1};
+    #local diff     {};
+    #loop {$begin} {$end} {idx} {
+        #local ch {$str1[$idx]};
+        #cat diff {$ch};
+    };
+
+    #return {$diff};
+};
+
+///=== {
+// #@ algo.ParseVertLayout <竖排文字串>
+//    将竖排文字重新排版成横排,方便做匹配。
+//    结果是一个列表,其中的每个元素代表一行转换后的文本。
+//    为方便计算对齐,其中竖排文字串要求用竖线定出每行的左右边界,且一个汉字必须对应两个空格。
+// };
+#func {algo.ParseVertLayout} {
+    #local text {%0};
+    #replace text {| |} {|};
+    #replace text {^|} {};
+    #replace text {|$} {};
+
+    #local ch       {};
+    #local lines    {};
+    #local state    {left};
+    #local colNo    {1};
+    #local newline  {|};
+
+    #parse {$text} {ch} {
+        #switch {"$state/$ch"} {
+            #case {"left/ "} {
+                #local state {right};
+            };
+            #case {"right/ "} {
+                #local state {left};
+                #local lines[$colNo] {$lines[$colNo]  };
+                #math colNo {$colNo + 1};
+            };
+            #case {"%*/$newline"} {
+                #math colNo {1};
+                #local state {left};
+            };
+            #case {"left/%*"} {
+                #local lines[$colNo] {$lines[$colNo]$ch};
+                #math colNo {$colNo + 1};
+            };
+            #default {
+                errLog find BUG;
+                #return {};
+            };
+        };
+    };
+
+    #local output {};
+    #loop {&lines[]} {1} {colNo} {
+        #replace {lines[$colNo]} {%+1..s$} {};
+        #if { {$lines[$colNo]} == {%*%+1..S%*} } {
+            #list output add {{$lines[$colNo]}};
+        };
+    };
+
+    #return {$output};
+};

+ 38 - 0
plugins/lib/xtintin/bool.tin

@@ -6,9 +6,27 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
 你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
 */
 
+#nop 本文件是 xtintin 的一部分,实现了一些布尔运算函数;
+
+///=== {
+///// 布尔运算函数:
+//
+// #@ true
+//    永远为真。
+//
+// #@ false
+//    永远为假。
+// };
 #func {true}    {#return 1};
 #func {false}   {#return 0};
 
+///=== {
+// #@ isTrue <值>
+//    判断一个值是否为字符串 true 或者整数值 1,如果是则返回真,否则反之。
+//
+// #@ isFalse <值>
+//    判断一个值是否为字符串 false 或者整数值 0,如果是则返回真,否则反之。
+// };
 #func {isTrue} {
     #if { "%0" == "{true|1}" } {
         #return 1;
@@ -27,6 +45,10 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     };
 };
 
+///=== {
+// #@ allTrue <值1> [<值2> ...]
+//    判断多个值,是否全部为真,如果是则返回真,否则反之。
+// };
 #func {allTrue} {
     #local value {};
     #foreach {%0} {value} {
@@ -38,6 +60,10 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #return 1;
 };
 
+///=== {
+// #@ allFalse <值1> [<值2> ...]
+//    判断多个值,是否全部为假,如果是则返回真,否则反之。
+// };
 #func {allFalse} {
     #local value {};
     #foreach {%0} {value} {
@@ -49,6 +75,10 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #return 1;
 };
 
+///=== {
+// #@ anyTrue <值1> [<值2> ...]
+//    判断多个值,是否有任意一个为真,如果是则返回真,否则反之。
+// };
 #func {anyTrue} {
     #if @allFalse{%0} {
         #return 0;
@@ -58,6 +88,10 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     };
 };
 
+///=== {
+// #@ anyFalse <值1> [<值2> ...]
+//    判断多个值,是否有任意一个为假,如果是则返回真,否则反之。
+// };
 #func {anyFalse} {
     #if @allTrue{%0} {
         #return 0;
@@ -67,6 +101,10 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     };
 };
 
+///=== {
+// #@ if <条件> <值1> [<值2>]
+//    三元运算符。判断条件,如果条件成立则给出值1,否则给出值2,如果值2未提供则视同为空字符串。
+// };
 #func {if} {
     #local cond {%1};
     #local then {%2};

+ 128 - 49
plugins/lib/xtintin/cmds.tin

@@ -6,6 +6,15 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
 你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
 */
 
+#nop 本文件是 xtintin 的一部分,实现了一些实用命令。;
+
+///=== {
+///// 实用命令:
+//
+// ## xtt.Tick <ID> <代码> <间隔时间>
+//    跟 #tick 功能类似,但是会立即执行一次代码。对于间隔时间比较长的定时器来说尤其有用。
+//    你也可以通过 Tick 别名来使用本别名。
+// };
 #alias {Tick} {xtt.Tick};
 #alias {xtt.Tick} {
     #local id       {%1};
@@ -17,39 +26,52 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     $code;
 };
 
+///=== {
+// ## xtt.ListTicker
+//    列出系统中所有的定时器。因为定时器一般不会很多因此暂时没有做过滤功能。
+//    你也可以通过 TICKS 别名来使用本别名。
+// };
 #alias {TICKS} {xtt.ListTicker};
 #alias {xtt.ListTicker} {
-	#info tickers save;
+    #info tickers save;
 
-	#echo {<128>%+20s %+20s %+20s} {定时器名称} {执行间隔} {距离下次执行(s)};
+    #echo {<128>   %-30s %+20s %+10s %+20s} {所属模块} {定时器名称} {执行周期} {距离下次执行(s)};
 
-	#draw Yellow scroll line 1 1 1 62;
+    #draw Yellow scroll line 1 1 1 90;
 
-	#format utime %U;
+    #format utime %U;
 
     #local index {};
-	#loop {1} {&info[TICKERS][]} {index} {
+    #loop {1} {&info[TICKERS][]} {index} {
         #local uval {};
-		#math uval $info[TICKERS][+$index][arg3] * 1000000;
+        #math uval $info[TICKERS][+$index][arg3] * 1000000;
 
-		#echo {%+20s %+20s %+20m}
-			{$info[TICKERS][+$index][arg1]}
-			{$info[TICKERS][+$index][arg3]}
-			{($uval - ($utime - $info[TICKERS][+$index][arg4]) % $uval) / 1000000.00};
-	};
+        #echo {   %-30s %+20s %+10s %+20m}
+            {@genModuleLink{$info[TICKERS][+$index][class];MOD}}
+            {$info[TICKERS][+$index][arg1]}
+            {$info[TICKERS][+$index][arg3]}
+            {($uval - ($utime - $info[TICKERS][+$index][arg4]) % $uval) / 1000000.00};
+    };
 };
 
+///=== {
+// ## xtt.ListAlias [<正则表达式>]
+//    列出系统中符合条件的别名,如果省略条件则列出所有别名。
+//    一些不够规整的别名不会被列出。只有符合 PaoTin++ 规范的别名才会被列出。
+//    正则表达式会被运用到别名所属模块名称和别名名称上,两者符合其一即可被列出。
+//    你也可以通过 ALIASES 别名来使用本别名。
+// };
 #alias {ALIASES} {xtt.ListAlias};
 #alias {xtt.ListAlias} {
     #local pattern {%1};
 
-	#info aliases save;
+    #info aliases save;
 
     #local aliasTable {};
     #local index {};
-	#loop {1} {&info[ALIASES][]} {index} {
-		#local name     {$info[ALIASES][+$index][arg1]};
-		#local class    {$info[ALIASES][+$index][class]};
+    #loop {1} {&info[ALIASES][]} {index} {
+        #local name     {$info[ALIASES][+$index][arg1]};
+        #local class    {$info[ALIASES][+$index][class]};
 
         #if { "$class" == "" && "$pattern" != "all" } {
             #continue;
@@ -68,14 +90,14 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
         };
 
         #list {aliasTable[$class]} sort {$name};
-	};
+    };
 
-    #local format {    %-20s %-30s %-10s};
-	#echo {<128>$format} {class} {别名} {类型};
+    #local format {    %-30s %-30s %-10s};
+    #echo {<128>$format} {class} {别名} {类型};
 
-	#draw Yellow scroll line 1 1 1 80;
+    #draw Yellow scroll line 1 1 1 80;
 
-    #local classList {@sort{*aliasTable[]}};
+    #local classList {@list.Sort{*aliasTable[]}};
     #local class {};
     #foreach {$classList} {class} {
         #local name {};
@@ -112,26 +134,33 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
             };
 
             #echo {<060>$format}
-                {$class}
-                {$name}
+                {@genModuleLink{$class;MOD}<060>}
+                {@linkToHelp{$class;$name}}
                 {$type};
         };
     };
 };
 
+///=== {
+// ## xtt.ListVar [<正则表达式>]
+//    列出系统中符合条件的变量,如果省略条件则列出所有变量。
+//    一些不够规整的变量不会被列出。只有符合 PaoTin++ 规范的变量才会被列出。
+//    正则表达式会被运用到变量所属模块名称和变量名称上,两者符合其一即可被列出。
+//    你也可以通过 VARS 别名来使用本别名。
+// };
 #alias {VARS} {xtt.ListVar};
 #alias {xtt.ListVar} {
     #local pattern {%1};
 
-	#info variable save;
+    #info variable save;
 
     #local varTable {};
     #local index {};
-	#loop {1} {&info[VARIABLES][]} {index} {
-		#local name     {$info[VARIABLES][+$index][arg1]};
-		#local class    {$info[VARIABLES][+$index][class]};
-		#local value    {$info[VARIABLES][+$index][arg2]};
-		#local nest     {$info[VARIABLES][+$index][nest]};
+    #loop {1} {&info[VARIABLES][]} {index} {
+        #local name     {$info[VARIABLES][+$index][arg1]};
+        #local class    {$info[VARIABLES][+$index][class]};
+        #local value    {$info[VARIABLES][+$index][arg2]};
+        #local nest     {$info[VARIABLES][+$index][nest]};
 
         #if { "$class" == "" && "$pattern" != "all" } {
             #continue;
@@ -149,18 +178,18 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
             {nest}{$nest}
             {value}{$value}
         };
-	};
+    };
 
-    #local format {    %-20s %-30s %-10s %s};
-	#echo {<128>$format} {class} {变量} {类型} {值};
+    #local format {    %-30s %-30s %-10s %s};
+    #echo {<128>$format} {class} {变量} {类型} {值};
 
-	#draw Yellow scroll line 1 1 1 80;
+    #draw Yellow scroll line 1 1 1 80;
 
-    #local classList {@sort{*varTable[]}};
+    #local classList {@slist.Sort{*varTable[]}};
     #local class {};
     #foreach {$classList} {class} {
         #local name {};
-        #local nameList {@sort{*varTable[$class][]}};
+        #local nameList {@slist.Sort{*varTable[$class][]}};
         #foreach {$nameList} {name} {
             #local type {<020>字符串<070>};
             #local value {<160>$varTable[$class][$name][value]<070>};
@@ -168,29 +197,36 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
 
             #if { $nest > 0 } {
                 #local type {<050>表格<070>};
-                #local value {<130>[... 共 $nest 项数据]<070>};
+                #local value {@mslp.Var{$name;<130>[... 共 $nest 项数据]<070>}};
             };
 
             #echo {<060>$format}
-                {$class}
-                {$name}
+                {@genModuleLink{$class;MOD}<060>}
+                {@linkToHelp{$class;$name}}
                 {$type}
                 {$value};
         };
     };
 };
 
+///=== {
+// ## xtt.ListFunc [<正则表达式>]
+//    列出系统中符合条件的函数,如果省略条件则列出所有函数。
+//    正则表达式会被运用到函数所属模块名称和函数名称上,两者符合其一即可被列出。
+//    一些不够规整的函数不会被列出。只有符合 PaoTin++ 规范的函数才会被列出。
+//    你也可以通过 FUNCS 别名来使用本别名。
+// };
 #alias {FUNCS} {xtt.ListFunc};
 #alias {xtt.ListFunc} {
     #local pattern {%1};
 
-	#info functions save;
+    #info functions save;
 
     #local funcsTable {};
     #local index {};
-	#loop {1} {&info[FUNCTIONS][]} {index} {
-		#local name     {$info[FUNCTIONS][+$index][arg1]};
-		#local class    {$info[FUNCTIONS][+$index][class]};
+    #loop {1} {&info[FUNCTIONS][]} {index} {
+        #local name     {$info[FUNCTIONS][+$index][arg1]};
+        #local class    {$info[FUNCTIONS][+$index][class]};
 
         #if { "$class" == "" && "$pattern" != "all" } {
             #continue;
@@ -209,14 +245,14 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
         };
 
         #list {funcsTable[$class]} sort {$name};
-	};
+    };
 
-    #local format {    %-20s %-30s %-10s};
-	#echo {<128>$format} {class} {函数} {类型};
+    #local format {    %-30s %-30s %-10s};
+    #echo {<128>$format} {class} {函数} {类型};
 
-	#draw Yellow scroll line 1 1 1 80;
+    #draw Yellow scroll line 1 1 1 80;
 
-    #local classList {@sort{*funcsTable[]}};
+    #local classList {@list.Sort{*funcsTable[]}};
     #local class {};
     #foreach {$classList} {class} {
         #local name {};
@@ -240,27 +276,43 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
             };
 
             #echo {<060>$format}
-                {$class}
-                {$name}
+                {@genModuleLink{$class;MOD}<060>}
+                {@linkToHelp{$class;$name}}
                 {$type};
         };
     };
 };
 
+///=== {
+// ## xtt.Send <命令> [<参数> ...]
+//    向服务器发送命令。如果命令拦截总开关被打开,则不会真的向服务器发送。
+// };
 #alias {xtt.Send} {
     #if { "$xttOptions[DisableOutput]" == "true" } {
         #echo {<160>命令已被抑制: <420>%p<070>} {%0};
         #return;
     };
 
-    #send %0
+    #send %0;
 };
 
+///=== {
+// ## xtt.SendAtOnce <分号分隔的命令序列>
+//    一次性向服务器发送多条命令。
+//    有的 MUD 服务器专门为这种方式开辟了通道,本方法可以使用这种通道。
+//    TODO: 需要区分 MUD,需要支持定制的命令分隔符。
+// };
 #alias {xtt.SendAtOnce} {
     #local cmds {%1};
     #send {#$cmds#};
 };
 
+///=== {
+// ## xtt.Answer <问题答复>
+//    如果游戏中有需要回答的问题,并且给出了特定的提示符(就是不带换行的文本),
+//    那么由于 TinTin++ 的某种机制,导致回答的内容和问题就会有重叠,不能正确显示。
+//    此时建议用 xtt.Answer 来回答此类问题。将会留下美观的 buffer 记录。
+// };
 #alias {xtt.Answer} {
     #delay 0 {
         #echo {};
@@ -268,3 +320,30 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
         #send {%1};
     };
 };
+
+#func {linkToHelp} {
+    #local module   {%1};
+    #local keyword  {%2};
+
+    #local text {$keyword};
+    #local cmd {HELP $keyword};
+    #if { "$keyword" == "" } {
+        #local text {$module};
+        #local keyword {MODULE};
+        #local cmd {HELP $module};
+    };
+
+    #if { "$module" == "" } {
+        #return {$text};
+    };
+
+    #if { &xtt.module-doc[$module][] == 0 } {
+        #return {$text};
+    };
+
+    #if { &xtt.module-doc[$module][$keyword][] == 0 } {
+        #return {$text};
+    };
+
+    #return {<140>@mslp.Exec{{$cmd};{$text}}<270>};
+};

+ 26 - 7
plugins/lib/xtintin/debug.tin

@@ -2,7 +2,7 @@
 
 /*
 本文件属于 PaoTin++ 的一部分。
-PaoTin++ © 2020~2022 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利
+PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利
 你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
 */
 
@@ -10,25 +10,44 @@ PaoTin++ © 2020~2022 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
 
 #var XTinTin[debug] {false};
 
+///=== {
+///// 调试开关函数:
+/////
+///// 由于 beautify 和 prompt 插件都会带来大量的代码调用,
+///// 所以为了尽量减少调试信息,设计了这组函数以开启或者关闭调试开关,
+///// 并在调试开启期间,临时禁用 beautify 和 prompt 插件。
+//
+// ## xtt.ToggleDebug
+//    切换调试状态。有一个短别名叫做 D,优先级非常低。方便玩家随时征用这个名字。
+// };
+#alias {D} {xtt.ToggleDebug} {9.999};
 #alias {xtt.ToggleDebug} {
     #if { "$XTinTin[debug]" == "false" } {
-        beautify.Off;
-        prompt.Disable;
         xtt.DebugOn;
     };
     #else {
         xtt.DebugOff;
-        prompt.Enable;
-        beautify.On;
     };
 };
 
+///=== {
+// ## xtt.DebugOn
+//    开启调试状态。
+// };
 #alias {xtt.DebugOn} {
     #var XTinTin[debug] {true};
-    #debug all on;
+    beautify.Off;
+    prompt.Disable;
+    #line quiet #debug all on;
 };
 
+///=== {
+// ## xtt.DebugOff
+//    关闭调试状态。
+// };
 #alias {xtt.DebugOff} {
     #var XTinTin[debug] {false};
-    #debug all off;
+    #line quiet #debug all off;
+    prompt.Enable;
+    beautify.On;
 };

+ 56 - 32
plugins/lib/xtintin/default.tin

@@ -6,7 +6,28 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
 你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
 */
 
-#nop 字符串默认值;
+#nop 本文件是 xtintin 的一部分,实现了一些默认值处理函数。;
+
+///=== {
+///// 默认值处理函数:
+//
+// #@ isEmpty <字符串>
+//    判断字符串是否为空,为空则返回真。
+// };
+#func {isEmpty} {
+    #local value {%0};
+
+    #if { "$value" == "" } {
+        #return 1;
+    };
+
+    #return 0;
+};
+
+///=== {
+// #@ default <字符串> <默认值>
+//    判断字符串是否为空,为空则返回默认值。
+// };
 #func {default} {
     #local value    {%1};
     #local default  {%2};
@@ -18,7 +39,10 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #return {$value};
 };
 
-#nop 数值默认值;
+///=== {
+// #@ defaultNum <字符串> <默认值>
+//    如果字符串为空,或者格式不像是一个数值(允许负数、小数)、或者等于 0,则返回默认值。
+// };
 #func {defaultNum} {
     #local value    {%1};
     #local default  {%2};
@@ -38,8 +62,28 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #return {$value};
 };
 
-#nop 变量展开的时候,如果变量不存在会直接展开成 $var 形式的变量名自身,;
-#nop 这种情况下也允许用 default 值代替;
+///=== {
+// #@ isEmptyVar <字符串>
+//    判断参数是否为空,或者变量展开失败。如果是则返回真。
+// };
+#func {isEmptyVar} {
+    #local value {%0};
+
+    #if { "$value" == "" } {
+        #return 1;
+    };
+
+    #if { "$value" == "$%*" } {
+        #return 1;
+    };
+
+    #return 0;
+};
+
+///=== {
+// #@ defaultVar <字符串> <默认值>
+//    判断参数是否为空,或者变量展开失败。如果是则返回默认值。
+// };
 #func {defaultVar} {
     #local value    {%1};
     #local default  {%2};
@@ -51,8 +95,14 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #return {$value};
 };
 
-#nop 变量展开的时候,如果变量不存在会直接展开成 $var 形式的变量名自身,;
-#nop 这种情况下也允许用 default 值代替;
+///=== {
+// #@ defaultNumVar <字符串> <默认值>
+//    判断参数是否不像是一个数字,或者变量展开失败。如果是则返回默认值。
+//
+///// 由于 TinTin++ 的变量展开有个特点,那就是如果不存在变量 foo,则 $foo 展开的
+///// 结果并不是空字符串,而是字符串 "$foo",会导致误以为不是空串。
+///// 以上三个函数将该情形也视同为空字符串并做相应处理。
+// };
 #func {defaultNumVar} {
     #local value    {%1};
     #local default  {%2};
@@ -71,29 +121,3 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
 
     #return {$value};
 };
-
-#nop 判断参数是否为空;
-#func {isEmpty} {
-    #local value {%0};
-
-    #if { "$value" == "" } {
-        #return 1;
-    };
-
-    #return 0;
-};
-
-#nop 判断参数是否为空,或者变量展开失败;
-#func {isEmptyVar} {
-    #local value {%0};
-
-    #if { "$value" == "" } {
-        #return 1;
-    };
-
-    #if { "$value" == "$%*" } {
-        #return 1;
-    };
-
-    #return 0;
-};

+ 0 - 52
plugins/lib/xtintin/diff.tin

@@ -1,52 +0,0 @@
-#nop 两个字符串参数,其中一个比另一个多包含了一个子串,本函数可返回该子串(即任务信息);
-#func {diff} {
-    #local str1 {%1};
-    #local str2 {%2};
-
-    #list str1 {tokenize} {$str1};
-    #list str2 {tokenize} {$str2};
-
-    #local size1 {&str1[]}; 
-    #local size2 {&str2[]}; 
-
-    #if { $size1 < $size2 } {
-        #local tmp      {$str1};
-        #local str1     {$str2};
-        #local str2     {$tmp};
-        #local tmp      {$size1};
-        #local size1    {$size2};
-        #local size2    {$tmp};
-    };
-
-    #local idx {};
-    #loop {1} {$size2} {idx} {
-        #local ch1 {$str1[$idx]};
-        #local ch2 {$str2[$idx]};
-        #if { "$ch1" != "$ch2" } {
-            #break;
-        };
-    };
-
-    #local begin    {$idx};
-    #local end      {};
-    #math  end      {$size1 - $size2 + $begin - 1};
-    #local diff     {};
-    #loop {$begin} {$end} {idx} {
-        #local ch {$str1[$idx]};
-        #cat diff {$ch};
-    };
-
-    #return {$diff};
-};
-
-/*
-#alias {diff.test} {
-    #local str1 {洞中无丝毫光亮,脚华山的练功房下平整,便似走在石板路上一般。};
-    #local str2 {洞中无丝毫光亮,脚下平整,便似走在石板路上一般。};
-    #echo {STR1: $str1};
-    #echo {STR2: $str2};
-    #local diff {@diff{{$str1};{$str2}}};
-    #nop 会得到「华山的练功房」
-    #echo {DIFF: $diff};
-};
-*/

+ 263 - 3
plugins/lib/xtintin/doc.tin

@@ -2,7 +2,7 @@
 
 /*
 本文件属于 PaoTin++ 的一部分。
-PaoTin++ © 2020~2022 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利
+PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利
 你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
 */
 
@@ -52,7 +52,7 @@ PaoTin++ © 2020~2022 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
             };
         };
     };
-    #elseif { @indexOfStrList{{$aliasList}; $what} > 0 } {
+    #elseif { @slist.IndexOf{{$aliasList}; $what} > 0 } {
         #var __XTinTinAPIDoc__ {};
         $what 'XTinTinAPI;
         #if { "$__XTinTinAPIDoc__" == "" } {
@@ -62,7 +62,7 @@ PaoTin++ © 2020~2022 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
             #local _ @xtt.GenHelpDoc{{$__XTinTinAPIDoc__}};
         };
     };
-    #elseif { @indexOfStrList{{$funcList}; $what} > 0 } {
+    #elseif { @slist.IndexOf{{$funcList}; $what} > 0 } {
         #var __XTinTinAPIDoc__ {};
         #local {funcCall} {#local _ {@$what{'XTinTinAPI}}};
         $funcCall;
@@ -211,3 +211,263 @@ PaoTin++ © 2020~2022 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
 
     #return $ok;
 };
+
+#var xtt.module-doc {};
+
+#alias {HELP} {xtt.Help %0; #buffer end};
+#alias {xtt.Help} {
+    #local keyword {%1};
+    #local len {@str.Len{$keyword}};
+
+    #if { $len == 0 } {
+        warnLog 用法: HELP <关键字>;
+        #return;
+    };
+
+    #local maybe {};
+    #local module {};
+    #foreach {*xtt.module-doc[]} {module} {
+        #if { "$module" == "%*$keyword%*" } {
+            #local likely {@math.Eval{$len * 100 / @str.Len{$module}}};
+            #list maybe {add} {{
+                {likely} {$likely}
+                {module} {$module}
+                {keyword}{__MODULE__}
+            }};
+            #continue;
+        };
+
+        #local key {};
+        #foreach {*xtt.module-doc[$module][]} {key} {
+            #if { "$key" == "__MODULE__" } {
+                #continue;
+            };
+            #if { "$key" == "%*$keyword%*" } {
+                #local likely {@math.Eval{$len * 100 / @str.Len{$key}}};
+                #list maybe {add} {{
+                    {likely} {$likely}
+                    {module} {$module}
+                    {keyword}{$key}
+                }};
+            };
+        };
+    };
+
+    #local doc {};
+    #if { &maybe[] == 0 } {
+        errLog 没有找到关键字「$keyword」;
+        #return;
+    };
+
+    #local doc {$maybe[1]};
+
+    #if { &maybe[] > 1 } {
+        #list maybe {indexate} {likely};
+        #list maybe order;
+        #local doc {$maybe[-1]};
+        #if { "$doc[module]/$doc[keyword]" == "%*/$keyword/" } {
+            okLog 以下是模块 $doc[module] 的文档。;
+        };
+        #elseif { $doc[likely] < 100 } {
+            warnLog 找到 &maybe[] 个关键字,我猜你想看的是 <110>$doc[keyword]<120>@$doc[module]<099>;
+        };
+        #else {
+            okLog 以下是关键字 $doc[keyword] 的文档。;
+        };
+    };
+
+    #local module {$doc[module]};
+    #local keyword {$doc[keyword]};
+
+    #local idx {};
+    #loop 1 {&xtt.module-doc[$module][$keyword][]} {idx} {
+        #local line {$xtt.module-doc[$module][$keyword][$idx]};
+        #regex {$line} {{##|#@} %S %*} {
+            #if { "&1" == "##" } {
+                #echo {<099>%s} {<B407><128> ## 别名 <138>&2 <868>&3<099>};
+            };
+            #else {
+                #echo {<099>%s} {<B04a><128> ## 函数 <138>&2 <868>&3<099>};
+            };
+        } {
+            #echo {<099>%s} {$line};
+        };
+    };
+
+    #if { &maybe[] > 1 } {
+        warnLog 也许你还想看看:;
+        #local idx {};
+        #loop {&maybe[]-1} {1} {idx} {
+            #if { "$maybe[$idx][keyword]" == "__MODULE__" } {
+                #echo {%s} {@mslp.Exec{xtt.Help $maybe[$idx][module];<120>$maybe[$idx][module] 模块<099>}};
+            };
+            #else {
+                #echo {%s} {@mslp.Exec{xtt.Help $maybe[$idx][keyword];<120>$maybe[$idx][module] 模块的 <110>$maybe[$idx][keyword]<099>}};
+            };
+        };
+    };
+};
+
+#alias {///===} {xtt.Doc};
+
+#alias {xtt.Doc} {
+    #local doc {%1};
+    #replace {doc} {;} {\x3b};
+    #replace {doc} {\x7B} {\x7b};
+    #replace {doc} {\x7D} {\x7d};
+    #replace {doc} {///// } {;== };
+    #replace {doc} {/////;} {;== ;};
+    #replace {doc} {// } {;= };
+    #replace {doc} {//;} {;= ;};
+
+    #class xtt.doc.parse open;
+
+    #var xtt-doc-keyword {};
+
+    #alias {xtt.doc.add.module-doc} {
+        #local doc {%%1};
+
+        #regex {$doc} {== %*} {
+            #list {xtt.module-doc[$MODULE][__MODULE__]} add {<060>&1<099>};
+        };
+    };
+
+    #alias {xtt.doc.add.keyword-doc} {
+        #local doc {%%1};
+        #regex {$doc} {={([ ]{3,})- (\d\.\s*)?(\S+?)(: |:)| }%*} {
+            #local text {};
+            #if { "&1" != " " } {
+                #local text {<099>&2- <168>&3&4<278>&5&6};
+            };
+            #else {
+                #local text {<099> &6};
+            };
+            #list {xtt.module-doc[$MODULE][__MODULE__]} add {$text};
+            #if { "$xtt-doc-keyword" != "" } {
+                #list {xtt.module-doc[$MODULE][$xtt-doc-keyword]} add {$text};
+            };
+        };
+    };
+
+    #alias {xtt.doc.found.keyword} {
+        #local doc {%%1};
+        #if { !@isEmpty{$xtt-doc-keyword} } {
+            #LOCAL idx {};
+            #loop {&xtt.module-doc[$MODULE][$xtt-doc-keyword][]} {1} {idx} {
+                #if @isEmpty{$xtt.module-doc[$MODULE][$xtt-doc-keyword][$idx]} {
+                    #list {xtt.module-doc[$MODULE][$xtt-doc-keyword]} delete {$idx};
+                };
+            };
+        };
+        #regex {$doc} {= {##|#@} %S %*} {
+            #var xtt-doc-keyword {&2};
+            xtt.doc.add.keyword-doc {= &1 &2 &3};
+        };
+    };
+    #class xtt.doc.parse close;
+
+    #local line {};
+    #foreach {$doc} {line} {
+        #switch {{$xtt-doc-keyword/$line}} {
+            #case {{/}}             {#0};
+            #case {{%*/= #@ %S %*}} {xtt.doc.found.keyword {$line}};
+            #case {{%*/= ## %S %*}} {xtt.doc.found.keyword {$line}};
+            #case {{%*/= %*}}       {xtt.doc.add.keyword-doc {$line}};
+            #default                {xtt.doc.add.module-doc {$line}; #var xtt-doc-keyword {}};
+        };
+    };
+
+    #class xtt.doc.parse kill;
+};
+
+///=== {
+///// xtintin 是一个 TinTin++ 功能扩展集合,主要包含一些 MUD 无关的函数和别名,用于对 TinTin++ 的语法进行增强。
+///// 类似于其它语言的标准库。
+/////
+///// 本文档采用 xtintin 的文档化注释系统生成。
+///// 有关 xtintin 的文档化注释,请查看 HELP xtt.Doc。
+/////
+// ## DataType
+//    打印 TinTin++ 和 xtintin 的<130>数据类型<299>说明文档。
+//
+//    TinTin++ 中存在几种不同的数据类型,xtintin 基于这些数据类型扩展了一些函数,
+//    以增强对它们的处理能力。下面简单予以介绍:
+//
+//      - 字符串: 简单变量,可以包含拉丁字母、阿拉伯数字、标点符号、UTF-8 中文、表情符号。
+//            理论上,所有其它类型的变量都有字符串表达形式,也都可以看作是字符串来操作。
+//
+//            字符串支持变量内插,也就是说字符串当中出现的变量和复合变量表达式都会被替换
+//            成相应的值,而函数调用则会被替换成调用结果。参见 #help SUBSTITUTIONS
+//
+//      - 数值: TinTin++ 中并不严格区分字符串和数值,凡是内容长得像是数字一样的字符串,都
+//            可以直接当作数值来使用,主要是进行算术运算。大部分场合下数字都像字符串一样。
+//            只有少数几个场合才会被当成数值,请参考 #help math。
+//
+//      - 布尔值: TinTin++ 中的 bool 值可以用来进行布尔逻辑运算,或者作为条件判断的条件。
+//            1 表示真,0 表示假。为了提高可读性,PaoTin++ 允许用 true 来表示真,用 false
+//            来表示假。
+//
+//      - 正则表达式: 正则表达式是具有特殊含义的字符串。仅在正则表达式上下文中生效。
+//            TinTin++ 中有两类正则表达式,一类是 TinTin++ 自身的语法,参见 #help regex,
+//            还有一类是 pcre 语法,参见 #help pcre。有关 pcre 的更多信息和高级用法,可以
+//            参考 man pcre 和 man pcresyntax。
+//
+//      - 字符串列表: 字符串列表是由多个字符串构成的一个列表,多个字符串之间以分号隔开。
+//            例如 {a;b;c;d}。如前所述,字符串列表本身也可以看作是一个字符串。但由于其
+//            特殊的结构,可以视作是多个字符串构成的序列。
+//
+//            TinTin++ 原生对字符串列表的支持不多,仅可用来遍历,或者是作为 #list 命令的
+//            参数。xtintin 对它进行了扩充,并基于字符串列表实现了集合运算。
+//
+//            还有另一种字符串列表,那就是用花括号代替分号分隔的字符串序列,例如
+//            {a}{b}{c}{d},也被称为花括号列表,由于花括号列表在使用时存在一些限制,
+//            并且大部分情况下可以用分号分隔的字符串列表代替,所以不建议大家使用。
+//            即使如此,在许多要求使用字符串列表的场合,仍可接受大括号列表。本文档中将
+//            不再特别说明,请自行体会。
+//
+//      - 表格: 表格是指由多组键值对构成的一种高级结构。类似与其它编程语言的 map(C++)、
+//            hash(Perl)、dict(Python)、table(Lua)、Object(JSON)。例如:
+//
+//            {a}{AAA}{b}{BBB}{c}{CCC}
+//
+//            习惯上一般把 a/b/c 称为 KEY(键),把 AAA/BBB/CCC 称为 VALUE(值)。键和值
+//            都可以是任意字符串。但习惯上 KEY 一般是一个简单字符串,而 VALUE 则可以
+//            包含复杂的数据结构(别忘了 TinTin++ 里任何数据结构都可以当作是字符串),
+//            因此表格实际上是可以嵌套的。TinTin++ 为嵌套的表格提供了专门的语法,可以
+//            方便地访问其中的元素。
+//
+//            请注意表格中的键值对通常被视作是没有顺序的。
+//
+//      - 列表: 列表实际上是一种特殊的表格,也就是说,这个表格的所有键都是数值,
+//            并且是按顺序排列的、从 1 开始的。参见 #help list。
+//
+//            尽管如此,但列表在内存中的结构却和看上去并不相同。内存中的列表可以用
+//            #list 命令高效操作,花括号在内存中实际上并不存在,只是在代码中和输出
+//            表达中为了区分键和值的边界而存在。
+//
+//            和表格一样,列表的值可以存储任意字符串,从而达到嵌套的目的。
+//
+//      - 表格列表: 表格列表不是一种新的数据结构,只是说如果一个列表的所有值都是
+//            同一种格式的表格,那么这个数据结构就可以被称为表格列表。TinTin++ 和
+//            xtintin 都为表格列表提供了更多的支持。参见 #help list。
+//
+//      - 嵌套数据结构: 上面已经提到,所有的数据结构都可以被当作是字符串。而字符串
+//            又是 TinTin++ 中最基本的数据结构。所以这就为嵌套提供了语法基础。
+//            嵌套的数据结构有许多用处,最常见的比如表格列表。此外还可以模拟面向对象
+//            编程,或者是抽象数据类型。请自行体会。
+//
+//    你可以通过 #help lists 来了解详细说明。或者是参考小乖乖的中文手册:
+//    https://github.com/zixijian/tt/blob/master/Wiki.md#lists
+//
+///// 下面是 xtintin 的全部内容:
+// };
+
+#alias {xtt.Usage} {
+    #local name     {%1};
+    #local message  {@default{%2;用法不对}};
+
+    errLog $message。正确用法如下:;
+    xtt.Help $name;
+};
+
+#alias {DataType} {HELP DataType};

+ 49 - 8
plugins/lib/xtintin/fp.tin

@@ -8,7 +8,13 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
 
 #nop 本文件是 xtintin 的一部分,实现了一些 FP 风格函数;
 
-#func {map} {
+///=== {
+///// FP 风格函数:
+//
+// #@ fp.Map <表格> <内容表达式>
+//    遍历表格,根据内容表达式重新生成新内容。
+// };
+#func {fp.Map} {
     #local table {%1};
     #local body {%2};
 
@@ -29,7 +35,11 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #return {$newTable};
 };
 
-#func {mapIf} {
+///=== {
+// #@ fp.MapIf <表格> <条件表达式> <内容表达式>
+//    遍历表格,对符合条件的表格项,根据内容表达式重新生成新内容,不符合条件的保留原值。
+// };
+#func {fp.MapIf} {
     #local table {%1};
     #local cond {%2};
     #local body {%3};
@@ -57,7 +67,11 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #return {$newTable};
 };
 
-#func {filter} {
+///=== {
+// #@ fp.Filter <表格> <条件表达式>
+//    遍历表格,根据过滤条件生成新表格。如果条件为真则收集,否则跳过。
+// };
+#func {fp.Filter} {
     #local table {%1};
     #local cond  {%2};
 
@@ -80,7 +94,14 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #return {$newTable};
 };
 
-#func {filterMap} {
+///=== {
+// #@ fp.FilterMap <表格> <条件表达式> <内容表达式>
+//    遍历表格,根据过滤条件生成新表格。如果条件为真则收集,否则跳过。
+//    收集的内容可以通过内容表达式重新生成。
+//
+///// 上述函数中,条件表达式和内容表达式都可以通过 KEY 和 VALUE 关键字来指代表格项的 key 和 value。
+// };
+#func {fp.FilterMap} {
     #local table {%1};
     #local cond {%2};
     #local body {%3};
@@ -108,7 +129,11 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #return {$newTable};
 };
 
-#func {transform} {
+///=== {
+// #@ fp.Transform <字符串列表> <内容表达式>
+//    遍历字符串列表,根据内容表达式重新生成新内容
+// };
+#func {fp.Transform} {
     #local strList {%1};
     #local body {%2};
 
@@ -125,7 +150,11 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #return {$newStrList};
 };
 
-#func {transformIf} {
+///=== {
+// #@ fp.TransformIf <字符串列表> <条件表达式> <内容表达式>
+//    遍历字符串列表,对符合条件的项,根据内容表达式重新生成新内容,不符合条件的保留原值。
+// };
+#func {fp.TransformIf} {
     #local strList {%1};
     #local cond {%2};
     #local body {%3};
@@ -148,7 +177,11 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #return {$newStrList};
 };
 
-#func {select} {
+///=== {
+// #@ fp.Select <字符串列表> <条件表达式>
+//    遍历字符串列表,根据过滤条件生成新字符串列表。如果条件为真则收集,否则跳过。
+// };
+#func {fp.Select} {
     #local strList {%1};
     #local cond {%2};
 
@@ -169,7 +202,15 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #return {$newStrList};
 };
 
-#func {selectTransform} {
+///=== {
+// #@ fp.SelectTransform <字符串列表> <条件表达式> <内容表达式>
+//    遍历字符串列表,根据过滤条件生成新字符串列表。如果条件为真则收集,否则跳过。
+//    收集的内容可以通过内容表达式重新生成。
+//
+///// 上述函数中,条件表达式和内容表达式都可以通过 VALUE 关键字来指代字符串列表中的项。
+///// 不难看出,上述四个函数与前面四个很像,只是针对字符串列表而不是表格。
+// };
+#func {fp.SelectTransform} {
     #local strList {%1};
     #local cond {%2};
     #local body {%3};

+ 240 - 11
plugins/lib/xtintin/list.tin

@@ -6,21 +6,132 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
 你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
 */
 
-#func {string2list}     {#list result {create} {%0}};
-#func {list2string}     {#var result {%0}; #list result simplify};
+#nop 本文件是 xtintin 的一部分,实现了一些列表处理函数;
 
-#func {strListSize}     {#list l create {%0}; #list l size result};
-#func {indexOfStrList}  {#list l create {%1}; #list l find {%2} result};
-#func {indexOf}         {#var l {%1}; #list l find {%2} result};
-#func {inList}          {#math result { @indexOf{{%1};{%2}} > 0 }};
+///=== {
+///// 列表/表格列表处理函数:
+//
+// #@ list.FromSlist <字符串列表>
+//    将字符串列表转换为列表。
+// };
+#func {list.FromSlist} {
+    #list result {create} {%0};
+};
+
+///=== {
+// #@ list.IndexOf <列表> <字符串>
+//    从列表中查找某个字符串,并返回其位置(从 1 开始计),0 表示没找到。
+// };
+#func {list.IndexOf} {
+    #local list  {%1};
+    #local str   {%2};
 
-#func {sort}            {#var l {%0}; #var l2 {}; #foreach {$l} {tmp} {#list l2 {sort} {$tmp}}; #return {@list2string{$l2}}};
-#func {sortList}        {#var l {@list2string{%0}}; #var result {}; #foreach {$l} {tmp} {#list result {sort} {$tmp}}};
+    #local idx {0};
+    #foreach {*list[]} {idx} {
+        #if { {$list[$idx]} === {$str} } {
+            #return $idx;
+        };
+    };
 
-#func {listSet}         {#var l {%1}; #loc idx {%2}; #loc data {%3}; #var l @listExtend{{$l};$idx}; #list l set {$idx} {$data}; #return {$l} };
-#func {listExtend}      {#var l {%1}; #loc len {@eval{@defaultNum{%2;0} - &l[]}}; #if { $len > 0 } { #loop {1} {$len} {id} { #list l add {{}} } }; #return {$l} };
+    #return 0;
+};
 
-#func {reverseList} {
+///=== {
+// #@ list.Contains <列表> <字符串>
+//    判断字符串是否在列表中。
+// };
+#func {list.Contains} {
+    #if { @list.IndexOf{{%1};{%2}} > 0 } {
+        #return 1;
+    };
+    #else {
+        #return 0;
+    };
+};
+
+///=== {
+// #@ list.Sort <列表>
+//    按照值的字母顺序重新排序列表。
+//
+// #@ list.Order <列表>
+//    按照值的数值顺序重新排序列表。
+//
+// #@ list.SortBy <表格列表> <关键字段>
+//    按照值的关键字段的字母顺序重新排序列表。
+//
+// #@ list.OrderBy <表格列表> <关键字段>
+//    按照值的关键字段的数值顺序重新排序列表。
+// };
+#func {list.Sort}            {#var result {%0}; #list result sort};
+#func {list.Order}           {#var result {%0}; #list result order};
+#func {list.SortBy}          {#var result {%1}; #list result {indexate} {%2}; #list result sort};
+#func {list.OrderBy}         {#var result {%1}; #list result {indexate} {%2}; #list result order};
+
+///=== {
+// #@ list.Extend <列表> <长度>
+//    将列表长度扩展至指定长度。如果长度已经足够,则什么也不做。
+// };
+#func {list.Extend} {
+    #local l {%1};
+    #local len {@math.Eval{@defaultNum{%2;0} - &l[]}};
+    #if { $len > 0 } {
+        #local idx {};
+        #loop {1} {$len} {idx} {
+            #list l add {{}};
+        };
+    };
+    #return {$l};
+};
+
+///=== {
+// #@ list.Append <列表> <内容>
+//    向列表的末尾追加新的元素。
+// };
+#func {list.Append} {
+    #local l {%1};
+    #loc data {%2};
+    #list l add {{$data}};
+    #return {$l};
+};
+
+///=== {
+// #@ list.Set <列表> <下标> <内容>
+//    根据下标设置列表的单个元素的值,如果下标超过列表现有长度,则先扩展列表。
+// };
+#func {list.Set} {
+    #local l {%1};
+    #loc idx {%2};
+    #loc data {%3};
+    #var l {@list.Extend{{$l};$idx}};
+    #list l set {$idx} {$data};
+    #return {$l};
+};
+
+///=== {
+// #@ list.Insert <列表> <下标> <内容>
+//    将内容插入到列表的指定下标处,如果下标超过列表现有长度,则先扩展列表。
+// };
+#func {list.Insert} {
+    #local l {%1};
+    #loc idx {%2};
+    #loc data {%3};
+
+    #var l {@list.Extend{{$l};@math.Eval{$idx - 1}}};
+    #if { $idx > &l[] } {
+        #list l add {$data};
+    };
+    #else {
+        #list l set {$idx} {$data};
+    };
+
+    #return {$l};
+};
+
+///=== {
+// #@ list.Reverse <列表>
+//    按照倒序重新排列列表内容。
+// };
+#func {list.Reverse} {
     #local list {%0};
     #var result {};
 
@@ -32,3 +143,121 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
         };
     };
 };
+
+///=== {
+// #@ list.Get <列表> <下标>
+//    从列表中按照下标取出一个值。
+// };
+#func {list.Get} {
+    #local list     {%1};
+    #local index    {@defaultNum{%2;1}};
+
+    #return {$list[+$index]};
+};
+
+///=== {
+// #@ list.RandomGet <列表>
+//    从列表中随机取出一个值。
+// };
+#func {list.RandomGet} {
+    #local list     {%1};
+
+    #local index {@math.Random{1;&list[]}};
+    #return {$list[+$index]};
+};
+
+///=== {
+// #@ list.Zip <列表1> <列表2> [...]
+//    将多个列表合并成一个列表。结果列表的每个元素分别对应参数列表中对应位置的元素。
+//    各个参数列表中的元素将按顺序一一对应,以分号分隔连接到一起成为结果列表的元素。
+// };
+#func {list.Zip} {
+    #info arguments save;
+    #unvar info[ARGUMENTS][0];
+    #local argv {$info[ARGUMENTS]};
+
+    #if { &argv[] < 2 } {
+        #return {%1};
+    };
+
+    #local i {};
+    #local maxSize {0};
+    #loop {1} {&argv[]} {i} {
+        #local list {$argv[+$i]};
+        #local size {&list[]};
+        #if { $size > $maxSize } {
+            #local maxSize $size;
+        };
+    };
+
+    #if { $maxSize < 1 } {
+        #return {};
+    };
+
+    #local zipped {};
+    #loop {1} {$maxSize} {i} {
+        #local n {};
+        #local l {};
+        #loop {1} {&argv[]} {n} {
+            #if { $i > &argv[+$n][] } {
+                #list l add {{}};
+            };
+            #else {
+                #list l add {{$argv[+$n][$i]}};
+            };
+        };
+        #list zipped {add} {{@slist.FromList{$l}}};
+    };
+
+    #return {$zipped};
+};
+
+///=== {
+// #@ list.ZipAs <格式串> <列表1> <列表2> [...]
+//    将多个列表合并成一个列表。结果列表的每个元素分别对应参数列表中对应位置的元素。
+//    合并时使用格式串对所有参数列表中的元素进行格式化合并。
+// };
+#func {list.ZipAs} {
+    #info arguments save;
+    #unvar info[ARGUMENTS][0];
+    #local argv {$info[ARGUMENTS]};
+    #local format {$argv[1]};
+    #list argv delete {1};
+
+    #if { &argv[] < 1 } {
+        #return {};
+    };
+
+    #local i {};
+    #local maxSize {0};
+    #loop {1} {&argv[]} {i} {
+        #local list {$argv[+$i]};
+        #local size {&list[]};
+        #if { $size > $maxSize } {
+            #local maxSize $size;
+        };
+    };
+
+    #if { $maxSize < 1 } {
+        #return {};
+    };
+
+    #local zipped {};
+    #loop {1} {$maxSize} {i} {
+        #local n {};
+        #local l {};
+        #loop {1} {&argv[]} {n} {
+            #if { $i > &argv[+$n][] } {
+                #list l add {{}};
+            };
+            #else {
+                #list l add {{$argv[+$n][$i]}};
+            };
+        };
+        #local value {};
+        #line sub var #format value {$format} $l[];
+        #list zipped {add} {{$value}};
+    };
+
+    #return {$zipped};
+};

+ 84 - 4
plugins/lib/xtintin/mslp.tin

@@ -1,11 +1,91 @@
-#func {mslpExec} {
+#nop vim: set filetype=tt:;
+
+/*
+本文件属于 PaoTin++ 的一部分。
+PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利
+你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
+*/
+
+#nop 本文件是 xtintin 的一部分,实现了一些 MSLP 工具函数。;
+
+///=== {
+///// MSLP 工具函数:
+//
+// #@ mslp.Send <命令> <文本>
+//    生成链接。当点击指定的文本时,会向服务器发送指定的命令。本函数返回生成的链接代码,#echo 后即可生效。
+// };
+#func {mslp.Send} {
+    #local send {%1};
+    #local text {%2};
+    #return {\e]68;1;SEND;$send\a\e[4m$text\e[24m};
+};
+
+///=== {
+// #@ mslp.Exec <代码> <文本>
+//    生成链接。当点击指定的文本时,会执行指定的代码。本函数返回生成的链接代码,#echo 后即可生效。
+// };
+#func {mslp.Exec} {
     #local exec {%1};
     #local text {%2};
     #return {\e]68;1;EXEC;$exec\a\e[4m$text\e[24m};
 };
 
-#func {mslpSend} {
-    #local send {%1};
+///=== {
+// #@ mslp.Help <关键字> <文本>
+//    生成链接。当点击指定的文本时,会执行 HELP <关键字>。本函数返回生成的链接代码,#echo 后即可生效。
+// };
+#func {mslp.Help} {
+    #local word {%1};
     #local text {%2};
-    #return {\e]68;1;SEND;$send\a\e[4m$text\e[24m};
+    #return {\e]68;1;EXEC;HELP $word\a\e[4m$text\e[24m};
+};
+
+///=== {
+// #@ mslp.Module <模块名称> <文本>
+//    生成链接。当点击指定的文本时,会执行 MOD <模块名称>。本函数返回生成的链接代码,#echo 后即可生效。
+// };
+#func {mslp.Module} {
+    #local name {%1};
+    #local text {%2};
+    #return {\e]68;1;EXEC;MOD $name\a\e[4m$text\e[24m};
 };
+
+#alias {xtt.mslp-helper} {
+    #local cmd {%1};
+    $cmd;
+    #buffer end;
+};
+
+///=== {
+// #@ mslp.TinTin <关键字> <文本>
+//    生成链接。当点击指定的文本时,会执行 #help <关键字>。本函数返回生成的链接代码,#echo 后即可生效。
+// };
+#func {mslp.TinTin} {
+    #local word {%1};
+    #local text {%2};
+    #return {\e]68;1;EXEC;xtt.mslp-helper {#help $word}\a\e[4m$text\e[24m};
+};
+
+///=== {
+// #@ mslp.Var <变量名> <文本>
+//    生成链接。当点击指定的文本时,会显示变量的值。本函数返回生成的链接代码,#echo 后即可生效。
+// };
+#func {mslp.Var} {
+    #local name {%1};
+    #local text {%2};
+    #return {\e]68;1;EXEC;xtt.mslp-helper {#var $name}\a\e[4m$text\e[24m};
+};
+
+///=== {
+// #@ mslp.Alias <别名> <文本>
+//    生成链接。当点击指定的文本时,会显示别名的代码。本函数返回生成的链接代码,#echo 后即可生效。
+// };
+#func {mslp.Alias} {
+    #local name {%1};
+    #local text {%2};
+    #return {\e]68;1;EXEC;xtt.mslp-helper {#alias $name}\a\e[4m$text\e[24m};
+};
+
+///=== {
+///// MSLP 需要鼠标支持。请确认你的终端已经正确配置了鼠标,并在 PaoTin++ 中打开了鼠标支持。
+// };

+ 106 - 20
plugins/lib/xtintin/number.tin

@@ -6,27 +6,84 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
 你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
 */
 
-#func {eval}    {#math result {%0}};
+#nop 本文件是 xtintin 的一部分,实现了一些数值计算函数;
 
-#func {random} {
+///=== {
+///// 数值计算函数:
+//
+// #@ math.Eval <字符串>
+//    将给定字符串作为算术表达式,进行算术运算,并返回结果。参见 #help math。
+//    本函数也可以简写为 eval,两个名称用途和用法完全相同。
+// };
+#func {eval}        {#math result {%0}};
+#func {math.Eval}   {#math result {%0}};
+
+///=== {
+// #@ math.Random <最小值> <最大值>
+//    给出一个介于最小值和最大值之间的随机整数,注意随机范围包含了最小值和最大值,为闭区间。
+//    本函数也可以简写为 random,两个名称用途和用法完全相同。
+// };
+#func {random} {#return @math.Random{%0}};
+#func {math.Random} {
     #local min {%1};
     #local max {%2};
-    #local range {@eval{$max - $min + 1}};
+    #local range {@math.Eval{$max - $min + 1}};
 
     #math result { ( 1 d $range ) + $min - 1 };
 };
 
-#func {int}     {#return @floor{@eval{%0 + 0.5}}};
-#func {floor}   {#format result {%d} {%0}};
-#func {ceil}    {#return @eval{@floor{%0} + 1}};
+///=== {
+// #@ math.Int <值>
+//    对参数值按照四舍五入取整。
+//    本函数也可以简写为 int,两个名称用途和用法完全相同。
+//
+// #@ math.Floor <值>
+//    对参数值向下取整。
+//
+// #@ math.Ceil <值>
+//    对参数值向上取整。
+// };
+#func {int}         {#return @math.Floor{1.000 * %0 + 0.5}};
+#func {math.Int}    {#return @math.Floor{1.000 * %0 + 0.5}};
+#func {math.Floor}  {#format result {%d} {1.000 * %0}};
+#func {math.Ceil} {
+    #local value @math.Eval{1.000 * %0};
+    #local floor @math.Floor{%0};
+    #if { $floor < $value } {
+        #return @math.Eval{$floor + 1};
+    };
+    #else {
+        #return $floor;
+    };
+};
 
-#func {power}   {#return @eval{%1 ** %2}};
-#func {root}    {#return @eval{%1 // %2}};
+///=== {
+// #@ math.Power <底数> <指数>
+//    幂运算。
+//
+// #@ math.Root <幂> <指数>
+//    开方运算。
+// };
+#func {math.Power}  {#return @math.Eval{%1 ** %2}};
+#func {math.Root}   {#return @math.Eval{%1 // %2}};
 
-#func {abs}     {#if {%0 >= 0} {#return %0} {#math result {- %0}}};
-#func {sign}    {#if {%0 >= 0} {#return 1}  {#return -1}};
+///=== {
+// #@ math.Abs <值>
+//    取绝对值。
+//
+// #@ math.Sign <值>
+//    取符号。
+// };
+#func {math.Abs}    {#if {%0 >= 0} {#return %0} {#math result {- %0}}};
+#func {math.Sign}   {#if {%0 >= 0} {#return 1}  {#return -1}};
 
-#func {max} {
+///=== {
+// #@ math.Max <值1> [<值2> ...]
+//    求一系列值的最大值。
+//    本函数也可以简写为 max,两个名称用途和用法完全相同。
+// };
+#func {max} {#return @math.Max{%0}};
+#func {math.Max} {
     #var result {%1};
     #local elem {};
     #foreach {%0} {elem} {
@@ -39,7 +96,13 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     };
 };
 
-#func {min} {
+///=== {
+// #@ math.Min <值1> [<值2> ...]
+//    求一系列值的最小值。
+//    本函数也可以简写为 min,两个名称用途和用法完全相同。
+// };
+#func {min} {#return @math.Min{%0}};
+#func {math.Min} {
     #var result {%1};
     #local elem {};
     #foreach {%0} {elem} {
@@ -52,7 +115,13 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     };
 };
 
-#func {sum} {
+///=== {
+// #@ math.Sum <值1> [<值2> ...]
+//    求一系列值的和。
+//    本函数也可以简写为 sum,两个名称用途和用法完全相同。
+// };
+#func {sum} {#return @math.Sum{%0}};
+#func {math.Sum} {
     #local sum {0};
     #local tmp {};
     #foreach {%0} {tmp} {
@@ -61,7 +130,13 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #return {$sum};
 };
 
-#func {avg} {
+///=== {
+// #@ math.Avg <值1> [<值2> ...]
+//    求一系列值的算术平均值。
+//    本函数也可以简写为 avg,两个名称用途和用法完全相同。
+// };
+#func {avg} {#return @math.Avg{%0}};
+#func {math.Avg} {
     #var sum {0};
     #local tmp {};
     #local count {0};
@@ -74,11 +149,17 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
         #return 0;
     };
     #else {
-        #return @eval{$sum / $count};
+        #return @math.Eval{$sum / $count};
     };
 };
 
-#func {c2d} {
+///=== {
+// #@ math.ParseCN <中文数字串>
+//    将中文数字串转换成十进制整数。
+//    本函数也可以简写为 c2d,两个名称用途和用法完全相同。
+// };
+#func {c2d} {#return @math.ParseCN{%0}};
+#func {math.ParseCN} {
     #local string {%0};
 
     #local number1  {0};    #nop 个位(覆盖);
@@ -87,7 +168,7 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #local number4  {0};    #nop 亿位(乘法);
 
     #local ch {};
-	#parse {$string} {ch} {
+    #parse {$string} {ch} {
         #if { "$ch" == "{1|2|3|4|5|6|7|8|9|0|\.}" } {
             #format number1 {%d} {$number1$ch};
             #continue;
@@ -138,7 +219,12 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #return $number;
 };
 
-#func {grade} {
+///=== {
+// #@ util.Grade <当前值> <地板结果> <阈值1> <结果1> [...]
+//    根据当前值,计算落入哪个区间,返回对应的结果。阈值被定义为区间的下边沿。
+//    如果当前值比最小的区间的阈值还要小,则返回地板结果。
+// };
+#func {util.Grade} {
     #local current {@defaultNum{%1;0}};
     #local default {%2};
 
@@ -151,8 +237,8 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #list grade create {};
     #local count {0};
     #while { $count < &args[] } {
-        #local threshold    {$args[@eval{$count + 1}]};
-        #local value        {$args[@eval{$count + 2}]};
+        #local threshold    {$args[@math.Eval{$count + 1}]};
+        #local value        {$args[@math.Eval{$count + 2}]};
         #list grade {add} {{
             {threshold}{$threshold}
             {value}{$value}

+ 95 - 18
plugins/lib/xtintin/path.tin

@@ -6,33 +6,66 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
 你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
 */
 
-#func {dirName}     { #return @convertDir{name; %1; %2}; };
-#func {shortDir}    { #return @convertDir{short; %1; %2}; };
-#func {longDir}     { #return @convertDir{long; %1; %2}; };
-
-#func {reverseDir}  {
+#nop 本文件是 xtintin 的一部分,实现了一些方向和路径运算函数;
+
+///=== {
+///// 方向和路径运算函数:
+//
+// #@ dir.Name <方向全拼|方向缩写> [<严格模式>]
+//    给出指定方向的中文名称。方向可以通过英文单词或者英文单词的缩写给出。
+//
+// #@ dir.Short <方向全拼> [<严格模式>]
+//    给出方向对应的英文单词缩写。
+//
+// #@ dir.Long <方向缩写> [<严格模式>]
+//    给出方向对应的英文单词全拼。
+//
+// #@ dir.Reverse <方向> [<严格模式>]
+//    给出方向对应的相反方向。
+//
+///// 以上函数中,凡是支持严格模式参数的,如果严格模式为 true,则给错误方向给出空字符串结果。
+// };
+#func {dir.Name}    { #return @__dir_convert_dir__{name; %1; %2}; };
+#func {dir.Short}   { #return @__dir_convert_dir__{short; %1; %2}; };
+#func {dir.Long}    { #return @__dir_convert_dir__{long; %1; %2}; };
+
+#func {dir.Reverse}  {
     #if { "%1" == "enter{| .*}" } {
         #return {out};
     };
     #else {
-        #return @convertDir{reverse; %1; %2};
+        #return @__dir_convert_dir__{reverse; %1; %2};
     };
 };
 
-#func {reverseCmds} {
-    #local cmds     {%0};
-    #local newCmds  {};
+///=== {
+// #@ dir.IsDir <方向全拼|方向缩写>
+//    检查参数是否是一个合格的方向指令。
+// };
+#func {dir.IsDir} {
+    #local cmd {%0};
+    #if { "@dir.Name{$cmd}" == "" } {
+        #return 0;
+    };
+    #else {
+        #return 1;
+    };
+};
 
-    #foreach {$cmds} {item} {
-        #if { "$newCmds" == "" } {
-            #local newCmds {@reverseDir{$item}};
-        };
-        #else {
-            #format {newCmds} {%s;%s} {@reverseDir{$item}} {$newCmds};
-        };
+///=== {
+// #@ path.Reverse <路径>
+//    给出路径对应的反向路径。
+// };
+#func {path.Reverse} {
+    #local path     {%0};
+    #local newPath  {};
+
+    #local step {};
+    #foreach {$path} {step} {
+        #list newPath insert {1} {@dir.Reverse{$step}};
     };
 
-    #return {$newCmds};
+    #return {@slist.FromList{$newPath}};
 };
 
 #var xtt.dir.table {
@@ -85,7 +118,7 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     {enter}     {{name}{进去}   {short}{enter}  {long}{enter}   {reverse}{out}      }
 };
 
-#func {convertDir} {
+#func {__dir_convert_dir__} {
     #local field        {%1};
     #local dir          {%2};
     #local restricted   {%2};
@@ -108,3 +141,47 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
         #return {$entry[$field]};
     };
 };
+
+///=== {
+// #@ path.Compact <路径>
+//    压缩路径。返回压缩后的新路径。
+// };
+#func {path.Compact} {
+    #local path {@list.FromSlist{%0}};
+    #local count {&path[]};
+    #if { $count == 0 } {
+        #return {};
+    };
+
+    #local idx {1};
+    #while { $idx < $count } {
+        #local current {$path[$idx]};
+        #local next {$path[@math.Eval{$idx + 1}]};
+        #if { "@dir.Long{$current}" !== "@dir.Reverse{@dir.Long{$next}}" } {
+            #math idx {$idx + 1};
+            #continue;
+        };
+
+        #list path delete $idx 2;
+        #local idx      {@math.Max{1;@math.Eval{$idx - 2}}};
+        #local count    {@math.Eval{$count - 2}};
+    };
+
+    #return {@slist.FromList{$path}};
+};
+
+///=== {
+// #@ path.Long <路径>
+//    返回长版本的路径。路径中的每个方向命令都会被扩展成完整的单词。
+// };
+#func {path.Long} {
+    #return {@fp.Transform{{%0};{\@dir.Long{VALUE}}}};
+};
+
+///=== {
+// #@ path.Short <路径>
+//    返回长版本的路径。路径中的每个方向命令都会被简化。
+// };
+#func {path.Short} {
+    #return {@fp.Transform{{%0};{\@dir.Short{VALUE}}}};
+};

+ 637 - 0
plugins/lib/xtintin/set.tin

@@ -0,0 +1,637 @@
+#nop vim: set filetype=tt:;
+
+/*
+本文件属于 PaoTin++ 的一部分。
+PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利
+你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
+*/
+
+#nop 本文件是 xtintin 的一部分,实现了一些集合处理函数;
+
+///=== {
+///// 集合处理函数:
+/////
+///// 集合在 TinTin++ 中并无原生概念对应,PaoTin++ 中的集合用 #list 来实现,保证每个元素都不相同。
+///// 也就是说,无论是对集合进行何种运算,重复元素在集合中只会出现一次。
+/////
+///// 由于 PaoTin++ 中是用 #list 来存储 set,因此 #list 相关的命令和 xtintin API 都可以用于集合。
+//
+// #@ set.Create <元素> [...]
+//    利用提供的多个元素创建新集合。
+// };
+#func {set.Create} {
+    #return {@set.Add{{};%0}};
+};
+
+///=== {
+// #@ set.FromList <列表>
+//    将列表转换成集合。
+// };
+#func {set.FromList} {
+    #local list {%0};
+    #return {@set.Add{{};$list[%*]}};
+};
+
+///=== {
+// #@ set.Size <集合>
+//    计算集合的元素数量。
+// };
+#func {set.Size} {
+    #local set {%0};
+    #return &set[];
+};
+
+///=== {
+// #@ set.Contains <集合> <元素>
+//    判断元素是否在集合中。
+// };
+#func {set.Contains} {
+    #info arguments save;
+    #local set  {$info[ARGUMENTS][1]};
+    #local elem {$info[ARGUMENTS][2]};
+
+    #if { @list.IndexOf{{$set};{$elem}} > 0 } {
+        #return 1;
+    };
+    #else {
+        #return 0;
+    };
+};
+
+///=== {
+// #@ set.Add <集合> <元素> [...]
+//    将一个或者多个元素添加到集合当中。
+// };
+#func {set.Add} {
+    #info arguments save;
+    #unvar info[ARGUMENTS][0];
+    #local argv {$info[ARGUMENTS]};
+
+    #if { &argv[] < 2 } {
+        #return {$argv[1]};
+    };
+
+    #local set  {$argv[1]};
+    #local idx  {0};
+    #loop {2} {&argv[]} {idx} {
+        #local elem {$argv[+$idx]};
+        #if { !@set.Contains{{$set};{$elem}} } {
+            #list set {add} {{$elem}};
+        };
+    };
+
+    #return {$set};
+};
+
+///=== {
+// #@ set.Remove <集合> <元素> [...]
+//    从集合中删除一个或者多个元素。
+// };
+#func {set.Remove} {
+    #info arguments save;
+    #unvar info[ARGUMENTS][0];
+    #local argv {$info[ARGUMENTS]};
+
+    #if { &argv[] < 2 } {
+        #return {$argv[1]};
+    };
+
+    #local set  {$argv[1]};
+    #local idx  {0};
+    #loop {2} {&argv[]} {idx} {
+        #local elem {$argv[+$idx]};
+        #local idx @list.IndexOf{{$set};{$elem}};
+        #if { $idx > 0 } {
+            #list set {delete} {$idx};
+        };
+    };
+
+    #return {$set};
+};
+
+///=== {
+// #@ set.Equal <集合1> <集合2>
+//    判断集合1和集合2是否相等。
+//    📖  如果两个集合中包含的元素是相同的,那么就判定为相等。相等判定并不要求元素顺序相同。
+// };
+#func {set.Equal} {
+    #local set1  {%1};
+    #local set2  {%2};
+
+    #if { @set.IsSubset{{$set1};{$set2}} && &set1[] == &set2[] } {
+        #return 1;
+    };
+    #else {
+        #return 0;
+    };
+};
+
+///=== {
+// #@ set.IsSubset <集合1> <集合2>
+//    判断集合2是否为集合1的子集。
+//    📖  如果集合1包含了集合2中的每个元素,那么集合2就是集合1的子集。
+// };
+#func {set.IsSubset} {
+    #local set1  {%1};
+    #local set2  {%2};
+
+    #local idx {0};
+    #foreach {*set2[]} {idx} {
+        #if { !@set.Contains{{$set1};{$set2[$idx]}} } {
+            #return 0;
+        };
+    };
+
+    #return 1;
+};
+
+///=== {
+// #@ set.IsProperSubset <集合1> <集合2>
+//    判断集合2是否为集合1的真子集。
+//    📖  如果集合1包含了集合中2的每个元素,而且集合1比集合2的元素还要多,
+//       那么集合2就是集合1的真子集。
+// };
+#func {set.IsProperSubset} {
+    #local set1  {%1};
+    #local set2  {%2};
+
+    #if { @set.IsSubset{{$set1};{$set2}} && &set1[] > &set2[] } {
+        #return 1;
+    };
+    #else {
+        #return 0;
+    };
+};
+
+///=== {
+// #@ set.IsSuperset <集合1> <集合2>
+//    判断集合2是否为集合1的超集。
+//    📖  如果集合2包含了集合1中的每个元素,那么集合2就是集合1的超集。
+// };
+#func {set.IsSuperset} {
+    #return {@set.IsSubset{{%2};{%1}}};
+};
+
+///=== {
+// #@ set.IsProperSuperset <集合1> <集合2>
+//    判断集合2是否为集合1的真超集。
+//    📖  如果集合2包含了集合中1的每个元素,而且集合2比集合1的元素还要多,
+//       那么集合2就是集合1的真超集。
+// };
+#func {set.IsProperSuperset} {
+    #return {@set.IsProperSubset{{%2};{%1}}};
+};
+
+///=== {
+// #@ set.Union <集合1> <集合2> [...]
+//    求两个或者多个集合的并集。
+//    📖  并集是指把所有集合元素放在一块儿之后得到的新集合。
+// };
+#func {set.Union} {
+    #info arguments save;
+    #unvar info[ARGUMENTS][0];
+    #local argv {$info[ARGUMENTS]};
+
+    #if { &argv[] < 2 } {
+        #return {$argv[1]};
+    };
+
+    #local set  {$argv[1]};
+    #local idx  {0};
+    #loop {2} {&argv[]} {idx} {
+        #local other {$argv[+$idx]};
+        #local set {@set.Add{{$set};$other[%*]}};
+    };
+
+    #return {$set};
+};
+
+///=== {
+// #@ set.Intersection <集合1> <集合2> [...]
+//    求两个或者多个集合的交集。
+//    📖  交集是指由同时出现在所有集合中的元素组成的新集合。
+// };
+#func {set.Intersection} {
+    #info arguments save;
+    #unvar info[ARGUMENTS][0];
+    #local argv {$info[ARGUMENTS]};
+
+    #if { &argv[] < 2 } {
+        #return {};
+    };
+
+    #local set  {$argv[1]};
+    #local idx  {0};
+    #loop {2} {&argv[]} {idx} {
+        #if { &set[] == 0 } {
+            #return {};
+        };
+
+        #local other {$argv[+$idx]};
+        #loop {&set[]} {1} {idx} {
+            #local elem {$set[$idx]};
+            #if { !@set.Contains{{$other};{$elem}} } {
+                #list set {delete} {$idx};
+            };
+        };
+    };
+
+    #return {$set};
+};
+
+///=== {
+// #@ set.IsDisjoint <集合1> <集合2> [...]
+//    判断两个或者多个集合是否为不交集。
+//    📖  不交集是交集为空的意思。
+// };
+#func {set.IsDisjoint} {
+    #local set {@set.Intersection{%0}};
+    #if { &set[] == 0 } {
+        #return 1;
+    };
+    #else {
+        #return 0;
+    };
+};
+
+///=== {
+// #@ set.Product <集合1> <集合2> [...]
+//    计算两个或者多个集合的笛卡尔积。返回结果集合中的每个元素都是一个字符串列表,
+//    其值分别来自各个参数集合的元素。
+//    📖  两个集合的笛卡尔积是指由两个集合的元素组成的所有可能的有序对的集合。
+//       多个集合的积也可由此扩展。
+// };
+#func {set.Product} {
+    #info arguments save;
+    #unvar info[ARGUMENTS][0];
+    #local argv {$info[ARGUMENTS]};
+
+    #if { &argv[] < 2 } {
+        #return {};
+    };
+
+    #local set  {$argv[1]};
+    #if { &set[] == 0 } {
+        #return {};
+    };
+
+    #local idx  {0};
+    #loop {2} {&argv[]} {idx} {
+        #local other {$argv[+$idx]};
+        #if { &other[] == 0 } {
+            #return {};
+        };
+
+        #local newSet {};
+        #local left {};
+        #local right {};
+        #loop {1} {&set[]} {left} {
+            #local left {$set[+$left]};
+            #loop {1} {&other[]} {right} {
+                #local right {$other[+$right]};
+                #list newSet {add} {{$left;$right}};
+            };
+        };
+
+        #local set {$newSet};
+    };
+
+    #return {$set};
+};
+
+///=== {
+// #@ set.Diff <集合1> <集合2> [...]
+//    计算集合1与其它集合的相对差。
+//    📖  相对差是指从集合1中去掉所有在其它集合中出现过的元素之后剩下的集合。
+// };
+#func {set.Diff} {
+    #info arguments save;
+    #unvar info[ARGUMENTS][0];
+    #local argv {$info[ARGUMENTS]};
+
+    #if { &argv[] < 2 } {
+        #return {$argv[1]};
+    };
+
+    #local set  {$argv[1]};
+    #local idx  {0};
+    #loop {2} {&argv[]} {idx} {
+        #if { &set[] == 0 } {
+            #return {};
+        };
+
+        #local other {$argv[+$idx]};
+        #local set {@set.Remove{{$set};$other[%*]}};
+    };
+
+    #return {$set};
+};
+
+///=== {
+// #@ set.SymmDiff <集合1> <集合2>
+//    计算集合1与集合2的对称差。
+//    📖  对称差是指并集与交集的相对差。
+// };
+#func {set.SymmDiff} {
+    #return {@set.Diff{{@set.Union{{%1};{%2}}};{@set.Intersection{{%1};{%2}}}}};
+};
+
+///=== {
+///// 下面这一组 API 采用字符串列表来作为集合表达。功能与上面相同。只是数据类型不一样。
+///// 为了和上面以示区分,前缀 set 改为 sset,意指 string set。
+//
+// #@ sset.Create <字符串列表>
+//    从字符串列表创建集合。
+// };
+#func {sset.Create} {
+    #return {@sset.Add{{};%0}};
+};
+
+///=== {
+// #@ sset.Size <集合>
+//    计算集合的元素数量。
+// };
+#func {sset.Size} {
+    #return {@slist.Size{%0}};
+};
+
+///=== {
+// #@ sset.Contains <集合> <元素>
+//    判断元素是否在集合中。
+// };
+#func {sset.Contains} {
+    #info arguments save;
+    #local set  {$info[ARGUMENTS][1]};
+    #local elem {$info[ARGUMENTS][2]};
+
+    #if { @slist.IndexOf{{$set};{$elem}} > 0 } {
+        #return 1;
+    };
+    #else {
+        #return 0;
+    };
+};
+
+///=== {
+// #@ sset.Add <集合> <元素> [...]
+//    将一个或者多个元素添加到集合当中。
+// } ;
+#func {sset.Add} {
+    #info arguments save;
+    #unvar info[ARGUMENTS][0];
+    #local argv {$info[ARGUMENTS]};
+
+    #if { &argv[] < 2 } {
+        #return {$argv[1]};
+    };
+
+    #local set  {$argv[1]};
+    #return {@slist.FromList{@set.Add{{@list.FromSlist{$set}};$argv[2..-1]}}};
+};
+
+///=== {
+// #@ sset.Remove <集合> <元素> [...]
+//    从集合中删除一个或者多个元素。
+// };
+#func {sset.Remove} {
+    #info arguments save;
+    #unvar info[ARGUMENTS][0];
+    #local argv {$info[ARGUMENTS]};
+
+    #if { &argv[] < 2 } {
+        #return {$argv[1]};
+    };
+
+    #local set  {$argv[1]};
+    #return {@slist.FromList{@set.Remove{{@list.FromSlist{$set}};$argv[2..-1]}}};
+};
+
+///=== {
+// #@ sset.Equal <集合1> <集合2>
+//    判断集合1和集合2是否相等。
+//    📖  如果两个集合中包含的元素是相同的,那么就判定为相等。相等判定并不要求元素顺序相同。
+// };
+#func {sset.Equal} {
+    #local set1  {%1};
+    #local set2  {%2};
+
+    #if { @sset.IsSubset{{$set1};{$set2}} && @slist.Size{$set1} == @slist.Size{$set2} } {
+        #return 1;
+    };
+    #else {
+        #return 0;
+    };
+};
+
+///=== {
+// #@ sset.IsSubset <集合1> <集合2>
+//    判断集合2是否为集合1的子集。
+//    📖  如果集合1包含了集合2中的每个元素,那么集合2就是集合1的子集。
+// };
+#func {sset.IsSubset} {
+    #local set1  {%1};
+    #local set2  {%2};
+
+    #local elem {0};
+    #foreach {$set2} {elem} {
+        #if { !@sset.Contains{{$set1};{$elem}} } {
+            #return 0;
+        };
+    };
+
+    #return 1;
+};
+
+///=== {
+// #@ sset.IsProperSubset <集合1> <集合2>
+//    判断集合2是否为集合1的真子集。
+//    📖  如果集合1包含了集合中2的每个元素,而且集合1比集合2的元素还要多,
+//       那么集合2就是集合1的真子集。
+// };
+#func {sset.IsProperSubset} {
+    #local set1  {%1};
+    #local set2  {%2};
+
+    #if { @sset.IsSubset{{$set1};{$set2}} && @slist.Size{$set1} > @slist.Size{$set2} } {
+        #return 1;
+    };
+    #else {
+        #return 0;
+    };
+};
+
+///=== {
+// #@ sset.IsSuperset <集合1> <集合2>
+//    判断集合2是否为集合1的超集。
+//    📖  如果集合2包含了集合1中的每个元素,那么集合2就是集合1的超集。
+// };
+#func {sset.IsSuperset} {
+    #return {@sset.IsSubset{{%2};{%1}}};
+};
+
+///=== {
+// #@ sset.IsProperSuperset <集合1> <集合2>
+//    判断集合2是否为集合1的真超集。
+//    📖  如果集合2包含了集合中1的每个元素,而且集合2比集合1的元素还要多,
+//       那么集合2就是集合1的真超集。
+// };
+#func {sset.IsProperSuperset} {
+    #return {@sset.IsProperSubset{{%2};{%1}}};
+};
+
+///=== {
+// #@ sset.Union <集合1> <集合2> [...]
+//    求两个或者多个集合的并集。
+//    📖  并集是指把所有集合放在一块儿之后得到的新集合。
+// };
+#func {sset.Union} {
+    #info arguments save;
+    #unvar info[ARGUMENTS][0];
+    #local argv {$info[ARGUMENTS]};
+
+    #if { &argv[] < 2 } {
+        #return {$argv[1]};
+    };
+
+    #local set  {@list.FromSlist{$argv[1]}};
+    #local idx  {0};
+    #loop {2} {&argv[]} {idx} {
+        #local other {$argv[+$idx]};
+        #local set {@set.Add{{$set};$other}};
+    };
+
+    #return {@slist.FromList{$set}};
+};
+
+///=== {
+// #@ sset.Intersection <集合1> <集合2> [...]
+//    求两个或者多个集合的交集。
+//    📖  交集是指由同时出现在所有集合中的元素组成的新集合。
+// };
+#func {sset.Intersection} {
+    #info arguments save;
+    #unvar info[ARGUMENTS][0];
+    #local argv {$info[ARGUMENTS]};
+
+    #if { &argv[] < 2 } {
+        #return {};
+    };
+
+    #local set  {@list.FromSlist{$argv[1]}};
+    #local idx  {0};
+    #loop {2} {&argv[]} {idx} {
+        #if { &set[] == 0 } {
+            #return {};
+        };
+
+        #local other {$argv[+$idx]};
+        #loop {&set[]} {1} {idx} {
+            #local elem {$set[$idx]};
+            #if { !@sset.Contains{{$other};{$elem}} } {
+                #list set {delete} {$idx};
+            };
+        };
+    };
+
+    #return {@slist.FromList{$set}};
+};
+
+///=== {
+// #@ sset.IsDisjoint <集合1> <集合2> [...]
+//    判断两个或者多个集合是否为不交集。
+//    📖  不交集是交集为空的意思。
+// };
+#func {sset.IsDisjoint} {
+    #local set {@sset.Intersection{%0}};
+    #if { @slist.Size{$set} == 0 } {
+        #return 1;
+    };
+    #else {
+        #return 0;
+    };
+};
+
+///=== {
+// #@ sset.Product <集合1> <集合2> [...]
+//    计算两个或者多个集合的笛卡尔积。返回结果集合中的每个元素都是一个字符串,
+//    其值由各个参数集合的元素拼接而成。
+//    如果调用前设置了环境变量 sset-product-divider,则会以它为分隔符拼接。
+//    <171>注意<299>,sset-product-divider 不能被设置为分号,这会导致灾难性的后果。
+//    📖  两个集合的笛卡尔积是指由两个集合的元素组成的所有可能的有序对的集合。
+//       多个集合的积也可由此扩展。
+// };
+#func {sset.Product} {
+    #info arguments save;
+    #unvar info[ARGUMENTS][0];
+    #local argv {$info[ARGUMENTS]};
+
+    #if { &argv[] < 2 } {
+        #return {};
+    };
+
+    #local set  {@list.FromSlist{$argv[1]}};
+    #if { &set[] == 0 } {
+        #return {};
+    };
+
+    #local divider {@defaultVar{$sset-product-divider}};
+
+    #local idx  {0};
+    #loop {2} {&argv[]} {idx} {
+        #local other {$argv[+$idx]};
+        #if { "$other" == "" } {
+            #return {};
+        };
+
+        #local newSet {};
+        #local left {};
+        #local right {};
+        #loop {1} {&set[]} {left} {
+            #local left {$set[+$left]};
+            #foreach {$other} {right} {
+                #list newSet {add} {{$left$divider$right}};
+            };
+        };
+
+        #local set {$newSet};
+    };
+
+    #return {@slist.FromList{$set}};
+};
+
+///=== {
+// #@ sset.Diff <集合1> <集合2> [...]
+//    计算集合1与其它集合的相对差。
+//    📖  相对差是指从集合1中去掉所有在其它集合中出现过的元素之后剩下的集合。
+// };
+#func {sset.Diff} {
+    #info arguments save;
+    #unvar info[ARGUMENTS][0];
+    #local argv {$info[ARGUMENTS]};
+
+    #if { &argv[] < 2 } {
+        #return {$argv[1]};
+    };
+
+    #local set  {@list.FromSlist{$argv[1]}};
+    #local idx  {0};
+    #loop {2} {&argv[]} {idx} {
+        #if { &set[] == 0 } {
+            #return {};
+        };
+
+        #local other {$argv[+$idx]};
+        #local set {@set.Remove{{$set};$other}};
+    };
+
+    #return {@slist.FromList{$set}};
+};
+
+///=== {
+// #@ sset.SymmDiff <集合1> <集合2>
+//    计算集合1与集合2的对称差。
+//    📖  对称差是指并集与交集的相对差。
+// };
+#func {sset.SymmDiff} {
+    #return {@sset.Diff{{@sset.Union{{%1};{%2}}};{@sset.Intersection{{%1};{%2}}}}};
+};

+ 435 - 0
plugins/lib/xtintin/slist.tin

@@ -0,0 +1,435 @@
+#nop vim: set filetype=tt:;
+
+/*
+本文件属于 PaoTin++ 的一部分。
+PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利
+你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
+*/
+
+#nop 本文件是 xtintin 的一部分,实现了一些字符串列表处理函数;
+
+///=== {
+///// 字符串列表处理函数:
+//
+// #@ slist.FromList <列表>
+//    将列表转换为字符串列表。
+//    <119>注意<299>,由于 TinTin++ 自身语法的缺陷,字符串列表的第一个元素只能是
+//    简单字符串,不能是列表、字符串列表、表格,其余元素不受此限制。
+// };
+#func {slist.FromList} {
+    #local list {%0};
+
+    #if { &list[] == 0 } {
+        #return {};
+    };
+
+    #local output {};
+    #local idx {};
+    #loop {1} {&list[]} {idx} {
+        #local elem {$list[$idx]};
+        #local complex {false};
+        #if { {$elem} == {%*;%*} || {$elem} == {{\{.*\}}} } {
+            #local complex {true};
+        };
+
+        #if { $idx == 1 } {
+            #if { @isTrue{$complex} } {
+                #line sub {var;escapes;secure} #cat output {$elem};
+            };
+            #else {
+                #cat output {$elem};
+            };
+        };
+        #elseif { @isTrue{$complex} } {
+            #cat output {;{$elem}};
+        };
+        #else {
+            #cat output {;$elem};
+        };
+    };
+
+    #return {$output};
+};
+
+///=== {
+// #@ slist.Size <字符串列表>
+//    计算字符串列表中一共有几项内容。
+// };
+#func {slist.Size} {
+    #local l {};
+    #list l create {%0};
+    #list l size result;
+};
+
+///=== {
+// #@ slist.Append <字符串列表> <字符串>
+//    往字符串列表里追加新内容。
+//    <119>注意<299>,由于 TinTin++ 自身语法的缺陷,字符串列表的第一个元素只能是
+//    简单字符串,不能是列表、字符串列表、表格,其余元素不受此限制。
+// };
+#func {slist.Append} {
+    #local list {%1};
+    #local str  {%2};
+
+    #if { {$list} === {} } {
+        #line sub {var;secure} #cat list $str;
+    };
+    #elseif { "$str" == "%*;%*" } {
+        #cat list {;{$str}};
+    };
+    #elseif { "&str" == "{.*}" } {
+        #cat list {;{$str}};
+    };
+    #else {
+        #cat list {;$str};
+    };
+
+    #return {$list};
+};
+
+///=== {
+// #@ slist.Insert <字符串列表> <位置> <字符串>
+//    在字符串列表的指定位置插入新内容,元素位置从 1 开始计。
+//    <119>注意<299>,由于 TinTin++ 自身语法的缺陷,字符串列表的第一个元素只能是
+//    简单字符串,不能是列表、字符串列表、表格,其余元素不受此限制。
+// };
+#func {slist.Insert} {
+    #local list     {%1};
+    #local index    {%2};
+    #local str      {%3};
+
+    #local l {};
+    #list l create {%1};
+
+    #local l {@list.Extend{{$l};@math.Eval{$index - 1}}};
+
+    #if { $index > &l[] } {
+        #list l add {{$str}};
+    };
+    #else {
+        #if { $index == 1 } {
+            #line sub {var;secure} #format str {%s} $str;
+        };
+        #list l insert {$index} {$str};
+    };
+
+    #return {@slist.FromList{$l}};
+};
+
+///=== {
+// #@ slist.Remove <字符串列表> <位置> [<N>]
+//    删除字符串列表中,从指定位置开始的连续 N 个元素,元素位置从 1 开始计。
+//    N 为可选值,默认为 1。
+// };
+#func {slist.Remove} {
+    #local index    {%2};
+    #local amount   {@defaultNum{%3;1}};
+
+    #local l {};
+    #list l create {%1};
+
+    #list l delete {$index} {$amount};
+
+    #return {@slist.FromList{$l}};
+};
+
+///=== {
+// #@ slist.IndexOf <字符串列表> <字符串>
+//    从字符串列表中查找某个字符串,并返回其位置(从 1 开始计),0 表示没找到。
+// };
+#func {slist.IndexOf} {
+    #local list {%1};
+    #local str  {%2};
+
+    #local idx {0};
+    #local elem {};
+    #foreach {$list} {elem} {
+        #math idx {$idx + 1};
+        #if { {$elem} === {$str} } {
+            #return $idx;
+        };
+    };
+
+    #return 0;
+};
+
+///=== {
+// #@ slist.Find <字符串列表> <正则表达式>
+//    从字符串列表中按顺序查找某个模式,并返回第一个符合模式的元素的位置(从 1 开始计),0 表示没找到。
+// };
+#func {slist.Find} {
+    #local list {%1};
+    #local str  {%2};
+
+    #local idx {0};
+    #local elem {};
+    #foreach {$list} {elem} {
+        #math idx {$idx + 1};
+        #if { "$elem" == "$str" } {
+            #return $idx;
+        };
+    };
+
+    #return 0;
+};
+
+///=== {
+// #@ slist.Filter <字符串列表> <正则表达式>
+//    遍历字符串列表,根据正则表达式过滤元素并生成新字符串列表。如果条件为真则收集,否则跳过。
+//    本函数与 select 作用类似。但 select 支持条件表达式,可进行逻辑运算,本函数仅支持模式匹配。
+// };
+#func {slist.Filter} {
+    #local list     {%1};
+    #local regex    {%2};
+
+    #local output {};
+    #local elem {};
+    #foreach {$list} {elem} {
+        #if { "$elem" == "$regex" } {
+            #local output {@slist.Append{{$output};{$elem}}};
+        };
+    };
+
+    #return {$output};
+};
+
+///=== {
+// #@ slist.Contains <字符串列表> <字符串>
+//    判断字符串是否在字符串列表中。
+// };
+#func {slist.Contains}       {#return {@math.Eval{@slist.IndexOf{{%1};{%2}} > 0 }}};
+
+///=== {
+// #@ slist.Sort <字符串列表>
+//    按照字母顺序重新排序字符串列表中的各项。
+//
+// #@ slist.Order <字符串列表>
+//    按照数值顺序重新排序字符串列表中的各项。
+// };
+#func {slist.Sort}     {#return {@slist.FromList{@list.Sort{@list.FromSlist{%0}}}}};
+#func {slist.Order}    {#return {@slist.FromList{@list.Order{@list.FromSlist{%0}}}}};
+
+///=== {
+// #@ slist.Reverse <字符串列表>
+//    按照倒序重新排列字符串列表内容。
+// };
+#func {slist.Reverse} {
+    #return {@slist.FromList{@list.Reverse{@list.FromSlist{%0}}}};
+};
+
+///=== {
+// #@ slist.Get <字符串列表> <下标>
+//    从字符串列表中按照下标取出一个值。
+// };
+#func {slist.Get} {
+    #local list     {%1};
+    #local index    {%2};
+
+    #local index {@math.Floor{@defaultNum{$index;0}}};
+
+    #local n {0};
+    #local elem {};
+    #local selected {};
+    #foreach {$list} {elem} {
+        #math n {$n + 1};
+        #if { $index > 0 } {
+            #if { $n == $index } {
+                #local selected {$elem};
+                #if { $index == 1 } {
+                    #line sub {var;escapes} #format selected {%s} {$selected};
+                };
+                #break;
+            };
+        };
+        #else {
+            #math random {1 d $n};
+            #if { $random == 1 } {
+                #local selected {$elem};
+            };
+        };
+    };
+
+    #return {$selected};
+};
+
+///=== {
+// #@ slist.RandomGet <字符串列表>
+//    从字符串列表中随机取出一个值。
+// };
+#func {slist.RandomGet} {
+    #return {@slist.Get{{%0};}};
+};
+
+///=== {
+// #@ slist.Range <开始值> <结束值> [<步长>]
+//    生成等差数列,从开始值开始,一直到不超过结束值为止。结果用字符串列表表示。
+//    可选的步长用来决定公差,默认为 1。
+//    如果开始值小于结束值,则步长自动取正值。
+//    如果开始值大于结束值,则步长自动取其负值(如果参数为正则取相反数否则不变)。
+// };
+#func {slist.Range} {
+    #local begin {%1};
+    #local end   {%2};
+    #local delta {@defaultNum{%3;1}};
+
+    #local op       {<=};
+    #local delta    {@math.Abs{$delta}};
+    #if { $begin > $end } {
+        #local op       {>=};
+        #local delta    {@math.Eval{- $delta}};
+    };
+
+    #local list {$begin};
+    #math begin {$begin + $delta};
+    #while { $begin $op $end } {
+        #cat list {;$begin};
+        #math begin {$begin + $delta};
+    };
+
+    #return {$list};
+};
+
+///=== {
+// #@ slist.Join <字符串列表> <分隔符> [<是否保留空元素>]
+//    将字符串列表的每个元素用分隔符连接在一起。返回连接后的字符串。
+//    第三个参数是可选的,用来指定是否保留字符串列表中的空元素。
+//    默认不保留,如果想要保留请设置为 true。
+// };
+#func {slist.Join} {#return {@slist.JoinAs{{%1};;%2;%3}}};
+
+///=== {
+// #@ slist.JoinAs <字符串列表> <格式串> <分隔符> [<是否保留空元素>]
+//    将字符串列表的每个元素用格式串渲染之后,再用分隔符连接在一起。
+//    返回连接后的字符串。
+//    格式串默认为 %s。
+//    第四个参数是可选的,用来指定是否保留字符串列表中的空元素。
+//    默认不保留,如果想要保留请设置为 true。
+// };
+#func {slist.JoinAs} {
+    #local strList  {%1};
+    #local format   {@default{%2;%s}};
+    #local sep      {%3};
+    #local keepNull {@isTrue{@default{%4;false}}};
+
+    #local str {};
+
+    #local first {1};
+    #local elem {};
+    #foreach {$strList} {elem} {
+        #if { @isEmpty{$elem} && !$keepNull } {
+            #continue;
+        };
+
+        #format elem {$format} {$elem};
+
+        #if $first {
+            #local str {$elem};
+            #local first {0};
+        };
+        #else {
+            #cat str {$sep$elem};
+        };
+    };
+
+    #return {$str};
+};
+
+///=== {
+// #@ slist.Zip <列表1> <列表2> [...]
+//    将多个列表合并成一个列表。结果列表的每个元素分别对应参数列表中对应位置的元素。
+//    各个参数列表中的元素将按顺序一一对应,以分号分隔连接到一起成为结果列表的元素。
+// };
+#func {slist.Zip} {
+    #info arguments save;
+    #unvar info[ARGUMENTS][0];
+    #local argv {$info[ARGUMENTS]};
+
+    #if { &argv[] < 2 } {
+        #return {%1};
+    };
+
+    #local i {};
+    #local maxSize {0};
+    #loop {1} {&argv[]} {i} {
+        #local list {};
+        #list list create {$argv[+$i]};
+        #local {argv[+$i]} {$list};
+        #local size {&list[]};
+        #if { $size > $maxSize } {
+            #local maxSize $size;
+        };
+    };
+
+    #if { $maxSize < 1 } {
+        #return {};
+    };
+
+    #local zipped {};
+    #loop {1} {$maxSize} {i} {
+        #local n {};
+        #local l {};
+        #loop {1} {&argv[]} {n} {
+            #if { $i > &argv[+$n][] } {
+                #list l add {{}};
+            };
+            #else {
+                #list l add {{$argv[+$n][$i]}};
+            };
+        };
+        #list zipped {add} {{@slist.FromList{$l}}};
+    };
+
+    #return {@slist.FromList{$zipped}};
+};
+
+///=== {
+// #@ slist.ZipAs <格式串> <列表1> <列表2> [...]
+//    将多个列表合并成一个列表。结果列表的每个元素分别对应参数列表中对应位置的元素。
+//    合并时使用格式串对所有参数列表中的元素进行格式化合并。
+// };
+#func {slist.ZipAs} {
+    #info arguments save;
+    #unvar info[ARGUMENTS][0];
+    #local argv {$info[ARGUMENTS]};
+    #local format {$argv[1]};
+    #list argv delete {1};
+
+    #if { &argv[] < 1 } {
+        #return {};
+    };
+
+    #local i {};
+    #local maxSize {0};
+    #loop {1} {&argv[]} {i} {
+        #local list {};
+        #list list create {$argv[+$i]};
+        #local {argv[+$i]} {$list};
+        #local size {&list[]};
+        #if { $size > $maxSize } {
+            #local maxSize $size;
+        };
+    };
+
+    #if { $maxSize < 1 } {
+        #return {};
+    };
+
+    #local zipped {};
+    #loop {1} {$maxSize} {i} {
+        #local n {};
+        #local l {};
+        #loop {1} {&argv[]} {n} {
+            #if { $i > &argv[+$n][] } {
+                #list l add {{}};
+            };
+            #else {
+                #list l add {{$argv[+$n][$i]}};
+            };
+        };
+        #local value {};
+        #line sub var #format value {$format} $l[];
+        #list zipped {add} {{$value}};
+    };
+
+    #return {@slist.FromList{$zipped}};
+};

+ 184 - 38
plugins/lib/xtintin/string.tin

@@ -6,41 +6,168 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
 你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
 */
 
-#func {len}         {#format result {%L} {%0}};
-#func {width}       {#format result {%W} {%0}};
-
-#func {space}       {#var result {@repeat{%0;{ }}}};
-#func {repeat}      {#var result {}; #loop 1 %1 tmp {#cat result {%2}}};
-#func {reverseStr}  {#var result {}; #parse {%1} {tmp} {#var result {$tmp$result}}};
-
-#func {trim}        {#format result {%p} {%0}};
-#func {trimAll}     {#var result {%0}; #replace {result} {%+1..s} {}};
-#func {plain}       {#format result {%P} {%0}};
-
-#func {replace}     {#var result %1; #replace result {%2} {%3}};
-#func {toLower}     {#format result {%l} {%0}};
-#func {toUpper}     {#format result {%u} {%0}};
-#func {capital}     {#format result {%n} {%0}};
-
-#func {char}        {#format result {%X} {%0}; #format result {%x} {$result}};
-#func {codepoint}   {#format result {%A} {%0}};
-#func {hex2char}    {#format result {%x} {%0}};
-#func {char2hex}    {#format result {%A} {%0}; #format result {%X} {$result}};
-
-#func {format}      {#format result {%1} {%2}};
-
-#func {left}        {#local width {%2}; #if {$width<=0} {#return {}} {#format result {%.${width}s} {%1}}};
-#func {right}       {#local width {%2}; #if {$width<=0} {#return {}} {#format result {%.-${width}s} {%1}}};
-#func {substr}      {#return {@left{{@right{{%1}; @eval{@width{%1} - %2}}}; %3}}};
-
-#func {alignLeft}   {#format result {%-%2s} {%1}};
-#func {alignRight}  {#format result {%+%2s} {%1}};
-
-#func {alignCenter}  {
+#nop 本文件是 xtintin 的一部分,实现了一些字符串处理函数;
+
+///=== {
+///// 字符串处理函数:
+//
+// #@ str.Len <字符串>
+//    给出指定字符串的长度。
+//    本函数也可以简写为 len,两个名称用途和用法完全相同。
+//
+// #@ str.Width <字符串>
+//    给出指定字符串的屏幕显示宽度(忽略颜色等控制字符,每个汉字按两个字符宽度计算。
+//    本函数也可以简写为 width,两个名称用途和用法完全相同。
+// };
+#func {str.Len}     {#format result {%L} {%0}};
+#func {len}         {#return {@str.Len{%0}}};
+
+#func {str.Width}   {#format result {%W} {%0}};
+#func {width}       {#return {@str.Width{%0}}};
+
+///=== {
+// #@ str.Space <长度>
+//    生成指定长度的空白字符串。
+//    本函数也可以简写为 space,两个名称用途和用法完全相同。
+//
+// #@ str.Repeat <次数> <字符串>
+//    将指定的字符串重复若干次。
+//    本函数也可以简写为 repeat,两个名称用途和用法完全相同。
+//
+// #@ str.Reverse <字符串>
+//    将字符串按照倒序重排每个字符并返回新的字符串。
+// };
+#func {str.Space}   {#return {@str.Repeat{{%0};{ }}}};
+#func {space}       {#return {@str.Space{%0}}};
+
+#func {str.Repeat}  {#var result {}; #loop 1 {%1} tmp {#cat result {%2}}};
+#func {repeat}      {#return {@str.Repeat{%0}}};
+
+#func {str.Reverse} {#var result {}; #parse {%0} {tmp} {#var result {$tmp$result}}};
+
+///=== {
+// #@ str.Trim <字符串>
+//    去除给定字符串首尾的连续空白。
+//    本函数也可以简写为 trim,两个名称用途和用法完全相同。
+//
+// #@ str.TrimAll <字符串>
+//    去除字符串中全部的空白。
+//    本函数也可以简写为 trimAll,两个名称用途和用法完全相同。
+//
+// #@ str.Plain <字符串>
+//    去除字符串中的颜色代码和 ANSI 控制字符。
+// };
+#func {str.Trim}    {#format result {%p} {%0}};
+#func {trim}        {#return {@str.Trim{%0}}};
+
+#func {str.TrimAll} {#var result {%0}; #replace {result} {%+1..s} {}};
+#func {trimAll}     {#return {@str.TrimAll{%0}}};
+
+#func {str.Plain}   {#format result {%P} {%0}};
+
+///=== {
+// #@ str.Replace <字符串> <正则表达式> <替换表达式>
+//    在给定字符串中执行替换,凡是被正则表达式匹配的东西都会用替换表达式进行替换。
+//
+// #@ str.ToLower <字符串>
+//    将给定字符串转换成小写。
+//    本函数也可以简写为 toLower,两个名称用途和用法完全相同。
+//
+// #@ str.ToUpper <字符串>
+//    将给定字符串转换成大写。
+//    本函数也可以简写为 toUpper,两个名称用途和用法完全相同。
+//
+// #@ str.Capital <字符串>
+//    将给定字符串转换成首字母大写。
+// };
+#func {str.Replace} {#var result {%1}; #replace result {%2} {%3}};
+
+#func {str.ToLower} {#format result {%l} {%0}};
+#func {toLower}     {#return {@str.ToLower{%0}}};
+
+#func {str.ToUpper} {#format result {%u} {%0}};
+#func {toUpper}     {#return {@str.ToUpper{%0}}};
+
+#func {str.Capital} {#format result {%n} {%0}};
+
+///=== {
+// #@ str.FromCode <十进制代码>
+//    将十进制代码转换成对应的 ASCII 或者 Unicode 字符。
+//    本函数也可以简写为 char,两个名称用途和用法完全相同。
+//
+// #@ str.ToCode <字符>
+//    获得该字符在 Unicode 编码中的码点,以十进制整数形式表示。
+//    本函数也可以简写为 codepoint,两个名称用途和用法完全相同。
+//
+// #@ str.FromHexCode <十六进制代码>
+//    将十六进制代码转换成对应的 ASCII 或者 Unicode 字符。
+//    本函数也可以简写为 hex2char,两个名称用途和用法完全相同。
+//
+// #@ str.ToHexCode <字符>
+//    获得该字符在 Unicode 编码中的码点,以十六进制整数形式表示。
+//    本函数也可以简写为 char2hex,两个名称用途和用法完全相同。
+// };
+#func {str.FromCode}    {#format result {%X} {%0}; #format result {%x} {$result}};
+#func {char}            {#return {@str.FromCode{%0}}};
+
+#func {str.ToCode}      {#format result {%A} {%0}};
+#func {codepoint}       {#return {@str.ToCode{%0}}};
+
+#func {str.FromHexCode} {#format result {%x} {%0}};
+#func {hex2char}        {#return {@str.FromHexCode{%0}}};
+
+#func {str.ToHexCode}   {#format result {%A} {%0}; #format result {%X} {$result}};
+#func {char2hex}        {#return {@str.ToHexCode{%0}}};
+
+///=== {
+// #@ str.Format <格式> <参数>
+//    执行 #format result <格式> <参数> 操作。
+//    本函数也可以简写为 format,两个名称用途和用法完全相同。
+// };
+#func {str.Format}  {#format result {%1} {%2}};
+#func {format}      {#return {@str.Format{%0}}};
+
+///=== {
+// #@ str.Left <字符串> <截取宽度>
+//    按照屏幕宽度标准,截取字符串左边的若干字符。
+//    本函数也可以简写为 left,两个名称用途和用法完全相同。
+//
+// #@ str.Right <字符串> <截取宽度>
+//    按照屏幕宽度标准,截取字符串右边的若干字符。
+//    本函数也可以简写为 right,两个名称用途和用法完全相同。
+//
+// #@ str.Sub <字符串> <开始宽度> <截取宽度>
+//    按照屏幕宽度标准,从字符串开始的某个宽度,截取连续的若干字符。
+//    本函数也可以简写为 substr,两个名称用途和用法完全相同。
+// };
+#func {str.Left}    {#local width {%2}; #if {$width<=0} {#return {}} {#format result {%.${width}s} {%1}}};
+#func {left}        {#return {@str.Left{%0}}};
+
+#func {str.Right}   {#local width {%2}; #if {$width<=0} {#return {}} {#format result {%.-${width}s} {%1}}};
+#func {right}       {#return {@str.Right{%0}}};
+
+#func {str.Sub}     {#return {@str.Left{{@str.Right{{%1}; @math.Eval{@str.Width{%1} - %2}}}; %3}}};
+#func {substr}      {#return {@str.Sub{%0}}};
+
+///=== {
+// #@ str.AlignLeft <字符串> <宽度>
+//    按照屏幕宽度标准,在字符串后面补空白,延长至指定宽度并使之居左对齐。
+//
+// #@ str.AlignRight <字符串> <宽度>
+//    按照屏幕宽度标准,在字符串前面补空白,延长至指定宽度并使之居右对齐。
+// };
+#func {str.AlignLeft}   {#format result {%-%2s} {%1}};
+#func {str.AlignRight}  {#format result {%+%2s} {%1}};
+
+///=== {
+// #@ str.AlignCenter <字符串> <宽度>
+//    按照屏幕宽度标准,在字符串前后补空白,延长至指定宽度并使之居中对齐。
+// };
+#func {str.AlignCenter}  {
     #local str  {%1};
     #local max  {@default{%2;80}};
 
-    #local width {@width{$str}};
+    #local width {@str.Width{$str}};
 
     #local left {};
     #local right {};
@@ -52,7 +179,26 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #return {$result};
 };
 
-#func {colorBar} {
+///=== {
+// #@ str.Split <字符串> <分隔符>
+//    将字符串按照分隔符拆分成多个子串,并作为字符串列表返回。
+//    这里的分隔符可以通过正则表达式来指定。
+//    本函数也可以简写为 split,两个名称用途和用法完全相同。
+// };
+#func {str.Split} {
+    #local str {%1};
+    #local sep {%2};
+    #replace str {$sep} {;};
+    #return {$str};
+};
+
+#func {split} {#return {@str.Split{%0}}};
+
+///=== {
+// #@ util.ColorBar <字符串> <颜色1> <权重1> <颜色2> <权重2> [...]
+//    将字符串按照颜色及其对应的权重占比,渲染成彩色字符串。注意颜色参数须按顺序排列。
+// };
+#func {util.ColorBar} {
     #local str {%1};
 
     #local args {};
@@ -64,8 +210,8 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #local count {0};
     #local sum {0};
     #while { $count < &args[] } {
-        #local color    {$args[@eval{$count + 1}]};
-        #local weight   {@defaultNum{$args[@eval{$count + 2}];0}};
+        #local color    {$args[@math.Eval{$count + 1}]};
+        #local weight   {@defaultNum{$args[@math.Eval{$count + 2}];0}};
         #list parts {add} {{
             {color}{$color}
             {weight}{$weight}
@@ -75,13 +221,13 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     };
 
     #local elem {};
-    #local len {@len{$str}};
+    #local len {@str.Len{$str}};
     #local leftLen {0};
     #local leftWeight {0};
     #local colorStr {};
     #foreach {$parts[%*]} {elem} {
-        #local elemLen {@eval{($elem[weight] + $leftWeight) * $len / $sum - $leftLen}};
-        #local text {@substr{{$str};$leftLen;$elemLen}};
+        #local elemLen {@math.Eval{($elem[weight] + $leftWeight) * $len / $sum - $leftLen}};
+        #local text {@str.Sub{{$str};$leftLen;$elemLen}};
         #cat colorStr {$elem[color]$text<099>};
         #math leftLen {$leftLen + $elemLen};
         #math leftWeight {$leftWeight + $elem[weight]};

+ 137 - 11
plugins/lib/xtintin/time.tin

@@ -6,9 +6,21 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
 你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
 */
 
-#func {now} {#format result {%T}};
+#nop 本文件是 xtintin 的一部分,实现了一些时间处理函数;
 
-#func {parseCDuration} {
+///=== {
+///// 时间处理函数:
+//
+// #@ time.Now
+//    返回当前系统时间戳。
+// };
+#func {time.Now} {#format result {%T}};
+
+///=== {
+// #@ time.ParseDoC <时间长度文本>
+//    将中文书写的时间长度(Duration of Chinese)文本转换成以秒为单位的整数值。
+// };
+#func {time.ParseDoC} {
     #local {timeStr} {%0};
 
     #nop 兼容不同的写法;
@@ -18,13 +30,13 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #replace {timeStr} {分钟} {分};
 
     #nop 注意这里用了个小技巧,末尾的空格不要去掉;
-    #replace timeStr {%S年}     {@c2d{&1}*31104000+ };
-    #replace timeStr {%S月}     {@c2d{&1}*2592000+ };
-    #replace timeStr {%S周}     {@c2d{&1}*604800+ };
-    #replace timeStr {%S天}     {@c2d{&1}*86400+ };
-    #replace timeStr {%S时}     {@c2d{&1}*3600+ };
-    #replace timeStr {%S分}     {@c2d{&1}*60+ };
-    #replace timeStr {%S秒}     {@c2d{&1}};
+    #replace timeStr {%S年}     {@math.ParseCN{&1}*31104000+ };
+    #replace timeStr {%S月}     {@math.ParseCN{&1}*2592000+ };
+    #replace timeStr {%S周}     {@math.ParseCN{&1}*604800+ };
+    #replace timeStr {%S天}     {@math.ParseCN{&1}*86400+ };
+    #replace timeStr {%S时}     {@math.ParseCN{&1}*3600+ };
+    #replace timeStr {%S分}     {@math.ParseCN{&1}*60+ };
+    #replace timeStr {%S秒}     {@math.ParseCN{&1}};
 
     #local time {};
     #math time {$timeStr + 0};
@@ -32,9 +44,123 @@ PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 
     #return {$time};
 };
 
-#func {formatTime} {
-    #local time   {@defaultNum{%1;@now{}}};
+///=== {
+// #@ time.Format [<时间戳> [<格式字符串>]]
+//    将指定时间戳按照格式字符串要求,转换成字符串。
+//    格式字符串参见 #help time,如果省略则为 %Y-%m-%d %H:%M:%S。
+//    时间戳如果省略则为当前系统时间。
+// };
+#func {time.Format} {
+    #local time   {@defaultNum{%1;@time.Now{}}};
     #local format {@default{%2;{%Y-%m-%d %H:%M:%S}}};
     #format result {%t} {{$format}{$time}};
     #return {$result};
 };
+
+///=== {
+// #@ time.FormatNow [<格式字符串>]]
+//    将指定时间戳按照格式字符串要求,转换成字符串。
+//    格式字符串参见 #help time,如果省略则为 %Y-%m-%d %H:%M:%S。
+// };
+#func {time.FormatNow} {
+    #local time   {@time.Now{}};
+    #local format {@default{%1;{%Y-%m-%d %H:%M:%S}}};
+    #format result {%t} {{$format}{$time}};
+    #return {$result};
+};
+
+///=== {
+// #@ time.Date [<时间戳>]
+//    将指定时间戳转换成 YYYY-mm-dd 格式的日期字符串。
+//    时间戳如果省略则为当前系统时间。
+// };
+#func {time.Date} {
+    #local time   {@defaultNum{%1;@time.Now{}}};
+    #local format {@default{%2;{%Y-%m-%d}}};
+    #format result {%t} {{$format}{$time}};
+    #return {$result};
+};
+
+///=== {
+// #@ time.Year [<时间戳>]
+//    将指定时间戳所在的年份,四位数字。
+//    时间戳如果省略则为当前系统时间。
+// };
+#func {time.Year} {
+    #local time   {@defaultNum{%1;@time.Now{}}};
+    #local format {@default{%2;{%Y}}};
+    #format result {%t} {{$format}{$time}};
+    #return {$result};
+};
+
+///=== {
+// #@ time.Month [<时间戳>]
+//    将指定时间戳所在的月份,两位数字。
+//    时间戳如果省略则为当前系统时间。
+// };
+#func {time.Month} {
+    #local time   {@defaultNum{%1;@time.Now{}}};
+    #local format {@default{%2;{%m}}};
+    #format result {%t} {{$format}{$time}};
+    #return {$result};
+};
+
+///=== {
+// #@ time.Day [<时间戳>]
+//    将指定时间戳所在的日期,两位数字。
+//    时间戳如果省略则为当前系统时间。
+// };
+#func {time.Day} {
+    #local time   {@defaultNum{%1;@time.Now{}}};
+    #local format {@default{%2;{%d}}};
+    #format result {%t} {{$format}{$time}};
+    #return {$result};
+};
+
+///=== {
+// #@ time.Time [<时间戳>]
+//    将指定时间戳转换成 HH:MM:SS 格式的时间字符串。
+//    时间戳如果省略则为当前系统时间。
+// };
+#func {time.Time} {
+    #local time   {@defaultNum{%1;@time.Now{}}};
+    #local format {@default{%2;{%H:%M:%S}}};
+    #format result {%t} {{$format}{$time}};
+    #return {$result};
+};
+
+///=== {
+// #@ time.Hour [<时间戳>]
+//    将指定时间戳所在时刻的小时值,24 小时制,两位数字。
+//    时间戳如果省略则为当前系统时间。
+// };
+#func {time.Hour} {
+    #local time   {@defaultNum{%1;@time.Now{}}};
+    #local format {@default{%2;{%H}}};
+    #format result {%t} {{$format}{$time}};
+    #return {$result};
+};
+
+///=== {
+// #@ time.Minute [<时间戳>]
+//    将指定时间戳所在时刻的分钟值,两位数字。
+//    时间戳如果省略则为当前系统时间。
+// };
+#func {time.Minute} {
+    #local time   {@defaultNum{%1;@time.Now{}}};
+    #local format {@default{%2;{%M}}};
+    #format result {%t} {{$format}{$time}};
+    #return {$result};
+};
+
+///=== {
+// #@ time.Second [<时间戳>]
+//    将指定时间戳所在时刻的秒值,两位数字。
+//    时间戳如果省略则为当前系统时间。
+// };
+#func {time.Second} {
+    #local time   {@defaultNum{%1;@time.Now{}}};
+    #local format {@default{%2;{%S}}};
+    #format result {%t} {{$format}{$time}};
+    #return {$result};
+};