├── 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 | 5 | 6 | 7 | monaco jsx syntax highlight demo 8 | 9 | 10 |
11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/worker/dispose-jsx-text.ts: -------------------------------------------------------------------------------- 1 | import { Data } from './types' 2 | import { calcPosition } from './tool' 3 | import { JsxToken } from './define' 4 | 5 | export const disposeJsxText = (data: Data) => { 6 | const { node, lines, classifications } = data 7 | const { positions } = calcPosition(node, lines) 8 | classifications.push({ 9 | start: positions[0], 10 | end: positions[1], 11 | tokens: [JsxToken.text] 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /scripts/generate-worker-json.js: -------------------------------------------------------------------------------- 1 | const Fs = require('fs') 2 | const Path = require('path') 3 | 4 | let source = '' 5 | try { 6 | source = Fs.readFileSync(Path.join(process.cwd(), './lib/worker/index.js')).toString() 7 | } catch { 8 | console.log('create empty json') 9 | } 10 | 11 | const targetPath = Path.join(process.cwd(), './src/worker.json') 12 | const content = { 13 | worker: source 14 | } 15 | Fs.writeFileSync(targetPath, JSON.stringify(content)) 16 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | export default { 7 | // Jest transformations -- this adds support for TypeScript 8 | // using ts-jest 9 | transform: { 10 | '^.+\\.ts?$': 'ts-jest' 11 | }, 12 | // Module file extensions for importing 13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint"], 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module", 7 | "ecmaFeatures": { 8 | "modules": true 9 | } 10 | }, 11 | "rules": { 12 | // 禁止使用 var 13 | "no-var": "error", 14 | // 优先使用 interface 而不是 type 15 | "@typescript-eslint/consistent-type-definitions": ["error", "interface"], 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/worker/index.ts: -------------------------------------------------------------------------------- 1 | import { analysis } from './analysis' 2 | 3 | // Respond to message from parent thread 4 | self.addEventListener('message', (event) => { 5 | const { code, filePath, version, config } = event.data 6 | try { 7 | const result = analysis(filePath, code, config) 8 | 9 | self.postMessage({ classifications: result, version, filePath }) 10 | } catch (e) { 11 | // 根据配置打印错误 12 | if (config?.enableConsole) { 13 | console.error(e) 14 | } 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /src/worker/dispose-jsx-attribute-key.ts: -------------------------------------------------------------------------------- 1 | import { Data } from './types' 2 | import { calcPosition } from './tool' 3 | import { JsxToken } from './define' 4 | 5 | /** 6 | * 分析jsx attribute key 7 | * @param data 8 | */ 9 | export const disposeJsxAttributeKey = (data: Data) => { 10 | const { node, lines, classifications } = data 11 | const { positions } = calcPosition(node, lines) 12 | classifications.push({ 13 | start: positions[0], 14 | end: positions[1], 15 | tokens: [JsxToken.attributeKey] 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/worker/typescript/build.ts: -------------------------------------------------------------------------------- 1 | import { Node as NodeOriginal } from 'typescript' 2 | 3 | const getTypescriptUrl = () => { 4 | const defaultUrl = 'https://cdnjs.cloudflare.com/ajax/libs/typescript/4.6.4/typescript.min.js' 5 | try { 6 | // @ts-ignore 7 | return __TYPESCRIPT_CUSTOM_URL__ || defaultUrl 8 | } catch { 9 | return defaultUrl 10 | } 11 | } 12 | 13 | // @ts-ignore 14 | self.importScripts([getTypescriptUrl()]) 15 | 16 | export const Typescript = (self as any).ts 17 | 18 | export declare namespace Typescript { 19 | type Node = NodeOriginal 20 | } 21 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDirs": ["./src"], 4 | "baseUrl": ".", 5 | "sourceMap": true, 6 | "noImplicitAny": true, 7 | "jsx": "react", 8 | "lib": ["es5", "dom", "ScriptHost", "es2015"], 9 | "allowJs": true, 10 | "strictNullChecks": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "esModuleInterop": true, 14 | "declaration": true, 15 | "declarationDir": "./lib", 16 | "resolveJsonModule": true 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "lib"] 20 | } 21 | -------------------------------------------------------------------------------- /src/worker/dispose-jsx-expression.ts: -------------------------------------------------------------------------------- 1 | import { Data } from './types' 2 | import { calcPosition } from './tool' 3 | import { JsxToken } from './define' 4 | 5 | export const disposeJsxExpression = (data: Data) => { 6 | const { node, lines, classifications } = data 7 | const { positions } = calcPosition(node, lines) 8 | 9 | // className={`666`} => "{" 10 | classifications.push({ 11 | start: positions[0], 12 | end: positions[0], 13 | tokens: [JsxToken.expressionBraces] 14 | }) 15 | // className={`666`} => "}" 16 | classifications.push({ 17 | start: positions[1], 18 | end: positions[1], 19 | tokens: [JsxToken.expressionBraces] 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /rollup.config.worker.js: -------------------------------------------------------------------------------- 1 | import pkg from './package.json' 2 | import typescript from 'rollup-plugin-typescript2' 3 | import uglify from '@lopatnov/rollup-plugin-uglify' 4 | 5 | export default [ 6 | { 7 | external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})], 8 | input: './src/worker/index.ts', 9 | output: [ 10 | { 11 | file: 'lib/worker/index.js', 12 | format: 'cjs', 13 | strict: false 14 | }, 15 | { 16 | file: 'lib/worker/index.module.js', 17 | format: 'es', 18 | strict: false 19 | } 20 | ], 21 | plugins: [ 22 | typescript({ 23 | useTsconfigDeclarationDir: true 24 | }), 25 | // 混淆压缩worker 26 | uglify() 27 | ] 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /src/worker/types.ts: -------------------------------------------------------------------------------- 1 | import { Typescript } from './typescript' 2 | 3 | export interface Position { 4 | row: number 5 | column: number 6 | } 7 | 8 | export interface Classification { 9 | start: Position 10 | end: Position 11 | tokens: string[] 12 | } 13 | 14 | export interface Config { 15 | /** 16 | * jsx tag 序号循环值 17 | * - 主要用作给相邻的tag渲染不同颜色作区分 18 | */ 19 | jsxTagCycle: number 20 | /** 21 | * 是否开启console 22 | */ 23 | enableConsole?: boolean 24 | } 25 | 26 | export interface Context { 27 | /** 28 | * 当前的jsx标签序号 29 | */ 30 | jsxTagOrder: number 31 | } 32 | 33 | export interface Data { 34 | node: Typescript.Node 35 | lines: number[] 36 | context: Context 37 | classifications: Classification[] 38 | config: Config 39 | index: number 40 | } 41 | -------------------------------------------------------------------------------- /scripts/mode.js: -------------------------------------------------------------------------------- 1 | const Minimist = require('minimist') 2 | const Path = require('path') 3 | const Fs = require('fs') 4 | const TsConfigJson = require('./tsconfig.json') 5 | 6 | // 解析参数 7 | const argv = Minimist(process.argv.slice(2)) 8 | // mode 9 | const mode = argv['mode'] 10 | // ts config target path 11 | const tsConfig = Path.join(process.cwd(),'./tsconfig.json') 12 | const requireTypescript = Path.join(process.cwd(),'./src/worker/typescript/index.ts') 13 | let requireTypescriptScript = '' 14 | 15 | if(mode === 'dev'){ 16 | TsConfigJson.compilerOptions.module = 'commonjs' 17 | requireTypescriptScript = `export * from './dev'` 18 | }else if(mode === 'build'){ 19 | TsConfigJson.compilerOptions.module = 'ES2015' 20 | requireTypescriptScript = `export * from './build'` 21 | } 22 | 23 | Fs.writeFileSync(tsConfig, JSON.stringify(TsConfigJson, null, 2)) 24 | Fs.writeFileSync(requireTypescript, requireTypescriptScript) 25 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 一行最多 100 字符 3 | printWidth: 100, 4 | // 使用 2 个空格缩进 5 | tabWidth: 2, 6 | // 不使用缩进符,而使用空格 7 | useTabs: false, 8 | // 行尾不需要有分号 9 | semi: false, 10 | // 使用单引号 11 | singleQuote: true, 12 | // 对象的 key 仅在必要时用引号 13 | quoteProps: 'as-needed', 14 | // jsx 不使用单引号,而使用双引号 15 | jsxSingleQuote: false, 16 | // 末尾不需要逗号 17 | trailingComma: 'none', 18 | // 大括号内的首尾需要空格 19 | bracketSpacing: true, 20 | // jsx 标签的反尖括号需要换行 21 | jsxBracketSameLine: false, 22 | // 箭头函数,只有一个参数的时候,也需要括号 23 | arrowParens: 'always', 24 | // 每个文件格式化的范围是文件的全部内容 25 | rangeStart: 0, 26 | rangeEnd: Infinity, 27 | // 不需要写文件开头的 @prettier 28 | requirePragma: false, 29 | // 不需要自动在文件开头插入 @prettier 30 | insertPragma: false, 31 | // 使用默认的折行标准 32 | proseWrap: 'preserve', 33 | // 根据显示样式决定 html 要不要折行 34 | htmlWhitespaceSensitivity: 'css', 35 | // 换行符使用 lf 36 | endOfLine: 'lf' 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run init 31 | - run: npm run test 32 | -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | const ExecShPromise = require('exec-sh').promise 2 | const Minimist = require('minimist') 3 | 4 | const Path = require('path') 5 | const Fs = require('fs') 6 | 7 | const argv = Minimist(process.argv.slice(2)) 8 | // version 9 | const version = argv['v'].trim() 10 | 11 | const run = async () => { 12 | // update package.json version 13 | const packageFilePath = Path.join(process.cwd(), './package.json') 14 | const packageObj = JSON.parse(Fs.readFileSync(packageFilePath).toString()) 15 | packageObj.version = version.toString() 16 | Fs.writeFileSync(packageFilePath, JSON.stringify(packageObj, null, 2)) 17 | 18 | await ExecShPromise('git checkout master') 19 | await ExecShPromise(`git checkout -b release/v${version}`) 20 | await ExecShPromise('git add .') 21 | await ExecShPromise(`git commit --m "chore: public v${version}"`) 22 | await ExecShPromise(`git push origin release/v${version}`) 23 | await ExecShPromise(`git tag v${version}`) 24 | await ExecShPromise(`git push origin v${version}`) 25 | } 26 | 27 | run() 28 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | import json from '@rollup/plugin-json' 3 | import pkg from './package.json' 4 | 5 | export default [ 6 | { 7 | external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})], 8 | input: './src/index.ts', 9 | output: [ 10 | { 11 | file: 'lib/index.js', 12 | format: 'cjs', 13 | strict: false 14 | }, 15 | { 16 | file: 'lib/index.module.js', 17 | format: 'es', 18 | strict: false 19 | }, 20 | { 21 | file: 'lib/index.min.js', 22 | format: 'iife', 23 | // sourcemap: !production, 24 | name: 'MonacoJsxSyntaxHighlight', 25 | strict: false 26 | }, 27 | { 28 | file: 'lib/index.umd.js', 29 | format: 'umd', 30 | name: 'MonacoJsxSyntaxHighlight', 31 | strict: false 32 | } 33 | ], 34 | plugins: [ 35 | typescript({ 36 | useTsconfigDeclarationDir: true 37 | }), 38 | json() 39 | ] 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 mayfly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/worker/tool.ts: -------------------------------------------------------------------------------- 1 | import { Typescript } from './typescript' 2 | 3 | /** 4 | * 获取对应下标所处行列数据 5 | * @param {*} index 索引下标(以1开始) 6 | * @param {*} lines 每行长度数据 7 | * @returns 8 | */ 9 | export const getRowAndColumn = (index: number, lines: number[]) => { 10 | let line = 0 11 | let offset = 0 12 | while (offset + lines[line] < index) { 13 | offset += lines[line] 14 | line += 1 15 | } 16 | 17 | return { row: line + 1, column: index - offset } 18 | } 19 | 20 | /** 21 | * 获取节点位置 22 | * @param {} node 节点 23 | * @returns 24 | */ 25 | export const getNodeRange = (node: Typescript.Node) => { 26 | if (typeof node.getStart === 'function' && typeof node.getEnd === 'function') { 27 | return [node.getStart(), node.getEnd()] 28 | } else if (typeof node.pos !== 'undefined' && typeof node.end !== 'undefined') { 29 | return [node.pos, node.end] 30 | } 31 | return [0, 0] 32 | } 33 | 34 | // 计算开始结束行列位置 35 | export const calcPosition = (node: Typescript.Node, lines: number[]) => { 36 | const [start, end] = getNodeRange(node) 37 | 38 | return { 39 | indexes: [start, end], 40 | positions: [getRowAndColumn(start + 1, lines), getRowAndColumn(end, lines)] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /__tests__/jsx-attribute-key.test.ts: -------------------------------------------------------------------------------- 1 | import { analysis } from '../src/worker/analysis' 2 | import { JsxToken } from '../src/worker/define' 3 | 4 | test('props', () => { 5 | const result = analysis('test.tsx', '
') 6 | const attribute = result.find((item) => item.tokens.includes(JsxToken.attributeKey)) 7 | 8 | expect(attribute).not.toBeNull() 9 | expect([ 10 | [attribute!.start.row, attribute!.start.column], 11 | [attribute!.end.row, attribute!.end.column] 12 | ]).toEqual([ 13 | [1, 6], 14 | [1, 14] 15 | ]) 16 | }) 17 | 18 | test('insert props', () => { 19 | const result = analysis('test.tsx', '
}/>') 20 | const attributes = result.filter((item) => item.tokens.includes(JsxToken.attributeKey)) 21 | 22 | expect(attributes.length).toBe(2) 23 | expect([ 24 | [attributes[0].start.row, attributes[0].start.column], 25 | [attributes[0].end.row, attributes[0].end.column] 26 | ]).toEqual([ 27 | [1, 6], 28 | [1, 11] 29 | ]) 30 | expect([ 31 | [attributes[1].start.row, attributes[1].start.column], 32 | [attributes[1].end.row, attributes[1].end.column] 33 | ]).toEqual([ 34 | [1, 19], 35 | [1, 23] 36 | ]) 37 | }) 38 | -------------------------------------------------------------------------------- /__tests__/jsx-text.test.ts: -------------------------------------------------------------------------------- 1 | import { analysis } from '../src/worker/analysis' 2 | import { JsxToken } from '../src/worker/define' 3 | 4 | const oneLine = '
123
' 5 | const multiLine = `
6 | 123 7 | 8 | 12345
9 | ` 10 | const breakLine = '
12{test}34
' 11 | 12 | test('pure one line text', () => { 13 | const result = analysis('test.tsx', oneLine) 14 | 15 | const jsxText = result.find((item) => item.tokens[0] === JsxToken.text) 16 | 17 | expect(jsxText).not.toBeNull() 18 | expect([jsxText!.start.row, jsxText!.start.column]).toEqual([1, 6]) 19 | expect([jsxText!.end.row, jsxText!.end.column]).toEqual([1, 8]) 20 | }) 21 | 22 | test('multi-line text', () => { 23 | const result = analysis('test.tsx', multiLine) 24 | 25 | const jsxText = result.find((item) => item.tokens[0] === JsxToken.text) 26 | 27 | expect(jsxText).not.toBeNull() 28 | expect([jsxText!.start.row, jsxText!.start.column]).toEqual([2, 1]) 29 | expect([jsxText!.end.row, jsxText!.end.column]).toEqual([4, 5]) 30 | }) 31 | 32 | test('break line text', () => { 33 | const result = analysis('test.tsx', breakLine) 34 | 35 | const jsxText = result.find((item) => item.tokens[0] === JsxToken.text) 36 | 37 | expect(jsxText).not.toBeNull() 38 | expect([jsxText!.start.row, jsxText!.start.column]).toEqual([1, 6]) 39 | expect([jsxText!.end.row, jsxText!.end.column]).toEqual([1, 7]) 40 | }) 41 | -------------------------------------------------------------------------------- /demo/index.scss: -------------------------------------------------------------------------------- 1 | $string-color: #55bb8a; 2 | $language-keyword-color: #619ac3; 3 | $global-variable-color: #ba2f7b; 4 | $unused-opacity: 0.4; 5 | $normal-text-color: #fffef8; 6 | $grammar-color: #57c3c2; 7 | $jsx-tag-symbol-color: $language-keyword-color; 8 | $jsx-tag-color: $global-variable-color; 9 | $jsx-attribute-color: #ffa60f; 10 | $jsx-text-color: #a0a0a0; 11 | 12 | .editor{ 13 | p { 14 | color: $string-color !important; 15 | } 16 | 17 | .mtk9 { 18 | color: $grammar-color; 19 | } 20 | 21 | .mtk1 { 22 | color: $normal-text-color; 23 | } 24 | 25 | .mtk22 { 26 | color: $global-variable-color; 27 | } 28 | 29 | .mtk8 { 30 | color: $language-keyword-color; 31 | } 32 | 33 | .mtk5 { 34 | color: $string-color; 35 | } 36 | 37 | .monaco-editor.showUnused .squiggly-inline-unnecessary { 38 | opacity: $unused-opacity; 39 | } 40 | 41 | .jsx-expression-braces { 42 | color: #f1908c; 43 | } 44 | 45 | .jsx-tag-angle-bracket { 46 | &.jsx-tag-order-1 { 47 | color: #12a182; 48 | } 49 | 50 | &.jsx-tag-order-2 { 51 | color: #4e7ca1; 52 | } 53 | 54 | &.jsx-tag-order-3 { 55 | color: #fb8b05; 56 | } 57 | 58 | color: $jsx-tag-symbol-color; 59 | } 60 | 61 | .jsx-tag-name { 62 | color: $jsx-tag-color; 63 | } 64 | 65 | .jsx-tag-attribute-key { 66 | color: $jsx-attribute-color; 67 | } 68 | 69 | .jsx-text { 70 | color: $jsx-text-color; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /demo/index.tsx: -------------------------------------------------------------------------------- 1 | import { MonacoJsxSyntaxHighlight, getWorker } from '../lib' 2 | import Editor from '@monaco-editor/react' 3 | import * as React from 'react' 4 | import { useCallback } from 'react' 5 | import * as ReactDOM from 'react-dom' 6 | import './index.scss' 7 | 8 | function App() { 9 | const handleEditorDidMount = useCallback((editor: any, monaco: any) => { 10 | monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ 11 | jsx: monaco.languages.typescript.JsxEmit.Preserve, 12 | target: monaco.languages.typescript.ScriptTarget.ES2020, 13 | esModuleInterop: true 14 | }) 15 | 16 | const monacoJsxSyntaxHighlight = new MonacoJsxSyntaxHighlight(getWorker(), monaco) 17 | 18 | // editor is the result of monaco.editor.create 19 | const { highlighter, dispose } = monacoJsxSyntaxHighlight.highlighterBuilder({ 20 | editor: editor 21 | }) 22 | // init highlight 23 | highlighter() 24 | 25 | editor.onDidChangeModelContent(() => { 26 | // content change, highlight 27 | highlighter() 28 | }) 29 | 30 | return dispose 31 | }, []) 32 | 33 | return ( 34 | 48 | ) 49 | } 50 | 51 | const rootElement = document.getElementById('root') 52 | ReactDOM.render(, rootElement) 53 | -------------------------------------------------------------------------------- /__tests__/jsx-expression.test.ts: -------------------------------------------------------------------------------- 1 | import { analysis } from '../src/worker/analysis' 2 | import { JsxToken } from '../src/worker/define' 3 | import { Classification } from '../src/worker/types' 4 | 5 | const inChildren = '
{test}
' 6 | const inAttribute = '
' 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 | //
=> " => ">" 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 | [![npm version](https://img.shields.io/npm/v/monaco-jsx-syntax-highlight.svg)](https://www.npmjs.com/package/monaco-jsx-highlighter) 4 | [![npm downloads](https://img.shields.io/npm/dm/monaco-jsx-syntax-highlight.svg)](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 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 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 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 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 | --------------------------------------------------------------------------------