├── .eslintrc ├── .github ├── release-drafter.yml └── workflows │ ├── pr.yml │ ├── publish.yml │ └── version-check.yml ├── .gitignore ├── .node-version ├── .npmignore ├── LICENSE ├── README.md ├── css ├── editor.css ├── index.css └── preview.css ├── demo ├── @types │ ├── txt.d.ts │ └── worker.d.ts ├── index.tsx ├── markdown.txt └── worker.ts ├── index.html ├── package.json ├── src ├── @types │ ├── dom.d.ts │ └── xss.d.ts ├── commands │ ├── bulletList.ts │ ├── index.ts │ ├── orderedList.ts │ ├── redo.ts │ └── undo.ts ├── components │ ├── Editor.tsx │ ├── Preview.tsx │ ├── SafeHTML.tsx │ ├── Textarea.tsx │ └── index.ts ├── decoration │ ├── index.ts │ ├── markdown.ts │ └── transforms │ │ ├── code.ts │ │ ├── index.ts │ │ ├── title.ts │ │ └── widget.ts ├── hooks │ ├── debounce.ts │ └── index.tsx ├── index.ts ├── parser │ └── index.ts ├── types │ ├── commands.ts │ ├── decorations.ts │ └── index.ts └── utils │ ├── index.ts │ ├── text.ts │ └── undo-redo.ts ├── tsconfig.json ├── tsconfig.prod.json ├── utility └── version-check.js ├── webpack.config.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": ["airbnb", "plugin:prettier/recommended"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": 6, 10 | "sourceType": "module", 11 | "ecmaFeatures": { 12 | "jsx": false 13 | } 14 | }, 15 | "settings": { 16 | "import/resolver": { 17 | "typescript": {}, // use /tsconfig.json 18 | "node": { "extensions": [".ts", ".js", ".tsx", "json"] } 19 | } 20 | }, 21 | "rules": { 22 | "arrow-parens": "off", 23 | "camelcase": "off", 24 | "class-methods-use-this": "off", 25 | "consistent-return": "off", 26 | "import/extensions": [ 27 | "error", 28 | { "js": "never", "ts": "never", "json": "always" } 29 | ], 30 | "import/no-extraneous-dependencies": "off", 31 | "import/prefer-default-export": "off", 32 | "lines-between-class-members": [ 33 | "error", 34 | "always", 35 | { "exceptAfterSingleLine": true } 36 | ], 37 | "no-nested-ternary": "off", 38 | "max-len": "off", 39 | "no-console": ["error", { "allow": ["warn", "error"] }], 40 | "no-empty": ["error", { "allowEmptyCatch": true }], 41 | "no-undef": "off", 42 | "no-bitwise": "off", 43 | "no-unused-vars": "off", 44 | "import/no-duplicates": "off", 45 | "no-restricted-globals": "off", 46 | "prefer-promise-reject-errors": "off", 47 | "no-underscore-dangle": "off", 48 | "max-classes-per-file": "warn", 49 | "prefer-object-spread": "warn", 50 | "react/jsx-props-no-spreading": "off", 51 | "react/jsx-filename-extension": "off", 52 | "react/destructuring-assignment": "off", 53 | "react/prop-types": "off", 54 | "react/jsx-pascal-case": "off", 55 | "react/jsx-wrap-multilines": "off", 56 | "react/require-default-props": "off", 57 | "react/no-unused-prop-types": "off", 58 | "jsx-a11y/label-has-associated-control": "off", 59 | "prettier/prettier": ["error", { "singleQuote": false }], 60 | "no-use-before-define": "off", 61 | "import/no-webpack-loader-syntax": "off" 62 | }, 63 | "plugins": ["prettier", "import"] 64 | } -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION 🌈' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | exclude-labels: 4 | - 'release' 5 | - 'auto' 6 | categories: 7 | - title: '🚀 Features' 8 | labels: 9 | - 'feature' 10 | - 'enhancement' 11 | - title: '🐛 Bug Fixes' 12 | labels: 13 | - 'fix' 14 | - 'bugfix' 15 | - 'bug' 16 | - title: '🧰 Maintenance' 17 | label: 'chore' 18 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 19 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 20 | template: | 21 | ## Changes 22 | 23 | $CHANGES -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | env: 3 | CI: true 4 | # preview環境更新および、stage環境やproduction環境の更新を行うworkflow 5 | on: 6 | push: 7 | branches: 8 | - main 9 | - develop 10 | pull_request: 11 | branches: 12 | - main 13 | - develop 14 | types: 15 | - opened 16 | - synchronize 17 | - closed 18 | - labeled 19 | - unlabeled 20 | tags: 21 | - "!*" 22 | jobs: 23 | release: 24 | name: Setup 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: check label 28 | if: | 29 | github.event_name == 'pull_request' && 30 | !contains(github.event.pull_request.labels.*.name, 'fix') && 31 | !contains(github.event.pull_request.labels.*.name, 'bugfix') && 32 | !contains(github.event.pull_request.labels.*.name, 'enhancement') && 33 | !contains(github.event.pull_request.labels.*.name, 'chore') && 34 | !contains(github.event.pull_request.labels.*.name, 'feature') && 35 | !contains(github.event.pull_request.labels.*.name, 'release') && 36 | !contains(github.event.pull_request.labels.*.name, 'auto') 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | URL: ${{ github.event.pull_request.comments_url }} 40 | run: | 41 | echo "Please add one of the following labels: fix, bugfix, enhancement, chore, feature, release" >> comments 42 | sed -i -z 's/\n/\\n/g' comments 43 | curl -X POST \ 44 | -H "Authorization: token ${GITHUB_TOKEN}" \ 45 | -d "{\"body\": \"$(cat comments)\"}" \ 46 | ${URL} 47 | exit 1 -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | env: 3 | CI: true 4 | # masterブランチにpushした時のみ実行するワークフロー 5 | on: 6 | push: 7 | branches: 8 | - main 9 | tags: 10 | - "!*" 11 | 12 | jobs: 13 | release: 14 | name: Setup 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: checkout 18 | uses: actions/checkout@v1 19 | - name: setup Node 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: 12.x 23 | registry-url: 'https://registry.npmjs.org' 24 | - name: install 25 | run: yarn --frozen-lockfile 26 | # まだtagがないバージョンなら、Git Tagをpushする 27 | - name: package-version 28 | run: node -p -e '`PACKAGE_VERSION=${require("./package.json").version}`' >> $GITHUB_ENV 29 | - name: package-version-to-git-tag 30 | uses: pkgdeps/action-package-version-to-git-tag@v1 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | github_repo: ${{ github.repository }} 34 | git_commit_sha: ${{ github.sha }} 35 | git_tag_prefix: "" 36 | version: ${{ env.PACKAGE_VERSION }} 37 | - name: get-npm-version 38 | id: package-version 39 | uses: martinbeentjes/npm-get-version-action@master 40 | - name: create draft 41 | uses: release-drafter/release-drafter@v5 42 | with: 43 | version: ${{ steps.package-version.outputs.current-version }} 44 | name: ${{ steps.package-version.outputs.current-version }} 45 | tag: ${{ steps.package-version.outputs.current-version }} 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | - name: build lib 49 | run: npm run tsc 50 | - name: publish to npm 51 | run: npm publish 52 | env: 53 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/version-check.yml: -------------------------------------------------------------------------------- 1 | name: Version Check 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | types: 7 | - opened 8 | - synchronize 9 | 10 | jobs: 11 | auto-bumping: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: checkout 15 | uses: actions/checkout@v1 16 | - name: setup Node 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 12.x 20 | registry-url: 'https://npm.pkg.github.com' 21 | - name: install 22 | run: yarn --frozen-lockfile 23 | - name: version check 24 | run: BRANCH_NAME=$HEAD_BRANCH node ./utility/version-check.js 25 | env: 26 | HEAD_BRANCH: ${{ github.head_ref }} -------------------------------------------------------------------------------- /.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/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .DS_Store 107 | lib/ 108 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v12.13.2 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | demo/* 3 | src/* 4 | webpack.config.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Zenn 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 Split MDE 2 | 3 | ![](https://github.com/steelydylan/react-split-mde/workflows/Node%20CI/badge.svg) 4 | [![npm version](https://badge.fury.io/js/react-split-mde.svg)](https://badge.fury.io/js/react-split-mde) 5 | [![npm download](http://img.shields.io/npm/dm/react-split-mde.svg)](https://www.npmjs.com/package/react-split-mde) 6 | [![GitHub license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://raw.githubusercontent.com/steelydylan/react-split-mde/master/LICENSE) 7 | [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Awesome%20Markdown%20Editor%20JavaScript%20%Editor&url=https://github.com/steelydylan/react-split-mde&via=zenn_dev&hashtags=zenn) 8 | 9 | 10 | React Split MDE is a Markdown Editor which enables you to write contents smoothly even with a large amount of content. 11 | 12 | | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [iOS Safari](http://godban.github.io/browsers-support-badges/)
iOS Safari | 13 | | --------- | --------- | --------- | --------- | 14 | 15 | ## ScreenShot 16 | 17 | Not Yet 18 | 19 | ## Features 20 | 21 | * Fully customizable 22 | * Synced scroll position across the contents and the preview 23 | * No stress writing even with a large amount of content 24 | 25 | ## Install 26 | 27 | You should also import zenn-markdown-html as peer dependencies 28 | 29 | ```sh 30 | $ npm install react-split-mde zenn-markdown-html --save 31 | ``` 32 | 33 | ## Usage 34 | 35 | ```js 36 | import React, { useCallback, useState } from 'react'; 37 | import { render } from 'react-dom'; 38 | import { Editor, useProvider } from 'react-split-mde'; 39 | import { parser } from 'react-split-mde/lib/parser'; 40 | import 'react-split-mde/css/index.css'; 41 | 42 | const MDE = () => { 43 | const [markdown, setMarkdown] = useState('') 44 | const handleValueChange = useCallback((newValue: string) => { 45 | setMarkdown(newValue); 46 | }, []); 47 | 48 | return ( 49 | 54 | ) 55 | } 56 | 57 | render(, document.getElementById("app")); 58 | ``` 59 | 60 | ## Try it on CodeSandbox 61 | 62 | Not yet... 63 | 64 | ## Props 65 | 66 | | Props | Description | Type | Default | 67 | |------------------|---------------------------------------------------------|---------------------------------------------|---------| 68 | | commands | key binds | Record< string, Command>; | | 69 | | previewClassName | class name to be applied to preview area | | "znc" | 70 | | previewCallback | morphdom callbacks to be applied to preview area | Record<string, Function> | {} | 71 | | parser | markdown parser function | ( text : string ) => Promise <string> | | 72 | | value | markdown | string | "" | 73 | | onChange | callback when markdown changed | ( value : string ) => void | | 74 | | psudoMode | highlight markdown area with highlight.js | boolean | false | 75 | | debounceTime | debounced time to apply markdown result to preview area | number | 3000 | 76 | 77 | ## Download 78 | [Download ZIP](https://github.com/steelydylan/react-split-mde/archive/master.zip) 79 | 80 | ## Github 81 | [https://github.com/steelydylan/react-split-mde](https://github.com/steelydylan/react-split-mde) 82 | 83 | ## Contributor 84 | [@steelydylan](https://github.com/steelydylan) 85 | 86 | ## License 87 | Code and documentation copyright 2020 by steelydylan, Inc. Code released under the [MIT License](https://github.com/steelydylan/react-split-mde/blob/master/LICENSE). -------------------------------------------------------------------------------- /css/editor.css: -------------------------------------------------------------------------------- 1 | .react-split-mde-wrap { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | box-shadow: 0 0 0 1px rgba(16,22,26,.1), 0 0 0 rgba(16,22,26,0), 0 1px 1px rgba(16,22,26,.2); 6 | } 7 | 8 | .react-split-mde { 9 | font-size: 14px; 10 | line-height: 18px; 11 | white-space: pre-wrap; 12 | word-break: keep-all; 13 | overflow-wrap: break-word; 14 | box-sizing: border-box; 15 | -webkit-font-variant-ligatures: common-ligatures; 16 | font-feature-settings: "liga","clig"; 17 | font-variant-ligatures: common-ligatures; 18 | } 19 | 20 | .react-split-mde * { 21 | box-sizing: border-box; 22 | } 23 | 24 | .react-split-mde-box { 25 | flex: 1; 26 | border: 1px solid #999; 27 | overflow: auto; 28 | padding: 5px; 29 | } 30 | 31 | .react-split-mde-textarea-wrap { 32 | position: relative; 33 | height: 100%; 34 | } 35 | 36 | .react-split-mde-textarea { 37 | position: relative; 38 | z-index: 2; 39 | width: 100%; 40 | min-height: 100%; 41 | display: block; 42 | background-color: transparent; 43 | -webkit-font-smoothing: antialiased; 44 | resize: none; 45 | color: inherit; 46 | border: none; 47 | line-height: 1.6; 48 | outline: none; 49 | padding: 0; 50 | overflow-anchor: none; 51 | } 52 | 53 | .react-split-mde-textarea-with-psudo { 54 | -webkit-text-fill-color: transparent; 55 | } 56 | 57 | .react-split-mde-psudo { 58 | z-index: 1; 59 | position: absolute; 60 | top: 0; 61 | left: 0; 62 | width: 100%; 63 | height: 100%; 64 | margin: 0; 65 | line-height: 1.6; 66 | overflow-y: auto; 67 | -ms-overflow-style: none; /* IE and Edge */ 68 | scrollbar-width: none; /* Firefox */ 69 | } 70 | 71 | .react-split-mde-psudo::-webkit-scrollbar { 72 | display: none; 73 | } 74 | 75 | .react-split-mde-textarea, 76 | .react-split-mde-psudo { 77 | box-sizing: inherit; 78 | display: inherit; 79 | font-family: inherit; 80 | font-size: inherit; 81 | font-style: inherit; 82 | -webkit-font-variant-ligatures: inherit; 83 | font-feature-settings: inherit; 84 | font-variant-ligatures: inherit; 85 | font-weight: inherit; 86 | letter-spacing: inherit; 87 | line-height: inherit; 88 | -moz-tab-size: inherit; 89 | tab-size: inherit; 90 | text-indent: inherit; 91 | text-rendering: inherit; 92 | text-transform: inherit; 93 | white-space: inherit; 94 | overflow-wrap: inherit; 95 | color: #333; 96 | } 97 | 98 | .react-split-mde-psudo .bullet-item { 99 | color: #999; 100 | } 101 | 102 | .react-split-mde-psudo .code-start, 103 | .react-split-mde-psudo .code-end { 104 | color: #999; 105 | } 106 | 107 | .react-split-mde-psudo .title { 108 | /* font-weight: bold */ 109 | color: #000; 110 | } 111 | 112 | .react-split-mde-psudo .sharp, 113 | .react-split-mde-psudo .hljs-bullet { 114 | color: #999; 115 | } 116 | 117 | .react-split-mde-psudo .hljs-link { 118 | color: #1199ff; 119 | } 120 | 121 | .katex { 122 | position: relative; 123 | } -------------------------------------------------------------------------------- /css/index.css: -------------------------------------------------------------------------------- 1 | @import "./editor.css"; 2 | @import "./preview.css"; -------------------------------------------------------------------------------- /css/preview.css: -------------------------------------------------------------------------------- 1 | .react-split-mde-preview { 2 | line-height: 1.9 3 | } 4 | 5 | .react-split-mde-preview>*:first-child { 6 | margin-top: 0 7 | } 8 | 9 | .react-split-mde-preview i, 10 | .react-split-mde-preview cite, 11 | .react-split-mde-preview em { 12 | font-style: italic 13 | } 14 | 15 | .react-split-mde-preview strong { 16 | font-weight: 600 17 | } 18 | 19 | .react-split-mde-preview a { 20 | color: #0f83fd 21 | } 22 | 23 | .react-split-mde-preview a:hover { 24 | text-decoration: underline 25 | } 26 | 27 | .react-split-mde-preview p+p { 28 | margin-top: 1.5em 29 | } 30 | 31 | .react-split-mde-preview ul, 32 | .react-split-mde-preview ol { 33 | margin: 1.4rem 0; 34 | line-height: 1.7 35 | } 36 | 37 | .react-split-mde-preview ul>li, 38 | .react-split-mde-preview ol>li { 39 | margin: 0.6rem 0 40 | } 41 | 42 | .react-split-mde-preview ul ul, 43 | .react-split-mde-preview ul ol, 44 | .react-split-mde-preview ol ul, 45 | .react-split-mde-preview ol ol { 46 | margin: 0.2em 0 47 | } 48 | 49 | .react-split-mde-preview ul p, 50 | .react-split-mde-preview ol p { 51 | margin: 0 52 | } 53 | 54 | .react-split-mde-preview ul { 55 | padding-left: 0 56 | } 57 | 58 | .react-split-mde-preview ul>li { 59 | list-style: none; 60 | list-style-position: inside; 61 | position: relative; 62 | padding-left: 1.6em 63 | } 64 | 65 | .react-split-mde-preview ul>li:before { 66 | content: ""; 67 | position: absolute; 68 | top: 0.7em; 69 | left: 0.35em; 70 | width: 7px; 71 | height: 7px; 72 | border-radius: 50%; 73 | background: rgba(24, 30, 50, 0.7) 74 | } 75 | 76 | .react-split-mde-preview .contains-task-list li:before { 77 | content: none !important 78 | } 79 | 80 | .react-split-mde-preview .task-list-item-checkbox { 81 | margin-left: -1.5em; 82 | font-size: 1em; 83 | pointer-events: none 84 | } 85 | 86 | .react-split-mde-preview ol { 87 | margin-left: 0; 88 | counter-reset: number 89 | } 90 | 91 | .react-split-mde-preview ol>li { 92 | list-style: none; 93 | position: relative; 94 | line-height: 24px; 95 | padding-left: 32px; 96 | margin: 1em 0 97 | } 98 | 99 | .react-split-mde-preview ol>li:before { 100 | display: inline-block; 101 | position: absolute; 102 | left: 0; 103 | top: 1px; 104 | width: 22px; 105 | height: 22px; 106 | line-height: 22px; 107 | border-radius: 50%; 108 | color: #fff; 109 | font-size: 11.5px; 110 | text-align: center; 111 | content: counter(number); 112 | counter-increment: number; 113 | background: rgba(2, 13, 60, 0.6); 114 | font-weight: 600 115 | } 116 | 117 | .react-split-mde-preview h1+p, 118 | .react-split-mde-preview h2+p, 119 | .react-split-mde-preview h3+p, 120 | .react-split-mde-preview h4+p, 121 | .react-split-mde-preview h5+p, 122 | .react-split-mde-preview h6+p { 123 | margin-top: 0.3em 124 | } 125 | 126 | .react-split-mde-preview h1, 127 | .react-split-mde-preview h2 { 128 | margin-top: 2.3em; 129 | margin-bottom: 0.5em 130 | } 131 | 132 | .react-split-mde-preview h3, 133 | .react-split-mde-preview h4, 134 | .react-split-mde-preview h5, 135 | .react-split-mde-preview h6 { 136 | margin-top: 2.25em; 137 | margin-bottom: 0.5em 138 | } 139 | 140 | .react-split-mde-preview h1 { 141 | padding-bottom: 0.2em; 142 | margin-bottom: 1.1rem; 143 | font-size: 1.7em; 144 | position: relative; 145 | border-bottom: solid 1px rgba(92, 147, 187, 0.17) 146 | } 147 | 148 | .react-split-mde-preview h2 { 149 | font-size: 1.5em 150 | } 151 | 152 | .react-split-mde-preview h3 { 153 | font-size: 1.3em 154 | } 155 | 156 | .react-split-mde-preview h4 { 157 | font-size: 1.1em 158 | } 159 | 160 | .react-split-mde-preview h5 { 161 | font-size: 1em 162 | } 163 | 164 | .react-split-mde-preview h5, 165 | .react-split-mde-preview h6 { 166 | color: #93a5b1 167 | } 168 | 169 | .react-split-mde-preview h6 { 170 | font-size: 0.85em 171 | } 172 | 173 | @media screen and (max-width: 576px) { 174 | .react-split-mde-preview h1 { 175 | font-size: 1.6em 176 | } 177 | .react-split-mde-preview h2 { 178 | font-size: 1.4em 179 | } 180 | .react-split-mde-preview h3 { 181 | font-size: 1.2em 182 | } 183 | .react-split-mde-preview h4 { 184 | font-size: 1.1em 185 | } 186 | .react-split-mde-preview h5 { 187 | font-size: 1em 188 | } 189 | .react-split-mde-preview h6 { 190 | font-size: 0.85em 191 | } 192 | } 193 | 194 | .react-split-mde-preview hr { 195 | border-top: 2px solid rgba(92, 147, 187, 0.17); 196 | margin: 2.5rem 0 197 | } 198 | 199 | .react-split-mde-preview blockquote { 200 | font-size: 0.95rem; 201 | margin: 1.4rem 0; 202 | border-left: solid 3px #b3bfc7; 203 | padding: 2px 0 2px 0.7em; 204 | color: #626e77 205 | } 206 | 207 | .react-split-mde-preview blockquote p { 208 | margin: 1rem 0 209 | } 210 | 211 | .react-split-mde-preview blockquote>:first-child { 212 | margin-top: 0 213 | } 214 | 215 | .react-split-mde-preview blockquote>:last-child { 216 | margin-bottom: 0 217 | } 218 | 219 | .react-split-mde-preview blockquote.twitter-tweet { 220 | display: none 221 | } 222 | 223 | .react-split-mde-preview table { 224 | margin: 1.2rem auto; 225 | width: auto; 226 | border-collapse: collapse; 227 | font-size: 0.95em; 228 | line-height: 1.5; 229 | word-break: normal; 230 | display: block; 231 | overflow: auto; 232 | -webkit-overflow-scrolling: touch 233 | } 234 | 235 | .react-split-mde-preview th, 236 | .react-split-mde-preview td { 237 | padding: 0.5rem; 238 | border: solid 1px #cfdce6 239 | } 240 | 241 | .react-split-mde-preview th { 242 | font-weight: 600; 243 | background: #edf2f7 244 | } 245 | 246 | .react-split-mde-preview code { 247 | padding: 0.2em 0.4em; 248 | background: rgba(33, 90, 160, 0.07); 249 | font-size: 0.85em; 250 | border-radius: 4px; 251 | vertical-align: 0.08em; 252 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" 253 | } 254 | 255 | .react-split-mde-preview pre { 256 | margin: 1.3rem 0; 257 | background: #2c2d3a; 258 | overflow-x: auto; 259 | -webkit-overflow-scrolling: touch; 260 | border-radius: 3px; 261 | word-break: normal; 262 | word-wrap: normal; 263 | display: flex 264 | } 265 | 266 | .react-split-mde-preview pre:after { 267 | content: ""; 268 | width: 8px; 269 | flex-shrink: 0 270 | } 271 | 272 | .react-split-mde-preview pre code { 273 | margin: 0; 274 | padding: 0; 275 | background: transparent; 276 | font-size: 0.9em; 277 | color: #fff 278 | } 279 | 280 | .react-split-mde-preview pre>code { 281 | display: block; 282 | padding: 1.1rem 283 | } 284 | 285 | @media screen and (max-width: 576px) { 286 | .react-split-mde-preview pre>code { 287 | padding: 1rem 0.8rem; 288 | font-size: 13px 289 | } 290 | } 291 | 292 | .react-split-mde-preview .code-block-container { 293 | position: relative; 294 | margin: 1.3rem 0; 295 | background: #2c2d3a; 296 | border-radius: 3px 297 | } 298 | 299 | .react-split-mde-preview .code-block-container pre { 300 | background: transparent; 301 | margin: 0 302 | } 303 | 304 | .react-split-mde-preview .code-block-filename-container { 305 | margin-bottom: -12px 306 | } 307 | 308 | .react-split-mde-preview .code-block-filename { 309 | display: inline-block; 310 | vertical-align: top; 311 | max-width: 100%; 312 | background: rgba(177, 197, 247, 0.25); 313 | color: #fff; 314 | font-size: 12px; 315 | height: 24px; 316 | line-height: 24px; 317 | padding: 0px 6px 0 8px; 318 | font-family: monospace; 319 | border-radius: 4px 0; 320 | white-space: nowrap; 321 | overflow: hidden; 322 | text-overflow: ellipsis 323 | } 324 | 325 | .react-split-mde-preview img:not(.emoji) { 326 | margin: 1.5rem auto; 327 | display: table; 328 | max-width: 100%; 329 | height: auto 330 | } 331 | 332 | .react-split-mde-preview img+br { 333 | display: none 334 | } 335 | 336 | .react-split-mde-preview img~em { 337 | display: block; 338 | margin: -1rem auto 0; 339 | line-height: 1.3; 340 | text-align: center; 341 | color: #93a5b1; 342 | font-size: 0.95rem 343 | } 344 | 345 | .react-split-mde-preview details { 346 | font-size: 0.95em; 347 | margin: 1rem 0; 348 | line-height: 1.7 349 | } 350 | 351 | .react-split-mde-preview summary { 352 | cursor: pointer; 353 | outline: 0; 354 | padding: 0.7em 0.7em 0.7em 0.9em; 355 | background: #f1f5f9; 356 | border-radius: 5px 357 | } 358 | 359 | .react-split-mde-preview summary::-webkit-details-marker { 360 | color: #93a5b1 361 | } 362 | 363 | .react-split-mde-preview details[open] summary { 364 | border-radius: 5px 5px 0 0 365 | } 366 | 367 | .react-split-mde-preview .details-content { 368 | padding: 0.5em 0.9em; 369 | border: solid 2px #f1f5f9; 370 | border-radius: 0 0 5px 5px 371 | } 372 | 373 | .react-split-mde-preview .details-content>* { 374 | margin: 0.5em 0 375 | } 376 | 377 | .react-split-mde-preview .embed-tweet, 378 | .react-split-mde-preview .embed-gist, 379 | .react-split-mde-preview .embed-speakerdeck, 380 | .react-split-mde-preview .embed-slideshare, 381 | .react-split-mde-preview .embed-codepen, 382 | .react-split-mde-preview .embed-jsfiddle, 383 | .react-split-mde-preview .embed-youtube, 384 | .react-split-mde-preview .embed-codesandbox, 385 | .react-split-mde-preview .embed-stackblitz { 386 | margin: 1.5rem 0 387 | } 388 | 389 | .react-split-mde-preview .embed-slideshare, 390 | .react-split-mde-preview .embed-speakerdeck, 391 | .react-split-mde-preview .embed-codepen, 392 | .react-split-mde-preview .embed-jsfiddle, 393 | .react-split-mde-preview .embed-youtube, 394 | .react-split-mde-preview .embed-stackblitz { 395 | padding-bottom: calc(56.25% + 38px); 396 | position: relative; 397 | width: 100%; 398 | height: 0 399 | } 400 | 401 | .react-split-mde-preview .embed-slideshare iframe, 402 | .react-split-mde-preview .embed-speakerdeck iframe, 403 | .react-split-mde-preview .embed-codepen iframe, 404 | .react-split-mde-preview .embed-jsfiddle iframe, 405 | .react-split-mde-preview .embed-youtube iframe, 406 | .react-split-mde-preview .embed-stackblitz iframe { 407 | position: absolute; 408 | top: 0; 409 | left: 0; 410 | width: 100%; 411 | height: 100%; 412 | border: none 413 | } 414 | 415 | .react-split-mde-preview .embed-slideshare iframe { 416 | border: 1px solid #2c2d3a 417 | } 418 | 419 | .react-split-mde-preview .embed-zenn-link { 420 | margin: 1rem auto 421 | } 422 | 423 | .react-split-mde-preview .embed-zenn-link iframe { 424 | height: 125px; 425 | width: 100%; 426 | display: block 427 | } 428 | 429 | .react-split-mde-preview eqn { 430 | display: block; 431 | overflow-x: auto 432 | } 433 | 434 | .react-split-mde-preview pre[class*="language-"] { 435 | position: relative 436 | } 437 | 438 | .react-split-mde-preview .token.namespace { 439 | opacity: 0.7 440 | } 441 | 442 | .react-split-mde-preview .token.comment, 443 | .react-split-mde-preview .token.prolog, 444 | .react-split-mde-preview .token.doctype, 445 | .react-split-mde-preview .token.cdata { 446 | color: #92a3ad 447 | } 448 | 449 | .react-split-mde-preview .token.operator, 450 | .react-split-mde-preview .token.boolean, 451 | .react-split-mde-preview .token.number { 452 | color: #ffc164 453 | } 454 | 455 | .react-split-mde-preview .token.attr-name, 456 | .react-split-mde-preview .token.string { 457 | color: #ffc164 458 | } 459 | 460 | .react-split-mde-preview .token.entity, 461 | .react-split-mde-preview .token.url, 462 | .react-split-mde-preview .language-css .token.string, 463 | .react-split-mde-preview .style .token.string { 464 | color: #ffc164 465 | } 466 | 467 | .react-split-mde-preview .token.selector { 468 | color: #ff8e8e 469 | } 470 | 471 | .react-split-mde-preview .token.atrule, 472 | .react-split-mde-preview .token.attr-value, 473 | .react-split-mde-preview .token.keyword, 474 | .react-split-mde-preview .token.important, 475 | .react-split-mde-preview .token.deleted { 476 | color: #ff8e8e 477 | } 478 | 479 | .react-split-mde-preview .token.inserted { 480 | color: #b4ff9b 481 | } 482 | 483 | .react-split-mde-preview .token.regex, 484 | .react-split-mde-preview .token.statement { 485 | color: #ffc164 486 | } 487 | 488 | .react-split-mde-preview .token.placeholder, 489 | .react-split-mde-preview .token.variable { 490 | color: #fff 491 | } 492 | 493 | .react-split-mde-preview .token.important, 494 | .react-split-mde-preview .token.statement, 495 | .react-split-mde-preview .token.bold { 496 | font-weight: 600 497 | } 498 | 499 | .react-split-mde-preview .token.punctuation { 500 | color: #859aff 501 | } 502 | 503 | .react-split-mde-preview .token.entity { 504 | cursor: help 505 | } 506 | 507 | .react-split-mde-preview .token.italic { 508 | font-style: italic 509 | } 510 | 511 | .react-split-mde-preview .token.tag, 512 | .react-split-mde-preview .token.property, 513 | .react-split-mde-preview .token.function { 514 | color: #56cdff 515 | } 516 | 517 | .react-split-mde-preview .token.attr-name { 518 | color: #ff8e8e 519 | } 520 | 521 | .react-split-mde-preview .token.attr-value { 522 | color: #ffc164 523 | } 524 | 525 | .react-split-mde-preview .token.style, 526 | .react-split-mde-preview .token.script { 527 | color: #ffc164 528 | } 529 | 530 | .react-split-mde-preview .token.script .token.keyword { 531 | color: #ffc164 532 | } 533 | 534 | .react-split-mde-preview .msg { 535 | position: relative; 536 | margin: 1.5rem 0; 537 | padding: 21px 15px 21px 45px; 538 | border-radius: 6px; 539 | background: #fff6e4; 540 | color: rgba(0, 0, 0, 0.65); 541 | font-size: 14.5px; 542 | line-height: 1.6 543 | } 544 | 545 | .react-split-mde-preview .msg:before { 546 | position: absolute; 547 | left: 15px; 548 | top: 50%; 549 | transform: translateY(-52%); 550 | content: "!"; 551 | display: block; 552 | width: 20px; 553 | height: 20px; 554 | line-height: 20px; 555 | font-size: 13px; 556 | text-align: center; 557 | color: #fff; 558 | background: #ffb84c; 559 | font-weight: 600; 560 | border-radius: 50% 561 | } 562 | 563 | .react-split-mde-preview .msg>* { 564 | margin: 0.5rem 0 565 | } 566 | 567 | .react-split-mde-preview .msg>*:first-child, 568 | .react-split-mde-preview .msg>*:last-child { 569 | margin: 0 570 | } 571 | 572 | .react-split-mde-preview .msg a { 573 | color: inherit; 574 | text-decoration: underline 575 | } 576 | 577 | .react-split-mde-preview .msg.alert { 578 | background: #ffeff2 579 | } 580 | 581 | .react-split-mde-preview .msg.alert:before { 582 | background: #ff7670 583 | } 584 | 585 | .react-split-mde-preview .footnotes { 586 | margin-top: 3rem; 587 | color: #77838c; 588 | font-size: 0.9em 589 | } 590 | 591 | .react-split-mde-preview .footnotes-title { 592 | padding-bottom: 3px; 593 | border-bottom: solid 1px #cfdce6; 594 | font-weight: 600; 595 | font-size: 15px 596 | } 597 | 598 | .react-split-mde-preview .footnotes-list { 599 | margin: 0 600 | } 601 | 602 | .react-split-mde-preview .footnotes-twemoji { 603 | border: none; 604 | margin: 0 7px 0 0; 605 | vertical-align: -3px 606 | } -------------------------------------------------------------------------------- /demo/@types/txt.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.txt" { 2 | const str: string; 3 | export = str; 4 | } 5 | -------------------------------------------------------------------------------- /demo/@types/worker.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'worker-loader!*' { 2 | // You need to change `Worker`, if you specified a different value for the `workerType` option 3 | class WebpackWorker extends Worker { 4 | constructor(); 5 | } 6 | 7 | // Uncomment this if you set the `esModule` option to `false` 8 | // export = WebpackWorker; 9 | export default WebpackWorker; 10 | } -------------------------------------------------------------------------------- /demo/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { render } from "react-dom"; 3 | import { useProvider } from "../src/hooks"; 4 | import "../css/index.css"; 5 | import { parser } from "../src/parser"; 6 | import { Editor, defaultCommands, EnterKey } from "../src"; 7 | import markdown from "./markdown.txt"; 8 | 9 | declare global { 10 | interface Window { 11 | twttr: any; 12 | } 13 | } 14 | 15 | const Main = () => { 16 | const [emit, Provider] = useProvider(); 17 | const [value, setValue] = React.useState(markdown); 18 | const handleValueChange = React.useCallback((newValue: string) => { 19 | setValue(newValue); 20 | }, []); 21 | const handleYouTubeClick = React.useCallback(() => { 22 | emit({ 23 | type: "insert", 24 | text: "@[youtube](ApXoWvfEYVU)", 25 | }); 26 | }, []); 27 | const handleImageUpload = React.useCallback( 28 | async (e: React.ChangeEvent) => { 29 | const uploadingMsg = "![](now uploading...)"; 30 | emit({ 31 | type: "insert", 32 | text: uploadingMsg, 33 | }); 34 | await new Promise((resolve) => { 35 | setTimeout(() => resolve(), 1000); 36 | }); 37 | emit({ 38 | type: "replace", 39 | targetText: uploadingMsg, 40 | text: "![](https://source.unsplash.com/1600x900/?nature,water)", 41 | }); 42 | }, 43 | [] 44 | ); 45 | 46 | const handleClear = () => { 47 | emit({ type: "clear" }); 48 | }; 49 | 50 | const handleFocus = () => { 51 | emit({ type: "focus", last: true }); 52 | }; 53 | 54 | return ( 55 | 56 |
57 | 60 | 61 | 64 | 67 |
68 |
69 | { 76 | const { composing, code, shiftKey, metaKey, ctrlKey } = option; 77 | if ((metaKey || ctrlKey) && !shiftKey) { 78 | // + Enterで送信 79 | if (!composing && code === EnterKey) { 80 | alert('command test') 81 | return { stop: true, change: false }; 82 | } 83 | } 84 | }, 85 | }} 86 | /> 87 |
88 |
89 | ); 90 | }; 91 | 92 | render(
, document.getElementById("main")); 93 | -------------------------------------------------------------------------------- /demo/markdown.txt: -------------------------------------------------------------------------------- 1 | This page lists the Markdown notation for React Split MDE. 2 | 3 | # Heading 4 | ``` 5 | # Heading1 6 | ## Heading2 7 | ### Heading3 8 | #### Heading4 9 | ``` 10 | 11 | # List 12 | ``` 13 | - Hello! 14 | - Hola! 15 | - Bonjour! 16 | * Hi! 17 | ``` 18 | - Hello! 19 | - Hola! 20 | - Bonjour! 21 | * Hi! 22 | 23 | Use `*` or `-` for items in the list. 24 | 25 | ### Ordered List 26 | ``` 27 | 1. First 28 | 2. Second 29 | ``` 30 | 1. First 31 | 2. Second 32 | 33 | 34 | 35 | # Image 36 | ``` 37 | ![alt text](https://path_to_the_image) 38 | ``` 39 | ![alt text](https://storage.googleapis.com/zenn-user-upload/gxnwu3br83nsbqs873uibiy6fd43) 40 | 41 | ### Specify the width of the image 42 | 43 | If the image is too large, you can specify the width of the image in px units by writing `= ○○ x` with a half-width space after the URL. 44 | 45 | ``` 46 | ![alt text](https://path_to_the_image =250x) 47 | ``` 48 | ![alt text](https://storage.googleapis.com/zenn-user-upload/gxnwu3br83nsbqs873uibiy6fd43 =250x) 49 | 50 | 51 | # Text link 52 | ``` 53 | [anchor text](URL) 54 | ``` 55 | [anchor text](https://zenn.dev) 56 | You can also insert with the shortcut `Ctrl + K`. 57 | 58 | # Table 59 | ``` 60 | | Head | Head | Head | 61 | | ---- | ---- | ---- | 62 | | Text | Text | Text | 63 | | Text | Text | Text | 64 | ``` 65 | | Head | Head | Head | 66 | | ---- | ---- | ---- | 67 | | Text | Text | Text | 68 | | Text | Text | Text | 69 | 70 | 71 | 72 | # Code Block 73 | 74 | Code can be inserted as a block by sandwiching it with `` `. If you specify the language as shown below, the decoration (syntax highlighting) will be applied to the code. 75 | 76 | > \```js 77 | > 78 | > \``` 79 | 80 | ```js 81 | const great = () => { 82 | console.log("Awesome") 83 | } 84 | ``` 85 | 86 | # Mathmatical Expression 87 | 88 | Supports mathematical expression display by ** KaTeX **. 89 | 90 | ### Insert a block of formulas 91 | 92 | A block of formulas is inserted by inserting the description between `$$`. 93 | 94 | ``` 95 | $$ 96 | e^{i\theta} = \cos\theta + i\sin\theta 97 | $$ 98 | ``` 99 | 100 | It will be displayed as follows. 101 | 102 | $$ 103 | e^{i\theta} = \cos\theta + i\sin\theta 104 | $$ 105 | 106 | :::message 107 | Before and After `$$`, space should be placed, otherwise it won't be shown correctly. 108 | ::: 109 | 110 | ### Insert formula inline 111 | 112 | You can include formulas inline by sandwiching them with one `$` such as `$a\ne0$`. For example, $a\ne0$. 113 | 114 | # Quote 115 | ``` 116 | > Quote 117 | > Quote 118 | ``` 119 | 120 | > Quote 121 | > Quote 122 | 123 | # Annotation 124 | 125 | If you specify an annotation, its contents will be displayed at the bottom of the page. 126 | ``` 127 | 128 | An example of a footnote [^ 1]. You can also write inline ^ [Footnote Content 2]. 129 | 130 | [^1]: footnote1 131 | ``` 132 | 133 | If you specify an annotation, its contents will be displayed at the bottom of the page. 134 | 135 | [^1]: footnote1 136 | 137 | 138 | # Separator 139 | ``` 140 | ----- 141 | ``` 142 | ----- 143 | 144 | 145 | # Inline Style 146 | ``` 147 | *Italic* 148 | **Bold** 149 | ~~Strikethrough~~ 150 | Insert `code` inline 151 | ``` 152 | *Italic* 153 | **Bold** 154 | ~~Strikethrough~~ 155 | Insert `code` inline 156 | 157 | 158 | 159 | # React Split MDE original notation 160 | 161 | ### Message 162 | 163 | ``` 164 | :::message 165 | message here 166 | ::: 167 | ``` 168 | 169 | 170 | :::message 171 | message here 172 | ::: 173 | 174 | ``` 175 | :::message alert 176 | alert message here 177 | ::: 178 | ``` 179 | 180 | :::message alert 181 | alert message here 182 | ::: 183 | 184 | 185 | ### Accordion(Toggle) 186 | 187 | ``` 188 | :::details title 189 | contents here 190 | ::: 191 | ``` 192 | 193 | :::details title 194 | contents here 195 | ::: 196 | 197 | 198 | It's comfusing, but it's not "detail" but "details". 199 | 200 | # Embedding external content 201 | 202 | 203 | ### Twitter 204 | 205 | ``` 206 | @[tweet](URL) 207 | ``` 208 | 209 | Please note that it is "tweet", not "twitter". 210 | 211 | @[tweet](https://twitter.com/catnose99/status/1309382877272879110) 212 | 213 | 214 | ### YouTube 215 | 216 | ``` 217 | @[youtube](video ID) 218 | ``` 219 | 220 | Enter a combination of alphanumeric characters in the URL. 221 | For example, if the URL is `https://youtube.com/watch?v=ApXoWvfEYVU`, 222 | specify `@[youtube](ApXoWvfEYVU)`. 223 | 224 | @[youtube](ApXoWvfEYVU) 225 | 226 | ### CodePen 227 | 228 | ``` 229 | @[codepen](URL) 230 | ``` 231 | 232 | The default display tab can be changed by specifying a query like `pageURL?default-tab=html,css`. 233 | 234 | 235 | ### SlideShare 236 | ``` 237 | @[slideshare](Slide key) 238 | ``` 239 | Enter the `◯◯` part of `...embed_code/key/○○...` included in the embedded iframe of SlideShare. 240 | 241 | 242 | ### SpeakerDeck 243 | ``` 244 | @[speakerdeck](Slide ID) 245 | ``` 246 | Enter the value of `data-id` included in the embed code obtained by SpeakerDeck. 247 | 248 | ### JSFiddle 249 | 250 | ``` 251 | @[jsfiddle](URL) 252 | ``` 253 | -------------------------------------------------------------------------------- /demo/worker.ts: -------------------------------------------------------------------------------- 1 | const ctx: Worker = self as any; 2 | ctx.Prism = {} 3 | ctx.Prism.disableWorkerMessageHandler = true; 4 | 5 | // you have to disable prism worker handle option first 6 | import markdownHTML, { enablePreview } from "zenn-markdown-html"; 7 | 8 | enablePreview(); 9 | // Respond to message from parent thread 10 | ctx.addEventListener("message", (event) => { 11 | const result = markdownHTML(event.data); 12 | ctx.postMessage(result); 13 | }); 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | Zenn 12 | 13 | 14 | 34 |
35 |

React Split MDE

36 |
37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-split-mde", 3 | "version": "0.3.4", 4 | "description": "", 5 | "main": "./lib/index.js", 6 | "types": "./lib/index.d.ts", 7 | "engines": { 8 | "node": "16.x" 9 | }, 10 | "scripts": { 11 | "dev": "webpack-dev-server --config webpack.config.js --host 0.0.0.0 --port 3031", 12 | "build": "webpack --mode production && cp index.html ./dist/index.html", 13 | "test": "echo \"Error: no test specified\" && exit 1", 14 | "lint": "eslint 'src/**/**.{ts,tsx}' --fix", 15 | "clean": "rm -rf ./lib", 16 | "tsc": "npm run clean && tsc --project ./tsconfig.prod.json", 17 | "patch": "npm run tsc && npm version patch && npm publish && npm run clean", 18 | "minor": "npm run tsc && npm version minor && npm publish && npm run clean", 19 | "major": "npm run tsc && npm version minor && npm publish && npm run clean" 20 | }, 21 | "keywords": [], 22 | "author": "steelydylan", 23 | "license": "MIT", 24 | "dependencies": { 25 | "@types/highlight.js": "^9.12.4", 26 | "eventmit": "^1.1.0", 27 | "highlight.js": "^10.2.0", 28 | "morphdom": "^2.6.1", 29 | "react-textarea-autosize": "^8.3.0", 30 | "use-eventmit": "^1.0.3", 31 | "xss": "^1.0.8" 32 | }, 33 | "peerDependencies": { 34 | "zenn-markdown-html": "^0.1.70" 35 | }, 36 | "devDependencies": { 37 | "@types/react": "^16.9.35", 38 | "@types/react-dom": "^16.9.8", 39 | "@typescript-eslint/eslint-plugin": "^4.2.0", 40 | "@typescript-eslint/parser": "^4.2.0", 41 | "css-loader": "^4.3.0", 42 | "eslint": "^7.11.0", 43 | "eslint-config-airbnb": "^18.0.1", 44 | "eslint-config-prettier": "^6.8.0", 45 | "eslint-import-resolver-typescript": "^2.3.0", 46 | "eslint-plugin-import": "^2.19.1", 47 | "eslint-plugin-jsx-a11y": "^6.2.3", 48 | "eslint-plugin-prettier": "^3.1.2", 49 | "eslint-plugin-react": "^7.21.4", 50 | "null-loader": "^4.0.0", 51 | "p-event": "^4.2.0", 52 | "prettier": "^2.0.5", 53 | "raw-loader": "^4.0.1", 54 | "react": "^16.13.1", 55 | "react-dom": "^16.13.1", 56 | "style-loader": "^1.2.1", 57 | "ts-loader": "^7.0.4", 58 | "typescript": "^4.0.3", 59 | "webpack": "^4.43.0", 60 | "webpack-cli": "^3.3.11", 61 | "webpack-dev-server": "^3.11.0", 62 | "worker-loader": "^3.0.3", 63 | "zenn-markdown-html": "^0.1.70" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/@types/dom.d.ts: -------------------------------------------------------------------------------- 1 | interface HTMLElement { 2 | createTextRange: () => any; 3 | selectionStart: any; 4 | selectionEnd: any; 5 | } 6 | 7 | interface Window { 8 | clipboardData: any; 9 | } 10 | 11 | interface Document { 12 | selection: any; 13 | } 14 | -------------------------------------------------------------------------------- /src/@types/xss.d.ts: -------------------------------------------------------------------------------- 1 | declare module "xss" { 2 | export type Option = { 3 | whiteList?: Record; 4 | stripIgnoreTag?: boolean; 5 | stripIgnoreTagBody?: string[]; 6 | }; 7 | const xss: (html: string, option?: Option) => string; 8 | // @ts-ignore 9 | export = xss; 10 | } 11 | -------------------------------------------------------------------------------- /src/commands/bulletList.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandOption, EnterKey, TabKey } from "../types"; 2 | import { 3 | insertTextAtCursor, 4 | insertTextAtCursorFirstLine, 5 | removeTextAtFirstLine, 6 | } from "../utils"; 7 | 8 | const generateSpace = (count: number) => { 9 | let i = 0; 10 | let text = ""; 11 | while (i < count) { 12 | text += " "; 13 | i += 1; 14 | } 15 | return text; 16 | }; 17 | 18 | export const bulletList: Command = (target, option) => { 19 | const { lineAll, line } = option; 20 | const lineWithoutSpace = lineAll.replace(/^(\s*)/g, ""); 21 | const spaces = lineAll.match(/^(\s*)/); 22 | let spaceLength = 0; 23 | if (option.composing) { 24 | return { stop: false, change: false }; 25 | } 26 | if (spaces.length) { 27 | const [_, space] = spaces; 28 | if (space) { 29 | spaceLength = space.length; 30 | } 31 | } 32 | const startWithHyphen = lineWithoutSpace.startsWith("-"); 33 | const startWithAsterisk = lineWithoutSpace.startsWith("*"); 34 | if (!startWithHyphen && !startWithAsterisk) { 35 | return { stop: false, change: false }; 36 | } 37 | if (lineWithoutSpace.charAt(1) !== " ") { 38 | return { stop: false, change: false }; 39 | } 40 | if (option.code === EnterKey) { 41 | if (option.metaKey || option.ctrlKey) { 42 | return { stop: true, change: false }; 43 | } 44 | if (line.length === 0) { 45 | return { stop: false, change: true }; 46 | } 47 | if (lineWithoutSpace.length > 2) { 48 | const text = startWithHyphen 49 | ? `\n${generateSpace(spaceLength)}- ` 50 | : `\n${generateSpace(spaceLength)}* `; 51 | insertTextAtCursor(target, text); 52 | target.setSelectionRange( 53 | option.start + text.length, 54 | option.start + text.length 55 | ); 56 | return { stop: true, change: true }; 57 | } 58 | if (lineWithoutSpace === "- " || lineWithoutSpace === "* ") { 59 | removeTextAtFirstLine(target, lineAll.length); 60 | insertTextAtCursor(target, "\n"); 61 | return { stop: false, change: true }; 62 | } 63 | } 64 | if (option.code === TabKey && option.shiftKey) { 65 | removeTextAtFirstLine(target, 2); 66 | return { stop: true, change: true }; 67 | } 68 | if (option.code === TabKey) { 69 | const text = " "; 70 | insertTextAtCursorFirstLine(target, text); 71 | target.setSelectionRange( 72 | option.start + text.length, 73 | option.start + text.length 74 | ); 75 | return { stop: true, change: true }; 76 | } 77 | return { stop: false, change: false }; 78 | }; 79 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./bulletList"; 2 | export * from "./orderedList"; 3 | export * from "./redo"; 4 | export * from "./undo"; 5 | // export * from "./linebreak"; 6 | -------------------------------------------------------------------------------- /src/commands/orderedList.ts: -------------------------------------------------------------------------------- 1 | import { Command, EnterKey, TabKey } from "../types"; 2 | import { 3 | insertTextAtCursor, 4 | insertTextAtCursorFirstLine, 5 | removeTextAtFirstLine, 6 | } from "../utils"; 7 | 8 | const generateSpace = (count: number) => { 9 | let i = 0; 10 | let text = ""; 11 | while (i < count) { 12 | text += " "; 13 | i += 1; 14 | } 15 | return text; 16 | }; 17 | 18 | export const orderedList: Command = (target, option) => { 19 | const { lineAll, line } = option; 20 | const lineWithoutSpace = lineAll.replace(/^(\s*)/g, ""); 21 | const spaces = lineAll.match(/^(\s*)/); 22 | let spaceLength = 0; 23 | if (option.composing) { 24 | return { stop: false, change: false }; 25 | } 26 | if (spaces.length) { 27 | const [_, space] = spaces; 28 | if (space) { 29 | spaceLength = space.length; 30 | } 31 | } 32 | if (!/^(\d+)\./.test(lineWithoutSpace)) { 33 | return { stop: false, change: false }; 34 | } 35 | if (option.code === EnterKey) { 36 | if (option.ctrlKey || option.metaKey) { 37 | return { stop: true, change: false }; 38 | } 39 | if (line.length === 0) { 40 | return { stop: false, change: true }; 41 | } 42 | const [_, number] = lineWithoutSpace.match(/^(\d+)/); 43 | if (lineWithoutSpace.length - number.length <= 2) { 44 | removeTextAtFirstLine(target, lineAll.length); 45 | insertTextAtCursor(target, "\n"); 46 | return { stop: false, change: true }; 47 | } 48 | const text = `\n${generateSpace(spaceLength)}${parseInt(number, 10) + 1}. `; 49 | insertTextAtCursor(target, text); 50 | target.setSelectionRange( 51 | option.start + text.length, 52 | option.start + text.length 53 | ); 54 | return { stop: true, change: true }; 55 | } 56 | if (option.code === EnterKey && lineWithoutSpace.length === 2) { 57 | removeTextAtFirstLine(target, lineAll.length); 58 | insertTextAtCursor(target, "\n"); 59 | } 60 | if (option.code === TabKey && option.shiftKey) { 61 | removeTextAtFirstLine(target, 2); 62 | return { stop: true, change: true }; 63 | } 64 | if (option.code === TabKey) { 65 | const text = " "; 66 | insertTextAtCursorFirstLine(target, text); 67 | target.setSelectionRange( 68 | option.start + text.length, 69 | option.start + text.length 70 | ); 71 | return { stop: true, change: true }; 72 | } 73 | return { stop: false, change: false }; 74 | }; 75 | -------------------------------------------------------------------------------- /src/commands/redo.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandOption } from "../types"; 2 | 3 | export const redo: Command = ( 4 | _, 5 | { code, metaKey, ctrlKey, shiftKey, emit } 6 | ) => { 7 | if (code === "z" && (metaKey || ctrlKey) && shiftKey) { 8 | emit({ type: "redo" }); 9 | return { stop: true, change: false }; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/commands/undo.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../types"; 2 | 3 | export const undo: Command = ( 4 | _, 5 | { code, metaKey, ctrlKey, shiftKey, emit } 6 | ) => { 7 | if (code === "z" && (metaKey || ctrlKey) && !shiftKey) { 8 | emit({ type: "undo" }); 9 | return { stop: true, change: false }; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/Editor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import { Preview } from "./Preview"; 3 | import { Textarea } from "./Textarea"; 4 | import * as defaultCommands from "../commands"; 5 | import { Command } from "../types"; 6 | import { useDebounce } from "../hooks/debounce"; 7 | 8 | type Props = { 9 | commands?: Record; 10 | previewClassName?: string; 11 | textareaClassName?: string; 12 | previewCallback?: Record any>; 13 | parser: (text: string) => Promise; 14 | value: string; 15 | onChange?: (value: string) => void; 16 | psudoMode?: boolean; 17 | debounceTime?: number; 18 | scrollSync?: boolean; 19 | placeholder?: string; 20 | }; 21 | 22 | export const Editor: React.FC = ({ 23 | commands = defaultCommands, 24 | textareaClassName, 25 | previewClassName = "react-split-mde-preview", 26 | previewCallback = {}, 27 | parser, 28 | value, 29 | onChange, 30 | psudoMode = false, 31 | debounceTime = 300, 32 | scrollSync = true, 33 | placeholder = "", 34 | }) => { 35 | const ref = useRef(null); 36 | const handleTextareaChange = React.useCallback((text: string) => { 37 | if (onChange) { 38 | onChange(text); 39 | } 40 | }, []); 41 | 42 | const debouncedValue = useDebounce(value, debounceTime); 43 | 44 | return ( 45 |
46 |
47 |