Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 9448a78

Browse filesBrowse files
authored
Custom template tokenizers (#148)
* add support for custom template tokenizers * fix types * Make existing tests pass * merge tokens into existing tokenizer * add ast test * Write documentation for custom template tokenizers * forward tokenizer controls to custom tokenizer * document attributes used to control tokenizer * refactor template text tokenizing into separate method. * fix mock tokenizer token ranges * guard against empty text nodes * don't parse mustaches when template lang isn't html * test if tokenizer gets unprocessed mustaches * don't call the custom tokinzer on root text nodes if we are already processing a custom template lang * don't have empty tokens in custom tokenizer * add disclaimer for templateTokenizer option * prevent nested template parsing by checking if template is top level instead of maintaining a flag
1 parent 62b6986 commit 9448a78
Copy full SHA for 9448a78

File tree

Expand file treeCollapse file tree

12 files changed

+1350
-3
lines changed
Filter options
Expand file treeCollapse file tree

12 files changed

+1350
-3
lines changed

‎README.md

Copy file name to clipboardExpand all lines: README.md
+23Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,29 @@ If set to `true`, to parse expressions in `v-bind` CSS functions inside `<style>
194194

195195
See also to [here](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0043-sfc-style-variables.md).
196196

197+
### parserOptions.templateTokenizer
198+
199+
You can use `parserOptions.templateTokenizer` property to specify custom tokenizers to parse `<template lang="...">` tags.
200+
201+
For example to enable parsing of pug templates:
202+
203+
```jsonc
204+
{
205+
"parser": "vue-eslint-parser",
206+
"parserOptions": {
207+
"templateTokenizer": {
208+
// template tokenizer for `<template lang="pug">`
209+
"pug": "vue-eslint-parser-template-tokenizer-pug",
210+
}
211+
}
212+
}
213+
```
214+
215+
This option is only intended for plugin developers. **Be careful** when using this option directly, as it may change behaviour of rules you might have enabled.
216+
If you just want **pug** support, use [eslint-plugin-vue-pug](https://github.com/rashfael/eslint-plugin-vue-pug) instead, which uses this option internally.
217+
218+
See [implementing-custom-template-tokenizers.md](./docs/implementing-custom-template-tokenizers.md) for information on creating your own template tokenizer.
219+
197220
## 🎇 Usage for custom rules / plugins
198221

199222
- This parser provides `parserServices` to traverse `<template>`.
+70Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Implementing custom template tokenizers
2+
3+
A custom template tokenizer needs to create two types of tokens from the text it is given:
4+
5+
- Low level [tokens](https://github.com/vuejs/vue-eslint-parser/blob/master/src/ast/tokens.ts), which can be of an [existing HTML type](https://github.com/vuejs/vue-eslint-parser/blob/master/src/html/tokenizer.ts#L59) or even new types.
6+
- Intermediate tokens, which **must** be of type `StartTag`, `EndTag`, `Text` or `Mustache` (see [IntermediateTokenizer](https://github.com/vuejs/vue-eslint-parser/blob/master/src/html/intermediate-tokenizer.ts#L33)).
7+
8+
Token ranges and locations must count from the start of the document. To help with this, custom tokenizers are initialized with a starting line and column.
9+
10+
## Interface
11+
12+
```ts
13+
class CustomTokenizer {
14+
/**
15+
* The tokenized low level tokens, excluding comments.
16+
*/
17+
tokens: Token[]
18+
/**
19+
* The tokenized low level comment tokens
20+
*/
21+
comments: Token[]
22+
errors: ParseError[]
23+
24+
/**
25+
* Used to control tokenization of {{ expressions }}. If false, don't produce VExpressionStart/End tokens
26+
*/
27+
expressionEnabled: boolean = true
28+
29+
/**
30+
* The current namespace. Set and used by the parser. You probably can ignore this.
31+
*/
32+
namespace: string = "http://www.w3.org/1999/xhtml"
33+
34+
/**
35+
* The current tokenizer state. Set by the parser. You can probably ignore this.
36+
*/
37+
state: string = "DATA"
38+
39+
/**
40+
* The complete source code text. Used by the parser and set via the constructor.
41+
*/
42+
text: string
43+
44+
/**
45+
* Initialize this tokenizer.
46+
* @param templateText The contents of the <template> tag.
47+
* @param text The complete source code
48+
* @param {startingLine, startingColumn} The starting location of the templateText. Your token positions need to include this offset.
49+
*/
50+
constructor (templateText: string, text: string, { startingLine: number, startingColumn: number }) {
51+
this.text = text
52+
}
53+
54+
/**
55+
* Get the next intermediate token.
56+
* @returns The intermediate token or null.
57+
*/
58+
nextToken (): IntermediateToken | null {
59+
60+
}
61+
}
62+
```
63+
64+
## Behaviour
65+
66+
When the html parser encounters a `<template lang="...">` tag that matches a configured custom tokenizer, it will initialize a new instance of this tokenizer with the contents of the template tag. It will then call the `nextToken` method of this tokenizer until it returns `null`. After having consumed all intermediate tokens it will copy the low level tokens, comments and errors from the tokenizer instance.
67+
68+
## Examples
69+
70+
For a working example, see [vue-eslint-parser-template-tokenizer-pug](https://github.com/rashfael/vue-eslint-parser-template-tokenizer-pug/).

‎src/common/parser-options.ts

Copy file name to clipboardExpand all lines: src/common/parser-options.ts
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export interface ParserOptions {
4040

4141
// others
4242
// [key: string]: any
43+
44+
templateTokenizer?: { [key: string]: string }
4345
}
4446

4547
export function isSFCFile(parserOptions: ParserOptions) {

‎src/html/parser.ts

Copy file name to clipboardExpand all lines: src/html/parser.ts
+66-2Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
VDocumentFragment,
1616
VElement,
1717
VExpressionContainer,
18+
VLiteral,
1819
} from "../ast"
1920
import { NS, ParseError } from "../ast"
2021
import { debug } from "../common/debug"
@@ -51,6 +52,8 @@ import {
5152
getScriptParser,
5253
getParserLangFromSFC,
5354
} from "../common/parser-options"
55+
import sortedIndexBy from "lodash/sortedIndexBy"
56+
import sortedLastIndexBy from "lodash/sortedLastIndexBy"
5457

5558
const DIRECTIVE_NAME = /^(?:v-|[.:@#]).*[^.:@#]$/u
5659
const DT_DD = /^d[dt]$/u
@@ -474,6 +477,48 @@ export class Parser {
474477
}
475478
}
476479

480+
/**
481+
* Process the given template text token with a configured template tokenizer, based on language.
482+
* @param token The template text token to process.
483+
* @param lang The template language the text token should be parsed as.
484+
*/
485+
private processTemplateText(token: Text, lang: string): void {
486+
// eslint-disable-next-line @typescript-eslint/no-require-imports
487+
const TemplateTokenizer = require(this.baseParserOptions
488+
.templateTokenizer![lang])
489+
const templateTokenizer = new TemplateTokenizer(
490+
token.value,
491+
this.text,
492+
{
493+
startingLine: token.loc.start.line,
494+
startingColumn: token.loc.start.column,
495+
},
496+
)
497+
498+
// override this.tokenizer to forward expressionEnabled and state changes
499+
const rootTokenizer = this.tokenizer
500+
this.tokenizer = templateTokenizer
501+
502+
let templateToken: IntermediateToken | null = null
503+
while ((templateToken = templateTokenizer.nextToken()) != null) {
504+
;(this as any)[templateToken.type](templateToken)
505+
}
506+
507+
this.tokenizer = rootTokenizer
508+
509+
const index = sortedIndexBy(
510+
this.tokenizer.tokens,
511+
token,
512+
(x) => x.range[0],
513+
)
514+
const count =
515+
sortedLastIndexBy(this.tokenizer.tokens, token, (x) => x.range[1]) -
516+
index
517+
this.tokenizer.tokens.splice(index, count, ...templateTokenizer.tokens)
518+
this.tokenizer.comments.push(...templateTokenizer.comments)
519+
this.tokenizer.errors.push(...templateTokenizer.errors)
520+
}
521+
477522
/**
478523
* Handle the start tag token.
479524
* @param token The token to handle.
@@ -575,11 +620,12 @@ export class Parser {
575620
const lang = langAttr?.value?.value
576621

577622
if (elementName === "template") {
623+
this.expressionEnabled = true
578624
if (lang && lang !== "html") {
579625
// It is not an HTML template.
580626
this.tokenizer.state = "RAWTEXT"
627+
this.expressionEnabled = false
581628
}
582-
this.expressionEnabled = true
583629
} else if (this.isSFC) {
584630
// Element is Custom Block. e.g. <i18n>
585631
// Referred to the Vue parser. See https://github.com/vuejs/vue-next/blob/cbaa3805064cb581fc2007cf63774c91d39844fe/packages/compiler-sfc/src/parse.ts#L127
@@ -639,8 +685,26 @@ export class Parser {
639685
*/
640686
protected Text(token: Text): void {
641687
debug("[html] Text %j", token)
642-
643688
const parent = this.currentNode
689+
if (
690+
token.value &&
691+
parent.type === "VElement" &&
692+
parent.name === "template" &&
693+
parent.parent.type === "VDocumentFragment"
694+
) {
695+
const langAttribute = parent.startTag.attributes.find(
696+
(a) => a.key.name === "lang",
697+
)
698+
const lang = (langAttribute?.value as VLiteral)?.value
699+
if (
700+
lang &&
701+
lang !== "html" &&
702+
this.baseParserOptions.templateTokenizer?.[lang]
703+
) {
704+
this.processTemplateText(token, lang)
705+
return
706+
}
707+
}
644708
parent.children.push({
645709
type: "VText",
646710
range: token.range,

‎src/index.ts

Copy file name to clipboardExpand all lines: src/index.ts
+2-1Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,14 @@ function parseAsSFC(code: string, options: ParserOptions) {
108108
const scripts = rootAST.children.filter(isScriptElement)
109109
const template = rootAST.children.find(isTemplateElement)
110110
const templateLang = getLang(template) || "html"
111+
const hasTemplateTokenizer = options?.templateTokenizer?.[templateLang]
111112
const concreteInfo: AST.HasConcreteInfo = {
112113
tokens: rootAST.tokens,
113114
comments: rootAST.comments,
114115
errors: rootAST.errors,
115116
}
116117
const templateBody =
117-
template != null && templateLang === "html"
118+
template != null && (templateLang === "html" || hasTemplateTokenizer)
118119
? Object.assign(template, concreteInfo)
119120
: undefined
120121

‎test/ast.js

Copy file name to clipboardExpand all lines: test/ast.js
+10Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,16 @@ describe("Template AST", () => {
186186
const services = fs.existsSync(servicesPath)
187187
? JSON.parse(fs.readFileSync(servicesPath, "utf8"))
188188
: null
189+
if (parserOptions.templateTokenizer) {
190+
parserOptions.templateTokenizer = Object.fromEntries(
191+
Object.entries(parserOptions.templateTokenizer).map(
192+
([key, value]) => [
193+
key,
194+
path.resolve(__dirname, "../", value),
195+
],
196+
),
197+
)
198+
}
189199
const options = Object.assign(
190200
{ filePath: sourcePath },
191201
PARSER_OPTIONS,

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.