项目使用gitdiff的文件变化比较功能,但是gitdiff返回的格式是纯文本,未解析。在网上找了相关的库,比如parse-git-patch,它使用的是gitformat-patch命令生成的补丁文件,不能直接接收命令行返回的文本格式。自己实现一个。普通文本的普通处理一般使用正则化,但是这里是一大段文本,即使写的有正则也很难维护。网上有一篇文章介绍了JavaScript实现的逻辑,但是用JavaScript处理字符串比较麻烦。这里我们选择语法分析生成器来实现。网上的文章经常看到抽象语法树(AST)这个词。将人类编写的文本转换成计算机最初可以读取的数据结构称为AST。在AST之前,需要对文本进行词法分析和语法分析,比如每天用到的Babel中的词法和解析器就是Babylon。解析器繁琐乏味,所以有了解析器的生成器。所有可以用JavaScript编写的东西最终都会用JavaScript编写。前端自然有相应的库可用。两个著名的库是PEGjs和Jison。我们可以用发电机做什么?小到替换正则表达式,大到实现您自己的领域特定语言(DSL)。与曾经最有希望替代JavaScript的CoffeeScript一样,它的V2版本使用Jison库作为自己的语法分析器。PEGjs语法规则很容易上手,这里使用PEGjs来实现解析器。(PEGjs不再有maintainer,在使用过程中无意中发现有人另外建了一个分支版本来维护PEGgy,可以无缝过渡。)PEGGYpeggy可以在浏览器中使用,也可以导入到项目中。或者写好规则,用命令生成解析器使用。简单的规则可以直接使用在线版调试https://peggyjs.org/online.html。如果在网页上安装npminstallpeggy使用,直接导入peggy.min.js即可。UseconstPEG=require("peggy");//导入规则生成解析器constparser=PEG.generate(RULE,{trace:true,});//使用解析器constresult=parser.parse(TEST_DATA,{示踪剂:真,});生成函数和解析函数都可以传入参数。生成函数参数,网上有湘南翻译,这里摘录一下。allowedStartRules:指定解析器启动的规则。(默认是语法中的第一条规则。)cache:如果设置为true,parser会缓存parse的结果,可以避免极端情况下过多的解析时间,但同时也会让parser变慢作为一个方面effect(defaultfalse).dependencies:设置解析器的依赖,它的值是一个对象,它的key是访问依赖变量,value是需要加载的依赖模块id。该参数仅在format参数设置为"amd"、"commonjs"、"umd"时生效。(默认为{})exportVar:当没有检测到模块加载器时,解析器对象被分配到的全局变量的名称;仅当format设置为“globals”或“umd”(默认值:null)时有效。format:生成的解析器格式,可选值有(“amd”、“bare”、“commonjs”、“globals”或“嗯”)。只有当输出设置为源时,该参数才会生效。optimize:为生成的解析器选择优化方案,可选值为“speed”或“size”。(default"speed")输出:设置generate()方法的返回格式。如果值为“parser”,则返回生成的解析器对象。如果设置为“source”,则返回parsersourcestringplugins:Theplugintracetouse:跟踪parser的执行过程(默认为false)。parse函数参数startRule:初始分析tracer的规则名称:显示解析器执行规则的过程...(任何其他参数):使用options变量接收参数,可以在解析函数中使用,可以传入自定义参数,可以为解析提供一些配置函数规则。规则非常简单。我举个例子,描述会很繁琐。可以直接阅读官方文档快速了解。总共没有多少规则。整体外观大致是这样的:可以在VSCode中安装PeggyLanguage插件,它可以提供语法高亮、跳转、错误提示等功能。{{functionprefix(str){return`Interger:${str.join("")}`}}}/*start*/start=integer_contentcontent="("integer")"/"[""TEXT:"i[a-zA-Z+-]*"]"/"{".*"}"integer"integer"=digits:[0-9]+{returnprefix(digits);}//matchspaces_=SPACE+{return""}SPACE=""/"\t"可以使用网络版,输入123456(00)或123456[text:Yes]类似格式解析字符串,可以看到匹配的数据。这个简短的规则列出了常用的规则并对其进行了解释。{{functionprefix(str){return`Interger:${str.join("")}`}}}{{}}初始化器顶部花括号的区域称为初始化器,用a{}two任何{{}}括号定义都可以,只要前后括号的数量匹配即可。在这里您可以自定义一些JavaScript代码。可以通过options获取传入的参数进行预处理,也可以为后续规则定义一些工具函数。接下来遇到第一个规则:/*START*/start=integer_content/**/and//commentssupport/**/and//commentslikeotherprogramminglanguages.startrulenamestart是规则的名称,可以任意起草,只要符合JavaScript命名规范即可。integer_contentparsingexpression=等号后面是解析表达式,这里的表达式是integer_content。你不明白这些是什么意思吗?也就是说里面的表达式很可能是另外一个规则名,所以可以继续往下看:content="("integer")"/"[""TEXT:"i[a-zA-Z+-]*"]"/"{".*"}"这里可以看到上面解析表达式中其中一条规则的内容。空白字符会发现这里的定义格式不一样,等号可以改到第二行。pegpy在解析规则的时候,词法分析首先把文本分成token(比如=,",/,strings,comments等)。不像JavaScript,空白字符不作为token,所以我们可以随便换行or插入一个空格。/Symbol/Symbol类似于or,意思是符号前后有两条规则,先匹配前面的规则,不匹配时继续匹配后面的规则。可以是任意数ofrulesconnecteduntilall规则匹配不成功,抛出异常。""引号先看第一个表达式"("整数")",我们对文本数据使用引号,所以有两个匹配的括号before和after中间的还是不知道,说明是另外一个规则名。所以可以推测这段是解析两个括号之间的一些字符,根据名字是匹配两个括号之间的数字。看下下一个:"[""TEXT:"i[a-zA-Z+-]*"]",这里也是一样,但是比较复杂,匹配方括号之间的值,但是没有参考其他规则,而是写显式的匹配信息。""iignorecase"TEXT:"i也是一样,引号中间是文本,所以匹配的是TEXT:,后面的i是干什么的?这个和regular一样,加i忽略字符串的大小写,所以也可以匹配text等其他形式。[]从集合中匹配[a-zA-Z+-]*看起来像一个规则模式?这里和正则模式一样,匹配a到z的所有英文字母,A-Z是大写英文字母,还有+,-两个符号。*+匹配控件[]中括号表示只列出其中一个项目将被匹配,所以最后一个*表示匹配的次数,零次或多次。同样,还有一个+号,表示一次或多次。?匹配失败返回null有时你可以看到?其他解析表达式中的符号。?不像正则表达式那样表示0到1次,而是表示匹配成功则返回结果,不成功则返回null。也没有{}表示附加的重复功能,只有*、+两个符号,相比常规功能没有那么丰富。"[""TEXT:"i[a-zA-Z+-]*"]",所以这段是匹配以TEXT:开头的方括号,后面跟着所有的大小写字母和加减号字符。第三条规则:“{”.*“}”。.任意字符按照之前学的,这个是匹配花括号里面的内容,*代表匹配的个数,那.呢?.这里表示匹配任意字符,包括空格等字符。所以这句话实际上匹配了花括号中的所有内容。到目前为止,我已经学会了基本规则。说的比较啰嗦,其实很简单。规则不多。上面是这样连接的:匹配括号内符合整数规则的信息。如果不符合,则切换到下一条规则,匹配以TEXT开头的方括号内容:不分大小写,内容只能是大小写字母和加减字符,如果有其他字符,匹配不成功,跳到最后一条规则,匹配花括号内的所有字符。如果再次失败,将弹出错误消息。往下看,可以看到一直提到的整数规则:integer"integer"=digits:[0-9]+{returnprefix(digits);}规则后的等号前有多个规则别名一个带引号的字符串“integer”,是规则的别名,用于调试,可以像其他前面的规则一样省略。digits:[0-9]+Analysisexpressionlabel在分析表达式中,可以看到除了之前已知的[0-9]+部分,还多了一个冒号语法,就是给分析结果起个名字.方便后续的action调用。{返回前缀(数字);}解析表达式的动作是相对于其他规则的。对于这个规则,我们在最后定义了一个类似函数的东西,就是JavaScript函数。我们可以在解析表达式后面加上花括号,这里是写JavaScript代码的地方。像这句话,就是调用我们在initializer中定义的函数,对获取到的文本进行处理并返回。这是peggy免费的地方。当语法分析能力不够,或者解析出来的文本比较零散(字符串往往被一个一个的分割成一个数组存储)的时候,就需要用到action了。这里结合整型规则,匹配一个或多个数字字符,处理成连续的字符串(处理前匹配的数据为[1,2,3,4,5],为了阅读和使用方便,往往需要被处理为12345)并以Integer:为前缀。最后一段是解析空格的规则://匹配空格_=SPACE+{return""}SPACE=""/"\t"之前说过,token之间的空格会被忽略,所以如果文本中有空格,也需要单独搭配。就用冒号"",如果需要匹配制表符之类的也可以直接写"\t"。为了不影响解析表达式的阅读,命名为_,便于识别。同时用一个动作将匹配到的结果转为空,以便后面对数据进行处理。匹配文本运行后返回的数据大致是这样的。如果需要,可以添加action将数据处理成指定的格式:gitdiff的数据格式说了这么多,现在我们可以开始输入我们需要做的需求了。要解析gitdiff返回的数据,自然要先知道格式规范。返回的数据如下所示:diff--gita/package.jsonb/package.jsonindexcb2f4bc..35455a2100644---a/package.json+++b/package.json@@-1,13+1,14@@{"name":"peg-git-diff-parser",-"version":"0.0.0",+"version":"1.0.0","description":"gitdifftextparser",-"main":"index.js",+"main":"src/index.js","scripts":{"build":"peggy-odist/gitDiffParser.jssrc/gitDiffParser.peggy","test":"nodesrc/index.js"},"author":"LnnCoCo",+"new":"new","license":"ISC","dependencies":{"peggy":"^1.2.0”让我们逐行解释。diff--gita/package.jsonb/package.jsondiff--git是一个固定字符,a和b代表变化前后的文件。indexcb2f4bc..35455a2100644..为分隔符,表示索引区hash为cb2f4bc的对象和工作区hash为35455a2的对象。100644是对象的模式,100代表普通文件,644代表权限信息。---a/package.json+++b/package.json对比文件名信息,---变化前,+++变化后。@@-1,13+1,14@@{"name":"peg-git-diff-parser",-"version":"0.0.0",+"version":"1.0.0","description":"gitdifftextparser",-"main":"index.js",+"main":"src/index.js","scripts":{"build":"peggy-odist/gitDiffParser.jssrc/gitDiffParser.peggy","test":"nodesrc/index.js"},"author":"LnnCoCo",+"new":"new","license":"ISC","依赖项":{"peggy":"^1.2.0"这是从这里开始的每个更改的信息块。以@@...@@开头,有多个@@...@@块,这里只有一个。-1,13+1,14,-代表变化前,+代表变化后,1,13,代表第一行之后的十三行。1,14也是一样,因为是新加的一行,所以改完之后就多了一行。然后接下来就是文字了,这里是两个很容易看出来的符号,其实是三个:+,-,空格。这对于解析非常重要,所有行都以这三个开头。-为变更前,+为变更后,空格为不变的内容。不变内容的显示逻辑是以变化的行为为中心上下显示最多三行内容。这样,整个gitdiff的输出格式就一目了然了。查资料的时候发现居然有新的增删改名,文件分为二进制和非二进制。但是在简单的命令行gitdiff的情况下,这些是不能输出数据的,所以目前的需求也是没用的,其他这些情况先不管了。新建工程实现解析规则,然后安装peggy。新建gitDiffParser.peggy文件编写规则,其他测试代码如读取测试数据到插件中可以自己补充。完整项目地址:peg-git-diff-parser可以在VSCode中安装PeggyLanguage插件,可以提供语法高亮和错误提示。先定义一些公共规则/***publicdefinition*///path文件名filePath=hit:[A-Za-z0-9\\\/\._\-@$()*&^+!]+{returnhit.join("")}//换行符LINE_END="\r\n"/"\n"//空白__=SPACE*{return""}_=SPACE+{return""}SPACE=""/"\t"走吧。diff--gita/package.jsonb/package.json这里唯一改变的就是文件名,所以其他部分可以写死,大概是这样的。header="diff--git"_filePath_filePathLINE_END很简单,匹配改变的部分规则即可。这里为了方便上层识别,加上一个标签,将包作为对象返回。/***第一行**/header="diff"i_"--git"i_'a'beforePath:filePath_'b'afterPath:filePathLINE_END{return{beforePath,afterPath}}其余类似。下面直接来看比较麻烦的changeblock的数据分析。很容易先解析head@@-1,13+1,14@@,从@@开始定位,@@结束。changeHeader="@@"_beforeChangeLine:changeLineInfo_afterChangeLine:changeLineInfo_"@@"LINE_END{返回{changeHeader:`@@${beforeChangeLine.text}${afterChangeLine.text}@@`,beforeChangeLine,afterChange}/geLine/更改行信息从第N行开始,共N行1,6从第一行开始,共6行(更改-+两行算一行)changeLineInfo=type:([-|+])line:([0-9]+","[0-9]+){constlineFormatText=formatLine(line);return{text:`${type}${lineFormatText}`,type,line:lineFormatText}}写在一起由于行信息比较麻烦,所以又写了一个formatLine函数来处理。然后就是比较麻烦的地方,后面的数据是变长的。而且,+、-、空格符号也会出现在剩下的部分,所以只能限制第一部分匹配这三个,分别输入三个不同的规则。但是这里没有像正则那样的开头标识符,所以换个角度想,在每一行的开头,上一行必须有一个换行符,所以可以定义LINE_END“-”或者LINE_END“+”这样。后面的内容需要全部匹配,直接.*是肯定不行的,后面的所有信息都会一起匹配。好在文档中也写到[]可以和^符号一起使用进行反向匹配。比如[^ABC]匹配除A、B、C以外的任意字符,可惜不能直接用^符号匹配规则,否则可以写出更复杂的匹配逻辑。所以当前的信息就可以解决了,变化块的数据一定是一行,所以我们只需要识别换行符,停止匹配即可。规则如下:changeBeforeContent=LINE_END"-"hit:[^\r\n]+{return{type:"-",text:hit.join("")}}changeAfterContent=LINE_END"+"hit:[^\r\n]+{return{type:"+",text:hit.join("")}}但是我们的变化数据在中间,前后都有上下文相关的背景数据,这些数据以空格开头,或者开头不是+、-符号的,都视为上下文相关内容。所以规则很简单changeContext=。{returnnull}然后我们组合规则,使四种情况包含所有文本。changeChunk=line:changeHeader/beforeContent:changeBeforeContent/afterContent:changeAfterContent/changeContext但是这样只能匹配一行内容,所以我们还需要加上count信息,过滤context内容返回的空数据。changeChunk=hit:(line:changeHeader/beforeContent:changeBeforeContent/afterContent:changeAfterContent/changeContext)*{returnhit.filter(item=>item)}在入口处,整理出需要的信息格式,返回我们期望的格式化diff数据.具体细节可以在这里查看:peg-git-diff-parser相关阅读Peggy官网PeggyGithubPEG.js文档阅读diff
