exportconstmatch=(pattern:string,text:string):boolean=>{// '^' at start of pattern matches start of next.
if(pattern[0]==='^'){returnmatchHere(pattern,1,text,0);}// Try all possible starting points for pattern.
letiText=0;do{if(matchHere(pattern,0,text,iText)){returntrue;}iText+=1;}while(iText<text.length);// Nothing worked.
returnfalse;}constmatchHere=(pattern:string,patternIndex:number,text:string,textIndex:number)=>{// There is no more pattern to match.
if(patternIndex===pattern.length){returntrue;}// '$' at end of pattern matches end of text.
if((patternIndex===(pattern.length-1))&&(pattern[patternIndex]==='$')&&(textIndex===text.length)){returntrue;}// '*' following current character means zero or more.
if(((pattern.length-patternIndex)>1)&&(pattern[patternIndex+1]==='*')){// Try matching zero occurences(skip the current char and the '*')
if(matchHere(pattern,patternIndex+2,text,textIndex)){returntrue;}// Try matching one or more occurences
while((textIndex<text.length)&&(pattern[patternIndex]==='.'||text[textIndex]===pattern[patternIndex])){// Try to match the rest of pattern after consuming this
// character
if(matchHere(pattern,patternIndex+2,text,textIndex+1)){returntrue;}textIndex+=1;}// if there is any match, it will return early in the while loop,
// so when reach this statement, it means nothing found.
returnfalse;}// Match a single chacater.
if(textIndex<text.length&&(pattern[patternIndex]==='.')||(pattern[patternIndex]===text[textIndex])){returnmatchHere(pattern,patternIndex+1,text,textIndex+1);}// Nothing worked.
returnfalse;}
exportconstINVALID_INDEX=-1;exportabstractclassRegexBase{// index to continue matching at or -1 indicating that matching failed
abstract_match(text:string,start:number):number;abstractrest:RegexBase;match(text:string):boolean{// check if the pattern matches at the start of the string
if(this._match(text,0)!==INVALID_INDEX){returntrue;}for(leti=1;i<text.length;i+=1){if(this._match(text,i)!==undefined){returntrue;}}returnfalse;}}
describe('Regex testsuite',()=>{it.each([['a','a',true,Lit('a')],['b','a',false,Lit('b')],['ab','ba',false,Lit('ab')],['^a','ab',true,Start(Lit('a'))],['^b','ab',false,Start(Lit('b'))],['a$','ab',false,Lit('a',End())],['a$','ba',true,Lit('a',End())],['a*','',true,Any(Lit('a'))],['a*','baac',true,Any(Lit('a'))],['ab*c','ac',true,Lit('a',Any(Lit('b'),Lit('c')))],['ab*c','acc',true,Lit('a',Any(Lit('b'),Lit('c')))],['ab*c','abc',true,Lit('a',Any(Lit('b'),Lit('c')))],['ab*c','abbbc',true,Lit('a',Any(Lit('b'),Lit('c')))],['ab*c','abxc',false,Lit('a',Any(Lit('b'),Lit('c')))],['ab*c','ac',true,Lit('a',LazyAny(Lit('b'),Lit('c')))],['ab*c','acc',true,Lit('a',LazyAny(Lit('b'),Lit('c')))],['ab*','ab',true,Lit('a',LazyAny(Lit('b')))],['ab+c','ac',false,Lit('a',Plus(Lit('b'),Lit('c')))],['ab+c','abc',true,Lit('a',Plus(Lit('b'),Lit('c')))],['a(b|c)d','xabdy',true,Lit('a',Alt(Lit('b'),Lit('c'),Lit('d')))],['a(b|c)d','xabady',false,Lit('a',Alt(Lit('b'),Lit('c'),Lit('d')))],['ab?c','abc',true,Lit('a',Opt(Lit('b'),Lit('c')))],['ab?c','acc',true,Lit('a',Opt(Lit('b'),Lit('c')))],['ab?c','a',false,Lit('a',Opt(Lit('b'),Lit('c')))],["[abcd]",'a',true,CharClass([Lit('a'),Lit('b'),Lit('c'),Lit('d')])],["[abcd]",'ab',true,CharClass([Lit('a'),Lit('b'),Lit('c'),Lit('d')])],["[abcd]",'xhy',false,CharClass([Lit('a'),Lit('b'),Lit('c'),Lit('d')])],["c[abcd]",'c',false,Lit('c',CharClass([Lit('a'),Lit('b'),Lit('c'),Lit('d')]))],])('Regex base test ("%s" "%s" "%p")',(_pattern,text,expected,matcher)=>{constactual=matcher.match(text);expect(actual).toBe(expected);})});
顺便一提的是,这种相同的验证逻辑, 但是输入多个不同的参数以验证不同case的做法,叫做 Parameterized Test
对于表达式 a, 我们可以创建一个 Lit 类型的 token (为了便于理解,「创建」指创建一个 token, 然后插入到 output.)
对于表达式 a* 呢?我们可以先创建一个 Lit('a') 的 token, 当遇到 * 时,因为 * 表示匹配0至任意的前一个字符, 所以我们可以创建一个 Any 类型的 token, 然后把 output 最后一个元素 pop 出来,作为 Any 的 child 元素.
对于表达式 (ab), 情况就变得复杂一些:
当遇到 ( 括号的时候,我们可以创建一个 Group ,但是问题在于,我们不知道这个 Group 什么时候结束,即不知道什么时候才会遇上 ).
所以我们需要换种解决思路:当遇到 (, 创建一个 GroupStart 类型的 token, 然后再继续处理 a, b, 当遇到 ) 时,创建一个 Group 类型的 token, 然后一直调用 pop 函数直到把 GroupStart 也 pop 出来, 然后把过程中 pop 出来的 token 都当作是 Group 的 children 列表,而 GroupStart 相当于起到一个标记符的作用。
这种思路就自动处理了 (a*) 和 (a(b*)c) 的差异:
对于表达式 a|b, 我们是否可以参考 Any 的做法呢?
遇到 a 的时候先创建一个 Lit('a'), 遇到 | 时再创建一个 Alt, 然后把 Lit('a') 从 output pop 出来作为 left 节点, 再遇到下一个字符 b 的时候,再把 Alt 从 output pop 出来,把 b 作为 right 节点。
听起来没问题,但是上面的算法无法正确解析 a|b*, 它表示匹配一个 a 或者是任意数量的 b, 但是我们的做法会把它解析成 (a|b)*, 即任意数量的 a 或 b.
更合理的做法是先部分组装 Alt 的 left 节点,等解析完所有字符之后,再重新解析一次,把 right 节点给组装上。
以 a|b* 为例子:
创建一个 Lit('a') token
当遇到 | 的时候,创建一个 Alt, 并将 Lit('a') pop 出来作为 left 节点
创建一个 Lit('b') token
创建一个 Any token, 并将 Lit('b') pop 出来作为 child 节点.
当解析完所有字符后, 再遍历一次 output, 如果遇到 Alt token, 那么就把它的下一个 token (即 Any) 作为它的 right 节点.
exportconstparse=(text:string)=>{constresult:Token[]=[];constallTokens=tokenize(text);for(leti=0;i<allTokens.length;i+=1){consttoken=allTokens[i];constisLast=i===allTokens.length-1;handle(result,token,isLast);}returncompress(result);}consthandle=(result:Token[],token:Token,isLast:boolean)=>{if(token.kind===TokenKind.Lit){result.push(token);}elseif(token.kind===TokenKind.Start){assert(result.length===0,'Should not have start token after other tokens');result.push(token);}elseif(token.kind===TokenKind.End){assert(isLast,`Should not have end token before other tokens`);result.push(token);}elseif(token.kind===TokenKind.GroupStart){result.push(token);}elseif(token.kind===TokenKind.GroupEnd){result.push(groupEnd(result,token));}elseif(token.kind===TokenKind.Any){assert(result.length>0,`No Operand for '*' (location ${token.location})`);token.child=result.pop();result.push(token)}elseif(token.kind===TokenKind.Alt){assert(result.length>0,`No Operand for '|' (location ${token.location})`);token.left=result.pop();token.right=null;result.push(token)}else{assert(false,`UNIMPLEMENTED`);}}constgroupEnd=(result:Token[],token:Token):Token=>{constgroup:Token={kind:TokenKind.Group,location:null,end:token.location,children:[]};while(true){assert(result.length>0,`Unmatched end parenthesis (location ${token.location})`);constchild=result.pop();if(child.kind===TokenKind.GroupStart){group.location=child.location;break;}group.children.unshift(child);}returngroup;}// go through the output list to fill in the right side of Alts:
constcompress=(raw:Token[])=>{constcooked:Token[]=[];while(raw.length>0){consttoken=raw.pop();if(token.kind===TokenKind.Alt){assert(cooked.length>0,`No right operand for alt (location ${token.location})`);token.right=cooked.shift();}cooked.unshift(token);}returncooked;}
exportconstcompile=(text:string):RegexBase=>{consttokens:Token[]=parse(text);returncreateObjectByAST(tokens);}// return instances of classes derived from RegexBase by abstract syntax tree
constcreateObjectByAST=(tokens:Token[]):RegexBase|null=>{if(tokens.length===0){returnnull;}consttoken=tokens.shift();if(token.kind===TokenKind.Lit){returnLit(token.value,createObjectByAST(tokens));}elseif(token.kind===TokenKind.Start){returnStart(createObjectByAST(tokens));}elseif(token.kind===TokenKind.End){assert(tokens.length===0,`Should not have end token before other tokens`);returnEnd();}elseif(token.kind===TokenKind.Alt){returnAlt(createObjectByAST([token.left]),createObjectByAST([token.right]),createObjectByAST(tokens));}elseif(token.kind===TokenKind.Group){returnGroup(token.children.map((childToken)=>createObjectByAST([childToken])),createObjectByAST(tokens));}elseif(token.kind===TokenKind.Any){returnAny(createObjectByAST([token.child]),createObjectByAST(tokens));}else{assert(false,`UNKNOWN token type ${token.kind}`);}}
it.each([['a','a',true,Lit('a')],['^a','ab',true,Start(Lit('a'))],['a$','ab',false,Lit('a',End())],['a*','baac',true,Any(Lit('a'))],['ab+c','abc',true,Lit('a',Plus(Lit('b'),Lit('c')))],['ab+c','abxc',false,Lit('a',Plus(Lit('b'),Lit('c')))],['(ab)|(cd)','xaby',true,Alt(Group([Lit('a'),Lit('b')]),Group([Lit('c'),Lit('d')]))],['a(b|c)d','xabdy',true,Lit('a',Group([Alt(Lit('b'),Lit('c'))],Lit('d')))],['ab?c','ac',true,Lit('a',Opt(Lit('b'),Lit('c')))],['ab?c','acc',true,Lit('a',Opt(Lit('b'),Lit('c')))],["[abcd]c",'ac',true,CharClass([Lit('a'),Lit('b'),Lit('c'),Lit('d')],Lit('c'))],["c[abcd]",'c',false,Lit('c',CharClass([Lit('a'),Lit('b'),Lit('c'),Lit('d')]))],])('parse, compile and matcher test ("%s" "%s" "%p")',(pattern,text,expected,expectedMatcher)=>{constactualMatcher=compile(pattern);expect(actualMatcher).toStrictEqual(expectedMatcher);constactual=actualMatcher.match(text);expect(actual).toBe(expected);})
大功告成,终于将所有的功能都组装起来实现这个正则表达式引擎了, 除去前文提到的功能之外,还实现了 \* 转义特殊字符, [xya] 匹配 x, y, z 其中任意字符,以及 *? 实现惰性匹配的功能。