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亿部。同时,极光持续赋能开发者和传统行业客户,推出精准营销、金融风控、市场洞察、商业地理服务产品,致力于为社会和各行各业提高运营效率,优化决策制定。