├── .eslintrc.js ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── _config.yml ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── MonacoJSXHighlighter.css ├── MonacoJSXHighlighter.js └── index.js ├── tsconfig.json └── webpack.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2021": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | // "standard-with-typescript", 10 | // "plugin:react/recommended" 11 | ], 12 | "overrides": [ 13 | { 14 | "env": { 15 | "node": true 16 | }, 17 | "files": [ 18 | ".eslintrc.{js,cjs}" 19 | ], 20 | "parserOptions": { 21 | "sourceType": "module" 22 | } 23 | } 24 | ], 25 | "parserOptions": { 26 | "ecmaVersion": "latest", 27 | "sourceType": "module", 28 | }, 29 | "plugins": [ 30 | "react" 31 | ], 32 | "rules": { 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # TypeScript cache 44 | *.tsbuildinfo 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Microbundle cache 53 | .rpt2_cache/ 54 | .rts2_cache_cjs/ 55 | .rts2_cache_es/ 56 | .rts2_cache_umd/ 57 | 58 | # Optional REPL history 59 | .node_repl_history 60 | 61 | # Output of 'npm pack' 62 | *.tgz 63 | 64 | # Yarn Integrity file 65 | .yarn-integrity 66 | 67 | # dotenv environment variables file 68 | .env 69 | .env.test 70 | 71 | # parcel-bundler cache (https://parceljs.org/) 72 | .cache 73 | 74 | # Next.js build output 75 | .next 76 | 77 | # Nuxt.js build / generate output 78 | .nuxt 79 | dist 80 | 81 | # Gatsby files 82 | .cache/ 83 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 84 | # https://nextjs.org/blog/next-9-1#public-directory-support 85 | # public 86 | 87 | # vuepress build output 88 | .vuepress/dist 89 | 90 | # Serverless directories 91 | .serverless/ 92 | 93 | # FuseBox cache 94 | .fusebox/ 95 | 96 | # DynamoDB Local files 97 | .dynamodb/ 98 | 99 | # TernJS port file 100 | .tern-port 101 | 102 | .github/ 103 | gh-pages/ 104 | 105 | node_modules 106 | .DS_Store 107 | .vscode 108 | jsconfig.json 109 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | build/ 3 | .eslintrc.js 4 | .gitignore 5 | _config.yml 6 | rollup.config.js 7 | webpack.config.js 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 David Gonzalez 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # monaco-jsx-highlighter 2 | [![npm version](https://img.shields.io/npm/v/monaco-jsx-highlighter.svg?style=flat-square)](https://www.npmjs.com/package/monaco-jsx-highlighter) 3 | [![npm downloads](https://img.shields.io/npm/dm/monaco-jsx-highlighter.svg?style=flat-square)](https://www.npmjs.com/package/monaco-jsx-highlighter) 4 | 5 | An extensible library to highlight (and comment) JSX syntax in the Monaco Editor 6 | using Babel. It exposes its AST after it does its magic, so you can add your own 7 | syntax-based or custom highlights. 8 | 9 | ## [LIVE DEMO](https://codesandbox.io/s/monaco-editor-react-with-jsx-highlighting-and-commenting-v1-urce8?file=/src/index.js) 10 | [![monaco-jsx-highlighter demo](https://github.com/luminaxster/syntax-highlighter/blob/demo_file/msh_demo.gif)](https://codesandbox.io/p/sandbox/monaco-editor-react-with-jsx-highlighting-and-commenting-v2-urce8?file=/src/index.js) 11 | ```sh 12 | # with npm (assuming you are already using monaco-editor) 13 | npm i @babel/parser @babel/traverse monaco-jsx-highlighter 14 | # with yarn (assuming you are already using monaco-editor) 15 | yarn add @babel/parser @babel/traverse monaco-jsx-highlighter 16 | ``` 17 | 18 | ## TL;DR 19 | 20 | ```js 21 | import monaco from 'monaco-editor'; 22 | import {parse} from "@babel/parser"; 23 | import traverse from "@babel/traverse"; 24 | import MonacoJSXHighlighter, {makeBabelParse} from 'monaco-jsx-highlighter'; 25 | 26 | // Minimal Babel setup for React JSX parsing: 27 | const babelParse = code => parse(code, { 28 | sourceType: "module", 29 | plugins: ["jsx"] 30 | }); 31 | 32 | // Instantiate the highlighter 33 | const monacoJSXHighlighter = new MonacoJSXHighlighter( 34 | monaco, babelParse, traverse, getMonacoEditor() 35 | ); 36 | // Activate highlighting (debounceTime default: 100ms) 37 | monacoJSXHighlighter.highlightOnDidChangeModelContent(100); 38 | // Activate JSX commenting 39 | monacoJSXHighlighter.addJSXCommentCommand(); 40 | // Done =) 41 | 42 | function getMonacoEditor(){ 43 | return monaco.editor.create( 44 | document.getElementById("editor"), { 45 | value: 'const AB={"hello"};', 46 | language: 'javascript' 47 | }); 48 | } 49 | 50 | //use makeBabelParse if unsure of the config you need for TSX 51 | ``` 52 | 53 | ## NL;PR 54 | 55 | ## New in v2 56 | 57 | - Adds `makeBabelParse`: babel's parse configuration for JSX/TSX (thanks [@HaimCandiTech](https://github.com/HaimCandiTech)) 58 | - TS codebase migration start. 59 | - Reported defect fixes(dispose). 60 | 61 | ## New in v1 62 | 63 | - Babel is now used directly instead of via JsCodeShift. 64 | - React fragment, spread child, spread attribute, and container expression 65 | highlighting. 66 | - highlightOnDidChangeModelContent(debounceTime) method debounces highlight 67 | updates. 68 | - Several defect repairs. 69 | 70 | ### Breaking Changes 71 | 72 | If you have used 0.x versions, you'll notice JsCodeShift has been replaced with 73 | Babel: 74 | 75 | ```diff 76 | - import j from 'jscodeshift'; 77 | + import {parse} from "@babel/parser"; 78 | + import traverse from "@babel/traverse"; 79 | ``` 80 | 81 | This only affects the constructor signature: 82 | 83 | ```diff 84 | + const babelParse = code => parse(code, {sourceType: "module", plugins: ["jsx"]}); 85 | const monacoJSXHighlighter = new MonacoJSXHighlighter( 86 | monaco, 87 | - j, 88 | + babelParse, traverse, 89 | monacoEditor 90 | ); 91 | ``` 92 | 93 | Also, `monacoJSXHighlighter.highlightOnDidChangeModelContent` method now has an 94 | optional debounce time as first parameter on its signature: 95 | 96 | ```diff 97 | monacoJSXHighlighter.highlightOnDidChangeModelContent( 98 | - afterHighlight: func, 99 | + debounceTime: number, afterHighlight: func, 100 | ...) 101 | ``` 102 | 103 | ### Dependencies 104 | 105 | It requires [`monaco-editor`](https://www.npmjs.com/package/monaco-editor) 106 | , [`@babel/parser`](https://www.npmjs.com/package/@babel/parser) 107 | and [`@babel/traverse`](https://www.npmjs.com/package/@babel/traverse), for 108 | convenience, they are listed as peer dependencies and passed by reference (so 109 | you can do lazy loading). Please install them before `monaco-jsx-highlighter`; 110 | 111 | ### Installation 112 | 113 | Install the package in your project directory with: 114 | 115 | #### NPM: 116 | ```sh 117 | # with npm 118 | npm install @babel/parser 119 | npm install @babel/traverse 120 | npm install monaco-jsx-highlighter 121 | ``` 122 | #### YARN: 123 | ```sh 124 | # with yarn 125 | yarn add @babel/parser 126 | yarn add @babel/traverse 127 | yarn add monaco-jsx-highlighter 128 | ``` 129 | 130 | ### Replacing CSS classes with your own 131 | 132 | ```js 133 | import {JSXTypes} from 'monaco-jsx-highlighter'; 134 | // JSXTypes:JSX Syntax types and their CSS classnames. 135 | // Customize the color font in JSX texts: .myCustomCSS {color: red;} 136 | JSXTypes.JSXText.options.inlineClassName = "myCustomCSS"; 137 | ``` 138 | 139 | ### Overriding CSS classes 140 | 141 | Take a look of 142 | the [`src/JSXColoringProvider.css` file](https://github.com/luminaxster/syntax-highlighter/blob/master/src/MonacoJSXHighlighter.css) 143 | and override the CSS classes you need. Make sure to import your customization 144 | CSS files after you import `monaco-jsx-highlighter`. 145 | 146 | ### Advanced Usage 147 | After your have a Monaco JSX Highlighter instance, `monacoJSXHighlighter`: 148 | ```js 149 | const defaultOptions = { 150 | parser: 'babel', // for reference only, only babel is supported right now 151 | isHighlightGlyph: false, // if JSX elements should decorate the line number gutter 152 | iShowHover: false, // if JSX types should tooltip with their type info 153 | isUseSeparateElementStyles: false, // if opening elements and closing elements have different styling 154 | isThrowJSXParseErrors: false, // Only JSX Syntax Errors are not thrown by default when parsing, true will throw like any other parsign error 155 | }; 156 | 157 | const monacoJSXHighlighter = new MonacoJSXHighlighter( 158 | monaco, babelParse, traverse, monacoEditor, defaultOptions 159 | ); 160 | ``` 161 | The highlight activation method, `monacoJSXHighlighter.highlightOnDidChangeModelContent(debounceTime: number, afterHighlight: func, ...)` 162 | , accepts a callback among other parameters. The callback `afterHighlight` 163 | passes the AST used to highlight the code. Passing parameters and using the disposer function returned by the call are optional. 164 | 165 | **Note:** The disposer is always called when the editor is disposed. 166 | 167 | ```js 168 | // Optional: Disable highlighting when needed (e.g. toggling, unmounting, pausing) 169 | const highlighterDisposeFunc = monacoJSXHighlighter. 170 | highlightOnDidChangeModelContent( 171 | 100, 172 | ast=>{} 173 | ); 174 | highlighterDisposeFunc(); // if you need to 175 | 176 | // Internally the highlighter is triggering after each code change debounced 177 | let tid = null; 178 | let debounceTime = 100; // default 179 | monacoEditor.onDidChangeModelContent(() => { 180 | clearTimeout(tid); 181 | tid = setTimeout(() => { 182 | monacoJSXHighlighter.highlightCode(); 183 | }, 184 | debounceTime, 185 | ); 186 | }); 187 | 188 | // You can do the higlighting directly at anytime 189 | monacoJSXHighlighter.highlightCode(); 190 | // or customize its behavior by adding custom highlighting after the JSX highlighting 191 | const afterHighlight = ( 192 | ast // the ast generate by Babel 193 | ) => { 194 | //... your customization code, check Babel for more info about AST types 195 | //optional: array with the decorators created by the highlighter, push your decorator ids to this array 196 | monacoJSXHighlighter.JSXDecoratorIds.push(...yourdecoratorsIds); 197 | }; 198 | 199 | monacoJSXHighlighter.highlightCode( 200 | afterHighlight, //default: ast=>ast 201 | onError, // default: error=>console.error(error) 202 | getAstPromise, // default: parse(monacoEditor.getValue()) 203 | onParseErrors, // default: error=>error 204 | ); 205 | ``` 206 | 207 | Additionally, you can add JSX commenting to your monaco editor with 208 | `monacoJSXHighlighter.addJSXCommentCommand()`: 209 | comments in JSX children will result in `{/*...*/}` instead of `//...`. It mimics the commenting behavior of 210 | the [WebStorm IDE](https://www.jetbrains.com/webstorm/). 211 | 212 | Follow this code to find out other perks: 213 | 214 | ```js 215 | // Optional: Disable JSX commenting when needed (e.g. toggling, unmounting, pausing) 216 | const commentDisposeFunc = monacoJSXHighlighter.addJSXCommentCommand(); 217 | commentDisposeFunc(); // if you need to 218 | ``` 219 | 220 | ### Creating Monaco compatible ranges from Babel 221 | 222 | ```js 223 | import {configureLocToMonacoRange} from 'monaco-jsx-highlighter'; 224 | // locToMonacoRange: converts Babel locations to Monaco Ranges 225 | const locToMonacoRange = configureLocToMonacoRange(monaco); 226 | const monacoRange = locToMonacoRange(babelAstNode.loc); 227 | ``` 228 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-midnight -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monaco-jsx-highlighter", 3 | "version": "2.7.77", 4 | "description": "An extensible library to highlight JSX syntax in the Monaco Editor using Babel.", 5 | "author": "luminaxster", 6 | "license": "MIT", 7 | "bugs": { 8 | "url": "https://github.com/luminaxster/syntax-highlighter/issues" 9 | }, 10 | "homepage": "https://luminaxster.github.io/syntax-highlighter/", 11 | "main": "dist/cjs/monaco-jsx-highlighter.js", 12 | "module": "dist/es/monaco-jsx-highlighter.js", 13 | "scripts": { 14 | "build-dev": "webpack --env development", 15 | "build": "rollup -c --environment INCLUDE_DEPS,BUILD:production --bundleConfigAsCjs", 16 | "clean": "rimraf dist", 17 | "lint": "eslint src --fix", 18 | "prerelease": "npm run lint && npm run test && npm run clean && npm run build", 19 | "release": "npm publish . --access public", 20 | "test": "echo \"Warning: no test specified\" && exit 0" 21 | }, 22 | "peerDependencies": { 23 | "@babel/parser": "6.x || 7.x", 24 | "@babel/traverse": "6.x || 7.x", 25 | "monaco-editor": ">=0.21" 26 | }, 27 | "devDependencies": { 28 | "@babel/cli": "^7.13.0", 29 | "@babel/core": "^7.13.8", 30 | "@babel/parser": "^7.13.9", 31 | "@babel/preset-env": "^7.13.9", 32 | "@babel/preset-react": "^7.12.13", 33 | "@babel/traverse": "^7.13.0", 34 | "@typescript-eslint/eslint-plugin": "^6.19.1", 35 | "babel-eslint": "^10.1.0", 36 | "babel-loader": "^8.2.2", 37 | "css-loader": "^6.9.1", 38 | "eslint": "^8.56.0", 39 | "eslint-config-standard-with-typescript": "^43.0.1", 40 | "eslint-plugin-import": "^2.29.1", 41 | "eslint-plugin-n": "^16.6.2", 42 | "eslint-plugin-node": "^11.1.0", 43 | "eslint-plugin-promise": "^6.1.1", 44 | "eslint-plugin-react": "^7.33.2", 45 | "monaco-editor": "^0.23.0", 46 | "react-refresh": "^0.14.0", 47 | "rimraf": "^3.0.2", 48 | "rollup": "^4.9.6", 49 | "rollup-plugin-babel": "^5.0.0-alpha.2", 50 | "rollup-plugin-cleanup": "^3.2.1", 51 | "rollup-plugin-commonjs": "^10.1.0", 52 | "rollup-plugin-node-resolve": "^5.2.0", 53 | "rollup-plugin-postcss": "^4.0.2", 54 | "rollup-plugin-terser": "^7.0.2", 55 | "schema-utils": "^4.2.0", 56 | "style-loader": "^3.3.4", 57 | "typescript": "^5.3.3", 58 | "url-loader": "^4.1.1", 59 | "webpack": "^5.90.0", 60 | "webpack-cli": "^5.1.4" 61 | }, 62 | "repository": { 63 | "type": "git", 64 | "url": "git+https://github.com/luminaxster/syntax-highlighter.git" 65 | }, 66 | "keywords": [ 67 | "monaco", 68 | "editor", 69 | "babel", 70 | "jsx", 71 | "syntax", 72 | "color", 73 | "coding", 74 | "highlighting" 75 | ], 76 | "engines": { 77 | "node": ">=10", 78 | "npm": ">=7" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from https://github.com/reduxjs/redux/blob/master/rollup.config.js 3 | * Copyright (c) 2015-present Dan Abramov 4 | */ 5 | 6 | import nodeResolve from 'rollup-plugin-node-resolve'; 7 | import babel from 'rollup-plugin-babel'; 8 | import commonjs from 'rollup-plugin-commonjs'; 9 | import cleanup from 'rollup-plugin-cleanup'; 10 | import postcss from "rollup-plugin-postcss"; 11 | import {terser} from 'rollup-plugin-terser'; 12 | 13 | const isProduction = true; 14 | 15 | export default [ 16 | { 17 | input: 'src/index.js', 18 | output: 19 | [ 20 | { 21 | file: 'dist/cjs/monaco-jsx-highlighter.js', 22 | format: 'cjs', 23 | indent: false, 24 | sourcemap: true, 25 | exports: 'named', 26 | }, 27 | { 28 | file: 'dist/cjs/monaco-jsx-highlighter.min.js', 29 | format: 'iife', 30 | name: 'version', 31 | exports: 'named', 32 | plugins: [terser()] 33 | } 34 | ], 35 | external: ['prop-types', 'react'], 36 | plugins: [ 37 | postcss({minimize: isProduction}), 38 | nodeResolve({ 39 | mainFields: ['module', 'jsnext:main', 'main'], 40 | }), 41 | commonjs({ 42 | include: 'node_modules/**', 43 | }), 44 | babel({ 45 | babelHelpers: 'bundled' 46 | }), 47 | cleanup() 48 | ], 49 | }, 50 | { 51 | input: 'src/index.js', 52 | output: 53 | [ 54 | { 55 | file: 'dist/es/monaco-jsx-highlighter.js', 56 | format: 'es', 57 | indent: false, 58 | sourcemap: true, 59 | exports: 'named', 60 | }, 61 | { 62 | file: 'dist/es/monaco-jsx-highlighter.min.js', 63 | format: 'iife', 64 | name: 'version', 65 | exports: 'named', 66 | plugins: [terser()] 67 | } 68 | ], 69 | external: ['prop-types', 'react'], 70 | plugins: [ 71 | postcss({minimize: isProduction}), 72 | nodeResolve({ 73 | mainFields: ['module', 'jsnext:main', 'main'], 74 | }), 75 | commonjs({ 76 | include: 'node_modules/**', 77 | }), 78 | babel({ 79 | babelHelpers: 'bundled' 80 | }), 81 | cleanup() 82 | ], 83 | }, 84 | { 85 | input: 'src/index.js', 86 | output: 87 | [ 88 | { 89 | file: 'dist/umd/monaco-jsx-highlighter.js', 90 | format: 'umd', 91 | name: 'MonacoJSXHighlighter', 92 | indent: false, 93 | exports: 'named', 94 | sourcemap: true, 95 | }, 96 | { 97 | file: 'dist/umd/monaco-jsx-highlighter.min.js', 98 | format: 'iife', 99 | name: 'version', 100 | exports: 'named', 101 | plugins: [terser()] 102 | } 103 | ], 104 | plugins: [ 105 | postcss({minimize: isProduction}), 106 | nodeResolve({ 107 | mainFields: ['module', 'jsnext:main', 'main'], 108 | }), 109 | babel({ 110 | babelHelpers: 'bundled', 111 | exclude: 'node_modules/**', 112 | }), 113 | commonjs({ 114 | namedExports: { 115 | 'node_modules/react/index.js': [ 116 | 'useContext', 117 | 'useLayoutEffect', 118 | 'useCallback', 119 | 'useState', 120 | 'useMemo', 121 | 'createContext', 122 | 'memo', 123 | 'Children', 124 | ], 125 | }, 126 | }), 127 | cleanup() 128 | ], 129 | }, 130 | ]; 131 | -------------------------------------------------------------------------------- /src/MonacoJSXHighlighter.css: -------------------------------------------------------------------------------- 1 | .JSXElement.JSXIdentifier{ 2 | color: royalblue; 3 | } 4 | 5 | .JSXElement.JSXBracket { 6 | color: darkorange; 7 | } 8 | 9 | .JSXElement.JSXText { 10 | color: darkgoldenrod; 11 | } 12 | 13 | .JSXElement.JSXGlyph { 14 | background: cyan; 15 | opacity: 0.25; 16 | } 17 | 18 | .JSXOpeningFragment.JSXBracket { 19 | color: darkorange; 20 | font-weight: bold; 21 | } 22 | 23 | .JSXClosingFragment.JSXBracket { 24 | color: darkorange; 25 | font-weight: bold; 26 | } 27 | 28 | .JSXOpeningElement.JSXBracket { 29 | color: darkorange; 30 | font-weight: bold; 31 | } 32 | 33 | .JSXOpeningElement.JSXIdentifier { 34 | color: royalblue; 35 | } 36 | 37 | .JSXClosingElement.JSXBracket { 38 | color: darkorange; 39 | font-weight: lighter; 40 | } 41 | 42 | .JSXClosingElement.JSXIdentifier { 43 | color: royalblue; 44 | font-weight: lighter; 45 | } 46 | 47 | .JSXAttribute.JSXIdentifier { 48 | color: steelblue; 49 | } 50 | 51 | .JSXExpressionContainer.JSXBracket { 52 | color: darkorange; 53 | } 54 | 55 | .JSXSpreadChild.JSXBracket { 56 | color: darkorange; 57 | } 58 | 59 | .JSXSpreadAttribute.JSXBracket { 60 | color: darkorange; 61 | } 62 | -------------------------------------------------------------------------------- /src/MonacoJSXHighlighter.js: -------------------------------------------------------------------------------- 1 | import './MonacoJSXHighlighter.css' 2 | 3 | let monaco = null 4 | 5 | const defaultOptions = { 6 | parser: 'babel', 7 | isHighlightGlyph: false, 8 | iShowHover: false, 9 | isUseSeparateElementStyles: false, 10 | isThrowJSXParseErrors: false 11 | } 12 | 13 | export const configureLocToMonacoRange = ( 14 | _monaco = monaco, parser = 'babel' 15 | ) => { 16 | switch (parser) { 17 | case 'babel': 18 | default: 19 | return ( 20 | loc, 21 | startLineOffset = 0, 22 | startColumnOffset = 0, 23 | endLineOffset = 0, 24 | endColumnOffset = 0 25 | ) => { 26 | if (!loc?.start) { 27 | return new _monaco.Range(1, 1, 1, 1) 28 | } 29 | return new _monaco.Range( 30 | startLineOffset + loc.start.line, 31 | startColumnOffset + loc.start.column + 1, 32 | endLineOffset + loc.end 33 | ? loc.end.line 34 | : loc.start.line, 35 | endColumnOffset + loc.end 36 | ? loc.end.column + 1 37 | : loc.start.column + 1 38 | ) 39 | } 40 | } 41 | } 42 | 43 | export function makeJSXTraverse() { 44 | const jsxExpressions = [] 45 | const jsxTraverse = (path) => { 46 | if (path.type.toUpperCase().includes('JSX')) { 47 | jsxExpressions.push(path) 48 | } 49 | } 50 | 51 | const find = (type) => { 52 | return jsxExpressions.filter(p => p.type === type) 53 | } 54 | 55 | const findJSXElements = () => find('JSXElement') 56 | 57 | return { 58 | jsxExpressions, 59 | find, 60 | findJSXElements, 61 | jsxTraverse 62 | } 63 | } 64 | 65 | export function prepareOptions( 66 | path, 67 | jsxTypeOptions = {}, 68 | highlighterOptions = {} 69 | ) { 70 | return highlighterOptions.iShowHover 71 | ? {...jsxTypeOptions, ...{hoverMessage: `(${path.type})`}} 72 | : jsxTypeOptions 73 | } 74 | 75 | export const HIGHLIGHT_TYPE = { 76 | ELEMENT: 'ELEMENT', // jsx elements 77 | ALL: 'ALL', // the whole node's location, e.g. identifier names 78 | IDENTIFIER: 'IDENTIFIER', // JSX identifiers 79 | EDGE: 'EDGE', // only the starting and ending characters in node's 80 | // location e.g. spread child or attribute, container expressions 81 | STYLE: 'STYLE' // for styling only, not used by node locations 82 | } 83 | 84 | export const HIGHLIGHT_MODE = { 85 | [HIGHLIGHT_TYPE.ELEMENT]: ( 86 | path, 87 | jsxTypeOptions, 88 | decorators = [], 89 | highlighterOptions, 90 | locToMonacoRange 91 | ) => { 92 | const loc = path.node.loc 93 | const openingElement = path.node.openingElement 94 | let elementName = null 95 | if (openingElement) { 96 | elementName = openingElement.name.name 97 | 98 | const startLoc = { 99 | start: {...openingElement.loc.start}, 100 | end: {...openingElement.name.loc.start} 101 | } 102 | 103 | const endLoc = { 104 | start: {...openingElement.loc.end}, 105 | end: {...openingElement.loc.end} 106 | } 107 | endLoc.start.column-- 108 | 109 | if (openingElement.selfClosing) { 110 | endLoc.start.column-- 111 | } 112 | 113 | decorators.push({ 114 | range: locToMonacoRange(startLoc), 115 | options: highlighterOptions.isUseSeparateElementStyles 116 | ? JSXTypes.JSXBracket.openingElementOptions 117 | : JSXTypes.JSXBracket.options 118 | }) 119 | 120 | decorators.push({ 121 | range: locToMonacoRange(endLoc), 122 | options: highlighterOptions.isUseSeparateElementStyles 123 | ? JSXTypes.JSXBracket.openingElementOptions 124 | : JSXTypes.JSXBracket.options 125 | }) 126 | } 127 | 128 | const closingElement = path.node.closingElement 129 | if (closingElement) { 130 | const startLoc = { 131 | start: {...closingElement.loc.start}, 132 | end: {...closingElement.name.loc.start} 133 | } 134 | 135 | const endLoc = { 136 | start: {...closingElement.loc.end}, 137 | end: {...closingElement.loc.end} 138 | } 139 | endLoc.start.column-- 140 | 141 | decorators.push({ 142 | range: locToMonacoRange(startLoc), 143 | options: highlighterOptions.isUseSeparateElementStyles 144 | ? JSXTypes.JSXBracket.closingElementOptions 145 | : JSXTypes.JSXBracket.options 146 | }) 147 | decorators.push({ 148 | range: locToMonacoRange(endLoc), 149 | options: highlighterOptions.isUseSeparateElementStyles 150 | ? JSXTypes.JSXBracket.closingElementOptions 151 | : JSXTypes.JSXBracket.options 152 | }) 153 | } 154 | 155 | highlighterOptions.isHighlightGlyph && decorators.push({ 156 | range: locToMonacoRange(loc), 157 | options: JSXTypes.JSXElement.options(elementName) 158 | }) 159 | }, 160 | [HIGHLIGHT_TYPE.ALL]: ( 161 | path, 162 | jsxTypeOptions, 163 | decorators = [], 164 | highlighterOptions, 165 | locToMonacoRange 166 | ) => { 167 | const loc = { 168 | start: {...path.node.loc.start}, 169 | end: {...path.node.loc.end} 170 | } 171 | 172 | if (path.key === 'object') { 173 | loc.end = {...path.container.property.loc.start} 174 | } 175 | locToMonacoRange && decorators.push({ 176 | range: locToMonacoRange(loc), 177 | options: prepareOptions(path, jsxTypeOptions, highlighterOptions) 178 | }) 179 | return decorators 180 | }, 181 | [HIGHLIGHT_TYPE.IDENTIFIER]: ( 182 | path, 183 | jsxTypeOptions = {}, 184 | decorators = [], 185 | highlighterOptions = {}, 186 | locToMonacoRange 187 | ) => { 188 | if ( 189 | path.key === 'object' || 190 | path.key === 'property' || 191 | path.key === 'name' || 192 | path.key === 'namespace' 193 | ) { 194 | HIGHLIGHT_MODE[HIGHLIGHT_TYPE.ALL]( 195 | path, 196 | path.parentPath?.isJSXAttribute() 197 | ? JSXTypes.JSXAttribute.options 198 | : jsxTypeOptions, 199 | decorators, 200 | highlighterOptions, 201 | locToMonacoRange 202 | ) 203 | } 204 | return decorators 205 | }, 206 | [HIGHLIGHT_TYPE.EDGE]: ( 207 | path, 208 | jsxTypeOptions, 209 | decorators = [], 210 | highlighterOptions, 211 | locToMonacoRange 212 | ) => { 213 | const options = prepareOptions(path, jsxTypeOptions, highlighterOptions) 214 | 215 | const loc = path.node.loc 216 | const innerLocKey = 217 | path.isJSXSpreadChild() 218 | ? 'expression' 219 | : path.isJSXSpreadAttribute() ? 'argument' : null 220 | 221 | let innerLoc = null 222 | 223 | if (innerLocKey) { 224 | const innerNode = path.node[innerLocKey] 225 | innerLoc = { 226 | start: {...innerNode.loc.start}, 227 | end: {...innerNode.loc.end} 228 | } 229 | if (innerNode.extra?.parenthesized) { 230 | innerLoc.start.column-- 231 | innerLoc.end.column++ 232 | } 233 | } else { 234 | innerLoc = {start: {...loc.start}, end: {...loc.end}} 235 | innerLoc.start.column++ 236 | innerLoc.end.column-- 237 | } 238 | 239 | const startEdgeLoc = {start: {...loc.start}, end: {...innerLoc.start}} 240 | 241 | const endEdgeLoc = {start: {...innerLoc.end}, end: {...loc.end}} 242 | 243 | decorators.push({ 244 | range: locToMonacoRange(startEdgeLoc), 245 | options 246 | }) 247 | decorators.push({ 248 | range: locToMonacoRange(endEdgeLoc), 249 | options 250 | }) 251 | 252 | return decorators 253 | }, 254 | [HIGHLIGHT_TYPE.STYLE]: () => [] // noop 255 | } 256 | 257 | export const JSXTypes = { 258 | JSXIdentifier: { 259 | highlightScope: HIGHLIGHT_TYPE.IDENTIFIER, 260 | options: { 261 | inlineClassName: 'JSXElement.JSXIdentifier' 262 | } 263 | }, 264 | JSXOpeningFragment: { 265 | highlightScope: HIGHLIGHT_TYPE.ALL, 266 | options: { 267 | inlineClassName: 'JSXOpeningFragment.JSXBracket' 268 | } 269 | }, 270 | JSXClosingFragment: { 271 | highlightScope: HIGHLIGHT_TYPE.ALL, 272 | options: { 273 | inlineClassName: 'JSXClosingFragment.JSXBracket' 274 | } 275 | }, 276 | JSXText: { 277 | highlightScope: HIGHLIGHT_TYPE.ALL, 278 | options: { 279 | inlineClassName: 'JSXElement.JSXText' 280 | } 281 | }, 282 | JSXExpressionContainer: { 283 | highlightScope: HIGHLIGHT_TYPE.EDGE, 284 | options: { 285 | inlineClassName: 'JSXExpressionContainer.JSXBracket' 286 | } 287 | }, 288 | JSXSpreadChild: { 289 | highlightScope: HIGHLIGHT_TYPE.EDGE, 290 | options: { 291 | inlineClassName: 'JSXSpreadChild.JSXBracket' 292 | } 293 | }, 294 | JSXSpreadAttribute: { 295 | highlightScope: HIGHLIGHT_TYPE.EDGE, 296 | options: { 297 | inlineClassName: 'JSXSpreadAttribute.JSXBracket' 298 | } 299 | }, 300 | JSXElement: { 301 | highlightScope: HIGHLIGHT_TYPE.STYLE, 302 | options: (elementName) => ( 303 | { 304 | glyphMarginClassName: 'JSXElement.JSXGlyph', 305 | glyphMarginHoverMessage: 306 | `JSX Element${elementName ? ': ' + elementName : ''}` 307 | } 308 | ) 309 | }, 310 | JSXBracket: { 311 | highlightScope: HIGHLIGHT_TYPE.STYLE, 312 | options: { 313 | inlineClassName: 'JSXElement.JSXBracket' 314 | }, 315 | openingElementOptions: { 316 | inlineClassName: 'JSXOpeningElement.JSXBracket' 317 | }, 318 | closingElementOptions: { 319 | inlineClassName: 'JSXClosingElement.JSXBracket' 320 | } 321 | }, 322 | JSXOpeningElement: { 323 | highlightScope: HIGHLIGHT_TYPE.STYLE, 324 | options: { 325 | inlineClassName: 'JSXOpeningElement.JSXIdentifier' 326 | } 327 | }, 328 | JSXClosingElement: { 329 | highlightScope: HIGHLIGHT_TYPE.STYLE, 330 | options: { 331 | inlineClassName: 'JSXClosingElement.JSXIdentifier' 332 | } 333 | }, 334 | JSXAttribute: { 335 | highlightScope: HIGHLIGHT_TYPE.STYLE, 336 | options: { 337 | inlineClassName: 'JSXAttribute.JSXIdentifier' 338 | } 339 | } 340 | } 341 | 342 | export const JSXCommentContexts = { 343 | JS: 'JS', 344 | JSX: 'JSX' 345 | } 346 | 347 | export const COMMENT_ACTION_ID = 'editor.action.commentLine' 348 | 349 | // thanks https://github.com/HaimCandiTech 350 | export const makeBabelParse = (parse, enableTsxHighlight) => { 351 | return (code, options = {}) => { 352 | return parse( 353 | code, 354 | { 355 | sourceType: 'module', 356 | plugins: ['jsx', ...(enableTsxHighlight ? ['typescript'] : [])], 357 | errorRecovery: true, 358 | ...options 359 | }) 360 | } 361 | } 362 | 363 | class MonacoJSXHighlighter { 364 | constructor( 365 | globalMonaco, 366 | parse, 367 | traverse, 368 | monacoEditor, 369 | options = {} 370 | ) { 371 | monaco = globalMonaco; 372 | this.monaco = monaco; 373 | this.parse = parse; 374 | this.traverse = traverse; 375 | this.monacoEditor = monacoEditor; 376 | this.options = {...defaultOptions, ...options} 377 | this.locToMonacoRange = configureLocToMonacoRange(monaco, this.options.parser) 378 | //avoiding breaking changes form v1s 379 | this.highLightOnDidChangeModelContent = this.highlightOnDidChangeModelContent; 380 | this.bindMethods(); 381 | 382 | this._isHighlightBoundToModelContentChanges = false 383 | this._isJSXCommentCommandActive = false 384 | 385 | this.resetState() 386 | } 387 | 388 | bindMethods() { 389 | const methods = [ 390 | 'resetState', 'resetDeltaDecorations', 'getAstPromise', 'highlightOnDidChangeModelContent', 'highlight', 391 | 'highlightCode', 'createDecoratorsByType', 'createJSXElementDecorators', 'extractAllDecorators', 'jsxTraverseAst', 392 | 'getJSXContext', 'runJSXCommentContextAndAction', 'addJSXCommentCommand', 'highLightOnDidChangeModelContent' 393 | ] 394 | methods.forEach(method => this[method] = this[method].bind(this)); 395 | } 396 | 397 | resetState() { 398 | this.prevEditorValue = null 399 | this.editorValue = null 400 | this.ast = null 401 | this.jsxManager = null 402 | } 403 | 404 | resetDeltaDecorations() { 405 | const model = this.monacoEditor?.getModel(); 406 | if(model.deltaDecorations){ 407 | this.JSXDecoratorIds = (model.deltaDecorations( 408 | this.JSXDecoratorIds ?? [], 409 | [] 410 | ) 411 | ) 412 | }else{ 413 | this.JSXDecoratorIds = []; 414 | } 415 | } 416 | 417 | async getAstPromise(forceUpdate) { 418 | return await new Promise((resolve) => { 419 | if ( 420 | !!forceUpdate || 421 | !this.editorValue || 422 | this.editorValue !== this.prevEditorValue 423 | ) { 424 | this.prevEditorValue = this.editorValue 425 | this.editorValue = this.monacoEditor.getValue() 426 | try { 427 | this.ast = this.parse(this.editorValue) 428 | } catch (e) { 429 | if ( 430 | e instanceof SyntaxError && 431 | !e.message.includes('JSX') 432 | ) { 433 | this.resetState() 434 | throw e 435 | } else { 436 | if (this.options.isThrowJSXParseErrors) { 437 | throw e 438 | } else { 439 | resolve(this.ast) 440 | } 441 | } 442 | } 443 | } 444 | resolve(this.ast) 445 | }) 446 | } 447 | 448 | highlightOnDidChangeModelContent( 449 | debounceTime = 100, 450 | afterHighlight = ast => ast, 451 | onHighlightError = error => { 452 | console.error(error) 453 | }, 454 | getAstPromise, 455 | onParseAstError = error => { 456 | console.log(error) 457 | } 458 | ) { 459 | getAstPromise = getAstPromise ?? this.getAstPromise 460 | const highlightCallback = () => { 461 | this.highlightCode( 462 | afterHighlight, 463 | onHighlightError, 464 | getAstPromise, 465 | onParseAstError 466 | ) 467 | } 468 | 469 | highlightCallback() 470 | 471 | const tid = null 472 | 473 | let highlighterDisposer = { 474 | onDidChangeModelContentDisposer: 475 | this.monacoEditor.onDidChangeModelContent( 476 | () => { 477 | clearTimeout(tid) 478 | setTimeout( 479 | highlightCallback, 480 | debounceTime 481 | ) 482 | }), 483 | onDidChangeModelDisposer: this.monacoEditor.onDidChangeModel( 484 | () => { 485 | highlightCallback() 486 | }) 487 | } 488 | 489 | highlighterDisposer.dispose = () => { 490 | highlighterDisposer.onDidChangeModelContentDisposer.dispose() 491 | highlighterDisposer.onDidChangeModelDisposer.dispose() 492 | } 493 | 494 | this._isHighlightBoundToModelContentChanges = true 495 | 496 | const onDispose = () => { 497 | clearTimeout(tid) 498 | this.resetState() 499 | this.resetDeltaDecorations() 500 | if ( 501 | !this._isHighlightBoundToModelContentChanges 502 | ) { 503 | return 504 | } 505 | this._isHighlightBoundToModelContentChanges = false 506 | highlighterDisposer?.dispose() 507 | highlighterDisposer = null 508 | } 509 | 510 | this.monacoEditor.onDidDispose(() => { 511 | clearTimeout(tid) 512 | this.resetDeltaDecorations() 513 | highlighterDisposer = null 514 | this._isHighlightBoundToModelContentChanges = false 515 | }) 516 | return onDispose 517 | } 518 | 519 | highlightCode( 520 | afterHighlight = ast => ast, 521 | onError = error => { 522 | console.error(error) 523 | }, 524 | getAstPromise, 525 | onJsParserErrors = error => error 526 | ) { 527 | getAstPromise = getAstPromise ?? this.getAstPromise 528 | return ( 529 | getAstPromise() 530 | .then(async ast => await this.highlight(ast)) 531 | .catch(onJsParserErrors) 532 | ) 533 | .then(afterHighlight) 534 | .catch(onError) 535 | } 536 | 537 | jsxTraverseAst(ast, traverse) { 538 | traverse = traverse ?? this.traverse; 539 | 540 | const jsxManager = makeJSXTraverse() 541 | traverse(ast, {enter: jsxManager.jsxTraverse}) 542 | return jsxManager 543 | } 544 | 545 | async highlight(ast, jsxTraverseAst) { 546 | jsxTraverseAst = jsxTraverseAst ?? this.jsxTraverseAst; 547 | 548 | return await new Promise((resolve) => { 549 | if (ast) { 550 | this.jsxManager = jsxTraverseAst(ast) 551 | this.decorators = this.extractAllDecorators(this.jsxManager) 552 | } 553 | resolve(ast) 554 | }) 555 | } 556 | 557 | createDecoratorsByType( 558 | jsxManager, 559 | jsxType, 560 | jsxTypeOptions, 561 | highlightScope, 562 | decorators = [], 563 | highlighterOptions, 564 | locToMonacoRange 565 | ) { 566 | highlighterOptions = highlighterOptions ?? this.options 567 | locToMonacoRange = locToMonacoRange ?? this.locToMonacoRange 568 | jsxManager?.find(jsxType).forEach(path => HIGHLIGHT_MODE[highlightScope]( 569 | path, 570 | jsxTypeOptions, 571 | decorators, 572 | highlighterOptions, 573 | locToMonacoRange 574 | ) 575 | ) 576 | 577 | return decorators 578 | } 579 | 580 | createJSXElementDecorators( 581 | jsxManager, 582 | decorators = [], 583 | highlighterOptions, 584 | locToMonacoRange 585 | ) { 586 | highlighterOptions = highlighterOptions ?? this.options 587 | locToMonacoRange = locToMonacoRange ?? this.locToMonacoRange 588 | jsxManager?.findJSXElements().forEach(path => HIGHLIGHT_MODE.ELEMENT( 589 | path, 590 | null, 591 | decorators, 592 | highlighterOptions, 593 | locToMonacoRange 594 | )) 595 | return decorators 596 | } 597 | 598 | extractAllDecorators(jsxManager) { 599 | jsxManager = jsxManager ?? this.jsxManager 600 | const decorators = this.createJSXElementDecorators(jsxManager) 601 | for (const jsxType in JSXTypes) { 602 | this.createDecoratorsByType( 603 | jsxManager, 604 | jsxType, 605 | JSXTypes[jsxType].options, 606 | JSXTypes[jsxType].highlightScope, 607 | decorators 608 | ) 609 | } 610 | 611 | this.JSXDecoratorIds = 612 | this.monacoEditor.getModel().deltaDecorations( 613 | this.JSXDecoratorIds ?? [], 614 | decorators 615 | ) 616 | return decorators 617 | } 618 | 619 | getJSXContext( 620 | selection, 621 | ast, 622 | monacoEditor, 623 | locToMonacoRange, 624 | jsxTraverseAst 625 | ) { 626 | monacoEditor = monacoEditor ?? this.monacoEditor 627 | locToMonacoRange = locToMonacoRange ?? this.locToMonacoRange 628 | jsxTraverseAst = jsxTraverseAst ?? this.jsxTraverseAst 629 | let jsxManager = ast ? this.jsxManager : null 630 | if (!this._isHighlightBoundToModelContentChanges) { 631 | jsxManager = ast ? jsxTraverseAst(ast) : null 632 | } 633 | 634 | if (!jsxManager) { 635 | return JSXCommentContexts.JS 636 | } 637 | 638 | let startColumn = 639 | monacoEditor.getModel().getLineFirstNonWhitespaceColumn( 640 | selection.startLineNumber 641 | ) 642 | 643 | const commentableRange = new monaco.Range( 644 | selection.startLineNumber, 645 | startColumn, 646 | selection.startLineNumber, 647 | startColumn 648 | ) 649 | 650 | startColumn = startColumn ? startColumn - 1 : 0 651 | const containingRange = new monaco.Range( 652 | selection.startLineNumber, 653 | startColumn, 654 | selection.startLineNumber, 655 | startColumn 656 | ) 657 | 658 | let minRange = null 659 | let minCommentableRange = null 660 | let path = null 661 | let commentablePath = null 662 | 663 | jsxManager.jsxExpressions.forEach(p => { 664 | const jsxRange = locToMonacoRange(p.node.loc) 665 | if ((p.key === 'name' || p.key === 'property') && 666 | p.isJSXIdentifier() && 667 | jsxRange.intersectRanges(commentableRange)) { 668 | if ( 669 | !minCommentableRange || 670 | minCommentableRange.containsRange(jsxRange) 671 | ) { 672 | minCommentableRange = jsxRange 673 | commentablePath = p 674 | } 675 | } 676 | if (jsxRange.intersectRanges(containingRange)) { 677 | if (!minRange || minRange.containsRange(jsxRange)) { 678 | minRange = jsxRange 679 | path = p 680 | } 681 | } 682 | }) 683 | 684 | if (!path || path.isJSXExpressionContainer() || commentablePath) { 685 | return JSXCommentContexts.JS 686 | } else { 687 | return JSXCommentContexts.JSX 688 | } 689 | } 690 | 691 | async runJSXCommentContextAndAction( 692 | selection, 693 | getAstPromise, 694 | onParseErrors = error => error, 695 | editor, 696 | runJsxCommentAction 697 | ) { 698 | getAstPromise = getAstPromise ?? this.getAstPromise 699 | editor = editor ?? this.monacoEditor 700 | 701 | return await new Promise((resolve) => { 702 | if (this._isHighlightBoundToModelContentChanges) { 703 | resolve( 704 | runJsxCommentAction( 705 | this.getJSXContext(selection, this.ast, editor) 706 | ) 707 | ) 708 | } else { 709 | getAstPromise().then(ast => { 710 | resolve( 711 | runJsxCommentAction( 712 | this.getJSXContext(selection, ast, editor) 713 | ) 714 | ) 715 | }) 716 | .catch( 717 | (error) => resolve( 718 | runJsxCommentAction( 719 | this.getJSXContext(selection, null, editor) 720 | ) 721 | ) ?? onParseErrors(error) 722 | ) 723 | } 724 | } 725 | ).catch(error => ( 726 | runJsxCommentAction( 727 | this.getJSXContext(selection, null, editor)) ?? 728 | onParseErrors(error) 729 | ) 730 | ) 731 | } 732 | 733 | addJSXCommentCommand( 734 | getAstPromise, 735 | onParseErrors = error => error, 736 | editor 737 | ) { 738 | getAstPromise = getAstPromise ?? this.getAstPromise 739 | editor = editor ?? this.monacoEditor 740 | 741 | if (this._editorCommandId) { 742 | this._isJSXCommentCommandActive = true 743 | return this.editorCommandOnDispose 744 | } 745 | 746 | this._editorCommandId = editor.addCommand( 747 | monaco.KeyMod.CtrlCmd | monaco.KeyCode.US_SLASH, 748 | () => { 749 | if (!this._isJSXCommentCommandActive) { 750 | editor.getAction(COMMENT_ACTION_ID).run() 751 | return 752 | } 753 | const selection = editor.getSelection() 754 | const model = editor.getModel() 755 | 756 | const jsCommentRange = new monaco.Range( 757 | selection.startLineNumber, 758 | model.getLineFirstNonWhitespaceColumn(selection.startLineNumber), 759 | selection.startLineNumber, 760 | model.getLineMaxColumn(selection.startLineNumber) 761 | ) 762 | const jsCommentText = model.getValueInRange(jsCommentRange) 763 | 764 | if (jsCommentText.match(/^\s*\/[/*]/)) { 765 | editor.getAction(COMMENT_ACTION_ID).run() 766 | this.resetState() 767 | return 768 | } 769 | 770 | const runJsxCommentAction = (commentContext) => { 771 | let isUnCommentAction = true 772 | const commentsData = [] 773 | 774 | for (let i = selection.startLineNumber; 775 | i <= selection.endLineNumber; 776 | i++) { 777 | const commentRange = new monaco.Range( 778 | i, 779 | model.getLineFirstNonWhitespaceColumn(i), 780 | i, 781 | model.getLineMaxColumn(i) 782 | ) 783 | 784 | const commentText = model.getValueInRange(commentRange) 785 | 786 | commentsData.push({ 787 | commentRange, 788 | commentText 789 | }) 790 | 791 | isUnCommentAction = isUnCommentAction && 792 | !!commentText.match(/{\/\*/) 793 | } 794 | 795 | if (commentContext !== JSXCommentContexts.JSX && 796 | !isUnCommentAction) { 797 | editor.getAction(COMMENT_ACTION_ID).run() 798 | this.resetState() 799 | return 800 | } 801 | 802 | const editOperations = [] 803 | let commentsDataIndex = 0 804 | 805 | for (let i = selection.startLineNumber; 806 | i <= selection.endLineNumber; 807 | i++) { 808 | let { 809 | commentText, 810 | commentRange 811 | } = commentsData[commentsDataIndex++] 812 | 813 | if (isUnCommentAction) { 814 | commentText = commentText.replace(/{\/\*/, '') 815 | commentText = commentText.replace(/\*\/}/, '') 816 | } else { 817 | commentText = `{/*${commentText}*/}` 818 | } 819 | 820 | editOperations.push({ 821 | identifier: {major: 1, minor: 1}, 822 | range: commentRange, 823 | text: commentText, 824 | forceMoveMarkers: true 825 | }) 826 | } 827 | ;(editOperations.length > 0) && 828 | editor.executeEdits(this._editorCommandId, editOperations) 829 | } 830 | 831 | this.runJSXCommentContextAndAction( 832 | selection, 833 | getAstPromise, 834 | onParseErrors, 835 | editor, 836 | runJsxCommentAction 837 | ).catch(onParseErrors) 838 | }) 839 | 840 | this.editorCommandOnDispose = () => { 841 | this._isJSXCommentCommandActive = false 842 | } 843 | 844 | this._isJSXCommentCommandActive = true 845 | 846 | editor.onDidDispose(this.editorCommandOnDispose) 847 | 848 | return this.editorCommandOnDispose 849 | } 850 | } 851 | 852 | // "standard-with-typescript", 853 | // "plugin:react/recommended" 854 | 855 | export default MonacoJSXHighlighter 856 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { default as MonacoJSXHighlighter } from './MonacoJSXHighlighter' 2 | export * from './MonacoJSXHighlighter' 3 | export default MonacoJSXHighlighter 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // now using rollup 2 | const path = require('path'); 3 | 4 | module.exports = function (env) { 5 | const devMode = !env.production; 6 | 7 | return { 8 | mode: devMode ? 'development' : 'production', 9 | entry: path.resolve(__dirname, 'src/index.js'), 10 | output: { 11 | path: path.resolve(__dirname, 'build'), 12 | publicPath: 'build/', 13 | filename: 'monaco-jsx-highlighter.js', 14 | sourceMapFilename: 'monaco-jsx-highlighter.map', 15 | library: 'MonacoJSXHighlighter', 16 | libraryTarget: 'umd', 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.(js|jsx)$/i, 22 | exclude: /node_modules/, 23 | use: { 24 | loader: "babel-loader", 25 | options: { 26 | presets: [ 27 | '@babel/preset-env', 28 | '@babel/preset-react', 29 | ] 30 | } 31 | } 32 | }, 33 | { 34 | test: /\.css$/i, 35 | exclude: /node_modules/, 36 | use: [ 37 | 'style-loader', 38 | 'css-loader' 39 | ] 40 | }, 41 | { 42 | test: /\.(png|jpg|gif)$/i, 43 | exclude: /node_modules/, 44 | use: { 45 | loader: 'url-loader', 46 | options: { 47 | limit: 8192 48 | } 49 | } 50 | } 51 | ], 52 | }, 53 | resolve: { 54 | extensions: ['.js'] 55 | }, 56 | } 57 | }; 58 | --------------------------------------------------------------------------------