├── test ├── setup │ ├── stylesMock.js │ └── index.ts ├── utils │ └── create-editor.ts └── module │ └── plugin.test.ts ├── .yarnrc ├── .eslintignore ├── .babelrc ├── postcss.config.js ├── .npmignore ├── commitlint.config.js ├── src ├── index.ts ├── extends.d.ts └── module │ ├── index.ts │ └── plugin.ts ├── .editorconfig ├── .release-it.js ├── .vscode └── settings.json ├── .github └── workflows │ ├── npm-publish.yml │ └── build.yml ├── .prettierrc.js ├── README.md ├── jest.config.js ├── README-en.md ├── tsconfig.json ├── .eslintrc.js ├── DEV.md ├── example ├── index.ts └── index.html ├── LICENSE ├── .cz-config.js ├── .gitignore └── package.json /test/setup/stylesMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.npm.taobao.org" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | dist/ 3 | lib/ 4 | *.html -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": [] 4 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('autoprefixer')], 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | .vscode/ 3 | build/ 4 | node_modules/ 5 | test/ 6 | yarn.lock 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['cz'], 3 | rules: { 4 | 'type-empty': [2, 'never'], 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description src entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import module from './module/index' 7 | 8 | export default module 9 | -------------------------------------------------------------------------------- /src/extends.d.ts: -------------------------------------------------------------------------------- 1 | import { SlateElement, SlateDescendant } from '@wangeditor/editor' 2 | 3 | declare module '@wangeditor/editor' { 4 | interface SlateElement { 5 | type: string 6 | children: SlateDescendant[] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/setup/index.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | import nodeCrypto from 'crypto' 3 | 4 | // @ts-ignore 5 | global.crypto = { 6 | getRandomValues: function (buffer: any) { 7 | return nodeCrypto.randomFillSync(buffer) 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /src/module/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description module entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/editor' 7 | 8 | import withMarkdown from './plugin' 9 | 10 | const module: Partial = { 11 | editorPlugin: withMarkdown, 12 | } 13 | 14 | export default module 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.txt] 16 | insert_final_newline = false 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /test/utils/create-editor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description create editor 3 | * @author wangfupeng 4 | */ 5 | 6 | import { createEditor } from '@wangeditor/editor' 7 | 8 | export default function (options: any = {}) { 9 | const container = document.createElement('div') 10 | document.body.appendChild(container) 11 | 12 | return createEditor({ 13 | selector: container, 14 | ...options, 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /.release-it.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | git: { 3 | tagName: "v${version}", 4 | commitMessage: "release: v${version}", 5 | requireCleanWorkingDir: false, 6 | requireBranch: "main", 7 | }, 8 | hooks: { 9 | "before:init": ["git pull origin main", "npm run test"], 10 | }, 11 | npm: { 12 | publish: false, 13 | }, 14 | prompt: { 15 | ghRelease: false, 16 | glRelease: false, 17 | publish: false, 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "cSpell.words": [ 4 | "codesandbox", 5 | "commitlint", 6 | "Elems", 7 | "hoverbar", 8 | "prettierrc", 9 | "wangeditor", 10 | "wangfupeng" 11 | ], 12 | "typescript.tsdk": "node_modules/typescript/lib", 13 | "editor.codeActionsOnSave": { 14 | "source.fixAll.eslint": true 15 | }, 16 | "eslint.validate": [ 17 | "javascript" 18 | ], 19 | "editor.detectIndentation": false, 20 | "editor.tabSize": 2 21 | } -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: publish npm Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | publish-npm: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: 12 16 | registry-url: https://registry.npmjs.org/ 17 | - run: npm i 18 | - run: npm run test 19 | - run: npm run build 20 | - run: npm publish --access=public 21 | env: 22 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 23 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 箭头函数只有一个参数的时候可以忽略括号 3 | arrowParens: 'avoid', 4 | // 括号内部不要出现空格 5 | bracketSpacing: true, 6 | // 行结束符使用 Unix 格式 7 | endOfLine: 'lf', 8 | // true: Put > on the last line instead of at a new line 9 | jsxBracketSameLine: false, 10 | // 行宽 11 | printWidth: 100, 12 | // 换行方式 13 | proseWrap: 'preserve', 14 | // 分号 15 | semi: false, 16 | // 使用单引号 17 | singleQuote: true, 18 | // 缩进 19 | tabWidth: 2, 20 | // 使用 tab 缩进 21 | useTabs: false, 22 | // 后置逗号,多行对象、数组在最后一行增加逗号 23 | trailingComma: 'es5', 24 | parser: 'typescript', 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wangEditor markdown 插件 2 | 3 | [English documentation](./README-en.md) 4 | 5 | ## 介绍 6 | 7 | 在 [wangEditor](https://www.wangeditor.com/) 中使用基本的 markdown 语法。 8 | 9 | - 标题 10 | - `#` 11 | - `##` 12 | - `###` 13 | - `####` 14 | - `#####` 15 | - 列表 `-` `+` `*` 16 | - 引用 `>` 17 | - 分割线 `---` 18 | - 代码块 ```js 19 | 20 | ## 安装 21 | 22 | ```sh 23 | yarn add @wangeditor/plugin-md 24 | ``` 25 | 26 | ## 使用 27 | 28 | 要在创建编辑器之前注册,且只能注册一次,不可重复注册。 29 | 30 | ```js 31 | import { Boot } from '@wangeditor/editor' 32 | import markdownModule from '@wangeditor/plugin-md' 33 | 34 | Boot.registerModule(markdownModule) 35 | 36 | // Then create editor and toolbar 37 | ``` 38 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: test and build 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | - 'master' 8 | - 'dev' 9 | - 'feature-*' 10 | - 'fix-*' 11 | - 'hotfix-*' 12 | - 'refactor-*' 13 | paths: 14 | - '.github/workflows/*' 15 | - 'src/**' 16 | - 'example/**' 17 | - 'test/**' 18 | - 'build/**' 19 | 20 | jobs: 21 | publish-npm: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions/setup-node@v1 26 | with: 27 | node-version: 12 28 | registry-url: https://registry.npmjs.org/ 29 | - run: npm i 30 | - run: npm run test 31 | - run: npm run build 32 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/test'], 3 | testEnvironment: 'jsdom', 4 | testMatch: ['**/(*.)+(spec|test).+(ts|js|tsx)'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest', 7 | '^.+\\.js$': 'ts-jest', 8 | }, 9 | globals: { 10 | 'ts-jest': { 11 | tsconfig: '/tsconfig.json', 12 | }, 13 | }, 14 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 15 | moduleNameMapper: { 16 | '^.+\\.(css|less)$': '/test/setup/stylesMock.js', 17 | 18 | // // import `dom7` 时默认是 esm 格式,换成 umd 格式 19 | // dom7: '/node_modules/dom7/dom7.js', 20 | }, 21 | transformIgnorePatterns: ['node_modules'], 22 | setupFilesAfterEnv: ['/test/setup/index.ts'], 23 | } 24 | -------------------------------------------------------------------------------- /README-en.md: -------------------------------------------------------------------------------- 1 | # wangEditor markdown plugin 2 | 3 | [中文文档](./README.md) 4 | 5 | ## Introduction 6 | 7 | Use basic markdown syntax in [wangEditor](https://www.wangeditor.com/en/). 8 | 9 | - Header 10 | - `#` 11 | - `##` 12 | - `###` 13 | - `####` 14 | - `#####` 15 | - List `-` `+` `*` 16 | - Blockquote `>` 17 | - Divider `---` 18 | - Codeblock ```js 19 | 20 | ## Installation 21 | 22 | ```sh 23 | yarn add @wangeditor/plugin-md 24 | ``` 25 | 26 | ## Usage 27 | 28 | You should register plugin before create editor, and register only once (not repeatedly). 29 | 30 | ```js 31 | import { Boot } from '@wangeditor/editor' 32 | import markdownModule from '@wangeditor/plugin-md' 33 | 34 | Boot.registerModule(markdownModule) 35 | 36 | // Then create editor and toolbar 37 | ``` 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "ES2015", 5 | "lib": ["es6", "dom", "esnext"], 6 | "declaration": true, 7 | "sourceMap": true, 8 | "outDir": "dist", 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "moduleResolution": "node", 14 | "forceConsistentCasingInFileNames": true, 15 | "jsx": "react", 16 | "jsxFactory": "jsx", 17 | "downlevelIteration": true 18 | }, 19 | "include": [ 20 | "src/**/*", 21 | "example/**/*", 22 | "test/**/*" 23 | ], 24 | "exclude": [ 25 | "node_modules", 26 | "build" 27 | ] 28 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | mocha: true, 6 | jest: true, 7 | node: true, 8 | }, 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:@typescript-eslint/eslint-recommended', 12 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 13 | ], 14 | globals: { 15 | Atomics: 'readonly', 16 | SharedArrayBuffer: 'readonly', 17 | }, 18 | parser: '@typescript-eslint/parser', 19 | parserOptions: { 20 | ecmaVersion: 2018, 21 | sourceType: 'module', 22 | }, 23 | plugins: ['@typescript-eslint', 'prettier'], 24 | rules: { 25 | 'no-unused-vars': 0, 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /DEV.md: -------------------------------------------------------------------------------- 1 | # Dev doc 2 | 3 | ## 主要目录 4 | 5 | - `src` 源代码 6 | - `test` 单元测试 7 | - `example` 本地测试 demo ,不用于 build 8 | - `build` 打包配置 9 | 10 | ## dev 本地运行 11 | 12 | `yarn dev` 启动本地服务,**使用 example 目录**。 13 | 14 | `yarn test` 单元测试,使用 test 目录。 15 | 16 | ## build 构建 17 | 18 | `yarn build` 构建代码,**使用 src 目录**。 19 | 20 | ## release 发布 21 | 22 | 第一,升级 package.json 版本 23 | 24 | 第二,提交 git tag 可触发 github actions 并发布 npm 25 | 26 | ```sh 27 | git tag -a v1.0.1 -m "v1.0.1" # 和 package.json 版本同步即可 28 | git push origin --tags 29 | ``` 30 | 31 | ## 注意事项 32 | 33 | package.json 34 | - 定义 `"main": "dist/index.js"` 35 | - 定义 `"module": "dist/index.js"` 36 | - 定义 `"types": "dist/src/index.d.ts"` 37 | - `@wangeditor/editor` 不要安装在 `dependencies` ,否则用户安装时也会安装它们 38 | 39 | webpack 配置 40 | - 定义 `library` 41 | - 定义 `externals` ,构建时忽略 `@wangeditor/editor` ,否则体积会很大 42 | -------------------------------------------------------------------------------- /example/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description examples entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IDomEditor, createEditor, createToolbar, Boot, IEditorConfig } from '@wangeditor/editor' 7 | import module from '../src/index' 8 | 9 | // 注册 10 | Boot.registerModule(module) 11 | 12 | // 编辑器配置 13 | const editorConfig: Partial = { 14 | onChange(editor: IDomEditor) { 15 | const html = editor.getHtml() 16 | // @ts-ignore 17 | document.getElementById('text-html').value = html 18 | const contentStr = JSON.stringify(editor.children, null, 2) 19 | // @ts-ignore 20 | document.getElementById('text-json').value = contentStr 21 | }, 22 | } 23 | 24 | // 创建编辑器 25 | const editor = createEditor({ 26 | selector: '#editor-container', 27 | config: editorConfig, 28 | html: `

hello world


`, 29 | }) 30 | const toolbar = createToolbar({ 31 | editor, 32 | selector: '#toolbar-container', 33 | config: {}, 34 | }) 35 | 36 | // @ts-ignore 为了便于调试,暴露到 window 37 | window.editor = editor 38 | // @ts-ignore 39 | window.toolbar = toolbar 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 wangEditor 团队 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 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | wangEditor markdown plugin demo 9 | 10 | 11 | 12 | 13 | 14 |

wangEditor markdown plugin demo

15 | 16 |
17 |
18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | 27 |
28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /.cz-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | types: [ 3 | { 4 | value: 'WIP', 5 | name: '💡 WIP: Work in progress', 6 | }, 7 | { 8 | value: 'feat', 9 | name: '🚀 feat: A new feature', 10 | }, 11 | { 12 | value: 'fix', 13 | name: '🔧 fix: A bug fix', 14 | }, 15 | { 16 | value: 'refactor', 17 | name: '🔨 refactor: A code change that neither fixes a bug nor adds a feature', 18 | }, 19 | { 20 | value: 'release', 21 | name: '🛳 release: Bump to a new Semantic version', 22 | }, 23 | { 24 | value: 'docs', 25 | name: '📚 docs: Documentation only changes', 26 | }, 27 | { 28 | value: 'test', 29 | name: '🔍 test: Add missing tests or correcting existing tests', 30 | }, 31 | { 32 | value: 'perf', 33 | name: '⚡️ perf: Changes that improve performance', 34 | }, 35 | { 36 | value: 'chore', 37 | name: 38 | "🚬 chore: Changes that don't modify src or test files. Such as updating build tasks, package manager", 39 | }, 40 | { 41 | value: 'workflow', 42 | name: 43 | '📦 workflow: Changes that only affect the workflow. Such as updating build systems or CI etc.', 44 | }, 45 | { 46 | value: 'style', 47 | name: 48 | '💅 style: Code Style, Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)', 49 | }, 50 | { 51 | value: 'revert', 52 | name: '⏱ revert: Revert to a commit', 53 | }, 54 | ], 55 | // Specify the scopes for your particular project 56 | scopes: [], 57 | allowCustomScopes: true, 58 | allowBreakingChanges: ['feat', 'fix'], 59 | } 60 | -------------------------------------------------------------------------------- /.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 | ISSUE.md 107 | -------------------------------------------------------------------------------- /test/module/plugin.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description plugin test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { SlateEditor, IDomEditor } from '@wangeditor/editor' 7 | import createEditor from '../utils/create-editor' 8 | import withMarkdown from '../../src/module/plugin' 9 | 10 | describe('markdown plugin', () => { 11 | let editor = withMarkdown(createEditor({})) 12 | function getStartLocation(editor: IDomEditor) { 13 | return SlateEditor.start(editor, []) 14 | } 15 | 16 | beforeEach(() => { 17 | editor = withMarkdown(createEditor({})) 18 | }) 19 | 20 | it('header', () => { 21 | editor.select(getStartLocation(editor)) 22 | editor.insertText('##') 23 | editor.insertText(' ') 24 | expect(editor.children).toEqual([ 25 | { 26 | type: 'header2', 27 | children: [{ text: '' }], 28 | }, 29 | ]) 30 | }) 31 | 32 | it('blockquote', () => { 33 | editor.select(getStartLocation(editor)) 34 | editor.insertText('>') 35 | editor.insertText(' ') 36 | expect(editor.children).toEqual([ 37 | { 38 | type: 'blockquote', 39 | children: [{ text: '' }], 40 | }, 41 | ]) 42 | }) 43 | 44 | it('list-item', () => { 45 | editor.select(getStartLocation(editor)) 46 | editor.insertText('-') 47 | editor.insertText(' ') 48 | expect(editor.children).toEqual([ 49 | { 50 | type: 'bulleted-list', 51 | children: [ 52 | { 53 | type: 'list-item', 54 | children: [{ text: '' }], 55 | }, 56 | ], 57 | }, 58 | ]) 59 | }) 60 | 61 | it('divider', () => { 62 | editor.select(getStartLocation(editor)) 63 | editor.insertText('---') 64 | editor.insertBreak() 65 | expect(editor.children).toEqual([ 66 | { 67 | type: 'divider', 68 | children: [{ text: '' }], 69 | }, 70 | { 71 | type: 'paragraph', 72 | children: [{ text: '' }], 73 | }, 74 | ]) 75 | }) 76 | 77 | it('pre/code', () => { 78 | editor.select(getStartLocation(editor)) 79 | editor.insertText('```js') 80 | editor.insertBreak() 81 | expect(editor.children).toEqual([ 82 | { 83 | type: 'paragraph', 84 | children: [ 85 | { 86 | text: '', 87 | }, 88 | ], 89 | }, 90 | { 91 | type: 'pre', 92 | children: [ 93 | { 94 | type: 'code', 95 | language: 'javascript', 96 | children: [ 97 | { 98 | text: '', 99 | }, 100 | ], 101 | }, 102 | ], 103 | }, 104 | { 105 | type: 'paragraph', 106 | children: [ 107 | { 108 | text: '', 109 | }, 110 | ], 111 | }, 112 | ]) 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wangeditor/plugin-md", 3 | "version": "1.0.0", 4 | "description": "wangEditor markdown plugin", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/wangeditor-team/wangEditor-plugin-md.git" 8 | }, 9 | "keywords": [ 10 | "wangeditor", 11 | "markdown" 12 | ], 13 | "author": "github.com/wangfupeng1988", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/wangeditor-team/wangEditor-plugin-md/issues" 17 | }, 18 | "homepage": "https://github.com/wangeditor-team/wangEditor-plugin-md#readme", 19 | "main": "dist/index.js", 20 | "module": "dist/index.js", 21 | "types": "dist/src/index.d.ts", 22 | "scripts": { 23 | "test": "cross-env NODE_OPTIONS=--unhandled-rejections=warn jest --detectOpenHandles --passWithNoTests", 24 | "test-c": "cross-env NODE_OPTIONS=--unhandled-rejections=warn jest --coverage", 25 | "dev": "cross-env NODE_ENV=development webpack serve --config build/webpack.dev.js", 26 | "build": "cross-env NODE_ENV=production webpack --config build/webpack.prod.js", 27 | "build:analyzer": "cross-env NODE_ENV=production_analyzer webpack --config build/webpack.prod.js", 28 | "release": "release-it", 29 | "format": "yarn prettier --write", 30 | "lint": "eslint \"{src,test,cypress,build,example}/**/*.{js,ts}\"", 31 | "lint-fix": "eslint --fix \"{src,test,cypress,build,example}/**/*.{js,ts}\"", 32 | "prettier": "prettier --write --config .prettierrc.js \"{src,test,cypress,build,example}/**/*.{js,ts}\"" 33 | }, 34 | "lint-staged": { 35 | "*.{ts,tsx,js}": [ 36 | "yarn prettier", 37 | "yarn lint", 38 | "yarn test" 39 | ] 40 | }, 41 | "husky": { 42 | "hooks": { 43 | "pre-commit": "lint-staged", 44 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 45 | } 46 | }, 47 | "config": { 48 | "commitizen": { 49 | "path": "node_modules/cz-customizable" 50 | } 51 | }, 52 | "devDependencies": { 53 | "@babel/core": "^7.13.14", 54 | "@babel/preset-env": "^7.13.12", 55 | "@testing-library/jest-dom": "^5.16.2", 56 | "@types/jest": "^27.4.0", 57 | "@typescript-eslint/eslint-plugin": "^5.12.0", 58 | "@typescript-eslint/parser": "^5.12.0", 59 | "@wangeditor/editor": "^5.0.0", 60 | "autoprefixer": "^10.2.5", 61 | "babel-jest": "^27.3.1", 62 | "babel-loader": "^8.2.2", 63 | "clean-webpack-plugin": "^3.0.0", 64 | "commitlint": "^16.2.1", 65 | "commitlint-config-cz": "^0.13.3", 66 | "cross-env": "^7.0.3", 67 | "crypto": "^1.0.1", 68 | "css-loader": "^5.2.0", 69 | "cz-customizable": "^6.3.0", 70 | "eslint": "^8.9.0", 71 | "eslint-config-prettier": "^8.3.0", 72 | "eslint-plugin-prettier": "^4.0.0", 73 | "html-webpack-plugin": "^5.3.1", 74 | "husky": "^7.0.4", 75 | "jest": "^27.5.1", 76 | "jest-environment-jsdom": "^27.5.1", 77 | "less": "^4.1.1", 78 | "less-loader": "^8.0.0", 79 | "lint-staged": "^12.3.4", 80 | "postcss-loader": "^5.2.0", 81 | "prettier": "^2.5.1", 82 | "raw-loader": "^4.0.2", 83 | "release-it": "^14.11.6", 84 | "style-loader": "^2.0.0", 85 | "ts-jest": "^27.0.7", 86 | "ts-loader": "^8.1.0", 87 | "typescript": "^4.2.3", 88 | "url-loader": "^4.1.1", 89 | "webpack": "^5.30.0", 90 | "webpack-bundle-analyzer": "^4.4.0", 91 | "webpack-cli": "^4.6.0", 92 | "webpack-dev-server": "^3.11.2", 93 | "webpack-merge": "^5.7.3" 94 | }, 95 | "peerDependencies": { 96 | "@wangeditor/editor": ">=5.0.0" 97 | }, 98 | "dependencies": {} 99 | } 100 | -------------------------------------------------------------------------------- /src/module/plugin.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description plugin 3 | * @author wangfupeng 4 | */ 5 | 6 | import { SlateRange, SlateEditor, SlateTransforms, SlateElement } from '@wangeditor/editor' 7 | import { DomEditor, IDomEditor } from '@wangeditor/editor' 8 | 9 | // 空格对应的 md 关键字 10 | const KEY_TO_TYPE_FOR_SPACE: { [key: string]: string } = { 11 | '*': 'list-item', 12 | '-': 'list-item', 13 | '+': 'list-item', 14 | '>': 'blockquote', 15 | '#': 'header1', 16 | '##': 'header2', 17 | '###': 'header3', 18 | '####': 'header4', 19 | '#####': 'header5', 20 | } 21 | 22 | // 回车对应的 md 关键字 23 | const KEY_TO_TYPE_FOR_ENTER: { [key: string]: string } = { 24 | '---': 'divider', 25 | '----': 'divider', 26 | '-----': 'divider', 27 | '------': 'divider', 28 | 29 | // PS: '```' 转换 pre/code 比较麻烦,单独写 30 | } 31 | 32 | /** 33 | * 获取 md 关键字和关键字的 range 34 | * @param editor editor 35 | */ 36 | function getBeforeText(editor: IDomEditor): { beforeText: string; range: SlateRange | null } { 37 | const { selection } = editor 38 | if (selection == null) return { beforeText: '', range: null } 39 | const { anchor } = selection 40 | // 找到当前文本上面的 block 元素,如 header1 paragraph 41 | const block = SlateEditor.above(editor, { 42 | match: n => SlateEditor.isBlock(editor, n), 43 | }) 44 | if (block == null) return { beforeText: '', range: null } 45 | const blockPath = block[1] 46 | const blockStart = SlateEditor.start(editor, blockPath) // block 元素的起始位置,就第一个文字的位置 47 | const range = { anchor, focus: blockStart } 48 | const beforeText = SlateEditor.string(editor, range) || '' 49 | return { beforeText, range } 50 | } 51 | 52 | function withMarkdown(editor: T) { 53 | const { insertBreak, insertText } = editor 54 | const newEditor = editor 55 | 56 | // 输入空格时,尝试转换 markdown 57 | newEditor.insertText = text => { 58 | const { selection } = editor 59 | if (selection == null) return insertText(text) 60 | if (SlateRange.isExpanded(selection)) return insertText(text) 61 | if (DomEditor.getSelectedNodeByType(editor, 'paragraph') == null) return insertText(text) // 必须在 paragraph 内 62 | if (text !== ' ') return insertText(text) // 必须是输入空格 63 | 64 | // 获取空格前面的文字 65 | const { beforeText, range } = getBeforeText(editor) 66 | if (!beforeText || !range) return insertText(text) 67 | 68 | // 根据 md 关键字,找到要转换 elem 的 type 69 | const type = KEY_TO_TYPE_FOR_SPACE[beforeText] 70 | if (!type) return insertText(text) 71 | 72 | // 转换为 type elem 73 | SlateTransforms.select(editor, range) 74 | SlateTransforms.delete(editor) 75 | const props: Partial = { 76 | type, 77 | } 78 | SlateTransforms.setNodes(editor, props, { 79 | match: n => SlateEditor.isBlock(editor, n), 80 | }) 81 | 82 | // 如果是 list-item ,则包裹一层 bulleted-list 83 | if (type === 'list-item') { 84 | const listElem = { 85 | type: 'bulleted-list', 86 | children: [], 87 | } 88 | SlateTransforms.wrapNodes(editor, listElem, { 89 | match: n => DomEditor.getNodeType(n) === 'list-item', 90 | }) 91 | } 92 | } 93 | 94 | // 换行时,尝试转换 markdown 95 | newEditor.insertBreak = () => { 96 | const { selection } = editor 97 | if (selection == null) return insertBreak() 98 | if (SlateRange.isExpanded(selection)) return insertBreak() 99 | if (DomEditor.getSelectedNodeByType(editor, 'paragraph') == null) return insertBreak() // 必须在 paragraph 内 100 | 101 | const { beforeText, range } = getBeforeText(editor) // 获取前面的文字 102 | if (!beforeText || !range) return insertBreak() 103 | 104 | // 单独处理 pre/code 105 | if (beforeText.indexOf('```') === 0) { 106 | let lang = beforeText.slice(3).toLowerCase().trim() // 获取 ```js 中的语言,如 js 107 | if (lang === 'js') lang = 'javascript' 108 | if (lang === 'ts') lang = 'typescript' 109 | if (lang === 'md') lang = 'markdown' 110 | if (lang === 'py') lang = 'python' 111 | if (lang === 'vb') lang = 'visual-basic' 112 | if (lang === 'c#') lang = 'csharp' 113 | 114 | const { codeLangs = [] } = (editor.getConfig().MENU_CONF || {}).codeSelectLang || [] 115 | if (lang) { 116 | // lang 有可能不在支持范围之内 117 | const isValid = codeLangs.some((item: any) => item.value === lang) 118 | if (!isValid) return insertBreak() 119 | } 120 | 121 | // 删除 ```js 文字 122 | SlateTransforms.select(editor, range) 123 | SlateTransforms.delete(editor) 124 | 125 | // 插入 pre/code elem 126 | const preElem = { 127 | type: 'pre', 128 | children: [ 129 | { 130 | type: 'code', 131 | language: lang, 132 | children: [{ text: '' }], 133 | }, 134 | ], 135 | } 136 | SlateTransforms.insertNodes(editor, preElem) 137 | return 138 | } 139 | 140 | // 根据 md 关键字,找到要转换 elem 的 type 141 | const type = KEY_TO_TYPE_FOR_ENTER[beforeText] 142 | if (!type) return insertBreak() 143 | 144 | // 转换为 type elem 145 | SlateTransforms.select(editor, range) 146 | SlateTransforms.delete(editor) 147 | const props: Partial = { 148 | type, 149 | } 150 | SlateTransforms.setNodes(editor, props, { 151 | match: n => SlateEditor.isBlock(editor, n), 152 | }) 153 | } 154 | 155 | return newEditor 156 | } 157 | 158 | export default withMarkdown 159 | --------------------------------------------------------------------------------