SASS profile

SASS is an enhanced CSS extension that allows developers to write CSS using variables, mixins, functions, and other capabilities.

This goal

Implementing the SASS compiler from zero to one (converting SASS to CSS syntax), this series of articles will follow the following flow:

code -> AST(Abstract Syntax Tree) -> transformed AST -> transformed code

Target example

Input:

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

Output:

.test {
    color: # 333;
}
Copy the code

Step1: Define the basic AST structure, which can be understood as a JSON expression for a Node node

Define the AST

The AST definition here is simplified for the example, but it will be easier to see how it is used and then come back to the definition:

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 is the outermost node type
export interface RootNode extends Node {
    type: NodeTypes.RootNode
    children: (RuleStatement | DeclarationStatement)[]
}

Copy the code

Source code and AST correspondence

Based on the AST definition above, the node JSON expression to be parsed should look like this:

$primary-color: # 333;
Copy the code

Need to parse into:

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

.test{
  color: $primary-color;
}
Copy the code

Need to parse into:

{
      "type": "RULE"."selector": {
        "type": "SELECTOR"."value": {
          "type": "TEXT"."value": ".test",}},"children": [{"type": "DECLARATION"."left": {
            "type": "TEXT"."value": "color",},"right":  {
            "type": "VARIABLE"."value": "$primary-color",},}]}Copy the code

Step2: sass The string parse is the target AST

Goal: Implement the following call

let ast:RootNode = parse(lexical(input_stream(sass)))
Copy the code

implementationinput_streamThe function reads the stream of input strings:

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;
    }
    // Manually set the current location information
    function setCoordination(coordination: Position) {
        offset = coordination.offset;
        line = coordination.line;
        column = coordination.column;
    }

    // Get the current reading position
    function getCoordination() {
        return {
            offset,
            line,
            column
        }
    }

    // Preread the contents of the next character, but do not move the position
    function peek() :string {
        return input.charAt(offset);
    }
    function eof() {
        return peek() === ""; }}Copy the code

implementationlexThe function streams the string into a token stream

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

    // A possible identifier for Variable
    function is_id_start(ch) {
        return / [$].test(ch);
    }

  
    // Possible identifiers of the declaration
    function is_assign_char(ch) {
        return ":".indexOf(ch) >= 0;
    }

    // Ordinary string reading
    function is_base_char(ch) {
        return /[a-z0-9_\.\#\@\%\-"'&\[\]]/i.test(ch);
    }

    // sass variable name restriction
    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
        };
    }

    // Consume as many characters as possible according to the condition
    function read_while(predicate) {
        let str = "";
        while(! input.eof() && predicate(input.peek())) str += input.next();return str;
    }

    // output variable token
    function read_ident() :Token {
        let id = read_while(is_id_char_limit);
        return {
            type: NodeTypes.VARIABLE,
            value: id
        };
    }

    // Reads the next token and moves the location
    function read_next() :Token {
         // Skip whitespace characters
        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();
    }

    // Read the next token, but do not change the read cursor information, so first get the information, read the token after restore location information
    function ll(n = 1) :Token {
        let coordination = input.getCoordination()
        let tok = read_next();
        input.setCoordination(coordination)
        return tok;
    }

    // Predict the next Token type
    function peek(n = 1) :Token {
        return ll(n);
    }

    function next() :Token {
        returnread_next(); }}Copy the code

Result of character stream converting Token stream:

{ type: 'VARIABLE'.value: '$primary-color' }
{ type: 'DECLARATION'.value: ':' }
{ type: 'TEXT'.value: '# 333' }
{ type: 'PUNC'.value: '; ' }
{ type: 'TEXT'.value: '.test' }
{ type: 'PUNC'.value: '{' }
{ type: 'TEXT'.value: 'color' }
{ type: 'DECLARATION'.value: ':' }
{ type: 'VARIABLE'.value: '$primary-color' }
{ type: 'PUNC'.value: '; ' }
{ type: 'PUNC'.value: '} ' }
Copy the code

It can be seen that the token is usually represented by a tuple similar to <type, value>, where type represents a token type and value is an attribute value (usually a source-related string).

implementationparseFunction willTokenFlow to AST syntax tree:

Token stream to the AST(Abstract Syntax Tree) Syntax Tree generation, you can experience the mapping between various source code and AST in astExplorer


function parse(input: LexicalStream) {

    function delimited(start: puncType, stop: puncType, separator: puncType, parser: Function) {// FIFO
        let statements: any[] = [], first = true;

        skipPunc(start);

        while(! input.eof()) {if (isPuncToken(stop)) break;
            if (first) {
                first = false;
            } else {
                if (separator === '; ') {
                    skipPuncSilent(separator)
                } else{ skipPunc(separator); }}if (isPuncToken(stop)) break;

            statements.push(parser());
        }
        skipPunc(stop);

        return statements;
    }

    // Token parse distribution

    function dispatchParser() {
        // predict Determines the next Token type
        let tok = input.peek();

        // VARIABLE returns the Token as part of the syntax tree.
        if (tok.type === NodeTypes.VARIABLE) {
            return input.next();
        }

        / / same as above
        if (tok.type === NodeTypes.PUNC) {
            return input.next()
        }

        if (tok.type === NodeTypes.TEXT) {
            return input.next()
        }
    }

    // Parse the DECLARATION node
    function parseDeclaration(left: DeclarationStatement['left']) :DeclarationStatement {
        input.next(); // skip ':'
        return {
            type: NodeTypes.DECLARATION,
            left: left,
            // Read Text value
            right: input.next()
        }
    }

    // Parse the RULE node
    function parseRule(selector: RuleStatement['selector']) :RuleStatement {
        let children = delimited("{"."}".";", parseStatement);
        return {
            type: NodeTypes.RULE,
            selector,
            children
        }
    }

    // Determine the AST Node type by predicting the next Token type
    function maybeDeclaration(exp) {
        let expr = exp();
         if (isAssignToken()) {
            if (expr.type === NodeTypes.VARIABLE) {
                return parseDeclaration(expr)
            }
         }
        if (isPuncToken('{')) {
            return parseRule({
                type: NodeTypes.SELECTOR,
                value: expr
            }) //passin selector
        }

        return expr;
    }

    // Parser for the underlying Statement node
    function parseStatement() {
        return maybeDeclaration(function () {
            return dispatchParser()
        })
    }

    // For children in the parse entry, see the RootNode node definition in the previous article
    function parsechildren() :Statement[] {
        let children: Statement[] = [];
        while(! isEnd()) { children.push(parseStatement()); skipPuncSilent(";");
        }
        return children
    }

    // Parser entry
    function parseProgram() :RootNode {
        return {
            type: NodeTypes.RootNode,
            children: parsechildren()
        }
    }
    return parseProgram()
}
Copy the code

Parse’s source code (SASS source code) corresponds to the abstract syntax tree as follows:

{
  "type": "RootNode"."children": [{"type": "DECLARATION"."left": {
        "type": "VARIABLE"."value": "$primary-color"
      },
      "right": {
        "type": "TEXT"."value": "# 333"}}, {"type": "RULE"."selector": {
        "type": "SELECTOR"."value": {
          "type": "TEXT"."value": ".test"}},"children": [{"type": "DECLARATION"."left": {
            "type": "TEXT"."value": "color"
          },
          "right": {
            "value": {
              "type": "VARIABLE"."value": "$primary-color"}}}]}Copy the code

You can see that it is a combination of the AST nodes that were originally defined

conclusion

The above is pseudocode, which is actually a bit more complicated than this, for example, there are a lot of considerations:

  1. Node location information is stored to facilitate source-map
  2. The file information of the node will be sorted by module dependencies
  3. .

The source code to view

Current project functions:

Sass features:

  1. Variables
  2. Nesting
  3. Extend/Inheritance
  4. Operators
  5. Mixins
  6. Modules

Compilation process:

  1. Lexical analysis
  2. Syntax analysis
  3. AST optimized conversion
  4. Source code generation (+sourceMap)

Finally I wish everyone a happy New Year’s day.

Transform transforms the AST associated with source code (SASS) into the AST associated with object code (CSS)

The original address