B3.语法概要
B3.语法概要
在使用模板的场景中,特别希望模板语法不要破坏目标文件的语法,两者无入侵的共存, 如用velocity生成java时,希望模板,同时受velocity和java语法检查和加持。 如生成html时,也希望模板能够不破坏html语法,能够直接在浏览器中预览。
模板语法- 底层模板的语法,如 FreeMarker, Velocity目标语法-母版语法,有自己语法的目标文件,如java,html米波语法- 利用母版语法的注释,做的简单标记指令,完成翻译- 文字语序均为从左至右,不支持从右至左语言。
在母版中通过注释做语法标记,逐行处理规则替换,以输出底层模板(backend)。 母版的处理分为解析parse和合并merge两个过程,解析时的查找依赖正则表达式。 合并时,除了部分RNA外,都是直接输出,效能等于StringBuilder.append。
在RNA中没有复杂的流程控制及执行函数的功能,所以一次解析后,后续合并非常高效,
- 没有
RNA时,相当于幂等操作的静态字符串,仅merge一次,后续直接使用。 RNA依赖于执行引擎,除动态语言外,相当于Map+StringBuilderjava执行引擎,动态编译成字节码,首次编译后等于原生java classStringBuilder预先计算长度,以避免过程中扩容复制。
B3.1.模板特性
米波模板语法中,存在以下基础语素和约定。
空白- 一个0x20或0x09,即英文空格或\t英数-[a-zA-z0-9],英文字母和数字,区分大小写母版注释- 母版语言的注释,如,//,/*和*/指令- 米波语法中特殊意义的特征标记,如前缀DNA- Defined Native AnnotationRNA- Runnable Native Annotation?+*- 分别为[0,1],[1,∞),[0,∞)指令行- 米波指令所在行,只被米波解析,merge后不可见行- 指[^\n]\n或[^\n]$两者格式。
为了简化,后续举例中,省略领起指令的母版注释+空白
HI-MEEPO嗨!米波,用来定义母版注释,以便后续解析DNA:SET设定替换,在一个范围内定义一个模板替换。DNA:END结束作用,结束多个作用的作用域。DNA:BKB免疫区域,被END之前的区域不进行解析。DNA:RAW原生模板,执行后,展现后面的原生模板语法。DNA:SON子模板,可以在解析时引入其他魔板,作为组件使用。RNA:PUT存入变量,使用引擎运行执行体,结果存入环境。RNA:USE使用变量,使用环境变量,内置或PUT的变量。RNA:RUN每次执行,每次都会执行功能体,比如计数器。RNA:WHEN条件执行,组合成if-elseif-else逻辑块。RNA:EACH循环执行,应用于数组或集合,循环输出。RNA:ELSE否则条件,对WHEN和EACH执行否则分支。RNA:DONE结束执行,结束WHEN和EACH的作用域。
在处理行符时,以\n断行,window的\r\n也做\n处理。 单行注释型,若结尾有\n,会作为语法的结束符,即合并时\n不会输出。
因为解析时使用了java的RegExp作为底层实现,所以需要一定的基础。
- 查找中,常误用字符有
.^$?+*{}()[]\|。[]内字符有些不用转义。 - 替换中,
\主要用作反向引用,部分()组合有特殊含义。
正则的compile选项是UNIX_LINES和MULTILINE,通过embedded flag设定
(?idmsuxU-idmsuxU)Nothing, but turns matchflagson/off(?idmsux-idmsux:X)X, as a non-capturing group with the givenflagson/off- i Pattern.CASE_INSENSITIVE 不区分大小写,默认关闭
- d Pattern.UNIX_LINES 只有
\n作为行符,默认开启 - m Pattern.MULTILINE
^和$会匹配行符,默认开启 - s Pattern.DOTTAL
.匹配行符,默认关闭 - u Pattern.UNICODE_CASE 如全角字母的大小写,默认关闭
- x Pattern.COMMENTS 忽略空白,支持行注释,默认关闭
- U Pattern.UNICODE_CHARACTER_CLASS,默认关闭
B3.2.HI-MEEPO 嗨,米波
语法:注释头 空白+ HI-MEEPO !? 空白+ 注释尾?
定义注释并标识此文件为米波模板,以解析其后续母版。
注释头- 单行注释或多行注释的首部,前一个非空白字符串HI-MEEPO- 这么怪的名字(hi meepo),主要是避免重复和转义。!- 如果存在!,表示保留指令前后的空白,后详述。注释尾- 多行注释的结尾,后一个非空白字符串
嗨!米波必须独占一行,最好有空白分割,以便阅读时清晰。 类似sql的DELIMITER定义结束符的用法和作用,举例如下,
- java -
// HI-MEEPO,以//为注释 - java -
/* HI-MEEPO */,以/*和*/为注释 - sql -
-- HI-MEEPO,以--为注释 - bash -
# HI-MEEPO,以#为注释 - html -
<!-- HI-MEEPO -->,以<!--和-->为注释
注意,注释头存在尾字符叠字的情况,米波只处理同字符的叠字,举例如下,
/*-/***** DNA:RAW,无效,不处理//-////// DNA:RAW,处理叠字#-##### DNA:RAW,处理叠字
对于后续文本(DNA和RNA)的解析,存在行解析和块解析2种,规则如下
HI-MEEPO始终是行解析,必须独占一行。单行注释型米波,会按行解析,按行解析。多行注释型米波,会跨行读取,按块解析。- 因非按行解析,故正则匹配时
^和$不定未行首和行尾。
关于HI-MEEPO!和HI-MEEPO处理指令行的首位空白存在以下规则。
- 无
!,并且指令独占一行,输出时忽略本行,即指令行后的第一个\n。 - 有
!时,只处理米波头尾之间的指令,前后空白保留。 DNA:RAW比较特殊,无视!设置,保留指令外,移除指令内的首位空白。@<!--_DNA:RAW_SUPER_-->@中@和_分别标识保留和移除的空白。
后续举例中,都以// HI-MEEPO 为例,但省略书写。
B3.3.厂长DNA,静态替换
DNA好比一个厂长,定义替换指令,在parse时,进行高效的静态文本替换。
B3.4.DNA:SET 设定替换
语法:DNA:SET 空白+ 界定 查找 界定 替换 界定 作用?
在一定作用域内,把符合特征的字符串替换成底层模板的字符串。其中,
界定- 第1个非(空白,!,英数)1-2字节的char,常用的如/,汉字。查找- 不含界定的正则特征,存在分组时参考分组引用。查找为空时,忽略此SET。替换- 不含界定的字符串,存在引用时参考分组引用。替换为空时,表示删除,即替换成空。作用- 生效的作用次数,即到何时结束作用,非空白。
分组引用指查找时有()的group或替换时使用\1的反向引用的情况。 这会对特征字符串的边界有影响,也要避开书写复杂的表达式,约定规则如下,
- 如果
查找中无group,在使用group(0),即全部匹配。 - 如果
查找有group时,取第一个(,即group(1)内容。 - 如
((A)(B(C))),按(从左到右出现的顺序计数。- group(1) - ((A)(B(C)))
- group(2) - (A)
- group(3) - (B(C))
- group(4) - (C)
作用即作用次数或作用域,默认作用1次,*表示匿名的无限次。
次数,以,分隔的单次或-连接的闭区间,如1-3,15。命名的无限次作用,可被END结束。
// DNA:SET /false/{{user.male}}/
var isMale = false;
/* 只把此行的false替换为user.male模板变量。底层模板输出为:
var isMale = {{user.male}};
*/B3.5.DNA:END 结束作用
语法:DNA:END (空白+ 作用)+
结束多个指令产生的作用的作用域,如SET的命名作用域。
// DNA:SET /1010100/{{id}}/id
// DNA:SET /"(性别)"/{{desc}}/1
// DNA:SET /性别/{{info}}/2
SUPER(1010100, "ConstantEnumTemplate", "性别", "性别")
// DNA:END id
/* 分别定义,命名的id;group(1)的desc;第2次匹配的info。底层模板输出为:
SUPER({{id}}, "ConstantEnumTemplate", "{{desc}}", "{{info}}")
*/B3.6.DNA:BKB 免疫区域
语法:DNA:BKB 空白+ 作用
定义一个命名的全局免疫作用,可以被END结束,之间的文本和指令不会被处理。
- 文本 - 任何非米波指令格式的文本
- 指令 - 除了当前生效的BKB对应的END外,都视为文本处理。
- 当前只能有一个生效的BKB
// DNA:BKB 黑皇杖
// DNA:SET /"(性别)"/desc/1
SUPER(1010100, "ConstantEnumTemplate", "性别", "性别")
// DNA:END 黑皇杖
/* 无视了SET指令,底层模板输出为:
// DNA:SET /"(性别)"/desc/1
SUPER(1010100, "ConstantEnumTemplate", "性别", "性别")
*/B3.7.DNA:RAW 原生模板
语法:DNA:RAW 空白+ 原生模板
用注释的语法定义一个模板,用以弥补母版语法不支持的情况。 使用单行注释表意清晰,多行注释时,只保留头尾直接的内容。 效果是,删除注释头,DNA:RAW 和注释尾及之间的空白。
/* 以下两行具有相同的输出效果,即删除了`// DNA:RAW ` */
SUPER(1010100, "ConstantEnumTemplate", "性别", "性别")
// DNA:RAW SUPER(1010100, "ConstantEnumTemplate", "性别", "性别")B3.8.DNA:SON 子模板
语法:DNA:SON 空白+ 路径
把路径资源以UTF8读取,并在当前位置展开,作为模板解析。 路径可包含协议部分,默认classpath。仅支持一下协议。
http://,https://时,以GET读取file://,/时,从file system读取classpath:时,从classloader读入,注意没有//- 以
/或.开头,在以父模板起相对路径,但不建议使用。
需要注意,子模板需要单独声明HI-MEEPO,属于静态解析, 在独立的context中解析,之后并入到当前的父模板中。
B3.9.主任RNA,动态执行
RNA好比车间主任,定义执行指令,在merge时调用执行引擎,用其结果做替换。
- 一个
执行引擎可以执行多种类型的功能体,一种类型简称一个引擎。 引擎的命名,必须为英数,区分大小写,如js。- 命名可以用
!结尾,如js!,执行时错误继续进行,返回null - 执行结果为
null时,在模板合并时会使用字符串空代替。
RNA中默认的引擎默认为map。用户可以通过RnaManager注册引擎,后详述。
map-session级,以功能体为key,到环境中取值,没有则输出key。raw-nothing级,直接把功能体当字符串返回,不会展开转义字符。
米波在多行注释时,使用多行的块解析,所以功能体天然支持多行,提高可读性。
B3.A.RNA:PUT 存入变量
语法:RNA:PUT 空白+ 引擎? 界定 变量 界定 功能体 界定
指定引擎执行功能体,把函数或执行结果存入环境(参加map引擎),以便其他RNA取值。
环境指米波context和部分脚本引擎上下文。引擎,参考引擎说明。界定同SET。变量指存入上下文的变量,非母版字面量。功能体由具体的执行引擎执行,如spring,则可当做SpEL执行。变量或功能体为空时,不进行任何操作。
// DNA:PUT os/who/basename $(pwd)/
/* 把`basename $(pwd)`的输出,以`who`为key存入context中 */B3.B.RNA:USE 使用变量
语法:RNA:USE 空白+ 界定 查找 界定 变量 界定 作用?
SET的RNA版本,区别在于从map引擎中取得变量值,而非底层模板的字面量替换。 变量获取规则(如,导航类对象,管道处理函数),详见map引擎说明。
在变量合并时,会根据变量值的类型进行自动多段缩排支持,同时满足,
被查找的字符串前有缩排的空白。变量值是Array和Collection时,其内条目数大于1个。
对2个以上的元素进行缩进,与第1个元素列对齐。缩排后会出现不智能的情况,影响了美观。
- 缩排的对象,没有
\n结尾,不换行,出现斑马线效果。 - 未缩排对象,包含
\n,换行了,出现呲牙的效果。
// DNA:USE /meepo/user.home/
var userHome = "meepo";
/* 读取System.getProperty("user.home")。底层模板输出为:
var userHome = "/home/trydofor";
*/B3.C.RNA:RUN 每次执行
语法:RNA:RUN 空白+ 引擎? 界定 查找 界定 功能体 界定 作用?
PUT和USE的结合体,同样支持缩排,区别在于,
查找为空时,表示仅执行,不替换功能体执行结果立即使用,不存入变量- 每次都执行,类似计数器功能,每次调用都会自增,无缓存。
// DNA:RUN os/rand/echo $RANDOM/1-3
var userName = "meepo-rand";
var userPass = "rand-rand";
/* 每次都获得随机数,输出3次。底层模板输出为:
var userName = "meepo-12599";
var userPass = "16345-31415";
*/B3.D.RNA:WHEN 条件执行
语法:RNA:WHEN 空白+ 引擎? 界定 真假 界定 功能体 界定 归组
可以使用多个WHEN组合成if-else if-else逻辑块。
真假- 必须是y|yes|n|no|not,表示求值的取真或取假。功能体- 引擎执行结果,并对结果求值。归组- 必须是英数,标识ELSE和DONE归组。
求值时,以下情况为false,对false执行not则为true
- boolean的
false - 对象
null - Number的double值是
NaN或在正负0.000000001间(9位) empty空字符串,空数组,空Collection,空Map
<!-- RNA:WHEN /yes/it.rem0/bg -->
<li value="code">rem0-name</li>
<!-- RNA:WHEN /not/it.rem1/bg -->
<li value="code">rem2-name</li>
<!-- RNA:ELSE bg -->
<li value="code">rem1-name</li>
<!-- RNA:DONE bg -->等同于以下js的伪代码的if(a){}else if(!b){}else{} 分支逻辑
if (it.rem0){
console.log('<li value="code">rem0-name</li>')
} else if (!it.rem1){
console.log('<li value="code">rem2-name</li>')
} else {
console.log('<li value="code">rem1-name</li>')
}B3.E.RNA:EACH 循环执行
语法:RNA:EACH 空白+ 引擎? 界定 步长 界定 功能体 界定 归组
通过归组做为元素引用的循环体。若归组名为it,则it.x表示当前元素的x属性。
步长- 必须-?[0-9]+,表示循环顺序和步长,负数表示倒序功能体- 引擎执行结果,需要是数组或集合,否则等同于RNA:PUT效果。归组- 必须是英数,标识ELSE和DONE归组,引用当前元素和内置状态属性。
根据不同的数据类型,执行不同的循环处理,空或null跳过,可被ELSE执行。
- Array - Class.isArray()
Collection<E>- instance of Collection- 其他类型,不做任何循环
- 倒序循环时,非RandomAccess和ReverseIterator,会toArray
循环体中,存在以下内置属性,用来表示循环的状态,若归组名为it,则,
it- 当前循环的元素,避免同名,而产生环境污染- 引用当前元素的
x属性时,其格式为it.x it._count- 内置变量,当前循环计数,1-base,未循环时为0it._total- 内置变量,归组内所有元素的数量it._first- 内置变量,当前是否第一个it._last- 内置变量,当前是否最后一个- 内置变量在循环结束后不移除,可以在循环外部使用。
因为米波是专业的非专业模板引擎,所以此for-each十分低级,
- 支持有限的对象导航,使用
.分隔对象,详见map引擎。 - 集合内元素仅支持
Map<String,?>和JavaBean的Getter取值。 - 没有作用域隔离,
归组的名称,会造成context内变量覆盖。
<!-- RNA:EACH map/2/items/it -->
<!-- RNA:USE /name/it.name/* -->
<li value="code">rem0-name</li>
<!-- RNA:ELSE it -->
<li>no item</li>
<!-- RNA:DONE it -->
<!-- RNA:USE /total/it._total/ -->
<!-- RNA:USE /count/it._count/ -->
<div>result=count/total</div>等同于以下js的伪代码的for(;;)或for-in循环逻辑,依集合类型和步长正负而定
let step=2 // 循环步长,负数为倒序,不可为0
let index=0 // 过程量
let it = null, count=0, total=items.length; // 内置变量
for(it in items){
if(index++ % step !== 0) continue // 控制步长
count++
console.log('<li value="code">rem0-'+it.name+'</li>')
}
if(count === 0){
console.log('<li>no item</li>')
}
console.log('<div>result='+count+'/'+total+'</div>')B3.F.RNA:ELSE 否则条件
语法:RNA:ELSE 空白+ 归组
通过作用归组,对同组的WHEN或EACH执行否则分支,情况如何。
WHEN时,表示没有任何一个WHEN被执行。EACH时,表示循环体从未执行(如集合无元素)EACH-ELSE和pebble语义相同,和python的for-else不同。
B3.G.RNA:DONE 结束执行
语法:RNA:DONE (空白+ 归组)+
通过归组归组,结束一个或多个WHEN和EACH的归组。
B3.H.占位符模板
简化模板,只进行表达式级的变量替换或函数处理,而非完整的Meepo模板语法。 比如,配置文件中的占位符,通常需要简单的替换或字符转换。
使用时,自定义变量的前后界定符即可,默认是{{和}}。
定义转义符可转义界定符,默认是空,不转义。转义有以下特点,以\为例,
- 只对
界定符有效,如\{{和\}},解析后为{{和}} - 界定符前的自身转义,如
\\{{var}},解析后为\+var变量值 - 占位符,从左到右配对最相邻,不匹配内容做普通字符处理。
- 其他情况无效,如
\n - 不支持占位符嵌套
- 变量名不能有空格,否则会按函数解析
B3.I.RNA的使用限制
因模板在静态解析时生成语法树,所以RNA中的条件及循环执行不能识别运行时结构。 以下模板,模板意图为,对hash特征,使用GitHash,ModTime中第一个truthy值。 直觉上比较符合面向过程的条件赋值,但会引起校验时的作用域交叉语法。
// HI-MEEPO
// RNA:WHEN /yes/GitHash/bg
// RNA:USE /hash/GitHash/
// RNA:ELSE bg
// RNA:USE /hash/ModTime/
// RNA:DONE bg
project hash出现交叉错误,是因为hash这个特征,存在2个RNA:USE与之对应, 在静态解析时,无法获得运行时的条件值,便无法确定其归属的语法树。修改如下
// HI-MEEPO
// RNA:USE /hash/GitHash/
// RNA:USE /time/ModTime/
// RNA:WHEN /yes/GitHash/bg
project hash
// RNA:ELSE bg
project time
// RNA:DONE bg使用fun:see函数辅助完成
// HI-MEEPO
// RNA:USE /hash/fun:see GitHash ModTime/
project hash