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();
    }

}复制代码

结语

以上是伪代码,实际会比这个复杂一些,比如还需要考虑很多:

  1. 节点位置信息,方便后续做source-map
  2. 节点所属文件信息,会有模块依赖关系整理
  3. ...

完整可运行的代码可以查看这里,还是比较通俗易懂的,覆盖了sass基本特性,以及编译基本流程:

  1. 词法分析
  2. 语法分析
  3. AST优化转换
  4. 源码生成(+sourceMap))

文章整理起来有点麻烦,需要对很多完整代码做删减,后续待完成。。。

最后祝大家????圣诞节快乐。。