Skip to content

Commit

Permalink
feat: support wxinterpolation (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
iChenLei authored Oct 21, 2021
1 parent b7189fd commit 1acb326
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 6 deletions.
19 changes: 19 additions & 0 deletions src/ast/build-ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
parseInlineJS,
convertLexerErrorToNode,
convertParseErrorToNode,
sortTokenChildren,
} from "./util";

type ICtx = Record<string, CstNode[]>;
Expand Down Expand Up @@ -76,6 +77,7 @@ class CstToAstVisitor extends BaseWxmlCstVisitor {
};
astNode.startTag = {
type: "WXStartTag",
name: "wxs",
attributes: ctx.attribute
? map(ctx.attribute, this.visit.bind(this))
: [],
Expand Down Expand Up @@ -145,6 +147,22 @@ class CstToAstVisitor extends BaseWxmlCstVisitor {
return astNode;
}

/**
* AST - WXInterpolation
*/
interpolation(ctx, { location }) {
const child = sortTokenChildren(ctx);
// @ts-expect-error
const value = (child || []).map((token) => token.image).join("");
const astNode = {
type: "WXInterpolation",
rawValue: value,
value: value.replace(/^{{/, "").replace(/}}$/, ""),
};
mergeLocation(astNode, location);
return astNode;
}

content(ctx) {
// sort child node first
const child = sortCstChildren(ctx);
Expand Down Expand Up @@ -173,6 +191,7 @@ class CstToAstVisitor extends BaseWxmlCstVisitor {
if (ctx.OPEN?.[0] && (ctx.START_CLOSE?.[0] || ctx.SLASH_CLOSE?.[0])) {
astNode.startTag = {
type: "WXStartTag",
name: astNode.name,
attributes: ctx.attribute
? map(ctx.attribute, this.visit.bind(this))
: [],
Expand Down
26 changes: 25 additions & 1 deletion src/ast/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,29 @@ interface IEspreeError {
stack: string;
}

/**
* sort token children
*/
export function sortTokenChildren(tokensArr) {
let tokens: CstNodeLocation[] = [];
let sortedTokens: CstNodeLocation[] = [];
Object.keys(tokensArr).forEach((key) => {
tokens.push(...tokensArr[key]);
});
sortedTokens = tokens.sort((nodeA, nodeB) => {
if (
nodeA.startLine > nodeB.startLine ||
(nodeA.startLine === nodeB.startLine &&
nodeA.startColumn > nodeB.startColumn)
) {
return 1;
} else {
return -1;
}
});
return sortedTokens;
}

/**
* sort cst children
*/
Expand Down Expand Up @@ -100,7 +123,8 @@ export function parseInlineJS(astNode): void {
});
espreeAst.type = "WXScriptProgram";
espreeAst.offset = [];
astNode.body = espreeAst;
// https://github.com/estree/estree/blob/master/es2015.md#programs
astNode.body = [espreeAst];
} catch (e) {
// IEspreeError
const error = e as IEspreeError;
Expand Down
18 changes: 16 additions & 2 deletions src/cst/lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ const INVALID_OPEN_INSIDE = createToken({
categories: [OPEN],
});

const TEXT = createToken({ name: "TEXT", pattern: /[^<]+/ });
const TEXT = createToken({ name: "TEXT", pattern: /((?!(<|{{)).)+/ });

const INTPN = createToken({ name: "INTPN", pattern: /((?!('|"|}})).)+/ });

const WXS_TEXT = createToken({
name: "WXS_TEXT",
Expand Down Expand Up @@ -111,11 +113,13 @@ const STRING = createToken({
const MUSTACHE_LEFT = createToken({
name: "MUSTACHE_LEFT",
pattern: /\{\{/,
push_mode: "INTPN_INSIDE",
});

const MUSTACHE_RIGHT = createToken({
name: "MUSTACHE_RIGHT",
pattern: /\}\}/,
pop_mode: true,
});

const EQUALS = createToken({ name: "EQUALS", pattern: /=/ });
Expand All @@ -132,7 +136,17 @@ const wxmlLexerDefinition = {
defaultMode: "OUTSIDE",

modes: {
OUTSIDE: [WXS_START, COMMENT, SEA_WS, SLASH_OPEN, OPEN, WXS_TEXT, TEXT],
OUTSIDE: [
WXS_START,
COMMENT,
SEA_WS,
SLASH_OPEN,
OPEN,
WXS_TEXT,
TEXT,
MUSTACHE_LEFT,
],
INTPN_INSIDE: [MUSTACHE_RIGHT, INTPN, STRING],
INSIDE: [
// Tokens from `OUTSIDE` to improve error recovery behavior
COMMENT,
Expand Down
22 changes: 22 additions & 0 deletions src/cst/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Parser extends CstParser {
attribute: IRule;
content: IRule;
wxscontent: IRule;
interpolation: IRule;

constructor() {
super(t, {
Expand All @@ -29,6 +30,7 @@ class Parser extends CstParser {
{ ALT: () => $.SUBRULE($.element) },
{ ALT: () => $.SUBRULE($.comment) },
{ ALT: () => $.SUBRULE($.chardata) },
{ ALT: () => $.SUBRULE($.interpolation) },
]);
});
});
Expand All @@ -38,6 +40,7 @@ class Parser extends CstParser {
$.OR([
{ ALT: () => $.SUBRULE($.wxs) },
{ ALT: () => $.SUBRULE($.element) },
{ ALT: () => $.SUBRULE($.interpolation) },
{ ALT: () => $.SUBRULE($.chardata) },
{ ALT: () => $.SUBRULE($.comment) },
]);
Expand Down Expand Up @@ -86,6 +89,25 @@ class Parser extends CstParser {
]);
});

$.RULE("interpolation", () => {
$.CONSUME(t.MUSTACHE_LEFT);
$.MANY(() => {
$.OR([
{ ALT: () => $.CONSUME(t.INTPN) },
{
ALT: () =>
$.CONSUME(t.STRING, {
ERR_MSG: "wx interpolation unexpected string",
}),
},
{ ALT: () => $.CONSUME(t.SEA_WS) },
]);
});
$.CONSUME(t.MUSTACHE_RIGHT, {
ERR_MSG: "wx interpolation unexpected end",
});
});

$.RULE("element", () => {
$.CONSUME(t.OPEN);
$.CONSUME(t.NAME);
Expand Down
66 changes: 63 additions & 3 deletions tests/base-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ describe("Base Test Suite", () => {
expect(wxElement.endTag.name).to.be.equals("element");
expect(wxElement).to.have.property("startTag");
expect(wxStartTag).to.have.property("selfClosing");
expect(wxStartTag.selfClosing).to.be.false
expect(wxStartTag.name).to.be.equals("element");
expect(wxStartTag.selfClosing).to.be.false;
expect(wxEndTag.name).to.be.equals("element");
})

it("can parse mismatch tag name WXElement", () => {
Expand Down Expand Up @@ -69,6 +71,8 @@ describe("Base Test Suite", () => {
expect(wxStartTag.attributes).to.be.lengthOf(2);
expect(wxStartTag).to.have.property("selfClosing");
expect(wxStartTag.selfClosing).to.be.equals(true);
expect(wxStartTag).to.have.property("name");
expect(wxStartTag.name).to.be.equals("wxs");
})

it("can parse WXEndTag", () => {
Expand Down Expand Up @@ -151,8 +155,8 @@ describe("Base Test Suite", () => {
tail text
`);
const matches = esquery(ast, "WXText");
expect(matches).to.be.lengthOf(5);
const wxText = matches[2];
expect(matches).to.be.lengthOf(7);
const wxText = matches[3];
expect(wxText).to.have.property("value");
expect(wxText.value).to.be.equals("text in wxml node")
});
Expand Down Expand Up @@ -245,4 +249,60 @@ describe("Base Test Suite", () => {
expect(attrMatches).to.be.lengthOf(1);
expect(attrMatches[0].value).to.be.equals("{{ index > 5 ? '</ss>' : '<pp />' }}");
})

it("support kebab-case WXElement name", () => {
const ast = parse(`
<v-table />
`);
const parseErrorMatchs = _.get(ast, 'errors') || [];
expect(parseErrorMatchs).to.be.lengthOf(0);
const matches = esquery(ast, "WXElement");
expect(matches).to.be.lengthOf(1);
expect(_.get(matches, "[0].name")).to.be.equals("v-table");
})

it("support camelCase WXElement name", () => {
const ast = parse(`
<mallHome />
`);
const parseErrorMatchs = _.get(ast, 'errors') || [];
expect(parseErrorMatchs).to.be.lengthOf(0);
const matches = esquery(ast, "WXElement");
expect(matches).to.be.lengthOf(1);
expect(_.get(matches, "[0].name")).to.be.equals("mallHome");
})

it("support PascalCase WXElement name", () => {
const ast = parse(`
<MallHome />
`);
const parseErrorMatchs = _.get(ast, 'errors') || [];
expect(parseErrorMatchs).to.be.lengthOf(0);
const matches = esquery(ast, "WXElement");
expect(matches).to.be.lengthOf(1);
expect(_.get(matches, "[0].name")).to.be.equals("MallHome");
})

it("support snake_case WXElement name", () => {
const ast = parse(`
<mall_home />
`);
const parseErrorMatchs = _.get(ast, 'errors') || [];
expect(parseErrorMatchs).to.be.lengthOf(0);
const matches = esquery(ast, "WXElement");
expect(matches).to.be.lengthOf(1);
expect(_.get(matches, "[0].name")).to.be.equals("mall_home");
})

it("support WXElement name conatin number", () => {
const ast = parse(`
<popup-v2 />
`);
const parseErrorMatchs = _.get(ast, 'errors') || [];
expect(parseErrorMatchs).to.be.lengthOf(0);
const matches = esquery(ast, "WXElement");
expect(matches).to.be.lengthOf(1);
expect(_.get(matches, "[0].name")).to.be.equals("popup-v2");
})

})
57 changes: 57 additions & 0 deletions tests/error-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,61 @@ describe("Error Test Suite", () => {
expect(attrsMatchs).to.be.lengthOf(1);
});

it("interpolation missing close bracket", () => {
const ast = parse(`
<app>
{{ nihao
</app>
`);

// @wxml/parser try to lookup missing close bracket
// {{ nihao }}
// ↑↑
const parseErrorMatchs = _.get(ast, 'errors') || [];
expect(parseErrorMatchs).to.be.lengthOf(3);
const parseError = parseErrorMatchs[2];
expect(parseError).to.have.property("rawType");
expect(parseError.rawType).to.be.equals("MismatchedTokenException");
expect(parseError).to.have.property("value");
expect(parseError.value).to.be.equals("wx interpolation unexpected end");

// tolerant parse
const elementMatchs = esquery(ast, "WXElement");
expect(elementMatchs).to.be.lengthOf(1);
});

it("interpolation missing close bracket #2", () => {
const ast = parse(`
<app key="value">
{{ nihao '" }}
</app>
`);

// @wxml/parser try to lookup missing close bracket
// {{ nihao }}
// ↑↑
const parseErrorMatchs = _.get(ast, 'errors') || [];
expect(parseErrorMatchs).to.be.lengthOf(1);
const parseError = parseErrorMatchs[0];
expect(parseError).to.have.property("type");
expect(parseError.type).to.be.equals("WXLexerError");
expect(parseError).to.have.property("value");
expect(parseError.value).to.be.equals("unexpected character: ->'<- at offset: 42, skipped 2 characters.");

// tolerant parse
const elementMatchs = esquery(ast, "WXElement");
const attrMatchs = esquery(ast, "WXAttribute");
const intpnMatchs = esquery(ast, "WXInterpolation");
expect(elementMatchs).to.be.lengthOf(1);
expect(attrMatchs).to.be.lengthOf(1);
expect(intpnMatchs).to.be.lengthOf(1);
expect(_.get(elementMatchs, '[0].startTag.selfClosing')).to.be.false;
expect(_.get(elementMatchs, '[0].startTag.name')).to.be.equals("app");
expect(_.get(elementMatchs, '[0].endTag.name')).to.be.equals("app");
expect(_.get(attrMatchs, "[0].key")).to.equals("key");
expect(_.get(attrMatchs, "[0].value")).to.equals("value");

expect(_.get(ast, ))
});

})
Loading

0 comments on commit 1acb326

Please sign in to comment.