├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── gh-pages.yml │ ├── nodejs-ci.yml │ └── nodejs-publish.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ ├── CodeEditor-test.js ├── ErrorBoundary-test.js ├── Preview-test.js ├── Renderer-test.js └── parseHTML-test.js ├── babel.config.js ├── docs ├── example.md ├── gh-pages.js ├── index.html ├── index.tsx ├── styles │ ├── index.less │ └── markdown.less └── webpack.config.js ├── gulpfile.js ├── jest.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── setup-jest.js ├── src ├── CodeEditor.tsx ├── CodeView.tsx ├── CopyCodeButton.tsx ├── ErrorBoundary.tsx ├── MarkdownRenderer.tsx ├── Preview.tsx ├── Renderer.tsx ├── icons │ ├── Check.tsx │ ├── Code.tsx │ └── Copy.tsx ├── index.ts ├── less │ ├── index.less │ ├── styles.less │ ├── themes │ │ └── hljs-atom-one-dark-syntax.less │ └── vendors.less └── utils │ ├── canUseDOM.ts │ ├── evalCode.ts │ ├── mergeRefs.ts │ ├── parseDom.ts │ └── parseHTML.ts ├── tsconfig.json └── webpack-md-loader ├── html-loader.js ├── index.js ├── loader.js ├── options.json └── renderer.js /.eslintignore: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const OFF = 0; 2 | const WARNING = 1; 3 | const ERROR = 2; 4 | 5 | module.exports = { 6 | env: { 7 | browser: true, 8 | es6: true 9 | }, 10 | parser: '@typescript-eslint/parser', 11 | extends: [ 12 | 'plugin:@typescript-eslint/recommended', 13 | 'plugin:react/recommended', 14 | 'plugin:react-hooks/recommended', 15 | 'prettier' 16 | ], 17 | parserOptions: {}, 18 | plugins: ['@typescript-eslint', 'react'], 19 | rules: { 20 | semi: [ERROR, 'always'], 21 | 'space-infix-ops': ERROR, 22 | 'prefer-spread': ERROR, 23 | 'no-multi-spaces': ERROR, 24 | 'class-methods-use-this': WARNING, 25 | 'arrow-parens': [ERROR, 'as-needed'], 26 | '@typescript-eslint/no-unused-vars': ERROR, 27 | '@typescript-eslint/no-explicit-any': OFF, 28 | '@typescript-eslint/explicit-function-return-type': OFF, 29 | '@typescript-eslint/explicit-member-accessibility': OFF, 30 | '@typescript-eslint/no-namespace': OFF, 31 | '@typescript-eslint/explicit-module-boundary-types': OFF, 32 | 'react/display-name': OFF, 33 | 'react/prop-types': OFF 34 | }, 35 | settings: { 36 | react: { 37 | version: 'detect' 38 | } 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-20.04 14 | permissions: 15 | contents: write 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Setup node 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: 'lts/*' 25 | 26 | - name: Install 27 | run: npm install 28 | 29 | - name: Build 30 | run: npm run build:docs 31 | 32 | - name: Deploy 33 | uses: peaceiris/actions-gh-pages@v3 34 | if: github.ref == 'refs/heads/main' 35 | with: 36 | github_token: ${{ secrets.GITHUB_TOKEN }} 37 | publish_dir: ./docs/assets 38 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, 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: 9 | - main 10 | pull_request: 11 | branches: 12 | - main 13 | jobs: 14 | test: 15 | name: 'Test' 16 | runs-on: ubuntu-latest 17 | container: node:16 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - env: 22 | HUSKY: 0 23 | run: npm install 24 | - run: npm test 25 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-publish.yml: -------------------------------------------------------------------------------- 1 | # see https://help.github.com/cn/actions/language-and-framework-guides/publishing-nodejs-packages 2 | 3 | name: Node.js Package 4 | 5 | on: 6 | push: 7 | tags: ['*'] 8 | 9 | jobs: 10 | publish: 11 | name: 'Publish' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | # Setup .npmrc file to publish to npm 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: '14.x' 19 | registry-url: 'https://registry.npmjs.org' 20 | 21 | - name: Install dependencies 22 | run: npm install 23 | 24 | - name: Build 25 | run: npm run build 26 | 27 | - name: Npm Publish 28 | run: npm publish dist 29 | env: 30 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | karma-* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | node_modules 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | .vscode 37 | .DS_Store 38 | build 39 | lib 40 | 41 | dist 42 | assets 43 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm test 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | tabWidth: 2, 4 | singleQuote: true, 5 | arrowParens: 'avoid', 6 | trailingComma: 'none' 7 | }; 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.4.1](https://github.com/simonguo/react-code-view/compare/2.4.0...2.4.1) (2025-02-18) 2 | 3 | 4 | 5 | # [2.4.0](https://github.com/simonguo/react-code-view/compare/2.3.1...2.4.0) (2024-04-17) 6 | 7 | 8 | ### Features 9 | 10 | * add support for `renderExtraFooter` and `onOpenEditor` and `onCloseEditor` ([#55](https://github.com/simonguo/react-code-view/issues/55)) ([886d862](https://github.com/simonguo/react-code-view/commit/886d8624b6efca9a6753293e73d3891e1cea3018)) 11 | 12 | 13 | ## [2.3.1](https://github.com/simonguo/react-code-view/compare/2.3.0...2.3.1) (2024-01-26) 14 | 15 | 16 | ### Bug Fixes 17 | 18 | * fix HTML parsing error ([#54](https://github.com/simonguo/react-code-view/issues/54)) ([5c1aa7f](https://github.com/simonguo/react-code-view/commit/5c1aa7f3a0c6401284490fa653edb6263049a7e1)) 19 | 20 | 21 | 22 | # [2.3.0](https://github.com/simonguo/react-code-view/compare/2.2.1...2.3.0) (2024-01-26) 23 | 24 | 25 | ### Features 26 | 27 | * add support for one-click copy code ([#53](https://github.com/simonguo/react-code-view/issues/53)) ([f379146](https://github.com/simonguo/react-code-view/commit/f3791464b6c55c325c50d89ed50953c15c2e08e6)) 28 | 29 | 30 | 31 | ## [2.2.1](https://github.com/simonguo/react-code-view/compare/2.2.0...2.2.1) (2022-09-07) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * **CodeEditor:** fix Codemirror being initialized twice ([#40](https://github.com/simonguo/react-code-view/issues/40)) ([bb4aa2c](https://github.com/simonguo/react-code-view/commit/bb4aa2cc3088b1ac64ca94a58ad04aa69f0e8dea)) 37 | 38 | 39 | 40 | # [2.2.0](https://github.com/simonguo/react-code-view/compare/2.1.0...2.2.0) (2022-08-01) 41 | 42 | ### Bug Fixes 43 | 44 | - **Renderer:** fix timely re-renders ([#39](https://github.com/simonguo/react-code-view/issues/39)) ([cf77850](https://github.com/simonguo/react-code-view/commit/cf77850e046baf131a54f0d5ace062990671ef39)) 45 | 46 | ### Performance Improvements 47 | 48 | - **transform:** use sucrase instead of @swc/wasm-web to improve transcoding performance [#38](https://github.com/simonguo/react-code-view/issues/38) 49 | 50 | # [2.1.0](https://github.com/simonguo/react-code-view/compare/2.0.0...2.1.0) (2022-07-12) 51 | 52 | ### Features 53 | 54 | - support for rendering multiple examples in markdown ([#35](https://github.com/simonguo/react-code-view/issues/35)) ([d021789](https://github.com/simonguo/react-code-view/commit/d021789b8ebcd540c54f34131c1aa1a1be79a442)) 55 | 56 | # 2.0.0 57 | 58 | ## Features 59 | 60 | ### Use @swc/wasm-web instead of babel to compile code in the browser. 61 | 62 | Importing `babel.min.js` on the page will no longer be required. [How it is used in v1](https://github.com/simonguo/react-code-view/blob/1.2.6/README.md#add-babel) 63 | 64 | ### Refactored webpack loader for markdown. 65 | 66 | ```js 67 | 68 | // v1 69 | 70 | export default { 71 | module: { 72 | rules: [ 73 | { 74 | test: /\.md$/, 75 | use: [{ 76 | loader: 'html-loader' 77 | }, { 78 | loader: 'markdown-loader', 79 | options: { 80 | renderer: markdownRenderer() 81 | } 82 | }] 83 | } 84 | ] 85 | 86 | } 87 | }; 88 | 89 | // v2 90 | export default { 91 | module: { 92 | rules: [ 93 | { 94 | test: /\.md$/, 95 | use:[ 96 | loader: 'react-code-view/webpack-md-loader', 97 | options:{ 98 | parseLanguages: ['typescript','rust'] 99 | } 100 | ] 101 | } 102 | ] 103 | } 104 | }; 105 | ``` 106 | 107 | ### Props redefined 108 | 109 | **v1** 110 | 111 | | Name | Type | Default value | Description | 112 | | --------------------- | -------- | ------------------------------------------- | ------------------------------------------------- | 113 | | babelTransformOptions | Object | { presets: ['stage-0', 'react', 'es2015'] } | Babel configuration parameters [options][babeljs] | 114 | | dependencies | Object | | Dependent components. | 115 | | renderToolbar | Function | | Custom toolbar. | 116 | | showCode | boolean | true | Display code. | 117 | | theme | string | 'light' | Theme, options `light` and `dark`. | 118 | 119 | **v2** 120 | 121 | | Name | Type | Default value | Description | 122 | | -------------- | --------------------------------- | ----------------------- | ------------------------------------------------------------------------- | 123 | | afterCompile | (code: string) => string | | Executed after compiling the code | 124 | | beforeCompile | (code: string) => string | | Executed before compiling the code | 125 | | children | any | | The code to be rendered is executed. Usually imported via markdown-loader | 126 | | compiler | (code: string) => string | | A compiler that transforms the code. Use swc.transformSync by default | 127 | | dependencies | object | | Dependent objects required by the executed code | 128 | | editable | boolean | false | Renders a code editor that can modify the source code | 129 | | editor | object | | Editor properties | 130 | | onChange | (code?: string) => void | | Callback triggered after code change | 131 | | renderToolbar | (buttons: ReactNode) => ReactNode | | Customize the rendering toolbar | 132 | | sourceCode | string | | The code to be rendered is executed | 133 | | theme | 'light' , 'dark' | 'light' | Code editor theme, applied to CodeMirror | 134 | | compileOptions | object | defaultTransformOptions | swc configuration https://swc.rs/docs/configuration/compilation | 135 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Simon Guo 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Code View 2 | 3 | **React Code View** can render source code in markdown documents. And brings you the ability to render React components with editable source code and live preview. 4 | 5 | ![React Code View](https://user-images.githubusercontent.com/1203827/178659124-f4a8658f-1087-4c55-b89b-04dcfc5568cb.gif) 6 | 7 | ## Install 8 | 9 | ``` 10 | npm install react-code-view 11 | ``` 12 | 13 | ### Configure Webpack 14 | 15 | ```js 16 | // webpack.config.js 17 | export default { 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.md$/, 22 | loader: 'react-code-view/webpack-md-loader' 23 | } 24 | ] 25 | } 26 | }; 27 | ``` 28 | 29 | #### Options 30 | 31 | ```js 32 | { 33 | "parseLanguages": [ 34 | // Supported languages for highlight.js 35 | // default: "javascript", "bash", "xml", "css", "markdown", "less", "typescript" 36 | // See https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md 37 | ], 38 | "htmlOptions": { 39 | // HTML Loader options 40 | // See https://github.com/webpack-contrib/html-loader#options 41 | }, 42 | "markedOptions": { 43 | // Pass options to marked 44 | // See https://marked.js.org/using_advanced#options 45 | } 46 | } 47 | ``` 48 | 49 | **webpack.config.js** 50 | 51 | ```js 52 | 53 | export default { 54 | module: { 55 | rules: [ 56 | { 57 | test: /\.md$/, 58 | use:[ 59 | loader: 'react-code-view/webpack-md-loader', 60 | options:{ 61 | parseLanguages: ['typescript','rust'] 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | }; 68 | ``` 69 | 70 | ## Usage 71 | 72 | ```js 73 | import CodeView from 'react-code-view'; 74 | import { Button } from 'rsuite'; 75 | 76 | import 'react-code-view/styles/react-code-view.css'; 77 | 78 | return ( 79 | 84 | {require('./example.md')} 85 | 86 | ); 87 | ``` 88 | 89 | The source code is written in markdown, refer to [example.md](https://raw.githubusercontent.com/simonguo/react-code-view/main/docs/example.md) 90 | 91 | > Note: The code to be rendered must be placed between `` and `` 92 | 93 | ## Props 94 | 95 | ### `` 96 | 97 | | Name | Type | Default value | Description | 98 | | ----------------- | --------------------------------- | ----------------------- | ------------------------------------------------------------------------- | 99 | | afterCompile | (code: string) => string | | Executed after compiling the code | 100 | | beforeCompile | (code: string) => string | | Executed before compiling the code | 101 | | children | any | | The code to be rendered is executed. Usually imported via markdown-loader | 102 | | compileOptions | object | defaultTransformOptions | https://github.com/alangpierce/sucrase#transforms | 103 | | dependencies | object | | Dependent objects required by the executed code | 104 | | editable | boolean | false | Renders a code editor that can modify the source code | 105 | | editor | object | | Editor properties | 106 | | onChange | (code?: string) => void | | Callback triggered after code change | 107 | | onCloseEditor | () => void | | Callback triggered when the editor is closed | 108 | | onOpenEditor | () => void | | Callback triggered when the editor is opened | 109 | | renderExtraFooter | () => ReactNode | | Customize the rendering footer | 110 | | renderToolbar | (buttons: ReactNode) => ReactNode | | Customize the rendering toolbar | 111 | | sourceCode | string | | The code to be rendered is executed | 112 | | theme | 'light' , 'dark' | 'light' | Code editor theme, applied to CodeMirror | 113 | -------------------------------------------------------------------------------- /__tests__/CodeEditor-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, act } from '@testing-library/react'; 3 | import CodeEditor from '../src/CodeEditor'; 4 | 5 | it('renders without crashing', async () => { 6 | await act(async () => { 7 | render(); 8 | }); 9 | }); 10 | 11 | it('editor should be initialized', async () => { 12 | await act(async () => { 13 | const { container } = render( 14 | { 16 | expect(container.querySelector('.rcv-editor-loader')).toBeFalsy(); 17 | }} 18 | /> 19 | ); 20 | expect(container.querySelector('.rcv-editor-loader')).toBeTruthy(); 21 | }); 22 | }); 23 | 24 | it('should be initialized code', async () => { 25 | await act(async () => { 26 | const { container } = render( 27 | { 30 | expect(container.querySelector('textarea').value).toBe('const a = 1;'); 31 | expect(container.querySelector('.CodeMirror').textContent).toBe('const a = 1;'); 32 | }} 33 | /> 34 | ); 35 | expect(container.querySelector('.rcv-editor-loader')).toBeTruthy(); 36 | }); 37 | }); 38 | 39 | it('should call onChange callback', async () => { 40 | await act(async () => { 41 | const { container } = render( 42 | { 45 | expect(container.querySelector('textarea').value).toBe('const a = 1;'); 46 | expect(value).toBe('const a = 2;'); 47 | }} 48 | onInitialized={editor => { 49 | editor.setValue('const a = 2;'); 50 | }} 51 | /> 52 | ); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /__tests__/ErrorBoundary-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import ErrorBoundary from '../src/ErrorBoundary'; 4 | 5 | it('renders error message', () => { 6 | const { getByText } = render(); 7 | 8 | expect(getByText('test message')).toBeTruthy(); 9 | }); 10 | 11 | it('render child elements', () => { 12 | const { getByText } = render(child); 13 | expect(getByText('child')).toBeTruthy(); 14 | }); 15 | -------------------------------------------------------------------------------- /__tests__/Preview-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import Preview from '../src/Preview'; 4 | 5 | it('renders without crashing', () => { 6 | const { container } = render(); 7 | expect(container).toBeTruthy(); 8 | }); 9 | 10 | it('render a loader', () => { 11 | const { container } = render(); 12 | expect(container.querySelector('.rcv-render-loader')).toBeTruthy(); 13 | }); 14 | 15 | it('render child elements', () => { 16 | const { getByText } = render( 17 | 18 | 19 | 20 | ); 21 | expect(getByText('test')).toBeTruthy(); 22 | }); 23 | -------------------------------------------------------------------------------- /__tests__/Renderer-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent } from '@testing-library/react'; 3 | import Renderer from '../src/Renderer'; 4 | 5 | it('refault is rendering', () => { 6 | const { container } = render(); 7 | expect(container.querySelector('.rcv-render')).toHaveTextContent('Rendering...'); 8 | }); 9 | 10 | it('should render a test div', () => { 11 | const { container } = render(); 12 | 13 | expect(container.querySelector('.rcv-render')).toContainHTML('
test
'); 14 | }); 15 | 16 | it('should render a test div with footer', () => { 17 | render(
footer
} />); 18 | 19 | expect(screen.getByText('footer')).toBeInTheDocument(); 20 | }); 21 | 22 | it('should call onOpenEditor and onCloseEditor callback', () => { 23 | const onOpenEditor = jest.fn(); 24 | const onCloseEditor = jest.fn(); 25 | 26 | render(); 27 | 28 | fireEvent.click(screen.getByRole('switch', { name: 'Show the full source' })); 29 | 30 | expect(onOpenEditor).toHaveBeenCalled(); 31 | 32 | fireEvent.click(screen.getByRole('switch', { name: 'Show the full source' })); 33 | 34 | expect(onCloseEditor).toHaveBeenCalled(); 35 | }); 36 | -------------------------------------------------------------------------------- /__tests__/parseHTML-test.js: -------------------------------------------------------------------------------- 1 | import parseHTML from '../src/utils/parseHTML'; 2 | 3 | const trim = str => { 4 | return str.replace(/[\n]+/g, '').trim(); 5 | }; 6 | 7 | it('parse be null', () => { 8 | const result = parseHTML(''); 9 | 10 | expect(result).toBe(null); 11 | }); 12 | 13 | it('parse be html', () => { 14 | const result = parseHTML('
'); 15 | 16 | expect(result.length).toBe(1); 17 | expect(result[0].type).toBe('html'); 18 | expect(result[0].content).toBe('
'); 19 | }); 20 | 21 | it('Parse into one piece of code and two pieces of html', () => { 22 | const html = `

header

23 | 24 | const a = 100; 25 | 26 |

footer

`; 27 | 28 | const result = parseHTML(html); 29 | 30 | expect(result.length).toBe(3); 31 | expect(result[0].type).toBe('html'); 32 | expect(result[1].type).toBe('code'); 33 | expect(result[2].type).toBe('html'); 34 | expect(trim(result[0].content)).toContain('

header

'); 35 | expect(trim(result[1].content)).toContain('const a = 100;'); 36 | expect(trim(result[2].content)).toContain('

footer

'); 37 | }); 38 | 39 | it('Parse into two pieces of code and three pieces of html', () => { 40 | const html = `

header

41 | 42 | const a = 100; 43 | 44 |

title

45 | 46 | const b = 200; 47 | 48 |

footer

`; 49 | 50 | const result = parseHTML(html); 51 | 52 | expect(result.length).toBe(5); 53 | expect(result[0].type).toBe('html'); 54 | expect(result[1].type).toBe('code'); 55 | expect(result[2].type).toBe('html'); 56 | expect(result[3].type).toBe('code'); 57 | expect(result[4].type).toBe('html'); 58 | 59 | expect(trim(result[0].content)).toBe('

header

'); 60 | expect(trim(result[1].content)).toBe('const a = 100;'); 61 | expect(trim(result[2].content)).toBe('

title

'); 62 | expect(trim(result[3].content)).toBe('const b = 200;'); 63 | expect(trim(result[4].content)).toBe('

footer

'); 64 | }); 65 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api, options) => { 2 | const { NODE_ENV } = options || process.env; 3 | const dev = NODE_ENV === 'development'; 4 | const modules = NODE_ENV === 'esm' ? false : 'commonjs'; 5 | 6 | if (api) { 7 | api.cache(() => NODE_ENV); 8 | } 9 | 10 | return { 11 | presets: [ 12 | ['@babel/preset-env', { modules, loose: true }], 13 | ['@babel/preset-react', { development: dev, runtime: 'automatic' }], 14 | '@babel/typescript' 15 | ], 16 | plugins: [ 17 | ['@babel/plugin-proposal-class-properties', { loose: true }], 18 | '@babel/plugin-proposal-optional-chaining', 19 | '@babel/plugin-proposal-export-namespace-from', 20 | '@babel/plugin-proposal-export-default-from', 21 | ['@babel/plugin-transform-runtime', { useESModules: !modules }] 22 | ] 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /docs/example.md: -------------------------------------------------------------------------------- 1 | # React Code View 2 | 3 | --- 4 | 5 | **React Code View** can render source code in markdown documents. And brings you the ability to render React components with editable source code and live preview. 6 | 7 | ## Install 8 | 9 | ```bash 10 | npm install react-code-view 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```jsx 16 | import CodeView from 'react-code-view'; 17 | import { Button } from 'rsuite'; 18 | 19 | import 'react-code-view/styles/react-code-view.css'; 20 | 21 | return {require('./example.md')}; 22 | ``` 23 | 24 | > See [example.md](https://github.com/simonguo/react-code-view/blob/main/docs/example.md) 25 | 26 | ## Example 27 | 28 | ### First example 29 | 30 | 31 | 32 | ```js 33 | import React from 'react'; 34 | import ReactDOM from 'react-dom'; 35 | import { Button } from 'rsuite'; 36 | 37 | const App = () => { 38 | const [count, setCount] = React.useState(1); 39 | 40 | return ( 41 | <> 42 | 45 | 46 | ); 47 | }; 48 | 49 | ReactDOM.render(); 50 | ``` 51 | 52 | 53 | 54 | ### Second example 55 | 56 | 57 | 58 | ```js 59 | ReactDOM.render(); 60 | ``` 61 | 62 | 63 | 64 | > Note: You can try changing the code above and see what changes. 65 | -------------------------------------------------------------------------------- /docs/gh-pages.js: -------------------------------------------------------------------------------- 1 | var ghpages = require('gh-pages'); 2 | var path = require('path'); 3 | 4 | ghpages.publish(path.join(__dirname, './assets'), function (err) { 5 | console.log(err); 6 | }); 7 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= htmlWebpackPlugin.options.title %> 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Button, Grid } from 'rsuite'; 4 | import CodeView from '../src'; 5 | 6 | import './styles/index.less'; 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-var-requires 9 | const example = require('./example.md'); 10 | 11 | const App = () => { 12 | return ( 13 | 14 | { 22 | return code.replace(/import\ [\*\w\,\{\}\ ]+\ from\ ?[\."'@/\w-]+;/gi, ''); 23 | }} 24 | onOpenEditor={() => { 25 | console.log('open editor'); 26 | }} 27 | onCloseEditor={() => { 28 | console.log('close editor'); 29 | }} 30 | renderExtraFooter={() => { 31 | return
Footer
; 32 | }} 33 | > 34 | {example} 35 |
36 |
37 | ); 38 | }; 39 | 40 | ReactDOM.render(, document.getElementById('app')); 41 | -------------------------------------------------------------------------------- /docs/styles/index.less: -------------------------------------------------------------------------------- 1 | @import '~rsuite/styles/index.less'; 2 | @import '../../src/less/index.less'; 3 | @import './markdown.less'; 4 | 5 | @enable-css-reset: false; 6 | 7 | :root { 8 | --font-family-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', 9 | monospace; 10 | --rs-border-primary: #e5e5ea; 11 | --rs-text-primary: #575757; 12 | } 13 | 14 | .rcv-render, 15 | .react-code-view-error { 16 | min-height: 60px; 17 | margin: 0; 18 | } 19 | -------------------------------------------------------------------------------- /docs/styles/markdown.less: -------------------------------------------------------------------------------- 1 | .rcv-markdown { 2 | h1 { 3 | font-size: 28px; 4 | code { 5 | font-size: 14px; 6 | border-radius: 4px; 7 | } 8 | } 9 | 10 | h2 { 11 | font-size: 24px; 12 | padding: 10px 0; 13 | font-weight: bold; 14 | margin-top: 10px; 15 | } 16 | h3 { 17 | font-size: 18px; 18 | margin-top: 10px; 19 | } 20 | h3 code { 21 | font-size: 18px !important; 22 | } 23 | 24 | h4 { 25 | font-size: 16px; 26 | margin-top: 10px; 27 | } 28 | 29 | ol, 30 | ul { 31 | padding: 10px 20px; 32 | list-style-type: circle; 33 | li { 34 | line-height: 26px; 35 | } 36 | } 37 | table { 38 | width: 100%; 39 | margin-top: 10px; 40 | td, 41 | th { 42 | padding: 10px; 43 | border-style: none none dashed none; 44 | border-width: 1px; 45 | border-color: var(--rs-border-primary); 46 | } 47 | } 48 | table th { 49 | text-align: left; 50 | } 51 | blockquote { 52 | padding: 2px 10px; 53 | margin: 2em 0; 54 | font-size: 14px; 55 | border-left: 5px solid #169de0; 56 | opacity: 0.8; 57 | } 58 | kbd { 59 | box-sizing: border-box; 60 | font-family: var(--font-family-mono); 61 | border-radius: 0.25em; 62 | background-color: rgb(235, 235, 235); 63 | padding: 0.2em 0.3em; 64 | border-style: solid; 65 | border-color: rgb(200, 200, 200); 66 | border-image: initial; 67 | border-width: 1px 1px 2px; 68 | font-size: 0.875em; 69 | color: var(--rs-text-primary); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /docs/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const HtmlwebpackPlugin = require('html-webpack-plugin'); 5 | 6 | const { NODE_ENV } = process.env; 7 | 8 | const docsPath = NODE_ENV === 'development' ? './assets' : './'; 9 | 10 | module.exports = { 11 | entry: './docs/index.tsx', 12 | devtool: 'source-map', 13 | resolve: { 14 | // Add '.ts' and '.tsx' as resolvable extensions. 15 | extensions: ['.ts', '.tsx', '.js', '.json'] 16 | }, 17 | devServer: { 18 | hot: true, 19 | contentBase: path.resolve(__dirname, ''), 20 | publicPath: '/' 21 | }, 22 | output: { 23 | path: path.resolve(__dirname, 'assets'), 24 | filename: 'bundle.js', 25 | publicPath: './' 26 | }, 27 | 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.tsx?$/, 32 | use: ['babel-loader'], 33 | exclude: /node_modules/ 34 | }, 35 | { 36 | test: /\.(less|css)$/, 37 | use: [ 38 | MiniCssExtractPlugin.loader, 39 | { 40 | loader: 'css-loader' 41 | }, 42 | { 43 | loader: 'less-loader', 44 | options: { 45 | sourceMap: true, 46 | lessOptions: { 47 | javascriptEnabled: true 48 | } 49 | } 50 | } 51 | ] 52 | }, 53 | { 54 | test: /\.md$/, 55 | loader: path.resolve('./webpack-md-loader') 56 | } 57 | ] 58 | }, 59 | plugins: [ 60 | new HtmlwebpackPlugin({ 61 | title: 'React Code View', 62 | filename: 'index.html', 63 | template: './docs/index.html', 64 | inject: true, 65 | hash: true, 66 | path: docsPath 67 | }), 68 | new MiniCssExtractPlugin({ 69 | filename: '[name].css', 70 | chunkFilename: '[id].css' 71 | }) 72 | ] 73 | }; 74 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const util = require('util'); 3 | const less = require('gulp-less'); 4 | const postcss = require('gulp-postcss'); 5 | const sourcemaps = require('gulp-sourcemaps'); 6 | const rename = require('gulp-rename'); 7 | const gulp = require('gulp'); 8 | const STYLE_SOURCE_DIR = './src/less'; 9 | const STYLE_DIST_DIR = './dist/styles'; 10 | const pkg = require('./package.json'); 11 | 12 | const writeFile = util.promisify(fs.writeFile); 13 | 14 | function buildLess() { 15 | return gulp 16 | .src([`${STYLE_SOURCE_DIR}/index.less`]) 17 | .pipe(sourcemaps.init()) 18 | .pipe(less({ javascriptEnabled: true, paths: ['*.css', '*.less'] })) 19 | .pipe(postcss([require('autoprefixer')])) 20 | .pipe(sourcemaps.write('./')) 21 | .pipe(rename('react-code-view.css')) 22 | .pipe(gulp.dest(`${STYLE_DIST_DIR}`)); 23 | } 24 | 25 | function buildCSS() { 26 | return gulp 27 | .src(`${STYLE_DIST_DIR}/react-code-view.css`) 28 | .pipe(sourcemaps.init()) 29 | .pipe(postcss()) 30 | .pipe(rename({ suffix: '.min' })) 31 | .pipe(sourcemaps.write('./')) 32 | .pipe(gulp.dest(`${STYLE_DIST_DIR}`)); 33 | } 34 | 35 | function copyDocs() { 36 | return gulp 37 | .src(['./README.md', './CHANGELOG.md', './LICENSE', 'package.json']) 38 | .pipe(gulp.dest('dist')); 39 | } 40 | 41 | function copyLoader() { 42 | return gulp.src(['./webpack-md-loader/*']).pipe(gulp.dest('dist/webpack-md-loader')); 43 | } 44 | 45 | function createPkgFile(done) { 46 | pkg.scripts = {}; 47 | 48 | writeFile('dist/package.json', JSON.stringify(pkg, null, 2) + '\n') 49 | .then(() => { 50 | done(); 51 | }) 52 | .catch(err => { 53 | if (err) console.error(err.toString()); 54 | }); 55 | } 56 | 57 | exports.build = gulp.series(buildLess, buildCSS, copyDocs, copyLoader, createPkgFile); 58 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | module.exports = { 7 | collectCoverage: true, 8 | coverageDirectory: 'coverage', 9 | coverageProvider: 'v8', 10 | testEnvironment: 'jsdom', 11 | setupFilesAfterEnv: ['/setup-jest.js'] 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-code-view", 3 | "version": "2.4.1", 4 | "description": "Code view for React", 5 | "main": "cjs/index.js", 6 | "module": "esm/index.js", 7 | "typings": "esm/index.d.ts", 8 | "scripts": { 9 | "build:esm": "NODE_ENV=esm npx babel --extensions .ts,.tsx src --out-dir dist/esm", 10 | "build:cjs": "NODE_ENV=commonjs npx babel --extensions .ts,.tsx src --out-dir dist/cjs", 11 | "build:types": "npx tsc --emitDeclarationOnly --outDir dist/cjs && npx tsc --emitDeclarationOnly --outDir dist/esm", 12 | "build:gulp": "gulp build", 13 | "build:docs": "rm -rf assets && webpack --mode production --config ./docs/webpack.config.js", 14 | "build": "npm run clean && npm run build:esm && npm run build:cjs && npm run build:types && npm run build:gulp", 15 | "clean": "rimraf dist", 16 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", 17 | "dev": "webpack serve --mode development --port 3100 --host 0.0.0.0 --progress --config ./docs/webpack.config.js", 18 | "format": "prettier --write \"{src,test}/**/*.{tsx,ts,js}\"", 19 | "format:check": "prettier --list-different \"{src,test}/**/*.{tsx,ts,js}\"", 20 | "lint": "eslint src/**/*.{ts,tsx}", 21 | "publish:docs": "node docs/gh-pages.js", 22 | "prepublishOnly": "npm run build", 23 | "test:watch": "jest --watch ", 24 | "test": "npm run format:check && npm run lint && jest", 25 | "prepare": "husky install" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/simonguo/react-code-view.git" 30 | }, 31 | "keywords": [ 32 | "markdown-viewer", 33 | "markdown-loader", 34 | "code-view", 35 | "editor" 36 | ], 37 | "author": "simonguo.2009@gmail.com", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/simonguo/react-code-view/issues" 41 | }, 42 | "homepage": "https://github.com/simonguo/react-code-view#readme", 43 | "dependencies": { 44 | "@babel/runtime": "^7.18.6", 45 | "@types/codemirror": "5.60.5", 46 | "classnames": "^2.2.5", 47 | "codemirror": "5.65.6", 48 | "copy-to-clipboard": "^3.3.3", 49 | "highlight.js": "^11.5.1", 50 | "html-loader": "^3.1.2", 51 | "marked": "^4.0.17", 52 | "sucrase": "^3.24.0" 53 | }, 54 | "peerDependencies": { 55 | "react": ">=16.8.0", 56 | "react-dom": ">=16.8.0" 57 | }, 58 | "devDependencies": { 59 | "@babel/cli": "^7.7.0", 60 | "@babel/core": "^7.7.2", 61 | "@babel/plugin-proposal-class-properties": "^7.0.0", 62 | "@babel/plugin-proposal-export-default-from": "^7.12.13", 63 | "@babel/plugin-proposal-export-namespace-from": "^7.12.13", 64 | "@babel/plugin-proposal-optional-chaining": "^7.6.0", 65 | "@babel/plugin-transform-runtime": "^7.1.0", 66 | "@babel/preset-env": "^7.7.7", 67 | "@babel/preset-react": "^7.7.4", 68 | "@babel/preset-typescript": "^7.12.7", 69 | "@testing-library/jest-dom": "^5.16.4", 70 | "@testing-library/react": "^12.1.5", 71 | "@types/jest": "^28.1.4", 72 | "@types/node": "^18.0.3", 73 | "@types/react": "^17.0.0", 74 | "@types/react-dom": "^17.0.0", 75 | "@typescript-eslint/eslint-plugin": "^5.30.5", 76 | "@typescript-eslint/parser": "^5.30.5", 77 | "autoprefixer": "^10.4.7", 78 | "babel-loader": "^8.1.0", 79 | "conventional-changelog-cli": "^2.2.2", 80 | "css-loader": "^6.7.1", 81 | "cssnano": "^5.1.12", 82 | "eslint": "^7.25.0", 83 | "eslint-config-prettier": "^8.3.0", 84 | "eslint-plugin-babel": "^5.3.1", 85 | "eslint-plugin-import": "^2.22.1", 86 | "eslint-plugin-react": "^7.23.2", 87 | "eslint-plugin-react-hooks": "^4.2.0", 88 | "gh-pages": "^1.1.0", 89 | "gulp": "^4.0.2", 90 | "gulp-less": "^5.0.0", 91 | "gulp-postcss": "^9.0.1", 92 | "gulp-rename": "^2.0.0", 93 | "gulp-sourcemaps": "^3.0.0", 94 | "html-webpack-plugin": "^5.5.0", 95 | "husky": "^8.0.1", 96 | "jest": "^28.1.2", 97 | "jest-environment-jsdom": "^28.1.2", 98 | "less": "^4.1.3", 99 | "less-loader": "^11.0.0", 100 | "markdown-loader": "^8.0.0", 101 | "mini-css-extract-plugin": "^2.6.1", 102 | "postcss": "^8.4.14", 103 | "prettier": "^2.4.1", 104 | "react": "^17.0.2", 105 | "react-dom": "^17.0.2", 106 | "rimraf": "^5.0.5", 107 | "rsuite": "^5.51.0", 108 | "typescript": "^4.6.4", 109 | "webpack": "^5.73.0", 110 | "webpack-cli": "^4.10.0", 111 | "webpack-dev-server": "^3.11.2" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const plugins = [ 2 | require('autoprefixer'), 3 | require('cssnano')({ 4 | preset: [ 5 | 'default', 6 | { 7 | discardComments: { 8 | removeAll: true 9 | } 10 | } 11 | ] 12 | }) 13 | ]; 14 | 15 | module.exports = { 16 | plugins 17 | }; 18 | -------------------------------------------------------------------------------- /setup-jest.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | 3 | // https://github.com/jsdom/jsdom/issues/3002 4 | Range.prototype.getBoundingClientRect = () => ({ 5 | bottom: 0, 6 | height: 0, 7 | left: 0, 8 | right: 0, 9 | top: 0, 10 | width: 0 11 | }); 12 | Range.prototype.getClientRects = () => ({ 13 | item: () => null, 14 | length: 0, 15 | [Symbol.iterator]: jest.fn() 16 | }); 17 | -------------------------------------------------------------------------------- /src/CodeEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState, useEffect, useRef } from 'react'; 2 | import { EditorFromTextArea, EditorConfiguration } from 'codemirror'; 3 | import CopyCodeButton from './CopyCodeButton'; 4 | export interface CodeEditorProps extends Omit, 'onChange'> { 5 | code?: string; 6 | editorConfig?: EditorConfiguration; 7 | copyCodeButtonAs?: React.ElementType; 8 | onChange?: (code?: string) => void; 9 | onInitialized?: (editor: EditorFromTextArea) => void; 10 | } 11 | 12 | const defaultEditorConfig = { 13 | mode: 'jsx', 14 | tabSize: 2, 15 | theme: 'default' 16 | }; 17 | 18 | async function importCodeMirror() { 19 | const CodeMirror = await import('codemirror').then(module => 20 | typeof module === 'function' ? module : module.default 21 | ); 22 | 23 | await Promise.all([ 24 | import('codemirror/mode/javascript/javascript'), 25 | import('codemirror/mode/jsx/jsx'), 26 | import('codemirror/addon/runmode/runmode') 27 | ]); 28 | 29 | return CodeMirror; 30 | } 31 | 32 | const CodeEditor = React.forwardRef((props: CodeEditorProps, ref: React.Ref) => { 33 | const { code, editorConfig, copyCodeButtonAs, onChange, onInitialized, ...rest } = props; 34 | 35 | const textareaRef = useRef(null); 36 | const editor = useRef(null); 37 | const [initialized, setInitialized] = useState(false); 38 | 39 | const handleChange = useCallback(() => { 40 | onChange?.(editor.current?.getValue()); 41 | }, [onChange]); 42 | 43 | useEffect(() => { 44 | importCodeMirror().then(CodeMirror => { 45 | if (!CodeMirror || !textareaRef.current) { 46 | return; 47 | } 48 | 49 | setInitialized(true); 50 | 51 | if (!editor.current) { 52 | editor.current = CodeMirror.fromTextArea(textareaRef.current, { 53 | ...defaultEditorConfig, 54 | ...editorConfig 55 | }); 56 | editor.current.on('change', handleChange); 57 | onInitialized?.(editor.current); 58 | } 59 | }); 60 | // eslint-disable-next-line react-hooks/exhaustive-deps 61 | }, []); 62 | 63 | useEffect(() => { 64 | if (code) { 65 | editor.current?.setValue(code); 66 | } 67 | }, [code]); 68 | 69 | return ( 70 |
71 | 76 | {!initialized &&
Editor initializing ...
} 77 |