极光小课堂 | PostCss浅析之词法分析

0

1

     前   言    

在前端日益发展的今天,抽象语法树(AST,Abstract Syntax Tree)已经成为了前端学者们耳熟能详的概念。趋于好奇心,在百忙之中窥探 PostCss 源码,好在代码量不多,消化整理了一下,望大牛们多多指教。

0

2

词法分析是什么

首先我们要搞清楚抽象语法树的解析流程,抽象语法树从形成一共经历了如下阶段:

字符流=>词法分析=>语法分析=>抽象语法树

是不是很简单?词法分析主要是读取字符流并根据词法规则组成一个个 Token,以便于语法分析阶段基于Token 流进行语法分析。

此处先拿 PostCss 的 String Token 来举例子,String 我们知道,其组成规则是两个成对的引号包裹起来的字符序列,词法分析阶段要做的是分析识别出当前读取的字符流是一个类型为 String 的 Token,并且其内容是匹配到的字符序列。这样语法分析器只要针对 Token 类型来判断是否合法,而不需要直接针对字符流来判断。

总结一下,词法分析要读取字符流并且提供Token的类型、内容,然后由语法分析通过读取 Token 流来判断当前 Token 组合是否合法,一切都检查通过后,最终由语法解析阶段生成抽象语法树(AST,Abstract Syntax Tree)

0

3

PostCss 词法分析

试想一下,除了前面我们提到的 String Token,还有哪儿些 Token 需要被解析出来?

我们不妨可以访问 https://astexplorer.net ,来辅助我们寻找答案。我们在该网站中,我们选中要解析的语言为 Css,并指定使用的解析库为 PostCss,并且输入以下内容:

.a {background: url(/*\\));

};.b {background: url("asdfasdf");

};

在前面我们说到,语法分析是通过 Token 流来判断合法。那我们试想一下,我们平时开发遇到过的 CSS 语法错误都有哪儿些?

我们随意地删掉一些内容,比如删掉最后一行的 } ,此时我们可以看到报错::1:1: Unclosed block,明眼人一看就明白:这个 { } 得成对出现,无论他们之间是否包裹了css代码,都需要去检查。

那我们粗略地得出结论:有一个Token,类型为brackets,其内容是被{}包裹起来的字符序列,在本例中就是 background: url(/*));

OK,这乍一看没啥问题,但是有个潜在问题,因为语法分析的对象是Token,负责判断Token组合是否合法,你将background: url(/*)); 作为Token brackets的一部分,那么其内容代码是否合法,语法解析器是不管的。

所以咱们得乖乖拆分出来,将上面代码分为三个部分:

1、名叫 { 的Token,其值和名字一样也是 {

2、other-tokens

3、名叫 } 的Token,其值和名字一样也是 }

这样才合理,当other-tokens存在语法错误时,语法分析器将会吐出错误。

那么我们是否可以推出字符串的解析也是分为这样的三个部分:

1、名叫 " 的Token,其值和名字一样也是 "

2、字符序列

3、名叫 " 的Token,其值和名字一样也是 "

其实不然,因为字符串里的内容不能由语法解析器去解析语法问题,即使你在里头写了错误的CSS代码也不管。所以需要在词法解析器阶段就专门设置一个Token,名字为String,这样就规避了语法解析器去检查字符串内的CSS语法问题了。

综合以上,我们可以猜测PostCss提供了如下token:

1、String2、{3、}4、(5、)6、[7、]

现在,我们在 https://astexplorer.net 额外输入代码后如下:

.a {background: url(/*\\));

};.b {background: url("asdfasdf");

};.c {background: url((();

};.d {background: url());

};.e {background: ()));

};.f {background: ((();

};

关于 Token:( )、[ ] 的存在,不必多说,因为( ),[ ] 内的语法需要语法解析器去检查语法,但是以上的代码报错位置却是 选择器 .f 的内容,难不成在此之前的代码语法都是对的吗?

其实 PostCss 还新增了一种 Token,叫做 brackets,它的生成条件如下:

1、以 url(开头,并且后面紧跟的字符不为特定的不可见字符并且不为单引号,双引号时,会生成 brackets Token

2、以(开头,并且直到匹配到)时的字符序列不符合正则 /.[/("'\n]/ 时,会生成一个 brackets

而生成一个 brackets 有什么效果?就是在语法解析阶段不会去检查 brackets 内容的语法,就不会报错了。

这就解释了上面的现象。

还有一些 Token,比如 space,at-word 等等,这里就不再赘述,这里列出所有 Token 后直接上代码解释:

  • space

  • string

  • at-word

  • word

  • comment

  • { } : ; ( )

  • brackets

0

4

PostCss Token 解析流程浅析

1

解析过程之 space

case NEWLINE:case SPACE:case TAB:case CR:case FEED:next = posdo {next += 1code = css.charCodeAt(next)if (code === NEWLINE) {

offset = nextline += 1}



} while (

code === SPACE ||code === NEWLINE ||code === TAB ||code === CR ||code === FEED

) {

currentToken = ['space', css.slice(pos, next)]

}



pos = next - 1break

这里的代码主要是将连续的 空格( ),制表符(\t),换行符(\n),回车符(\r),换页符(\f)转化为 space token,代码比较简单。

2

解析过程之 string

case SINGLE_QUOTE:

case DOUBLE_QUOTE:

quote = code === SINGLE_QUOTE ? '\'' : '"'next = posdo {

escaped = falsenext = css.indexOf(quote, next + 1)if (next === -1) {if (ignore || ignoreUnclosed) {next = pos + 1break} else {

unclosed('string')
}
}
escapePos = nextwhile (css.charCodeAt(escapePos - 1) === BACKSLASH) {

escapePos -= 1escaped = !escaped

}

} while (escaped)

content = css.slice(pos, next + 1)


lines = content.split('\n')last = lines.length - 1if (last > 0) {

nextLine = line + lastnextOffset = next - lines[last].length

} else {

nextLine = line

nextOffset = offset

}

currentToken = ['string', css.slice(pos, next + 1), line, pos - offset, nextLine, next - nextOffset]

offset = nextOffset

line = nextLinepos = nextbreak

这里可能有的理解困难点是针对转义字符的处理。代码如下:

do {escaped = falsenext = css.indexOf(quote, next + 1)if (next === -1) {if (ignore || ignoreUnclosed) {next = pos + 1break

} else {

unclosed('string')

}


}


escapePos = nextwhile (css.charCodeAt(escapePos - 1) === BACKSLASH) {

escapePos -= 1escaped = !escaped}


} while (escaped)

比如有字符串:

"你好,我是\\"小小前端攻城狮,我未来要成为\\"大大前端攻城狮" 外循环是不断寻找",内循环不断判断是否为转义引号,从而进行string token 的解析。

3

解析过程之 at-word

case AT:

RE_AT_END.lastIndex = pos + 1RE_AT_END.test(css)if (RE_AT_END.lastIndex === 0) {next = css.length - 1} else {next = RE_AT_END.lastIndex - 2}



currentToken = ['at-word', css.slice(pos, next + 1), line, pos - offset, line, next - offset]pos = nextbreak

其中:

const RE_AT_END = /[ \n\t\r\f{}()'";/[]#]/g

这里主要是通过@字符来解析出跟随在其后的name,比如

@import 解析出对应的Token为 ['at-word','import', line, pos - offset,line, next - offset]

4

解析过程之 word

case BACKSLASH:next = posescape = truewhile (css.charCodeAt(next + 1) === BACKSLASH) {next += 1escape = !escape

}



code = css.charCodeAt(next + 1)if (

escape &&

code !== SLASH &&

code !== SPACE &&

code !== NEWLINE &&

code !== TAB &&

code !== CR &&

code !== FEED

) {next += 1if (RE_HEX_ESCAPE.test(css.charAt(next))) {while (RE_HEX_ESCAPE.test(css.charAt(next + 1))) {next += 1}if (css.charCodeAt(next + 1) === SPACE) {next += 1}

}

}



currentToken = ['word', css.slice(pos, next + 1), line, pos - offset, line, next - offset]pos = nextbreakdefault:if (code === SLASH && css.charCodeAt(pos + 1) === ASTERISK) {

……

} else {

RE_WORD_END.lastIndex = pos + 1RE_WORD_END.test(css)if (RE_WORD_END.lastIndex === 0) {next = css.length - 1} else {next = RE_WORD_END.lastIndex - 2}



currentToken = ['word', css.slice(pos, next + 1),line, pos - offset,line, next - offset]

buffer.push(currentToken)pos = next}break

这里的 BACKSLASH 就是反斜杆\,以反斜杆开头的,有如下可能:

1、转义字符

2、转义序列

转义序列,就好比 Unicode 编码,就是转义序列。

这里的代码比较简单,不再赘述。

5

解析过程之 comment

default:if (code === SLASH && css.charCodeAt(pos + 1) === ASTERISK) {next = css.indexOf('*/', pos + 2) + 1if (next === 0) {if (ignore || ignoreUnclosed) {next = css.lengthelse {

unclosed('comment')

}

}

content = css.slice(pos, next + 1)

lines = content.split('\n')last = lines.length - 1if (last > 0) {

nextLine = line + lastnextOffset = next - lines[last].length

} else {

nextLine = line

nextOffset = offset

}


currentToken = ['comment', content, line, pos - offset, nextLine, next - nextOffset]

offset = nextOffset

line = nextLinepos = next} else {

……

}break;

对comment的解析,就是直接对 /* */ 进行匹配。

[ ] { } : ; )

case OPEN_SQUARE:case CLOSE_SQUARE:case OPEN_CURLY:case CLOSE_CURLY:case COLON:case SEMICOLON:case CLOSE_PARENTHESES:



let controlChar = String.fromCharCode(code)

currentToken = [controlChar, controlChar, line, pos - offset]break

对这类的处理就比较简单,直接将读到的字符作为Token返回给语法解析器。

6

解析过程之(和 brackets

case OPEN_PARENTHESES:



prev = buffer.length ? buffer.pop()[1] : ''n = css.charCodeAt(pos + 1)if (

prev === 'url' &&

n !== SINGLE_QUOTE && n !== DOUBLE_QUOTE &&

n !== SPACE && n !== NEWLINE && n !== TAB &&

n !== FEED && n !== CR

) {next = posdo {

escaped = falsenext = css.indexOf(')', next + 1)if (next === -1) {if (ignore || ignoreUnclosed) {next = posbreak} else {

unclosed('bracket')

}

}



escapePos = nextwhile (css.charCodeAt(escapePos - 1) === BACKSLASH) {

escapePos -= 1escaped = !escaped

}



} while (escaped)

currentToken = ['brackets', css.slice(pos, next + 1), line, pos - offset, line, next - offset]pos = next} else {next = css.indexOf(')', pos + 1)

content = css.slice(pos, next + 1)if (next === -1 || RE_BAD_BRACKET.test(content)) {

currentToken = ['(', '(', line, pos - offset]

} else {

currentToken = ['brackets', content,line, pos - offset,line, next - offset]pos = next}

}break

0

5

PostCss 总结

PostCss 将 括号归类为:普通括号、带有url前缀的括号 

普通括号:如果括号范围内的字符串符合正则 /.[\/("'\n]/ ,就将括号内的语法合法判断交给语法解析器,否则就生成一个 brackets token,这样就间接性规避了语法解析器去检查括号内的内容,这也就说明了为什么 background: (red)))))); 语法解析器认为是合法的。

带有url前缀的括号:括号内部语法检查只进行括号关闭校验,其他的统一默认合法,并生成一个 brackets token,这样语法解析器就不会解析你内部语法是否合法,即使你在括号里头输入 background: url(/\)); ,但是这种语法在普通括号里是非法的,比如  background: (/\));。

关于极光

极光(Aurora Mobile,纳斯达克股票代码:JG)成立于2011年,是中国领先的开发者服务提供商。极光专注于为移动应用开发者提供稳定高效的消息推送、即时通讯、统计分析、极光分享、短信、一键认证、深度链接等开发者服务。截止到2019年12月份,极光已经为超过50万移动开发者和145.2万款移动应用提供服务,其开发工具包(SDK)安装量累计336亿,月度独立活跃设备13.6亿部。同时,极光持续赋能开发者和传统行业客户,推出精准营销、金融风控、市场洞察、商业地理服务产品,致力于为社会和各行各业提高运营效率,优化决策制定。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值