linear-gradient(
[ <angle> | to <side-or-corner> ,]? <color-stop> [, <color-stop>]+ )
\---------------------------------/ \----------------------------/
Definition of the gradient line List of color stops
where <side-or-corner> = [left | right] || [top | bottom]
and <color-stop> = <color> [ <percentage> | <length> ]?
2018年10月11日更新:
截至
3.6.11
,evalExpr
支持了执行调用表达式(readCall)2018年9月2日更新:
随着 San 的版本迭代,截至
3.6.7
,parseExpr
支持了新的特性:parseExpr
支持解析调用(readCall),但evalExpr
中并没有相应的支持:https://github.com/baidu/san/commit/ed2d6c4424f32ff5c625c7585002b3d623fd8084-
:https://github.com/baidu/san/commit/3c43143ce95eb3a1630eb08fcb9d49981afc4fd3方法说明
san.parseExpr是San中主模块下的一个方法。用于将源字符串解析成表达式对象。该方法和san.evalExpr是一对,后者接收一个表达式对象和一个san.Data对象作为参数,用于对表达式进行求值。如下例:
单独拿出
parseExpr
来分析,其根据源字符串生成表达式对象,从San的表达式对象文档中,可以看到San支持的表达式类型以及这些表达式对象的结构。我们在这里简单记录一下,parseExpr
需要解析的表达式都有哪些:除了上述表示运算关系的表达式外,还有表示数据的表达式,如下:
由于
Accessor
存在意义,是为了在evalExpr
阶段从Data
对象中获取数据,所以这里我将Accessor
归类为表示数据的表达式。现在我们知道了所有的表达式类型,那么,
parseExpr
是如何从字符串中,解析出表达式对象的呢?如何读取字符串
parseExpr
方法定义在src/parser/parse-expr.js中。我们可以看到其依赖了一个Walker类,注释中的说明是字符串源码读取类。Walker
类包含以下内容:属性:
this.source
:保存要读取的源字符串this.len
:保存源字符串长度this.index
:保存当前对象读取字符的位置方法:
currentCode
方法:返回当前读取字符的 charCodecharCode
方法:返回指定位置字符的 charCodecut
方法:根据指定起始和结束位置返回字符串片段go
方法:将this.index
增加给定数值nextCode
方法:读取下一个字符并返回它的 charCodegoUntil 方法
match 方法
如何处理运算符的优先级
在初看
parseExpr
实现的时候,这就是一个困扰我的难题。学习过程中,我看到San
最先是将表达式丢给一个读取三元表达式的方法,这个方法里面去调用读取逻辑或表达式的方法,逻辑或里面调用逻辑与,逻辑与里面调用判等,判等里面调用关系⋯⋯看得我可以说是云里雾里。虽然大致能明白这是在处理运算优先级,但是我觉得肯定有一个更上层的指导思想来让San
选择这一方案。为了寻找这个“指导思想”,我转头去看了一段时间的编译原理,大致上理清了这部分思路。考虑到有些同学应该也和我一样没有系统地学习过这门课程,因此我在下面取《编译原理》中的例子来予以说明(下文内容包含了很多定义性的内容,且为了保证严谨,很多定义都是直接照搬书上的,所以如果你对这部分足够熟悉,跳过即可。)
上下文无关文法及其构成
假设我们现在要解析的
expr
是一个十以内的四则运算算式(编译原理将其视为一种语言),其包括加减乘除( +、-、*、/ )四则运算。我们可以使用一种叫做产生式的方式,来表示表达式的解析规则。有了产生式,我们可以将一个算式的解析规则表达成如下形式(这一解析过程被称为词法分析):这里介绍几个概念,这里的
digit
和+ - * / ()
等符号,被称为终结符号,表示语言中不可再分的基本符号;而像expr
这样能够用于表示终结符号序列的变量,被称为非终结符号。我们都知道,十以内的四则运算算式的解析是与上下文无关的。在编译原理中,将描述语言构造的层次化语法结构称为“文法”(grammar),我们的十以内的四则运算算式就是一个“上下文无关文法”(context-free grammar)。编译原理中定义了上下文无关文法由四个元素构成:
语法分析树
语法分析树是一种图形表示,他展现了从文法的开始符号推导出相应语言中的终结符号串的过程。例如一个给定一个算式:9 - 5 + 2,可以表示成如下的语法分析树:
二义性及其消除
单纯从 9 - 5 + 2 出发去画语法分析树,还能得到另一种结果,如下:
如果我们从下往上对语法分析树进行计算,前一棵树先计算 9 - 5 得 4,然后 4 + 2 得 6,但后一棵树的结果则是 5 + 2 得 7,9 - 7 得 2。这就是文法得二义性,其定义为:对于同一个给定的终结符号串,有两棵及以上的语法分析树。由于多棵树意味着多个含义,我们需要设计没有二义性的文法,或给二义性文法添加附加规则来对齐进行消除。
在本例中,我们采用设计文法的方式来消除二义性。由于四则运算中,加减位于一个优先级层次,乘除位于另一个,我们创建两个非终结符号
expr
和term
分别对应这两个层次,并使用另一个非终结符号factor
来生成表达式中的基本单元,可得到如下的产生式:有了新的文法之后,我们再看 9 - 5 + 2,其仅能生成如下的唯一语法分析树:
parseExpr 的实现
现在我们回到San中的表达式,有了前面的基础,相信大家都已经清楚了
parseExpr
解析表达式源字符串方法的缘由。接下来,我们只要合理的定义出来“San中的表达式”这一语言的产生式,函数实现就水到渠成了。表达式解析入口parseExpr:
其对应的产生式就是:
readTertiaryExpr:
可以看到,判断条件部分
conditional
是readLogicalORExpr
的结果。如果存在?
、:
两个和三元表达式相关的终结符号,就返回一个三元表达式类型的表达式对象;否则直接返回conditional
。可知产生式:由readLogicalORExpr可得产生式:
由readLogicalANDExpr得:
由readEqualityExpr得:
由readRelationalExpr得:
readAdditiveExpr:
读加法的这个函数有些特殊,其在第一步先调用了读乘法的方法,得到了变量
expr
,然后不断地更新expr
对象包住原来的对象,以保证结合性的正确。方法的产生式如下:
由readMultiplicativeExpr得:
readUnaryExpr这个函数,包含了除布尔值的表达式之外的,各个表示数据得表达式的解析部分。因此对应的产生式也相对复杂,为了便于说明,我自行引入了一些非终结符号:
由readParenthesizedExpr得:
由readCall得:
由readAccessor得:
至此,我们终于把所有的产生式都梳理清楚了。
和 JavaScript 文法的对比
在这里我附上一份JavaScript 1.4 Grammar供参考。通过对比两种文法产生式的不同,能找到很多两者之间解析结果得差异。下面是一个例子:
注意到 San 中关于
RelationalExpression
的产生式是:也就是说,对于
1 > 2 < 3
,其匹配了RelationalExpr ---> AdditiveExpr > AdditiveExpr
。其中1
传入了AdditiveExpr
解析成Number
的1
;2 < 3
则被视为另一个AdditiveExpr
进行解析,由于后面已经没有能够处理<
的逻辑了,所以会被解析成Number
的2
。所以,输入的1 > 2 < 3
,真正解析出来的就只有1 > 2
了,所以上面的代码会返回 false 。个人认为 San 在这里应该是刻意为之的。因为对于
1 > 2 < 3
这种表达式,真的没必要保证它按照JavaScript
的文法来解析——这种代码写出来肯定是要改的,没有顾及它的意义。拓展
了解了 parseExpr 是如何从源字符串得到表达式对象之后,也就发现其实很多地方都用了类似的方法来描述语法。比如CSS 线性渐变。这里我的链接直接指向了MDN上关于线性渐变的形式语法(Formal syntax)部分,可以看到这部分对线性渐变语法的描述,和我上面解析 parseExpr 的时候所用的产生式如出一辙。
这种语法形式是MDN定义的CSS属性值定义语法。
参照我们前面所写的产生式与上面的CSS属性值定义语法,我写出了如下的产生式:
结语
这一趟下来可以说是补了不少课,也揭示了 San 中内部原理的一角,后面计划把
evalExpr
、Data
、parseTemplate
等方法也学习一遍,进一步了解 San 的全貌。