Archive for the ‘系列文章’ Category

整体流程
终于说到整体流程上了。之前的文章一直在解说as中如何来做词法分析,js方面丝毫未提,更没说jssc到底在页面上是怎么工作的。现在,就来详细地解释一下。
环境需求:

现代浏览器(废话)
装有Adobe Flash Player插件,版本号为9或9以上
javascript启用
页面中放入jssc.swf文件

大致流程:

swf文件载入完毕
内嵌在swf中的js首先被执行,寻找页面中所有符合规则的pre节点
顺序遍历这些pre节点,提取每个pre节点的文本内容(即原始代码)
将原始代码传递给swf
swf对传递来的原始代码进行词法分析,并生成一段结果html片段(即一串li节点)
将结果传递回js
js对结果进行包装(一串li节点放入ol节点中,以及标题头、边距和复制等等)
将对应的原始pre节点隐藏(display:none)
将新生成的内容插入原始pre节点的前面

可以看出,js和as耦合的地方在哪里,以及它们之间是如何相互合作的。下面将对以上9个步骤细细说来,希望更多的开发者能提出改进意见~
1.载入swf
这里没什么好说的,在页面底部加入flash标签即可。值得注意的是不同浏览器中标签有所区别,理想情况下可以使用swfobject来插入标准的html标签。不过为了兼容以及简单,一般我是这样做的:
<object classid=”clsid:d27cdb6e-ae6d-11cf-96b8-444553540000″ id=”jssc5″ style=”position:absolute;visibility:hidden;”>
<param name=”movie” value=”jssc5.swf”/>
<param name=”flashvars” value=”find=brush”/>
<embed src=”jssc5.swf” flashvars=”find=brush” name=”jssc5″ type=”application/x-shockwave-flash” style=”position:absolute;visibility:hidden;”/>
</object>
这里面有许多可配置的地方。
object的id和embed的name必须一致且唯一(应该是唯二才对),为了满足不同浏览器的需要。我设为jssc5,这也是默认值。如果和页面上某个节点有冲突需要修改,那么就需要传入参数修改配置。比如说变成jssc5_modify,就得在flashvars中这样做:
<object classid=”clsid:d27cdb6e-ae6d-11cf-96b8-444553540000″ id=”jssc5_modify” style=”position:absolute;visibility:hidden;”>
<param name=”movie” value=”jssc5.swf”/>
<param…

内嵌解析
很容易遇到这样的情况:在需要高亮的代码中还混淆着其它语言种类的代码(最常见的例子为Html内嵌css和js,以下也将以此为例)。这是一件让人头疼的事情,因为无论采用何种方法,内嵌的语言和原本语言的规则一定是不同的。这意味着必须将它们区分开来对待。从这一点出发,自然而然就能引出问题的关键所在——如何区分?
剥离内容
先来考虑最简单的情况:
<head>
<script>var i = 0;</script>
</head>
假如没有第2行的script标签以及其内部的代码,那么整个就是纯html代码高亮,这个没有什么难度(如果已经完全理解前三篇的此法分析的话)。然而不凑巧的是,关键点就在于script标签中会出现js代码,html的此法分析中并没有js的词法规则,两者不能等同。那么怎么办呢?
答案是将它们剥离出来。上例是最简单的例子,我们在对html进行此法分析的时候,一旦读到了<script>开始标签,接着便去寻找</script>结束标签(一般会使用String.indexOf()来查找),然后将标签里面的内容单独提取出来。这是第一步,如果做完的话,此时html的高亮结果应该是:除了js代码没有高亮(即默认颜色)以外,其它的html代码均被正确高亮了。
更复杂的情况
剥离到此还没有结束,因为剥离要考虑其它一些复杂的元素。看以下代码:
<head>
<script>var i = 0;
//</script>
</script>
</head>
代码的第3行中,出现了单行注释,其中有被注释掉的script结束标签。这点需要格外注意。假若使用String.indexOf()来查询script结束标签的话,那么就会在第3行结束。这样就错了,因为第3行实际上是个注释,真正的结束符在第4行。
以此延伸,除了上面的情况以外,引号中的字符串、多行注释、正则里面均会出现类似情况。因此,单纯的String.indexOf()是肯定不行的。我们必须对js代码部分进行预处理。
预处理
在as的解析部分,实际上主要分为两大块:词法分析和存储结果。词法分析即是前面几篇一直在讲解的内容;存储结果即是将分析出来的代码链接起来,说白了就是简单的字符串拼接。
在html中的js代码可能会出现混淆script结束标签的情况之下,唯一解决的办法就是对js也进行简单的词法分析预处理,但不存储结果。因为分析是“读”,而存储是“写”。写的耗时要比读多多了,而且预处理只是为了防止注释、字符串和正则的混淆,不需要真正地进行解析,实现复杂读也比较低。等完全将js代码从html中剥离后,再交给js解析器来做真正的工作,如此也保证了代码的不重复。
<head>
<script>
var i = 0;
//</script>
“</script>”
/*</script>*/
/[</script>]/
</script>
</head>
html中的预处理,至少要保证能将3到7行的js代码完全剥离出来交给js解析器处理,其它html代码则由本身来完成高亮。
状态区分
接下来的难点可能还是在如何在html解析的时候完成区分上面。在这里,我设置了一个state变量用以标识状态。仔细考虑下html,无非发现它主要有以下几种状态:

html节点:即<>内的tag,还有随之的一些属性内容。如:<img width=”100px”/>。
text节点:文本内容,段落p中最常见到。
css节点:style中的css代码。
script节点:script中的js代码。

在默认最开始的时候,是文本节点。一旦遇到了左尖括号,并且随后跟的是个正确的节点名(<x>绝对不是个正确的节点,所以不能当成节点来处理),那么就进入节点状态来解析;当节点解析完了之后,返回文本状态。css和script节点是个两个特殊的节点,因为在它们的开始标签结束之后,要进行预处理查找结束标签。实际上会做其中的一个,另外一个也就懂了。
值得注意的是,html标签中有单个类型的存在,比如<br/>,它不需要成对出现,甚至可以写成<br>。省略/的又是另外一种自闭合类型。它们在处理起来有点麻烦,特别是涉及到深度折叠的时候。解决的办法也是设置状态变量,标识当前节点属于那种类型,以此来区分判断。
这篇写得可能有点简单,因为的确是比较抽象的东西。我也偷偷懒,相信能做到前章所提的词法分析的情况下,纯理论来读本篇也不是什么难事了。可能直接读我的源代码反而会更容易些。在下一篇当中,我会介绍as和js的交互以及jssc的大概处理流程,它将作为结束篇章。

区分正则
想要高亮正则表达式的前提是区分它。由于正则表达式以/开头,因此这和除法、单行注释、多行注释会产生混淆。那么究竟该如何辨别呢?

注释的区别最为简单,因为注释拥有最高优先级。除非注释符出现在字符串内,否则一旦开始,编译器便立刻识别//和/*。
如图所示,当向前看字符为斜线时,根据规则由初始状态0进入状态1。如果接下来读入的字符是*,则进入状态2,并成为多行注释;倘若是/,则进入状态3,变成单行注释;而其它的情况么,则是正则除法皆有可能。
我们不妨设想一下:正则和除法有哪些区别?从语境上说,原则性的区别就是除法的前面是被除数,正则的前面不是。而成为被除数的可能无非是以下3种:数字、变量、括号内的运算结果。我不知道js解析引擎是如何做的,它可能相当得严谨(也简单,因为它会忽略空白和注释)。但是在web端语法高亮的需求中,由于假设输入的代码一定是正确的,所以可以“投机取巧”般地识别二者:
//检查当前除号是否是perl风格正则表达式开头
protected function isPerlReg():Boolean {
for (var j:int = index – 3; j > -1; j–) {
//先要忽略之前的单行注释
if (j == singleCommentEnd) {
j =…

有穷自动机(状态图)
处理词法分析的关键在于建立正确的DFA(确定的有穷自动机),而DFA是如何由NFA(不确定的有穷自动机)构造而来,NFA又是如何从正则表达式演变,不在本文讨论范围之内。我们只需要关心如何去画好这个状态图就行了。
先从最简单的开始。现在来了一串源码,我们的解析器想要识别出关键字if,那么该怎么办呢?下面是它的状态图:

我们把每个圈称作一个状态,圈内的数字用以标识不同的状态,而双边的圈则是终结状态。这个状态图没画完,因为状态4和状态5不是终结状态,却没有转换规则。这不是主要讨论。
一般情况下,状态0被称之为初始状态。解析器会设置一个向前看的哨兵,从左到右遍历源码。假设当前字符是i,很显然,它就是状态0可接受的唯一字符。于是开始执行转换规则,状态0根据规则转换到状态1,继续读入下一个字符。
状态1可接受的字符有2种:f和other。这里other不是说字符串,而是指除f以外的所有字符。假如是other的情况,显然它不是我们想要的关键字if,所以转而进入状态4,进行其它处理——至于处理什么、如何处理,暂时不需要关心,后面会说到;而如果是字符f,这就符合我们的预期,状态1通过规则转换到状态2。
可能你认为已经结束了,但事实并非这么简单。if两个字母是读出了,但却未必是关键字,后面如果跟着一个字母s呢?ifs,它可能是定义的一个变量名,又可能是个方法名。然而不管是什么,总归不是关键字。所以状态2接受2种字符:数字、字母、下划线(js应该还有美元符号$,因为它也可作为变量名组成),这条路通往状态5,我们不管;其它的情况就好办了,空白也好回车也好括号也好,它一定是if关键字,由此进入状态3。
上面说了两个圈儿的是终结状态,识别出if关键字后,状态3就结束了。此时解析器会进行回退,把if后面读入的向前看字符放回源码流中,继续下一次循环。经过这一轮番的解释,相信你能对词法分析有了大概的印象吧。同理,其它的关键字、数字、字符串、注释……也是如此识别的。
或许有人要问:一种语言的关键字那么多,为每个关键字写处理程序岂不是要累死?没错,事实上编译器也不会这样做。上面那个例子只是为了简单起见所举,真正可行的办法是只建立标识符的DFA,将所有标识符识别出来,然后一一对比,看此标识符是否是个关键字。
识别标识符

这是识别js语言标识符的状态图。不难看出,状态3和4都是可能的终结状态(实际上基于最小化DFA数量的原则,它们应该被合并为一个)。在初始状态0时,接受字母、下划线和美元符号,如果读入的向前看字符是的话,那么进入状态1。
状态1接受2种情况:字母、数字、下划线和美元符号,很明显这是js标识符的组成规定。这种情况下会进入状态2,并且状态2也接受同样的情况返回本身状态(标识符的长度是不定的,因此状态2会循环本身),它上面的弧线即表达了这个意思。只有当状态2时读入的向前看字符不是字母、数字、下划线或美元符号任一种的时候,才会进入终结状态3。此时这个标识符也识别出来了,和前面一样,回退继续处理。
状态1还有个接受other的情况,这是当标识符仅由一个字符组成时才会发生的。
以下是jssc中处理ecmascript语言中标识符的源代码:
protected function dealWord():void {
var start:int = index – 1;
var cc:int;
//直到不是字母数字下划线美元符号为止
while (index <= code.length) {
readch();
cc = peek.charCodeAt(0);
if (Character.isLetterOrDigit(cc) || Character.isUnderline(cc) ||…

高亮环境
说到为网页添加代码高亮功能,使用服务器端语言处理无疑是更高效、更兼容的做法(比如基于PHP的GeSHi)。然而这一方式主要面对的问题有三个:

加重了服务器负担,每次读/写都要将代码解析成正确显示的结果,而且代码过多时传输解析结果也会浪费一定的带宽。
增加语言种类或者维护升级时更新高亮程序相当费事,倘若支持热部署并且没有集群时还轻松些。
服务器存储的结果是原始的代码还是高亮后的结果?前者会使得每次读操作都要解析一下浪费资源;后者在修改原始代码的时候则会造成一定困难。引入缓存机制或许是个解决办法,但缓存本身就会增加技术维护量。

抛开服务器端,倘若这一切采用web端来做会是怎样的结果?

每次读时都要将代码解析成正确显示的结果,然而这一切是在客户端做的,与服务器无关,也不会浪费带宽。
维护高亮程序就是维护前端程序(js或者as等),成本要低得多。
服务器存储的是原始代码,不影响代码本身的修改,只需做简单的过滤即可,完全脱离了高亮逻辑。

也正是如此,web端语法高亮成为了主要的潮流。目前web端流行的编程语言无非js和as,silverlight尚需时日。其中js虽然有点兼容性问题,但在前端开发工程师手中早已有无数破解之道;as拥有跨平台、高性能以及良好的OOP支持(这里as主要指as3,下同),却未必如js那般近100%的支持。由此,现在能见到的web端高亮器几乎都是js写的,除了jssc(jssc前3版也是js写的,名称前缀即因此而来)。
对比参数
既然目标锁定了web端语法高亮器(废话,要不然文章标题不是白起了),那么衡量一款语法高亮程序就有了针对性。无非从以下几个方面进行对比:平台支持和兼容性、支持语法种类、程序体积大小、解析速度(性能)、解析结果正确性、功能体验、可扩展性。

平台支持和兼容性
无疑只有js和as的对比。
兼容性不说,as生成abc字节码跨平台支持,只需有avm虚拟机(Flash Player的虚拟机)支持即可;js虽然在各个核心中表现不同,但对前端开发工程师来说并不构成问题。
可到了平台支持上as就不完美了,毕竟它要依靠avm才能运行——这可能是用as来编写高亮程序唯一的弱点了,尽管Adobe吹嘘fp的普及率有97%;而js却在现代浏览器中得到普遍支持,除非客户端手动关闭它。

支持语法种类
这个要看作者原意提供多少种语法支持了。提供的语法当然是越多越好,但同时出现的问题就是写程序的人必须通晓所有语法特性才行。不可能幻想一个高亮解析器能够自动对所有编程语言进行正确解析,除非掌握这门语言的基本语法知识。
这对作者来说是个相当大的挑战,因为不可能一个人能够知晓所有编程语言的特点。良好的做法是设计好接口,让其它有兴趣的人参与进来,编写未实现的语法高亮程序。

程序体积大小
这点其实和上面一条互斥,支持的语法越多自然体积会更大。可行的办法无非一方面尽可能减少程序本身大小,提升代码复用读;另一方面用按需装载或按需组合只加载用到的那一部分(JSI?)。

解析速度(性能)
as在这方面有着先天性的优势。js则不一样了,依赖于引擎的不同,各浏览器的表现也不一致。而且,为了优化算法提高性能,js编写的高亮引擎往往需要牺牲一定的准确性来达到目的。

解析结果正确性
这个上面提到了,最正确的做法无非是针对某种语言的词法分析。然而这样做势必对实现语言有着很大的性能要求,js目前还是远远不能够胜任的,它只能采取某些方法进行折中,使得结果尽量正确。当然那些难以高亮的代码很少有人去写。

功能体验
行数、复制和折叠功能是最基本的。行数统计无需任何干预,html本身的ol节点就是顺序列表,自动支持行数;复制在各个浏览器下表现不一,最终还是需要通过flash player来实现(从这一点上说,无论任何前端高亮器其实都用到了js和as,只是侧重点不同);而折叠功能只能通过词法分析来准确实现,as的优势再次体现出来。

可扩展性
和第2点一样,要增加语法种类同时就考验了程序的可扩展性。目前所有高亮器都有着不错的支持,js高亮器需要注意的是提取所有语言的逻辑共性,词法分析则要针对每一种语言编写lexer,提取同类语言的通用部分。

高亮解析方式
这可能是初心者最关注的问题之一了。我们最终的目的是正确将一段代码解析成能够在网页上显示的结果,因此如何实现这个解析器(我更习惯这样叫它,因为jssc就是基于词法分析的,倘若是其它方式,称呼为高亮器可能更准确一点)的逻辑,便成了核心。从2007年的jssc 1开始,我大抵尝试过3种基本模式,这也是目前前端高亮器中被广泛采用的。以下将一一叙述它们的优缺点:

正则模式
说到高亮一段未知代码,可能你第一反应就是使用正则。没错,基于模式匹配来高亮程序代码,这个方法至今都被大部分人所采用,只是在它基础上进行了许多加工。考虑到如下js代码(以后例子都将以javascript举例):
典型的编程语言主要需要高亮以下几个部分:关键字(保留字)、数字(整数和浮点数)、字符(字符串)、注释。
其中关键字无法精确匹配,但每种语言对关键字的结构都有明确规定,如:以美元符号、下划线或者字母开头,后跟美元符号、下划线或者数字字母,如此用正则找出所有这种组合,再比较是否预留关键字即可;然而数字、字符、注释都具有一定的格式,采用正则很容易做到:
/^[$_a-z][$_a-z0-9]+/i、/^\d*?(\.\d+?)?$/、/^(“|’).*?\1$/、/\/\/.*?\n/。
这里面其实有很多问题,等到以后我们就会知道。
/*这个例子很简单,只需用正则匹配出多行注释、单行注释、关键字、字符串和数字即可。*/
for(var i = 0; i < 10; i++) {
var s = “NO. ” + i;

web端语法高亮器到底是什么时候开始流行的?
SyntaxHighlighter发表于2007年;SHJS的网站上写着copyright © 2007的字样;google-code-prettify的开源项目主页,最早的反馈亦是Mar 2007;就连jssc的雏形也是出生在2007年初的一堂《编译原理》实验课上。这一年,似乎成为高亮web代码的热潮期。
然而,一切仅仅是开始。随着Yahoo官方采用sh(SyntaxHighlighter),用js编写的它一夜成名。sh的确是目前所有已知web端语法高亮中最出色的一个,许多网站都在使用这家伙,它的地位可以称得上是霸主。不过,开源世界的代码永远是竞争激烈的,其它高亮器如雨后春笋般诞生,互相之间无不在攀比性能、功用、大小等等。记得在jssc 2发表的时候,还是个学生的我就把矛头直指sh,意欲一较高下。当然,结果就是另外一回事了。
时至今日,jssc历经5个版本,各方面都已发展至成熟。然而技术推动却一直只有我一个人,各种因素都有,技术门槛肯定是最重要的一个。于是,我决定开写一个系列文章来介绍web端语法高亮原理——不仅仅是帮助jssc的发展,更是为了共享经验、推动web端高亮技术的进步。本篇文章就是作为序言而写的。
以上即是简单的概念介绍,下面是系列文章的目录,链接不定时更新:

web端语法高亮原理:走进jssc的世界(一)
web端语法高亮原理:走进jssc的世界(二)
web端语法高亮原理:走进jssc的世界(三)
web端语法高亮原理:走进jssc的世界(四)
web端语法高亮原理:走进jssc的世界(五)