Alibaba – Big Nail – Tianmo original article

Prettier is a tool for formatting code out of the box. Prettier is easy to configure, integrate, and expand. Prettier originally focused on Web development, but she did so well that the community used her extension mechanism to support formatting in languages like Java and PostgreSQL.

Prettier needless to say, it is necessary for development teams to use Prettier to ensure that code is written in a consistent style (not quite the same as a code specification) because code is run by machines but mostly read by people. And it’s not as easy to force a person to adopt a style he may not like as it is to get the tools to agree automatically.

Other content you can see the official documentation for more, but here is more.

Know the plugin

Prettier focuses primarily on Web development, so JavaScript, CSS, and HTML are supported by default, and even JSX and Vue are built in.

But obviously, if you invent a new DSL, Prettier doesn’t recognize it. So what? Write a plugin!

So plug-ins are a way for Prettier to support your own programming language. In essence, it is a normal JavaScript Module that exposes the following five modules:

As you can see, the following code is written in TypeScript.

// index.ts

import { SupportLanguage, Parser, Printer, SupportOption, ParserOptions } from 'prettier';

// List of supported languages
export const languages: Partial<SupportLanguage>[];

// Parser for each language
export const parsers: Record<string, Parser>;

// Core formatting logic
export const printers: Record<string, Printer>;

// Optional. PluginOptions need to be customized
export const options: Record<keyof PluginOptions, SupportOption>;

// Optional, default configuration item
export const defaultOptions: Partial<ParserOptions>;
Copy the code

Write a small program AXML plug-in

Ali small program (dingdingsmall program, Alipay small program, etc.) of the upper DSL has long been unified, but there has been no AXML automatic formatting tool.

Prettier is built-in for JS/TS,.acss is CSS

You might have tried formatting.axml as an XML file type, but it certainly didn’t work. JS expressions in AXML files cannot be formatted.

Let’s write a pretty simple AXML Prettier plug-in today.

languages

The parser list is [‘ AXML ‘] (A.1). This means that multiple parsers can be specified, but usually one is sufficient. We use axML as the parser (defined below).

Parser is a tool for parsing source code into an AST.

// index.ts

import { SupportLanguage } from 'prettier';

export const languages: Partial<SupportLanguage>[] = [
  {
    name: 'axml'.parsers: ['axml'].// (a.1)
    extensions: ['.axml'].// (a.2)},];Copy the code

parsers

Add export const parsers to index.ts.

// index.ts

import { SupportLanguage, Parser } from 'prettier';
import parse from './parse';

// Prettier specifies' node 'as any because different parsers return different types of nodes
function locStart(node: any) :number {
  return node.startIndex;
}

function locEnd(node: any) :number {
  return node.endIndex;
}

export const languages: Partial<SupportLanguage>[] = [
  {
    name: 'axml'.parsers: ['axml'].extensions: ['.axml'],},];export const parsers: Record<string, Parser> = {
  // Note that key must correspond to parsers of languages
  axml: {
    parse, // (b.1)
    locStart,
    locEnd,
    // Give the AST format a name, which will be used later
    astFormat: 'axml-ast',}};Copy the code

Parse (B.1) is a function, and before we can reveal it, we need to make sure AXML’s Parser is parsed.

There are a number of XmL-like DSLS on the market, so we used HTMLParser2 to parse AXML in line with the official implementation of applets. So parse (B.1) is defined as follows:

// parse.ts

import { parseDOM } from 'htmlparser2';
import { Node } from 'domhandler';

export default function parse(text: string) :Node[] {
  const dom = parseDOM(text, {
    xmlMode: true.withStartIndices: true.withEndIndices: true});return dom;
}
Copy the code

The AST parsed by HTMLParser2 is relatively simple. You can see it here to get a feel for it.

There is actually a thorny issue here, the “null attributes” in AXML (e.g.
=
(JSX syntax))
=
But in XML and htmlParser2 it is parsed as
. This requires our special treatment.

Now comes the core logic.

printers

// index.ts

import { SupportLanguage, Parser, Printer } from 'prettier';
import parse from './parse';
import print from './print';
import embed from './embed';

/ /... omit

export const printers: Record<string, Printer> = {
  // corresponds to astFormat in parsers
  'axml-ast': {
    print, // (c.1)
    embed, // (c.2)}};Copy the code

The print (C.1) function is responsible for the formatting logic of the target language source code itself, and the Embed (C.2) function is used to handle formatting of other languages embedded in the target language.

For AXML, there are only three types of AST parsed by HTMLParser2 (Node.type) :

  • tag– label,<view></view>, etc.
  • text– Text within the tag
  • comment– comments,<! -- -->, the same format as HTML comments

print

In print (c.1) :

// print.ts

import { FastPath, Doc, ParserOptions, doc } from 'prettier';

const { concat } = doc.builders;

export default function print(
  path: FastPath,
  _options: ParserOptions,
  _print: (path: FastPath) => Doc // (c.3)
) :Doc {

  // Get the node in the AST
  const node = path.getValue();

  if(! node)return ' ';
  
  Htmlparser2's AST is an array, so we need to call _print, which will recursively call our own print
  if (Array.isArray(node)) {
    return concat(path.map(_print));
  }
  
  // Continue to judge node.type, return different content, limited by space, omitted
}
Copy the code

For each formatted snippet of code, Prettier calls it a Doc (C.3).

Note that there are two places in AXML where JS expressions, tag attributes and text, exist in {{}}. These expressions also need formatting!

To process JS expressions in {{}}, you need to embed (c.2), where other Parses can be called within the embed function to process the target text (see usage below). Because it’s A JS expression, Prettier’s built-in Babel Parser is called to process the JS expression.

This requires us to parse {{}} first. The {{}} format is a very popular so-called mustache style, and for educational purposes we just go with mustache.

In fact, simply using mustache. Js can be problematic, because things like {{! A && b}} fragments like this have semantics in mustache. Js ({{! Note); But in AXML, it just means! A && b expression. We’re not going to expand it here. In addition, the applets framework implements a {{}} parser on its own.

embed

3, When Prettier is executed, embed (c.2) takes precedence over print (C.1) : If embed returns a non-null value, formatting ends; Instead, continue with the logic in print.

In Embed (c.2) :

// embed.ts

import { FastPath, Doc, ParserOptions, Options, doc } from 'prettier';
import { DataNode, Element } from 'domhandler';
import { parse } from 'mustache';

const {
  group,  // (D.1) Prettier the most basic method for Prettier, wrapping (or not wrapping) according to configuration items such as printWidth
  concat, // Concatenate Doc like array.prototype.concat
  line,   If the parent group(d.1) does not need a line break, it is converted to a space
  indent, // an indent is ignored if the parent group(d.1) does not need a newline
  softline, // a line break is ignored if the parent group(d.1) does not need a line break
} = doc.builders;

export default function embed(
  path: FastPath,
  print: (path: FastPath) => Doc,
  textToDoc: (text: string, options: Options) => Doc, // (d.2)
  options: ParserOptions // (d.3)
) :Doc | null {
  const node = path.getValue();

  Print (c.1) returns null
  if(! node || ! node.type)return null;

  switch (node.type) {
    // Text type
    case 'text':
      const text = (node as DataNode).data;
      // 1. Call mustache. Parse to parse text
      // 2. Call textToDoc(d.2) to format JS expressions (if any)
      // 3. Concatenate '{{', formatted expression,'}} '(if any)
      // 4. Call the group(d.1) method to wrap the previously concatenated content
    
    // Label type
    case 'tag':
      // 1. Call recursively if there are children
      // 2. Extract the attribute and call Mustache. Parse the text
      // 3. Call textToDoc(d.2) to format JS expressions (if any)
      // 4. Concatenate '{{', formatted expression,'}} '(if any)
      // 5. Call the group(D.1) method to wrap the previously concatenated content
    default:
      Print (c.1) returns null
      return null; }}Copy the code

In particular, the textToDoc (d.2) method can be used to parse JS expressions as follows:

// embed.ts

// ...

const doc: Doc = textToDoc(expressionExtractedByMustache, {
  parser: 'babel'.semi: false.singleQuote: true});return indent(concat([softline, doc]));
Copy the code

The options (D.3) parameters are some of the configuration items we specify, as well as custom configuration items (see below).

In addition, regarding methods such as group and Indent, I suggest you consult the documentation.

Style =”{{height: ‘100%’, width: ‘100%’}}” (in fact, all object attributes can be written simply like this), the text extracted from the braces is not a valid JS expression and requires special processing. Such details must be taken into account.

options

Export const options in index.ts specifies the custom configuration items supported by the plug-in.

Suppose we want the applets AXML plug-in to support an axmlBracketSameLine configuration item that acts like jsxBracketSameLine.

So it can be defined like this:

// index.ts

import { SupportLanguage, Parser, Printer, SupportOption, ParserOptions } from 'prettier';

/ /... omit

interface PluginOptions {
  axmlBracketSameLine: boolean;
}


// Plug-in custom configuration items
export const options: Record<keyof PluginOptions, SupportOption> = {
  axmlBracketSameLine: {
    name: 'axmlBracketSameLine'.category: 'Global'.type: 'boolean'.default: false.description: 'Put the `>` of a multiline AXML element on a new line',}};Copy the code

In this way, the above options (d. 3) in the parameters can be read to the options. The axmlBracketSameLine, to decide whether to open the end of the label character > placed in the same line.

defaultOptions

The default configuration item for a plug-in, which overrides Prettier’s default, can specify built-in and plug-in custom configuration items.

Such as:

// index.ts

import { SupportLanguage, Parser, Printer, SupportOption, ParserOptions } from 'prettier';

/ /... omit

export const defaultOptions: Partial<ParserOptions> = {
  tabWidth: 2.// 2 Spaces for indentation
  printWidth: 80.// Print width 80
};
Copy the code

At this point, our AXML plug-in is complete.

The use of plug-in

Prettier a plug-in is very simple to use, just publish our plug-in to NPM (or YARN, or a privatized NPM service like TNPM) with its package name beginning with the following character, Prettier automatically loads the plug-in, automatically identifies the file type and calls the plug-in:

  • @prettier/plugin-
  • prettier-plugin-
  • @<scope>/prettier-plugin-

Suppose we published the AXML plug-in for a small program called prettier-plugin-axml on NPM, then just install it in your project:

npm i --save-dev prettier prettier-plugin-axml
Copy the code

Then execute:

./node_modules/.bin/prettier --write "src/**/*.axml"
Copy the code

And you’re done.

Because we already specify the file suffix.axml in Extensions (A.2), Prettier automatically matches our plug-in for such files so we don’t explicitly specify plugin.

conclusion

To develop a Prettier plug-in, there are three steps:

  1. Parse source code into AST with one or more parsers;
  2. The API for calling Prettier adds newlines, Spaces, indentation, and so on as needed;
  3. Didn’t.

Is it easy?

Refer to the link

  • Prettier plugin documentation
  • prettier/plugin-xml

Finally, hire

The team I work for is the front end team of Dingding Education — yes, it is dingding education that the primary school students have received numerous five-star praise and had no choice but to get a certificate from STATION B.

We, make learning easier. We look forward to your excellence and work with us to make China’s education better!

Please send your resume directly to [email protected] or [email protected] and we will get back to you within one business day.