├── src
├── worker
│ ├── typescript
│ │ ├── dev.ts
│ │ └── build.ts
│ ├── define.ts
│ ├── dispose-jsx-text.ts
│ ├── index.ts
│ ├── dispose-jsx-attribute-key.ts
│ ├── dispose-jsx-expression.ts
│ ├── types.ts
│ ├── tool.ts
│ ├── analysis.ts
│ └── dispose-jsx-element-or-fragment.ts
├── get-worker.ts
└── index.ts
├── demo
├── index.html
├── index.scss
└── index.tsx
├── scripts
├── generate-worker-json.js
├── tsconfig.json
├── mode.js
└── release.js
├── jest.config.ts
├── .eslintrc
├── rollup.config.worker.js
├── prettier.config.js
├── .github
└── workflows
│ └── node.js.yaml
├── rollup.config.js
├── LICENSE
├── __tests__
├── jsx-attribute-key.test.ts
├── jsx-text.test.ts
├── jsx-expression.test.ts
└── jsx-element-or-fragment.test.ts
├── package.json
├── README.md
└── .gitignore
/src/worker/typescript/dev.ts:
--------------------------------------------------------------------------------
1 | import * as Typescript from 'typescript'
2 |
3 | export { Typescript }
4 |
--------------------------------------------------------------------------------
/src/get-worker.ts:
--------------------------------------------------------------------------------
1 | import Worker from './worker.json'
2 | import { WorkerStringContainer } from './index'
3 |
4 | export const getWorker = (): WorkerStringContainer => Worker
5 |
--------------------------------------------------------------------------------
/src/worker/define.ts:
--------------------------------------------------------------------------------
1 | export const JsxToken = {
2 | angleBracket: 'jsx-tag-angle-bracket',
3 | attributeKey: 'jsx-tag-attribute-key',
4 | tagName: 'jsx-tag-name',
5 | expressionBraces: 'jsx-expression-braces',
6 | text: 'jsx-text',
7 | orderTokenPrefix: 'jsx-tag-order'
8 | } as const
9 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | '
7 | const multiLine = `
8 | {{
9 | test: '666
10 | }}
11 | `
12 |
13 | test('in children', () => {
14 | const result = analysis('test.tsx', inChildren)
15 | const expressionBraces: Classification[] = []
16 |
17 | result.forEach((item) => {
18 | if (item.tokens.includes(JsxToken.expressionBraces)) {
19 | expressionBraces.push(item)
20 | }
21 | })
22 | expressionBraces.sort((a, b) => a.start.column - b.start.column)
23 |
24 | expect(expressionBraces.length).toBe(2)
25 |
26 | expect([expressionBraces[0].start.row, expressionBraces[0].start.column]).toEqual([1, 6])
27 |
28 | expect(expressionBraces[0].start).toEqual(expressionBraces[0].end)
29 |
30 | expect([expressionBraces[1].start.row, expressionBraces[1].start.column]).toEqual([1, 11])
31 | })
32 |
33 | test('in attribute', () => {
34 | const result = analysis('test.tsx', inAttribute)
35 | const expressionBraces: Classification[] = []
36 |
37 | result.forEach((item) => {
38 | if (item.tokens.includes(JsxToken.expressionBraces)) {
39 | expressionBraces.push(item)
40 | }
41 | })
42 | expressionBraces.sort((a, b) => a.start.column - b.start.column)
43 |
44 | expect(expressionBraces.length).toBe(2)
45 |
46 | expect([expressionBraces[0].start.row, expressionBraces[0].start.column]).toEqual([1, 12])
47 |
48 | expect([expressionBraces[1].start.row, expressionBraces[1].start.column]).toEqual([1, 14])
49 | })
50 |
51 | test('multi line', () => {
52 | const result = analysis('test.tsx', multiLine)
53 | const expressionBraces: Classification[] = []
54 |
55 | result.forEach((item) => {
56 | if (item.tokens.includes(JsxToken.expressionBraces)) {
57 | expressionBraces.push(item)
58 | }
59 | })
60 | expressionBraces.sort((a, b) => a.start.column - b.start.column)
61 |
62 | expect(expressionBraces.length).toBe(2)
63 |
64 | expect([expressionBraces[0].start.row, expressionBraces[0].start.column]).toEqual([2, 1])
65 |
66 | expect([expressionBraces[1].start.row, expressionBraces[1].start.column]).toEqual([4, 2])
67 | })
68 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "monaco-jsx-syntax-highlight",
3 | "version": "1.2.2",
4 | "description": "Highlight the jsx or tsx syntax for monaco editor",
5 | "keywords": [
6 | "monaco",
7 | "jsx",
8 | "highlight",
9 | "tsx",
10 | "monaco-editor",
11 | "syntax"
12 | ],
13 | "scripts": {
14 | "test": "jest",
15 | "clear": "rm -rf lib",
16 | "init": "npm run mode:dev && npm run generate-worker-json",
17 | "demo": "parcel demo/index.html",
18 | "generate-worker-json": "node ./scripts/generate-worker-json.js",
19 | "mode:build": "node ./scripts/mode.js --mode=build",
20 | "mode:dev": "node ./scripts/mode.js --mode=dev",
21 | "precompile": "npm run mode:dev && npm run test && npm run mode:build && npm run clear",
22 | "compile:worker": "rollup --config rollup.config.worker.js && npm run generate-worker-json",
23 | "compile:index": "rollup -c",
24 | "compile": "npm run compile:worker && npm run compile:index",
25 | "postcompile": "node ./scripts/mode.js --mode=dev"
26 | },
27 | "files": [
28 | "lib"
29 | ],
30 | "main": "./lib/index.js",
31 | "module": "./lib/index.module.js",
32 | "types": "./lib/index.d.ts",
33 | "repository": {
34 | "type": "git",
35 | "url": "git+https://github.com/x-glorious/monaco-jsx-syntax-highlight.git"
36 | },
37 | "author": "x-glorious",
38 | "license": "MIT",
39 | "bugs": {
40 | "url": "https://github.com/x-glorious/monaco-jsx-syntax-highlight/issues"
41 | },
42 | "homepage": "https://github.com/x-glorious/monaco-jsx-syntax-highlight#readme",
43 | "devDependencies": {
44 | "@lopatnov/rollup-plugin-uglify": "^2.1.2",
45 | "@monaco-editor/react": "^4.4.5",
46 | "@parcel/transformer-sass": "^2.6.2",
47 | "@rollup/plugin-json": "^4.1.0",
48 | "@types/jest": "^28.1.6",
49 | "@types/react": "^18.0.15",
50 | "@types/react-dom": "^18.0.6",
51 | "@typescript-eslint/eslint-plugin": "^5.23.0",
52 | "@typescript-eslint/parser": "^5.23.0",
53 | "buffer": "^6.0.3",
54 | "eslint": "^8.15.0",
55 | "exec-sh": "^0.4.0",
56 | "jest": "^28.1.3",
57 | "minimist": "^1.2.6",
58 | "parcel": "^2.6.2",
59 | "prettier": "^2.6.2",
60 | "process": "^0.11.10",
61 | "react": "^18.2.0",
62 | "react-dom": "^18.2.0",
63 | "rollup": "^2.72.1",
64 | "rollup-plugin-typescript2": "^0.31.2",
65 | "rollup-pluginutils": "^2.8.2",
66 | "ts-jest": "^28.0.7",
67 | "ts-node": "^10.7.0",
68 | "typescript": "^4.6.4"
69 | }
70 | }
--------------------------------------------------------------------------------
/src/worker/analysis.ts:
--------------------------------------------------------------------------------
1 | import { Classification, Config, Data } from './types'
2 | import { Typescript } from './typescript'
3 | import { disposeJsxElementOrFragment } from './dispose-jsx-element-or-fragment'
4 | import { disposeJsxAttributeKey } from './dispose-jsx-attribute-key'
5 | import { disposeJsxExpression } from './dispose-jsx-expression'
6 | import { disposeJsxText } from './dispose-jsx-text'
7 |
8 | const disposeNode = (data: Data) => {
9 | const { node, index } = data
10 | // 寻找到 jsx element or fragment 节点
11 | if (
12 | [
13 | Typescript.SyntaxKind.JsxFragment,
14 | Typescript.SyntaxKind.JsxElement,
15 | Typescript.SyntaxKind.JsxSelfClosingElement
16 | ].includes(node.kind)
17 | ) {
18 | disposeJsxElementOrFragment(data)
19 | }
20 |
21 | // jsx attribute key
22 | if (
23 | node.parent &&
24 | node.parent.kind === Typescript.SyntaxKind.JsxAttribute &&
25 | node.kind === Typescript.SyntaxKind.Identifier &&
26 | index === 0
27 | ) {
28 | disposeJsxAttributeKey(data)
29 | }
30 |
31 | // jsx expression
32 | if (node.kind === Typescript.SyntaxKind.JsxExpression) {
33 | disposeJsxExpression(data)
34 | }
35 |
36 | if (node.kind === Typescript.SyntaxKind.JsxText) {
37 | disposeJsxText(data)
38 | }
39 | }
40 |
41 | const walkAST = (data: Data) => {
42 | disposeNode(data)
43 |
44 | let counter = 0
45 | Typescript.forEachChild(data.node, (child: Typescript.Node) =>
46 | walkAST({
47 | ...data,
48 | node: child,
49 | index: counter++
50 | })
51 | )
52 | }
53 |
54 | const withDefaultConfig = (config?: Config): Config => {
55 | const { jsxTagCycle = 3 } = config || ({} as Config)
56 | return {
57 | jsxTagCycle
58 | }
59 | }
60 |
61 | export const analysis = (filePath: string, code: string, config?: Config) => {
62 | try {
63 | const classifications: Classification[] = []
64 | const sourceFile = Typescript.createSourceFile(
65 | filePath,
66 | code,
67 | Typescript.ScriptTarget.ES2020,
68 | true
69 | )
70 | // 切割分析每一行的长度
71 | const lines = code.split('\n').map((line) => line.length + 1)
72 | walkAST({
73 | node: sourceFile,
74 | lines,
75 | context: { jsxTagOrder: 1 },
76 | classifications,
77 | config: withDefaultConfig(config),
78 | index: 0
79 | })
80 | return classifications
81 | } catch (e) {
82 | // 根据配置打印错误
83 | if (config?.enableConsole) {
84 | console.error(e)
85 | }
86 | return []
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/worker/dispose-jsx-element-or-fragment.ts:
--------------------------------------------------------------------------------
1 | import { Data } from './types'
2 |
3 | import { Typescript } from './typescript'
4 |
5 | import { JsxToken } from './define'
6 | import { calcPosition } from './tool'
7 |
8 | /**
9 | * 处理 jsx element 或者 fragment
10 | * @param {*} data
11 | */
12 | export const disposeJsxElementOrFragment = (data: Data) => {
13 | const { node, lines, classifications } = data
14 | const config = data.config
15 | const context = data.context
16 | const orderToken = `${JsxToken.orderTokenPrefix}-${context.jsxTagOrder}`
17 | context.jsxTagOrder = context.jsxTagOrder + 1 > config.jsxTagCycle ? 1 : context.jsxTagOrder + 1
18 |
19 | // em
20 | if (node.kind === Typescript.SyntaxKind.JsxSelfClosingElement) {
21 | const { positions } = calcPosition(node, lines)
22 | const { positions: tagNamePositions } = calcPosition((node as any).tagName, lines)
23 | //
=> "<"
24 | classifications.push({
25 | start: positions[0],
26 | end: positions[0],
27 | tokens: [JsxToken.angleBracket, orderToken]
28 | })
29 | //
=> "/>"
30 | classifications.push({
31 | start: { ...positions[1], column: positions[1].column - 1 },
32 | end: positions[1],
33 | tokens: [JsxToken.angleBracket, orderToken]
34 | })
35 | //
=> "div"
36 | classifications.push({
37 | start: tagNamePositions[0],
38 | end: tagNamePositions[1],
39 | tokens: [JsxToken.tagName, orderToken]
40 | })
41 | } else {
42 | const openingNode =
43 | node.kind === Typescript.SyntaxKind.JsxFragment
44 | ? (node as any).openingFragment
45 | : (node as any).openingElement
46 | const closingNode =
47 | node.kind === Typescript.SyntaxKind.JsxFragment
48 | ? (node as any).closingFragment
49 | : (node as any).closingElement
50 | const { positions: openingPositions } = calcPosition(openingNode, lines)
51 | const { positions: closingPositions } = calcPosition(closingNode, lines)
52 | //
=> "<"
53 | classifications.push({
54 | start: openingPositions[0],
55 | end: openingPositions[0],
56 | tokens: [JsxToken.angleBracket, orderToken]
57 | })
58 | //
=> ">"
59 | classifications.push({
60 | start: openingPositions[1],
61 | end: openingPositions[1],
62 | tokens: [JsxToken.angleBracket, orderToken]
63 | })
64 | //
=> ""
65 | classifications.push({
66 | start: closingPositions[0],
67 | end: { ...closingPositions[0], column: closingPositions[0].column + 1 },
68 | tokens: [JsxToken.angleBracket, orderToken]
69 | })
70 | //
=> ">"
71 | classifications.push({
72 | start: closingPositions[1],
73 | end: closingPositions[1],
74 | tokens: [JsxToken.angleBracket, orderToken]
75 | })
76 |
77 | //
=> "div"
78 | if (node.kind === Typescript.SyntaxKind.JsxElement) {
79 | const { positions: openingTagNamePositions } = calcPosition(
80 | (openingNode as any).tagName,
81 | lines
82 | )
83 | const { positions: closingTagNamePositions } = calcPosition(
84 | (closingNode as any).tagName,
85 | lines
86 | )
87 | classifications.push({
88 | start: openingTagNamePositions[0],
89 | end: openingTagNamePositions[1],
90 | tokens: [JsxToken.tagName, orderToken]
91 | })
92 | classifications.push({
93 | start: closingTagNamePositions[0],
94 | end: closingTagNamePositions[1],
95 | tokens: [JsxToken.tagName, orderToken]
96 | })
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Classification, Config as HighlighterConfig } from './worker/types'
2 |
3 | export interface WorkerStringContainer {
4 | worker: string
5 | }
6 |
7 | export interface Config {
8 | /**
9 | * 自定义 typescript.min.js url
10 | * - 只在worker来源为 json 模式下生效
11 | */
12 | customTypescriptUrl?: string
13 | }
14 | /**
15 | * 高亮
16 | */
17 | export class MonacoJsxSyntaxHighlight {
18 | private worker: Worker
19 | private monaco: any
20 |
21 | constructor(worker: string | Worker | WorkerStringContainer, monaco: any, config?: Config) {
22 | this.monaco = monaco
23 | if (typeof worker === 'string') {
24 | this.worker = new Worker(worker)
25 | } else if (
26 | (worker as WorkerStringContainer).worker &&
27 | typeof (worker as WorkerStringContainer).worker === 'string'
28 | ) {
29 | this.worker = this.createWorkerFromPureString(
30 | (worker as WorkerStringContainer).worker,
31 | config
32 | )
33 | } else {
34 | this.worker = worker as Worker
35 | }
36 | }
37 |
38 | private createWorkerFromPureString = (content: string, config?: Config) => {
39 | // URL.createObjectURL
40 | window.URL = window.URL || window.webkitURL
41 | let blob
42 |
43 | // replace the custom url
44 | content = content.replace(
45 | '__TYPESCRIPT_CUSTOM_URL__',
46 | config?.customTypescriptUrl ? `'${config?.customTypescriptUrl}'` : 'undefined'
47 | )
48 |
49 | try {
50 | blob = new Blob([content], { type: 'application/javascript' })
51 | } catch (e) {
52 | // Backwards-compatibility
53 | ;(window as any).BlobBuilder =
54 | (window as any).BlobBuilder ||
55 | (window as any).WebKitBlobBuilder ||
56 | (window as any).MozBlobBuilder
57 | blob = new (window as any).BlobBuilder()
58 | blob.append(content)
59 | blob = blob.getBlob()
60 | }
61 |
62 | const worker = new Worker(URL.createObjectURL(blob))
63 | // free
64 | URL.revokeObjectURL(blob)
65 |
66 | return worker
67 | }
68 |
69 | public highlighterBuilder = (context: { editor: any; filePath?: string }, config?: HighlighterConfig) => {
70 | const { editor, filePath = editor.getModel().uri.toString() } = context
71 | const decorationsRef = { current: [] }
72 |
73 | const disposeMessage = (event: MessageEvent) => {
74 | const { classifications, version, filePath: disposeFilePath } = event.data
75 | requestAnimationFrame(() => {
76 | // 确认为本文件,并且为最新版本
77 | if (disposeFilePath === filePath && version === editor.getModel().getVersionId()) {
78 | const preDecoration = decorationsRef.current
79 | decorationsRef.current = editor.deltaDecorations(
80 | preDecoration,
81 | classifications.map((classification: Classification) => ({
82 | range: new this.monaco.Range(
83 | classification.start.row,
84 | classification.start.column,
85 | classification.end.row,
86 | classification.end.column + 1
87 | ),
88 | options: {
89 | inlineClassName: classification.tokens.join(' ')
90 | }
91 | }))
92 | )
93 | }
94 | })
95 | }
96 | // 注册监听事件
97 | this.worker.addEventListener('message', disposeMessage)
98 |
99 | return {
100 | highlighter: (code?: string) => {
101 | requestAnimationFrame(() => {
102 | const disposeCode = code || editor.getModel().getValue()
103 |
104 | // send message to worker
105 | this.worker.postMessage({
106 | code: disposeCode,
107 | filePath,
108 | version: editor.getModel().getVersionId(),
109 | config
110 | })
111 | })
112 | },
113 | dispose: () => {
114 | this.worker.removeEventListener('message', disposeMessage)
115 | }
116 | }
117 | }
118 | }
119 |
120 | export { getWorker } from './get-worker'
121 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # monaco-jsx-syntax-highlight
2 |
3 | [](https://www.npmjs.com/package/monaco-jsx-highlighter)
4 | [](https://www.npmjs.com/package/monaco-jsx-highlighter)
5 |
6 | Support monaco **jsx/tsx** syntax highlight
7 |
8 | Monaco only support the jsx **syntax checker**
9 |
10 | [Live demo](https://codesandbox.io/s/momaco-jsx-tsx-highlight-mp1sby)
11 |
12 | ## Installing
13 |
14 | ```shell
15 | $ npm install monaco-jsx-syntax-highlight
16 | ```
17 |
18 | ## Use
19 |
20 | The main part of this package is a worker for **analysing jsx syntax**
21 | So we have to way to init the **Controller class**
22 |
23 | ### Use blob create worker
24 |
25 | ```tsx
26 | import { MonacoJsxSyntaxHighlight, getWorker } from 'monaco-jsx-syntax-highlight'
27 |
28 | const controller = new MonacoJsxSyntaxHighlight(getWorker(), monaco)
29 | ```
30 |
31 | When using `getWorker` return value as Worker, we can **custom the typescript compile source file url**(for the purpose of **speeding up** load time)
32 |
33 | If do not set, the default source is https://cdnjs.cloudflare.com/ajax/libs/typescript/4.6.4/typescript.min.js
34 |
35 | ```tsx
36 | const controller = new MonacoJsxSyntaxHighlight(getWorker(), monaco, {
37 | customTypescriptUrl: 'https://xxx/typescript.min.js'
38 | })
39 | ```
40 |
41 | ### Use js worker file
42 |
43 | If your browser do not support to use blob worker, you can download the [worker file](https://github.com/x-glorious/monaco-jsx-syntax-highlight/releases) and save it in your project
44 |
45 | - web worker has same-origin policy
46 |
47 | ```tsx
48 | import { MonacoJsxSyntaxHighlight } from 'monaco-jsx-syntax-highlight'
49 |
50 | const controller = new MonacoJsxSyntaxHighlight('https://xxxx', monaco)
51 | ```
52 |
53 | ---
54 |
55 | ### Controller
56 |
57 | Remember, when this editor is disposed(`editor.dispose`), we should **invoke the `dispose`** function returned by the highlighterBuilder too
58 |
59 | - `highlighter`: send latest content to worker for analysing
60 | - `dispose`: remove event listener of the worker
61 |
62 | ```tsx
63 | // editor is the result of monaco.editor.create
64 | const { highlighter, dispose } = monacoJsxSyntaxHighlight.highlighterBuilder(
65 | { editor: editor }
66 | )
67 |
68 | // init hightlight
69 | highlighter()
70 |
71 | editor.onDidChangeModelContent(() => {
72 | // content change, highlight
73 | highlighter()
74 | })
75 | ```
76 |
77 | ```tsx
78 | interface HighlighterConfig {
79 | /**
80 | * max jsx tag order loop value
81 | * @default 3
82 | */
83 | jsxTagCycle: number
84 | /**
85 | * open console to log some error information
86 | * @default false
87 | */
88 | enableConsole?: boolean
89 | }
90 |
91 | type HighlighterBuilder = (context: {
92 | editor: any;
93 | filePath?: string;
94 | }, config?: HighlighterConfig) => {
95 | highlighter: (code?: string) => void;
96 | dispose: () => void;
97 | }
98 | ```
99 |
100 | ### Highlight class
101 |
102 | Use css class to highlight the jsx syntax
103 |
104 | - `'jsx-tag-angle-bracket'`: `<`、`>`、`/>`
105 | - `'jsx-tag-attribute-key'`: the attribute key
106 | - `'jsx-expression-braces'`: the braces of attribute value
107 | - `'jsx-text'`: the text in jsx tag content
108 | - `'jsx-tag-name'`: the tag name of jsx tag
109 | - `'jsx-tag-order-xxx'`: the tag order class
110 |
111 | ## FAQ
112 |
113 | ### monaco do not **check** the jsx syntax
114 |
115 | You can try below config code
116 |
117 | PS: the **file name must end with** `jsx` or `tsx`
118 |
119 | ```tsx
120 | monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
121 | jsx: monaco.languages.typescript.JsxEmit.Preserve,
122 | target: monaco.languages.typescript.ScriptTarget.ES2020,
123 | esModuleInterop: true
124 | })
125 |
126 | const model = monaco.editor.createModel(
127 | 'const test: number = 666',
128 | 'typescript',
129 | monaco.Uri.parse('index.tsx')
130 | )
131 |
132 | editor.current = monaco.editor.create(editorElement.current)
133 | editor.current.setModel(model)
134 | ```
135 |
--------------------------------------------------------------------------------
/__tests__/jsx-element-or-fragment.test.ts:
--------------------------------------------------------------------------------
1 | import { analysis } from '../src/worker/analysis'
2 | import { JsxToken } from '../src/worker/define'
3 |
4 | test('fragment', () => {
5 | const result = analysis('test.tsx', '<>>')
6 | expect(result.length).toBe(4)
7 |
8 | // start <
9 | expect([result[0].start.row, result[0].start.column]).toEqual([1, 1])
10 | // start >
11 | expect([result[1].start.row, result[1].start.column]).toEqual([1, 2])
12 |
13 | // end
14 | expect([
15 | [result[2].start.row, result[2].start.column],
16 | [result[2].end.row, result[2].end.column]
17 | ]).toEqual([
18 | [1, 3],
19 | [1, 4]
20 | ])
21 | // end >
22 | expect([result[3].start.row, result[3].start.column]).toEqual([1, 5])
23 | })
24 |
25 | test('self close element', () => {
26 | const result = analysis('test.tsx', '
')
27 | // <
28 | expect([result[0].start.row, result[0].start.column]).toEqual([1, 1])
29 | // />
30 | expect([
31 | [result[1].start.row, result[1].start.column],
32 | [result[1].end.row, result[1].end.column]
33 | ]).toEqual([
34 | [1, 5],
35 | [1, 6]
36 | ])
37 | // div
38 | expect([
39 | [result[2].start.row, result[2].start.column],
40 | [result[2].end.row, result[2].end.column]
41 | ]).toEqual([
42 | [1, 2],
43 | [1, 4]
44 | ])
45 | expect(result[2].tokens.includes(JsxToken.tagName)).toEqual(true)
46 | })
47 |
48 | test('open element', () => {
49 | const result = analysis('test.tsx', '
')
50 |
51 | // start <
52 | expect([result[0].start.row, result[0].start.column]).toEqual([1, 1])
53 | // start >
54 | expect([result[1].start.row, result[1].start.column]).toEqual([1, 5])
55 |
56 | // end
57 | expect([
58 | [result[2].start.row, result[2].start.column],
59 | [result[2].end.row, result[2].end.column]
60 | ]).toEqual([
61 | [1, 6],
62 | [1, 7]
63 | ])
64 | // end >
65 | expect([result[3].start.row, result[3].start.column]).toEqual([1, 11])
66 |
67 | // start div
68 | expect([
69 | [result[4].start.row, result[4].start.column],
70 | [result[4].end.row, result[4].end.column]
71 | ]).toEqual([
72 | [1, 2],
73 | [1, 4]
74 | ])
75 | expect(result[4].tokens.includes(JsxToken.tagName)).toEqual(true)
76 | // end div
77 | expect([
78 | [result[5].start.row, result[5].start.column],
79 | [result[5].end.row, result[5].end.column]
80 | ]).toEqual([
81 | [1, 8],
82 | [1, 10]
83 | ])
84 | expect(result[5].tokens.includes(JsxToken.tagName)).toEqual(true)
85 | })
86 |
87 | test('order', () => {
88 | expect(analysis('test.tsx', '
').map((item) => item.tokens)).toEqual([
89 | // open start <
90 | [JsxToken.angleBracket, JsxToken.orderTokenPrefix + '-1'],
91 | // open start >
92 | [JsxToken.angleBracket, JsxToken.orderTokenPrefix + '-1'],
93 | // open end
94 | [JsxToken.angleBracket, JsxToken.orderTokenPrefix + '-1'],
95 | // open end >
96 | [JsxToken.angleBracket, JsxToken.orderTokenPrefix + '-1'],
97 | // open start div
98 | [JsxToken.tagName, JsxToken.orderTokenPrefix + '-1'],
99 | // open end div
100 | [JsxToken.tagName, JsxToken.orderTokenPrefix + '-1'],
101 | // close < order 2
102 | [JsxToken.angleBracket, JsxToken.orderTokenPrefix + '-2'],
103 | // close /> order 2
104 | [JsxToken.angleBracket, JsxToken.orderTokenPrefix + '-2'],
105 | // close div order 2
106 | [JsxToken.tagName, JsxToken.orderTokenPrefix + '-2']
107 | ])
108 |
109 | // test for jsxTagCycle
110 | expect(analysis('test.tsx', '
', { jsxTagCycle: 1 }).map((item) => item.tokens)).toEqual([
111 | // open start <
112 | [JsxToken.angleBracket, JsxToken.orderTokenPrefix + '-1'],
113 | // open start >
114 | [JsxToken.angleBracket, JsxToken.orderTokenPrefix + '-1'],
115 | // open end
116 | [JsxToken.angleBracket, JsxToken.orderTokenPrefix + '-1'],
117 | // open end >
118 | [JsxToken.angleBracket, JsxToken.orderTokenPrefix + '-1'],
119 | // open start div
120 | [JsxToken.tagName, JsxToken.orderTokenPrefix + '-1'],
121 | // open end div
122 | [JsxToken.tagName, JsxToken.orderTokenPrefix + '-1'],
123 | // close < order 1
124 | [JsxToken.angleBracket, JsxToken.orderTokenPrefix + '-1'],
125 | // close /> order 1
126 | [JsxToken.angleBracket, JsxToken.orderTokenPrefix + '-1'],
127 | // close div order 1
128 | [JsxToken.tagName, JsxToken.orderTokenPrefix + '-1']
129 | ])
130 | })
131 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### JetBrains template
2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
3 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
4 |
5 | # User-specific stuff
6 | .idea/**/workspace.xml
7 | .idea/**/tasks.xml
8 | .idea/**/usage.statistics.xml
9 | .idea/**/dictionaries
10 | .idea/**/shelf
11 |
12 | # Generated files
13 | .idea/**/contentModel.xml
14 |
15 | # Sensitive or high-churn files
16 | .idea/**/dataSources/
17 | .idea/**/dataSources.ids
18 | .idea/**/dataSources.local.xml
19 | .idea/**/sqlDataSources.xml
20 | .idea/**/dynamic.xml
21 | .idea/**/uiDesigner.xml
22 | .idea/**/dbnavigator.xml
23 |
24 | # Gradle
25 | .idea/**/gradle.xml
26 | .idea/**/libraries
27 |
28 | # Gradle and Maven with auto-import
29 | # When using Gradle or Maven with auto-import, you should exclude module files,
30 | # since they will be recreated, and may cause churn. Uncomment if using
31 | # auto-import.
32 | # .idea/artifacts
33 | # .idea/compiler.xml
34 | # .idea/jarRepositories.xml
35 | # .idea/modules.xml
36 | # .idea/*.iml
37 | # .idea/modules
38 | # *.iml
39 | # *.ipr
40 |
41 | # CMake
42 | cmake-build-*/
43 |
44 | # Mongo Explorer plugin
45 | .idea/**/mongoSettings.xml
46 |
47 | # File-based project format
48 | *.iws
49 |
50 | # IntelliJ
51 | out/
52 |
53 | # mpeltonen/sbt-idea plugin
54 | .idea_modules/
55 |
56 | # JIRA plugin
57 | atlassian-ide-plugin.xml
58 |
59 | # Cursive Clojure plugin
60 | .idea/replstate.xml
61 |
62 | # Crashlytics plugin (for Android Studio and IntelliJ)
63 | com_crashlytics_export_strings.xml
64 | crashlytics.properties
65 | crashlytics-build.properties
66 | fabric.properties
67 |
68 | # Editor-based Rest Client
69 | .idea/httpRequests
70 |
71 | # Android studio 3.1+ serialized cache file
72 | .idea/caches/build_file_checksums.ser
73 |
74 | ### Node template
75 | # Logs
76 | logs
77 | *.log
78 | npm-debug.log*
79 | yarn-debug.log*
80 | yarn-error.log*
81 | lerna-debug.log*
82 |
83 | # Diagnostic reports (https://nodejs.org/api/report.html)
84 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
85 |
86 | # Runtime data
87 | pids
88 | *.pid
89 | *.seed
90 | *.pid.lock
91 |
92 | # Directory for instrumented libs generated by jscoverage/JSCover
93 | lib-cov
94 |
95 | # Coverage directory used by tools like istanbul
96 | coverage
97 | *.lcov
98 |
99 | # nyc test coverage
100 | .nyc_output
101 |
102 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
103 | .grunt
104 |
105 | # Bower dependency directory (https://bower.io/)
106 | bower_components
107 |
108 | # node-waf configuration
109 | .lock-wscript
110 |
111 | # Compiled binary addons (https://nodejs.org/api/addons.html)
112 | build/Release
113 |
114 | # Dependency directories
115 | node_modules/
116 | jspm_packages/
117 |
118 | # Snowpack dependency directory (https://snowpack.dev/)
119 | web_modules/
120 |
121 | # TypeScript cache
122 | *.tsbuildinfo
123 |
124 | # Optional npm cache directory
125 | .npm
126 |
127 | # Optional eslint cache
128 | .eslintcache
129 |
130 | # Microbundle cache
131 | .rpt2_cache/
132 | .rts2_cache_cjs/
133 | .rts2_cache_es/
134 | .rts2_cache_umd/
135 |
136 | # Optional REPL history
137 | .node_repl_history
138 |
139 | # Output of 'npm pack'
140 | *.tgz
141 |
142 | # Yarn Integrity file
143 | .yarn-integrity
144 |
145 | # dotenv environment variables file
146 | .env
147 | .env.test
148 |
149 | # parcel-bundler cache (https://parceljs.org/)
150 | .cache
151 | .parcel-cache
152 |
153 | # Next.js build output
154 | .next
155 | out
156 |
157 | # Nuxt.js build / generate output
158 | .nuxt
159 | dist
160 |
161 | # Gatsby files
162 | .cache/
163 | # Comment in the public line in if your project uses Gatsby and not Next.js
164 | # https://nextjs.org/blog/next-9-1#public-directory-support
165 | # public
166 |
167 | # vuepress build output
168 | .vuepress/dist
169 |
170 | # Serverless directories
171 | .serverless/
172 |
173 | # FuseBox cache
174 | .fusebox/
175 |
176 | # DynamoDB Local files
177 | .dynamodb/
178 |
179 | # TernJS port file
180 | .tern-port
181 |
182 | # Stores VSCode versions used for testing VSCode extensions
183 | .vscode-test
184 |
185 | # yarn v2
186 | .yarn/cache
187 | .yarn/unplugged
188 | .yarn/build-state.yml
189 | .yarn/install-state.gz
190 | .pnp.*
191 |
192 | ### macOS template
193 | # General
194 | .DS_Store
195 | .AppleDouble
196 | .LSOverride
197 |
198 | # Icon must end with two \r
199 | Icon
200 |
201 | # Thumbnails
202 | ._*
203 |
204 | # Files that might appear in the root of a volume
205 | .DocumentRevisions-V100
206 | .fseventsd
207 | .Spotlight-V100
208 | .TemporaryItems
209 | .Trashes
210 | .VolumeIcon.icns
211 | .com.apple.timemachine.donotpresent
212 |
213 | # Directories potentially created on remote AFP share
214 | .AppleDB
215 | .AppleDesktop
216 | Network Trash Folder
217 | Temporary Items
218 | .apdisk
219 |
220 | src/worker/typescript/index.ts
221 | tsconfig.json
222 |
223 | .idea
224 |
225 | # project
226 | lib
227 | src/worker.json
228 |
229 |
--------------------------------------------------------------------------------