SASS简介
SASS是css的增强扩展,让开发能够使用variables, nested rules, mixins, functions等能力去书写css;
本篇目标
从零到1实现SASS编译器可以将sass语法转为css语法,展示基本的语言之间的编译转换如何完成
目标例子
输入:
$primary-color: #333;.test{ color: $primary-color; }复制代码
输出:
.test {color: #333; }复制代码
Step1:定义基本的 AST(Abstract Syntax Tree 抽象语法树) 结构,可以理解为node节点的JSON表达式
定义AST
这里的AST定义针对例子做了简化,先看后面的运用再回过头来看定义会更好理解些:
export const enum NodeTypes {TEXT: "TEXT",VARIABLE: "VARIABLE",SELECTOR: "SELECTOR",DECLARATION: "DECLARATION",RULE: "RULE",RootNode: "RootNode", }interface Node { [key: string]: anytype: NodeTypes }interface VariableNode extends Node{type: NodeTypes.VARIABLEvalue: string}interface TextNode extends Node {type: NodeTypes.TEXTvalue: string}interface SelectorNode extends Node {type: NodeTypes.SELECTORvalue: TextNode }export interface DeclarationStatement extends Node {type: NodeTypes.DECLARATIONleft: VariableNode | TextNoderight: VariableNode | TextNode }export interface RuleStatement extends Node {type: NodeTypes.RULEselector: SelectorNodechildren: DeclarationStatement[] }// RootNode 是最外层的节点类型export interface RootNode extends Node {type: NodeTypes.RootNodechildren: (RuleStatement | DeclarationStatement)[] }复制代码
源码跟AST的对应关系
根据以上的AST定义,需要解析出的节点JSON表达式应该如下所示:
$primary-color: #333;复制代码
需要parse成:
{"type": "DECLARATION","left": {"type": "VARIABLE","value": "$primary-color", },"right": {"type": "TEXT","value": "#333", } }复制代码
.test{ color: $primary-color; }复制代码
需要parse成:
{ "type": "RULE", "selector": {"type": "SELECTOR","value": { "type": "TEXT", "value": ".test", } }, "children": [ { "type": "DECLARATION", "left": {"type": "TEXT","value": "color", }, "right": {"type": "VARIABLE","value": "$primary-color", }, } ] }复制代码
Step2: sass字符串parse为目标 AST
目标: 实现如下的调用
let ast:RootNode = parse(lexical(input_stream(sass)))复制代码
实现input_stream函数读取输入字符串流:
function input_stream(input: string):InputStream{let offset = 0, line = 1, column = 1; return { next, peek, setCoordination, getCoordination, eof }function next():string {let ch = input.charAt(offset++);if (ch == "\n") line++, column = 1; else column++;return ch; }// 手动设置当前位置信息function setCoordination(coordination: Position) { offset = coordination.offset; line = coordination.line; column = coordination.column; }// 获取当前读取的位置function getCoordination() {return { offset, line, column } }// 预先读取下一个字符的内容,但是不做位置移动function peek():string {return input.charAt(offset); }function eof() {return peek() === ""; } }复制代码
实现lex函数将字符串流转为 token 流
export type Token = {type: Node['type']value: string}function lex(input: InputStream):TokenStream {return { next, peek, eof }function is_whitespace(ch) {return " \t\n".indexOf(ch) >= 0; }// Variable的可能标识function is_id_start(ch) {return /[$]/.test(ch); } // declaration的可能标识function is_assign_char(ch) {return ":".indexOf(ch) >= 0; }// 普通字符串读取function is_base_char(ch) {return /[a-z0-9_\.\#\@\%\-"'&\[\]]/i.test(ch); }// sass变量名限制function is_id_char_limit(ch) {return is_id_start(ch) || /[a-z0-9_-]/i.test(ch); }function read_assign_char():Token {return {type: NodeTypes.DECLARATION,value: input.next() } }function read_string():Token {/** * '#' end eg: * .icon-#{$size} {} */let str = read_end(/[,;{}():#\s]/);if (internalCallIdentifiers.includes(str)) {//possible internal urllet callStr = readInternalCall(str);return callStr; }return {type: NodeTypes.TEXT,value: str }; }// 根据条件限制读取消费掉尽可能多的字符function read_while(predicate) {let str = "";while (!input.eof() && predicate(input.peek())) str += input.next();return str; }// 产出变量 tokenfunction read_ident(): Token {let id = read_while(is_id_char_limit);return {type: NodeTypes.VARIABLE,value: id }; }// 读取下一个 token 并移动位置function read_next(): Token { // 跳过空白字符read_while(is_whitespace);if (is_assign_char(ch)) return read_assign_char();if (is_id_start(ch)) return read_ident();if (is_base_char(ch)) return read_string(); }//读取下一个 token,但是不改变读取游标信息,所以有先获取信息,读取token后还原位置信息function ll(n = 1): Token {let coordination = input.getCoordination()let tok = read_next(); input.setCoordination(coordination)return tok; }// 预测下一个 Token 类型function peek(n = 1): Token {return ll(n); }function next(): Token {return read_next(); } }复制代码
结语
以上是伪代码,实际会比这个复杂一些,比如还需要考虑很多:
- 节点位置信息,方便后续做source-map
- 节点所属文件信息,会有模块依赖关系整理
- ...
完整可运行的代码可以查看这里,还是比较通俗易懂的,覆盖了sass基本特性,以及编译基本流程:
- 词法分析
- 语法分析
- AST优化转换
- 源码生成(+sourceMap))
文章整理起来有点麻烦,需要对很多完整代码做删减,后续待完成。。。
最后祝大家????圣诞节快乐。。