前言
Mapfile是MapServer
用来描述一个地图的配置文件。它是一个很简单的声明式
语言,一个地图(Map)可以有多个层(Layer),每个层可以有很多属性(键值对)。在一个层的定义中,还可以定义若干个类(Class),这个类用以管理不同的样式(Style)。而每个类或者样式都可以由若干个属性(键值对)。
这里有一个实际的例子:
LAYER
NAME "counties"
DATA "counties-in-shaanxi-3857"
STATUS default
TYPE POLYGON
TRANSPARENCY 70
CLASS
NAME "polygon"
STYLE
COLOR 255 255 255
OUTLINECOLOR 40 44 52
END
END
END
最简单的层的定义
最简单的情形是,我们定义了一个层Layer
,但是没有指定任何的属性:
LAYER
END
我们期望parser可以输出:
{layer: null}
要做到这一步,首先需要定义符号LAYER
和END
,以及一些对空格,非法字符的处理等:
\s+ /* skip whitespace */
\n|\r\n /* skip whitespace */
"LAYER" return "LAYER"
"END" return "END"
<<EOF>> return 'EOF'
. return 'INVALID'
对于,空格,回车换行等,我们都直接跳过。对应的BNF
也非常简单:
expressions
: decls EOF {return $1;}
;
decls
: LAYER END {$$ = {layer: null}}
;
为层添加属性
接下来我们来为层添加Name
属性,首先还是添加符号NAME
和对字符串的定义。这里的字符串被定义为:由双引号括起来的所有内容。
"NAME" return "NAME"
'"'("\\"["]|[^"])*'"' return 'STRING'
[a-zA-Z]+ return 'WORD'
然后我们就可以为BNF
添加一个新的节:
decls:
LAYER decl END
{$$ = {layer: $2}}
;
decl:
NAME STRING
{$$ = $2.substring(1, $2.length - 1)};
在decl
中,我们将获得的字符串两头的引号去掉$2.substring
。这样decl
的值就会是字符串本身,而不是带着双引号的字符串了。修改之后的代码可以解析诸如这样的声明:
LAYER
NAME "counties"
END
并产生这样的输出:
{ layer: 'counties' }
但是如果我们用来解析两个以上的属性:
LAYER
NAME "counties"
DATA "counties-in-shaanxi-3857"
END
解析器会报告一个错误:
$ node map.js expr
/Users/jtqiu/develop/ideas/jison-demo/mapfile/map.js:106
throw new Error(str);
^
Error: Parse error on line 2:
... "counties" DATA "counti
----------------------^
Expecting 'END', got 'WORD'
at Object.parseError (/Users/jtqiu/develop/ideas/jison-demo/mapfile/map.js:106:15)
at Object.parse (/Users/jtqiu/develop/ideas/jison-demo/mapfile/map.js:171:22)
at Object.commonjsMain [as main] (/Users/jtqiu/develop/ideas/jison-demo/mapfile/map.js:620:27)
at Object.<anonymous> (/Users/jtqiu/develop/ideas/jison-demo/mapfile/map.js:623:11)
at Module._compile (module.js:456:26)
at Object.Module._extensions..js (module.js:474:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:312:12)
at Function.Module.runMain (module.js:497:10)
at startup (node.js:119:16)
即,期望一个END
符号,但是却看到了一个WORD
符号。我们只需要稍事修改,就可以让当前的语法支持多个属性的定义:
decls:
LAYER pairs END
{$$ = {layer: $2}}
;
pairs:
pair
{$$ = $1}
|
pairs pair
{$$ = merge($1, $2)}
;
pair:
NAME STRING
{$$ = {name: $2.substring(1, $2.length - 1)}}
| DATA STRING
{$$ = {data: $2.substring(1, $2.length - 1)}};
先看,pair
的定义,它由NAME STRING
或者DATA STRING
组成,是我们语法中的终结符。再来看pairs
的定义:
pairs: pair | pairs pair;
这个递归的定义可以保证我们可以写一条pair
或者多条pairs pair
属性定义语句。而对于多条的情况,我们需要将这行属性规约
在一起,即当遇到这样的情形时:
NAME "counties"
DATA "counties-in-shaanxi-3857"
我们需要产生这样的输出:{name: "counties", data: "counties-in-shaanxi-3857"}
。但是由于符号是逐个匹配的,我们会得到这样的匹配结果:{name: "counties"}
和{data: "counties-in-shaanxi-3857"}
,因此我们需要编写一个简单的函数来合并这些属性:
function merge(o1, o2) {
var obj = {};
for(var k in o1) {
obj[k] = o1[k];
}
for(var v in o2) {
obj[v] = o2[v];
}
return obj;
}
按照惯例,这种自定义的函数需要被定义在%{
和}%
括起来的section
中:
...
[a-zA-Z]+ return 'WORD'
[0-9]+("."[0-9]+)? return 'NUMBER'
<<EOF>> return 'EOF'
. return 'INVALID'
/lex
%{
function merge(o1, o2) {
var obj = {};
for(var k in o1) {
obj[k] = o1[k];
}
for(var v in o2) {
obj[v] = o2[v];
}
return obj;
}
%}
%start expressions
%% /* language grammar */
...
现在我们的解析器就可以识别多条属性定义了:
$ node map.js expr
{ layer: { name: 'counties', data: 'counties-in-shaanxi-3857' } }
嵌套的结构
现在新的问题又来了,我们的解析器现在可以识别对层的对个属性的解析了,不过由于CLASS
并不是由简单的键值对定义的,所以还需要进一步的修改:
classes:
CLASS pairs END
{$$ = {class: $2}}
;
类由CLASS
关键字和END
关键字定义,而类的属性定义和Layer
的属性定义并无二致,都可以使用pairs
(多条属性)。而classes
事实上是pair
的另一种形式,就像对属性的定义一样,所以:
pair:
NAME STRING
{$$ = {name: $2.substring(1, $2.length - 1)}}
| DATA STRING
{$$ = {data: $2.substring(1, $2.length - 1)}}
| classes
{$$ = $1};
这样,解析器就可以识别CLASS
子句了。我们注意到,在CLASS
中,还可以定义STYLE
,因此又需要稍作扩展:
pair:
NAME STRING
{$$ = {name: $2.substring(1, $2.length - 1)}}
| DATA STRING
{$$ = {data: $2.substring(1, $2.length - 1)}}
| classes
{$$ = $1}
| styles
{$$ = $1};
styles:
STYLE pairs END
{$$ = {style: $2}}
;
这样,我们的解析器就可以处理样例中的所有语法了:
node map.js expr
{ layer:
{ name: 'counties',
data: 'counties-in-shaanxi-3857',
status: 'default',
type: 70,
class: { name: 'polygon', style: [Object] } } }
完整的代码在github上的这个repo中。
总结
使用BNF
定义一个复杂配置文件的规则,事实上一个比较容易的工作。要手写这样一个解析器需要花费很多的时间,而且当你需要parser多种配置文件时,这将是一个非常无聊且痛苦的事情。学习jison
可以帮助你很快的编写出小巧的解析器,在上面的Mapfile
的例子中,所有的代码还不到100
行。下一次再遇到诸如复杂的文本解析,配置文件读取的时候,先不要忙着编写正则表达式
,试试更高效,更轻便的jison
吧。