最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 从零到1写一个简单的sass编译器(第一篇)

    正文概述 掘金(wizardpisces)   2020-12-25   521

    目标

    输入:

    $primary-color: #333;
    .test{
      color: $primary-color;
    }
    

    输出:

    .test {
        color: #333;
    }
    

    Step1:定义基本的 AST 结构

    定义AST

    这里的AST定义针对例子做了简化,先看后面的运用再回过头来看定义会更好理解些:

    export const enum NodeTypes {
        TEXT: "TEXT",
        VARIABLE: "VARIABLE",
        SELECTOR: "SELECTOR",
        DECLARATION: "DECLARATION",
        RULE: "RULE",
        RootNode: "RootNode",
    }
    
    interface Node {
        [key: string]: any
        type: NodeTypes
    }
    
    interface VariableNode extends Node{
        type: NodeTypes.VARIABLE
        value: string
    }
    
    interface TextNode extends Node {
        type: NodeTypes.TEXT
        value: string
    }
    
    interface SelectorNode extends Node {
        type: NodeTypes.SELECTOR
        value: TextNode
    }
    
    export interface DeclarationStatement extends Node {
        type: NodeTypes.DECLARATION
        left: VariableNode | TextNode
        right: VariableNode | TextNode
    }
    
    export interface RuleStatement extends Node {
        type: NodeTypes.RULE
        selector: SelectorNode
        children: DeclarationStatement[]
    }
    
    // RootNode 是最外层的节点类型
    export interface RootNode extends Node {
        type: NodeTypes.RootNode
        children: (RuleStatement | DeclarationStatement)[]
    }
    
    

    源码跟AST的对应关系

    根据以上的AST定义,例子解析出的AST目标:

    $primary-color: #333;
    

    需要翻译成:

    {
        "type": "DECLARATION",
        "left": {
            "type": "VARIABLE",
            "value": "$primary-color",
        },
        "right": {
            "type": "TEXT",
            "value": "#333",
        }
    }
    

    .test{
      color: $primary-color;
    }
    

    需要翻译成:

    {
          "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字符串读取成为目标 AST

    目标: 实现如下的调用

    let ast:RootNode = parse(lexical(input_stream(scss)))
    

    实现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 url
                let 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;
        }
    
        // 产出变量 token
        function 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))

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

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


    起源地下载网 » 从零到1写一个简单的sass编译器(第一篇)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元