做一个Emmet[html速写]【三】正则和设计

284 阅读5分钟

当翻看做一个Emmet[html速写]【一】的时候,总觉得直接写代码不合适。

这么多的规则,要如何整理,如何梳理。不管什么时候,代码必定要是有设计

从一个问题开始入手

将给到的div转成标签对,你会怎么做

要我的话就是用占位符替换

function tagCreate(str){
    let tag = str;
    return `<${tag}></${tag}>`
}
tagCreate("div") ==> `<div></div>`

简洁明了

那再提个问题,将给到的 #username 转换成标签对,并且把上面的函数利用起来,你要怎么做?

这个可以交给正则解决。

function tagCreate(str){
    let matched = str.match(/(?<tag>\w+)?(?:#(?<id>\w+))?/);
    let {tag='div',id=''} = matched.groups||{}
    
    return `<${tag} id="${id}"></${tag}>`
}
tagCreate("div") ==> `<div id=""></div>`
tagCreate("div#name") ==> `<div id="name"></div>`
tagCreate("#name") ==> `<div id="name"></div>`

看起来满足,但是不是很完美。id这个属性在任何时候都会有,不完美。

接下来,增加class的规则用 .classname 并且满足上述内容,你要怎么做

这个正则依旧可以解决:

function tagCreate(str){
    let matched = str.match(/(?<tag>\w+)?(?:#(?<id>\w+))?(?:\.(?<clazz>\w+))?(?:#(?<id2>\w+))?(?:\.(?<clazz2>\w+))?/);
    let {tag='div',id='',id2="",clazz="",clazz2=""} = matched.groups||{}
    id = id||id2;
    clazz = clazz||clazz2;
    return `<${tag} id="${id}" class="${clazz}"></${tag}>`
}
tagCreate("div") ==> `<div id="" class=""></div>`
tagCreate("div#name") ==> `<div id="name" class=""></div>`
tagCreate("#name") ==> `<div id="name" class=""></div>`
tagCreate("#name.classname") ==> `<div id="name" class="classname"></div>`

这里靠正则的穷举,可以组合成

  • tagidclass
  • tag idtag classclass idid class
  • tag id classtag class id

但是会显得稍微复杂点,如果接下来增加更多的内容,单纯的用正则靠不住了,这时候代码就需要一些设计,不能光靠平铺解决问题了

换一种思路

其他的先不多说,先贴代码上


var loaders = [
    {
        pattern: /^[a-zA-Z][a-zA-Z0-9]*/g,
        loader: tabLoader
    },
    {
        pattern: /(?<=#)\w+/g,
        loader: idLoader
    },
    {
        pattern: /(?<=\.)[\w-]+/g,
        loader: classLoader
    },
    {
        pattern: /(?<=\[).+?(?=\])/g,
        loader: attrLoader
    }
]


function tabLoader(resource, context) {
    context.tag = resource[0];
}

function idLoader(resource, context) {
    context.id = resource[0]
}

function classLoader(resource, context) {
    context.classes = [...resource]
}

function attrLoader(resource, context) {
    context.attrs = resource.reduce((attrs, attr) => {
        if (/^\w+$/.test(attr)) {
            // [attr]
            attrs.push([attr, ""])
        } else {
            // [attr=value attr='value' attr="value"]
            (attr + " ").match(/\w+=('|")?[\w\s]+(\1)(?=\s)/g).forEach((eachattr) => {
                let [key, value] = eachattr.split("=");
                attrs.push([key, value.replace(/^('|")(.*)(\1)$/, "$2")])
            })
        }
        return attrs;
    }, [])
}

function create(source) {
    let context = {
        input: source
    }
    loaders.forEach(({ pattern, loader }) => {
        let resource = source.match(pattern);
        resource && loader(resource, context, { pattern });
    });
    return context
}


let div = create("div.class#id.value[value=key value2=key2]")


console.log(div)



--->

{
  input: 'div.class#id.value[value=key value2=key2]',
  tag: 'div',
  id: 'id',
  classes: [ 'class', 'value' ],
  attrs: [ [ 'value', 'key' ], [ 'value2', 'key2' ] ]
}

这么一大段代码,相信很难让人看完,那我就先一部分一部分拆开看吧

var loaders = []
function create(){}

这是这段代码的核心部分。 create是代码的入口,loaders是代码的处理器。主要的处理过程是


loaders.forEach(({ pattern, loader }) => {
    let resource = source.match(pattern);
    resource && loader(resource, context, { pattern });
});

loaders是一个数组,里面放的是每一个类型的处理方法,用正则去捕获内容,将捕获结果用来处理。loader有两部分,第一部分是 pattern处理的匹配工作,第二部分是处理方法,用来对匹配的结果做相应的处理。

loader的解读

1. tag 处理

tag使用的正则表达式为/^[a-zA-Z][a-zA-Z0-9]*/g,满足所需的英文首字母开头并且后续跟随的是英文和数字的任意组合,比如 divh1,如果不严谨的表示方法可以直接用\w替换掉

2. #ID 处理

很多人做这种匹配,第一眼应该是顺位逻辑,从#开始查询后续的规则,一般会写成 /#\w+/的格式,查询的是 #ID的整个内容,这么写也没有错,不过我这里换了一种思路,id查询的是ID前面限定跟随的是 #存在的纯单词内容。所以只要保留id本身的内容即可,并不需要#,不然还是要二次处理

所以写为/(?<=#)\w+/g

3. .class 处理

class的处理思路和ID一样,没有什么区别,唯一需要区别的是id只有一个,而class可以有多个,并且在class命名规则上,除了常见的字符,还包括-,因此正则表达式为/(?<=\.)[\w-]+/g

4. attrs

属性相对于其他来说,稍微复杂些,从处理的逻辑上就能看出来

function attrLoader(resource, context) {
    context.attrs = resource.reduce((attrs, attr) => {
        if (/^\w+$/.test(attr)) {
            // [attr]
            attrs.push([attr, ""])
        } else {
            // [attr=value attr='value' attr="value"]
            attr.match(/\w+?=('|")?[\w\s]+?(\1)/g).forEach((eachattr) => {
                let [key, value] = eachattr.split("=");
                attrs.push([key, value.replace(/^('|")(.*)(\1)$/,"$2")])
            })
        }
        return attrs;
    }, [])
}

属性支持以下几种方式,

  • [attr] 只有属性名
  • [attr=value] 属性和值
  • [attr='value'] 属性带单引号
  • [attr="value"] 属性带双引号
  • [attr=value attr2=value2] 多属性

看起来简单,但是想要处理好就需要费一些心思了。这么看,这里只有第一种没有带 =,只需要一个简单的正则判断即可/^\w+$/优先把第一种处理掉。接下来需要想一个方案,可以同时处理其他集中。key值都是可以相同的 \w+

但是呢value就不太确定了, 所以前面的内容就可以确定了 \w+=,接下来,234都是有一个共同点,不带引号,带单引号,带双引号。那么可以推断出,他们都是由共同的组成的,value的前面带什么,value就会以什么而结尾。这里就需要用到分组的反向引用了,也是之前我提过的内容,现学现用谁的教程会比我优秀🤗。('|")?带上问号表示可能存在,那么后面就可以直接做\1分组引用,前面匹配到空那么\1必定也是空,正好同步。

所以就差最后一种了,一个括号内带多个k=v的内容怎么办。

莫慌,先从句式上分析,假如我们以空格作为分界线,attr=value attr2=value2这种显然不符合重复的要求。不够重复就不能用正则查询到,因为句式不同,如果我们直接以空格做切割,那又会引发如果value中间带空格的问题,就比较难处理。所以,我们首先要做的是句式匹配,将句尾增加一个空格,attr=value attr2=value2 这样句式就变成了 K=V 的模式,格式重复了,那么用正则查找重复项不久简单了。

所以又稍微修改了以下,完整的匹配为/\w+=('|")?[\w\s]+(\1)(?=\s)/g

最后,对分割出的K=V的每一项又做了一次处理,将value的值重新切割一下,去掉了所有的'"部分,只保留原始值。

说到这儿还没说怎么匹配上[]呢把,先看看格式,中括号的匹配有两种,第一种单纯的 [K=V]第二种多种[K=V][K=V]的结合。那我们该怎么做?当然是按照 []拆分开了。但是拆的时候会发现,第一个[包裹的是最后一个],中间的]被忽略了,这时候还记不记得,曾经提到的贪婪非贪婪,只要第一次满足就不再继续向后查询的特点?就是利用这个完成操作,限定量词的行为。

所以就诞生了这个匹配条件:/(?<=\[).+?(?=\])/g

修正✅

代码在编写的时候,总是不完整的,就比如tag的处理,如果没有tag,那么如何保证默认tag的存在?靠后期补救还是在前期直接做好?都是编写的过程中不断思考的问题。这里就重新将loader调整一下匹配规则,让tag无论如何都是存在的

var tagPattern = /^([a-zA-Z][a-zA-Z0-9]+)?/g;
function tagLoader(resource,context){
  context.tag = resource[0]||'div'
}

以上保证了默认的标签,即便 #id的形式也可以补充为 div#id的方式

结语

这样一通下来,是不是豁然开朗,问题迎刃而解,抽丝剥茧的思考逻辑才是我们应该掌握的,至于你会什么代码,用什么框架,本质上来说都不重要,不是吗?

谢谢😎