• Column address: Front-end compilation and engineering
  • Jouryjc

The previous article we understand the basic use of PEg. js, forget the children’s shoes suggested review, for this article’s edible effect will be better!

All talk and no action is worth learning. So this article implements a compiler (muck, toy, joy).

demand

We know that Vue’s template does not support Chinese tag names, as in this code:

< = "tomato" drop-down box to select values: data = "{" a list" : [{" name ":" πŸ… ", "id" : "tomatoes"}, {" name ":" 🍌 ", "id" : "banana"}], "total" : 2}"> < subcomponent ></ subcomponent ></ drop-down box >Copy the code

Results generated using AstExplorer:

As you can see, vue-template-Compiler recognizes the < drop-down box > component as text. The requirement is to compile the above code into the same AST as the other components. Let’s take a look at the component AST compiled correctly:

We focus on the four attributes type, Tag, children, and attrs, and the other fields are additional information. Since this article focuses on compilation logic, other fields can be added based on peg.js actions, so we won’t go into details.

Let’s implement zh-template-Compiler in the figure above.

Analysis of the

Based on the above requirements, we can analyze and get the morphology and grammar we need to identify:

  • Correctly identify component parent-child relationships; invue2Template compilation, through the re and stack to maintain the relationship between the start tag and the end tag, no contact with children can go toTemplate compilationUnderstanding.PEG.jsCan be directly through the rules to match;
  • Component property matching; Be able to place thepropsIdentify aastIn thename ε’Œ valueAnd can distinguish between static and dynamic properties (v-bind); For complex typesvalueObjects, for example, that are expected to behave better than just strings;
  • The component name and property name can only contain Chinese;

The test case

We tend to use single tests to understand the smallest and most fine-grained features of a framework, and the same goes for combing a scene.

For the first requirement of the above analysis, we can write the following use cases (πŸ˜‰ self-closing component logic is not handled oh, interested children can fork the project to practice) :

const { parse } = require('.. /src/zh-template-compiler')

describe('zh-template-compiler'.() = > {
  test('Component without attributes'.() = > {
    const template = '< component >
      '
    const ast = parse(template)

    expect(ast.attrs.length).toBe(0)
    expect(ast.children.length).toBe(0)
    expect(ast.type).toBe(1)
    expect(ast.tag).toBe('components')
  })
  
  test('Include child components'.() = > {
    const template = '< component >< subcomponent >
      
      '
    const ast = parse(template)

    expect(ast).toMatchSnapshot()
  })

  test('Contains multiple child components'.() = > {
    const template = '< component >< first sub-component >
      
      
      
      '
    const ast = parse(template)

    expect(ast).toMatchSnapshot()
  })

  test('Contains multiple layers of child components'.() = > {
    const template = '< component >< subcomponent >< grandchild component >
      
      
      '
    const ast = parse(template)

    expect(ast).toMatchSnapshot()
  })
})
Copy the code

The second requirement is to identify props and distinguish between static and dynamic:

describe('zh-template-compiler'.() = > {
  / /... Follow the above code
  
  test('Components with static properties'.() = > {
    const template = '< component properties =" value ">
      '
    const ast = parse(template)

    const attr = ast.attrs
    expect(attr.length).toBe(1)
    expect(attr[0]).toEqual({
      isBind: false.name: "Properties".value: "Value"
    })
  })

  test('Components with dynamic properties'.() = > {
    const template = '< component: Property =" value ">
      '
    const ast = parse(template)

    const attr = ast.attrs
    expect(attr.length).toBe(1)
    expect(attr[0]).toEqual({
      isBind: true.name: "Properties".value: "Value"
    })
  })

  test('Complex attribute values'.() = > {
    const template = ` < = "tomato" drop-down box to select values: data = "{" a list" : [{" name ":" πŸ… ", "id" : "tomatoes"}, {" name ":" 🍌 ", "id" : "banana"}], "total" : 2}"> < subcomponent >
      
       '

    const ast = parse(template)
    expect(ast).toMatchSnapshot()
  })

  test('Components with static + dynamic properties'.() = > {
    const template = '< component static property =" value of static property ": Dynamic property =" value of dynamic property ">
      '
    const ast = parse(template)

    const attrs = ast.attrs
    expect(attrs.length).toBe(2)
    expect(attrs).toEqual([{
      isBind: false.name: "Static properties".value: "Static property values"
    }, {
      isBind: true.name: "Dynamic properties".value: "Values of dynamic properties"})})})Copy the code

Finally, component and attribute names that can only contain Chinese use cases are simpler:

describe('zh-template-compiler'.() = > {
  / /... Follow the above code
  
  test('Component name can contain only Chinese characters'.() = > {
    const template = '< component 1>
      '

    try {
      parse(template)
    } catch (e) {
      console.log(e)
      expect(e.message).toBe('Expected ":", ">", or [δΈ€-ιΎ₯] but "1" found.')
    }
  })

  test('Attribute names can contain only Chinese characters'.() = > {
    const template = '< component property 1=" value 1">
      '

    try {
      parse(template)
    } catch (e) {
      console.log(e)
      expect(e.message).toBe('Expected '=' or [0-9] but '1' found.')}})})Copy the code

coding

Start by writing entry rules:

Program
 = program:Tag {
 	return program;
 }
Copy the code

Remember that –allowed-start-rules is configured earlier. If not, the first rule is implemented by default. Next comes the core rule definition:

// A complete template definition
// ws is a whitespace. You can type as many whitespace characters as you like before starting the tag
// StartTag, StartTag matching
// children: (Tag*) key, key, key!! Repeatedly matches the Tag rule.
// EndTag, EndTag matching
// The last action is the key, you can do any judgment, formatting when you get the matching information
// If the component name does not match the start tag and end tag, an error must be reported. Vue2 maintains this relationship through the stack, and you can see that peg.js is much simpler.
Tag
 = ws
 start:StartTag
 children: (Tag*)
 end:EndTag
 ws
 {
   if(start.tag ! == end.tag) {throw Error('Start tag and end tag do not match')}return {
     ...start,
     children
   }
 }

// Start tags and attributes
// Component :$zh = [u4e00-\u9fa5]+ Matches more than one Chinese character. If you don't add the $, you get an array of matches. If you forget this grammar, go back to the previous chapter
// attrsList: (...) * Matches any attR and stores attrList
// attrs: attrs matches individual component properties
// Finally, the action process returns an object with type = 1, the same as VNode in VUe2, representing the component type
StartTag
 = "<"
   ws
   component:$zh
   attrList: (
   	 ws
     attrs:Attrs
     ws
     {
       if (attrs.name) {
         return attrs
       }
     }
   )*
   ">" {
     return {
       type: 1.tag: component,
       attrs: attrList
     }
   }

// End tag
EndTag
 = "< /"
 component:$zh
 ">"
 {
   return {
     tag: component
   }
 }

// Matches Chinese characters
zh = [\u4e00-\u9fa5]+

// Matches the component properties
// isBind:name_separator ? Matches: returns the corresponding string, and then returns NULL
// attrName:$zh+ The attribute name is a String in Chinese
/ / quotation_mark quotes
// attrValue:($zh/JSON_text) the value of the attrValue attribute can be a Chinese or a JSON text. JSON_text uses the definition in the previous article.
// When all matches are complete, return the matching object
Attrs
 = isBind:name_separator ?
 attrName:$zh+
 "="
 quotation_mark
 attrValue:(
   $zh / JSON_text
 )
 quotation_mark {
   if (attrName) {
     let hasVbind = isBind ? true : false
     return {
       isBind: hasVbind,
       name: attrName,
       value: attrValue
     }
   } 
 }
Copy the code

The core rule definition is as shown in the above code, the difficulty is to parse the sub-components, by using the idea of rule recursion (similar to function recursion) to solve it becomes so easy.

validation

Finally, generate a compiler with the above rules:

npx pegjs -o zh-template-compiler.js src/zh-template-compiler.pegjs
Copy the code

🌰 at the beginning of the article produces the following AST results:

{
   "type": 1."tag": "Drop-down box"."attrs": [{"isBind": false."name": "Selected value"."value": "Tomato"
      },
      {
         "isBind": true."name": "Data"."value": {
            "list": [{"Name": "πŸ…"."id": "Tomato"
               },
               {
                  "Name": "🍌"."id": "Banana"}]."total": 2}}]."children": [{"type": 1."tag": "Subcomponent"."attrs": []."children": []}]}Copy the code

The result of executing the test case is shown below:

The simplest Chinese template compiler is complete. With this exercise, you will become more familiar with the basics of PEg.js and will be able to use it to solve everyday development problems. If you want to further refine the compiler, fork the zh-template-compiler

The next article will generate actual drop-down boxes on the page based on AST results. What would you do?

Pay attention to our

Everyone’s support is our motivation to continue to move forward, come to pay attention to our deep trust in the front end team ~

In the meantime, if you are interested, please join us and send your resume to [email protected].