[
  {
    "path": ".browserslistrc",
    "content": "defaults\nnot IE 11\nmaintained node versions"
  },
  {
    "path": ".cz-config.js",
    "content": "module.exports = {\n  types: [\n    {\n      value: 'WIP',\n      name: '💡  WIP: Work in progress',\n    },\n    {\n      value: 'feat',\n      name: '🚀  feat: A new feature',\n    },\n    {\n      value: 'fix',\n      name: '🔧  fix: A bug fix',\n    },\n    {\n      value: 'refactor',\n      name: '🔨  refactor: A code change that neither fixes a bug nor adds a feature',\n    },\n    {\n      value: 'release',\n      name: '🛳  release: Bump to a new Semantic version',\n    },\n    {\n      value: 'docs',\n      name: '📚  docs: Documentation only changes',\n    },\n    {\n      value: 'test',\n      name: '🔍  test: Add missing tests or correcting existing tests',\n    },\n    {\n      value: 'perf',\n      name: '⚡️  perf: Changes that improve performance',\n    },\n    {\n      value: 'chore',\n      name:\n        \"🚬  chore: Changes that don't modify src or test files. Such as updating build tasks, package manager\",\n    },\n    {\n      value: 'workflow',\n      name:\n        '📦  workflow: Changes that only affect the workflow. Such as updating build systems or CI etc.',\n    },\n    {\n      value: 'style',\n      name:\n        '💅  style: Code Style, Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)',\n    },\n    {\n      value: 'revert',\n      name: '⏱  revert: Revert to a commit',\n    },\n  ],\n  // Specify the scopes for your particular project\n  scopes: [],\n  allowCustomScopes: true,\n  allowBreakingChanges: ['feat', 'fix'],\n} \n"
  },
  {
    "path": ".eslintignore",
    "content": "node_modules/*\ndist/\nlib/\n*.html"
  },
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n  env: {\n    browser: true,\n    es6: true,\n    mocha: true,\n    jest: true,\n    node: true,\n  },\n  extends: [\n    'eslint:recommended',\n    'plugin:@typescript-eslint/eslint-recommended',\n    'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier\n    '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.\n  ],\n  globals: {\n    Atomics: 'readonly',\n    SharedArrayBuffer: 'readonly',\n  },\n  parser: '@typescript-eslint/parser',\n  parserOptions: {\n    ecmaVersion: 2018,\n    sourceType: 'module',\n  },\n  plugins: ['@typescript-eslint', 'prettier'],\n  rules: {\n    'no-unused-vars': 0,\n  },\n}\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\nopen_collective: wangeditor\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.md",
    "content": "---\nname: 提交 bug\nabout: 请大家一定要按照该模板填写，以方便我们更快复现，否则该 issue 将不予受理！\n---\n\n## bug 描述\n\n*请输入内容……*\n\n## 你预期的样子是？\n\n*请输入内容……*\n\n## 系统和浏览器及版本号\n\n- 操作系统\n- 浏览器和版本\n\n## wangEditor 版本\n\n*请输入内容……*\n\n## demo 能否复现该 bug ？\n\n能/不能\n\n- 中文 demo https://www.wangeditor.com/demo/\n- English demo https://www.wangeditor.com/demo/?lang=en\n\n## 在线 demo\n\n*请尽量提供在线 demo （推荐以下网站），帮助我们最低成本复现 bug*\n\n- https://codesandbox.io/\n- https://codepen.io/\n- https://stackblitz.com/\n\n## 最小成本的复现步骤\n\n（请告诉我们，如何最快的复现该 bug）\n\n- 步骤一\n- 步骤二\n- 步骤三\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.md",
    "content": "---\nname: 建议增加新功能\nabout: 请按照该模板填写，以便我们能真正了解你的需求，否则该 issue 将不予受理！\n---\n\n## 功能描述\n\n*请输入内容……*\n\n## 提炼几个功能点\n\n- 功能1\n- 功能2\n- 功能3\n\n## 原型图\n\n*涉及到 UI 改动的功能，请一定提供原型图。原型图能表明功能即可，不要求规范和美观*\n\n## 可参考的案例\n\n*是否已有可参考的案例（如其他编辑器），有的话请给出链接*\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/question.md",
    "content": "---\nname: 使用时遇到了问题（非 bug）\nabout: 请按照该模板填写，以便我们能真正了解你的问题，否则该 issue 将不予受理！\n---\n\n## 问题描述\n\n*请输入遇到的问题...*\n\n## wangEditor 版本\n\n*请输入内容……*\n\n## 是否查阅了文档 ？\n\n（文档链接 [www.wangeditor.com](https://www.wangeditor.com/) ）\n\n*是/否*\n\n## 最小成本的复现步骤\n\n（请告诉我们，如何**最快的**复现该问题？）\n\n- 步骤一\n- 步骤二\n- 步骤三\n"
  },
  {
    "path": ".github/workflows/deploy-demos.yml",
    "content": "# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node\n# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions\n# github actions 中文文档 https://docs.github.com/cn/actions/getting-started-with-github-actions\n\nname: deploy demos\n\non:\n  push:\n    branches: [ deploy-demos ] # 特定分支\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v2\n    - name: set ssh key # 临时设置 ssh key\n      run: |\n        mkdir -p ~/.ssh/\n        echo \"${{secrets.SSH_KEY_FOR_GITHUB_WANGEDITOR}}\" > ~/.ssh/id_rsa\n        chmod 600 ~/.ssh/id_rsa\n        ssh-keyscan \"github.com\" >> ~/.ssh/known_hosts\n        echo \"---------- set ssh-key ok ----------\"\n    - name: download and replace # 下载现有文件，替换\n      run: |\n        git clone git@github.com:wangEditor/demo.git\n        echo \"---------- git clone ok ----------\"\n        cp -r ./packages/editor/demo/* ./demo ## 用最新构建出来的文件，替换现有的\n        echo \"---------- replace ok ----------\"\n    - name: upload # 上传文件\n      run: |\n        cd ./demo\n        git config user.name \"github-actions\"\n        git config user.email \"github-actions@github.com\"\n        echo \"---------- begin git status ----------\"\n        echo `git status`\n        echo \"---------- end git status ----------\"\n        git add .\n        git commit -m \"update by github actions\"\n        echo \"---------- begin git push ----------\"\n        git push origin main\n        echo \"---------- end git push ----------\"\n    - name: delete ssh key # 删除 ssh key\n      run: rm -rf ~/.ssh/id_rsa\n\n\n"
  },
  {
    "path": ".github/workflows/deploy-examples.yml.bak",
    "content": "# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node\n# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions\n# github actions 中文文档 https://docs.github.com/cn/actions/getting-started-with-github-actions\n\nname: deploy to baidu server - example page\n\non:\n  push:\n    branches:\n      - 'main'\n      - 'master'\n      - 'dev'\n      - 'feature-*'\n      - 'fix-*'\n      - 'hotfix-*'\n      - 'refactor-*'\n    paths:\n      - '.github/workflows/*'\n      - 'packages/**'\n      - 'tests/**'\n      - 'build/**'\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n    - name: Checkout repo\n      uses: actions/checkout@v2\n      with:\n        fetch-depth: 0\n    - name: Setup node\n      uses: actions/setup-node@v2\n      with:\n        node-version: 12.x\n        registry-url: https://registry.npmjs.com\n    - name: Install dependencies\n      run: yarn run bootstrap\n    - name: Build packages\n      run: yarn build\n    - name: Unit test\n      run: yarn run test\n\n\n\n    # 2022.06.07 百度云服务器到期，就不再部署到测试机了 - wangfupeng\n    - name: set ssh key # 临时设置 ssh key\n      run: |\n        mkdir -p ~/.ssh/\n        echo \"${{secrets.WFP_ID_RSA}}\" > ~/.ssh/id_rsa\n        chmod 600 ~/.ssh/id_rsa\n        ssh-keyscan ${{secrets.BAIDU_SERVER}} >> ~/.ssh/known_hosts\n    - name: scp example files # 拷贝测试页面，到远程服务器\n      run: |\n        ## 获取当前分支名称，并创建一个同名的文件夹\n        currentBranchName=`git branch | awk '$1 == \"*\"{print $2}'`\n        mkdir -p $currentBranchName/dist/css\n\n        ## 将 dist examples 移到刚创建的文件夹之内\n        mv packages/editor/dist/css/* $currentBranchName/dist/css/\n        mv packages/editor/dist/index.js $currentBranchName/dist/index.js\n        mv packages/editor/dist/index.js.map $currentBranchName/dist/index.js.map\n        mv packages/editor/examples/ $currentBranchName/examples/\n\n        ## 将该文件夹，及其所有文件，上传到服务器\n        echo current branch name is: $currentBranchName\n        ssh work@${{secrets.BAIDU_SERVER}} \"rm -rf /home/work/wangEditor-team/v5-examples/$currentBranchName\"\n        scp -r ./$currentBranchName work@${{secrets.BAIDU_SERVER}}:/home/work/wangEditor-team/v5-examples/$currentBranchName\n    - name: delete ssh key # 删除 ssh key\n      run: rm -rf ~/.ssh/id_rsa\n"
  },
  {
    "path": ".github/workflows/e2e.yml",
    "content": "# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node\n# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions\n# github actions 中文文档 https://docs.github.com/cn/actions/getting-started-with-github-actions\n\nname: Cypress tests\n\non:\n  push:\n    branches:\n      - 'master'\n      - 'dev'\n      - 'feature-*'\n      - 'fix-*'\n      - 'hotfix-*'\n      - 'refactor-*'\n      - 'test-*'\n    paths:\n      - '.github/workflows/*'\n      - 'packages/**'\n      - 'scripts/**'\n      - 'tests/**'\n      - 'build/**'\n      - 'cypress/**'\n      - 'babel.config.json'\n      - 'cypress.json'\n\njobs:\n  test-e2e:\n    runs-on: ubuntu-latest\n    container: cypress/browsers:node12.13.0-chrome78-ff70\n    steps:\n    - uses: actions/checkout@v2\n    - name: Use Node.js ${{ matrix.node-version }}\n      uses: actions/setup-node@v1\n      with:\n        node-version: ${{ matrix.node-version }}\n    - name: Install dependencies\n      run: yarn install\n    - name: Build packages\n      run: yarn build  \n    - uses: cypress-io/github-action@v2\n      with:\n        browser: chrome\n        start: yarn run example\n        wait-on: 'http://localhost:8881/examples/default-mode.html'\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    strategy:\n      max-parallel: 1\n\n    steps:\n      - name: Checkout repo\n        uses: actions/checkout@v2\n        with:\n          fetch-depth: 0\n\n      - name: Setup node\n        uses: actions/setup-node@v2\n        with:\n          node-version: 14.x\n          registry-url: https://registry.npmjs.com\n\n      - name: Install dependencies\n        run: yarn run bootstrap\n\n      - name: Build packages\n        run: yarn build\n\n      - name: Unit test\n        run: yarn run test \n\n      - name: E2E test\n        uses: cypress-io/github-action@v2\n        with:\n          browser: chrome\n          start: yarn run example\n          wait-on: 'http://localhost:8881/examples/default-mode.html'   \n\n      - name: Publish npm\n        run: yarn run release:publish\n        env:\n          NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node\n# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions\n# github actions 中文文档 https://docs.github.com/cn/actions/getting-started-with-github-actions\n\nname: Build and test\n\non:\n  push:\n    branches:\n      - 'main'\n      - 'master'\n      - 'dev'\n      - 'feature-*'\n      - 'fix-*'\n      - 'hotfix-*'\n      - 'refactor-*'\n    paths:\n      - '.github/workflows/*'\n      - 'packages/**'\n      - 'tests/**'\n      - 'build/**'\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n    - name: Checkout repo\n      uses: actions/checkout@v2\n      with:\n        fetch-depth: 0\n    - name: Setup node\n      uses: actions/setup-node@v2\n      with:\n        node-version: 14.x\n        registry-url: https://registry.npmjs.com\n    - name: Install dependencies\n      run: yarn run bootstrap\n    - name: Build packages\n      run: yarn build\n    - name: Unit test\n      run: yarn run test"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n\n# Next.js build output\n.next\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and *not* Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\npackage-lock.json\n\n.DS_Store\n\nISSUE.md\nISSUE1.md\n\n.idea/\n\n# cypress\n*/cypress/videos\n*/cypress/screenshots\ncypress/videos\ncypress/screenshots\ncypress/results\ncypress/logs\n\npackages/*/dist\npackages/*/dist-example\n\nbak/\n\n# rollup 分析包体积结果\nstats.html\n"
  },
  {
    "path": ".npmignore",
    "content": ".github/\n.vscode\n"
  },
  {
    "path": ".prettierrc.js",
    "content": "module.exports = {\n  // 箭头函数只有一个参数的时候可以忽略括号\n  arrowParens: 'avoid',\n  // 括号内部不要出现空格\n  bracketSpacing: true,\n  // 行结束符使用 Unix 格式\n  endOfLine: 'lf',\n  // true: Put > on the last line instead of at a new line\n  jsxBracketSameLine: false,\n  // 行宽\n  printWidth: 100,\n  // 换行方式\n  proseWrap: 'preserve',\n  // 分号\n  semi: false,\n  // 使用单引号\n  singleQuote: true,\n  // 缩进\n  tabWidth: 2,\n  // 使用 tab 缩进\n  useTabs: false,\n  // 后置逗号，多行对象、数组在最后一行增加逗号\n  trailingComma: 'es5',\n  parser: 'typescript',\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"editor.formatOnSave\": true,\n    \"cSpell.words\": [\n      \"beforeinput\",\n      \"bodyparser\",\n      \"browserslist\",\n      \"chmod\",\n      \"clonedeep\",\n      \"compositionend\",\n      \"compositionstart\",\n      \"config\",\n      \"contenteditable\",\n      \"elems\",\n      \"hoverbar\",\n      \"img\",\n      \"isequal\",\n      \"js\",\n      \"keyscan\",\n      \"luochao\",\n      \"middlewares\",\n      \"mkdir\",\n      \"next\",\n      \"nocheck\",\n      \"prettier\",\n      \"prettierrc\",\n      \"prismjs\",\n      \"snabbdom\",\n      \"src\",\n      \"team\",\n      \"tecent\",\n      \"toarray\",\n      \"ts\",\n      \"uppy\",\n      \"vdom\",\n      \"vnode\",\n      \"wangeditor\",\n      \"wangfupeng\",\n      \"we\",\n      \"write\",\n      \"yuque\"\n    ],\n    \"typescript.tsdk\": \"node_modules/typescript/lib\",\n    \"editor.codeActionsOnSave\": {\n        \"source.fixAll.eslint\": true\n    },\n    \"eslint.validate\": [\"javascript\"],\n\n    \"editor.detectIndentation\": false,\n    \"editor.tabSize\": 2\n}"
  },
  {
    "path": ".yarnrc",
    "content": "registry \"https://registry.npm.taobao.org\""
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog Link\n- [basic-modules](./packages/basic-modules/CHANGELOG.md)\n- [code-highlight](./packages/code-highlight/CHANGELOG.md)\n- [core](./packages/core/CHANGELOG.md)\n- [editor](./packages/editor/CHANGELOG.md)\n- [list-module](./packages/list-module/CHANGELOG.md)\n- [table-module](./packages/table-module/CHANGELOG.md)\n- [upload-image-module](./packages/upload-image-module/CHANGELOG.md)\n- [video-module](./packages/video-module/CHANGELOG.md)"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 - present wangEditor-team\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README-en.md",
    "content": "# wangEditor 5\n\n[中文](./README.md)\n\n## Introduction\n\nOpen source web rich text editor, run right out of the box. Support JS Vue React.\n\n- [Document](https://www.wangeditor.com/en/)\n- [Demo](https://www.wangeditor.com/demo/?lang=en)\n\n![](./docs/images/editor-en.png)\n\n## Communication\n\nYou can [commit an issue]((https://github.com/wangeditor-team/wangEditor/issues)) if you have any question.\n\n## Donation\n\nSupport wangEditor open-source work https://opencollective.com/wangeditor\n"
  },
  {
    "path": "README.md",
    "content": "# wangEditor 5\n\n[English](./README-en.md)\n\n## 介绍\n\n开源 Web 富文本编辑器，开箱即用，配置简单。支持 JS Vue React 。\n\n- [文档](https://www.wangeditor.com/)\n- [demo](https://www.wangeditor.com/demo/)\n\n![](./docs/images/editor.png)\n\n## 交流\n\n- [讨论问题和建议](https://github.com/wangeditor-team/wangEditor/issues)\n\n## 捐赠\n\n支持 wangEditor 开源工作 https://opencollective.com/wangeditor\n"
  },
  {
    "path": "babel.config.json",
    "content": "{\n  \"presets\": [\n    [\n      \"@babel/preset-env\",\n      {\n        \"modules\": false,\n        \"useBuiltIns\": \"usage\",\n        \"corejs\": 3,\n        \"targets\": \"ie 11\"\n      }\n    ],\n    \"@babel/preset-typescript\"\n  ],\n  \"plugins\": [\n    [\n      \"@babel/plugin-transform-runtime\",\n      {\n        \"absoluteRuntime\": false,\n        \"corejs\": 3,\n        \"helpers\": true,\n        \"regenerator\": true,\n        \"useESModules\": false\n      }\n    ]\n  ]\n}"
  },
  {
    "path": "build/build-all.sh",
    "content": "#!/bin/bash\n\n## 一键打包所有 package\n\n# 获取 yarn dev/build 类型\nbuildType=build\nif [ -n \"$1\" ]; then  \n  buildType=$1\nfi\n\ncd ./packages\n\n# core 要第一个打包\ncd ./core\nrm -rf dist # 清空 dist 目录\nyarn \"$buildType\"\n\ncd ../basic-modules\nrm -rf dist # 清空 dist 目录\nyarn \"$buildType\"\n\n# code-highlight 依赖于 basic-modules 中的 code-block\ncd ../code-highlight\nrm -rf dist # 清空 dist 目录\nyarn \"$buildType\"\n\ncd ../list-module\nrm -rf dist # 清空 dist 目录\nyarn \"$buildType\"\n\ncd ../table-module\nrm -rf dist # 清空 dist 目录\nyarn \"$buildType\"\n\n# upload-image 依赖于 basic-modules 中的 image\ncd ../upload-image-module\nrm -rf dist # 清空 dist 目录\nyarn \"$buildType\"\n\ncd ../video-module\nrm -rf dist # 清空 dist 目录\nyarn \"$buildType\"\n\n# editor 依赖于上述的 core + modules\ncd ../editor\nrm -rf dist # 清空 dist 目录\nyarn \"$buildType\"\n"
  },
  {
    "path": "build/config/common.js",
    "content": "/**\n * @description rollup common config\n * @author wangfupeng\n */\n\nimport path from 'path'\nimport commonjs from '@rollup/plugin-commonjs'\nimport json from '@rollup/plugin-json'\nimport nodeResolve from '@rollup/plugin-node-resolve'\nimport typescript from 'rollup-plugin-typescript2'\nimport replace from '@rollup/plugin-replace'\nimport peerDepsExternal from 'rollup-plugin-peer-deps-external'\n// import del from 'rollup-plugin-delete'\n\nexport const extensions = ['.js', '.jsx', '.ts', '.tsx']\nconst isProd = process.env.NODE_ENV === 'production'\n\n/**\n * 生成 common conf\n * @param {string} format 'umd' 'esm'\n * @returns common conf\n */\nfunction genCommonConf(format) {\n  return {\n    input: path.resolve(__dirname, './src/index.ts'),\n    output: {\n      // 属性有 file format name sourcemap 等\n      // https://www.rollupjs.com/guide/big-list-of-options\n    },\n    plugins: [\n      peerDepsExternal(), // 打包结果不包含 package.json 的 peerDependencies\n      json({\n        compact: true,\n        indent: '  ',\n        preferConst: true,\n      }),\n      typescript({\n        clean: true,\n        tsconfig: path.resolve(__dirname, './tsconfig.json'),\n      }),\n      nodeResolve({\n        browser: true, // 重要\n        mainFields: format === 'esm' ? ['module', 'main'] : ['main'],\n        extensions,\n      }),\n      commonjs(),\n      replace({\n        'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),\n        preventAssignment: true,\n      }),\n      // del({ targets: 'dist/*' }),\n    ],\n  }\n}\n\nexport default genCommonConf\n"
  },
  {
    "path": "build/config/dev.js",
    "content": "/**\n * @description rollup dev config\n * @author wangfupeng\n */\n\nimport postcss from 'rollup-plugin-postcss'\nimport autoprefixer from 'autoprefixer'\nimport genCommonConf from './common'\n\n/**\n * 生成 dev config\n * @param {string} format 'umd' 'esm'\n */\nfunction genDevConf(format) {\n  const { input, output = {}, plugins = [], external } = genCommonConf(format)\n\n  return {\n    input,\n    output,\n    external,\n    plugins: [\n      ...plugins,\n\n      postcss({\n        plugins: [autoprefixer()],\n        extract: 'css/style.css',\n      }),\n    ],\n  }\n}\n\nexport default genDevConf\n"
  },
  {
    "path": "build/config/prd.js",
    "content": "/**\n * @description rollup prd config\n * @author wangfupeng\n */\n\nimport babel from '@rollup/plugin-babel'\nimport postcss from 'rollup-plugin-postcss'\nimport autoprefixer from 'autoprefixer'\nimport cssnano from 'cssnano'\nimport { terser } from 'rollup-plugin-terser'\nimport cleanup from 'rollup-plugin-cleanup'\nimport genCommonConf from './common'\nimport { extensions } from './common'\n\n/**\n * 生成 prd config\n * @param {string} format 'umd' 'esm'\n */\nfunction genPrdConf(format) {\n  const { input, output = {}, plugins = [], external } = genCommonConf(format)\n\n  const finalPlugins = [\n    ...plugins,\n    babel({\n      rootMode: 'upward',\n      babelHelpers: 'runtime',\n      exclude: 'node_modules/**',\n      include: 'src/**',\n      extensions,\n    }),\n    postcss({\n      plugins: [\n        autoprefixer(),\n        cssnano(), // 压缩 css\n      ],\n      extract: 'css/style.css',\n    }),\n    cleanup({\n      comments: 'none',\n      extensions: ['.ts', '.tsx'],\n    }),\n    terser(), // 压缩 js\n  ]\n\n  return {\n    input,\n    output: {\n      sourcemap: true,\n      ...output,\n    },\n    external,\n    plugins: finalPlugins,\n  }\n}\n\nexport default genPrdConf\n"
  },
  {
    "path": "build/create-rollup-config.js",
    "content": "/**\n * @description 创建 rollup 配置\n * @author wangfupeng\n */\n\nimport { merge } from 'lodash'\nimport { visualizer } from 'rollup-plugin-visualizer'\nimport genDevConf from './config/dev'\nimport genPrdConf from './config/prd'\n\n// 环境变量\nconst ENV = process.env.NODE_ENV || 'production'\nconst IS_SIZE_STATS = ENV.indexOf('size_stats') >= 0 // 分析包体积\nexport const IS_DEV = ENV.indexOf('development') >= 0\nexport const IS_PRD = ENV.indexOf('production') >= 0\n\n/**\n * 生成单个 rollup 配置\n * @param {object} customConfig { input, output, plugins ... }\n */\nexport function createRollupConfig(customConfig = {}) {\n  const { input, output = {}, plugins = [] } = customConfig\n  const { format } = output\n\n  let baseConfig\n  if (IS_PRD) {\n    baseConfig = genPrdConf(format)\n  } else {\n    baseConfig = genDevConf(format)\n  }\n\n  if (IS_SIZE_STATS) {\n    // 分析包体积。运行之后可查看 package 下的 `stats.html`\n    plugins.push(visualizer())\n  }\n\n  const config = {\n    input: input ? input : baseConfig.input,\n    output,\n    plugins,\n  }\n\n  const res = merge({}, baseConfig, config)\n  return res\n}\n"
  },
  {
    "path": "commitlint.config.js",
    "content": "module.exports = {\n  extends: ['cz'],\n  rules: {\n    'type-empty': [2, 'never'],\n  },\n}\n"
  },
  {
    "path": "cypress/cypress.d.ts",
    "content": "/// <reference types=\"cypress\" />\n\ndeclare namespace Cypress {\n  interface CustomWindow extends Window {}\n\n  interface Chainable {\n    /**\n     *  Window object with additional properties used during test.\n     */\n    window(options?: Partial<Loggable & Timeoutable>): Chainable<CustomWindow>\n\n    getByClass(dataTestAttribute: string, args?: any): Chainable<Element>\n  }\n}\n"
  },
  {
    "path": "cypress/fixtures/example.json",
    "content": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mock data for responses to routes\"\n}\n"
  },
  {
    "path": "cypress/integration/editor.spec.ts",
    "content": "describe('Basic Editor', () => {\n  it('create editor', () => {\n    cy.visit('/examples/default-mode.html')\n\n    cy.get('#btn-create').click()\n\n    cy.get('#editor-toolbar').should('have.attr', 'data-w-e-toolbar', 'true')\n    cy.get('#editor-text-area').should('have.attr', 'data-w-e-textarea', 'true')\n    cy.get('#w-e-textarea-1').contains('一行标题')\n  })\n})\n"
  },
  {
    "path": "cypress/plugins/index.ts",
    "content": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins/index.js can be used to load plugins\n//\n// You can change the location of this file or turn off loading\n// the plugins file with the 'pluginsFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/plugins-guide\n// ***********************************************************\n\n// This function is called when a project is opened or re-opened (e.g. due to\n// the project's config changing)\n\n/**\n * @type {Cypress.PluginConfig}\n */\nexport default (on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) => {\n  // `on` is used to hook into various events Cypress emits\n  // `config` is the resolved Cypress config\n  // codeCoverageTask(on, config)\n  return config\n}\n"
  },
  {
    "path": "cypress/support/commands.ts",
    "content": "Cypress.Commands.add('getByClass', (selector, ...args) => {\n  return cy.get(`.w-e-${selector}`, ...args)\n})\n"
  },
  {
    "path": "cypress/support/index.ts",
    "content": "import './commands'\n"
  },
  {
    "path": "cypress/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"es2015\", \"dom\", \"esnext\"],\n    \"types\": [\"cypress\"],\n    \"isolatedModules\": false,\n    \"allowJs\": true,\n    \"noEmit\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n  },\n  \"include\": [\n    \"./**/*.ts\"\n  ]\n}\n"
  },
  {
    "path": "cypress.json",
    "content": "{\n  \"baseUrl\": \"http://localhost:8881\",\n  \"defaultCommandTimeout\": 8000,\n  \"video\": false\n}"
  },
  {
    "path": "docs/README.md",
    "content": "# 文档\n\n- [开发文档](./dev.md)\n- [发布到 npm](./publish.md)\n- [加入研发团队](./join.md)\n"
  },
  {
    "path": "docs/dev.md",
    "content": "# 开发\n\n## 准备工作\n\n- 了解 slate.js\n- 了解 vdom 和 snabbdom.js\n- 了解 lerna\n- 已安装 yarn\n\n## 本地启动\n\n### 打包\n\n- 下载代码到本地，进入 `wangEditor` 目录\n- 安装所有依赖 `yarn bootstrap`\n- 打包所有模块 `yarn dev` 或者 `yarn build`\n\n### 运行 demo\n\n- 进入 `packages/editor` 目录，运行 `yarn example` ，浏览器打开 `http://localhost:8881/examples/`\n\n## 注意事项\n\n- 修改代码、重新打包后，要**强制刷新**浏览器\n- 如果本地包依赖有问题，试试 `lerna link` 关联内部包\n\n## 记录\n\n全局安装一个插件 `yarn add xxx --dev -W`\n\n注意合理使用 `peerDependencies` 和 `dependencies` ，不要重复打包一个第三方库\n\n执行 `lerna add ...` 之后，需要重新 `lerna link` 建立内部连接\n\n分析包体积\n- 命令行，进入某个 package ，如 `cd packages/editor`\n- 执行 `yarn size-stats` ，等待执行完成\n- 结果会记录在 `packages/editor/stats.html` 用浏览器打开"
  },
  {
    "path": "docs/join.md",
    "content": "# 加入团队\n\n欢迎加入 wangEditor 研发团队～\n\n## V5 研发人员\n\n- [王福朋](https://github.com/wangfupeng1988/) - wangEditor 创始人，资深前端工程师，PMP，曾就职于百度、滴滴\n- [罗超](https://github.com/echoLC) - 天才就是百分之一的灵感加上百分之九十九的努力\n- [TGuoW](https://github.com/TGuoW)\n- [刘庆华(火热) ](https://github.com/liuqh0609) -  热爱着，年轻着\n- [haha](https://github.com/hahaaha)\n\n## 加入条件\n\n- 熟悉 typescript ，并实际应用过\n- 熟悉 webpack 或者 rollup\n- 熟悉 React 或者 Vue\n- 熟悉 vdom 结构，熟悉 [snabbdom.js](https://github.com/snabbdom/snabbdom) （不了解的可以先去学习一下）\n- 熟悉 [slate.js](https://www.slatejs.org/) 包括：熟悉数据模型，熟悉 API，看过源码（不了解的可以先去学习一下）\n\n## 申请加入\n\n- 首先自我评价，符合上述加入条件\n- 加入 QQ 群，私聊群主，发送一份个人简历\n"
  },
  {
    "path": "docs/publish.md",
    "content": "# 发布到 NPM\n\n因为我们的项目是使用 `independent` 的方式组织 `muti-packgae`，所以每个包都有单独的版本号，默认使用 `lerna publish` 发布包，我们需要根据包的修改内容选择合适的版本号。**对于没有变动的 `package`，lerna 发布的时候不会算在本次发布的内容里面**。\n\n发布的流程分两步：\n\n第一步：将所有要发版的代码合并到 `master`  分支后，先在本地执行 `yarn release:version` 生成各个本次变动的 `package` 的版本后，自动生成 `changelog`，接着 lerna 会生成 `git tag` 并 `push` 到远程。\n\n第二步：上面步骤完成后， `lerna` push `git tag` 到远程的时候会触发我们配置的 `git action`，走完正常的发版 `action`，具体看 [`action` 配置]('./../.github/workflows/release.yml') 。\n\n因为目前我们还在开发当中，所以为了更加方便发版到 `npm` 进行测试，目前，项目中集成了以下 `release` 的 `script command`：\n\n## 正常发布一个版本\n\n```bash\nyarn release:publish\n```\n\n## 发布指定的 dist-tag 版本\n\n发布一个 `experimental` [dist-tag](https://docs.npmjs.com/cli/v7/commands/npm-dist-tag) 的版本：\n\n```bash\nyarn release:publish:experimental\n```\n\n发布一个 `next` [dist-tag](https://docs.npmjs.com/cli/v7/commands/npm-dist-tag) 的版本：\n\n```bash\nyarn release:next\n```\n\n## 发布 canary 版本\n\n发布一个 `canary` 版本：\n```bash\n# 1.0.0 => 1.0.1-alpha.0+${SHA} of packages changed since the previous commit\nlerna publish --canary\n```\n\n"
  },
  {
    "path": "docs/test.md",
    "content": "# 测试\n目前我们的项目已经集成了基于 `Jest` 的单元测试和基于 `Cypress` 的 `E2E` 测试，下面简单介绍两种测试运行和编写的方式。\n\n## 单元测试\n单元测试是从最底层 `API` 的角度出发，保证编辑功能的质量。虽然我们是基于 `lerna` 的 monorepo 管理方式，为了方便组织，我们的所有测试还是都放在根目录下的 `tests/units` 下，每个 `packages` 下面的包都在 `tests/units` 下对应一个目录。所以如果需要新增 `test`，可以按照这个目录组织方式决定把新的 `test` 放在哪个目录。\n\n### 运行单元测试\n目前单元测试的运行已经集成在 CI 流程中，如果本地开发后，需要自动执行单元测试，运行如下 `scripts` 命令：\n```bash\nyarn run test\n```\n查看单元测试的覆盖率：\n```bash\nyarn run test-c\n```\n\n### 注意事项\n- **因为各个模块依赖了 `core`，如果修改了 `core` 的代码，增加了 `API`，需要运行 `yarn build` 命令，使得各个模块能读到最新的代码**。\n\n## E2E 测试\n目前我们的项目 `E2E` 测试基于 [Cypress](https://docs.cypress.io/)，对于编辑器这种强依赖用户交互运作的产品，通过 `E2E` 保证编辑器交互更加稳定。\n\n目前 `E2E` 测试只写了基本的创建基础编辑器的用例，保证打包后的代码能正常创建编辑器。\n\n**`E2E` 测试用例目前都放在根目录下的 `cypress/integration` 目录下，如果需要增加新的测试用例，应该在此目录下创建文件。**\n\n### 运行 E2E 测试\n目前 E2E 测试的同样集成到了 CI 流程中，如果本地开发后，需要编写 E2E 测试，运行如下 `scripts` 命令：\n```bash\nyarn e2e:dev\n```\n该命令首先会启动 `packages/editor` 下面的 `example` 服务，然后再启动 Cypress 的命令， Cypress 会在本地调起 UI 界面：\n\n![cypress](images/cypress.jpg)\n\n然后你可以选择想要执行 E2E 的用例，然后执行后 Cypress 会调起浏览器，运行所有的测试用例，你可以直接在受控的浏览器直接调试你的测试：\n\n![cypress-run](images/cypress-run.jpg)\n\n如果不是为了开发新的测试用例，只是想要本地运行所有的 E2E 测试，则执行：\n```bash\nyarn e2e\n```\nCypress 则会自己后台运行所有测试，并不会打开 UI 界面和浏览器。"
  },
  {
    "path": "jest.config.js",
    "content": "module.exports = {\n  roots: ['<rootDir>/packages'],\n  testEnvironment: 'jsdom',\n  testMatch: ['**/(*.)+(spec|test).+(ts|js|tsx)'],\n  transform: {\n    '^.+\\\\.tsx?$': 'ts-jest',\n    '^.+\\\\.js$': 'ts-jest',\n  },\n  globals: {\n    'ts-jest': {\n      tsconfig: '<rootDir>/tsconfig.json',\n    },\n  },\n  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],\n  moduleNameMapper: {\n    '^.+\\\\.(css|less)$': '<rootDir>/tests/utils/stylesMock.js',\n  },\n  transformIgnorePatterns: ['node_modules/(?!(html-void-elements)/)'],\n  setupFilesAfterEnv: ['<rootDir>/tests/setup/index.ts'],\n  collectCoverageFrom: ['<rootDir>/packages/**/src/**/*.(ts|tsx)'],\n  coveragePathIgnorePatterns: [\n    'dist',\n    'locale',\n    'index.ts',\n    'config.ts',\n    'browser-polyfill.ts',\n    'node-polyfill.ts',\n  ],\n}\n"
  },
  {
    "path": "lerna.json",
    "content": "{\n  \"packages\": [\n    \"packages/*\"\n  ],\n  \"version\": \"independent\",\n  \"npmClient\": \"yarn\",\n  \"useWorkspaces\": true,\n  \"command\": {\n    \"publish\": {\n      \"ignoreChanges\": [\"ignored-file\", \"*.md\"],\n      \"message\": \"chore(release): publish\",\n      \"conventionalCommits\": true,\n      \"registry\": \"https://npm.pkg.github.com\" \n    },\n    \"version\": {\n      \"message\": \"chore(release): publish\",\n      \"allowBranch\": \"master\"\n    }\n  },\n  \"changelog\": {\n    \"repo\": \"wangeditor-team/wangEditor\",\n    \"labels\": {\n      \"tag: new feature\": \":rocket: New Feature\",\n      \"tag: breaking change\": \":boom: Breaking Change\",\n      \"tag: bug fix\": \":bug: Bug Fix\",\n      \"tag: enhancement\": \":nail_care: Enhancement\",\n      \"tag: documentation\": \":memo: Documentation\",\n      \"tag: internal\": \":house: Internal\"\n    },\n    \"cacheDir\": \".changelog\"\n  },\n  \"changelogPreset\": \"angular\"\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@wangeditor-team/wangeditor\",\n  \"private\": true,\n  \"scripts\": {\n    \"test\": \"cross-env NODE_OPTIONS=--unhandled-rejections=warn jest --detectOpenHandles --passWithNoTests\",\n    \"test-c\": \"cross-env NODE_OPTIONS=--unhandled-rejections=warn jest --coverage\",\n    \"dev\": \"sh build/build-all.sh dev\",\n    \"build\": \"sh build/build-all.sh\",\n    \"bootstrap\": \"lerna bootstrap --use-workspaces\",\n    \"release:version\": \"git pull origin master && lerna version --conventional-commits && node ./scripts/release-tag.js\",\n    \"release:publish:experimental\": \"lerna publish --dist-tag experimental\",\n    \"release:publish:canary\": \"lerna publish --canary\",\n    \"release:next\": \"yarn prerelease && lerna publish --dist-tag next\",\n    \"release:publish\": \"lerna publish from-git --yes\",\n    \"release:package\": \"lerna publish from-package --yes\",\n    \"prerelease\": \"yarn build\",\n    \"format\": \"yarn prettier --write\",\n    \"lint\": \"eslint \\\"packages/*/+(src|__tests__)/**/*.+(ts|tsx)\\\"\",\n    \"prettier\": \"prettier --ignore-path .gitignore \\\"packages/*/+(src|__tests__)/**/*.+(ts|tsx)\\\"\",\n    \"cypress:open\": \"cypress open\",\n    \"cypress:run\": \"cypress run\",\n    \"e2e:dev\": \"concurrently \\\"yarn example\\\" \\\"yarn run cypress:open\\\"\",\n    \"e2e\": \"concurrently \\\"yarn example\\\" \\\"yarn run cypress:run\\\"\",\n    \"example\": \"lerna exec --scope @wangeditor/editor -- yarn run example\"\n  },\n  \"workspaces\": [\n    \"packages/*\"\n  ],\n  \"lint-staged\": {\n    \"packages/**/*.{ts,tsx}\": [\n      \"yarn lint\",\n      \"yarn format\",\n      \"git add .\"\n    ]\n  },\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"lint-staged\",\n      \"commit-msg\": \"commitlint -E HUSKY_GIT_PARAMS\"\n    }\n  },\n  \"config\": {\n    \"commitizen\": {\n      \"path\": \"node_modules/cz-customizable\"\n    }\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.14.6\",\n    \"@babel/plugin-proposal-class-properties\": \"^7.10.4\",\n    \"@babel/plugin-proposal-object-rest-spread\": \"^7.11.0\",\n    \"@babel/plugin-transform-runtime\": \"^7.14.5\",\n    \"@babel/preset-env\": \"^7.14.5\",\n    \"@babel/preset-typescript\": \"^7.14.5\",\n    \"@babel/runtime-corejs3\": \"^7.14.7\",\n    \"@rollup/plugin-babel\": \"^5.3.0\",\n    \"@rollup/plugin-commonjs\": \"^17.1.0\",\n    \"@rollup/plugin-json\": \"^4.1.0\",\n    \"@rollup/plugin-node-resolve\": \"^11.2.0\",\n    \"@rollup/plugin-replace\": \"^2.4.2\",\n    \"@testing-library/jest-dom\": \"^5.14.1\",\n    \"@types/jest\": \"^25.2.1\",\n    \"@typescript-eslint/eslint-plugin\": \"^2.31.0\",\n    \"@typescript-eslint/parser\": \"^4.4.1\",\n    \"autoprefixer\": \"^10.2.5\",\n    \"babel-core\": \"^7.0.0-bridge.0\",\n    \"babel-jest\": \"^27.0.6\",\n    \"babel-plugin-istanbul\": \"^6.0.0\",\n    \"commitlint\": \"^11.0.0\",\n    \"commitlint-config-cz\": \"^0.13.2\",\n    \"concurrently\": \"^6.2.0\",\n    \"conventional-changelog\": \"^3.1.24\",\n    \"cross-env\": \"^7.0.2\",\n    \"cssnano\": \"^5.0.3\",\n    \"cypress\": \"^8.6.0\",\n    \"cz-customizable\": \"^6.3.0\",\n    \"eslint\": \"^7.21.0\",\n    \"eslint-config-prettier\": \"^6.11.0\",\n    \"eslint-plugin-prettier\": \"^3.1.3\",\n    \"http-server\": \"^0.12.3\",\n    \"husky\": \"^4.2.5\",\n    \"jest\": \"^27.0.6\",\n    \"lerna\": \"^3.20.2\",\n    \"lerna-changelog\": \"^1.0.1\",\n    \"less\": \"^3.11.1\",\n    \"lint-staged\": \"^10.2.2\",\n    \"lodash\": \"^4.17.21\",\n    \"nock\": \"^13.2.4\",\n    \"nodemon\": \"^2.0.6\",\n    \"postcss\": \"^8.2.15\",\n    \"prettier\": \"^2.0.5\",\n    \"release-it\": \"^14.2.0\",\n    \"rollup\": \"^2.41.0\",\n    \"rollup-plugin-cleanup\": \"^3.2.1\",\n    \"rollup-plugin-copy\": \"^3.4.0\",\n    \"rollup-plugin-delete\": \"^2.0.0\",\n    \"rollup-plugin-generate-html-template\": \"^1.7.0\",\n    \"rollup-plugin-peer-deps-external\": \"^2.2.4\",\n    \"rollup-plugin-postcss\": \"^4.0.0\",\n    \"rollup-plugin-serve\": \"^1.1.0\",\n    \"rollup-plugin-terser\": \"^7.0.2\",\n    \"rollup-plugin-typescript2\": \"^0.30.0\",\n    \"rollup-plugin-visualizer\": \"^5.5.0\",\n    \"ts-jest\": \"^27.0.4\",\n    \"tslib\": \"^2.3.0\",\n    \"typescript\": \"4.3.2\"\n  },\n  \"dependencies\": {\n    \"@babel/runtime\": \"^7.14.6\"\n  }\n}\n"
  },
  {
    "path": "packages/basic-modules/CHANGELOG.md",
    "content": "# Change Log\n\nAll notable changes to this project will be documented in this file.\nSee [Conventional Commits](https://conventionalcommits.org) for commit guidelines.\n\n## [1.1.7](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/basic-modules@1.1.6...@wangeditor/basic-modules@1.1.7) (2022-11-14)\n\n\n### Bug Fixes\n\n* **font family menu:** 处理 setHtml 的时候字体样式回显失败的问题 ([b941bab](https://github.com/wangeditor-team/wangEditor/commit/b941babbdc6bd5bf7da0cce826803a8fde011e07))\n\n\n\n\n\n## [1.1.6](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/basic-modules@1.1.5...@wangeditor/basic-modules@1.1.6) (2022-09-27)\n\n**Note:** Version bump only for package @wangeditor/basic-modules\n\n\n\n\n\n## [1.1.5](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/basic-modules@1.1.4...@wangeditor/basic-modules@1.1.5) (2022-09-15)\n\n\n### Bug Fixes\n\n* 图片 100% 有横向滚动条 ([d21322a](https://github.com/wangeditor-team/wangEditor/commit/d21322a1a9f2e3172a1bd5e175f5ebbb5f2ed074))\n* 插入表格会删掉去掉 issue 4711 ([d4fac4e](https://github.com/wangeditor-team/wangEditor/commit/d4fac4efd06480457a95c2b06e7472cf6204de58))\n\n\n\n\n\n## [1.1.4](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/basic-modules@1.1.3...@wangeditor/basic-modules@1.1.4) (2022-09-14)\n\n\n### Bug Fixes\n\n* font-size - 支持配置 name value ([206ebd9](https://github.com/wangeditor-team/wangEditor/commit/206ebd994d2635704d93ef9ebe0022d7d72ddea8))\n\n\n\n\n\n## [1.1.3](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/basic-modules@1.1.2...@wangeditor/basic-modules@1.1.3) (2022-07-13)\n\n\n### Bug Fixes\n\n* parse indent style 负数 ([c8d746e](https://github.com/wangeditor-team/wangEditor/commit/c8d746e0464bdda626313c17af4d015681ccc3e8))\n* 兼容 word 文字背景色 ([e820b26](https://github.com/wangeditor-team/wangEditor/commit/e820b26730d34480994a343ab262c043c30a4495))\n\n\n\n\n\n## [1.1.2](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/basic-modules@1.1.1...@wangeditor/basic-modules@1.1.2) (2022-07-11)\n\n\n### Bug Fixes\n\n* editor.focus() 参数语法错误 ([334fa21](https://github.com/wangeditor-team/wangEditor/commit/334fa217d43fdaa95454e7c85a53526b7b777fda))\n* 修复html回显时，部分字体回显问题 ([c83ffa7](https://github.com/wangeditor-team/wangEditor/commit/c83ffa70da655d03bfd639f2d1fd04986440ead2))\n\n\n\n\n\n## [1.1.1](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/basic-modules@1.1.0...@wangeditor/basic-modules@1.1.1) (2022-06-02)\n\n\n### Bug Fixes\n\n* issue 4308 - 自定义字号、字体无法回显 ([ad38b8c](https://github.com/wangeditor-team/wangEditor/commit/ad38b8ce6dbcff1d65785c8d6701238ad351f562))\n* 修复在空字符上插入 link 报错的问题 ([e838f06](https://github.com/wangeditor-team/wangEditor/commit/e838f069f556a5d3206e48a5ed76f8d1e0ae3d05))\n\n\n\n\n\n# [1.1.0](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/basic-modules@1.0.1...@wangeditor/basic-modules@1.1.0) (2022-05-25)\n\n\n### Bug Fixes\n\n* 粘贴 HTML 后 font-size font-family line-height 不显示 ([2281957](https://github.com/wangeditor-team/wangEditor/commit/2281957020a30de9cda1c5e9d5e20c6668b7f592))\n\n\n### Features\n\n* enter menu ([988fc31](https://github.com/wangeditor-team/wangEditor/commit/988fc31f31de3d37dffbf54abb784cceb8e6118d))\n\n\n\n\n\n## 1.0.1 (2022-04-18)\n\n\n### Bug Fixes\n\n* 部分菜单 disabled ([87f1233](https://github.com/wangeditor-team/wangEditor/commit/87f12332a087072406c1988dc5cef2eae8335375))\n* 插入图片的 < > 替换 ([5721560](https://github.com/wangeditor-team/wangEditor/commit/57215609ada8b9d15f5505d1ba52e49707b5b183))\n* 错别字 alwaysEnable ([82c5136](https://github.com/wangeditor-team/wangEditor/commit/82c5136f8496be420dfa26b0f30522e19924a907))\n* 分割线后无法输入内容 ([146fd05](https://github.com/wangeditor-team/wangEditor/commit/146fd05108592d50d036d0f37a2e29fcdd2a97be))\n* 更新各包之间依赖版本 ([75c552c](https://github.com/wangeditor-team/wangEditor/commit/75c552cc8ed54765bebb86a7ec5329a7fc79e85f))\n* 更新了 basic-module 依赖版本 ([20c9543](https://github.com/wangeditor-team/wangEditor/commit/20c9543dc9249af6fc7e3a9895ed7f64709ca6ee))\n* 禁用时 image 的样式 ([42c993a](https://github.com/wangeditor-team/wangEditor/commit/42c993a7668d90ce049b88a01df21b28912c679f))\n* 全选设置字体报错 ([cdb14d1](https://github.com/wangeditor-team/wangEditor/commit/cdb14d10330b5736534e7aaf3a070df2804a8be2))\n* 上下标文案显示 ([0e97da1](https://github.com/wangeditor-team/wangEditor/commit/0e97da18279cee6ea06c217972fee4faf9e4758f))\n* 添加链接,空格也会在链接中的问题 ([c656827](https://github.com/wangeditor-team/wangEditor/commit/c65682743bd49eba9ab64be847f1f9527fb6170b))\n* 修复 pnpm 安装 @wangeditor/editor 出现警告的问题 ([4087fbe](https://github.com/wangeditor-team/wangEditor/commit/4087fbee01c76bdd55e747a5e86c5e4a8d6a8353))\n* 修复多选文字且选择空白行无法修改文字样式 ([99a9150](https://github.com/wangeditor-team/wangEditor/commit/99a91509c6e12220bb105cc6d15a0f0a4b375cea))\n* 修复光标状态下设置文字样式,菜单不 active 的问题 ([b1b2dba](https://github.com/wangeditor-team/wangEditor/commit/b1b2dbaaae11f74bd36ec79ff50de336c252fef5))\n* 修复清除格式不完全的问题 ([1181a23](https://github.com/wangeditor-team/wangEditor/commit/1181a23e6de71162dc490d9b348379c9b2ef4251))\n* 修复设置文字颜色与背景色行为与预期不一致的问题 ([25d3381](https://github.com/wangeditor-team/wangEditor/commit/25d3381aa65ce8fe862617e7b1b03cfa5370715d))\n* 选中文字，创建链接（同时修改文字） ([5fdf6ae](https://github.com/wangeditor-team/wangEditor/commit/5fdf6ae33b1bebe9b7373e4b7ee8c568480a3c08))\n* 移除了每个包下的 publishConfig directory 配置 ([16559f0](https://github.com/wangeditor-team/wangEditor/commit/16559f052545c111318be760e64291a521bdcc65))\n* 优化 custom-types.d.ts 中类型声明，修复测试文件 ts 报错 ([3a6c455](https://github.com/wangeditor-team/wangEditor/commit/3a6c4553245bc734dae1e17d605af389971782a2))\n* 有内联元素时对齐失败 ([076c694](https://github.com/wangeditor-team/wangEditor/commit/076c694a4b3474080b89f52692595b84bf4d8207))\n* blockquote & header insertBreak ([06678c9](https://github.com/wangeditor-team/wangEditor/commit/06678c963e8c8421ecded448de7510b254117550))\n* code node 顶层 ([a927938](https://github.com/wangeditor-team/wangEditor/commit/a9279388f14212319505f6a5da300cd15e81c214))\n* code-block 换行 - 自动加入前面的空格 ([c214032](https://github.com/wangeditor-team/wangEditor/commit/c2140327842d803cd18a9acf47ec3225182bf940))\n* color toHtml ([2c9718c](https://github.com/wangeditor-team/wangEditor/commit/2c9718cb2feb4dd0a7bf39238598707fa6d2bb21))\n* delete divider ([f04cbd6](https://github.com/wangeditor-team/wangEditor/commit/f04cbd6009099629e3cd41be19d20b6788fe7f28))\n* disabled - img 和 todo 可编辑 ([cf6a3f2](https://github.com/wangeditor-team/wangEditor/commit/cf6a3f2a1e05b6231f46aa74c422561e4147f7ae))\n* divider - 键盘删除 ([31db059](https://github.com/wangeditor-team/wangEditor/commit/31db0593dbc77fba9b4a719bc0f48f1223afd680))\n* emptyP toHtml 增加 br ([c347c29](https://github.com/wangeditor-team/wangEditor/commit/c347c2916416edc96a99d1bf53c0e18cd22d80f9))\n* getHtml API ([c0b60cf](https://github.com/wangeditor-team/wangEditor/commit/c0b60cf47d8eaae4292265906fbe07875e1564c9))\n* header 不禁用 bold ([f4cd3d0](https://github.com/wangeditor-team/wangEditor/commit/f4cd3d0b85725701c3ec650e4d6ae7d8831f5105))\n* hoverbar 被点击多次隐藏 ([bf4fc19](https://github.com/wangeditor-team/wangEditor/commit/bf4fc193847e8caba3a67c8dd152eae4f1950c4f))\n* hoverbar modal 重复创建 ([70d2b61](https://github.com/wangeditor-team/wangEditor/commit/70d2b618a0662c88cd5e6691f513009726ce1b9b))\n* hoverbar show/hide ([c96bc83](https://github.com/wangeditor-team/wangEditor/commit/c96bc8378939fecd78807fea4f2b7e1eec2a9ea0))\n* image 拖拽，设置最小值 ([0205023](https://github.com/wangeditor-team/wangEditor/commit/0205023d8c1ec3fafcba3a950afcaef9f5f5170f))\n* insert link ([a104682](https://github.com/wangeditor-team/wangEditor/commit/a10468279f730c9a4216474cf3d44d41f124cb6b))\n* insertHtml - 空行 ([53a8fbb](https://github.com/wangeditor-team/wangEditor/commit/53a8fbb5cf665ef0d6f7fd1c2fee73dba0d98e32))\n* insertHtml - 空行 ([c61f415](https://github.com/wangeditor-team/wangEditor/commit/c61f415c41d393f203ae5e5c17d9167ec60a1824))\n* justify - disable ([3a4b24e](https://github.com/wangeditor-team/wangEditor/commit/3a4b24e8e628024de248f0b52bb4066f626e7480))\n* justify indent ([5a81e52](https://github.com/wangeditor-team/wangEditor/commit/5a81e527a45e7a92eb36a2aefa50d93e20c4cec2))\n* justify menu disabled ([19e2f80](https://github.com/wangeditor-team/wangEditor/commit/19e2f8008a435101c6ecd4d4a7eadd423cb1070f))\n* link 无文本 ([af4fb32](https://github.com/wangeditor-team/wangEditor/commit/af4fb3218bd4651763f66c804fec2b872e99e8f3))\n* link, text hoverbar 选区问题 ([e0b7438](https://github.com/wangeditor-team/wangEditor/commit/e0b7438c89a347f1b0b940d9c11150b72d595529))\n* lodash.throttle 引用 bug ([50aeff9](https://github.com/wangeditor-team/wangEditor/commit/50aeff94859bf328346cb9cfe89d0abd57c3b641))\n* maxLength - 拼音 + 粘贴 ([3ac4db6](https://github.com/wangeditor-team/wangEditor/commit/3ac4db6d78cbe7a8d1fe19747deb0a17edd9b552))\n* menu active ([10829e2](https://github.com/wangeditor-team/wangEditor/commit/10829e2e9e1d864d4900821ee3d5fa516b8cca2a))\n* parse html - 有些 elem children 需要过滤 ([63cbb80](https://github.com/wangeditor-team/wangEditor/commit/63cbb804c8c7a778a4ee1f4ba8717a11b4b6b5a3))\n* parse-html - sub sup ([2c15a5f](https://github.com/wangeditor-team/wangEditor/commit/2c15a5f9c9c2de8b34770a6bebfe765d203a03f6))\n* parse-html pre/code ([d9bd773](https://github.com/wangeditor-team/wangEditor/commit/d9bd773f9a40f9531d9163700253d0b5f717afb8))\n* rename es module filename ([1821d4e](https://github.com/wangeditor-team/wangEditor/commit/1821d4eef49e64efcb41b848849ca7a5e6472044))\n* shadow dom 中 modal 输入框异常 ([ef3b199](https://github.com/wangeditor-team/wangEditor/commit/ef3b199a3e74c6b8ba61ed781e1aa13a1c5acfde))\n* style-to-html - 输入 a 会删除外部的 <a> 标签 ([af1f523](https://github.com/wangeditor-team/wangEditor/commit/af1f523983f2bc4b7eaf9726d4b8a35227ab27dc))\n* table - elemToHtml ([e36e609](https://github.com/wangeditor-team/wangEditor/commit/e36e6092ef721723169afc8bf0560a47ac9f4dfc))\n* tableCell 中 br 报错 ([8604db7](https://github.com/wangeditor-team/wangEditor/commit/8604db751b622c01fa5391af59328236cf13effc))\n\n\n### Features\n\n* 两端对齐 ([e5080d3](https://github.com/wangeditor-team/wangEditor/commit/e5080d3dd102f7a951d8e1f370db834778ecbdfa))\n* 上标 下标 ([40dab08](https://github.com/wangeditor-team/wangEditor/commit/40dab085a061ea3e838f0cfa86260c6c6f894c69))\n* 增加 enable disable API（删除 setConfig setMenuConfig API） ([984fc50](https://github.com/wangeditor-team/wangEditor/commit/984fc50520061fc34ea08f4136bdeb93dee46564))\n* 增加 header4 header5 ([cc48734](https://github.com/wangeditor-team/wangEditor/commit/cc4873412ce3f4de1ecc1dbf4c313094dceb5a77))\n* 支持 nodejs 环境 ([484f18c](https://github.com/wangeditor-team/wangEditor/commit/484f18c3abc70d19e51c556f48491c18d390b1e1))\n* basic text paste ([f0a5b98](https://github.com/wangeditor-team/wangEditor/commit/f0a5b980c95fa1e2fc59a898c6e0d0723c276c28))\n* clearStyle menu ([8002f70](https://github.com/wangeditor-team/wangEditor/commit/8002f707ed04b914180ec36fdca0edf48c815e01))\n* drag resize image ([cd72028](https://github.com/wangeditor-team/wangEditor/commit/cd72028f1786e2e53079ad5cbef1b8569731ca79))\n* editor 生命周期，自定义事件 ([00e9bc2](https://github.com/wangeditor-team/wangEditor/commit/00e9bc2cfcb8b622764db1c76394491d72ffd93e))\n* focus支持focus到文档末尾 ([628830e](https://github.com/wangeditor-team/wangEditor/commit/628830ef06ff85b3e67001ce30dd9e0557b0aa28))\n* fullScreen ([e7ccd88](https://github.com/wangeditor-team/wangEditor/commit/e7ccd88a7dd58f64b7bd484de428e3a76cc994f7))\n* getHeaders & editor.srcollToElem ([2bfb813](https://github.com/wangeditor-team/wangEditor/commit/2bfb813e4957f080c6676ec38f8f051275cdf44a))\n* groupButton disabled ([8ffd44c](https://github.com/wangeditor-team/wangEditor/commit/8ffd44c9a44758e951ca7bd02dd46746fcac1c03))\n* header button menu ([6413135](https://github.com/wangeditor-team/wangEditor/commit/64131354d54705e11fd6992fcf5a4389371c3560))\n* i18n ([c11b244](https://github.com/wangeditor-team/wangEditor/commit/c11b2440f91b99d40bca18b675c66a22b6e160c9))\n* image menu - width 50% 100% ([f9b4c68](https://github.com/wangeditor-team/wangEditor/commit/f9b4c68dff3232b50491b07949c20eb4c18baa6b))\n* image menu config ([bb18774](https://github.com/wangeditor-team/wangEditor/commit/bb187740e9703b4a76cde4f5e4d32ac714aa793a))\n* insertHtml - text style ([6f303c5](https://github.com/wangeditor-team/wangEditor/commit/6f303c5be81dc8763a28bc982928e5bc9f2936d9))\n* link menu config ([fe6b6db](https://github.com/wangeditor-team/wangEditor/commit/fe6b6db62086a5014c25ea96aa9308c2028a5b60))\n* parse html ([2a5eace](https://github.com/wangeditor-team/wangEditor/commit/2a5eace00f33cded50b68e8164748ec2480213fd))\n* parse src (link image video) ([715a841](https://github.com/wangeditor-team/wangEditor/commit/715a841fc6c730ee2b448a1799a07ce778128aad))\n* placeholder ([a3e4cdc](https://github.com/wangeditor-team/wangEditor/commit/a3e4cdcd474063e4f436327aaf4074bb2126d941))\n* react 组件 ([448fc83](https://github.com/wangeditor-team/wangEditor/commit/448fc838d64dbef52cbcddde0e98eb021d8a9122))\n* todo ([9608fef](https://github.com/wangeditor-team/wangEditor/commit/9608fef2ff86368cdcbb950a74af1246a58709de))\n* toHtml 机制 ([1c4d872](https://github.com/wangeditor-team/wangEditor/commit/1c4d8729f84aaab6a448f23064b34a20596305e9))\n* upload video ([ac8e6f8](https://github.com/wangeditor-team/wangEditor/commit/ac8e6f8b5258e593714676a6f6be359ba525833c))\n* video menu config ([7fa3783](https://github.com/wangeditor-team/wangEditor/commit/7fa3783c42aa83f7d53c8be34be3c8b7c8a64754))\n\n\n### Performance Improvements\n\n* 优化较大的svg图片 ([2c360e7](https://github.com/wangeditor-team/wangEditor/commit/2c360e7628eb655e8df67cc7b764f4981b283a2f))\n"
  },
  {
    "path": "packages/basic-modules/README.md",
    "content": "# wangEditor basic-modules\n\nBasic modules built in [wangEditor](https://www.wangeditor.com/) by default.\n"
  },
  {
    "path": "packages/basic-modules/__tests__/blockquote/blockquote-menu.test.ts",
    "content": "/**\n * @description blockquote menu test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport BlockquoteMenu from '../../src/modules/blockquote/menu/BlockquoteMenu'\n\ndescribe('blockquote menu', () => {\n  let editor: any\n  let startLocation: any\n  const menu = new BlockquoteMenu()\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  // getValue 无逻辑，不用测试\n\n  it('is disabled', () => {\n    editor.select(startLocation)\n    expect(menu.isDisabled(editor)).toBeFalsy()\n\n    Transforms.setNodes(editor, { type: 'blockquote' })\n    expect(menu.isDisabled(editor)).toBeFalsy()\n\n    Transforms.setNodes(editor, { type: 'header1' })\n    expect(menu.isDisabled(editor)).toBeTruthy() // 非 p blockquote ，则禁用\n  })\n\n  it('exec and isActive', () => {\n    editor.select(startLocation)\n\n    menu.exec(editor, '') // 转换为 blockquote\n    const blockquotes1 = editor.getElemsByTypePrefix('blockquote')\n    expect(blockquotes1.length).toBe(1)\n    expect(menu.isActive(editor)).toBeTruthy()\n\n    menu.exec(editor, '') // 取消 blockquote\n    const blockquotes2 = editor.getElemsByTypePrefix('blockquote')\n    expect(blockquotes2.length).toBe(0)\n    expect(menu.isActive(editor)).toBeFalsy()\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/blockquote/elem-to-html.test.ts",
    "content": "/**\n * @description blockquote - elem to html test\n * @author wangfupeng\n */\n\nimport { quoteToHtmlConf } from '../../src/modules/blockquote/elem-to-html'\n\ndescribe('blockquote elem to html', () => {\n  it('blockquote to html', () => {\n    expect(quoteToHtmlConf.type).toBe('blockquote')\n\n    const elem = { type: 'blockquote', children: [] }\n    const html = quoteToHtmlConf.elemToHtml(elem, 'hello')\n    expect(html).toBe('<blockquote>hello</blockquote>')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/blockquote/parse-html.test.ts",
    "content": "/**\n * @description parse html test\n * @author wangfupeng\n */\n\nimport { $ } from 'dom7'\nimport { BaseElement } from 'slate'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { parseHtmlConf } from '../../src/modules/blockquote/parse-elem-html'\n\ndescribe('blockquote - parse html', () => {\n  const editor = createEditor()\n\n  it('without children', () => {\n    const $elem = $('<blockquote>hello&nbsp;world</blockquote>')\n\n    // match selector\n    expect($elem[0].matches(parseHtmlConf.selector)).toBeTruthy()\n\n    // parse\n    const res = parseHtmlConf.parseElemHtml($elem[0], [], editor)\n    expect(res).toEqual({\n      type: 'blockquote',\n      children: [{ text: 'hello world' }],\n    })\n  })\n\n  it('with children', () => {\n    const $elem = $('<blockquote></blockquote>')\n    const children = [{ text: 'hello ' }, { text: 'world', bold: true }]\n\n    // parse\n    const res = parseHtmlConf.parseElemHtml($elem[0], children, editor)\n    expect(res).toEqual({\n      type: 'blockquote',\n      children: [{ text: 'hello ' }, { text: 'world', bold: true }],\n    })\n  })\n\n  it('with inline children', () => {\n    const $elem = $('<blockquote></blockquote>')\n    const children: any[] = [\n      { text: 'hello ' },\n      { type: 'link', url: 'http://wangeditor.com' },\n      { type: 'paragraph', children: [] },\n    ]\n\n    const isInline = editor.isInline\n    editor.isInline = (element: any) => {\n      if (element.type === 'link') return true\n      return isInline(element)\n    }\n\n    // parse\n    const res = parseHtmlConf.parseElemHtml($elem[0], children, editor)\n    expect(res).toEqual({\n      type: 'blockquote',\n      children: [{ text: 'hello ' }, { type: 'link', url: 'http://wangeditor.com' }],\n    })\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/blockquote/plugin.test.ts",
    "content": "/**\n * @description blockquote plugin test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport withBlockquote from '../../src/modules/blockquote/plugin'\n\ndescribe('blockquote plugin', () => {\n  let editor = withBlockquote(createEditor())\n  let startLocation = Editor.start(editor, [])\n\n  beforeEach(() => {\n    editor = withBlockquote(createEditor())\n    startLocation = Editor.start(editor, [])\n  })\n\n  it('insert break', () => {\n    expect(1).toBeTruthy()\n\n    // TODO 该测试一直报错（找不到 blockquote path），待定处理\n    // editor.select(startLocation)\n\n    // // @ts-ignore\n    // Transforms.setNodes(editor, { type: 'blockquote' }) // 设置 blockquote\n\n    // const pList1 = editor.getElemsByType('paragraph')\n    // expect(pList1.length).toBe(0)\n\n    // editor.insertText('hello')\n    // console.log(11, JSON.stringify(editor.children))\n    // console.log(22, JSON.stringify(editor.selection))\n    // editor.insertBreak() // 第一次换行，内部插入 \\n\n\n    // const pList2 = editor.getElemsByType('paragraph')\n    // expect(pList2.length).toBe(0)\n\n    // editor.insertBreak() // 再一次换行，生成 p\n    // const pList3 = editor.getElemsByType('paragraph')\n    // expect(pList3.length).toBe(1)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/blockquote/render-elem.test.ts",
    "content": "/**\n * @description blockquote render elem test\n * @author wangfupeng\n */\n\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { renderBlockQuoteConf } from '../../src/modules/blockquote/render-elem'\n\ndescribe('blockquote - render elem', () => {\n  const editor = createEditor()\n\n  it('render blockquote elem', () => {\n    expect(renderBlockQuoteConf.type).toBe('blockquote')\n\n    const elem = { type: 'blockquote', children: [] }\n    const vnode = renderBlockQuoteConf.renderElem(elem, null, editor)\n    expect(vnode.sel).toBe('blockquote')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/code-block/code-block-menu.test.ts",
    "content": "/**\n * @description code-block menu test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms, Element } from 'slate'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport CodeBlockMenu from '../../src/modules/code-block/menu/CodeBlockMenu'\n\ndescribe('code-block menu', () => {\n  const menu = new CodeBlockMenu()\n  let editor: any\n  let startLocation: any\n\n  const codeElem = {\n    type: 'code',\n    language: 'javascript',\n    children: [{ text: 'var' }],\n  }\n  const preElem = {\n    type: 'pre',\n    children: [codeElem],\n  }\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  it('getValue and isActive', done => {\n    editor.select(startLocation)\n    expect(menu.isActive(editor)).toBeFalsy()\n    expect(menu.getValue(editor)).toBe('')\n\n    editor.insertNode(preElem) // 插入 code node\n    editor.select({\n      path: [1, 0, 0], // 选中 code node\n      offset: 3,\n    })\n    setTimeout(() => {\n      expect(menu.isActive(editor)).toBeTruthy()\n      expect(menu.getValue(editor)).toBe('javascript')\n      done()\n    })\n  })\n\n  it('is disabled', () => {\n    editor.select(startLocation)\n    expect(menu.isDisabled(editor)).toBeFalsy()\n\n    Transforms.setNodes(editor, { type: 'header1' } as Partial<Element>)\n    expect(menu.isDisabled(editor)).toBeTruthy() // 非 p pre ，则禁用\n\n    editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] })\n    expect(menu.isDisabled(editor)).toBeFalsy()\n    // Transforms.removeNodes(editor, { mode: 'highest' }) // 移除 pre/code\n  })\n\n  it('exec - to code-block', () => {\n    editor.select(startLocation)\n\n    menu.exec(editor, 'javascript') // 生成 code-block\n    const preList = editor.getElemsByTypePrefix('pre')\n    expect(preList.length).toBe(1)\n    const codeLis = editor.getElemsByTypePrefix('code')\n    expect(codeLis.length).toBe(1)\n  })\n\n  it('exec - to paragraph', () => {\n    editor.select(startLocation)\n    editor.insertNode(preElem) // 插入 code node\n    editor.select({\n      path: [1, 0, 0], // 选中 code node\n      offset: 3,\n    })\n\n    menu.exec(editor, '') // 取消 code-block\n    const preList = editor.getElemsByTypePrefix('pre')\n    expect(preList.length).toBe(0)\n    const codeLis = editor.getElemsByTypePrefix('code')\n    expect(codeLis.length).toBe(0)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/code-block/elem-to-html.test.ts",
    "content": "/**\n * @description code-block elem to html test\n * @author wangfupeng\n */\n\nimport { codeToHtmlConf, preToHtmlConf } from '../../src/modules/code-block/elem-to-html'\n\ndescribe('code-block - elem to html', () => {\n  it('code to html', () => {\n    expect(codeToHtmlConf.type).toBe('code')\n    const elem = { type: 'code', children: [] }\n    const html = codeToHtmlConf.elemToHtml(elem, 'hello')\n    expect(html).toBe('<code>hello</code>')\n  })\n\n  it('pre to html', () => {\n    expect(preToHtmlConf.type).toBe('pre')\n    const elem = { type: 'pre', children: [] }\n    const html = preToHtmlConf.elemToHtml(elem, 'hello')\n    expect(html).toBe('<pre>hello</pre>')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/code-block/parse-html.test.ts",
    "content": "/**\n * @description parse elem html\n * @author wangfupeng\n */\n\nimport { $ } from 'dom7'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { parseCodeHtmlConf, parsePreHtmlConf } from '../../src/modules/code-block/parse-elem-html'\nimport { preParseHtmlConf } from '../../src/modules/code-block/pre-parse-html'\n\ndescribe('code block - pre parse html', () => {\n  it('pre parse html', () => {\n    const $pre = $('<pre></pre>')\n    const $code = $('<code><xmp>var a = 100;</xmp></code>')\n    $pre.append($code)\n\n    // match selector\n    expect($code[0].matches(preParseHtmlConf.selector)).toBeTruthy()\n\n    // pre parse\n    const res = preParseHtmlConf.preParseHtml($code[0])\n    expect(res.innerHTML).toBe('var a = 100;')\n  })\n})\n\ndescribe('code block - parse html', () => {\n  const editor = createEditor()\n\n  it('parse code html', () => {\n    const $pre = $('<pre></pre>')\n    const $code = $('<code><xmp>var a = 100;</xmp></code>')\n    $pre.append($code)\n\n    // match selector\n    expect($code[0].matches(parseCodeHtmlConf.selector)).toBeTruthy()\n\n    // parse\n    const res = parseCodeHtmlConf.parseElemHtml($code[0], [], editor)\n    expect(res).toEqual({\n      type: 'code',\n      language: '',\n      children: [{ text: 'var a = 100;' }],\n    })\n  })\n  it('parse pre html', () => {\n    const $pre = $('<pre></pre>')\n    const children = [\n      {\n        type: 'code',\n        language: '',\n        children: [{ text: 'var a = 100;' }],\n      },\n    ]\n\n    // match selector\n    expect($pre[0].matches(parsePreHtmlConf.selector)).toBeTruthy()\n\n    // parse\n    const res = parsePreHtmlConf.parseElemHtml($pre[0], children, editor)\n    expect(res).toEqual({\n      type: 'pre',\n      children: [\n        {\n          type: 'code',\n          language: '',\n          children: [{ text: 'var a = 100;' }],\n        },\n      ],\n    })\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/code-block/plugin.test.ts",
    "content": "/**\n * @description code-block plugin test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport withCodeBlock from '../../src/modules/code-block/plugin'\n\n// 模拟 DataTransfer\nclass MyDataTransfer {\n  private values: object = {}\n  setData(type: string, value: string) {\n    this.values[type] = value\n  }\n  getData(type: string): string {\n    return this.values[type]\n  }\n}\n\ndescribe('code-block plugin', () => {\n  let editor: any\n  let startLocation: any\n\n  const codeElem = {\n    type: 'code',\n    children: [{ text: 'var' }],\n  }\n  const preElem = {\n    type: 'pre',\n    children: [codeElem],\n  }\n\n  beforeEach(() => {\n    editor = withCodeBlock(createEditor())\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  it('insert break', () => {\n    editor.select(startLocation)\n    editor.insertNode(preElem) // 插入 code-block\n\n    // code-block 前后会自动生成两个 p\n    const pList1 = editor.getElemsByTypePrefix('paragraph')\n    expect(pList1.length).toBe(2)\n\n    editor.select({\n      path: [1, 0, 0], // 选中 code-block\n      offset: 3,\n    })\n\n    // 换行都在 code-block 内部\n    editor.insertBreak()\n    editor.insertBreak()\n    editor.insertBreak()\n    expect(editor.getText()).toBe('\\nvar\\n\\n\\n\\n')\n\n    // 不会再生成新的 p\n    const pList2 = editor.getElemsByTypePrefix('paragraph')\n    expect(pList2.length).toBe(2)\n  })\n\n  it('insert data', () => {\n    editor.select(startLocation)\n    editor.insertNode(preElem) // 插入 code node\n    editor.select({\n      path: [1, 0, 0], // 选中 code node\n      offset: 3,\n    })\n\n    const data = new MyDataTransfer()\n    data.setData('text/plain', ' hello')\n\n    editor.insertData(data)\n    expect(editor.getText()).toBe('\\nvar hello\\n')\n  })\n\n  it('normalizeNode - code node 不能是顶级元素，否则替换为 p', () => {\n    editor.select(startLocation)\n    editor.insertNode(codeElem)\n\n    const pList = editor.getElemsByTypePrefix('paragraph')\n    expect(pList.length).toBe(2)\n  })\n\n  it('normalizeNode - pre node 不能是第一个节点，否则前面插入 p', () => {\n    editor.select(startLocation)\n    editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] })\n\n    const pList = editor.getElemsByTypePrefix('paragraph')\n    expect(pList.length).toBe(2)\n\n    const preList = editor.getElemsByTypePrefix('pre')\n    expect(preList.length).toBe(1)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/code-block/render-elem.test.ts",
    "content": "/**\n * @description code-block render elem test\n * @author wangfupeng\n */\n\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { renderPreConf, renderCodeConf } from '../../src/modules/code-block/render-elem'\n\ndescribe('code-block render elem', () => {\n  const editor = createEditor()\n\n  it('render code elem', () => {\n    expect(renderCodeConf.type).toBe('code')\n\n    const elem = { type: 'code', children: [] }\n    const vnode = renderCodeConf.renderElem(elem, null, editor)\n    expect(vnode.sel).toBe('code')\n  })\n\n  it('render pre elem', () => {\n    expect(renderPreConf.type).toBe('pre')\n\n    const elem = { type: 'pre', children: [] }\n    const vnode = renderPreConf.renderElem(elem, null, editor)\n    expect(vnode.sel).toBe('pre')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/color/color-menus.test.ts",
    "content": "/**\n * @description color menus test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport ColorMenu from '../../src/modules/color/menu/ColorMenu'\nimport BgColorMenu from '../../src/modules/color/menu/BgColorMenu'\n\ndescribe('color menus', () => {\n  let editor: any\n  let startLocation: any\n\n  const menus = [\n    {\n      mark: 'color',\n      menu: new ColorMenu(),\n    },\n    {\n      mark: 'bgColor',\n      menu: new BgColorMenu(),\n    },\n  ]\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  // exec 无代码，不用测试\n\n  it('getValue and isActive', () => {\n    editor.select(startLocation)\n\n    menus.forEach(({ menu }) => {\n      expect(menu.getValue(editor)).toBe('')\n      expect(menu.isActive(editor)).toBeFalsy()\n    })\n\n    editor.insertText('hello') // 插入文字\n    editor.select([]) // 全选\n    menus.forEach(({ mark, menu }) => {\n      editor.addMark(mark, 'rgb(51, 51, 51)') // 添加 color bgColor\n      expect(menu.getValue(editor)).toBe('rgb(51, 51, 51)')\n      expect(menu.isActive(editor)).toBeTruthy()\n    })\n  })\n\n  it('is disabled', () => {\n    editor.select(startLocation)\n    menus.forEach(({ menu }) => {\n      expect(menu.isDisabled(editor)).toBeFalsy()\n    })\n\n    editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] })\n    menus.forEach(({ menu }) => {\n      expect(menu.isDisabled(editor)).toBeTruthy()\n    })\n    // Transforms.removeNodes(editor, { mode: 'highest' }) // 移除 pre/code\n  })\n\n  it('get panel content elem', () => {\n    menus.forEach(({ menu }) => {\n      const elem = menu.getPanelContentElem(editor)\n      expect(elem instanceof HTMLElement).toBeTruthy()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/color/parse-html.test.ts",
    "content": "/**\n * @description parse html test\n * @author wangfupeng\n */\n\nimport { $ } from 'dom7'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { parseStyleHtml } from '../../src/modules/color/parse-style-html'\nimport { preParseHtmlConf } from '../../src/modules/color/pre-parse-html'\n\ndescribe('color - pre parse html', () => {\n  it('pre parse html', () => {\n    const $font = $('<font color=\"rgb(204, 204, 204)\">hello</font>')\n\n    // match selector\n    expect($font[0].matches(preParseHtmlConf.selector)).toBeTruthy()\n\n    // pre parse\n    const res = preParseHtmlConf.preParseHtml($font[0])\n    expect(res.outerHTML).toBe('<font style=\"color: rgb(204, 204, 204);\">hello</font>')\n  })\n})\n\ndescribe('color - parse style html', () => {\n  const editor = createEditor()\n\n  it('parse style html', () => {\n    const $span = $(\n      '<span style=\"color: rgb(235, 144, 58); background-color: rgb(231, 246, 213);\"></span>'\n    )\n    const textNode = { text: 'hello' }\n\n    // parse style\n    const res = parseStyleHtml($span[0], textNode, editor)\n    expect(res).toEqual({\n      text: 'hello',\n      color: 'rgb(235, 144, 58)',\n      bgColor: 'rgb(231, 246, 213)',\n    })\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/color/render-text-style.test.tsx",
    "content": "/**\n * @description color - render text style test\n * @author wangfupeng\n */\n\nimport { jsx } from 'snabbdom'\nimport { renderStyle } from '../../src/modules/color/render-style'\n\ndescribe('color - render text style', () => {\n  it('render color style', () => {\n    const color = 'rgb(51, 51, 51)'\n    const bgColor = 'rgb(204, 204, 204)'\n    const textNode = { text: 'hello', color, bgColor }\n    const vnode = <span>hello</span>\n\n    // @ts-ignore\n    const newVnode = renderStyle(textNode, vnode) as any\n    expect(newVnode.sel).toBe('span')\n    expect(newVnode.data.style.color).toBe(color)\n    expect(newVnode.data.style.backgroundColor).toBe(bgColor)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/color/text-style-to-html.test.ts",
    "content": "/**\n * @description color - text style to html test\n * @author wangfupeng\n */\n\nimport { styleToHtml } from '../../src/modules/color/style-to-html'\n\ndescribe('color - text style to html', () => {\n  it('color to html', () => {\n    const color = 'rgb(51, 51, 51)'\n    const bgColor = 'rgb(204, 204, 204)'\n    const textNode = { text: '', color, bgColor }\n\n    const html = styleToHtml(textNode, '<span>hello</span>')\n    expect(html).toBe(`<span style=\"color: ${color}; background-color: ${bgColor};\">hello</span>`)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/divider/elem-to-html.test.ts",
    "content": "/**\n * @description divider - elem to html test\n * @author wangfupeng\n */\n\nimport { dividerToHtmlConf } from '../../src/modules/divider/elem-to-html'\n\ndescribe('divider - elem to html', () => {\n  it('divider to html', () => {\n    expect(dividerToHtmlConf.type).toBe('divider')\n\n    const elem = { type: 'divider', children: [{ text: '' }] }\n    const html = dividerToHtmlConf.elemToHtml(elem, '')\n    expect(html).toBe('<hr/>')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/divider/insert-divider-menu.test.ts",
    "content": "/**\n * @description insert divider menu test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport InsertDividerMenu from '../../src/modules/divider/menu/InsertDividerMenu'\n\ndescribe('divider plugin', () => {\n  const menu = new InsertDividerMenu()\n  let editor: any\n  let startLocation: any\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  // getValue isActive 无逻辑，不用测试\n\n  it('is disabled', () => {\n    editor.deselect()\n    expect(menu.isDisabled(editor)).toBeTruthy()\n\n    editor.select(startLocation)\n    expect(menu.isDisabled(editor)).toBeFalsy()\n\n    const elem = { type: 'divider', children: [{ text: '' }] }\n    editor.insertNode(elem) // 插入 divider\n    editor.select({\n      path: [1, 0], // 选中 divider\n      offset: 0,\n    })\n    expect(menu.isDisabled(editor)).toBeTruthy()\n  })\n\n  it('exec', () => {\n    editor.select(startLocation)\n    menu.exec(editor, '')\n\n    const dividers = editor.getElemsByTypePrefix('divider')\n    expect(dividers.length).toBe(1)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/divider/parse-html.test.ts",
    "content": "/**\n * @description parse html test\n * @author wangfupeng\n */\n\nimport { $ } from 'dom7'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { parseHtmlConf } from '../../src/modules/divider/parse-elem-html'\n\ndescribe('divider - parse html', () => {\n  const editor = createEditor()\n\n  it('parse html', () => {\n    const $hr = $('<hr>')\n\n    // match selector\n    expect($hr[0].matches(parseHtmlConf.selector)).toBeTruthy()\n\n    // parse\n    const res = parseHtmlConf.parseElemHtml($hr[0], [], editor)\n    expect(res).toEqual({\n      type: 'divider',\n      children: [{ text: '' }], // void node 有一个空白 text\n    })\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/divider/plugin.test.ts",
    "content": "/**\n * @description divider plugin test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport withDivider from '../../src/modules/divider/plugin'\n\ndescribe('divider plugin', () => {\n  let editor: any\n  let startLocation: any\n\n  beforeEach(() => {\n    editor = withDivider(createEditor())\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  it('divider is void node', () => {\n    const elem = { type: 'divider', children: [{ text: '' }] }\n    expect(editor.isVoid(elem)).toBeTruthy()\n  })\n\n  it('normalizeNode - divider 不能是最后一个元素，否则后面追加 p', () => {\n    const elem = { type: 'divider', children: [{ text: '' }] }\n    editor.select(startLocation)\n    editor.insertNode(elem) // 插入 divider\n\n    const length = editor.children.length\n    expect(length).toBe(3) // 3 个顶级节点：p, divider, p\n\n    const divider = editor.children[1] // 第 2 个节点应该是 divider\n    expect(divider.type).toBe('divider')\n    const p = editor.children[2] // 第 3 个节点应该是 p\n    expect(p.type).toBe('paragraph')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/divider/render-elem.test.ts",
    "content": "/**\n * @description divider - render elem test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { renderDividerConf } from '../../src/modules/divider/render-elem'\n\ndescribe('divider - render elem test', () => {\n  const editor = createEditor()\n  const startLocation = Editor.start(editor, [])\n\n  it('render divider elem', () => {\n    expect(renderDividerConf.type).toBe('divider')\n\n    const elem = { type: 'divider', children: [{ text: '' }] }\n    const vnode1 = renderDividerConf.renderElem(elem, null, editor) as any\n    expect(vnode1.sel).toBe('div')\n    expect(vnode1.data.props.className).toBe('w-e-textarea-divider')\n    expect(vnode1.data.dataset.selected).toBe('') // 未选中\n    expect(vnode1.children[0].sel).toBe('hr')\n\n    editor.select(startLocation)\n    editor.insertNode(elem) // 插入 divider\n    editor.select({\n      path: [1, 0], // 选中 divider\n      offset: 0,\n    })\n    const vnode2 = renderDividerConf.renderElem(elem, null, editor) as any\n    expect(vnode2.data.dataset.selected).toBe('true') // 选中\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/emotion/emotion-menu.test.ts",
    "content": "/**\n * @description emotion menu test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport EmotionMenu from '../../src/modules/emotion/menu/EmotionMenu'\n\ndescribe('font family menu', () => {\n  const menu = new EmotionMenu()\n  let editor: any\n  let startLocation: any\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  // exec getValue isActive 无代码逻辑，不用测试\n\n  it('is disabled', () => {\n    editor.select(startLocation)\n    expect(menu.isDisabled(editor)).toBeFalsy()\n\n    editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] })\n    expect(menu.isDisabled(editor)).toBeTruthy()\n    // Transforms.removeNodes(editor, { mode: 'highest' }) // 移除 pre/code\n  })\n\n  it('get panel content elem', () => {\n    const elem = menu.getPanelContentElem(editor)\n    expect(elem instanceof HTMLElement).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/font-size-family/menu/font-family-menu.test.ts",
    "content": "/**\n * @description font family menu test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport createEditor from '../../../../../tests/utils/create-editor'\nimport FontFamilyMenu from '../../../src/modules/font-size-family/menu/FontFamilyMenu'\n\ndescribe('font family menu', () => {\n  const menu = new FontFamilyMenu()\n  let editor: any\n  let startLocation: any\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  it('get options', () => {\n    editor.select(startLocation)\n    const options1 = menu.getOptions(editor)\n    const selectedDefault = options1.some(opt => opt.selected && opt.value === '')\n    expect(selectedDefault).toBeTruthy() // 空白 p ，选中“默认”\n\n    editor.insertText('hello')\n    editor.select([]) // 全选\n    editor.addMark('fontFamily', '黑体') // 设置字体\n    const options2 = menu.getOptions(editor)\n    const selectedHeiti = options2.some(opt => opt.selected && opt.value === '黑体')\n    expect(selectedHeiti).toBeTruthy()\n  })\n\n  // isActive 无代码逻辑，不用测试\n\n  it('is disabled', () => {\n    editor.select(startLocation)\n    expect(menu.isDisabled(editor)).toBeFalsy()\n\n    editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] })\n    expect(menu.isDisabled(editor)).toBeTruthy()\n    // Transforms.removeNodes(editor, { mode: 'highest' }) // 移除 pre/code\n  })\n\n  it('exec and getValue', () => {\n    editor.select(startLocation)\n    expect(menu.getValue(editor)).toBe('')\n\n    editor.insertText('hello')\n    editor.select([]) // 全选\n    menu.exec(editor, '黑体') // 设置字体\n    expect(menu.getValue(editor)).toBe('黑体')\n\n    menu.exec(editor, '') // 取消字体\n    expect(menu.getValue(editor)).toBe('')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/font-size-family/menu/font-size-menu.test.ts",
    "content": "/**\n * @description font size menu test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport createEditor from '../../../../../tests/utils/create-editor'\nimport FontSizeMenu from '../../../src/modules/font-size-family/menu/FontSizeMenu'\n\ndescribe('font family menu', () => {\n  const menu = new FontSizeMenu()\n  let editor: any\n  let startLocation: any\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  it('get options', () => {\n    editor.select(startLocation)\n    const options1 = menu.getOptions(editor)\n    const selectedDefault = options1.some(opt => opt.selected && opt.value === '')\n    expect(selectedDefault).toBeTruthy() // 空白 p ，选中“默认”\n\n    editor.insertText('hello')\n    editor.select([]) // 全选\n    editor.addMark('fontSize', '40px') // 设置字号\n    const options2 = menu.getOptions(editor)\n    const selected = options2.some(opt => opt.selected && opt.value === '40px')\n    expect(selected).toBeTruthy()\n  })\n\n  // isActive 无代码逻辑，不用测试\n\n  it('is disabled', () => {\n    editor.select(startLocation)\n    expect(menu.isDisabled(editor)).toBeFalsy()\n\n    editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] })\n    expect(menu.isDisabled(editor)).toBeTruthy()\n    // Transforms.removeNodes(editor, { mode: 'highest' }) // 移除 pre/code\n  })\n\n  it('exec and getValue', () => {\n    editor.select(startLocation)\n    expect(menu.getValue(editor)).toBe('')\n\n    editor.insertText('hello')\n    editor.select([]) // 全选\n    menu.exec(editor, '40px') // 设置字号\n    expect(menu.getValue(editor)).toBe('40px')\n\n    menu.exec(editor, '') // 取消字号\n    expect(menu.getValue(editor)).toBe('')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/font-size-family/parse-html.test.ts",
    "content": "/**\n * @description parse html test\n * @author wangfupeng\n */\n\nimport { $ } from 'dom7'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { parseStyleHtml } from '../../src/modules/font-size-family/parse-style-html'\nimport { preParseHtmlConf } from '../../src/modules/font-size-family/pre-parse-html'\n\ndescribe('font size family - pre parse html', () => {\n  it('pre parse html', () => {\n    const $font = $('<font size=\"1\" face=\"黑体\">hello</font>')\n\n    // match selector\n    expect($font[0].matches(preParseHtmlConf.selector)).toBeTruthy()\n\n    // pre parse\n    const res = preParseHtmlConf.preParseHtml($font[0])\n    expect(res.outerHTML).toBe('<font style=\"font-size: 12px; font-family: 黑体;\">hello</font>')\n  })\n})\n\ndescribe('font size family - parse style html', () => {\n  const editor = createEditor()\n\n  it('parse style html', () => {\n    const $span = $('<span style=\"font-size: 12px; font-family: 黑体;\"></span>')\n    const textNode = { text: 'hello' }\n\n    // parse style\n    const res = parseStyleHtml($span[0], textNode, editor)\n    expect(res).toEqual({\n      text: 'hello',\n      fontSize: '12px',\n      fontFamily: '黑体',\n    })\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/font-size-family/render-text-style.test.tsx",
    "content": "/**\n * @description font size and family - render text style test\n * @author wangfupeng\n */\n\nimport { jsx } from 'snabbdom'\nimport { renderStyle } from '../../src/modules/font-size-family/render-style'\n\ndescribe('font size and family - render text style', () => {\n  it('render text style', () => {\n    const fontSize = '20px'\n    const fontFamily = '黑体'\n    const textNode = { text: 'hello', fontSize, fontFamily }\n    const vnode = <span>hello</span>\n\n    // @ts-ignore 忽略 vnode 格式检查\n    const newVnode = renderStyle(textNode, vnode) as any\n    expect(newVnode.data.style.fontSize).toBe(fontSize)\n    expect(newVnode.data.style.fontFamily).toBe(fontFamily)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/font-size-family/text-style-to-html.test.ts",
    "content": "/**\n * @description font size and family - text style to html test\n * @author wangfupeng\n */\n\nimport { styleToHtml } from '../../src/modules/font-size-family/style-to-html'\n\ndescribe('font size and family - text style to html', () => {\n  it('text style to html', () => {\n    const fontSize = '20px'\n    const fontFamily = '黑体'\n    const textNode = { text: '', fontSize, fontFamily }\n\n    const html = styleToHtml(textNode, '<span>hello</span>')\n    expect(html).toBe(\n      `<span style=\"font-size: ${fontSize}; font-family: ${fontFamily};\">hello</span>`\n    )\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/full-screen/full-screen-menu.test.ts",
    "content": "/**\n * @description full screen menu test\n * @author wangfupeng\n */\n\nimport createEditor from '../../../../tests/utils/create-editor'\nimport FullScreen from '../../src/modules/full-screen/menu/FullScreen'\n\ndescribe('full screen menu', () => {\n  const editor = createEditor()\n  const menu = new FullScreen()\n\n  it('full screen menu', done => {\n    menu.exec(editor, '') // 设置全屏\n    expect(menu.isActive(editor)).toBeTruthy()\n\n    menu.exec(editor, '') // 取消全屏（有延迟）\n    setTimeout(() => {\n      expect(menu.isActive(editor)).toBeFalsy()\n      done()\n    }, 500)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/header/elem-to-html.test.ts",
    "content": "/**\n * @description header - elem to html test\n * @author wangfupeng\n */\n\nimport {\n  header1ToHtmlConf,\n  header2ToHtmlConf,\n  header3ToHtmlConf,\n  header4ToHtmlConf,\n  header5ToHtmlConf,\n} from '../../src/modules/header/elem-to-html'\n\ndescribe('header - elem to html', () => {\n  const elem = { type: 'header1', children: [{ text: '' }] }\n  it('header1 to html', () => {\n    expect(header1ToHtmlConf.type).toBe('header1')\n    const html = header1ToHtmlConf.elemToHtml(elem, 'hello')\n    expect(html).toBe('<h1>hello</h1>')\n  })\n\n  it('header2 to html', () => {\n    expect(header2ToHtmlConf.type).toBe('header2')\n    const html = header2ToHtmlConf.elemToHtml(elem, 'hello')\n    expect(html).toBe('<h2>hello</h2>')\n  })\n\n  it('header3 to html', () => {\n    expect(header3ToHtmlConf.type).toBe('header3')\n    const html = header3ToHtmlConf.elemToHtml(elem, 'hello')\n    expect(html).toBe('<h3>hello</h3>')\n  })\n\n  it('header4 to html', () => {\n    expect(header4ToHtmlConf.type).toBe('header4')\n    const html = header4ToHtmlConf.elemToHtml(elem, 'hello')\n    expect(html).toBe('<h4>hello</h4>')\n  })\n\n  it('header5 to html', () => {\n    expect(header5ToHtmlConf.type).toBe('header5')\n    const html = header5ToHtmlConf.elemToHtml(elem, 'hello')\n    expect(html).toBe('<h5>hello</h5>')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/header/helper.test.ts",
    "content": "/**\n * @description header helper test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { getHeaderType, isMenuDisabled, setHeaderType } from '../../src/modules/header/helper'\n\ndescribe('header helper', () => {\n  let editor: any\n  let startLocation: any\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  it('get header type', () => {\n    editor.select(startLocation)\n    expect(getHeaderType(editor)).toBe('paragraph')\n\n    Transforms.setNodes(editor, { type: 'header1' })\n    expect(getHeaderType(editor)).toBe('header1')\n  })\n\n  it('is menu disabled', () => {\n    editor.select(startLocation)\n    expect(isMenuDisabled(editor)).toBeFalsy()\n\n    Transforms.setNodes(editor, { type: 'header1' })\n    expect(isMenuDisabled(editor)).toBeFalsy()\n\n    editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] })\n    expect(isMenuDisabled(editor)).toBeTruthy() // 只能用于 p header\n    // Transforms.removeNodes(editor, { mode: 'highest' }) // 移除 pre/code\n  })\n\n  it('set header type', () => {\n    editor.select(startLocation)\n    setHeaderType(editor, 'header1')\n\n    const headers = editor.getElemsByTypePrefix('header1')\n    expect(headers.length).toBe(1)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/header/menu/header-select-menu.test.ts",
    "content": "/**\n * @description header select menu test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport createEditor from '../../../../../tests/utils/create-editor'\nimport HeaderSelectMenu from '../../../src/modules/header/menu/HeaderSelectMenu'\n\ndescribe('header select menu', () => {\n  const editor = createEditor()\n  const startLocation = Editor.start(editor, [])\n  const menu = new HeaderSelectMenu()\n\n  it('get options', () => {\n    editor.select(startLocation)\n    const options1 = menu.getOptions(editor)\n    const selectedP = options1.some(opt => opt.selected && opt.value === 'paragraph') // 选中“文本”\n    expect(selectedP).toBeTruthy()\n\n    Transforms.setNodes(editor, { type: 'header1' })\n    const options2 = menu.getOptions(editor)\n    const selectedHeader = options2.some(opt => opt.selected && opt.value === 'header1') // 选中“h1”\n    expect(selectedHeader).toBeTruthy()\n  })\n\n  // isActive 无逻辑，不用测试\n\n  // getValue isDisabled exec 已经在 helper.test.ts 中测试过了\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/header/menu/header1-menu.test.ts",
    "content": "/**\n * @description header1 menu test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport createEditor from '../../../../../tests/utils/create-editor'\nimport Header1ButtonMenu from '../../../src/modules/header/menu/Header1ButtonMenu'\nimport Header2ButtonMenu from '../../../src/modules/header/menu/Header2ButtonMenu'\nimport Header3ButtonMenu from '../../../src/modules/header/menu/Header3ButtonMenu'\nimport Header4ButtonMenu from '../../../src/modules/header/menu/Header4ButtonMenu'\nimport Header5ButtonMenu from '../../../src/modules/header/menu/Header5ButtonMenu'\n\ndescribe('header menu', () => {\n  const editor = createEditor()\n  const startLocation = Editor.start(editor, [])\n\n  describe('header1 menu', () => {\n    const menu = new Header1ButtonMenu()\n\n    it('exec', () => {\n      editor.select(startLocation)\n\n      menu.exec(editor, 'paragraph') // 设置 header （ paragraph 是当前选中的 node type ）\n      const headers1 = editor.getElemsByTypePrefix('header1')\n      expect(headers1.length).toBe(1)\n\n      menu.exec(editor, 'header1') // 取消 header（ header1 是当前选中的 node type ）\n      const headers2 = editor.getElemsByTypePrefix('header1')\n      expect(headers2.length).toBe(0)\n    })\n  })\n\n  describe('header2 menu', () => {\n    const menu = new Header2ButtonMenu()\n\n    it('exec', () => {\n      editor.select(startLocation)\n\n      menu.exec(editor, 'paragraph') // 设置 header （ paragraph 是当前选中的 node type ）\n      const headers1 = editor.getElemsByTypePrefix('header2')\n      expect(headers1.length).toBe(1)\n\n      menu.exec(editor, 'header2') // 取消 header（ header2 是当前选中的 node type ）\n      const headers2 = editor.getElemsByTypePrefix('header2')\n      expect(headers2.length).toBe(0)\n    })\n  })\n\n  describe('header3 menu', () => {\n    const menu = new Header3ButtonMenu()\n\n    it('exec', () => {\n      editor.select(startLocation)\n\n      menu.exec(editor, 'paragraph') // 设置 header （ paragraph 是当前选中的 node type ）\n      const headers1 = editor.getElemsByTypePrefix('header3')\n      expect(headers1.length).toBe(1)\n\n      menu.exec(editor, 'header3') // 取消 header（ header3 是当前选中的 node type ）\n      const headers2 = editor.getElemsByTypePrefix('header3')\n      expect(headers2.length).toBe(0)\n    })\n  })\n\n  describe('header4 menu', () => {\n    const menu = new Header4ButtonMenu()\n\n    it('exec', () => {\n      editor.select(startLocation)\n\n      menu.exec(editor, 'paragraph') // 设置 header （ paragraph 是当前选中的 node type ）\n      const headers1 = editor.getElemsByTypePrefix('header4')\n      expect(headers1.length).toBe(1)\n\n      menu.exec(editor, 'header4') // 取消 header（ header4 是当前选中的 node type ）\n      const headers2 = editor.getElemsByTypePrefix('header4')\n      expect(headers2.length).toBe(0)\n    })\n  })\n\n  describe('header5 menu', () => {\n    const menu = new Header5ButtonMenu()\n\n    it('exec', () => {\n      editor.select(startLocation)\n\n      menu.exec(editor, 'paragraph') // 设置 header （ paragraph 是当前选中的 node type ）\n      const headers1 = editor.getElemsByTypePrefix('header5')\n      expect(headers1.length).toBe(1)\n\n      menu.exec(editor, 'header5') // 取消 header（ header5 是当前选中的 node type ）\n      const headers2 = editor.getElemsByTypePrefix('header5')\n      expect(headers2.length).toBe(0)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/header/parse-html.test.ts",
    "content": "/**\n * @description parse html test\n * @author wangfupeng\n */\n\nimport { $ } from 'dom7'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { parseHeader1HtmlConf } from '../../src/modules/header/parse-elem-html'\n\ndescribe('header - parse html', () => {\n  const editor = createEditor()\n\n  it('with children', () => {\n    const $h1 = $(`<h1></h1>`)\n    const children = [{ text: 'hello ' }, { text: 'world', bold: true }]\n\n    // match selector\n    expect($h1[0].matches(parseHeader1HtmlConf.selector)).toBeTruthy()\n\n    // parse html\n    const res = parseHeader1HtmlConf.parseElemHtml($h1[0], children, editor)\n    expect(res).toEqual({\n      type: `header1`,\n      children: [{ text: 'hello ' }, { text: 'world', bold: true }],\n    })\n  })\n\n  it('without children', () => {\n    const $h1 = $(`<h1>hello world</h1>`)\n\n    // match selector\n    expect($h1[0].matches(parseHeader1HtmlConf.selector)).toBeTruthy()\n\n    // parse html\n    const res = parseHeader1HtmlConf.parseElemHtml($h1[0], [], editor)\n    expect(res).toEqual({\n      type: `header1`,\n      children: [{ text: 'hello world' }],\n    })\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/header/plugin.test.ts",
    "content": "/**\n * @description header plugin test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport withHeader from '../../src/modules/header/plugin'\n\ndescribe('header plugin', () => {\n  const editor = withHeader(createEditor())\n  const startLocation = Editor.start(editor, [])\n\n  it('header break', () => {\n    editor.select(startLocation)\n\n    Transforms.setNodes(editor, { type: 'header1' })\n    editor.insertBreak() // 在 header 换行，会生成 p\n\n    const paragraphs = editor.getElemsByTypePrefix('paragraph')\n    expect(paragraphs.length).toBe(1)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/header/render-elem.test.ts",
    "content": "/**\n * @description header - render elem test\n * @author wangfupeng\n */\n\nimport createEditor from '../../../../tests/utils/create-editor'\nimport {\n  renderHeader1Conf,\n  renderHeader2Conf,\n  renderHeader3Conf,\n  renderHeader4Conf,\n  renderHeader5Conf,\n} from '../../src/modules/header/render-elem'\n\ndescribe('render header elem', () => {\n  const editor = createEditor()\n\n  it('render h1', () => {\n    expect(renderHeader1Conf.type).toBe('header1')\n\n    const elem = { type: 'header1', children: [] }\n    const vnode = renderHeader1Conf.renderElem(elem, null, editor)\n    expect(vnode.sel).toBe('h1')\n  })\n\n  it('render h2', () => {\n    expect(renderHeader2Conf.type).toBe('header2')\n\n    const elem = { type: 'header2', children: [] }\n    const vnode = renderHeader2Conf.renderElem(elem, null, editor)\n    expect(vnode.sel).toBe('h2')\n  })\n\n  it('render h3', () => {\n    expect(renderHeader3Conf.type).toBe('header3')\n\n    const elem = { type: 'header3', children: [] }\n    const vnode = renderHeader3Conf.renderElem(elem, null, editor)\n    expect(vnode.sel).toBe('h3')\n  })\n\n  it('render h4', () => {\n    expect(renderHeader4Conf.type).toBe('header4')\n\n    const elem = { type: 'header4', children: [] }\n    const vnode = renderHeader4Conf.renderElem(elem, null, editor)\n    expect(vnode.sel).toBe('h4')\n  })\n\n  it('render h5', () => {\n    expect(renderHeader5Conf.type).toBe('header5')\n\n    const elem = { type: 'header5', children: [] }\n    const vnode = renderHeader5Conf.renderElem(elem, null, editor)\n    expect(vnode.sel).toBe('h5')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/image/elem-to-html.test.ts",
    "content": "/**\n * @description image - elem to html test\n * @author wangfupeng\n */\n\nimport { imageToHtmlConf } from '../../src/modules/image/elem-to-html'\n\ndescribe('image to html', () => {\n  it('to html', () => {\n    expect(imageToHtmlConf.type).toBe('image')\n\n    const src = 'https://www.wangeditor.com/imgs/logo.png'\n    const href = 'https://www.wangeditor.com/'\n    const elem = {\n      type: 'image',\n      src,\n      alt: 'logo',\n      href,\n      style: { width: '100', height: '80' },\n      children: [{ text: '' }], // void node 必须包含一个空 text\n    }\n    const html = imageToHtmlConf.elemToHtml(elem, '')\n\n    expect(html).toBe(\n      `<img src=\"${src}\" alt=\"logo\" data-href=\"${href}\" style=\"width: 100;height: 80;\"/>`\n    )\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/image/helper.test.ts",
    "content": "/**\n * @description image helper test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport { DomEditor } from '@wangeditor/core'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport {\n  insertImageNode,\n  updateImageNode,\n  isInsertImageMenuDisabled,\n} from '../../src/modules/image/helper'\n\ndescribe('image helper', () => {\n  let editor: any\n  let startLocation: any\n\n  const src = 'https://www.wangeditor.com/imgs/logo.png'\n  const alt = 'logo'\n  const href = 'https://www.wangeditor.com/'\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  it('insert image node', async () => {\n    editor.select(startLocation)\n    await insertImageNode(editor, src, alt, href)\n    const images = editor.getElemsByTypePrefix('image')\n    expect(images.length).toBe(1)\n  })\n\n  it('update image node', async () => {\n    editor.select(startLocation)\n\n    const elem = {\n      type: 'image',\n      src,\n      alt,\n      href,\n      style: { width: '100', height: '80' },\n      children: [{ text: '' }], // void node 必须包含一个空 text\n    }\n    editor.insertNode(elem) // 插入图片\n    editor.select({\n      path: [0, 1, 0], // 选中图片\n      offset: 0,\n    })\n\n    const newSrc = 'https://www.baidu.com/logo.png'\n    const newAlt = 'baidu'\n    const newHref = 'https://www.baidu.com/'\n    await updateImageNode(editor, newSrc, newAlt, newHref, {}) // 更新图片信息\n\n    const imageNode = DomEditor.getSelectedNodeByType(editor, 'image')\n    expect(imageNode).not.toBeNull()\n  })\n\n  it('is menu disable', async () => {\n    editor.deselect()\n    expect(isInsertImageMenuDisabled(editor)).toBeTruthy()\n\n    editor.select(startLocation)\n    expect(isInsertImageMenuDisabled(editor)).toBeFalsy()\n\n    editor.insertText('hello')\n    editor.select([])\n    expect(isInsertImageMenuDisabled(editor)).toBeTruthy()\n\n    editor.select(startLocation)\n    Transforms.setNodes(editor, { type: 'header1' })\n    expect(isInsertImageMenuDisabled(editor)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/image/menu/del-image.test.ts",
    "content": "/**\n * @description delete image menu test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport createEditor from '../../../../../tests/utils/create-editor'\nimport DeleteImage from '../../../src/modules/image/menu/DeleteImage'\n\ndescribe('delete image menu', () => {\n  const menu = new DeleteImage()\n  let editor: any\n  let startLocation: any\n\n  const src = 'https://www.wangeditor.com/imgs/logo.png'\n  const alt = 'logo'\n  const href = 'https://www.wangeditor.com/'\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  // getValue isActive 无逻辑，不用测试\n\n  it('is disabled', () => {\n    editor.deselect()\n    expect(menu.isDisabled(editor)).toBeTruthy()\n\n    editor.select(startLocation)\n    expect(menu.isDisabled(editor)).toBeTruthy()\n\n    const elem = {\n      type: 'image',\n      src,\n      alt,\n      href,\n      children: [{ text: '' }], // void node 必须包含一个空 text\n    }\n    editor.insertNode(elem) // 插入图片\n    editor.select({\n      path: [0, 1, 0], // 选中图片\n      offset: 0,\n    })\n    expect(menu.isDisabled(editor)).toBeFalsy()\n  })\n\n  it('exec', () => {\n    editor.select(startLocation)\n    const elem = {\n      type: 'image',\n      src,\n      alt,\n      href,\n      children: [{ text: '' }], // void node 必须包含一个空 text\n    }\n    editor.insertNode(elem) // 插入图片\n    editor.select({\n      path: [0, 1, 0], // 选中图片\n      offset: 0,\n    })\n\n    menu.exec(editor, '')\n    const images = editor.getElemsByTypePrefix('image')\n    expect(images.length).toBe(0)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/image/menu/edit-image.test.ts",
    "content": "/**\n * @description edit image menu test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport createEditor from '../../../../../tests/utils/create-editor'\nimport EditImage from '../../../src/modules/image/menu/EditImage'\n\ndescribe('edit image menu', () => {\n  const menu = new EditImage()\n  let editor: any\n  let startLocation: any\n\n  const src = 'https://www.wangeditor.com/imgs/logo.png'\n  const alt = 'logo'\n  const href = 'https://www.wangeditor.com/'\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  // getValue isActive exec 无逻辑，不用测试\n\n  it('is disabled', () => {\n    editor.deselect()\n    expect(menu.isDisabled(editor)).toBeTruthy()\n\n    editor.select(startLocation)\n    expect(menu.isDisabled(editor)).toBeTruthy()\n\n    const elem = {\n      type: 'image',\n      src,\n      alt,\n      href,\n      style: { width: '100', height: '80' },\n      children: [{ text: '' }], // void node 必须包含一个空 text\n    }\n    editor.insertNode(elem) // 插入图片\n    editor.select({\n      path: [0, 1, 0], // 选中图片\n      offset: 0,\n    })\n    expect(menu.isDisabled(editor)).toBeFalsy()\n  })\n\n  it('get modal position node', () => {\n    editor.select(startLocation)\n    expect(menu.getModalPositionNode(editor)).toBeNull()\n\n    const elem = {\n      type: 'image',\n      src,\n      alt,\n      href,\n      style: { width: '100', height: '80' },\n      children: [{ text: '' }], // void node 必须包含一个空 text\n    }\n    editor.insertNode(elem) // 插入图片\n    editor.select({\n      path: [0, 1, 0], // 选中图片\n      offset: 0,\n    })\n    const imageNode = menu.getModalPositionNode(editor)\n    expect((imageNode as any).src).toBe(src)\n  })\n\n  it('get modal content elem', () => {\n    editor.select(startLocation)\n    const imageElem = {\n      type: 'image',\n      src,\n      alt,\n      href,\n      style: { width: '100', height: '80' },\n      children: [{ text: '' }], // void node 必须包含一个空 text\n    }\n    editor.insertNode(imageElem) // 插入图片\n    editor.select({\n      path: [0, 1, 0], // 选中图片\n      offset: 0,\n    })\n\n    const elem = menu.getModalContentElem(editor)\n    expect(elem.tagName).toBe('DIV')\n\n    // updateImage 在 helper.test.ts 中测试\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/image/menu/insert-image.test.ts",
    "content": "/**\n * @description insert image menu test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport createEditor from '../../../../../tests/utils/create-editor'\nimport InsertImage from '../../../src/modules/image/menu/InsertImage'\n\ndescribe('insert image menu', () => {\n  const menu = new InsertImage()\n  let editor: any\n  let startLocation: any\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  // getValue isActive exec 无逻辑，不用测试\n\n  it('is disabled', () => {\n    editor.deselect()\n    expect(menu.isDisabled(editor)).toBeTruthy()\n\n    editor.select(startLocation)\n    expect(menu.isDisabled(editor)).toBeFalsy()\n\n    editor.insertText('xxx')\n    editor.select([]) // 全选文字\n    expect(menu.isDisabled(editor)).toBeTruthy() // 非折叠选区，则不可用\n\n    editor.select(startLocation)\n    Transforms.setNodes(editor, { type: 'header1' })\n    expect(menu.isDisabled(editor)).toBeTruthy() // header 中不可用\n\n    Transforms.setNodes(editor, { type: 'blockquote' })\n    expect(menu.isDisabled(editor)).toBeTruthy() // blockquote 中不可用\n  })\n\n  // getModalPositionNode 无逻辑，不用测试\n\n  it('get modal content elem', () => {\n    const elem = menu.getModalContentElem(editor)\n    expect(elem.tagName).toBe('DIV')\n\n    // insertImage 在 helper.test.ts 中测试\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/image/menu/view-image-link.test.ts",
    "content": "/**\n * @description view image link menu test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport createEditor from '../../../../../tests/utils/create-editor'\nimport ViewImageLink from '../../../src/modules/image/menu/ViewImageLink'\n\ndescribe('view image link menu', () => {\n  const menu = new ViewImageLink()\n  let editor: any\n  let startLocation: any\n\n  const src = 'https://www.wangeditor.com/imgs/logo.png'\n  const alt = 'logo'\n  const href = 'https://www.wangeditor.com/'\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  it('getValue and isDisabled', () => {\n    editor.select(startLocation)\n    expect(menu.getValue(editor)).toBe('')\n    expect(menu.isDisabled(editor)).toBeTruthy()\n\n    const elem = {\n      type: 'image',\n      src,\n      alt,\n      href,\n      style: { width: '100', height: '80' },\n      children: [{ text: '' }], // void node 必须包含一个空 text\n    }\n    editor.insertNode(elem) // 插入图片\n    editor.select({\n      path: [0, 1, 0], // 选中图片\n      offset: 0,\n    })\n    expect(menu.getValue(editor)).toBe(href)\n    expect(menu.isDisabled(editor)).toBeFalsy()\n  })\n\n  // isActive 无逻辑，不用测试\n\n  // exec 逻辑简单，不用测试\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/image/menu/width-menus.test.ts",
    "content": "/**\n * @description image width menus test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport createEditor from '../../../../../tests/utils/create-editor'\nimport Width30 from '../../../src/modules/image/menu/Width30'\nimport Width50 from '../../../src/modules/image/menu/Width50'\nimport Width100 from '../../../src/modules/image/menu/Width100'\n\ndescribe('image width menus', () => {\n  const width30Menu = new Width30()\n  const width50Menu = new Width50()\n  const width100Menu = new Width100()\n\n  let editor: any\n  let startLocation: any\n\n  const src = 'https://www.wangeditor.com/imgs/logo.png'\n  const alt = 'logo'\n  const href = 'https://www.wangeditor.com/'\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  // getValue isActive 无逻辑，不用测试\n\n  it('is disabled', () => {\n    editor.deselect()\n    expect(width30Menu.isDisabled(editor)).toBeTruthy()\n    expect(width50Menu.isDisabled(editor)).toBeTruthy()\n    expect(width100Menu.isDisabled(editor)).toBeTruthy()\n\n    editor.select(startLocation)\n    expect(width30Menu.isDisabled(editor)).toBeTruthy()\n    expect(width50Menu.isDisabled(editor)).toBeTruthy()\n    expect(width100Menu.isDisabled(editor)).toBeTruthy()\n\n    const elem = {\n      type: 'image',\n      src,\n      alt,\n      href,\n      style: { width: '100', height: '80' },\n      children: [{ text: '' }], // void node 必须包含一个空 text\n    }\n    editor.insertNode(elem) // 插入图片\n    editor.select({\n      path: [0, 1, 0], // 选中图片\n      offset: 0,\n    })\n    expect(width30Menu.isDisabled(editor)).toBeFalsy()\n    expect(width50Menu.isDisabled(editor)).toBeFalsy()\n    expect(width100Menu.isDisabled(editor)).toBeFalsy()\n  })\n\n  it('exec', () => {\n    editor.select(startLocation)\n    const elem = {\n      type: 'image',\n      src,\n      alt,\n      href,\n      style: { width: '100', height: '80' },\n      children: [{ text: '' }], // void node 必须包含一个空 text\n    }\n    editor.insertNode(elem) // 插入图片\n    editor.select({\n      path: [0, 1, 0], // 选中图片\n      offset: 0,\n    })\n\n    width30Menu.exec(editor, '')\n    const image1 = editor.getElemsByTypePrefix('image')[0]\n    expect(image1.style.width).toBe('30%')\n    expect(image1.style.height).toBe('')\n\n    width50Menu.exec(editor, '')\n    const image2 = editor.getElemsByTypePrefix('image')[0]\n    expect(image2.style.width).toBe('50%')\n    expect(image2.style.height).toBe('')\n\n    width100Menu.exec(editor, '')\n    const image3 = editor.getElemsByTypePrefix('image')[0]\n    expect(image3.style.width).toBe('100%')\n    expect(image3.style.height).toBe('')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/image/parse-html.test.ts",
    "content": "/**\n * @description parse html test\n * @author wangfupeng\n */\n\nimport { $ } from 'dom7'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { parseHtmlConf } from '../../src/modules/image/parse-elem-html'\n\ndescribe('image - parse html', () => {\n  const editor = createEditor()\n\n  it('parse html', () => {\n    const $img = $(\n      '<img src=\"hello.png\" alt=\"hello\" data-href=\"http://localhost/\" style=\"width: 10px; height: 5px;\"/>'\n    )\n\n    // match selector\n    expect($img[0].matches(parseHtmlConf.selector)).toBeTruthy()\n\n    // parse\n    const res = parseHtmlConf.parseElemHtml($img[0], [], editor)\n    expect(res).toEqual({\n      type: 'image',\n      src: 'hello.png',\n      alt: 'hello',\n      href: 'http://localhost/',\n      style: {\n        width: '10px',\n        height: '5px',\n      },\n      children: [{ text: '' }],\n    })\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/image/plugin.test.ts",
    "content": "/**\n * @description image plugin test\n * @author wangfupeng\n */\n\nimport createEditor from '../../../../tests/utils/create-editor'\nimport withImage from '../../src/modules/image/plugin'\n\ndescribe('image plugin', () => {\n  const editor = withImage(createEditor())\n  const elem = { type: 'image', children: [{ text: '' }] }\n\n  it('image is inline', () => {\n    expect(editor.isInline(elem)).toBeTruthy()\n  })\n\n  it('image is void', () => {\n    expect(editor.isVoid(elem)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/image/render-elem.test.ts",
    "content": "/**\n * @description image - render elem test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport { renderImageConf } from '../../src/modules/image/render-elem'\nimport createEditor from '../../../../tests/utils/create-editor'\n\ndescribe('image render elem', () => {\n  let editor: any\n  let startLocation: any\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor.clear()\n    editor.destroy()\n    editor = null\n    startLocation = null\n  })\n\n  it('render image - unselected image', () => {\n    expect(renderImageConf.type).toBe('image')\n\n    const src = 'https://www.wangeditor.com/imgs/logo.png'\n    const href = 'https://www.wangeditor.com/'\n    const elem = {\n      type: 'image',\n      src,\n      alt: 'logo',\n      href,\n      style: { width: '100', height: '80' },\n      children: [{ text: '' }], // void node 必须包含一个空 text\n    }\n\n    const containerVnode = renderImageConf.renderElem(elem, null, editor) as any\n    expect(containerVnode.sel).toBe('div')\n    expect(containerVnode.data.className).toBe('w-e-image-container')\n    expect(containerVnode.data.style.width).toBe('100')\n    expect(containerVnode.data.style.height).toBe('80')\n\n    const imageVnode = containerVnode.children[0] as any\n    expect(imageVnode.sel).toBe('img')\n    expect(imageVnode.data.src).toBe(src)\n    expect(imageVnode.data['data-href']).toBe(href)\n  })\n\n  it('render image - selected image', () => {\n    const src = 'https://www.wangeditor.com/imgs/logo.png'\n    const href = 'https://www.wangeditor.com/'\n    const elem = {\n      type: 'image',\n      src,\n      alt: 'logo',\n      href,\n      style: { width: '100', height: '80' },\n      children: [{ text: '' }], // void node 必须包含一个空 text\n    }\n\n    editor.select(startLocation)\n    editor.insertNode(elem) // 插入图片\n    editor.select({\n      path: [0, 1, 0], // 选中图片\n      offset: 0,\n    })\n\n    const containerVnode = renderImageConf.renderElem(elem, null, editor) as any\n    expect(containerVnode.sel).toBe('div')\n    expect(containerVnode.data.className.indexOf('w-e-selected-image-container')).toBeGreaterThan(0)\n    expect(containerVnode.children.length).toBe(5) // image + 4 个拖拽触手\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/indent/menu/decrease-indent-menu.test.ts",
    "content": "/**\n * @description decrease indent menu test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport createEditor from '../../../../../tests/utils/create-editor'\nimport DecreaseIndentMenu from '../../../src/modules/indent/menu/DecreaseIndentMenu'\n\ndescribe('decrease indent menu', () => {\n  let editor: any\n  let startLocation: any\n\n  const menu = new DecreaseIndentMenu()\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  it('is disabled', () => {\n    editor.select(startLocation)\n    expect(menu.isDisabled(editor)).toBeTruthy() // 没有 indent 则 disabled\n\n    Transforms.setNodes(editor, { type: 'header1', children: [] })\n    expect(menu.isDisabled(editor)).toBeTruthy() // 没有 indent 则 disabled\n\n    editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] })\n    expect(menu.isDisabled(editor)).toBeTruthy() // 除了 p header 之外，其他 type 不可用 indent\n    // Transforms.removeNodes(editor, { mode: 'highest' }) // 移除 pre/code\n  })\n\n  // isActive 不用测试\n\n  // getValue 在 increase menu 已测试过\n\n  it('exec', () => {\n    editor.select(startLocation)\n    Transforms.setNodes(editor, { type: 'paragraph', indent: '2em', children: [] })\n\n    expect(menu.isDisabled(editor)).toBeFalsy() // 有 indent 则取消 disabled\n\n    menu.exec(editor, '')\n    expect(menu.getValue(editor)).toBe('')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/indent/menu/increase-indent-menu.test.ts",
    "content": "/**\n * @description increase indent menu test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport createEditor from '../../../../../tests/utils/create-editor'\nimport IncreaseIndentMenu from '../../../src/modules/indent/menu/IncreaseIndentMenu'\n\ndescribe('increase indent menu', () => {\n  let editor: any\n  let startLocation: any\n\n  const menu = new IncreaseIndentMenu()\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  it('is disabled', () => {\n    editor.select(startLocation)\n    expect(menu.isDisabled(editor)).toBeFalsy()\n\n    Transforms.setNodes(editor, { type: 'header1', children: [] })\n    expect(menu.isDisabled(editor)).toBeFalsy()\n\n    editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] })\n    expect(menu.isDisabled(editor)).toBeTruthy() // 除了 p header 之外，其他 type 不可用 indent\n    // Transforms.removeNodes(editor, { mode: 'highest' }) // 移除 pre/code\n  })\n\n  // isActive 不用测试\n\n  it('exec and getValue', () => {\n    editor.select(startLocation)\n    expect(menu.getValue(editor)).toBe('')\n\n    menu.exec(editor, '')\n    expect(menu.getValue(editor)).toBe('2em')\n  })\n\n  it('indent value', () => {\n    editor.insertNode({\n      type: 'paragraph',\n      children: [{ fontSize: '18px', text: 'text1' } as any],\n    })\n\n    menu.exec(editor, '')\n\n    expect(menu.getValue(editor)).toBe('36px')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/indent/parse-html.test.ts",
    "content": "/**\n * @description parse html test\n * @author wangfupeng\n */\n\nimport { $ } from 'dom7'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { parseStyleHtml } from '../../src/modules/indent/parse-style-html'\nimport { preParseHtmlConf } from '../../src/modules/indent/pre-parse-html'\n\ndescribe('indent - parse style', () => {\n  const editor = createEditor()\n\n  it('parse style', () => {\n    const $p = $('<p style=\"text-indent: 2em;\"></p>')\n    const paragraph = { type: 'paragraph', children: [{ text: 'hello' }] }\n\n    // parse\n    const res = parseStyleHtml($p[0], paragraph, editor)\n    expect(res).toEqual({\n      type: 'paragraph',\n      indent: '2em',\n      children: [{ text: 'hello' }],\n    })\n  })\n})\n\ndescribe('indent - pre parse html', () => {\n  it('pre parse', () => {\n    expect(preParseHtmlConf.selector).toBe('p,h1,h2,h3,h4,h5')\n\n    const $p = $('<p style=\"padding-left: 2em;\"></p>')\n\n    // parse\n    const res = preParseHtmlConf.preParseHtml($p[0])\n    expect((res as HTMLParagraphElement).style.textIndent).toBe('2em')\n  })\n\n  it('pre parse with px unit', () => {\n    expect(preParseHtmlConf.selector).toBe('p,h1,h2,h3,h4,h5')\n\n    const $p = $('<p style=\"padding-left: 32px;\"></p>')\n\n    // parse\n    const res = preParseHtmlConf.preParseHtml($p[0])\n    expect((res as HTMLParagraphElement).style.textIndent).toBe('2em')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/indent/render-text-style.test.tsx",
    "content": "/**\n * @description indent - render text style\n * @author wangfupeng\n */\n\nimport { jsx } from 'snabbdom'\nimport { renderStyle } from '../../src/modules/indent/render-style'\n\ndescribe('indent - render text style', () => {\n  it('render text style', () => {\n    const indent = '2em'\n    const elem = { type: 'paragraph', indent, children: [] }\n    const vnode = <p>hello</p>\n\n    // @ts-ignore\n    const newVnode = renderStyle(elem, vnode)\n    // @ts-ignore\n    expect(newVnode.data.style.textIndent).toBe(indent)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/indent/text-style-to-html.test.ts",
    "content": "/**\n * @description indent - text style to html test\n * @author wangfupeng\n */\n\nimport { styleToHtml } from '../../src/modules/indent/style-to-html'\n\ndescribe('indent - text style to html', () => {\n  it('text style to html', () => {\n    const indent = '2em'\n    const elem = { type: 'paragraph', indent, children: [] }\n    const html = styleToHtml(elem, '<p>hello</p>')\n    expect(html).toBe(`<p style=\"text-indent: ${indent};\">hello</p>`)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/justify/menus.test.ts",
    "content": "/**\n * @description justify menus test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport JustifyCenterMenu from '../../src/modules/justify/menu/JustifyCenterMenu'\nimport JustifyJustifyMenu from '../../src/modules/justify/menu/JustifyJustifyMenu'\nimport JustifyLeftMenu from '../../src/modules/justify/menu/JustifyLeftMenu'\nimport JustifyRightMenu from '../../src/modules/justify/menu/JustifyRightMenu'\n\ndescribe('justify menus', () => {\n  let editor: any\n  let startLocation: any\n\n  const centerMenu = new JustifyCenterMenu()\n  const justifyMenu = new JustifyJustifyMenu()\n  const leftMenu = new JustifyLeftMenu()\n  const rightMenu = new JustifyRightMenu()\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  // getValue getActive 不需要测试\n\n  it('is disabled', () => {\n    editor.deselect()\n    expect(centerMenu.isDisabled(editor)).toBeTruthy()\n\n    editor.select(startLocation)\n    expect(centerMenu.isDisabled(editor)).toBeFalsy()\n\n    editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] })\n    expect(centerMenu.isDisabled(editor)).toBeTruthy()\n    // Transforms.removeNodes(editor, { mode: 'highest' }) // 移除 pre/code\n  })\n\n  it('exec', () => {\n    editor.select(startLocation)\n\n    centerMenu.exec(editor, '')\n    const p1 = editor.getElemsByTypePrefix('paragraph')[0]\n    expect(p1.textAlign).toBe('center')\n\n    justifyMenu.exec(editor, '')\n    const p2 = editor.getElemsByTypePrefix('paragraph')[0]\n    expect(p2.textAlign).toBe('justify')\n\n    leftMenu.exec(editor, '')\n    const p3 = editor.getElemsByTypePrefix('paragraph')[0]\n    expect(p3.textAlign).toBe('left')\n\n    rightMenu.exec(editor, '')\n    const p4 = editor.getElemsByTypePrefix('paragraph')[0]\n    expect(p4.textAlign).toBe('right')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/justify/parse-html.test.ts",
    "content": "/**\n * @description parse html test\n * @author wangfupeng\n */\n\nimport { $ } from 'dom7'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { parseStyleHtml } from '../../src/modules/justify/parse-style-html'\n\ndescribe('text align - parse style', () => {\n  const editor = createEditor()\n\n  it('parse style', () => {\n    const $p = $('<p style=\"text-align: center;\"></p>')\n    const paragraph = { type: 'paragraph', children: [{ text: 'hello' }] }\n\n    // parse\n    const res = parseStyleHtml($p[0], paragraph, editor)\n    expect(res).toEqual({\n      type: 'paragraph',\n      textAlign: 'center',\n      children: [{ text: 'hello' }],\n    })\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/justify/render-text-style.test.tsx",
    "content": "/**\n * @description justify - render text style test\n * @author wangfupeng\n */\n\nimport { jsx } from 'snabbdom'\nimport { renderStyle } from '../../src/modules/justify/render-style'\n\ndescribe('justify - render text style', () => {\n  it('render text style', () => {\n    const elem = { type: 'paragraph', textAlign: 'center', children: [] }\n    const vnode = <span>hello</span>\n    // @ts-ignore 忽略 vnode 格式\n    const newVnode = renderStyle(elem, vnode)\n    // @ts-ignore 忽略 vnode 格式\n    expect(newVnode.data.style?.textAlign).toBe('center')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/justify/text-style-to-html.test.ts",
    "content": "/**\n * @description justify - text style to html test\n * @author wangfupeng\n */\n\nimport { styleToHtml } from '../../src/modules/justify/style-to-html'\n\ndescribe('justify text-style-to-html', () => {\n  it('text style to html', () => {\n    const elem = { type: 'paragraph', textAlign: 'center', children: [] }\n    const html = styleToHtml(elem, '<span>hello</span>')\n    expect(html).toBe('<span style=\"text-align: center;\">hello</span>')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/line-height/line-height-menu.test.ts",
    "content": "/**\n * @description line-height menu test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport LineHeightMenu from '../../src/modules/line-height/menu/LineHeightMenu'\n\ndescribe('line-height menu', () => {\n  let editor: any\n  let startLocation: any\n  const menu = new LineHeightMenu()\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  it('get options', () => {\n    editor.select(startLocation)\n\n    const options = menu.getOptions(editor)\n    expect(options.length).toBeGreaterThan(0)\n\n    // 默认选中 空\n    const selectedEmptyOne = options.some(opt => opt.value === '' && opt.selected)\n    expect(selectedEmptyOne).toBe(true)\n  })\n\n  // isActive 返回 false ，不用测试\n\n  it('get value', () => {\n    editor.select(startLocation)\n    expect(menu.getValue(editor)).toBe('')\n\n    // 设置 lineHeight\n    Transforms.setNodes(editor, { lineHeight: '1.5' }, { mode: 'highest' })\n    expect(menu.getValue(editor)).toBe('1.5')\n  })\n\n  it('is disable', () => {\n    editor.deselect()\n    expect(menu.isDisabled(editor)).toBeTruthy()\n\n    editor.select(startLocation)\n    expect(menu.isDisabled(editor)).toBeFalsy()\n\n    Transforms.setNodes(editor, { type: 'header1' })\n    expect(menu.isDisabled(editor)).toBeFalsy()\n    Transforms.setNodes(editor, { type: 'blockquote' })\n    expect(menu.isDisabled(editor)).toBeFalsy()\n    Transforms.setNodes(editor, { type: 'list-item' })\n    expect(menu.isDisabled(editor)).toBeFalsy()\n\n    editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] })\n    expect(menu.isDisabled(editor)).toBeTruthy()\n    // Transforms.removeNodes(editor, { mode: 'highest' }) // 移除 pre/code\n  })\n\n  it('exec', () => {\n    editor.select(startLocation)\n    menu.exec(editor, '1.5')\n    expect(menu.getValue(editor)).toBe('1.5')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/line-height/parse-html.test.ts",
    "content": "/**\n * @description parse html test\n * @author wangfupeng\n */\n\nimport { $ } from 'dom7'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { parseStyleHtml } from '../../src/modules/line-height/parse-style-html'\n\ndescribe('line height - parse style', () => {\n  const editor = createEditor()\n\n  it('parse style', () => {\n    const $p = $('<p style=\"line-height: 2.5;\"></p>')\n    const paragraph = { type: 'paragraph', children: [{ text: 'hello' }] }\n\n    // parse\n    const res = parseStyleHtml($p[0], paragraph, editor)\n    expect(res).toEqual({\n      type: 'paragraph',\n      lineHeight: '2.5',\n      children: [{ text: 'hello' }],\n    })\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/line-height/render-text-style.test.tsx",
    "content": "/**\n * @description line-height render text style test\n * @author wangfupeng\n */\n\nimport { jsx } from 'snabbdom'\nimport { renderStyle } from '../../src/modules/line-height/render-style'\n\ndescribe('line-height render-text-style', () => {\n  it('render text style', () => {\n    const elem = { type: 'paragraph', lineHeight: '1.5', children: [] }\n    const vnode = <span>hello</span>\n    // @ts-ignore 忽略 vnode 格式检查\n    const newVnode = renderStyle(elem, vnode)\n    // @ts-ignore 忽略 vnode 格式检查\n    expect(newVnode.data.style.lineHeight).toBe('1.5')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/line-height/text-style-to-html.test.ts",
    "content": "/**\n * @description line-height text-style-to-html test\n * @author wangfupeng\n */\n\nimport { styleToHtml } from '../../src/modules/line-height/style-to-html'\n\ndescribe('line-height text-style-to-html', () => {\n  it('text style to html', () => {\n    const elem = { type: 'paragraph', lineHeight: '1.5', children: [] }\n    const html = styleToHtml(elem, '<span>hello</span>')\n    expect(html).toBe('<span style=\"line-height: 1.5;\">hello</span>')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/link/elem-to-html.test.ts",
    "content": "/**\n * @description link - elem to html test\n * @author wangfupeng\n */\n\nimport { linkToHtmlConf } from '../../src/modules/link/elem-to-html'\n\ndescribe('link elem to html', () => {\n  it('link to html', () => {\n    expect(linkToHtmlConf.type).toBe('link')\n\n    const url = 'https://www.wangeditor.com/'\n    const target = '_blank'\n    const elem = { type: 'link', url, target, children: [] }\n\n    const html = linkToHtmlConf.elemToHtml(elem, 'hello')\n    expect(html).toBe(`<a href=\"${url}\" target=\"${target}\">hello</a>`)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/link/helper.test.ts",
    "content": "/**\n * @description link module helper test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { isMenuDisabled, insertLink, updateLink } from '../../src/modules/link/helper'\n\ndescribe('link module helper', () => {\n  let editor: any\n  let startLocation: any\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  it('menu disable', () => {\n    editor.deselect()\n    expect(isMenuDisabled(editor)).toBeTruthy()\n\n    editor.select(startLocation)\n    expect(isMenuDisabled(editor)).toBeFalsy()\n\n    editor.insertNode({\n      type: 'link',\n      url: 'https://www.wangeditor.com/',\n      children: [{ text: 'xxx' }],\n    })\n    expect(isMenuDisabled(editor)).toBeTruthy() // 选中 link ，则禁用\n\n    editor.clear()\n    editor.insertNode({\n      type: 'pre',\n      children: [\n        {\n          type: 'code',\n          children: [{ text: 'var' }],\n        },\n      ],\n    })\n    expect(isMenuDisabled(editor)).toBeTruthy() // 选中 code-block ，则禁用\n  })\n\n  it('insert link with collapsed selection', async () => {\n    editor.select(startLocation)\n\n    const url = 'https://www.wangeditor.com/'\n    await insertLink(editor, 'hello', url)\n\n    const links = editor.getElemsByTypePrefix('link')\n    expect(links.length).toBe(1)\n    const linkElem = links[0]\n    expect(linkElem.url).toBe(url)\n  })\n\n  it('insert link with expand selection', async () => {\n    editor.select(startLocation)\n    editor.insertText('hello')\n    Transforms.move(editor, {\n      distance: 3, // 选中 3 个字母\n      unit: 'character',\n    })\n    editor.select([]) // 全选\n\n    const url = 'https://www.wangeditor.com/'\n    await insertLink(editor, 'hello', url)\n\n    const links = editor.getElemsByTypePrefix('link')\n    expect(links.length).toBe(1)\n    const linkElem = links[0]\n    expect(linkElem.url).toBe(url)\n  })\n\n  it('update link', async () => {\n    editor.select(startLocation)\n\n    const url = 'https://www.wangeditor.com/'\n    await insertLink(editor, 'hello', url)\n\n    // 选区移动到 link 内部\n    editor.select({\n      path: [0, 1, 0],\n      offset: 3,\n    })\n\n    // 更新链接\n    const newUrl = 'https://www.wangeditor.com/123'\n    await updateLink(editor, '', newUrl)\n\n    const links = editor.getElemsByTypePrefix('link')\n    expect(links.length).toBe(1)\n    const linkElem = links[0]\n    expect(linkElem.url).toBe(newUrl)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/link/menu/edit-link-menu.test.ts",
    "content": "/**\n * @description edit link menu test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport createEditor from '../../../../../tests/utils/create-editor'\nimport EditLink from '../../../src/modules/link/menu/EditLink'\n\ndescribe('edit link menu', () => {\n  let editor: any\n  let startLocation: any\n  const menu = new EditLink()\n\n  const linkNode = {\n    type: 'link',\n    url: 'https://www.wangeditor.com/',\n    children: [{ text: 'xxx' }],\n  }\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  it('get value', () => {\n    editor.select(startLocation)\n    expect(menu.getValue(editor)).toBe('')\n\n    editor.insertNode(linkNode)\n    editor.select({\n      path: [0, 1, 0], // 选区定位到 link 内部\n      offset: 1,\n    })\n    expect(menu.getValue(editor)).toBe(linkNode.url)\n  })\n\n  it('is active', () => {\n    expect(menu.isActive(editor)).toBeFalsy()\n  })\n\n  it('is disable', () => {\n    editor.select(startLocation)\n    expect(menu.isDisabled(editor)).toBeTruthy()\n\n    editor.insertNode(linkNode)\n    editor.select({\n      path: [0, 1, 0], // 选区定位到 link 内部\n      offset: 1,\n    })\n    expect(menu.isDisabled(editor)).toBeFalsy()\n  })\n\n  it('get modal position node', () => {\n    editor.select(startLocation)\n    expect(menu.getModalPositionNode(editor)).toBeNull()\n\n    editor.insertNode(linkNode)\n    editor.select({\n      path: [0, 1, 0], // 选区定位到 link 内部\n      offset: 1,\n    })\n    const node = menu.getModalPositionNode(editor) as any\n    expect(node.type).toBe('link')\n    expect(node.url).toBe(linkNode.url)\n  })\n\n  it('get modal content elem', () => {\n    editor.select(startLocation)\n    const elem = menu.getModalContentElem(editor)\n    expect(elem.tagName).toBe('DIV')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/link/menu/insert-link-menu.test.ts",
    "content": "/**\n * @description insert link menu test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport createEditor from '../../../../../tests/utils/create-editor'\nimport InsertLinkMenu from '../../../src/modules/link/menu/InsertLink'\n\ndescribe('insert link menu', () => {\n  const editor = createEditor()\n  const menu = new InsertLinkMenu()\n  const startLocation = Editor.start(editor, [])\n\n  afterEach(() => {\n    editor.select(startLocation)\n    editor.clear()\n    editor.deselect()\n  })\n\n  it('get value', () => {\n    expect(menu.getValue(editor)).toBe('')\n  })\n\n  it('is active', () => {\n    expect(menu.isActive(editor)).toBeFalsy()\n  })\n\n  it('get modal position node', () => {\n    expect(menu.getModalPositionNode(editor)).toBeNull()\n  })\n\n  it('is disable', () => {\n    editor.select(startLocation)\n    expect(menu.isDisabled(editor)).toBeFalsy()\n  })\n\n  it('get modal content elem', () => {\n    const elem = menu.getModalContentElem(editor)\n    expect(elem.tagName).toBe('DIV')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/link/menu/unlink-menu.test.ts",
    "content": "/**\n * @description unlink menu test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport createEditor from '../../../../../tests/utils/create-editor'\nimport UnLink from '../../../src/modules/link/menu/UnLink'\n\ndescribe('unlink menu test', () => {\n  let editor: any\n  let startLocation: any\n  const menu = new UnLink()\n\n  const linkNode = {\n    type: 'link',\n    url: 'https://www.wangeditor.com/',\n    children: [{ text: 'xxx' }],\n  }\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  it('get value', () => {\n    expect(menu.getValue(editor)).toBe('')\n  })\n\n  it('is active', () => {\n    expect(menu.isActive(editor)).toBe(false)\n  })\n\n  it('is disable', () => {\n    editor.select(startLocation)\n    expect(menu.isDisabled(editor)).toBeTruthy()\n\n    editor.insertNode(linkNode)\n    editor.select({\n      path: [0, 1, 0], // 选区定位到 link 内部\n      offset: 1,\n    })\n    expect(menu.isDisabled(editor)).toBeFalsy()\n  })\n\n  it('exec', () => {\n    editor.select(startLocation)\n    editor.insertNode(linkNode)\n    editor.select({\n      path: [0, 1, 0], // 选区定位到 link 内部\n      offset: 1,\n    })\n\n    menu.exec(editor, '')\n    const links = editor.getElemsByTypePrefix('link')\n    expect(links.length).toBe(0)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/link/menu/view-link-menu.test.ts",
    "content": "/**\n * @description view link menu test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport createEditor from '../../../../../tests/utils/create-editor'\nimport ViewLink from '../../../src/modules/link/menu/ViewLink'\n\ndescribe('view link menu', () => {\n  let editor: any\n  let startLocation: any\n  const menu = new ViewLink()\n\n  const linkNode = {\n    type: 'link',\n    url: 'https://www.wangeditor.com/',\n    children: [{ text: 'xxx' }],\n  }\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  it('get value', () => {\n    editor.select(startLocation)\n    expect(menu.getValue(editor)).toBe('')\n\n    editor.insertNode(linkNode)\n    editor.select({\n      path: [0, 1, 0], // 选区定位到 link 内部\n      offset: 1,\n    })\n    expect(menu.getValue(editor)).toBe(linkNode.url)\n  })\n\n  it('is active', () => {\n    expect(menu.isActive(editor)).toBe(false)\n  })\n\n  it('is disable', () => {\n    editor.select(startLocation)\n    expect(menu.isDisabled(editor)).toBeTruthy()\n\n    editor.insertNode(linkNode)\n    editor.select({\n      path: [0, 1, 0], // 选区定位到 link 内部\n      offset: 1,\n    })\n    expect(menu.isDisabled(editor)).toBeFalsy()\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/link/parse-html.test.ts",
    "content": "/**\n * @description parse html test\n * @author wangfupeng\n */\n\nimport { $ } from 'dom7'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { parseHtmlConf } from '../../src/modules/link/parse-elem-html'\n\ndescribe('link - parse html', () => {\n  const editor = createEditor()\n\n  it('without children', () => {\n    const $link = $('<a href=\"http://localhost/\" target=\"_blank\">hello world</a>')\n\n    // match selector\n    expect($link[0].matches(parseHtmlConf.selector)).toBeTruthy()\n\n    // parse\n    const res = parseHtmlConf.parseElemHtml($link[0], [], editor)\n    expect(res).toEqual({\n      type: 'link',\n      url: 'http://localhost/',\n      target: '_blank',\n      children: [{ text: 'hello world' }],\n    })\n  })\n\n  it('with children', () => {\n    const $link = $('<a href=\"http://localhost/\" target=\"_blank\"></a>')\n    const children = [{ text: 'hello ' }, { text: 'world', bold: true }]\n\n    // match selector\n    expect($link[0].matches(parseHtmlConf.selector)).toBeTruthy()\n\n    // parse\n    const res = parseHtmlConf.parseElemHtml($link[0], children, editor)\n    expect(res).toEqual({\n      type: 'link',\n      url: 'http://localhost/',\n      target: '_blank',\n      children: [{ text: 'hello ' }, { text: 'world', bold: true }],\n    })\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/link/plugin.test.ts",
    "content": "/**\n * @description link plugin test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport withLink from '../../src/modules/link/plugin'\nimport createEditor from '../../../../tests/utils/create-editor'\n\n// 模拟 DataTransfer\nclass MyDataTransfer {\n  private values: object = {}\n  setData(type: string, value: string) {\n    this.values[type] = value\n  }\n  getData(type: string): string {\n    return this.values[type]\n  }\n}\n\ndescribe('link plugin', () => {\n  const editor = withLink(createEditor())\n  const startLocation = Editor.start(editor, [])\n\n  it('link is inline elem', () => {\n    const elem = { type: 'link', children: [] }\n    expect(editor.isInline(elem)).toBeTruthy()\n  })\n\n  it('link insert data', done => {\n    const url = 'https://www.wangeditor.com/'\n\n    const data = new MyDataTransfer()\n    data.setData('text/plain', url)\n\n    editor.select(startLocation)\n    // @ts-ignore\n    editor.insertData(data)\n\n    setTimeout(() => {\n      const links = editor.getElemsByTypePrefix('link')\n      expect(links.length).toBe(1)\n      const linkElem = links[0] as any\n      expect(linkElem.url).toBe(url)\n      done()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/link/render-elem.test.ts",
    "content": "/**\n * @description link - render elem test\n * @author wangfupeng\n */\n\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { renderLinkConf } from '../../src/modules/link/render-elem'\n\ndescribe('link render elem', () => {\n  const editor = createEditor()\n\n  it('render elem', () => {\n    expect(renderLinkConf.type).toBe('link')\n\n    const url = 'https://www.wangeditor.com/'\n    const target = '_blank'\n    const elem = { type: 'link', url, target, children: [] }\n\n    const vnode = renderLinkConf.renderElem(elem, null, editor) as any\n    expect(vnode.sel).toBe('a')\n    expect(vnode.data.href).toBe(url)\n    expect(vnode.data.target).toBe(target)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/paragraph/elem-to-html.test.ts",
    "content": "import { html } from 'dom7'\n/**\n * @description paragraph - elem to html test\n * @author wangfupeng\n */\n\nimport { pToHtmlConf } from '../../src/modules/paragraph/elem-to-html'\n\ndescribe('paragraph - elem to html', () => {\n  it('paragraph to html', () => {\n    expect(pToHtmlConf.type).toBe('paragraph')\n\n    const elem = { type: 'paragraph', children: [] }\n    const html = pToHtmlConf.elemToHtml(elem, 'hello')\n    expect(html).toBe('<p>hello</p>')\n  })\n\n  it('paragraph to html with empty children', () => {\n    expect(pToHtmlConf.type).toBe('paragraph')\n\n    const elem = { type: 'paragraph', children: [] }\n    const html = pToHtmlConf.elemToHtml(elem, '')\n    expect(html).toBe('<p><br></p>')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/paragraph/parse-html.test.ts",
    "content": "/**\n * @description parse html test\n * @author wangfupeng\n */\n\nimport { $ } from 'dom7'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { parseParagraphHtmlConf } from '../../src/modules/paragraph/parse-elem-html'\n\ndescribe('paragraph - parse html', () => {\n  const editor = createEditor()\n\n  it('without children', () => {\n    const $elem = $('<p>hello&nbsp;world</p>')\n\n    // match selector\n    expect($elem[0].matches(parseParagraphHtmlConf.selector)).toBeTruthy()\n\n    // parse\n    const res = parseParagraphHtmlConf.parseElemHtml($elem[0], [], editor)\n    expect(res).toEqual({\n      type: 'paragraph',\n      children: [{ text: 'hello world' }],\n    })\n  })\n\n  it('with children', () => {\n    const $elem = $('<p></p>')\n    const children = [{ text: 'hello ' }, { text: 'world', bold: true }]\n\n    // parse\n    const res = parseParagraphHtmlConf.parseElemHtml($elem[0], children, editor)\n    expect(res).toEqual({\n      type: 'paragraph',\n      children: [{ text: 'hello ' }, { text: 'world', bold: true }],\n    })\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/paragraph/plugin.test.ts",
    "content": "/**\n * @description paragraph plugin test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms, Point } from 'slate'\nimport { DomEditor, IDomEditor } from '@wangeditor/core'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport withParagraph from '../../src/modules/paragraph/plugin'\n\nlet editor: IDomEditor\nlet startLocation: Point\ndescribe('paragraph plugin', () => {\n  beforeEach(() => {\n    editor = withParagraph(createEditor())\n    startLocation = Editor.start(editor, [])\n  })\n\n  it('delete to clear text', () => {\n    editor.select(startLocation)\n    Transforms.setNodes(editor, { type: 'header1' }) // 设置 header\n    editor.deleteBackward('character') // 向后删除\n    const selectedParagraph1 = DomEditor.getSelectedNodeByType(editor, 'paragraph')\n    expect(selectedParagraph1).not.toBeNull() // 执行删除后，header 变为 paragraph\n\n    Transforms.setNodes(editor, { type: 'blockquote' }) // 设置 blockquote\n    editor.deleteForward('character') // 向前删除\n    const selectedParagraph2 = DomEditor.getSelectedNodeByType(editor, 'paragraph')\n    expect(selectedParagraph2).not.toBeNull() // 执行删除后，header 变为 paragraph\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/paragraph/render-elem.test.ts",
    "content": "/**\n * @description paragraph render elem test\n * @author wangfupeng\n */\n\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { renderParagraphConf } from '../../src/modules/paragraph/render-elem'\n\ndescribe('paragraph - render elem', () => {\n  const editor = createEditor()\n\n  it('render paragraph', () => {\n    expect(renderParagraphConf.type).toBe('paragraph')\n\n    const elem = { type: 'paragraph', children: [] }\n    const vnode = renderParagraphConf.renderElem(elem, null, editor)\n    expect(vnode.sel).toBe('p')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/text-style/menu/clear-style-menu.test.ts",
    "content": "/**\n * @description clear style menu test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport createEditor from '../../../../../tests/utils/create-editor'\nimport ClearStyleMenu from '../../../src/modules/text-style/menu/ClearStyleMenu'\n\ndescribe('clear style menu', () => {\n  let editor = createEditor()\n  const startLocation = Editor.start(editor, [])\n  const menu = new ClearStyleMenu()\n\n  afterEach(() => {\n    editor.select(startLocation)\n    editor.clear()\n  })\n\n  it('exec', () => {\n    editor.select(startLocation)\n    editor.insertText('hello')\n\n    editor.select([])\n    editor.addMark('bold', true)\n    editor.addMark('italic', true)\n\n    menu.exec(editor, '') // 清空样式\n\n    const marks = Editor.marks(editor) as any\n    expect(marks.bold).toBeUndefined()\n    expect(marks.italic).toBeUndefined()\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/text-style/menu/menus.test.ts",
    "content": "/**\n * @description style menus test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms, Element } from 'slate'\nimport createEditor from '../../../../../tests/utils/create-editor'\nimport BoldMenu from '../../../src/modules/text-style/menu/BoldMenu'\nimport CodeMenu from '../../../src/modules/text-style/menu/CodeMenu'\nimport ItalicMenu from '../../../src/modules/text-style/menu/ItalicMenu'\nimport SubMenu from '../../../src/modules/text-style/menu/SubMenu'\nimport SupMenu from '../../../src/modules/text-style/menu/SupMenu'\nimport ThroughMenu from '../../../src/modules/text-style/menu/ThroughMenu'\nimport UnderlineMenu from '../../../src/modules/text-style/menu/UnderlineMenu'\n\nconst MENU_INFO_LIST = [\n  { mark: 'bold', menu: new BoldMenu() },\n  { mark: 'code', menu: new CodeMenu() },\n  { mark: 'italic', menu: new ItalicMenu() },\n  { mark: 'sub', menu: new SubMenu() },\n  { mark: 'sup', menu: new SupMenu() },\n  { mark: 'through', menu: new ThroughMenu() },\n  { mark: 'underline', menu: new UnderlineMenu() },\n]\n\ndescribe('text style menus', () => {\n  let editor = createEditor()\n  const startLocation = Editor.start(editor, [])\n\n  afterEach(() => {\n    editor.select(startLocation)\n    editor.clear()\n  })\n\n  // getValue 已经被 isActive 覆盖\n\n  it('is active', () => {\n    MENU_INFO_LIST.forEach(info => {\n      const { mark, menu } = info\n\n      editor.select(startLocation)\n      editor.clear()\n      editor.insertText('hello')\n      expect(menu.isActive(editor)).toBeFalsy()\n\n      editor.select([])\n      editor.addMark(mark, true)\n      expect(menu.isActive(editor)).toBeTruthy()\n    })\n  })\n\n  it('is disable', () => {\n    MENU_INFO_LIST.forEach(info => {\n      const { mark, menu } = info\n\n      editor.select(startLocation)\n      editor.clear()\n      editor.insertText('hello')\n      expect(menu.isDisabled(editor)).toBeFalsy() // 正常文字，不禁用\n\n      editor.insertNode({\n        type: 'pre',\n        children: [\n          {\n            type: 'code',\n            children: [{ text: 'var' }],\n          } as Element,\n        ],\n      } as Element)\n      expect(menu.isDisabled(editor)).toBeTruthy() // 选中代码块，禁用各个 menu\n    })\n  })\n\n  it('exec', () => {\n    MENU_INFO_LIST.forEach(info => {\n      const { mark, menu } = info\n\n      editor.select(startLocation)\n      editor.clear()\n      editor.insertText('hello')\n      editor.select([])\n\n      // 增加 mark\n      menu.exec(editor, false)\n      const marks1 = Editor.marks(editor) as any\n      expect(marks1[mark]).toBeTruthy()\n\n      // 取消 mark\n      editor.select([])\n      menu.exec(editor, true)\n      const marks2 = Editor.marks(editor) as any\n      expect(marks2[mark]).toBeUndefined()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/text-style/parse-html.test.ts",
    "content": "/**\n * @description parse html test\n * @author wangfupeng\n */\n\n// import { $ } from 'dom7'\n// import { parseStyleHtml } from '../../../../packages/basic-modules/src/modules/text-style/parse-style-html'\n\ndescribe('text style - parse style html', () => {\n  it('占位', () => {\n    expect(1 + 1).toBe(2)\n  })\n  // TODO 执行以下代码会有 Dom7 一个怪异的 bug ，先暂且注释，后面再解决 wangfupeng 2022.01.17\n\n  // it('bold', () => {\n  //   const $text = $('<b></b>')\n  //   const textNode = { text: 'hello' }\n\n  //   // parse style\n  //   const res = parseStyleHtml($text[0], textNode)\n  //   expect(res).toEqual({\n  //     text: 'hello',\n  //     bold: true,\n  //   })\n  // })\n\n  // // italic underline... 等\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/text-style/parse-style-html.test.ts",
    "content": "import { parseStyleHtml } from '../../src/modules/text-style/parse-style-html'\nimport $ from '../../src/utils/dom'\nimport createEditor from '../../../../tests/utils/create-editor'\n\ndescribe('parse style html', () => {\n  const editor = createEditor()\n\n  it('it should return directly if give node that type is not text', () => {\n    const element = $('<p></p>')\n    const node = { type: 'paragraph', children: [] }\n    expect(parseStyleHtml(element[0], node, editor)).toEqual(node)\n  })\n\n  it('it should do nothing if give not exist element', () => {\n    const element = $('#text')\n    const node = { type: 'paragraph', children: [] }\n    expect(parseStyleHtml(element[0], node, editor)).toEqual(node)\n  })\n\n  it('it should set bold property for node if give strong element', () => {\n    const element = $('<strong></strong>')\n    const node = { text: 'text' }\n    expect(parseStyleHtml(element[0], node, editor)).toEqual({ ...node, bold: true })\n  })\n\n  it('it should set bold property for node if give b element', () => {\n    const element = $('<b></b>')\n    const node = { text: 'text' }\n    expect(parseStyleHtml(element[0], node, editor)).toEqual({ ...node, bold: true })\n  })\n\n  it('it should set italic property for node if give i element', () => {\n    const element = $('<i></i>')\n    const node = { text: 'text' }\n    expect(parseStyleHtml(element[0], node, editor)).toEqual({ ...node, italic: true })\n  })\n\n  it('it should set italic property for node if give em element', () => {\n    const element = $('<em></em>')\n    const node = { text: 'text' }\n    expect(parseStyleHtml(element[0], node, editor)).toEqual({ ...node, italic: true })\n  })\n\n  it('it should set underline property for node if give u element', () => {\n    const element = $('<u></u>')\n    const node = { text: 'text' }\n    expect(parseStyleHtml(element[0], node, editor)).toEqual({ ...node, underline: true })\n  })\n\n  it('it should set through property for node if give s element', () => {\n    const element = $('<s></s>')\n    const node = { text: 'text' }\n    expect(parseStyleHtml(element[0], node, editor)).toEqual({ ...node, through: true })\n  })\n\n  it('it should set through property for node if give strike element', () => {\n    const element = $('<strike></strike>')\n    const node = { text: 'text' }\n    expect(parseStyleHtml(element[0], node, editor)).toEqual({ ...node, through: true })\n  })\n\n  it('it should set sub property for node if give sub element', () => {\n    const element = $('<sub></sub>')\n    const node = { text: 'text' }\n    expect(parseStyleHtml(element[0], node, editor)).toEqual({ ...node, sub: true })\n  })\n\n  it('it should set sup property for node if give sup element', () => {\n    const element = $('<sup></sup>')\n    const node = { text: 'text' }\n    expect(parseStyleHtml(element[0], node, editor)).toEqual({ ...node, sup: true })\n  })\n\n  it('it should set code property for node if give code element', () => {\n    const element = $('<code></code>')\n    const node = { text: 'text' }\n    expect(parseStyleHtml(element[0], node, editor)).toEqual({ ...node, code: true })\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/text-style/text-style.test.tsx",
    "content": "/**\n * @description text style test\n * @author wangfupeng\n */\n\nimport { jsx } from 'snabbdom'\nimport { renderStyle } from '../../src/modules/text-style/render-style'\nimport { StyledText } from '../../src/modules/text-style/custom-types'\n\ndescribe('text style - render text style', () => {\n  it('render text style', () => {\n    const vnode = <span>hello</span>\n    let newVnode\n\n    const textNode: StyledText = { text: '' }\n\n    textNode.bold = true\n    // @ts-ignore 忽略 vnode 格式\n    newVnode = renderStyle(textNode, vnode)\n    expect(newVnode.sel).toBe('strong')\n\n    textNode.code = true\n    // @ts-ignore 忽略 vnode 格式\n    newVnode = renderStyle(textNode, vnode)\n    expect(newVnode.sel).toBe('code')\n\n    textNode.italic = true\n    // @ts-ignore 忽略 vnode 格式\n    newVnode = renderStyle(textNode, vnode)\n    expect(newVnode.sel).toBe('em')\n\n    textNode.underline = true\n    // @ts-ignore 忽略 vnode 格式\n    newVnode = renderStyle(textNode, vnode)\n    expect(newVnode.sel).toBe('u')\n\n    textNode.through = true\n    // @ts-ignore 忽略 vnode 格式\n    newVnode = renderStyle(textNode, vnode)\n    expect(newVnode.sel).toBe('s')\n\n    textNode.sub = true\n    // @ts-ignore 忽略 vnode 格式\n    newVnode = renderStyle(textNode, vnode)\n    expect(newVnode.sel).toBe('sub')\n\n    textNode.sup = true\n    // @ts-ignore 忽略 vnode 格式\n    newVnode = renderStyle(textNode, vnode)\n    expect(newVnode.sel).toBe('sup')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/text-style/text-to-html.test.ts",
    "content": "/**\n * @description text to html test\n * @author wangfupeng\n */\n\nimport { styleToHtml } from '../../src/modules/text-style/style-to-html'\n\ndescribe('text style - text to html', () => {\n  it('text to html', () => {\n    const textNode = {\n      text: '',\n      bold: true,\n      italic: true,\n      underline: true,\n      code: true,\n      through: true,\n      sub: true,\n      sup: true,\n    }\n\n    const html1 = styleToHtml(textNode, 'hello')\n    expect(html1).toBe(\n      '<sup><sub><s><u><em><code><strong>hello</strong></code></em></u></s></sub></sup>'\n    )\n\n    const html2 = styleToHtml(textNode, '<span>world</span>')\n    expect(html2).toBe(\n      '<span><sup><sub><s><u><em><code><strong>world</strong></code></em></u></s></sub></sup></span>'\n    )\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/todo/elem-to-html.test.ts",
    "content": "/**\n * @description todo elem to html test\n * @author wangfupeng\n */\n\nimport { todoToHtmlConf } from '../../src/modules/todo/elem-to-html'\n\ndescribe('todo - elem to html', () => {\n  it('todo elem to html', () => {\n    expect(todoToHtmlConf.type).toBe('todo')\n\n    const todoNode1 = {\n      type: 'todo',\n      checked: true,\n      children: [{ text: '' }],\n    }\n    const html1 = todoToHtmlConf.elemToHtml(todoNode1, 'hello')\n    expect(html1).toBe(\n      `<div data-w-e-type=\"todo\"><input type=\"checkbox\" disabled checked>hello</div>`\n    )\n\n    const todoNode2 = {\n      type: 'todo',\n      checked: false,\n      children: [{ text: '' }],\n    }\n    const html2 = todoToHtmlConf.elemToHtml(todoNode2, 'hello')\n    expect(html2).toBe(`<div data-w-e-type=\"todo\"><input type=\"checkbox\" disabled >hello</div>`)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/todo/menu/todo-menu.test.ts",
    "content": "/**\n * @description todo-menu test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport createEditor from '../../../../../tests/utils/create-editor'\nimport TodoMenu from '../../../src/modules/todo/menu/Todo'\n\ndescribe('todo-menu', () => {\n  let editor: any\n  let startLocation: any\n  const menu = new TodoMenu()\n\n  beforeEach(() => {\n    editor = createEditor()\n    startLocation = Editor.start(editor, [])\n  })\n\n  afterEach(() => {\n    editor = null\n    startLocation = null\n  })\n\n  it('get value', () => {\n    expect(menu.getValue(editor)).toBe('')\n  })\n\n  it('is active', () => {\n    editor.select(startLocation)\n    expect(menu.isActive(editor)).toBeFalsy()\n\n    // @ts-ignore\n    Transforms.setNodes(editor, { type: 'todo' })\n    expect(menu.isActive(editor)).toBeTruthy()\n  })\n\n  it('is disable - paragraph and todo', () => {\n    editor.select(startLocation)\n    expect(menu.isDisabled(editor)).toBeFalsy()\n\n    // @ts-ignore\n    Transforms.setNodes(editor, { type: 'todo' })\n    expect(menu.isDisabled(editor)).toBeFalsy()\n  })\n\n  it('is disable - list', () => {\n    editor.select(startLocation)\n    editor.insertNode({\n      type: 'bulleted-list',\n      children: [\n        {\n          type: 'list-item',\n          children: [{ text: 'hello' }],\n        },\n      ],\n    })\n    expect(menu.isDisabled(editor)).toBeTruthy()\n  })\n\n  it('is disable - table', () => {\n    editor.select(startLocation)\n    editor.insertNode({\n      type: 'table',\n      children: [\n        {\n          type: 'table-row',\n          children: [\n            {\n              type: 'table-cell',\n              children: [{ text: 'hello' }],\n            },\n          ],\n        },\n      ],\n    })\n    expect(menu.isDisabled(editor)).toBeTruthy()\n  })\n\n  it('is disable - pre/code', () => {\n    editor.select(startLocation)\n    editor.insertNode({\n      type: 'pre',\n      children: [\n        {\n          type: 'code',\n          children: [{ text: 'hello' }],\n        },\n      ],\n    })\n    expect(menu.isDisabled(editor)).toBeTruthy()\n  })\n\n  it('exec - paragraph to todo', () => {\n    editor.select(startLocation)\n    menu.exec(editor, '')\n\n    const todoElems = editor.getElemsByType('todo')\n    expect(todoElems.length).toBe(1)\n  })\n\n  it('exec - todo to paragraph', () => {\n    editor.select(startLocation)\n    // @ts-ignore\n    Transforms.setNodes(editor, { type: 'todo' })\n    menu.exec(editor, '')\n\n    const todoElems = editor.getElemsByType('todo')\n    expect(todoElems.length).toBe(0)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/todo/parse-html.test.ts",
    "content": "/**\n * @description todo parse html test\n * @author wangfupeng\n */\n\nimport { $ } from 'dom7'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { parseHtmlConf } from '../../src/modules/todo/parse-elem-html'\n\ndescribe('todo - parse html', () => {\n  const editor = createEditor()\n\n  it('with children, checked', () => {\n    const $todo = $('<div data-w-e-type=\"todo\"><input type=\"checkbox\" disabled checked>hello</div>')\n\n    // match selector\n    expect($todo[0].matches(parseHtmlConf.selector)).toBeTruthy()\n\n    // parse\n    const res = parseHtmlConf.parseElemHtml($todo[0], [], editor)\n    expect(res).toEqual({\n      type: 'todo',\n      checked: true,\n      children: [{ text: 'hello' }],\n    })\n  })\n\n  it('without children, unchecked', () => {\n    const $todo = $('<div data-w-e-type=\"todo\"><input type=\"checkbox\" disabled></div>')\n    const children = [{ text: 'hello ' }, { text: 'world', bold: true }]\n\n    // match selector\n    expect($todo[0].matches(parseHtmlConf.selector)).toBeTruthy()\n\n    // parse\n    const res = parseHtmlConf.parseElemHtml($todo[0], children, editor)\n    expect(res).toEqual({\n      type: 'todo',\n      checked: false,\n      children: [{ text: 'hello ' }, { text: 'world', bold: true }],\n    })\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/todo/plugin.test.ts",
    "content": "/**\n * @description todo plugin test\n * @author wangfupeng\n */\n\nimport withTodo from '../../src/modules/todo/plugin'\nimport createEditor from '../../../../tests/utils/create-editor'\n\ndescribe('todo - plugin', () => {\n  it('delete backward', () => {\n    const editor = withTodo(\n      createEditor({\n        content: [{ type: 'todo', children: [{ text: '' }] }],\n      })\n    )\n    editor.select({\n      path: [0, 0],\n      offset: 0,\n    })\n\n    const todoElems1 = editor.getElemsByType('todo')\n    expect(todoElems1.length).toBe(1)\n\n    editor.deleteBackward('character')\n\n    const todoElems2 = editor.getElemsByType('todo')\n    expect(todoElems2.length).toBe(0)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/todo/pre-parse-html.test.ts",
    "content": "/**\n * @description todo pre-parse html\n * @author wangfupeng\n */\n\nimport { $ } from 'dom7'\nimport { preParseHtmlConf } from '../../src/modules/todo/pre-parse-html'\n\ndescribe('todo - pre-parse html', () => {\n  it('pre-parse html', () => {\n    // v4 todo html 格式\n    const $ul = $(\n      '<ul class=\"w-e-todo\"><li><span contenteditable=\"false\"><input type=\"checkbox\"/></span>hello <b>world</b></li></ul>'\n    )\n\n    // match selector\n    expect($ul[0].matches(preParseHtmlConf.selector)).toBeTruthy()\n\n    // parse\n    const res = preParseHtmlConf.preParseHtml($ul[0])\n    expect(res.outerHTML).toBe(\n      '<div data-w-e-type=\"todo\"><input type=\"checkbox\">hello <b>world</b></div>'\n    )\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/todo/render-elem.test.ts",
    "content": "/**\n * @description todo render elem\n * @author wangfupeng\n */\n\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { renderTodoConf } from '../../src/modules/todo/render-elem'\n\ndescribe('todo - render elem', () => {\n  const editor = createEditor()\n\n  it('render elem', () => {\n    expect(renderTodoConf.type).toBe('todo')\n\n    const todo = { type: 'todo', checked: true, children: [{ text: '' }] }\n    const vnode = renderTodoConf.renderElem(todo, null, editor) as any\n    expect(vnode.sel).toBe('div')\n    expect(vnode.children.length).toBe(2)\n\n    const spanForInput = vnode.children[0]\n    expect(spanForInput.sel).toBe('span')\n    expect(spanForInput.data.contentEditable).toBe(false)\n\n    const input = spanForInput.children[0]\n    expect(input.sel).toBe('input')\n    expect(input.data.type).toBe('checkbox')\n    expect(input.data.checked).toBe(true)\n    expect(typeof input.data.on.change).toBe('function')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/undo-redo/redo-menu.test.ts",
    "content": "/**\n * @description redo menu test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport RedoMenu from '../../src/modules/undo-redo/menu/RedoMenu'\n\ndescribe('redo menu', () => {\n  const editor = createEditor()\n  const menu = new RedoMenu()\n  const location = Editor.start(editor, []) // 选区位置\n\n  it('tag', () => {\n    expect(menu.tag).toBe('button')\n  })\n\n  it('get value', () => {\n    expect(menu.getValue(editor)).toBe('')\n  })\n\n  it('is active', () => {\n    expect(menu.isActive(editor)).toBeFalsy()\n  })\n\n  it('is disable', () => {\n    // 有选区\n    editor.select(location)\n    expect(menu.isDisabled(editor)).toBeFalsy()\n\n    // 无选区\n    editor.deselect()\n    expect(menu.isDisabled(editor)).toBeTruthy()\n  })\n\n  it('exec', () => {\n    const text = editor.getText()\n\n    editor.select(location)\n    editor.insertText('xxx')\n    if (typeof editor.undo === 'function') {\n      editor.undo()\n    }\n    menu.exec(editor, '')\n\n    const newText = editor.getText()\n    expect(newText).toBe(text + 'xxx')\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/__tests__/undo-redo/undo-menu.test.ts",
    "content": "/**\n * @description undo menu test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport UndoMenu from '../../src/modules/undo-redo/menu/UndoMenu'\n\ndescribe('undo menu', () => {\n  const editor = createEditor()\n  const menu = new UndoMenu()\n  const location = Editor.start(editor, []) // 选区位置\n\n  it('tag', () => {\n    expect(menu.tag).toBe('button')\n  })\n\n  it('get value', () => {\n    expect(menu.getValue(editor)).toBe('')\n  })\n\n  it('is active', () => {\n    expect(menu.isActive(editor)).toBeFalsy()\n  })\n\n  it('is disable', () => {\n    // 有选区\n    editor.select(location)\n    expect(menu.isDisabled(editor)).toBeFalsy()\n\n    // 无选区\n    editor.deselect()\n    expect(menu.isDisabled(editor)).toBeTruthy()\n  })\n\n  it('exec', () => {\n    const text = editor.getText()\n\n    editor.select(location)\n    editor.insertText('xxx')\n    menu.exec(editor, '')\n\n    const newText = editor.getText()\n    expect(newText).toBe(text)\n  })\n})\n"
  },
  {
    "path": "packages/basic-modules/package.json",
    "content": "{\n  \"name\": \"@wangeditor/basic-modules\",\n  \"version\": \"1.1.7\",\n  \"description\": \"wangEditor basic modules\",\n  \"author\": \"wangfupeng1988 <wangfupeng1988@163.com>\",\n  \"contributors\": [],\n  \"homepage\": \"https://github.com/wangeditor-team/wangEditor#readme\",\n  \"license\": \"MIT\",\n  \"types\": \"dist/basic-modules/src/index.d.ts\",\n  \"main\": \"dist/index.js\",\n  \"module\": \"dist/index.esm.js\",\n  \"browser\": {\n    \"./dist/index.js\": \"./dist/index.js\",\n    \"./dist/index.esm.js\": \"./dist/index.esm.js\"\n  },\n  \"directories\": {\n    \"lib\": \"dist\",\n    \"test\": \"__tests__\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.com/\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wangeditor-team/wangEditor.git\"\n  },\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"test-c\": \"jest --coverage\",\n    \"dev\": \"cross-env NODE_ENV=development rollup -c rollup.config.js\",\n    \"dev-watch\": \"cross-env NODE_ENV=development rollup -c rollup.config.js -w\",\n    \"build\": \"cross-env NODE_ENV=production rollup -c rollup.config.js\",\n    \"dev-size-stats\": \"cross-env NODE_ENV=development:size_stats rollup -c rollup.config.js\",\n    \"size-stats\": \"cross-env NODE_ENV=production:size_stats rollup -c rollup.config.js\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/wangeditor-team/wangEditor/issues\"\n  },\n  \"peerDependencies\": {\n    \"@wangeditor/core\": \"1.x\",\n    \"dom7\": \"^3.0.0\",\n    \"lodash.throttle\": \"^4.1.1\",\n    \"nanoid\": \"^3.2.0\",\n    \"slate\": \"^0.72.0\",\n    \"snabbdom\": \"^3.1.0\"\n  },\n  \"dependencies\": {\n    \"is-url\": \"^1.2.4\"\n  },\n  \"devDependencies\": {\n    \"@types/is-url\": \"^1.2.29\"\n  }\n}\n"
  },
  {
    "path": "packages/basic-modules/rollup.config.js",
    "content": "import { createRollupConfig, IS_PRD } from '../../build/create-rollup-config'\nimport pkg from './package.json'\n\nconst name = 'WangEditorBasicModules'\n\nconst configList = []\n\n// esm\nconst esmConf = createRollupConfig({\n  output: {\n    file: pkg.module,\n    format: 'esm',\n    name,\n  },\n})\nconfigList.push(esmConf)\n\n// umd\nconst umdConf = createRollupConfig({\n  output: {\n    file: pkg.main,\n    format: 'umd',\n    name,\n  },\n})\nconfigList.push(umdConf)\n\nexport default configList\n"
  },
  {
    "path": "packages/basic-modules/src/assets/blockquote.less",
    "content": "@import \"../../../vars.less\";\n\n.w-e-text-container [data-slate-editor] blockquote {\n  display: block;\n  border-left: 8px solid @textarea-selected-border-color;\n  padding: 10px 10px;\n  margin: 10px 0;\n  line-height: 1.5;\n  font-size: 100%;\n  background-color: @textarea-slight-bg-color;\n}\n"
  },
  {
    "path": "packages/basic-modules/src/assets/code-block.less",
    "content": "@import \"../../../vars.less\";\n\n.w-e-text-container [data-slate-editor] pre>code {\n  display: block;\n  border: 1px solid @textarea-slight-border-color;\n  border-radius: 4px 4px;\n  text-indent: 0;\n  background-color: @textarea-slight-bg-color;\n  padding: 10px;\n  font-size: @size;\n}\n"
  },
  {
    "path": "packages/basic-modules/src/assets/color.less",
    "content": "@import \"../../../vars.less\";\n\n.w-e-panel-content-color {\n  list-style: none;\n  text-align: left;\n  width: 230px;\n\n  li {\n    display: inline-block;\n    padding: 2px;\n    cursor: pointer;\n    border-radius: 3px 3px;\n    border: 1px solid @toolbar-bg-color;\n\n    &:hover {\n      border-color: @toolbar-color;\n    }\n\n    .color-block {\n      width: 17px;\n      height: 17px;\n      border: 1px solid @toolbar-border-color;\n      border-radius: 3px 3px;\n    }\n  }\n\n  .active {\n    border-color: @toolbar-color;\n  }\n\n  .clear {\n    width: 100%;\n    line-height: 1.5;\n    margin-bottom: 5px;\n\n    svg {\n      width: 16px;\n      height: 16px;\n      margin-bottom: -4px;\n    }\n  }\n}"
  },
  {
    "path": "packages/basic-modules/src/assets/divider.less",
    "content": "@import \"../../../vars.less\";\n\n.w-e-textarea-divider {\n  padding: 20px 20px;\n  margin: 20px auto;\n  border-radius: 3px;\n\n  // &:hover {\n  //   background-color: @textarea-slight-bg-color;\n  // }\n\n  hr {\n    display: block;\n    border: 0;\n    height: 1px;\n    background-color: @textarea-border-color;\n  }\n}\n"
  },
  {
    "path": "packages/basic-modules/src/assets/emotion.less",
    "content": "@import \"../../../vars.less\";\n\n.w-e-panel-content-emotion {\n  list-style: none;\n  text-align: left;\n  width: 300px;\n  font-size: 20px;\n\n  li {\n    display: inline-block;\n    padding: 0 5px;\n    cursor: pointer;\n    border-radius: 3px 3px;\n\n    &:hover {\n      background-color: @textarea-slight-bg-color;\n    }\n  }\n}"
  },
  {
    "path": "packages/basic-modules/src/assets/image.less",
    "content": "@import \"../../../vars.less\";\n\n// 拖拽，修改图片尺寸\n.w-e-text-container [data-slate-editor] {\n  .w-e-image-container {\n    display: inline-block;\n    margin: 0 3px; // 从 10px 改为 3px ，可规避 issue 4523\n\n    &:hover {\n      box-shadow: 0 0 0 2px @textarea-selected-border-color;\n    }\n  }\n  .w-e-selected-image-container {\n    position: relative;\n    overflow: hidden;\n\n    .w-e-image-dragger {\n      width: 7px;\n      height: 7px;\n      background-color: @textarea-handler-bg-color;\n      position: absolute;\n    }\n    .left-top {\n      top: 0;\n      left: 0;\n      cursor: nwse-resize;\n    }\n    .right-top {\n      top: 0;\n      right: 0;\n      cursor: nesw-resize;\n    }\n    .left-bottom {\n      left: 0;\n      bottom: 0;\n      cursor: nesw-resize;\n    }\n    .right-bottom {\n      right: 0;\n      bottom: 0;\n      cursor: nwse-resize;\n    }\n\n    // 选中之后，不需要 hover 效果\n    &:hover {\n      box-shadow: none;\n    }\n  }\n}\n\n// 禁用编辑器时，img hover 不要有样式\n.w-e-text-container [contenteditable=\"false\"] {\n  .w-e-image-container {\n    &:hover {\n      box-shadow: none;\n    }\n  }\n}"
  },
  {
    "path": "packages/basic-modules/src/assets/index.less",
    "content": "@import \"simple-style.less\";\n@import \"color.less\";\n@import \"blockquote.less\";\n@import \"emotion.less\";\n@import \"divider.less\";\n@import \"blockquote.less\";\n@import \"code-block.less\";\n@import \"image.less\";\n"
  },
  {
    "path": "packages/basic-modules/src/assets/simple-style.less",
    "content": "@import \"../../../vars.less\";\n\n.w-e-text-container [data-slate-editor] code {\n  font-family: monospace;\n  background-color: @textarea-slight-bg-color;\n  padding: 3px;\n  border-radius: 3px;\n}\n"
  },
  {
    "path": "packages/basic-modules/src/constants/icon-svg.ts",
    "content": "/**\n * @description icon svg\n * @author wangfupeng\n */\n\n/**\n * 【注意】svg 字符串的长度 ，否则会导致代码体积过大\n * 尽量选择 https://www.iconfont.cn/collections/detail?spm=a313x.7781069.0.da5a778a4&cid=20293\n * 找不到再从 iconfont.com 搜索\n */\n\n// 加粗\nexport const BOLD_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M707.872 484.64A254.88 254.88 0 0 0 768 320c0-141.152-114.848-256-256-256H192v896h384c141.152 0 256-114.848 256-256a256.096 256.096 0 0 0-124.128-219.36zM384 192h101.504c55.968 0 101.504 57.408 101.504 128s-45.536 128-101.504 128H384V192z m159.008 640H384v-256h159.008c58.464 0 106.016 57.408 106.016 128s-47.552 128-106.016 128z\"></path></svg>'\n\n// 下划线\nexport const UNDER_LINE_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M704 64l128 0 0 416c0 159.072-143.264 288-320 288s-320-128.928-320-288l0-416 128 0 0 416c0 40.16 18.24 78.688 51.36 108.512 36.896 33.216 86.848 51.488 140.64 51.488s103.744-18.304 140.64-51.488c33.12-29.792 51.36-68.352 51.36-108.512l0-416zM192 832l640 0 0 128-640 0z\"></path></svg>'\n\n// 斜体\nexport const ITALIC_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M896 64v64h-128L448 896h128v64H128v-64h128L576 128h-128V64z\"></path></svg>'\n\n// 删除线\nexport const THROUGH_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M1024 512v64h-234.496c27.52 38.496 42.496 82.688 42.496 128 0 70.88-36.672 139.04-100.576 186.976C672.064 935.488 594.144 960 512 960s-160.064-24.512-219.424-69.024C228.64 843.04 192 774.88 192 704h128c0 69.376 87.936 128 192 128s192-58.624 192-128-87.936-128-192-128H0v-64h299.52a385.984 385.984 0 0 1-6.944-5.024C228.64 459.04 192 390.88 192 320s36.672-139.04 100.576-186.976C351.936 88.512 429.856 64 512 64s160.064 24.512 219.424 69.024C795.328 180.96 832 249.12 832 320h-128c0-69.376-87.936-128-192-128s-192 58.624-192 128 87.936 128 192 128c78.976 0 154.048 22.688 212.48 64H1024z\"></path></svg>'\n\n// 代码\nexport const CODE_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M576 736l96 96 320-320L672 192l-96 96 224 224zM448 288l-96-96L32 512l320 320 96-96-224-224z\"></path></svg>'\n\n// 清除格式\nexport const ERASER_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M969.382408 288.738615l-319.401123-270.852152a67.074236 67.074236 0 0 0-96.459139 5.74922l-505.931379 574.922021a68.35184 68.35184 0 0 0-17.886463 47.910169 74.101061 74.101061 0 0 0 24.274486 47.910168l156.50655 132.232065h373.060512L975.131628 383.281347a67.074236 67.074236 0 0 0-5.74922-96.459139z m-440.134747 433.746725H264.144729l-90.071117-78.572676c-5.74922-5.74922-12.137243-12.137243-12.137243-17.886463a36.411728 36.411728 0 0 1 5.749221-24.274485l210.804741-240.828447 265.102932 228.691204z m-439.495945 180.781036h843.218964a60.047411 60.047411 0 1 1 0 120.733624H89.751716a60.047411 60.047411 0 1 1 0-120.733624z m0 0\"></path></svg>'\n\n// 链接\nexport const LINK_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M440.224 635.776a51.84 51.84 0 0 1-36.768-15.232c-95.136-95.136-95.136-249.92 0-345.056l192-192C641.536 37.408 702.816 12.032 768 12.032s126.432 25.376 172.544 71.456c95.136 95.136 95.136 249.92 0 345.056l-87.776 87.776a51.968 51.968 0 1 1-73.536-73.536l87.776-87.776a140.16 140.16 0 0 0 0-197.984c-26.432-26.432-61.6-40.992-99.008-40.992s-72.544 14.56-99.008 40.992l-192 192a140.16 140.16 0 0 0 0 197.984 51.968 51.968 0 0 1-36.768 88.768z\"></path><path d=\"M256 1012a242.4 242.4 0 0 1-172.544-71.456c-95.136-95.136-95.136-249.92 0-345.056l87.776-87.776a51.968 51.968 0 1 1 73.536 73.536l-87.776 87.776a140.16 140.16 0 0 0 0 197.984c26.432 26.432 61.6 40.992 99.008 40.992s72.544-14.56 99.008-40.992l192-192a140.16 140.16 0 0 0 0-197.984 51.968 51.968 0 1 1 73.536-73.536c95.136 95.136 95.136 249.92 0 345.056l-192 192A242.4 242.4 0 0 1 256 1012z\"></path></svg>'\n\n// 取消链接\nexport const UN_LINK_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M608.16328 811.815036c9.371954 9.371954 9.371954 24.56788 0 33.941834l-89.347563 89.347564c-118.525421 118.523421-311.38448 118.531421-429.919901 0-118.527421-118.529421-118.527421-311.39048 0-429.917901l89.349564-89.349563c9.371954-9.371954 24.56788-9.371954 33.941834 0l79.195613 79.195613c9.371954 9.371954 9.371954 24.56788 0 33.941834l-89.349563 89.347564c-56.143726 56.145726-56.143726 147.49928 0 203.645005 56.143726 56.143726 147.49928 56.145726 203.647005 0l89.347564-89.347563c9.371954-9.371954 24.56788-9.371954 33.941834 0l79.193613 79.195613z m-113.135447-520.429459c9.371954 9.371954 24.56788 9.371954 33.941834 0l89.347564-89.347564c56.143726-56.149726 147.49928-56.145726 203.647006 0 56.143726 56.145726 56.143726 147.49928 0 203.645006l-89.349564 89.347564c-9.371954 9.371954-9.371954 24.56788 0 33.941834l79.195613 79.195613c9.371954 9.371954 24.56788 9.371954 33.941834 0l89.349564-89.349563c118.529421-118.529421 118.529421-311.38848 0-429.917901-118.531421-118.527421-311.38848-118.527421-429.919901 0l-89.347563 89.347564c-9.371954 9.371954-9.371954 24.56788 0 33.941834l79.193613 79.195613z m469.653707 718.556492l45.253779-45.253779c18.745908-18.745908 18.745908-49.13776 0-67.881669L127.195629 14.062931c-18.745908-18.745908-49.13776-18.745908-67.881669 0L14.058181 59.31871c-18.745908 18.745908-18.745908 49.13776 0 67.881669l882.74169 882.74169c18.745908 18.743908 49.13776 18.743908 67.881669 0z\"></path></svg>'\n\n// 编辑\nexport const PENCIL_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M864 0a160 160 0 0 1 128 256l-64 64-224-224 64-64c26.752-20.096 59.968-32 96-32zM64 736l-64 288 288-64 592-592-224-224L64 736z m651.584-372.416l-448 448-55.168-55.168 448-448 55.168 55.168z\"></path></svg>'\n\n// 外部（链接）\nexport const EXTERNAL_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M924.402464 1023.068211H0.679665V99.345412h461.861399v98.909208H99.596867v725.896389h725.896389V561.206811h98.909208z\" p-id=\"10909\"></path><path d=\"M930.805104 22.977336l69.965436 69.965436-453.492405 453.492404-69.965435-69.901489z\" p-id=\"10910\"></path><path d=\"M1022.464381 304.030081h-98.917201V99.345412H709.230573V0.428211h313.233808z\"></path></svg>'\n\n// 标题\nexport const HEADER_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M960 960c-51.2 0-102.4-3.2-153.6-3.2-51.2 0-99.2 3.2-150.4 3.2-19.2 0-28.8-22.4-28.8-38.4 0-51.2 57.6-28.8 86.4-48 19.2-12.8 19.2-60.8 19.2-80v-224-19.2c-9.6-3.2-19.2-3.2-28.8-3.2H320c-9.6 0-19.2 0-28.8 3.2V780.8c0 22.4 0 80 22.4 92.8 28.8 19.2 96-6.4 96 44.8 0 16-9.6 41.6-28.8 41.6-54.4 0-105.6-3.2-160-3.2-48 0-96 3.2-147.2 3.2-19.2 0-28.8-22.4-28.8-38.4 0-51.2 51.2-28.8 80-48 19.2-12.8 19.2-60.8 19.2-83.2V294.4c0-28.8 3.2-115.2-22.4-131.2-25.6-16-86.4 9.6-86.4-41.6 0-16 6.4-41.6 28.8-41.6 51.2 0 105.6 3.2 156.8 3.2 48 0 96-3.2 144-3.2 19.2 0 28.8 22.4 28.8 41.6 0 48-57.6 25.6-83.2 41.6-19.2 12.8-19.2 73.6-19.2 92.8v201.6c6.4 3.2 16 3.2 22.4 3.2h400c6.4 0 12.8 0 22.4-3.2V256c0-22.4 0-80-19.2-92.8-28.8-16-86.4 6.4-86.4-41.6 0-16 9.6-41.6 28.8-41.6 51.2 0 99.2 3.2 150.4 3.2 48 0 99.2-3.2 147.2-3.2 19.2 0 28.8 22.4 28.8 41.6 0 51.2-57.6 25.6-86.4 41.6-19.2 12.8-19.2 70.4-19.2 92.8v537.6c0 19.2 0 67.2 19.2 80 28.8 19.2 89.6-6.4 89.6 44.8 0 19.2-6.4 41.6-28.8 41.6z\"></path></svg>'\n\n// 字体颜色\nexport const FONT_COLOR_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M64 864h896v96H64zM360.58 576h302.85l81.53 224h102.16L579.24 64H444.77L176.89 800h102.16l81.53-224zM512 159.96L628.49 480H395.52L512 159.96z\"></path></svg>'\n\n// 背景颜色\nexport const BG_COLOR_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M510.030769 315.076923l84.676923 196.923077h-177.230769l76.8-196.923077h15.753846zM945.230769 157.538462v708.923076c0 43.323077-35.446154 78.769231-78.769231 78.769231H157.538462c-43.323077 0-78.769231-35.446154-78.769231-78.769231V157.538462c0-43.323077 35.446154-78.769231 78.769231-78.769231h708.923076c43.323077 0 78.769231 35.446154 78.769231 78.769231z m-108.307692 643.938461L600.615385 216.615385c-5.907692-11.815385-15.753846-19.692308-29.538462-19.692308h-139.815385c-11.815385 0-23.630769 7.876923-27.56923 19.692308l-216.615385 584.861538c-3.938462 11.815385 3.938462 25.6 17.723077 25.6h80.738462c11.815385 0 23.630769-9.846154 27.56923-21.661538l63.015385-175.261539h263.876923l68.923077 175.261539c3.938462 11.815385 15.753846 21.661538 27.569231 21.661538h80.738461c13.784615 0 23.630769-13.784615 19.692308-25.6z\"></path></svg>'\n\n// 清空（颜色）\nexport const CLEAN_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M236.8 128L896 787.2V128H236.8z m614.4 704L192 172.8V832h659.2zM192 64h704c38.4 0 64 25.6 64 64v704c0 38.4-25.6 64-64 64H192c-38.4 0-64-25.6-64-64V128c0-38.4 25.6-64 64-64z\"></path></svg>'\n\n// 图片\nexport const IMAGE_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M959.877 128l0.123 0.123v767.775l-0.123 0.122H64.102l-0.122-0.122V128.123l0.122-0.123h895.775zM960 64H64C28.795 64 0 92.795 0 128v768c0 35.205 28.795 64 64 64h896c35.205 0 64-28.795 64-64V128c0-35.205-28.795-64-64-64zM832 288.01c0 53.023-42.988 96.01-96.01 96.01s-96.01-42.987-96.01-96.01S682.967 192 735.99 192 832 234.988 832 288.01zM896 832H128V704l224.01-384 256 320h64l224.01-192z\"></path></svg>'\n\n// 垃圾桶（删除）\nexport const TRASH_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M826.8032 356.5312c-19.328 0-36.3776 15.6928-36.3776 35.0464v524.2624c0 19.328-16 34.56-35.328 34.56H264.9344c-19.328 0-35.5072-15.3088-35.5072-34.56V390.0416c0-19.328-14.1568-35.0464-33.5104-35.0464s-33.5104 15.6928-33.5104 35.0464V915.712c0 57.9328 44.6208 108.288 102.528 108.288H755.2c57.9328 0 108.0832-50.4576 108.0832-108.288V391.4752c-0.1024-19.2512-17.1264-34.944-36.48-34.944z\" p-id=\"9577\"></path><path d=\"M437.1712 775.7568V390.6048c0-19.328-14.1568-35.0464-33.5104-35.0464s-33.5104 15.616-33.5104 35.0464v385.152c0 19.328 14.1568 35.0464 33.5104 35.0464s33.5104-15.7184 33.5104-35.0464zM649.7024 775.7568V390.6048c0-19.328-17.0496-35.0464-36.3776-35.0464s-36.3776 15.616-36.3776 35.0464v385.152c0 19.328 17.0496 35.0464 36.3776 35.0464s36.3776-15.7184 36.3776-35.0464zM965.0432 217.0368h-174.6176V145.5104c0-57.9328-47.2064-101.76-104.6528-101.76h-350.976c-57.8304 0-105.3952 43.8528-105.3952 101.76v71.5264H54.784c-19.4304 0-35.0464 14.1568-35.0464 33.5104 0 19.328 15.616 33.5104 35.0464 33.5104h910.3616c19.328 0 35.0464-14.1568 35.0464-33.5104 0-19.3536-15.6928-33.5104-35.1488-33.5104z m-247.3728 0H297.3952V145.5104c0-19.328 18.2016-34.7648 37.4272-34.7648h350.976c19.1488 0 31.872 15.1296 31.872 34.7648v71.5264z\"></path></svg>'\n\n// 引用\nexport const QUOTE_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M894.6 907.1H605.4c-32.6 0-59-26.4-59-59V608.2l-4-14.9c0-315.9 125.5-485.1 376.5-507.5v59.8C752.7 180.4 711.3 315.8 711.3 442.4v41.2l31.5 12.3h151.8c32.6 0 59 26.4 59 59v293.2c0 32.5-26.4 59-59 59z m-472 0H133.4c-32.6 0-59-26.4-59-59V608.2l-4-14.9c0-315.9 125.5-485.1 376.5-507.5v59.8C280.7 180.4 239.3 315.8 239.3 442.4v41.2l31.5 12.3h151.8c32.6 0 59 26.4 59 59v293.2c0 32.5-26.4 59-59 59z\"></path></svg>'\n\n// 表情\nexport const EMOTION_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M512 1024C230.4 1024 0 793.6 0 512S230.4 0 512 0s512 230.4 512 512-230.4 512-512 512z m0-102.4c226.742857 0 409.6-182.857143 409.6-409.6S738.742857 102.4 512 102.4 102.4 285.257143 102.4 512s182.857143 409.6 409.6 409.6z m-204.8-358.4h409.6c0 113.371429-91.428571 204.8-204.8 204.8s-204.8-91.428571-204.8-204.8z m0-102.4c-43.885714 0-76.8-32.914286-76.8-76.8s32.914286-76.8 76.8-76.8 76.8 32.914286 76.8 76.8-32.914286 76.8-76.8 76.8z m409.6 0c-43.885714 0-76.8-32.914286-76.8-76.8s32.914286-76.8 76.8-76.8c43.885714 0 76.8 32.914286 76.8 76.8s-32.914286 76.8-76.8 76.8z\"></path></svg>'\n\n// fontSize\nexport const FONT_SIZE_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M64 512h384v128h-128V1024h-128V640h-128z m896-256H708.2496v768h-136.4992V256H320V128h640z\"></path></svg>'\n\n// 字体\nexport const FONT_FAMILY_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M956.788364 152.110545h-24.110546l23.924364 9.029819 0.186182 121.018181h-65.070546l-86.574545-130.048H566.551273v650.14691l130.048 64.977454v65.163636h-390.050909v-65.163636l129.954909-64.977454V152.110545H198.283636L111.429818 282.065455H46.545455V69.259636C46.545455 33.792 82.664727 22.062545 98.955636 22.062545h812.683637c23.738182 0 45.056 15.173818 45.056 41.053091V169.425455v-17.221819z\"></path></svg>'\n\n// 缩进 left\nexport const INDENT_LEFT_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M0 64h1024v128H0z m384 192h640v128H384z m0 192h640v128H384z m0 192h640v128H384zM0 832h1024v128H0z m256-512v384l-256-192z\"></path></svg>'\n\n// 缩进 right\nexport const INDENT_RIGHT_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M0 64h1024v128H0z m384 192h640v128H384z m0 192h640v128H384z m0 192h640v128H384zM0 832h1024v128H0z m0-128V320l256 192z\"></path></svg>'\n\n// 左对齐\nexport const JUSTIFY_LEFT_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M768 793.6v102.4H51.2v-102.4h716.8z m204.8-230.4v102.4H51.2v-102.4h921.6z m-204.8-230.4v102.4H51.2v-102.4h716.8zM972.8 102.4v102.4H51.2V102.4h921.6z\"></path></svg>'\n\n// 右对齐\nexport const JUSTIFY_RIGHT_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M972.8 793.6v102.4H256v-102.4h716.8z m0-230.4v102.4H51.2v-102.4h921.6z m0-230.4v102.4H256v-102.4h716.8zM972.8 102.4v102.4H51.2V102.4h921.6z\"></path></svg>'\n\n// 居中对齐\nexport const JUSTIFY_CENTER_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M870.4 793.6v102.4H153.6v-102.4h716.8z m102.4-230.4v102.4H51.2v-102.4h921.6z m-102.4-230.4v102.4H153.6v-102.4h716.8zM972.8 102.4v102.4H51.2V102.4h921.6z\"></path></svg>'\n\n// 两端对齐\nexport const JUSTIFY_JUSTIFY_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M0 64h1024v128H0z m0 192h1024v128H0z m0 192h1024v128H0z m0 192h1024v128H0z m0 192h1024v128H0z\"></path></svg>'\n\n// 行高\nexport const LINE_HEIGHT_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M964 788a8 8 0 0 1 8 8v98a8 8 0 0 1-8 8H438a8 8 0 0 1-8-8v-98a8 8 0 0 1 8-8h526zM198.93 144.306c6.668-5.798 16.774-5.094 22.573 1.574l122.26 140.582a16 16 0 0 1 3.927 10.5c0 8.836-7.164 16-16 16h-61.8a8 8 0 0 0-8 8v390.077h69.819a16 16 0 0 1 10.502 3.928c6.666 5.8 7.37 15.906 1.57 22.573L221.476 878.123a16 16 0 0 1-1.57 1.57c-6.668 5.8-16.774 5.097-22.574-1.57L75.051 737.538a16 16 0 0 1-3.928-10.5c0-8.837 7.163-16 16-16h69.822V312.96H87.127a16 16 0 0 1-10.502-3.928c-6.666-5.8-7.37-15.906-1.57-22.573l122.303-140.582a16 16 0 0 1 1.572-1.572zM964 465a8 8 0 0 1 8 8v98a8 8 0 0 1-8 8H438a8 8 0 0 1-8-8v-98a8 8 0 0 1 8-8h526z m0-323a8 8 0 0 1 8 8v98a8 8 0 0 1-8 8H438a8 8 0 0 1-8-8v-98a8 8 0 0 1 8-8h526z\"></path></svg>'\n\n// 撤销\nexport const UNDO_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M512 64A510.272 510.272 0 0 0 149.984 213.984L0.032 64v384h384L240.512 304.48A382.784 382.784 0 0 1 512.032 192c212.064 0 384 171.936 384 384 0 114.688-50.304 217.632-130.016 288l84.672 96a510.72 510.72 0 0 0 173.344-384c0-282.784-229.216-512-512-512z\"></path></svg>'\n\n// 重做\nexport const REDO_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M0.00032 576a510.72 510.72 0 0 0 173.344 384l84.672-96A383.136 383.136 0 0 1 128.00032 576C128.00032 363.936 299.93632 192 512.00032 192c106.048 0 202.048 42.976 271.52 112.48L640.00032 448h384V64l-149.984 149.984A510.272 510.272 0 0 0 512.00032 64C229.21632 64 0.00032 293.216 0.00032 576z\"></path></svg>'\n\n// 分割线\nexport const DIVIDER_SVG =\n  '<svg viewBox=\"0 0 1092 1024\"><path d=\"M0 51.2m51.2 0l989.866667 0q51.2 0 51.2 51.2l0 0q0 51.2-51.2 51.2l-989.866667 0q-51.2 0-51.2-51.2l0 0q0-51.2 51.2-51.2Z\"></path><path d=\"M0 460.8m51.2 0l170.666667 0q51.2 0 51.2 51.2l0 0q0 51.2-51.2 51.2l-170.666667 0q-51.2 0-51.2-51.2l0 0q0-51.2 51.2-51.2Z\"></path><path d=\"M819.2 460.8m51.2 0l170.666667 0q51.2 0 51.2 51.2l0 0q0 51.2-51.2 51.2l-170.666667 0q-51.2 0-51.2-51.2l0 0q0-51.2 51.2-51.2Z\"></path><path d=\"M409.6 460.8m51.2 0l170.666667 0q51.2 0 51.2 51.2l0 0q0 51.2-51.2 51.2l-170.666667 0q-51.2 0-51.2-51.2l0 0q0-51.2 51.2-51.2Z\"></path><path d=\"M0 870.4m51.2 0l989.866667 0q51.2 0 51.2 51.2l0 0q0 51.2-51.2 51.2l-989.866667 0q-51.2 0-51.2-51.2l0 0q0-51.2 51.2-51.2Z\"></path></svg>'\n\n// 代码块\nexport const CODE_BLOCK_SVG =\n  '<svg viewBox=\"0 0 1280 1024\"><path d=\"M832 736l96 96 320-320L928 192l-96 96 224 224zM448 288l-96-96L32 512l320 320 96-96-224-224zM701.312 150.528l69.472 18.944-192 704.032-69.472-18.944 192-704.032z\"></path></svg>'\n\n// 全屏\nexport const FULL_SCREEN_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M133.705143 335.433143V133.851429h201.581714a29.622857 29.622857 0 0 0 29.622857-29.549715V68.754286a29.622857 29.622857 0 0 0-29.622857-29.622857H61.732571A22.893714 22.893714 0 0 0 38.765714 62.025143V335.725714c0 16.310857 13.238857 29.622857 29.622857 29.622857h35.547429a29.842286 29.842286 0 0 0 29.696-29.842285zM690.980571 133.851429h201.581715v201.654857c0 16.310857 13.238857 29.549714 29.622857 29.549714h35.547428a29.622857 29.622857 0 0 0 29.549715-29.549714V61.952a22.893714 22.893714 0 0 0-22.820572-22.893714h-273.554285a29.622857 29.622857 0 0 0-29.549715 29.622857v35.547428c0 16.310857 13.238857 29.696 29.622857 29.696zM335.286857 892.781714H133.705143V691.2a29.622857 29.622857 0 0 0-29.622857-29.622857H68.534857a29.622857 29.622857 0 0 0-29.549714 29.622857v273.554286c0 12.653714 10.24 22.893714 22.820571 22.893714h273.554286a29.622857 29.622857 0 0 0 29.696-29.622857v-35.547429a29.769143 29.769143 0 0 0-29.769143-29.696z m557.348572-201.581714v201.581714H690.907429a29.622857 29.622857 0 0 0-29.622858 29.622857v35.547429c0 16.310857 13.238857 29.622857 29.622858 29.622857h273.554285c12.580571 0 22.893714-10.313143 22.893715-22.893714V691.2a29.622857 29.622857 0 0 0-29.622858-29.622857h-35.547428a29.622857 29.622857 0 0 0-29.696 29.622857z\"></path></svg>'\n\n// 上标\nexport const SUP_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M768 206.016v50.016h128v64h-192V174.016l128-60V64h-128V0h192v146.016zM676 256h-136L352 444 164 256H28l256 256-256 256h136L352 580 540 768h136l-256-256z\"></path></svg>'\n\n// 下标\nexport const SUB_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M768 910.016v50.016h128v64h-192v-146.016l128-60V768h-128v-64h192v146.016zM676 256h-136L352 444 164 256H28l256 256-256 256h136L352 580 540 768h136l-256-256z\"></path></svg>'\n\n// checkbox\nexport const CHECK_BOX_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M278.755556 403.911111l-79.644445 79.644445L455.111111 739.555556l568.888889-568.888889-79.644444-79.644445L455.111111 580.266667l-176.355555-176.355556zM910.222222 910.222222H113.777778V113.777778h568.888889V0H113.777778C51.2 0 0 51.2 0 113.777778v796.444444c0 62.577778 51.2 113.777778 113.777778 113.777778h796.444444c62.577778 0 113.777778-51.2 113.777778-113.777778V455.111111h-113.777778v455.111111z\"></path></svg>'\n\n// 回车\nexport const ENTER_SVG =\n  '<svg viewBox=\"0 0 1255 1024\"><path d=\"M1095.111111 731.477333h-625.777778V1024L0 658.318222 469.333333 292.408889v292.636444h625.777778V0h156.444445v731.477333z\"></path></svg>'\n"
  },
  {
    "path": "packages/basic-modules/src/index.ts",
    "content": "/**\n * @description basic index\n * @author wangfupeng\n */\n\nimport './assets/index.less'\n\n// 配置多语言\nimport './locale/index'\n\nimport wangEditorParagraphModule from './modules/paragraph'\nimport wangEditorTextStyleModule from './modules/text-style'\nimport wangEditorHeaderModule from './modules/header'\nimport wangEditorColorModule from './modules/color'\nimport wangEditorLinkModule from './modules/link'\nimport wangEditorImageModule from './modules/image'\nimport wangEditorTodoModule from './modules/todo'\nimport wangEditorBlockQuoteModule from './modules/blockquote'\nimport wangEditorEmotionModule from './modules/emotion'\nimport wangEditorFontSizeAndFamilyModule from './modules/font-size-family'\nimport wangEditorIndentModule from './modules/indent'\nimport wangEditorJustifyModule from './modules/justify'\nimport wangEditorLineHeightModule from './modules/line-height'\nimport wangEditorUndoRedoModule from './modules/undo-redo'\nimport wangEditorDividerModule from './modules/divider'\nimport wangEditorCodeBlockModule from './modules/code-block'\nimport wangEditorFullScreenModule from './modules/full-screen'\nimport wangEditorCommonModule from './modules/common'\n\nexport default [\n  // text style\n  wangEditorTextStyleModule,\n  wangEditorColorModule,\n  wangEditorFontSizeAndFamilyModule,\n\n  // elem style\n  wangEditorIndentModule,\n  wangEditorJustifyModule,\n  wangEditorLineHeightModule,\n\n  // void node\n  wangEditorImageModule,\n  wangEditorDividerModule,\n\n  // inline node\n  wangEditorEmotionModule,\n  wangEditorLinkModule,\n\n  // block node —— 【注意】要放在 void-node 和 inline-node 后面！！！\n  wangEditorCodeBlockModule,\n  wangEditorBlockQuoteModule,\n  wangEditorHeaderModule,\n  wangEditorParagraphModule,\n  wangEditorTodoModule,\n\n  // command\n  wangEditorUndoRedoModule,\n  wangEditorFullScreenModule,\n  wangEditorCommonModule,\n]\n\n// 输出 image 操作，供 updateImageModule 使用\nexport * from './modules/image/helper'\n"
  },
  {
    "path": "packages/basic-modules/src/locale/en.ts",
    "content": "/**\n * @description i18n en\n * @author wangfupeng\n */\n\nexport default {\n  // 通用的词\n  common: {\n    ok: 'OK',\n    delete: 'Delete',\n    enter: 'Enter',\n  },\n\n  blockQuote: {\n    title: 'Quote',\n  },\n  codeBlock: {\n    title: 'Code block',\n  },\n  color: {\n    color: 'Font color',\n    bgColor: 'Back color',\n    default: 'Default color',\n    clear: 'Clear back color',\n  },\n  divider: {\n    title: 'Divider',\n  },\n  emotion: {\n    title: 'Emotion',\n  },\n  fontSize: {\n    title: 'Font size',\n    default: 'Default',\n  },\n  fontFamily: {\n    title: 'Font family',\n    default: 'Default',\n  },\n  fullScreen: {\n    title: 'Full screen',\n  },\n  header: {\n    title: 'Header',\n    text: 'Text',\n  },\n  image: {\n    netImage: 'Net image',\n    delete: 'Delete image',\n    edit: 'Edit image',\n    viewLink: 'View link',\n    src: 'Image src',\n    desc: 'Description',\n    link: 'Image link',\n  },\n  indent: {\n    decrease: 'Decrease',\n    increase: 'Increase',\n  },\n  justify: {\n    left: 'Left',\n    right: 'Right',\n    center: 'Center',\n    justify: 'Justify',\n  },\n  lineHeight: {\n    title: 'Line height',\n    default: 'Default',\n  },\n  link: {\n    insert: 'Insert link',\n    text: 'Link text',\n    url: 'Link source',\n    unLink: 'Unlink',\n    edit: 'Edit link',\n    view: 'View link',\n  },\n  textStyle: {\n    bold: 'Bold',\n    clear: 'Clear styles',\n    code: 'Inline code',\n    italic: 'Italic',\n    sub: 'Sub',\n    sup: 'Sup',\n    through: 'Through',\n    underline: 'Underline',\n  },\n  undo: {\n    undo: 'undo',\n    redo: 'Redo',\n  },\n  todo: {\n    todo: 'Todo',\n  },\n}\n"
  },
  {
    "path": "packages/basic-modules/src/locale/index.ts",
    "content": "/**\n * @description i18n entry\n * @author wangfupeng\n */\n\nimport { i18nAddResources } from '@wangeditor/core'\nimport enResources from './en'\nimport zhResources from './zh-CN'\n\ni18nAddResources('en', enResources)\ni18nAddResources('zh-CN', zhResources)\n"
  },
  {
    "path": "packages/basic-modules/src/locale/zh-CN.ts",
    "content": "/**\n * @description i18n zh-CN\n * @author wangfupeng\n */\n\nexport default {\n  // 通用的词\n  common: {\n    ok: '确定',\n    delete: '删除',\n    enter: '回车',\n  },\n\n  blockQuote: {\n    title: '引用',\n  },\n  codeBlock: {\n    title: '代码块',\n  },\n  color: {\n    color: '文字颜色',\n    bgColor: '背景色',\n    default: '默认颜色',\n    clear: '清除背景色',\n  },\n  divider: {\n    title: '分割线',\n  },\n  emotion: {\n    title: '表情',\n  },\n  fontSize: {\n    title: '字号',\n    default: '默认字号',\n  },\n  fontFamily: {\n    title: '字体',\n    default: '默认字体',\n  },\n  fullScreen: {\n    title: '全屏',\n  },\n  header: {\n    title: '标题',\n    text: '正文',\n  },\n  image: {\n    netImage: '网络图片',\n    delete: '删除图片',\n    edit: '编辑图片',\n    viewLink: '查看链接',\n    src: '图片地址',\n    desc: '图片描述',\n    link: '图片链接',\n  },\n  indent: {\n    decrease: '减少缩进',\n    increase: '增加缩进',\n  },\n  justify: {\n    left: '左对齐',\n    right: '右对齐',\n    center: '居中对齐',\n    justify: '两端对齐',\n  },\n  lineHeight: {\n    title: '行高',\n    default: '默认行高',\n  },\n  link: {\n    insert: '插入链接',\n    text: '链接文本',\n    url: '链接地址',\n    unLink: '取消链接',\n    edit: '修改链接',\n    view: '查看链接',\n  },\n  textStyle: {\n    bold: '粗体',\n    clear: '清除格式',\n    code: '行内代码',\n    italic: '斜体',\n    sub: '下标',\n    sup: '上标',\n    through: '删除线',\n    underline: '下划线',\n  },\n  undo: {\n    undo: '撤销',\n    redo: '重做',\n  },\n  todo: {\n    todo: '待办',\n  },\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/blockquote/custom-types.ts",
    "content": "/**\n * @description 自定义 element\n * @author wangfupeng\n */\n\nimport { Text } from 'slate'\n\n//【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts\n\nexport type BlockQuoteElement = {\n  type: 'blockquote'\n  children: Text[]\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/blockquote/elem-to-html.ts",
    "content": "/**\n * @description to html\n * @author wangfupeng\n */\n\nimport { Element } from 'slate'\n\nfunction quoteToHtml(elem: Element, childrenHtml: string): string {\n  return `<blockquote>${childrenHtml}</blockquote>`\n}\n\nexport const quoteToHtmlConf = {\n  type: 'blockquote',\n  elemToHtml: quoteToHtml,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/blockquote/index.ts",
    "content": "/**\n * @description blockquote entry\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport { renderBlockQuoteConf } from './render-elem'\nimport { quoteToHtmlConf } from './elem-to-html'\nimport { parseHtmlConf } from './parse-elem-html'\nimport { blockquoteMenuConf } from './menu/index'\nimport withBlockquote from './plugin'\n\nconst blockquote: Partial<IModuleConf> = {\n  renderElems: [renderBlockQuoteConf],\n  elemsToHtml: [quoteToHtmlConf],\n  parseElemsHtml: [parseHtmlConf],\n  menus: [blockquoteMenuConf],\n  editorPlugin: withBlockquote,\n}\n\nexport default blockquote\n"
  },
  {
    "path": "packages/basic-modules/src/modules/blockquote/menu/BlockquoteMenu.ts",
    "content": "/**\n * @description blockquote menu class\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport { QUOTE_SVG } from '../../../constants/icon-svg'\n\nclass BlockquoteMenu implements IButtonMenu {\n  readonly title = t('blockQuote.title')\n  readonly iconSvg = QUOTE_SVG\n  readonly tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 用不到 getValue\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    const node = DomEditor.getSelectedNodeByType(editor, 'blockquote')\n    return !!node\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n\n    const [nodeEntry] = Editor.nodes(editor, {\n      match: n => {\n        const type = DomEditor.getNodeType(n)\n\n        // 只可用于 p 和 blockquote\n        if (type === 'paragraph') return true\n        if (type === 'blockquote') return true\n\n        return false\n      },\n      universal: true,\n      mode: 'highest', // 匹配最高层级\n    })\n\n    // 匹配到 p blockquote ，不禁用\n    if (nodeEntry) {\n      return false\n    }\n    // 未匹配到，则禁用\n    return true\n  }\n\n  /**\n   * 执行命令\n   * @param editor editor\n   * @param value node.type\n   */\n  exec(editor: IDomEditor, value: string | boolean) {\n    if (this.isDisabled(editor)) return\n\n    const active = this.isActive(editor)\n    const newType = active ? 'paragraph' : 'blockquote'\n\n    // 执行命令\n    Transforms.setNodes(editor, { type: newType }, { mode: 'highest' })\n  }\n}\n\nexport default BlockquoteMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/blockquote/menu/index.ts",
    "content": "/**\n * @description block quote menu\n * @author wangfupeng\n */\n\nimport BlockquoteMenu from './BlockquoteMenu'\n\nexport const blockquoteMenuConf = {\n  key: 'blockquote',\n  factory() {\n    return new BlockquoteMenu()\n  },\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/blockquote/parse-elem-html.ts",
    "content": "/**\n * @description parse html\n * @author wangfupeng\n */\n\nimport { Descendant, Text } from 'slate'\nimport $, { DOMElement } from '../../utils/dom'\nimport { IDomEditor } from '@wangeditor/core'\nimport { BlockQuoteElement } from './custom-types'\n\nfunction parseHtml(\n  elem: DOMElement,\n  children: Descendant[],\n  editor: IDomEditor\n): BlockQuoteElement {\n  const $elem = $(elem)\n\n  children = children.filter(child => {\n    if (Text.isText(child)) return true\n    if (editor.isInline(child)) return true\n    return false\n  })\n\n  // 无 children ，则用纯文本\n  if (children.length === 0) {\n    children = [{ text: $elem.text().replace(/\\s+/gm, ' ') }]\n  }\n\n  return {\n    type: 'blockquote',\n    // @ts-ignore\n    children,\n  }\n}\n\nexport const parseHtmlConf = {\n  selector: 'blockquote:not([data-w-e-type])', // data-w-e-type 属性，留给自定义元素，保证扩展性\n  parseElemHtml: parseHtml,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/blockquote/plugin.ts",
    "content": "/**\n * @description editor 插件，重写 editor API\n * @author wangfupeng\n */\n\nimport { Editor, Transforms, Node, Point } from 'slate'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\n\nfunction withBlockquote<T extends IDomEditor>(editor: T): T {\n  const { insertBreak, insertText } = editor\n  const newEditor = editor\n\n  // 重写 insertBreak - 换行时插入 p\n  newEditor.insertBreak = () => {\n    const { selection } = newEditor\n    if (selection == null) return insertBreak()\n\n    const [nodeEntry] = Editor.nodes(editor, {\n      match: n => DomEditor.checkNodeType(n, 'blockquote'),\n      universal: true,\n    })\n    if (!nodeEntry) return insertBreak()\n\n    const quoteElem = nodeEntry[0]\n    const quotePath = DomEditor.findPath(editor, quoteElem)\n    const quoteEndLocation = Editor.end(editor, quotePath)\n\n    if (Point.equals(quoteEndLocation, selection.focus)) {\n      // 光标位于 blockquote 最后\n      const str = Node.string(quoteElem)\n      if (str && str.slice(-1) === '\\n') {\n        // blockquote 文本最后一个是 \\n\n        editor.deleteBackward('character') // 删除最后一个 \\n\n\n        // 则插入一个 paragraph\n        const p = { type: 'paragraph', children: [{ text: '' }] }\n        Transforms.insertNodes(newEditor, p, { mode: 'highest' })\n        return\n      }\n    }\n\n    // 情况情况，插入换行符\n    insertText('\\n')\n  }\n\n  // 返回 editor ，重要！\n  return newEditor\n}\n\nexport default withBlockquote\n"
  },
  {
    "path": "packages/basic-modules/src/modules/blockquote/render-elem.tsx",
    "content": "/**\n * @description render elem\n * @author wangfupeng\n */\n\nimport { Element as SlateElement } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { IDomEditor } from '@wangeditor/core'\n\n/**\n * render block quote elem\n * @param elemNode slate elem\n * @param children children\n * @param editor editor\n * @returns vnode\n */\nfunction renderBlockQuote(\n  elemNode: SlateElement,\n  children: VNode[] | null,\n  editor: IDomEditor\n): VNode {\n  const vnode = <blockquote>{children}</blockquote>\n  return vnode\n}\n\nexport const renderBlockQuoteConf = {\n  type: 'blockquote',\n  renderElem: renderBlockQuote,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/code-block/custom-types.ts",
    "content": "/**\n * @description 自定义 element\n * @author wangfupeng\n */\n\n//【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts\n\ntype PureText = {\n  text: string\n}\n\nexport type PreElement = {\n  type: 'pre'\n  children: CodeElement[]\n}\n\nexport type CodeElement = {\n  type: 'code'\n  language: string\n  children: PureText[]\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/code-block/elem-to-html.ts",
    "content": "/**\n * @description to html\n * @author wangfupeng\n */\n\nimport { Element } from 'slate'\n\nfunction codeToHtml(elem: Element, childrenHtml: string): string {\n  // 代码高亮 `class=\"language-xxx\"` 在 code-highlight 中实现\n  return `<code>${childrenHtml}</code>`\n}\n\nexport const codeToHtmlConf = {\n  type: 'code',\n  elemToHtml: codeToHtml,\n}\n\nfunction preToHtml(elem: Element, childrenHtml: string): string {\n  return `<pre>${childrenHtml}</pre>`\n}\n\nexport const preToHtmlConf = {\n  type: 'pre',\n  elemToHtml: preToHtml,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/code-block/index.ts",
    "content": "/**\n * @description code block module\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport { codeBlockMenuConf } from './menu/index'\nimport withCodeBlock from './plugin'\nimport { renderPreConf, renderCodeConf } from './render-elem'\nimport { preParseHtmlConf } from './pre-parse-html'\nimport { parseCodeHtmlConf, parsePreHtmlConf } from './parse-elem-html'\nimport { codeToHtmlConf, preToHtmlConf } from './elem-to-html'\n\nconst codeBlockModule: Partial<IModuleConf> = {\n  menus: [codeBlockMenuConf],\n  editorPlugin: withCodeBlock,\n  renderElems: [renderPreConf, renderCodeConf],\n  elemsToHtml: [codeToHtmlConf, preToHtmlConf],\n  preParseHtml: [preParseHtmlConf],\n  parseElemsHtml: [parseCodeHtmlConf, parsePreHtmlConf],\n}\n\nexport default codeBlockModule\n"
  },
  {
    "path": "packages/basic-modules/src/modules/code-block/menu/CodeBlockMenu.ts",
    "content": "/**\n * @description insert code-block menu\n * @author wangfupeng\n */\n\nimport { Editor, Element, Transforms, Node, Range } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport { CODE_BLOCK_SVG } from '../../../constants/icon-svg'\nimport { CodeElement } from '../custom-types'\n\nclass CodeBlockMenu implements IButtonMenu {\n  readonly title = t('codeBlock.title')\n  readonly iconSvg = CODE_BLOCK_SVG\n  readonly tag = 'button'\n\n  private getSelectCodeElem(editor: IDomEditor): CodeElement | null {\n    const codeNode = DomEditor.getSelectedNodeByType(editor, 'code')\n    if (codeNode == null) return null\n    const preNode = DomEditor.getParentNode(editor, codeNode)\n    if (preNode == null) return null\n    if (DomEditor.getNodeType(preNode) !== 'pre') return null\n\n    return codeNode as CodeElement\n  }\n\n  /**\n   * 获取语言类型\n   * @param editor editor\n   */\n  getValue(editor: IDomEditor): string | boolean {\n    const elem = this.getSelectCodeElem(editor)\n    if (elem == null) return ''\n    return elem.language || ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    const elem = this.getSelectCodeElem(editor)\n    return !!elem\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    const { selection } = editor\n    if (selection == null) return true\n\n    const selectedElems = DomEditor.getSelectedElems(editor)\n\n    const hasVoid = selectedElems.some(elem => editor.isVoid(elem))\n    if (hasVoid) return true\n\n    const isMatch = selectedElems.some(elem => {\n      const type = DomEditor.getNodeType(elem)\n      if (type === 'pre' || type === 'paragraph') return true // 匹配 pre 或 paragraph\n    })\n    if (isMatch) return false // 匹配到，则 enable\n    return true // 否则 disable\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    const active = this.isActive(editor)\n    if (active) {\n      // 当前处于 code-block ，需要转换为普通文本\n      this.changeToPlainText(editor)\n    } else {\n      // 当前未处于 code-block ，需要转换为 code-block\n      this.changeToCodeBlock(editor, value.toString())\n    }\n  }\n\n  private changeToPlainText(editor: IDomEditor) {\n    const elem = this.getSelectCodeElem(editor)\n    if (elem == null) return\n\n    // 获取 code 文本\n    const str = Node.string(elem)\n\n    // 删除当前最高层级的节点，即 pre 节点\n    Transforms.removeNodes(editor, { mode: 'highest' })\n\n    // 插入 p 节点\n    const pList = str.split('\\n').map(s => {\n      return { type: 'paragraph', children: [{ text: s }] }\n    })\n    Transforms.insertNodes(editor, pList, { mode: 'highest' })\n  }\n\n  private changeToCodeBlock(editor: IDomEditor, language: string) {\n    // 汇总选中的最高层级节点的字符串\n    const strArr: string[] = []\n    const nodeEntries = Editor.nodes(editor, {\n      match: n => editor.children.includes(n as Element), // 匹配选中的最高层级的节点\n      universal: true,\n    })\n    for (let nodeEntry of nodeEntries) {\n      const [n] = nodeEntry\n      if (n) strArr.push(Node.string(n))\n    }\n\n    // 删除选中的最高层级的节点\n    Transforms.removeNodes(editor, { mode: 'highest' })\n\n    // 插入 pre 节点\n    const newPreNode = {\n      type: 'pre',\n      children: [\n        {\n          type: 'code',\n          language,\n          children: [\n            { text: strArr.join('\\n') }, // 选中节点的纯文本\n          ],\n        },\n      ],\n    }\n    Transforms.insertNodes(editor, newPreNode, { mode: 'highest' })\n  }\n}\n\nexport default CodeBlockMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/code-block/menu/index.ts",
    "content": "/**\n * @description code-block menu\n * @author wangfupeng\n */\n\nimport CodeBlockMenu from './CodeBlockMenu'\n\nexport const codeBlockMenuConf = {\n  key: 'codeBlock',\n  factory() {\n    return new CodeBlockMenu()\n  },\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/code-block/parse-elem-html.ts",
    "content": "/**\n * @description parse html\n * @author wangfupeng\n */\n\nimport { Descendant } from 'slate'\nimport $, { DOMElement } from '../../utils/dom'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\nimport { PreElement, CodeElement } from './custom-types'\n\nfunction parseCodeHtml(elem: DOMElement, children: Descendant[], editor: IDomEditor): CodeElement {\n  const $elem = $(elem)\n\n  return {\n    type: 'code',\n    language: '', // language 在 code-highlight 中实现\n    children: [{ text: $elem[0].textContent || '' }],\n  }\n}\n\nexport const parseCodeHtmlConf = {\n  selector: 'pre:not([data-w-e-type])>code', // 匹配 <pre> 下的 <code>\n  parseElemHtml: parseCodeHtml,\n}\n\nfunction parsePreHtml(elem: DOMElement, children: Descendant[], editor: IDomEditor): PreElement {\n  const $elem = $(elem)\n\n  children = children.filter(child => DomEditor.getNodeType(child) === 'code')\n  if (children.length === 0) {\n    children = [{ type: 'code', language: '', children: [{ text: $elem[0].textContent || '' }] }]\n  }\n\n  return {\n    type: 'pre',\n    // @ts-ignore\n    children: children.filter(child => DomEditor.getNodeType(child) === 'code'),\n  }\n}\n\nexport const parsePreHtmlConf = {\n  selector: 'pre:not([data-w-e-type])', // data-w-e-type 属性，留给自定义元素，保证扩展性\n  parseElemHtml: parsePreHtml,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/code-block/plugin.ts",
    "content": "/**\n * @description editor 插件，重写 editor API\n * @author wangfupeng\n */\n\nimport { Editor, Transforms, Node as SlateNode, Element as SlateElement } from 'slate'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\n\nfunction getLastTextLineBeforeSelection(codeNode: SlateNode, editor: IDomEditor): string {\n  const selection = editor.selection\n  if (selection == null) return ''\n\n  const codeText = SlateNode.string(codeNode)\n  const anchorOffset = selection.anchor.offset\n  const textBeforeAnchor = codeText.slice(0, anchorOffset) // 选区前的 text\n  const arr = textBeforeAnchor.split('\\n') // 选区前的 text ，按换行拆分\n  const length = arr.length\n  if (length === 0) return ''\n\n  return arr[length - 1]\n}\n\nfunction withCodeBlock<T extends IDomEditor>(editor: T): T {\n  const { insertBreak, normalizeNode, insertData, insertNode } = editor\n  const newEditor = editor\n\n  // 重写换行操作\n  newEditor.insertBreak = () => {\n    const codeNode = DomEditor.getSelectedNodeByType(newEditor, 'code')\n    if (codeNode == null) {\n      insertBreak() // 执行默认的换行\n      return\n    }\n\n    // 回车时，根据当前行的空格，自动插入空格\n    const lastLineBeforeSelection = getLastTextLineBeforeSelection(codeNode, newEditor)\n    if (lastLineBeforeSelection) {\n      const arr = lastLineBeforeSelection.match(/^\\s+/) // 行开始的空格\n      if (arr != null && arr[0] != null) {\n        const spaces = arr[0]\n        newEditor.insertText(`\\n${spaces}`) // 换行后插入空格\n        return\n      }\n    }\n\n    // 普通换行\n    newEditor.insertText('\\n')\n  }\n\n  // 重写 normalizeNode\n  newEditor.normalizeNode = ([node, path]) => {\n    const type = DomEditor.getNodeType(node)\n\n    // -------------- code node 不能是顶层，否则替换为 p --------------\n    if (type === 'code' && path.length <= 1) {\n      Transforms.setNodes(newEditor, { type: 'paragraph' }, { at: path })\n    }\n\n    if (type === 'pre') {\n      // -------------- pre 是 editor 最后一个节点，需要后面插入 p --------------\n      const isLast = DomEditor.isLastNode(newEditor, node)\n      if (isLast) {\n        Transforms.insertNodes(newEditor, DomEditor.genEmptyParagraph(), { at: [path[0] + 1] })\n      }\n\n      // -------------- pre 下面必须是 code --------------\n      if (DomEditor.getNodeType((node as SlateElement).children[0]) !== 'code') {\n        Transforms.unwrapNodes(newEditor)\n        Transforms.setNodes(newEditor, { type: 'paragraph' }, { mode: 'highest' })\n      }\n    }\n\n    // 执行默认行为\n    return normalizeNode([node, path])\n  }\n\n  // 重写 insertData - 粘贴文本\n  newEditor.insertData = (data: DataTransfer) => {\n    const codeNode = DomEditor.getSelectedNodeByType(newEditor, 'code')\n    if (codeNode == null) {\n      insertData(data) // 执行默认的 insertData\n      return\n    }\n\n    // 获取文本，并插入到代码块\n    const text = data.getData('text/plain')\n    Editor.insertText(newEditor, text)\n  }\n\n  // 返回 editor ，重要！\n  return newEditor\n}\n\nexport default withCodeBlock\n"
  },
  {
    "path": "packages/basic-modules/src/modules/code-block/pre-parse-html.ts",
    "content": "/**\n * @description pre parse html\n * @author wangfupeng\n */\n\nimport $, { DOMElement } from '../../utils/dom'\nimport { getTagName } from '../../utils/dom'\n\n/**\n * pre-prase <code> ，去掉其中的 <xmp> （兼容 V4）\n * @param codeElem codeElem\n */\nfunction preParse(codeElem: DOMElement): DOMElement {\n  const $code = $(codeElem)\n  const tagName = getTagName($code)\n  if (tagName !== 'code') return codeElem\n\n  const $xmp = $code.find('xmp')\n  if ($xmp.length === 0) return codeElem // 不是 V4 格式\n\n  const codeText = $xmp.text()\n  $xmp.remove()\n  $code.text(codeText)\n\n  return $code[0]\n}\n\nexport const preParseHtmlConf = {\n  selector: 'pre>code', // 匹配 <pre> 下的 <code>\n  preParseHtml: preParse,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/code-block/render-elem.tsx",
    "content": "/**\n * @description render elem\n * @author wangfupeng\n */\n\nimport { Element as SlateElement } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { IDomEditor } from '@wangeditor/core'\n\nfunction renderPre(elemNode: SlateElement, children: VNode[] | null, editor: IDomEditor): VNode {\n  const vnode = <pre>{children}</pre>\n  return vnode\n}\n\nfunction renderCode(elemNode: SlateElement, children: VNode[] | null, editor: IDomEditor): VNode {\n  // 和 basic/simple-style module 的“行内代码”并不冲突。一个是根据 mark 渲染，一个是根据 node.type 渲染\n  const vnode = <code>{children}</code>\n  return vnode\n}\n\nexport const renderPreConf = {\n  type: 'pre',\n  renderElem: renderPre,\n}\n\nexport const renderCodeConf = {\n  type: 'code',\n  renderElem: renderCode,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/color/custom-types.ts",
    "content": "/**\n * @description 自定义 element\n * @author wangfupeng\n */\n\n//【注意】需要把自定义的 Text 引入到最外层的 custom-types.d.ts\n\nexport type ColorText = {\n  text: string\n  color?: string\n  bgColor?: string\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/color/index.ts",
    "content": "/**\n * @description color bgColor\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport { renderStyle } from './render-style'\nimport { styleToHtml } from './style-to-html'\nimport { preParseHtmlConf } from './pre-parse-html'\nimport { parseStyleHtml } from './parse-style-html'\nimport { colorMenuConf, bgColorMenuConf } from './menu/index'\n\nconst color: Partial<IModuleConf> = {\n  renderStyle,\n  styleToHtml,\n  preParseHtml: [preParseHtmlConf],\n  parseStyleHtml,\n  menus: [colorMenuConf, bgColorMenuConf],\n}\n\nexport default color\n"
  },
  {
    "path": "packages/basic-modules/src/modules/color/menu/BaseMenu.ts",
    "content": "/**\n * @description color base menu\n * @author wangfupeng\n */\n\nimport { Editor, Range } from 'slate'\nimport { IDropPanelMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport $, { Dom7Array, DOMElement } from '../../../utils/dom'\nimport { CLEAN_SVG } from '../../../constants/icon-svg'\n\nabstract class BaseMenu implements IDropPanelMenu {\n  abstract readonly title: string\n  abstract readonly iconSvg: string\n  readonly tag = 'button'\n  readonly showDropPanel = true // 点击 button 时显示 dropPanel\n  protected abstract readonly mark: string\n  private $content: Dom7Array | null = null\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    // 点击菜单时，弹出 droPanel 之前，不需要执行其他代码\n    // 此处空着即可\n  }\n\n  getValue(editor: IDomEditor): string | boolean {\n    const mark = this.mark\n    const curMarks = Editor.marks(editor)\n    // @ts-ignore\n    if (curMarks && curMarks[mark]) return curMarks[mark]\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    const color = this.getValue(editor)\n    return !!color\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n\n    const [match] = Editor.nodes(editor, {\n      match: n => {\n        const type = DomEditor.getNodeType(n)\n\n        if (type === 'pre') return true // 代码块\n        if (Editor.isVoid(editor, n)) return true // void node\n\n        return false\n      },\n      universal: true,\n    })\n\n    // 命中，则禁用\n    if (match) return true\n    return false\n  }\n\n  getPanelContentElem(editor: IDomEditor): DOMElement {\n    const mark = this.mark\n\n    if (this.$content == null) {\n      // 第一次渲染\n      const $content = $('<ul class=\"w-e-panel-content-color\"></ul>')\n\n      // 绑定事件（只在第一次绑定，不要重复绑定）\n      $content.on('click', 'li', (e: Event) => {\n        const { target } = e\n        if (target == null) return\n        e.preventDefault()\n\n        const { selection } = editor\n        if (selection == null) return\n\n        const $li = $(target)\n        const val = $li.attr('data-value')\n\n        // 修改文本样式\n        if (val === '0') {\n          Editor.removeMark(editor, mark)\n        } else {\n          Editor.addMark(editor, mark, val)\n        }\n      })\n\n      this.$content = $content\n    }\n    const $content = this.$content\n    if ($content == null) return document.createElement('ul')\n    $content.empty() // 清空之后再重置内容\n\n    // 当前选中文本的颜色之\n    const selectedColor = this.getValue(editor)\n\n    // 获取菜单配置\n    const colorConf = editor.getMenuConfig(mark)\n    const { colors = [] } = colorConf\n    // 根据菜单配置生成 panel content\n    colors.forEach((color: string) => {\n      const $block = $(`<div class=\"color-block\" data-value=\"${color}\"></div>`)\n      $block.css('background-color', color)\n\n      const $li = $(`<li data-value=\"${color}\"></li>`)\n      if (selectedColor === color) {\n        $li.addClass('active')\n      }\n      $li.append($block)\n\n      $content.append($li)\n    })\n\n    // 清除颜色\n    let clearText = ''\n    if (mark === 'color') clearText = t('color.default')\n    if (mark === 'bgColor') clearText = t('color.clear')\n    const $clearLi = $(`\n      <li data-value=\"0\" class=\"clear\">\n        ${CLEAN_SVG}\n        ${clearText}\n      </li>\n    `)\n    $content.prepend($clearLi)\n\n    return $content[0]\n  }\n}\n\nexport default BaseMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/color/menu/BgColorMenu.ts",
    "content": "/**\n * @description bg color menu\n * @author wangfupeng\n */\n\nimport { t } from '@wangeditor/core'\nimport BaseMenu from './BaseMenu'\nimport { BG_COLOR_SVG } from '../../../constants/icon-svg'\n\nclass BgColorMenu extends BaseMenu {\n  readonly title = t('color.bgColor')\n  readonly iconSvg = BG_COLOR_SVG\n  readonly mark = 'bgColor'\n}\n\nexport default BgColorMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/color/menu/ColorMenu.ts",
    "content": "/**\n * @description color menu\n * @author wangfupeng\n */\n\nimport { t } from '@wangeditor/core'\nimport BaseMenu from './BaseMenu'\nimport { FONT_COLOR_SVG } from '../../../constants/icon-svg'\n\nclass ColorMenu extends BaseMenu {\n  readonly title = t('color.color')\n  readonly iconSvg = FONT_COLOR_SVG\n  readonly mark = 'color'\n}\n\nexport default ColorMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/color/menu/config.ts",
    "content": "/**\n * @description menu config\n * @author wangfupeng\n */\n\nconst COLORS = [\n  'rgb(0, 0, 0)',\n  'rgb(38, 38, 38)',\n  'rgb(89, 89, 89)',\n  'rgb(140, 140, 140)',\n  'rgb(191, 191, 191)',\n  'rgb(217, 217, 217)',\n  'rgb(233, 233, 233)',\n  'rgb(245, 245, 245)',\n  'rgb(250, 250, 250)',\n  'rgb(255, 255, 255)', // 10\n  'rgb(225, 60, 57)',\n  'rgb(231, 95, 51)',\n  'rgb(235, 144, 58)',\n  'rgb(245, 219, 77)',\n  'rgb(114, 192, 64)',\n  'rgb(89, 191, 192)',\n  'rgb(66, 144, 247)',\n  'rgb(54, 88, 226)',\n  'rgb(106, 57, 201)',\n  'rgb(216, 68, 147)', // 10\n  'rgb(251, 233, 230)',\n  'rgb(252, 237, 225)',\n  'rgb(252, 239, 212)',\n  'rgb(252, 251, 207)',\n  'rgb(231, 246, 213)',\n  'rgb(218, 244, 240)',\n  'rgb(217, 237, 250)',\n  'rgb(224, 232, 250)',\n  'rgb(237, 225, 248)',\n  'rgb(246, 226, 234)', // 10\n  'rgb(255, 163, 158)',\n  'rgb(255, 187, 150)',\n  'rgb(255, 213, 145)',\n  'rgb(255, 251, 143)',\n  'rgb(183, 235, 143)',\n  'rgb(135, 232, 222)',\n  'rgb(145, 213, 255)',\n  'rgb(173, 198, 255)',\n  'rgb(211, 173, 247)',\n  'rgb(255, 173, 210)', // 10\n  'rgb(255, 77, 79)',\n  'rgb(255, 122, 69)',\n  'rgb(255, 169, 64)',\n  'rgb(255, 236, 61)',\n  'rgb(115, 209, 61)',\n  'rgb(54, 207, 201)',\n  'rgb(64, 169, 255)',\n  'rgb(89, 126, 247)',\n  'rgb(146, 84, 222)',\n  'rgb(247, 89, 171)', // 10\n  'rgb(207, 19, 34)',\n  'rgb(212, 56, 13)',\n  'rgb(212, 107, 8)',\n  'rgb(212, 177, 6)',\n  'rgb(56, 158, 13)',\n  'rgb(8, 151, 156)',\n  'rgb(9, 109, 217)',\n  'rgb(29, 57, 196)',\n  'rgb(83, 29, 171)',\n  'rgb(196, 29, 127)', // 10\n  'rgb(130, 0, 20)',\n  'rgb(135, 20, 0)',\n  'rgb(135, 56, 0)',\n  'rgb(97, 71, 0)',\n  'rgb(19, 82, 0)',\n  'rgb(0, 71, 79)',\n  'rgb(0, 58, 140)',\n  'rgb(6, 17, 120)',\n  'rgb(34, 7, 94)',\n  'rgb(120, 6, 80)', // 10\n]\n\nexport function genColors() {\n  return COLORS\n}\n\nexport function genBgColors() {\n  return COLORS\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/color/menu/index.ts",
    "content": "/**\n * @description menu entry\n * @author wangfupeng\n */\n\nimport ColorMenu from './ColorMenu'\nimport BgColorMenu from './BgColorMenu'\nimport { genColors, genBgColors } from './config'\n\nexport const colorMenuConf = {\n  key: 'color',\n  factory() {\n    return new ColorMenu()\n  },\n\n  // 默认的菜单菜单配置，将存储在 editorConfig.MENU_CONF[key] 中\n  // 创建编辑器时，可通过 editorConfig.MENU_CONF[key] = {...} 来修改\n  config: {\n    colors: genColors(),\n  },\n}\n\nexport const bgColorMenuConf = {\n  key: 'bgColor',\n  factory() {\n    return new BgColorMenu()\n  },\n  config: {\n    colors: genBgColors(),\n  },\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/color/parse-style-html.ts",
    "content": "/**\n * @description parse style html\n * @author wangfupeng\n */\n\nimport { Descendant, Text } from 'slate'\nimport { IDomEditor } from '@wangeditor/core'\nimport { ColorText } from './custom-types'\nimport $, { DOMElement, getStyleValue } from '../../utils/dom'\n\nexport function parseStyleHtml(text: DOMElement, node: Descendant, editor: IDomEditor): Descendant {\n  const $text = $(text)\n  if (!Text.isText(node)) return node\n\n  const textNode = node as ColorText\n\n  const color = getStyleValue($text, 'color')\n  if (color) {\n    textNode.color = color\n  }\n\n  let bgColor = getStyleValue($text, 'background-color')\n  if (!bgColor) bgColor = getStyleValue($text, 'background') // word 背景色\n  if (bgColor) {\n    textNode.bgColor = bgColor\n  }\n\n  return textNode\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/color/pre-parse-html.ts",
    "content": "/**\n * @description pre-parse html\n * @author wangfupeng\n */\n\nimport $, { DOMElement, getTagName } from '../../utils/dom'\n\n/**\n * pre-prase font ，兼容 V4\n * @param fontElem fontElem\n */\nfunction preParse(fontElem: DOMElement): DOMElement {\n  const $font = $(fontElem)\n  const tagName = getTagName($font)\n  if (tagName !== 'font') return fontElem\n\n  // 处理 color （V4 使用 <font color=\"#ccc\">xx</font> 格式）\n  const color = $font.attr('color') || ''\n  if (color) {\n    $font.removeAttr('color')\n    $font.css('color', color)\n  }\n\n  return $font[0]\n}\n\nexport const preParseHtmlConf = {\n  selector: 'font',\n  preParseHtml: preParse,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/color/render-style.tsx",
    "content": "/**\n * @description render color style\n * @author wangfupeng\n */\n\nimport { Descendant } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { addVnodeStyle } from '../../utils/vdom'\nimport { ColorText } from './custom-types'\n\n/**\n * 添加样式\n * @param node text node\n * @param vnode vnode\n * @returns vnode\n */\nexport function renderStyle(node: Descendant, vnode: VNode): VNode {\n  const { color, bgColor } = node as ColorText\n  let styleVnode: VNode = vnode\n\n  if (color) {\n    addVnodeStyle(styleVnode, { color })\n  }\n  if (bgColor) {\n    addVnodeStyle(styleVnode, { backgroundColor: bgColor })\n  }\n\n  return styleVnode\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/color/style-to-html.ts",
    "content": "/**\n * @description textStyle to html\n * @author wangfupeng\n */\n\nimport { Text, Descendant } from 'slate'\nimport $, { getOuterHTML, getTagName, isPlainText } from '../../utils/dom'\nimport { ColorText } from './custom-types'\n\n/**\n * style to html\n * @param textNode slate text node\n * @param textHtml text html\n * @returns styled html\n */\nexport function styleToHtml(textNode: Descendant, textHtml: string): string {\n  if (!Text.isText(textNode)) return textHtml\n\n  const { color, bgColor } = textNode as ColorText\n  if (!color && !bgColor) return textHtml\n\n  let $text\n\n  if (isPlainText(textHtml)) {\n    // textHtml 是纯文本，不是 html tag\n    $text = $(`<span>${textHtml}</span>`)\n  } else {\n    // textHtml 是 html tag\n    $text = $(textHtml)\n    const tagName = getTagName($text)\n    if (tagName !== 'span') {\n      // 如果不是 span ，则包裹一层，接下来要设置 css\n      $text = $(`<span>${textHtml}</span>`)\n    }\n  }\n\n  // 设置样式\n  if (color) $text.css('color', color)\n  if (bgColor) $text.css('background-color', bgColor)\n\n  // 输出 html\n  return getOuterHTML($text)\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/common/index.ts",
    "content": "/**\n * @description common module\n * @author wangfupeng\n */\nimport { IModuleConf } from '@wangeditor/core'\nimport { enterMenuConf } from './menu/index'\n\nconst commonModule: Partial<IModuleConf> = {\n  menus: [enterMenuConf],\n}\n\nexport default commonModule\n"
  },
  {
    "path": "packages/basic-modules/src/modules/common/menu/EnterMenu.ts",
    "content": "/**\n * @description enter menu\n * @author wangfupeng\n */\n\nimport { Range, Transforms, Editor } from 'slate'\nimport { IButtonMenu, IDomEditor, t } from '@wangeditor/core'\nimport { ENTER_SVG } from '../../../constants/icon-svg'\n\nclass EnterMenu implements IButtonMenu {\n  title = t('common.enter')\n  iconSvg = ENTER_SVG\n  tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    return false\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    const { selection } = editor\n    if (selection == null) return true\n    if (Range.isExpanded(selection)) return true\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    const { selection } = editor\n    if (selection == null) return\n    const { anchor } = selection\n    const { path } = anchor\n\n    // 在当前位置插入空行，当前元素下移\n    const newElem = { type: 'paragraph', children: [{ text: '' }] }\n    const newPath = [path[0]]\n    Transforms.insertNodes(editor, newElem, { at: newPath })\n    editor.select(Editor.start(editor, newPath))\n  }\n}\n\nexport default EnterMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/common/menu/index.ts",
    "content": "/**\n * @description common menu config\n * @author wangfupeng\n */\n\nimport EnterMenu from './EnterMenu'\n\nexport const enterMenuConf = {\n  key: 'enter',\n  factory() {\n    return new EnterMenu()\n  },\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/divider/README.md",
    "content": "# 分割线"
  },
  {
    "path": "packages/basic-modules/src/modules/divider/custom-types.ts",
    "content": "/**\n * @description divider element\n * @author wangfupeng\n */\n\n//【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts\n\ntype EmptyText = {\n  text: ''\n}\n\nexport type DividerElement = {\n  type: 'divider'\n  children: EmptyText[]\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/divider/elem-to-html.ts",
    "content": "/**\n * @description to html\n * @author wangfupeng\n */\n\nimport { Element } from 'slate'\n\nfunction dividerToHtml(elem: Element, childrenHtml: string): string {\n  return `<hr/>`\n}\n\nexport const dividerToHtmlConf = {\n  type: 'divider',\n  elemToHtml: dividerToHtml,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/divider/index.ts",
    "content": "/**\n * @description divider module\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport withDivider from './plugin'\nimport { renderDividerConf } from './render-elem'\nimport { dividerToHtmlConf } from './elem-to-html'\nimport { parseHtmlConf } from './parse-elem-html'\nimport { insertDividerMenuConf } from './menu/index'\n\nconst image: Partial<IModuleConf> = {\n  renderElems: [renderDividerConf],\n  elemsToHtml: [dividerToHtmlConf],\n  parseElemsHtml: [parseHtmlConf],\n  menus: [insertDividerMenuConf],\n  editorPlugin: withDivider,\n}\n\nexport default image\n"
  },
  {
    "path": "packages/basic-modules/src/modules/divider/menu/DeleteDividerMenu.ts.bak",
    "content": "/**\n * @description delete divider menu\n * @author wangfupeng\n */\n\nimport { Transforms } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport { TRASH_SVG } from '../../../constants/icon-svg'\n\nclass DeleteDividerMenu implements IButtonMenu {\n  readonly title = t('common.delete')\n  readonly iconSvg = TRASH_SVG\n  readonly tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 无需获取 val\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 无需 active\n    return false\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n\n    const dividerNode = DomEditor.getSelectedNodeByType(editor, 'divider')\n    if (dividerNode == null) {\n      // 选区未处于 divider node ，则禁用\n      return true\n    }\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    if (this.isDisabled(editor)) return\n\n    // 删除\n    Transforms.removeNodes(editor, {\n      match: n => DomEditor.checkNodeType(n, 'divider'),\n    })\n  }\n}\n\nexport default DeleteDividerMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/divider/menu/InsertDividerMenu.ts",
    "content": "/**\n * @description insert divider menu\n * @author wangfupeng\n */\n\nimport { Transforms } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport { DIVIDER_SVG } from '../../../constants/icon-svg'\nimport { DividerElement } from '../custom-types'\n\nclass InsertDividerMenu implements IButtonMenu {\n  readonly title = t('divider.title')\n  readonly iconSvg = DIVIDER_SVG\n  readonly tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 不需要 active\n    return false\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    const { selection } = editor\n    if (selection == null) return true\n\n    const selectedElems = DomEditor.getSelectedElems(editor)\n    const hasVoidOrTableOrPre = selectedElems.some(elem => {\n      if (editor.isVoid(elem)) return true\n      const type = DomEditor.getNodeType(elem)\n      if (type === 'table') return true\n      if (type === 'pre') return true\n    })\n    if (hasVoidOrTableOrPre) return true // 匹配，则 disable\n\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean): void {\n    const node: DividerElement = {\n      type: 'divider',\n      children: [{ text: '' }], // 【注意】void node 需要一个空 text 作为 children\n    }\n\n    Transforms.insertNodes(editor, node, { mode: 'highest' })\n  }\n}\n\nexport default InsertDividerMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/divider/menu/index.ts",
    "content": "/**\n * @description divider menu\n * @author wangfupeng\n */\n\nimport InsertDividerMenu from './InsertDividerMenu'\n// import DeleteDividerMenu from './DeleteDividerMenu.ts'\n\nexport const insertDividerMenuConf = {\n  key: 'divider',\n  factory() {\n    return new InsertDividerMenu()\n  },\n}\n\n// export const deleteDividerMenuConf = {\n//   key: 'deleteDivider',\n//   factory() {\n//     return new DeleteDividerMenu()\n//   },\n// }\n// divider 可用键盘删除了，所以注释掉该菜单 wangfupeng 22.02.23\n"
  },
  {
    "path": "packages/basic-modules/src/modules/divider/parse-elem-html.ts",
    "content": "/**\n * @description parse html\n * @author wangfupeng\n */\n\nimport { Descendant } from 'slate'\nimport $, { DOMElement } from '../../utils/dom'\nimport { IDomEditor } from '@wangeditor/core'\nimport { DividerElement } from './custom-types'\n\nfunction parseHtml(elem: DOMElement, children: Descendant[], editor: IDomEditor): DividerElement {\n  return {\n    type: 'divider',\n    children: [{ text: '' }], // void node 有一个空白 text\n  }\n}\n\nexport const parseHtmlConf = {\n  selector: 'hr:not([data-w-e-type])', // data-w-e-type 属性，留给自定义元素，保证扩展性\n  parseElemHtml: parseHtml,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/divider/plugin.ts",
    "content": "/**\n * @description editor 插件，重写 editor API\n * @author wangfupeng\n */\n\nimport { Transforms, Element } from 'slate'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\n\nfunction withDivider<T extends IDomEditor>(editor: T): T {\n  const { isVoid, normalizeNode } = editor\n  const newEditor = editor\n\n  // 重写 isVoid\n  newEditor.isVoid = elem => {\n    const { type } = elem\n\n    if (type === 'divider') {\n      return true\n    }\n\n    return isVoid(elem)\n  }\n\n  // 重新 normalize\n  newEditor.normalizeNode = ([node, path]) => {\n    const type = DomEditor.getNodeType(node)\n    if (type !== 'divider') {\n      // 未命中 divider ，执行默认的 normalizeNode\n      return normalizeNode([node, path])\n    }\n\n    // -------------- divider 是 editor 最后一个节点，需要后面插入 p --------------\n    const isLast = DomEditor.isLastNode(newEditor, node)\n    if (isLast) {\n      Transforms.insertNodes(newEditor, DomEditor.genEmptyParagraph(), { at: [path[0] + 1] })\n    }\n  }\n\n  // 返回 editor ，重要！\n  return newEditor\n}\n\nexport default withDivider\n"
  },
  {
    "path": "packages/basic-modules/src/modules/divider/render-elem.tsx",
    "content": "/**\n * @description render divider elem\n * @author wangfupeng\n */\n\nimport { Element as SlateElement } from 'slate'\nimport { h, VNode } from 'snabbdom'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\n\nfunction renderDivider(\n  elemNode: SlateElement,\n  children: VNode[] | null,\n  editor: IDomEditor\n): VNode {\n  const renderStyle: any = {}\n\n  // 是否选中\n  const selected = DomEditor.isNodeSelected(editor, elemNode)\n\n  const vnode = h(\n    'div',\n    {\n      props: {\n        contentEditable: false,\n        className: 'w-e-textarea-divider',\n      },\n      dataset: {\n        selected: selected ? 'true' : '',\n      },\n      style: renderStyle,\n      on: {\n        mousedown: event => event.preventDefault(),\n      },\n    },\n    [h('hr')]\n  )\n  // 【注意】void node 中，renderElem 不用处理 children 。core 会统一处理。\n\n  return vnode\n}\n\nconst renderDividerConf = {\n  type: 'divider', // 和 elemNode.type 一致\n  renderElem: renderDivider,\n}\n\nexport { renderDividerConf }\n"
  },
  {
    "path": "packages/basic-modules/src/modules/emotion/index.ts",
    "content": "/**\n * @description emotion entry\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport { emotionMenuConf } from './menu/index'\n\nconst emotion: Partial<IModuleConf> = {\n  menus: [emotionMenuConf],\n}\n\nexport default emotion\n"
  },
  {
    "path": "packages/basic-modules/src/modules/emotion/menu/EmotionMenu.ts",
    "content": "/**\n * @description emotion menu\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport { IDropPanelMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport $, { Dom7Array, DOMElement } from '../../../utils/dom'\nimport { EMOTION_SVG } from '../../../constants/icon-svg'\n\nclass EmotionMenu implements IDropPanelMenu {\n  readonly title = t('emotion.title')\n  readonly iconSvg = EMOTION_SVG\n  readonly tag = 'button'\n  readonly showDropPanel = true // 点击 button 时显示 dropPanel\n  private $content: Dom7Array | null = null\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    // 点击菜单时，弹出 droPanel 之前，不需要执行其他代码\n    // 此处空着即可\n  }\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 不需要 getValue\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 不需要 active\n    return false\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n\n    const [match] = Editor.nodes(editor, {\n      match: n => {\n        const type = DomEditor.getNodeType(n)\n\n        if (type === 'pre') return true // 代码块\n        if (Editor.isVoid(editor, n)) return true // void node\n\n        return false\n      },\n      universal: true,\n    })\n\n    if (match) return true\n    return false\n  }\n\n  getPanelContentElem(editor: IDomEditor): DOMElement {\n    if (this.$content == null) {\n      // 第一次渲染\n      const $content = $('<ul class=\"w-e-panel-content-emotion\"></ul>')\n\n      // 绑定事件（仅第一次绑定，不可重复绑定）\n      $content.on('click', 'li', (e: Event) => {\n        const { target } = e\n        if (target == null) return\n        e.preventDefault()\n\n        const $li = $(target)\n        const emotionStr = $li.text()\n        editor.insertText(emotionStr)\n      })\n\n      this.$content = $content\n    }\n\n    const $content = this.$content\n    if ($content == null) return document.createElement('ul')\n    $content.empty() // 清空之后再重置内容\n\n    // 获取菜单配置\n    const colorConf = editor.getMenuConfig('emotion')\n    const { emotions = [] } = colorConf\n    // 根据菜单配置生成 panel content\n    emotions.forEach((emotion: string) => {\n      const $li = $(`<li>${emotion}</li>`)\n      $content.append($li)\n    })\n\n    return $content[0]\n  }\n}\n\nexport default EmotionMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/emotion/menu/config.ts",
    "content": "/**\n * @description menu config\n * @author wangfupeng\n */\n\nexport function genConfig() {\n  const emotions =\n    '😀 😃 😄 😁 😆 😅 😂 🤣 😊 😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 😛 😝 😜 🤓 😎 😏 😒 😞 😔 😟 😕 🙁 😣 😖 😫 😩 😢 😭 😤 😠 😡 😳 😱 😨 🤗 🤔 😶 😑 😬 🙄 😯 😴 😷 🤑 😈 🤡 💩 👻 💀 👀 👣 👐 🙌 👏 🤝 👍 👎 👊 ✊ 🤛 🤜 🤞 ✌️ 🤘 👌 👈 👉 👆 👇 ☝️ ✋ 🤚 🖐 🖖 👋 🤙 💪 🖕 ✍️ 🙏'\n  return emotions.split(' ')\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/emotion/menu/index.ts",
    "content": "/**\n * @description emotion menu\n * @author wangfupeng\n */\n\nimport EmotionMenu from './EmotionMenu'\nimport { genConfig } from './config'\n\nexport const emotionMenuConf = {\n  key: 'emotion',\n  factory() {\n    return new EmotionMenu()\n  },\n\n  // 默认的菜单菜单配置，将存储在 editorConfig.MENU_CONF[key] 中\n  // 创建编辑器时，可通过 editorConfig.MENU_CONF[key] = {...} 来修改\n  config: {\n    emotions: genConfig(),\n  },\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/font-size-family/custom-types.ts",
    "content": "/**\n * @description 自定义 element\n * @author wangfupeng\n */\n\n//【注意】需要把自定义的 Text 引入到最外层的 custom-types.d.ts\n\nexport type FontSizeAndFamilyText = {\n  text: string\n  fontSize?: string\n  fontFamily?: string\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/font-size-family/index.ts",
    "content": "/**\n * @description font-size font-family\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport { renderStyle } from './render-style'\nimport { styleToHtml } from './style-to-html'\nimport { preParseHtmlConf } from './pre-parse-html'\nimport { parseStyleHtml } from './parse-style-html'\nimport { fontSizeMenuConf, fontFamilyMenuConf } from './menu/index'\n\nconst fontSizeAndFamily: Partial<IModuleConf> = {\n  renderStyle,\n  styleToHtml,\n  preParseHtml: [preParseHtmlConf],\n  parseStyleHtml,\n  menus: [fontSizeMenuConf, fontFamilyMenuConf],\n}\n\nexport default fontSizeAndFamily\n"
  },
  {
    "path": "packages/basic-modules/src/modules/font-size-family/menu/BaseMenu.ts",
    "content": "/**\n * @description header menu\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport { ISelectMenu, IDomEditor, DomEditor, IOption } from '@wangeditor/core'\n\nabstract class BaseMenu implements ISelectMenu {\n  abstract readonly title: string\n  abstract readonly iconSvg: string\n  abstract readonly mark: string // 'fontSize'/'fontFamily'\n  readonly tag = 'select'\n  readonly width = 80\n\n  abstract getOptions(editor: IDomEditor): IOption[]\n\n  isActive(editor: IDomEditor): boolean {\n    // select menu 会显示 selected value ，用不到 active\n    return false\n  }\n\n  getValue(editor: IDomEditor): string | boolean {\n    const mark = this.mark\n    const curMarks = Editor.marks(editor)\n    // @ts-ignore\n    if (curMarks && curMarks[mark]) return curMarks[mark]\n    return ''\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n\n    const mark = this.mark\n    const [match] = Editor.nodes(editor, {\n      match: n => {\n        const type = DomEditor.getNodeType(n)\n        if (type === 'pre') return true // 代码块\n        if (Editor.isVoid(editor, n)) return true // void node\n\n        return false\n      },\n      universal: true,\n    })\n\n    // 匹配到，则禁用\n    if (match) return true\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    const mark = this.mark\n    if (value) {\n      editor.addMark(mark, value)\n    } else {\n      editor.removeMark(mark)\n    }\n  }\n}\n\nexport default BaseMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/font-size-family/menu/FontFamilyMenu.ts",
    "content": "/**\n * @description font-family menu\n * @author wangfupeng\n */\n\nimport { IDomEditor, IOption, t } from '@wangeditor/core'\nimport BaseMenu from './BaseMenu'\nimport { FONT_FAMILY_SVG } from '../../../constants/icon-svg'\n\nclass FontFamilyMenu extends BaseMenu {\n  readonly title = t('fontFamily.title')\n  readonly iconSvg = FONT_FAMILY_SVG\n  readonly mark = 'fontFamily'\n  readonly selectPanelWidth = 150\n\n  getOptions(editor: IDomEditor): IOption[] {\n    const options: IOption[] = []\n\n    // 获取配置，参考 './config.ts'\n    const { fontFamilyList = [] } = editor.getMenuConfig(this.mark)\n\n    // 生成 options\n    options.push({\n      text: t('fontFamily.default'),\n      value: '', // this.getValue(editor) 未找到结果时，会返回 '' ，正好对应到这里\n    })\n    fontFamilyList.forEach((family: string | { name: string; value: string }) => {\n      if (typeof family === 'string') {\n        options.push({\n          text: family,\n          value: family,\n          styleForRenderMenuList: { 'font-family': family },\n        })\n      } else if (typeof family === 'object') {\n        const { name, value } = family\n        options.push({\n          text: name,\n          value,\n          styleForRenderMenuList: { 'font-family': value },\n        })\n      }\n    })\n\n    // 设置 selected\n    const curValue = this.getValue(editor)\n    options.forEach(opt => {\n      if (opt.value === curValue) {\n        opt.selected = true\n      } else {\n        delete opt.selected\n      }\n    })\n\n    return options\n  }\n}\n\nexport default FontFamilyMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/font-size-family/menu/FontSizeMenu.ts",
    "content": "/**\n * @description font-size menu\n * @author wangfupeng\n */\n\nimport { IDomEditor, IOption, t } from '@wangeditor/core'\nimport BaseMenu from './BaseMenu'\nimport { FONT_SIZE_SVG } from '../../../constants/icon-svg'\n\nclass FontSizeMenu extends BaseMenu {\n  readonly title = t('fontSize.title')\n  readonly iconSvg = FONT_SIZE_SVG\n  readonly mark = 'fontSize'\n\n  getOptions(editor: IDomEditor): IOption[] {\n    const options: IOption[] = []\n\n    // 获取配置，参考 './config.ts'\n    const { fontSizeList = [] } = editor.getMenuConfig(this.mark)\n\n    // 生成 options\n    options.push({\n      text: t('fontSize.default'),\n      value: '', // this.getValue(editor) 未找到结果时，会返回 '' ，正好对应到这里\n    })\n    fontSizeList.forEach((size: string | { name: string; value: string }) => {\n      if (typeof size === 'string') {\n        options.push({\n          text: size,\n          value: size,\n        })\n      } else if (typeof size === 'object') {\n        const { name, value } = size\n        options.push({\n          text: name,\n          value: value,\n        })\n      }\n    })\n\n    // 设置 selected\n    const curValue = this.getValue(editor)\n    options.forEach(opt => {\n      if (opt.value === curValue) {\n        opt.selected = true\n      } else {\n        delete opt.selected\n      }\n    })\n\n    return options\n  }\n}\n\nexport default FontSizeMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/font-size-family/menu/config.ts",
    "content": "/**\n * @description font-size font-family config\n * @author wangfupeng\n */\n\nexport function genFontSizeConfig() {\n  const fontSizeList: Array<string | { name: string; value: string }> = [\n    // 元素支持两种形式：1. 字符串；2. { name: 'xxx', value: 'xxx' }\n    '12px',\n    { name: '13px', value: '13px' },\n    '14px',\n    '15px',\n    '16px',\n    '19px',\n    { name: '22px', value: '22px' },\n    '24px',\n    '29px',\n    '32px',\n    '40px',\n    '48px',\n  ]\n\n  return fontSizeList\n}\n\nexport function getFontFamilyConfig() {\n  let fontFamilyList: Array<string | { name: string; value: string }> = [\n    // 元素支持两种形式：1. 字符串；2. { name: 'xxx', value: 'xxx' }\n    '黑体',\n    { name: '仿宋', value: '仿宋' },\n    '楷体',\n    '标楷体',\n    '华文仿宋',\n    '华文楷体',\n    { name: '宋体', value: '宋体' },\n    '微软雅黑',\n    'Arial',\n    'Tahoma',\n    'Verdana',\n    'Times New Roman',\n    'Courier New',\n  ]\n\n  return fontFamilyList\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/font-size-family/menu/index.ts",
    "content": "/**\n * @description font-size font-family menu entry\n * @author wangfupeng\n */\n\nimport FontSizeMenu from './FontSizeMenu'\nimport FontFamilyMenu from './FontFamilyMenu'\nimport { genFontSizeConfig, getFontFamilyConfig } from './config'\n\nexport const fontSizeMenuConf = {\n  key: 'fontSize',\n  factory() {\n    return new FontSizeMenu()\n  },\n\n  // 默认的菜单菜单配置，将存储在 editorConfig.MENU_CONF[key] 中\n  // 创建编辑器时，可通过 editorConfig.MENU_CONF[key] = {...} 来修改\n  config: {\n    fontSizeList: genFontSizeConfig(),\n  },\n}\n\nexport const fontFamilyMenuConf = {\n  key: 'fontFamily',\n  factory() {\n    return new FontFamilyMenu()\n  },\n  config: {\n    fontFamilyList: getFontFamilyConfig(),\n  },\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/font-size-family/parse-style-html.ts",
    "content": "/**\n * @description parse style html\n * @author wangfupeng\n */\n\nimport { Descendant, Text } from 'slate'\nimport { IDomEditor } from '@wangeditor/core'\nimport { FontSizeAndFamilyText } from './custom-types'\nimport $, { DOMElement, getStyleValue } from '../../utils/dom'\n\nexport function parseStyleHtml(text: DOMElement, node: Descendant, editor: IDomEditor): Descendant {\n  const $text = $(text)\n  if (!Text.isText(node)) return node\n\n  const textNode = node as FontSizeAndFamilyText\n\n  // -------- 处理 font-size --------\n  const { fontSizeList = [] } = editor.getMenuConfig('fontSize')\n  const fontSize = getStyleValue($text, 'font-size')\n\n  const includesSize =\n    fontSizeList.find(item => item.value && item.value === fontSize) ||\n    fontSizeList.includes(fontSize)\n\n  if (fontSize && includesSize) {\n    textNode.fontSize = fontSize\n  }\n\n  // -------- 处理 font-family --------\n  const { fontFamilyList = [] } = editor.getMenuConfig('fontFamily')\n  // 这里需要替换掉 \"， css 设置 font-family，会将有空格的字体使用 \" 包裹\n  const fontFamily = getStyleValue($text, 'font-family').replace(/\"/g, '')\n\n  // getFontFamilyConfig 配置支持对象形式\n  const includesFamily =\n    fontFamilyList.find(item => item.value && item.value === fontFamily) ||\n    fontFamilyList.includes(fontFamily)\n\n  if (fontFamily && includesFamily) {\n    textNode.fontFamily = fontFamily\n  }\n\n  return textNode\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/font-size-family/pre-parse-html.ts",
    "content": "/**\n * @description pre-parse html\n * @author wangfupeng\n */\n\nimport $, { DOMElement, getTagName } from '../../utils/dom'\n\n// V4 font-size 对应关系（V4 使用 <font size=\"1\">xxx</font> 格式）\nconst FONT_SIZE_MAP_FOR_V4 = {\n  '1': '12px',\n  '2': '14px',\n  '3': '16px',\n  '4': '19px',\n  '5': '24px',\n  '6': '32px',\n  '7': '48px',\n}\n\n/**\n * pre-prase font ，兼容 V4\n * @param fontElem fontElem\n */\nfunction preParse(fontElem: DOMElement): DOMElement {\n  const $font = $(fontElem)\n  const tagName = getTagName($font)\n  if (tagName !== 'font') return fontElem\n\n  // 处理 size （V4 使用 <font size=\"1\">xxx</font> 格式）\n  const size = $font.attr('size') || ''\n  if (size) {\n    $font.removeAttr('size')\n    $font.css('font-size', FONT_SIZE_MAP_FOR_V4[size])\n  }\n\n  // 处理 face （V4 使用 <font face=\"黑体\">xx</font> 格式）\n  const face = $font.attr('face') || ''\n  if (face) {\n    $font.removeAttr('face')\n    $font.css('font-family', face)\n  }\n\n  return $font[0]\n}\n\nexport const preParseHtmlConf = {\n  selector: 'font',\n  preParseHtml: preParse,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/font-size-family/render-style.tsx",
    "content": "/**\n * @description render font-size font-family style\n * @author wangfupeng\n */\n\nimport { Descendant } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { addVnodeStyle } from '../../utils/vdom'\nimport { FontSizeAndFamilyText } from './custom-types'\n\n/**\n * 添加样式\n * @param node slate elem\n * @param vnode vnode\n * @returns vnode\n */\nexport function renderStyle(node: Descendant, vnode: VNode): VNode {\n  const { fontSize, fontFamily } = node as FontSizeAndFamilyText\n  let styleVnode: VNode = vnode\n\n  if (fontSize) {\n    addVnodeStyle(styleVnode, { fontSize })\n  }\n  if (fontFamily) {\n    addVnodeStyle(styleVnode, { fontFamily })\n  }\n\n  return styleVnode\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/font-size-family/style-to-html.ts",
    "content": "/**\n * @description textStyle to html\n * @author wangfupeng\n */\n\nimport { Text, Descendant } from 'slate'\nimport $, { getOuterHTML, getTagName, isPlainText } from '../../utils/dom'\nimport { FontSizeAndFamilyText } from './custom-types'\n\n/**\n * style to html\n * @param textNode slate text node\n * @param textHtml text html\n * @returns styled html\n */\nexport function styleToHtml(textNode: Descendant, textHtml: string): string {\n  if (!Text.isText(textNode)) return textHtml\n\n  const { fontSize, fontFamily } = textNode as FontSizeAndFamilyText\n  if (!fontSize && !fontFamily) return textHtml\n\n  let $text\n\n  if (isPlainText(textHtml)) {\n    // textHtml 是纯文本，不是 html tag\n    $text = $(`<span>${textHtml}</span>`)\n  } else {\n    // textHtml 是 html tag\n    $text = $(textHtml)\n    const tagName = getTagName($text)\n    if (tagName !== 'span') {\n      // 如果不是 span ，则包裹一层，接下来要设置 css\n      $text = $(`<span>${textHtml}</span>`)\n    }\n  }\n\n  if (fontSize) $text.css('font-size', fontSize)\n  if (fontFamily) $text.css('font-family', fontFamily)\n\n  return getOuterHTML($text)\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/full-screen/index.ts",
    "content": "/**\n * @description 全屏\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport { fullScreenConf } from './menu/index'\n\nconst fullScreen: Partial<IModuleConf> = {\n  menus: [fullScreenConf],\n}\n\nexport default fullScreen\n"
  },
  {
    "path": "packages/basic-modules/src/modules/full-screen/menu/FullScreen.ts",
    "content": "/**\n * @description redo menu\n * @author wangfupeng\n */\n\nimport { IButtonMenu, IDomEditor, t } from '@wangeditor/core'\nimport { FULL_SCREEN_SVG } from '../../../constants/icon-svg'\n\nclass FullScreen implements IButtonMenu {\n  title = t('fullScreen.title')\n  iconSvg = FULL_SCREEN_SVG\n  tag = 'button'\n  alwaysEnable = true\n\n  getValue(editor: IDomEditor): string | boolean {\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    return editor.isFullScreen\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    if (editor.isFullScreen) {\n      editor.unFullScreen()\n    } else {\n      editor.fullScreen()\n    }\n  }\n}\n\nexport default FullScreen\n"
  },
  {
    "path": "packages/basic-modules/src/modules/full-screen/menu/index.ts",
    "content": "/**\n * @description menu entry\n * @author wangfupeng\n */\n\nimport FullScreen from './FullScreen'\n\nexport const fullScreenConf = {\n  key: 'fullScreen',\n  factory() {\n    return new FullScreen()\n  },\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/header/custom-types.ts",
    "content": "/**\n * @description 自定义 element\n * @author wangfupeng\n */\n\nimport { Text } from 'slate'\n\n//【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts\n\nexport type Header1Element = {\n  type: 'header1'\n  children: Text[]\n}\n\nexport type Header2Element = {\n  type: 'header2'\n  children: Text[]\n}\n\nexport type Header3Element = {\n  type: 'header3'\n  children: Text[]\n}\n\nexport type Header4Element = {\n  type: 'header4'\n  children: Text[]\n}\n\nexport type Header5Element = {\n  type: 'header5'\n  children: Text[]\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/header/elem-to-html.ts",
    "content": "/**\n * @description to html\n * @author wangfupeng\n */\n\nimport { Element } from 'slate'\n\nfunction genToHtmlFn(level: number) {\n  function headerToHtml(elem: Element, childrenHtml: string): string {\n    return `<h${level}>${childrenHtml}</h${level}>`\n  }\n  return headerToHtml\n}\n\nexport const header1ToHtmlConf = {\n  type: 'header1',\n  elemToHtml: genToHtmlFn(1),\n}\n\nexport const header2ToHtmlConf = {\n  type: 'header2',\n  elemToHtml: genToHtmlFn(2),\n}\n\nexport const header3ToHtmlConf = {\n  type: 'header3',\n  elemToHtml: genToHtmlFn(3),\n}\n\nexport const header4ToHtmlConf = {\n  type: 'header4',\n  elemToHtml: genToHtmlFn(4),\n}\n\nexport const header5ToHtmlConf = {\n  type: 'header5',\n  elemToHtml: genToHtmlFn(5),\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/header/helper.ts",
    "content": "/**\n * @description header helper\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\n\n/**\n * 获取 node type（'header1' 'header2' 等），未匹配则返回 'paragraph'\n */\nexport function getHeaderType(editor: IDomEditor): string {\n  const [match] = Editor.nodes(editor, {\n    match: n => {\n      const type = DomEditor.getNodeType(n)\n      return type.startsWith('header') // 匹配 node.type 是 header 开头的 node\n    },\n    universal: true,\n  })\n\n  // 未匹配到 header\n  if (match == null) return 'paragraph'\n\n  // 匹配到 header\n  const [n] = match\n\n  return DomEditor.getNodeType(n)\n}\n\nexport function isMenuDisabled(editor: IDomEditor): boolean {\n  if (editor.selection == null) return true\n\n  const [nodeEntry] = Editor.nodes(editor, {\n    match: n => {\n      const type = DomEditor.getNodeType(n)\n\n      // 只可用于 p 和 header\n      if (type === 'paragraph') return true\n      if (type.startsWith('header')) return true\n\n      return false\n    },\n    universal: true,\n    mode: 'highest', // 匹配最高层级\n  })\n\n  // 匹配到 p header ，不禁用\n  if (nodeEntry) {\n    return false\n  }\n  // 未匹配到 p header ，则禁用\n  return true\n}\n\n/**\n * 设置 node type （'header1' 'header2' 'paragraph' 等）\n */\nexport function setHeaderType(editor: IDomEditor, type: string) {\n  if (!type) return\n\n  // 执行命令\n  Transforms.setNodes(editor, {\n    type: type,\n  })\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/header/index.ts",
    "content": "/**\n * @description header entry\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport {\n  renderHeader1Conf,\n  renderHeader2Conf,\n  renderHeader3Conf,\n  renderHeader4Conf,\n  renderHeader5Conf,\n} from './render-elem'\nimport {\n  HeaderSelectMenuConf,\n  Header1ButtonMenuConf,\n  Header2ButtonMenuConf,\n  Header3ButtonMenuConf,\n  Header4ButtonMenuConf,\n  Header5ButtonMenuConf,\n} from './menu/index'\nimport {\n  header1ToHtmlConf,\n  header2ToHtmlConf,\n  header3ToHtmlConf,\n  header4ToHtmlConf,\n  header5ToHtmlConf,\n} from './elem-to-html'\nimport {\n  parseHeader1HtmlConf,\n  parseHeader2HtmlConf,\n  parseHeader3HtmlConf,\n  parseHeader4HtmlConf,\n  parseHeader5HtmlConf,\n} from './parse-elem-html'\nimport withHeader from './plugin'\n\nconst header: Partial<IModuleConf> = {\n  renderElems: [\n    renderHeader1Conf,\n    renderHeader2Conf,\n    renderHeader3Conf,\n    renderHeader4Conf,\n    renderHeader5Conf,\n  ],\n  elemsToHtml: [\n    header1ToHtmlConf,\n    header2ToHtmlConf,\n    header3ToHtmlConf,\n    header4ToHtmlConf,\n    header5ToHtmlConf,\n  ],\n  parseElemsHtml: [\n    parseHeader1HtmlConf,\n    parseHeader2HtmlConf,\n    parseHeader3HtmlConf,\n    parseHeader4HtmlConf,\n    parseHeader5HtmlConf,\n  ],\n  menus: [\n    HeaderSelectMenuConf,\n    Header1ButtonMenuConf,\n    Header2ButtonMenuConf,\n    Header3ButtonMenuConf,\n    Header4ButtonMenuConf,\n    Header5ButtonMenuConf,\n  ],\n  editorPlugin: withHeader,\n}\n\nexport default header\n"
  },
  {
    "path": "packages/basic-modules/src/modules/header/menu/Header1ButtonMenu.ts",
    "content": "/**\n * @description header1 button menu\n * @author wangfupeng\n */\n\nimport HeaderButtonMenuBase from './HeaderButtonMenuBase'\n\nclass Header1ButtonMenu extends HeaderButtonMenuBase {\n  title = 'H1'\n  type = 'header1'\n}\n\nexport default Header1ButtonMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/header/menu/Header2ButtonMenu.ts",
    "content": "/**\n * @description header2 button menu\n * @author wangfupeng\n */\n\nimport HeaderButtonMenuBase from './HeaderButtonMenuBase'\n\nclass Header2ButtonMenu extends HeaderButtonMenuBase {\n  title = 'H2'\n  type = 'header2'\n}\n\nexport default Header2ButtonMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/header/menu/Header3ButtonMenu.ts",
    "content": "/**\n * @description header3 button menu\n * @author wangfupeng\n */\n\nimport HeaderButtonMenuBase from './HeaderButtonMenuBase'\n\nclass Header3ButtonMenu extends HeaderButtonMenuBase {\n  title = 'H3'\n  type = 'header3'\n}\n\nexport default Header3ButtonMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/header/menu/Header4ButtonMenu.ts",
    "content": "/**\n * @description header4 button menu\n * @author wangfupeng\n */\n\nimport HeaderButtonMenuBase from './HeaderButtonMenuBase'\n\nclass Header4ButtonMenu extends HeaderButtonMenuBase {\n  title = 'H4'\n  type = 'header4'\n}\n\nexport default Header4ButtonMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/header/menu/Header5ButtonMenu.ts",
    "content": "/**\n * @description header5 button menu\n * @author wangfupeng\n */\n\nimport HeaderButtonMenuBase from './HeaderButtonMenuBase'\n\nclass Header5ButtonMenu extends HeaderButtonMenuBase {\n  title = 'H5'\n  type = 'header5'\n}\n\nexport default Header5ButtonMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/header/menu/HeaderButtonMenuBase.ts",
    "content": "/**\n * @description button menu base\n * @author wangfupeng\n */\n\nimport { IButtonMenu, IDomEditor } from '@wangeditor/core'\nimport { getHeaderType, isMenuDisabled, setHeaderType } from '../helper'\n\nabstract class HeaderButtonMenuBase implements IButtonMenu {\n  abstract readonly title: string\n  abstract readonly type: string // 'header1' 'header2' 等\n  readonly tag = 'button'\n\n  /**\n   * 获取选中节点的 node.type\n   * @param editor editor\n   */\n  getValue(editor: IDomEditor): string | boolean {\n    return getHeaderType(editor)\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    return this.getValue(editor) === this.type\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    return isMenuDisabled(editor)\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    const { type } = this\n    let newType\n    if (value === type) {\n      // 选中的 node.type 和当前 type 一样，则取消\n      newType = 'paragraph'\n    } else {\n      // 否则，则设置\n      newType = type\n    }\n\n    setHeaderType(editor, newType)\n  }\n}\n\nexport default HeaderButtonMenuBase\n"
  },
  {
    "path": "packages/basic-modules/src/modules/header/menu/HeaderSelectMenu.ts",
    "content": "/**\n * @description header menu\n * @author wangfupeng\n */\n\nimport { ISelectMenu, IDomEditor, IOption, t } from '@wangeditor/core'\nimport { HEADER_SVG } from '../../../constants/icon-svg'\nimport { getHeaderType, isMenuDisabled, setHeaderType } from '../helper'\n\nclass HeaderSelectMenu implements ISelectMenu {\n  readonly title = t('header.title')\n  readonly iconSvg = HEADER_SVG\n  readonly tag = 'select'\n  readonly width = 60\n\n  getOptions(editor: IDomEditor): IOption[] {\n    // 基本的 options 列表\n    const options = [\n      // value 和 elemNode.type 对应\n      {\n        value: 'header1',\n        text: 'H1',\n        styleForRenderMenuList: { 'font-size': '32px', 'font-weight': 'bold' },\n      },\n      {\n        value: 'header2',\n        text: 'H2',\n        styleForRenderMenuList: { 'font-size': '24px', 'font-weight': 'bold' },\n      },\n      {\n        value: 'header3',\n        text: 'H3',\n        styleForRenderMenuList: { 'font-size': '18px', 'font-weight': 'bold' },\n      },\n      {\n        value: 'header4',\n        text: 'H4',\n        styleForRenderMenuList: { 'font-size': '16px', 'font-weight': 'bold' },\n      },\n      {\n        value: 'header5',\n        text: 'H5',\n        styleForRenderMenuList: { 'font-size': '13px', 'font-weight': 'bold' },\n      },\n      { value: 'paragraph', text: t('header.text') },\n    ]\n\n    // 获取 value ，设置 selected\n    const curValue = this.getValue(editor).toString()\n    options.forEach((opt: IOption) => {\n      if (opt.value === curValue) {\n        opt.selected = true\n      } else {\n        delete opt.selected\n      }\n    })\n\n    return options\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // select menu 会显示 selected value ，用不到 active\n    return false\n  }\n\n  /**\n   * 获取选中节点的 node.type\n   * @param editor editor\n   */\n  getValue(editor: IDomEditor): string | boolean {\n    return getHeaderType(editor)\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    return isMenuDisabled(editor)\n  }\n\n  /**\n   * 执行命令\n   * @param editor editor\n   * @param value node.type\n   */\n  exec(editor: IDomEditor, value: string | boolean) {\n    //【注意】value 是 select change 时获取的，并不是 this.getValue 的值\n    setHeaderType(editor, value.toString())\n  }\n}\n\nexport default HeaderSelectMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/header/menu/index.ts",
    "content": "/**\n * @description menu entry\n * @author wangfupeng\n */\n\nimport HeaderSelectMenu from './HeaderSelectMenu'\nimport Header1ButtonMenu from './Header1ButtonMenu'\nimport Header2ButtonMenu from './Header2ButtonMenu'\nimport Header3ButtonMenu from './Header3ButtonMenu'\nimport Header4ButtonMenu from './Header4ButtonMenu'\nimport Header5ButtonMenu from './Header5ButtonMenu'\n\nexport const HeaderSelectMenuConf = {\n  key: 'headerSelect',\n  factory() {\n    return new HeaderSelectMenu()\n  },\n}\n\nexport const Header1ButtonMenuConf = {\n  key: 'header1',\n  factory() {\n    return new Header1ButtonMenu()\n  },\n}\n\nexport const Header2ButtonMenuConf = {\n  key: 'header2',\n  factory() {\n    return new Header2ButtonMenu()\n  },\n}\n\nexport const Header3ButtonMenuConf = {\n  key: 'header3',\n  factory() {\n    return new Header3ButtonMenu()\n  },\n}\n\nexport const Header4ButtonMenuConf = {\n  key: 'header4',\n  factory() {\n    return new Header4ButtonMenu()\n  },\n}\n\nexport const Header5ButtonMenuConf = {\n  key: 'header5',\n  factory() {\n    return new Header5ButtonMenu()\n  },\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/header/parse-elem-html.ts",
    "content": "/**\n * @description parse html\n * @author wangfupeng\n */\n\nimport { Descendant, Text } from 'slate'\nimport $, { DOMElement } from '../../utils/dom'\nimport { IDomEditor } from '@wangeditor/core'\nimport {\n  Header1Element,\n  Header2Element,\n  Header3Element,\n  Header4Element,\n  Header5Element,\n} from './custom-types'\n\nfunction genParser<T>(level: number) {\n  function parseHtml(elem: DOMElement, children: Descendant[], editor: IDomEditor): T {\n    const $elem = $(elem)\n    children = children.filter(child => {\n      if (Text.isText(child)) return true\n      if (editor.isInline(child)) return true\n      return false\n    })\n\n    // 无 children ，则用纯文本\n    if (children.length === 0) {\n      children = [{ text: $elem.text().replace(/\\s+/gm, ' ') }]\n    }\n\n    const headerNode = {\n      type: `header${level}`,\n      children,\n    } as unknown as T\n\n    return headerNode\n  }\n  return parseHtml\n}\n\nexport const parseHeader1HtmlConf = {\n  selector: 'h1:not([data-w-e-type])', // data-w-e-type 属性，留给自定义元素，保证扩展性\n  parseElemHtml: genParser<Header1Element>(1),\n}\n\nexport const parseHeader2HtmlConf = {\n  selector: 'h2:not([data-w-e-type])', // data-w-e-type 属性，留给自定义元素，保证扩展性\n  parseElemHtml: genParser<Header2Element>(2),\n}\n\nexport const parseHeader3HtmlConf = {\n  selector: 'h3:not([data-w-e-type])', // data-w-e-type 属性，留给自定义元素，保证扩展性\n  parseElemHtml: genParser<Header3Element>(3),\n}\n\nexport const parseHeader4HtmlConf = {\n  selector: 'h4:not([data-w-e-type])', // data-w-e-type 属性，留给自定义元素，保证扩展性\n  parseElemHtml: genParser<Header4Element>(4),\n}\n\nexport const parseHeader5HtmlConf = {\n  selector: 'h5:not([data-w-e-type])', // data-w-e-type 属性，留给自定义元素，保证扩展性\n  parseElemHtml: genParser<Header5Element>(5),\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/header/plugin.ts",
    "content": "/**\n * @description editor 插件，重写 editor API\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\n\nfunction withHeader<T extends IDomEditor>(editor: T): T {\n  const { insertBreak, insertNode } = editor\n  const newEditor = editor\n\n  // 重写 insertBreak - header 末尾回车时要插入 paragraph\n  newEditor.insertBreak = () => {\n    const [match] = Editor.nodes(newEditor, {\n      match: n => {\n        const type = DomEditor.getNodeType(n)\n        return type.startsWith('header') // 匹配 node.type 是 header 开头的 node\n      },\n      universal: true,\n    })\n\n    if (!match) {\n      // 未匹配到\n      insertBreak()\n      return\n    }\n\n    const isAtLineEnd = DomEditor.isSelectionAtLineEnd(editor, match[1])\n\n    // 如果在行末插入一个空 p，否则正常换行\n    if (isAtLineEnd) {\n      const p = { type: 'paragraph', children: [{ text: '' }] }\n      Transforms.insertNodes(newEditor, p, { mode: 'highest' })\n    } else {\n      insertBreak()\n    }\n  }\n\n  // 返回 editor ，重要！\n  return newEditor\n}\n\nexport default withHeader\n"
  },
  {
    "path": "packages/basic-modules/src/modules/header/render-elem.tsx",
    "content": "/**\n * @description render header\n * @author wangfupeng\n */\n\nimport { Element as SlateElement } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { IDomEditor } from '@wangeditor/core'\n\nfunction genRenderElem(level: number) {\n  /**\n   * render header elem\n   * @param elemNode slate elem\n   * @param children children\n   * @param editor editor\n   * @returns vnode\n   */\n  function renderHeader(\n    elemNode: SlateElement,\n    children: VNode[] | null,\n    editor: IDomEditor\n  ): VNode {\n    const Tag = `h${level}`\n    const vnode = <Tag>{children}</Tag>\n    return vnode\n  }\n\n  return renderHeader\n}\n\nconst renderHeader1Conf = {\n  type: 'header1', // 和 elemNode.type 一致\n  renderElem: genRenderElem(1),\n}\nconst renderHeader2Conf = {\n  type: 'header2',\n  renderElem: genRenderElem(2),\n}\nconst renderHeader3Conf = {\n  type: 'header3',\n  renderElem: genRenderElem(3),\n}\nconst renderHeader4Conf = {\n  type: 'header4',\n  renderElem: genRenderElem(4),\n}\nconst renderHeader5Conf = {\n  type: 'header5',\n  renderElem: genRenderElem(5),\n}\n\nexport {\n  renderHeader1Conf,\n  renderHeader2Conf,\n  renderHeader3Conf,\n  renderHeader4Conf,\n  renderHeader5Conf,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/image/custom-types.ts",
    "content": "/**\n * @description image element\n * @author wangfupeng\n */\n\n//【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts\n\ntype EmptyText = {\n  text: ''\n}\n\nexport type ImageStyle = {\n  width?: string\n  height?: string\n}\n\nexport type ImageElement = {\n  type: 'image'\n  src: string\n  alt?: string\n  href?: string\n  style?: ImageStyle\n  children: EmptyText[]\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/image/elem-to-html.ts",
    "content": "/**\n * @description to html\n * @author wangfupeng\n */\n\nimport { Element } from 'slate'\nimport { ImageElement } from './custom-types'\n\nfunction imageToHtml(elemNode: Element, childrenHtml: string): string {\n  const { src, alt = '', href = '', style = {} } = elemNode as ImageElement\n  const { width = '', height = '' } = style\n\n  let styleStr = ''\n  if (width) styleStr += `width: ${width};`\n  if (height) styleStr += `height: ${height};`\n  return `<img src=\"${src}\" alt=\"${alt}\" data-href=\"${href}\" style=\"${styleStr}\"/>`\n}\n\nexport const imageToHtmlConf = {\n  type: 'image',\n  elemToHtml: imageToHtml,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/image/helper.ts",
    "content": "/**\n * @description image menu helper\n * @author wangfupeng\n */\n\nimport { Transforms, Range, Editor } from 'slate'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\nimport { ImageElement, ImageStyle } from './custom-types'\nimport { replaceSymbols } from '../../utils/util'\n\nasync function check(\n  menuKey: string,\n  editor: IDomEditor,\n  src: string,\n  alt: string = '',\n  href: string = ''\n): Promise<boolean> {\n  const { checkImage } = editor.getMenuConfig(menuKey)\n  if (checkImage) {\n    const res = await checkImage(src, alt, href)\n    if (typeof res === 'string') {\n      // 检验未通过，提示信息\n      editor.alert(res, 'error')\n      return false\n    }\n    if (res == null) {\n      // 检验未通过，不提示信息\n      return false\n    }\n  }\n\n  return true\n}\n\nasync function parseSrc(menuKey: string, editor: IDomEditor, src: string): Promise<string> {\n  const { parseImageSrc } = editor.getMenuConfig(menuKey)\n  if (parseImageSrc) {\n    const newSrc = await parseImageSrc(src)\n    return newSrc\n  }\n  return src\n}\n\nexport async function insertImageNode(\n  editor: IDomEditor,\n  src: string,\n  alt: string = '',\n  href: string = ''\n) {\n  const res = await check('insertImage', editor, src, alt, href)\n  if (!res) return // 检查失败，终止操作\n\n  const parsedSrc = await parseSrc('insertImage', editor, src)\n\n  // 新建一个 image node\n  const image: ImageElement = {\n    type: 'image',\n    src: replaceSymbols(parsedSrc),\n    href,\n    alt,\n    style: {},\n    children: [{ text: '' }], // 【注意】void node 需要一个空 text 作为 children\n  }\n\n  // 如果 blur ，则恢复选区\n  if (editor.selection === null) editor.restoreSelection()\n\n  // 如果当前正好选中了图片，则 move 一下（如：连续上传多张图片时）\n  if (DomEditor.getSelectedNodeByType(editor, 'image')) {\n    editor.move(1)\n  }\n\n  if (isInsertImageMenuDisabled(editor)) return\n\n  // 插入图片\n  Transforms.insertNodes(editor, image)\n\n  // 回调\n  const { onInsertedImage } = editor.getMenuConfig('insertImage')\n  if (onInsertedImage) onInsertedImage(image)\n}\n\nexport async function updateImageNode(\n  editor: IDomEditor,\n  src: string,\n  alt: string = '',\n  href: string = '',\n  style: ImageStyle = {}\n) {\n  const res = await check('editImage', editor, src, alt, href)\n  if (!res) return // 检查失败，终止操作\n\n  const parsedSrc = await parseSrc('editImage', editor, src)\n\n  const selectedImageNode = DomEditor.getSelectedNodeByType(editor, 'image')\n  if (selectedImageNode == null) return\n  const { style: curStyle = {} } = selectedImageNode as ImageElement\n\n  // 修改图片\n  const nodeProps: Partial<ImageElement> = {\n    src: parsedSrc,\n    alt,\n    href,\n    style: {\n      ...curStyle,\n      ...style,\n    },\n  }\n  Transforms.setNodes(editor, nodeProps, {\n    match: n => DomEditor.checkNodeType(n, 'image'),\n  })\n\n  // 回调\n  const imageNode = DomEditor.getSelectedNodeByType(editor, 'image')\n  const { onUpdatedImage } = editor.getMenuConfig('editImage')\n  if (onUpdatedImage) onUpdatedImage(imageNode)\n}\n\n/**\n * 判断菜单是否要 disabled\n * @param editor editor\n */\nexport function isInsertImageMenuDisabled(editor: IDomEditor): boolean {\n  const { selection } = editor\n  if (selection == null) return true\n  if (!Range.isCollapsed(selection)) return true // 选区非折叠，禁用\n\n  const [match] = Editor.nodes(editor, {\n    match: n => {\n      const type = DomEditor.getNodeType(n)\n\n      if (type === 'code') return true // 代码块\n      if (type === 'pre') return true // 代码块\n      if (type === 'link') return true // 链接\n      if (type === 'list-item') return true // list\n      if (type.startsWith('header')) return true // 标题\n      if (type === 'blockquote') return true // 引用\n      if (Editor.isVoid(editor, n)) return true // void\n\n      return false\n    },\n    universal: true,\n  })\n\n  if (match) return true\n  return false\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/image/index.ts",
    "content": "/**\n * @description image module entry\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport withImage from './plugin'\nimport { renderImageConf } from './render-elem'\nimport { imageToHtmlConf } from './elem-to-html'\nimport { parseHtmlConf } from './parse-elem-html'\nimport {\n  insertImageMenuConf,\n  deleteImageMenuConf,\n  editImageMenuConf,\n  viewImageLinkMenuConf,\n  imageWidth30MenuConf,\n  imageWidth50MenuConf,\n  imageWidth100MenuConf,\n} from './menu/index'\n\nconst image: Partial<IModuleConf> = {\n  renderElems: [renderImageConf],\n  elemsToHtml: [imageToHtmlConf],\n  parseElemsHtml: [parseHtmlConf],\n  menus: [\n    insertImageMenuConf,\n    deleteImageMenuConf,\n    editImageMenuConf,\n    viewImageLinkMenuConf,\n    imageWidth30MenuConf,\n    imageWidth50MenuConf,\n    imageWidth100MenuConf,\n  ],\n  editorPlugin: withImage,\n}\n\nexport default image\n"
  },
  {
    "path": "packages/basic-modules/src/modules/image/menu/DeleteImage.ts",
    "content": "/**\n * @description delete image menu\n * @author wangfupeng\n */\n\nimport { Transforms } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport { TRASH_SVG } from '../../../constants/icon-svg'\n\nclass DeleteImage implements IButtonMenu {\n  readonly title = t('image.delete')\n  readonly iconSvg = TRASH_SVG\n  readonly tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 无需获取 val\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 无需 active\n    return false\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n\n    const imageNode = DomEditor.getSelectedNodeByType(editor, 'image')\n    if (imageNode == null) {\n      // 选区未处于 image node ，则禁用\n      return true\n    }\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    if (this.isDisabled(editor)) return\n\n    // 删除图片\n    Transforms.removeNodes(editor, {\n      match: n => DomEditor.checkNodeType(n, 'image'),\n    })\n  }\n}\n\nexport default DeleteImage\n"
  },
  {
    "path": "packages/basic-modules/src/modules/image/menu/EditImage.ts",
    "content": "/**\n * @description editor image menu\n * @author wangfupeng\n */\n\nimport { Node, Range } from 'slate'\nimport {\n  IModalMenu,\n  IDomEditor,\n  DomEditor,\n  genModalInputElems,\n  genModalButtonElems,\n  t,\n} from '@wangeditor/core'\nimport $, { Dom7Array, DOMElement } from '../../../utils/dom'\nimport { genRandomStr } from '../../../utils/util'\nimport { PENCIL_SVG } from '../../../constants/icon-svg'\nimport { updateImageNode } from '../helper'\nimport { ImageElement, ImageStyle } from '../custom-types'\n\n/**\n * 生成唯一的 DOM ID\n */\nfunction genDomID(): string {\n  return genRandomStr('w-e-edit-image')\n}\n\nclass EditImage implements IModalMenu {\n  readonly title = t('image.edit')\n  readonly iconSvg = PENCIL_SVG\n  readonly tag = 'button'\n  readonly showModal = true // 点击 button 时显示 modal\n  readonly modalWidth = 300\n  private $content: Dom7Array | null = null\n  private readonly srcInputId = genDomID()\n  private readonly altInputId = genDomID()\n  private readonly hrefInputId = genDomID()\n  private readonly buttonId = genDomID()\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 编辑图片，用不到 getValue\n    return ''\n  }\n\n  private getImageNode(editor: IDomEditor): Node | null {\n    return DomEditor.getSelectedNodeByType(editor, 'image')\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 无需 active\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    // 点击菜单时，弹出 modal 之前，不需要执行其他代码\n    // 此处空着即可\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    const { selection } = editor\n    if (selection == null) return true\n    if (!Range.isCollapsed(selection)) return true // 选区非折叠，禁用\n\n    const imageNode = DomEditor.getSelectedNodeByType(editor, 'image')\n\n    // 未匹配到 image node 则禁用\n    if (imageNode == null) return true\n    return false\n  }\n\n  getModalPositionNode(editor: IDomEditor): Node | null {\n    return this.getImageNode(editor)\n  }\n\n  getModalContentElem(editor: IDomEditor): DOMElement {\n    const { srcInputId, altInputId, hrefInputId, buttonId } = this\n\n    const selectedImageNode = this.getImageNode(editor)\n    if (selectedImageNode == null) {\n      throw new Error('Not found selected image node')\n    }\n\n    // 获取 input button elem\n    const [srcContainerElem, inputSrcElem] = genModalInputElems(t('image.src'), srcInputId)\n    const $inputSrc = $(inputSrcElem)\n    const [altContainerElem, inputAltElem] = genModalInputElems(t('image.desc'), altInputId)\n    const $inputAlt = $(inputAltElem)\n    const [hrefContainerElem, inputHrefElem] = genModalInputElems(t('image.link'), hrefInputId)\n    const $inputHref = $(inputHrefElem)\n    const [buttonContainerElem] = genModalButtonElems(buttonId, t('common.ok'))\n\n    if (this.$content == null) {\n      // 第一次渲染\n      const $content = $('<div></div>')\n\n      // 绑定事件（第一次渲染时绑定，不要重复绑定）\n      $content.on('click', `#${buttonId}`, e => {\n        e.preventDefault()\n\n        const src = $content.find(`#${srcInputId}`).val()\n        const alt = $content.find(`#${altInputId}`).val()\n        const href = $content.find(`#${hrefInputId}`).val()\n        this.updateImage(editor, src, alt, href)\n        editor.hidePanelOrModal() // 隐藏 modal\n      })\n\n      // 记录属性，重要\n      this.$content = $content\n    }\n\n    const $content = this.$content\n    $content.empty() // 先清空内容\n\n    // append inputs and button\n    $content.append(srcContainerElem)\n    $content.append(altContainerElem)\n    $content.append(hrefContainerElem)\n    $content.append(buttonContainerElem)\n\n    // 设置 input val\n    const { src, alt = '', href = '' } = selectedImageNode as ImageElement\n    $inputSrc.val(src)\n    $inputAlt.val(alt)\n    $inputHref.val(href)\n\n    // focus 一个 input（异步，此时 DOM 尚未渲染）\n    setTimeout(() => {\n      $inputSrc.focus()\n    })\n\n    return $content[0]\n  }\n\n  private updateImage(\n    editor: IDomEditor,\n    src: string,\n    alt: string = '',\n    href: string = '',\n    style: ImageStyle = {}\n  ) {\n    if (!src) return\n\n    // 还原选区\n    editor.restoreSelection()\n\n    if (this.isDisabled(editor)) return\n\n    // 修改图片信息\n    updateImageNode(editor, src, alt, href, style)\n  }\n}\n\nexport default EditImage\n"
  },
  {
    "path": "packages/basic-modules/src/modules/image/menu/InsertImage.ts",
    "content": "/**\n * @description insert image menu\n * @author wangfupeng\n */\n\nimport { Node } from 'slate'\nimport {\n  IModalMenu,\n  IDomEditor,\n  genModalInputElems,\n  genModalButtonElems,\n  t,\n} from '@wangeditor/core'\nimport $, { Dom7Array, DOMElement } from '../../../utils/dom'\nimport { genRandomStr } from '../../../utils/util'\nimport { IMAGE_SVG } from '../../../constants/icon-svg'\nimport { insertImageNode, isInsertImageMenuDisabled } from '../helper'\n\n/**\n * 生成唯一的 DOM ID\n */\nfunction genDomID(): string {\n  return genRandomStr('w-e-insert-image')\n}\n\nclass InsertImage implements IModalMenu {\n  readonly title = t('image.netImage')\n  readonly iconSvg = IMAGE_SVG\n  readonly tag = 'button'\n  readonly showModal = true // 点击 button 时显示 modal\n  readonly modalWidth = 300\n  private $content: Dom7Array | null = null\n  private readonly srcInputId = genDomID()\n  private readonly altInputId = genDomID()\n  private readonly hrefInputId = genDomID()\n  private readonly buttonId = genDomID()\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 插入菜单，不需要 value\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 任何时候，都不用激活 menu\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    // 点击菜单时，弹出 modal 之前，不需要执行其他代码\n    // 此处空着即可\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    return isInsertImageMenuDisabled(editor)\n  }\n\n  getModalPositionNode(editor: IDomEditor): Node | null {\n    return null // modal 依据选区定位\n  }\n\n  getModalContentElem(editor: IDomEditor): DOMElement {\n    const { srcInputId, altInputId, hrefInputId, buttonId } = this\n\n    // 获取 input button elem\n    const [srcContainerElem, inputSrcElem] = genModalInputElems(t('image.src'), srcInputId)\n    const $inputSrc = $(inputSrcElem)\n    const [altContainerElem, inputAltElem] = genModalInputElems(t('image.desc'), altInputId)\n    const $inputAlt = $(inputAltElem)\n    const [hrefContainerElem, inputHrefElem] = genModalInputElems(t('image.link'), hrefInputId)\n    const $inputHref = $(inputHrefElem)\n    const [buttonContainerElem] = genModalButtonElems(buttonId, t('common.ok'))\n\n    if (this.$content == null) {\n      // 第一次渲染\n      const $content = $('<div></div>')\n\n      // 绑定事件（第一次渲染时绑定，不要重复绑定）\n      $content.on('click', `#${buttonId}`, e => {\n        e.preventDefault()\n        const src = $content.find(`#${srcInputId}`).val().trim()\n        const alt = $content.find(`#${altInputId}`).val().trim()\n        const href = $content.find(`#${hrefInputId}`).val().trim()\n        this.insertImage(editor, src, alt, href)\n        editor.hidePanelOrModal() // 隐藏 modal\n      })\n\n      // 记录属性，重要\n      this.$content = $content\n    }\n\n    const $content = this.$content\n    $content.empty() // 先清空内容\n\n    // append inputs and button\n    $content.append(srcContainerElem)\n    $content.append(altContainerElem)\n    $content.append(hrefContainerElem)\n    $content.append(buttonContainerElem)\n\n    // 设置 input val\n    $inputSrc.val('')\n    $inputAlt.val('')\n    $inputHref.val('')\n\n    // focus 一个 input（异步，此时 DOM 尚未渲染）\n    setTimeout(() => {\n      $inputSrc.focus()\n    })\n\n    return $content[0]\n  }\n\n  private insertImage(editor: IDomEditor, src: string, alt: string = '', href: string = '') {\n    if (!src) return\n\n    // 还原选区\n    editor.restoreSelection()\n\n    if (this.isDisabled(editor)) return\n\n    // 插入图片\n    insertImageNode(editor, src, alt, href)\n  }\n}\n\nexport default InsertImage\n"
  },
  {
    "path": "packages/basic-modules/src/modules/image/menu/ViewImageLink.ts",
    "content": "/**\n * @description view image link menu\n * @author wangfupeng\n */\n\nimport { IButtonMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport { EXTERNAL_SVG } from '../../../constants/icon-svg'\nimport { ImageElement } from '../custom-types'\n\nclass ViewImageLink implements IButtonMenu {\n  readonly title = t('image.viewLink')\n  readonly iconSvg = EXTERNAL_SVG\n  readonly tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    const imageNode = DomEditor.getSelectedNodeByType(editor, 'image')\n    if (imageNode) {\n      // 选区处于 image node\n      return (imageNode as ImageElement).href || ''\n    }\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 无需 active\n    return false\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n\n    const href = this.getValue(editor)\n    if (href) {\n      // 有 image href ，则不禁用\n      return false\n    }\n    return true\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    if (this.isDisabled(editor)) return\n\n    if (!value || typeof value !== 'string') {\n      throw new Error(`View image link failed, image.href is '${value}'`)\n      return\n    }\n\n    // 查看链接\n    window.open(value, '_blank')\n  }\n}\n\nexport default ViewImageLink\n"
  },
  {
    "path": "packages/basic-modules/src/modules/image/menu/Width100.ts",
    "content": "/**\n * @description image width 100%\n * @author wangfupeng\n */\n\nimport ImageWidthBaseClass from './WidthBase'\n\nclass ImageWidth100 extends ImageWidthBaseClass {\n  readonly title = '100%' // 菜单标题\n  readonly value = '100%' // css width 的值\n}\n\nexport default ImageWidth100\n"
  },
  {
    "path": "packages/basic-modules/src/modules/image/menu/Width30.ts",
    "content": "/**\n * @description image width 30%\n * @author wangfupeng\n */\n\nimport ImageWidthBaseClass from './WidthBase'\n\nclass ImageWidth30 extends ImageWidthBaseClass {\n  readonly title = '30%' // 菜单标题\n  readonly value = '30%' // css width 的值\n}\n\nexport default ImageWidth30\n"
  },
  {
    "path": "packages/basic-modules/src/modules/image/menu/Width50.ts",
    "content": "/**\n * @description image width 50%\n * @author wangfupeng\n */\n\nimport ImageWidthBaseClass from './WidthBase'\n\nclass ImageWidth50 extends ImageWidthBaseClass {\n  readonly title = '50%' // 菜单标题\n  readonly value = '50%' // css width 的值\n}\n\nexport default ImageWidth50\n"
  },
  {
    "path": "packages/basic-modules/src/modules/image/menu/WidthBase.ts",
    "content": "/**\n * @description image width base class\n * @author wangfupeng\n */\n\nimport { Transforms, Node } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor } from '@wangeditor/core'\nimport { ImageElement } from '../custom-types'\n\nabstract class ImageWidthBaseClass implements IButtonMenu {\n  abstract readonly title: string // 菜单标题\n  readonly tag = 'button'\n  abstract readonly value: string // css width 的值\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 无需获取 val\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 无需 active\n    return false\n  }\n\n  private getSelectedNode(editor: IDomEditor): Node | null {\n    return DomEditor.getSelectedNodeByType(editor, 'image')\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n\n    const imageNode = this.getSelectedNode(editor)\n    if (imageNode == null) {\n      // 选区未处于 image node ，则禁用\n      return true\n    }\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    if (this.isDisabled(editor)) return\n\n    const imageNode = this.getSelectedNode(editor)\n    if (imageNode == null) return\n\n    // 隐藏 hoverbar\n    const hoverbar = DomEditor.getHoverbar(editor)\n    if (hoverbar) hoverbar.hideAndClean()\n\n    const { style = {} } = imageNode as ImageElement\n    const props: Partial<ImageElement> = {\n      style: {\n        ...style,\n        width: this.value, // 修改 width\n        height: '', // 清空 height\n      },\n    }\n\n    Transforms.setNodes(editor, props, {\n      match: n => DomEditor.checkNodeType(n, 'image'),\n    })\n  }\n}\n\nexport default ImageWidthBaseClass\n"
  },
  {
    "path": "packages/basic-modules/src/modules/image/menu/config.ts",
    "content": "/**\n * @description 图片菜单配置\n * @author wangfupeng\n */\n\nimport { ImageElement } from '../custom-types'\n\nexport function genImageMenuConfig() {\n  return {\n    /**\n     * 插入图片之后的回调\n     * @param imageElem ImageElement\n     */\n    onInsertedImage(imageElem: ImageElement) {\n      /*自定义*/\n    },\n\n    /**\n     * 更新图片之后的回调\n     * @param node image node\n     */\n    onUpdatedImage(node: ImageElement | null) {\n      /*自定义*/\n    },\n\n    /**\n     * 检查图片信息，支持 async fn\n     * @param src image src\n     * @param alt image alt\n     * @param href image href\n     */\n    checkImage(src: string, alt: string, href: string): boolean | string | undefined {\n      // 1. 返回 true ，说明检查通过\n      // 2. 返回一个字符串，说明检查未通过，编辑器会阻止图片插入。会 alert 出错误信息（即返回的字符串）\n      // 3. 返回 undefined（即没有任何返回），说明检查未通过，编辑器会阻止图片插入\n      return true\n    },\n\n    /**\n     * parse image src\n     * @param src image src\n     * @returns new src\n     */\n    parseImageSrc(src: string): string {\n      return src\n    },\n  }\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/image/menu/index.ts",
    "content": "/**\n * @description image menu entry\n * @author wangfupeng\n */\n\nimport InsertImage from './InsertImage'\nimport DeleteImage from './DeleteImage'\nimport EditImage from './EditImage'\nimport ViewImageLink from './ViewImageLink'\nimport ImageWidth30 from './Width30'\nimport ImageWidth50 from './Width50'\nimport ImageWidth100 from './Width100'\nimport { genImageMenuConfig } from './config'\n\nconst config = genImageMenuConfig() // menu config\n\nexport const insertImageMenuConf = {\n  key: 'insertImage',\n  factory() {\n    return new InsertImage()\n  },\n\n  // 默认的菜单菜单配置，将存储在 editorConfig.MENU_CONF[key] 中\n  // 创建编辑器时，可通过 editorConfig.MENU_CONF[key] = {...} 来修改\n  config,\n}\n\nexport const deleteImageMenuConf = {\n  key: 'deleteImage',\n  factory() {\n    return new DeleteImage()\n  },\n}\n\nexport const editImageMenuConf = {\n  key: 'editImage',\n  factory() {\n    return new EditImage()\n  },\n  config,\n}\n\nexport const viewImageLinkMenuConf = {\n  key: 'viewImageLink',\n  factory() {\n    return new ViewImageLink()\n  },\n}\n\nexport const imageWidth30MenuConf = {\n  key: 'imageWidth30',\n  factory() {\n    return new ImageWidth30()\n  },\n}\n\nexport const imageWidth50MenuConf = {\n  key: 'imageWidth50',\n  factory() {\n    return new ImageWidth50()\n  },\n}\n\nexport const imageWidth100MenuConf = {\n  key: 'imageWidth100',\n  factory() {\n    return new ImageWidth100()\n  },\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/image/parse-elem-html.ts",
    "content": "/**\n * @description parse html\n * @author wangfupeng\n */\n\nimport { Descendant } from 'slate'\nimport { IDomEditor } from '@wangeditor/core'\nimport { ImageElement } from './custom-types'\nimport $, { DOMElement, getStyleValue } from '../../utils/dom'\n\nfunction parseHtml(elem: DOMElement, children: Descendant[], editor: IDomEditor): ImageElement {\n  const $elem = $(elem)\n  let href = $elem.attr('data-href') || ''\n  href = decodeURIComponent(href) // 兼容 V4\n\n  return {\n    type: 'image',\n    src: $elem.attr('src') || '',\n    alt: $elem.attr('alt') || '',\n    href,\n    style: {\n      width: getStyleValue($elem, 'width'),\n      height: getStyleValue($elem, 'height'),\n    },\n    children: [{ text: '' }], // void node 有一个空白 text\n  }\n}\n\nexport const parseHtmlConf = {\n  selector: 'img:not([data-w-e-type])', // data-w-e-type 属性，留给自定义元素，保证扩展性\n  parseElemHtml: parseHtml,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/image/plugin.ts",
    "content": "/**\n * @description editor 插件，重写 editor API\n * @author wangfupeng\n */\n\n// import { Editor, Path, Operation } from 'slate'\nimport { IDomEditor } from '@wangeditor/core'\n\nfunction withImage<T extends IDomEditor>(editor: T): T {\n  const { isInline, isVoid, insertNode } = editor\n  const newEditor = editor\n\n  // 重写 isInline\n  newEditor.isInline = elem => {\n    const { type } = elem\n\n    if (type === 'image') {\n      return true\n    }\n\n    return isInline(elem)\n  }\n\n  // 重写 isVoid\n  newEditor.isVoid = elem => {\n    const { type } = elem\n\n    if (type === 'image') {\n      return true\n    }\n\n    return isVoid(elem)\n  }\n\n  // 返回 editor ，重要！\n  return newEditor\n}\n\nexport default withImage\n"
  },
  {
    "path": "packages/basic-modules/src/modules/image/render-elem.tsx",
    "content": "/**\n * @description image render elem\n * @author wangfupeng\n */\n\nimport throttle from 'lodash.throttle'\nimport { Element as SlateElement, Transforms } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\nimport $, { Dom7Array } from '../../utils/dom'\nimport { ImageElement } from './custom-types'\n\ninterface IImageSize {\n  width?: string\n  height?: string\n}\n\nfunction genContainerId(editor: IDomEditor, elemNode: SlateElement) {\n  const { id } = DomEditor.findKey(editor, elemNode) // node 唯一 id\n  return `w-e-image-container-${id}`\n}\n\n/**\n * 未选中时，渲染 image container\n */\nfunction renderContainer(\n  editor: IDomEditor,\n  elemNode: SlateElement,\n  imageVnode: VNode,\n  imageInfo: IImageSize\n): VNode {\n  const { width, height } = imageInfo\n\n  const style: any = {}\n  if (width) style.width = width\n  if (height) style.height = height\n\n  const containerId = genContainerId(editor, elemNode)\n\n  return (\n    <div id={containerId} style={style} className=\"w-e-image-container\">\n      {imageVnode}\n    </div>\n  )\n}\n\n/**\n * 选中状态下，渲染 image container（渲染拖拽容器，修改图片尺寸）\n */\nfunction renderResizeContainer(\n  editor: IDomEditor,\n  elemNode: SlateElement,\n  imageVnode: VNode,\n  imageInfo: IImageSize\n) {\n  const $body = $('body')\n  const containerId = genContainerId(editor, elemNode)\n  const { width, height } = imageInfo\n\n  let originalX = 0\n  let originalWith = 0\n  let originalHeight = 0\n  let revers = false // 是否反转。如向右拖拽 right-top 需增加宽度（非反转），但向右拖拽 left-top 则需要减少宽度（反转）\n  let $container: Dom7Array | null = null\n\n  function getContainerElem(): Dom7Array {\n    const $container = $(`#${containerId}`)\n    if ($container.length === 0) throw new Error('Cannot find image container elem')\n    return $container\n  }\n\n  /**\n   * 初始化。监听事件，记录原始数据\n   */\n  function init(clientX: number) {\n    $container = getContainerElem()\n\n    // 记录当前 x 坐标值\n    originalX = clientX\n\n    // 记录 img 原始宽高\n    const $img = $container.find('img')\n    if ($img.length === 0) throw new Error('Cannot find image elem')\n    originalWith = $img.width()\n    originalHeight = $img.height()\n\n    // 监听 mousemove\n    $body.on('mousemove', onMousemove)\n\n    // 监听 mouseup\n    $body.on('mouseup', onMouseup)\n\n    // 隐藏 hoverbar\n    const hoverbar = DomEditor.getHoverbar(editor)\n    if (hoverbar) hoverbar.hideAndClean()\n  }\n\n  // mouseover callback （节流）\n  const onMousemove = throttle((e: Event) => {\n    e.preventDefault()\n\n    const { clientX } = e as MouseEvent\n    const gap = revers ? originalX - clientX : clientX - originalX // 考虑是否反转\n    const newWidth = originalWith + gap\n    const newHeight = originalHeight * (newWidth / originalWith) // 根据 width ，按比例计算 height\n\n    // 实时修改 img 宽高 -【注意】这里只修改 DOM ，mouseup 时再统一不修改 node\n    if ($container == null) return\n    if (newWidth <= 15 || newHeight <= 15) return // 最小就是 15px\n\n    $container.css('width', `${newWidth}px`)\n    $container.css('height', `${newHeight}px`)\n  }, 100)\n\n  function onMouseup(e: Event) {\n    // 取消监听 mousemove\n    $body.off('mousemove', onMousemove)\n\n    if ($container == null) return\n    const newWidth = $container.width().toFixed(2)\n    const newHeight = $container.height().toFixed(2)\n\n    // 修改 node\n    const props: Partial<ImageElement> = {\n      style: {\n        ...(elemNode as ImageElement).style,\n        width: `${newWidth}px`,\n        height: `${newHeight}px`,\n      },\n    }\n    Transforms.setNodes(editor, props, { at: DomEditor.findPath(editor, elemNode) })\n\n    // 取消监听 mouseup\n    $body.off('mouseup', onMouseup)\n  }\n\n  const style: any = {}\n  if (width) style.width = width\n  if (height) style.height = height\n  // style.boxShadow = '0 0 0 1px #B4D5FF' // 自定义 selected 样式，因为有拖拽触手\n\n  return (\n    <div\n      id={containerId}\n      style={style}\n      className=\"w-e-image-container w-e-selected-image-container\"\n      on={{\n        // 统一绑定拖拽触手的 mousedown 事件\n        mousedown: (e: MouseEvent) => {\n          const $target = $(e.target as Element)\n          if (!$target.hasClass('w-e-image-dragger')) {\n            // target 不是 .w-e-image-dragger 拖拽触手，则忽略\n            return\n          }\n          e.preventDefault()\n\n          if ($target.hasClass('left-top') || $target.hasClass('left-bottom')) {\n            revers = true // 反转。向右拖拽，减少宽度\n          }\n          init(e.clientX) // 初始化\n        },\n      }}\n    >\n      {imageVnode}\n\n      {/* 拖拽的触手，会统一在上级 DOM 绑定拖拽事件 */}\n      <div className=\"w-e-image-dragger left-top\"></div>\n      <div className=\"w-e-image-dragger right-top\"></div>\n      <div className=\"w-e-image-dragger left-bottom\"></div>\n      <div className=\"w-e-image-dragger right-bottom\"></div>\n    </div>\n  )\n}\n\nfunction renderImage(elemNode: SlateElement, children: VNode[] | null, editor: IDomEditor): VNode {\n  const { src, alt = '', href = '', style = {} } = elemNode as ImageElement\n  const { width = '', height = '' } = style\n  const selected = DomEditor.isNodeSelected(editor, elemNode) // 图片是否选中\n\n  const imageStyle: any = {}\n  if (width) imageStyle.width = '100%'\n  if (height) imageStyle.height = '100%'\n\n  // 【注意】void node 中，renderElem 不用处理 children 。core 会统一处理。\n  const vnode = <img style={imageStyle} src={src} alt={alt} data-href={href} />\n\n  const isDisabled = editor.isDisabled()\n\n  if (selected && !isDisabled) {\n    // 选中，未禁用 - 渲染 resize container\n    return renderResizeContainer(editor, elemNode, vnode, { width, height })\n  }\n\n  // 其他，渲染普通 image container\n  return renderContainer(editor, elemNode, vnode, { width, height })\n}\n\nconst renderImageConf = {\n  type: 'image', // 和 elemNode.type 一致\n  renderElem: renderImage,\n}\n\nexport { renderImageConf }\n"
  },
  {
    "path": "packages/basic-modules/src/modules/indent/custom-types.ts",
    "content": "/**\n * @description 自定义 element\n * @author wangfupeng\n */\n\nimport { Text } from 'slate'\n\n//【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts\n\nexport type IndentElement = {\n  type: string\n  indent?: string | null\n  children: Text[]\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/indent/index.ts",
    "content": "/**\n * @description indent entry\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport { renderStyle } from './render-style'\nimport { styleToHtml } from './style-to-html'\nimport { preParseHtmlConf } from './pre-parse-html'\nimport { parseStyleHtml } from './parse-style-html'\nimport { indentMenuConf, delIndentMenuConf } from './menu/index'\n\nconst indent: Partial<IModuleConf> = {\n  renderStyle,\n  styleToHtml,\n  preParseHtml: [preParseHtmlConf],\n  parseStyleHtml,\n  menus: [indentMenuConf, delIndentMenuConf],\n}\n\nexport default indent\n"
  },
  {
    "path": "packages/basic-modules/src/modules/indent/menu/BaseMenu.ts",
    "content": "/**\n * @description indent base menu\n * @author wangfupeng\n */\n\nimport { Editor, Node } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor } from '@wangeditor/core'\n\nabstract class BaseMenu implements IButtonMenu {\n  abstract readonly title: string\n  abstract readonly iconSvg: string\n  readonly tag = 'button'\n\n  /**\n   * 获取 node.indent 的值，如 `2em`\n   * @param editor editor\n   */\n  getValue(editor: IDomEditor): string | boolean {\n    const [nodeEntry] = Editor.nodes(editor, {\n      // @ts-ignore\n      match: n => !!n.indent,\n      universal: true,\n    })\n\n    if (nodeEntry == null) return ''\n    const [n] = nodeEntry\n    // @ts-ignore\n    return n.indent || ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 不需要 active\n    return false\n  }\n\n  /**\n   * 获取 node 节点\n   * @param editor editor\n   */\n  protected getMatchNode(editor: IDomEditor): Node | null {\n    const [nodeEntry] = Editor.nodes(editor, {\n      match: n => {\n        const type = DomEditor.getNodeType(n)\n\n        // 只可用于 p 和 header\n        if (type === 'paragraph') return true\n        if (type.startsWith('header')) return true\n\n        return false\n      },\n      universal: true,\n      mode: 'highest', // 匹配最高层级\n    })\n\n    if (nodeEntry == null) return null\n    return nodeEntry[0]\n  }\n\n  abstract isDisabled(editor: IDomEditor): boolean\n\n  abstract exec(editor: IDomEditor, value: string | boolean): void\n}\n\nexport default BaseMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/indent/menu/DecreaseIndentMenu.ts",
    "content": "/**\n * @description 减少缩进\n * @author wangfupeng\n */\n\nimport { Transforms, Element } from 'slate'\nimport { IDomEditor, t } from '@wangeditor/core'\nimport BaseMenu from './BaseMenu'\nimport { INDENT_LEFT_SVG } from '../../../constants/icon-svg'\nimport { IndentElement } from '../custom-types'\n\nclass DecreaseIndentMenu extends BaseMenu {\n  readonly title = t('indent.decrease')\n  readonly iconSvg = INDENT_LEFT_SVG\n\n  isDisabled(editor: IDomEditor): boolean {\n    const matchNode = this.getMatchNode(editor)\n    if (matchNode == null) return true // 未匹配 p header 等，则禁用\n\n    const { indent } = matchNode as IndentElement\n    if (!indent) {\n      // 没有 indent ，则禁用\n      return true\n    }\n\n    return false // 其他情况，不禁用\n  }\n\n  exec(editor: IDomEditor, value: string | boolean): void {\n    Transforms.setNodes(\n      editor,\n      {\n        indent: null,\n      },\n      { match: n => Element.isElement(n) }\n    )\n  }\n}\n\nexport default DecreaseIndentMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/indent/menu/IncreaseIndentMenu.ts",
    "content": "/**\n * @description 增加缩进\n * @author wangfupeng\n */\n\nimport { Transforms, Element, Editor, Text } from 'slate'\nimport { IDomEditor, t, DomEditor } from '@wangeditor/core'\nimport BaseMenu from './BaseMenu'\nimport { INDENT_RIGHT_SVG } from '../../../constants/icon-svg'\nimport { IndentElement } from '../custom-types'\nimport type { FontSizeAndFamilyText } from '../../font-size-family/custom-types'\n\nclass IncreaseIndentMenu extends BaseMenu {\n  readonly title = t('indent.increase')\n  readonly iconSvg = INDENT_RIGHT_SVG\n  private DEFAULT_INDENT_VALUE = '2em'\n\n  isDisabled(editor: IDomEditor): boolean {\n    const matchNode = this.getMatchNode(editor)\n    if (matchNode == null) return true // 未匹配 p header 等，则禁用\n\n    const { indent } = matchNode as IndentElement\n    if (indent) {\n      // 有 indent ，则禁用\n      return true\n    }\n\n    return false\n  }\n\n  private getIndentValue(editor: IDomEditor) {\n    const matchNode = this.getMatchNode(editor)\n\n    if (!matchNode) return this.DEFAULT_INDENT_VALUE\n    const textChildren = (matchNode as Element).children.filter(Text.isText)\n\n    const lastTextNode = textChildren[0] as FontSizeAndFamilyText\n\n    if (!lastTextNode || !lastTextNode.fontSize) return this.DEFAULT_INDENT_VALUE\n\n    // 如果段落的第一个 Text 节点 设置了 fontSize 样式，indent 值需要根据 fontSize 进行计算\n    const fontSize = lastTextNode.fontSize\n    const value = parseInt(lastTextNode.fontSize)\n    const unit = fontSize.replace(`${value}`, '')\n\n    return `${value * 2}${unit}`\n  }\n\n  exec(editor: IDomEditor, value: string | boolean): void {\n    const indent = this.getIndentValue(editor)\n\n    Transforms.setNodes(\n      editor,\n      {\n        indent,\n      },\n      {\n        match: n => Element.isElement(n),\n        mode: 'highest',\n      }\n    )\n  }\n}\n\nexport default IncreaseIndentMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/indent/menu/index.ts",
    "content": "/**\n * @description indent menu entry\n * @author wangfupeng\n */\n\nimport DecreaseIndentMenu from './DecreaseIndentMenu'\nimport IncreaseIndentMenu from './IncreaseIndentMenu'\n\nexport const indentMenuConf = {\n  key: 'indent',\n  factory() {\n    return new IncreaseIndentMenu()\n  },\n}\n\nexport const delIndentMenuConf = {\n  key: 'delIndent',\n  factory() {\n    return new DecreaseIndentMenu()\n  },\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/indent/parse-style-html.ts",
    "content": "/**\n * @description parse style html\n * @author wangfupeng\n */\n\nimport { Descendant, Element } from 'slate'\nimport { IDomEditor } from '@wangeditor/core'\nimport { IndentElement } from './custom-types'\nimport $, { DOMElement, getStyleValue } from '../../utils/dom'\n\nexport function parseStyleHtml(elem: DOMElement, node: Descendant, editor: IDomEditor): Descendant {\n  const $elem = $(elem)\n  if (!Element.isElement(node)) return node\n\n  const elemNode = node as IndentElement\n\n  const indent = getStyleValue($elem, 'text-indent')\n  const indentNumber = parseInt(indent, 10)\n  if (indent && indentNumber > 0) {\n    elemNode.indent = indent\n  }\n\n  return elemNode\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/indent/pre-parse-html.ts",
    "content": "/**\n * @description pre-parse html\n * @author wangfupeng\n */\n\nimport $, { DOMElement, getStyleValue } from '../../utils/dom'\n\n/**\n * pre-prase text-indent 兼容 V4 和 V5 早期格式（都使用 padding-left）\n * @param elem elem\n */\nfunction preParse(elem: DOMElement): DOMElement {\n  const $elem = $(elem)\n  const paddingLeft = getStyleValue($elem, 'padding-left')\n\n  if (/\\dem/.test(paddingLeft)) {\n    // 如 '2em' ，V4 格式\n    $elem.css('text-indent', '2em')\n  }\n\n  if (/\\dpx/.test(paddingLeft)) {\n    // px 单位\n    const num = parseInt(paddingLeft, 10)\n    if (num % 32 === 0) {\n      // 如 32px 64px ，V5 早期格式\n      $elem.css('text-indent', '2em')\n    }\n  }\n\n  return $elem[0]\n}\n\nexport const preParseHtmlConf = {\n  selector: 'p,h1,h2,h3,h4,h5',\n  preParseHtml: preParse,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/indent/render-style.tsx",
    "content": "/**\n * @description render indent style\n * @author wangfupeng\n */\n\nimport { Element, Descendant } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { addVnodeStyle } from '../../utils/vdom'\nimport { IndentElement } from './custom-types'\n\n/**\n * 添加样式\n * @param node slate elem\n * @param vnode vnode\n * @returns vnode\n */\nexport function renderStyle(node: Descendant, vnode: VNode): VNode {\n  if (!Element.isElement(node)) return vnode\n\n  const { indent } = node as IndentElement // 如 '2em'\n  let styleVnode: VNode = vnode\n\n  if (indent) {\n    addVnodeStyle(styleVnode, { textIndent: indent })\n  }\n\n  return styleVnode\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/indent/style-to-html.ts",
    "content": "/**\n * @description textStyle to html\n * @author wangfupeng\n */\n\nimport { Element, Descendant } from 'slate'\nimport $, { getOuterHTML } from '../../utils/dom'\nimport { IndentElement } from './custom-types'\n\nexport function styleToHtml(node: Descendant, elemHtml: string): string {\n  if (!Element.isElement(node)) return elemHtml\n\n  const { indent } = node as IndentElement // 如 '2em'\n  if (!indent) return elemHtml\n\n  // 设置样式\n  const $elem = $(elemHtml)\n  $elem.css('text-indent', indent)\n\n  // 输出 html\n  return getOuterHTML($elem)\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/justify/custom-types.ts",
    "content": "/**\n * @description 自定义 element\n * @author wangfupeng\n */\n\nimport { Text } from 'slate'\n\n//【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts\n\nexport type JustifyElement = {\n  type: string\n  textAlign?: string\n  children: Text[]\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/justify/index.ts",
    "content": "/**\n * @description justify module entry\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport { renderStyle } from './render-style'\nimport { styleToHtml } from './style-to-html'\nimport { parseStyleHtml } from './parse-style-html'\nimport {\n  justifyLeftMenuConf,\n  justifyRightMenuConf,\n  justifyCenterMenuConf,\n  justifyJustifyMenuConf,\n} from './menu/index'\n\nconst justify: Partial<IModuleConf> = {\n  renderStyle,\n  styleToHtml,\n  parseStyleHtml,\n  menus: [justifyLeftMenuConf, justifyRightMenuConf, justifyCenterMenuConf, justifyJustifyMenuConf],\n}\n\nexport default justify\n"
  },
  {
    "path": "packages/basic-modules/src/modules/justify/menu/BaseMenu.ts",
    "content": "/**\n * @description justify base menu\n * @author wangfupeng\n */\n\nimport { Editor, Node, Element } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor } from '@wangeditor/core'\n\nabstract class BaseMenu implements IButtonMenu {\n  abstract readonly title: string\n  abstract readonly iconSvg: string\n  readonly tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 不需要 value\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 不需要 active\n    return false\n  }\n\n  /**\n   * 获取 node 节点\n   * @param editor editor\n   */\n  protected getMatchNode(editor: IDomEditor): Node | null {\n    const [nodeEntry] = Editor.nodes(editor, {\n      match: n => {\n        const type = DomEditor.getNodeType(n)\n\n        // 只可用于 p blockquote header\n        if (type === 'paragraph') return true\n        if (type === 'blockquote') return true\n        if (type.startsWith('header')) return true\n\n        return false\n      },\n      universal: true,\n      mode: 'highest', // 匹配最高层级\n    })\n\n    if (nodeEntry == null) return null\n    return nodeEntry[0]\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n\n    const selectedElems = DomEditor.getSelectedElems(editor)\n    const notMatch = selectedElems.some((elem: Node) => {\n      if (Editor.isVoid(editor, elem) && Editor.isBlock(editor, elem)) return true\n\n      const { type } = elem as unknown as Element\n      if (['pre', 'code'].includes(type)) return true\n    })\n    if (notMatch) return true\n\n    return false\n  }\n\n  abstract exec(editor: IDomEditor, value: string | boolean): void\n}\n\nexport default BaseMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/justify/menu/JustifyCenterMenu.ts",
    "content": "/**\n * @description justify center menu\n * @author wangfupeng\n */\n\nimport { Transforms, Element } from 'slate'\nimport { IDomEditor, t } from '@wangeditor/core'\nimport BaseMenu from './BaseMenu'\nimport { JUSTIFY_CENTER_SVG } from '../../../constants/icon-svg'\n\nclass JustifyCenterMenu extends BaseMenu {\n  readonly title = t('justify.center')\n  readonly iconSvg = JUSTIFY_CENTER_SVG\n\n  exec(editor: IDomEditor, value: string | boolean): void {\n    Transforms.setNodes(\n      editor,\n      {\n        textAlign: 'center',\n      },\n      { match: n => Element.isElement(n) && !editor.isInline(n) } // inline 元素设置text-align 是没作用的\n    )\n  }\n}\n\nexport default JustifyCenterMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/justify/menu/JustifyJustifyMenu.ts",
    "content": "/**\n * @description 两端对齐\n * @author wangfupeng\n */\n\nimport { Transforms, Element } from 'slate'\nimport { IDomEditor, t } from '@wangeditor/core'\nimport BaseMenu from './BaseMenu'\nimport { JUSTIFY_JUSTIFY_SVG } from '../../../constants/icon-svg'\n\nclass JustifyJustifyMenu extends BaseMenu {\n  readonly title = t('justify.justify')\n  readonly iconSvg = JUSTIFY_JUSTIFY_SVG\n\n  exec(editor: IDomEditor, value: string | boolean): void {\n    Transforms.setNodes(\n      editor,\n      {\n        textAlign: 'justify',\n      },\n      { match: n => Element.isElement(n) && !editor.isInline(n) }\n    )\n  }\n}\n\nexport default JustifyJustifyMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/justify/menu/JustifyLeftMenu.ts",
    "content": "/**\n * @description justify left menu\n * @author wangfupeng\n */\n\nimport { Transforms, Element } from 'slate'\nimport { IDomEditor, t } from '@wangeditor/core'\nimport BaseMenu from './BaseMenu'\nimport { JUSTIFY_LEFT_SVG } from '../../../constants/icon-svg'\n\nclass JustifyLeftMenu extends BaseMenu {\n  readonly title = t('justify.left')\n  readonly iconSvg = JUSTIFY_LEFT_SVG\n\n  exec(editor: IDomEditor, value: string | boolean): void {\n    Transforms.setNodes(\n      editor,\n      {\n        textAlign: 'left',\n      },\n      { match: n => Element.isElement(n) && !editor.isInline(n) }\n    )\n  }\n}\n\nexport default JustifyLeftMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/justify/menu/JustifyRightMenu.ts",
    "content": "/**\n * @description justify right menu\n * @author wangfupeng\n */\n\nimport { Transforms, Element } from 'slate'\nimport { IDomEditor, t } from '@wangeditor/core'\nimport BaseMenu from './BaseMenu'\nimport { JUSTIFY_RIGHT_SVG } from '../../../constants/icon-svg'\n\nclass JustifyRightMenu extends BaseMenu {\n  readonly title = t('justify.right')\n  readonly iconSvg = JUSTIFY_RIGHT_SVG\n\n  exec(editor: IDomEditor, value: string | boolean): void {\n    Transforms.setNodes(\n      editor,\n      {\n        textAlign: 'right',\n      },\n      { match: n => Element.isElement(n) && !editor.isInline(n) }\n    )\n  }\n}\n\nexport default JustifyRightMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/justify/menu/index.ts",
    "content": "/**\n * @description justify menu entry\n * @author wangfupeng\n */\n\nimport JustifyLeftMenu from './JustifyLeftMenu'\nimport JustifyRightMenu from './JustifyRightMenu'\nimport JustifyCenterMenu from './JustifyCenterMenu'\nimport JustifyJustifyMenu from './JustifyJustifyMenu'\n\nexport const justifyLeftMenuConf = {\n  key: 'justifyLeft',\n  factory() {\n    return new JustifyLeftMenu()\n  },\n}\n\nexport const justifyRightMenuConf = {\n  key: 'justifyRight',\n  factory() {\n    return new JustifyRightMenu()\n  },\n}\n\nexport const justifyCenterMenuConf = {\n  key: 'justifyCenter',\n  factory() {\n    return new JustifyCenterMenu()\n  },\n}\n\nexport const justifyJustifyMenuConf = {\n  key: 'justifyJustify',\n  factory() {\n    return new JustifyJustifyMenu()\n  },\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/justify/parse-style-html.ts",
    "content": "/**\n * @description parse style html\n * @author wangfupeng\n */\n\nimport { Descendant, Element } from 'slate'\nimport { IDomEditor } from '@wangeditor/core'\nimport { JustifyElement } from './custom-types'\nimport $, { DOMElement, getStyleValue } from '../../utils/dom'\n\nexport function parseStyleHtml(elem: DOMElement, node: Descendant, editor: IDomEditor): Descendant {\n  const $elem = $(elem)\n  if (!Element.isElement(node)) return node\n\n  const elemNode = node as JustifyElement\n\n  const textAlign = getStyleValue($elem, 'text-align')\n  if (textAlign) {\n    elemNode.textAlign = textAlign\n  }\n\n  return elemNode\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/justify/render-style.tsx",
    "content": "/**\n * @description render justify style\n * @author wangfupeng\n */\n\nimport { Descendant, Element } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { addVnodeStyle } from '../../utils/vdom'\nimport { JustifyElement } from './custom-types'\n\n/**\n * 添加样式\n * @param node slate elem\n * @param vnode vnode\n * @returns vnode\n */\nexport function renderStyle(node: Descendant, vnode: VNode): VNode {\n  if (!Element.isElement(node)) return vnode\n\n  const { textAlign } = node as JustifyElement // 如 'left'/'right'/'center' 等\n  let styleVnode: VNode = vnode\n\n  if (textAlign) {\n    addVnodeStyle(styleVnode, { textAlign })\n  }\n\n  return styleVnode\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/justify/style-to-html.ts",
    "content": "/**\n * @description textStyle to html\n * @author wangfupeng\n */\n\nimport { Element, Descendant } from 'slate'\nimport $, { getOuterHTML } from '../../utils/dom'\nimport { JustifyElement } from './custom-types'\n\nexport function styleToHtml(node: Descendant, elemHtml: string): string {\n  if (!Element.isElement(node)) return elemHtml\n\n  const { textAlign } = node as JustifyElement // 如 'left'/'right'/'center' 等\n  if (!textAlign) return elemHtml\n\n  // 设置样式\n  const $elem = $(elemHtml)\n  $elem.css('text-align', textAlign)\n\n  // 输出 html\n  const outerHtml = getOuterHTML($elem)\n  return outerHtml\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/line-height/custom-types.ts",
    "content": "/**\n * @description 自定义 element\n * @author wangfupeng\n */\n\nimport { Text } from 'slate'\n\n//【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts\n\nexport type LineHeightElement = {\n  type: string\n  lineHeight?: string\n  children: Text[]\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/line-height/index.ts",
    "content": "/**\n * @description line-height module entry\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport { renderStyle } from './render-style'\nimport { styleToHtml } from './style-to-html'\nimport { lineHeightMenuConf } from './menu/index'\nimport { parseStyleHtml } from './parse-style-html'\n\nconst lineHeight: Partial<IModuleConf> = {\n  renderStyle,\n  styleToHtml,\n  parseStyleHtml,\n  menus: [lineHeightMenuConf],\n}\n\nexport default lineHeight\n"
  },
  {
    "path": "packages/basic-modules/src/modules/line-height/menu/LineHeightMenu.ts",
    "content": "/**\n * @description header menu\n * @author wangfupeng\n */\n\nimport { Editor, Node, Element, Transforms } from 'slate'\nimport { ISelectMenu, IDomEditor, DomEditor, IOption, t } from '@wangeditor/core'\nimport { LINE_HEIGHT_SVG } from '../../../constants/icon-svg'\nimport { LineHeightElement } from '../custom-types'\n\nclass LineHeightMenu implements ISelectMenu {\n  readonly title = t('lineHeight.title')\n  readonly iconSvg = LINE_HEIGHT_SVG\n  readonly tag = 'select'\n  readonly width = 80\n\n  getOptions(editor: IDomEditor): IOption[] {\n    const options: IOption[] = []\n\n    // 获取配置，参考 './config.ts'\n    const { lineHeightList = [] } = editor.getMenuConfig('lineHeight')\n\n    // 生成 options\n    options.push({\n      text: t('lineHeight.default'),\n      value: '', // this.getValue(editor) 未找到结果时，会返回 '' ，正好对应到这里\n    })\n    lineHeightList.forEach((height: string) => {\n      options.push({\n        text: height,\n        value: height,\n      })\n    })\n\n    // 设置 selected\n    const curValue = this.getValue(editor)\n    options.forEach(opt => {\n      if (opt.value === curValue) {\n        opt.selected = true\n      } else {\n        delete opt.selected\n      }\n    })\n\n    return options\n  }\n\n  /**\n   * 获取匹配的 node 节点\n   * @param editor editor\n   */\n  private getMatchNode(editor: IDomEditor): Node | null {\n    const [nodeEntry] = Editor.nodes(editor, {\n      match: n => {\n        const type = DomEditor.getNodeType(n)\n\n        // line-height 匹配如下类型的 node\n        if (type.startsWith('header')) return true\n        if (['paragraph', 'blockquote', 'list-item'].includes(type)) {\n          return true\n        }\n\n        return false\n      },\n      universal: true,\n      mode: 'highest', // 匹配最高层级\n    })\n\n    if (nodeEntry == null) return null\n    return nodeEntry[0]\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // select menu 会显示 selected value ，用不到 active\n    return false\n  }\n\n  /**\n   * 获取 node.lineHeight 的值（如 '1' '1.5'），没有则返回 ''\n   * @param editor editor\n   */\n  getValue(editor: IDomEditor): string | boolean {\n    const node = this.getMatchNode(editor)\n    if (node == null) return ''\n    if (!Element.isElement(node)) return ''\n\n    return (node as LineHeightElement).lineHeight || ''\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true // 禁用\n\n    const node = this.getMatchNode(editor)\n    if (node == null) return true // 未匹配到指定 node ，禁用\n\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    Transforms.setNodes(\n      editor,\n      {\n        lineHeight: value.toString(),\n      },\n      { mode: 'highest' }\n    )\n  }\n}\n\nexport default LineHeightMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/line-height/menu/config.ts",
    "content": "/**\n * @description line-height config\n * @author wangfupeng\n */\n\nexport function genLineHeightConfig() {\n  return ['1', '1.15', '1.5', '2', '2.5', '3']\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/line-height/menu/index.ts",
    "content": "/**\n * @description line-height menu entry\n * @author wangfupeng\n */\n\nimport LineHeightMenu from './LineHeightMenu'\nimport { genLineHeightConfig } from './config'\n\nexport const lineHeightMenuConf = {\n  key: 'lineHeight',\n  factory() {\n    return new LineHeightMenu()\n  },\n\n  // 默认的菜单菜单配置，将存储在 editorConfig.MENU_CONF[key] 中\n  // 创建编辑器时，可通过 editorConfig.MENU_CONF[key] = {...} 来修改\n  config: {\n    lineHeightList: genLineHeightConfig(),\n  },\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/line-height/parse-style-html.ts",
    "content": "/**\n * @description parse style html\n * @author wangfupeng\n */\n\nimport { Descendant, Element } from 'slate'\nimport { IDomEditor } from '@wangeditor/core'\nimport { LineHeightElement } from './custom-types'\nimport $, { DOMElement, getStyleValue } from '../../utils/dom'\n\nexport function parseStyleHtml(elem: DOMElement, node: Descendant, editor: IDomEditor): Descendant {\n  const $elem = $(elem)\n  if (!Element.isElement(node)) return node\n\n  const elemNode = node as LineHeightElement\n\n  const { lineHeightList = [] } = editor.getMenuConfig('lineHeight')\n  const lineHeight = getStyleValue($elem, 'line-height')\n  if (lineHeight && lineHeightList.includes(lineHeight)) {\n    elemNode.lineHeight = lineHeight\n  }\n\n  return elemNode\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/line-height/render-style.tsx",
    "content": "/**\n * @description render line-height style\n * @author wangfupeng\n */\n\nimport { Element, Descendant } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { addVnodeStyle } from '../../utils/vdom'\nimport { LineHeightElement } from './custom-types'\n\n/**\n * 添加样式\n * @param node slate elem\n * @param vnode vnode\n * @returns vnode\n */\nexport function renderStyle(node: Descendant, vnode: VNode): VNode {\n  if (!Element.isElement(node)) return vnode\n\n  const { lineHeight } = node as LineHeightElement // 如 '1' '1.5'\n  let styleVnode: VNode = vnode\n\n  if (lineHeight) {\n    addVnodeStyle(styleVnode, { lineHeight })\n  }\n\n  return styleVnode\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/line-height/style-to-html.ts",
    "content": "/**\n * @description textStyle to html\n * @author wangfupeng\n */\n\nimport { Element, Descendant } from 'slate'\nimport $, { getOuterHTML } from '../../utils/dom'\nimport { LineHeightElement } from './custom-types'\n\nexport function styleToHtml(node: Descendant, elemHtml: string): string {\n  if (!Element.isElement(node)) return elemHtml\n\n  const { lineHeight } = node as LineHeightElement // 如 '1' '1.5'\n  if (!lineHeight) return elemHtml\n\n  // 设置样式\n  const $elem = $(elemHtml)\n  $elem.css('line-height', lineHeight)\n\n  // 输出 html\n  return getOuterHTML($elem)\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/link/custom-types.ts",
    "content": "/**\n * @description 自定义 element\n * @author wangfupeng\n */\n\nimport { Text } from 'slate'\n\n//【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts\n\nexport type LinkElement = {\n  type: 'link'\n  url: string\n  target?: string\n  children: Text[]\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/link/elem-to-html.ts",
    "content": "/**\n * @description to html\n * @author wangfupeng\n */\n\nimport { Element } from 'slate'\nimport { LinkElement } from './custom-types'\n\nfunction linkToHtml(elem: Element, childrenHtml: string): string {\n  const { url, target = '_blank' } = elem as LinkElement\n\n  return `<a href=\"${url}\" target=\"${target}\">${childrenHtml}</a>`\n}\n\nexport const linkToHtmlConf = {\n  type: 'link',\n  elemToHtml: linkToHtml,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/link/helper.ts",
    "content": "/**\n * @description link helper\n * @author wangfupeng\n */\n\nimport { Editor, Range, Transforms } from 'slate'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\nimport { LinkElement } from './custom-types'\nimport { replaceSymbols } from '../../utils/util'\n\n/**\n * 校验 link\n * @param menuKey menu key\n * @param editor editor\n * @param text menu text\n * @param url menu url\n */\nasync function check(\n  menuKey: string,\n  editor: IDomEditor,\n  text: string,\n  url: string\n): Promise<boolean> {\n  const { checkLink } = editor.getMenuConfig(menuKey)\n  if (checkLink) {\n    const res = await checkLink(text, url)\n    if (typeof res === 'string') {\n      // 检验未通过，提示信息\n      editor.alert(res, 'error')\n      return false\n    }\n    if (res == null) {\n      // 检验未通过，不提示信息\n      return false\n    }\n  }\n\n  return true // 校验通过\n}\n\n/**\n * 转换链接 url\n * @param menuKey menu key\n * @param editor editor\n * @param url url\n * @returns parsedUrl\n */\nasync function parse(menuKey: string, editor: IDomEditor, url: string): Promise<string> {\n  const { parseLinkUrl } = editor.getMenuConfig(menuKey)\n  if (parseLinkUrl) {\n    const newUrl = await parseLinkUrl(url)\n    return newUrl\n  }\n  return url\n}\n\nexport function isMenuDisabled(editor: IDomEditor): boolean {\n  if (editor.selection == null) return true\n\n  const selectedElems = DomEditor.getSelectedElems(editor)\n  const notMatch = selectedElems.some(elem => {\n    const { type } = elem\n    if (editor.isVoid(elem)) return true\n    if (['pre', 'code', 'link'].includes(type)) return true\n  })\n  if (notMatch) return true // disabled\n  return false // enable\n}\n\n/**\n * 生成 link node\n * @param url url\n * @param text text\n */\nfunction genLinkNode(url: string, text?: string): LinkElement {\n  const linkNode: LinkElement = {\n    type: 'link',\n    url: replaceSymbols(url),\n    children: text ? [{ text }] : [],\n  }\n  return linkNode\n}\n\n/**\n * 插入 link\n * @param editor editor\n * @param text text\n * @param url url\n */\nexport async function insertLink(editor: IDomEditor, text: string, url: string) {\n  if (!url) return\n  if (!text) text = url // 无 text 则用 url 代替\n\n  // 还原选区\n  editor.restoreSelection()\n\n  if (isMenuDisabled(editor)) return\n\n  // 校验\n  const checkRes = await check('insertLink', editor, text, url)\n  if (!checkRes) return // 校验未通过\n\n  // 转换 url\n  const parsedUrl = await parse('insertLink', editor, url)\n\n  // 判断选区是否折叠\n  const { selection } = editor\n  if (selection == null) return\n  const isCollapsed = Range.isCollapsed(selection)\n\n  // 执行：插入链接\n  if (isCollapsed) {\n    // 链接前后插入空格，方便操作\n    editor.insertText(' ')\n\n    const linkNode = genLinkNode(parsedUrl, text)\n    Transforms.insertNodes(editor, linkNode)\n\n    // https://github.com/wangeditor-team/wangEditor/issues/332\n    // 不能直接使用 insertText, 会造成添加的空格被添加到链接文本中，参考上面 issue，替换为 insertFragment 方式添加空格\n    editor.insertFragment([{ text: ' ' }])\n  } else {\n    const selectedText = Editor.string(editor, selection) // 选中的文字\n    if (selectedText !== text) {\n      // 选中的文字和输入的文字不一样，则删掉文字，插入链接\n      editor.deleteFragment()\n      const linkNode = genLinkNode(parsedUrl, text)\n      Transforms.insertNodes(editor, linkNode)\n    } else {\n      // 选中的文字和输入的文字一样，则只包裹链接即可\n      const linkNode = genLinkNode(parsedUrl)\n      Transforms.wrapNodes(editor, linkNode, { split: true })\n      Transforms.collapse(editor, { edge: 'end' })\n    }\n  }\n}\n\n/**\n * 修改 link url\n * @param editor editor\n * @param text text\n * @param url link url\n */\nexport async function updateLink(editor: IDomEditor, text: string, url: string) {\n  if (!url) return\n\n  // 校验\n  const checkRes = await check('editLink', editor, text, url)\n  if (!checkRes) return // 校验未通过\n\n  // 转换 url\n  const parsedUrl = await parse('editLink', editor, url)\n\n  // 修改链接\n  const props: Partial<LinkElement> = { url: replaceSymbols(parsedUrl) }\n  Transforms.setNodes(editor, props, {\n    match: n => DomEditor.checkNodeType(n, 'link'),\n  })\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/link/index.ts",
    "content": "/**\n * @description link entry\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport withLink from './plugin'\nimport { renderLinkConf } from './render-elem'\nimport { linkToHtmlConf } from './elem-to-html'\nimport { parseHtmlConf } from './parse-elem-html'\nimport {\n  insertLinkMenuConf,\n  editLinkMenuConf,\n  unLinkMenuConf,\n  viewLinkMenuConf,\n} from './menu/index'\n\nconst link: Partial<IModuleConf> = {\n  renderElems: [renderLinkConf],\n  elemsToHtml: [linkToHtmlConf],\n  parseElemsHtml: [parseHtmlConf],\n  menus: [insertLinkMenuConf, editLinkMenuConf, unLinkMenuConf, viewLinkMenuConf],\n  editorPlugin: withLink,\n}\n\nexport default link\n"
  },
  {
    "path": "packages/basic-modules/src/modules/link/menu/EditLink.ts",
    "content": "/**\n * @description update link menu\n * @author wangfupeng\n */\n\nimport { Node } from 'slate'\nimport {\n  IModalMenu,\n  IDomEditor,\n  DomEditor,\n  genModalInputElems,\n  genModalButtonElems,\n  t,\n} from '@wangeditor/core'\nimport $, { Dom7Array, DOMElement } from '../../../utils/dom'\nimport { genRandomStr } from '../../../utils/util'\nimport { PENCIL_SVG } from '../../../constants/icon-svg'\nimport { updateLink } from '../helper'\nimport { LinkElement } from '../custom-types'\n\n/**\n * 生成唯一的 DOM ID\n */\nfunction genDomID(): string {\n  return genRandomStr('w-e-update-link')\n}\n\nclass EditLinkMenu implements IModalMenu {\n  readonly title = t('link.edit')\n  readonly iconSvg = PENCIL_SVG\n  readonly tag = 'button'\n  readonly showModal = true // 点击 button 时显示 modal\n  readonly modalWidth = 300\n\n  private $content: Dom7Array | null = null\n  private urlInputId = genDomID()\n  private buttonId = genDomID()\n\n  private getSelectedLinkElem(editor: IDomEditor): LinkElement | null {\n    const node = DomEditor.getSelectedNodeByType(editor, 'link')\n    if (node == null) return null\n    return node as LinkElement\n  }\n\n  /**\n   * 获取 node.url\n   * @param editor editor\n   */\n  getValue(editor: IDomEditor): string | boolean {\n    const linkElem = this.getSelectedLinkElem(editor)\n    if (linkElem) {\n      return linkElem.url || ''\n    }\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 无需 active\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    // 点击菜单时，弹出 modal 之前，不需要执行其他代码\n    // 此处空着即可\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n\n    const linkElem = this.getSelectedLinkElem(editor)\n\n    // 未匹配到 link node 则禁用\n    if (linkElem == null) return true\n    return false\n  }\n\n  // modal 定位\n  getModalPositionNode(editor: IDomEditor): Node | null {\n    return DomEditor.getSelectedNodeByType(editor, 'link')\n  }\n\n  getModalContentElem(editor: IDomEditor): DOMElement {\n    const { urlInputId, buttonId } = this\n\n    // 获取 input button elem\n    const [urlContainerElem, inputUrlElem] = genModalInputElems(t('link.url'), urlInputId)\n    const $inputUrl = $(inputUrlElem)\n    const [buttonContainerElem] = genModalButtonElems(buttonId, t('common.ok'))\n\n    if (this.$content == null) {\n      // 第一次渲染\n      const $content = $('<div></div>')\n\n      // 绑定事件（第一次渲染时绑定，不要重复绑定）\n      $content.on('click', 'button', e => {\n        e.preventDefault()\n        editor.restoreSelection() // 还原选区\n\n        const n = DomEditor.getSelectedNodeByType(editor, 'link')\n        const text = n ? Node.string(n) : ''\n        const url = $content.find(`#${urlInputId}`).val()\n        updateLink(editor, text, url) // 修改链接\n\n        editor.hidePanelOrModal() // 隐藏 modal\n      })\n\n      // 记录属性，重要\n      this.$content = $content\n    }\n\n    const $content = this.$content\n    $content.empty() // 先清空内容\n\n    // append input and button\n    $content.append(urlContainerElem)\n    $content.append(buttonContainerElem)\n\n    // 设置 input val\n    const url = this.getValue(editor)\n    $inputUrl.val(url)\n\n    // focus 一个 input（异步，此时 DOM 尚未渲染）\n    setTimeout(() => {\n      $inputUrl.focus()\n    })\n\n    return $content[0]\n  }\n}\n\nexport default EditLinkMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/link/menu/InsertLink.ts",
    "content": "/**\n * @description insert link menu\n * @author wangfupeng\n */\n\nimport { Editor, Range, Node } from 'slate'\nimport {\n  IModalMenu,\n  IDomEditor,\n  genModalInputElems,\n  genModalButtonElems,\n  t,\n} from '@wangeditor/core'\nimport $, { Dom7Array, DOMElement } from '../../../utils/dom'\nimport { genRandomStr } from '../../../utils/util'\nimport { LINK_SVG } from '../../../constants/icon-svg'\nimport { isMenuDisabled, insertLink } from '../helper'\n\n/**\n * 生成唯一的 DOM ID\n */\nfunction genDomID(): string {\n  return genRandomStr('w-e-insert-link')\n}\n\nclass InsertLinkMenu implements IModalMenu {\n  readonly title = t('link.insert')\n  readonly iconSvg = LINK_SVG\n  readonly tag = 'button'\n  readonly showModal = true // 点击 button 时显示 modal\n  readonly modalWidth = 300\n  private $content: Dom7Array | null = null\n  private readonly textInputId = genDomID()\n  private readonly urlInputId = genDomID()\n  private readonly buttonId = genDomID()\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 插入菜单，不需要 value\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 任何时候，都不用激活 menu\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    // 点击菜单时，弹出 modal 之前，不需要执行其他代码\n    // 此处空着即可\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    return isMenuDisabled(editor)\n  }\n\n  getModalPositionNode(editor: IDomEditor): Node | null {\n    return null // modal 依据选区定位\n  }\n\n  getModalContentElem(editor: IDomEditor): DOMElement {\n    const { selection } = editor\n    const { textInputId, urlInputId, buttonId } = this\n\n    // 获取 input button elem\n    const [textContainerElem, inputTextElem] = genModalInputElems(t('link.text'), textInputId)\n    const $inputText = $(inputTextElem)\n    const [urlContainerElem, inputUrlElem] = genModalInputElems(t('link.url'), urlInputId)\n    const $inputUrl = $(inputUrlElem)\n    const [buttonContainerElem] = genModalButtonElems(buttonId, t('common.ok'))\n\n    if (this.$content == null) {\n      // 第一次渲染\n      const $content = $('<div></div>')\n\n      // 绑定事件（第一次渲染时绑定，不要重复绑定）\n      $content.on('click', `#${buttonId}`, e => {\n        e.preventDefault()\n        const text = $content.find(`#${textInputId}`).val()\n        const url = $content.find(`#${urlInputId}`).val()\n        insertLink(editor, text, url) // 插入链接\n        editor.hidePanelOrModal() // 隐藏 modal\n      })\n\n      // 记录属性，重要\n      this.$content = $content\n    }\n\n    const $content = this.$content\n    $content.empty() // 先清空内容\n\n    // append inputs and button\n    $content.append(textContainerElem)\n    $content.append(urlContainerElem)\n    $content.append(buttonContainerElem)\n\n    // 设置 input val\n    if (selection == null || Range.isCollapsed(selection)) {\n      // 选区无内容\n      $inputText.val('')\n    } else {\n      // 选区有内容\n      const selectionText = Editor.string(editor, selection)\n      $inputText.val(selectionText)\n    }\n    $inputUrl.val('')\n\n    // focus 一个 input（异步，此时 DOM 尚未渲染）\n    setTimeout(() => {\n      $inputText.focus()\n    })\n\n    return $content[0]\n  }\n}\n\nexport default InsertLinkMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/link/menu/UnLink.ts",
    "content": "/**\n * @description unlink menu\n * @author wangfupeng\n */\n\nimport { Transforms } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport { UN_LINK_SVG } from '../../../constants/icon-svg'\n\nclass UnLink implements IButtonMenu {\n  readonly title = t('link.unLink')\n  readonly iconSvg = UN_LINK_SVG\n  readonly tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 无需获取 val\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 无需 active\n    return false\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n\n    const linkNode = DomEditor.getSelectedNodeByType(editor, 'link')\n    if (linkNode == null) {\n      // 选区未处于 link node ，则禁用\n      return true\n    }\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    if (this.isDisabled(editor)) return\n\n    // 取消链接\n    Transforms.unwrapNodes(editor, {\n      match: n => DomEditor.checkNodeType(n, 'link'),\n    })\n  }\n}\n\nexport default UnLink\n"
  },
  {
    "path": "packages/basic-modules/src/modules/link/menu/ViewLink.ts",
    "content": "/**\n * @description view link menu\n * @author wangfupeng\n */\n\nimport { IButtonMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport { EXTERNAL_SVG } from '../../../constants/icon-svg'\nimport { LinkElement } from '../custom-types'\n\nclass ViewLink implements IButtonMenu {\n  readonly title = t('link.view')\n  readonly iconSvg = EXTERNAL_SVG\n  readonly tag = 'button'\n\n  private getSelectedLinkElem(editor: IDomEditor): LinkElement | null {\n    const node = DomEditor.getSelectedNodeByType(editor, 'link')\n    if (node == null) return null\n    return node as LinkElement\n  }\n\n  getValue(editor: IDomEditor): string | boolean {\n    const linkElem = this.getSelectedLinkElem(editor)\n    if (linkElem) {\n      return linkElem.url || ''\n    }\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 无需 active\n    return false\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n\n    const linkElem = this.getSelectedLinkElem(editor)\n    if (linkElem == null) {\n      // 选区未处于 link node ，则禁用\n      return true\n    }\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    if (this.isDisabled(editor)) return\n\n    if (!value || typeof value !== 'string') {\n      throw new Error(`View link failed, link url is '${value}'`)\n    }\n\n    // 查看链接\n    window.open(value, '_blank')\n  }\n}\n\nexport default ViewLink\n"
  },
  {
    "path": "packages/basic-modules/src/modules/link/menu/config.ts",
    "content": "/**\n * @description link menu config\n * @author wangfupeng\n */\n\nexport function genLinkMenuConfig() {\n  return {\n    /**\n     * 检查链接，支持 async fn\n     * @param text link text\n     * @param url link url\n     */\n    checkLink(text: string, url: string): boolean | string | undefined {\n      // 1. 返回 true ，说明检查通过\n      // 2. 返回一个字符串，说明检查未通过，编辑器会阻止插入。会 alert 出错误信息（即返回的字符串）\n      // 3. 返回 undefined（即没有任何返回），说明检查未通过，编辑器会阻止插入\n      return true\n    },\n\n    /**\n     * parse link url\n     * @param url url\n     * @returns newUrl\n     */\n    parseLinkUrl(url: string): string {\n      return url\n    },\n  }\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/link/menu/index.ts",
    "content": "/**\n * @description link menu entry\n * @author wangfupeng\n */\n\nimport InsertLink from './InsertLink'\nimport EditLink from './EditLink'\nimport UnLink from './UnLink'\nimport ViewLink from './ViewLink'\nimport { genLinkMenuConfig } from './config'\n\nconst config = genLinkMenuConfig() // menu config\n\nconst insertLinkMenuConf = {\n  key: 'insertLink',\n  factory() {\n    return new InsertLink()\n  },\n\n  // 默认的菜单菜单配置，将存储在 editorConfig.MENU_CONF[key] 中\n  // 创建编辑器时，可通过 editorConfig.MENU_CONF[key] = {...} 来修改\n  config,\n}\n\nconst editLinkMenuConf = {\n  key: 'editLink',\n  factory() {\n    return new EditLink()\n  },\n  config,\n}\n\nconst unLinkMenuConf = {\n  key: 'unLink',\n  factory() {\n    return new UnLink()\n  },\n}\n\nconst viewLinkMenuConf = {\n  key: 'viewLink',\n  factory() {\n    return new ViewLink()\n  },\n}\n\nexport { insertLinkMenuConf, editLinkMenuConf, unLinkMenuConf, viewLinkMenuConf }\n"
  },
  {
    "path": "packages/basic-modules/src/modules/link/parse-elem-html.ts",
    "content": "/**\n * @description parse html\n * @author wangfupeng\n */\n\nimport { Descendant, Text } from 'slate'\nimport { IDomEditor } from '@wangeditor/core'\nimport { LinkElement } from './custom-types'\nimport $, { DOMElement } from '../../utils/dom'\n\nfunction parseHtml(elem: DOMElement, children: Descendant[], editor: IDomEditor): LinkElement {\n  const $elem = $(elem)\n  children = children.filter(child => {\n    if (Text.isText(child)) return true\n    if (editor.isInline(child)) return true\n    return false\n  })\n\n  // 无 children ，则用纯文本\n  if (children.length === 0) {\n    children = [{ text: $elem.text().replace(/\\s+/gm, ' ') }]\n  }\n\n  return {\n    type: 'link',\n    url: $elem.attr('href') || '',\n    target: $elem.attr('target') || '',\n    // @ts-ignore\n    children,\n  }\n}\n\nexport const parseHtmlConf = {\n  selector: 'a:not([data-w-e-type])', // data-w-e-type 属性，留给自定义元素，保证扩展性\n  parseElemHtml: parseHtml,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/link/plugin.ts",
    "content": "/**\n * @description editor 插件，重写 editor API\n * @author wangfupeng\n */\n\nimport { Editor, Node, Transforms } from 'slate'\nimport { DomEditor, IDomEditor } from '@wangeditor/core'\nimport isUrl from 'is-url'\nimport { isMenuDisabled, insertLink } from './helper'\n\nfunction withLink<T extends IDomEditor>(editor: T): T {\n  const { isInline, insertData, normalizeNode, insertNode, insertText } = editor\n  const newEditor = editor\n\n  // 重写 isInline\n  newEditor.isInline = elem => {\n    const { type } = elem\n\n    if (type === 'link') {\n      return true\n    }\n\n    return isInline(elem)\n  }\n\n  // 重写 insertData ，粘贴插入链接\n  newEditor.insertData = (data: DataTransfer) => {\n    const text = data.getData('text/plain')\n    if (!isUrl(text)) {\n      // 非链接\n      insertData(data)\n      return\n    }\n\n    // 插入链接\n    if (isMenuDisabled(newEditor)) return // disabled\n    const { selection } = newEditor\n    if (selection == null) return\n    const selectedText = Editor.string(newEditor, selection) // 获取选中的文字\n    insertLink(newEditor, selectedText, text)\n  }\n\n  newEditor.normalizeNode = ([node, path]) => {\n    const type = DomEditor.getNodeType(node)\n    if (type !== 'link') {\n      // 未命中 link ，执行默认的 normalizeNode\n      return normalizeNode([node, path])\n    }\n\n    // 如果链接内容为空，则删除\n    const str = Node.string(node)\n    if (str === '') {\n      return Transforms.removeNodes(newEditor, { at: path })\n    }\n\n    return normalizeNode([node, path])\n  }\n\n  // 返回 editor ，重要！\n  return newEditor\n}\n\nexport default withLink\n"
  },
  {
    "path": "packages/basic-modules/src/modules/link/render-elem.tsx",
    "content": "/**\n * @description render link elem\n * @author wangfupeng\n */\n\nimport { Element as SlateElement } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { IDomEditor } from '@wangeditor/core'\nimport { LinkElement } from './custom-types'\n\n/**\n * render link elem\n * @param elemNode slate elem\n * @param children children\n * @param editor editor\n * @returns vnode\n */\nfunction renderLink(elemNode: SlateElement, children: VNode[] | null, editor: IDomEditor): VNode {\n  const { url, target = '_blank' } = elemNode as LinkElement\n  const vnode = (\n    <a href={url} target={target}>\n      {children}\n    </a>\n  )\n\n  return vnode\n}\n\nconst renderLinkConf = {\n  type: 'link', // 和 elemNode.type 一致\n  renderElem: renderLink,\n}\n\nexport { renderLinkConf }\n"
  },
  {
    "path": "packages/basic-modules/src/modules/paragraph/custom-types.ts",
    "content": "/**\n * @description 自定义 element\n * @author wangfupeng\n */\n\nimport { Text } from 'slate'\n\n//【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts\n\nexport type ParagraphElement = {\n  type: 'paragraph'\n  children: Text[]\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/paragraph/elem-to-html.ts",
    "content": "/**\n * @description to html\n * @author wangfupeng\n */\n\nimport { Element } from 'slate'\n\nfunction pToHtml(elem: Element, childrenHtml: string): string {\n  if (childrenHtml === '') {\n    return '<p><br></p>'\n  }\n  return `<p>${childrenHtml}</p>`\n}\n\nexport const pToHtmlConf = {\n  type: 'paragraph',\n  elemToHtml: pToHtml,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/paragraph/index.ts",
    "content": "/**\n * @description paragraph entry\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport { renderParagraphConf } from './render-elem'\nimport { pToHtmlConf } from './elem-to-html'\nimport { parseParagraphHtmlConf } from './parse-elem-html'\nimport withParagraph from './plugin'\n\nconst p: Partial<IModuleConf> = {\n  renderElems: [renderParagraphConf],\n  elemsToHtml: [pToHtmlConf],\n  parseElemsHtml: [parseParagraphHtmlConf],\n  editorPlugin: withParagraph,\n}\n\nexport default p\n"
  },
  {
    "path": "packages/basic-modules/src/modules/paragraph/parse-elem-html.ts",
    "content": "/**\n * @description parse html\n * @author wangfupeng\n */\n\nimport { Descendant, Text } from 'slate'\nimport { IDomEditor } from '@wangeditor/core'\nimport { ParagraphElement } from './custom-types'\nimport $, { DOMElement } from '../../utils/dom'\n\nfunction parseParagraphHtml(\n  elem: DOMElement,\n  children: Descendant[],\n  editor: IDomEditor\n): ParagraphElement {\n  const $elem = $(elem)\n\n  children = children.filter(child => {\n    if (Text.isText(child)) return true\n    if (editor.isInline(child)) return true\n    return false\n  })\n\n  // 无 children ，则用纯文本\n  if (children.length === 0) {\n    children = [{ text: $elem.text().replace(/\\s+/gm, ' ') }]\n  }\n\n  return {\n    type: 'paragraph',\n    // @ts-ignore\n    children,\n  }\n}\n\nexport const parseParagraphHtmlConf = {\n  selector: 'p:not([data-w-e-type])', // data-w-e-type 属性，留给自定义元素，保证扩展性\n  parseElemHtml: parseParagraphHtml,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/paragraph/plugin.ts",
    "content": "/**\n * @description editor 插件，重写 editor API\n * @author wangfupeng\n */\n\nimport {\n  Editor,\n  Element as SlateElement,\n  Transforms,\n  Node as SlateNode,\n  Text as SlateText,\n} from 'slate'\nimport { IDomEditor } from '@wangeditor/core'\n\nfunction deleteHandler(newEditor: IDomEditor): boolean {\n  const [nodeEntry] = Editor.nodes(newEditor, {\n    match: n => newEditor.children[0] === n, // editor 第一个节点\n    mode: 'highest', // 最高层级\n  })\n  if (nodeEntry == null) return false\n\n  const n = nodeEntry[0]\n  if (!SlateElement.isElement(n)) return false\n  if (n.type === 'paragraph') return false // 命中了 paragraph ，则不再继续判断\n  if (SlateNode.string(n) !== '') return false // 未删除全部内容，则不再继续判断\n\n  const { children = [] } = n\n  if (!SlateText.isText(children[0])) return false // n.children 不是 text （如 table），则不再继续判断\n\n  // 至此，就命中了一个（非 paragraph）+（children 都是 text）+（内容为空）的顶级 node ，如 header blockQuote 等\n  // 然后，将其却换为 paragraph\n  Transforms.setNodes(newEditor, {\n    type: 'paragraph',\n  })\n  return true\n}\n\nfunction withParagraph<T extends IDomEditor>(editor: T): T {\n  const { deleteBackward, deleteForward, insertText, insertBreak } = editor\n  const newEditor = editor\n\n  // 删除非 p 的文本 elem（如 header blockQuote 等），删除没有内容时，切换为 p\n  newEditor.deleteBackward = unit => {\n    const res = deleteHandler(newEditor)\n    if (res) return // 命中结果，则 return\n\n    // 执行默认的删除\n    deleteBackward(unit)\n  }\n  newEditor.deleteForward = unit => {\n    const res = deleteHandler(newEditor)\n    if (res) return // 命中结果，则 return\n\n    // 执行默认的删除\n    deleteForward(unit)\n  }\n\n  // 返回 editor ，重要！\n  return newEditor\n}\n\nexport default withParagraph\n"
  },
  {
    "path": "packages/basic-modules/src/modules/paragraph/render-elem.tsx",
    "content": "/**\n * @description render paragraph elem\n * @author wangfupeng\n */\n\nimport { Element as SlateElement } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { IDomEditor } from '@wangeditor/core'\n\n/**\n * render paragraph elem\n * @param elemNode slate elem\n * @param children children\n * @param editor editor\n * @returns vnode\n */\nfunction renderParagraph(\n  elemNode: SlateElement,\n  children: VNode[] | null,\n  editor: IDomEditor\n): VNode {\n  const vnode = <p>{children}</p>\n  return vnode\n}\n\nexport const renderParagraphConf = {\n  type: 'paragraph',\n  renderElem: renderParagraph,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/text-style/custom-types.ts",
    "content": "/**\n * @description 自定义 element\n * @author wangfupeng\n */\n\n//【注意】需要把自定义的 Text 引入到最外层的 custom-types.d.ts\n\nexport type StyledText = {\n  text: string\n  bold?: boolean\n  code?: boolean\n  italic?: boolean\n  through?: boolean\n  underline?: boolean\n  sup?: boolean\n  sub?: boolean\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/text-style/helper.ts",
    "content": "/**\n * @description helper\n * @author wangfupeng\n */\n\nimport { Editor, Node } from 'slate'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\n\nexport function isMenuDisabled(editor: IDomEditor, mark?: string): boolean {\n  if (editor.selection == null) return true\n\n  const [match] = Editor.nodes(editor, {\n    match: n => {\n      const type = DomEditor.getNodeType(n)\n\n      if (type === 'pre') return true // 代码块\n      if (Editor.isVoid(editor, n)) return true // void node\n\n      return false\n    },\n    universal: true,\n  })\n\n  // 命中，则禁用\n  if (match) return true\n  return false\n}\n\nexport function removeMarks(editor: IDomEditor, textNode: Node) {\n  // 遍历 text node 属性，清除样式\n  const keys = Object.keys(textNode as object)\n  keys.forEach(key => {\n    if (key === 'text') {\n      // 保留 text 属性，text node 必须的\n      return\n    }\n    // 其他属性，全部清除\n    Editor.removeMark(editor, key)\n  })\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/text-style/index.ts",
    "content": "/**\n * @description text style entry\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport { renderStyle } from './render-style'\nimport { styleToHtml } from './style-to-html'\nimport { parseStyleHtml } from './parse-style-html'\nimport {\n  boldMenuConf,\n  underlineMenuConf,\n  italicMenuConf,\n  throughMenuConf,\n  codeMenuConf,\n  subMenuConf,\n  supMenuConf,\n  clearStyleMenuConf,\n} from './menu/index'\n\nconst textStyle: Partial<IModuleConf> = {\n  renderStyle,\n  menus: [\n    boldMenuConf,\n    underlineMenuConf,\n    italicMenuConf,\n    throughMenuConf,\n    codeMenuConf,\n    subMenuConf,\n    supMenuConf,\n    clearStyleMenuConf,\n  ],\n  styleToHtml,\n  parseStyleHtml,\n}\n\nexport default textStyle\n"
  },
  {
    "path": "packages/basic-modules/src/modules/text-style/menu/BaseMenu.ts",
    "content": "/**\n * @description simply style base menu\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport { IButtonMenu, IDomEditor } from '@wangeditor/core'\nimport { isMenuDisabled } from '../helper'\n\nabstract class BaseMenu implements IButtonMenu {\n  abstract readonly mark: string\n  protected readonly marksNeedToRemove: string[] = [] // 增加 mark 的同时，需要移除哪些 mark （互斥，不能共存的）\n  abstract readonly title: string\n  abstract readonly iconSvg: string\n  abstract readonly hotkey: string\n  readonly tag = 'button'\n\n  /**\n   * 获取：是否有 mark\n   * @param editor editor\n   */\n  getValue(editor: IDomEditor): string | boolean {\n    const mark = this.mark\n    const curMarks = Editor.marks(editor)\n\n    // 当 curMarks 存在时，说明用户手动设置，以 curMarks 为准\n    if (curMarks) {\n      return curMarks[mark]\n    } else {\n      const [match] = Editor.nodes(editor, {\n        // @ts-ignore\n        match: n => n[mark] === true,\n      })\n      return !!match\n    }\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    const isMark = this.getValue(editor)\n    return !!isMark\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    return isMenuDisabled(editor, this.mark)\n  }\n\n  /**\n   * 执行命令\n   * @param editor editor\n   * @param value 是否有 mark\n   */\n  exec(editor: IDomEditor, value: string | boolean) {\n    const { mark, marksNeedToRemove } = this\n    if (value) {\n      // 已，则取消\n      editor.removeMark(mark)\n    } else {\n      // 没有，则执行\n      editor.addMark(mark, true)\n\n      // 移除互斥、不能共存的 marks\n      if (marksNeedToRemove) {\n        marksNeedToRemove.forEach(m => editor.removeMark(m))\n      }\n    }\n  }\n}\n\nexport default BaseMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/text-style/menu/BoldMenu.ts",
    "content": "/**\n * @description bold menu\n * @author wangfupeng\n */\n\nimport { t } from '@wangeditor/core'\nimport BaseMenu from './BaseMenu'\nimport { BOLD_SVG } from '../../../constants/icon-svg'\n\nclass BoldMenu extends BaseMenu {\n  readonly mark = 'bold'\n  readonly title = t('textStyle.bold')\n  readonly iconSvg = BOLD_SVG\n  readonly hotkey = 'mod+b'\n}\n\nexport default BoldMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/text-style/menu/ClearStyleMenu.ts",
    "content": "/**\n * @description clear style menu\n * @author wangfupeng\n */\n\nimport { Editor, Text } from 'slate'\nimport { IButtonMenu, IDomEditor, t } from '@wangeditor/core'\nimport { ERASER_SVG } from '../../../constants/icon-svg'\nimport { isMenuDisabled, removeMarks } from '../helper'\n\nclass ClearStyleMenu implements IButtonMenu {\n  readonly title = t('textStyle.clear')\n  readonly iconSvg = ERASER_SVG\n  readonly tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    return false\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    return isMenuDisabled(editor)\n  }\n\n  /**\n   * 执行命令\n   * @param editor editor\n   * @param value 是否有 mark\n   */\n  exec(editor: IDomEditor, value: string | boolean) {\n    // 获取所有 text node\n    const nodeEntries = Editor.nodes(editor, {\n      match: n => Text.isText(n),\n      universal: true,\n    })\n    for (const nodeEntry of nodeEntries) {\n      // 单个 text node\n      const n = nodeEntry[0]\n      removeMarks(editor, n)\n    }\n  }\n}\n\nexport default ClearStyleMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/text-style/menu/CodeMenu.ts",
    "content": "/**\n * @description code menu\n * @author wangfupeng\n */\n\nimport { t } from '@wangeditor/core'\nimport BaseMenu from './BaseMenu'\nimport { CODE_SVG } from '../../../constants/icon-svg'\n\nclass CodeMenu extends BaseMenu {\n  readonly mark = 'code'\n  readonly title = t('textStyle.code')\n  readonly iconSvg = CODE_SVG\n  readonly hotkey = 'mod+e'\n}\n\nexport default CodeMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/text-style/menu/ItalicMenu.ts",
    "content": "/**\n * @description italic menu\n * @author wangfupeng\n */\n\nimport { t } from '@wangeditor/core'\nimport BaseMenu from './BaseMenu'\nimport { ITALIC_SVG } from '../../../constants/icon-svg'\n\nclass ItalicMenu extends BaseMenu {\n  readonly mark = 'italic'\n  readonly title = t('textStyle.italic')\n  readonly iconSvg = ITALIC_SVG\n  readonly hotkey = 'mod+i'\n}\n\nexport default ItalicMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/text-style/menu/SubMenu.ts",
    "content": "/**\n * @description sub menu\n * @author wangfupeng\n */\n\nimport { t } from '@wangeditor/core'\nimport BaseMenu from './BaseMenu'\nimport { SUB_SVG } from '../../../constants/icon-svg'\n\nclass SubMenu extends BaseMenu {\n  readonly mark = 'sub'\n  readonly marksNeedToRemove = ['sup'] // sub 和 sup 不能共存\n  readonly title = t('textStyle.sub')\n  readonly iconSvg = SUB_SVG\n  readonly hotkey = ''\n}\n\nexport default SubMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/text-style/menu/SupMenu.ts",
    "content": "/**\n * @description sup menu\n * @author wangfupeng\n */\n\nimport { t } from '@wangeditor/core'\nimport BaseMenu from './BaseMenu'\nimport { SUP_SVG } from '../../../constants/icon-svg'\n\nclass SupMenu extends BaseMenu {\n  readonly mark = 'sup'\n  readonly marksNeedToRemove = ['sub'] // sup 和 sub 不能共存\n  readonly title = t('textStyle.sup')\n  readonly iconSvg = SUP_SVG\n  readonly hotkey = ''\n}\n\nexport default SupMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/text-style/menu/ThroughMenu.ts",
    "content": "/**\n * @description through menu\n * @author wangfupeng\n */\n\nimport { t } from '@wangeditor/core'\nimport BaseMenu from './BaseMenu'\nimport { THROUGH_SVG } from '../../../constants/icon-svg'\n\nclass ThroughMenu extends BaseMenu {\n  readonly mark = 'through'\n  readonly title = t('textStyle.through')\n  readonly iconSvg = THROUGH_SVG\n  readonly hotkey = 'mod+shift+x'\n}\n\nexport default ThroughMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/text-style/menu/UnderlineMenu.ts",
    "content": "/**\n * @description underline menu\n * @author wangfupeng\n */\n\nimport { t } from '@wangeditor/core'\nimport BaseMenu from './BaseMenu'\nimport { UNDER_LINE_SVG } from '../../../constants/icon-svg'\n\nclass UnderlineMenu extends BaseMenu {\n  readonly mark = 'underline'\n  readonly title = t('textStyle.underline')\n  readonly iconSvg = UNDER_LINE_SVG\n  readonly hotkey = 'mod+u'\n}\n\nexport default UnderlineMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/text-style/menu/index.ts",
    "content": "/**\n * @description menu entry\n * @author wangfupeng\n */\n\nimport BoldMenu from './BoldMenu'\nimport CodeMenu from './CodeMenu'\nimport ItalicMenu from './ItalicMenu'\nimport ThroughMenu from './ThroughMenu'\nimport UnderlineMenu from './UnderlineMenu'\nimport SubMenu from './SubMenu'\nimport SupMenu from './SupMenu'\nimport ClearStyleMenu from './ClearStyleMenu'\n\nexport const boldMenuConf = {\n  key: 'bold',\n  factory() {\n    return new BoldMenu()\n  },\n}\n\nexport const codeMenuConf = {\n  key: 'code',\n  factory() {\n    return new CodeMenu()\n  },\n}\n\nexport const italicMenuConf = {\n  key: 'italic',\n  factory() {\n    return new ItalicMenu()\n  },\n}\n\nexport const throughMenuConf = {\n  key: 'through',\n  factory() {\n    return new ThroughMenu()\n  },\n}\n\nexport const underlineMenuConf = {\n  key: 'underline',\n  factory() {\n    return new UnderlineMenu()\n  },\n}\n\nexport const supMenuConf = {\n  key: 'sup',\n  factory() {\n    return new SupMenu()\n  },\n}\n\nexport const subMenuConf = {\n  key: 'sub',\n  factory() {\n    return new SubMenu()\n  },\n}\n\nexport const clearStyleMenuConf = {\n  key: 'clearStyle',\n  factory() {\n    return new ClearStyleMenu()\n  },\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/text-style/parse-style-html.ts",
    "content": "/**\n * @description parse style html\n * @author wangfupeng\n */\n\nimport { Descendant, Text } from 'slate'\nimport { IDomEditor } from '@wangeditor/core'\nimport { StyledText } from './custom-types'\nimport $, { Dom7Array, DOMElement } from '../../utils/dom'\n\n/**\n * $text 是否匹配 tags\n * @param $text $text\n * @param selector selector 如 'b,strong' 或 'sub'\n */\nfunction isMatch($text: Dom7Array, selector: string): boolean {\n  if ($text.length === 0) return false\n\n  if ($text[0].matches(selector)) return true\n\n  if ($text.find(selector).length > 0) return true\n\n  return false\n}\n\nexport function parseStyleHtml(\n  textElem: DOMElement,\n  node: Descendant,\n  editor: IDomEditor\n): Descendant {\n  const $text = $(textElem)\n\n  if (!Text.isText(node)) return node\n\n  const textNode = node as StyledText\n\n  // bold\n  if (isMatch($text, 'b,strong')) {\n    textNode.bold = true\n  }\n\n  // italic\n  if (isMatch($text, 'i,em')) {\n    textNode.italic = true\n  }\n\n  // underline\n  if (isMatch($text, 'u')) {\n    textNode.underline = true\n  }\n\n  // through\n  if (isMatch($text, 's,strike')) {\n    textNode.through = true\n  }\n\n  // sub\n  if (isMatch($text, 'sub')) {\n    textNode.sub = true\n  }\n\n  // sup\n  if (isMatch($text, 'sup')) {\n    textNode.sup = true\n  }\n\n  // code\n  if (isMatch($text, 'code')) {\n    textNode.code = true\n  }\n\n  return textNode\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/text-style/render-style.tsx",
    "content": "/**\n * @description render text style\n * @author wangfupeng\n */\n\nimport { Descendant } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { StyledText } from './custom-types'\n\n/**\n * 添加样式\n * @param node slate text\n * @param vnode vnode\n * @returns vnode\n */\nexport function renderStyle(node: Descendant, vnode: VNode): VNode {\n  const { bold, italic, underline, code, through, sub, sup } = node as StyledText\n  let styleVnode: VNode = vnode\n\n  // color bgColor 在另外的菜单\n\n  if (bold) {\n    styleVnode = <strong>{styleVnode}</strong>\n  }\n  if (code) {\n    styleVnode = <code>{styleVnode}</code>\n  }\n  if (italic) {\n    styleVnode = <em>{styleVnode}</em>\n  }\n  if (underline) {\n    styleVnode = <u>{styleVnode}</u>\n  }\n  if (through) {\n    styleVnode = <s>{styleVnode}</s>\n  }\n  if (sub) {\n    styleVnode = <sub>{styleVnode}</sub>\n  }\n  if (sup) {\n    styleVnode = <sup>{styleVnode}</sup>\n  }\n\n  return styleVnode\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/text-style/style-to-html.ts",
    "content": "/**\n * @description text to html\n * @author wangfupeng\n */\n\nimport { Text, Descendant } from 'slate'\nimport { StyledText } from './custom-types'\nimport $, { getOuterHTML, getTagName, isPlainText } from '../../utils/dom'\n\n//【注意】color bgColor fontSize fontFamily 在另外的菜单\n\n/**\n * 生成加了样式的 text html\n * @param textNode textNode\n * @param html text html\n */\nfunction genStyledHtml(textNode: Descendant, html: string): string {\n  let styledHtml = html\n  const { bold, italic, underline, code, through, sub, sup } = textNode as StyledText\n  if (bold) styledHtml = `<strong>${styledHtml}</strong>`\n  if (code) styledHtml = `<code>${styledHtml}</code>`\n  if (italic) styledHtml = `<em>${styledHtml}</em>`\n  if (underline) styledHtml = `<u>${styledHtml}</u>`\n  if (through) styledHtml = `<s>${styledHtml}</s>`\n  if (sub) styledHtml = `<sub>${styledHtml}</sub>`\n  if (sup) styledHtml = `<sup>${styledHtml}</sup>`\n  return styledHtml\n}\n\n/**\n * style to html\n * @param textNode slate text node\n * @param textHtml text html\n * @returns styled html\n */\nexport function styleToHtml(textNode: Descendant, textHtml: string): string {\n  if (!Text.isText(textNode)) return textHtml\n\n  if (isPlainText(textHtml)) {\n    // textHtml 是纯文本，而不是 html tag\n    return genStyledHtml(textNode, textHtml)\n  }\n\n  // textHtml 是 html tag\n  const $text = $(textHtml)\n  const tagName = getTagName($text)\n\n  if (tagName === 'br') {\n    return genStyledHtml(textNode, '<br>')\n  }\n\n  let innerHtml = $text.html()\n  innerHtml = genStyledHtml(textNode, innerHtml)\n  $text.html(innerHtml)\n  return getOuterHTML($text)\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/todo/custom-types.ts",
    "content": "/**\n * @description 自定义 element\n * @author wangfupeng\n */\n\nimport { Text } from 'slate'\n\n//【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts\n\nexport type TodoElement = {\n  type: 'todo'\n  checked: boolean\n  children: Text[]\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/todo/elem-to-html.ts",
    "content": "/**\n * @description to html\n * @author wangfupeng\n */\n\nimport { Element } from 'slate'\nimport { TodoElement } from './custom-types'\n\nfunction todoToHtml(elem: Element, childrenHtml: string): string {\n  const { checked } = elem as TodoElement\n  const checkedAttr = checked ? 'checked' : ''\n  return `<div data-w-e-type=\"todo\"><input type=\"checkbox\" disabled ${checkedAttr}>${childrenHtml}</div>`\n}\n\nexport const todoToHtmlConf = {\n  type: 'todo',\n  elemToHtml: todoToHtml,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/todo/index.ts",
    "content": "/**\n * @description todo entry\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport { renderTodoConf } from './render-elem'\nimport withTodo from './plugin'\nimport { todoMenuConf } from './menu/index'\nimport { todoToHtmlConf } from './elem-to-html'\nimport { parseHtmlConf } from './parse-elem-html'\nimport { preParseHtmlConf } from './pre-parse-html'\n\nconst todo: Partial<IModuleConf> = {\n  renderElems: [renderTodoConf],\n  elemsToHtml: [todoToHtmlConf],\n  preParseHtml: [preParseHtmlConf],\n  parseElemsHtml: [parseHtmlConf],\n  menus: [todoMenuConf],\n  editorPlugin: withTodo,\n}\n\nexport default todo\n"
  },
  {
    "path": "packages/basic-modules/src/modules/todo/menu/Todo.ts",
    "content": "/**\n * @description Todo menu\n * @author wangfupeng\n */\n\nimport { Editor, Element, Transforms } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport { CHECK_BOX_SVG } from '../../../constants/icon-svg'\n\nclass TodoMenu implements IButtonMenu {\n  readonly title = t('todo.todo')\n  readonly iconSvg = CHECK_BOX_SVG\n  readonly tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 无需获取 val\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    return !!DomEditor.getSelectedNodeByType(editor, 'todo')\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n\n    const selectedElems = DomEditor.getSelectedElems(editor)\n    const notMatch = selectedElems.some((elem: Element) => {\n      if (Editor.isVoid(editor, elem) && Editor.isBlock(editor, elem)) return true\n\n      const { type } = elem as Element\n      if (['pre', 'table', 'list-item'].includes(type)) return true\n    })\n    if (notMatch) return true\n\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    const active = this.isActive(editor)\n    Transforms.setNodes(editor, { type: active ? 'paragraph' : 'todo' })\n  }\n}\n\nexport default TodoMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/todo/menu/index.ts",
    "content": "/**\n * @description todo menu entry\n * @author wangfupeng\n */\n\nimport TodoMenu from './Todo'\n\nexport const todoMenuConf = {\n  key: 'todo',\n  factory() {\n    return new TodoMenu()\n  },\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/todo/parse-elem-html.ts",
    "content": "/**\n * @description parse html\n * @author wangfupeng\n */\n\nimport { Descendant, Text } from 'slate'\nimport { IDomEditor } from '@wangeditor/core'\nimport { TodoElement } from './custom-types'\nimport $, { DOMElement } from '../../utils/dom'\n\nfunction parseHtml(elem: DOMElement, children: Descendant[], editor: IDomEditor): TodoElement {\n  const $elem = $(elem)\n\n  children = children.filter(child => {\n    if (Text.isText(child)) return true\n    if (editor.isInline(child)) return true\n    return false\n  })\n\n  // 无 children ，则用纯文本\n  if (children.length === 0) {\n    children = [{ text: $elem.text().replace(/\\s+/gm, ' ') }]\n  }\n\n  // 获取 checked\n  let checked = false\n  const $input = $elem.find('input[type=\"checkbox\"]')\n  if ($input.attr('checked') != null) {\n    checked = true\n  }\n\n  return {\n    type: 'todo',\n    checked,\n    // @ts-ignore\n    children,\n  }\n}\n\nexport const parseHtmlConf = {\n  selector: 'div[data-w-e-type=\"todo\"]',\n  parseElemHtml: parseHtml,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/todo/plugin.ts",
    "content": "/**\n * @description editor 插件，重写 editor API\n * @author wangfupeng\n */\n\nimport { Node, Transforms, Range } from 'slate'\nimport { DomEditor, IDomEditor } from '@wangeditor/core'\n\nfunction withTodo<T extends IDomEditor>(editor: T): T {\n  const { deleteBackward } = editor\n  const newEditor = editor\n\n  /**\n   * 删除 todo 无内容时，变为 paragraph\n   */\n  newEditor.deleteBackward = unit => {\n    const { selection } = editor\n\n    if (selection && Range.isCollapsed(selection)) {\n      // 获取选中的 todo\n      const selectedTodo = DomEditor.getSelectedNodeByType(editor, 'todo')\n      if (selectedTodo) {\n        if (Node.string(selectedTodo).length === 0) {\n          // 当前 todo 已经没有文字，则转换为 paragraph\n          Transforms.setNodes(editor, { type: 'paragraph' }, { mode: 'highest' })\n          return\n        }\n      }\n    }\n\n    deleteBackward(unit)\n  }\n\n  return newEditor\n}\n\nexport default withTodo\n"
  },
  {
    "path": "packages/basic-modules/src/modules/todo/pre-parse-html.ts",
    "content": "/**\n * @description pre parse html\n * @author wangfupeng\n */\n\nimport $, { DOMElement } from '../../utils/dom'\n\n/**\n * pre-prase todo ，兼容 V4\n * @param elem elem\n */\nfunction preParse(elem: DOMElement): DOMElement {\n  const $elem = $(elem)\n\n  // $elem 格式如\n  // <ul class=\"w-e-todo\"><li><span contenteditable=\"false\"><input type=\"checkbox\"/></span>hello <b>world</b></li></ul>\n  const $li = $elem.find('li')\n\n  const $container = $('<div data-w-e-type=\"todo\"></div>')\n\n  // 1. 把 input 移动到 $container\n  const $input = $li.find('input[type]')\n  $container.append($input)\n\n  // 2. 删除之前包裹 input 的 span\n  const $spanForInput = $li.children()[0]\n  $spanForInput.remove()\n\n  // 3. 再把剩余的内容移动到 $container （有纯文本内容，不能用 children ，得用 innerHTML）\n  $container[0].innerHTML = $container[0].innerHTML + $li[0].innerHTML\n\n  return $container[0]\n}\n\nexport const preParseHtmlConf = {\n  selector: 'ul.w-e-todo', // 匹配 v4 todo\n  preParseHtml: preParse,\n}\n"
  },
  {
    "path": "packages/basic-modules/src/modules/todo/render-elem.tsx",
    "content": "/**\n * @description render todo\n * @author wangfupeng\n */\n\nimport { Element as SlateElement, Transforms } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\nimport { TodoElement } from './custom-types'\n\n/**\n * render todo elem\n * @param elemNode slate elem\n * @param children children\n * @param editor editor\n * @returns vnode\n */\nfunction renderTodo(elemNode: SlateElement, children: VNode[] | null, editor: IDomEditor): VNode {\n  // 判断 disabled\n  let disabled = false\n  if (editor.isDisabled()) disabled = true\n\n  const { checked } = elemNode as TodoElement\n  const vnode = (\n    <div style={{ margin: '5px 0' }}>\n      <span contentEditable={false} style={{ marginRight: '0.5em' }}>\n        <input\n          type=\"checkbox\"\n          checked={checked}\n          disabled={disabled}\n          on={{\n            change: event => {\n              const path = DomEditor.findPath(editor, elemNode)\n              const newProps: Partial<TodoElement> = {\n                // @ts-ignore\n                checked: event.target.checked,\n              }\n              Transforms.setNodes(editor, newProps, { at: path })\n            },\n          }}\n        />\n      </span>\n      <span>{children}</span>\n    </div>\n  )\n\n  return vnode\n}\n\nconst renderTodoConf = {\n  type: 'todo', // 和 elemNode.type 一致\n  renderElem: renderTodo,\n}\n\nexport { renderTodoConf }\n"
  },
  {
    "path": "packages/basic-modules/src/modules/undo-redo/index.ts",
    "content": "/**\n * @description undo redo\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport { redoMenuConf, undoMenuConf } from './menu/index'\n\nconst undoRedo: Partial<IModuleConf> = {\n  menus: [redoMenuConf, undoMenuConf],\n}\n\nexport default undoRedo\n"
  },
  {
    "path": "packages/basic-modules/src/modules/undo-redo/menu/RedoMenu.ts",
    "content": "/**\n * @description redo menu\n * @author wangfupeng\n */\n\nimport { IButtonMenu, IDomEditor, t } from '@wangeditor/core'\nimport { REDO_SVG } from '../../../constants/icon-svg'\n\nclass RedoMenu implements IButtonMenu {\n  title = t('undo.redo')\n  iconSvg = REDO_SVG\n  tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    return false\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    if (typeof editor.redo === 'function') {\n      editor.redo()\n    }\n  }\n}\n\nexport default RedoMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/undo-redo/menu/UndoMenu.ts",
    "content": "/**\n * @description undo menu\n * @author wangfupeng\n */\n\nimport { IButtonMenu, IDomEditor, t } from '@wangeditor/core'\nimport { UNDO_SVG } from '../../../constants/icon-svg'\n\nclass UndoMenu implements IButtonMenu {\n  title = t('undo.undo')\n  iconSvg = UNDO_SVG\n  tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    return false\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    if (typeof editor.undo === 'function') {\n      editor.undo()\n    }\n  }\n}\n\nexport default UndoMenu\n"
  },
  {
    "path": "packages/basic-modules/src/modules/undo-redo/menu/index.ts",
    "content": "/**\n * @description menu entry\n * @author wangfupeng\n */\n\nimport RedoMenu from './RedoMenu'\nimport UndoMenu from './UndoMenu'\n\nexport const undoMenuConf = {\n  key: 'undo',\n  factory() {\n    return new UndoMenu()\n  },\n}\n\nexport const redoMenuConf = {\n  key: 'redo',\n  factory() {\n    return new RedoMenu()\n  },\n}\n"
  },
  {
    "path": "packages/basic-modules/src/utils/dom.ts",
    "content": "/**\n * @description DOM 操作\n * @author wangfupeng\n */\n\nimport $, {\n  css,\n  append,\n  prepend,\n  addClass,\n  removeClass,\n  hasClass,\n  on,\n  off,\n  focus,\n  attr,\n  hide,\n  show,\n  parents,\n  dataset,\n  val,\n  text,\n  removeAttr,\n  children,\n  html,\n  remove,\n  find,\n  width,\n  height,\n  Dom7Array,\n  filter,\n  empty,\n} from 'dom7'\nexport { Dom7Array } from 'dom7'\n\nif (css) $.fn.css = css\nif (append) $.fn.append = append\nif (prepend) $.fn.prepend = prepend\nif (addClass) $.fn.addClass = addClass\nif (removeClass) $.fn.removeClass = removeClass\nif (hasClass) $.fn.hasClass = hasClass\nif (on) $.fn.on = on\nif (off) $.fn.off = off\nif (focus) $.fn.focus = focus\nif (attr) $.fn.attr = attr\nif (removeAttr) $.fn.removeAttr = removeAttr\nif (hide) $.fn.hide = hide\nif (show) $.fn.show = show\nif (parents) $.fn.parents = parents\nif (dataset) $.fn.dataset = dataset\nif (val) $.fn.val = val\nif (text) $.fn.text = text\nif (html) $.fn.html = html\nif (children) $.fn.children = children\nif (remove) $.fn.remove = remove\nif (find) $.fn.find = find\nif (width) $.fn.width = width\nif (height) $.fn.height = height\nif (filter) $.fn.filter = filter\nif (empty) $.fn.empty = empty\n\nexport default $\n\n/**\n * 判断 str 是不是纯字符串，而不是 html tag\n * @param str str\n */\nexport function isPlainText(str: string) {\n  const $container = $(`<div>${str}</div>`)\n\n  // 获取 children length （过滤 `<br>`）\n  const childrenLength = $container.children().filter((child: DOMElement) => {\n    if (child.tagName === 'BR') return false\n    return true\n  }).length\n\n  return childrenLength === 0\n}\n\n/**\n * 获取 outerHTML\n * @param $elem dom7 elem\n */\nexport function getOuterHTML($elem: Dom7Array) {\n  if ($elem.length === 0) return ''\n  return $elem[0].outerHTML\n}\n\n/**\n * 获取 tagName lower-case\n * @param $elem $elem\n */\nexport function getTagName($elem: Dom7Array): string {\n  if ($elem.length) return $elem[0].tagName.toLowerCase()\n  return ''\n}\n\n/**\n * 获取 $elem 某一个 style 值\n * @param $elem $elem\n * @param styleKey style key\n */\nexport function getStyleValue($elem: Dom7Array, styleKey: string): string {\n  let res = ''\n\n  const styleStr = $elem.attr('style') || '' // 如 'line-height: 2.5; color: red;'\n  const styleArr = styleStr.split(';') // 如 ['line-height: 2.5', ' color: red', '']\n  const length = styleArr.length\n  for (let i = 0; i < length; i++) {\n    const styleItemStr = styleArr[i] // 如 'line-height: 2.5'\n    if (styleItemStr) {\n      const arr = styleItemStr.split(':') // ['line-height', ' 2.5']\n      if (arr[0].trim() === styleKey) {\n        res = arr[1].trim()\n      }\n    }\n  }\n\n  return res\n}\n\n// COMPAT: This is required to prevent TypeScript aliases from doing some very\n// weird things for Slate's types with the same name as globals. (2019/11/27)\n// https://github.com/microsoft/TypeScript/issues/35002\nimport DOMNode = globalThis.Node\nimport DOMComment = globalThis.Comment\nimport DOMElement = globalThis.Element\nimport DOMText = globalThis.Text\nimport DOMRange = globalThis.Range\nimport DOMSelection = globalThis.Selection\nimport DOMStaticRange = globalThis.StaticRange\nexport { DOMNode, DOMComment, DOMElement, DOMText, DOMRange, DOMSelection, DOMStaticRange }\n"
  },
  {
    "path": "packages/basic-modules/src/utils/util.ts",
    "content": "/**\n * @description 工具函数\n * @author wangfupeng\n */\n\nimport { nanoid } from 'nanoid'\n\n/**\n * 获取随机数字符串\n * @param prefix 前缀\n * @returns 随机数字符串\n */\nexport function genRandomStr(prefix: string = 'r'): string {\n  return `${prefix}-${nanoid()}`\n}\n\nexport function replaceSymbols(str: string) {\n  return str.replace(/</g, '&lt;').replace(/>/g, '&gt;')\n}\n"
  },
  {
    "path": "packages/basic-modules/src/utils/vdom.ts",
    "content": "/**\n * @description vdom utils fn\n * @author wangfupeng\n */\n\nimport { VNode, VNodeStyle, Dataset } from 'snabbdom'\n\n// /**\n//  * 给 vnode 添加 dataset\n//  * @param vnode vnode\n//  * @param newDataset { key: val }\n//  */\n// export function addVnodeDataset(vnode: VNode, newDataset: Dataset) {\n//   if (vnode.data == null) vnode.data = {}\n//   const data = vnode.data\n//   if (data.dataset == null) data.dataset = {}\n\n//   Object.assign(data.dataset, newDataset)\n// }\n\n/**\n * 给 vnode 添加样式\n * @param vnode vnode\n * @param newStyle { key: val }\n */\nexport function addVnodeStyle(vnode: VNode, newStyle: VNodeStyle) {\n  if (vnode.data == null) vnode.data = {}\n  const data = vnode.data\n  if (data.style == null) data.style = {}\n\n  Object.assign(data.style, newStyle)\n}\n"
  },
  {
    "path": "packages/basic-modules/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {},\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\n    \"./src/**/*\",\n    \"../custom-types.d.ts\"\n  ]\n}"
  },
  {
    "path": "packages/code-highlight/CHANGELOG.md",
    "content": "# Change Log\n\nAll notable changes to this project will be documented in this file.\nSee [Conventional Commits](https://conventionalcommits.org) for commit guidelines.\n\n## [1.0.3](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/code-highlight@1.0.2...@wangeditor/code-highlight@1.0.3) (2022-09-14)\n\n\n### Bug Fixes\n\n* 代码块 - 增加 lua groovy  语言 ([ef4f62a](https://github.com/wangeditor-team/wangEditor/commit/ef4f62a876e95995f7c8f6f41d8d44b2505dd5f6))\n\n\n\n\n\n## [1.0.2](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/code-highlight@1.0.1...@wangeditor/code-highlight@1.0.2) (2022-06-02)\n\n\n### Bug Fixes\n\n* issue 4308 - 自定义字号、字体无法回显 ([ad38b8c](https://github.com/wangeditor-team/wangEditor/commit/ad38b8ce6dbcff1d65785c8d6701238ad351f562))\n\n\n\n\n\n## 1.0.1 (2022-04-18)\n\n\n### Bug Fixes\n\n* 更新各包之间依赖版本 ([75c552c](https://github.com/wangeditor-team/wangEditor/commit/75c552cc8ed54765bebb86a7ec5329a7fc79e85f))\n* 修复 pnpm 安装 @wangeditor/editor 出现警告的问题 ([4087fbe](https://github.com/wangeditor-team/wangEditor/commit/4087fbee01c76bdd55e747a5e86c5e4a8d6a8353))\n* 移除了每个包下的 publishConfig directory 配置 ([16559f0](https://github.com/wangeditor-team/wangEditor/commit/16559f052545c111318be760e64291a521bdcc65))\n* 粘贴 <code> 代码块出错 ([fc44d9f](https://github.com/wangeditor-team/wangEditor/commit/fc44d9ff36cb9566d9dc5490b4be14f2e5bd3f3c))\n* rename es module filename ([1821d4e](https://github.com/wangeditor-team/wangEditor/commit/1821d4eef49e64efcb41b848849ca7a5e6472044))\n* table - elemToHtml ([e36e609](https://github.com/wangeditor-team/wangEditor/commit/e36e6092ef721723169afc8bf0560a47ac9f4dfc))\n\n\n### Features\n\n* code highlight ([42b2f8d](https://github.com/wangeditor-team/wangEditor/commit/42b2f8d192e2433593c11ad0b8424737f6cffb58))\n* i18n ([c11b244](https://github.com/wangeditor-team/wangEditor/commit/c11b2440f91b99d40bca18b675c66a22b6e160c9))\n* parse html ([2a5eace](https://github.com/wangeditor-team/wangEditor/commit/2a5eace00f33cded50b68e8164748ec2480213fd))\n* toHtml 机制 ([1c4d872](https://github.com/wangeditor-team/wangEditor/commit/1c4d8729f84aaab6a448f23064b34a20596305e9))\n* upload video ([ac8e6f8](https://github.com/wangeditor-team/wangEditor/commit/ac8e6f8b5258e593714676a6f6be359ba525833c))\n"
  },
  {
    "path": "packages/code-highlight/README.md",
    "content": "# wangEditor code highlight\n\nCode highlight module built in [wangEditor](https://www.wangeditor.com/) by default.\n"
  },
  {
    "path": "packages/code-highlight/__tests__/content.ts",
    "content": "/**\n * @description code content\n * @author wangfupeng\n */\n\nexport const text = 'const a = 100;'\n\nexport const textNode = { text: text }\n\nexport const language = 'javascript'\n\nexport const codeNode = {\n  type: 'code',\n  language,\n  children: [textNode],\n}\n\nexport const preNode = {\n  type: 'pre',\n  children: [codeNode],\n}\n\nexport const content = [{ type: 'paragraph', children: [{ text: 'hello world' }] }, preNode]\n\nexport const textNodePath = [1, 0, 0]\n\nexport const codeLocation = {\n  anchor: { offset: text.length, path: textNodePath },\n  focus: { offset: text.length, path: textNodePath },\n}\n\nexport const paragraphLocation = {\n  anchor: { offset: 0, path: [0, 0] },\n  focus: { offset: 0, path: [0, 0] },\n}\n\ndescribe('加一个 case 防止报错～', () => {\n  it('1 + 1 = 2', () => {\n    expect(1 + 1).toBe(2)\n  })\n})\n"
  },
  {
    "path": "packages/code-highlight/__tests__/decorate.test.ts",
    "content": "/**\n * @description code-highlight decorate test\n * @author wangfupeng\n */\n\nimport { IDomEditor } from '@wangeditor/core'\nimport createEditor from '../../../tests/utils/create-editor'\nimport codeHighLightDecorate from '../src/decorate/index'\nimport { content, textNode, textNodePath } from './content'\n\ndescribe('code-highlight decorate', () => {\n  let editor: IDomEditor | null = null\n\n  beforeAll(() => {\n    // 把 content 创建到一个编辑器中\n    editor = createEditor({\n      content,\n    })\n  })\n\n  afterAll(() => {\n    // 销毁 editor\n    if (editor == null) return\n    editor.destroy()\n    editor = null\n  })\n\n  it('code-highlight decorate 拆分代码字符串', () => {\n    const ranges = codeHighLightDecorate([textNode, textNodePath])\n    expect(ranges.length).toBe(4) // 把 textNode 内容拆分为 4 段\n  })\n})\n"
  },
  {
    "path": "packages/code-highlight/__tests__/elem-to-html.test.ts",
    "content": "/**\n * @description code-hight elem-to-html\n * @author wangfupeng\n */\n\nimport { IDomEditor } from '@wangeditor/core'\nimport createEditor from '../../../tests/utils/create-editor'\nimport { codeToHtmlConf } from '../src/module/elem-to-html'\nimport { content, codeNode, language } from './content'\n\ndescribe('code-highlight elem to html', () => {\n  let editor: IDomEditor | null = null\n\n  beforeAll(() => {\n    // 把 content 创建到一个编辑器中\n    editor = createEditor({\n      content,\n    })\n  })\n\n  afterAll(() => {\n    // 销毁 editor\n    if (editor == null) return\n    editor.destroy()\n    editor = null\n  })\n\n  it('codeNode to html', () => {\n    expect(codeToHtmlConf.type).toBe('code')\n\n    if (editor == null) throw new Error('editor is null')\n    const text = 'var n = 100;'\n    const html = codeToHtmlConf.elemToHtml(codeNode, text)\n    expect(html).toBe(`<code class=\"language-${language}\">${text}</code>`)\n  })\n})\n"
  },
  {
    "path": "packages/code-highlight/__tests__/parse-html.test.ts",
    "content": "/**\n * @description parse html test\n * @author wangfupeng\n */\n\nimport { $ } from 'dom7'\nimport { parseCodeStyleHtml } from '../src/module/parse-style-html'\nimport createEditor from '../../../tests/utils/create-editor'\n\ndescribe('code highlight - parse style html', () => {\n  const editor = createEditor()\n\n  it('v5 format', () => {\n    const $code = $('<code class=\"language-javascript\"></code>') // v5 html format\n    const code = { type: 'code', children: [{ text: 'var a = 100;' }] }\n\n    const res = parseCodeStyleHtml($code[0], code, editor)\n    expect(res).toEqual({\n      type: 'code',\n      language: 'javascript',\n      children: [{ text: 'var a = 100;' }],\n    })\n  })\n\n  it('v4 format', () => {\n    const $code = $('<code class=\"Javascript\"></code>') // v4 html format\n    const code = { type: 'code', children: [{ text: 'var a = 100;' }] }\n\n    const res = parseCodeStyleHtml($code[0], code, editor)\n    expect(res).toEqual({\n      type: 'code',\n      language: 'javascript',\n      children: [{ text: 'var a = 100;' }],\n    })\n  })\n})\n"
  },
  {
    "path": "packages/code-highlight/__tests__/render-text-style.test.tsx",
    "content": "/**\n * @description code-highlight render text style test\n * @author wangfupeng\n */\n\nimport { renderStyle } from '../src/module/render-style'\nimport { jsx } from 'snabbdom'\n\ndescribe('code-highlight render text style', () => {\n  it('code text style', () => {\n    const leafNode = { text: 'let', keyword: true } // 定义一个 keyword leaf text node\n    const vnode = <span>let</span>\n\n    // @ts-ignore 忽略 vnode 格式检查\n    const newVnode = renderStyle(leafNode, vnode)\n    expect(newVnode.data?.props?.className).toBe('token keyword')\n  })\n})\n"
  },
  {
    "path": "packages/code-highlight/__tests__/select-lang-menu.test.ts",
    "content": "/**\n * @description code-highlight select lang menu test\n * @author wangfupeng\n */\n\nimport { IDomEditor } from '@wangeditor/core'\nimport createEditor from '../../../tests/utils/create-editor'\nimport { content, codeLocation, paragraphLocation, language } from './content'\nimport SelectLangMenu from '../src/module/menu/SelectLangMenu'\n\ndescribe('code-highlight select lang menu', () => {\n  let editor: IDomEditor | null = null\n  let menu: SelectLangMenu | null = null\n\n  beforeAll(() => {\n    // 创建 editor\n    editor = createEditor({\n      content,\n    })\n\n    // 创建 menu\n    menu = new SelectLangMenu()\n  })\n\n  afterAll(() => {\n    // 销毁 editor\n    if (editor == null) return\n    editor.destroy()\n    editor = null\n\n    // 销毁 menu\n    menu = null\n  })\n\n  it('get langs and selected one', () => {\n    if (editor == null || menu == null) throw new Error('editor or menu is null')\n\n    // select codeNode\n    editor.select(codeLocation)\n\n    const langs = menu.getOptions(editor)\n\n    // 包括多个 lang\n    expect(langs.length).toBeGreaterThan(0)\n\n    // 其中有一个 'plain text'\n    const hasPlainText = langs.some(lang => lang.text === 'plain text' && lang.value === '')\n    expect(hasPlainText).toBeTruthy()\n\n    // 选中的语言\n    const selectedLangs = langs.filter(lang => lang.selected)\n    expect(selectedLangs.length).toBe(1)\n    const selectedLang: any = selectedLangs[0] || {}\n    expect(selectedLang.value).toBe(language)\n  })\n\n  it('menu active is always false', () => {\n    if (editor == null || menu == null) throw new Error('editor or menu is null')\n\n    expect(menu.isActive(editor)).toBeFalsy()\n  })\n\n  it('get menu value (selected lang)', () => {\n    if (editor == null || menu == null) throw new Error('editor or menu is null')\n\n    // select codeNode\n    editor.select(codeLocation)\n    expect(menu.getValue(editor)).toBe(language)\n\n    // select paragraph\n    editor.select(paragraphLocation)\n    expect(menu.getValue(editor)).toBe('')\n  })\n\n  it('menu disable', () => {\n    if (editor == null || menu == null) throw new Error('editor or menu is null')\n\n    // deselect\n    editor.deselect()\n    expect(menu.isDisabled(editor)).toBeTruthy()\n\n    // select paragraph\n    editor.select(paragraphLocation)\n    expect(menu.isDisabled(editor)).toBeTruthy()\n\n    // select codeNode\n    editor.select(codeLocation)\n    expect(menu.isDisabled(editor)).toBeFalsy()\n  })\n\n  it('menu exec (change lang)', done => {\n    if (editor == null || menu == null) throw new Error('editor or menu is null')\n\n    // select codeNode\n    editor.select(codeLocation)\n    menu.exec(editor, 'html') // change lang\n\n    setTimeout(() => {\n      if (editor == null || menu == null) return\n\n      editor.select(codeLocation)\n      expect(menu.getValue(editor)).toBe('html')\n      done()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/code-highlight/package.json",
    "content": "{\n  \"name\": \"@wangeditor/code-highlight\",\n  \"version\": \"1.0.3\",\n  \"description\": \"wangEditor code-highlight module\",\n  \"author\": \"wangfupeng1988 <wangfupeng1988@163.com>\",\n  \"contributors\": [],\n  \"homepage\": \"https://github.com/wangeditor-team/wangEditor#readme\",\n  \"license\": \"MIT\",\n  \"types\": \"dist/code-highlight/src/index.d.ts\",\n  \"main\": \"dist/index.js\",\n  \"module\": \"dist/index.esm.js\",\n  \"browser\": {\n    \"./dist/index.js\": \"./dist/index.js\",\n    \"./dist/index.esm.js\": \"./dist/index.esm.js\"\n  },\n  \"directories\": {\n    \"lib\": \"dist\",\n    \"test\": \"__tests__\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.com/\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wangeditor-team/wangEditor.git\"\n  },\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"test-c\": \"jest --coverage\",\n    \"dev\": \"cross-env NODE_ENV=development rollup -c rollup.config.js\",\n    \"dev-watch\": \"cross-env NODE_ENV=development rollup -c rollup.config.js -w\",\n    \"build\": \"cross-env NODE_ENV=production rollup -c rollup.config.js\",\n    \"dev-size-stats\": \"cross-env NODE_ENV=development:size_stats rollup -c rollup.config.js\",\n    \"size-stats\": \"cross-env NODE_ENV=production:size_stats rollup -c rollup.config.js\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/wangeditor-team/wangEditor/issues\"\n  },\n  \"peerDependencies\": {\n    \"@wangeditor/core\": \"1.x\",\n    \"dom7\": \"^3.0.0\",\n    \"slate\": \"^0.72.0\",\n    \"snabbdom\": \"^3.1.0\"\n  },\n  \"dependencies\": {\n    \"prismjs\": \"^1.23.0\"\n  },\n  \"devDependencies\": {\n    \"@types/prismjs\": \"^1.16.5\"\n  }\n}\n"
  },
  {
    "path": "packages/code-highlight/rollup.config.js",
    "content": "import { createRollupConfig, IS_PRD } from '../../build/create-rollup-config'\nimport pkg from './package.json'\n\nconst name = 'WangEditorCodeHighLight'\n\nconst configList = []\n\n// esm\nconst esmConf = createRollupConfig({\n  output: {\n    file: pkg.module,\n    format: 'esm',\n    name,\n  },\n})\nconfigList.push(esmConf)\n\n// umd\nconst umdConf = createRollupConfig({\n  output: {\n    file: pkg.main,\n    format: 'umd',\n    name,\n  },\n})\nconfigList.push(umdConf)\n\nexport default configList\n"
  },
  {
    "path": "packages/code-highlight/src/assets/index.less",
    "content": "// 样式参考 https://github.com/PrismJS/prism/blob/master/themes/prism.css\n// TODO 开发 themes 主题，可以参考 prismjs 主题 https://github.com/PrismJS/prism/tree/master/themes\n\n.w-e-text-container [data-slate-editor] pre>code {\n\ttext-shadow: 0 1px white;\n\tfont-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;\n\ttext-align: left;\n\twhite-space: pre;\n\tword-spacing: normal;\n\tword-break: normal;\n\tword-wrap: normal;\n\tline-height: 1.5;\n\ttab-size: 4;\n\thyphens: none;\n\n  padding: 1em;\n\tmargin: .5em 0;\n\toverflow: auto;\n\n  .token.comment,\n  .token.prolog,\n  .token.doctype,\n  .token.cdata {\n    color: slategray;\n  }\n\n  .token.punctuation {\n    color: #999;\n  }\n\n  .token.namespace {\n    opacity: .7;\n  }\n\n  .token.property,\n  .token.tag,\n  .token.boolean,\n  .token.number,\n  .token.constant,\n  .token.symbol,\n  .token.deleted {\n    color: #905;\n  }\n\n  .token.selector,\n  .token.attr-name,\n  .token.string,\n  .token.char,\n  .token.builtin,\n  .token.inserted {\n    color: #690;\n  }\n\n  .token.operator,\n  .token.entity,\n  .token.url,\n  .language-css .token.string,\n  .style .token.string {\n    color: #9a6e3a;\n  }\n\n  .token.atrule,\n  .token.attr-value,\n  .token.keyword {\n    color: #07a;\n  }\n\n  .token.function,\n  .token.class-name {\n    color: #DD4A68;\n  }\n\n  .token.regex,\n  .token.important,\n  .token.variable {\n    color: #e90;\n  }\n\n  .token.important,\n  .token.bold {\n    font-weight: bold;\n  }\n  .token.italic {\n    font-style: italic;\n  }\n\n  .token.entity {\n    cursor: help;\n  }\n}\n"
  },
  {
    "path": "packages/code-highlight/src/constants/svg.ts",
    "content": "/**\n * @description icon svg\n * @author wangfupeng\n */\n\n/**\n * 【注意】svg 字符串的长度 ，否则会导致代码体积过大\n * 尽量选择 https://www.iconfont.cn/collections/detail?spm=a313x.7781069.0.da5a778a4&cid=20293\n * 找不到再从 iconfont.com 搜索\n */\n\nexport const JS_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M64 64v896h896V64H64z m487.6 698.8c0 87.2-51.2 127-125.8 127-67.4 0-106.4-34.8-126.4-77l68.6-41.4c13.2 23.4 25.2 43.2 54.2 43.2 27.6 0 45.2-10.8 45.2-53V475.4h84.2v287.4z m199.2 127c-78.2 0-128.8-37.2-153.4-86l68.6-39.6c18 29.4 41.6 51.2 83 51.2 34.8 0 57.2-17.4 57.2-41.6 0-28.8-22.8-39-61.4-56l-21-9c-60.8-25.8-101-58.4-101-127 0-63.2 48.2-111.2 123.2-111.2 53.6 0 92 18.6 119.6 67.4L800 580c-14.4-25.8-30-36-54.2-36-24.6 0-40.2 15.6-40.2 36 0 25.2 15.6 35.4 51.8 51.2l21 9c71.6 30.6 111.8 62 111.8 132.4 0 75.6-59.6 117.2-139.4 117.2z\"></path></svg>'\n"
  },
  {
    "path": "packages/code-highlight/src/custom-types.ts",
    "content": "/**\n * @description 自定义 element\n * @author wangfupeng\n */\n\n// 拷贝自 basic-modules/src/modules/code-block/custom-types.ts\n\ntype PureText = {\n  text: string\n}\n\nexport type PreElement = {\n  type: 'pre'\n  children: CodeElement[]\n}\n\nexport type CodeElement = {\n  type: 'code'\n  language: string\n  children: PureText[]\n}\n"
  },
  {
    "path": "packages/code-highlight/src/decorate/index.ts",
    "content": "/**\n * @description code-highlight decorate\n * @author wangfupeng\n */\n\nimport { Node, NodeEntry, Range, Text } from 'slate'\nimport { DomEditor } from '@wangeditor/core'\nimport { getPrismTokens, getPrismTokenLength } from '../vendor/prism'\nimport { CodeElement } from '../custom-types'\n\n/**\n * 获取 code elem\n * @param node text node\n */\nfunction getCodeElem(textNode: Node): CodeElement | null {\n  if (!Text.isText(textNode)) return null // 非文本 node\n\n  const codeNode = DomEditor.getParentNode(null, textNode)\n  if (codeNode && DomEditor.getNodeType(codeNode) === 'code') {\n    const preNode = DomEditor.getParentNode(null, codeNode)\n    if (preNode && DomEditor.getNodeType(preNode) === 'pre') {\n      return codeNode as CodeElement\n    }\n  }\n  return null\n}\n\nconst codeHighLightDecorate = (nodeEntry: NodeEntry): Range[] => {\n  const [n, path] = nodeEntry\n  const ranges: Range[] = []\n\n  // 节点不合法，则不处理\n  const codeElem = getCodeElem(n)\n  if (codeElem == null) return ranges\n  const { language = '' } = codeElem\n  if (!language) return ranges\n\n  const textNode = n as Text\n  const tokens = getPrismTokens(textNode, language)\n\n  let start = 0\n  for (const token of tokens) {\n    const length = getPrismTokenLength(token)\n    const end = start + length\n\n    if (typeof token !== 'string') {\n      // 遇到关键字，则拆分多个 range —— decorate 规则\n      ranges.push({\n        [token.type]: true, // 记录类型，以便 css 使用不同的颜色\n        anchor: { path, offset: start },\n        focus: { path, offset: end },\n      })\n    }\n\n    start = end\n  }\n\n  return ranges\n}\n\nexport default codeHighLightDecorate\n"
  },
  {
    "path": "packages/code-highlight/src/index.ts",
    "content": "/**\n * @description code-highlight\n * @author wangfupeng\n */\n\nimport './assets/index.less'\n\n// 配置多语言\nimport './locale/index'\n\nimport wangEditorCodeHighlightModule from './module/index'\nimport wangEditorCodeHighLightDecorate from './decorate'\n\nexport { wangEditorCodeHighlightModule, wangEditorCodeHighLightDecorate }\n"
  },
  {
    "path": "packages/code-highlight/src/locale/en.ts",
    "content": "/**\n * @description i18n en\n * @author wangfupeng\n */\n\nexport default {\n  highLightModule: {\n    selectLang: 'Language',\n  },\n}\n"
  },
  {
    "path": "packages/code-highlight/src/locale/index.ts",
    "content": "/**\n * @description i18n entry\n * @author wangfupeng\n */\n\nimport { i18nAddResources } from '@wangeditor/core'\nimport enResources from './en'\nimport zhResources from './zh-CN'\n\ni18nAddResources('en', enResources)\ni18nAddResources('zh-CN', zhResources)\n"
  },
  {
    "path": "packages/code-highlight/src/locale/zh-CN.ts",
    "content": "/**\n * @description i18n zh-CN\n * @author wangfupeng\n */\n\nexport default {\n  highLightModule: {\n    selectLang: '选择语言',\n  },\n}\n"
  },
  {
    "path": "packages/code-highlight/src/module/elem-to-html.ts",
    "content": "/**\n * @description to html\n * @author wangfupeng\n */\n\nimport { Element } from 'slate'\nimport { CodeElement } from '../custom-types'\n\nfunction codeToHtml(elem: Element, childrenHtml: string): string {\n  const { language = '' } = elem as CodeElement\n\n  const cssClass = language\n    ? `class=\"language-${language}\"` // prism.js 根据 language 代码高亮\n    : ''\n\n  return `<code ${cssClass}>${childrenHtml}</code>`\n}\n\n// 覆盖 basic-module 中的 code to html\nexport const codeToHtmlConf = {\n  type: 'code',\n  elemToHtml: codeToHtml,\n}\n"
  },
  {
    "path": "packages/code-highlight/src/module/index.ts",
    "content": "/**\n * @description code highlight module\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport { renderStyle } from './render-style'\nimport { parseCodeStyleHtml } from './parse-style-html'\nimport { selectLangMenuConf } from './menu/index'\nimport { codeToHtmlConf } from './elem-to-html'\n\nconst codeHighlightModule: Partial<IModuleConf> = {\n  renderStyle,\n  parseStyleHtml: parseCodeStyleHtml,\n  menus: [selectLangMenuConf],\n  elemsToHtml: [codeToHtmlConf],\n}\n\nexport default codeHighlightModule\n"
  },
  {
    "path": "packages/code-highlight/src/module/menu/SelectLangMenu.ts",
    "content": "/**\n * @description code-highlight select lang\n * @author wangfupeng\n */\n\nimport { Transforms, Element } from 'slate'\nimport { ISelectMenu, IDomEditor, IOption, DomEditor, t } from '@wangeditor/core'\nimport { JS_SVG } from '../../constants/svg'\nimport { CodeElement } from '../../custom-types'\n\nclass SelectLangMenu implements ISelectMenu {\n  readonly title = t('highLightModule.selectLang')\n  readonly iconSvg = JS_SVG\n  readonly tag = 'select'\n  readonly width = 95\n  readonly selectPanelWidth = 115\n\n  getOptions(editor: IDomEditor): IOption[] {\n    const options: IOption[] = []\n\n    // 获取配置，参考 './config.ts'\n    const { codeLangs = [] } = editor.getMenuConfig('codeSelectLang') // 第二个参数 menu key\n\n    options.push({\n      text: 'plain text',\n      value: '', // getValue 默认会返回 ''\n    })\n    codeLangs.forEach((lang: { text: string; value: string }) => {\n      const { text, value } = lang\n      options.push({ text, value })\n    })\n\n    // 设置 selected\n    const curValue = this.getValue(editor)\n    options.forEach(opt => {\n      if (opt.value === curValue) {\n        opt.selected = true\n      } else {\n        delete opt.selected\n      }\n    })\n\n    return options\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // select menu 会显示 selected value ，用不到 active\n    return false\n  }\n\n  /**\n   * 获取语言类型\n   * @param editor editor\n   */\n  getValue(editor: IDomEditor): string | boolean {\n    const elem = this.getSelectCodeElem(editor)\n    if (elem == null) return ''\n    if (!Element.isElement(elem)) return ''\n\n    const lang = elem.language.toString()\n\n    // 当前 elem.language 是否在已配置的 langs 中？\n    const { codeLangs = [] } = editor.getMenuConfig('codeSelectLang')\n    const hasLang = codeLangs.some(item => item.value === lang)\n\n    if (hasLang) return lang\n    return ''\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n    const elem = this.getSelectCodeElem(editor)\n    if (elem) return false\n    return true\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    const elem = this.getSelectCodeElem(editor)\n    if (elem == null) return\n\n    // 设置语言\n    const props: Partial<CodeElement> = { language: value.toString() }\n    Transforms.setNodes(editor, props, {\n      match: n => DomEditor.checkNodeType(n, 'code'),\n    })\n  }\n\n  private getSelectCodeElem(editor: IDomEditor): CodeElement | null {\n    const codeNode = DomEditor.getSelectedNodeByType(editor, 'code')\n    if (codeNode == null) return null\n    const preNode = DomEditor.getParentNode(editor, codeNode)\n    if (!Element.isElement(preNode)) return null\n    if (preNode.type !== 'pre') return null\n\n    return codeNode as CodeElement\n  }\n}\n\nexport default SelectLangMenu\n"
  },
  {
    "path": "packages/code-highlight/src/module/menu/config.ts",
    "content": "/**\n * @description menu config\n * @author wangfupeng\n */\n\nexport function genCodeLangs() {\n  // 1. text value 对应关系参考 prism 官网 https://prismjs.com/#supported-languages\n  // 2. 要加入一个新语言时，要引入相应的 js 模块（代码在 `vender/prism.ts`），例如 `import 'prismjs/components/prism-php'`\n\n  return [\n    { text: 'CSS', value: 'css' },\n    { text: 'HTML', value: 'html' },\n    { text: 'XML', value: 'xml' },\n    { text: 'Javascript', value: 'javascript' },\n    { text: 'Typescript', value: 'typescript' },\n    { text: 'JSX', value: 'jsx' },\n    { text: 'Go', value: 'go' },\n    { text: 'PHP', value: 'php' },\n    { text: 'C', value: 'c' },\n    { text: 'Python', value: 'python' },\n    { text: 'Java', value: 'java' },\n    { text: 'C++', value: 'cpp' },\n    { text: 'C#', value: 'csharp' },\n    { text: 'Visual Basic', value: 'visual-basic' },\n    { text: 'SQL', value: 'sql' },\n    { text: 'Ruby', value: 'ruby' },\n    { text: 'Swift', value: 'swift' },\n    { text: 'Bash', value: 'bash' },\n    { text: 'Lua', value: 'lua' },\n    { text: 'Groovy', value: 'groovy' },\n    { text: 'Markdown', value: 'markdown' },\n  ]\n}\n"
  },
  {
    "path": "packages/code-highlight/src/module/menu/index.ts",
    "content": "/**\n * @description code-highlight menu\n * @author wangfupeng\n */\n\nimport SelectLangMenu from './SelectLangMenu'\nimport { genCodeLangs } from './config'\n\nexport const selectLangMenuConf = {\n  key: 'codeSelectLang',\n  factory() {\n    return new SelectLangMenu()\n  },\n  config: {\n    codeLangs: genCodeLangs(),\n  },\n}\n"
  },
  {
    "path": "packages/code-highlight/src/module/parse-style-html.ts",
    "content": "/**\n * @description parse style html\n * @author wangfupeng\n */\n\nimport $, { DOMElement } from '../utils/dom'\nimport { Descendant, Element } from 'slate'\nimport { DomEditor, IDomEditor } from '@wangeditor/core'\nimport { CodeElement } from '../custom-types'\n\nexport function parseCodeStyleHtml(\n  elem: DOMElement,\n  node: Descendant,\n  editor: IDomEditor\n): Descendant {\n  const $elem = $(elem)\n\n  if (!Element.isElement(node)) return node\n  if (DomEditor.getNodeType(node) !== 'code') return node // 只针对 pre/code 元素\n\n  const elemNode = node as CodeElement\n\n  const langAttr = $elem.attr('class') || ''\n  if (langAttr.indexOf('language-') === 0) {\n    // V5 版本，格式如 class=\"language-javascript\"\n    elemNode.language = langAttr.split('-')[1] || '' // 获取 'javascript'\n  } else {\n    // 兼容 V4 版本，格式如 class=\"Javascript\"\n    elemNode.language = langAttr.toLowerCase()\n  }\n\n  return elemNode\n}\n"
  },
  {
    "path": "packages/code-highlight/src/module/render-style.tsx",
    "content": "/**\n * @description render code highlight style\n * @author wangfupeng\n */\n\nimport { Text as SlateText, Descendant } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { addVnodeClassName } from '../utils/vdom'\nimport { prismTokenTypes } from '../vendor/prism'\n\n/**\n * 添加样式\n * @param node slate text\n * @param vnode vnode\n * @returns vnode\n */\nexport function renderStyle(node: Descendant, vnode: VNode): VNode {\n  const leafNode = node as SlateText & { [key: string]: string }\n  let styleVnode: VNode = vnode\n\n  let className = ''\n  prismTokenTypes.forEach(type => {\n    if (leafNode[type]) className = type\n  })\n\n  if (className) {\n    className = `token ${className}` // 如 'token keyword' - prismjs 渲染的规则\n    addVnodeClassName(styleVnode, className)\n  }\n\n  return styleVnode\n}\n"
  },
  {
    "path": "packages/code-highlight/src/utils/dom.ts",
    "content": "/**\n * @description DOM 操作\n * @author wangfupeng\n */\n\nimport $, { attr } from 'dom7'\n\nif (attr) $.fn.attr = attr\n\nexport { Dom7Array } from 'dom7'\n\nexport default $\n\n// COMPAT: This is required to prevent TypeScript aliases from doing some very\n// weird things for Slate's types with the same name as globals. (2019/11/27)\n// https://github.com/microsoft/TypeScript/issues/35002\nimport DOMNode = globalThis.Node\nimport DOMComment = globalThis.Comment\nimport DOMElement = globalThis.Element\nimport DOMText = globalThis.Text\nimport DOMRange = globalThis.Range\nimport DOMSelection = globalThis.Selection\nimport DOMStaticRange = globalThis.StaticRange\nexport { DOMNode, DOMComment, DOMElement, DOMText, DOMRange, DOMSelection, DOMStaticRange }\n"
  },
  {
    "path": "packages/code-highlight/src/utils/vdom.ts",
    "content": "/**\n * @description vdom utils fn\n * @author wangfupeng\n */\n\nimport { VNode, VNodeStyle } from 'snabbdom'\n\n/**\n * 给 vnode 添加 className\n * @param vnode vnode\n * @param className css class\n */\nexport function addVnodeClassName(vnode: VNode, className: string) {\n  if (vnode.data == null) vnode.data = {}\n  const data = vnode.data\n  if (data.props == null) data.props = {}\n\n  Object.assign(data.props, { className })\n}\n\n/**\n * 给 vnode 添加样式\n * @param vnode vnode\n * @param newStyle { key: val }\n */\nexport function addVnodeStyle(vnode: VNode, newStyle: VNodeStyle) {\n  if (vnode.data == null) vnode.data = {}\n  const data = vnode.data\n  if (data.style == null) data.style = {}\n\n  Object.assign(data.style, newStyle)\n}\n"
  },
  {
    "path": "packages/code-highlight/src/vendor/prism.ts",
    "content": "/**\n * @description prismjs\n * @author wangfupeng\n */\n\nimport { Text } from 'slate'\n\nimport Prism from 'prismjs'\nimport 'prismjs/components/prism-jsx'\nimport 'prismjs/components/prism-typescript'\nimport 'prismjs/components/prism-markup'\nimport 'prismjs/components/prism-go'\nimport 'prismjs/components/prism-php'\nimport 'prismjs/components/prism-c'\nimport 'prismjs/components/prism-python'\nimport 'prismjs/components/prism-java'\nimport 'prismjs/components/prism-cpp'\nimport 'prismjs/components/prism-csharp'\nimport 'prismjs/components/prism-visual-basic'\nimport 'prismjs/components/prism-sql'\nimport 'prismjs/components/prism-ruby'\nimport 'prismjs/components/prism-swift'\nimport 'prismjs/components/prism-bash'\nimport 'prismjs/components/prism-markdown'\nimport 'prismjs/components/prism-lua'\nimport 'prismjs/components/prism-groovy'\n// 语言模块，参考 https://github.com/PrismJS/prism/tree/master/components\n\n// prismjs 的 token 类型汇总\nexport const prismTokenTypes = [\n  'comment',\n  'prolog',\n  'doctype',\n  'cdata',\n  'punctuation',\n  'namespace',\n  'property',\n  'tag',\n  'boolean',\n  'number',\n  'constant',\n  'symbol',\n  'deleted',\n  'selector',\n  'attr-name',\n  'string',\n  'builtin',\n  'inserted',\n  'operator',\n  'entity',\n  'url',\n  'string',\n  'atrule',\n  'attr-value',\n  'keyword',\n  'function',\n  'class-name',\n  'regex',\n  'important',\n  'variable',\n  'bold',\n  'italic',\n  'entity',\n  'char',\n]\n\n/**\n * 获取 prism token 的字符串长度\n * @param token prism token\n */\nexport function getPrismTokenLength(token: any) {\n  if (typeof token === 'string') {\n    return token.length\n  } else if (typeof token.content === 'string') {\n    return token.content.length\n  } else {\n    // 累加 length\n    return token.content.reduce(\n      // @ts-ignore\n      (l, t) => l + getPrismTokenLength(t),\n      0\n    )\n  }\n}\n\n/**\n * 获取 prism 解析的 token 列表\n * @param textNode text node\n * @param language 代码语言\n */\nexport function getPrismTokens(textNode: Text, language: string) {\n  if (!language) return []\n\n  const langGrammar = Prism.languages[language]\n  if (!langGrammar) return []\n\n  return Prism.tokenize(textNode.text, langGrammar)\n\n  // tokens 即 Prism 对整个字符串的拆分，有普通文字也有高亮的关键字\n  // 例如 `const a = 100;` 的 tokens 是一个数组 [ token, ' a ', token, ' ', token ] ，有对象有字符串，对象就表示关键字\n  // 如数组第一个 token 是 { type: \"keyword\", content: \"const\" } 。关键字类型不同 type 也不同\n}\n"
  },
  {
    "path": "packages/code-highlight/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {},\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\n    \"./src/**/*\",\n    \"../custom-types.d.ts\"\n  ]\n}"
  },
  {
    "path": "packages/core/CHANGELOG.md",
    "content": "# Change Log\n\nAll notable changes to this project will be documented in this file.\nSee [Conventional Commits](https://conventionalcommits.org) for commit guidelines.\n\n## [1.1.19](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.1.18...@wangeditor/core@1.1.19) (2022-11-14)\n\n\n### Bug Fixes\n\n* **font family menu:** 处理 setHtml 的时候字体样式回显失败的问题 ([b941bab](https://github.com/wangeditor-team/wangEditor/commit/b941babbdc6bd5bf7da0cce826803a8fde011e07))\n* **fontFamily menu:** fix font-family value quote symbol ([2c25231](https://github.com/wangeditor-team/wangEditor/commit/2c25231a088de14edbf7516fc448a6483125e3ed))\n\n\n\n\n\n## [1.1.18](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.1.17...@wangeditor/core@1.1.18) (2022-10-18)\n\n\n### Bug Fixes\n\n* mousedown事件添加passive的默认值 ([60229cc](https://github.com/wangeditor-team/wangEditor/commit/60229cc2f9647a5f17dc0fd85c4bb1dc396a5e9c))\n* **video menu:** fix invoke clear api can not clear video node when insert video ([68c1f8e](https://github.com/wangeditor-team/wangEditor/commit/68c1f8ee68ab2cb7b202b6d9b4d4db192a927725))\n\n\n\n\n\n## [1.1.17](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.1.16...@wangeditor/core@1.1.17) (2022-10-04)\n\n\n### Bug Fixes\n\n* 修复 compositionend 时错误修改dom的问题 ([1187154](https://github.com/wangeditor-team/wangEditor/commit/1187154aa077594f55211307c00e3493d1ab5676))\n* 修复设置 maxlength 后粘贴异常的问题 ([14003d0](https://github.com/wangeditor-team/wangEditor/commit/14003d0ba01eeb9a264d15fac514dd4b4bd89ff7))\n\n\n\n\n\n## [1.1.16](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.1.15...@wangeditor/core@1.1.16) (2022-09-27)\n\n\n### Bug Fixes\n\n* list-item - 遇到 style 是 toHtml 出错 ([9854308](https://github.com/wangeditor-team/wangEditor/commit/98543083a1cb09207aceb2a4d8f3c1ce020b106d))\n\n\n\n\n\n## [1.1.15](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.1.14...@wangeditor/core@1.1.15) (2022-09-27)\n\n**Note:** Version bump only for package @wangeditor/core\n\n\n\n\n\n## [1.1.14](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.1.13...@wangeditor/core@1.1.14) (2022-09-16)\n\n**Note:** Version bump only for package @wangeditor/core\n\n\n\n\n\n## [1.1.13](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.1.12...@wangeditor/core@1.1.13) (2022-09-15)\n\n\n### Bug Fixes\n\n* focus table 时 isFocused 异常 ([5c52bf3](https://github.com/wangeditor-team/wangEditor/commit/5c52bf33e91b1a4677e7bbc04c5d80698abfeeab))\n* snabbdom 增加 attributesModule ([2c597b6](https://github.com/wangeditor-team/wangEditor/commit/2c597b6a52ffa96c820128d63fd84b903a6faebf))\n\n\n\n\n\n## [1.1.12](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.1.11...@wangeditor/core@1.1.12) (2022-08-30)\n\n\n### Bug Fixes\n\n* fix https://github.com/wangeditor-team/wangEditor/issues/4754 ([e0216b9](https://github.com/wangeditor-team/wangEditor/commit/e0216b98b0ea9ebf4f9cc8a8fd820d68fcd230d3))\n\n\n\n\n\n## [1.1.11](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.1.10...@wangeditor/core@1.1.11) (2022-07-27)\n\n**Note:** Version bump only for package @wangeditor/core\n\n\n\n\n\n## [1.1.10](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.1.9...@wangeditor/core@1.1.10) (2022-07-27)\n\n\n### Bug Fixes\n\n* setHtml 支持空字符串 ([d438157](https://github.com/wangeditor-team/wangEditor/commit/d43815766320d9cb0548bae0415c54ce7b147efb))\n* upload file callback error ([bf20e07](https://github.com/wangeditor-team/wangEditor/commit/bf20e07f12ed242b0ab4bb2290d876153a822972))\n\n\n\n\n\n## [1.1.9](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.1.8...@wangeditor/core@1.1.9) (2022-07-22)\n\n\n### Bug Fixes\n\n* 粘贴 HTML <a> bug ([b935ef6](https://github.com/wangeditor-team/wangEditor/commit/b935ef622b9d4f8f3a9954d26a41c89d4e8042bd))\n\n\n\n\n\n## [1.1.8](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.1.7...@wangeditor/core@1.1.8) (2022-07-18)\n\n\n### Bug Fixes\n\n* 粘贴文字报错 ([a11ea56](https://github.com/wangeditor-team/wangEditor/commit/a11ea56af4f7976f5664232e80a164cd37d84d8c))\n\n\n\n\n\n## [1.1.7](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.1.6...@wangeditor/core@1.1.7) (2022-07-16)\n\n\n### Bug Fixes\n\n* setHtml() 多一个空行 ([994954f](https://github.com/wangeditor-team/wangEditor/commit/994954fcbae72808e3488e0936a5f82253b603f4))\n* 图片受 indent 影响 ([3d737f1](https://github.com/wangeditor-team/wangEditor/commit/3d737f11e457c46e1aeee40ebd834a2470198dfd))\n\n\n\n\n\n## [1.1.6](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.1.5...@wangeditor/core@1.1.6) (2022-07-14)\n\n\n### Bug Fixes\n\n* 粘贴网页 HTML 报错 ([939cb22](https://github.com/wangeditor-team/wangEditor/commit/939cb2229a11eea827e1bea4420f7502db1e7eb6))\n\n\n\n\n\n## [1.1.5](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.1.4...@wangeditor/core@1.1.5) (2022-07-13)\n\n\n### Bug Fixes\n\n* setHtml 问题 - table 后面 p 格式错误 ([b525b4a](https://github.com/wangeditor-team/wangEditor/commit/b525b4aaa69b834204232774971367beba7db975))\n\n\n\n\n\n## [1.1.4](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.1.3...@wangeditor/core@1.1.4) (2022-07-12)\n\n**Note:** Version bump only for package @wangeditor/core\n\n\n\n\n\n## [1.1.3](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.1.2...@wangeditor/core@1.1.3) (2022-07-11)\n\n\n### Bug Fixes\n\n* scroll 滚动问题 ([bc133e1](https://github.com/wangeditor-team/wangEditor/commit/bc133e1e4ca89ab5042cbc0971578ad144499805))\n* 修复选中内容输入时,出现光标位置不对或者输入重复内容的问题 ([9596a4c](https://github.com/wangeditor-team/wangEditor/commit/9596a4ccaca2e2c4eed7ffc16fc4b042f73cef5d))\n\n\n\n\n\n## [1.1.2](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.1.1...@wangeditor/core@1.1.2) (2022-07-11)\n\n\n### Bug Fixes\n\n* editor.focus() 参数语法错误 ([334fa21](https://github.com/wangeditor-team/wangEditor/commit/334fa217d43fdaa95454e7c85a53526b7b777fda))\n* focus blur 问题 ([4a1997b](https://github.com/wangeditor-team/wangEditor/commit/4a1997b9f19cdce9d6aa6ff4e8e13d439b12af05))\n* 单词之间空格问题 issue 4403 ([2f1d6f5](https://github.com/wangeditor-team/wangEditor/commit/2f1d6f5275c8a9e106b66213bb276c58a70aff79))\n\n\n\n\n\n## [1.1.1](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.1.0...@wangeditor/core@1.1.1) (2022-06-02)\n\n\n### Bug Fixes\n\n* issue 4308 - 自定义字号、字体无法回显 ([ad38b8c](https://github.com/wangeditor-team/wangEditor/commit/ad38b8ce6dbcff1d65785c8d6701238ad351f562))\n\n\n\n\n\n# [1.1.0](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/core@1.0.1...@wangeditor/core@1.1.0) (2022-05-25)\n\n\n### Bug Fixes\n\n* 修复 readonly 模式下,特定内容下editor初始化报错的问题 ([f3bc8b8](https://github.com/wangeditor-team/wangEditor/commit/f3bc8b8d485765cfa8fa7d19e530aa1a1b4bc4e2))\n* 粘贴 HTML 后 font-size font-family line-height 不显示 ([2281957](https://github.com/wangeditor-team/wangEditor/commit/2281957020a30de9cda1c5e9d5e20c6668b7f592))\n\n\n### Features\n\n* editVideoSize ([375eecb](https://github.com/wangeditor-team/wangEditor/commit/375eecba826eac681268c55c47bcd922f7157d63))\n* setHtml ([f4f91b8](https://github.com/wangeditor-team/wangEditor/commit/f4f91b883298091e3679ca6b206ae0d796003772))\n\n\n\n\n\n## 1.0.1 (2022-04-18)\n\n\n### Bug Fixes\n\n* 部分菜单 disabled ([87f1233](https://github.com/wangeditor-team/wangEditor/commit/87f12332a087072406c1988dc5cef2eae8335375))\n* 错别字 alwaysEnable ([82c5136](https://github.com/wangeditor-team/wangEditor/commit/82c5136f8496be420dfa26b0f30522e19924a907))\n* 弹出 modal 时 blur ([53454ef](https://github.com/wangeditor-team/wangEditor/commit/53454ef74b0775391aecf2d745561c9281715934))\n* 点击编辑器区域，未关闭 dropPanel ([b23123b](https://github.com/wangeditor-team/wangEditor/commit/b23123bb361ac2acadcacdfeaa78dd7bf878f86e))\n* 多余的空行 ([4af6c64](https://github.com/wangeditor-team/wangEditor/commit/4af6c648861c2c56db62fae28e9dfa0d27ca5d51))\n* 多余的空行 ([9dde85c](https://github.com/wangeditor-team/wangEditor/commit/9dde85cec5a27be21e0b89c24288d418e1f6d2de))\n* 更新各包之间依赖版本 ([75c552c](https://github.com/wangeditor-team/wangEditor/commit/75c552cc8ed54765bebb86a7ec5329a7fc79e85f))\n* 获取 activeElement 兼容 Document 和 ShadowRoot ([d904e5d](https://github.com/wangeditor-team/wangEditor/commit/d904e5dc263ce670362779b0cfa51ca9f7a8bd86))\n* 拼音输入 bug we-2021/issues/47 ([20b7429](https://github.com/wangeditor-team/wangEditor/commit/20b74298509d9463d6aa1aaffabc21bd33bd7857))\n* 拼音隐藏 placeholder ([aec1a9f](https://github.com/wangeditor-team/wangEditor/commit/aec1a9f62af8944b7894beeca953076ec73545d5))\n* 全屏边距 ([1acb129](https://github.com/wangeditor-team/wangEditor/commit/1acb12974848af28e2d0f574f85a59145675cdbc))\n* 全选 ([3cb8f42](https://github.com/wangeditor-team/wangEditor/commit/3cb8f428a0b94c280b63d42f46c148a9f0e2d9fd))\n* 上传图片 - base64 仍触发上传 + 超出 maxSize 的报错提醒 ([a1d469a](https://github.com/wangeditor-team/wangEditor/commit/a1d469accb7f87f8ea0282a1699d002aaaa4e79a))\n* 使用了 ts 类型空间导入方式优化 ([5d7b509](https://github.com/wangeditor-team/wangEditor/commit/5d7b5094e561af138b2569c669fd4daad2808f73))\n* 图片上传，提示 ([3754012](https://github.com/wangeditor-team/wangEditor/commit/37540129dff1212c5ebfd4ca3f4d4e8def735e73))\n* 完善了 isDOMEventHandled ([745f1d7](https://github.com/wangeditor-team/wangEditor/commit/745f1d7b949eb8839cbdb0fb1690c33c386b697f))\n* 完善了 metaWithUrl 类型声明 ([3542834](https://github.com/wangeditor-team/wangEditor/commit/3542834b9aa65eba5b1c352d106f6623e5fcdc06))\n* 修复 firefox 上全选编辑器内容使用拼音输入异常 ([87dafcb](https://github.com/wangeditor-team/wangEditor/commit/87dafcbe4c51d588ac97d3825a9389571fa16404))\n* 修复 modal 中的 input 没有被 focus ([484c51e](https://github.com/wangeditor-team/wangEditor/commit/484c51e4629defe9eac3f2acaf83ccb62a669d5d))\n* 修复 modal close 时没有恢复选区的问题 ([16f5a57](https://github.com/wangeditor-team/wangEditor/commit/16f5a57b2815026741249e8b4ef9e7222071353f))\n* 修复回车超过视口后没有自动滚动的问题 ([f088b52](https://github.com/wangeditor-team/wangEditor/commit/f088b52ff8c9386ba9efc2d7d3e97f76c702b26d))\n* 修复了使用拼音输入法在 safari 上光标位置没有正常更新的问题 ([cb4cf12](https://github.com/wangeditor-team/wangEditor/commit/cb4cf12bcb6448e5964c47674281f37db96069fa))\n* 修复连续输入空格滚动条不滚动的bug ([3bd358d](https://github.com/wangeditor-team/wangEditor/commit/3bd358d83969a53f1ed4f3fd349eb186750f9461))\n* 修复内容重复和编辑器内容拖动的一些 bug ([5a9c9d0](https://github.com/wangeditor-team/wangEditor/commit/5a9c9d0b0880dc006180a5c4e5828f54cd1905da))\n* 修复行间距过小无效 ([5f13a5b](https://github.com/wangeditor-team/wangEditor/commit/5f13a5b3dc859a45ad25f88ad363f408d23bcee1))\n* 修复选中内容中文输入时光标定位问题 ([51596a8](https://github.com/wangeditor-team/wangEditor/commit/51596a8b0b920dc1d1a9e39fff7c3624c0aa6f52))\n* 修复用户自定义change事件获取html时tabal报错 ([5204f8e](https://github.com/wangeditor-team/wangEditor/commit/5204f8ebf63abdf8a7093e202411b63ce86c2964))\n* 修复在 Chrome 和 Safari 中删除内容时，内联空节点被选中 ([a47c73f](https://github.com/wangeditor-team/wangEditor/commit/a47c73fc5fa008096165d5ac9c55d01f4a6b045b))\n* 修复在 Safari 下，即使 contenteditable 元素非聚焦状态，并不会删除所选内容 ([3e8ca3c](https://github.com/wangeditor-team/wangEditor/commit/3e8ca3c86074454a75054e5ded03154f6b6544ea))\n* 修复在代码块中中文输入会有多余字符的问题 ([a138c3f](https://github.com/wangeditor-team/wangEditor/commit/a138c3f0a2f25d9f89afb912cff45596f99e6b05))\n* 修复在destory可能出现editor not find的问题 ([ce60416](https://github.com/wangeditor-team/wangEditor/commit/ce604165527435952b5ac4b011842714ec8cd5dd))\n* 修复Safari上table内空行输入报错的问题 ([dae6dc5](https://github.com/wangeditor-team/wangEditor/commit/dae6dc544f714f195989a05970cb6bf272f6eb8b))\n* 修复ua正则不支持100+的问题 ([c488ba0](https://github.com/wangeditor-team/wangEditor/commit/c488ba09183cbfcabef223709464c42fac53aea0))\n* 选择图片会滚动 ([d2a8762](https://github.com/wangeditor-team/wangEditor/commit/d2a87629cedc3533e268a31ca822f414082bf48d))\n* 选中内容输入中文报错 ([890cc68](https://github.com/wangeditor-team/wangEditor/commit/890cc686e566be68227641d5f31b42de66351126))\n* 移除了每个包下的 publishConfig directory 配置 ([16559f0](https://github.com/wangeditor-team/wangEditor/commit/16559f052545c111318be760e64291a521bdcc65))\n* 优化插入新文本的滚动交互 ([71131a4](https://github.com/wangeditor-team/wangEditor/commit/71131a4355d24b805052fa9bcf1515432e4351ad))\n* 优化当父元素有滚动条，插入新文本的滚动交互 ([9275090](https://github.com/wangeditor-team/wangEditor/commit/9275090399f068db14854f2794b9aab996bee22e))\n* 优化了 core 类型声明 ([5b5ee1e](https://github.com/wangeditor-team/wangEditor/commit/5b5ee1ee34300748460cedab6fcd46463820f8ef))\n* 优化了 deleteFragment  函数调用传参 ([8d8145c](https://github.com/wangeditor-team/wangEditor/commit/8d8145c5e496a28e2d586722101d217ba1be7079))\n* 优化了 normalizeDOMPoint 函数 ([31b9999](https://github.com/wangeditor-team/wangEditor/commit/31b99992bdc5bc2cc239320200da7d5ba7d6cfc0))\n* 优化了当编辑失焦编辑区域滚动到顶部的问题 ([ebb966b](https://github.com/wangeditor-team/wangEditor/commit/ebb966bce81023c79727bae846920323f733008d))\n* 优化了浏览器是否支持 beforeinput 事件的兼容性判断 ([ea221bb](https://github.com/wangeditor-team/wangEditor/commit/ea221bb3e176ace7a99854673fd727dedc0b3ba7))\n* 优化选中代码块不应该展示 hoverbar 的交互 ([33dcbd6](https://github.com/wangeditor-team/wangEditor/commit/33dcbd6560dccfbe77e18cfbce8c9f077f19f6cd))\n* 在移动 word 之前折叠展开选区 ([6b9b0f3](https://github.com/wangeditor-team/wangEditor/commit/6b9b0f3c9755c1950b0645c34166bd043a9d05f0))\n* 增加 EXTEND_CONF 配置扩展能力 ([ff75a16](https://github.com/wangeditor-team/wangEditor/commit/ff75a16643b26d2d0e7a92cfdd827d5f0f56a849))\n* 重复创建 ([3682c53](https://github.com/wangeditor-team/wangEditor/commit/3682c53b181b89d2c16b5d9845b381a4813c9e3c))\n* autoFucos ([fea2faf](https://github.com/wangeditor-team/wangEditor/commit/fea2faf0af83a3eec67ee7bc7d76328409d2d703))\n* beforeinput support ([60e6efc](https://github.com/wangeditor-team/wangEditor/commit/60e6efc3b3d6c31c4834e3b40e02fc8bc4ceaea6))\n* blockquote & header insertBreak ([06678c9](https://github.com/wangeditor-team/wangEditor/commit/06678c963e8c8421ecded448de7510b254117550))\n* button 增加 type ([37b3390](https://github.com/wangeditor-team/wangEditor/commit/37b33903e0ae5ffe95ab907791ab484facd052d9))\n* chrome 链接后输入拼音，js 错误 ([6c04fab](https://github.com/wangeditor-team/wangEditor/commit/6c04fabb2c5ec78e13c1e1583685cf726887dcae))\n* clear API ([c188b56](https://github.com/wangeditor-team/wangEditor/commit/c188b567379ae32abcfa879620c995c8d45818c4))\n* code-block 选择语言 - 点击拖拽滚动条 ([b8c75e7](https://github.com/wangeditor-team/wangEditor/commit/b8c75e7dc5332c9da622433380802886dedc4344))\n* composition-end ([082561d](https://github.com/wangeditor-team/wangEditor/commit/082561dc341b45791933757e2cf6102190004674))\n* create - 判断 content length ([c0eadc9](https://github.com/wangeditor-team/wangEditor/commit/c0eadc9bf03edc7576c1d3e957babede4c0b546f))\n* dangerouslyInsertHtml - 兼容异常情况 ([8b549f4](https://github.com/wangeditor-team/wangEditor/commit/8b549f480434782107eda3412bf6530d0d7eb9ba))\n* droplist 过长 ([1de2a76](https://github.com/wangeditor-team/wangEditor/commit/1de2a76ac802b80c1b45537c129e5833b4d73d33))\n* dropPanel 定位 ([e76310a](https://github.com/wangeditor-team/wangEditor/commit/e76310a1c6d4aafb2385faebb005bdddd38f9838))\n* editor.blur() api 无效 ([48cbff3](https://github.com/wangeditor-team/wangEditor/commit/48cbff3142d961ff2eaf2f76a3182488de2e5b93))\n* firefox下全选输入出现多余字符 ([659b107](https://github.com/wangeditor-team/wangEditor/commit/659b1078e3395ff00ddc0d1792fbf9c4d448ca41))\n* fix https://github.com/wangeditor-team/wangEditor-v5/issues/457 ([1d8a46a](https://github.com/wangeditor-team/wangEditor/commit/1d8a46a1b5402c2ecb418db24d9d22532d152cea))\n* fullScreen 隐藏 hoverbar ([ec463d3](https://github.com/wangeditor-team/wangEditor/commit/ec463d302cdc527987741ae6208a625af91ea61c))\n* getElems 增加 id ([1dcedd9](https://github.com/wangeditor-team/wangEditor/commit/1dcedd9392d2eecef29f9c93e8915a2f2f83b8a5))\n* getHtml 死循环 ([4614bfb](https://github.com/wangeditor-team/wangEditor/commit/4614bfb5c3a2658348a59749dd800a349e6c33a9))\n* getHtml API ([c0b60cf](https://github.com/wangeditor-team/wangEditor/commit/c0b60cf47d8eaae4292265906fbe07875e1564c9))\n* group-menu 考虑 excludeKeys ([ecc29f3](https://github.com/wangeditor-team/wangEditor/commit/ecc29f3b24992c8dc0adf006d81b0d4a252683c5))\n* hotkey mod ([d480c20](https://github.com/wangeditor-team/wangEditor/commit/d480c206fd83ecc8d12f36147c210208aa6d6ab3))\n* hoverbar - 处于网页下部 ([6cfb3e2](https://github.com/wangeditor-team/wangEditor/commit/6cfb3e2d364f4532cbafe5c8c6e4b3bc13fa2d78))\n* hoverbar 被点击多次隐藏 ([bf4fc19](https://github.com/wangeditor-team/wangEditor/commit/bf4fc193847e8caba3a67c8dd152eae4f1950c4f))\n* hoverbar active ([ceb3f41](https://github.com/wangeditor-team/wangEditor/commit/ceb3f41deafd8fc2cb8d3e8a498cb8d90ad1c73f))\n* hoverbar modal 重复创建 ([70d2b61](https://github.com/wangeditor-team/wangEditor/commit/70d2b618a0662c88cd5e6691f513009726ce1b9b))\n* hoverbar show/hide ([c96bc83](https://github.com/wangeditor-team/wangEditor/commit/c96bc8378939fecd78807fea4f2b7e1eec2a9ea0))\n* hoverbarKeys - text ([59b4840](https://github.com/wangeditor-team/wangEditor/commit/59b48406b4c373ef029a5f5bdb0d15d925a91a0f))\n* html 特殊字符 ([b3eb81b](https://github.com/wangeditor-team/wangEditor/commit/b3eb81bc9c4aa15c2ff7451c173de15d6c4552bc))\n* i18n - 获取多语言配置 ([9f81597](https://github.com/wangeditor-team/wangEditor/commit/9f815970f8c3c6dddb6bf846ecb672325e80444b))\n* i18n 切换语言 ([b3b4642](https://github.com/wangeditor-team/wangEditor/commit/b3b4642c6e72ab0b13b05657745abb87e71c633d))\n* insertHtml - maxLength ([8c7dc8b](https://github.com/wangeditor-team/wangEditor/commit/8c7dc8b8efe1705af9989b040b04e2f98932cb77))\n* insertHtml - maxLength ([52d72ec](https://github.com/wangeditor-team/wangEditor/commit/52d72ec4778a7a6c6f31a7e95d82fb91c9384ae8))\n* insertHtml - maxLength ([b573359](https://github.com/wangeditor-team/wangEditor/commit/b5733597966b16d876b0c0e18509f04638e1c4df))\n* insertKeys ([0a89420](https://github.com/wangeditor-team/wangEditor/commit/0a8942050bd0b39afb5bbc55ca7842461a5b98eb))\n* link, text hoverbar 选区问题 ([e0b7438](https://github.com/wangeditor-team/wangEditor/commit/e0b7438c89a347f1b0b940d9c11150b72d595529))\n* maxLength - 拼音 + 粘贴 ([3ac4db6](https://github.com/wangeditor-team/wangEditor/commit/3ac4db6d78cbe7a8d1fe19747deb0a17edd9b552))\n* maxLength 对于拼音输入无效 ([117faa6](https://github.com/wangeditor-team/wangEditor/commit/117faa635e99667c4762b58757f045c80f949323))\n* menu 点击多次才能生效 ([6497e39](https://github.com/wangeditor-team/wangEditor/commit/6497e39225a993c4d87f9ffddf20086446a4fbc2))\n* min-height ([460fad5](https://github.com/wangeditor-team/wangEditor/commit/460fad56001e83842786629b1d1f8ed6411f4fd4))\n* modal close ([dbfb3b4](https://github.com/wangeditor-team/wangEditor/commit/dbfb3b42504ae97aa0f641ff7fe5eba208b43580))\n* normalize when create editor ([2b51962](https://github.com/wangeditor-team/wangEditor/commit/2b5196244a93ad7beb316bfa42e557221967d063))\n* parse html - 有些 elem children 需要过滤 ([63cbb80](https://github.com/wangeditor-team/wangEditor/commit/63cbb804c8c7a778a4ee1f4ba8717a11b4b6b5a3))\n* parse-html - space 160 ([54e72bc](https://github.com/wangeditor-team/wangEditor/commit/54e72bcb5ed38b8dc77e957ebd5d35881466b5b3))\n* parse-html - sub sup ([2c15a5f](https://github.com/wangeditor-team/wangEditor/commit/2c15a5f9c9c2de8b34770a6bebfe765d203a03f6))\n* parseHtml - 多空格文本 ([5d4479c](https://github.com/wangeditor-team/wangEditor/commit/5d4479c5d11fc23233ea63f0b69c845fa2ab8630))\n* placeholder - 全选输入中文 ([fe4dd2a](https://github.com/wangeditor-team/wangEditor/commit/fe4dd2a85d54d64e2411c3dfc6cb90ac18003e28))\n* placeHolder elem ([7d577ac](https://github.com/wangeditor-team/wangEditor/commit/7d577ac4d6003d1b4c8575be1c014cfa6632d248))\n* readOnly 时菜单还可操作 ([0d4a29b](https://github.com/wangeditor-team/wangEditor/commit/0d4a29bb5ba8b62ac11a09d3f814abcb1fcf46be))\n* readOnly 依然可以 insertText ([096eeaf](https://github.com/wangeditor-team/wangEditor/commit/096eeafd0fc62edf196ed3a9549c04ce19b6b159))\n* rename es module filename ([1821d4e](https://github.com/wangeditor-team/wangEditor/commit/1821d4eef49e64efcb41b848849ca7a5e6472044))\n* shadowDOM 节点支持问题 ([5eb41f1](https://github.com/wangeditor-team/wangEditor/commit/5eb41f1048ad110003b2ef95e0f22e26b7fd757c))\n* shadowDOM 在失焦状态下元素获取失败 ([98aeccc](https://github.com/wangeditor-team/wangEditor/commit/98aeccc5be85513d577397642a9a2d2f730a0406))\n* table - 粘贴合并单元格的表格 ([56ecb63](https://github.com/wangeditor-team/wangEditor/commit/56ecb6392510d433e092653f0f08183361778a3d))\n* table - elemToHtml ([e36e609](https://github.com/wangeditor-team/wangEditor/commit/e36e6092ef721723169afc8bf0560a47ac9f4dfc))\n* table-cell 全选 ([1ef4872](https://github.com/wangeditor-team/wangEditor/commit/1ef48729e6d99e7414bc89bc4ef0d66c172fc566))\n* tableCell 中 br 报错 ([8604db7](https://github.com/wangeditor-team/wangEditor/commit/8604db751b622c01fa5391af59328236cf13effc))\n* td th 中换行不起作用 ([89c6032](https://github.com/wangeditor-team/wangEditor/commit/89c6032a1c41100b7adaf9927e6bc9c06d0228db))\n* textarea height ([873b04a](https://github.com/wangeditor-team/wangEditor/commit/873b04a65a7140afdc2427ac07fce57b3e2c423e))\n* tooltip ([7e066d1](https://github.com/wangeditor-team/wangEditor/commit/7e066d1368f1bfaaca21e3385647be2dee6837f9))\n* upload progress 0 ([9e660be](https://github.com/wangeditor-team/wangEditor/commit/9e660be126adb969dd8a80166b60d6f62be17b2a))\n* url 后面中文输入异常 ([3bcebc7](https://github.com/wangeditor-team/wangEditor/commit/3bcebc78352e05cfec92eed92ee0b05d233feaef))\n* void node - 不清理 text ([1bc891c](https://github.com/wangeditor-team/wangEditor/commit/1bc891c46318f5c5ab969752b3ddb8d75ee1faf7))\n* vue 组件增加 customPaste ([e764248](https://github.com/wangeditor-team/wangEditor/commit/e76424870c75e09ab6267b604a951444b2e847c5))\n* w-e-menu-tooltip 和 v4 冲突 ([762403b](https://github.com/wangeditor-team/wangEditor/commit/762403b2c4e860b3855cbc0caa883b1443d3c862))\n* z-index ([02ec2d5](https://github.com/wangeditor-team/wangEditor/commit/02ec2d54605e747b7d4e1377a58fc9e14c9bba7c))\n\n\n### Features\n\n* 增加 API ([63d6fe8](https://github.com/wangeditor-team/wangEditor/commit/63d6fe85f17fea31c95fec727126799a979ec2f9))\n* 增加 enable disable API（删除 setConfig setMenuConfig API） ([984fc50](https://github.com/wangeditor-team/wangEditor/commit/984fc50520061fc34ea08f4136bdeb93dee46564))\n* 支持 nodejs 环境 ([484f18c](https://github.com/wangeditor-team/wangEditor/commit/484f18c3abc70d19e51c556f48491c18d390b1e1))\n* API - getElemsByType + move + moveReverse ([748ad71](https://github.com/wangeditor-team/wangEditor/commit/748ad710b55d26ade4df1d8caa0a6ea5d2f6f8c7))\n* basic text paste ([f0a5b98](https://github.com/wangeditor-team/wangEditor/commit/f0a5b980c95fa1e2fc59a898c6e0d0723c276c28))\n* basic text style module ([005b343](https://github.com/wangeditor-team/wangEditor/commit/005b343573ba98f2d0b8480d034ff6807a499aa3))\n* bold & header ([8130c23](https://github.com/wangeditor-team/wangEditor/commit/8130c23ad84485a68cf9ca4b53d52fab1cec4e96))\n* clear color ([93b1a18](https://github.com/wangeditor-team/wangEditor/commit/93b1a189395ba113dfe9f793c69e136607f9a28f))\n* clear editor api ([01b07f2](https://github.com/wangeditor-team/wangEditor/commit/01b07f2a2250661ef121919192d40a4852d50a91))\n* clearStyle menu ([8002f70](https://github.com/wangeditor-team/wangEditor/commit/8002f707ed04b914180ec36fdca0edf48c815e01))\n* close modal ([b5106f4](https://github.com/wangeditor-team/wangEditor/commit/b5106f4428813cf794c468034c80824b0a4f08db))\n* code highlight ([42b2f8d](https://github.com/wangeditor-team/wangEditor/commit/42b2f8d192e2433593c11ad0b8424737f6cffb58))\n* code-block - part ([a8bcd63](https://github.com/wangeditor-team/wangEditor/commit/a8bcd63d882832ac05a32878df0f767d145e0fa7))\n* create editor ([12d98e4](https://github.com/wangeditor-team/wangEditor/commit/12d98e4bee179e9d277ec3ec2ecb827962ed0e75))\n* customPaste ([0f25f5c](https://github.com/wangeditor-team/wangEditor/commit/0f25f5cae3a2cd5ae5832f3fc1026b3ab6d047e0))\n* dangerouslyInsertHtml ([4dc3d0c](https://github.com/wangeditor-team/wangEditor/commit/4dc3d0cb403d751ae067a541868e77083c8ce74c))\n* drag resize image ([cd72028](https://github.com/wangeditor-team/wangEditor/commit/cd72028f1786e2e53079ad5cbef1b8569731ca79))\n* editor 生命周期，自定义事件 ([00e9bc2](https://github.com/wangeditor-team/wangEditor/commit/00e9bc2cfcb8b622764db1c76394491d72ffd93e))\n* editor with-selection plugin ([9f0a39f](https://github.com/wangeditor-team/wangEditor/commit/9f0a39fecf6d92888d2a97929820d3be038efb31))\n* editor.alert ([f147c8f](https://github.com/wangeditor-team/wangEditor/commit/f147c8f234510959c770860ac2f194e8d720f177))\n* editor.isSelectedAll ([960c845](https://github.com/wangeditor-team/wangEditor/commit/960c8455f85a6bc7350f9944be80b3997bc1fea1))\n* editor.showProgressBar ([51761d4](https://github.com/wangeditor-team/wangEditor/commit/51761d466ab3ef7c99e872954d4724ab51d8e28c))\n* focus支持focus到文档末尾 ([628830e](https://github.com/wangeditor-team/wangEditor/commit/628830ef06ff85b3e67001ce30dd9e0557b0aa28))\n* font-size + font-family ([cc649e0](https://github.com/wangeditor-team/wangEditor/commit/cc649e0918ce58e78b4d5ee49a400197b9d04b70))\n* fullScreen ([e7ccd88](https://github.com/wangeditor-team/wangEditor/commit/e7ccd88a7dd58f64b7bd484de428e3a76cc994f7))\n* getElemsByTypePrefix （删掉 getHeaders） ([c18834b](https://github.com/wangeditor-team/wangEditor/commit/c18834b3ebfd97fb36ccbe0faa84e6fe8c30eb67))\n* getHeaders & editor.srcollToElem ([2bfb813](https://github.com/wangeditor-team/wangEditor/commit/2bfb813e4957f080c6676ec38f8f051275cdf44a))\n* getSelectionText + maxLength ([58f6648](https://github.com/wangeditor-team/wangEditor/commit/58f66489b65f857238d96b93120f6de7e2750c81))\n* groupButton disabled ([8ffd44c](https://github.com/wangeditor-team/wangEditor/commit/8ffd44c9a44758e951ca7bd02dd46746fcac1c03))\n* hover bar ([107356e](https://github.com/wangeditor-team/wangEditor/commit/107356eff7bfaf53ce25e39244f8133c80518375))\n* i18n ([c11b244](https://github.com/wangeditor-team/wangEditor/commit/c11b2440f91b99d40bca18b675c66a22b6e160c9))\n* image menu - width 50% 100% ([f9b4c68](https://github.com/wangeditor-team/wangEditor/commit/f9b4c68dff3232b50491b07949c20eb4c18baa6b))\n* image menu config ([bb18774](https://github.com/wangeditor-team/wangEditor/commit/bb187740e9703b4a76cde4f5e4d32ac714aa793a))\n* image menus & position ([bf5beba](https://github.com/wangeditor-team/wangEditor/commit/bf5beba7b3014d63f0b9fe0063530c8b101a5011))\n* indent menu + groupMenu ([08db901](https://github.com/wangeditor-team/wangEditor/commit/08db901cd3a3f2ddb2173cc4b36d471e4e68237e))\n* insert link ([b04242f](https://github.com/wangeditor-team/wangEditor/commit/b04242ffa252d4088f5360c3de45c24d6f493552))\n* list menu ([fe6c083](https://github.com/wangeditor-team/wangEditor/commit/fe6c0830b2c43e335e5972f85096f490694bbe19))\n* menu color - part ([3a6cc86](https://github.com/wangeditor-team/wangEditor/commit/3a6cc86a7f9133d0862310c408abafb30c531734))\n* menu color & dropPanel & menu config ([5d0d41b](https://github.com/wangeditor-team/wangEditor/commit/5d0d41b9a765a7deb583393f129925414c36ef35))\n* menu hotkey ([fee05f1](https://github.com/wangeditor-team/wangEditor/commit/fee05f189434d1e57a32ff0dea1a57db6830318a))\n* modal appendTo body ([fc0ab06](https://github.com/wangeditor-team/wangEditor/commit/fc0ab06d5c7177eceb04643234a8c301ca4de396))\n* onBlur onFocus ([590ab4a](https://github.com/wangeditor-team/wangEditor/commit/590ab4a990048bb22cf15787a5fd4615db5b9ef6))\n* parse html ([2a5eace](https://github.com/wangeditor-team/wangEditor/commit/2a5eace00f33cded50b68e8164748ec2480213fd))\n* placeholder ([a3e4cdc](https://github.com/wangeditor-team/wangEditor/commit/a3e4cdcd474063e4f436327aaf4074bb2126d941))\n* react 组件 ([448fc83](https://github.com/wangeditor-team/wangEditor/commit/448fc838d64dbef52cbcddde0e98eb021d8a9122))\n* scroll config ([b4942b4](https://github.com/wangeditor-team/wangEditor/commit/b4942b4334f255b3d537389be3dacf1642dd5441))\n* selectList ([b7366ab](https://github.com/wangeditor-team/wangEditor/commit/b7366ab2dafd379145d85881052d6f400bd13c85))\n* text and toolbar ([3ae5d0c](https://github.com/wangeditor-team/wangEditor/commit/3ae5d0c4138fec7397ac8629e0012affe6b7dfa4))\n* toHtml 机制 ([1c4d872](https://github.com/wangeditor-team/wangEditor/commit/1c4d8729f84aaab6a448f23064b34a20596305e9))\n* toolbar config - insertKeys ([a2f3c4b](https://github.com/wangeditor-team/wangEditor/commit/a2f3c4be3762831723495bbc9d50eb6c9b05d195))\n* toolbar excludeKeys ([09bd196](https://github.com/wangeditor-team/wangEditor/commit/09bd196ea24c19b04e5e7e38227ca94332847bf8))\n* tooltip ([994d875](https://github.com/wangeditor-team/wangEditor/commit/994d875fee81cf01271c2e440c1df202aa067d0e))\n* updateLink + unLink + viewLink ([254d554](https://github.com/wangeditor-team/wangEditor/commit/254d55466b3c8527dd9f0bf34681abd801c8c8ce))\n* vue2 组件 ([fd7847a](https://github.com/wangeditor-team/wangEditor/commit/fd7847a72db661bbf29cf636d454c075fd331224))\n"
  },
  {
    "path": "packages/core/README.md",
    "content": "# wangEditor core\n\n[wangEditor](https://www.wangeditor.com/) core.\n\n## Main Functionalities\n- View（ model -> vdom -> DOM ） + Selection\n- Menus + toolbar + hoverbar\n- Core editor APIs and events\n- Register third-party modules (menus, plugins...)\n\n## Main dependencies\n- [slate.js](https://docs.slatejs.org/)\n- [snabbdom.js](https://github.com/snabbdom/snabbdom)\n"
  },
  {
    "path": "packages/core/__tests__/config/editor-config.test.ts",
    "content": "/**\n * @description editor config test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport createCoreEditor from '../create-core-editor' // packages/core 不依赖 packages/editor ，不能使用后者的 createEditor\n\ndescribe('editor config', () => {\n  function getStartLocation(editor) {\n    return Editor.start(editor, [])\n  }\n\n  it('if set placeholder option, it will show placeholder element when editor content is empty', () => {\n    const container = document.createElement('div')\n    createCoreEditor({\n      selector: container,\n      config: {\n        placeholder: 'editor placeholder',\n      },\n    })\n    const el = container.querySelector('.w-e-text-placeholder')\n    expect(el!.textContent).toBe('editor placeholder')\n  })\n\n  it('if set placeholder option, it will hide placeholder element when editor content is not empty', () => {\n    const container = document.createElement('div')\n    createCoreEditor({\n      selector: container,\n      config: {\n        placeholder: 'editor placeholder',\n      },\n      content: [{ type: 'paragraph', children: [{ text: '123' }] }],\n    })\n    const el = container.querySelector('.w-e-text-placeholder')\n    expect(el).toBeNull()\n  })\n\n  it('if set readOnly option, isDisabled return true', () => {\n    const editor = createCoreEditor({\n      config: {\n        readOnly: true,\n      },\n    })\n    expect(editor.isDisabled()).toBeTruthy()\n  })\n\n  it('if set readOnly option, can not insert text to editor', () => {\n    const editor = createCoreEditor({\n      config: {\n        readOnly: true,\n      },\n    })\n\n    editor.select(getStartLocation(editor))\n    editor.insertText('xxx') // readOnly 时无法插入文本\n    expect(editor.getText()).toBe('')\n  })\n\n  it('if set maxLength option, the editor can not update content when text length is equal to maxLength', done => {\n    const editor = createCoreEditor({\n      config: {\n        maxLength: 10,\n        onMaxLength: () => {\n          done() // 触发回调，才能完成该测试\n        },\n      },\n    })\n    editor.select(getStartLocation(editor))\n\n    // 插入 9 个字符，小于 maxLength\n    editor.insertText('123456789')\n    expect(editor.getText()).toBe('123456789')\n\n    // 再插入其他字符，则只能插入一个\n    editor.insertText('abc')\n    expect(editor.getText()).toBe('123456789a')\n  })\n\n  it('if set onCreated option, it will be called when created editor', done => {\n    const fn = jest.fn()\n\n    createCoreEditor({\n      config: {\n        onCreated: fn,\n      },\n    })\n\n    setTimeout(() => {\n      expect(fn).toHaveBeenCalled()\n      done()\n    })\n  })\n\n  it('if set onChange option, it will be called when change editor selection', done => {\n    const fn = jest.fn()\n\n    const editor = createCoreEditor({\n      config: {\n        onChange: fn,\n      },\n    })\n\n    editor.select(getStartLocation(editor)) // 选区变化，触发 onchange\n    setTimeout(() => {\n      expect(fn).toHaveBeenCalledWith(editor)\n      done()\n    })\n  })\n\n  it('if set onChange option, it will be called when change editor content', done => {\n    const fn = jest.fn()\n\n    const editor = createCoreEditor({\n      config: {\n        onChange: fn,\n      },\n    })\n\n    editor.select(getStartLocation(editor))\n\n    // 避免选区干扰\n    setTimeout(() => {\n      editor.insertText('123')\n    }, 50)\n    setTimeout(() => {\n      expect(fn).toHaveBeenCalledTimes(2)\n      done()\n    }, 80)\n  })\n\n  it('if set onDestroyed option, it will be called when destroy editor', done => {\n    const fn = jest.fn()\n    const editor = createCoreEditor({\n      config: {\n        onDestroyed: fn,\n      },\n    })\n\n    setTimeout(() => {\n      editor.destroy()\n    })\n\n    setTimeout(() => {\n      expect(fn).toHaveBeenCalledWith(editor)\n      done()\n    }, 20)\n  })\n})\n"
  },
  {
    "path": "packages/core/__tests__/config/menu-config.test.ts",
    "content": "/**\n * @description menu config test\n * @author wangfupeng\n */\n\nimport createCoreEditor from '../create-core-editor' // packages/core 不依赖 packages/editor ，不能使用后者的 createEditor\nimport { registerGlobalMenuConf } from '../../src/config/register'\n\ndescribe('menu config', () => {\n  it('set and get', () => {\n    // 先注册一下菜单 key ，再设置配置（专为单元测试，用户使用时不涉及）\n    registerGlobalMenuConf('bold', {})\n\n    const menuKey = 'bold' // 必须是一个存在的 menu key\n    const menuConfig = {\n      x: 100,\n    }\n\n    const editor = createCoreEditor({\n      config: {\n        MENU_CONF: {\n          [menuKey]: menuConfig,\n        },\n      },\n    })\n\n    expect(editor.getMenuConfig(menuKey)).toEqual(menuConfig)\n  })\n})\n"
  },
  {
    "path": "packages/core/__tests__/config/toolbar-config.test.ts",
    "content": "/**\n * @description toolbar config test\n * @author wangfupeng\n */\n\nimport createCoreEditor from '../create-core-editor'\nimport { IDomEditor } from '../../src/editor/interface'\nimport createToolbarForSrc from '../../src/create/create-toolbar'\n\n// 注册几个菜单，测试用\nimport '../menus/register-menus/index'\n\n// 创建 toolbar\nfunction createToolbar(editor: IDomEditor, customConfig = {}) {\n  const container = document.createElement('div')\n  document.body.appendChild(container)\n  return createToolbarForSrc(editor, {\n    selector: container,\n    config: {\n      toolbarKeys: ['myButtonMenu', 'mySelectMenu', 'myModalMenu'], // 已注册的菜单 key\n      ...customConfig,\n    },\n  })\n}\n\ndescribe('toolbar config', () => {\n  const editor = createCoreEditor()\n\n  it('default config', () => {\n    const toolbar = createToolbar(editor)\n    const defaultConfig = toolbar.getConfig()\n    const { excludeKeys = [], toolbarKeys = [] } = defaultConfig\n    expect(excludeKeys.length).toBe(0)\n    expect(toolbarKeys.length).toBeGreaterThan(0)\n  })\n\n  it('toolbarKeys', () => {\n    const keys = ['mySelectMenu', 'myModalMenu']\n\n    const toolbar = createToolbar(editor, {\n      toolbarKeys: keys,\n    })\n\n    const { toolbarKeys = [] } = toolbar.getConfig()\n    expect(toolbarKeys).toEqual(keys)\n  })\n\n  it('excludeKeys', () => {\n    const keys = ['myButtonMenu', 'mySelectMenu']\n    const toolbar = createToolbar(editor, {\n      excludeKeys: keys,\n    })\n    const { excludeKeys = [] } = toolbar.getConfig()\n    expect(excludeKeys).toEqual(keys)\n  })\n\n  it('insertKeys', () => {\n    const toolbarKeys = ['mySelectMenu', 'myModalMenu']\n    const insertKeysInfo = {\n      index: 0,\n      keys: ['myButtonMenu'],\n    }\n    const toolbar = createToolbar(editor, {\n      toolbarKeys,\n      insertKeys: insertKeysInfo,\n    })\n    const { insertKeys = {} } = toolbar.getConfig()\n    expect(insertKeys).toEqual(insertKeysInfo)\n  })\n})\n"
  },
  {
    "path": "packages/core/__tests__/create/content-to-html.test.ts",
    "content": "/**\n * @description convert to html test\n * @author wangfupeng\n */\n\nimport createEditor from '../../src/create/create-editor'\n\ndescribe('convert to html or text', () => {\n  let container = document.createElement('div')\n\n  beforeEach(() => {\n    container = document.createElement('div')\n    document.body.appendChild(container)\n  })\n\n  afterEach(() => {\n    document.body.removeChild(container)\n  })\n\n  it('convert to html if give selector option', () => {\n    const editor = createEditor({\n      selector: container,\n      content: [{ type: 'paragraph', children: [{ text: 'hello' }] }],\n    })\n    expect(editor.getHtml()).toBe('<div>hello</div>')\n  })\n\n  it('convert to html if not give selector option', () => {\n    const editor = createEditor({\n      // 不传入 selector ，只有 content\n      content: [{ type: 'paragraph', children: [{ text: 'hello' }] }],\n    })\n    expect(editor.getHtml()).toBe('<div>hello</div>')\n  })\n\n  it('convert to text if give selector option', () => {\n    const editor = createEditor({\n      selector: container,\n      content: [\n        { type: 'paragraph', children: [{ text: 'hello' }] },\n        { type: 'paragraph', children: [{ text: 'world' }] },\n      ],\n    })\n    expect(editor.getText()).toBe('hello\\nworld')\n  })\n\n  it('convert to text if not give selector option', () => {\n    const editor = createEditor({\n      // 不传入 selector ，只有 content\n      content: [\n        { type: 'paragraph', children: [{ text: 'hello' }] },\n        { type: 'paragraph', children: [{ text: 'world' }] },\n      ],\n    })\n    expect(editor.getText()).toBe('hello\\nworld')\n  })\n})\n"
  },
  {
    "path": "packages/core/__tests__/create-core-editor.ts",
    "content": "/**\n * @description create editor - 用于 packages/core 单元测试\n * @author wangfupeng\n */\n\nimport createEditor from '../src/create/create-editor'\n\nexport default function (options: any = {}) {\n  const container = document.createElement('div')\n  document.body.appendChild(container)\n\n  return createEditor({\n    selector: container,\n    ...options,\n  })\n}\n"
  },
  {
    "path": "packages/core/__tests__/editor/dom-editor.test.ts",
    "content": "/**\n * @description core editor test\n * @author luochao\n */\n\nimport { Editor, Range as SlateRange } from 'slate'\nimport { DomEditor } from '../../src/editor/dom-editor'\nimport { IDomEditor } from '../../src/editor/interface'\nimport createCoreEditor from '../create-core-editor' // packages/core 不依赖 packages/editor ，不能使用后者的 createEditor\nimport { Key } from '../../src/utils/key'\nimport { NODE_TO_KEY } from '../../src/utils/weak-maps'\n\nlet editor: IDomEditor\n\ndescribe('Core DomEditor', () => {\n  function genStartLocation() {\n    return Editor.start(editor, [])\n  }\n\n  beforeEach(() => {\n    editor = createCoreEditor()\n    editor.select(genStartLocation())\n  })\n\n  afterEach(() => {\n    editor.destroy()\n  })\n\n  test('DomEditor getWindow should throw Error', () => {\n    try {\n      DomEditor.getWindow(editor)\n    } catch (err) {\n      expect(err.message).toBe('Unable to find a host window element for this editor')\n    }\n  })\n\n  test('DomEditor findKey should return Key for a node', () => {\n    editor.apply({\n      type: 'insert_text',\n      path: [0, 0],\n      text: 'test123',\n      offset: 0,\n    })\n\n    const node = editor.children[0]\n\n    expect(DomEditor.findKey(editor, node) instanceof Key).toBeTruthy()\n  })\n\n  test('DomEditor findKey should return unique Key for different node', () => {\n    editor.apply({\n      type: 'insert_node',\n      path: [0, 0],\n      node: {\n        type: 'paragraph',\n        children: [{ text: 'test123' }],\n      },\n    })\n\n    editor.apply({\n      type: 'insert_node',\n      path: [0, 1],\n      node: {\n        type: 'header1',\n        children: [{ text: 'test456' }],\n      },\n    })\n\n    const [node1, node2] = (editor.children[0] as any).children\n    const keyId1 = DomEditor.findKey(editor, node1).id\n    const keyId2 = DomEditor.findKey(editor, node2).id\n\n    expect(keyId1).not.toBe(keyId2)\n  })\n\n  test('DomEditor findKey should generate new key if node do not exist in NODE_TO_KEY', () => {\n    const node = {\n      type: 'header2',\n      children: [{ text: '123' }],\n    }\n\n    // 防卫断言\n    expect(NODE_TO_KEY.get(node)).toBeUndefined()\n\n    const newKey = DomEditor.findKey(editor, node)\n    expect(NODE_TO_KEY.get(node)).toEqual(newKey)\n  })\n\n  test('DomEditor setNewKey should set new value to NODE_TO_KEY', () => {\n    const node = {\n      type: 'header2',\n      children: [{ text: '123' }],\n    }\n\n    expect(NODE_TO_KEY.get(node)).toBeUndefined()\n\n    DomEditor.setNewKey(node)\n\n    expect(NODE_TO_KEY.get(node)).not.toBeUndefined()\n  })\n\n  test('findPath', () => {\n    const p = editor.children[0]\n    // @ts-ignore\n    const textNode = p.children[0]\n\n    const path = DomEditor.findPath(null, textNode)\n    expect(path).toEqual([0, 0])\n  })\n\n  test('findDocumentOrShadowRoot', () => {\n    const doc = DomEditor.findDocumentOrShadowRoot(editor)\n    expect(doc).toBe(document)\n  })\n\n  test('getParentNode', () => {\n    const p = editor.children[0]\n    // @ts-ignore\n    const textNode = p.children[0]\n\n    expect(DomEditor.getParentNode(null, textNode)).toBe(p)\n    expect(DomEditor.getParentNode(null, p)).toBe(editor)\n  })\n\n  test('getParentsNodes', () => {\n    const p = editor.children[0]\n    // @ts-ignore\n    const textNode = p.children[0]\n\n    const parents = DomEditor.getParentsNodes(editor, textNode)\n    expect(parents[0]).toBe(p)\n    expect(parents[1]).toBe(editor)\n  })\n\n  test('getTopNode', () => {\n    const p = editor.children[0]\n    // @ts-ignore\n    const textNode = p.children[0]\n\n    const topNode = DomEditor.getTopNode(editor, textNode)\n    expect(topNode).toBe(p)\n  })\n\n  test('toDOMNode', () => {\n    const p = editor.children[0]\n\n    const key = DomEditor.findKey(editor, p)\n\n    const domNode = DomEditor.toDOMNode(editor, p)\n    expect(domNode.tagName).toBe('DIV')\n    expect(domNode.id).toBe(`w-e-element-${key.id}`)\n  })\n\n  test('hasDOMNode', () => {\n    const p = editor.children[0]\n    const domNode = DomEditor.toDOMNode(editor, p)\n\n    const res = DomEditor.hasDOMNode(editor, domNode)\n    expect(res).toBeTruthy()\n  })\n\n  // TODO 待写...\n  // test('toDOMRange', () => {})\n\n  // TODO 待写...\n  // test('toDOMPoint', () => {})\n\n  test('toSlateNode', () => {\n    const p = editor.children[0]\n    const domNode = DomEditor.toDOMNode(editor, p)\n\n    const slateNode = DomEditor.toSlateNode(null, domNode)\n    expect(slateNode).toBe(p)\n  })\n\n  // TODO 待写...\n  // test('findEventRange', () => {})\n\n  // TODO 待写...\n  // test('toSlateRange', () => {})\n\n  // TODO 待写...\n  // test('toSlatePoint', () => {})\n\n  test('hasRange', () => {\n    editor.insertText('hello')\n    editor.selectAll()\n\n    const res = DomEditor.hasRange(editor, editor.selection as SlateRange)\n    expect(res).toBeTruthy()\n\n    // expect(1).toBe(1)\n  })\n\n  test('getNodeType', () => {\n    const p = editor.children[0]\n    // @ts-ignore\n    const textNode = p.children[0]\n\n    expect(DomEditor.getNodeType(p)).toBe('paragraph')\n    expect(DomEditor.getNodeType(textNode)).toBe('')\n  })\n\n  test('checkNodeType', () => {\n    const p = editor.children[0]\n    expect(DomEditor.checkNodeType(p, 'paragraph')).toBeTruthy()\n  })\n\n  test('getSelectedElems', () => {\n    editor.insertNode({\n      type: 'some-elem',\n      children: [{ text: 'hello' }],\n    })\n    editor.selectAll()\n\n    const selectedElems = DomEditor.getSelectedElems(editor)\n\n    expect(selectedElems.length).toBe(2)\n    expect(selectedElems[1].type).toBe('some-elem')\n  })\n\n  test('getSelectedNodeByType', () => {\n    const p = editor.children[0]\n    const selectedNode = DomEditor.getSelectedNodeByType(editor, 'paragraph')\n    expect(selectedNode).toBe(p)\n  })\n\n  test('getSelectedTextNode', () => {\n    const p = editor.children[0]\n    // @ts-ignore\n    const textNode = p.children[0]\n\n    const selectedTextNode = DomEditor.getSelectedTextNode(editor)\n    expect(selectedTextNode).toBe(textNode)\n  })\n\n  test('isNodeSelected', () => {\n    const p = editor.children[0]\n    // @ts-ignore\n    const textNode = p.children[0]\n\n    expect(DomEditor.isNodeSelected(editor, p)).toBeTruthy()\n    expect(DomEditor.isNodeSelected(editor, textNode)).toBeTruthy()\n  })\n\n  test('isSelectionAtLineEnd', () => {\n    editor.insertText('hello')\n    expect(DomEditor.isSelectionAtLineEnd(editor, [0])).toBeTruthy() // 在第一行的末尾\n\n    editor.select(genStartLocation()) // 选中开始\n    expect(DomEditor.isSelectionAtLineEnd(editor, [0])).toBeFalsy() // 在第一行的开头\n  })\n})\n"
  },
  {
    "path": "packages/core/__tests__/editor/plugins/with-config.test.ts",
    "content": "/**\n * @description config API test\n * @author wangfupeng\n */\n\nimport createCoreEditor from '../../create-core-editor' // packages/core 不依赖 packages/editor ，不能使用后者的 createEditor\nimport { withConfig } from '../../../src/editor/plugins/with-config'\n\nfunction createEditor(...args) {\n  return withConfig(createCoreEditor(...args))\n}\n\ndescribe('editor config API', () => {\n  it('get config', () => {\n    const editor = createEditor()\n    const defaultConfig = editor.getConfig()\n    expect(defaultConfig).not.toBeNull()\n    expect(defaultConfig.autoFocus).toBeTruthy()\n    expect(defaultConfig.readOnly).toBeFalsy()\n    // 其他 props 不一一写了\n  })\n\n  it('get menu config', () => {\n    const editor = createEditor()\n    const insertLinkConfig = editor.getMenuConfig('insertLink')\n    expect(insertLinkConfig).not.toBeNull()\n  })\n\n  it('get all menus', () => {\n    const editor = createEditor()\n    const menuKeys = editor.getAllMenuKeys()\n    expect(Array.isArray(menuKeys)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "packages/core/__tests__/editor/plugins/with-content.test.ts",
    "content": "/**\n * @description content API test\n * @author wangfupeng\n */\n\nimport { Editor, Transforms, Node, Selection } from 'slate'\nimport createCoreEditor from '../../create-core-editor' // packages/core 不依赖 packages/editor ，不能使用后者的 createEditor\nimport { withContent } from '../../../src/editor/plugins/with-content'\nimport { IDomEditor } from '../../../src/editor/interface'\n\nfunction createEditor(...args) {\n  return withContent(createCoreEditor(...args))\n}\n\nlet editor: IDomEditor\n\nfunction setEditorSelection(\n  editor: IDomEditor,\n  selection: Selection = {\n    anchor: { path: [0, 0], offset: 0 },\n    focus: { path: [0, 0], offset: 0 },\n  }\n) {\n  editor.selection = selection\n}\n\nconst ignoreTag = [\n  'doctype',\n  '!doctype',\n  'meta',\n  'script',\n  'style',\n  'link',\n  'frame',\n  'iframe',\n  'title',\n  'svg',\n]\n\ndescribe('editor content API', () => {\n  function getStartLocation(editor) {\n    return Editor.start(editor, [])\n  }\n\n  it('handleTab', () => {\n    const editor = createEditor()\n    editor.select(getStartLocation(editor))\n    editor.handleTab()\n    expect(editor.getText().length).toBe(4) // 默认 tab 键，输入 4 空格\n  })\n\n  it('getHtml', () => {\n    const editor = createEditor({\n      content: [{ type: 'paragraph', children: [{ text: 'hello' }] }],\n    })\n\n    const html = editor.getHtml()\n    expect(html).toBe('<div>hello</div>')\n  })\n\n  it('getHtml with void element', () => {\n    const editor = createEditor({\n      content: [\n        { type: 'paragraph', children: [{ text: 'hello' }] },\n        { type: 'image', children: [{ text: '' }], src: 'test.jpg' },\n      ],\n    })\n\n    const html = editor.getHtml()\n    expect(html).toBe('<div>hello</div><div></div>')\n  })\n\n  it('getText', () => {\n    const editor = createEditor({\n      content: [\n        { type: 'paragraph', children: [{ text: 'hello' }] },\n        { type: 'paragraph', children: [{ text: 'world' }] },\n      ],\n    })\n    const text = editor.getText()\n    expect(text).toBe('hello\\nworld')\n  })\n\n  it('isEmpty', () => {\n    const editor1 = createEditor()\n    expect(editor1.isEmpty()).toBeTruthy()\n\n    const editor2 = createEditor({\n      content: [{ type: 'paragraph', children: [{ text: 'hello' }] }],\n    })\n    expect(editor2.isEmpty()).toBeFalsy()\n  })\n\n  it('getSelectionText', () => {\n    const editor = createEditor({\n      content: [{ type: 'paragraph', children: [{ text: 'hello' }] }],\n    })\n    editor.select(getStartLocation(editor)) // 光标在开始位置\n    expect(editor.getSelectionText()).toBe('')\n\n    editor.select([]) // 全选\n    expect(editor.getSelectionText()).toBe('hello')\n  })\n\n  it('getElemsByTypePrefix', () => {\n    const editor = createEditor({\n      content: [\n        { type: 'header1', children: [{ text: 'a' }] },\n        { type: 'header2', children: [{ text: 'b' }] },\n        { type: 'paragraph', children: [{ text: 'c' }] },\n      ],\n    })\n    const headers = editor.getElemsByTypePrefix('header')\n    expect(headers.length).toBe(2)\n    const pList = editor.getElemsByTypePrefix('paragraph')\n    expect(pList.length).toBe(1)\n    const images = editor.getElemsByTypePrefix('image')\n    expect(images.length).toBe(0)\n  })\n\n  it('getElemsByType', () => {\n    const editor = createEditor({\n      content: [\n        { type: 'header1', children: [{ text: 'a' }] },\n        { type: 'header2', children: [{ text: 'b' }] },\n        { type: 'paragraph', children: [{ text: 'c' }] },\n      ],\n    })\n    const headers = editor.getElemsByType('header')\n    expect(headers.length).toBe(0)\n    const pList = editor.getElemsByType('paragraph')\n    expect(pList.length).toBe(1)\n    const images = editor.getElemsByType('image')\n    expect(images.length).toBe(0)\n  })\n\n  it('deleteBackward with character', () => {\n    const editor = createEditor({\n      content: [{ type: 'paragraph', children: [{ text: 'hello' }] }],\n    })\n    editor.select(getStartLocation(editor)) // 光标在开始位置\n    Transforms.move(editor, { distance: 2, unit: 'character' }) // 光标移动 2 个字符\n\n    editor.deleteBackward('character') // 向后删除\n    expect(editor.getText()).toBe('hllo')\n  })\n\n  it('deleteBackward with word', () => {\n    const editor = createEditor({\n      content: [{ type: 'paragraph', children: [{ text: 'hello world' }] }],\n    })\n    editor.select(getStartLocation(editor)) // 光标在开始位置\n    Transforms.move(editor, { distance: 1, unit: 'word' }) // 光标移动 1 个单词\n\n    editor.deleteBackward('word') // 向后删除\n    expect(editor.getText()).toBe(' world')\n  })\n\n  it('deleteForward with character', () => {\n    const editor = createEditor({\n      content: [{ type: 'paragraph', children: [{ text: 'hello' }] }],\n    })\n    editor.select(getStartLocation(editor)) // 光标在开始位置\n    Transforms.move(editor, { distance: 1, unit: 'character' }) // 光标移动 1 个字符\n\n    editor.deleteForward('character') // 向前删除\n    expect(editor.getText()).toBe('hllo')\n  })\n\n  it('deleteForward with word', () => {\n    const editor = createEditor({\n      content: [{ type: 'paragraph', children: [{ text: 'hello world' }] }],\n    })\n    editor.select(getStartLocation(editor)) // 光标在开始位置\n    Transforms.move(editor, { distance: 1, unit: 'word' }) // 光标移动 1 个 word\n\n    editor.deleteForward('word') // 向前删除\n    expect(editor.getText()).toBe('hello')\n  })\n\n  it('deleteForward with line', () => {\n    const editor = createEditor({\n      content: [\n        { type: 'paragraph', children: [{ text: 'hello' }] },\n        { type: 'paragraph', children: [{ text: 'world' }] },\n      ],\n    })\n    editor.select(getStartLocation(editor)) // 光标在开始位置\n\n    editor.deleteForward('line') // 向前删除\n    expect(editor.getText()).toBe('\\nworld')\n  })\n\n  it('getFragment', () => {\n    const editor = createEditor({\n      content: [{ type: 'paragraph', children: [{ text: 'hello' }] }],\n    })\n    // 选中 'hel'lo\n    editor.select({\n      anchor: {\n        path: [0, 0],\n        offset: 0,\n      },\n      focus: {\n        path: [0, 0],\n        offset: 3,\n      },\n    })\n\n    const fragment = editor.getFragment() // 获取选中内容\n    expect(Node.string(fragment[0])).toBe('hel')\n  })\n\n  it('deleteFragment', () => {\n    const editor = createEditor({\n      content: [{ type: 'paragraph', children: [{ text: 'hello' }] }],\n    })\n    // 选中 'hel'lo\n    editor.select({\n      anchor: {\n        path: [0, 0],\n        offset: 0,\n      },\n      focus: {\n        path: [0, 0],\n        offset: 3,\n      },\n    })\n\n    editor.deleteFragment() // 删除选中内容\n    expect(editor.getText()).toBe('lo')\n  })\n\n  it('insertBreak', () => {\n    const editor = createEditor()\n    editor.select(getStartLocation(editor)) // 光标在开始位置\n\n    editor.insertBreak()\n    const pList = editor.getElemsByTypePrefix('paragraph')\n    expect(pList.length).toBe(2)\n  })\n\n  it('insertText', () => {\n    const editor = createEditor()\n    editor.select(getStartLocation(editor)) // 光标在开始位置\n    editor.insertText('xxx')\n    expect(editor.getText()).toBe('xxx')\n  })\n\n  it('clear', () => {\n    const editor = createEditor({\n      content: [{ type: 'paragraph', children: [{ text: 'hello' }] }],\n    })\n    editor.clear()\n    expect(editor.getText()).toBe('')\n  })\n\n  it('undo', () => {\n    const editor = createEditor()\n    editor.select(getStartLocation(editor)) // 光标在开始位置\n\n    editor.insertText('hello')\n\n    // @ts-ignore\n    editor.undo()\n    expect(editor.getText()).toBe('')\n  })\n\n  it('redo', () => {\n    const editor = createEditor()\n    editor.select(getStartLocation(editor)) // 光标在开始位置\n\n    editor.insertText('hello')\n\n    // @ts-ignore\n    editor.undo()\n    // @ts-ignore\n    editor.redo()\n    expect(editor.getText()).toBe('hello')\n  })\n\n  describe('dangerouslyInsertHtml API', () => {\n    beforeEach(() => {\n      editor = createEditor()\n    })\n\n    // 现在使用的是 packages/core 的 createEditor ，创建的 editor 没有内置各种 module\n    // 所以 dangerouslyInsertHtml 在此测试基本功能即可。其他 tag 在各自的 module 中测试\n\n    test('dangerouslyInsertHtml should insert text with no blank to editor', () => {\n      // insertText 必须要设置 selection 才能生效\n      setEditorSelection(editor)\n\n      const htmlString = '<div>wangEditor!</div>'\n      editor.dangerouslyInsertHtml(htmlString)\n\n      expect(editor.getText().indexOf('wangEditor')).toBeGreaterThan(-1)\n    })\n\n    ignoreTag.forEach(tag => {\n      test(`insert html string with ${tag} element should to be ignore`, () => {\n        setEditorSelection(editor)\n        const htmlString = `<${tag}></${tag}>`\n        editor.dangerouslyInsertHtml(htmlString)\n\n        expect(editor.getHtml().indexOf(tag)).toBe(-1)\n      })\n    })\n  })\n\n  it('getParentNode', () => {\n    const textNode = { text: 'hello' }\n    const p = { type: 'paragraph', children: [textNode] }\n    const editor = createEditor({\n      content: [p],\n    })\n\n    const parentNode = editor.getParentNode(textNode) as any\n    expect(parentNode).not.toBeNull()\n    expect(parentNode.type).toBe('paragraph')\n  })\n\n  it('insertNode', () => {\n    const editor = createEditor()\n    editor.select(getStartLocation(editor))\n\n    const p = { type: 'paragraph', children: [{ text: 'hello' }] }\n    editor.insertNode(p)\n\n    const pList = editor.getElemsByTypePrefix('paragraph')\n    expect(pList.length).toBe(2)\n  })\n\n  describe('setHtml', () => {\n    it('setHtml normal', () => {\n      const editor = createEditor({ html: '<div>hello</div>' })\n      editor.select(getStartLocation(editor))\n\n      const newHtml = '<div>world</div>'\n      editor.setHtml(newHtml)\n\n      expect(editor.getHtml()).toBe(newHtml)\n    })\n\n    it('setHtml blur', () => {\n      const editor = createEditor({\n        html: '<div>hello</div>',\n        autoFocus: false,\n      })\n      expect(editor.isFocused()).toBe(false)\n\n      const newHtml = '<div>world</div>'\n      editor.setHtml(newHtml)\n\n      expect(editor.getHtml()).toBe(newHtml)\n      expect(editor.isFocused()).toBe(false)\n    })\n\n    it('setHtml disabled', () => {\n      const editor = createEditor({ html: '<div>hello</div>' })\n      editor.disable()\n      expect(editor.isDisabled()).toBe(true)\n\n      const newHtml = '<div>world</div>'\n      editor.setHtml(newHtml)\n\n      expect(editor.getHtml()).toBe(newHtml)\n      expect(editor.isDisabled()).toBe(true)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/__tests__/editor/plugins/with-dom.test.ts",
    "content": "/**\n * @description editor DOM API test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport createCoreEditor from '../../create-core-editor' // packages/core 不依赖 packages/editor ，不能使用后者的 createEditor\nimport { withDOM } from '../../../src/editor/plugins/with-dom'\n\nfunction createEditor(...args) {\n  return withDOM(createCoreEditor(...args))\n}\n\ndescribe('editor DOM API', () => {\n  function getStartLocation(editor) {\n    return Editor.start(editor, [])\n  }\n\n  it('editor id', () => {\n    const editor = createEditor()\n    expect(editor.id).not.toBeNull()\n  })\n\n  it('isFullScreen fullScreen unFullScreen', done => {\n    const editor = createEditor()\n\n    expect(editor.isFullScreen).toBeFalsy()\n\n    editor.fullScreen()\n    expect(editor.isFullScreen).toBeTruthy()\n\n    editor.unFullScreen()\n    setTimeout(() => {\n      expect(editor.isFullScreen).toBeFalsy()\n      done()\n    }, 1000)\n  })\n\n  // TODO focus blur isFocused 用 jest 测试异常，以及 editor-config.test.ts 中的 `onFocus` `onBlur`\n\n  it('disable isDisabled enable', () => {\n    const editor = createEditor()\n    editor.select(getStartLocation(editor))\n\n    expect(editor.isDisabled()).toBeFalsy()\n    editor.insertText('123')\n    expect(editor.getText().length).toBe(3)\n\n    editor.disable()\n    expect(editor.isDisabled()).toBeTruthy()\n    editor.insertText('123') // disabled ，不会插入\n    expect(editor.getText().length).toBe(3)\n\n    editor.enable()\n    expect(editor.isDisabled()).toBeFalsy()\n    editor.insertText('123') // enable ，可以插入\n    expect(editor.getText().length).toBe(6)\n  })\n\n  it('destroy', done => {\n    const editor = createEditor()\n    expect(editor.isDestroyed).toBeFalsy()\n\n    setTimeout(() => {\n      editor.destroy()\n      expect(editor.isDestroyed).toBeTruthy()\n      done()\n    })\n  })\n\n  it('toDOMNode', done => {\n    const p = { type: 'paragraph', children: [{ text: 'hello' }] }\n    const editor = createEditor({\n      content: [p],\n    })\n\n    setTimeout(() => {\n      const domNode = editor.toDOMNode(p)\n      expect(domNode.tagName).toBe('DIV')\n      done()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/__tests__/editor/plugins/with-emitter.test.ts",
    "content": "/**\n * @description editor eventBus API test\n * @author wangfupeng\n */\n\nimport createCoreEditor from '../../create-core-editor' // packages/core 不依赖 packages/editor ，不能使用后者的 createEditor\nimport { withEmitter } from '../../../src/editor/plugins/with-emitter'\n\nfunction createEditor(...args) {\n  return withEmitter(createCoreEditor(...args))\n}\n\ndescribe('eventBus API', () => {\n  it('bind and emit', () => {\n    const editor = createEditor()\n\n    const fn1 = jest.fn() // jest mock function\n    const fn2 = jest.fn()\n    const fn3 = jest.fn()\n\n    editor.on('key1', fn1)\n    editor.on('key1', fn2)\n    editor.on('xxxx', fn3)\n\n    editor.emit('key1', 10, 20)\n\n    expect(fn1).toBeCalledWith(10, 20)\n    expect(fn2).toBeCalledWith(10, 20)\n    expect(fn3).not.toBeCalled()\n  })\n\n  it('off single event', () => {\n    const editor = createEditor()\n\n    const fn1 = jest.fn()\n    const fn2 = jest.fn()\n\n    editor.on('key1', fn1)\n    editor.on('key1', fn2)\n\n    editor.off('key1', fn1)\n\n    editor.emit('key1', 10, 20)\n\n    expect(fn1).not.toBeCalled()\n    expect(fn2).toBeCalledWith(10, 20)\n  })\n\n  it('once', () => {\n    const editor = createEditor()\n\n    let n = 1\n\n    const fn1 = jest.fn(() => n++)\n    const fn2 = jest.fn(() => n++)\n\n    editor.once('key1', fn1)\n    editor.once('key1', fn2)\n\n    // 无论 emit 多少次，只有一次生效\n    editor.emit('key1')\n    editor.emit('key1')\n    editor.emit('key1')\n    editor.emit('key1')\n    editor.emit('key1')\n\n    expect(n).toBe(3)\n  })\n})\n"
  },
  {
    "path": "packages/core/__tests__/editor/plugins/with-selection.test.ts",
    "content": "/**\n * @description selection API test\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport createCoreEditor from '../../create-core-editor' // packages/core 不依赖 packages/editor ，不能使用后者的 createEditor\nimport { withSelection } from '../../../src/editor/plugins/with-selection'\n\nfunction createEditor(...args) {\n  return withSelection(createCoreEditor(...args))\n}\n\ndescribe('editor selection API', () => {\n  function getStartLocation(editor) {\n    return Editor.start(editor, [])\n  }\n  function genParagraph() {\n    return { type: 'paragraph', children: [{ text: 'hello' }] }\n  }\n\n  // selection select deselect move 是 slate 自带 API 或属性，不测试\n\n  // // TODO 运行报错，看源码有使用 focus ，可能和这个相关？？？\n  // it('restoreSelection', () => {\n  //   const editor = createEditor()\n  //   editor.select(getStartLocation(editor))\n\n  //   editor.deselect()\n  //   expect(editor.selection).toBeNull()\n\n  //   editor.restoreSelection()\n  //   expect(editor.selection).not.toBeNull()\n  //   // console.log(111, JSON.stringify(editor.selection))\n  // })\n\n  it('isSelectedAll', () => {\n    const p = genParagraph()\n    const editor = createEditor({ content: [p] })\n    expect(editor.isSelectedAll()).toBeFalsy()\n\n    editor.select(getStartLocation(editor))\n    expect(editor.isSelectedAll()).toBeFalsy()\n\n    editor.select([])\n    expect(editor.isSelectedAll()).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "packages/core/__tests__/i18n/index.test.ts",
    "content": "/**\n * @description i18n test\n * @author wangfupeng\n */\n\nimport i18next, { i18nAddResources, i18nChangeLanguage, t } from '../../src/i18n'\n\ndescribe('i18n', () => {\n  // 添加语言项\n  i18nAddResources('en', {\n    module1: {\n      hello: 'hello',\n    },\n  })\n  i18nAddResources('zh-CN', {\n    module1: {\n      hello: '你好',\n    },\n  })\n\n  it('default lang', () => {\n    expect(i18next.language).toBe('zh-CN')\n    expect(t('module1.hello')).toBe('你好')\n  })\n\n  it('change lang', () => {\n    i18nChangeLanguage('en')\n    expect(i18next.language).toBe('en')\n    expect(t('module1.hello')).toBe('hello')\n  })\n})\n"
  },
  {
    "path": "packages/core/__tests__/menus/README.md",
    "content": "# menus test\n\nTODO 各个 modules 中没有这块代码的测试，待编写...\n"
  },
  {
    "path": "packages/core/__tests__/menus/register-menus/index.ts",
    "content": "/**\n * @description 注册菜单，入口\n * @author wangfupeng\n */\n\nimport './register-button-menu'\nimport './register-select-menu'\nimport './register-modal-menu'\n"
  },
  {
    "path": "packages/core/__tests__/menus/register-menus/register-button-menu.ts",
    "content": "/**\n * @description 注册菜单 - button menu\n * @author wangfupeng\n */\n\nimport { registerMenu, IButtonMenu } from '../../../src/menus/index'\nimport { IDomEditor } from '../../../src/editor/interface'\n\nclass MyButtonMenu implements IButtonMenu {\n  readonly title = 'My Button Menu'\n  readonly tag = 'button'\n  getValue(editor: IDomEditor) {\n    return ''\n  }\n  isActive(editor: IDomEditor) {\n    return false\n  }\n  isDisabled(editor: IDomEditor) {\n    return false\n  }\n  exec(editor: IDomEditor, value: string | boolean) {\n    console.log('do..')\n  }\n}\n\nregisterMenu({\n  key: 'myButtonMenu',\n  factory() {\n    return new MyButtonMenu()\n  },\n})\n"
  },
  {
    "path": "packages/core/__tests__/menus/register-menus/register-modal-menu.ts",
    "content": "/**\n * @description 注册菜单 - modal menu\n * @author wangfupeng\n */\n\nimport { registerMenu, IModalMenu } from '../../../src/menus/index'\nimport { IDomEditor } from '../../../src/editor/interface'\n\nclass MyModalMenu implements IModalMenu {\n  readonly title = 'My Modal Menu'\n  readonly tag = 'button'\n  readonly showModal = true\n  readonly modalWidth = 300\n  getValue(editor: IDomEditor) {\n    return ''\n  }\n  isActive(editor: IDomEditor) {\n    return false\n  }\n  isDisabled(editor: IDomEditor) {\n    return false\n  }\n  exec(editor: IDomEditor, value: string | boolean) {\n    console.log('do..')\n  }\n  getModalContentElem(editor: IDomEditor) {\n    return document.createElement('div')\n  }\n  getModalPositionNode(editor: IDomEditor) {\n    return null\n  }\n}\n\nregisterMenu({\n  key: 'myModalMenu',\n  factory() {\n    return new MyModalMenu()\n  },\n})\n"
  },
  {
    "path": "packages/core/__tests__/menus/register-menus/register-select-menu.ts",
    "content": "/**\n * @description 注册菜单 - select menu\n * @author wangfupeng\n */\n\nimport { registerMenu, ISelectMenu, IOption } from '../../../src/menus/index'\nimport { IDomEditor } from '../../../src/editor/interface'\n\nclass MySelectMenu implements ISelectMenu {\n  readonly title = 'My Select Menu'\n  readonly tag = 'select'\n  getValue(editor: IDomEditor) {\n    return ''\n  }\n  isActive(editor: IDomEditor) {\n    return false\n  }\n  isDisabled(editor: IDomEditor) {\n    return false\n  }\n  exec(editor: IDomEditor, value: string | boolean) {\n    console.log('do..')\n  }\n  getOptions(): IOption[] {\n    return [\n      { value: 'a', text: 'a' },\n      { value: 'b', text: 'b' },\n    ]\n  }\n}\n\nregisterMenu({\n  key: 'mySelectMenu',\n  factory() {\n    return new MySelectMenu()\n  },\n})\n"
  },
  {
    "path": "packages/core/__tests__/parse-html/README.md",
    "content": "# parse-html test\n\n各个 module `parseHtml` 已经测试了该模块的代码。\n"
  },
  {
    "path": "packages/core/__tests__/render/README.md",
    "content": "# render test\n\n各个 module `renderElem` 已经测试了该模块的代码。\n"
  },
  {
    "path": "packages/core/__tests__/to-html/README.md",
    "content": "# to-html test\n\n各个 module 中的 `editor.getHtml()` API 会测试到这部分代码。\n"
  },
  {
    "path": "packages/core/__tests__/upload/uploader.test.ts",
    "content": "/**\n * @description uploader test\n * @author wangfupeng\n */\n\nimport createUploader from '../../src/upload/createUploader'\nimport { IUploadConfig } from '../../src/upload/interface'\nimport nock from 'nock'\n\nconst server = 'https://fake-endpoint.wangeditor-v5.com'\n\ndescribe('uploader', () => {\n  test('if should return Uppy object if invoke createUploader function', () => {\n    const uppy = createUploader({\n      server: '/upload',\n      fieldName: 'file1',\n      metaWithUrl: true,\n      meta: {\n        token: 'xxx',\n      },\n      onSuccess: (file, res) => {},\n      onFailed: (file, res) => {},\n      onError: (file, err, res) => {},\n    })\n    expect(uppy).not.toBeNull()\n  })\n\n  test('it should throw can not get address error if not pass server option', () => {\n    try {\n      createUploader({\n        fieldName: 'file1',\n        metaWithUrl: false,\n        onSuccess: (file, res) => {},\n        onFailed: (file, res) => {},\n        onError: (file, err, res) => {},\n      } as IUploadConfig)\n    } catch (err: unknown) {\n      expect((err as Error).message).toBe('Cannot get upload server address\\n没有配置上传地址')\n    }\n  })\n\n  test('it should throw can not get fileName error if not pass fileName option', () => {\n    try {\n      createUploader({\n        server: '/upload',\n        metaWithUrl: false,\n        onSuccess: (file, res) => {},\n        onFailed: (file, res) => {},\n        onError: (file, err, res) => {},\n      } as IUploadConfig)\n    } catch (err: unknown) {\n      expect((err as Error).message).toBe('Cannot get fieldName\\n没有配置 fieldName')\n    }\n  })\n\n  test('it should invoke success callback if file be uploaded successfully', () => {\n    nock(server)\n      .defaultReplyHeaders({\n        'access-control-allow-method': 'POST',\n        'access-control-allow-origin': '*',\n      })\n      .options('/')\n      .reply(200, {})\n      .post('/')\n      .reply(200, {})\n\n    const fn = jest.fn()\n    const uppy = createUploader({\n      server,\n      fieldName: 'file1',\n      metaWithUrl: false,\n      onSuccess: fn,\n      onFailed: (file, res) => {},\n      onError: (file, err, res) => {},\n    })\n\n    // reference https://github.com/transloadit/uppy/blob/main/packages/%40uppy/xhr-upload/src/index.test.js\n    uppy.addFile({\n      source: 'jest',\n      name: 'foo.jpg',\n      type: 'image/jpeg',\n      data: new Blob([Buffer.alloc(8192)]),\n    })\n\n    return uppy.upload().then(() => {\n      expect(fn).toBeCalled()\n    })\n  })\n\n  test('it should invoke onProgress callback if file be uploaded successfully', () => {\n    nock(server)\n      .defaultReplyHeaders({\n        'access-control-allow-method': 'POST',\n        'access-control-allow-origin': '*',\n      })\n      .options('/')\n      .reply(200, {})\n      .post('/')\n      .reply(200, {})\n\n    const fn = jest.fn()\n    const uppy = createUploader({\n      server,\n      fieldName: 'file1',\n      metaWithUrl: false,\n      onSuccess: () => {},\n      onProgress: fn,\n      onFailed: (file, res) => {},\n      onError: (file, err, res) => {},\n    })\n\n    // reference https://github.com/transloadit/uppy/blob/main/packages/%40uppy/xhr-upload/src/index.test.js\n    uppy.addFile({\n      source: 'jest',\n      name: 'foo.jpg',\n      type: 'image/jpeg',\n      data: new Blob([Buffer.alloc(8192)]),\n    })\n\n    return uppy.upload().then(() => {\n      expect(fn).toBeCalled()\n    })\n  })\n\n  test('it should invoke error callback if file be uploaded failed', () => {\n    nock(server)\n      .defaultReplyHeaders({\n        'access-control-allow-method': 'POST',\n        'access-control-allow-origin': '*',\n      })\n      .options('/')\n      .reply(200, {})\n      .post('/')\n      .reply(400, {})\n\n    const fn = jest.fn()\n    const uppy = createUploader({\n      server,\n      fieldName: 'file1',\n      metaWithUrl: false,\n      onSuccess: () => {},\n      onFailed: (file, res) => {},\n      onError: fn,\n    })\n\n    // reference https://github.com/transloadit/uppy/blob/main/packages/%40uppy/xhr-upload/src/index.test.js\n    uppy.addFile({\n      source: 'jest',\n      name: 'foo.jpg',\n      type: 'image/jpeg',\n      data: new Blob([Buffer.alloc(8192)]),\n    })\n\n    return uppy.upload().catch(() => {\n      expect(fn).toBeCalled()\n    })\n  })\n\n  test('it should invoke console.error method if file be uploaded failed and not pass onError option', () => {\n    nock(server)\n      .defaultReplyHeaders({\n        'access-control-allow-method': 'POST',\n        'access-control-allow-origin': '*',\n      })\n      .options('/')\n      .reply(200, {})\n      .post('/')\n      .reply(400, {})\n\n    const fn = jest.fn()\n    console.error = fn\n    const uppy = createUploader({\n      server,\n      fieldName: 'file1',\n      metaWithUrl: false,\n      onSuccess: () => {},\n      onFailed: (file, res) => {},\n    } as any)\n\n    // reference https://github.com/transloadit/uppy/blob/main/packages/%40uppy/xhr-upload/src/index.test.js\n    uppy.addFile({\n      source: 'jest',\n      name: 'foo.jpg',\n      type: 'image/jpeg',\n      data: new Blob([Buffer.alloc(8192)]),\n    })\n\n    return uppy.upload().catch(() => {\n      expect(fn).toBeCalled()\n    })\n  })\n\n  test('it should invoke error callback if file size over max size', () => {\n    const fn = jest.fn()\n    const uppy = createUploader({\n      server,\n      fieldName: 'file1',\n      metaWithUrl: false,\n      onSuccess: () => {},\n      onFailed: (file, res) => {},\n      onError: fn,\n      maxFileSize: 5,\n    })\n\n    try {\n      uppy.addFile({\n        source: 'jest',\n        name: 'foo.jpg',\n        type: 'image/jpeg',\n        data: new Blob([Buffer.alloc(8192)]),\n      })\n    } catch (err) {\n      expect(fn).toBeCalled()\n    }\n  })\n})\n"
  },
  {
    "path": "packages/core/__tests__/utils/util.test.ts",
    "content": "/**\n * @description util fns test\n * @author wangfupeng\n */\n\nimport {\n  genRandomStr,\n  addQueryToUrl,\n  replaceHtmlSpecialSymbols,\n  deReplaceHtmlSpecialSymbols,\n} from '../../src/utils/util'\n\ndescribe('utils', () => {\n  it('gen random', () => {\n    const r1 = genRandomStr()\n    const r2 = genRandomStr()\n    expect(r1).not.toBe(r2)\n  })\n\n  it('add query to url', () => {\n    const params = { a: 10, b: 'hello' }\n\n    const url1 = 'https://wangeditor.com/'\n    expect(addQueryToUrl(url1, params)).toBe('https://wangeditor.com/?a=10&b=hello')\n\n    const url2 = 'https://wangeditor.com/?x=1#123'\n    expect(addQueryToUrl(url2, params)).toBe('https://wangeditor.com/?x=1&a=10&b=hello#123')\n  })\n\n  it('replace html symbol', () => {\n    const html = '<p>hello  world</p>'\n    const res = replaceHtmlSpecialSymbols(html)\n    expect(res).toBe('&lt;p&gt;hello &nbsp;world&lt;/p&gt;')\n  })\n\n  it('replace html symbol', () => {\n    const html = '&lt;p&gt;hello &nbsp;world&lt;/p&gt;'\n    const res = deReplaceHtmlSpecialSymbols(html)\n    expect(res).toBe('<p>hello  world</p>')\n  })\n\n  it('decode html quote symbol', () => {\n    const html = '<p style=\"font-family:&quot;Times New Roman&quot;;\">hello world</p>'\n    const res = deReplaceHtmlSpecialSymbols(html)\n    expect(res).toBe('<p style=\"font-family:\"Times New Roman\";\">hello world</p>')\n  })\n})\n"
  },
  {
    "path": "packages/core/__tests__/utils/vdom.test.ts",
    "content": "/**\n * @description vdom util fns test\n * @author wangfupeng\n */\n\nimport { h, VNode } from 'snabbdom'\nimport {\n  normalizeVnodeData,\n  addVnodeProp,\n  addVnodeDataset,\n  addVnodeStyle,\n} from '../../src/utils/vdom'\n\ndescribe('vdom util fns', () => {\n  it('normalize vnode data', () => {\n    const vnode = h(\n      'div',\n      {\n        key: 'someKey',\n        id: 'div1',\n        className: 'someClassName',\n        'data-custom-name': 'someCustomName',\n      },\n      [\n        h(\n          'p',\n          {\n            id: 'p1',\n          },\n          ['hello']\n        ),\n      ]\n    )\n\n    normalizeVnodeData(vnode)\n\n    // 转换 div 自身\n    const { data = {}, children = [] } = vnode\n    expect(data.key).toBe('someKey')\n    const { props = {}, dataset = {} } = data\n    expect(props.id).toBe('div1')\n    expect(props.className).toBe('someClassName')\n    expect(dataset.customName).toBe('someCustomName')\n\n    // 转换 div 子节点 p\n    const pVNode = (children[0] || {}) as VNode\n    const { props: pProps = {} } = pVNode.data || {}\n    expect(pProps.id).toBe('p1')\n  })\n\n  it('add vnode props', () => {\n    const vnode = h('div', {})\n    addVnodeProp(vnode, { k1: 'v1' })\n\n    const { props = {} } = vnode.data || {}\n    expect(props.k1).toBe('v1')\n  })\n\n  it('add vnode dataset', () => {\n    const vnode = h('div', {})\n    addVnodeDataset(vnode, { k1: 'v1' })\n\n    const { dataset = {} } = vnode.data || {}\n    expect(dataset.k1).toBe('v1')\n  })\n\n  it('add vnode style', () => {\n    const vnode = h('div', {})\n    addVnodeStyle(vnode, { k1: 'v1' })\n\n    const { style = {} } = vnode.data || {}\n    expect(style.k1).toBe('v1')\n  })\n})\n"
  },
  {
    "path": "packages/core/package.json",
    "content": "{\n  \"name\": \"@wangeditor/core\",\n  \"version\": \"1.1.19\",\n  \"description\": \"wangEditor core\",\n  \"author\": \"wangfupeng1988 <wangfupeng1988@163.com>\",\n  \"contributors\": [],\n  \"homepage\": \"https://github.com/wangeditor-team/wangEditor#readme\",\n  \"license\": \"MIT\",\n  \"types\": \"dist/core/src/index.d.ts\",\n  \"main\": \"dist/index.js\",\n  \"module\": \"dist/index.esm.js\",\n  \"browser\": {\n    \"./dist/index.js\": \"./dist/index.js\",\n    \"./dist/index.esm.js\": \"./dist/index.esm.js\"\n  },\n  \"directories\": {\n    \"lib\": \"dist\",\n    \"test\": \"__tests__\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.com/\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wangeditor-team/wangEditor.git\"\n  },\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"test-c\": \"jest --coverage\",\n    \"dev\": \"cross-env NODE_ENV=development rollup -c rollup.config.js\",\n    \"dev-watch\": \"cross-env NODE_ENV=development rollup -c rollup.config.js -w\",\n    \"build\": \"cross-env NODE_ENV=production rollup -c rollup.config.js\",\n    \"dev-size-stats\": \"cross-env NODE_ENV=development:size_stats rollup -c rollup.config.js\",\n    \"size-stats\": \"cross-env NODE_ENV=production:size_stats rollup -c rollup.config.js\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/wangeditor-team/wangEditor/issues\"\n  },\n  \"peerDependencies\": {\n    \"@uppy/core\": \"^2.1.1\",\n    \"@uppy/xhr-upload\": \"^2.0.3\",\n    \"dom7\": \"^3.0.0\",\n    \"is-hotkey\": \"^0.2.0\",\n    \"lodash.camelcase\": \"^4.3.0\",\n    \"lodash.clonedeep\": \"^4.5.0\",\n    \"lodash.debounce\": \"^4.0.8\",\n    \"lodash.foreach\": \"^4.5.0\",\n    \"lodash.isequal\": \"^4.5.0\",\n    \"lodash.throttle\": \"^4.1.1\",\n    \"lodash.toarray\": \"^4.4.0\",\n    \"nanoid\": \"^3.2.0\",\n    \"slate\": \"^0.72.0\",\n    \"snabbdom\": \"^3.1.0\"\n  },\n  \"dependencies\": {\n    \"@types/event-emitter\": \"^0.3.3\",\n    \"event-emitter\": \"^0.3.5\",\n    \"html-void-elements\": \"^2.0.0\",\n    \"i18next\": \"^20.4.0\",\n    \"scroll-into-view-if-needed\": \"^2.2.28\",\n    \"slate-history\": \"^0.66.0\"\n  },\n  \"devDependencies\": {\n    \"@types/is-hotkey\": \"^0.1.2\"\n  }\n}\n"
  },
  {
    "path": "packages/core/rollup.config.js",
    "content": "import { createRollupConfig, IS_PRD } from '../../build/create-rollup-config'\nimport pkg from './package.json'\n\nconst name = 'WangEditorCore'\n\nconst configList = []\n\n// esm\nconst esmConf = createRollupConfig({\n  output: {\n    file: pkg.module,\n    format: 'esm',\n    name,\n  },\n})\nconfigList.push(esmConf)\n\n// umd\nconst umdConf = createRollupConfig({\n  output: {\n    file: pkg.main,\n    format: 'umd',\n    name,\n  },\n})\nconfigList.push(umdConf)\n\nexport default configList\n"
  },
  {
    "path": "packages/core/src/assets/bar-item.less",
    "content": "@import \"../../../vars.less\"; // var and mixin\n\n.w-e-bar-divider {\n  display: inline-flex;\n  width: 1px;\n  height: @toolbar-height;\n  background-color: @toolbar-border-color; // 分割线 bgColor\n  margin: 0 5px;\n}\n\n.w-e-bar-item {\n  position: relative;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  text-align: center;\n  padding: 4px;\n  height: @toolbar-height;\n\n  button {\n    border: none;\n    background: transparent;\n    height: calc(@toolbar-height - 8px);\n    padding: 0 8px;\n    cursor: pointer;\n    display: inline-flex;\n    justify-content: center;\n    align-items: center;\n    color: @toolbar-color;\n    white-space: nowrap; /* 不换行 */\n    overflow: hidden;\n\n    &:hover {\n      background-color: @toolbar-active-bg-color;\n      color: @toolbar-active-color;\n    }\n\n    .title {\n      margin-left: 5px;\n    }\n  }\n\n  .active {\n    background-color: @toolbar-active-bg-color;\n    color: @toolbar-active-color;\n  }\n\n  .disabled {\n    color: @toolbar-disabled-color;\n    cursor: not-allowed;\n\n    svg {\n      fill: @toolbar-disabled-color;\n    }\n\n    &:hover {\n      background-color: @toolbar-bg-color;\n      color: @toolbar-disabled-color;\n  \n      svg {\n        fill: @toolbar-disabled-color;\n      }\n    }\n  }\n}\n\n// ------------------------------------- 分割线 -------------------------------------\n\n// tooltip - bottom\n.w-e-menu-tooltip-v5 {\n  &:before {\n    content: attr(data-tooltip);\n    position: absolute;\n    background-color: @toolbar-active-color; // tooltip 颜色反转，黑底白字\n    color: @toolbar-bg-color;  // tooltip 颜色反转，黑底白字\n    text-align: center;\n    padding: 5px 10px;\n    border-radius: 5px;\n    z-index: 1;\n    opacity: 0;\n    transition: opacity 0.6s;\n    font-size: 0.75em;\n    visibility: hidden;\n    top: @toolbar-height;\n    white-space: pre;\n  }\n  // arrow\n  &:after {\n    content: \"\";\n    position: absolute;\n    border-width: 5px;\n    border-style: solid;\n    opacity: 0;\n    transition: opacity 0.6s;\n    border-color:  transparent transparent @toolbar-active-color transparent;\n    visibility: hidden;\n    top: 30px;\n  }\n  &:hover:before,\n  &:hover:after {\n    opacity: 1;\n    visibility: visible;\n  }\n}\n// tooltip - right\n.w-e-menu-tooltip-v5.tooltip-right {\n  &:before {\n    left: 100%;\n    top: 10px;\n  }\n  // arrow\n  &:after {\n    left: 100%;\n    margin-left: -10px;\n    top: 16px;\n    border-color:  transparent @toolbar-active-color transparent transparent;\n  }\n}\n\n// ------------------------------------- 分割线 -------------------------------------\n\n// barItem group\n.w-e-bar-item-group {\n  .w-e-bar-item-menus-container {\n    display: none; /* 默认隐藏 */\n\n    z-index: 1;\n    background-color: @toolbar-bg-color;\n    position: absolute;\n    top: 0;\n    left: 0;\n    margin-top: @toolbar-height;\n    .shadowBordered(10px);\n  }\n  &:hover {\n    /* hover 时显示下级菜单 */\n    .w-e-bar-item-menus-container {\n      display: block;\n    }\n  }\n}\n\n"
  },
  {
    "path": "packages/core/src/assets/bar.less",
    "content": "@import \"../../../vars.less\"; // var and mixin\n\n.w-e-bar {\n  background-color: @toolbar-bg-color;\n  padding: 0 5px;\n  font-size: @size;\n  color: @toolbar-color;\n\n  svg {\n    width: @size;\n    height: @size;\n    fill: @toolbar-color;\n  }\n}\n.w-e-bar-show {\n  display: flex;\n}\n.w-e-bar-hidden {\n  display: none;\n}\n\n.w-e-hover-bar {\n  position: absolute;\n  .shadowBordered();\n}\n\n.w-e-toolbar {\n  flex-wrap: wrap;\n  position: relative;\n}\n"
  },
  {
    "path": "packages/core/src/assets/common.less",
    "content": ".w-e-text-container *,\n.w-e-toolbar * {\n  padding: 0;\n  margin: 0;\n  box-sizing: border-box;\n  outline: none;\n}\n\n.w-e-text-container {\n  p, li, td, th, blockquote {\n    line-height: 1.5;\n  }\n}\n\n.w-e-toolbar * {\n  line-height: 1.5;\n}"
  },
  {
    "path": "packages/core/src/assets/drop-panel.less",
    "content": "@import \"../../../vars.less\"; // var and mixin\n\n.w-e-drop-panel {\n  z-index: 1;\n  background-color: @toolbar-bg-color;\n  position: absolute;\n  top: 0;\n  .shadowBordered(10px);\n  margin-top: @toolbar-height;\n  min-width: 200px;\n  padding: 10px;\n}\n\n// 当 bar 处于页面下方，则 dropPanel 要显示在 bar 上方\n.w-e-bar-bottom .w-e-drop-panel {\n  top: inherit;\n  bottom: 0;\n  margin-top: 0;\n  margin-bottom: @toolbar-height;\n}\n"
  },
  {
    "path": "packages/core/src/assets/full-screen.less",
    "content": ".w-e-full-screen-container {\n  position: fixed;\n  margin: 0 !important;\n  padding: 0 !important;\n  top: 0 !important;\n  left: 0 !important;\n  right: 0 !important;\n  bottom: 0 !important;\n  height: 100% !important;\n  width: 100% !important;\n  display: flex !important;\n  flex-direction: column !important;\n\n  // [data-w-e-toolbar=\"true\"] {\n  // }\n\n  [data-w-e-textarea=\"true\"] {\n    flex: 1 !important;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/assets/index.less",
    "content": "@import \"common.less\";\n@import \"textarea.less\";\n@import \"bar.less\";\n@import \"bar-item.less\";\n@import \"select-list.less\";\n@import \"drop-panel.less\";\n@import \"modal.less\";\n@import \"progress.less\";\n@import \"full-screen.less\";\n"
  },
  {
    "path": "packages/core/src/assets/modal.less",
    "content": "@import \"../../../vars.less\"; // var and mixin\n\n.w-e-modal {\n  z-index: 1;\n  background-color: @toolbar-bg-color;\n  position: absolute;\n  padding: 20px 15px 0 15px;\n  min-width: 100px;\n  min-height: 40px;\n  color: @toolbar-color;\n  text-align: left;\n  font-size: @size;\n  .shadowBordered(10px);\n\n  .btn-close {\n    position: absolute;\n    right: 8px;\n    top: 7px;\n    cursor: pointer;\n    padding: 5px;\n    line-height: 1;\n\n    svg {\n      width: 10px;\n      height: 10px;\n      fill: @toolbar-color;\n    }\n  }\n\n  .babel-container {\n    display: block;\n    margin-bottom: 15px;\n\n    span {\n      display: block;\n      margin-bottom: 10px;\n    }\n  }\n\n  .button-container {\n    margin-bottom: 15px;\n  }\n\n  button {\n    font-weight: 400;\n    white-space: nowrap;\n    cursor: pointer;\n    transition: all .3s cubic-bezier(.645,.045,.355,1);\n    user-select: none;\n    touch-action: manipulation;\n    height: 32px;\n    padding: 4.5px 15px;\n    color: @toolbar-color;\n    background-color: @modal-button-bg-color;\n    text-align: center;\n    border: 1px solid @modal-button-border-color;\n    border-radius: 4px;\n  }\n\n  textarea,\n  input[type=\"text\"],\n  input[type=\"number\"] {\n    font-variant: tabular-nums;\n    font-feature-settings: \"tnum\";\n    padding: 4.5px 11px;\n    color: @toolbar-color;\n    background-color: @toolbar-bg-color;\n    border: 1px solid @modal-button-border-color;\n    border-radius: 4px;\n    transition: all .3s;\n    width: 100%;\n  }\n\n  textarea {\n    min-height: 60px;\n  }\n}\n\n// modal 有可能直接 append 到 <body> 下面\nbody .w-e-modal {\n  box-sizing: border-box;\n\n  * {\n    box-sizing: border-box;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/assets/progress.less",
    "content": "@import \"../../../vars.less\";\n\n.w-e-progress-bar {\n  position: absolute;\n  width: 0;\n  height: 1px;\n  background-color: @textarea-handler-bg-color;\n  transition: width 0.3s;\n}\n"
  },
  {
    "path": "packages/core/src/assets/select-list.less",
    "content": "@import \"../../../vars.less\"; // var and mixin\n\n\n.w-e-select-list {\n  z-index: 1;\n  position: absolute;\n  left: 0;\n  top: 0;\n  background-color: @toolbar-bg-color;\n  margin-top: @toolbar-height;\n  min-width: 100px;\n  .shadowBordered(10px);\n\n  max-height: 350px;\n  overflow-y: auto;\n\n  ul {\n    list-style: none;\n    line-height: 1;\n\n    .selected {\n      background-color: @toolbar-active-bg-color;\n    }\n    li {\n      cursor: pointer;\n      padding: 7px 0 7px 25px;\n      position: relative;\n      text-align: left;\n      white-space: nowrap; /* 不换行 */\n\n      &:hover {\n        background-color: @toolbar-active-bg-color;\n      }\n\n      svg {\n        position: absolute;\n        left: 0;\n        margin-left: 5px;\n        top: 50%;\n        margin-top: -7px;\n      }\n    }\n  }\n}\n\n// 当 bar 处于页面下方，则 selectList 要显示在 bar 上方\n.w-e-bar-bottom .w-e-select-list {\n  top: inherit;\n  bottom: 0;\n  margin-top: 0;\n  margin-bottom: @toolbar-height;\n}\n"
  },
  {
    "path": "packages/core/src/assets/textarea.less",
    "content": "@import \"../../../vars.less\"; // var and mixin\n\n.w-e-text-container {\n  color: @textarea-color;\n  background-color: @textarea-bg-color;\n  position: relative;\n  height: 100%;\n}\n\n.w-e-text-container .w-e-scroll {\n  height: 100%;\n  // overflow-y: auto; // 在 js 中设置，根据 config 判断是否增加 scroll\n  -webkit-overflow-scrolling: touch;\n}\n\n.w-e-text-container [data-slate-editor] {\n  outline: 0;\n  white-space: pre-wrap; /* 【重要】可以显示空格，在连续多空格的情况下 */\n  word-wrap: break-word;\n  padding: 0 10px;\n  border-top: 1px solid transparent; // 防止 margin-top 溢出\n  min-height: 100%;\n\n  p {\n    margin: 15px 0;\n  }\n  h1,h2,h3,h4,h5 {\n    margin: 20px 0 20px 0;\n  }\n\n  img {\n    max-width: 100%;\n    min-width: 20px;\n    min-height: 20px;\n    cursor: default;\n    display: inline !important;\n  }\n\n  span {\n    text-indent: 0; // issues#4536\n  }\n\n  // 选中的节点\n  [data-selected=\"true\"] {\n    box-shadow: 0 0 0 2px @textarea-selected-border-color;\n  }\n}\n\n.w-e-text-placeholder {\n  color: @textarea-slight-color;\n  position: absolute;\n  font-style: italic;\n  width: 90%;\n  left: 10px;\n  top: 17px;\n  pointer-events: none; // 忽略鼠标行为，重要\n  user-select: none;\n}\n\n.w-e-max-length-info {\n  position: absolute;\n  color: @textarea-slight-color;\n  bottom: 0.5em;\n  right: 1em;\n  pointer-events: none; // 忽略鼠标行为，重要\n  user-select: none;\n}\n"
  },
  {
    "path": "packages/core/src/config/index.ts",
    "content": "/**\n * @description editor config\n * @author wangfupeng\n */\n\nimport forEach from 'lodash.foreach'\nimport cloneDeep from 'lodash.clonedeep'\nimport { IEditorConfig, IMenuConfig, IToolbarConfig } from './interface'\nimport { GLOBAL_MENU_CONF } from './register'\n\n/**\n * 生成编辑器默认配置\n */\nexport function genEditorConfig(userConfig: Partial<IEditorConfig> = {}): IEditorConfig {\n  const defaultMenuConf = cloneDeep(GLOBAL_MENU_CONF)\n  const newMenuConf: IMenuConfig = {}\n\n  // 单独处理 menuConf\n  const { MENU_CONF: userMenuConf = {} } = userConfig\n  forEach(defaultMenuConf, (menuConf, menuKey) => {\n    // 生成新的 menu config\n    newMenuConf[menuKey] = {\n      ...menuConf,\n      ...(userMenuConf[menuKey] || {}),\n    }\n  })\n  delete userConfig.MENU_CONF // 处理完，则删掉 menuConf ，以防下面 merge 时造成干扰\n\n  return {\n    // 默认配置\n    scroll: true,\n    readOnly: false,\n    autoFocus: true,\n    decorate: () => [],\n    maxLength: 0, // 默认不限制\n    MENU_CONF: newMenuConf,\n    hoverbarKeys: {\n      // 'link': { menuKeys: ['editLink', 'unLink', 'viewLink'] },\n    },\n    customAlert(info: string, type: string) {\n      window.alert(`${type}:\\n${info}`)\n    },\n\n    // 合并用户配置\n    ...userConfig,\n  }\n}\n\n/**\n * 生成 toolbar 默认配置\n */\nexport function genToolbarConfig(userConfig?: Partial<IToolbarConfig>): IToolbarConfig {\n  return {\n    // 默认配置\n    toolbarKeys: [],\n    excludeKeys: [],\n    insertKeys: { index: 0, keys: [] },\n    modalAppendToBody: false,\n\n    // 合并用户配置\n    ...(userConfig || {}),\n  }\n}\n"
  },
  {
    "path": "packages/core/src/config/interface.ts",
    "content": "/**\n * @description config interface\n * @author wangfupeng\n */\n\nimport { Range, NodeEntry, Node } from 'slate'\nimport { IDomEditor } from '../editor/interface'\nimport { IMenuGroup } from '../menus/interface'\n\ninterface IHoverbarConf {\n  // key 即 element type\n  [key: string]: {\n    match?: (editor: IDomEditor, n: Node) => boolean // 自定义匹配函数，优先级高于“key 即 element type”\n    menuKeys: string[]\n  }\n}\n\nexport type AlertType = 'success' | 'info' | 'warning' | 'error'\n\nexport interface ISingleMenuConfig {\n  [key: string]: any\n}\n\nexport interface IMenuConfig {\n  [key: string]: ISingleMenuConfig\n}\n\n/**\n * editor config\n */\nexport interface IEditorConfig {\n  //【注意】如增加 onXxx 回调函数时，要同步到 vue2/vue3 组件\n  customAlert: (info: string, type: AlertType) => void\n\n  onCreated?: (editor: IDomEditor) => void\n  onChange?: (editor: IDomEditor) => void\n  onDestroyed?: (editor: IDomEditor) => void\n\n  onMaxLength?: (editor: IDomEditor) => void\n  onFocus?: (editor: IDomEditor) => void\n  onBlur?: (editor: IDomEditor) => void\n\n  /**\n   * 自定义粘贴。返回 true 则继续粘贴，返回 false 则自行实现粘贴，阻止默认粘贴\n   */\n  customPaste?: (editor: IDomEditor, e: ClipboardEvent) => boolean\n\n  // edit state\n  scroll: boolean\n  placeholder?: string\n  readOnly: boolean\n  autoFocus: boolean\n  decorate?: (nodeEntry: NodeEntry) => Range[]\n  maxLength?: number\n\n  // 各个 menu 的配置汇总，可以通过 key 获取单个 menu 的配置\n  MENU_CONF?: IMenuConfig\n\n  // 悬浮菜单栏 menu\n  hoverbarKeys?: IHoverbarConf\n\n  // 自由扩展其他配置\n  EXTEND_CONF?: any\n}\n\n/**\n * toolbar config\n */\nexport interface IToolbarConfig {\n  toolbarKeys: Array<string | IMenuGroup>\n  insertKeys: { index: number; keys: string | Array<string | IMenuGroup> }\n  excludeKeys: Array<string> // 排除哪些菜单\n  modalAppendToBody: boolean // modal append 到 body ，而非 $textAreaContainer 内\n}\n"
  },
  {
    "path": "packages/core/src/config/register.ts",
    "content": "/**\n * @description config register\n * @author wangfupeng\n */\n\nimport { IMenuConfig, ISingleMenuConfig } from '../config/interface'\n\n// 全局的菜单配置\nexport const GLOBAL_MENU_CONF: IMenuConfig = {}\n\n/**\n * 注册全局菜单配置\n * @param key menu key\n * @param config config\n */\nexport function registerGlobalMenuConf(key: string, config?: ISingleMenuConfig) {\n  if (config == null) return\n  GLOBAL_MENU_CONF[key] = config\n}\n"
  },
  {
    "path": "packages/core/src/constants/index.ts",
    "content": "export const IGNORE_TAGS = new Set([\n  'doctype',\n  '!doctype',\n  'meta',\n  'script',\n  'style',\n  'link',\n  'frame',\n  'iframe',\n  'title',\n  'svg', // TODO 暂时忽略\n])\n"
  },
  {
    "path": "packages/core/src/constants/svg.ts",
    "content": "/**\n * @description svg tag\n * @author wangfupeng\n */\n\n/**\n * 【注意】svg 字符串的长度 ，否则会导致代码体积过大\n * 尽量选择 https://www.iconfont.cn/collections/detail?spm=a313x.7781069.0.da5a778a4&cid=20293\n * 找不到再从 iconfont.com 搜索\n */\n\n// 对号\nexport const SVG_CHECK_MARK =\n  '<svg viewBox=\"0 0 1446 1024\"><path d=\"M574.116299 786.736392 1238.811249 48.517862C1272.390222 11.224635 1329.414799 7.827718 1366.75664 41.450462 1403.840015 74.840484 1406.731043 132.084741 1373.10189 169.433699L655.118888 966.834607C653.072421 969.716875 650.835807 972.514337 648.407938 975.210759 615.017957 1012.29409 558.292155 1015.652019 521.195664 982.250188L72.778218 578.493306C35.910826 545.297758 32.859041 488.584019 66.481825 451.242134 99.871807 414.158803 156.597563 410.800834 193.694055 444.202665L574.116299 786.736392Z\"></path></svg>'\n\n// 向下的箭头\nexport const SVG_DOWN_ARROW =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M498.7 655.8l-197.6-268c-8.1-10.9-0.3-26.4 13.3-26.4h395.2c13.6 0 21.4 15.4 13.3 26.4l-197.6 268c-6.6 9-20 9-26.6 0z\"></path></svg>'\n\n// 关闭\nexport const SVG_CLOSE =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M1024 896.1024l-128 128L512 640 128 1024 0 896 384 512 0 128 128 0 512 384 896.1024 0l128 128L640 512z\"></path></svg>'\n"
  },
  {
    "path": "packages/core/src/create/bind-node-relation.ts",
    "content": "/**\n * @description 绑定 node 的关系\n * @author wangfupeng\n */\n\nimport { Element, Editor, Node, Ancestor } from 'slate'\nimport { IDomEditor } from '../editor/interface'\nimport { NODE_TO_INDEX, NODE_TO_PARENT } from '../utils/weak-maps'\n\n/**\n * createEditor 未传递 selector 时，绑定 node 的关系（ NODE_TO_PARENT, NODE_TO_INDEX 等 ）\n * @param node node\n * @param index index\n * @param parent parent node\n * @param editor editor\n */\nfunction bindNodeRelation(node: Node, index: number, parent: Ancestor, editor: IDomEditor) {\n  // 设置相关 weakMap 信息\n  NODE_TO_INDEX.set(node, index)\n  NODE_TO_PARENT.set(node, parent)\n\n  if (Element.isElement(node)) {\n    const { children = [] } = node\n    children.forEach((child: Node, i: number) => bindNodeRelation(child, i, node, editor)) // 递归子节点\n\n    const isVoid = Editor.isVoid(editor, node)\n    if (isVoid) {\n      const [[text]] = Node.texts(node)\n      // 记录 text 相关 weakMap\n      NODE_TO_INDEX.set(text, 0)\n      NODE_TO_PARENT.set(text, node)\n    }\n  }\n}\n\nexport default bindNodeRelation\n"
  },
  {
    "path": "packages/core/src/create/create-editor.ts",
    "content": "/**\n * @description create editor\n * @author wangfupeng\n */\n\nimport { createEditor, Descendant } from 'slate'\nimport { withHistory } from 'slate-history'\nimport { withDOM } from '../editor/plugins/with-dom'\nimport { withConfig } from '../editor/plugins/with-config'\nimport { withContent } from '../editor/plugins/with-content'\nimport { withEventData } from '../editor/plugins/with-event-data'\nimport { withEmitter } from '../editor/plugins/with-emitter'\nimport { withSelection } from '../editor/plugins/with-selection'\nimport { withMaxLength } from '../editor/plugins/with-max-length'\nimport TextArea from '../text-area/TextArea'\nimport HoverBar from '../menus/bar/HoverBar'\nimport { genEditorConfig } from '../config/index'\nimport { IDomEditor } from '../editor/interface'\nimport { DomEditor } from '../editor/dom-editor'\nimport { IEditorConfig } from '../config/interface'\nimport { promiseResolveThen } from '../utils/util'\nimport { isRepeatedCreateTextarea, genDefaultContent, htmlToContent } from './helper'\nimport type { DOMElement } from '../utils/dom'\nimport {\n  EDITOR_TO_TEXTAREA,\n  TEXTAREA_TO_EDITOR,\n  EDITOR_TO_CONFIG,\n  HOVER_BAR_TO_EDITOR,\n  EDITOR_TO_HOVER_BAR,\n} from '../utils/weak-maps'\nimport bindNodeRelation from './bind-node-relation'\nimport $ from '../utils/dom'\n\ntype PluginFnType = <T extends IDomEditor>(editor: T) => T\n\ninterface ICreateOption {\n  selector: string | DOMElement\n  config: Partial<IEditorConfig>\n  content?: Descendant[]\n  html?: string\n  plugins: PluginFnType[]\n}\n\n/**\n * 创建编辑器\n */\nexport default function (option: Partial<ICreateOption>) {\n  const { selector = '', config = {}, content, html, plugins = [] } = option\n\n  // 创建实例 - 使用插件\n  let editor = withHistory(\n    withMaxLength(\n      withEmitter(withSelection(withContent(withConfig(withDOM(withEventData(createEditor()))))))\n    )\n  )\n  if (selector) {\n    // 检查是否对同一个 DOM 重复创建\n    if (isRepeatedCreateTextarea(editor, selector)) {\n      throw new Error(`Repeated create editor by selector '${selector}'`)\n    }\n  }\n\n  // 处理配置\n  const editorConfig = genEditorConfig(config)\n  EDITOR_TO_CONFIG.set(editor, editorConfig)\n  const { hoverbarKeys = {} } = editorConfig\n\n  // 注册第三方插件\n  plugins.forEach(plugin => {\n    editor = plugin(editor)\n  })\n\n  // 初始化内容（要在 config 和 plugins 后面）\n  if (html != null) {\n    // 传入 html ，转换为 JSON content\n    editor.children = htmlToContent(editor, html)\n  }\n  if (content && content.length) {\n    editor.children = content // 传入 JSON content\n  }\n  if (editor.children.length === 0) {\n    editor.children = genDefaultContent() // 默认内容\n  }\n  DomEditor.normalizeContent(editor) // 格式化，用户输入的 content 可能不规范（如两个相连的 text 没有合并）\n\n  if (selector) {\n    // 传入了 selector ，则创建 textarea DOM\n    const textarea = new TextArea(selector)\n    EDITOR_TO_TEXTAREA.set(editor, textarea)\n    TEXTAREA_TO_EDITOR.set(textarea, editor)\n    textarea.changeViewState() // 初始化时触发一次，以便能初始化 textarea DOM 和 selection\n\n    // 判断 textarea 最小高度，并给出提示\n    promiseResolveThen(() => {\n      const $scroll = textarea.$scroll\n      if ($scroll == null) return\n      if ($scroll.height() < 300) {\n        let info = '编辑区域高度 < 300px 这可能会导致 modal hoverbar 定位异常'\n        info += '\\nTextarea height < 300px . This may be cause modal and hoverbar position error'\n        console.warn(info, $scroll)\n      }\n    })\n\n    // 创建 hoverbar DOM\n    let hoverbar: HoverBar | null\n    if (Object.keys(hoverbarKeys).length > 0) {\n      hoverbar = new HoverBar()\n      HOVER_BAR_TO_EDITOR.set(hoverbar, editor)\n      EDITOR_TO_HOVER_BAR.set(editor, hoverbar)\n    }\n\n    // 隐藏 panel and modal\n    editor.on('change', () => {\n      editor.hidePanelOrModal()\n    })\n    editor.on('scroll', () => {\n      editor.hidePanelOrModal()\n    })\n  } else {\n    // 未传入 selector ，则遍历 content ，绑定一些 WeakMap 关系 （ NODE_TO_PARENT, NODE_TO_INDEX 等 ）\n    editor.children.forEach((node, i) => bindNodeRelation(node, i, editor, editor))\n  }\n\n  // 触发生命周期\n  const { onCreated, onDestroyed } = editorConfig\n  if (onCreated) {\n    editor.on('created', () => onCreated(editor))\n  }\n  if (onDestroyed) {\n    editor.on('destroyed', () => onDestroyed(editor))\n  }\n\n  // 创建完毕，异步触发 created\n  promiseResolveThen(() => editor.emit('created'))\n\n  return editor\n}\n"
  },
  {
    "path": "packages/core/src/create/create-toolbar.ts",
    "content": "/**\n * @description create toolbar\n * @author wangfupeng\n */\n\nimport { IDomEditor } from '../editor/interface'\nimport Toolbar from '../menus/bar/Toolbar'\nimport { IToolbarConfig } from '../config/interface'\nimport { genToolbarConfig } from '../config/index'\nimport { isRepeatedCreateToolbar } from './helper'\nimport { DOMElement } from '../utils/dom'\nimport { TOOLBAR_TO_EDITOR, EDITOR_TO_TOOLBAR } from '../utils/weak-maps'\n\ninterface ICreateOption {\n  selector: string | DOMElement\n  config?: Partial<IToolbarConfig>\n}\n\nexport default function (editor: IDomEditor | null, option: ICreateOption): Toolbar {\n  if (editor == null) {\n    throw new Error(`Cannot create toolbar, because editor is null`)\n  }\n  const { selector, config = {} } = option\n\n  // 避免重复创建\n  if (isRepeatedCreateToolbar(editor, selector)) {\n    // 对同一个 DOM 重复创建\n    throw new Error(`Repeated create toolbar by selector '${selector}'`)\n  }\n\n  // 处理配置\n  const toolbarConfig = genToolbarConfig(config)\n\n  // 创建 toolbar ，并记录和 editor 关系\n  const toolbar = new Toolbar(selector, toolbarConfig)\n  TOOLBAR_TO_EDITOR.set(toolbar, editor)\n  EDITOR_TO_TOOLBAR.set(editor, toolbar)\n\n  return toolbar\n}\n"
  },
  {
    "path": "packages/core/src/create/helper.ts",
    "content": "/**\n * @description create helper\n * @author wangfupeng\n */\n\nimport { Descendant } from 'slate'\nimport { IDomEditor } from '../editor/interface'\nimport parseElemHtml from '../parse-html/parse-elem-html'\nimport $, { DOMElement } from '../utils/dom'\n\nfunction isRepeatedCreate(\n  editor: IDomEditor,\n  attrKey: string,\n  selector: string | DOMElement\n): boolean {\n  // @ts-ignore\n  const $elem = $(selector)\n  if ($elem.attr(attrKey)) {\n    return true // 有属性，说明已经创建过\n  }\n\n  // 至此，说明未创建过，则记录\n  $elem.attr(attrKey, 'true')\n\n  // 销毁时删除属性\n  editor.on('destroyed', () => {\n    $elem.removeAttr(attrKey)\n  })\n\n  return false\n}\n\n/**\n * 检查是否重复创建 textarea\n */\nexport function isRepeatedCreateTextarea(\n  editor: IDomEditor,\n  selector: string | DOMElement\n): boolean {\n  return isRepeatedCreate(editor, 'data-w-e-textarea', selector)\n}\n\n/**\n * 检查是否重复创建 toolbar\n */\nexport function isRepeatedCreateToolbar(\n  editor: IDomEditor,\n  selector: string | DOMElement\n): boolean {\n  return isRepeatedCreate(editor, 'data-w-e-toolbar', selector)\n}\n\n/**\n * 生成默认 content\n */\nexport function genDefaultContent() {\n  return [\n    {\n      type: 'paragraph',\n      children: [{ text: '' }],\n    },\n  ]\n}\n\n/**\n * html 字符串 -> content\n * @param editor editor\n * @param html html 字符串\n */\nexport function htmlToContent(editor: IDomEditor, html: string = ''): Descendant[] {\n  const res: Descendant[] = []\n\n  // 空白内容\n  if (html === '') html = '<p><br></p>'\n\n  // 非 HTML 格式，文本格式，用 <p> 包裹\n  if (html.indexOf('<') !== 0) {\n    html = html\n      .split(/\\n/)\n      .map(line => `<p>${line}</p>`)\n      .join('')\n  }\n\n  const $content = $(`<div>${html}</div>`)\n  const list = Array.from($content.children())\n  list.forEach(child => {\n    const $child = $(child)\n    const parsedRes = parseElemHtml($child, editor)\n\n    if (Array.isArray(parsedRes)) {\n      parsedRes.forEach(el => res.push(el))\n    } else {\n      res.push(parsedRes)\n    }\n  })\n\n  return res\n}\n"
  },
  {
    "path": "packages/core/src/create/index.ts",
    "content": "/**\n * @description create entry\n * @author wangfupeng\n */\n\nimport coreCreateEditor from './create-editor'\nimport coreCreateToolbar from './create-toolbar'\n\nexport { coreCreateEditor, coreCreateToolbar }\n"
  },
  {
    "path": "packages/core/src/editor/dom-editor.ts",
    "content": "/**\n * @description 扩展 slate Editor（参考 slate-react react-editor.ts ）\n * @author wangfupeng\n */\n\nimport toArray from 'lodash.toarray'\nimport { Editor, Node, Element, Path, Point, Range, Ancestor, Text } from 'slate'\nimport type { IDomEditor } from './interface'\nimport { Key } from '../utils/key'\nimport TextArea from '../text-area/TextArea'\nimport Toolbar from '../menus/bar/Toolbar'\nimport HoverBar from '../menus/bar/HoverBar'\nimport {\n  EDITOR_TO_ELEMENT,\n  ELEMENT_TO_NODE,\n  KEY_TO_ELEMENT,\n  NODE_TO_INDEX,\n  NODE_TO_KEY,\n  NODE_TO_PARENT,\n  EDITOR_TO_TEXTAREA,\n  EDITOR_TO_TOOLBAR,\n  EDITOR_TO_HOVER_BAR,\n  EDITOR_TO_WINDOW,\n} from '../utils/weak-maps'\nimport $, {\n  DOMElement,\n  DOMNode,\n  DOMPoint,\n  DOMRange,\n  DOMSelection,\n  DOMStaticRange,\n  isDOMElement,\n  normalizeDOMPoint,\n  isDOMSelection,\n  hasShadowRoot,\n  walkTextNodes,\n} from '../utils/dom'\nimport { IS_CHROME, IS_FIREFOX } from '../utils/ua'\n\n/**\n * 自定义全局 command\n */\nexport const DomEditor = {\n  /**\n   * Return the host window of the current editor.\n   */\n  getWindow(editor: IDomEditor): Window {\n    const window = EDITOR_TO_WINDOW.get(editor)\n    if (!window) {\n      throw new Error('Unable to find a host window element for this editor')\n    }\n    return window\n  },\n\n  /**\n   * Find a key for a Slate node.\n   * key 即一个累加不重复的 id ，每一个 slate node 都对对应一个 key ，意思相当于 node.id\n   */\n  findKey(editor: IDomEditor | null, node: Node): Key {\n    let key = NODE_TO_KEY.get(node)\n\n    // 如果没绑定，就立马新建一个 key 来绑定\n    if (!key) {\n      key = new Key()\n      NODE_TO_KEY.set(node, key)\n    }\n\n    return key\n  },\n\n  setNewKey(node: Node) {\n    const key = new Key()\n    NODE_TO_KEY.set(node, key)\n  },\n\n  /**\n   * Find the path of Slate node.\n   * path 是一个数组，代表 slate node 的位置 https://docs.slatejs.org/concepts/03-locations#path\n   */\n  findPath(editor: IDomEditor | null, node: Node): Path {\n    const path: Path = []\n    let child = node\n\n    // eslint-disable-next-line\n    while (true) {\n      const parent = NODE_TO_PARENT.get(child)\n\n      if (parent == null) {\n        if (Editor.isEditor(child)) {\n          // 已到达最顶层，返回 patch\n          return path\n        } else {\n          break\n        }\n      }\n\n      // 获取该节点在父节点中的 index\n      const i = NODE_TO_INDEX.get(child)\n\n      if (i == null) {\n        break\n      }\n\n      // 拼接 patch\n      path.unshift(i)\n\n      // 继续向上递归\n      child = parent\n    }\n\n    throw new Error(`Unable to find the path for Slate node: ${JSON.stringify(node)}`)\n  },\n\n  /**\n   * Find the DOM node that implements DocumentOrShadowRoot for the editor.\n   */\n  findDocumentOrShadowRoot(editor: IDomEditor): Document | ShadowRoot {\n    if (editor.isDestroyed) {\n      return window.document\n    }\n\n    const el = DomEditor.toDOMNode(editor, editor)\n    const root = el.getRootNode()\n\n    if ((root instanceof Document || root instanceof ShadowRoot) && root.getSelection != null) {\n      return root\n    }\n    return el.ownerDocument\n  },\n\n  /**\n   * 获取父节点\n   * @param editor editor\n   * @param node cur node\n   */\n  getParentNode(editor: IDomEditor | null, node: Node): Ancestor | null {\n    return NODE_TO_PARENT.get(node) || null\n  },\n\n  /**\n   * 获取当前节点的所有父节点\n   * @param editor editor\n   * @param node cur node\n   */\n  getParentsNodes(editor: IDomEditor, node: Node): Ancestor[] {\n    const nodes: Ancestor[] = []\n    let curNode = node\n    while (curNode !== editor && curNode != null) {\n      const parentNode = DomEditor.getParentNode(editor, curNode)\n      if (parentNode == null) {\n        break\n      } else {\n        nodes.push(parentNode)\n        curNode = parentNode\n      }\n    }\n    return nodes\n  },\n\n  /**\n   * 获取当前节点对应的顶级节点\n   * @param editor editor\n   * @param curNode cur node\n   */\n  getTopNode(editor: IDomEditor, curNode: Node): Node {\n    const path = DomEditor.findPath(editor, curNode)\n    const topPath = [path[0]]\n    return Node.get(editor, topPath)\n  },\n\n  /**\n   * Find the native DOM element from a Slate node or editor.\n   */\n  toDOMNode(editor: IDomEditor, node: Node): HTMLElement {\n    let domNode\n    const isEditor = Editor.isEditor(node)\n    if (isEditor) {\n      domNode = EDITOR_TO_ELEMENT.get(editor)\n    } else {\n      const key = DomEditor.findKey(editor, node)\n      domNode = KEY_TO_ELEMENT.get(key)\n    }\n\n    if (!domNode) {\n      throw new Error(`Cannot resolve a DOM node from Slate node: ${JSON.stringify(node)}`)\n    }\n\n    return domNode\n  },\n\n  /**\n   * Check if a DOM node is within the editor.\n   */\n  hasDOMNode(editor: IDomEditor, target: DOMNode, options: { editable?: boolean } = {}): boolean {\n    const { editable = false } = options\n    const editorEl = DomEditor.toDOMNode(editor, editor)\n    let targetEl\n\n    // COMPAT: In Firefox, reading `target.nodeType` will throw an error if\n    // target is originating from an internal \"restricted\" element (e.g. a\n    // stepper arrow on a number input). (2018/05/04)\n    // https://github.com/ianstormtaylor/slate/issues/1819\n    try {\n      targetEl = (isDOMElement(target) ? target : target.parentElement) as HTMLElement\n    } catch (err) {\n      if (!err.message.includes('Permission denied to access property \"nodeType\"')) {\n        throw err\n      }\n    }\n\n    if (!targetEl) {\n      return false\n    }\n\n    return (\n      // 祖先节点中包括 data-slate-editor 属性，即 textarea\n      targetEl.closest(`[data-slate-editor]`) === editorEl &&\n      // 通过参数 editable 控制开启是否验证是可编辑元素或零宽字符\n      (!editable || targetEl.isContentEditable || !!targetEl.getAttribute('data-slate-zero-width'))\n    )\n  },\n\n  /**\n   * Find a native DOM range from a Slate `range`.\n   *\n   * Notice: the returned range will always be ordinal regardless of the direction of Slate `range` due to DOM API limit.\n   *\n   * there is no way to create a reverse DOM Range using Range.setStart/setEnd\n   * according to https://dom.spec.whatwg.org/#concept-range-bp-set.\n   */\n  toDOMRange(editor: IDomEditor, range: Range): DOMRange {\n    const { anchor, focus } = range\n    const isBackward = Range.isBackward(range)\n    const domAnchor = DomEditor.toDOMPoint(editor, anchor)\n    const domFocus = Range.isCollapsed(range) ? domAnchor : DomEditor.toDOMPoint(editor, focus)\n\n    const window = DomEditor.getWindow(editor)\n    const domRange = window.document.createRange()\n    const [startNode, startOffset] = isBackward ? domFocus : domAnchor\n    const [endNode, endOffset] = isBackward ? domAnchor : domFocus\n\n    // A slate Point at zero-width Leaf always has an offset of 0 but a native DOM selection at\n    // zero-width node has an offset of 1 so we have to check if we are in a zero-width node and\n    // adjust the offset accordingly.\n    const startEl = (isDOMElement(startNode) ? startNode : startNode.parentElement) as HTMLElement\n    const isStartAtZeroWidth = !!startEl.getAttribute('data-slate-zero-width')\n    const endEl = (isDOMElement(endNode) ? endNode : endNode.parentElement) as HTMLElement\n    const isEndAtZeroWidth = !!endEl.getAttribute('data-slate-zero-width')\n\n    domRange.setStart(startNode, isStartAtZeroWidth ? 1 : startOffset)\n    domRange.setEnd(endNode, isEndAtZeroWidth ? 1 : endOffset)\n    return domRange\n  },\n\n  /**\n   * Find a native DOM selection point from a Slate point.\n   */\n  toDOMPoint(editor: IDomEditor, point: Point): DOMPoint {\n    const [node] = Editor.node(editor, point.path)\n    const el = DomEditor.toDOMNode(editor, node)\n    let domPoint: DOMPoint | undefined\n\n    // If we're inside a void node, force the offset to 0, otherwise the zero\n    // width spacing character will result in an incorrect offset of 1\n    if (Editor.void(editor, { at: point })) {\n      // void 节点，offset 必须为 0\n      point = { path: point.path, offset: 0 }\n    }\n\n    // For each leaf, we need to isolate its content, which means filtering\n    // to its direct text and zero-width spans. (We have to filter out any\n    // other siblings that may have been rendered alongside them.)\n    const selector = `[data-slate-string], [data-slate-zero-width]`\n    const texts = Array.from(el.querySelectorAll(selector))\n    let start = 0\n\n    for (const text of texts) {\n      const domNode = text.childNodes[0] as HTMLElement\n\n      if (domNode == null || domNode.textContent == null) {\n        continue\n      }\n\n      const { length } = domNode.textContent\n      const attr = text.getAttribute('data-slate-length')\n      const trueLength = attr == null ? length : parseInt(attr, 10)\n      const end = start + trueLength\n\n      if (point.offset <= end) {\n        const offset = Math.min(length, Math.max(0, point.offset - start))\n        domPoint = [domNode, offset]\n        break\n      }\n\n      start = end\n    }\n\n    if (!domPoint) {\n      throw new Error(`Cannot resolve a DOM point from Slate point: ${JSON.stringify(point)}`)\n    }\n\n    return domPoint\n  },\n\n  /**\n   * Find a Slate node from a native DOM `element`.\n   */\n  toSlateNode(editor: IDomEditor | null, domNode: DOMNode): Node {\n    let domEl = isDOMElement(domNode) ? domNode : domNode.parentElement\n\n    if (domEl && !domEl.hasAttribute('data-slate-node')) {\n      domEl = domEl.closest(`[data-slate-node]`)\n    }\n\n    const node = domEl ? ELEMENT_TO_NODE.get(domEl as HTMLElement) : null\n\n    if (!node) {\n      throw new Error(`Cannot resolve a Slate node from DOM node: ${domEl}`)\n    }\n\n    return node\n  },\n\n  /**\n   * Get the target range from a DOM `event`.\n   */\n  findEventRange(editor: IDomEditor, event: any): Range {\n    if ('nativeEvent' in event) {\n      // 兼容 react 的合成事件，DOM 事件中没什么用\n      event = event.nativeEvent\n    }\n\n    const { clientX: x, clientY: y, target } = event\n\n    if (x == null || y == null) {\n      throw new Error(`Cannot resolve a Slate range from a DOM event: ${event}`)\n    }\n\n    const node = DomEditor.toSlateNode(editor, event.target)\n    const path = DomEditor.findPath(editor, node)\n\n    // If the drop target is inside a void node, move it into either the\n    // next or previous node, depending on which side the `x` and `y`\n    // coordinates are closest to.\n    if (Editor.isVoid(editor, node)) {\n      const rect = target.getBoundingClientRect()\n      const isPrev = editor.isInline(node)\n        ? x - rect.left < rect.left + rect.width - x\n        : y - rect.top < rect.top + rect.height - y\n\n      const edge = Editor.point(editor, path, {\n        edge: isPrev ? 'start' : 'end',\n      })\n      const point = isPrev ? Editor.before(editor, edge) : Editor.after(editor, edge)\n\n      if (point) {\n        const range = Editor.range(editor, point)\n        return range\n      }\n    }\n\n    // Else resolve a range from the caret position where the drop occured.\n    let domRange\n    const { document } = this.getWindow(editor)\n\n    // COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)\n    if (document.caretRangeFromPoint) {\n      domRange = document.caretRangeFromPoint(x, y)\n    } else {\n      const position = document.caretPositionFromPoint(x, y)\n      if (position) {\n        domRange = document.createRange()\n        domRange.setStart(position.offsetNode, position.offset)\n        domRange.setEnd(position.offsetNode, position.offset)\n      }\n    }\n\n    if (!domRange) {\n      throw new Error(`Cannot resolve a Slate range from a DOM event: ${event}`)\n    }\n\n    // Resolve a Slate range from the DOM range.\n    const range = DomEditor.toSlateRange(editor, domRange, {\n      exactMatch: false,\n      suppressThrow: false,\n    })\n    return range\n  },\n\n  /**\n   * Find a Slate range from a DOM range or selection.\n   */\n  toSlateRange<T extends boolean>(\n    editor: IDomEditor,\n    domRange: DOMRange | DOMStaticRange | DOMSelection,\n    options: {\n      exactMatch: T\n      suppressThrow: T\n    }\n  ): T extends true ? Range | null : Range {\n    const { exactMatch, suppressThrow } = options\n    const el = isDOMSelection(domRange) ? domRange.anchorNode : domRange.startContainer\n    let anchorNode\n    let anchorOffset\n    let focusNode\n    let focusOffset\n    let isCollapsed\n\n    if (el) {\n      if (isDOMSelection(domRange)) {\n        anchorNode = domRange.anchorNode\n        anchorOffset = domRange.anchorOffset\n        focusNode = domRange.focusNode\n        focusOffset = domRange.focusOffset\n        // COMPAT: There's a bug in chrome that always returns `true` for\n        // `isCollapsed` for a Selection that comes from a ShadowRoot.\n        // (2020/08/08)\n        // https://bugs.chromium.org/p/chromium/issues/detail?id=447523\n        if (IS_CHROME && hasShadowRoot()) {\n          isCollapsed =\n            domRange.anchorNode === domRange.focusNode &&\n            domRange.anchorOffset === domRange.focusOffset\n        } else {\n          isCollapsed = domRange.isCollapsed\n        }\n      } else {\n        anchorNode = domRange.startContainer\n        anchorOffset = domRange.startOffset\n        focusNode = domRange.endContainer\n        focusOffset = domRange.endOffset\n        isCollapsed = domRange.collapsed\n      }\n    }\n\n    if (anchorNode == null || focusNode == null || anchorOffset == null || focusOffset == null) {\n      throw new Error(`Cannot resolve a Slate range from DOM range: ${domRange}`)\n    }\n\n    const anchor = DomEditor.toSlatePoint(editor, [anchorNode, anchorOffset], {\n      exactMatch,\n      suppressThrow,\n    })\n    if (!anchor) {\n      return null as T extends true ? Range | null : Range\n    }\n\n    const focus = isCollapsed\n      ? anchor\n      : DomEditor.toSlatePoint(editor, [focusNode, focusOffset], { exactMatch, suppressThrow })\n    if (!focus) {\n      return null as T extends true ? Range | null : Range\n    }\n\n    // return { anchor, focus } as unknown as T extends true ? Range | null : Range\n\n    let range: Range = { anchor: anchor as Point, focus: focus as Point }\n    // if the selection is a hanging range that ends in a void\n    // and the DOM focus is an Element\n    // (meaning that the selection ends before the element)\n    // unhang the range to avoid mistakenly including the void\n    if (\n      Range.isExpanded(range) &&\n      Range.isForward(range) &&\n      isDOMElement(focusNode) &&\n      Editor.void(editor, { at: range.focus, mode: 'highest' })\n    ) {\n      range = Editor.unhangRange(editor, range, { voids: true })\n    }\n\n    return range as unknown as T extends true ? Range | null : Range\n  },\n\n  /**\n   * Find a Slate point from a DOM selection's `domNode` and `domOffset`.\n   */\n  toSlatePoint<T extends boolean>(\n    editor: IDomEditor,\n    domPoint: DOMPoint,\n    options: {\n      exactMatch: T\n      suppressThrow: T\n    }\n  ): T extends true ? Point | null : Point {\n    const { exactMatch, suppressThrow } = options\n    const [nearestNode, nearestOffset] = exactMatch ? domPoint : normalizeDOMPoint(domPoint)\n    const parentNode = nearestNode.parentNode as DOMElement\n    let textNode: DOMElement | null = null\n    let offset = 0\n\n    if (parentNode) {\n      const voidNode = parentNode.closest('[data-slate-void=\"true\"]')\n      let leafNode = parentNode.closest('[data-slate-leaf]')\n      let domNode: DOMElement | null = null\n\n      // Calculate how far into the text node the `nearestNode` is, so that we\n      // can determine what the offset relative to the text node is.\n      if (leafNode) {\n        textNode = leafNode.closest('[data-slate-node=\"text\"]')!\n        const window = DomEditor.getWindow(editor)\n        const range = window.document.createRange()\n        range.setStart(textNode, 0)\n        range.setEnd(nearestNode, nearestOffset)\n        const contents = range.cloneContents()\n        const removals = [\n          ...toArray(contents.querySelectorAll('[data-slate-zero-width]')),\n          ...toArray(contents.querySelectorAll('[contenteditable=false]')),\n        ]\n\n        removals.forEach(el => {\n          el!.parentNode!.removeChild(el)\n        })\n\n        // COMPAT: Edge has a bug where Range.prototype.toString() will\n        // convert \\n into \\r\\n. The bug causes a loop when slate-react\n        // attempts to reposition its cursor to match the native position. Use\n        // textContent.length instead.\n        // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/10291116/\n        offset = contents.textContent!.length\n        domNode = textNode\n      } else if (voidNode) {\n        // For void nodes, the element with the offset key will be a cousin, not an\n        // ancestor, so find it by going down from the nearest void parent.\n        leafNode = voidNode.querySelector('[data-slate-leaf]')!\n\n        // COMPAT: In read-only editors the leaf is not rendered.\n        if (!leafNode) {\n          offset = 1\n        } else {\n          textNode = leafNode.closest('[data-slate-node=\"text\"]')!\n          domNode = leafNode\n          offset = domNode.textContent!.length\n          domNode.querySelectorAll('[data-slate-zero-width]').forEach(el => {\n            offset -= el.textContent!.length\n          })\n        }\n      }\n\n      if (\n        domNode &&\n        offset === domNode.textContent!.length &&\n        // COMPAT: If the parent node is a Slate zero-width space, editor is\n        // because the text node should have no characters. However, during IME\n        // composition the ASCII characters will be prepended to the zero-width\n        // space, so subtract 1 from the offset to account for the zero-width\n        // space character.\n        (parentNode.hasAttribute('data-slate-zero-width') ||\n          // COMPAT: In Firefox, `range.cloneContents()` returns an extra trailing '\\n'\n          // when the document ends with a new-line character. This results in the offset\n          // length being off by one, so we need to subtract one to account for this.\n          (IS_FIREFOX && domNode.textContent?.endsWith('\\n')))\n      ) {\n        offset--\n      }\n    }\n\n    if (!textNode) {\n      if (suppressThrow) {\n        return null as T extends true ? Point | null : Point\n      }\n      throw new Error(`Cannot resolve a Slate point from DOM point: ${domPoint}`)\n    }\n\n    // COMPAT: If someone is clicking from one Slate editor into another,\n    // the select event fires twice, once for the old editor's `element`\n    // first, and then afterwards for the correct `element`. (2017/03/03)\n    const slateNode = DomEditor.toSlateNode(editor, textNode!)\n    const path = DomEditor.findPath(editor, slateNode)\n    return { path, offset } as T extends true ? Point | null : Point\n  },\n\n  hasRange(editor: IDomEditor, range: Range): boolean {\n    const { anchor, focus } = range\n    return Editor.hasPath(editor, anchor.path) && Editor.hasPath(editor, focus.path)\n  },\n\n  getNodeType(node: Node): string {\n    if (Element.isElement(node)) {\n      return node.type\n    }\n    return ''\n  },\n\n  checkNodeType(node: Node, type: string) {\n    return this.getNodeType(node) === type\n  },\n\n  getNodesStr(nodes: Node[]): string {\n    return nodes.map(node => Node.string(node)).join('')\n  },\n\n  getSelectedElems(editor: IDomEditor): Element[] {\n    const elems: Element[] = []\n\n    const nodeEntries = Editor.nodes(editor, { universal: true })\n    for (let nodeEntry of nodeEntries) {\n      const [node] = nodeEntry\n      if (Element.isElement(node)) elems.push(node)\n    }\n\n    return elems\n  },\n\n  getSelectedNodeByType(editor: IDomEditor, type: string): Node | null {\n    const [nodeEntry] = Editor.nodes(editor, {\n      match: n => this.checkNodeType(n, type),\n      universal: true,\n    })\n\n    if (nodeEntry == null) return null\n    return nodeEntry[0]\n  },\n\n  getSelectedTextNode(editor: IDomEditor): Node | null {\n    const [nodeEntry] = Editor.nodes(editor, {\n      match: n => Text.isText(n),\n      universal: true,\n    })\n\n    if (nodeEntry == null) return null\n    return nodeEntry[0]\n  },\n\n  isNodeSelected(editor: IDomEditor, node: Node): boolean {\n    const [nodeEntry] = Editor.nodes(editor, {\n      match: n => n === node,\n      universal: true,\n    })\n    if (nodeEntry == null) return false\n\n    const [n] = nodeEntry\n    if (n === node) return true\n\n    return false\n  },\n\n  isSelectionAtLineEnd(editor: IDomEditor, path: Path): boolean {\n    const { selection } = editor\n\n    if (!selection) return false\n\n    const isAtLineEnd =\n      Editor.isEnd(editor, selection.anchor, path) || Editor.isEnd(editor, selection.focus, path)\n\n    return isAtLineEnd\n  },\n\n  // 获取 textarea 实例\n  getTextarea(editor: IDomEditor): TextArea {\n    const textarea = EDITOR_TO_TEXTAREA.get(editor)\n    if (textarea == null) throw new Error('Cannot find textarea instance by editor')\n    return textarea\n  },\n\n  // 获取 toolbar 实例\n  getToolbar(editor: IDomEditor): Toolbar | null {\n    return EDITOR_TO_TOOLBAR.get(editor) || null\n  },\n\n  // 获取 hoverbar 实例\n  getHoverbar(editor: IDomEditor): HoverBar | null {\n    return EDITOR_TO_HOVER_BAR.get(editor) || null\n  },\n\n  // 格式化 editor content\n  normalizeContent(editor: IDomEditor) {\n    editor.children.forEach((node, index) => {\n      editor.normalizeNode([node, [index]])\n    })\n  },\n\n  /**\n   * 获取：距离触发 maxLength，还可以插入多少字符\n   * @param editor editor\n   */\n  getLeftLengthOfMaxLength(editor: IDomEditor): number {\n    const { maxLength, onMaxLength } = editor.getConfig()\n\n    // 未设置 maxLength ，则返回 number 最大值\n    if (typeof maxLength !== 'number' || maxLength <= 0) return Infinity\n\n    const editorText = editor.getText().replace(/\\r|\\n|(\\r\\n)/g, '') // 去掉换行\n    const curLength = editorText.length\n    const leftLength = maxLength - curLength\n\n    if (leftLength <= 0) {\n      // 触发 maxLength 限制，不再继续插入文字\n      if (onMaxLength) onMaxLength(editor)\n    }\n\n    return leftLength\n  },\n\n  // 清理暴露的 text 节点（拼音输入时经常出现）\n  cleanExposedTexNodeInSelectionBlock(editor: IDomEditor) {\n    // 有时候全选删除新增的文本节点可能不在段落内，因此遍历textArea删除掉\n    const { $textArea } = DomEditor.getTextarea(editor)\n    const childNodes = $textArea?.[0].childNodes\n    if (childNodes) {\n      for (const node of Array.from(childNodes)) {\n        if (node.nodeType === 3) {\n          node.remove()\n        } else {\n          break\n        }\n      }\n    }\n\n    const nodeEntries = Editor.nodes(editor, {\n      match: n => {\n        if (Element.isElement(n)) {\n          if (!editor.isInline(n)) {\n            // 匹配 block element\n            return true\n          }\n        }\n        return false\n      },\n      universal: true,\n    })\n    for (let nodeEntry of nodeEntries) {\n      if (nodeEntry != null) {\n        const n = nodeEntry[0]\n        const elem = DomEditor.toDOMNode(editor, n)\n\n        // 只遍历 elem 范围，考虑性能\n        walkTextNodes(elem, (textNode, parent) => {\n          const $parent = $(parent)\n          if ($parent.attr('data-slate-string')) {\n            return // 正常的 text\n          }\n          if ($parent.attr('data-slate-zero-width')) {\n            return // 正常的 text\n          }\n          if ($parent.attr('data-w-e-reserve')) {\n            return // 故意保留的节点\n          }\n\n          // 暴露的 text node ，删除\n          parent.removeChild(textNode)\n        })\n      }\n    }\n  },\n\n  /**\n   * 是否是编辑器里最后一个元素\n   * @param editor editor\n   * @param node node\n   */\n  isLastNode(editor: IDomEditor, node: Node) {\n    const editorChildren = editor.children || []\n    const editorChildrenLength = editorChildren.length\n    return editorChildren[editorChildrenLength - 1] === node\n  },\n\n  /**\n   * 生成空白 paragraph\n   */\n  genEmptyParagraph(): Element {\n    return { type: 'paragraph', children: [{ text: '' }] }\n  },\n\n  /**\n   * 是否选中了 void node\n   * @param editor editor\n   */\n  isSelectedVoidNode(editor: IDomEditor): boolean {\n    const voidNodes = Editor.nodes(editor, {\n      match: n => editor.isVoid(n as Element),\n    })\n    let len = 0\n    for (const n of voidNodes) {\n      len++\n    }\n    return len > 0\n  },\n\n  /**\n   * 选区是否在一个空行\n   * @param editor editor\n   */\n  isSelectedEmptyParagraph(editor: IDomEditor) {\n    const { selection } = editor\n    if (selection == null) return false\n\n    if (Range.isExpanded(selection)) return false\n\n    const selectedNode = DomEditor.getSelectedNodeByType(editor, 'paragraph')\n    if (selectedNode === null) return false\n\n    const { children } = selectedNode as Element\n    if (children.length !== 1) return false\n\n    const { text } = children[0] as Text\n    if (text === '') return true\n  },\n\n  /**\n   * 当前 path 指向的 node ，是否是空的（无内容）\n   * @param editor editor\n   * @param path path\n   */\n  isEmptyPath(editor: IDomEditor, path: Path): boolean {\n    const entry = Editor.node(editor, path)\n    if (entry == null) return false\n\n    const [node] = entry\n\n    const { children } = node as Element\n    if (children.length === 1) {\n      const { text } = children[0] as Text\n      if (text === '') return true // 内容为空\n    }\n\n    return false\n  },\n}\n"
  },
  {
    "path": "packages/core/src/editor/interface.ts",
    "content": "/**\n * @description editor interface\n * @author wangfupeng\n */\n\nimport { Editor, Location, Node, Ancestor, Element } from 'slate'\nimport ee from 'event-emitter'\nimport { IEditorConfig, AlertType, ISingleMenuConfig } from '../config/interface'\nimport { IPositionStyle } from '../menus/interface'\nimport { DOMElement } from '../utils/dom'\n\nexport type ElementWithId = Element & { id: string }\n\n/**\n * 扩展 slate Editor 接口\n */\nexport interface IDomEditor extends Editor {\n  // data 相关（粘贴、拖拽等）\n  insertData: (data: DataTransfer) => void\n  setFragmentData: (data: Pick<DataTransfer, 'getData' | 'setData'>) => void\n\n  // config\n  getConfig: () => IEditorConfig\n  getMenuConfig: (menuKey: string) => ISingleMenuConfig\n  getAllMenuKeys: () => string[]\n  alert: (info: string, type: AlertType) => void\n\n  // 内容处理\n  handleTab: () => void\n  getHtml: () => string\n  getText: () => string\n  getSelectionText: () => string // 获取选区文字\n  getElemsByTypePrefix: (typePrefix: string) => ElementWithId[]\n  getElemsByType: (type: string, isPrefix?: boolean) => ElementWithId[]\n  getParentNode: (node: Node) => Ancestor | null\n  isEmpty: () => boolean\n  clear: () => void\n  dangerouslyInsertHtml: (html: string, isRecursive?: boolean) => void\n  setHtml: (html: string) => void\n\n  // dom 相关\n  id: string\n  isDestroyed: boolean\n  isFullScreen: boolean\n  focus: (isEnd?: boolean) => void\n  isFocused: () => boolean\n  blur: () => void\n  updateView: () => void\n  destroy: () => void\n  scrollToElem: (id: string) => void\n  showProgressBar: (progress: number) => void\n  hidePanelOrModal: () => void\n  enable: () => void\n  disable: () => void\n  isDisabled: () => boolean\n  toDOMNode: (node: Node) => HTMLElement\n  fullScreen: () => void\n  unFullScreen: () => void\n  getEditableContainer: () => DOMElement\n\n  // selection 相关\n  select: (at: Location) => void\n  deselect: () => void\n  move: (distance: number, reverse?: boolean) => void\n  moveReverse: (distance: number) => void\n  restoreSelection: () => void\n  getSelectionPosition: () => Partial<IPositionStyle>\n  getNodePosition: (node: Node) => Partial<IPositionStyle>\n  isSelectedAll: () => boolean\n  selectAll: () => void\n\n  // 自定义事件\n  on: (type: string, listener: ee.EventListener) => void\n  off: (type: string, listener: ee.EventListener) => void\n  once: (type: string, listener: ee.EventListener) => void\n  emit: (type: string, ...args: any[]) => void\n\n  // undo redo - 不用自己实现，使用 slate-history 扩展\n  undo?: () => void\n  redo?: () => void\n}\n"
  },
  {
    "path": "packages/core/src/editor/plugins/with-config.ts",
    "content": "/**\n * @description slate 插件 - config 相关\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport { IDomEditor } from '../..'\nimport { EDITOR_TO_CONFIG } from '../../utils/weak-maps'\nimport { IEditorConfig, AlertType, ISingleMenuConfig } from '../../config/interface'\nimport { MENU_ITEM_FACTORIES } from '../../menus/register'\n\nexport const withConfig = <T extends Editor>(editor: T) => {\n  const e = editor as T & IDomEditor\n\n  e.getAllMenuKeys = (): string[] => {\n    const arr: string[] = []\n    for (let key in MENU_ITEM_FACTORIES) {\n      arr.push(key)\n    }\n    return arr\n  }\n\n  // 获取 editor 配置信息\n  e.getConfig = (): IEditorConfig => {\n    const config = EDITOR_TO_CONFIG.get(e)\n    if (config == null) throw new Error('Can not get editor config')\n    return config\n  }\n\n  // 获取 menu config\n  e.getMenuConfig = (menuKey: string): ISingleMenuConfig => {\n    const { MENU_CONF = {} } = e.getConfig()\n    return MENU_CONF[menuKey] || {}\n  }\n\n  // alert\n  e.alert = (info: string, type: AlertType = 'info') => {\n    const { customAlert } = e.getConfig()\n    if (customAlert) customAlert(info, type)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/core/src/editor/plugins/with-content.ts",
    "content": "/**\n * @description slate 插件 - content\n * @author wangfupeng\n */\n\nimport { Editor, Node, Text, Path, Operation, Range, Transforms, Element, Descendant } from 'slate'\nimport { DomEditor } from '../dom-editor'\nimport { IDomEditor } from '../..'\nimport { EDITOR_TO_SELECTION, NODE_TO_KEY } from '../../utils/weak-maps'\nimport node2html from '../../to-html/node2html'\nimport { genElemId } from '../../render/helper'\nimport { Key } from '../../utils/key'\nimport $, { DOMElement, NodeType } from '../../utils/dom'\nimport { findCurrentLineRange } from '../../utils/line'\nimport { ElementWithId } from '../interface'\nimport { PARSE_ELEM_HTML_CONF, TEXT_TAGS } from '../../parse-html/index'\nimport parseElemHtml from '../../parse-html/parse-elem-html'\nimport { htmlToContent } from '../../create/helper'\nimport { IGNORE_TAGS } from '../../constants'\n\n/**\n * 把 elem 插入到编辑器\n * @param editor editor\n * @param elem slate elem\n */\nfunction insertElemToEditor(editor: IDomEditor, elem: Element) {\n  if (editor.isInline(elem)) {\n    // inline elem 直接插入\n    editor.insertNode(elem)\n\n    // link 特殊处理，否则后面插入的文字全都在 a 里面 issue#4573\n    if (elem.type === 'link') editor.insertFragment([{ text: '' }])\n  } else {\n    // block elem ，另起一行插入 —— 重要\n    Transforms.insertNodes(editor, elem, { mode: 'highest' })\n  }\n}\n\nexport const withContent = <T extends Editor>(editor: T) => {\n  const e = editor as T & IDomEditor\n  const { onChange, insertText, apply, deleteBackward } = e\n\n  e.insertText = (text: string) => {\n    const { readOnly } = e.getConfig()\n    if (readOnly) return\n\n    insertText(text)\n  }\n\n  // 重写 apply 方法\n  // apply 方法非常重要，它最终执行 operation https://docs.slatejs.org/concepts/05-operations\n  // operation 的接口定义参考 slate src/interfaces/operation.ts\n  e.apply = (op: Operation) => {\n    const matches: [Path, Key][] = []\n\n    switch (op.type) {\n      case 'insert_text':\n      case 'remove_text':\n      case 'set_node': {\n        for (const [node, path] of Editor.levels(e, { at: op.path })) {\n          // 在当前节点寻找\n          const key = DomEditor.findKey(e, node)\n          matches.push([path, key])\n        }\n        break\n      }\n\n      case 'insert_node':\n      case 'remove_node':\n      case 'merge_node':\n      case 'split_node': {\n        for (const [node, path] of Editor.levels(e, { at: Path.parent(op.path) })) {\n          // 在父节点寻找\n          const key = DomEditor.findKey(e, node)\n          matches.push([path, key])\n        }\n        break\n      }\n\n      case 'move_node': {\n        for (const [node, path] of Editor.levels(e, {\n          at: Path.common(Path.parent(op.path), Path.parent(op.newPath)),\n        })) {\n          const key = DomEditor.findKey(e, node)\n          matches.push([path, key])\n        }\n        break\n      }\n    }\n\n    // 执行原本的 apply - 重要！！！\n    apply(op)\n\n    // 绑定 node 和 key\n    for (const [path, key] of matches) {\n      const [node] = Editor.node(e, path)\n      NODE_TO_KEY.set(node, key)\n    }\n  }\n\n  e.deleteBackward = unit => {\n    if (unit !== 'line') {\n      return deleteBackward(unit)\n    }\n\n    if (editor.selection && Range.isCollapsed(editor.selection)) {\n      const parentBlockEntry = Editor.above(editor, {\n        match: n => Editor.isBlock(editor, n),\n        at: editor.selection,\n      })\n\n      if (parentBlockEntry) {\n        const [, parentBlockPath] = parentBlockEntry\n        const parentElementRange = Editor.range(editor, parentBlockPath, editor.selection.anchor)\n\n        const currentLineRange = findCurrentLineRange(e, parentElementRange)\n\n        if (!Range.isCollapsed(currentLineRange)) {\n          Transforms.delete(editor, { at: currentLineRange })\n        }\n      }\n    }\n  }\n\n  // 重写 onchange API\n  e.onChange = () => {\n    // 记录当前选区\n    const { selection } = e\n    if (selection != null) {\n      EDITOR_TO_SELECTION.set(e, selection)\n    }\n\n    // 触发配置的 change 事件\n    e.emit('change')\n\n    onChange()\n  }\n\n  // tab\n  e.handleTab = () => {\n    e.insertText('    ')\n  }\n\n  // 获取 html （去掉了格式化 2021.12.10）\n  e.getHtml = (): string => {\n    const { children = [] } = e\n    const html = children.map(child => node2html(child, e)).join('')\n    return html\n  }\n\n  // 获取 text\n  e.getText = (): string => {\n    const { children = [] } = e\n    return children.map(child => Node.string(child)).join('\\n')\n  }\n\n  // 获取选区文字\n  e.getSelectionText = (): string => {\n    const { selection } = e\n    if (selection == null) return ''\n    return Editor.string(editor, selection)\n  }\n\n  // 根据 type 获取 elems\n  e.getElemsByType = (type: string, isPrefix = false): ElementWithId[] => {\n    const elems: ElementWithId[] = []\n\n    // 获取 editor 所有 nodes\n    const nodeEntries = Editor.nodes(e, {\n      at: [],\n      universal: true,\n    })\n    for (let nodeEntry of nodeEntries) {\n      const [node] = nodeEntry\n      if (Element.isElement(node)) {\n        // 判断 type （前缀 or 全等）\n        let flag = isPrefix ? node.type.indexOf(type) >= 0 : node.type === type\n        if (flag) {\n          const key = DomEditor.findKey(e, node)\n          const id = genElemId(key.id)\n\n          // node + id\n          elems.push({\n            ...node,\n            id,\n          })\n        }\n      }\n    }\n\n    return elems\n  }\n\n  // 根据 type 前缀，获取 elems\n  e.getElemsByTypePrefix = (typePrefix: string): ElementWithId[] => {\n    return e.getElemsByType(typePrefix, true)\n  }\n\n  /**\n   * 判断 editor 是否为空（只有一个空 paragraph）\n   */\n  e.isEmpty = () => {\n    const { children = [] } = e\n    if (children.length > 1) return false // >1 个顶级节点\n\n    const firstNode = children[0]\n    if (firstNode == null) return true // editor.children 空数组\n\n    if (Element.isElement(firstNode) && firstNode.type === 'paragraph') {\n      const { children: texts = [] } = firstNode\n      if (texts.length > 1) return false // >1 text node\n\n      const t = texts[0]\n      if (t == null) return true // 无 text 节点\n\n      if (Text.isText(t) && t.text === '') return true // 只有一个 text 且是空字符串\n    }\n\n    return false\n  }\n\n  /**\n   * 清空内容\n   */\n  e.clear = () => {\n    const initialEditorValue: Node[] = [\n      {\n        type: 'paragraph',\n        children: [{ text: '' }],\n      },\n    ]\n\n    Transforms.delete(e, {\n      at: {\n        anchor: Editor.start(e, []),\n        focus: Editor.end(e, []),\n      },\n    })\n\n    if (e.children.length === 0) {\n      Transforms.insertNodes(e, initialEditorValue)\n    }\n  }\n\n  e.getParentNode = (node: Node) => {\n    return DomEditor.getParentNode(e, node)\n  }\n\n  /**\n   * 插入 html （不保证语义完全正确），用于粘贴\n   * @param html html string\n   * @param isRecursive 是否递归调用（内部使用，使用者不要传参）\n   */\n  e.dangerouslyInsertHtml = (html: string = '', isRecursive = false) => {\n    if (!html) return\n\n    // ------------- 把 html 转换为 DOM nodes -------------\n    const div = document.createElement('div')\n    div.innerHTML = html\n    let domNodes = Array.from(div.childNodes)\n\n    // 过滤一下，只保留 elem 和 text ，并却掉一些无用标签（如 style script 等）\n    domNodes = domNodes.filter(n => {\n      const { nodeType, nodeName } = n\n      // Text Node\n      if (nodeType === NodeType.TEXT_NODE) return true\n\n      // Element Node\n      if (nodeType === NodeType.ELEMENT_NODE) {\n        // 过滤掉忽略的 tag\n        if (IGNORE_TAGS.has(nodeName.toLowerCase())) return false\n        else return true\n      }\n      return false\n    })\n    if (domNodes.length === 0) return\n\n    // ------------- 把 DOM nodes 转换为 slate nodes ，并插入到编辑器 -------------\n\n    const { selection } = e\n    if (selection == null) return\n    let curEmptyParagraphPath: Path | null = null\n\n    // 是否当前选中了一个空 p （如果是，后面会删掉）\n    // 递归调用时不判断\n    if (DomEditor.isSelectedEmptyParagraph(e) && !isRecursive) {\n      const { focus } = selection\n      curEmptyParagraphPath = [focus.path[0]] // 只记录顶级 path 即可\n    }\n\n    div.setAttribute('hidden', 'true')\n    document.body.appendChild(div)\n\n    let insertedElemNum = 0 // 记录插入 elem 的数量 ( textNode 不算 )\n    domNodes.forEach(n => {\n      const { nodeType, nodeName, textContent = '' } = n\n\n      // ------ Text node ------\n      if (nodeType === NodeType.TEXT_NODE) {\n        if (!textContent || !textContent.trim()) return // 无内容的 Text\n\n        // 插入文本\n        //【注意】insertNode 和 insertText 有区别：后者会继承光标处的文本样式（如加粗）；前者会加入纯文本，无样式；\n        e.insertNode({ text: textContent })\n        return\n      }\n\n      // ------ Element Node ------\n      if (nodeName === 'BR') {\n        e.insertText('\\n') // 换行\n        return\n      }\n\n      // 判断当前的 el 是否是可识别的 tag\n      const el = n as DOMElement\n      let isParseMatch = false\n      if (TEXT_TAGS.includes(nodeName.toLowerCase())) {\n        // text elem，如 <span>\n        isParseMatch = true\n      } else {\n        for (let selector in PARSE_ELEM_HTML_CONF) {\n          if (el.matches(selector)) {\n            // 普通 elem，如 <p> <a> 等（非 text elem）\n            isParseMatch = true\n            break\n          }\n        }\n      }\n\n      // 匹配上了，则生成 slate elem 并插入\n      if (isParseMatch) {\n        // 生成并插入\n        const $el = $(el)\n        const parsedRes = parseElemHtml($el, e) as Element\n\n        if (Array.isArray(parsedRes)) {\n          parsedRes.forEach(el => insertElemToEditor(e, el))\n          insertedElemNum++ // 记录数量\n        } else {\n          insertElemToEditor(e, parsedRes)\n          insertedElemNum++ // 记录数量\n        }\n\n        // 如果当前选中 void node ，则选区移动一下\n        if (DomEditor.isSelectedVoidNode(e)) e.move(1)\n\n        return\n      }\n\n      // 没有匹配上（如 div ）\n      const display = window.getComputedStyle(el).display\n      if (!DomEditor.isSelectedEmptyParagraph(e)) {\n        // 当前不是空行，且 非 inline - 则换行\n        if (display.indexOf('inline') < 0) e.insertBreak()\n      }\n      e.dangerouslyInsertHtml(el.innerHTML, true) // 继续插入子内容\n    })\n\n    // 删除第一个空行\n    if (insertedElemNum && curEmptyParagraphPath) {\n      if (DomEditor.isEmptyPath(e, curEmptyParagraphPath)) {\n        Transforms.removeNodes(e, { at: curEmptyParagraphPath })\n      }\n    }\n\n    div.remove() // 粘贴完了，移除 div\n  }\n\n  /**\n   * 重置 HTML 内容\n   * @param html html string\n   */\n  e.setHtml = (html: string = '') => {\n    // 记录编辑器当前状态\n    const isEditorDisabled = e.isDisabled()\n    const isEditorFocused = e.isFocused()\n    const editorSelectionStr = JSON.stringify(e.selection)\n\n    // 删除当前内容\n    e.enable()\n    e.focus()\n    // 需要标准的{anchor:xxx, focus: xxxx} 否则无法通过slate history的检查\n    // 使用 e.select([]) e.selectAll() 生成的location不是标准的{anchor: xxxx, focus: xxx}形式\n    // https://github.com/wangeditor-team/wangEditor/issues/4754\n    e.clear()\n    // 设置新内容\n    const newContent = htmlToContent(e, html)\n    Transforms.insertFragment(e, newContent)\n\n    // 恢复编辑器状态和选区\n    if (!isEditorFocused) {\n      e.deselect()\n      e.blur()\n    }\n    if (isEditorDisabled) {\n      e.deselect()\n      e.disable()\n    }\n    if (e.isFocused()) {\n      try {\n        e.select(JSON.parse(editorSelectionStr)) // 选中原来的位置\n      } catch (ex) {\n        e.select(Editor.start(e, [])) // 选中开始\n      }\n    }\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/core/src/editor/plugins/with-dom.ts",
    "content": "/**\n * @description slate 插件 - dom 相关\n * @author wangfupeng\n */\n\nimport { Node, Editor, Transforms } from 'slate'\nimport { DomEditor } from '../dom-editor'\nimport { IDomEditor } from '../..'\nimport $, { Dom7Array } from '../../utils/dom'\nimport {\n  IS_FOCUSED,\n  EDITOR_TO_PANEL_AND_MODAL,\n  EDITOR_TO_TEXTAREA,\n  TEXTAREA_TO_EDITOR,\n  EDITOR_TO_TOOLBAR,\n  TOOLBAR_TO_EDITOR,\n  EDITOR_TO_HOVER_BAR,\n  HOVER_BAR_TO_EDITOR,\n  EDITOR_TO_SELECTION,\n} from '../../utils/weak-maps'\n\nlet ID = 1\n\n/**\n * `withDOM` adds DOM specific behaviors to the editor.\n */\nexport const withDOM = <T extends Editor>(editor: T) => {\n  const e = editor as T & IDomEditor\n\n  e.id = `wangEditor-${ID++}`\n\n  e.isDestroyed = false\n\n  e.isFullScreen = false\n\n  // focus\n  e.focus = (isEnd?: boolean) => {\n    const el = DomEditor.toDOMNode(e, e)\n    el.focus({ preventScroll: true })\n\n    IS_FOCUSED.set(e, true)\n\n    // 恢复选区\n    if (isEnd) {\n      // 选区定位到结尾\n      const end = Editor.end(e, [])\n      Transforms.select(e, end)\n    } else {\n      const selection = EDITOR_TO_SELECTION.get(e)\n      if (selection) {\n        Transforms.select(e, selection) // 选区定位到之前的位置\n      } else {\n        Transforms.select(e, Editor.start(e, [])) // 选区定位到开始\n      }\n    }\n  }\n\n  // isFocused\n  e.isFocused = () => {\n    return !!IS_FOCUSED.get(e)\n  }\n\n  // blur\n  e.blur = () => {\n    const el = DomEditor.toDOMNode(e, e)\n    el.blur()\n\n    // 手动执行一次光标 deselect, 触发 onchange 回调，改变 Toolbar 的状态\n    Transforms.deselect(e)\n\n    IS_FOCUSED.set(e, false)\n  }\n\n  // 手动更新试图\n  e.updateView = () => {\n    const textarea = DomEditor.getTextarea(e)\n    textarea.changeViewState()\n\n    const toolbar = DomEditor.getToolbar(e)\n    toolbar && toolbar.changeToolbarState()\n\n    const hoverbar = DomEditor.getHoverbar(e)\n    hoverbar && hoverbar.changeHoverbarState()\n  }\n\n  // destroy\n  e.destroy = () => {\n    // 销毁相关实例（会销毁 DOM）\n    if (e.isDestroyed) return\n    // fix https://github.com/wangeditor-team/wangEditor-v5/issues/457\n    const textarea = DomEditor.getTextarea(e)\n    textarea.destroy()\n    EDITOR_TO_TEXTAREA.delete(e)\n    TEXTAREA_TO_EDITOR.delete(textarea)\n\n    const toolbar = DomEditor.getToolbar(e)\n    if (toolbar) {\n      toolbar.destroy()\n      EDITOR_TO_TOOLBAR.delete(e)\n      TOOLBAR_TO_EDITOR.delete(toolbar)\n    }\n\n    const hoverbar = DomEditor.getHoverbar(e)\n    if (hoverbar) {\n      hoverbar.destroy()\n      EDITOR_TO_HOVER_BAR.delete(e)\n      HOVER_BAR_TO_EDITOR.delete(hoverbar)\n    }\n\n    // 修改属性\n    e.isDestroyed = true\n\n    // 触发自定义事件\n    e.emit('destroyed')\n  }\n\n  // scroll to elem\n  e.scrollToElem = (id: string) => {\n    const { scroll } = e.getConfig()\n    if (!scroll) {\n      // 没有设置编辑区域滚动，则不能用\n      let info = '编辑器禁用了 scroll ，编辑器内容无法滚动，请自行实现该功能'\n      info += '\\nYou has disabled editor scroll, please do this yourself'\n      console.warn(info)\n      return\n    }\n\n    const $elem = $(`#${id}`)\n    if ($elem.length === 0) return\n\n    // $elem 不在 editor DOM 范围之内\n    const elem = $elem[0]\n    if (!DomEditor.hasDOMNode(e, elem)) {\n      let info = `Element (found by id is '${id}') is not in editor DOM`\n      info += `\\n 通过 id '${id}' 找到的 element 不在 editor DOM 之内`\n      console.error(info, elem)\n      return\n    }\n\n    const textarea = DomEditor.getTextarea(e)\n    const { $textAreaContainer, $scroll } = textarea\n\n    const { top: elemTop } = $elem.offset()\n    const { top: containerTop } = $textAreaContainer.offset()\n\n    // 滚动到指定元素\n    $scroll[0].scrollBy({ top: elemTop - containerTop, behavior: 'smooth' })\n  }\n\n  // showProgressBar\n  e.showProgressBar = (progress: number) => {\n    // progress 值范围： 0 - 100\n    if (progress < 1) return\n\n    // 显示进度条\n    const textarea = DomEditor.getTextarea(e)\n    textarea.changeProgress(progress)\n  }\n\n  // 隐藏 panel 或 modal\n  e.hidePanelOrModal = () => {\n    const set = EDITOR_TO_PANEL_AND_MODAL.get(e)\n    if (set == null) return\n    set.forEach(panelOrModal => panelOrModal.hide())\n  }\n\n  e.enable = () => {\n    const config = e.getConfig()\n    config.readOnly = false\n\n    // 更新视图\n    e.updateView()\n  }\n\n  e.disable = () => {\n    const config = e.getConfig()\n    config.readOnly = true\n\n    // 更新视图\n    e.updateView()\n  }\n\n  e.isDisabled = () => {\n    const config = e.getConfig()\n    return config.readOnly\n  }\n\n  e.toDOMNode = (node: Node) => {\n    return DomEditor.toDOMNode(e, node)\n  }\n\n  e.fullScreen = () => {\n    if (e.isFullScreen) return\n\n    let $toolbarBox: Dom7Array | null = null\n    const toolbar = DomEditor.getToolbar(e)\n    if (toolbar) {\n      $toolbarBox = toolbar.$box\n    }\n\n    const textarea = DomEditor.getTextarea(e)\n    const $textAreaBox = textarea.$box\n    const $parent = $textAreaBox.parent()\n\n    if ($toolbarBox && $toolbarBox.parent()[0] !== $parent[0]) {\n      // toolbar DOM 父节点，和 editor DOM 父节点不一致，则不能设置全屏\n      let info =\n        'Can not set full screen, cause toolbar DOM parent is not equal to textarea DOM parent'\n      info += '\\n不能设置全屏，因为 toolbar DOM 父节点和 textarea DOM 父节点不一致'\n      throw new Error(info)\n    }\n\n    // 设置全屏\n    $parent.addClass('w-e-full-screen-container')\n\n    // 设置 z-index\n    const curZIndex = $parent.css('z-index')\n    $parent.attr('data-z-index', curZIndex.toString())\n\n    // 记录属性\n    e.isFullScreen = true\n\n    // 触发自定义事件\n    e.emit('fullScreen')\n  }\n\n  e.unFullScreen = () => {\n    if (!e.isFullScreen) return\n\n    const textarea = DomEditor.getTextarea(e)\n    const $textAreaBox = textarea.$box\n    const $parent = $textAreaBox.parent()\n\n    // 解决#issue175, 编辑器取消全屏 - element dialog组件会被隐藏\n    setTimeout(() => {\n      // 取消全屏\n      $parent.removeClass('w-e-full-screen-container')\n\n      // 记录属性\n      e.isFullScreen = false\n\n      // 触发自定义事件\n      e.emit('unFullScreen')\n    }, 200)\n  }\n\n  /**\n   * 获取编辑区域 DOM 容器\n   */\n  e.getEditableContainer = () => {\n    const textarea = DomEditor.getTextarea(e)\n    return textarea.$textAreaContainer[0]\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/core/src/editor/plugins/with-emitter.ts",
    "content": "/**\n * @description 自定义事件 插件\n * @author wangfupeng\n */\n\nimport ee, { Emitter } from 'event-emitter'\nimport { Editor } from 'slate'\nimport { IDomEditor } from '../interface'\nimport { EDITOR_TO_EMITTER } from '../../utils/weak-maps'\n\n/**\n * 获取 editor 的 emitter 实例\n * @param editor editor\n */\nfunction getEmitter(editor: IDomEditor): Emitter {\n  let emitter = EDITOR_TO_EMITTER.get(editor)\n  if (emitter == null) {\n    emitter = ee()\n    EDITOR_TO_EMITTER.set(editor, emitter)\n  }\n  return emitter\n}\n\n// 记录下当前 editor 的 destroy listeners\nconst EDITOR_TO_DESTROY_LISTENERS: WeakMap<IDomEditor, Set<Function>> = new WeakMap()\nfunction recordDestroyListeners(editor: IDomEditor, fn: Function) {\n  let listeners = EDITOR_TO_DESTROY_LISTENERS.get(editor)\n  if (listeners == null) {\n    listeners = new Set<Function>()\n    EDITOR_TO_DESTROY_LISTENERS.set(editor, listeners)\n  }\n  listeners.add(fn)\n}\nfunction getDestroyListeners(editor: IDomEditor): Set<Function> {\n  return EDITOR_TO_DESTROY_LISTENERS.get(editor) || new Set()\n}\nfunction clearDestroyListeners(editor: IDomEditor) {\n  EDITOR_TO_DESTROY_LISTENERS.set(editor, new Set())\n}\n\nexport const withEmitter = <T extends Editor>(editor: T) => {\n  const e = editor as T & IDomEditor\n\n  // 自定义事件\n  e.on = (type, listener) => {\n    const emitter = getEmitter(e)\n\n    // 绑定事件\n    emitter.on(type, listener)\n\n    // destroyed 事件需要记录下来，以便最后统一 off 掉\n    if (type === 'destroyed') recordDestroyListeners(e, listener)\n\n    // editor 销毁时，取消绑定 - 重要\n    if (type !== 'destroyed') {\n      const fn = () => emitter.off(type, listener)\n      emitter.on('destroyed', fn)\n      recordDestroyListeners(e, fn) // 记录下来\n    }\n  }\n  e.once = (type, listener) => {\n    const emitter = getEmitter(e)\n    emitter.once(type, listener)\n  }\n  e.off = (type, listener) => {\n    const emitter = getEmitter(e)\n    emitter.off(type, listener)\n  }\n  e.emit = (type, ...args: any[]) => {\n    const emitter = getEmitter(e)\n    emitter.emit(type, ...args)\n\n    // editor 销毁时，off 掉 destroyed listeners\n    if (type === 'destroyed') {\n      const listeners = getDestroyListeners(e)\n      listeners.forEach(fn => emitter.off('destroyed', fn as ee.EventListener))\n      clearDestroyListeners(e) // 清空 destroyed listeners\n    }\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/core/src/editor/plugins/with-event-data.ts",
    "content": "/**\n * @description slate 插件 - event data 相关\n * @author wangfupeng\n */\n\nimport { Editor, Node, Transforms, Range } from 'slate'\nimport { DomEditor } from '../dom-editor'\nimport { IDomEditor } from '../..'\n\nimport { isDOMText, getPlainText } from '../../utils/dom'\n\nexport const withEventData = <T extends Editor>(editor: T) => {\n  const e = editor as T & IDomEditor\n  const { insertText, insertFragment } = e\n\n  e.setFragmentData = (data: Pick<DataTransfer, 'getData' | 'setData'>) => {\n    const { selection } = e\n\n    if (!selection) {\n      return\n    }\n\n    // 获取开始、结束两个 point { path, offset }\n    const [start, end] = Range.edges(selection)\n    // Editor.void - Match a void node in the current branch of the editor.\n    const startVoid = Editor.void(e, { at: start.path })\n    const endVoid = Editor.void(e, { at: end.path })\n\n    if (Range.isCollapsed(selection) && !startVoid) {\n      return\n    }\n\n    // Create a fake selection so that we can add a Base64-encoded copy of the\n    // fragment to the HTML, to decode on future pastes.\n    const domRange = DomEditor.toDOMRange(e, selection)\n    let contents = domRange.cloneContents()\n    let attach = contents.childNodes[0] as HTMLElement\n\n    // Make sure attach is non-empty, since empty nodes will not get copied.\n    contents.childNodes.forEach(node => {\n      if (node.textContent && node.textContent.trim() !== '') {\n        attach = node as HTMLElement\n      }\n    })\n\n    // COMPAT: If the end node is a void node, we need to move the end of the\n    // range from the void node's spacer span, to the end of the void node's\n    // content, since the spacer is before void's content in the DOM.\n    if (endVoid) {\n      const [voidNode] = endVoid\n      const r = domRange.cloneRange()\n      const domNode = DomEditor.toDOMNode(e, voidNode)\n      r.setEndAfter(domNode)\n      contents = r.cloneContents()\n    }\n\n    // COMPAT: If the start node is a void node, we need to attach the encoded\n    // fragment to the void node's content node instead of the spacer, because\n    // attaching it to empty `<div>/<span>` nodes will end up having it erased by\n    // most browsers. (2018/04/27)\n    if (startVoid) {\n      attach = contents.querySelector('[data-slate-spacer]')! as HTMLElement\n    }\n\n    // Remove any zero-width space spans from the cloned DOM so that they don't\n    // show up elsewhere when pasted.\n    Array.from(contents.querySelectorAll('[data-slate-zero-width]')).forEach(zw => {\n      const isNewline = zw.getAttribute('data-slate-zero-width') === 'n'\n      zw.textContent = isNewline ? '\\n' : ''\n    })\n\n    // Set a `data-slate-fragment` attribute on a non-empty node, so it shows up\n    // in the HTML, and can be used for intra-Slate pasting. If it's a text\n    // node, wrap it in a `<span>` so we have something to set an attribute on.\n    if (isDOMText(attach)) {\n      const span = attach.ownerDocument.createElement('span')\n      // COMPAT: In Chrome and Safari, if we don't add the `white-space` style\n      // then leading and trailing spaces will be ignored. (2017/09/21)\n      span.style.whiteSpace = 'pre'\n      span.appendChild(attach)\n      contents.appendChild(span)\n      attach = span\n    }\n\n    const fragment = e.getFragment()\n    const string = JSON.stringify(fragment)\n    const encoded = window.btoa(encodeURIComponent(string))\n    attach.setAttribute('data-slate-fragment', encoded)\n    data.setData('application/x-slate-fragment', encoded)\n\n    // Add the content to a <div> so that we can get its inner HTML.\n    const div = contents.ownerDocument.createElement('div')\n    div.appendChild(contents)\n    div.setAttribute('hidden', 'true')\n    contents.ownerDocument.body.appendChild(div)\n    data.setData('text/html', div.innerHTML)\n    data.setData('text/plain', getPlainText(div))\n    contents.ownerDocument.body.removeChild(div)\n\n    return data\n  }\n\n  e.insertData = (data: DataTransfer) => {\n    const fragment = data.getData('application/x-slate-fragment')\n    if (fragment) {\n      const decoded = decodeURIComponent(window.atob(fragment))\n      const parsed = JSON.parse(decoded) as Node[]\n      e.insertFragment(parsed)\n      return\n    }\n\n    const text = data.getData('text/plain')\n    const html = data.getData('text/html')\n    // const rtf = data.getData('text/rtf')\n\n    if (html) {\n      e.dangerouslyInsertHtml(html)\n      return\n    }\n\n    if (text) {\n      const lines = text.split(/\\r\\n|\\r|\\n/)\n      let split = false\n\n      for (const line of lines) {\n        if (split) {\n          Transforms.splitNodes(e, { always: true })\n        }\n\n        insertText(line)\n        split = true\n      }\n      return\n    }\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/core/src/editor/plugins/with-max-length.ts",
    "content": "/**\n * @description slate 插件 - maxLength\n * @author wangfupeng\n */\n\n//【注意】拼音输入时 maxLength 限制在 CompositionEnd 事件中处理\n\nimport { Editor, Node } from 'slate'\nimport { IDomEditor, DomEditor } from '../..'\nimport { IGNORE_TAGS } from '../../constants'\nimport { NodeType } from '../../utils/dom'\n\nexport const withMaxLength = <T extends Editor>(editor: T) => {\n  const e = editor as T & IDomEditor\n  const { insertText, insertNode, insertFragment, dangerouslyInsertHtml } = e\n\n  // 处理 text\n  e.insertText = (text: string) => {\n    const { maxLength } = e.getConfig()\n    if (!maxLength) {\n      insertText(text)\n      return\n    }\n\n    const leftLength = DomEditor.getLeftLengthOfMaxLength(e)\n    if (leftLength <= 0) {\n      // 已经触发 maxLength ，不再输入文字\n      return\n    }\n\n    if (leftLength < text.length) {\n      // 剩余长度小于 text 长度，则截取 text\n      insertText(text.slice(0, leftLength))\n      return\n    }\n\n    insertText(text)\n  }\n\n  // 处理 node\n  e.insertNode = (node: Node) => {\n    const { maxLength } = e.getConfig()\n    if (!maxLength) {\n      insertNode(node)\n      return\n    }\n\n    const leftLength = DomEditor.getLeftLengthOfMaxLength(e)\n    if (leftLength <= 0) {\n      // 已经触发 maxLength ，不再插入\n      return\n    }\n\n    const text = Node.string(node)\n    if (leftLength < text.length) {\n      // 剩余长度，不够 node text 长度，不再插入\n      return\n    }\n\n    insertNode(node)\n  }\n\n  // 处理 fragment\n  e.insertFragment = (fragment: Node[]) => {\n    const { maxLength } = e.getConfig()\n    if (!maxLength) {\n      // 无 maxLength\n      insertFragment(fragment)\n      return\n    }\n\n    // 只有一个 node 时，使用 insertFragment ，防止换行\n    if (fragment.length === 1) {\n      const node = fragment[0]\n      const leftLength = DomEditor.getLeftLengthOfMaxLength(e)\n      const text = Node.string(node)\n\n      if (leftLength < text.length) {\n        // 已经触发 maxLength ，不再插入\n        return\n      }\n\n      insertFragment(fragment)\n      return\n    }\n    // 有 maxLength ，则分别插入 node\n    fragment.forEach(n => {\n      e.insertNode(n) //【注意】这里必须使用 `e.insertNode` ，而不是 insertNode\n    })\n  }\n\n  e.dangerouslyInsertHtml = (html: string = '', isRecursive = false) => {\n    if (!html) return\n\n    const { maxLength } = e.getConfig()\n    if (!maxLength) {\n      // 无 maxLength\n      dangerouslyInsertHtml(html, isRecursive)\n      return\n    }\n    const leftLength = DomEditor.getLeftLengthOfMaxLength(e)\n    if (leftLength <= 0) {\n      // 已经触发 maxLength ，不再输入文字\n      return\n    }\n\n    // ------------- 把 html 转换为 DOM nodes -------------\n    const div = document.createElement('div')\n    div.innerHTML = html\n    const text = Array.from(div.childNodes).reduce<string>((acc, node) => {\n      const { nodeType, nodeName } = node\n      if (!node) {\n        return acc\n      }\n      // Text Node\n      if (nodeType === NodeType.TEXT_NODE) return acc + (node.textContent || '')\n\n      // Element Node\n      if (nodeType === NodeType.ELEMENT_NODE) {\n        // 过滤掉忽略的 tag\n        if (IGNORE_TAGS.has(nodeName.toLowerCase())) return acc\n        else return acc + (node.textContent || '')\n      }\n      return acc\n    }, '')\n\n    if (leftLength < text.length) {\n      return\n    }\n\n    dangerouslyInsertHtml(html, isRecursive)\n  }\n\n  return e // 返回 editor 实例\n}\n"
  },
  {
    "path": "packages/core/src/editor/plugins/with-selection.ts",
    "content": "/**\n * @description slate 插件 - selection 相关\n * @author wangfupeng\n */\n\nimport { Editor, Transforms, Location, Node, Range, Point } from 'slate'\nimport { IDomEditor } from '../interface'\nimport { DomEditor } from '../dom-editor'\nimport { getPositionByNode, getPositionBySelection } from '../../menus/helpers/position'\nimport { EDITOR_TO_SELECTION } from '../../utils/weak-maps'\n\nexport const withSelection = <T extends Editor>(editor: T) => {\n  const e = editor as T & IDomEditor\n\n  // 选中\n  e.select = (at: Location) => {\n    Transforms.select(e, at)\n  }\n\n  // 取消选中\n  e.deselect = () => {\n    const { selection } = e\n    const root = DomEditor.findDocumentOrShadowRoot(e)\n    const domSelection = root.getSelection()\n\n    if (domSelection && domSelection.rangeCount > 0) {\n      domSelection.removeAllRanges()\n    }\n\n    if (selection) {\n      Transforms.deselect(editor)\n    }\n  }\n\n  // 移动光标\n  e.move = (distance: number, reverse = false) => {\n    if (!distance) return\n    if (distance < 0) return\n\n    Transforms.move(editor, {\n      distance,\n      unit: 'character',\n      reverse,\n    })\n  }\n\n  // 反向移动光标\n  e.moveReverse = (distance: number) => {\n    e.move(distance, true)\n  }\n\n  /**\n   * 还原选区\n   */\n  e.restoreSelection = () => {\n    const selection = EDITOR_TO_SELECTION.get(e)\n    if (selection == null) return\n\n    e.focus()\n    Transforms.select(e, selection)\n  }\n\n  /**\n   * 获取选区的 position\n   */\n  e.getSelectionPosition = () => {\n    return getPositionBySelection(e)\n  }\n\n  /**\n   * 获取 node 的 position\n   */\n  e.getNodePosition = (node: Node) => {\n    return getPositionByNode(e, node)\n  }\n\n  /**\n   * 是否全选\n   */\n  e.isSelectedAll = () => {\n    const { selection } = e\n    if (selection == null) return false\n\n    const [start1, end1] = Range.edges(selection) // 获取当前选取的开始、结束 point\n    const [start2, end2] = Editor.edges(e, []) // 获取编辑器全部的开始、结束 point\n\n    if (Point.equals(start1, start2) && Point.equals(end1, end2)) {\n      return true\n    }\n    return false\n  }\n\n  /**\n   * 全选\n   */\n  e.selectAll = () => {\n    const start = Editor.start(e, [])\n    const end = Editor.end(e, [])\n\n    Transforms.select(e, {\n      anchor: start,\n      focus: end,\n    })\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/core/src/i18n/index.ts",
    "content": "/**\n * @description i18n entry\n * @author wangfupeng\n */\n\nimport i18next from 'i18next'\n\n// i18n nameSpace\nconst NS = 'translation'\n\ni18next.init({\n  lng: 'zh-CN',\n  // debug: true,\n  resources: {}, // 资源为空，随后添加\n})\n\n/**\n * 添加多语言配置\n * @param lng 语言\n * @param resources 多语言配置\n */\nexport function i18nAddResources(lng: string, resources: object) {\n  i18next.addResourceBundle(lng, NS, resources, true, true)\n}\n\n/**\n * 设置语言\n * @param lng 语言\n */\nexport function i18nChangeLanguage(lng: string) {\n  i18next.changeLanguage(lng)\n}\n\n/**\n * 获取多语言配置\n * @param lng lang\n */\nexport function i18nGetResources(lng: string) {\n  return i18next.getResourceBundle(lng, NS)\n}\n\n/**\n * 翻译\n */\nexport const t = i18next.t.bind(i18next)\n\nexport default i18next\n"
  },
  {
    "path": "packages/core/src/index.ts",
    "content": "/**\n * @description core index\n * @author wangfupeng\n */\n\nimport './assets/index.less'\n\nimport { RenderStyleFnType, IRenderElemConf } from './render/index'\nimport { styleToHtmlFnType, IElemToHtmlConf } from './to-html/index'\nimport { IPreParseHtmlConf, ParseStyleHtmlFnType, IParseElemHtmlConf } from './parse-html/index'\nimport { IRegisterMenuConf } from './menus/index'\nimport { IDomEditor } from './editor/interface'\n\n// 创建\nexport * from './create/index'\n\n// config\nexport { IEditorConfig, IToolbarConfig } from './config/interface'\n\n// editor 接口和 command\nexport * from './editor/interface'\nexport * from './editor/dom-editor'\n\n// 注册 render\nexport * from './render/index'\n\n// 注册 toHtml\nexport * from './to-html/index'\n\n// 注册 parseHtml\nexport * from './parse-html/index'\n\n// menu 的接口、注册、方法等\nexport * from './menus/index'\n\n// upload\nexport * from './upload/index'\n\n// i18n\nexport * from './i18n/index'\n\nexport interface IModuleConf {\n  // 注册菜单\n  menus: Array<IRegisterMenuConf>\n\n  // 渲染 modal -> view\n  renderStyle: RenderStyleFnType\n  renderElems: Array<IRenderElemConf>\n\n  // to html\n  styleToHtml: styleToHtmlFnType\n  elemsToHtml: Array<IElemToHtmlConf>\n\n  // parse html\n  preParseHtml: Array<IPreParseHtmlConf>\n  parseStyleHtml: ParseStyleHtmlFnType\n  parseElemsHtml: Array<IParseElemHtmlConf>\n\n  // 注册插件\n  editorPlugin: <T extends IDomEditor>(editor: T) => T\n}\n"
  },
  {
    "path": "packages/core/src/menus/README.md",
    "content": "# menus\n\n统一注册 menu ，menu 支持\n- classic toolbar\n- hovering toolbar\n- tooltip\n- contextMenu\n"
  },
  {
    "path": "packages/core/src/menus/bar/HoverBar.ts",
    "content": "/**\n * @description hover bar class\n * @author wangfupeng\n */\n\nimport debounce from 'lodash.debounce'\nimport { Editor, Node, Element, Text, Path, Range } from 'slate'\nimport $ from '../../utils/dom'\nimport { MENU_ITEM_FACTORIES } from '../register'\nimport { promiseResolveThen } from '../../utils/util'\nimport { IDomEditor } from '../../editor/interface'\nimport { DomEditor } from '../../editor/dom-editor'\nimport { HOVER_BAR_TO_EDITOR, BAR_ITEM_TO_EDITOR } from '../../utils/weak-maps'\nimport { IBarItem, createBarItem } from '../bar-item/index'\nimport { gen$barItemDivider } from '../helpers/helpers'\nimport { getPositionBySelection, getPositionByNode, correctPosition } from '../helpers/position'\nimport { IButtonMenu, ISelectMenu, IDropPanelMenu, IModalMenu } from '../interface'\nimport { CustomElement } from '../../../../custom-types'\n\ntype MenuType = IButtonMenu | ISelectMenu | IDropPanelMenu | IModalMenu\n\n/**\n * 是否选中了 text （用于 text hoverbarKeys）\n * @param editor editor\n * @param n node\n */\nfunction isSelectedText(editor: IDomEditor, n: Node) {\n  const { selection } = editor\n  if (selection == null) return false // 无选区\n  if (Range.isCollapsed(selection)) return false // 未选中文字，选区的是折叠的\n\n  const selectedElems = DomEditor.getSelectedElems(editor)\n  const notMatch = selectedElems.some((elem: CustomElement) => {\n    if (editor.isVoid(elem)) return true\n\n    const { type } = elem\n    if (['pre', 'code', 'table'].includes(type)) return true\n  })\n  if (notMatch) return false\n\n  if (Text.isText(n)) return true // 匹配 text node\n  return false\n}\n\nclass HoverBar {\n  private readonly $elem = $('<div class=\"w-e-bar w-e-bar-hidden w-e-hover-bar\"></div>')\n  private menus: { [key: string]: MenuType } = {}\n  private hoverbarItems: IBarItem[] = []\n  private prevSelectedNode: Node | null = null // 上一次选中的 node\n  private isShow = false\n\n  constructor() {\n    // 异步，否则获取不到 DOM 和 editor\n    promiseResolveThen(() => {\n      const editor = this.getEditorInstance()\n\n      // 将 elem 渲染为 DOM\n      const $elem = this.$elem\n      // @ts-ignore\n      $elem.on('mousedown', e => e.preventDefault(), { passive: false }) // 防止点击失焦\n      const textarea = DomEditor.getTextarea(editor)\n      textarea.$textAreaContainer.append($elem)\n\n      // 绑定 editor onchange\n      editor.on('change', this.changeHoverbarState)\n\n      // 滚动时隐藏\n      const hideAndClean = this.hideAndClean.bind(this)\n      editor.on('scroll', hideAndClean)\n\n      // fullScreen 时隐藏\n      editor.on('fullScreen', hideAndClean)\n      editor.on('unFullScreen', hideAndClean)\n    })\n  }\n\n  getMenus() {\n    return this.menus\n  }\n\n  hideAndClean() {\n    const $elem = this.$elem\n    $elem.removeClass('w-e-bar-show').addClass('w-e-bar-hidden')\n\n    // 及时先清空内容，否则影响下次\n    this.hoverbarItems = []\n    $elem.empty()\n\n    this.isShow = false\n  }\n\n  /**\n   * 判断 hoverbar 是否在网页下部？\n   * 如果是，SelectList 和 DropPanel 要显示在 hoverbar 上面\n   */\n  private checkPositionBottom() {\n    const $elem = this.$elem\n\n    let isBottom = false\n    const { innerHeight } = window\n    const minDistance = 360 // 距离底部最小 360px\n    if (innerHeight && innerHeight >= minDistance) {\n      const { bottom } = $elem[0].getBoundingClientRect()\n      if (innerHeight - bottom < minDistance) {\n        // hoverbar 距离底部不足 360\n        isBottom = true\n      }\n    }\n    if (isBottom) {\n      $elem.addClass('w-e-bar-bottom')\n    } else {\n      $elem.removeClass('w-e-bar-bottom')\n    }\n  }\n\n  private show() {\n    this.$elem.removeClass('w-e-bar-hidden').addClass('w-e-bar-show')\n    this.isShow = true\n\n    // 判断 hoverbar 是否在网页下部\n    this.checkPositionBottom()\n  }\n\n  private changeItemsState() {\n    promiseResolveThen(() => {\n      this.hoverbarItems.forEach(item => {\n        item.changeMenuState()\n      })\n    })\n  }\n\n  private registerItems(menuKeys: string[]) {\n    const $elem = this.$elem\n\n    menuKeys.forEach(key => {\n      if (key === '|') {\n        // 分割线\n        const $divider = gen$barItemDivider()\n        $elem.append($divider)\n        return\n      }\n\n      // 正常菜单\n      this.registerSingleItem(key)\n    })\n  }\n\n  // 注册单个 bar item\n  private registerSingleItem(key: string) {\n    const editor = this.getEditorInstance()\n\n    // 尝试从缓存中获取\n    const { menus } = this\n    let menu = menus[key]\n\n    if (menu == null) {\n      // 缓存获取失败，则重新创建\n      const factory = MENU_ITEM_FACTORIES[key]\n      if (factory == null) {\n        throw new Error(`Not found menu item factory by key '${key}'`)\n      }\n      if (typeof factory !== 'function') {\n        throw new Error(`Menu item factory (key='${key}') is not a function`)\n      }\n\n      // 创建 barItem 并记录缓存\n      menu = factory()\n      menus[key] = menu\n    }\n\n    const barItem = createBarItem(key, menu)\n    this.hoverbarItems.push(barItem)\n\n    // 保存 barItem 和 editor 的关系\n    BAR_ITEM_TO_EDITOR.set(barItem, editor)\n\n    // 添加 DOM\n    const $elem = this.$elem\n    $elem.append(barItem.$elem)\n  }\n\n  private setPosition(node: Node) {\n    const editor = this.getEditorInstance()\n    const $elem = this.$elem\n    $elem.attr('style', '') // 先清空 style ，再重新设置\n\n    if (Element.isElement(node)) {\n      // 根据 elem node 定位\n      const positionStyle = getPositionByNode(editor, node, 'bar')\n      $elem.css(positionStyle)\n      correctPosition(editor, $elem) // 修正 position 避免超出 textContainer 边界\n      return\n    }\n    if (Text.isText(node)) {\n      // text node ，根据选区定位\n      const positionStyle = getPositionBySelection(editor)\n      $elem.css(positionStyle)\n      correctPosition(editor, $elem) // 修正 position 避免超出 textContainer 边界\n      return\n    }\n    // 其他情况，非 elem 非 text ，不处理\n    throw new Error('hoverbar.setPosition error, current selected node is not elem nor text')\n  }\n\n  /**\n   * 获取选中的 node ，以及对应的 menu keys\n   */\n  private getSelectedNodeAndMenuKeys(): { node: Node; menuKeys: string[] } | null {\n    const editor = this.getEditorInstance()\n\n    if (editor.selection == null) {\n      return null\n    }\n\n    // 获取 hover bar 配置\n    const keysConf = this.getHoverbarKeysConf()\n    // 开始匹配\n    let matchNode: Node | null = null\n    let matchMenuKeys: string[] = []\n\n    for (const elemType in keysConf) {\n      const conf = keysConf[elemType]\n      const { match, menuKeys = [] } = conf\n\n      // 定义了 match 则用 match 。未定义 match 则用 elemType\n      const matchFn = match\n        ? match\n        : (editor: IDomEditor, n: Node) => DomEditor.checkNodeType(n, elemType)\n\n      const [nodeEntry] = Editor.nodes(editor, {\n        match: n => matchFn(editor, n),\n        universal: true,\n      })\n\n      // 匹配成功（找到第一个就停止，不再继续找了）\n      if (nodeEntry != null) {\n        matchNode = nodeEntry[0]\n        matchMenuKeys = menuKeys\n        break\n      }\n    }\n\n    // 未匹配成功\n    if (matchNode == null || matchMenuKeys.length === 0) return null\n\n    // 匹配成功\n    return {\n      node: matchNode,\n      menuKeys: matchMenuKeys,\n    }\n  }\n\n  /**\n   * editor onChange 时触发（涉及 DOM 操作，加防抖）\n   */\n  changeHoverbarState = debounce(() => {\n    // 获取选中的 node ，以及对应的 menu keys\n    const { isShow } = this\n    const { node = null, menuKeys = [] } = this.getSelectedNodeAndMenuKeys() || {}\n\n    if (node != null) {\n      this.changeItemsState() // 更新菜单状态\n    }\n\n    if (node && Element.isElement(node)) {\n      // 选中了 elem node（不可以是 text node）\n      if (isShow) {\n        // hoverbar 当前已显示\n        const samePath = this.isSamePath(node, this.prevSelectedNode)\n        if (samePath) {\n          // 和之前选中的 node path 相同 —— 满足这些条件，即终止\n          return\n        }\n      }\n    }\n\n    // 选择了新的 node（或选区是 null），先隐藏\n    this.hideAndClean()\n\n    if (node != null) {\n      // 选中了新的 node\n      this.registerItems(menuKeys)\n      this.setPosition(node)\n      this.show()\n    }\n\n    // 最后，重新记录 prevSelectedNode ，重要\n    this.prevSelectedNode = node\n  }, 200)\n\n  private getEditorInstance(): IDomEditor {\n    const editor = HOVER_BAR_TO_EDITOR.get(this)\n    if (editor == null) throw new Error('Can not get editor instance')\n    return editor\n  }\n\n  private getHoverbarKeysConf() {\n    const editor = this.getEditorInstance()\n    const { hoverbarKeys = {} } = editor.getConfig()\n\n    const textHoverbarKeys = hoverbarKeys.text\n    if (textHoverbarKeys && textHoverbarKeys.match == null) {\n      // 对 text hoverbarKeys 增加 match 函数（否则无法判断是否选中了 text）\n      textHoverbarKeys.match = isSelectedText\n    }\n\n    return hoverbarKeys\n  }\n\n  /**\n   * 检查两个 node 是否 path 相等\n   */\n  private isSamePath(node1: Node | null, node2: Node | null) {\n    if (node1 == null || node2 == null) {\n      return false\n    }\n\n    const path1 = DomEditor.findPath(null, node1)\n    const path2 = DomEditor.findPath(null, node2)\n    const res = Path.equals(path1, path2)\n    return res\n  }\n\n  /**\n   * 销毁 hoverbar\n   */\n  destroy() {\n    // fix https://github.com/wangeditor-team/wangEditor-v5/issues/410\n    this.changeHoverbarState.cancel()\n    // 销毁 DOM\n    this.$elem.remove()\n\n    // 清空属性\n    this.menus = {}\n    this.hoverbarItems = []\n    this.prevSelectedNode = null\n  }\n}\n\nexport default HoverBar\n"
  },
  {
    "path": "packages/core/src/menus/bar/Toolbar.ts",
    "content": "/**\n * @description classic toolbar\n * @author wangfupeng\n */\n\nimport debounce from 'lodash.debounce'\nimport clonedeep from 'lodash.clonedeep'\nimport $, { Dom7Array, DOMElement } from '../../utils/dom'\nimport { MENU_ITEM_FACTORIES } from '../register'\nimport { promiseResolveThen } from '../../utils/util'\nimport { TOOLBAR_TO_EDITOR, BAR_ITEM_TO_EDITOR } from '../../utils/weak-maps'\nimport { IDomEditor } from '../../editor/interface'\nimport { IBarItem, createBarItem, createBarItemGroup } from '../bar-item/index'\nimport { gen$barItemDivider } from '../helpers/helpers'\nimport { IMenuGroup, IButtonMenu, ISelectMenu, IDropPanelMenu, IModalMenu } from '../interface'\nimport GroupButton from '../bar-item/GroupButton'\nimport { IToolbarConfig } from '../../config/interface'\n\ntype MenuType = IButtonMenu | ISelectMenu | IDropPanelMenu | IModalMenu\n\nclass Toolbar {\n  $box: Dom7Array\n  private readonly $toolbar: Dom7Array = $(`<div class=\"w-e-bar w-e-bar-show w-e-toolbar\"></div>`)\n  private menus: { [key: string]: MenuType } = {}\n  private toolbarItems: IBarItem[] = []\n  private config: Partial<IToolbarConfig> = {}\n\n  constructor(boxSelector: string | DOMElement, config: Partial<IToolbarConfig>) {\n    this.config = config\n\n    // @ts-ignore 初始化 DOM\n    const $box = $(boxSelector)\n    if ($box.length === 0) {\n      throw new Error(`Cannot find toolbar DOM by selector '${boxSelector}'`)\n    }\n    this.$box = $box\n    const $toolbar = this.$toolbar\n    // @ts-ignore\n    $toolbar.on('mousedown', e => e.preventDefault(), { passive: false }) // 防止点击失焦\n    $box.append($toolbar)\n\n    // 异步，否则拿不到 editor 实例\n    promiseResolveThen(() => {\n      // 注册 items\n      this.registerItems()\n\n      // 创建完，先模拟一次 onchange\n      this.changeToolbarState()\n\n      // 监听 editor onchange\n      const editor = this.getEditorInstance()\n      editor.on('change', this.changeToolbarState)\n    })\n  }\n\n  getMenus() {\n    return this.menus\n  }\n\n  getConfig() {\n    return this.config\n  }\n\n  // 注册 toolbarItems\n  private registerItems() {\n    let prevKey = ''\n    const $toolbar = this.$toolbar\n    const { toolbarKeys = [], insertKeys = { index: 0, keys: [] }, excludeKeys = [] } = this.config // 格式如 ['a', '|', 'b', 'c', '|', 'd']\n\n    // 新插入菜单\n    const toolbarKeysWithInsertedKeys = clonedeep(toolbarKeys)\n    if (insertKeys.keys.length > 0) {\n      if (typeof insertKeys.keys === 'string') {\n        insertKeys.keys = [insertKeys.keys]\n      }\n\n      insertKeys.keys.forEach((k, i) => {\n        toolbarKeysWithInsertedKeys.splice(insertKeys.index + i, 0, k)\n      })\n    }\n\n    // 排除某些菜单\n    const filteredKeys = toolbarKeysWithInsertedKeys.filter(key => {\n      if (typeof key === 'string') {\n        // 普通菜单\n        if (excludeKeys.includes(key)) return false\n      } else {\n        // group\n        if (excludeKeys.includes(key.key)) return false\n      }\n      return true\n    })\n    const filteredKeysLength = filteredKeys.length\n\n    // 开始注册菜单\n    filteredKeys.forEach((key, index) => {\n      if (key === '|') {\n        // 第一个就是 `|` ，忽略\n        if (index === 0) return\n\n        // 最后一个是 `|` ，忽略\n        if (index + 1 === filteredKeysLength) return\n\n        // 多个紧挨着的 `|` ，只显示一个\n        if (prevKey === '|') return\n\n        // 分割线\n        const $divider = gen$barItemDivider()\n        $toolbar.append($divider)\n        prevKey = key\n        return\n      }\n\n      // 正常菜单\n      if (typeof key === 'string') {\n        this.registerSingleItem(key, this)\n        prevKey = key\n        return\n      }\n\n      // 菜单组\n      this.registerGroup(key)\n      prevKey = 'group'\n    })\n  }\n\n  // 注册菜单组\n  private registerGroup(menu: IMenuGroup) {\n    const $toolbar = this.$toolbar\n    const group = createBarItemGroup(menu)\n    const { menuKeys = [] } = menu\n    const { excludeKeys = [] } = this.config\n\n    // 注册子菜单\n    menuKeys.forEach(key => {\n      if (excludeKeys.includes(key)) return\n      this.registerSingleItem(\n        key,\n        group // 将子菜单，添加到 group\n      )\n    })\n\n    // 添加到 DOM\n    $toolbar.append(group.$elem)\n  }\n\n  // 注册单个 toolbarItem\n  private registerSingleItem(key: string, container: GroupButton | Toolbar) {\n    const editor = this.getEditorInstance()\n    const inGroup = container instanceof GroupButton // 要添加到 groupButton\n\n    // 尝试从缓存中获取\n    const { menus } = this\n    let menu = menus[key]\n\n    if (menu == null) {\n      // 缓存中没有，则创建\n      const factory = MENU_ITEM_FACTORIES[key]\n      if (factory == null) {\n        throw new Error(`Not found menu item factory by key '${key}'`)\n      }\n      if (typeof factory !== 'function') {\n        throw new Error(`Menu item factory (key='${key}') is not a function`)\n      }\n\n      // 创建 toolbarItem 并记录缓存\n      menu = factory()\n      menus[key] = menu\n    } else {\n      console.warn(`Duplicated toolbar menu key '${key}'\\n重复注册了菜单栏 menu '${key}'`)\n    }\n\n    const toolbarItem = createBarItem(key, menu, inGroup)\n    this.toolbarItems.push(toolbarItem)\n\n    // 保存 toolbarItem 和 editor 的关系\n    BAR_ITEM_TO_EDITOR.set(toolbarItem, editor)\n\n    // 添加 DOM\n    if (inGroup) {\n      // barItem 是 groupButton\n      const group = container as GroupButton\n      group.appendBarItem(toolbarItem)\n    } else {\n      // barItem 添加到 toolbar\n      const toolbar = container as Toolbar\n      toolbar.$toolbar.append(toolbarItem.$elem)\n    }\n  }\n\n  private getEditorInstance(): IDomEditor {\n    const editor = TOOLBAR_TO_EDITOR.get(this)\n    if (editor == null) throw new Error('Can not get editor instance')\n    return editor\n  }\n\n  /**\n   * editor onChange 时触发（涉及 DOM 操作，加防抖）\n   */\n  changeToolbarState = debounce(() => {\n    this.toolbarItems.forEach(toolbarItem => {\n      toolbarItem.changeMenuState()\n    })\n  }, 200)\n\n  /**\n   * 销毁 toolbar\n   */\n  destroy() {\n    // 销毁 DOM\n    this.$toolbar.remove()\n\n    // 清空属性\n    this.menus = {}\n    this.toolbarItems = []\n  }\n}\n\nexport default Toolbar\n"
  },
  {
    "path": "packages/core/src/menus/bar-item/BaseButton.ts",
    "content": "/**\n * @description base button class\n * @author wangfupeng\n */\n\nimport { IButtonMenu, IDropPanelMenu, IModalMenu } from '../interface'\nimport $, { Dom7Array } from '../../utils/dom'\nimport { IBarItem, getEditorInstance } from './index'\nimport { clearSvgStyle } from '../helpers/helpers'\nimport { promiseResolveThen } from '../../utils/util'\nimport { addTooltip } from './tooltip'\n\nabstract class BaseButton implements IBarItem {\n  readonly $elem: Dom7Array = $(`<div class=\"w-e-bar-item\"></div>`)\n  protected readonly $button: Dom7Array = $(`<button type=\"button\"></button>`)\n  menu: IButtonMenu | IDropPanelMenu | IModalMenu\n  private disabled = false\n\n  constructor(key: string, menu: IButtonMenu | IDropPanelMenu | IModalMenu, inGroup = false) {\n    this.menu = menu\n\n    // 验证 tag\n    const { tag, width } = menu\n    if (tag !== 'button') throw new Error(`Invalid tag '${tag}', expected 'button'`)\n\n    // ----------------- 初始化 dom -----------------\n    const { title, hotkey = '', iconSvg = '' } = menu\n    const { $button } = this\n    if (iconSvg) {\n      const $svg = $(iconSvg)\n      clearSvgStyle($svg) // 清理 svg 样式（扩展的菜单，svg 是不可控的，所以要清理一下）\n      $button.append($svg)\n    } else {\n      // 无 icon 则显示 title\n      $button.text(title)\n    }\n    addTooltip($button, iconSvg, title, hotkey, inGroup) // 设置 tooltip\n    if (inGroup && iconSvg) {\n      // in groupButton（且有 icon），显示 menu title\n      // 如果没有 icon ，上面已添加 title ，不用重复添加\n      $button.append($(`<span class=\"title\">${title}</span>`))\n    }\n    if (width) {\n      $button.css('width', `${width}px`)\n    }\n    $button.attr('data-menu-key', key) // menu key\n    this.$elem.append($button)\n\n    // ----------------- 异步绑定事件 -----------------\n    promiseResolveThen(() => this.init())\n  }\n\n  private init() {\n    // 设置 button 属性\n    this.setActive()\n    this.setDisabled()\n\n    // button click\n    this.$button.on('click', e => {\n      e.preventDefault()\n      const editor = getEditorInstance(this)\n\n      editor.hidePanelOrModal() // 隐藏当前的各种 panel\n\n      if (this.disabled) return\n\n      this.exec() // 执行 menu.exec\n      this.onButtonClick() // 执行其他的逻辑\n    })\n  }\n\n  /**\n   * 执行 menu.exec\n   */\n  private exec() {\n    const editor = getEditorInstance(this)\n    const menu = this.menu\n    const value = menu.getValue(editor)\n    menu.exec(editor, value)\n  }\n\n  // 交给子类去扩展\n  abstract onButtonClick(): void\n\n  private setActive() {\n    const editor = getEditorInstance(this)\n    const { $button } = this\n    const active = this.menu.isActive(editor)\n\n    const className = 'active'\n    if (active) {\n      // 设置为 active\n      $button.addClass(className)\n    } else {\n      // 取消 active\n      $button.removeClass(className)\n    }\n  }\n\n  private setDisabled() {\n    const editor = getEditorInstance(this)\n    const { $button } = this\n    let disabled = this.menu.isDisabled(editor)\n\n    if (editor.selection == null || editor.isDisabled()) {\n      // 未选中，或者 readOnly ，强行设置为 disabled\n      disabled = true\n    }\n\n    // 永远 enable\n    if (this.menu.alwaysEnable) disabled = false\n\n    const className = 'disabled'\n    if (disabled) {\n      // 设置为 disabled\n      $button.addClass(className)\n    } else {\n      // 取消 disabled\n      $button.removeClass(className)\n    }\n\n    this.disabled = disabled // 记录下来\n  }\n\n  changeMenuState() {\n    this.setActive()\n    this.setDisabled()\n  }\n}\n\nexport default BaseButton\n"
  },
  {
    "path": "packages/core/src/menus/bar-item/DropPanelButton.ts",
    "content": "/**\n * @description dropPanel button class\n * @author wangfupeng\n */\n\nimport { IDropPanelMenu } from '../interface'\nimport BaseButton from './BaseButton'\nimport DropPanel from '../panel-and-modal/DropPanel'\nimport { gen$downArrow } from '../helpers/helpers'\nimport { getEditorInstance } from './index'\n\nclass DropPanelButton extends BaseButton {\n  private dropPanel: DropPanel | null = null\n  menu: IDropPanelMenu\n\n  constructor(key: string, menu: IDropPanelMenu, inGroup = false) {\n    super(key, menu, inGroup)\n    this.menu = menu\n\n    if (menu.showDropPanel) {\n      const $arrow = gen$downArrow()\n      this.$button.append($arrow)\n    }\n  }\n\n  // button 点击之后\n  onButtonClick() {\n    if (this.menu.showDropPanel) {\n      this.handleDropPanel()\n    }\n  }\n\n  // 显示/隐藏 dropPanel\n  private handleDropPanel() {\n    const menu = this.menu\n    if (menu.getPanelContentElem == null) return\n    const editor = getEditorInstance(this)\n\n    if (this.dropPanel == null) {\n      // 初次创建\n      const dropPanel = new DropPanel(editor)\n      const contentElem = menu.getPanelContentElem(editor)\n      dropPanel.renderContent(contentElem)\n      dropPanel.appendTo(this.$elem)\n      dropPanel.show()\n\n      // 记录下来，防止重复创建\n      this.dropPanel = dropPanel\n    } else {\n      // 不是初次创建\n      const dropPanel = this.dropPanel\n      if (dropPanel.isShow) {\n        // 当前处于显示状态，则隐藏\n        dropPanel.hide()\n      } else {\n        // 当前未处于显示状态，则重新渲染内容 ，并显示\n        const contentElem = menu.getPanelContentElem(editor)\n        dropPanel.renderContent(contentElem)\n        dropPanel.show()\n      }\n    }\n\n    // 判断 dropPanel 的位置：在菜单右侧/左侧\n    const dropPanel = this.dropPanel\n    if (dropPanel.isShow) {\n      const $menu = this.$elem\n      const { left } = $menu.offset() // 菜单元素 left\n\n      const $toolbar = $menu.parents('.w-e-bar')\n      const { left: toolbarLeft } = $toolbar.offset() // toolbar left\n      const toolbarWidth = $toolbar.width() // toolbar width\n      const halfToolbarWidth = toolbarWidth / 2 // toolbar width 的 1/2\n\n      if (left - toolbarLeft >= halfToolbarWidth) {\n        // 菜单在 toolbar 的右半部分，则 dropPanel 要显示在菜单左侧\n        dropPanel.$elem.css({\n          left: 'none',\n          right: '0',\n        })\n      } else {\n        // 菜单在 toolbar 左半部分，则 dropPanel 显示在菜单右侧\n        dropPanel.$elem.css({\n          left: '0',\n          right: 'none',\n        })\n      }\n    }\n  }\n}\n\nexport default DropPanelButton\n"
  },
  {
    "path": "packages/core/src/menus/bar-item/GroupButton.ts",
    "content": "/**\n * @description group button class\n * @author wangfupeng\n */\n\nimport { gen$downArrow } from '../helpers/helpers'\nimport $, { Dom7Array } from '../../utils/dom'\nimport { IMenuGroup } from '../interface'\nimport { clearSvgStyle } from '../helpers/helpers'\nimport { IBarItem } from './index'\nclass GroupButton {\n  readonly $elem: Dom7Array = $(`<div class=\"w-e-bar-item w-e-bar-item-group\"></div>`)\n  private readonly $container: Dom7Array = $('<div class=\"w-e-bar-item-menus-container\"></div>')\n  readonly $button = $(`<button type=\"button\"></button>`)\n\n  constructor(menu: IMenuGroup) {\n    const { key, iconSvg, title /*, menuKeys = [] */ } = menu\n    const { $elem, $button } = this\n\n    // button\n    if (iconSvg) {\n      const $svg = $(iconSvg)\n      clearSvgStyle($svg) // 清理 svg 样式（扩展的菜单，svg 是不可控的，所以要清理一下）\n      $button.append($svg)\n    } else {\n      // 无 icon 则显示 title\n      $button.text(title)\n    }\n    $button.attr('data-menu-key', key) // menu key\n\n    const $arrow = gen$downArrow()\n    $button.append($arrow)\n    $elem.append($button)\n\n    // menu container\n    const { $container } = this\n    $elem.append($container)\n\n    // 监听 container 内容变化，以判断 $button 是否应该禁用\n    const observer = this.createObserver()\n    this.observe(observer)\n  }\n\n  appendBarItem(barItem: IBarItem) {\n    const { $elem } = barItem\n    this.$container.append($elem)\n  }\n\n  private observe(observer: MutationObserver) {\n    const { $container } = this\n    observer.observe($container[0], { childList: true, subtree: true, attributes: true })\n  }\n\n  private createObserver(): MutationObserver {\n    const { $container, $button } = this\n\n    const observer = new MutationObserver(() => {\n      // 找出 container 下所有的 button\n      const $buttons = $container.find('button')\n      const buttonsLength = $buttons.length\n      if (buttonsLength === 0) return\n\n      // 找出所有 disabled 的 button\n      let disabledButtonsLength = 0\n      $buttons.each(btn => {\n        const $btn = $(btn)\n        if ($btn.hasClass('disabled')) {\n          disabledButtonsLength++\n        }\n      })\n\n      // 判断 group button 是否应该被禁用\n      observer.disconnect()\n      if (disabledButtonsLength === buttonsLength) {\n        // 如果 container 所有的 button 都已经 disabled ，则当前的 GroupButton 也需要 disabled\n        $button.addClass('disabled')\n      } else {\n        // 否则，取消当前的 GroupButton disabled\n        $button.removeClass('disabled')\n      }\n      this.observe(observer)\n    })\n\n    return observer\n  }\n}\n\nexport default GroupButton\n"
  },
  {
    "path": "packages/core/src/menus/bar-item/ModalButton.ts",
    "content": "/**\n * @description modal button class\n * @author wangfupeng\n */\n\nimport { Element } from 'slate'\nimport { IModalMenu, IPositionStyle } from '../interface'\nimport BaseButton from './BaseButton'\nimport Modal from '../panel-and-modal/Modal'\nimport { getEditorInstance } from './index'\nimport { getPositionBySelection, getPositionByNode, correctPosition } from '../helpers/position'\nimport { DomEditor } from '../../editor/dom-editor'\nimport $ from '../../utils/dom'\n\nclass ModalButton extends BaseButton {\n  private $body = $('body')\n  private modal: Modal | null = null\n  menu: IModalMenu\n\n  constructor(key: string, menu: IModalMenu, inGroup = false) {\n    super(key, menu, inGroup)\n    this.menu = menu\n  }\n\n  onButtonClick() {\n    if (this.menu.showModal) {\n      this.handleModal()\n    }\n  }\n\n  /**\n   * 获取 modal 定位\n   */\n  private getPosition(): Partial<IPositionStyle> {\n    const editor = getEditorInstance(this)\n    const positionNode = this.menu.getModalPositionNode(editor)\n\n    if (Element.isElement(positionNode)) {\n      // elem node ，按 node 定位\n      return getPositionByNode(editor, positionNode, 'modal')\n    }\n\n    // 其他情况（如 positionNode == null 或是 text node）则按选区定位\n    return getPositionBySelection(editor)\n  }\n\n  // 显示/隐藏 modal\n  private handleModal() {\n    const editor = getEditorInstance(this)\n    const menu = this.menu\n\n    if (this.modal == null) {\n      // 初次创建\n      const modal = new Modal(editor, menu.modalWidth)\n      this.renderAndShowModal(modal, true)\n\n      // 记录下来，防止重复创建\n      this.modal = modal\n    } else {\n      // 不是初次创建\n      const modal = this.modal\n      if (modal.isShow) {\n        // 当前处于显示状态，则隐藏\n        modal.hide()\n      } else {\n        // 当前未处于显示状态，则重新渲染内容 ，并显示\n        this.renderAndShowModal(modal, false)\n      }\n    }\n  }\n\n  /**\n   * 渲染并显示 modal\n   * @param modal modal\n   * @param firstTime 是否第一次显示 modal\n   */\n  private renderAndShowModal(modal: Modal, firstTime: boolean = false) {\n    const editor = getEditorInstance(this)\n    const menu = this.menu\n    if (menu.getModalContentElem == null) return\n\n    const textarea = DomEditor.getTextarea(editor)\n    const toolbar = DomEditor.getToolbar(editor)\n    const { modalAppendToBody } = toolbar?.getConfig() || {}\n\n    const contentElem = menu.getModalContentElem(editor)\n    modal.renderContent(contentElem)\n\n    if (modalAppendToBody) {\n      // appendTo body 时，用户自己设置 modal 定位\n      modal.setStyle({ left: '0', right: '0' })\n    } else {\n      // 计算并设置 modal position\n      const positionStyle = this.getPosition()\n      modal.setStyle(positionStyle)\n    }\n\n    if (firstTime) {\n      if (modalAppendToBody) {\n        modal.appendTo(this.$body)\n      } else {\n        modal.appendTo(textarea.$textAreaContainer)\n      }\n    }\n\n    modal.show()\n\n    if (!modalAppendToBody) {\n      // 修正 modal 定位，避免超出 textContainer 边界（ appendTo body 则不用设置，用户自己设置 ）\n      correctPosition(editor, modal.$elem)\n    }\n\n    // 让 editor 失焦，否则点击 modal 触发 onChange 会导致 modal 隐藏\n    setTimeout(() => {\n      editor.blur()\n    })\n  }\n}\n\nexport default ModalButton\n"
  },
  {
    "path": "packages/core/src/menus/bar-item/Select.ts",
    "content": "/**\n * @description select\n * @author wangfupeng\n */\n\nimport $, { Dom7Array } from '../../utils/dom'\nimport { IBarItem, getEditorInstance } from './index'\nimport { IOption, ISelectMenu } from '../interface'\nimport SelectList from '../panel-and-modal/SelectList'\nimport { gen$downArrow } from '../helpers/helpers'\nimport { promiseResolveThen } from '../../utils/util'\nimport { addTooltip } from './tooltip'\n\n// 根据 option value 获取 text\nfunction getOptionText(options: IOption[], value: string): string {\n  const length = options.length\n  let text = ''\n  for (let i = 0; i < length; i++) {\n    const opt = options[i]\n    if (opt.value === value) {\n      text = opt.text\n      break\n    }\n  }\n  return text\n}\n\nclass BarItemSelect implements IBarItem {\n  readonly $elem: Dom7Array = $(`<div class=\"w-e-bar-item\"></div>`)\n  private readonly $button: Dom7Array = $(`<button type=\"button\" class=\"select-button\"></button>`)\n  menu: ISelectMenu\n  private disabled = false\n  private selectList: SelectList | null = null\n\n  constructor(key: string, menu: ISelectMenu, inGroup = false) {\n    // 验证 tag\n    const { tag, title, width, iconSvg = '', hotkey = '' } = menu\n    if (tag !== 'select') throw new Error(`Invalid tag '${tag}', expected 'select'`)\n\n    // 初始化 dom\n    const $button = this.$button\n    if (width) {\n      $button.css('width', `${width}px`)\n    }\n    $button.attr('data-menu-key', key) // menu key\n    addTooltip($button, iconSvg, title, hotkey, inGroup) // 设置 tooltip\n    this.$elem.append($button)\n\n    this.menu = menu\n\n    // 异步绑定事件\n    promiseResolveThen(() => this.init())\n  }\n\n  private init() {\n    // 设置 select 属性\n    this.setSelectedValue()\n\n    // select button click\n    this.$button.on('click', (e: Event) => {\n      e.preventDefault()\n      const editor = getEditorInstance(this)\n      editor.hidePanelOrModal() // 隐藏当前的各种 panel\n      this.trigger()\n    })\n  }\n\n  private trigger() {\n    const editor = getEditorInstance(this)\n\n    if (editor.isDisabled()) return\n    if (this.disabled) return\n\n    const menu = this.menu\n\n    // 显示下拉列表\n    if (this.selectList == null) {\n      // 初次创建，渲染 list 并显示\n      this.selectList = new SelectList(editor, menu.selectPanelWidth)\n      const selectList = this.selectList\n      const options = menu.getOptions(editor)\n      selectList.renderList(options)\n      selectList.appendTo(this.$elem)\n      selectList.show()\n\n      // 初次创建，绑定事件\n      selectList.$elem.on('click', 'li', (e: Event) => {\n        const { target } = e\n        if (target == null) return\n\n        e.preventDefault()\n        const $li = $(target)\n        const val = $li.attr('data-value')\n        this.onChange(val)\n      })\n    } else {\n      // 不是初次创建\n      const selectList = this.selectList\n      if (selectList.isShow) {\n        // 当前处于显示状态，则隐藏\n        selectList.hide()\n      } else {\n        // 当前未处于显示状态，则重新渲染 list ，并显示\n        const options = menu.getOptions(editor) // 每次都要重新获取 options ，因为选中项可能会变化\n        selectList.renderList(options)\n        selectList.show()\n      }\n    }\n  }\n\n  private onChange(value: string) {\n    const editor = getEditorInstance(this)\n    const menu = this.menu\n    menu.exec && menu.exec(editor, value)\n  }\n\n  private setSelectedValue() {\n    const editor = getEditorInstance(this)\n    const menu = this.menu\n    const value = menu.getValue(editor)\n\n    const options = menu.getOptions(editor)\n    const optText = getOptionText(options, value.toString())\n\n    const $button = this.$button\n    const $downArrow = gen$downArrow() // 向下的箭头图标\n    $button.empty()\n    $button.text(optText)\n    $button.append($downArrow)\n  }\n\n  private setDisabled() {\n    const editor = getEditorInstance(this)\n    const menu = this.menu\n    let disabled = menu.isDisabled(editor)\n    const $button = this.$button\n\n    if (editor.selection == null || editor.isDisabled()) {\n      // 未选中，或者 readOnly ，强行设置为 disabled\n      disabled = true\n    }\n\n    const className = 'disabled'\n    if (disabled) {\n      // 设置为 disabled\n      $button.addClass(className)\n    } else {\n      // 取消 disabled\n      $button.removeClass(className)\n    }\n\n    this.disabled = disabled // 记录下来\n  }\n\n  changeMenuState() {\n    this.setSelectedValue()\n    this.setDisabled()\n  }\n}\n\nexport default BarItemSelect\n"
  },
  {
    "path": "packages/core/src/menus/bar-item/SimpleButton.ts",
    "content": "/**\n * @description button class\n * @author wangfupeng\n */\n\nimport { IButtonMenu } from '../interface'\nimport BaseButton from './BaseButton'\n\nclass SimpleButton extends BaseButton {\n  constructor(key: string, menu: IButtonMenu, inGroup = false) {\n    super(key, menu, inGroup)\n  }\n  onButtonClick() {\n    // menu.exec 已经在 BaseButton 实现了\n    // 所以，此处不用做任何逻辑\n  }\n}\n\nexport default SimpleButton\n"
  },
  {
    "path": "packages/core/src/menus/bar-item/index.ts",
    "content": "/**\n * @description bar item\n * @author wangfupeng\n */\n\nimport { Dom7Array } from '../../utils/dom'\nimport { IButtonMenu, ISelectMenu, IDropPanelMenu, IModalMenu, IMenuGroup } from '../interface'\nimport { IDomEditor } from '../../editor/interface'\nimport { BAR_ITEM_TO_EDITOR } from '../../utils/weak-maps'\nimport SimpleButton from './SimpleButton'\nimport DropPanelButton from './DropPanelButton'\nimport ModalButton from './ModalButton'\nimport Select from './Select'\nimport GroupButton from './GroupButton'\n\ntype MenuType = IButtonMenu | ISelectMenu | IDropPanelMenu | IModalMenu\n\nexport interface IBarItem {\n  $elem: Dom7Array\n  menu: MenuType\n  changeMenuState: () => void\n}\n\n// menu -> barItem\nconst MENU_TO_BAR_ITEM = new WeakMap<MenuType, IBarItem>()\n\nexport function getEditorInstance(item: IBarItem): IDomEditor {\n  const editor = BAR_ITEM_TO_EDITOR.get(item)\n  if (editor == null) throw new Error('Can not get editor instance')\n  return editor\n}\n\n/**\n * 创建 bar button/select\n * @param key menu key\n * @param menu menu\n * @param inGroup 在 groupButton 中\n */\nexport function createBarItem(key: string, menu: MenuType, inGroup: boolean = false): IBarItem {\n  // 尝试从缓存获取\n  let barItem = MENU_TO_BAR_ITEM.get(menu)\n  if (barItem) return barItem\n\n  // 缓存没有则创建\n  const { tag } = menu\n  if (tag === 'button') {\n    // @ts-ignore\n    const { showDropPanel, showModal } = menu\n    if (showDropPanel) {\n      barItem = new DropPanelButton(key, menu as IDropPanelMenu, inGroup)\n    } else if (showModal) {\n      barItem = new ModalButton(key, menu as IModalMenu, inGroup)\n    } else {\n      barItem = new SimpleButton(key, menu, inGroup)\n    }\n  }\n  if (tag === 'select') {\n    barItem = new Select(key, menu as ISelectMenu, inGroup)\n  }\n\n  if (barItem == null) throw new Error(`Invalid tag in menu ${JSON.stringify(menu)}`)\n\n  // 记录缓存\n  MENU_TO_BAR_ITEM.set(menu, barItem)\n\n  return barItem\n}\n\nexport function createBarItemGroup(menu: IMenuGroup): GroupButton {\n  return new GroupButton(menu)\n}\n"
  },
  {
    "path": "packages/core/src/menus/bar-item/tooltip.ts",
    "content": "/**\n * @description tooltip 功能\n * @author wangfupeng\n */\n\nimport { Dom7Array } from '../../utils/dom'\nimport { IS_APPLE } from '../../utils/ua'\n\nexport function addTooltip(\n  $button: Dom7Array,\n  iconSvg: string,\n  title: string,\n  hotkey: string,\n  inGroup = false\n) {\n  if (!iconSvg) {\n    // 没有 icon 直接显示 title ，不用 tooltip\n    return\n  }\n\n  if (hotkey) {\n    const fnKey = IS_APPLE ? 'cmd' : 'ctrl' // mac OS 转换为 cmd ，windows 转换为 ctrl\n    hotkey = hotkey.replace('mod', fnKey)\n  }\n\n  if (inGroup) {\n    // in groupButton ，tooltip 只显示 快捷键\n    if (hotkey) {\n      $button.attr('data-tooltip', hotkey)\n      $button.addClass('w-e-menu-tooltip-v5')\n      $button.addClass('tooltip-right') // tooltip 显示在右侧\n    }\n  } else {\n    // 非 in groupButton ，正常实现 tooltip\n    const tooltip = hotkey ? `${title}\\n${hotkey}` : title\n    $button.attr('data-tooltip', tooltip)\n    $button.addClass('w-e-menu-tooltip-v5')\n  }\n}\n"
  },
  {
    "path": "packages/core/src/menus/helpers/helpers.ts",
    "content": "/**\n * @description menu helpers\n * @author wangfupeng\n */\n\nimport $, { Dom7Array } from '../../utils/dom'\nimport { SVG_DOWN_ARROW } from '../../constants/svg'\n\n/**\n * 清理 svg 的样式\n * @param $elem svg elem\n */\nexport function clearSvgStyle($elem: Dom7Array) {\n  $elem.removeAttr('width')\n  $elem.removeAttr('height')\n  $elem.removeAttr('fill')\n  $elem.removeAttr('class')\n  $elem.removeAttr('t')\n  $elem.removeAttr('p-id')\n\n  const children = $elem.children()\n  if (children.length) {\n    clearSvgStyle(children)\n  }\n}\n\n/**\n * 向下箭头 icon svg\n */\nexport function gen$downArrow() {\n  const $downArrow = $(SVG_DOWN_ARROW)\n  return $downArrow\n}\n\n/**\n * bar item 分割线\n */\nexport function gen$barItemDivider() {\n  return $('<div class=\"w-e-bar-divider\"></div>')\n}\n"
  },
  {
    "path": "packages/core/src/menus/helpers/position.ts",
    "content": "/**\n * @description menu position helpers\n * @author wangfupeng\n */\n\nimport { Node, Element } from 'slate'\nimport { Dom7Array, getFirstVoidChild } from '../../utils/dom'\nimport { IDomEditor } from '../../editor/interface'\nimport { DomEditor } from '../../editor/dom-editor'\nimport { NODE_TO_ELEMENT } from '../../utils/weak-maps'\nimport { IPositionStyle } from '../interface'\nimport { promiseResolveThen } from '../../utils/util'\n\n/**\n * 获取 textContainer 尺寸和定位\n * @param editor editor\n */\nexport function getTextContainerRect(editor: IDomEditor): {\n  top: number\n  left: number\n  width: number\n  height: number\n} | null {\n  const textarea = DomEditor.getTextarea(editor)\n\n  // 获取 textareaContainer\n  const $textareaContainer = textarea.$textAreaContainer\n  const width = $textareaContainer.width()\n  const height = $textareaContainer.height()\n  const { top, left } = $textareaContainer.offset()\n\n  return { top, left, width, height }\n}\n\n/**\n * 根据选区，计算定位（用于 modal hoverbar）\n * @param editor editor\n */\nexport function getPositionBySelection(editor: IDomEditor): Partial<IPositionStyle> {\n  // 默认情况下 { top: 0, left: 0 }\n  const defaultStyle = { top: '0', left: '0' }\n\n  const { selection } = editor\n  if (selection == null) return defaultStyle // 默认 position\n\n  // 获取 textContainer rect\n  const containerRect = getTextContainerRect(editor)\n  if (containerRect == null) return defaultStyle // 默认 position\n  const {\n    top: containerTop,\n    left: containerLeft,\n    width: containerWidth,\n    height: containerHeight,\n  } = containerRect\n\n  // 获取当前选区的 rect\n  const range = DomEditor.toDOMRange(editor, selection)\n  const rangeRect = range.getClientRects()[0]\n  if (rangeRect == null) return defaultStyle // 默认 position\n  const { width: rangeWidth, height: rangeHeight, top: rangeTop, left: rangeLeft } = rangeRect\n\n  // 存储计算结构\n  const positionStyle: Partial<IPositionStyle> = {}\n\n  // 获取 选区 top left 和 container top left 的差值（< 0 则使用 0）\n  let relativeTop = rangeTop - containerTop\n  let relativeLeft = rangeLeft - containerLeft\n\n  // 判断水平位置： modal/bar 显示在选区左侧，还是右侧？\n  if (relativeLeft > containerWidth / 2) {\n    // 选区 left 大于 containerWidth/2 （选区在 container 的右侧），则 modal/bar 显示在选区左侧\n    let r = containerWidth - relativeLeft\n    positionStyle.right = `${r + 5}px` // 5px 间隔\n  } else {\n    // 否则（选区在 container 的左侧），modal/bar 显示在选区右侧\n    positionStyle.left = `${relativeLeft + 5}px` // 5px 间隔\n  }\n\n  // 判断垂直的位置： modal/bar 显示在选区上面，还是下面？\n  if (relativeTop > containerHeight / 2) {\n    // 选区 top  > containerHeight/2 （选区在 container 的下半部分），则 modal/bar 显示在选区的上面\n    let b = containerHeight - relativeTop\n    positionStyle.bottom = `${b + 5}px` // 5px 间隔\n  } else {\n    // 否则（选区在 container 的上半部分），则 modal/bar 显示在选区的下面\n    let t = relativeTop + rangeHeight\n    if (t < 0) t = 0\n    positionStyle.top = `${t + 5}px` // 5px 间隔\n  }\n\n  return positionStyle\n}\n\n/**\n * 根据 node ，计算定位（用于 modal hoverbar）\n * @param editor editor\n * @param node slate node\n * @param type 'modal'/'bar'\n */\nexport function getPositionByNode(\n  editor: IDomEditor,\n  node: Node,\n  type: string = 'modal'\n): Partial<IPositionStyle> {\n  // 默认情况下 { top: 0, left: 0 }\n  const defaultStyle = { top: '0', left: '0' }\n\n  const { selection } = editor\n  if (selection == null) return defaultStyle // 默认 position\n\n  // 根据 node 获取 elem\n  const isVoidElem = Element.isElement(node) && editor.isVoid(node)\n  const isInlineElem = Element.isElement(node) && editor.isInline(node)\n  const elem = NODE_TO_ELEMENT.get(node)\n  if (elem == null) return defaultStyle // 默认 position\n  let {\n    top: elemTop,\n    left: elemLeft,\n    height: elemHeight,\n    width: elemWidth,\n  } = elem.getBoundingClientRect()\n  if (isVoidElem) {\n    // void node ，重新计算 top 和 height\n    const voidElem = getFirstVoidChild(elem)\n    if (voidElem != null) {\n      const { top, height } = voidElem.getBoundingClientRect()\n      elemTop = top\n      elemHeight = height\n    }\n  }\n\n  // 获取 textContainer rect\n  const containerRect = getTextContainerRect(editor)\n  if (containerRect == null) return defaultStyle // 默认 position\n  const {\n    top: containerTop,\n    left: containerLeft,\n    width: containerWidth,\n    height: containerHeight,\n  } = containerRect\n\n  // 存储计算结构\n  const positionStyle: Partial<IPositionStyle> = {}\n\n  // 获取 elem top left 和 container top left 的差值（< 0 则使用 0）\n  let relativeTop = elemTop - containerTop\n  let relativeLeft = elemLeft - containerLeft\n\n  if (type === 'bar') {\n    // bar - 1. left 对齐 elem.left ；2. 尽量显示在 elem 上方\n    positionStyle.left = `${relativeLeft}px`\n    if (relativeTop > 40) {\n      // top > 40 则显示在上方\n      positionStyle.bottom = `${containerHeight - relativeTop + 5}px` // 5px 间隙\n    } else {\n      // 否则，显示在下方\n      positionStyle.top = `${relativeTop + elemHeight + 5}px` // 5px 间隙\n    }\n\n    return positionStyle\n  }\n\n  if (type === 'modal') {\n    // modal - 1. top 和 elem 需要计算，尽量不遮挡 elem\n\n    // 水平\n    if (!isVoidElem) {\n      // 非 void node - left 和 elem left 对齐\n      positionStyle.left = `${relativeLeft}px`\n    } else {\n      if (isInlineElem) {\n        // inline void node 需要计算\n        if (relativeLeft > (containerWidth - elemWidth) / 2) {\n          // elem 在 container 的右侧，则 modal 显示在 elem 左侧\n          positionStyle.right = `${containerWidth - relativeLeft + 5}px`\n        } else {\n          // 否则 elem 在 container 左侧，则 modal 显示在 elem 右侧\n          positionStyle.left = `${relativeLeft + elemWidth + 5}px`\n        }\n      } else {\n        // block void node 水平靠左即可\n        positionStyle.left = `20px`\n      }\n    }\n\n    // 垂直\n    if (isVoidElem) {\n      // void node - top 和 elem top 对齐\n      let t = relativeTop\n      if (t < 0) t = 0 // top 不能小于 0\n      positionStyle.top = `${t}px`\n    } else {\n      // 非 void node ，计算 top\n      if (relativeTop > (containerHeight - elemHeight) / 2) {\n        // elem 在 container 的下半部分，则 modal 显示在 elem 上方\n        positionStyle.bottom = `${containerHeight - relativeTop + 5}px`\n      } else {\n        // elem 在 container 的上半部分，则 modal 显示在 elem 下方\n        let t = relativeTop + elemHeight\n        if (t < 0) t = 0\n        positionStyle.top = `${t + 5}px`\n      }\n    }\n\n    return positionStyle\n  }\n\n  throw new Error(`type '${type}' is invalid`)\n}\n\n/**\n * 异步修正 position ，不能超出 textContainer 边界\n * @param editor editor\n * @param $positionElem modal/bar\n */\nexport function correctPosition(editor: IDomEditor, $positionElem: Dom7Array) {\n  // 异步，否则 DOM 尚未渲染\n  promiseResolveThen(() => {\n    // 获取 textContainer rect\n    const containerRect = getTextContainerRect(editor)\n    if (containerRect == null) return\n    const {\n      top: containerTop,\n      left: containerLeft,\n      width: containerWidth,\n      height: containerHeight,\n    } = containerRect\n\n    // 获取 modal bar 的 rect\n    const { top: positionElemTop, left: positionElemLeft } = $positionElem.offset()\n    const positionElemWidth = $positionElem.width()\n    const positionElemHeight = $positionElem.height()\n    const relativeTop = positionElemTop - containerTop\n    const relativeLeft = positionElemLeft - containerLeft\n\n    // 获取 modal bar 设置的 style\n    const styleStr = $positionElem.attr('style')\n\n    if (styleStr.indexOf('top') >= 0) {\n      // 设置了 top ，则有可能超过 textContainer 的下边界\n      const d = relativeTop + positionElemHeight - containerHeight\n      if (d > 0) {\n        // 已超过 textContainer 的下边界，则上移\n        const curTopStr = $positionElem.css('top')\n        const curTop = parseInt(curTopStr.toString())\n        let newTop = curTop - d\n        if (newTop < 0) newTop = 0 // 不能超过 textContainer 上边界\n        $positionElem.css('top', `${newTop}px`)\n      }\n    }\n\n    if (styleStr.indexOf('bottom') >= 0) {\n      // 设置了 bottom ，则有可能超过 textContainer 的上边界\n      if (positionElemTop < 0) {\n        // 已超出了上边界\n        const curBottomStr = $positionElem.css('bottom')\n        const curBottom = parseInt(curBottomStr.toString())\n        const newBottom = curBottom - Math.abs(positionElemTop) // 保证上边界和 textContainer 对齐即可，下边界不管\n        $positionElem.css('bottom', `${newBottom}px`)\n      }\n    }\n\n    if (styleStr.indexOf('left') >= 0) {\n      // 设置了 left ，则有可能超过 textContainer 的右边界\n      const d = relativeLeft + positionElemWidth - containerWidth\n      if (d > 0) {\n        // 已超过 textContainer 的右边界，需左移\n        const curLeftStr = $positionElem.css('left')\n        const curLeft = parseInt(curLeftStr.toString())\n        let newLeft = curLeft - d\n        if (newLeft < 0) newLeft = 0 // 不能超过 textContainer 左边界\n        $positionElem.css('left', `${newLeft}px`)\n      }\n    }\n\n    if (styleStr.indexOf('right') >= 0) {\n      // 设置了 right ，则有可能超过 textContainer 的左边界\n      if (positionElemLeft < 0) {\n        // 已超出了左边界\n        const curRightStr = $positionElem.css('right')\n        const curRight = parseInt(curRightStr.toString())\n        const newRight = curRight - Math.abs(positionElemLeft) // 保证左边界和 textContainer 对齐即可，右边界不管\n        $positionElem.css('right', `${newRight}px`)\n      }\n    }\n  })\n}\n"
  },
  {
    "path": "packages/core/src/menus/index.ts",
    "content": "/**\n * @description menus entry\n * @author wangfupeng\n */\n\nimport Toolbar from './bar/Toolbar'\n\n// 注册\nexport { registerMenu } from './register'\n\n// menu 相关接口\nexport {\n  IButtonMenu,\n  ISelectMenu,\n  IDropPanelMenu,\n  IModalMenu,\n  IRegisterMenuConf,\n  IOption,\n} from './interface'\n\n// 输出 modal 相关方法\nexport {\n  genModalInputElems,\n  genModalButtonElems,\n  genModalTextareaElems,\n} from './panel-and-modal/Modal'\n\nexport { Toolbar }\n"
  },
  {
    "path": "packages/core/src/menus/interface.ts",
    "content": "/**\n * @description menu interface\n * @author wangfupeng\n */\n\nimport { Node } from 'slate'\nimport { IDomEditor } from '../editor/interface'\nimport { DOMElement } from '../utils/dom'\n\nexport interface IMenuGroup {\n  key: string\n  title: string\n  iconSvg?: string\n  menuKeys: string[]\n}\n\nexport interface IPositionStyle {\n  top: string\n  left: string\n  right: string\n  bottom: string\n}\n\nexport interface IOption {\n  value: string\n  text: string\n  selected?: boolean\n  styleForRenderMenuList?: { [key: string]: string } // 渲染菜单 list 时的样式\n}\n\ninterface IBaseMenu {\n  readonly title: string\n  readonly iconSvg?: string\n  readonly hotkey?: string // 快捷键，使用 https://www.npmjs.com/package/is-hotkey\n  readonly alwaysEnable?: boolean // 永远不 disabled ，如“全屏”\n\n  readonly tag: string // 'button' | 'select'\n  readonly width?: number // 设置 button 宽度\n\n  getValue: (editor: IDomEditor) => string | boolean // 获取菜单相关的 val 。如是否加粗、颜色值、h1/h2/h3 等\n  isActive: (editor: IDomEditor) => boolean // 是否激活菜单，如选区处于加粗文本时，激活 bold\n  isDisabled: (editor: IDomEditor) => boolean // 是否禁用菜单，如选区处于 code-block 时，禁用 bold 等样式操作\n\n  exec: (editor: IDomEditor, value: string | boolean) => void // button click 或 select change 时触发\n}\n\nexport interface IButtonMenu extends IBaseMenu {\n  /* 其他属性 */\n}\n\nexport interface ISelectMenu extends IBaseMenu {\n  readonly selectPanelWidth?: number\n  getOptions: (editor: IDomEditor) => IOption[] // select -> options\n}\n\nexport interface IDropPanelMenu extends IBaseMenu {\n  readonly showDropPanel: boolean // 点击 'button' 显示 dropPanel\n  getPanelContentElem: (editor: IDomEditor) => DOMElement // showDropPanel 情况下，获取 content elem\n}\n\nexport interface IModalMenu extends IBaseMenu {\n  readonly showModal: boolean // 点击 'button' 显示 modal\n  readonly modalWidth: number\n  getModalContentElem: (editor: IDomEditor) => DOMElement // showModal 情况下，获取 content elem\n  getModalPositionNode: (editor: IDomEditor) => Node | null // 获取 modal 定位的 node ，null 即依据选区定位\n}\n\nexport type MenuFactoryType = () => IButtonMenu | ISelectMenu | IDropPanelMenu | IModalMenu\n\nexport interface IRegisterMenuConf {\n  key: string\n  factory: MenuFactoryType\n  config?: { [key: string]: any }\n}\n"
  },
  {
    "path": "packages/core/src/menus/panel-and-modal/BaseClass.ts",
    "content": "/**\n * @description panel modal baseClass\n * @author wangfupeng\n */\n\nimport { IDomEditor } from '../../editor/interface'\nimport { Dom7Array, DOMElement } from '../../utils/dom'\nimport { EDITOR_TO_PANEL_AND_MODAL, PANEL_OR_MODAL_TO_EDITOR } from '../../utils/weak-maps'\n\nabstract class PanelAndModal {\n  abstract readonly type: string\n  abstract readonly $elem: Dom7Array\n  isShow: boolean = false\n  private showTime: number = 0 // 显示时的时间戳\n\n  constructor(editor: IDomEditor) {\n    this.record(editor)\n  }\n\n  /**\n   * 记录下来，以便隐藏，API editor.hidePanelOrModal\n   */\n  private record(editor: IDomEditor) {\n    let set = EDITOR_TO_PANEL_AND_MODAL.get(editor)\n    if (set == null) {\n      set = new Set()\n      EDITOR_TO_PANEL_AND_MODAL.set(editor, set)\n    }\n    set.add(this)\n\n    PANEL_OR_MODAL_TO_EDITOR.set(this, editor)\n  }\n\n  /**\n   * 除了 content 之外的其他自己要增加的 elem\n   */\n  abstract genSelfElem(): Dom7Array | null\n\n  renderContent(contentElem: DOMElement) {\n    const { $elem } = this\n    $elem.empty() // 先清空，再填充内容\n    $elem.append(contentElem)\n\n    // 添加自己额外的 elem\n    const $selfElem = this.genSelfElem()\n    if ($selfElem) {\n      $elem.append($selfElem)\n    }\n  }\n\n  appendTo($menuElem: Dom7Array) {\n    const { $elem } = this\n    $menuElem.append($elem)\n  }\n\n  show() {\n    if (this.isShow) return\n    this.showTime = Date.now()\n\n    const { $elem } = this\n    $elem.show()\n    this.isShow = true\n\n    // 触发事件\n    const editor = PANEL_OR_MODAL_TO_EDITOR.get(this)\n    if (editor) editor.emit('modalOrPanelShow', this)\n  }\n\n  hide() {\n    if (!this.isShow) return\n\n    const now = Date.now()\n    if (now - this.showTime < 200) {\n      // 刚显示的，不要立刻隐藏（避免频繁触发 show/hide ）\n      return\n    }\n\n    const { $elem } = this\n    $elem.hide()\n    this.isShow = false\n\n    // 触发事件\n    const editor = PANEL_OR_MODAL_TO_EDITOR.get(this)\n    if (editor) editor.emit('modalOrPanelHide')\n  }\n}\n\nexport default PanelAndModal\n"
  },
  {
    "path": "packages/core/src/menus/panel-and-modal/DropPanel.ts",
    "content": "/**\n * @description dropPanel class\n * @author wangfupeng\n */\n\nimport { IDomEditor } from '../../editor/interface'\nimport $, { Dom7Array } from '../../utils/dom'\nimport PanelAndModal from './BaseClass'\n\nclass DropPanel extends PanelAndModal {\n  type = 'dropPanel'\n  readonly $elem: Dom7Array = $(`<div class=\"w-e-drop-panel\"></div>`)\n\n  constructor(editor: IDomEditor) {\n    super(editor)\n  }\n\n  genSelfElem(): Dom7Array | null {\n    return null\n  }\n}\n\nexport default DropPanel\n"
  },
  {
    "path": "packages/core/src/menus/panel-and-modal/Modal.ts",
    "content": "/**\n * @description modal class\n * @author wangfupeng\n */\n\nimport $, { Dom7Array, DOMElement } from '../../utils/dom'\nimport { IPositionStyle } from '../interface'\nimport PanelAndModal from './BaseClass'\nimport { IDomEditor } from '../../editor/interface'\n// import { DomEditor } from '../../editor/dom-editor'\nimport { SVG_CLOSE } from '../../constants/svg'\nimport { PANEL_OR_MODAL_TO_EDITOR } from '../../utils/weak-maps'\n\nclass Modal extends PanelAndModal {\n  type = 'modal'\n  readonly $elem: Dom7Array = $(`<div class=\"w-e-modal\"></div>`)\n  private width: number = 0\n\n  constructor(editor: IDomEditor, width: number = 0) {\n    super(editor)\n    if (width) this.width = width\n\n    const { $elem } = this\n\n    // mousedown 阻止冒泡，因为在 $textContainer 通过 mousedown 隐藏 panel & modal\n    $elem.on('click', e => e.stopPropagation())\n\n    // esc 关闭 modal\n    $elem.on('keyup', e => {\n      const event = e as KeyboardEvent\n      if (event.code === 'Escape') {\n        this.hide()\n        editor.restoreSelection() // 还原选区\n      }\n    })\n  }\n\n  /**\n   * 生成要添加到 modal $elem 的元素\n   * 【注意】不要直接 append 到 modal $elem ，因为它每次都会清空 html('')\n   */\n  genSelfElem(): Dom7Array | null {\n    // 关闭按钮\n    const $closeButton = $(`<span class=\"btn-close\">${SVG_CLOSE}</span>`)\n    const editor = PANEL_OR_MODAL_TO_EDITOR.get(this)\n\n    $closeButton.on('click', () => {\n      this.hide()\n      editor?.restoreSelection()\n    })\n    return $closeButton\n  }\n\n  setStyle(positionStyle: Partial<IPositionStyle>) {\n    const { width, $elem } = this\n\n    $elem.attr('style', '') // 先清空 style ，再重新设置\n\n    if (width) $elem.css('width', `${width}px`)\n    $elem.css(positionStyle)\n  }\n}\n\nexport default Modal\n\n// ---------------------------------- 分割线 ----------------------------------\n\n/**\n * 生成 modal input elems\n * @param labelText label text\n * @param inputId input dom id\n * @param placeholder input placeholder\n * @returns [$container, $input]\n */\nexport function genModalInputElems(\n  labelText: string,\n  inputId: string,\n  placeholder?: string\n): DOMElement[] {\n  const $container = $('<label class=\"babel-container\"></label>')\n  $container.append(`<span>${labelText}</span>`)\n  const $input = $(`<input type=\"text\" id=\"${inputId}\" placeholder=\"${placeholder || ''}\">`)\n  $container.append($input)\n\n  return [$container[0], $input[0]]\n}\n\n/**\n * 生成 modal textarea elems\n * @param labelText label text\n * @param textareaId input dom id\n * @param placeholder input placeholder\n * @returns [$container, $textarea]\n */\nexport function genModalTextareaElems(\n  labelText: string,\n  textareaId: string,\n  placeholder?: string\n): DOMElement[] {\n  const $container = $('<label class=\"babel-container\"></label>')\n  $container.append(`<span>${labelText}</span>`)\n  const $textarea = $(\n    `<textarea type=\"text\" id=\"${textareaId}\" placeholder=\"${placeholder || ''}\"></textarea>`\n  )\n  $container.append($textarea)\n\n  return [$container[0], $textarea[0]]\n}\n\n/**\n * 生成 modal button elems\n * @param buttonId button dom id\n * @param buttonText button text\n * @returns [ $container, $button ]\n */\nexport function genModalButtonElems(buttonId: string, buttonText: string): DOMElement[] {\n  const $buttonContainer = $('<div class=\"button-container\"></div>')\n  const $button = $(`<button type=\"button\" id=\"${buttonId}\">${buttonText}</button>`)\n  $buttonContainer.append($button)\n\n  return [$buttonContainer[0], $button[0]]\n}\n"
  },
  {
    "path": "packages/core/src/menus/panel-and-modal/SelectList.ts",
    "content": "/**\n * @description SelectList class\n * @author wangfupeng\n */\n\nimport $, { Dom7Array } from '../../utils/dom'\nimport { IOption } from '../interface'\nimport PanelAndModal from './BaseClass'\nimport { IDomEditor } from '../../editor/interface'\nimport { SVG_CHECK_MARK } from '../../constants/svg'\n\n// “对号”icon\nfunction gen$SelectedIcon() {\n  return $(SVG_CHECK_MARK)\n}\n\nclass SelectList extends PanelAndModal {\n  type = 'selectList'\n  readonly $elem: Dom7Array = $(`<div class=\"w-e-select-list\"></div>`)\n\n  constructor(editor: IDomEditor, width?: number) {\n    super(editor)\n\n    if (width) {\n      this.$elem.css('width', `${width}px`)\n    }\n\n    this.$elem.on('click', (e: Event) => {\n      // selectList 如有滚动条，可能会点击拖拽，参考 https://github.com/wangeditor-team/wangEditor-v5/issues/325\n      // 此时需要阻止冒泡，因为在 $container.on('mousedown', () => editor.hidePanelOrModal()) ，$container 就是 `.w-e-text-container`\n      e.stopPropagation()\n    })\n  }\n\n  /**\n   * 渲染 list\n   * @param options select options\n   */\n  renderList(options: IOption[]) {\n    const $elem = this.$elem\n    $elem.empty() // 先清空内容，再重新渲染\n\n    const $list = $(`<ul></ul>`)\n    options.forEach(opt => {\n      const { value, text, selected, styleForRenderMenuList } = opt\n      const $li = $(`<li data-value=\"${value}\"></li>`) // 【注意】必须用 <li> 必须用 data-value！！！\n\n      if (styleForRenderMenuList) {\n        $li.css(styleForRenderMenuList)\n      }\n\n      if (selected) {\n        const $selectedIcon = gen$SelectedIcon()\n        $li.append($selectedIcon)\n        $li.addClass('selected')\n      }\n\n      $li.append($(`<span data-value=\"${value}\">${text}</span>`))\n      $li.attr('title', text)\n      $list.append($li)\n    })\n    $elem.append($list)\n  }\n\n  genSelfElem(): Dom7Array | null {\n    return null\n  }\n}\n\nexport default SelectList\n"
  },
  {
    "path": "packages/core/src/menus/register.ts",
    "content": "/**\n * @description register menu\n * @author wangfupeng\n */\n\nimport { MenuFactoryType, IRegisterMenuConf } from './interface'\nimport { registerGlobalMenuConf } from '../config/register'\n\n// menu item 的工厂函数 - 集合\nexport const MENU_ITEM_FACTORIES: {\n  [key: string]: MenuFactoryType\n} = {}\n\n/**\n * 注册菜单配置\n * @param registerMenuConf { key, factory, config } ，各个 menu key 不能重复\n * @param customConfig 自定义 menu config\n */\nexport function registerMenu(\n  registerMenuConf: IRegisterMenuConf,\n  customConfig?: { [key: string]: any }\n) {\n  const { key, factory, config } = registerMenuConf\n\n  // 合并 config\n  const newConfig = { ...config, ...(customConfig || {}) }\n\n  // 注册 menu\n  if (MENU_ITEM_FACTORIES[key] != null) {\n    throw new Error(`Duplicated key '${key}' in menu items`)\n  }\n  MENU_ITEM_FACTORIES[key] = factory\n\n  // 将 config 保存到全局\n  registerGlobalMenuConf(key, newConfig)\n}\n"
  },
  {
    "path": "packages/core/src/parse-html/README.md",
    "content": "# parse html\n\n把 html 转换为 JSON content\n"
  },
  {
    "path": "packages/core/src/parse-html/helper.ts",
    "content": "/**\n * @description parse-html helper fns\n * @author wangfupeng\n */\n\nconst REPLACE_SPACE_160_REG = new RegExp(String.fromCharCode(160), 'g')\n\n/**\n * 把 charCode 160 的空格（`&nbsp` 转换的），替换为 charCode 32 的空格（JS 默认的）\n * @param str str\n * @returns str\n */\nexport function replaceSpace160(str: string): string {\n  const res = str.replace(REPLACE_SPACE_160_REG, ' ')\n  return res\n}\n"
  },
  {
    "path": "packages/core/src/parse-html/index.ts",
    "content": "/**\n * @description parse html\n * @author wangfupeng\n */\n\nimport { DOMElement } from '../utils/dom'\nimport { Element as SlateElement, Descendant } from 'slate'\nimport { IDomEditor } from '../editor/interface'\n\n// 常见的 text tag\nexport const TEXT_TAGS = [\n  'span',\n  'b',\n  'strong',\n  'i',\n  'em',\n  's',\n  'strike',\n  'u',\n  'font',\n  'sub',\n  'sup',\n]\n\n// ------------------------------------ pre-parse html ------------------------------------\nexport type PreParseHtmlFnType = ($node: DOMElement) => DOMElement\n\nexport interface IPreParseHtmlConf {\n  selector: string // css 选择器，如 `p` `div[data-type=\"xxx\"]`\n  preParseHtml: PreParseHtmlFnType\n}\n\nexport const PRE_PARSE_HTML_CONF_LIST: IPreParseHtmlConf[] = []\n\n/**\n * 注册 pre-parse html 配置\n * @param conf pre-parse html conf\n */\nexport function registerPreParseHtmlConf(conf: IPreParseHtmlConf) {\n  PRE_PARSE_HTML_CONF_LIST.push(conf)\n}\n\n// ------------------------------------ parse style html ------------------------------------\n\nexport type ParseStyleHtmlFnType = (\n  $node: DOMElement,\n  node: Descendant,\n  editor: IDomEditor\n) => Descendant\n\nexport const PARSE_STYLE_HTML_FN_LIST: ParseStyleHtmlFnType[] = []\n\n/**\n * 注册 parseStyleHtml 函数\n * @param fn parse style html 的函数\n */\nexport function registerParseStyleHtmlHandler(fn: ParseStyleHtmlFnType) {\n  PARSE_STYLE_HTML_FN_LIST.push(fn)\n}\n\n// ------------------------------------ parse elem html ------------------------------------\n\nexport type ParseElemHtmlFnType = (\n  $elem: DOMElement,\n  children: Descendant[],\n  editor: IDomEditor\n) => SlateElement | SlateElement[]\n\nexport const PARSE_ELEM_HTML_CONF: {\n  [key: string]: ParseElemHtmlFnType // key 是 css 选择器，如 `p` `div[data-type=\"xxx\"]`\n} = {}\n\nexport interface IParseElemHtmlConf {\n  selector: string\n  parseElemHtml: ParseElemHtmlFnType\n}\n\nexport function registerParseElemHtmlConf(conf: IParseElemHtmlConf) {\n  const { selector, parseElemHtml } = conf\n  PARSE_ELEM_HTML_CONF[selector] = parseElemHtml\n}\n"
  },
  {
    "path": "packages/core/src/parse-html/parse-common-elem-html.ts",
    "content": "/**\n * @description parse elem html\n * @author wangfupeng\n */\n\nimport $, { Dom7Array } from 'dom7'\nimport { Editor, Element, Descendant, Text } from 'slate'\nimport { IDomEditor } from '../editor/interface'\nimport parseElemHtml from './parse-elem-html'\nimport { PARSE_ELEM_HTML_CONF, ParseElemHtmlFnType, PARSE_STYLE_HTML_FN_LIST } from './index'\nimport { NodeType, DOMElement } from '../utils/dom'\nimport { replaceSpace160 } from './helper'\n\n/**\n * 往 children 最后一个 item（如果是 text node） 插入文字\n * @param children children\n * @param str str\n * @returns 是否插入成功\n */\nfunction tryInsertTextToChildrenLastItem(children: Descendant[], str: string): boolean {\n  const len = children.length\n  if (len) {\n    const lastItem = children[len - 1]\n    if (Text.isText(lastItem)) {\n      const keys = Object.keys(lastItem)\n      if (keys.length === 1 && keys[0] === 'text') {\n        // lastItem 必须是纯文本，没有 marks\n        lastItem.text = lastItem.text + str\n        return true\n      }\n    }\n  }\n  return false\n}\n\n/**\n * 生成 slate node children\n * @param $elem $elem\n * @param editor editor\n */\nfunction genChildren($elem: Dom7Array, editor: IDomEditor): Descendant[] {\n  const children: Descendant[] = []\n\n  // void node（ html 中编辑的，如 video 的 html 中会有 data-w-e-is-void 属性 ），不需要生成 children\n  const isVoid = $elem.attr('data-w-e-is-void') != null\n  if (isVoid) {\n    return children\n  }\n\n  const childNodes = $elem[0].childNodes\n\n  // 处理空行（只有一个 child ，是 <br>）\n  if (childNodes.length === 1) {\n    if (childNodes[0].nodeName === 'BR') {\n      children.push({ text: '' })\n      return children // 直接返回\n    }\n  }\n\n  // 遍历 DOM 子节点，生成 slate elem node children\n  childNodes.forEach(child => {\n    if (child.nodeType === NodeType.ELEMENT_NODE) {\n      // <br> ，则往 children 最后一个元素（如果是 text ）追加 `\\n`\n      if (child.nodeName === 'BR') {\n        // 尝试把 text 插入到最后一个 children\n        const res = tryInsertTextToChildrenLastItem(children, '\\n')\n        if (!res) {\n          // 若插入失败，则新建 item\n          children.push({ text: '\\n' })\n        }\n        return\n      }\n\n      // 其他 elem\n      const $child = $(child)\n      const parsedRes = parseElemHtml($child, editor)\n      if (Array.isArray(parsedRes)) {\n        parsedRes.forEach(el => children.push(el))\n      } else {\n        children.push(parsedRes)\n      }\n      return\n    }\n    if (child.nodeType === NodeType.TEXT_NODE) {\n      // text\n      let text = child.textContent || ''\n      if (text.trim() === '' && text.indexOf('\\n') >= 0) {\n        // 有换行，但无实际内容\n        return\n      }\n\n      if (text) {\n        // 把 charCode 160 的空格（`&nbsp` 转换的），替换为 charCode 32 的空格（JS 默认的）\n        text = replaceSpace160(text)\n\n        // 尝试把 text 插入到最后一个 children\n        const res = tryInsertTextToChildrenLastItem(children, text)\n        if (!res) {\n          // 若插入失败，则新建 item\n          children.push({ text })\n        }\n      }\n      return\n    }\n  })\n  return children\n}\n\n/**\n * 默认的 parseElemHtml ，直接转换为 paragraph\n * @param elem elem\n * @param children children\n */\nfunction defaultParser(elem: DOMElement, children: Descendant[], editor: IDomEditor): Element {\n  return {\n    type: 'paragraph',\n    children: [{ text: $(elem).text().replace(/\\s+/gm, ' ') }],\n  }\n}\n\n/**\n * 获取当前 html 元素的 parseElemHtml 函数\n * @param $elem $elem\n */\nfunction getParser($elem: Dom7Array): ParseElemHtmlFnType {\n  for (let selector in PARSE_ELEM_HTML_CONF) {\n    if ($elem[0].matches(selector)) {\n      return PARSE_ELEM_HTML_CONF[selector]\n    }\n  }\n  return defaultParser\n}\n\n/**\n * 处理普通 DOM elem html ，非 span font 等文本 elem\n * @param $elem $elem\n * @param editor editor\n * @returns slate element\n */\nfunction parseCommonElemHtml($elem: Dom7Array, editor: IDomEditor): Element[] {\n  const children = genChildren($elem, editor)\n\n  // parse\n  const parser = getParser($elem)\n  let parsedRes = parser($elem[0], children, editor)\n\n  if (!Array.isArray(parsedRes)) parsedRes = [parsedRes] // 临时处理为数组\n\n  parsedRes.forEach(elem => {\n    const isVoid = Editor.isVoid(editor, elem)\n    if (!isVoid) {\n      // 非 void ，如果没有 children ，则取纯文本\n      if (children.length === 0) {\n        elem.children = [{ text: $elem.text().replace(/\\s+/gm, ' ') }]\n      }\n\n      // 处理 style\n      PARSE_STYLE_HTML_FN_LIST.forEach(fn => {\n        elem = fn($elem[0], elem, editor) as Element\n      })\n    }\n  })\n\n  return parsedRes\n}\n\nexport default parseCommonElemHtml\n"
  },
  {
    "path": "packages/core/src/parse-html/parse-elem-html.ts",
    "content": "/**\n * @description parse node html\n * @author wangfupeng\n */\n\nimport $, { Dom7Array } from 'dom7'\nimport { Descendant } from 'slate'\nimport { IDomEditor } from '../editor/interface'\nimport parseCommonElemHtml from './parse-common-elem-html'\nimport parseTextElemHtml from './parse-text-elem-html'\nimport { getTagName } from '../utils/dom'\nimport { PRE_PARSE_HTML_CONF_LIST, TEXT_TAGS } from '../index'\n\n/**\n * 处理 DOM Elem html\n * @param $elem $elem\n * @param editor editor\n * @returns slate Descendant\n */\nfunction parseElemHtml($elem: Dom7Array, editor: IDomEditor): Descendant | Descendant[] {\n  // pre-parse\n  PRE_PARSE_HTML_CONF_LIST.forEach(conf => {\n    const { selector, preParseHtml } = conf\n    if ($elem[0].matches(selector)) {\n      $elem = $(preParseHtml($elem[0]))\n    }\n  })\n\n  const tagName = getTagName($elem)\n\n  // <span> 判断有没有 data-w-e-type 属性。有则是 elem ，没有则是 text\n  if (tagName === 'span') {\n    if ($elem.attr('data-w-e-type')) {\n      return parseCommonElemHtml($elem, editor)\n    } else {\n      return parseTextElemHtml($elem, editor)\n    }\n  }\n\n  // <code> 特殊处理\n  if (tagName === 'code') {\n    const parentTagName = getTagName($elem.parent())\n    if (parentTagName === 'pre') {\n      // <code> 在 <pre> 内，则是 elem\n      return parseCommonElemHtml($elem, editor)\n    } else {\n      // <code> 不在 <pre> 内，则是 text\n      return parseTextElemHtml($elem, editor)\n    }\n  }\n\n  // 非 <code> ，正常处理\n  if (TEXT_TAGS.includes(tagName)) {\n    // text node\n    return parseTextElemHtml($elem, editor)\n  } else {\n    // elem node\n    return parseCommonElemHtml($elem, editor)\n  }\n}\n\nexport default parseElemHtml\n"
  },
  {
    "path": "packages/core/src/parse-html/parse-text-elem-html.ts",
    "content": "/**\n * @description parse text html\n * @author wangfupeng\n */\n\nimport { Dom7Array } from 'dom7'\nimport { Text } from 'slate'\nimport { IDomEditor } from '../editor/interface'\nimport { PARSE_STYLE_HTML_FN_LIST } from './index'\nimport { deReplaceHtmlSpecialSymbols } from '../utils/util'\nimport { replaceSpace160 } from './helper'\n\n/**\n * 处理 text elem ，如 <span> <strong> <em> 等（并不是 DOM Text Node）\n * @param $text $text\n * @param editor editor\n * @returns slate text\n */\nfunction parseTextElemHtml($text: Dom7Array, editor: IDomEditor): Text {\n  if ($text.parents('pre').length === 0) {\n    // 不在 <pre> 内部\n    // 1. 替换无用空格、换行； 2. 将 <br> 替换为 `\\n`\n    $text[0].innerHTML = $text[0].innerHTML.replace(/\\s+/gm, ' ').replace(/<br>/g, '\\n')\n  }\n\n  // 用 textContent ，不能用 .text() 。后者无法识别 text 开头和末尾的 &nbsp;\n  let text = $text[0].textContent || ''\n\n  //【翻转】替换 html 特殊字符，如 &lt; 替换为 <\n  text = deReplaceHtmlSpecialSymbols(text)\n\n  // 把 charCode 160 的空格（`&nbsp` 转换的），替换为 charCode 32 的空格（JS 默认的）\n  text = replaceSpace160(text)\n\n  // 生成 text node\n  let textNode = { text }\n\n  // 处理 style\n  PARSE_STYLE_HTML_FN_LIST.forEach(fn => {\n    textNode = fn($text[0], textNode, editor) as Text\n  })\n\n  return textNode\n}\n\nexport default parseTextElemHtml\n"
  },
  {
    "path": "packages/core/src/render/README.md",
    "content": "# render\n\n把 JSON content 转换为 vdom\n"
  },
  {
    "path": "packages/core/src/render/element/getRenderElem.tsx",
    "content": "/**\n * @description 获取 elem render 函数\n * @author wangfupeng\n */\n\nimport { Element as SlateElement } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { IDomEditor } from '../../editor/interface'\nimport { RENDER_ELEM_CONF, RenderElemFnType } from '../index'\n\n/**\n * 默认的 render elem\n * @param elemNode elem\n * @param editor editor\n * @param children children vnode\n * @returns vnode\n */\nfunction defaultRender(\n  elemNode: SlateElement,\n  children: VNode[] | null,\n  editor: IDomEditor\n): VNode {\n  const Tag = editor.isInline(elemNode) ? 'span' : 'div'\n\n  const vnode = <Tag>{children}</Tag>\n\n  return vnode\n}\n\n/**\n * 根据 elemNode.type 获取 renderElement 函数\n * @param type elemNode.type\n */\nfunction getRenderElem(type: string): RenderElemFnType {\n  const fn = RENDER_ELEM_CONF[type]\n  return fn || defaultRender\n}\n\nexport default getRenderElem\n"
  },
  {
    "path": "packages/core/src/render/element/renderElement.tsx",
    "content": "/**\n * @description render element node\n * @author wangfupeng\n */\n\nimport { Editor, Node, Element as SlateElement } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { node2Vnode } from '../node2Vnode'\nimport { DomEditor } from '../../editor/dom-editor'\nimport { IDomEditor } from '../../editor/interface'\nimport {\n  KEY_TO_ELEMENT,\n  NODE_TO_ELEMENT,\n  ELEMENT_TO_NODE,\n  NODE_TO_INDEX,\n  NODE_TO_PARENT,\n} from '../../utils/weak-maps'\nimport getRenderElem from './getRenderElem'\nimport renderStyle from './renderStyle'\nimport { promiseResolveThen } from '../../utils/util'\nimport { genElemId } from '../helper'\nimport { getElementById } from '../../utils/dom'\n\ninterface IAttrs {\n  id: string\n  key: string | number\n  'data-slate-node': 'element'\n  'data-slate-inline'?: boolean\n  'data-slate-void'?: boolean\n  contentEditable?: Boolean\n}\n\nfunction renderElement(elemNode: SlateElement, editor: IDomEditor): VNode {\n  const key = DomEditor.findKey(editor, elemNode)\n  // const readOnly = editor.isDisabled()\n  const isInline = editor.isInline(elemNode)\n  const isVoid = Editor.isVoid(editor, elemNode)\n  const domId = genElemId(key.id)\n  const attrs: IAttrs = {\n    id: domId,\n    key: key.id,\n    'data-slate-node': 'element',\n    'data-slate-inline': isInline,\n  }\n\n  // 根据 type 生成 vnode 的函数\n  const { type, children = [] } = elemNode\n  let renderElem = getRenderElem(type)\n\n  let childrenVnode\n  if (isVoid) {\n    childrenVnode = null // void 节点 render elem 时不传入 children\n  } else {\n    childrenVnode = children.map((child: Node, index: number) => {\n      return node2Vnode(child, index, elemNode, editor)\n    })\n  }\n\n  // 创建 vnode\n  let vnode = renderElem(elemNode, childrenVnode, editor)\n\n  // void node 要特殊处理\n  if (isVoid) {\n    attrs['data-slate-void'] = true\n\n    // 如果这里设置 contentEditable = false ，那图片就无法删除了 ？？？\n    // if (!readOnly && isInline) {\n    //     attrs.contentEditable = false\n    // }\n\n    const Tag = isInline ? 'span' : 'div'\n    const [[text]] = Node.texts(elemNode)\n\n    const textVnode = node2Vnode(text, 0, elemNode, editor)\n    const textWrapperVnode = (\n      <Tag\n        data-slate-spacer\n        style={{\n          height: '0',\n          color: 'transparent',\n          outline: 'none',\n          position: 'absolute',\n        }}\n      >\n        {textVnode}\n      </Tag>\n    )\n\n    // 重写 vnode\n    vnode = (\n      // 设置 position: relative，保证 absolute 的 textWrapperVnode 不乱跑\n      <Tag style={{ position: 'relative' }}>\n        {vnode}\n        {textWrapperVnode}\n      </Tag>\n    )\n\n    // 记录 text 相关 weakMap\n    NODE_TO_INDEX.set(text, 0)\n    NODE_TO_PARENT.set(text, elemNode)\n  }\n\n  // 添加 element 属性\n  if (vnode.data == null) vnode.data = {}\n  Object.assign(vnode.data, attrs)\n\n  // 添加文本相关的样式，如 text-align\n  if (!isVoid && !isInline) {\n    // 非 void + 非 inline\n    vnode = renderStyle(elemNode, vnode)\n  }\n\n  // 更新 element 相关的 weakMap\n  promiseResolveThen(() => {\n    // 异步，否则拿不到 DOM 节点\n    const dom = getElementById(domId)\n    if (dom == null) return\n    KEY_TO_ELEMENT.set(key, dom)\n    NODE_TO_ELEMENT.set(elemNode, dom)\n    ELEMENT_TO_NODE.set(dom, elemNode)\n  })\n\n  return vnode\n}\n\nexport default renderElement\n"
  },
  {
    "path": "packages/core/src/render/element/renderStyle.ts",
    "content": "/**\n * @description 添加文本相关的样式\n * @author wangfupeng\n */\n\nimport { Element as SlateElement } from 'slate'\nimport { VNode } from 'snabbdom'\nimport { RENDER_STYLE_HANDLER_LIST } from '../index'\n\n/**\n * 渲染样式\n * @param elem slate elem node\n * @param vnode elem Vnode\n */\nfunction renderStyle(elem: SlateElement, vnode: VNode): VNode {\n  let newVnode = vnode\n\n  RENDER_STYLE_HANDLER_LIST.forEach(styleHandler => {\n    newVnode = styleHandler(elem, vnode)\n  })\n\n  return newVnode\n}\n\nexport default renderStyle\n"
  },
  {
    "path": "packages/core/src/render/helper.ts",
    "content": "/**\n * @description formats helper\n * @author wangfupeng\n */\n\nexport function genElemId(id: string) {\n  return `w-e-element-${id}`\n}\n\nexport function genTextId(id: string) {\n  return `w-e-text-${id}`\n}\n"
  },
  {
    "path": "packages/core/src/render/index.ts",
    "content": "/**\n * @description formats entry\n * @author wangfupeng\n */\n\nimport { Element as SlateElement, Descendant } from 'slate'\nimport { VNode } from 'snabbdom'\nimport { IDomEditor } from '../editor/interface'\n\n// ------------------------------------ render style ------------------------------------\n\nexport type RenderStyleFnType = (node: Descendant, vnode: VNode) => VNode\n\n// 存储：处理文本样式的函数，如 b u color 等\nexport const RENDER_STYLE_HANDLER_LIST: RenderStyleFnType[] = []\n\n/**\n * 注册处理文本样式的函数\n * @param fn 处理文本样式的函数\n */\nexport function registerStyleHandler(fn: RenderStyleFnType) {\n  RENDER_STYLE_HANDLER_LIST.push(fn)\n}\n\n// ------------------------------------ render elem ------------------------------------\n\nexport type RenderElemFnType = (\n  elemNode: SlateElement,\n  children: VNode[] | null,\n  editor: IDomEditor\n) => VNode\n\n// 注册 render element 配置\nexport const RENDER_ELEM_CONF: {\n  [key: string]: RenderElemFnType // key 要和 node.type 对应 ！！！\n} = {}\n\nexport interface IRenderElemConf {\n  type: string\n  renderElem: RenderElemFnType\n}\n\n/**\n * 注册 render elem 函数\n * @param conf { type, renderElem } ，type 即 node.type\n */\nexport function registerRenderElemConf(conf: IRenderElemConf) {\n  const { type, renderElem } = conf\n  const key = type || ''\n\n  // 如果 key 重复了，就后者覆盖前者\n  RENDER_ELEM_CONF[key] = renderElem\n}\n"
  },
  {
    "path": "packages/core/src/render/node2Vnode.ts",
    "content": "/**\n * @description slate node to vnode\n * @author wangfupeng\n */\n\nimport { Element, Text, Node, Ancestor } from 'slate'\nimport { VNode } from 'snabbdom'\nimport { IDomEditor } from '../editor/interface'\nimport renderElement from './element/renderElement'\nimport renderText from './text/renderText'\nimport { NODE_TO_INDEX, NODE_TO_PARENT } from '../utils/weak-maps'\n\n/**\n * 根据 slate node 生成 snabbdom vnode\n * @param node node\n * @param index node index in parent.children\n * @param parent parent node\n * @param editor editor\n */\nexport function node2Vnode(node: Node, index: number, parent: Ancestor, editor: IDomEditor): VNode {\n  // 设置相关 weakMap 信息\n  NODE_TO_INDEX.set(node, index)\n  NODE_TO_PARENT.set(node, parent)\n\n  let vnode: VNode\n  if (Element.isElement(node)) {\n    // element\n    vnode = renderElement(node as Element, editor)\n  } else {\n    // text\n    vnode = renderText(node as Text, parent, editor)\n  }\n\n  return vnode\n}\n"
  },
  {
    "path": "packages/core/src/render/text/genVnode.tsx",
    "content": "/**\n * @description 生成 text vnode\n * @author wangfupeng\n */\n\nimport { Editor, Path, Node, Text as SlateText, Ancestor } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { DomEditor } from '../../editor/dom-editor'\nimport { IDomEditor } from '../../editor/interface'\n\nfunction str(text: string, isTrailing = false): VNode {\n  return <span data-slate-string>{isTrailing ? text + '\\n' : text}</span>\n}\n\nfunction zeroWidthStr(length = 0, isLineBreak = false): VNode {\n  return (\n    <span data-slate-zero-width={isLineBreak ? 'n' : 'z'} data-slate-length={length}>\n      {'\\uFEFF'}\n      {isLineBreak ? <br /> : null}\n    </span>\n  )\n}\n\nfunction genTextVnode(\n  leafNode: SlateText,\n  isLast: boolean = false,\n  textNode: SlateText,\n  parent: Ancestor,\n  editor: IDomEditor\n): VNode {\n  const { text } = leafNode\n  const path = DomEditor.findPath(editor, textNode)\n  const parentPath = Path.parent(path)\n\n  if (Editor.isEditor(parent)) {\n    throw new Error(`Text node ${JSON.stringify(textNode)} parent is Editor`)\n  }\n\n  // COMPAT: Render text inside void nodes with a zero-width space.\n  // So the node can contain selection but the text is not visible.\n  if (editor.isVoid(parent)) {\n    return zeroWidthStr(Node.string(parent).length)\n  }\n\n  // COMPAT: If this is the last text node in an empty block, render a zero-\n  // width space that will convert into a line break when copying and pasting\n  // to support expected plain text.\n  if (\n    text === '' &&\n    parent.children[parent.children.length - 1] === textNode &&\n    !editor.isInline(parent) &&\n    Editor.string(editor, parentPath) === ''\n  ) {\n    return zeroWidthStr(0, true)\n  }\n\n  // COMPAT: If the text is empty, it's because it's on the edge of an inline\n  // node, so we render a zero-width space so that the selection can be\n  // inserted next to it still.\n  if (text === '') {\n    return zeroWidthStr()\n  }\n\n  // COMPAT: Browsers will collapse trailing new lines at the end of blocks,\n  // so we need to add an extra trailing new lines to prevent that.\n  if (isLast && text.slice(-1) === '\\n') {\n    return str(text, true)\n  }\n\n  return str(text)\n}\n\nexport default genTextVnode\n"
  },
  {
    "path": "packages/core/src/render/text/renderStyle.ts",
    "content": "/**\n * @description text 样式\n * @author wangfupeng\n */\n\nimport { Text as SlateText } from 'slate'\nimport { VNode } from 'snabbdom'\nimport { RENDER_STYLE_HANDLER_LIST } from '../index'\n\n/**\n * 给字符串增加样式\n * @param leafNode slate text leaf node\n * @param textVnode textVnode\n */\nfunction addTextVnodeStyle(leafNode: SlateText, textVnode: VNode): VNode {\n  let newTextVnode = textVnode\n\n  RENDER_STYLE_HANDLER_LIST.forEach(styleHandler => {\n    newTextVnode = styleHandler(leafNode, newTextVnode)\n  })\n\n  return newTextVnode\n}\n\nexport default addTextVnodeStyle\n"
  },
  {
    "path": "packages/core/src/render/text/renderText.tsx",
    "content": "/**\n * @description render text node\n * @author wangfupeng\n */\n\nimport { Text as SlateText, Ancestor } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { DomEditor } from '../../editor/dom-editor'\nimport { IDomEditor } from '../../editor/interface'\nimport { KEY_TO_ELEMENT, NODE_TO_ELEMENT, ELEMENT_TO_NODE } from '../../utils/weak-maps'\nimport genTextVnode from './genVnode'\nimport addTextVnodeStyle from './renderStyle'\nimport { promiseResolveThen } from '../../utils/util'\nimport { genTextId } from '../helper'\nimport { getElementById } from '../../utils/dom'\n\nfunction renderText(textNode: SlateText, parent: Ancestor, editor: IDomEditor): VNode {\n  if (textNode.text == null)\n    throw new Error(`Current node is not slate Text ${JSON.stringify(textNode)}`)\n  const key = DomEditor.findKey(editor, textNode)\n\n  // 根据 decorate 将 text 拆分为多个叶子节点 text[]\n  const { decorate } = editor.getConfig()\n  if (decorate == null) throw new Error(`Can not get config.decorate`)\n  const path = DomEditor.findPath(editor, textNode)\n  const ds = decorate([textNode, path])\n  const leaves = SlateText.decorations(textNode, ds)\n\n  // 生成 leaves vnode\n  const leavesVnode = leaves.map((leafNode, index) => {\n    // 文字和样式\n    const isLast = index === leaves.length - 1\n    let strVnode = genTextVnode(leafNode, isLast, textNode, parent, editor)\n    strVnode = addTextVnodeStyle(leafNode, strVnode)\n    // 生成每一个 leaf 节点\n    return <span data-slate-leaf>{strVnode}</span>\n  })\n\n  // 生成 text vnode\n  const textId = genTextId(key.id)\n  const vnode = (\n    <span data-slate-node=\"text\" id={textId} key={key.id}>\n      {leavesVnode /* 一个 text 可能包含多个 leaf */}\n    </span>\n  )\n\n  // 更新 weak-map\n  promiseResolveThen(() => {\n    // 异步，否则拿不到 DOM\n    const dom = getElementById(textId)\n    if (dom == null) return\n    KEY_TO_ELEMENT.set(key, dom)\n    NODE_TO_ELEMENT.set(textNode, dom)\n    ELEMENT_TO_NODE.set(dom, textNode)\n  })\n\n  return vnode\n}\n\nexport default renderText\n"
  },
  {
    "path": "packages/core/src/text-area/TextArea.ts",
    "content": "/**\n * @description text-area class\n * @author wangfupeng\n */\n\nimport { Range } from 'slate'\nimport throttle from 'lodash.throttle'\nimport forEach from 'lodash.foreach'\nimport $, { Dom7Array, DOMElement } from '../utils/dom'\nimport { TEXTAREA_TO_EDITOR } from '../utils/weak-maps'\nimport { IDomEditor } from '../editor/interface'\nimport { DomEditor } from '../editor/dom-editor'\nimport updateView from './update-view'\nimport { handlePlaceholder } from './place-holder'\nimport { editorSelectionToDOM, DOMSelectionToEditor } from './syncSelection'\nimport { promiseResolveThen } from '../utils/util'\nimport eventHandlerConf from './event-handlers/index'\n\nlet ID = 1\n\nclass TextArea {\n  readonly id = ID++\n  $box: Dom7Array\n  $textAreaContainer: Dom7Array\n  $scroll: Dom7Array\n  $textArea: Dom7Array | null = null\n  private readonly $progressBar = $('<div class=\"w-e-progress-bar\"></div>')\n  private readonly $maxLengthInfo = $('<div class=\"w-e-max-length-info\"></div>')\n  isComposing: boolean = false\n  isUpdatingSelection: boolean = false\n  isDraggingInternally: boolean = false\n  latestElement: DOMElement | null = null\n  showPlaceholder = false\n  $placeholder: Dom7Array | null = null\n  private latestEditorSelection: Range | null = null\n\n  constructor(boxSelector: string | DOMElement) {\n    // @ts-ignore 初始化 dom\n    const $box = $(boxSelector)\n    if ($box.length === 0) {\n      throw new Error(`Cannot find textarea DOM by selector '${boxSelector}'`)\n    }\n    this.$box = $box\n    const $container = $(`<div class=\"w-e-text-container\"></div>`)\n    $container.append(this.$progressBar) // 进度条\n    $container.append(this.$maxLengthInfo) // max length 提示信息\n    $box.append($container)\n    const $scroll = $(`<div class=\"w-e-scroll\"></div>`)\n    $container.append($scroll)\n    this.$scroll = $scroll\n    this.$textAreaContainer = $container\n\n    // 异步，否则获取不到 editor 和 DOM\n    promiseResolveThen(() => {\n      const editor = this.editorInstance\n      const window = DomEditor.getWindow(editor)\n\n      // 监听 selection change\n      window.document.addEventListener('selectionchange', this.onDOMSelectionChange)\n      // editor 销毁时，解绑 selection change\n      editor.on('destroyed', () => {\n        window.document.removeEventListener('selectionchange', this.onDOMSelectionChange)\n      })\n\n      // 点击编辑区域，关闭 panel\n      $container.on('click', () => editor.hidePanelOrModal())\n\n      // editor onchange 时更新视图\n      editor.on('change', this.changeViewState.bind(this))\n\n      // editor onchange 时触发用户配置的 onChange （需要在 changeViewState 后执行）\n      const { onChange } = editor.getConfig()\n      if (onChange) {\n        editor.on('change', () => onChange(editor))\n      }\n\n      // 监听 onfocus onblur\n      this.onFocusAndOnBlur()\n\n      // 实时修改 maxLength 提示信息\n      editor.on('change', this.changeMaxLengthInfo.bind(this))\n\n      // 绑定 DOM 事件\n      this.bindEvent()\n    })\n  }\n\n  private get editorInstance(): IDomEditor {\n    const editor = TEXTAREA_TO_EDITOR.get(this)\n    if (editor == null) throw new Error('Can not get editor instance')\n    return editor\n  }\n\n  private onDOMSelectionChange = throttle(() => {\n    const editor = this.editorInstance\n    DOMSelectionToEditor(this, editor)\n  }, 100)\n\n  /**\n   * 绑定事件，如 beforeinput onblur onfocus keydown click copy/paste drag/drop 等\n   */\n  private bindEvent() {\n    const { $textArea, $scroll } = this\n    const editor = this.editorInstance\n\n    if ($textArea == null) return\n\n    // 遍历所有事件类型，绑定\n    forEach(eventHandlerConf, (fn, eventType) => {\n      $textArea.on(eventType, event => {\n        fn(event, this, editor)\n      })\n    })\n\n    // 设置 scroll\n    const { scroll } = editor.getConfig()\n    if (scroll) {\n      $scroll.css('overflow-y', 'auto')\n      // scroll 自定义事件\n      $scroll.on(\n        'scroll',\n        throttle(() => {\n          editor.emit('scroll')\n        }, 100)\n      )\n    }\n  }\n\n  private onFocusAndOnBlur() {\n    const editor = this.editorInstance\n    const { onBlur, onFocus } = editor.getConfig()\n    this.latestEditorSelection = editor.selection\n\n    editor.on('change', () => {\n      if (this.latestEditorSelection == null && editor.selection != null) {\n        // 异步触发 focus\n        setTimeout(() => onFocus && onFocus(editor))\n      } else if (this.latestEditorSelection != null && editor.selection == null) {\n        // 异步触发 blur\n        setTimeout(() => onBlur && onBlur(editor))\n      }\n\n      this.latestEditorSelection = editor.selection // 重新记录 selection\n    })\n  }\n\n  /**\n   * 修改 maxLength 提示信息\n   */\n  private changeMaxLengthInfo() {\n    const editor = this.editorInstance\n    const { maxLength } = editor.getConfig()\n    if (maxLength) {\n      const leftLength = DomEditor.getLeftLengthOfMaxLength(editor)\n      const curLength = maxLength - leftLength\n      this.$maxLengthInfo[0].innerHTML = `${curLength}/${maxLength}`\n    }\n  }\n\n  /**\n   * 修改进度条\n   * @param progress 进度\n   */\n  changeProgress(progress: number) {\n    const $progressBar = this.$progressBar\n    $progressBar.css('width', `${progress}%`)\n\n    // 进度 100% 之后，定时隐藏\n    if (progress >= 100) {\n      setTimeout(() => {\n        $progressBar.hide()\n        $progressBar.css('width', '0')\n        $progressBar.show()\n      }, 1000)\n    }\n  }\n\n  /**\n   * 修改 view 状态\n   */\n  changeViewState() {\n    const editor = this.editorInstance\n\n    // 更新 DOM\n    // TODO 注意这里是否会有性能瓶颈？因为每次键盘输入，都会触发这里 —— 可单独测试大文件、多内容，如几万个字\n    updateView(this, editor)\n\n    // 处理 placeholder\n    handlePlaceholder(this, editor)\n\n    // 同步选区（异步，否则拿不到 DOM 渲染结果，vdom）\n    promiseResolveThen(() => {\n      editorSelectionToDOM(this, editor)\n    })\n  }\n\n  /**\n   * 销毁 textarea\n   */\n  destroy() {\n    // 销毁 DOM （只销毁最外层 DOM 即可）\n    this.$textAreaContainer.remove()\n  }\n}\n\nexport default TextArea\n"
  },
  {
    "path": "packages/core/src/text-area/event-handlers/beforeInput.ts",
    "content": "/**\n * @description 处理 beforeInput 事件\n * @author wangfupeng\n */\n\nimport { Editor, Transforms, Range } from 'slate'\nimport { DomEditor } from '../../editor/dom-editor'\nimport { IDomEditor } from '../../editor/interface'\nimport TextArea from '../TextArea'\nimport { hasEditableTarget } from '../helpers'\nimport { DOMStaticRange } from '../../utils/dom'\nimport { HAS_BEFORE_INPUT_SUPPORT } from '../../utils/ua'\nimport { EDITOR_TO_CAN_PASTE } from '../../utils/weak-maps'\n\n// 补充 beforeInput event 的属性\ninterface BeforeInputEventType {\n  data: string | null\n  dataTransfer: DataTransfer | null\n  getTargetRanges(): DOMStaticRange[]\n  inputType: string\n  isComposing: boolean\n}\n\nfunction handleBeforeInput(e: Event, textarea: TextArea, editor: IDomEditor) {\n  const event = e as Event & BeforeInputEventType\n  const { readOnly } = editor.getConfig()\n\n  if (!HAS_BEFORE_INPUT_SUPPORT) return // 有些浏览器完全不支持 beforeInput ，会用 keypress 和 keydown 兼容\n  if (readOnly) return\n  if (!hasEditableTarget(editor, event.target)) return\n\n  const { selection } = editor\n  const { inputType: type } = event\n  const data = event.dataTransfer || event.data || undefined\n\n  // These two types occur while a user is composing text and can't be\n  // cancelled. Let them through and wait for the composition to end.\n  if (type === 'insertCompositionText' || type === 'deleteCompositionText') {\n    return\n  }\n\n  // 阻止默认行为，劫持所有的富文本输入\n  event.preventDefault()\n\n  // COMPAT: For the deleting forward/backward input types we don't want\n  // to change the selection because it is the range that will be deleted,\n  // and those commands determine that for themselves.\n  if (!type.startsWith('delete') || type.startsWith('deleteBy')) {\n    const [targetRange] = event.getTargetRanges()\n\n    if (targetRange) {\n      const range = DomEditor.toSlateRange(editor, targetRange, {\n        exactMatch: false,\n        suppressThrow: false,\n      })\n      if (!selection || !Range.equals(selection, range)) {\n        Transforms.select(editor, range)\n      }\n    }\n  }\n\n  // COMPAT: If the selection is expanded, even if the command seems like\n  // a delete forward/backward command it should delete the selection.\n  if (selection && Range.isExpanded(selection) && type.startsWith('delete')) {\n    const direction = type.endsWith('Backward') ? 'backward' : 'forward'\n    Editor.deleteFragment(editor, { direction })\n    return\n  }\n\n  // 根据 beforeInput 的 event.inputType\n  switch (type) {\n    case 'deleteByComposition':\n    case 'deleteByCut':\n    case 'deleteByDrag': {\n      Editor.deleteFragment(editor)\n      break\n    }\n\n    case 'deleteContent':\n    case 'deleteContentForward': {\n      Editor.deleteForward(editor)\n      break\n    }\n\n    case 'deleteContentBackward': {\n      Editor.deleteBackward(editor)\n      break\n    }\n\n    case 'deleteEntireSoftLine': {\n      Editor.deleteBackward(editor, { unit: 'line' })\n      Editor.deleteForward(editor, { unit: 'line' })\n      break\n    }\n\n    case 'deleteHardLineBackward': {\n      Editor.deleteBackward(editor, { unit: 'block' })\n      break\n    }\n\n    case 'deleteSoftLineBackward': {\n      Editor.deleteBackward(editor, { unit: 'line' })\n      break\n    }\n\n    case 'deleteHardLineForward': {\n      Editor.deleteForward(editor, { unit: 'block' })\n      break\n    }\n\n    case 'deleteSoftLineForward': {\n      Editor.deleteForward(editor, { unit: 'line' })\n      break\n    }\n\n    case 'deleteWordBackward': {\n      Editor.deleteBackward(editor, { unit: 'word' })\n      break\n    }\n\n    case 'deleteWordForward': {\n      Editor.deleteForward(editor, { unit: 'word' })\n      break\n    }\n\n    case 'insertLineBreak':\n    case 'insertParagraph': {\n      Editor.insertBreak(editor)\n      break\n    }\n\n    case 'insertFromDrop':\n    case 'insertFromPaste':\n    case 'insertFromYank':\n    case 'insertReplacementText':\n    case 'insertText': {\n      if (type === 'insertFromPaste') {\n        if (!EDITOR_TO_CAN_PASTE.get(editor)) break // 不可默认粘贴\n      }\n\n      if (data instanceof DataTransfer) {\n        // 这里处理非纯文本（如 html 图片文件等）的粘贴。对于纯文本的粘贴，使用 paste 事件\n        editor.insertData(data)\n      } else if (typeof data === 'string') {\n        Editor.insertText(editor, data)\n      }\n      break\n    }\n  }\n}\n\nexport default handleBeforeInput\n"
  },
  {
    "path": "packages/core/src/text-area/event-handlers/blur.ts",
    "content": "/**\n * @description 处理 onblur 事件\n * @author wangfupeng\n */\n\nimport { Element } from 'slate'\nimport { DomEditor } from '../../editor/dom-editor'\nimport { IDomEditor } from '../../editor/interface'\nimport TextArea from '../TextArea'\nimport { hasEditableTarget } from '../helpers'\nimport { isDOMElement, isDOMNode } from '../../utils/dom'\nimport { IS_FOCUSED } from '../../utils/weak-maps'\nimport { IS_SAFARI } from '../../utils/ua'\n\nfunction handleOnBlur(e: Event, textarea: TextArea, editor: IDomEditor) {\n  const event = e as FocusEvent\n\n  const { isUpdatingSelection, latestElement } = textarea\n  const { readOnly } = editor.getConfig()\n\n  if (readOnly) return\n  if (isUpdatingSelection) return\n  if (!hasEditableTarget(editor, event.target)) return\n  const root = DomEditor.findDocumentOrShadowRoot(editor)\n\n  // COMPAT: If the current `activeElement` is still the previous\n  // one, this is due to the window being blurred when the tab\n  // itself becomes unfocused, so we want to abort early to allow to\n  // editor to stay focused when the tab becomes focused again.\n  if (latestElement === root.activeElement) return\n\n  // relatedTarget 即 blur 之后又 focus 到了哪个元素，如果没有则是 null\n  const { relatedTarget } = event\n  const el = DomEditor.toDOMNode(editor, editor)\n\n  // COMPAT: The event should be ignored if the focus is returning\n  // to the editor from an embedded editable element (eg. an <input>\n  // element inside a void node).\n  if (relatedTarget === el) {\n    return\n  }\n\n  // COMPAT: The event should be ignored if the focus is moving from\n  // the editor to inside a void node's spacer element.\n  if (isDOMElement(relatedTarget) && relatedTarget.hasAttribute('data-slate-spacer')) {\n    return\n  }\n\n  // COMPAT: The event should be ignored if the focus is moving to a\n  // non- editable section of an element that isn't a void node (eg.\n  // a list item of the check list example).\n  if (\n    relatedTarget != null &&\n    isDOMNode(relatedTarget) &&\n    DomEditor.hasDOMNode(editor, relatedTarget)\n  ) {\n    const node = DomEditor.toSlateNode(editor, relatedTarget)\n    if (Element.isElement(node) && !editor.isVoid(node)) {\n      return\n    }\n  }\n\n  // COMPAT: Safari doesn't always remove the selection even if the content-\n  // editable element no longer has focus. Refer to:\n  // https://stackoverflow.com/questions/12353247/force-contenteditable-div-to-stop-accepting-input-after-it-loses-focus-under-web\n  // 修复在 Safari 下，即使 contenteditable 元素非聚焦状态，并不会删除所选内容\n  if (IS_SAFARI) {\n    const domSelection = root.getSelection()\n    domSelection?.removeAllRanges()\n  }\n\n  // 检验完毕，可正式触发 onblur\n  IS_FOCUSED.delete(editor)\n}\n\nexport default handleOnBlur\n"
  },
  {
    "path": "packages/core/src/text-area/event-handlers/click.ts",
    "content": "/**\n * @description 处理 click 事件\n * @author wangfupeng\n */\n\nimport { Editor, Path, Transforms, Node } from 'slate'\nimport { IDomEditor } from '../../editor/interface'\nimport { DomEditor } from '../../editor/dom-editor'\nimport TextArea from '../TextArea'\nimport { hasTarget } from '../helpers'\nimport { isDOMNode } from '../../utils/dom'\n\nfunction handleOnClick(event: Event, textarea: TextArea, editor: IDomEditor) {\n  const { readOnly } = editor.getConfig()\n\n  if (readOnly) return\n  if (!hasTarget(editor, event.target)) return\n  if (!isDOMNode(event.target)) return\n\n  const node = DomEditor.toSlateNode(editor, event.target)\n  const path = DomEditor.findPath(editor, node)\n\n  // At this time, the Slate document may be arbitrarily different,\n  // because onClick handlers can change the document before we get here.\n  // Therefore we must check that this path actually exists,\n  // and that it still refers to the same node.\n  if (Editor.hasPath(editor, path)) {\n    const lookupNode = Node.get(editor, path)\n    if (lookupNode === node) {\n      const start = Editor.start(editor, path)\n      const end = Editor.end(editor, path)\n\n      const startVoid = Editor.void(editor, { at: start })\n      const endVoid = Editor.void(editor, { at: end })\n\n      if (startVoid && endVoid && Path.equals(startVoid[1], endVoid[1])) {\n        const range = Editor.range(editor, start)\n        Transforms.select(editor, range)\n      }\n    }\n  }\n}\n\nexport default handleOnClick\n"
  },
  {
    "path": "packages/core/src/text-area/event-handlers/composition.ts",
    "content": "/**\n * @description 监听 composition 事件\n * @author wangfupeng\n */\n\nimport { Editor, Range, Element } from 'slate'\nimport { IDomEditor } from '../../editor/interface'\nimport { DomEditor } from '../../editor/dom-editor'\nimport TextArea from '../TextArea'\nimport { hasEditableTarget } from '../helpers'\nimport { IS_SAFARI, IS_CHROME, IS_FIREFOX } from '../../utils/ua'\nimport { DOMNode } from '../../utils/dom'\nimport { hidePlaceholder } from '../place-holder'\nimport { editorSelectionToDOM } from '../syncSelection'\n\nconst EDITOR_TO_TEXT: WeakMap<IDomEditor, string> = new WeakMap()\nconst EDITOR_TO_START_CONTAINER: WeakMap<IDomEditor, DOMNode> = new WeakMap()\n\n/**\n * composition start 事件\n * @param e event\n * @param textarea textarea\n * @param editor editor\n */\nexport function handleCompositionStart(e: Event, textarea: TextArea, editor: IDomEditor) {\n  const event = e as CompositionEvent\n\n  if (!hasEditableTarget(editor, event.target)) return\n\n  const { selection } = editor\n  if (selection && Range.isExpanded(selection)) {\n    Editor.deleteFragment(editor)\n\n    Promise.resolve().then(() => {\n      // deleteFragment 会在一个 Promise 后更新 dom，导致浏览器选区不正确\n      // 因此这里延迟一下再设置选区，使选区在正确位置\n      // 这里 model 选区没有发生变化，不能使用 editor.restoreSelection\n      // restoreSelection 会对比前后 model 选区是否相同，相同就不更新了\n      editorSelectionToDOM(textarea, editor, true)\n    })\n  }\n\n  if (selection && Range.isCollapsed(selection)) {\n    // 记录下 dom text ，以便触发 maxLength 时使用\n    const domRange = DomEditor.toDOMRange(editor, selection)\n    const startContainer = domRange.startContainer\n    const curText = startContainer.textContent || ''\n    EDITOR_TO_TEXT.set(editor, curText)\n\n    // 记录下 dom range startContainer\n    EDITOR_TO_START_CONTAINER.set(editor, startContainer)\n  }\n  textarea.isComposing = true\n\n  // 隐藏 placeholder\n  hidePlaceholder(textarea, editor)\n}\n\n/**\n * composition update 事件\n * @param e event\n * @param textarea textarea\n * @param editor editor\n */\nexport function handleCompositionUpdate(event: Event, textarea: TextArea, editor: IDomEditor) {\n  if (!hasEditableTarget(editor, event.target)) return\n\n  textarea.isComposing = true\n}\n\n/**\n * composition end 事件\n * @param e event\n * @param textarea textarea\n * @param editor editor\n */\nexport function handleCompositionEnd(e: Event, textarea: TextArea, editor: IDomEditor) {\n  const event = e as CompositionEvent\n\n  if (!hasEditableTarget(editor, event.target)) return\n  textarea.isComposing = false\n\n  const { selection } = editor\n  if (selection == null) return\n\n  // 清理可能暴露的 text 节点\n  // 例如 chrome 在链接后面，输入拼音，就会出现有暴露出来的 text node\n  if (IS_CHROME || IS_FIREFOX) {\n    DomEditor.cleanExposedTexNodeInSelectionBlock(editor)\n  }\n\n  // 在中文输入法下，浏览器的默认行为会使一些dom产生不可逆的变化\n  // 比如在 Safari 中 url 后面输入，初始是 a > span > spans\n  // 输入后变成 span > span > a\n  // 因此需要设置新的 key 来强刷整行\n  const start = Range.isBackward(selection) ? selection.focus : selection.anchor\n  const [paragraph] = Editor.node(editor, [start.path[0]])\n\n  for (let i = 0; i < start.path.length; i++) {\n    const [node] = Editor.node(editor, start.path.slice(0, i + 1))\n    if (Element.isElement(node)) {\n      if (((IS_SAFARI || IS_FIREFOX) && node.type === 'link') || node.type === 'code') {\n        DomEditor.setNewKey(paragraph)\n        break\n      }\n    }\n  }\n\n  const { data } = event\n  if (!data) return\n\n  // 检查 maxLength -【注意】这里只处理拼音输入的 maxLength 限制。其他限制，在插件 with-max-length.ts 中处理\n  const { maxLength } = editor.getConfig()\n  if (maxLength) {\n    const leftLengthOfMaxLength = DomEditor.getLeftLengthOfMaxLength(editor)\n    if (leftLengthOfMaxLength < data.length) {\n      const domRange = DomEditor.toDOMRange(editor, selection)\n      domRange.startContainer.textContent = EDITOR_TO_TEXT.get(editor) || ''\n      if (leftLengthOfMaxLength > 0) {\n        // 剩余长度 >0 ，但小于 data 长度，截取一部分插入\n        Editor.insertText(editor, data.slice(0, leftLengthOfMaxLength))\n      }\n      textarea.changeViewState() // 重新定位光标\n    } else {\n      Editor.insertText(editor, data)\n    }\n  } else {\n    Editor.insertText(editor, data)\n  }\n\n  // 检查拼音输入是否夸 DOM 节点了，解决 wangEditor-v5/issues/47\n  if (!IS_SAFARI) {\n    setTimeout(() => {\n      const { selection } = editor\n      if (selection == null) return\n      const oldStartContainer = EDITOR_TO_START_CONTAINER.get(editor) // 拼音输入开始时的 text node\n      if (oldStartContainer == null) return\n      const curStartContainer = DomEditor.toDOMRange(editor, selection).startContainer // 拼音输入结束时的 text node\n      if (curStartContainer === oldStartContainer) {\n        // 拼音输入的开始和结束，都在同一个 text node ，则不做处理\n        return\n      }\n      // 否则，拼音输入的开始和结束，不是同一个 text node ，则将第一个 text node 重新设置 text\n      oldStartContainer.textContent = EDITOR_TO_TEXT.get(editor) || ''\n    })\n  }\n}\n"
  },
  {
    "path": "packages/core/src/text-area/event-handlers/copy.ts",
    "content": "/**\n * @description 处理 copy 事件\n * @author wangfupeng\n */\n\nimport { IDomEditor } from '../../editor/interface'\n// import { DomEditor } from '../../editor/dom-editor'\nimport TextArea from '../TextArea'\nimport { hasEditableTarget } from '../helpers'\n\nfunction handleOnCopy(e: Event, textarea: TextArea, editor: IDomEditor) {\n  const event = e as ClipboardEvent\n\n  if (!hasEditableTarget(editor, event.target)) return\n  event.preventDefault()\n\n  const data = event.clipboardData\n  if (data == null) return\n  editor.setFragmentData(data)\n}\n\nexport default handleOnCopy\n"
  },
  {
    "path": "packages/core/src/text-area/event-handlers/cut.ts",
    "content": "/**\n * @description 处理 cut 事件\n * @author wangfupeng\n */\n\nimport { Editor, Range, Node, Transforms } from 'slate'\nimport { IDomEditor } from '../../editor/interface'\nimport TextArea from '../TextArea'\nimport { hasEditableTarget } from '../helpers'\n\nfunction handleOnCut(e: Event, textarea: TextArea, editor: IDomEditor) {\n  const event = e as ClipboardEvent\n  const { selection } = editor\n  const { readOnly } = editor.getConfig()\n\n  if (readOnly) return\n  if (!hasEditableTarget(editor, event.target)) return\n\n  event.preventDefault()\n\n  const data = event.clipboardData\n  if (data == null) return\n  editor.setFragmentData(data)\n\n  if (selection) {\n    if (Range.isExpanded(selection)) {\n      Editor.deleteFragment(editor)\n    } else {\n      const node = Node.parent(editor, selection.anchor.path)\n      if (Editor.isVoid(editor, node)) {\n        Transforms.delete(editor)\n      }\n    }\n  }\n}\n\nexport default handleOnCut\n"
  },
  {
    "path": "packages/core/src/text-area/event-handlers/drag.ts",
    "content": "/**\n * @description 处理 dragover 事件\n * @author wangfupeng\n */\n\nimport { Editor, Transforms } from 'slate'\nimport { DomEditor } from '../../editor/dom-editor'\nimport { IDomEditor } from '../../editor/interface'\nimport TextArea from '../TextArea'\nimport { hasTarget } from '../helpers'\n\nexport function handleOnDragstart(e: Event, textarea: TextArea, editor: IDomEditor) {\n  const event = e as DragEvent\n  if (!hasTarget(editor, event.target)) return\n\n  const { readOnly } = editor.getConfig()\n  if (readOnly) return\n\n  const node = DomEditor.toSlateNode(editor, event.target)\n  const path = DomEditor.findPath(editor, node)\n  const voidMatch = Editor.isVoid(editor, node) || Editor.void(editor, { at: path, voids: true })\n\n  // If starting a drag on a void node, make sure it is selected\n  // so that it shows up in the selection's fragment.\n  if (voidMatch) {\n    const range = Editor.range(editor, path)\n    Transforms.select(editor, range)\n  }\n\n  const data = event.dataTransfer\n  if (data == null) return\n\n  textarea.isDraggingInternally = true\n\n  editor.setFragmentData(data)\n}\n\nexport function handleOnDragover(event: Event, textarea: TextArea, editor: IDomEditor) {\n  if (!hasTarget(editor, event.target)) return\n\n  // Only when the target is void, call `preventDefault` to signal\n  // that drops are allowed. Editable content is droppable by\n  // default, and calling `preventDefault` hides the cursor.\n  const node = DomEditor.toSlateNode(editor, event.target)\n  if (Editor.isVoid(editor, node)) {\n    event.preventDefault()\n  }\n}\n\nexport function handleOnDragend(e: Event, textarea: TextArea, editor: IDomEditor) {\n  const event = e as DragEvent\n  const { readOnly } = editor.getConfig()\n\n  if (readOnly) return\n  if (!textarea.isDraggingInternally) return\n  if (!hasTarget(editor, event.target)) return\n\n  textarea.isDraggingInternally = false\n}\n"
  },
  {
    "path": "packages/core/src/text-area/event-handlers/drop.ts",
    "content": "/**\n * @description 处理 drop 事件\n * @author wangfupeng\n */\n\nimport { Transforms } from 'slate'\nimport { IDomEditor } from '../../editor/interface'\nimport { DomEditor } from '../../editor/dom-editor'\nimport TextArea from '../TextArea'\nimport { hasTarget } from '../helpers'\nimport { HAS_BEFORE_INPUT_SUPPORT, IS_SAFARI } from '../../utils/ua'\n\nfunction handleOnDrop(e: Event, textarea: TextArea, editor: IDomEditor) {\n  const event = e as DragEvent\n  const data = event.dataTransfer\n  const { readOnly } = editor.getConfig()\n\n  if (readOnly) return\n  if (!hasTarget(editor, event.target)) return\n  if (data == null) return\n\n  if (HAS_BEFORE_INPUT_SUPPORT) {\n    if (IS_SAFARI) {\n      // safari 不支持拖拽文件\n      if (data.files.length > 0) return\n    }\n  }\n\n  event.preventDefault()\n\n  // Keep a reference to the dragged range before updating selection\n  const draggedRange = editor.selection\n  const range = DomEditor.findEventRange(editor, event)\n  Transforms.select(editor, range)\n\n  if (textarea.isDraggingInternally) {\n    if (draggedRange) {\n      Transforms.delete(editor, {\n        at: draggedRange,\n      })\n    }\n\n    textarea.isDraggingInternally = false\n  }\n\n  editor.insertData(data)\n\n  // When dragging from another source into the editor, it's possible\n  // that the current editor does not have focus.\n  if (!editor.isFocused()) {\n    editor.focus()\n  }\n}\n\nexport default handleOnDrop\n"
  },
  {
    "path": "packages/core/src/text-area/event-handlers/focus.ts",
    "content": "/**\n * @description 处理 onfocus 事件\n * @author wangfupeng\n */\n\nimport { IDomEditor } from '../../editor/interface'\nimport { DomEditor } from '../../editor/dom-editor'\nimport TextArea from '../TextArea'\nimport { IS_FIREFOX } from '../../utils/ua'\nimport { IS_FOCUSED } from '../../utils/weak-maps'\n\nfunction handleOnFocus(event: Event, textarea: TextArea, editor: IDomEditor) {\n  const el = DomEditor.toDOMNode(editor, editor)\n  const root = DomEditor.findDocumentOrShadowRoot(editor)\n  textarea.latestElement = root.activeElement\n\n  // COMPAT: If the editor has nested editable elements, the focus\n  // can go to them. In Firefox, this must be prevented because it\n  // results in issues with keyboard navigation. (2017/03/30)\n  if (IS_FIREFOX && event.target !== el) {\n    el.focus()\n    return\n  }\n\n  IS_FOCUSED.set(editor, true)\n}\n\nexport default handleOnFocus\n"
  },
  {
    "path": "packages/core/src/text-area/event-handlers/index.ts",
    "content": "/**\n * @description textarea event handlers entry\n * @author wangfupeng\n */\n\nimport handleBeforeInput from './beforeInput'\nimport handleOnBlur from './blur'\nimport handleOnFocus from './focus'\nimport handleOnClick from './click'\nimport {\n  handleCompositionStart,\n  handleCompositionEnd,\n  handleCompositionUpdate,\n} from './composition'\nimport handleOnKeydown from './keydown'\nimport handleKeypress from './keypress'\nimport handleOnCopy from './copy'\nimport handleOnCut from './cut'\nimport handleOnPaste from './paste'\nimport { handleOnDragover, handleOnDragstart, handleOnDragend } from './drag'\nimport handleOnDrop from './drop'\n\nconst eventConf = {\n  beforeinput: handleBeforeInput,\n  blur: handleOnBlur,\n  focus: handleOnFocus,\n  click: handleOnClick,\n  compositionstart: handleCompositionStart,\n  compositionend: handleCompositionEnd,\n  compositionupdate: handleCompositionUpdate,\n  keydown: handleOnKeydown,\n  keypress: handleKeypress,\n  copy: handleOnCopy,\n  cut: handleOnCut,\n  paste: handleOnPaste,\n  dragover: handleOnDragover,\n  dragstart: handleOnDragstart,\n  dragend: handleOnDragend,\n  drop: handleOnDrop,\n}\n\nexport default eventConf\n"
  },
  {
    "path": "packages/core/src/text-area/event-handlers/keydown.ts",
    "content": "/**\n * @description 监听 onKeydown 事件\n * @author wangfupeng\n */\n\nimport { isHotkey } from 'is-hotkey'\nimport { Editor, Transforms, Range, Node, Element } from 'slate'\nimport { IDomEditor } from '../../editor/interface'\nimport TextArea from '../TextArea'\nimport Hotkeys from '../../utils/hotkeys'\nimport { hasEditableTarget } from '../helpers'\nimport { HAS_BEFORE_INPUT_SUPPORT, IS_CHROME, IS_SAFARI } from '../../utils/ua'\nimport { EDITOR_TO_TOOLBAR, EDITOR_TO_HOVER_BAR } from '../../utils/weak-maps'\n\nfunction preventDefault(event: Event) {\n  event.preventDefault()\n}\n\n// 触发 menu 快捷键\nfunction triggerMenuHotKey(editor: IDomEditor, event: KeyboardEvent) {\n  const toolbar = EDITOR_TO_TOOLBAR.get(editor)\n  const toolbarMenus = toolbar && toolbar.getMenus()\n  const hoverbar = EDITOR_TO_HOVER_BAR.get(editor)\n  const hoverbarMenus = hoverbar && hoverbar.getMenus()\n\n  // 合并所有 menus\n  const allMenus = { ...toolbarMenus, ...hoverbarMenus }\n  for (let key in allMenus) {\n    const menu = allMenus[key]\n    const { hotkey } = menu\n    if (hotkey && isHotkey(hotkey, event)) {\n      const disabled = menu.isDisabled(editor)\n      if (!disabled) {\n        const val = menu.getValue(editor)\n        menu.exec(editor, val) // 执行 menu 命令\n      }\n    }\n  }\n}\n\nfunction handleOnKeydown(e: Event, textarea: TextArea, editor: IDomEditor) {\n  const event = e as KeyboardEvent\n  const { selection } = editor\n  const { readOnly } = editor.getConfig()\n\n  if (readOnly) return\n  if (textarea.isComposing) return\n  if (!hasEditableTarget(editor, event.target)) return\n\n  // 触发 menu 快捷键\n  triggerMenuHotKey(editor, event)\n\n  // tab\n  if (Hotkeys.isTab(event)) {\n    preventDefault(event)\n    editor.handleTab()\n    return\n  }\n\n  // COMPAT: Since we prevent the default behavior on\n  // `beforeinput` events, the browser doesn't think there's ever\n  // any history stack to undo or redo, so we have to manage these\n  // hotkeys ourselves. (2019/11/06)\n  if (Hotkeys.isRedo(event)) {\n    preventDefault(event)\n    if (typeof editor.redo === 'function') {\n      editor.redo()\n    }\n    return\n  }\n  if (Hotkeys.isUndo(event)) {\n    preventDefault(event)\n    if (typeof editor.undo === 'function') {\n      editor.undo()\n    }\n    return\n  }\n\n  // COMPAT: Certain browsers don't handle the selection updates\n  // properly. In Chrome, the selection isn't properly extended.\n  // And in Firefox, the selection isn't properly collapsed.\n  // (2017/10/17)\n  if (Hotkeys.isMoveLineBackward(event)) {\n    preventDefault(event)\n    Transforms.move(editor, { unit: 'line', reverse: true }) // Transforms.move 修改 selection\n    return\n  }\n  if (Hotkeys.isMoveLineForward(event)) {\n    preventDefault(event)\n    Transforms.move(editor, { unit: 'line' })\n    return\n  }\n\n  if (Hotkeys.isExtendLineBackward(event)) {\n    preventDefault(event)\n    Transforms.move(editor, { unit: 'line', edge: 'focus', reverse: true })\n    return\n  }\n  if (Hotkeys.isExtendLineForward(event)) {\n    preventDefault(event)\n    Transforms.move(editor, { unit: 'line', edge: 'focus' })\n    return\n  }\n\n  // COMPAT: If a void node is selected, or a zero-width text node\n  // adjacent to an inline is selected, we need to handle these\n  // hotkeys manually because browsers won't be able to skip over\n  // the void node with the zero-width space not being an empty\n  // string.\n  // todo 移动 word 考虑 Node 排版模式是否为 rtl 的情况\n  if (Hotkeys.isMoveBackward(event)) {\n    preventDefault(event)\n\n    if (selection && Range.isCollapsed(selection)) {\n      Transforms.move(editor, { reverse: true })\n    } else {\n      Transforms.collapse(editor, { edge: 'start' })\n    }\n    return\n  }\n  if (Hotkeys.isMoveForward(event)) {\n    preventDefault(event)\n\n    if (selection && Range.isCollapsed(selection)) {\n      Transforms.move(editor)\n    } else {\n      Transforms.collapse(editor, { edge: 'end' })\n    }\n    return\n  }\n\n  if (Hotkeys.isMoveWordBackward(event)) {\n    preventDefault(event)\n\n    if (selection && Range.isExpanded(selection)) {\n      Transforms.collapse(editor, { edge: 'focus' })\n    }\n\n    Transforms.move(editor, { unit: 'word', reverse: true })\n    return\n  }\n  if (Hotkeys.isMoveWordForward(event)) {\n    preventDefault(event)\n\n    if (selection && Range.isExpanded(selection)) {\n      Transforms.collapse(editor, { edge: 'focus' })\n    }\n\n    Transforms.move(editor, { unit: 'word' })\n    return\n  }\n\n  if (Hotkeys.isSelectAll(event)) {\n    preventDefault(event)\n    editor.selectAll()\n    return\n  }\n\n  // COMPAT: Certain browsers don't support the `beforeinput` event, so we\n  // fall back to guessing at the input intention for hotkeys.\n  // COMPAT: In iOS, some of these hotkeys are handled in the\n  if (!HAS_BEFORE_INPUT_SUPPORT) {\n    // 这里是兼容不完全支持 beforeInput 的浏览器。对于支持 beforeInput 的浏览器，会用 beforeinput 事件处理\n    // 这里兼容了 beforeInput 的一些功能键（如回车、删除等）没有文本输入。文本输入使用 keypress 兼容。\n\n    // We don't have a core behavior for these, but they change the\n    // DOM if we don't prevent them, so we have to.\n    if (Hotkeys.isBold(event) || Hotkeys.isItalic(event) || Hotkeys.isTransposeCharacter(event)) {\n      preventDefault(event)\n      return\n    }\n\n    if (Hotkeys.isSplitBlock(event)) {\n      preventDefault(event)\n      Editor.insertBreak(editor)\n      return\n    }\n\n    if (Hotkeys.isDeleteBackward(event)) {\n      preventDefault(event)\n      if (selection && Range.isExpanded(selection)) {\n        Editor.deleteFragment(editor, { direction: 'backward' })\n      } else {\n        Editor.deleteBackward(editor)\n      }\n      return\n    }\n    if (Hotkeys.isDeleteForward(event)) {\n      preventDefault(event)\n      if (selection && Range.isExpanded(selection)) {\n        Editor.deleteFragment(editor, { direction: 'forward' })\n      } else {\n        Editor.deleteForward(editor)\n      }\n      return\n    }\n\n    if (Hotkeys.isDeleteLineBackward(event)) {\n      preventDefault(event)\n      if (selection && Range.isExpanded(selection)) {\n        Editor.deleteFragment(editor, { direction: 'backward' })\n      } else {\n        Editor.deleteBackward(editor, { unit: 'line' })\n      }\n      return\n    }\n    if (Hotkeys.isDeleteLineForward(event)) {\n      preventDefault(event)\n      if (selection && Range.isExpanded(selection)) {\n        Editor.deleteFragment(editor, { direction: 'forward' })\n      } else {\n        Editor.deleteForward(editor, { unit: 'line' })\n      }\n      return\n    }\n\n    if (Hotkeys.isDeleteWordBackward(event)) {\n      preventDefault(event)\n      if (selection && Range.isExpanded(selection)) {\n        Editor.deleteFragment(editor, { direction: 'backward' })\n      } else {\n        Editor.deleteBackward(editor, { unit: 'word' })\n      }\n      return\n    }\n    if (Hotkeys.isDeleteWordForward(event)) {\n      preventDefault(event)\n      if (selection && Range.isExpanded(selection)) {\n        Editor.deleteFragment(editor, { direction: 'forward' })\n      } else {\n        Editor.deleteForward(editor, { unit: 'word' })\n      }\n      return\n    }\n  } else {\n    if (IS_CHROME || IS_SAFARI) {\n      // COMPAT: Chrome and Safari support `beforeinput` event but do not fire\n      // an event when deleting backwards in a selected void inline node\n      // 修复在 Chrome 和 Safari 中删除内容时，内联空节点被选中\n      if (\n        selection &&\n        (Hotkeys.isDeleteBackward(event) || Hotkeys.isDeleteForward(event)) &&\n        Range.isCollapsed(selection)\n      ) {\n        const currentNode = Node.parent(editor, selection.anchor.path)\n\n        if (\n          Element.isElement(currentNode) &&\n          Editor.isVoid(editor, currentNode) &&\n          Editor.isInline(editor, currentNode)\n        ) {\n          event.preventDefault()\n          Transforms.delete(editor, { unit: 'block' })\n\n          return\n        }\n      }\n    }\n  }\n}\n\nexport default handleOnKeydown\n"
  },
  {
    "path": "packages/core/src/text-area/event-handlers/keypress.ts",
    "content": "/**\n * @description 监听 keypress 事件\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport { IDomEditor } from '../../editor/interface'\nimport TextArea from '../TextArea'\nimport { HAS_BEFORE_INPUT_SUPPORT } from '../../utils/ua'\nimport { hasEditableTarget } from '../helpers'\n\n// 【注意】虽然 keypress 事件已经过时（建议用 keydown 取代），但这里是为了兼容 beforeinput ，所以不会在高级浏览器生效，不用升级 keydown\n\nfunction handleKeypress(event: Event, textarea: TextArea, editor: IDomEditor) {\n  // 这里是兼容不完全支持 beforeInput 的浏览器。对于支持 beforeInput 的浏览器，会用 beforeinput 事件处理\n  if (HAS_BEFORE_INPUT_SUPPORT) return\n\n  const { readOnly } = editor.getConfig()\n  if (readOnly) return\n  if (!hasEditableTarget(editor, event.target)) return\n\n  event.preventDefault()\n\n  const text = (event as any).key as string\n\n  // 这里只兼容 beforeInput 的 insertText 类型，其他的（如删除、换行）使用 keydown 来兼容\n  Editor.insertText(editor, text)\n}\n\nexport default handleKeypress\n"
  },
  {
    "path": "packages/core/src/text-area/event-handlers/paste.ts",
    "content": "/**\n * @description 处理 paste 事件\n * @author wangfupeng\n */\n\nimport { IDomEditor } from '../../editor/interface'\nimport { DomEditor } from '../../editor/dom-editor'\nimport TextArea from '../TextArea'\nimport { hasEditableTarget } from '../helpers'\nimport { isPlainTextOnlyPaste } from '../../utils/dom'\nimport { HAS_BEFORE_INPUT_SUPPORT } from '../../utils/ua'\nimport { EDITOR_TO_CAN_PASTE } from '../../utils/weak-maps'\n\nfunction handleOnPaste(e: Event, textarea: TextArea, editor: IDomEditor) {\n  EDITOR_TO_CAN_PASTE.set(editor, true) // 标记为：可执行默认粘贴\n\n  const event = e as ClipboardEvent\n  const { readOnly } = editor.getConfig()\n\n  if (readOnly) return\n  if (!hasEditableTarget(editor, event.target)) return\n\n  const { customPaste } = editor.getConfig()\n  if (customPaste) {\n    const res = customPaste(editor, event)\n    if (res === false) {\n      // 自行实现粘贴，不执行默认粘贴\n      EDITOR_TO_CAN_PASTE.set(editor, false) // 标记为：不可执行默认粘贴\n      return\n    }\n  }\n\n  // 如果支持 beforeInput 且不是纯粘贴文本（如 html、图片文件），则使用 beforeInput 来实现\n  // 这里只处理：不支持 beforeInput 或者 粘贴纯文本\n  if (HAS_BEFORE_INPUT_SUPPORT && !isPlainTextOnlyPaste(event)) return\n\n  event.preventDefault()\n\n  const data = event.clipboardData\n  if (data == null) return\n  editor.insertData(data)\n}\n\nexport default handleOnPaste\n"
  },
  {
    "path": "packages/core/src/text-area/helpers.ts",
    "content": "/**\n * @description textarea helper fns\n * @author wangfupeng\n */\n\nimport { Editor } from 'slate'\nimport { DOMRange, DOMNode, isDOMNode } from '../utils/dom'\nimport { IDomEditor } from '../editor/interface'\nimport { DomEditor } from '../editor/dom-editor'\n\n/**\n * Check if two DOM range objects are equal.\n */\nexport const isRangeEqual = (a: DOMRange, b: DOMRange) => {\n  return (\n    (a.startContainer === b.startContainer &&\n      a.startOffset === b.startOffset &&\n      a.endContainer === b.endContainer &&\n      a.endOffset === b.endOffset) ||\n    (a.startContainer === b.endContainer &&\n      a.startOffset === b.endOffset &&\n      a.endContainer === b.startContainer &&\n      a.endOffset === b.startOffset)\n  )\n}\n\n/**\n * Check if the target is editable and in the editor.\n */\nexport function hasEditableTarget(\n  editor: IDomEditor,\n  target: EventTarget | null\n): target is DOMNode {\n  return isDOMNode(target) && DomEditor.hasDOMNode(editor, target, { editable: true })\n}\n\n/**\n * Check if the target is inside void and in an non-readonly editor.\n */\nexport function isTargetInsideNonReadonlyVoid(\n  editor: IDomEditor,\n  target: EventTarget | null\n): boolean {\n  const { readOnly } = editor.getConfig()\n  if (readOnly) return false\n\n  const slateNode = hasTarget(editor, target) && DomEditor.toSlateNode(editor, target)\n  return Editor.isVoid(editor, slateNode)\n}\n\n/**\n * Check if the target is in the editor.\n */\nexport function hasTarget(editor: IDomEditor, target: EventTarget | null): target is DOMNode {\n  return isDOMNode(target) && DomEditor.hasDOMNode(editor, target)\n}\n\n/**\n * Check if a DOM event is overrode by a handler.\n */\nexport function isDOMEventHandled(event: Event, handler?: (event: Event) => void | boolean) {\n  if (!handler) {\n    return false\n  }\n\n  // The custom event handler may return a boolean to specify whether the event\n  // shall be treated as being handled or not.\n  const shouldTreatEventAsHandled = handler(event)\n\n  if (shouldTreatEventAsHandled != null) {\n    return shouldTreatEventAsHandled\n  }\n\n  return event.defaultPrevented\n}\n"
  },
  {
    "path": "packages/core/src/text-area/place-holder.ts",
    "content": "/**\n * @description 显示/隐藏 placeholder\n * @author wangfupeng\n */\n\nimport { IDomEditor } from '../editor/interface'\nimport TextArea from './TextArea'\nimport $ from '../utils/dom'\n\n/**\n * 处理 placeholder\n * @param textarea textarea\n * @param editor editor\n */\nexport function handlePlaceholder(textarea: TextArea, editor: IDomEditor) {\n  const { placeholder } = editor.getConfig()\n  if (!placeholder) return\n\n  const isEmpty = editor.isEmpty()\n\n  // 内容为空，且目前未显示 placeholder ，则显示\n  if (isEmpty && !textarea.showPlaceholder && !textarea.isComposing) {\n    if (textarea.$placeholder == null) {\n      const $placeholder = $(`<div class=\"w-e-text-placeholder\">${placeholder}</div>`)\n      textarea.$textAreaContainer.append($placeholder)\n      textarea.$placeholder = $placeholder\n    }\n    textarea.$placeholder.show()\n    textarea.showPlaceholder = true // 记录\n    return\n  }\n\n  // 内容不是空，且目前显示着 placeholder ，则隐藏\n  if (!isEmpty && textarea.showPlaceholder) {\n    textarea.$placeholder?.hide()\n    textarea.showPlaceholder = false // 记录\n    return\n  }\n}\n\n/**\n * 隐藏 placeholder （如拼音输入 compositionStart 时，要先隐藏，等 compositionEnd 时再判断是否显示）\n * @param textarea textarea\n * @param editor editor\n */\nexport function hidePlaceholder(textarea: TextArea, editor: IDomEditor) {\n  const { placeholder } = editor.getConfig()\n  if (!placeholder) return\n\n  const isEmpty = editor.isEmpty()\n  if (!isEmpty) return\n\n  if (textarea.showPlaceholder) {\n    textarea.$placeholder?.hide()\n    textarea.showPlaceholder = false // 记录\n  }\n}\n"
  },
  {
    "path": "packages/core/src/text-area/syncSelection.ts",
    "content": "/**\n * @description 同步 selection\n * @author wangfupeng\n */\n\nimport { Range, Transforms } from 'slate'\nimport scrollIntoView from 'scroll-into-view-if-needed'\n\nimport { IDomEditor } from '../editor/interface'\nimport { DomEditor } from '../editor/dom-editor'\nimport TextArea from './TextArea'\nimport { EDITOR_TO_ELEMENT, IS_FOCUSED } from '../utils/weak-maps'\nimport { IS_FIREFOX } from '../utils/ua'\nimport { hasEditableTarget, isTargetInsideNonReadonlyVoid } from './helpers'\nimport { DOMElement } from '../utils/dom'\n\n/**\n * editor onchange 时，将 editor selection 同步给 DOM\n * @param textarea textarea\n * @param editor editor\n * @param focus 是否强制更新选区\n */\nexport function editorSelectionToDOM(textarea: TextArea, editor: IDomEditor, focus = false): void {\n  const { selection } = editor\n  const config = editor.getConfig()\n  const root = DomEditor.findDocumentOrShadowRoot(editor)\n  const domSelection = root.getSelection()\n\n  if (!domSelection) return\n  if (textarea.isComposing && !focus) return\n  if (!editor.isFocused()) return\n\n  const hasDomSelection = domSelection.type !== 'None'\n\n  // If the DOM selection is properly unset, we're done.\n  if (!selection && !hasDomSelection) return\n\n  // verify that the dom selection is in the editor\n  const editorElement = EDITOR_TO_ELEMENT.get(editor)!\n  let hasDomSelectionInEditor = false\n  if (\n    editorElement.contains(domSelection.anchorNode) &&\n    editorElement.contains(domSelection.focusNode)\n  ) {\n    hasDomSelectionInEditor = true\n  }\n\n  // If the DOM selection is in the editor and the editor selection is already correct, we're done.\n  if (hasDomSelection && hasDomSelectionInEditor && selection) {\n    const slateRange = DomEditor.toSlateRange(editor, domSelection, {\n      exactMatch: true,\n\n      // domSelection is not necessarily a valid Slate range\n      // (e.g. when clicking on contentEditable:false element)\n      suppressThrow: true,\n    })\n    if (slateRange && Range.equals(slateRange, selection)) {\n      let canReturn = true\n\n      // 选区在 table 时，需要特殊处理\n      if (Range.isCollapsed(selection)) {\n        const { anchorNode, anchorOffset } = domSelection\n        if (anchorNode === editorElement) {\n          const childNodes = editorElement.childNodes\n          let tableElem\n\n          // 光标在 table 前面时\n          tableElem = childNodes[anchorOffset] as DOMElement\n          if (tableElem && tableElem.matches('table')) {\n            canReturn = false // 不能就此结束，需要重置光标\n          }\n\n          // 光标在 table 后面时\n          tableElem = childNodes[anchorOffset - 1] as DOMElement\n          if (tableElem && tableElem.matches('table')) {\n            canReturn = false // 不能就此结束，需要重置光标\n          }\n        }\n      }\n\n      // 其他情况，就此结束\n      if (canReturn) return\n    }\n  }\n\n  // when <Editable/> is being controlled through external value\n  // then its children might just change - DOM responds to it on its own\n  // but Slate's value is not being updated through any operation\n  // and thus it doesn't transform selection on its own\n  if (selection && !DomEditor.hasRange(editor, selection)) {\n    editor.selection = DomEditor.toSlateRange(editor, domSelection, {\n      exactMatch: false,\n      suppressThrow: false,\n    })\n    return\n  }\n\n  // Otherwise the DOM selection is out of sync, so update it.\n  textarea.isUpdatingSelection = true\n\n  const newDomRange = selection && DomEditor.toDOMRange(editor, selection)\n  if (newDomRange) {\n    if (Range.isBackward(selection!)) {\n      domSelection.setBaseAndExtent(\n        newDomRange.endContainer,\n        newDomRange.endOffset,\n        newDomRange.startContainer,\n        newDomRange.startOffset\n      )\n    } else {\n      domSelection.setBaseAndExtent(\n        newDomRange.startContainer,\n        newDomRange.startOffset,\n        newDomRange.endContainer,\n        newDomRange.endOffset\n      )\n    }\n\n    // 滚动到选区\n    let leafEl = newDomRange.startContainer.parentElement! as Element\n    const spacer = leafEl.closest('[data-slate-spacer]')\n\n    // 这个 if 防止选中图片时发生滚动\n    if (!spacer) {\n      leafEl.getBoundingClientRect = newDomRange.getBoundingClientRect.bind(newDomRange)\n      const body = document.body\n      scrollIntoView(leafEl, {\n        scrollMode: 'if-needed',\n        boundary: config.scroll ? editorElement.parentElement : body, // issue 4215\n        block: 'end',\n        behavior: 'smooth',\n      })\n      // @ts-ignore\n      delete leafEl.getBoundingClientRect\n    }\n  } else {\n    domSelection.removeAllRanges()\n  }\n\n  setTimeout(() => {\n    // COMPAT: In Firefox, it's not enough to create a range, you also need\n    // to focus the contenteditable element too. (2016/11/16)\n    if (newDomRange && IS_FIREFOX) {\n      editorElement.focus()\n    }\n\n    textarea.isUpdatingSelection = false\n  })\n}\n\n/**\n * DOM selection change 时，把 DOM selection 同步给 slate\n * @param textarea textarea\n * @param editor editor\n */\nexport function DOMSelectionToEditor(textarea: TextArea, editor: IDomEditor) {\n  const { isComposing, isUpdatingSelection, isDraggingInternally } = textarea\n  const config = editor.getConfig()\n\n  if (config.readOnly) return\n  if (isComposing) return\n  if (isUpdatingSelection) return\n  if (isDraggingInternally) return\n\n  const root = DomEditor.findDocumentOrShadowRoot(editor)\n  const { activeElement } = root\n  const el = DomEditor.toDOMNode(editor, editor)\n  const domSelection = root.getSelection()\n\n  if (activeElement === el) {\n    textarea.latestElement = activeElement\n    IS_FOCUSED.set(editor, true)\n  } else {\n    IS_FOCUSED.delete(editor)\n  }\n\n  if (!domSelection) {\n    return Transforms.deselect(editor)\n  }\n\n  const { anchorNode, focusNode } = domSelection\n\n  const anchorNodeSelectable =\n    hasEditableTarget(editor, anchorNode) || isTargetInsideNonReadonlyVoid(editor, anchorNode)\n  const focusNodeSelectable =\n    hasEditableTarget(editor, focusNode) || isTargetInsideNonReadonlyVoid(editor, focusNode)\n\n  if (anchorNodeSelectable && focusNodeSelectable) {\n    const range = DomEditor.toSlateRange(editor, domSelection, {\n      exactMatch: false,\n      suppressThrow: false,\n    })\n    Transforms.select(editor, range)\n  } else {\n    Transforms.deselect(editor)\n  }\n}\n"
  },
  {
    "path": "packages/core/src/text-area/update-view.ts",
    "content": "/**\n * @description patch textarea view\n * @author wangfupeng\n */\n\nimport { h, VNode } from 'snabbdom'\nimport { IDomEditor } from '../editor/interface'\nimport TextArea from './TextArea'\nimport { genPatchFn, normalizeVnodeData } from '../utils/vdom'\nimport $, { Dom7Array, getDefaultView, getElementById } from '../utils/dom'\nimport { node2Vnode } from '../render/node2Vnode'\nimport {\n  IS_FIRST_PATCH,\n  TEXTAREA_TO_PATCH_FN,\n  TEXTAREA_TO_VNODE,\n  EDITOR_TO_ELEMENT,\n  NODE_TO_ELEMENT,\n  ELEMENT_TO_NODE,\n  EDITOR_TO_WINDOW,\n} from '../utils/weak-maps'\n\nfunction genElemId(id: number) {\n  return `w-e-textarea-${id}`\n}\n\n/**\n * 生成编辑区域节点的 vnode\n * @param elemId elemId\n * @param readOnly readOnly\n */\nfunction genRootVnode(elemId: string, readOnly = false): VNode {\n  return h(`div#${elemId}`, {\n    props: {\n      contentEditable: readOnly ? false : true,\n    },\n  })\n  // 其他属性在 genRootElem 中定，这里不用重复写\n}\n\n/**\n * 生成编辑区域的 elem\n * @param elemId elemId\n * @param readOnly readOnly\n */\nfunction genRootElem(elemId: string, readOnly = false): Dom7Array {\n  const $elem = $(`<div\n        id=\"${elemId}\"\n        data-slate-editor\n        data-slate-node=\"value\"\n        suppressContentEditableWarning\n        role=\"textarea\"\n        spellCheck=\"true\"\n        autoCorrect=\"true\"\n        autoCapitalize=\"true\"\n    ></div>`)\n\n  // role=\"textarea\" - 增强语义，div 语义太弱\n\n  return $elem\n}\n\n/**\n * 获取 editor.children 渲染 DOM\n * @param textarea textarea\n * @param editor editor\n */\nfunction updateView(textarea: TextArea, editor: IDomEditor) {\n  const $scroll = textarea.$scroll\n  const elemId = genElemId(textarea.id)\n  const { readOnly, autoFocus } = editor.getConfig()\n\n  // 生成 newVnode\n  const newVnode = genRootVnode(elemId, readOnly)\n  const content = editor.children || []\n  newVnode.children = content.map((node, i) => {\n    let vnode = node2Vnode(node, i, editor, editor)\n    normalizeVnodeData(vnode) // 整理 vnode.data 以符合 snabbdom 的要求\n    return vnode\n  })\n\n  let textareaElem\n  let isFirstPatch = IS_FIRST_PATCH.get(textarea)\n  if (isFirstPatch == null) isFirstPatch = true // 尚未赋值，也是第一次\n  if (isFirstPatch) {\n    // 第一次 patch ，先生成 elem\n    const $textArea = genRootElem(elemId, readOnly)\n    $scroll.append($textArea)\n    textarea.$textArea = $textArea // 存储下编辑区域的 DOM 节点\n    textareaElem = $textArea[0]\n\n    // 再生成 patch 函数，并执行\n    const patchFn = genPatchFn()\n    patchFn(textareaElem, newVnode)\n\n    // 存储相关信息\n    IS_FIRST_PATCH.set(textarea, false) // 不再是第一次 patch\n    TEXTAREA_TO_PATCH_FN.set(textarea, patchFn) // 存储 patch 函数\n  } else {\n    // 不是第一次 patch\n    const curVnode = TEXTAREA_TO_VNODE.get(textarea)\n    const patchFn = TEXTAREA_TO_PATCH_FN.get(textarea)\n    if (curVnode == null || patchFn == null) return\n    textareaElem = curVnode.elm\n\n    patchFn(curVnode, newVnode)\n  }\n\n  if (textareaElem == null) {\n    textareaElem = getElementById(elemId)\n\n    // 通过 getElementById 获取的有可能是 null （销毁、重建时，可能会发生这种情况）\n    if (textareaElem == null) return\n  }\n\n  // focus\n  let isFocused\n  if (isFirstPatch) {\n    // 初次渲染\n    isFocused = autoFocus\n  } else {\n    // 非初次渲染\n    isFocused = editor.isFocused()\n  }\n  if (isFocused) {\n    textareaElem.focus({\n      preventScroll: true, // 必须添加 preventScroll 选项，否则弹窗或者编辑器失焦会导致编辑区域自动滚动到顶部\n    })\n  }\n\n  // 存储相关信息\n  if (isFirstPatch) {\n    const window = getDefaultView(textareaElem)\n    window && EDITOR_TO_WINDOW.set(editor, window)\n  }\n\n  EDITOR_TO_ELEMENT.set(editor, textareaElem) // 存储 editor -> elem 对应关系\n  NODE_TO_ELEMENT.set(editor, textareaElem)\n  ELEMENT_TO_NODE.set(textareaElem, editor)\n  TEXTAREA_TO_VNODE.set(textarea, newVnode) // 存储 vnode\n}\n\nexport default updateView\n"
  },
  {
    "path": "packages/core/src/to-html/README.md",
    "content": "# to html\n\n把 content 为 html\n"
  },
  {
    "path": "packages/core/src/to-html/elem2html.ts",
    "content": "/**\n * @description elem -> html\n * @author wangfupeng\n */\n\nimport { Editor, Element } from 'slate'\nimport { IDomEditor } from '../editor/interface'\nimport node2html from './node2html'\nimport { ElemToHtmlFnType, ELEM_TO_HTML_CONF, STYLE_TO_HTML_FN_LIST } from './index'\n\n/**\n * 默认的 toHtml 函数\n * @param elemNode elem node\n * @param childrenHtml children html\n * @param editor editor\n */\nfunction defaultParser(elemNode: Element, childrenHtml: string, editor: IDomEditor) {\n  const isInline = editor.isInline(elemNode)\n  const tag = isInline ? 'span' : 'div'\n  return `<${tag}>${childrenHtml}</${tag}>`\n}\n\n/**\n * 根据 type 获取 toHtml 函数\n * @param type node.type\n */\nfunction getParser(type: string): ElemToHtmlFnType {\n  const fn = ELEM_TO_HTML_CONF[type]\n  return fn || defaultParser\n}\n\nfunction elemToHtml(elemNode: Element, editor: IDomEditor): string {\n  const { type = '', children = [] } = elemNode\n  const isVoid = Editor.isVoid(editor, elemNode)\n\n  // 计算 children html\n  let childrenHtml = ''\n  if (!isVoid) {\n    // 非 void node\n    childrenHtml = children.map(child => node2html(child, editor)).join('')\n  }\n\n  // 生成 html\n  const toHtmlFn = getParser(type)\n  const res = toHtmlFn(elemNode, childrenHtml, editor)\n\n  let elemHtml = ''\n  if (typeof res === 'string') elemHtml = res\n  else elemHtml = res.html || ''\n\n  // 添加样式（如 text-align line-height 等）\n  if (!isVoid) {\n    STYLE_TO_HTML_FN_LIST.forEach(fn => (elemHtml = fn(elemNode, elemHtml)))\n  }\n\n  // 直接返回 html 字符串\n  if (typeof res === 'string') return elemHtml\n\n  // 解析 prefix suffix （如 list-item）\n  const { prefix = '', suffix = '' } = res\n  if (prefix) elemHtml = prefix + elemHtml\n  if (suffix) elemHtml = elemHtml + suffix\n  return elemHtml\n}\n\nexport default elemToHtml\n"
  },
  {
    "path": "packages/core/src/to-html/index.ts",
    "content": "/**\n * @description to-html entry\n * @author wangfupeng\n */\n\nimport { Element as SlateElement, Descendant } from 'slate'\nimport { IDomEditor } from '../editor/interface'\n\n// ------------------------------------ style to html ------------------------------------\n\nexport type styleToHtmlFnType = (node: Descendant, elemHtml: string) => string\n\nexport const STYLE_TO_HTML_FN_LIST: styleToHtmlFnType[] = []\n\n/**\n * 注册 toHtml 处理文本样式的函数\n * @param fn 处理 toHtml 文本样式的函数\n */\nexport function registerStyleToHtmlHandler(fn: styleToHtmlFnType) {\n  STYLE_TO_HTML_FN_LIST.push(fn)\n}\n\n// ------------------------------------ elem node to html ------------------------------------\n\ninterface IElemToHtmlRes {\n  html: string\n  prefix?: string\n  suffix?: string\n}\n\nexport type ElemToHtmlFnType = (\n  elemNode: SlateElement,\n  childrenHtml: string,\n  editor?: IDomEditor\n) => string | IElemToHtmlRes\n\n// 注册 element->html 配置\nexport const ELEM_TO_HTML_CONF: {\n  [key: string]: ElemToHtmlFnType // key 要和 node.type 对应 ！！！\n} = {}\n\nexport interface IElemToHtmlConf {\n  type: string\n  elemToHtml: ElemToHtmlFnType\n}\n\n/**\n * 注册 elem to html 函数\n * @param conf { type, elemToHtml } ，type 即 node.type\n */\nexport function registerElemToHtmlConf(conf: IElemToHtmlConf) {\n  const { type, elemToHtml } = conf\n  const key = type || ''\n\n  // key 如果重复了，就后者覆盖前者\n  ELEM_TO_HTML_CONF[key] = elemToHtml\n}\n"
  },
  {
    "path": "packages/core/src/to-html/node2html.ts",
    "content": "/**\n * @description node -> html\n * @author wangfupeng\n */\n\nimport { Element, Descendant } from 'slate'\nimport { IDomEditor } from '../editor/interface'\nimport elemToHtml from './elem2html'\nimport textToHtml from './text2html'\n\nfunction node2html(node: Descendant, editor: IDomEditor): string {\n  if (Element.isElement(node)) {\n    // elem node\n    return elemToHtml(node, editor)\n  } else {\n    // text node\n    return textToHtml(node, editor)\n  }\n}\n\nexport default node2html\n"
  },
  {
    "path": "packages/core/src/to-html/text2html.ts",
    "content": "/**\n * @description text -> html\n * @author wangfupeng\n */\n\nimport { Text } from 'slate'\nimport { IDomEditor } from '../editor/interface'\nimport { DomEditor } from '../editor/dom-editor'\nimport { STYLE_TO_HTML_FN_LIST } from './index'\nimport { replaceHtmlSpecialSymbols } from '../utils/util'\n\nfunction textToHtml(textNode: Text, editor: IDomEditor): string {\n  const { text } = textNode\n  if (text == null) throw new Error(`Current node is not slate Text ${JSON.stringify(textNode)}`)\n  let textHtml = text\n\n  // 替换 html 特殊字符\n  textHtml = replaceHtmlSpecialSymbols(textHtml)\n\n  // 替换 \\n 为 <br> （一定要在替换特殊字符之后）\n  const parents = DomEditor.getParentsNodes(editor, textNode)\n  const hasPre = parents.some(p => DomEditor.getNodeType(p) === 'pre') // 上级节点中，是否存在 <pre>\n  // 在 <pre> 标签不替换，其他都替换\n  if (!hasPre) {\n    textHtml = textHtml.replace(/\\r\\n|\\r|\\n/g, '<br>')\n  }\n\n  // 在 <pre> 内部，&nbsp; 替换为空格\n  if (hasPre) {\n    textHtml = textHtml.replace(/&nbsp;/g, ' ')\n  }\n\n  // 处理空字符串\n  if (textHtml === '') {\n    const parentNode = DomEditor.getParentNode(null, textNode)\n    if (parentNode && parentNode.children.length === 0) {\n      // textNode 是唯一的子节点，则改为 <br>\n      textHtml = '<br>'\n    } else {\n      // 其他情况的 空字符串 ，直接返回\n      return textHtml\n    }\n  }\n\n  // 增加文本样式，如 color bgColor\n  STYLE_TO_HTML_FN_LIST.forEach(fn => (textHtml = fn(textNode, textHtml)))\n\n  return textHtml\n}\n\nexport default textToHtml\n"
  },
  {
    "path": "packages/core/src/upload/createUploader.ts",
    "content": "/**\n * @description gen uploader\n * @author wangfupeng\n */\n\nimport Uppy from '@uppy/core'\nimport XHRUpload from '@uppy/xhr-upload'\nimport { IUploadConfig } from './interface'\nimport { addQueryToUrl } from '../utils/util'\n\nfunction createUploader(config: IUploadConfig): Uppy {\n  // 获取配置\n  const {\n    server = '',\n    fieldName = '',\n    maxFileSize = 10 * 1024 * 1024, // 10M\n    maxNumberOfFiles = 100, // 最多多少个文件\n    meta = {},\n    metaWithUrl = false,\n    headers = {},\n    withCredentials = false,\n    timeout = 10 * 1000, // 10s\n    onBeforeUpload = files => files,\n    onSuccess = (file, res) => {\n      /* on success */\n    },\n    onError = (file, err, res?) => {\n      console.error(`${file.name} upload error`, err, res)\n    },\n    onProgress = progress => {\n      /* on progress */\n    },\n  } = config\n\n  // 判断配置项\n  if (!server) {\n    throw new Error('Cannot get upload server address\\n没有配置上传地址')\n  }\n  if (!fieldName) {\n    throw new Error('Cannot get fieldName\\n没有配置 fieldName')\n  }\n\n  // 是否要追加 url 参数\n  let url = server\n  if (metaWithUrl) {\n    url = addQueryToUrl(url, meta)\n  }\n\n  // 生成 uppy 实例，参考文档 https://uppy.io/docs/uppy/\n  const uppy = new Uppy({\n    onBeforeUpload,\n    restrictions: {\n      maxFileSize,\n      maxNumberOfFiles,\n    },\n    meta, // 自定义添加到 formData 中的参数\n  }).use(XHRUpload, {\n    endpoint: url, // 服务端 url\n    headers, // 自定义 headers\n    formData: true,\n    fieldName,\n    bundle: true,\n    withCredentials,\n    timeout,\n  })\n\n  // 各个 callback\n  uppy.on('upload-success', (file, response) => {\n    const { body = {} } = response\n    try {\n      // 有用户传入的第三方代码，得用 try catch 包裹\n      onSuccess(file, body)\n    } catch (err) {\n      console.error('wangEditor upload file - onSuccess error', err)\n    }\n    uppy.removeFile(file.id) // 清空文件\n  })\n\n  uppy.on('progress', progress => {\n    // progress 值范围： 0 - 100\n    if (progress < 1) return\n    onProgress(progress)\n  })\n\n  // uppy.on('error', error => {\n  //   console.error('wangEditor file upload error', error.stack)\n  // })\n\n  uppy.on('upload-error', (file, error, response) => {\n    try {\n      // 有用户传入的第三方代码，得用 try catch 包裹\n      onError(file, error, response)\n    } catch (err) {\n      console.error('wangEditor upload file - onError error', err)\n    }\n    uppy.removeFile(file.id) // 清空文件\n  })\n\n  uppy.on('restriction-failed', (file, error) => {\n    try {\n      // 有用户传入的第三方代码，得用 try catch 包裹\n      onError(file, error)\n    } catch (err) {\n      console.error('wangEditor upload file - onError error', err)\n    }\n    uppy.removeFile(file.id) // 清空文件\n  })\n\n  // 返回实例\n  return uppy\n}\n\nexport default createUploader\n"
  },
  {
    "path": "packages/core/src/upload/index.ts",
    "content": "/**\n * @description upload entry\n * @author wangfupeng\n */\n\nimport createUploader from './createUploader'\nimport { IUploadConfig } from './interface'\n\nexport { createUploader, IUploadConfig }\n\n// TODO upload 能力，写到文档中，二次开发使用\n"
  },
  {
    "path": "packages/core/src/upload/interface.ts",
    "content": "/**\n * @description upload interface\n * @author wangfupeng\n */\n\nimport { UppyFile } from '@uppy/core'\n\ntype FilesType = { [key: string]: UppyFile<{}, {}> }\n\n/**\n * 配置参考 https://uppy.io/docs/uppy/\n */\nexport interface IUploadConfig {\n  server: string\n  fieldName?: string\n  maxFileSize?: number\n  maxNumberOfFiles?: number\n  meta?: Record<string, unknown>\n  metaWithUrl: boolean\n  headers?:\n    | Headers\n    | ((file: UppyFile<Record<string, unknown>, Record<string, unknown>>) => Headers)\n    | undefined\n  withCredentials?: boolean\n  timeout?: number\n  onBeforeUpload?: (files: FilesType) => boolean | FilesType\n  onSuccess: (file: UppyFile<{}, {}>, response: any) => void\n  onProgress?: (progress: number) => void\n  onFailed: (file: UppyFile<{}, {}>, response: any) => void\n  onError: (file: UppyFile<{}, {}>, error: any, res: any) => void\n}\n"
  },
  {
    "path": "packages/core/src/utils/dom.ts",
    "content": "/**\n * @description DOM 操作 part1 - DOM7 文档 https://framework7.io/docs/dom7.html\n * @author wangfupeng\n */\n\nimport { htmlVoidElements } from 'html-void-elements'\nimport $, {\n  css,\n  append,\n  addClass,\n  removeClass,\n  hasClass,\n  on,\n  focus,\n  attr,\n  hide,\n  show,\n  // scrollTop,\n  // scrollLeft,\n  offset,\n  width,\n  height,\n  parent,\n  parents,\n  is,\n  dataset,\n  val,\n  text,\n  removeAttr,\n  children,\n  html,\n  remove,\n  find,\n  each,\n  empty,\n  Dom7Array,\n} from 'dom7'\nexport { Dom7Array } from 'dom7'\n\nif (css) $.fn.css = css\nif (append) $.fn.append = append\nif (addClass) $.fn.addClass = addClass\nif (removeClass) $.fn.removeClass = removeClass\nif (hasClass) $.fn.hasClass = hasClass\nif (on) $.fn.on = on\nif (focus) $.fn.focus = focus\nif (attr) $.fn.attr = attr\nif (removeAttr) $.fn.removeAttr = removeAttr\nif (hide) $.fn.hide = hide\nif (show) $.fn.show = show\n// if (scrollTop) $.fn.scrollTop = scrollTop\n// if (scrollLeft) $.fn.scrollLeft = scrollLeft\nif (offset) $.fn.offset = offset\nif (width) $.fn.width = width\nif (height) $.fn.height = height\nif (parent) $.fn.parent = parent\nif (parents) $.fn.parents = parents\nif (is) $.fn.is = is\nif (dataset) $.fn.dataset = dataset\nif (val) $.fn.val = val\nif (text) $.fn.text = text\nif (html) $.fn.html = html\nif (children) $.fn.children = children\nif (remove) $.fn.remove = remove\nif (find) $.fn.find = find\nif (each) $.fn.each = each\nif (empty) $.fn.empty = empty\n\nexport default $\n\n// ------------------------------- 分割线，以下内容参考 slate-react dom.ts -------------------------------\n\n// COMPAT: This is required to prevent TypeScript aliases from doing some very\n// weird things for Slate's types with the same name as globals. (2019/11/27)\n// https://github.com/microsoft/TypeScript/issues/35002\nimport DOMNode = globalThis.Node\nimport DOMComment = globalThis.Comment\nimport DOMElement = globalThis.Element\nimport DOMText = globalThis.Text\nimport DOMRange = globalThis.Range\nimport DOMSelection = globalThis.Selection\nimport DOMStaticRange = globalThis.StaticRange\nexport { DOMNode, DOMComment, DOMElement, DOMText, DOMRange, DOMSelection, DOMStaticRange }\n\nexport type DOMPoint = [Node, number]\n\n/**\n * Returns the host window of a DOM node\n */\nexport const getDefaultView = (value: any): Window | null => {\n  return (value && value.ownerDocument && value.ownerDocument.defaultView) || null\n}\n\n/**\n * Check if a DOM node is a comment node.\n */\nexport const isDOMComment = (value: any): value is DOMComment => {\n  return isDOMNode(value) && value.nodeType === 8\n}\n\n/**\n * Check if a DOM node is an element node.\n */\nexport const isDOMElement = (value: any): value is DOMElement => {\n  return isDOMNode(value) && value.nodeType === 1\n}\n\n/**\n * Check if a value is a DOM node.\n */\nexport const isDOMNode = (value: any): value is DOMNode => {\n  const window = getDefaultView(value)\n  return (\n    !!window &&\n    // @ts-ignore\n    value instanceof window.Node\n  )\n}\n\n/**\n * Check if a value is a DOM selection.\n */\nexport const isDOMSelection = (value: any): value is DOMSelection => {\n  const window = value && value.anchorNode && getDefaultView(value.anchorNode)\n  return !!window && value instanceof window.Selection\n}\n\n/**\n * Check if a DOM node is an element node.\n */\nexport const isDOMText = (value: any): value is DOMText => {\n  return isDOMNode(value) && value.nodeType === 3\n}\n\n/**\n * Checks whether a paste event is a plaintext-only event.\n */\nexport const isPlainTextOnlyPaste = (event: ClipboardEvent) => {\n  return (\n    event.clipboardData &&\n    event.clipboardData.getData('text/plain') !== '' &&\n    event.clipboardData.types.length === 1\n  )\n}\n\n/**\n * Normalize a DOM point so that it always refers to a text node.\n */\nexport const normalizeDOMPoint = (domPoint: DOMPoint): DOMPoint => {\n  let [node, offset] = domPoint\n\n  // If it's an element node, its offset refers to the index of its children\n  // including comment nodes, so try to find the right text child node.\n  if (isDOMElement(node) && node.childNodes.length) {\n    let isLast = offset === node.childNodes.length\n    let index = isLast ? offset - 1 : offset\n    ;[node, index] = getEditableChildAndIndex(node, index, isLast ? 'backward' : 'forward')\n\n    // If the editable child found is in front of input offset, we instead seek to its end\n    // 如果编辑区域的内容被发现在输入光标位置前面，也就是光标位置不正常，则修正光标的位置到结尾\n    isLast = index < offset\n\n    // If the node has children, traverse until we have a leaf node. Leaf nodes\n    // can be either text nodes, or other void DOM nodes.\n    while (isDOMElement(node) && node.childNodes.length) {\n      const i = isLast ? node.childNodes.length - 1 : 0\n      node = getEditableChild(node, i, isLast ? 'backward' : 'forward')\n    }\n\n    // Determine the new offset inside the text node.\n    offset = isLast && node.textContent != null ? node.textContent.length : 0\n  }\n\n  // Return the node and offset.\n  return [node, offset]\n}\n\n/**\n * Determines wether the active element is nested within a shadowRoot\n */\nexport const hasShadowRoot = () => {\n  return !!(window.document.activeElement && window.document.activeElement.shadowRoot)\n}\n\n/**\n * Get the element with the specified id\n */\nexport const getElementById = (id: string): null | HTMLElement => {\n  return (\n    window.document.getElementById(id) ??\n    (window.document.activeElement?.shadowRoot?.getElementById(id) || null)\n  )\n}\n\n/**\n * Get the nearest editable child and index at `index` in a `parent`, preferring `direction`.\n */\nexport const getEditableChildAndIndex = (\n  parent: DOMElement,\n  index: number,\n  direction: 'forward' | 'backward'\n): [DOMNode, number] => {\n  const { childNodes } = parent\n  let child = childNodes[index]\n  let i = index\n  let triedForward = false\n  let triedBackward = false\n\n  // While the child is a comment node, or an element node with no children,\n  // keep iterating to find a sibling non-void, non-comment node.\n  while (\n    isDOMComment(child) ||\n    (isDOMElement(child) && child.childNodes.length === 0) ||\n    (isDOMElement(child) && child.getAttribute('contenteditable') === 'false')\n  ) {\n    if (triedForward && triedBackward) {\n      break\n    }\n\n    if (i >= childNodes.length) {\n      triedForward = true\n      i = index - 1\n      direction = 'backward'\n      continue\n    }\n\n    if (i < 0) {\n      triedBackward = true\n      i = index + 1\n      direction = 'forward'\n      continue\n    }\n\n    child = childNodes[i]\n    index = i\n    i += direction === 'forward' ? 1 : -1\n  }\n\n  return [child, index]\n}\n\n/**\n * Get the nearest editable child at `index` in a `parent`, preferring\n * `direction`.\n */\n\nexport const getEditableChild = (\n  parent: DOMElement,\n  index: number,\n  direction: 'forward' | 'backward'\n): DOMNode => {\n  const [child] = getEditableChildAndIndex(parent, index, direction)\n  return child\n}\n\n/**\n * Get a plaintext representation of the content of a node, accounting for block\n * elements which get a newline appended.\n *\n * The domNode must be attached to the DOM.\n */\nexport const getPlainText = (domNode: DOMNode) => {\n  let text = ''\n\n  if (isDOMText(domNode) && domNode.nodeValue) {\n    return domNode.nodeValue\n  }\n\n  if (isDOMElement(domNode)) {\n    for (const childNode of Array.from(domNode.childNodes)) {\n      text += getPlainText(childNode)\n    }\n\n    const display = getComputedStyle(domNode).getPropertyValue('display')\n\n    if (\n      display === 'block' ||\n      display === 'list' ||\n      display === 'table-row' ||\n      domNode.tagName === 'BR'\n    ) {\n      text += '\\n'\n    }\n  }\n\n  return text\n}\n\n/**\n * 在下级节点中找到第一个 void elem\n * @param elem elem\n */\nexport function getFirstVoidChild(elem: DOMElement): DOMElement | null {\n  // 深度优先遍历\n  const stack: Array<DOMElement> = []\n  stack.push(elem)\n\n  let num = 0\n\n  // 开始遍历\n  while (stack.length > 0) {\n    const curElem = stack.pop()\n    if (curElem == null) break\n\n    num++\n    if (num > 10000) break\n\n    const { nodeName, nodeType } = curElem\n    if (nodeType === 1) {\n      const name = nodeName.toLowerCase()\n      if (\n        htmlVoidElements.includes(name) ||\n        // 补充一些\n        name === 'iframe' ||\n        name === 'video'\n      ) {\n        return curElem // 得到 void elem 并返回\n      }\n\n      // 继续遍历子节点\n      const children = curElem.children || []\n      const length = children.length\n      if (length) {\n        for (let i = length - 1; i >= 0; i--) {\n          // 注意，需要**逆序**追加自节点\n          stack.push(children[i])\n        }\n      }\n    }\n  }\n\n  // 未找到结果，返回 null\n  return null\n}\n\n/**\n * 遍历一个 elem 内所有的 text node ，执行函数\n * @param elem elem\n * @param handler handler\n */\nexport function walkTextNodes(\n  elem: DOMElement,\n  handler: (textNode: DOMNode, parent: DOMElement) => void\n) {\n  // void elem 内部的 text 不处理\n  if (elem instanceof HTMLElement && elem.dataset.slateVoid === 'true') return\n\n  for (let nodes = elem.childNodes, i = nodes.length; i--; ) {\n    const node = nodes[i]\n    const nodeType = node.nodeType\n    if (nodeType == 3) {\n      // 匹配到 text node ，执行函数\n      handler(node, elem)\n    } else if (nodeType == 1 || nodeType == 9 || nodeType == 11) {\n      // 继续遍历子节点\n      walkTextNodes(node as DOMElement, handler)\n    }\n  }\n}\n\nexport enum NodeType {\n  ELEMENT_NODE = 1,\n  TEXT_NODE = 3,\n  CDATA_SECTION_NODE = 4,\n  PROCESSING_INSTRUCTION_NODE = 7,\n  COMMENT_NODE = 8,\n  DOCUMENT_NODE = 9,\n  DOCUMENT_TYPE_NODE = 10,\n  DOCUMENT_FRAGMENT_NODE = 11,\n}\n\n/**\n * 获取 tagName lower-case\n * @param $elem $elem\n */\nexport function getTagName($elem: Dom7Array): string {\n  if ($elem.length === 0) return ''\n  const elem = $elem[0]\n  if (elem.nodeType !== NodeType.ELEMENT_NODE) return ''\n  return elem.tagName.toLowerCase()\n}\n"
  },
  {
    "path": "packages/core/src/utils/hotkeys.ts",
    "content": "/**\n * @description 快捷键\n * @author wangfupeng\n */\n\nimport { isKeyHotkey } from 'is-hotkey'\nimport { IS_APPLE } from './ua'\n\ninterface KEYS {\n  [key: string]: string | string[]\n}\n\n/**\n * Hotkey mappings for each platform.\n */\nconst HOTKEYS: KEYS = {\n  bold: 'mod+b',\n  compose: ['down', 'left', 'right', 'up', 'backspace', 'enter'],\n  moveBackward: 'left',\n  moveForward: 'right',\n  moveWordBackward: 'ctrl+left',\n  moveWordForward: 'ctrl+right',\n  deleteBackward: 'shift?+backspace',\n  deleteForward: 'shift?+delete',\n  extendBackward: 'shift+left',\n  extendForward: 'shift+right',\n  italic: 'mod+i',\n  splitBlock: 'shift?+enter',\n  undo: 'mod+z',\n  tab: 'tab',\n  selectAll: 'mod+a',\n}\n\nconst APPLE_HOTKEYS: KEYS = {\n  moveLineBackward: 'opt+up',\n  moveLineForward: 'opt+down',\n  moveWordBackward: 'opt+left',\n  moveWordForward: 'opt+right',\n  deleteBackward: ['ctrl+backspace', 'ctrl+h'],\n  deleteForward: ['ctrl+delete', 'ctrl+d'],\n  deleteLineBackward: 'cmd+shift?+backspace',\n  deleteLineForward: ['cmd+shift?+delete', 'ctrl+k'],\n  deleteWordBackward: 'opt+shift?+backspace',\n  deleteWordForward: 'opt+shift?+delete',\n  extendLineBackward: 'opt+shift+up',\n  extendLineForward: 'opt+shift+down',\n  redo: 'cmd+shift+z',\n  transposeCharacter: 'ctrl+t',\n}\n\nconst WINDOWS_HOTKEYS: KEYS = {\n  deleteWordBackward: 'ctrl+shift?+backspace',\n  deleteWordForward: 'ctrl+shift?+delete',\n  redo: ['ctrl+y', 'ctrl+shift+z'],\n}\n\n/**\n * Create a platform-aware hotkey checker.\n */\nconst create = (key: string) => {\n  const generic = HOTKEYS[key]\n  const apple = APPLE_HOTKEYS[key]\n  const windows = WINDOWS_HOTKEYS[key]\n  const isGeneric = generic && isKeyHotkey(generic)\n  const isApple = apple && isKeyHotkey(apple)\n  const isWindows = windows && isKeyHotkey(windows)\n\n  return (event: KeyboardEvent) => {\n    if (isGeneric && isGeneric(event)) return true\n    if (IS_APPLE && isApple && isApple(event)) return true\n    if (!IS_APPLE && isWindows && isWindows(event)) return true\n    return false\n  }\n}\n\n/**\n * Hotkeys.\n */\nexport default {\n  isBold: create('bold'),\n  isCompose: create('compose'),\n  isMoveBackward: create('moveBackward'),\n  isMoveForward: create('moveForward'),\n  isDeleteBackward: create('deleteBackward'),\n  isDeleteForward: create('deleteForward'),\n  isDeleteLineBackward: create('deleteLineBackward'),\n  isDeleteLineForward: create('deleteLineForward'),\n  isDeleteWordBackward: create('deleteWordBackward'),\n  isDeleteWordForward: create('deleteWordForward'),\n  isExtendBackward: create('extendBackward'),\n  isExtendForward: create('extendForward'),\n  isExtendLineBackward: create('extendLineBackward'),\n  isExtendLineForward: create('extendLineForward'),\n  isItalic: create('italic'),\n  isMoveLineBackward: create('moveLineBackward'),\n  isMoveLineForward: create('moveLineForward'),\n  isMoveWordBackward: create('moveWordBackward'),\n  isMoveWordForward: create('moveWordForward'),\n  isRedo: create('redo'),\n  isSplitBlock: create('splitBlock'),\n  isTransposeCharacter: create('transposeCharacter'),\n  isUndo: create('undo'),\n  isTab: create('tab'),\n  isSelectAll: create('selectAll'),\n}\n"
  },
  {
    "path": "packages/core/src/utils/key.ts",
    "content": "/**\n * An auto-incrementing identifier for keys.\n */\n\nlet n = 0\n\n/**\n * A class that keeps track of a key string. We use a full class here because we\n * want to be able to use them as keys in `WeakMap` objects.\n */\nexport class Key {\n  id: string\n\n  constructor() {\n    this.id = `${n++}`\n  }\n}\n"
  },
  {
    "path": "packages/core/src/utils/line.ts",
    "content": "/**\n * @description Utilities for single-line deletion\n */\n\nimport { Range, Editor } from 'slate'\nimport { IDomEditor } from '../editor/interface'\nimport { DomEditor } from '../editor/dom-editor'\n\nconst doRectsIntersect = (rect: DOMRect, compareRect: DOMRect) => {\n  const middle = (compareRect.top + compareRect.bottom) / 2\n  return rect.top <= middle && rect.bottom >= middle\n}\n\nconst areRangesSameLine = (editor: IDomEditor, range1: Range, range2: Range) => {\n  const rect1 = DomEditor.toDOMRange(editor, range1).getBoundingClientRect()\n  const rect2 = DomEditor.toDOMRange(editor, range2).getBoundingClientRect()\n  return doRectsIntersect(rect1, rect2) && doRectsIntersect(rect2, rect1)\n}\n\n/**\n * A helper utility that returns the end portion of a `Range`\n * which is located on a single line.\n *\n * @param {Editor} editor The editor object to compare against\n * @param {Range} parentRange The parent range to compare against\n * @returns {Range} A valid portion of the parentRange which is one a single line\n */\nexport const findCurrentLineRange = (editor: IDomEditor, parentRange: Range): Range => {\n  const parentRangeBoundary = Editor.range(editor, Range.end(parentRange))\n  const positions = Array.from(Editor.positions(editor, { at: parentRange }))\n\n  let left = 0\n  let right = positions.length\n  let middle = Math.floor(right / 2)\n\n  if (areRangesSameLine(editor, Editor.range(editor, positions[left]), parentRangeBoundary)) {\n    return Editor.range(editor, positions[left], parentRangeBoundary)\n  }\n\n  if (positions.length < 2) {\n    return Editor.range(editor, positions[positions.length - 1], parentRangeBoundary)\n  }\n\n  while (middle !== positions.length && middle !== left) {\n    if (areRangesSameLine(editor, Editor.range(editor, positions[middle]), parentRangeBoundary)) {\n      right = middle\n    } else {\n      left = middle\n    }\n\n    middle = Math.floor((left + right) / 2)\n  }\n\n  return Editor.range(editor, positions[right], parentRangeBoundary)\n}\n"
  },
  {
    "path": "packages/core/src/utils/ua.ts",
    "content": "/**\n * @description 通过 UA 判断浏览器\n * @author wangfupeng\n */\n\nexport const IS_IOS =\n  typeof globalThis.navigator !== 'undefined' &&\n  typeof globalThis.window !== 'undefined' &&\n  /iPad|iPhone|iPod/.test(navigator.userAgent) &&\n  !globalThis.window.MSStream\n\nexport const IS_APPLE = typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent)\n\nexport const IS_FIREFOX =\n  typeof navigator !== 'undefined' && /^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent)\n\nexport const IS_FIREFOX_LEGACY =\n  typeof navigator !== 'undefined' &&\n  /^(?!.*Seamonkey)(?=.*Firefox\\/(?:[0-7][0-9]|[0-8][0-6])(?:\\.)).*/i.test(navigator.userAgent)\n\nexport const IS_SAFARI =\n  typeof navigator !== 'undefined' && /Version\\/[\\d\\.]+.*Safari/.test(navigator.userAgent) // eslint-disable-line\n\n// \"modern\" Edge was released at 79.x\nexport const IS_EDGE_LEGACY =\n  typeof navigator !== 'undefined' &&\n  /Edge?\\/(?:[0-6][0-9]|[0-7][0-8])(?:\\.)/i.test(navigator.userAgent)\n\n// Native beforeInput events don't work well with react on Chrome 75 and older, Chrome 76+ can use beforeInput\nexport const IS_CHROME_LEGACY =\n  typeof navigator !== 'undefined' &&\n  /Chrome?\\/(?:[0-7][0-5]|[0-6][0-9])(?:\\.)/i.test(navigator.userAgent)\n\nexport const IS_CHROME = typeof navigator !== 'undefined' && /Chrome/i.test(navigator.userAgent)\n\n// qq browser\nexport const IS_QQBROWSER =\n  typeof navigator !== 'undefined' && /.*QQBrowser/.test(navigator.userAgent)\n\n// @ts-ignore 判断浏览器是否支持 beforeinput 事件 https://www.caniuse.com/?search=beforeinput\n// COMPAT: Firefox/Edge Legacy don't support the `beforeinput` event\n// Chrome Legacy doesn't support `beforeinput` correctly\nexport const HAS_BEFORE_INPUT_SUPPORT =\n  !IS_CHROME_LEGACY &&\n  !IS_EDGE_LEGACY &&\n  // globalThis is undefined in older browsers\n  typeof globalThis !== 'undefined' &&\n  globalThis.InputEvent &&\n  // @ts-ignore The `getTargetRanges` property isn't recognized.\n  typeof globalThis.InputEvent.prototype.getTargetRanges === 'function'\n"
  },
  {
    "path": "packages/core/src/utils/util.ts",
    "content": "/**\n * @description 工具函数\n * @author wangfupeng\n */\n\nimport forEach from 'lodash.foreach'\nimport { nanoid } from 'nanoid'\n\ntype PromiseCallback = (value: void) => void | PromiseLike<void>\n\n/**\n * 获取随机数字符串\n * @param prefix 前缀\n * @returns 随机数字符串\n */\nexport function genRandomStr(prefix: string = 'r'): string {\n  return `${prefix}-${nanoid()}`\n}\n\nexport function promiseResolveThen(fn: Function) {\n  Promise.resolve().then(fn as PromiseCallback)\n}\n\n/**\n * 追加 url query 参数\n * @param url url\n * @param data data\n */\nexport function addQueryToUrl(url: string, data: object): string {\n  let [urlWithoutHash, hash] = url.split('#')\n\n  // 拼接 query string\n  const queryArr: string[] = []\n  forEach(data, (val, key) => {\n    queryArr.push(`${key}=${val}`)\n  })\n  const queryStr = queryArr.join('&')\n\n  // 拼接到 url\n  if (urlWithoutHash.indexOf('?') > 0) {\n    // 已有 query\n    urlWithoutHash = `${urlWithoutHash}&${queryStr}`\n  } else {\n    // 没有 query\n    urlWithoutHash = `${urlWithoutHash}?${queryStr}`\n  }\n\n  // 返回拼接好的 url\n  if (hash) {\n    return `${urlWithoutHash}#${hash}`\n  } else {\n    return urlWithoutHash\n  }\n}\n\n/**\n * 替换 html 特殊字符，如 > 替换为 &gt;\n * @param str html str\n */\nexport function replaceHtmlSpecialSymbols(str: string) {\n  return (\n    str\n      /**\n       * 遇到两个空格时才替换，一个空格不替换\n       * 两个英文单词之间有一个空格，就不用替换，否则无法默认换行 issue #4403\n       */\n      .replace(/ {2}/g, ' &nbsp;')\n\n      .replace(/</g, '&lt;')\n      .replace(/>/g, '&gt;')\n      .replace(/®/g, '&reg;')\n      .replace(/©/g, '&copy;')\n      .replace(/™/g, '&trade;')\n  )\n}\n\n/**\n *【反转】替换 html 特殊字符，如 &gt; 替换为 >\n * @param str html str\n */\nexport function deReplaceHtmlSpecialSymbols(str: string) {\n  return str\n    .replace(/&nbsp;/g, ' ')\n    .replace(/&lt;/g, '<')\n    .replace(/&gt;/g, '>')\n    .replace(/&reg;/g, '®')\n    .replace(/&copy;/g, '©')\n    .replace(/&trade;/g, '™')\n    .replace(/&quot;/g, '\"')\n}\n"
  },
  {
    "path": "packages/core/src/utils/vdom.ts",
    "content": "/**\n * @description vdom 相关方法\n * @author wangfupeng\n */\n\nimport camelCase from 'lodash.camelcase'\nimport {\n  VNode,\n  init,\n  classModule,\n  propsModule,\n  styleModule,\n  datasetModule,\n  VNodeStyle,\n  Props,\n  Dataset,\n  eventListenersModule,\n  attributesModule,\n} from 'snabbdom'\n\nexport type PatchFn = (oldVnode: VNode | Element, vnode: VNode) => VNode\n\n/**\n * 创建 snabbdom patch\n * @returns snabbdom patch 函数\n */\nexport function genPatchFn(): PatchFn {\n  const patch = init([\n    // Init patch function with chosen modules\n    classModule, // makes it easy to toggle classes\n    propsModule, // for setting properties on DOM elements\n    styleModule, // handles styling on elements with support for animations\n    datasetModule,\n    eventListenersModule, // attaches event listeners\n    attributesModule,\n  ])\n  return patch\n}\n\n// vnode.data 保留属性，参考 snabbdom VNodeData\nconst DATA_PRESERVE_KEYS = ['props', 'attrs', 'style', 'dataset', 'on', 'hook']\n\n/**\n * 整理 vnode.data ，将暴露出来的零散属性（如 id className data-xxx）放在 data.props 或 data.dataset\n * @param vnode vnode\n */\nexport function normalizeVnodeData(vnode: VNode) {\n  const { data = {}, children = [] } = vnode\n  const dataKeys = Object.keys(data)\n  dataKeys.forEach((key: string) => {\n    const value = data[key]\n\n    // 赋值 key\n    if (key === 'key') {\n      vnode.key = value\n      return\n    }\n\n    // 忽略 data 保留属性\n    if (DATA_PRESERVE_KEYS.includes(key)) return\n\n    // dataset\n    if (key.startsWith('data-')) {\n      let datasetKey = key.slice(5) // 截取掉最前面的 'data-'\n      datasetKey = camelCase(datasetKey) // 转为驼峰写法\n\n      // 存储到 data.dataset\n      addVnodeDataset(vnode, { [datasetKey]: value })\n\n      delete data[key] // 删掉原有的属性\n      return\n    }\n\n    // 其他的，都算 props ，存储到 props\n    addVnodeProp(vnode, { [key]: value })\n\n    delete data[key] // 删掉原有的属性\n  })\n\n  // 遍历 children\n  if (children.length > 0) {\n    children.forEach(child => {\n      if (typeof child === 'string') return\n      normalizeVnodeData(child)\n    })\n  }\n}\n\n/**\n * 给 vnode 添加 prop\n * @param vnode vnode\n * @param newProp { key: val }\n */\nexport function addVnodeProp(vnode: VNode, newProp: Props) {\n  if (vnode.data == null) vnode.data = {}\n  const data = vnode.data\n  if (data.props == null) data.props = {}\n\n  Object.assign(data.props, newProp)\n}\n\n/**\n * 给 vnode 添加 dataset\n * @param vnode vnode\n * @param newDataset { key: val }\n */\nexport function addVnodeDataset(vnode: VNode, newDataset: Dataset) {\n  if (vnode.data == null) vnode.data = {}\n  const data = vnode.data\n  if (data.dataset == null) data.dataset = {}\n\n  Object.assign(data.dataset, newDataset)\n}\n\n/**\n * 给 vnode 添加样式\n * @param vnode vnode\n * @param newStyle { key: val }\n */\nexport function addVnodeStyle(vnode: VNode, newStyle: VNodeStyle) {\n  if (vnode.data == null) vnode.data = {}\n  const data = vnode.data\n  if (data.style == null) data.style = {}\n\n  Object.assign(data.style, newStyle)\n}\n"
  },
  {
    "path": "packages/core/src/utils/weak-maps.ts",
    "content": "/**\n * @description 对象关联关系（部分参考 slate-react weak-maps.ts）\n * @author wangfupeng\n */\n\nimport { Emitter } from 'event-emitter'\nimport { VNode } from 'snabbdom'\nimport { Node, Ancestor, Editor, Path, Range } from 'slate'\nimport { IDomEditor } from '../editor/interface'\nimport TextArea from '../text-area/TextArea'\nimport Toolbar from '../menus/bar/Toolbar'\nimport HoverBar from '../menus/bar/HoverBar'\nimport { IBarItem } from '../menus/bar-item/index'\nimport { Key } from './key'\nimport { PatchFn } from '../utils/vdom'\nimport { IEditorConfig } from '../config/interface'\nimport PanelAndModal from '../menus/panel-and-modal/BaseClass'\n\n// textarea - editor\nexport const EDITOR_TO_TEXTAREA = new WeakMap<IDomEditor, TextArea>()\nexport const TEXTAREA_TO_EDITOR = new WeakMap<TextArea, IDomEditor>()\n\n// bar - editor\nexport const TOOLBAR_TO_EDITOR = new WeakMap<Toolbar, IDomEditor>()\nexport const EDITOR_TO_TOOLBAR = new WeakMap<IDomEditor, Toolbar>()\nexport const HOVER_BAR_TO_EDITOR = new WeakMap<HoverBar, IDomEditor>()\nexport const EDITOR_TO_HOVER_BAR = new WeakMap<IDomEditor, HoverBar>()\nexport const BAR_ITEM_TO_EDITOR = new WeakMap<IBarItem, IDomEditor>()\nexport const EDITOR_TO_PANEL_AND_MODAL = new WeakMap<IDomEditor, Set<PanelAndModal>>()\nexport const PANEL_OR_MODAL_TO_EDITOR = new WeakMap<PanelAndModal, IDomEditor>()\n\n// config\nexport const EDITOR_TO_CONFIG = new WeakMap<IDomEditor, IEditorConfig>()\n\n// vdom 相关的属性\nexport const IS_FIRST_PATCH = new WeakMap<TextArea, boolean>()\nexport const TEXTAREA_TO_PATCH_FN = new WeakMap<TextArea, PatchFn>()\nexport const TEXTAREA_TO_VNODE = new WeakMap<TextArea, VNode>()\n\n/**\n * Two weak maps that allow us rebuild a path given a node. They are populated\n * at render time such that after a render occurs we can always backtrack.\n */\nexport const NODE_TO_INDEX: WeakMap<Node, number> = new WeakMap()\nexport const NODE_TO_PARENT: WeakMap<Node, Ancestor> = new WeakMap()\n\n/**\n * Weak maps that allow us to go between Slate nodes and DOM nodes. These\n * are used to resolve DOM event-related logic into Slate actions.\n */\nexport const EDITOR_TO_ELEMENT: WeakMap<Editor, HTMLElement> = new WeakMap()\nexport const EDITOR_TO_PLACEHOLDER: WeakMap<Editor, string> = new WeakMap()\nexport const ELEMENT_TO_NODE: WeakMap<HTMLElement, Node> = new WeakMap()\nexport const KEY_TO_ELEMENT: WeakMap<Key, HTMLElement> = new WeakMap()\nexport const NODE_TO_ELEMENT: WeakMap<Node, HTMLElement> = new WeakMap()\nexport const NODE_TO_KEY: WeakMap<Node, Key> = new WeakMap()\nexport const EDITOR_TO_WINDOW: WeakMap<Editor, Window> = new WeakMap()\n\n/**\n * Weak maps for storing editor-related state.\n */\nexport const IS_FOCUSED: WeakMap<Editor, boolean> = new WeakMap()\nexport const IS_DRAGGING: WeakMap<Editor, boolean> = new WeakMap()\nexport const IS_CLICKING: WeakMap<Editor, boolean> = new WeakMap()\n\n// /**\n//  * Weak map for associating the context `onChange` context with the plugin.\n//  */\n// export const EDITOR_TO_ON_CHANGE = new WeakMap<Editor, () => void>()\n\n// 正在更新，但尚未更新完的节点 path ，临时记录下\n// 例如，table 插入 col ，需要一行一行的插入，在更新期间，不能收到其他的（如 normalize）干扰\nexport const CHANGING_NODE_PATH: WeakMap<Editor, Path> = new WeakMap()\n\n// 保存 editor -> selection ，用于还原 editor 选区\nexport const EDITOR_TO_SELECTION: WeakMap<Editor, Range> = new WeakMap()\n\n// editor -> eventEmitter 自定义事件\nexport const EDITOR_TO_EMITTER: WeakMap<Editor, Emitter> = new WeakMap()\n\n// editor 是否可执行粘贴\nexport const EDITOR_TO_CAN_PASTE: WeakMap<Editor, boolean> = new WeakMap()\n"
  },
  {
    "path": "packages/core/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {},\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\n    \"./src/**/*\",\n    \"../custom-types.d.ts\"\n  ]\n}"
  },
  {
    "path": "packages/custom-types.d.ts",
    "content": "/**\n * @description 自定义扩展 slate 接口属性\n * @author wangfupeng\n */\nimport { StyledText } from './basic-modules/src/modules/text-style/custom-types'\nimport { ColorText } from './basic-modules/src/modules/color/custom-types'\nimport { FontSizeAndFamilyText } from './basic-modules/src/modules/font-size-family/custom-types'\nimport { LineHeightElement } from './basic-modules/src/modules/line-height/custom-types'\nimport { JustifyElement } from './basic-modules/src/modules/justify/custom-types'\nimport { IndentElement } from './basic-modules/src/modules/indent/custom-types'\nimport { ParagraphElement } from './basic-modules/src/modules/paragraph/custom-types'\nimport { LinkElement } from './basic-modules/src/modules/link/custom-types'\nimport { BlockQuoteElement } from './basic-modules/src/modules/blockquote/custom-types'\nimport {\n  Header1Element,\n  Header2Element,\n  Header3Element,\n  Header4Element,\n  Header5Element,\n} from './basic-modules/src/modules/header/custom-types'\nimport { DividerElement } from './basic-modules/src/modules/divider/custom-types'\nimport { ImageElement } from './basic-modules/src/modules/image/custom-types'\nimport { TodoElement } from './basic-modules/src/modules/todo/custom-types'\nimport { PreElement, CodeElement } from './basic-modules/src/modules/code-block/custom-types'\nimport { VideoElement } from './video-module/src/module/custom-types'\nimport {\n  TableCellElement,\n  TableRowElement,\n  TableElement,\n} from './table-module/src/module/custom-types'\nimport { ListItemElement } from './list-module/src/module/custom-types'\n\ntype PureText = {\n  text: string\n}\n\ntype CustomText = PureText | StyledText | FontSizeAndFamilyText | ColorText\n\ntype BaseElement = {\n  type: string\n  children: Array<CustomElement | CustomText>\n}\n\ntype CustomElement =\n  | BaseElement\n  | LineHeightElement\n  | JustifyElement\n  | IndentElement\n  | ParagraphElement\n  | LinkElement\n  | BlockQuoteElement\n  | Header1Element\n  | Header2Element\n  | Header3Element\n  | Header4Element\n  | Header5Element\n  | DividerElement\n  | ImageElement\n  | TodoElement\n  | PreElement\n  | CodeElement\n  | VideoElement\n  | TableCellElement\n  | TableRowElement\n  | TableElement\n  | ListItemElement\n\ndeclare module 'slate' {\n  interface CustomTypes {\n    // 扩展 Text\n    Text: CustomText\n\n    // 扩展 Element\n    Element: CustomElement\n  }\n}\n"
  },
  {
    "path": "packages/editor/CHANGELOG.md",
    "content": "# Change Log\n\nAll notable changes to this project will be documented in this file.\nSee [Conventional Commits](https://conventionalcommits.org) for commit guidelines.\n\n## [5.1.23](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.22...@wangeditor/editor@5.1.23) (2022-11-14)\n\n**Note:** Version bump only for package @wangeditor/editor\n\n\n\n\n\n## [5.1.22](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.21...@wangeditor/editor@5.1.22) (2022-10-18)\n\n**Note:** Version bump only for package @wangeditor/editor\n\n\n\n\n\n## [5.1.21](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.20...@wangeditor/editor@5.1.21) (2022-10-04)\n\n**Note:** Version bump only for package @wangeditor/editor\n\n\n\n\n\n## [5.1.20](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.19...@wangeditor/editor@5.1.20) (2022-09-27)\n\n**Note:** Version bump only for package @wangeditor/editor\n\n\n\n\n\n## [5.1.19](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.18...@wangeditor/editor@5.1.19) (2022-09-27)\n\n**Note:** Version bump only for package @wangeditor/editor\n\n\n\n\n\n## [5.1.18](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.17...@wangeditor/editor@5.1.18) (2022-09-16)\n\n**Note:** Version bump only for package @wangeditor/editor\n\n\n\n\n\n## [5.1.17](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.16...@wangeditor/editor@5.1.17) (2022-09-15)\n\n\n### Bug Fixes\n\n* customInsert 不触发 onSuccess ([d6f4a1b](https://github.com/wangeditor-team/wangEditor/commit/d6f4a1b1494864b116a1310cce2d9e8632c92c6f))\n* focus table 时 isFocused 异常 ([5c52bf3](https://github.com/wangeditor-team/wangEditor/commit/5c52bf33e91b1a4677e7bbc04c5d80698abfeeab))\n* 上传视频 - customBrowseAndUpload 缺少 poster ([c24627a](https://github.com/wangeditor-team/wangEditor/commit/c24627aaa4c173c5d435e3077dfe8f6b4a9a87b1))\n\n\n\n\n\n## [5.1.16](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.15...@wangeditor/editor@5.1.16) (2022-09-14)\n\n**Note:** Version bump only for package @wangeditor/editor\n\n\n\n\n\n## [5.1.15](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.14...@wangeditor/editor@5.1.15) (2022-08-30)\n\n\n### Bug Fixes\n\n* checkVideo 增加 poster 参数 ([c0402e1](https://github.com/wangeditor-team/wangEditor/commit/c0402e155470233d256e037d863dab74c026b7f6))\n\n\n\n\n\n## [5.1.14](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.13...@wangeditor/editor@5.1.14) (2022-07-27)\n\n**Note:** Version bump only for package @wangeditor/editor\n\n\n\n\n\n## [5.1.13](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.12...@wangeditor/editor@5.1.13) (2022-07-27)\n\n\n### Bug Fixes\n\n* setHtml 支持空字符串 ([d438157](https://github.com/wangeditor-team/wangEditor/commit/d43815766320d9cb0548bae0415c54ce7b147efb))\n* upload file callback error ([bf20e07](https://github.com/wangeditor-team/wangEditor/commit/bf20e07f12ed242b0ab4bb2290d876153a822972))\n\n\n\n\n\n## [5.1.12](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.11...@wangeditor/editor@5.1.12) (2022-07-22)\n\n**Note:** Version bump only for package @wangeditor/editor\n\n\n\n\n\n## [5.1.11](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.10...@wangeditor/editor@5.1.11) (2022-07-18)\n\n**Note:** Version bump only for package @wangeditor/editor\n\n\n\n\n\n## [5.1.10](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.9...@wangeditor/editor@5.1.10) (2022-07-16)\n\n**Note:** Version bump only for package @wangeditor/editor\n\n\n\n\n\n## [5.1.9](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.8...@wangeditor/editor@5.1.9) (2022-07-14)\n\n**Note:** Version bump only for package @wangeditor/editor\n\n\n\n\n\n## [5.1.8](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.7...@wangeditor/editor@5.1.8) (2022-07-14)\n\n**Note:** Version bump only for package @wangeditor/editor\n\n\n\n\n\n## [5.1.7](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.6...@wangeditor/editor@5.1.7) (2022-07-13)\n\n**Note:** Version bump only for package @wangeditor/editor\n\n\n\n\n\n## [5.1.6](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.5...@wangeditor/editor@5.1.6) (2022-07-12)\n\n**Note:** Version bump only for package @wangeditor/editor\n\n\n\n\n\n## [5.1.5](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.3...@wangeditor/editor@5.1.5) (2022-07-11)\n\n\n### Bug Fixes\n\n* 尝试修复 nuxt 报错 issue[#4409](https://github.com/wangeditor-team/wangEditor/issues/4409) ([912f888](https://github.com/wangeditor-team/wangEditor/commit/912f8889a11d962b3ac2c65cb5835f4e8c58c372))\n\n\n\n\n\n## [5.1.4](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.3...@wangeditor/editor@5.1.4) (2022-07-11)\n\n\n### Bug Fixes\n\n* 尝试修复 nuxt 报错 issue[#4409](https://github.com/wangeditor-team/wangEditor/issues/4409) ([912f888](https://github.com/wangeditor-team/wangEditor/commit/912f8889a11d962b3ac2c65cb5835f4e8c58c372))\n\n\n\n\n\n## [5.1.3](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.2...@wangeditor/editor@5.1.3) (2022-07-11)\n\n\n### Bug Fixes\n\n* scroll 滚动问题 ([bc133e1](https://github.com/wangeditor-team/wangEditor/commit/bc133e1e4ca89ab5042cbc0971578ad144499805))\n\n\n\n\n\n## [5.1.2](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.1...@wangeditor/editor@5.1.2) (2022-07-11)\n\n**Note:** Version bump only for package @wangeditor/editor\n\n\n\n\n\n## [5.1.1](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.1.0...@wangeditor/editor@5.1.1) (2022-06-02)\n\n**Note:** Version bump only for package @wangeditor/editor\n\n\n\n\n\n# [5.1.0](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/editor@5.0.1...@wangeditor/editor@5.1.0) (2022-05-25)\n\n\n### Features\n\n* editVideoSize ([375eecb](https://github.com/wangeditor-team/wangEditor/commit/375eecba826eac681268c55c47bcd922f7157d63))\n* enter menu ([988fc31](https://github.com/wangeditor-team/wangEditor/commit/988fc31f31de3d37dffbf54abb784cceb8e6118d))\n* setHtml ([f4f91b8](https://github.com/wangeditor-team/wangEditor/commit/f4f91b883298091e3679ca6b206ae0d796003772))\n\n\n\n\n\n## 5.0.1 (2022-04-18)\n\n\n### Bug Fixes\n\n* 不支持 IE 浏览器的提醒 ([70c5cae](https://github.com/wangeditor-team/wangEditor/commit/70c5caefd8f6f663225b7a0b796a035d274ef4e1))\n* 打包问题 ([c4e87cc](https://github.com/wangeditor-team/wangEditor/commit/c4e87ccac82bcf90d20b7304aff83745e52fb1b1))\n* 更新各包之间依赖版本 ([75c552c](https://github.com/wangeditor-team/wangEditor/commit/75c552cc8ed54765bebb86a7ec5329a7fc79e85f))\n* 兼容 AggregateError ([0cbd82d](https://github.com/wangeditor-team/wangEditor/commit/0cbd82d30d350b2313f6373e2b5f6d168e47e1bc))\n* 兼容next.js及nuxt.js ([233728e](https://github.com/wangeditor-team/wangEditor/commit/233728eb984f541437c62a1390fa0542b2cc6227))\n* 开放几个第三方用的 API ([bdf3e70](https://github.com/wangeditor-team/wangEditor/commit/bdf3e70c52bac71e2056e21237fe4ac9e2b0818f))\n* 拼音隐藏 placeholder ([aec1a9f](https://github.com/wangeditor-team/wangEditor/commit/aec1a9f62af8944b7894beeca953076ec73545d5))\n* 上传图片 - base64 仍触发上传 + 超出 maxSize 的报错提醒 ([a1d469a](https://github.com/wangeditor-team/wangEditor/commit/a1d469accb7f87f8ea0282a1699d002aaaa4e79a))\n* 添加QQ浏览器polyfill ([a1b476a](https://github.com/wangeditor-team/wangEditor/commit/a1b476a0bed52315f3e398c586d73f85996f9431))\n* 图片上传，提示 ([3754012](https://github.com/wangeditor-team/wangEditor/commit/37540129dff1212c5ebfd4ca3f4d4e8def735e73))\n* 修复 node 环境下报错问题 ([5a635a5](https://github.com/wangeditor-team/wangEditor/commit/5a635a5e8fac942ee214dd22b097e09057abc69c))\n* 修复取消链接后撤销再重做报错的问题 ([9b233a9](https://github.com/wangeditor-team/wangEditor/commit/9b233a92c95571235248623a6ca5212eb4237f2a))\n* 移除了每个包下的 publishConfig directory 配置 ([16559f0](https://github.com/wangeditor-team/wangEditor/commit/16559f052545c111318be760e64291a521bdcc65))\n* 优化选中代码块不应该展示 hoverbar 的交互 ([33dcbd6](https://github.com/wangeditor-team/wangEditor/commit/33dcbd6560dccfbe77e18cfbce8c9f077f19f6cd))\n* delete divider ([f04cbd6](https://github.com/wangeditor-team/wangEditor/commit/f04cbd6009099629e3cd41be19d20b6788fe7f28))\n* divider - 键盘删除 ([31db059](https://github.com/wangeditor-team/wangEditor/commit/31db0593dbc77fba9b4a719bc0f48f1223afd680))\n* example/code-hightlight ([7885988](https://github.com/wangeditor-team/wangEditor/commit/78859884cefc18d15ce2f87507380a78c2ad65e5))\n* globalThis 兼容性 ([7a47f4b](https://github.com/wangeditor-team/wangEditor/commit/7a47f4b904815516d3b5749ab652ff80478411bc))\n* group-menu 考虑 excludeKeys ([ecc29f3](https://github.com/wangeditor-team/wangEditor/commit/ecc29f3b24992c8dc0adf006d81b0d4a252683c5))\n* hoverbar config - 同时选中文字和 table ([8f6b4d1](https://github.com/wangeditor-team/wangEditor/commit/8f6b4d1a20e3b1b75da69b20bd5893ce08a27185))\n* hoverbarKeys - text ([59b4840](https://github.com/wangeditor-team/wangEditor/commit/59b48406b4c373ef029a5f5bdb0d15d925a91a0f))\n* html 特殊字符 ([b3eb81b](https://github.com/wangeditor-team/wangEditor/commit/b3eb81bc9c4aa15c2ff7451c173de15d6c4552bc))\n* i18n - 获取多语言配置 ([9f81597](https://github.com/wangeditor-team/wangEditor/commit/9f815970f8c3c6dddb6bf846ecb672325e80444b))\n* i18n 切换语言 ([b3b4642](https://github.com/wangeditor-team/wangEditor/commit/b3b4642c6e72ab0b13b05657745abb87e71c633d))\n* insertKeys ([0a89420](https://github.com/wangeditor-team/wangEditor/commit/0a8942050bd0b39afb5bbc55ca7842461a5b98eb))\n* link, text hoverbar 选区问题 ([e0b7438](https://github.com/wangeditor-team/wangEditor/commit/e0b7438c89a347f1b0b940d9c11150b72d595529))\n* menu 点击多次才能生效 ([6497e39](https://github.com/wangeditor-team/wangEditor/commit/6497e39225a993c4d87f9ffddf20086446a4fbc2))\n* normalize when create editor ([2b51962](https://github.com/wangeditor-team/wangEditor/commit/2b5196244a93ad7beb316bfa42e557221967d063))\n* parse html - v4 video ([8dca822](https://github.com/wangeditor-team/wangEditor/commit/8dca822f9f1b52fd71dd6e17f0954d6aa016324b))\n* qq 浏览器报错 ([8a09ed5](https://github.com/wangeditor-team/wangEditor/commit/8a09ed5d810fc1e2c4d0c529aa1269ed0c06425e))\n* readOnly 时菜单还可操作 ([0d4a29b](https://github.com/wangeditor-team/wangEditor/commit/0d4a29bb5ba8b62ac11a09d3f814abcb1fcf46be))\n* registerModule ([189981c](https://github.com/wangeditor-team/wangEditor/commit/189981c73db07d5b15ee4c46b1639f76f6f63ba1))\n* rename es module filename ([1821d4e](https://github.com/wangeditor-team/wangEditor/commit/1821d4eef49e64efcb41b848849ca7a5e6472044))\n* shadow dom 样式缺失 ([2fcb69c](https://github.com/wangeditor-team/wangEditor/commit/2fcb69c866266cc5b0265cff031ae9279d368b84))\n* style-to-html - 输入 a 会删除外部的 <a> 标签 ([af1f523](https://github.com/wangeditor-team/wangEditor/commit/af1f523983f2bc4b7eaf9726d4b8a35227ab27dc))\n* table - elemToHtml ([e36e609](https://github.com/wangeditor-team/wangEditor/commit/e36e6092ef721723169afc8bf0560a47ac9f4dfc))\n* tableCell 中 br 报错 ([8604db7](https://github.com/wangeditor-team/wangEditor/commit/8604db751b622c01fa5391af59328236cf13effc))\n* text hoverbar ([c7de4f8](https://github.com/wangeditor-team/wangEditor/commit/c7de4f815d6f5b9e009a3149ed042052576c424e))\n* text hoverbar ([efe9a34](https://github.com/wangeditor-team/wangEditor/commit/efe9a34d85f8baaeced27543a7bcd508b50f6bca))\n* video - 键盘删除 ([5a6bedd](https://github.com/wangeditor-team/wangEditor/commit/5a6bedd80fa0d758270731f62115637ad7f313d0))\n\n\n### Features\n\n* 两端对齐 ([e5080d3](https://github.com/wangeditor-team/wangEditor/commit/e5080d3dd102f7a951d8e1f370db834778ecbdfa))\n* 上标 下标 ([40dab08](https://github.com/wangeditor-team/wangEditor/commit/40dab085a061ea3e838f0cfa86260c6c6f894c69))\n* 上传图片 metaWithUrl ([2485157](https://github.com/wangeditor-team/wangEditor/commit/24851576a1dcc07b1a8931d17a147c3640222e85))\n* 增加 enable disable API（删除 setConfig setMenuConfig API） ([984fc50](https://github.com/wangeditor-team/wangEditor/commit/984fc50520061fc34ea08f4136bdeb93dee46564))\n* 支持 nodejs 环境 ([484f18c](https://github.com/wangeditor-team/wangEditor/commit/484f18c3abc70d19e51c556f48491c18d390b1e1))\n* basic text style module ([005b343](https://github.com/wangeditor-team/wangEditor/commit/005b343573ba98f2d0b8480d034ff6807a499aa3))\n* block quote ([c3c87a5](https://github.com/wangeditor-team/wangEditor/commit/c3c87a5c09b311eb14c799df94fc4826aa3f4262))\n* bold & header ([8130c23](https://github.com/wangeditor-team/wangEditor/commit/8130c23ad84485a68cf9ca4b53d52fab1cec4e96))\n* clearStyle menu ([8002f70](https://github.com/wangeditor-team/wangEditor/commit/8002f707ed04b914180ec36fdca0edf48c815e01))\n* code highlight ([42b2f8d](https://github.com/wangeditor-team/wangEditor/commit/42b2f8d192e2433593c11ad0b8424737f6cffb58))\n* code-block - part ([a8bcd63](https://github.com/wangeditor-team/wangEditor/commit/a8bcd63d882832ac05a32878df0f767d145e0fa7))\n* create editor ([12d98e4](https://github.com/wangeditor-team/wangEditor/commit/12d98e4bee179e9d277ec3ec2ecb827962ed0e75))\n* create mode ([63c2eef](https://github.com/wangeditor-team/wangEditor/commit/63c2eef9a9a0a2838dfadd23483de35a76f88b0b))\n* customPaste ([0f25f5c](https://github.com/wangeditor-team/wangEditor/commit/0f25f5cae3a2cd5ae5832f3fc1026b3ab6d047e0))\n* divider menu ([5262634](https://github.com/wangeditor-team/wangEditor/commit/526263445616725541bf374b80260e73b1d4c6ec))\n* drag resize image ([cd72028](https://github.com/wangeditor-team/wangEditor/commit/cd72028f1786e2e53079ad5cbef1b8569731ca79))\n* editor 生命周期，自定义事件 ([00e9bc2](https://github.com/wangeditor-team/wangEditor/commit/00e9bc2cfcb8b622764db1c76394491d72ffd93e))\n* editor with-selection plugin ([9f0a39f](https://github.com/wangeditor-team/wangEditor/commit/9f0a39fecf6d92888d2a97929820d3be038efb31))\n* editor.isSelectedAll ([960c845](https://github.com/wangeditor-team/wangEditor/commit/960c8455f85a6bc7350f9944be80b3997bc1fea1))\n* editor.showProgressBar ([51761d4](https://github.com/wangeditor-team/wangEditor/commit/51761d466ab3ef7c99e872954d4724ab51d8e28c))\n* emotion ([736f955](https://github.com/wangeditor-team/wangEditor/commit/736f955211287bafca2375de3c8193cd0aa0856f))\n* font-size + font-family ([cc649e0](https://github.com/wangeditor-team/wangEditor/commit/cc649e0918ce58e78b4d5ee49a400197b9d04b70))\n* fullScreen ([e7ccd88](https://github.com/wangeditor-team/wangEditor/commit/e7ccd88a7dd58f64b7bd484de428e3a76cc994f7))\n* getElemsByTypePrefix （删掉 getHeaders） ([c18834b](https://github.com/wangeditor-team/wangEditor/commit/c18834b3ebfd97fb36ccbe0faa84e6fe8c30eb67))\n* header button menu ([6413135](https://github.com/wangeditor-team/wangEditor/commit/64131354d54705e11fd6992fcf5a4389371c3560))\n* hover bar ([107356e](https://github.com/wangeditor-team/wangEditor/commit/107356eff7bfaf53ce25e39244f8133c80518375))\n* i18n ([c11b244](https://github.com/wangeditor-team/wangEditor/commit/c11b2440f91b99d40bca18b675c66a22b6e160c9))\n* image menu - width 50% 100% ([f9b4c68](https://github.com/wangeditor-team/wangEditor/commit/f9b4c68dff3232b50491b07949c20eb4c18baa6b))\n* image menus & position ([bf5beba](https://github.com/wangeditor-team/wangEditor/commit/bf5beba7b3014d63f0b9fe0063530c8b101a5011))\n* indent menu + groupMenu ([08db901](https://github.com/wangeditor-team/wangEditor/commit/08db901cd3a3f2ddb2173cc4b36d471e4e68237e))\n* insert link ([b04242f](https://github.com/wangeditor-team/wangEditor/commit/b04242ffa252d4088f5360c3de45c24d6f493552))\n* justify ([2ed7b88](https://github.com/wangeditor-team/wangEditor/commit/2ed7b883ca759dc4a9e0eefbd23cfe603a0f46fd))\n* line-height menu ([755a752](https://github.com/wangeditor-team/wangEditor/commit/755a752d76803423f2794b85004d75055c9b54ec))\n* list menu ([fe6c083](https://github.com/wangeditor-team/wangEditor/commit/fe6c0830b2c43e335e5972f85096f490694bbe19))\n* menu color - part ([3a6cc86](https://github.com/wangeditor-team/wangEditor/commit/3a6cc86a7f9133d0862310c408abafb30c531734))\n* menu color & dropPanel & menu config ([5d0d41b](https://github.com/wangeditor-team/wangEditor/commit/5d0d41b9a765a7deb583393f129925414c36ef35))\n* modal appendTo body ([fc0ab06](https://github.com/wangeditor-team/wangEditor/commit/fc0ab06d5c7177eceb04643234a8c301ca4de396))\n* parse html ([2a5eace](https://github.com/wangeditor-team/wangEditor/commit/2a5eace00f33cded50b68e8164748ec2480213fd))\n* parse src (link image video) ([715a841](https://github.com/wangeditor-team/wangEditor/commit/715a841fc6c730ee2b448a1799a07ce778128aad))\n* selectList ([b7366ab](https://github.com/wangeditor-team/wangEditor/commit/b7366ab2dafd379145d85881052d6f400bd13c85))\n* shadow dom example ([c55f38d](https://github.com/wangeditor-team/wangEditor/commit/c55f38dab7886b9115c4353118d1f182885878ae))\n* table module ([a397116](https://github.com/wangeditor-team/wangEditor/commit/a397116de73e088232d9c41828f30f8d56a22dd4))\n* table module - header + fullWidth ([9a8a0e0](https://github.com/wangeditor-team/wangEditor/commit/9a8a0e093af944ee7deab674f47c2ec7baae0e63))\n* text and toolbar ([3ae5d0c](https://github.com/wangeditor-team/wangEditor/commit/3ae5d0c4138fec7397ac8629e0012affe6b7dfa4))\n* todo ([9608fef](https://github.com/wangeditor-team/wangEditor/commit/9608fef2ff86368cdcbb950a74af1246a58709de))\n* toHtml 机制 ([1c4d872](https://github.com/wangeditor-team/wangEditor/commit/1c4d8729f84aaab6a448f23064b34a20596305e9))\n* toolbar config - insertKeys ([a2f3c4b](https://github.com/wangeditor-team/wangEditor/commit/a2f3c4be3762831723495bbc9d50eb6c9b05d195))\n* toolbar excludeKeys ([09bd196](https://github.com/wangeditor-team/wangEditor/commit/09bd196ea24c19b04e5e7e38227ca94332847bf8))\n* tooltip ([994d875](https://github.com/wangeditor-team/wangEditor/commit/994d875fee81cf01271c2e440c1df202aa067d0e))\n* undo redo - menu ([bfb3014](https://github.com/wangeditor-team/wangEditor/commit/bfb3014791cfcb2d7897f916bf280a57b1906e4d))\n* updateLink + unLink + viewLink ([254d554](https://github.com/wangeditor-team/wangEditor/commit/254d55466b3c8527dd9f0bf34681abd801c8c8ce))\n* upload image ([0a0564b](https://github.com/wangeditor-team/wangEditor/commit/0a0564bf14edd4dea6eb958e653272a9a216cec1))\n* upload video ([ac8e6f8](https://github.com/wangeditor-team/wangEditor/commit/ac8e6f8b5258e593714676a6f6be359ba525833c))\n* video menu ([c1faa1c](https://github.com/wangeditor-team/wangEditor/commit/c1faa1cfa896e1d240f5a2a100e1fd9b89dbef0b))\n"
  },
  {
    "path": "packages/editor/README-en.md",
    "content": "# wangEditor editor\n\n[中文](./README.md)\n\nOpen source web rich text editor, run right out of the box. Support JS Vue React.\n\n- [Document](https://www.wangeditor.com/en/)\n- [Demo](https://www.wangeditor.com/demo/?lang=en)\n\n![](../../docs/images/editor-en.png)\n\nYou can [commit an issue]((https://github.com/wangeditor-team/wangEditor/issues)) if you have any question.\n"
  },
  {
    "path": "packages/editor/README.md",
    "content": "# wangEditor editor\n\n[English](./README-en.md)\n\n开源 Web 富文本编辑器，开箱即用，配置简单。支持 JS Vue React 。\n\n- [文档](https://www.wangeditor.com/)\n- [demo](https://www.wangeditor.com/demo/)\n\n![](../../docs/images/editor.png)\n\n交流\n- [提交问题和建议](https://github.com/wangeditor-team/wangEditor/issues)\n- 加入 QQ 群（[官网](https://www.wangeditor.com/)有群号）\n"
  },
  {
    "path": "packages/editor/__tests__/create.test.ts",
    "content": "/**\n * @description create editor and toolbar test\n * @author wangfupeng\n */\n\nimport { createEditor, createToolbar } from '../../../packages/editor/src/index'\nimport { ICreateEditorOption, ICreateToolbarOption } from '../../../packages/editor/src/create'\n\nfunction customCreateEditor(config: Partial<ICreateEditorOption> = {}) {\n  const editorContainer = document.createElement('div')\n  document.body.appendChild(editorContainer)\n\n  // create editor\n  const editor = createEditor({\n    selector: editorContainer,\n    ...config,\n  })\n\n  return editor\n}\n\nfunction customCreateToolbar(config: Partial<ICreateToolbarOption> = {}) {\n  const toolbarContainer = document.createElement('div')\n  document.body.appendChild(toolbarContainer)\n\n  // create editor\n  const editor = customCreateEditor()\n\n  // create toolbar\n  const toolbar = createToolbar({\n    editor,\n    selector: toolbarContainer,\n    ...config,\n  })\n\n  return toolbar\n}\n\ndescribe('create editor and toolbar', () => {\n  test('create editor with default mode', () => {\n    const editor = customCreateEditor()\n\n    expect(editor.id).not.toBeNull()\n  })\n\n  test('create editor with default mode that has text hoverbar', () => {\n    const editor = customCreateEditor()\n    const config = editor.getConfig()\n\n    expect(config.hoverbarKeys!.text).not.toBeNull()\n  })\n\n  test('create editor with simple mode', () => {\n    const editor = customCreateEditor({\n      mode: 'simple',\n    })\n    expect(editor.id).not.toBeNull()\n  })\n\n  test('create editor with simple mode that does not has text hoverbar', () => {\n    const editor = customCreateEditor({\n      mode: 'simple',\n    })\n    const config = editor.getConfig()\n\n    expect(config.hoverbarKeys!.text).toBeUndefined()\n  })\n\n  test('create editor can not be called twice with same container', () => {\n    const editorContainer = document.createElement('div')\n    document.body.appendChild(editorContainer)\n    // create editor\n    customCreateEditor({\n      selector: editorContainer,\n    })\n\n    try {\n      customCreateEditor({\n        selector: editorContainer,\n      })\n    } catch (ex) {\n      expect(ex.message.indexOf('Repeated create editor by selector')).not.toBe(-1)\n    }\n  })\n\n  test('create toolbar with default mode', () => {\n    const toolbar = customCreateToolbar()\n    expect(toolbar.$box).not.toBeNull()\n  })\n\n  test('create toolbar with simple mode', () => {\n    const toolbar = customCreateToolbar({\n      mode: 'simple',\n    })\n    expect(toolbar.$box).not.toBeNull()\n  })\n\n  test('create toolbar with simple mode that the config hoverbarKeys is different from default mode', () => {\n    const simpleToolbar = customCreateToolbar({\n      mode: 'simple',\n    })\n    const defaultToolbar = customCreateToolbar()\n    expect(simpleToolbar.getConfig().toolbarKeys).not.toEqual(\n      defaultToolbar.getConfig().toolbarKeys\n    )\n  })\n\n  test('create toolbar can not be called twice with same container', () => {\n    const toolbarContainer = document.createElement('div')\n    document.body.appendChild(toolbarContainer)\n\n    customCreateToolbar({\n      selector: toolbarContainer,\n    })\n    try {\n      customCreateToolbar({\n        selector: toolbarContainer,\n      })\n    } catch (ex) {\n      expect(ex.message.indexOf('Repeated create toolbar by selector')).not.toBe(-1)\n    }\n  })\n\n  test('create editor with html', () => {\n    const html = `<h1>header</h1>\n<p>hello&nbsp;<strong>world</strong>\n</p><p><br></p>`\n\n    const editor = customCreateEditor({ html })\n    expect(editor.children).toEqual([\n      { type: 'header1', children: [{ text: 'header' }] },\n      {\n        type: 'paragraph',\n        children: [{ text: 'hello ' }, { text: 'world', bold: true }],\n      },\n      { type: 'paragraph', children: [{ text: '' }] },\n    ])\n  })\n})\n"
  },
  {
    "path": "packages/editor/demo/README.md",
    "content": "# wangEditor demo\n\n修改左侧目录，在 demo 目录搜索 `MENU_CONF`\n\ndemo 部署参考 `deploy-demos.yml` 配置\n"
  },
  {
    "path": "packages/editor/demo/catalog.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>wangEditor catalog</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <!-- <link href=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\"> -->\n  <link href=\"https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\">\n  <link href=\"./css/layout.css\" rel=\"stylesheet\">\n\n  <script src=\"./js/custom-elem.js\"></script>\n\n  <style>\n    #header-container {\n      list-style-type: none;\n      padding-left: 20px;\n    }\n\n    #header-container li {\n      color: #333;\n      margin: 10px 0;\n      cursor: pointer;\n    }\n\n    #header-container li:hover {\n      text-decoration: underline;\n    }\n\n    #header-container li[type=\"header1\"] {\n      font-size: 20px;\n      font-weight: bold;\n    }\n\n    #header-container li[type=\"header2\"] {\n      font-size: 16px;\n      padding-left: 15px;\n      font-weight: bold;\n    }\n\n    #header-container li[type=\"header3\"] {\n      font-size: 14px;\n      padding-left: 30px;\n    }\n\n    #header-container li[type=\"header4\"] {\n      font-size: 12px;\n      padding-left: 45px;\n    }\n\n    #header-container li[type=\"header5\"] {\n      font-size: 12px;\n      padding-left: 60px;\n    }\n  </style>\n</head>\n\n<body>\n  <demo-nav title=\"wangEditor catalog\"></demo-nav>\n  <div class=\"page-container\">\n    <div class=\"page-left\">\n      <demo-menu></demo-menu>\n    </div>\n    <div class=\"page-right\" style=\"display: flex;\">\n      <!-- 编辑器 DOM -->\n      <div style=\"border: 1px solid #ccc; flex: 1; width: calc(100vw - 370px);\">\n        <div id=\"editor-toolbar\" style=\"border-bottom: 1px solid #ccc;\"></div>\n        <div id=\"editor-text-area\" style=\"height: 600px\"></div>\n      </div>\n\n      <!-- 标题目录 -->\n      <div style=\"width: 200px; background-color: #f1f1f1;\">\n        <ul id=\"header-container\"></ul>\n      </div>\n    </div>\n  </div>\n\n  <!-- <script src=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/index.min.js\"></script> -->\n  <script src=\"https://unpkg.com/@wangeditor/editor@latest/dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    // 切换语言\n    const LANG = location.href.indexOf('lang=en') > 0 ? 'en' : 'zh-CN'\n    E.i18nChangeLanguage(LANG)\n\n    // 标题 DOM 容器\n    const headerContainer = document.getElementById('header-container')\n    headerContainer.addEventListener('mousedown', event => {\n      if (event.target.tagName !== 'LI') return\n      event.preventDefault()\n      const id = event.target.id\n      editor.scrollToElem(id) // 滚动到标题\n    })\n\n    window.editor = E.createEditor({\n      selector: '#editor-text-area',\n      html: '<h1>标题</h1><h2>标题A</h2><p>文本</p><p>文本</p><p>文本</p><h3>标题A1</h3><p>文本</p><p>文本</p><p>文本</p><h3>标题A2</h3><p>文本</p><p>文本</p><p>文本</p><h2>标题B</h2><p>文本</p><p>文本</p><p>文本</p><h3>标题B1</h3><p>文本</p><p>文本</p><p>文本</p><h3>标题B2</h3><p>文本</p><p>文本</p><p>文本</p>',\n      config: {\n        placeholder: 'Type here...',\n        MENU_CONF: {\n          uploadImage: {\n            fieldName: 'your-fileName',\n            base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n          }\n        },\n        onChange(editor) {\n          const headers = editor.getElemsByTypePrefix('header')\n          headerContainer.innerHTML = headers.map(header => {\n            const text = E.SlateNode.string(header)\n            const { id, type } = header\n            return `<li id=\"${id}\" type=\"${type}\">${text}</li>`\n          }).join('')\n        }\n      }\n    })\n\n    window.toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      config: {}\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/demo/code-highlight.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>wangEditor code highlight</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <link href=\"./css/layout.css\" rel=\"stylesheet\">\n\n  <script src=\"./js/custom-elem.js\"></script>\n\n  <!-- 引入 prism css -->\n  <!-- <link href=\"https://cdn.jsdelivr.net/npm/prismjs@latest/themes/prism.css\" rel=\"stylesheet\"> -->\n  <link href=\"https://unpkg.com/prismjs@latest/themes/prism.css\" rel=\"stylesheet\">\n</head>\n\n<body>\n  <demo-nav title=\"wangEditor code highlight\"></demo-nav>\n  <div class=\"page-container\">\n    <div class=\"page-left\">\n      <demo-menu></demo-menu>\n    </div>\n    <div class=\"page-right\">\n\n      <!-- 静态 -->\n      <div style=\"border: 1px solid #ccc; padding: 0 10px; border-radius: 5px;\">\n        <p>wangEditor 输出的 Javascript 代码：</p>\n        <pre><code class=\"language-javascript\">const a = 100;\nfunction fn(x) { return x + 10 };\n// 注释\n</code></pre>\n\n        <p>wangEditor 输出的 HTML 代码：</p>\n        <pre><code class=\"language-html\">&lt;div&gt;text1&lt;/div&gt;</code></pre>\n      </div>\n\n      <!-- 动态 -->\n      <div id=\"content-container\"\n        style=\"border: 1px solid #ccc; padding: 0 10px; margin-top: 15px; border-radius: 5px;\"></div>\n      <div style=\"margin-top: 15px;\">\n        <button id=\"btn-render\">Dynamic render HTML</button>\n      </div>\n    </div>\n  </div>\n\n  <!-- 引入 prism js -->\n  <!-- <script src=\"https://cdn.jsdelivr.net/npm/prismjs@latest/prism.min.js\"></script> -->\n  <script src=\"https://unpkg.com/prismjs@latest/prism.js\"></script>\n  <!-- <script src=\"https://cdn.jsdelivr.net/npm/prismjs@latest/components/prism-core.min.js\"></script> -->\n  <script src=\"https://unpkg.com/prismjs@latest/components/prism-core.js\"></script>\n  <!-- <script src=\"https://cdn.jsdelivr.net/npm/prismjs@latest/plugins/autoloader/prism-autoloader.min.js\"></script> -->\n  <script src=\"https://unpkg.com/prismjs@latest/plugins/autoloader/prism-autoloader.js\"></script>\n\n  <script>\n    const html = `<p>wangEditor&nbsp;输入的 Javascript 代码：</p><pre><code class=\"language-javascript\">function fn(a) {\n  if (typeof a === 'number') {\n      return a + 100 // comments\n  }\n  return 0\n}</code></pre><p><br></p>`\n\n    document.getElementById('btn-render').addEventListener('click', () => {\n      document.getElementById('content-container').innerHTML = html\n      Prism.highlightAll()\n    })\n\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/demo/css/layout.css",
    "content": "/* body {\n  margin: 20px;\n} */\n\n.page-container {\n  margin-top: 15px;\n  display: flex;\n}\n\n.page-left {\n  width: 150px;\n  padding: 0 10px;\n}\n\n.page-right {\n  padding: 0 10px;\n  flex: 1;\n  width: calc(100vw - 170px);\n}"
  },
  {
    "path": "packages/editor/demo/css/view.css",
    "content": ".editor-content-view {\n  border: 3px solid #ccc;\n  border-radius: 5px;\n  padding: 0 10px;\n  margin-top: 20px;\n  overflow-x: auto;\n}\n\n.editor-content-view p,\n.editor-content-view li {\n  white-space: pre-wrap; /* 保留空格 */\n}\n\n.editor-content-view blockquote {\n  border-left: 8px solid #d0e5f2;\n  padding: 10px 10px;\n  margin: 10px 0;\n  background-color: #f1f1f1;\n}\n\n.editor-content-view code {\n  font-family: monospace;\n  background-color: #eee;\n  padding: 3px;\n  border-radius: 3px;\n}\n.editor-content-view pre>code {\n  display: block;\n  padding: 10px;\n}\n\n.editor-content-view table {\n  border-collapse: collapse;\n}\n.editor-content-view td,\n.editor-content-view th {\n  border: 1px solid #ccc;\n  min-width: 50px;\n  height: 20px;\n}\n.editor-content-view th {\n  background-color: #f1f1f1;\n}\n\n.editor-content-view ul,\n.editor-content-view ol {\n  padding-left: 20px;\n}\n\n.editor-content-view input[type=\"checkbox\"] {\n  margin-right: 5px;\n}"
  },
  {
    "path": "packages/editor/demo/extend-menu-drop-panel.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>wangEditor extend dropPanel menu</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <!-- <link href=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\"> -->\n  <link href=\"https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\">\n  <link href=\"./css/layout.css\" rel=\"stylesheet\">\n  <style>\n    .w-e-panel-my-list {\n      text-align: left;\n    }\n\n    .w-e-panel-my-list li {\n      display: inline;\n      cursor: pointer;\n      padding: 3px 5px;\n    }\n\n    .w-e-panel-my-list li:hover {\n      background-color: #f1f1f1;\n    }\n  </style>\n\n  <script src=\"./js/custom-elem.js\"></script>\n</head>\n\n<body>\n  <demo-nav title=\"wangEditor extend dropPanel menu\"></demo-nav>\n  <div class=\"page-container\">\n    <div class=\"page-left\">\n      <demo-menu></demo-menu>\n    </div>\n    <div class=\"page-right\">\n      <!-- 编辑器 DOM -->\n      <div style=\"border: 1px solid #ccc;\">\n        <div id=\"editor-toolbar\" style=\"border-bottom: 1px solid #ccc;\"></div>\n        <div id=\"editor-text-area\" style=\"height: 500px\"></div>\n      </div>\n\n      <!-- 内容状态 -->\n      <p style=\"background-color: #f1f1f1;\">\n        Text length: <span id=\"total-length\"></span>；\n        Selected text length: <span id=\"selected-length\"></span>；\n      </p>\n    </div>\n  </div>\n\n  <script src=\"https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js\"></script>\n  <!-- <script src=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/index.min.js\"></script> -->\n  <script src=\"https://unpkg.com/@wangeditor/editor@latest/dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    const LANG = location.href.indexOf('lang=en') > 0 ? 'en' : 'zh-CN'\n    E.i18nChangeLanguage(LANG) // 切换语言\n\n\n\n    // Extend menu\n    class MyMenu {\n      constructor() {\n        this.title = 'My menu'\n        // this.iconSvg = '<svg >...</svg>'\n        this.tag = 'button'\n        this.showDropPanel = true\n      }\n      getValue(editor) {\n        return ''\n      }\n      isActive(editor) {\n        return false // or true\n      }\n      isDisabled(editor) {\n        return false // or true\n      }\n      exec(editor, value) {\n        // do nothing 什么都不用做\n      }\n      getPanelContentElem(editor) {\n        const $list = $(`<ul class=\"w-e-panel-my-list\">\n            <li>北京</li>\n            <li>上海</li>\n            <li>深圳</li>\n            <li>广州</li>\n            <li>天津</li>\n            <li>成都</li>\n            <li>南京</li>\n            <li>郑州</li>\n          </ul>`)\n\n        $list.on('click', 'li', function () {\n          editor.insertText(this.innerHTML)\n          editor.insertText(' ')\n        })\n\n        return $list[0]\n\n        // PS：也可以把 $list 缓存下来，这样不用每次重复创建、重复绑定事件，优化性能\n      }\n    }\n    const myMenuConf = {\n      key: 'myMenu',\n      factory() {\n        return new MyMenu()\n      }\n    }\n    E.Boot.registerMenu(myMenuConf)\n\n\n\n    window.editor = E.createEditor({\n      selector: '#editor-text-area',\n      html: '<p><br></p>',\n      config: {\n        placeholder: 'Type here...',\n        MENU_CONF: {\n          uploadImage: {\n            fieldName: 'your-fileName',\n            base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n          }\n        },\n        onChange(editor) {\n          console.log(editor.getHtml())\n\n          // 选中文字\n          const selectionText = editor.getSelectionText()\n          document.getElementById('selected-length').innerHTML = selectionText.length\n          // 全部文字\n          const text = editor.getText().replace(/\\n|\\r/mg, '')\n          document.getElementById('total-length').innerHTML = text.length\n        }\n      }\n    })\n\n    window.toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      config: {\n        insertKeys: {\n          index: 0,\n          keys: ['myMenu'], // show menu in toolbar\n        }\n      }\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/demo/extend-menu-modal.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>wangEditor extend modal menu</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <!-- <link href=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\"> -->\n  <link href=\"https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\">\n  <link href=\"./css/layout.css\" rel=\"stylesheet\">\n\n  <script src=\"./js/custom-elem.js\"></script>\n</head>\n\n<body>\n  <demo-nav title=\"wangEditor extend modal menu\"></demo-nav>\n  <div class=\"page-container\">\n    <div class=\"page-left\">\n      <demo-menu></demo-menu>\n    </div>\n    <div class=\"page-right\">\n      <!-- 编辑器 DOM -->\n      <div style=\"border: 1px solid #ccc;\">\n        <div id=\"editor-toolbar\" style=\"border-bottom: 1px solid #ccc;\"></div>\n        <div id=\"editor-text-area\" style=\"height: 500px\"></div>\n      </div>\n\n      <!-- 内容状态 -->\n      <p style=\"background-color: #f1f1f1;\">\n        Text length: <span id=\"total-length\"></span>；\n        Selected text length: <span id=\"selected-length\"></span>；\n      </p>\n    </div>\n  </div>\n\n  <script src=\"https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js\"></script>\n  <!-- <script src=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/index.min.js\"></script> -->\n  <script src=\"https://unpkg.com/@wangeditor/editor@latest/dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    const LANG = location.href.indexOf('lang=en') > 0 ? 'en' : 'zh-CN'\n    E.i18nChangeLanguage(LANG) // 切换语言\n\n\n\n    // Extend menu\n    class MyMenu {\n      constructor() {\n        this.title = 'My menu'\n        // this.iconSvg = '<svg >...</svg>'\n        this.tag = 'button'\n        this.showModal = true\n        this.modalWidth = 300\n      }\n      getValue(editor) {\n        return ''\n      }\n      isActive(editor) {\n        return false // or true\n      }\n      isDisabled(editor) {\n        return false // or true\n      }\n      exec(editor, value) {\n        // do nothing 什么都不用做\n      }\n      getModalPositionNode(editor) {\n        return null // modal 依据选区定位\n      }\n      getModalContentElem(editor) {\n        const $container = $('<div></div>')\n\n        const inputId = `input-${Math.random().toString(16).slice(-8)}`\n        const buttonId = `button-${Math.random().toString(16).slice(-8)}`\n\n        const $inputContainer = $(`<label class=\"babel-container\">\n            <span>Text</span>\n            <input type=\"text\" id=\"${inputId}\" value=\"hello world\">\n          </label>`)\n        const $buttonContainer = $(`<div class=\"button-container\">\n            <button id=\"${buttonId}\">insert text</button>\n          </div>`)\n\n        $container.append($inputContainer).append($buttonContainer)\n\n        $container.on('click', `#${buttonId}`, e => {\n          e.preventDefault()\n\n          const text = $(`#${inputId}`).val()\n          if (!text) return\n\n          editor.restoreSelection() // 恢复选区\n          editor.insertText(text)\n          editor.insertText(' ')\n        })\n\n        setTimeout(() => {\n          $(`#${inputId}`).focus()\n        })\n\n        return $container[0]\n\n        // PS：也可以把 $container 缓存下来，这样不用每次重复创建、重复绑定事件，优化性能\n      }\n    }\n    const myMenuConf = {\n      key: 'myMenu',\n      factory() {\n        return new MyMenu()\n      }\n    }\n    E.Boot.registerMenu(myMenuConf)\n\n\n\n    window.editor = E.createEditor({\n      selector: '#editor-text-area',\n      html: '<p><br></p>',\n      config: {\n        placeholder: 'Type here...',\n        MENU_CONF: {\n          uploadImage: {\n            fieldName: 'your-fileName',\n            base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n          }\n        },\n        onChange(editor) {\n          console.log(editor.getHtml())\n\n          // 选中文字\n          const selectionText = editor.getSelectionText()\n          document.getElementById('selected-length').innerHTML = selectionText.length\n          // 全部文字\n          const text = editor.getText().replace(/\\n|\\r/mg, '')\n          document.getElementById('total-length').innerHTML = text.length\n        }\n      }\n    })\n\n    window.toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      config: {\n        insertKeys: {\n          index: 0,\n          keys: ['myMenu'], // show menu in toolbar\n        }\n      }\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/demo/extend-menu-select.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>wangEditor extend select menu</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <!-- <link href=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\"> -->\n  <link href=\"https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\">\n  <link href=\"./css/layout.css\" rel=\"stylesheet\">\n\n  <script src=\"./js/custom-elem.js\"></script>\n</head>\n\n<body>\n  <demo-nav title=\"wangEditor extend select menu\"></demo-nav>\n  <div class=\"page-container\">\n    <div class=\"page-left\">\n      <demo-menu></demo-menu>\n    </div>\n    <div class=\"page-right\">\n      <!-- 编辑器 DOM -->\n      <div style=\"border: 1px solid #ccc;\">\n        <div id=\"editor-toolbar\" style=\"border-bottom: 1px solid #ccc;\"></div>\n        <div id=\"editor-text-area\" style=\"height: 500px\"></div>\n      </div>\n\n      <!-- 内容状态 -->\n      <p style=\"background-color: #f1f1f1;\">\n        Text length: <span id=\"total-length\"></span>；\n        Selected text length: <span id=\"selected-length\"></span>；\n      </p>\n    </div>\n  </div>\n\n  <!-- <script src=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/index.min.js\"></script> -->\n  <script src=\"https://unpkg.com/@wangeditor/editor@latest/dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    const LANG = location.href.indexOf('lang=en') > 0 ? 'en' : 'zh-CN'\n    E.i18nChangeLanguage(LANG) // 切换语言\n\n\n\n    class MyMenuClass {\n      constructor() {\n        this.title = 'My Select Menu',\n          // this.iconSvg = '<svg>...</svg>'\n          this.tag = 'select'\n        this.width = 60\n      }\n\n      getOptions(editor) {\n        const options = [\n          { value: 'beijing', text: '北京', styleForRenderMenuList: { 'font-size': '32px', 'font-weight': 'bold' } },\n          { value: 'shanghai', text: '上海', selected: true },\n          { value: 'shenzhen', text: '深圳' }\n        ]\n        return options\n      }\n\n      getValue(editor) {\n        return 'shanghai' // 匹配 options 其中一个 value\n      }\n      isActive(editor) {\n        return false // or true\n      }\n      isDisabled(editor) {\n        return false // or true\n      }\n      exec(editor, value) {\n        editor.insertText(value) // value 即 this.getValue(editor) 的返回值\n        editor.insertText(' ')\n      }\n    }\n\n    const myMenuConf = {\n      key: 'myMenu',\n      factory() {\n        return new MyMenuClass()\n      }\n    }\n    E.Boot.registerMenu(myMenuConf)\n\n\n\n    window.editor = E.createEditor({\n      selector: '#editor-text-area',\n      html: '<p><br></p>',\n      config: {\n        placeholder: 'Type here...',\n        MENU_CONF: {\n          uploadImage: {\n            fieldName: 'your-fileName',\n            base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n          }\n        },\n        onChange(editor) {\n          console.log(editor.getHtml())\n\n          // 选中文字\n          const selectionText = editor.getSelectionText()\n          document.getElementById('selected-length').innerHTML = selectionText.length\n          // 全部文字\n          const text = editor.getText().replace(/\\n|\\r/mg, '')\n          document.getElementById('total-length').innerHTML = text.length\n        }\n      }\n    })\n\n    window.toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      config: {\n        insertKeys: {\n          index: 0,\n          keys: ['myMenu'], // show menu in toolbar\n        }\n      }\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/demo/extend-menu.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>wangEditor extend menu</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <!-- <link href=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\"> -->\n  <link href=\"https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\">\n  <link href=\"./css/layout.css\" rel=\"stylesheet\">\n\n  <script src=\"./js/custom-elem.js\"></script>\n</head>\n\n<body>\n  <demo-nav title=\"wangEditor extend menu\"></demo-nav>\n  <div class=\"page-container\">\n    <div class=\"page-left\">\n      <demo-menu></demo-menu>\n    </div>\n    <div class=\"page-right\">\n      <!-- 编辑器 DOM -->\n      <div style=\"border: 1px solid #ccc;\">\n        <div id=\"editor-toolbar\" style=\"border-bottom: 1px solid #ccc;\"></div>\n        <div id=\"editor-text-area\" style=\"height: 500px\"></div>\n      </div>\n\n      <!-- 内容状态 -->\n      <p style=\"background-color: #f1f1f1;\">\n        Text length: <span id=\"total-length\"></span>；\n        Selected text length: <span id=\"selected-length\"></span>；\n      </p>\n    </div>\n  </div>\n\n  <!-- <script src=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/index.min.js\"></script> -->\n  <script src=\"https://unpkg.com/@wangeditor/editor@latest/dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    const LANG = location.href.indexOf('lang=en') > 0 ? 'en' : 'zh-CN'\n    E.i18nChangeLanguage(LANG) // 切换语言\n\n\n\n    // Extend menu\n    class MyMenu {\n      constructor() {\n        this.title = 'My menu'\n        // this.iconSvg = '<svg >...</svg>'\n        this.tag = 'button'\n      }\n      getValue(editor) {\n        return ' hello '\n      }\n      isActive(editor) {\n        return false // or true\n      }\n      isDisabled(editor) {\n        return false // or true\n      }\n      exec(editor, value) {\n        editor.insertText(value) // value 即 this.getValue(editor) 的返回值\n      }\n    }\n    const myMenuConf = {\n      key: 'myMenu',\n      factory() {\n        return new MyMenu()\n      }\n    }\n    E.Boot.registerMenu(myMenuConf)\n\n\n\n    window.editor = E.createEditor({\n      selector: '#editor-text-area',\n      html: '<p><br></p>',\n      config: {\n        placeholder: 'Type here...',\n        MENU_CONF: {\n          uploadImage: {\n            fieldName: 'your-fileName',\n            base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n          }\n        },\n        onChange(editor) {\n          console.log(editor.getHtml())\n\n          // 选中文字\n          const selectionText = editor.getSelectionText()\n          document.getElementById('selected-length').innerHTML = selectionText.length\n          // 全部文字\n          const text = editor.getText().replace(/\\n|\\r/mg, '')\n          document.getElementById('total-length').innerHTML = text.length\n        }\n      }\n    })\n\n    window.toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      config: {\n        insertKeys: {\n          index: 0,\n          keys: ['myMenu'], // show menu in toolbar\n        }\n      }\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/demo/get-html.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>wangEditor get HTML</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <!-- <link href=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\"> -->\n  <link href=\"https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\">\n  <link href=\"./css/layout.css\" rel=\"stylesheet\">\n  <link href=\"./css/view.css\" rel=\"stylesheet\">\n\n  <script src=\"./js/custom-elem.js\"></script>\n</head>\n\n<body>\n  <demo-nav title=\"wangEditor get HTML\"></demo-nav>\n  <div class=\"page-container\">\n    <div class=\"page-left\">\n      <demo-menu></demo-menu>\n    </div>\n    <div class=\"page-right\">\n      <!-- 编辑器 DOM -->\n      <div style=\"border: 1px solid #ccc;\">\n        <div id=\"editor-toolbar\" style=\"border-bottom: 1px solid #ccc;\"></div>\n        <div id=\"editor-text-area\" style=\"height: 350px\"></div>\n      </div>\n\n      <!-- 显示内容 -->\n      <div style=\"margin-top: 20px;\">\n        <textarea id=\"editor-content-textarea\" style=\"width: 100%; height: 100px; outline: none;\" readonly></textarea>\n      </div>\n      <div id=\"editor-content-view\" class=\"editor-content-view\"></div>\n    </div>\n  </div>\n\n  <!-- <script src=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/index.min.js\"></script> -->\n  <script src=\"https://unpkg.com/@wangeditor/editor@latest/dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    // 切换语言\n    const LANG = location.href.indexOf('lang=en') > 0 ? 'en' : 'zh-CN'\n    E.i18nChangeLanguage(LANG)\n\n    window.editor = E.createEditor({\n      selector: '#editor-text-area',\n      html: '<p>hello&nbsp;world</p><p><br></p>',\n      config: {\n        placeholder: 'Type here...',\n        MENU_CONF: {\n          uploadImage: {\n            fieldName: 'your-fileName',\n            base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n          }\n        },\n        onChange(editor) {\n          const html = editor.getHtml()\n          document.getElementById('editor-content-view').innerHTML = html\n          document.getElementById('editor-content-textarea').value = html\n        }\n      }\n    })\n\n    window.toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      config: {}\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/demo/huge-doc.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>wangEditor huge doc</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <!-- <link href=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\"> -->\n  <link href=\"https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\">\n  <link href=\"./css/layout.css\" rel=\"stylesheet\">\n\n  <script src=\"./js/custom-elem.js\"></script>\n  <script src=\"./js/huge-content.js\"></script>\n</head>\n\n<body>\n  <demo-nav title=\"wangEditor huge doc\"></demo-nav>\n  <div class=\"page-container\">\n    <div class=\"page-left\">\n      <demo-menu></demo-menu>\n    </div>\n    <div class=\"page-right\">\n      <!-- 编辑器 DOM -->\n      <div style=\"border: 1px solid #ccc;\">\n        <div id=\"editor-toolbar\" style=\"border-bottom: 1px solid #ccc;\"></div>\n        <div id=\"editor-text-area\" style=\"height: 700px\"></div>\n      </div>\n\n      <!-- 内容状态 -->\n      <p style=\"background-color: #f1f1f1;\">\n        Text length: <span id=\"total-length\"></span>\n      </p>\n    </div>\n  </div>\n\n  <!-- <script src=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/index.min.js\"></script> -->\n  <script src=\"https://unpkg.com/@wangeditor/editor@latest/dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    // 切换语言\n    const LANG = location.href.indexOf('lang=en') > 0 ? 'en' : 'zh-CN'\n    E.i18nChangeLanguage(LANG)\n\n    window.editor = E.createEditor({\n      selector: '#editor-text-area',\n      content: window.HUGE_CONTENT,\n      config: {\n        placeholder: 'Type here...',\n        MENU_CONF: {\n          uploadImage: {\n            fieldName: 'your-fileName',\n            base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n          }\n        },\n        onChange(editor) {\n          const text = editor.getText().replace(/\\n|\\r/mg, '')\n          document.getElementById('total-length').innerHTML = text.length\n        }\n      }\n    })\n\n    window.toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      config: {}\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/demo/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>wangEditor default mode</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <!-- <link href=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\"> -->\n  <link href=\"https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\">\n  <link href=\"./css/layout.css\" rel=\"stylesheet\">\n\n  <script src=\"./js/custom-elem.js\"></script>\n</head>\n\n<body>\n  <demo-nav title=\"wangEditor default mode\"></demo-nav>\n  <div class=\"page-container\">\n    <div class=\"page-left\">\n      <demo-menu></demo-menu>\n    </div>\n    <div class=\"page-right\">\n      <!-- 编辑器 DOM -->\n      <div style=\"border: 1px solid #ccc;\">\n        <div id=\"editor-toolbar\" style=\"border-bottom: 1px solid #ccc;\"></div>\n        <div id=\"editor-text-area\" style=\"height: 500px\"></div>\n      </div>\n\n      <!-- 内容状态 -->\n      <p style=\"background-color: #f1f1f1;\">\n        Text length: <span id=\"total-length\"></span>；\n        Selected text length: <span id=\"selected-length\"></span>；\n      </p>\n    </div>\n  </div>\n\n  <!-- <script src=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/index.min.js\"></script> -->\n  <script src=\"https://unpkg.com/@wangeditor/editor@latest/dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    // 切换语言\n    const LANG = location.href.indexOf('lang=en') > 0 ? 'en' : 'zh-CN'\n    E.i18nChangeLanguage(LANG)\n\n    window.editor = E.createEditor({\n      selector: '#editor-text-area',\n      html: '<p><br></p>',\n      config: {\n        placeholder: 'Type here...',\n        MENU_CONF: {\n          uploadImage: {\n            fieldName: 'your-fileName',\n            base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n          }\n        },\n        onChange(editor) {\n          console.log(editor.getHtml())\n\n          // 选中文字\n          const selectionText = editor.getSelectionText()\n          document.getElementById('selected-length').innerHTML = selectionText.length\n          // 全部文字\n          const text = editor.getText().replace(/\\n|\\r/mg, '')\n          document.getElementById('total-length').innerHTML = text.length\n        }\n      }\n    })\n\n    window.toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      config: {}\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/demo/js/custom-elem.js",
    "content": "/**\n * @description 自定义 elem\n * @author wangfupeng\n */\n\n// ------------------------------------------ native-shim start ------------------------------------------\n\n// 参考 https://github.com/webcomponents/custom-elements/blob/master/src/native-shim.js\n/**\n * @license\n * Copyright (c) 2016 The Polymer Project Authors. All rights reserved.\n * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt\n * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt\n * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt\n * Code distributed by Google as part of the polymer project is also\n * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt\n */\n\n/**\n * This shim allows elements written in, or compiled to, ES5 to work on native\n * implementations of Custom Elements v1. It sets new.target to the value of\n * this.constructor so that the native HTMLElement constructor can access the\n * current under-construction element's definition.\n */\n;(function () {\n  if (\n    // No Reflect, no classes, no need for shim because native custom elements\n    // require ES2015 classes or Reflect.\n    window.Reflect === undefined ||\n    window.customElements === undefined ||\n    // The webcomponentsjs custom elements polyfill doesn't require\n    // ES2015-compatible construction (`super()` or `Reflect.construct`).\n    window.customElements.polyfillWrapFlushCallback\n  ) {\n    return\n  }\n  const BuiltInHTMLElement = HTMLElement\n  /**\n   * With jscompiler's RECOMMENDED_FLAGS the function name will be optimized away.\n   * However, if we declare the function as a property on an object literal, and\n   * use quotes for the property name, then closure will leave that much intact,\n   * which is enough for the JS VM to correctly set Function.prototype.name.\n   */\n  const wrapperForTheName = {\n    HTMLElement: /** @this {!Object} */ function HTMLElement() {\n      return Reflect.construct(BuiltInHTMLElement, [], /** @type {!Function} */ this.constructor)\n    },\n  }\n  window.HTMLElement = wrapperForTheName['HTMLElement']\n  HTMLElement.prototype = BuiltInHTMLElement.prototype\n  HTMLElement.prototype.constructor = HTMLElement\n  Object.setPrototypeOf(HTMLElement, BuiltInHTMLElement)\n})()\n// ------------------------------------------ native-shim end ------------------------------------------\n\n// ------------------------------------------ 顶部导航 start ------------------------------------------\n!(function () {\n  // 当前语言\n  const LANG = location.href.indexOf('lang=en') > 0 ? 'en' : 'zh-CN'\n\n  // 自定义组件\n  class MyNav extends HTMLElement {\n    constructor() {\n      super()\n\n      const shadow = this.attachShadow({ mode: 'open' })\n      const document = shadow.ownerDocument\n\n      const style = document.createElement('style')\n      style.innerHTML = `\n      .container {\n        display: flex;\n        padding: 10px;\n        background-color: #4474c8;\n        color: #fff;\n      }\n      .container a {\n        color: #fff;\n        text-decoration: none;\n      }\n      .container h1 {\n        flex: 1;\n        margin: 0;\n        font-size: 26px;\n      }\n      .container .right-container {\n        width: 200px;\n        text-align: right;\n        line-height: 26px;\n      }\n    `\n      shadow.appendChild(style)\n\n      // 容器\n      const container = document.createElement('div')\n      container.className = 'container'\n\n      // 标题\n      const header = document.createElement('h1')\n      header.textContent = ''\n      this.header = header\n\n      // 右侧链接\n      const rightContainer = document.createElement('div')\n      rightContainer.className = 'right-container'\n      if (LANG === 'en') {\n        rightContainer.innerHTML = `\n        <a href=\"https://www.wangeditor.com/en/\">Document</a>\n        &nbsp;\n        <a href=\"https://github.com/wangeditor-team/wangEditor/tree/master/packages/editor/demo\">Source</a>\n      `\n      } else {\n        rightContainer.innerHTML = `\n        <a href=\"https://www.wangeditor.com/\">文档</a>\n        &nbsp;\n        <a href=\"https://github.com/wangeditor-team/wangEditor/tree/master/packages/editor/demo\">源码</a>\n      `\n      }\n\n      container.appendChild(header)\n      container.appendChild(rightContainer)\n\n      shadow.appendChild(container)\n    }\n\n    attributeChangedCallback(name, oldValue, newValue) {\n      if (name === 'title') {\n        if (oldValue == newValue) return\n        this.header.textContent = newValue\n      }\n    }\n  }\n  MyNav.observedAttributes = ['title']\n  window.customElements.define('demo-nav', MyNav)\n})()\n// ------------------------------------------ 顶部导航 end ------------------------------------------\n\n// ------------------------------------------ 左侧菜单 start ------------------------------------------\n// 菜单配置\nconst MENU_CONF = [\n  {\n    'zh-CN': { text: '默认模式', link: './index.html' },\n    en: { text: 'Default mode', link: './index.html?lang=en' },\n  },\n  {\n    'zh-CN': { text: '简洁模式', link: './simple-mode.html' },\n    en: { text: 'Simple mode', link: './simple-mode.html?lang=en' },\n  },\n  {\n    'zh-CN': { text: '获取 HTML', link: './get-html.html' },\n    en: { text: 'Get HTML', link: './get-html.html?lang=en' },\n  },\n  {\n    'zh-CN': { text: '设置 HTML', link: './set-html.html' },\n    en: { text: 'Set HTML', link: './set-html.html?lang=en' },\n  },\n  {\n    'zh-CN': { text: '模拟腾讯文档', link: './like-qq-doc.html' },\n    en: { text: 'Like QQ doc', link: './like-qq-doc.html?lang=en' },\n  },\n  {\n    'zh-CN': {\n      text: '上传图片',\n      link: 'https://github.com/wangeditor-team/server',\n    },\n    en: {\n      text: 'Upload Image',\n      link: 'https://github.com/wangeditor-team/server',\n    },\n  },\n  {\n    'zh-CN': {\n      text: '上传视频',\n      link: 'https://github.com/wangeditor-team/server',\n    },\n    en: {\n      text: 'Upload Video',\n      link: 'https://github.com/wangeditor-team/server',\n    },\n  },\n  {\n    'zh-CN': { text: '代码高亮', link: './code-highlight.html' },\n    en: { text: 'Code highlight', link: './code-highlight.html?lang=en' },\n  },\n  {\n    'zh-CN': { text: '多个编辑器', link: './multi-editor.html' },\n    en: { text: 'Multi editor', link: './multi-editor.html?lang=en' },\n  },\n  {\n    'zh-CN': { text: '标题目录', link: './catalog.html' },\n    en: { text: 'Catalog', link: './catalog.html?lang=en' },\n  },\n  {\n    'zh-CN': { text: 'Max Length', link: './max-length.html' },\n    en: { text: 'Max Length', link: './max-length.html?lang=en' },\n  },\n  {\n    'zh-CN': { text: '大文件 10w 字', link: './huge-doc.html' },\n    en: { text: 'Huge doc', link: './huge-doc.html?lang=en' },\n  },\n  {\n    'zh-CN': {\n      text: 'Shadow DOM',\n      link: 'https://github.com/wangeditor-team/wangEditor/blob/master/packages/editor/examples/shadow-dom.html',\n    },\n    en: {\n      text: 'Shadow DOM',\n      link: 'https://github.com/wangeditor-team/wangEditor/blob/master/packages/editor/examples/shadow-dom.html',\n    },\n  },\n  {\n    'zh-CN': { text: '扩展菜单 Button', link: './extend-menu.html' },\n    en: { text: 'Extend Button menu', link: './extend-menu.html?lang=en' },\n  },\n  {\n    'zh-CN': { text: '扩展菜单 select', link: './extend-menu-select.html' },\n    en: { text: 'Extend select menu', link: './extend-menu-select.html?lang=en' },\n  },\n  {\n    'zh-CN': { text: '扩展菜单 dropPanel', link: './extend-menu-drop-panel.html' },\n    en: { text: 'Extend dropPanel menu', link: './extend-menu-drop-panel.html?lang=en' },\n  },\n  {\n    'zh-CN': { text: '扩展菜单 modal', link: './extend-menu-modal.html' },\n    en: { text: 'Extend modal menu', link: './extend-menu-modal.html?lang=en' },\n  },\n  {\n    'zh-CN': { text: 'Vue2 demo', link: 'https://www.wangeditor.com/v5/for-frame.html#vue2' },\n    en: { text: 'Vue2 demo', link: 'https://www.wangeditor.com/en/v5/for-frame.html#vue2' },\n  },\n  {\n    'zh-CN': { text: 'Vue3 demo', link: 'https://www.wangeditor.com/v5/for-frame.html#vue3' },\n    en: { text: 'Vue3 demo', link: 'https://www.wangeditor.com/en/v5/for-frame.html#vue3' },\n  },\n  {\n    'zh-CN': {\n      text: 'React demo',\n      link: 'https://www.wangeditor.com/v5/for-frame.html#react',\n    },\n    en: {\n      text: 'React demo',\n      link: 'https://www.wangeditor.com/en/v5/for-frame.html#react',\n    },\n  },\n  {\n    'zh-CN': {\n      text: 'Webpack demo',\n      link: 'https://github.com/wangfupeng1988/webpack-wangeditor-demo',\n    },\n    en: { text: 'Webpack demo', link: 'https://github.com/wangfupeng1988/webpack-wangeditor-demo' },\n  },\n]\n\n!(function () {\n  // 当前语言\n  const LANG = location.href.indexOf('lang=en') > 0 ? 'en' : 'zh-CN'\n\n  // 自定义组件\n  class MyMenu extends HTMLElement {\n    constructor() {\n      super()\n\n      const shadow = this.attachShadow({ mode: 'open' })\n      const document = shadow.ownerDocument\n\n      const style = document.createElement('style')\n      style.innerHTML = `\n        ul {\n          list-style-type: none;\n          margin: 0;\n          padding: 0;\n        }\n        ul li {\n          margin: 0;\n          margin-bottom: 18px;\n        }\n        a {\n          color: #333;\n          text-decoration: none;\n        }\n        a:hover {\n          text-decoration: underline;\n        }\n      `\n      shadow.appendChild(style)\n\n      const container = document.createElement('div')\n      container.innerHTML = `<ul>\n        ${MENU_CONF.map(item => {\n          const { link, text } = item[LANG]\n          return `<li><a href=\"${link}\">${text}</a></li>`\n        }).join('')}\n      </ul>`\n\n      shadow.appendChild(container)\n    }\n  }\n  window.customElements.define('demo-menu', MyMenu)\n})()\n// ------------------------------------------ 左侧菜单 end ------------------------------------------\n"
  },
  {
    "path": "packages/editor/demo/js/huge-content.js",
    "content": ";(function () {\n  function deepClone(obj) {\n    const str = JSON.stringify(obj)\n    return JSON.parse(str)\n  }\n\n  const header = {\n    type: 'header1',\n    children: [\n      {\n        text: '水浒传简介',\n      },\n    ],\n  }\n  const text1 =\n    '全书通过描写梁山好汉反抗欺压、水泊梁山壮大和受宋朝招安，以及受招安后为宋朝征战，最终消亡的宏大故事，艺术地反映了中国历史上宋江起义从发生、发展直至失败的全过程，深刻揭示了起义的社会根源，满腔热情地歌颂了起义英雄的反抗斗争和他们的社会理想，也具体揭示了起义失败的内在历史原因。'\n  const text2 =\n    '《水浒传》是中国古典四大名著之一，问世后，在社会上产生了巨大的影响，成了后世中国小说创作的典范。《水浒传》是中国历史上最早用白话文写成的章回小说之一，流传极广，脍炙人口；同时也是汉语言文学中具备史诗特征的作品之一，对中国乃至东亚的叙事文学都有深远的影响。'\n  const p1 = {\n    type: 'paragraph',\n    children: [{ text: text1 }],\n  }\n  const p2 = {\n    type: 'paragraph',\n    children: [{ text: text2 }],\n  }\n  // const code = {\n  //   type: 'pre',\n  //   children: [\n  //     {\n  //       type: 'code',\n  //       language: 'javascript',\n  //       children: [{ text: 'const a = 100;' }],\n  //     },\n  //   ],\n  // }\n\n  // 拼接大文件\n  window.HUGE_CONTENT = []\n  for (let i = 0; i < 370; i++) {\n    window.HUGE_CONTENT.push(deepClone(header))\n    window.HUGE_CONTENT.push(deepClone(p1))\n    window.HUGE_CONTENT.push(deepClone(p2))\n    // window.HUGE_CONTENT.push(deepClone(code))\n  }\n})()\n"
  },
  {
    "path": "packages/editor/demo/like-qq-doc.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>wangEditor 仿腾讯文档</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <!-- <link href=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\"> -->\n  <link href=\"https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\">\n  <style>\n    html,\n    body {\n      background-color: #fff;\n      height: 100%;\n      overflow: hidden;\n      color: #333;\n    }\n\n    #top-container {\n      border-bottom: 1px solid #e8e8e8;\n      padding-left: 30px;\n    }\n\n    #editor-toolbar {\n      width: 1350px;\n      background-color: #FCFCFC;\n      margin: 0 auto;\n    }\n\n    #content {\n      height: calc(100% - 40px);\n      background-color: rgb(245, 245, 245);\n      overflow-y: auto;\n      position: relative;\n    }\n\n    #editor-container {\n      width: 850px;\n      margin: 30px auto 150px auto;\n      background-color: #fff;\n      padding: 20px 50px 50px 50px;\n      border: 1px solid #e8e8e8;\n      box-shadow: 0 2px 10px rgb(0 0 0 / 12%);\n    }\n\n    #title-container {\n      padding: 20px 0;\n      border-bottom: 1px solid #e8e8e8;\n    }\n\n    #title-container input {\n      font-size: 30px;\n      border: 0;\n      outline: none;\n      width: 100%;\n      line-height: 1;\n    }\n\n    #editor-text-area {\n      min-height: 900px;\n      margin-top: 20px;\n    }\n  </style>\n</head>\n\n<body>\n  <div id=\"top-container\">\n    <p>\n      <a href=\"./index.html\">&lt;&lt; 返回 Back to demo</a>\n    </p>\n  </div>\n  <div style=\"border-bottom: 1px solid #e8e8e8;\">\n    <div id=\"editor-toolbar\"></div>\n  </div>\n  <div id=\"content\">\n    <div id=\"editor-container\">\n      <div id=\"title-container\">\n        <input placeholder=\"Page title...\">\n      </div>\n      <div id=\"editor-text-area\"></div>\n    </div>\n  </div>\n\n  <!-- <script src=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/index.min.js\"></script> -->\n  <script src=\"https://unpkg.com/@wangeditor/editor@latest/dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    // 切换语言\n    const LANG = location.href.indexOf('lang=en') > 0 ? 'en' : 'zh-CN'\n    E.i18nChangeLanguage(LANG)\n\n    const editorConfig = {\n      placeholder: 'Type here...',\n      scroll: false, // 禁止编辑器滚动\n      MENU_CONF: {\n        uploadImage: {\n          fieldName: 'your-fileName',\n          base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n        }\n      },\n      onChange(editor) {\n        console.log(editor.getHtml())\n      }\n    }\n\n    // 先创建 editor\n    const editor = E.createEditor({\n      selector: '#editor-text-area',\n      content: [],\n      // html: '',\n      config: editorConfig\n    })\n\n    // 创建 toolbar\n    const toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      config: {\n        excludeKeys: 'fullScreen',\n      }\n    })\n\n    // 点击空白处 focus 编辑器\n    document.getElementById('editor-text-area').addEventListener('click', e => {\n      if (e.target.id === 'editor-text-area') {\n        editor.blur()\n        editor.focus(true) // focus 到末尾\n      }\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/demo/max-length.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>wangEditor maxlength</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <!-- <link href=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\"> -->\n  <link href=\"https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\">\n  <link href=\"./css/layout.css\" rel=\"stylesheet\">\n\n  <script src=\"./js/custom-elem.js\"></script>\n</head>\n\n<body>\n  <demo-nav title=\"wangEditor maxlength\"></demo-nav>\n  <div class=\"page-container\">\n    <div class=\"page-left\">\n      <demo-menu></demo-menu>\n    </div>\n    <div class=\"page-right\">\n      <!-- 编辑器 DOM -->\n      <div style=\"border: 1px solid #ccc;\">\n        <div id=\"editor-toolbar\" style=\"border-bottom: 1px solid #ccc;\"></div>\n        <div id=\"editor-text-area\" style=\"height: 500px\"></div>\n      </div>\n\n      <!-- 内容状态 -->\n      <p style=\"background-color: #f1f1f1;\">\n        Text length: <span id=\"total-length\"></span>；\n        Selected text length: <span id=\"selected-length\"></span>；\n      </p>\n    </div>\n  </div>\n\n  <!-- <script src=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/index.min.js\"></script> -->\n  <script src=\"https://unpkg.com/@wangeditor/editor@latest/dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    // 切换语言\n    const LANG = location.href.indexOf('lang=en') > 0 ? 'en' : 'zh-CN'\n    E.i18nChangeLanguage(LANG)\n\n    // 定义最大长度\n    const MAX_LENGTH = 30\n\n    window.editor = E.createEditor({\n      selector: '#editor-text-area',\n      html: `<p>MaxLength: ${MAX_LENGTH}</p><p><br></p>`,\n      config: {\n        placeholder: 'Type here...',\n        maxLength: MAX_LENGTH,\n        onMaxLength(editor) {\n          alert('Trigger maxlength callback')\n        },\n        MENU_CONF: {\n          uploadImage: {\n            fieldName: 'your-fileName',\n            base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n          }\n        },\n        onChange(editor) {\n          console.log(editor.getHtml())\n\n          // 选中文字\n          const selectionText = editor.getSelectionText()\n          document.getElementById('selected-length').innerHTML = selectionText.length\n          // 全部文字\n          const text = editor.getText().replace(/\\n|\\r/mg, '')\n          document.getElementById('total-length').innerHTML = text.length\n        }\n      }\n    })\n\n    window.toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      config: {}\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/demo/multi-editor.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>wangEditor multi editor</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <!-- <link href=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\"> -->\n  <link href=\"https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\">\n  <link href=\"./css/layout.css\" rel=\"stylesheet\">\n  <link href=\"./css/view.css\" rel=\"stylesheet\">\n\n  <script src=\"./js/custom-elem.js\"></script>\n</head>\n\n<body>\n  <demo-nav title=\"wangEditor multi editor\"></demo-nav>\n  <div class=\"page-container\">\n    <div class=\"page-left\">\n      <demo-menu></demo-menu>\n    </div>\n    <div class=\"page-right\">\n\n      <div style=\"display: flex;\">\n        <div style=\"flex: 1\">\n          <div style=\"border: 1px solid #ccc; margin-right: 5px;\">\n            <div id=\"editor-toolbar-1\" style=\"border-bottom: 1px solid #ccc;\"></div>\n            <div id=\"editor-text-area-1\" style=\"height: 400px;\"></div>\n          </div>\n          <div id=\"content-view-1\" class=\"editor-content-view\"></div>\n        </div>\n        <div style=\"flex: 1\">\n          <div style=\"border: 1px solid #ccc; margin-left: 5px;\">\n            <div id=\"editor-toolbar-2\" style=\"border-bottom: 1px solid #ccc;\"></div>\n            <div id=\"editor-text-area-2\" style=\"height: 400px;\"></div>\n          </div>\n          <div id=\"content-view-2\" class=\"editor-content-view\"></div>\n        </div>\n      </div>\n\n    </div>\n  </div>\n\n  <!-- <script src=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/index.min.js\"></script> -->\n  <script src=\"https://unpkg.com/@wangeditor/editor@latest/dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    // 切换语言\n    const LANG = location.href.indexOf('lang=en') > 0 ? 'en' : 'zh-CN'\n    E.i18nChangeLanguage(LANG)\n\n    // 第一个编辑器\n    const editor1 = E.createEditor({\n      selector: '#editor-text-area-1',\n      config: {\n        placeholder: 'Type here...',\n        autoFocus: false,\n        MENU_CONF: {\n          uploadImage: {\n            fieldName: 'your-file-name1',\n            base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n          }\n        },\n        onChange(editor) {\n          document.getElementById('content-view-1').innerHTML = editor.getHtml()\n        }\n      },\n      html: '<p>editor1</p><p><br></p>'\n    })\n    const toolbar1 = E.createToolbar({\n      editor: editor1,\n      selector: '#editor-toolbar-1',\n      config: {}\n    })\n\n    // 第二个编辑器\n    const editor2 = E.createEditor({\n      selector: '#editor-text-area-2',\n      config: {\n        placeholder: 'Type here...',\n        autoFocus: false,\n        MENU_CONF: {\n          uploadImage: {\n            fieldName: 'your-file-name2',\n            base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n          }\n        },\n        onChange(editor) {\n          document.getElementById('content-view-2').innerHTML = editor.getHtml()\n        }\n      },\n      html: '<p>editor2</p><p><br></p>',\n      mode: 'simple'\n    })\n    const toolbar2 = E.createToolbar({\n      editor: editor2,\n      selector: '#editor-toolbar-2',\n      config: {}\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/demo/set-html.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>wangEditor set HTML</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <!-- <link href=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\"> -->\n  <link href=\"https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\">\n  <link href=\"./css/layout.css\" rel=\"stylesheet\">\n  <link href=\"./css/view.css\" rel=\"stylesheet\">\n\n  <script src=\"./js/custom-elem.js\"></script>\n</head>\n\n<body>\n  <demo-nav title=\"wangEditor set HTML\"></demo-nav>\n  <div class=\"page-container\">\n    <div class=\"page-left\">\n      <demo-menu></demo-menu>\n    </div>\n    <div class=\"page-right\">\n      <textarea id=\"editor-content-textarea\" style=\"width: 100%; height: 100px; outline: none;\"></textarea>\n      <div style=\"margin-top: 10px;\">\n        <button id=\"btn-set-html\">Set HTML</button>\n      </div>\n\n      <!-- 编辑器 DOM -->\n      <div style=\"border: 1px solid #ccc; margin-top: 20px;\">\n        <div id=\"editor-toolbar\" style=\"border-bottom: 1px solid #ccc;\"></div>\n        <div id=\"editor-text-area\" style=\"height: 350px\"></div>\n      </div>\n    </div>\n  </div>\n\n  <!-- <script src=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/index.min.js\"></script> -->\n  <script src=\"https://unpkg.com/@wangeditor/editor@latest/dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    // 切换语言\n    const LANG = location.href.indexOf('lang=en') > 0 ? 'en' : 'zh-CN'\n    E.i18nChangeLanguage(LANG)\n\n    window.editor = E.createEditor({\n      selector: '#editor-text-area',\n      html: '<p>编辑器创建时的默认内容。</p><p>Default content set when editor created.</p><p><br></p>',\n      config: {\n        placeholder: 'Type here...',\n        MENU_CONF: {\n          uploadImage: {\n            fieldName: 'your-fileName',\n            base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n          }\n        },\n        onChange(editor) {\n          const html = editor.getHtml()\n          console.log(html)\n        }\n      }\n    })\n\n    window.toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      config: {}\n    })\n\n    // textarea 初始化值\n    const textarea = document.getElementById('editor-content-textarea')\n    textarea.value = '<p>wangEditor 只识别 editor.getHtml() 生成的 html 格式，不可以随意自定义 html 代码（html 格式太灵活了，不会全部兼容）</p>\\n<p>wangEditor can only understand the HTML format from editor.getHtml() , but not all HTML formats.</p>\\n<p><br></p>'\n\n    // Set HTML\n    document.getElementById('btn-set-html').addEventListener('click', () => {\n      editor.setHtml(textarea.value)\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/demo/simple-mode.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>wangEditor simple mode</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <!-- <link href=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\"> -->\n  <link href=\"https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css\" rel=\"stylesheet\">\n  <link href=\"./css/layout.css\" rel=\"stylesheet\">\n\n  <script src=\"./js/custom-elem.js\"></script>\n</head>\n\n<body>\n  <demo-nav title=\"wangEditor simple mode\"></demo-nav>\n  <div class=\"page-container\">\n    <div class=\"page-left\">\n      <demo-menu></demo-menu>\n    </div>\n    <div class=\"page-right\">\n      <!-- 编辑器 DOM -->\n      <div style=\"border: 1px solid #ccc;\">\n        <div id=\"editor-toolbar\" style=\"border-bottom: 1px solid #ccc;\"></div>\n        <div id=\"editor-text-area\" style=\"height: 500px\"></div>\n      </div>\n\n      <!-- 内容状态 -->\n      <p style=\"background-color: #f1f1f1;\">\n        Text length: <span id=\"total-length\"></span>；\n        Selected text length: <span id=\"selected-length\"></span>；\n      </p>\n    </div>\n  </div>\n\n  <!-- <script src=\"https://cdn.jsdelivr.net/npm/@wangeditor/editor@latest/dist/index.min.js\"></script> -->\n  <script src=\"https://unpkg.com/@wangeditor/editor@latest/dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    // 切换语言\n    const LANG = location.href.indexOf('lang=en') > 0 ? 'en' : 'zh-CN'\n    E.i18nChangeLanguage(LANG)\n\n    // 默认内容\n    let html = `<h1>简洁模式：</h1><ol><li>简化工具栏菜单</li><li>取消选中文字的悬浮菜单</li></ol><p><br></p>`\n    if (LANG === 'en') html = `<h1>Simple&nbsp;mode.</h1><ol><li>Simplify&nbsp;toolbar&nbsp;menus</li><li>Hide&nbsp;hover-bar&nbsp;when&nbsp;selected&nbsp;text</li></ol><p><br></p>`\n\n    window.editor = E.createEditor({\n      selector: '#editor-text-area',\n      html,\n      mode: 'simple',\n      config: {\n        placeholder: 'Type here...',\n        MENU_CONF: {\n          uploadImage: {\n            fieldName: 'your-fileName',\n            base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n          }\n        },\n        onChange() {\n          console.log(editor.getHtml())\n\n          // 选中文字\n          const selectionText = editor.getSelectionText()\n          document.getElementById('selected-length').innerHTML = selectionText.length\n          // 全部文字\n          // 全部文字\n          const text = editor.getText().replace(/\\n|\\r/mg, '')\n          document.getElementById('total-length').innerHTML = text.length\n        }\n      }\n    })\n\n    window.toolbar = E.createToolbar({\n      editor,\n      mode: 'simple',\n      selector: '#editor-toolbar',\n      config: {}\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/examples/README.md",
    "content": "# examples\n\n- 本地测试\n- 提交 `master` 会发布到测试机\n"
  },
  {
    "path": "packages/editor/examples/batch-destroy.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>destroy demo</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <link href=\"./css/view.css\" rel=\"stylesheet\">\n  <link href=\"./css/editor.css\" rel=\"stylesheet\">\n\n  <link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n</head>\n\n<body>\n  <p>执行大量循环，频繁执行 创建/销毁 ，监控内存是否明显增加</p>\n  <p>循环次数 <input id=\"input-num\" type=\"number\" value=\"100\" /> <button id=\"btn-go\">开始</button></p>\n  <p>可使用 Chrome devTools 的 Performance 和 Memory 工具来检测 js 内存</p>\n  <!-- 编辑器 -->\n  <div style=\"width: 950px; margin: 0 auto;\">\n    <div id=\"editor-toolbar\" class=\"editor-toolbar\"></div>\n    <div id=\"editor-text-area\" class=\"editor-text-area\"></div>\n  </div>\n</body>\n\n<script src=\"js/init-content.js\"></script>\n<script src=\"../dist/index.js\"></script>\n<script>\n  (function () {\n    const E = window.wangEditor\n\n    document.getElementById('btn-go')?.addEventListener('click', () => {\n      let editor, toolbar\n\n      // ----------------------------- editor config -----------------------------\n      const editorConfig = {}\n      editorConfig.placeholder = '请输入内容'\n      editorConfig.onCreated = (editor) => {\n        console.log('on created', editor.id)\n      }\n      editorConfig.onDestroyed = (editor) => {\n        console.log('on destroyed', editor.id)\n      }\n\n      // ----------------------------- 获取 循环次数 -----------------------------\n      const maxNum = parseInt(document.getElementById('input-num').value)\n      if (!maxNum) {\n        console.error('循环次数，值非法 ', maxNum)\n        return\n      }\n      if (maxNum >= 1000) {\n        console.error('循环次数太多，耗时会比较长！')\n        return\n      }\n\n      // ----------------------------- 开始循环 -----------------------------\n      console.log('go...')\n      let num = 0\n      const intervalId = setInterval(() => {\n        if (num >= maxNum) {\n          clearInterval(intervalId)\n          return\n        }\n\n        if (editor == null) {\n          console.log('createCore', num)\n\n          // 先创建 editor 再创建 toolbar\n          editor = E.createEditor({\n            selector: '#editor-text-area',\n            config: editorConfig,\n            content: window.content1\n          })\n          toolbar = E.createToolbar({\n            editor,\n            selector: '#editor-toolbar'\n          })\n        } else {\n          console.log('destroyCore', num)\n          editor.destroy()\n          editor = null\n          num++ // 计数\n        }\n      }, 100)\n      console.log('end...')\n    })\n  })()\n</script>\n\n</html>"
  },
  {
    "path": "packages/editor/examples/check.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>check demo</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <link href=\"./css/view.css\" rel=\"stylesheet\">\n  <link href=\"./css/editor.css\" rel=\"stylesheet\">\n\n  <link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n</head>\n<body>\n  <p>视频、图片等资源校验，自定义 alert，XSS 预防</p>\n\n  <!-- 编辑器 -->\n  <div style=\"width: 950px; margin: 0 auto;\">\n    <div id=\"editor-toolbar\" class=\"editor-toolbar\"></div>\n    <div id=\"editor-text-area\" class=\"editor-text-area\"></div>\n  </div>\n\n  <script src=\"js/init-content.js\"></script>\n  <script src=\"../dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    const editorConfig = { MENU_CONF: {} }\n    editorConfig.placeholder = '请输入内容'\n    \n    editorConfig.customAlert = (info, type) => {\n      alert(`customAlert: \\n${type}:\\n${info}`)\n    }\n\n    editorConfig.MENU_CONF['uploadImage'] = {\n      fieldName: 'your-fileName',\n      base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n    }\n\n    editorConfig.MENU_CONF['insertImage'] = {\n      onInsertedImage(imageNode) {\n        console.log('inserted image', imageNode)\n      },\n      checkImage(src, alt, url) {\n        if (src.indexOf('http') !== 0) {\n          return '图片网址必须以 http/https 开头'\n        }\n        return true\n      },\n\n      // // 异步检查\n      // async checkImage(src, alt, url) {\n      //   return new Promise((resolve, reject) => {\n      //     setTimeout(() => {\n      //       if (src.indexOf('http') !== 0) {\n      //         resolve('图片网址必须以 http/https 开头')\n      //         return\n      //       }\n      //       resolve(true)\n      //     }, 1000)\n      //   })\n      // },\n\n      parseImageSrc(src) {\n        return src + '#123'\n      },\n\n      // // 异步转换\n      // async parseImageSrc(src) {\n      //   return new Promise((resolve, reject) => {\n      //     setTimeout(() => {\n      //       resolve(src + '#abc')\n      //     }, 1000)\n      //   })\n      // },\n    }\n    editorConfig.MENU_CONF['editImage'] = {\n      onUpdatedImage(imageNode) {\n        console.log('updated image', imageNode)\n      },\n      checkImage(src, alt, url) {\n        if (src.indexOf('http') !== 0) {\n          return '图片网址必须以 http/https 开头'\n        }\n        return true\n      },\n\n      // // 异步检查\n      // async checkImage(src, alt, url) {\n      //   return new Promise((resolve, reject) => {\n      //     setTimeout(() => {\n      //       if (src.indexOf('http') !== 0) {\n      //         resolve('图片网址必须以 http/https 开头')\n      //         return\n      //       }\n      //       resolve(true)\n      //     }, 1000)\n      //   })\n      // },\n\n      parseImageSrc(src) {\n        return src + '#123'\n      },\n    }\n    editorConfig.MENU_CONF['insertLink'] = {\n      checkLink(text, url) {\n        console.log('check insert link - ', text, url)\n\n        if (url.indexOf('http') !== 0) {\n          return '链接必须以 http/https 开头'\n        }\n        return true\n      },\n\n      // // 异步检查\n      // async checkLink(text, url) {\n      //   console.log('check insert link - ', text, url)\n\n      //   return new Promise((resolve, reject) => {\n      //     setTimeout(() => {\n      //       if (url.indexOf('http') !== 0) {\n      //         resolve('链接必须以 http/https 开头')\n      //         return\n      //       }\n      //       resolve(true)\n      //     }, 1000)\n      //   })\n      // },\n\n      parseLinkUrl(url) {\n        return url + '#123'\n      },\n\n      // // 异步转换\n      // async parseLinkUrl(url) {\n      //   console.log('parse insert link - ', url)\n\n      //   return new Promise((resolve, reject) => {\n      //     setTimeout(() => {\n      //       resolve(url + '#123')\n      //     }, 1000)\n      //   })\n      // },\n    }\n    editorConfig.MENU_CONF['editLink'] = {\n      checkLink(text, url) {\n        if (url.indexOf('http') !== 0) {\n          return '链接必须以 http/https 开头'\n        }\n        return true\n      },\n\n      // // 异步检查\n      // async checkLink(text, url) {\n      //   console.log('check insert link - ', text, url)\n\n      //   return new Promise((resolve, reject) => {\n      //     setTimeout(() => {\n      //       if (url.indexOf('http') !== 0) {\n      //         resolve('链接必须以 http/https 开头')\n      //         return\n      //       }\n      //       resolve(true)\n      //     }, 1000)\n      //   })\n      // },\n\n      parseLinkUrl(url) {\n        return url + '#123'\n      },\n    }\n    editorConfig.MENU_CONF['insertVideo'] = {\n      onInsertedVideo(videoNode) {\n        console.log('inserted video', videoNode)\n      },\n\n      checkVideo(src, poster) {\n        console.log('video src', src)\n        console.log('video poster', poster)\n        if (src.indexOf('http') !== 0) {\n          return '视频地址必须以 http/https 开头'\n        }\n        return true\n      },\n\n      // // 异步检查\n      // async checkVideo(src) {\n      //   return new Promise((resolve, reject) => {\n      //     setTimeout(() => {\n      //       if (src.indexOf('http') !== 0) {\n      //         resolve('视频地址必须以 http/https 开头')\n      //         return\n      //       }\n      //       resolve(true)\n      //     }, 1000)\n      //   })\n      // },\n\n      // 也支持 promise\n      parseVideoSrc(src) {\n        if (src.includes('.bilibili.com')) {\n          // 转换 bilibili url 为 iframe\n          const arr = location.pathname.split('/')\n          const vid = arr[arr.length - 1]\n          return `<iframe src=\"//player.bilibili.com/player.html?aid=421814407&bvid=${vid}\" scrolling=\"no\" border=\"0\" frameborder=\"no\" framespacing=\"0\" allowfullscreen=\"true\"> </iframe>`\n        }\n        return src\n      }\n    }\n\n    const editor = E.createEditor({\n      selector: '#editor-text-area',\n      content: window.content1,\n      config: editorConfig\n    })\n    const toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n    })\n  </script>\n</body>\n</html>"
  },
  {
    "path": "packages/editor/examples/code-highlight.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>code-highlight demo</title>\n\n  <!-- 引入 prism css -->\n  <link href=\"https://unpkg.com/prismjs@latest/themes/prism.css\" rel=\"stylesheet\">\n</head>\n<body>\n  <p>由 wangEditor 生成的代码，可支持代码高亮显示。使用 <a href=\"https://prismjs.com/\" target=\"_blank\">prism.js</a> ，支持多主题</p>\n  <p>【注意】异步设置 html 内容时，需要执行 <code>Prism.highlightAll()</code> <button id=\"btn-test\">测试一下</button></p>\n\n  <div>\n    <p>javascript</p>\n    <pre>\n      <code id=\"code1\" class=\"language-javascript\">const a = 100;\n      function fn(x) { return x + 10 };\n      // 注释\n      </code>\n    </pre>\n\n    <p>html</p>\n    <pre>\n      <code id=\"code2\" class=\"language-html\">&lt;div&gt;text1&lt;/div&gt;</code>\n    </pre>\n  </div>\n\n  <!-- 引入 prism js -->\n  <script src=\"https://unpkg.com/prismjs@latest/prism.js\"></script>\n  <script src=\"https://unpkg.com/prismjs@latest/components/prism-core.js\"></script>\n  <script src=\"https://unpkg.com/prismjs@latest/plugins/autoloader/prism-autoloader.js\"></script>\n  <script>\n    document.getElementById('btn-test').addEventListener('click', () => {\n      document.getElementById('code1').innerHTML = 'const b = 200;\\nfunction fn(y) { return y + 20 };\\n// comment'\n      document.getElementById('code2').innerHTML = '&lt;p&gt;text2&lt;/p&gt;'\n      Prism.highlightAll()\n    })\n  </script>\n</body>\n</html>"
  },
  {
    "path": "packages/editor/examples/content-to-html.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>content to html</title>\n  <link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n</head>\n<body>\n  <p>content to html</p>\n\n  <div style=\"margin-top: 10px;\">\n    <textarea id=\"text-content\" style=\"width: 100%; height: 300px;\">[\n  {\n    \"type\": \"paragraph\",\n    \"children\": [\n      {\n        \"text\": \"你好\"\n      }\n    ]\n  }\n]</textarea>\n  </div>\n\n  <button id=\"btn-convert\">convert</button>\n\n  <div style=\"margin-top: 20px;\">\n    <textarea id=\"text-html\" readonly style=\"width: 100%; height: 300px;\"></textarea>\n  </div>\n\n  <script src=\"../dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    document.getElementById('btn-convert').addEventListener('click', () => {\n      const contentStr = document.getElementById('text-content').value\n      const content = JSON.parse(contentStr)\n      const editor = E.createEditor({ content })\n      document.getElementById('text-html').innerHTML = editor.getHtml()\n    })\n  </script>\n</body>\n</html>"
  },
  {
    "path": "packages/editor/examples/css/editor.css",
    "content": "body {\n  margin: 0 10px;\n}\n\n.editor-toolbar {\n  border: 1px solid #ccc;\n}\n\n.editor-text-area {\n  border: 1px solid #ccc;\n  border-top: 0;\n  height: 400px;\n}"
  },
  {
    "path": "packages/editor/examples/css/view.css",
    "content": ".editor-content-view {\n  border: 1px solid #ccc;\n  padding: 10px;\n  margin-top: 30px;\n  overflow-x: auto;\n}\n\n.editor-content-view p,\n.editor-content-view li {\n  white-space: pre-wrap; /* 保留空格 */\n}\n\n.editor-content-view blockquote {\n  border-left: 8px solid #d0e5f2;\n  padding: 10px 10px;\n  margin: 10px 0;\n  background-color: #f1f1f1;\n}\n\n.editor-content-view code {\n  font-family: monospace;\n  background-color: #eee;\n  padding: 3px;\n  border-radius: 3px;\n}\n.editor-content-view pre>code {\n  display: block;\n  padding: 10px;\n}\n\n.editor-content-view table {\n  border-collapse: collapse;\n}\n.editor-content-view td,\n.editor-content-view th {\n  border: 1px solid #ccc;\n  min-width: 50px;\n  height: 20px;\n}\n.editor-content-view th {\n  background-color: #f1f1f1;\n}"
  },
  {
    "path": "packages/editor/examples/default-mode.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<meta charset=\"UTF-8\">\n<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>default mode</title>\n<link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n<link href=\"./css/view.css\" rel=\"stylesheet\">\n<link href=\"./css/editor.css\" rel=\"stylesheet\">\n\n<link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n</head>\n\n<body>\n  <div style=\"width: 1000px; margin: 0 auto;\">\n    <p>\n      <button id=\"btn-create\">create editor</button>\n      <button id=\"btn-toggle-enable\">disable/enable</button>\n      <button id=\"btn-destroy\">destroy editor</button>\n    </p>\n  \n    <!-- 编辑器 -->\n    <div>\n      <div id=\"editor-toolbar\" class=\"editor-toolbar\"></div>\n      <div id=\"editor-text-area\" class=\"editor-text-area\"></div>\n    </div>\n  \n    <!-- 内容状态 -->\n    <p style=\"background-color: #f1f1f1;\">\n      当前文字数量：<span id=\"total-length\"></span>；\n      选中文字数量：<span id=\"selected-length\"></span>；\n      选中文字：\"<span id=\"selected-text\"></span>\"\n    </p>\n  \n    <!-- 显示内容 -->\n    <div id=\"editor-content-view\" class=\"editor-content-view\"></div>\n  </div>\n\n  <script src=\"js/init-content.js\"></script>\n  <script src=\"../dist/index.js?t=14\"></script>\n  <script>\n    const E = window.wangEditor\n\n    const editorConfig = { MENU_CONF: {} }\n    // editorConfig.autoFocus = false\n    // editorConfig.readOnly = true\n    // editorConfig.scroll = false\n    editorConfig.placeholder = '请输入内容'\n    editorConfig.MENU_CONF['uploadImage'] = {\n      fieldName: 'your-fileName',\n      base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n    }\n    console.log('测试上传图片，请使用 upload-image.html')\n    editorConfig.onCreated = (editor) => {\n      console.log('on created', editor)\n    }\n    editorConfig.onChange = (editor) => {\n      // console.log(editor.children)\n\n      const html = editor.getHtml()\n      document.getElementById('editor-content-view').innerHTML = html\n\n      // 选中文字\n      const selectionText = editor.getSelectionText()\n      document.getElementById('selected-text').innerHTML = selectionText\n      document.getElementById('selected-length').innerHTML = selectionText.length\n      // 全部文字\n      document.getElementById('total-length').innerHTML = editor.getText().length\n\n      // // isSelectedAll\n      // console.log('isSelectedAll', editor.isSelectedAll())\n    }\n    editorConfig.onDestroyed = (editor) => {\n      console.log('on destroyed', editor)\n    }\n    editorConfig.onFocus = (editor) => {\n      console.log('onFocus', editor.isFocused())\n    }\n    editorConfig.onBlur = (editor) => {\n      console.log('onBlur', editor.isFocused())\n    }\n\n    // editorConfig.customPaste = (editor, event) => {\n    //   console.log('customPage')\n    //   // editor.insertText('xxx------') // 同步\n    //   setTimeout(() => {\n    //     editor.insertText('yyy------') // 异步\n    //   }, 1000)\n    //   return false // 阻止默认粘贴，自定义实现粘贴\n\n    //   // return true // 执行默认粘贴\n    // }\n\n    const toolbarConfig = {\n      // excludeKeys: ['uploadVideo'],\n    }\n\n    // create\n    let editor, toolbar\n    document.getElementById('btn-create').addEventListener('click', () => {\n      editor = E.createEditor({\n        selector: '#editor-text-area',\n        // selector: document.getElementById('editor-text-area'),\n        content: window.content1,\n        config: editorConfig\n      })\n\n      toolbar = E.createToolbar({\n        editor,\n        selector: '#editor-toolbar',\n        // selector: document.getElementById('editor-toolbar')\n        config: toolbarConfig\n      })\n    })\n    console.log(`如果页面没有编辑器，点击 'create' 按钮创建`)\n\n    // toggle enable\n    document.getElementById('btn-toggle-enable').addEventListener('mousedown', (e) => {\n      // 使用 mousedown ，且 preventDefault ，否则触发编辑器 blur ，导致无法正确 focus\n      // TODO 文档中说明（以及其他的 editState）\n      e.preventDefault()\n\n      if (editor.getConfig().readOnly) {\n        editor.enable()\n      } else {\n        editor.disable()\n      }\n    })\n\n    // destroy\n    document.getElementById('btn-destroy').addEventListener('click', () => {\n      editor.destroy()\n      editor = undefined\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/examples/dom7-demo.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>dom7 demo</title>\n</head>\n<body>\n  <p>dom7 demo</p>\n\n  <script src=\"https://unpkg.com/dom7@latest/dom7.js\"></script>\n  <script>\n    const $text = Dom7('<span data-a><b>hello</b></span>')\n    const $p = Dom7('<p style=\"line-height: 2.5; color: red;\"><span>行高文字 line-height</span></p>')\n    // Dom7('body').append(p)\n\n    const tableStr = `\n      <table border=\"0\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tbody><tr><th></th><th></th><th></th><th></th><th></th></tr><tr><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td></tr></tbody></table>\n    `\n    const $table = Dom7(tableStr)\n    const $tbody = $table.find('tbody')\n    const $tr = $table.find('tr')\n    $table.append($tr)\n    $tbody.remove()\n  </script>\n</body>\n</html>"
  },
  {
    "path": "packages/editor/examples/headers.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>headers demo</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <link href=\"./css/editor.css\" rel=\"stylesheet\">\n  <link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n</head>\n\n<body>\n  <p id=\"p1\">获取标题、滚动到标题</p>\n\n  <!-- 编辑器 -->\n  <div>\n    <div id=\"editor-toolbar\" class=\"editor-toolbar\"></div>\n    <div id=\"editor-text-area\" class=\"editor-text-area\"></div>\n  </div>\n\n  <!-- 显示 headers -->\n  <div style=\"margin-top: 20px;\">\n    <div style=\"margin-bottom: 10px;\">\n      <input id=\"input-id\" placeholder=\"header id\" />\n      <button id=\"btn-scroll-to\">scrollTo</button>\n    </div>\n    <textarea readonly id=\"text-headers\" style=\"width: 800px; height: 400px;\"></textarea>\n  </div>\n\n  <script src=\"https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js\"></script>\n  <script src=\"js/init-content.js\"></script>\n  <script src=\"../dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    const editorConfig = {}\n    editorConfig.onCreated = (editor) => {\n      console.log('on created', editor)\n    }\n    editorConfig.onChange = (editor) => {\n      // 获取并展示 headers\n      const headers = editor.getElemsByTypePrefix('header')\n      $('#text-headers').val(JSON.stringify(headers, null, 4))\n    }\n\n    // 先创建 editor ，再创建 toolbar\n    const editor = E.createEditor({\n      selector: '#editor-text-area',\n      config: editorConfig,\n      content: window.content1\n    })\n    const toolbar =E.createToolbar({\n      editor,\n      selector: '#editor-toolbar'\n    })\n    \n    // scrollTo\n    const $inputId = $('#input-id')\n    $('#btn-scroll-to').on('click', () => {\n      const id = $inputId.val()\n      editor.scrollToElem(id)\n      $inputId.val('')\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/examples/huge-doc.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>huge doc</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <link href=\"./css/editor.css\" rel=\"stylesheet\">\n\n  <link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n</head>\n<body>\n  <p>huge doc 大文件</p>\n\n  <!-- 编辑器 -->\n  <div>\n    <div id=\"editor-toolbar\" class=\"editor-toolbar\"></div>\n    <div id=\"editor-text-area\" class=\"editor-text-area\" style=\"height: 800px;\"></div>\n  </div>\n\n  <!-- 内容状态 -->\n  <p style=\"background-color: #f1f1f1;\">\n    当前文字数量：<span id=\"total-length\"></span>；\n    选中文字数量：<span id=\"selected-length\"></span>；\n    选中文字：\"<span id=\"selected-text\"></span>\"\n  </p>\n\n  <script src=\"./js/huge-content.js\"></script>\n  <script src=\"../dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    const editorConfig = { MENU_CONF: {} }\n    editorConfig.placeholder = '请输入内容'\n    editorConfig.onChange = (editor) => {\n      // 选中文字\n      const selectionText = editor.getSelectionText()\n      document.getElementById('selected-text').innerHTML = selectionText\n      document.getElementById('selected-length').innerHTML = selectionText.length\n      // 全部文字\n      document.getElementById('total-length').innerHTML = editor.getText().length\n    }\n\n    const editor = E.createEditor({\n      selector: '#editor-text-area',\n      content: window.content2,\n      config: editorConfig\n    })\n    const toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n    })\n  </script>\n</body>\n</html>"
  },
  {
    "path": "packages/editor/examples/i18n.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>i18n</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <link href=\"./css/view.css\" rel=\"stylesheet\">\n  <link href=\"./css/editor.css\" rel=\"stylesheet\">\n  <link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n</head>\n<body>\n  <p>i18n demo</p>\n\n  <!-- 编辑器 -->\n  <div>\n    <div id=\"editor-toolbar\" class=\"editor-toolbar\"></div>\n    <div id=\"editor-text-area\" class=\"editor-text-area\"></div>\n  </div>\n\n  <script src=\"js/init-content.js\"></script>\n  <script src=\"../dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    // // 添加新语言，如日语 ja\n    // E.i18nAddResources('ja', {\n    //     // 标题\n    //     header: {\n    //         title: 'ヘッダー',\n    //         text: 'テキスト',\n    //     },\n    //     // ... 其他语言词汇，下文说明 ...\n    // })\n    // // 切换为日语 ja\n    // E.i18nChangeLanguage('ja')\n\n    // 切换语言 'en' 'zh-CN'\n    E.i18nChangeLanguage('en')\n\n    // 使用多语言\n    console.log('使用多语言', E.t('editor.image'))\n\n    // editor 配置\n    const editorConfig = {}\n    editorConfig.placeholder = '请输入内容...'\n\n    const editor = E.createEditor({\n      selector: '#editor-text-area',\n      config: editorConfig,\n      content: window.content1,\n    })\n\n    const toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      config: {},\n    })\n  </script>\n</body>\n</html>"
  },
  {
    "path": "packages/editor/examples/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>wangEditor examples</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <style>\n    body {\n      margin: 20px;\n    }\n    ul {\n      padding-left: 20px;\n    }\n    li {\n      margin: 10px 0;\n    }\n  </style>\n</head>\n<body>\n  <h1>wangEditor examples</h1>\n  <ul>\n    <li><a href=\"./default-mode.html\">Default mode 默认模式</a></li>\n    <li><a href=\"./simple-mode.html\">Simple mode 简洁模式</a></li>\n    <li><a href=\"./parse-html.html\">Parse html 回显使用 html</a></li>\n    <li><a href=\"./menu.html\">Menu config 菜单配置</a></li>\n    <li><a href=\"./like-yuque.html\">Like QQ Doc 模仿腾讯文档编辑器</a></li>\n    <li><a href=\"./simple-mode.html\">Sync to textarea 同步到 textarea</a></li>\n    <li><a href=\"./maxlength.html\">Maxlength</a></li>\n    <li><a href=\"./upload-image.html\">Upload images 上传图片</a></li>\n    <li><a href=\"./upload-video.html\">Upload videos 上传视频</a></li>\n    <li><a href=\"./check.html\">Check callback and custom alert 资源校验、回调、自定义 alert</a></li>\n    <li><a href=\"./multi-editors.html\">Multi editors 多个编辑器</a></li>\n    <li><a href=\"./headers.html\">Get headers and scroll 获取标题/滚动到标题</a></li>\n    <li><a href=\"./huge-doc.html\">Huge doc 大文件（几万个字）</a></li>\n    <li><a href=\"./modal-appendTo-body.html\">Modal appendTo body</a></li>\n    <li><a href=\"./i18n.html\">i18n 国际化</a></li>\n    <li><a href=\"./theme.html\">Theme 主题</a></li>\n    <li><a href=\"./code-highlight.html\">Code highlight 代码高亮</a></li>\n    <li><a href=\"./shadow-dom.html\">Shadow DOM</a></li>\n    <li><a href=\"./batch-destroy.html\">Batch destroy, test memory leak 批量销毁，测试内存泄漏</a></li>\n    <li><a href=\"./content-to-html.html\">Content to html</a></li>\n    <li><a href=\"./new-menu.html\">New menu 新注册菜单</a></li>\n  </ul>\n</body>\n</html>\n"
  },
  {
    "path": "packages/editor/examples/js/huge-content.js",
    "content": ";(function () {\n  function deepClone(obj) {\n    const str = JSON.stringify(obj)\n    return JSON.parse(str)\n  }\n\n  const header = {\n    type: 'header1',\n    children: [\n      {\n        text: '水浒传简介',\n      },\n    ],\n  }\n  const text1 =\n    '全书通过描写梁山好汉反抗欺压、水泊梁山壮大和受宋朝招安，以及受招安后为宋朝征战，最终消亡的宏大故事，艺术地反映了中国历史上宋江起义从发生、发展直至失败的全过程，深刻揭示了起义的社会根源，满腔热情地歌颂了起义英雄的反抗斗争和他们的社会理想，也具体揭示了起义失败的内在历史原因。'\n  const text2 =\n    '《水浒传》是中国古典四大名著之一，问世后，在社会上产生了巨大的影响，成了后世中国小说创作的典范。《水浒传》是中国历史上最早用白话文写成的章回小说之一，流传极广，脍炙人口；同时也是汉语言文学中具备史诗特征的作品之一，对中国乃至东亚的叙事文学都有深远的影响。'\n  const p1 = {\n    type: 'paragraph',\n    children: [{ text: text1 }],\n  }\n  const p2 = {\n    type: 'paragraph',\n    children: [{ text: text2 }],\n  }\n  // const code = {\n  //   type: 'pre',\n  //   children: [\n  //     {\n  //       type: 'code',\n  //       language: 'javascript',\n  //       children: [{ text: 'const a = 100;' }],\n  //     },\n  //   ],\n  // }\n\n  // 拼接大文件\n  window.content2 = []\n  for (let i = 0; i < 370; i++) {\n    window.content2.push(deepClone(header))\n    window.content2.push(deepClone(p1))\n    window.content2.push(deepClone(p2))\n    // window.content2.push(deepClone(code))\n  }\n})()\n"
  },
  {
    "path": "packages/editor/examples/js/init-content.js",
    "content": "/**\n * @description demo 页，初始化内容\n * @author wangfupeng\n */\n\nwindow.content1 = [\n  {\n    type: 'header1',\n    textAlign: 'center',\n    children: [\n      {\n        text: '一行标题',\n      },\n    ],\n  },\n\n  {\n    type: 'paragraph',\n    children: [\n      { text: 'hello world ~~~ ' },\n      {\n        type: 'link',\n        url: 'https://www.slatejs.org/examples/links',\n        children: [{ text: 'slate examples' }],\n      },\n      { text: '!' },\n    ],\n  },\n  {\n    type: 'pre',\n    children: [\n      {\n        type: 'code',\n        language: 'javascript',\n        children: [{ text: 'const a = 100;' }],\n      },\n    ],\n  },\n  {\n    type: 'paragraph',\n    children: [\n      { text: '图片' },\n      {\n        type: 'image',\n        src: 'https://www.wangeditor.com/imgs/logo.png',\n        children: [{ text: '' }], // void node 要有一个空 text\n      },\n      { text: 'image' },\n    ],\n  },\n  {\n    type: 'paragraph',\n    children: [{ text: '结束' }],\n  },\n  {\n    type: 'paragraph',\n    children: [{ text: '一行文字' }],\n  },\n  {\n    type: 'header2',\n    children: [\n      {\n        text: '二级标题',\n      },\n    ],\n  },\n  {\n    type: 'pre',\n    children: [\n      {\n        type: 'code',\n        language: 'html',\n        children: [{ text: '<div>text</div>' }],\n      },\n    ],\n  },\n  {\n    type: 'paragraph',\n    children: [{ text: '一行文字' }],\n  },\n  {\n    type: 'paragraph',\n    children: [\n      { text: '一行文字' },\n      {\n        type: 'image',\n        src: 'https://www.baidu.com/img/flexible/logo/pc/result@2.png',\n        alt: '百度',\n        url: 'https://www.baidu.com/',\n        style: { width: '101px', height: '33px' },\n        children: [{ text: '' }], // void node 要有一个空 text\n      },\n      { text: '一行文字' },\n    ],\n  },\n  {\n    type: 'blockquote',\n    children: [{ text: '一行文字' }],\n  },\n  {\n    type: 'paragraph',\n    children: [{ text: '一行文字' }],\n  },\n  {\n    type: 'divider',\n    children: [{ text: '' }],\n  },\n  {\n    type: 'header3',\n    children: [\n      {\n        text: '三级标题',\n      },\n    ],\n  },\n  {\n    type: 'paragraph',\n    children: [{ text: '一行文字' }],\n  },\n  {\n    type: 'paragraph',\n    children: [{ text: '一行文字' }],\n  },\n]\n"
  },
  {
    "path": "packages/editor/examples/like-yuque.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>like yuque</title>\n\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n  <style>\n    html,\n    body {\n      background-color: #fff;\n      height: 100%;\n      overflow: hidden;\n      color: #333;\n    }\n\n    #top-container {\n      border-bottom: 1px solid #e8e8e8;\n      padding-left: 50px;\n    }\n\n    #editor-toolbar {\n      width: 1300px;\n      background-color: #FCFCFC;\n      margin: 0 auto;\n    }\n\n    #content {\n      height: calc(100% - 40px);\n      background-color: rgb(245, 245, 245);\n      overflow-y: auto;\n      position: relative;\n    }\n\n    #editor-container {\n      width: 850px;\n      margin: 30px auto 150px auto;\n      background-color: #fff;\n      padding: 20px 50px 50px 50px;\n      border: 1px solid #e8e8e8;\n      box-shadow: 0 2px 10px rgb(0 0 0 / 12%);\n    }\n\n    #title-container {\n      padding: 20px 0;\n      border-bottom: 1px solid #e8e8e8;\n    }\n\n    #title-container input {\n      font-size: 30px;\n      border: 0;\n      outline: none;\n      width: 100%;\n      line-height: 1;\n    }\n\n    #editor-text-area {\n      min-height: 900px;\n      margin-top: 20px;\n    }\n  </style>\n\n</head>\n\n<body>\n  <div id=\"top-container\">\n    <p>\n      <svg width=\"16\" height=\"16\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path\n          d=\"M648.533333 85.333333L853.333333 290.133333V938.666667H170.666667V85.333333h477.866666m34.133334-85.333333H85.333333v1024h853.333334V256l-256-256z\">\n        </path>\n        <path d=\"M256 341.333333h512v85.333334H256zM256 512h512v85.333333H256zM256 682.666667h512v85.333333H256z\">\n        </path>\n      </svg>\n      文章标题信息\n    </p>\n  </div>\n  <div style=\"border-bottom: 1px solid #e8e8e8;\">\n    <div id=\"editor-toolbar\"></div>\n  </div>\n  <div id=\"content\">\n    <div id=\"editor-container\">\n      <div id=\"title-container\">\n        <input value=\"请输入标题\">\n      </div>\n      <div id=\"editor-text-area\"></div>\n    </div>\n  </div>\n\n  <script src=\"js/init-content.js\"></script>\n  <script src=\"../dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    const editorConfig = { MENU_CONF: {} }\n    editorConfig.placeholder = '请输入内容'\n    editorConfig.scroll = false // 禁止编辑器滚动\n    editorConfig.MENU_CONF['uploadImage'] = {\n      fieldName: 'your-fileName',\n      base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n    }\n    editorConfig.onChange = (editor) => {\n      // console.log('content', editor.children)\n    }\n\n    // 先创建 editor\n    const editor = E.createEditor({\n      selector: '#editor-text-area',\n      content: window.content1,\n      config: editorConfig\n    })\n\n    // 创建 toolbar\n    const toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      config: {\n        excludeKeys: 'fullScreen',\n      }\n    })\n\n    // 点击空白处 focus 编辑器\n    document.getElementById('editor-text-area').addEventListener('click', e => {\n      if (e.target.id === 'editor-text-area') {\n        editor.blur()\n        editor.focus(true) // focus 到末尾\n      }\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/examples/maxlength.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>maxLength demo</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <link href=\"./css/view.css\" rel=\"stylesheet\">\n  <link href=\"./css/editor.css\" rel=\"stylesheet\">\n\n  <link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n</head>\n<body>\n  <p>maxLength（慎用，可能影响性能）</p>\n\n  <!-- 编辑器 -->\n  <div>\n    <div id=\"editor-toolbar\" class=\"editor-toolbar\"></div>\n    <div id=\"editor-text-area\" class=\"editor-text-area\"></div>\n  </div>\n\n  <!-- 内容状态 -->\n  <p style=\"background-color: #f1f1f1;\">\n    当前文字数量：<span id=\"total-length\"></span>；\n    选中文字数量：<span id=\"selected-length\"></span>；\n    选中文字：\"<span id=\"selected-text\"></span>\"\n  </p>\n\n  <!-- 显示内容 -->\n  <div id=\"editor-content-view\" class=\"editor-content-view\"></div>\n\n  <script src=\"js/init-content.js\"></script>\n  <script src=\"../dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    const editorConfig = { MENU_CONF: {} }\n    editorConfig.placeholder = '请输入内容'\n\n    editorConfig.maxLength = 100 // 慎用，可能影响性能，需在文档中说明\n    editorConfig.onMaxLength = (editor) => {\n      console.log('onMaxLength')\n    }\n\n    editorConfig.onChange = (editor) => {\n      const html = editor.getHtml()\n      document.getElementById('editor-content-view').innerHTML = html\n\n      // 选中文字\n      const selectionText = editor.getSelectionText()\n      document.getElementById('selected-text').innerHTML = selectionText\n      document.getElementById('selected-length').innerHTML = selectionText.length\n      // 全部文字\n      document.getElementById('total-length').innerHTML = editor.getText().length\n    }\n\n    const editor = E.createEditor({\n      selector: '#editor-text-area',\n      // content: window.content1,\n      config: editorConfig\n    })\n    const toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n    })\n  </script>\n</body>\n</html>"
  },
  {
    "path": "packages/editor/examples/menu.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>menu config demo</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <link href=\"./css/view.css\" rel=\"stylesheet\">\n  <link href=\"./css/editor.css\" rel=\"stylesheet\">\n\n  <link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n</head>\n<body>\n  <p>menu 配置，包括：hoverbar toolbar</p>\n\n  <!-- 编辑器 -->\n  <div>\n    <div id=\"editor-toolbar\" class=\"editor-toolbar\"></div>\n    <div id=\"editor-text-area\" class=\"editor-text-area\"></div>\n  </div>\n\n  <script src=\"js/init-content.js\"></script>\n  <script src=\"../dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n    const DomEditor = window.wangEditor.DomEditor\n\n    const editorConfig = { MENU_CONF: {} }\n    editorConfig.placeholder = '请输入内容'\n\n    editorConfig.hoverbarKeys = {\n      text: {\n        menuKeys: ['bold', 'insertLink'],\n      },\n      'link': {\n        menuKeys: ['editLink', 'unLink', 'viewLink'],\n      },\n      'image': {\n        menuKeys: [\n          'imageWidth30',\n          'imageWidth50',\n          'imageWidth100',\n          'editImage',\n          'viewImageLink',\n          'deleteImage',\n        ],\n      }\n      // 其他参考 https://github.com/wangeditor-team/wangEditor/blob/master/packages/editor/src/init-default-config/config/hoverbar.ts\n    }\n\n    // 各个菜单的配置\n    editorConfig.MENU_CONF['color'] = {\n      colors: ['#000', '#333', '#999', '#ccc']\n    }\n    editorConfig.MENU_CONF['fontSize'] = {\n      fontSizeList: ['12px', '16px', '24px', '40px']\n    }\n    // 其他菜单配置项可通过 editor.getMenuConfig(menuKey) 查询，然后使用 editorConfig.MENU_CONF[menuKey] 修改\n\n    const editor = E.createEditor({\n      selector: '#editor-text-area',\n      content: window.content1,\n      config: editorConfig\n    })\n    const toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      config: {\n        toolbarKeys: [\n          '|', // 第一个是 `|` 不显示\n          'headerSelect',\n          'blockquote',\n          '|', '|', '|', // 多个紧挨者的 `|` 只显示一个\n          'bold',\n          'underline',\n          'italic',\n          {\n            key: 'group-more-style', // 以 group 开头\n            title: '更多样式',\n            iconSvg: '<svg viewBox=\"0 0 1024 1024\"><path d=\"M204.8 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z\"></path><path d=\"M505.6 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z\"></path><path d=\"M806.4 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z\"></path></svg>',\n            menuKeys: ['through', 'code'],\n          },\n          'color',\n          'bgColor',\n          'clearStyle',\n          '|',\n          'fontSize',\n          'fontFamily',\n          'lineHeight',\n          '|',\n          'undo',\n          'redo',\n          '|', // 最后一个是 `|` 不显示\n        ],\n        // insertKeys: {\n        //   index: 5,\n        //   keys: ['insertImage', 'insertVideo']\n        // },\n        // excludeKeys: ['headerSelect', 'underline', 'clearStyle', 'fontFamily', 'group-image']\n      },\n    })\n  </script>\n</body>\n</html>"
  },
  {
    "path": "packages/editor/examples/modal-appendTo-body.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>modal appendTo body - demo</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <link href=\"./css/view.css\" rel=\"stylesheet\">\n  <link href=\"./css/editor.css\" rel=\"stylesheet\">\n  <link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n\n  <style>\n    #mask {\n      position: fixed;\n      top: 0;\n      left: 0;\n      right: 0;\n      bottom: 0;\n      z-index: 999;\n      background-color: #00000073;\n      display: none;\n    }\n  </style>\n</head>\n\n<body>\n  <p>\n    modal appendTo body\n  </p>\n\n  <div>\n    <div id=\"editor-toolbar\" class=\"editor-toolbar\"></div>\n    <div id=\"editor-text-area\" class=\"editor-text-area\"></div>\n  </div>\n\n  <!-- mask div 蒙层 -->\n  <div id=\"mask\"></div>\n\n  <script src=\"https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js\"></script>\n  <script src=\"js/init-content.js\"></script>\n  <script src=\"../dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    const editorConfig = { MENU_CONF: {} }\n\n    const toolbarConfig = {\n      modalAppendToBody: true\n    }\n\n    const editor = E.createEditor({\n      selector: '#editor-text-area',\n      content: window.content1,\n      config: editorConfig\n    })\n\n    const toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      config: toolbarConfig\n    })\n\n    editor.on('modalOrPanelShow', modalOrPanel => {\n      if (modalOrPanel.type !== 'modal') return\n\n      const { $elem } = modalOrPanel // modal element\n      const width = $elem.width()\n      const height = $elem.height()\n\n      // set modal position z-index\n      $elem.css({\n        left: '50%',\n        top: '50%',\n        marginLeft: `-${width / 2}px`,\n        marginTop: `-${height / 2}px`,\n        zIndex: 1000\n      })\n\n      // show mask div\n      document.getElementById('mask').style.display = 'block'\n    })\n    editor.on('modalOrPanelHide', () => {\n      console.log('hide')\n\n      // hide mask div\n      document.getElementById('mask').style.display = 'none'\n    })\n    // click mask div to hide modal\n    document.getElementById('mask').addEventListener('click', () => {\n      editor.hidePanelOrModal()\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/examples/multi-editors.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>多编辑器 demo</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <link href=\"./css/view.css\" rel=\"stylesheet\">\n  <link href=\"./css/editor.css\" rel=\"stylesheet\">\n  <style>\n    .container {\n      display: flex;\n    }\n    .container-item {\n      flex: 1;\n      margin: 0 5px;\n      max-width: 50%;\n    }\n  </style>\n  <link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n</head>\n<body>\n  <p>多编辑器 demo</p>\n\n  <div class=\"container\">\n    <div class=\"container-item\">\n      <div id=\"editor-toolbar-1\" class=\"editor-toolbar\"></div>\n      <div id=\"editor-text-area-1\" class=\"editor-text-area\"></div>\n      <div id=\"content-view-1\" class=\"editor-content-view\"></div>\n    </div>\n    <div class=\"container-item\">\n      <div id=\"editor-toolbar-2\" class=\"editor-toolbar\"></div>\n      <div id=\"editor-text-area-2\" class=\"editor-text-area\"></div>\n      <div id=\"content-view-2\" class=\"editor-content-view\"></div>\n    </div>\n  </div>\n\n  <script src=\"js/init-content.js\"></script>\n  <script src=\"../dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    // --------------------- editor1 ---------------------\n    const editorConfig1 = { MENU_CONF: {} }\n    editorConfig1.placeholder = '请输入内容1...'\n    editorConfig1.MENU_CONF['uploadImage'] = {\n      base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n    }\n    editorConfig1.onChange = (editor) => {\n      Promise.resolve().then(() => {\n        document.getElementById('content-view-1').innerHTML = editor1.getHtml()\n      })\n    }\n\n    const editor1 = E.createEditor({\n      selector: '#editor-text-area-1',\n      config: editorConfig1,\n      content: [{ type: 'paragraph', children: [{ text: '编辑器1' }] }]\n    })\n    const toolbar1 = E.createToolbar({\n      editor: editor1,\n      selector: '#editor-toolbar-1',\n      config: {}\n    })\n\n\n    // --------------------- editor2 ---------------------\n    const editorConfig2 = { MENU_CONF: {} }\n    editorConfig2.placeholder = '请输入内容2...'\n    editorConfig2.hoverbarKeys = {\n      // 禁用部分 hoverbar\n      'link': [],\n      'table': []\n    }\n    editorConfig2.MENU_CONF['uploadImage'] = {\n      fieldName: 'your-fileName',\n      base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n    }\n    editorConfig2.onChange = (editor) => {\n      Promise.resolve().then(() => {\n        document.getElementById('content-view-2').innerHTML = editor2.getHtml()\n      })\n    }\n\n    const editor2 = E.createEditor({\n      selector: '#editor-text-area-2',\n      config: editorConfig2,\n      content: [{ type: 'paragraph', children: [{ text: '编辑器2' }] }],\n      mode: 'simple'\n    })\n    const toolbar2 = E.createToolbar({\n      editor: editor2,\n      selector: '#editor-toolbar-2',\n      mode: 'simple'\n    })\n  </script>\n</body>\n</html>"
  },
  {
    "path": "packages/editor/examples/new-menu.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>New menu</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <link href=\"./css/editor.css\" rel=\"stylesheet\">\n  <link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n</head>\n<body>\n  <p>New menu</p>\n  <p>\n    <button id=\"btn-create\">create editor</button>\n    <button id=\"btn-destroy\">destroy editor</button>\n  </p>\n\n  <div>\n    <div id=\"editor-toolbar\" class=\"editor-toolbar\"></div>\n    <div id=\"editor-text-area\" class=\"editor-text-area\"></div>\n  </div>\n\n  <script src=\"js/init-content.js\"></script>\n  <script src=\"../dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    // ---------- 注册新菜单 start ----------\n    class MyButtonMenu {\n        constructor() {\n            this.title = 'menu1',\n            this.tag = 'button'\n        }\n        getValue() { return '' }\n        isActive() { return false }\n        isDisabled() { return false }\n        exec(editor) {\n            console.log(editor)\n            alert('menu1 exec')\n        }\n    }\n    const menuConf = {\n      key: 'my-menu-1', // menu key ，唯一。注册之后，需通过 toolbarKeys 配置到工具栏\n      factory() {\n        return new MyButtonMenu()\n      },\n    }\n    E.Boot.registerMenu(menuConf)\n    // ---------- 注册新菜单 end ----------\n\n    // editor 配置\n    const editorConfig = {\n      placeholder: '请输入内容...',\n      onChange(editor) {}\n    }\n\n    // toolbar 配置\n    const toolbarConfig = {\n        // toolbarKeys: ['headerSelect', 'bold', 'my-menu-1'],\n        // excludeKeys: [],\n        insertKeys: {\n            index: 3,\n            keys: 'my-menu-1'\n        }\n    }\n\n    let editor, toolbar\n\n    // create\n    document.getElementById('btn-create').addEventListener('click', () => {\n      editor = E.createEditor({\n        selector: '#editor-text-area',\n        config: editorConfig\n      })\n\n      toolbar = E.createToolbar({\n        editor,\n        selector: '#editor-toolbar',\n        config: toolbarConfig,\n      })\n    })\n\n    // destroy\n    document.getElementById('btn-destroy').addEventListener('click', () => {\n      editor.destroy()\n      editor = null\n    })\n  </script>\n</body>\n</html>"
  },
  {
    "path": "packages/editor/examples/parse-html.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>回显 html</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <link href=\"./css/editor.css\" rel=\"stylesheet\">\n  <link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n</head>\n<body>\n  <p style=\"background-color: #ccc; padding: 5px 0;\"><b>回显 html</b> - 使用 html 初始化编辑器内容</p>\n\n  <div style=\"margin: 10px 0;\">\n    <button id=\"btn-v4-html\">填入 V4 html 示例</button>\n    <button id=\"btn-v5-html\">填入 V5 html 示例</button>\n    <span style=\"color: red;\"><b>【注意】只识别从 wangEditor 生成的 html</b> ，不可以随意自定义 html 代码（html 格式太灵活了，不会全部兼容）</span>\n  </div>\n  <textarea id=\"text-html\" style=\"width: 100%; height: 300px; font-size: 12px;\" placeholder=\"输入 html 然后创建编辑器\">&lt;p&gt;hello world&lt;/p&gt;</textarea>\n\n  <div style=\"margin: 10px 0;\">\n    <button id=\"btn-create\">创建编辑器</button>\n    <button id=\"btn-set-html\">setHtml</button>\n    （如有 JS 报错，再次创建时要刷新页面）\n  </div>\n  <div>\n    <div id=\"editor-toolbar\" class=\"editor-toolbar\"></div>\n    <div id=\"editor-text-area\" class=\"editor-text-area\"></div>\n  </div>\n\n  <script src=\"../dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n    const textarea = document.getElementById('text-html')\n    let editor\n\n    // 创建编辑器\n    document.getElementById('btn-create').addEventListener('click', () => {\n      if (editor) editor.destroy()\n\n      const html = textarea.value\n      editor = E.createEditor({\n        selector: '#editor-text-area',\n        html,\n      })\n      E.createToolbar({\n        editor,\n        selector: '#editor-toolbar',\n      })\n    })\n\n      // setHtml\n      document.getElementById('btn-set-html').addEventListener('click', () => {\n        if (!editor) alert('editor 尚未创建')\n\n        const html = textarea.value\n        editor.setHtml(html)\n      })\n\n    // 填入 v4 html\n    document.getElementById('btn-v4-html').addEventListener('click', () => {\n      textarea.value = `<p>你好&nbsp;<font size=\"6\">世界</font> <font face=\"黑体\">黑体文字</font>！</p>\n<p>欢迎<font color=\"#eeece0\">使用</font> <b>wangEditor</b> <span style=\"background-color: rgb(139, 170, 74);\">富文本</span>编辑器</p>\n<h1 id=\"94fco\">标题</h1>\n<blockquote><p>欢迎使用 <b>wangEditor</b> 富文本编辑器</p></blockquote>\n<p>欢迎<i>使用</i> <b>wangEditor</b><u>富文本</u><strike>编辑器</strike>~</p>\n<p style=\"padding-left:2em;\">缩进&nbsp;<a href=\"https://github.com/wangeditor-team\" target=\"_blank\">链接</a></p>\n<p data-we-empty-p=\"\" style=\"line-height:3;\">行高&nbsp;</p><p>\n  <img src=\"https://www.wangeditor.com/imgs/logo.png\"/></p>\n<p style=\"text-align:center;\">欢迎使用 <b>wangEditor</b> 富文本编辑器</p>\n<ol>\n  <li>abc</li>\n  <li>def</li>\n</ol>\n<ul><li>123</li><li>123</li></ul>\n<p>\n  <img src=\"https://www.wangeditor.com/imgs/logo.png\" alt=\"111\" data-href=\"https%3A%2F%2Fgithub.com%2Fwangeditor-team\" style=\"max-width:100%;\" contenteditable=\"false\"/>\n</p>\n<table border=\"0\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n  <tbody>\n    <tr><th>是的</th><th>你好</th><th>世界</th></tr><tr><td>aaa</td><td></td><td></td></tr>\n    <tr><td>bb</td><td>33</td><td></td></tr><tr><td></td><td></td><td>55</td></tr>\n    <tr><td></td><td></td><td></td></tr>\n  </tbody>\n</table>\n<p>100</p>\n<p><video src=\"https://media.w3.org/2010/05/sintel/trailer.mp4\" controls=\"\"></video></p>\n<p>200</p>\n<p><iframe src=\"//player.bilibili.com/player.html?aid=250348909&amp;bvid=BV1Pv411w7Xr&amp;cid=401518678&amp;page=1\" scrolling=\"no\" border=\"0\" frameborder=\"no\" framespacing=\"0\" allowfullscreen=\"true\"> </iframe></p>\n<p>123123 🤣😎</p>\n<pre><code class=\"JavaScript\"><xmp>const a = 100;\nfunction fn() {\n  return a;\n}</xmp></code></pre>\n<p><br/></p>\n<ul class=\"w-e-todo\"><li><span contenteditable=\"false\"><input type=\"checkbox\"/></span>吃饭</li></ul>\n<ul class=\"w-e-todo\"><li><span contenteditable=\"false\"><input type=\"checkbox\" checked=\"true\"/></span>睡觉<b>11</b></li></ul>\n<ul class=\"w-e-todo\"><li><span contenteditable=\"false\"><input type=\"checkbox\" checked=\"true\"/></span>打豆豆</li></ul>\n<p><br/></p>`\n    })\n\n    // 填入 v5 html\n    document.getElementById('btn-v5-html').addEventListener('click', () => {\n      textarea.value = `<h1>标题1</h1>\n<h3>标题3<br>-换行1<br>-换行2<br>-换行3</h3>\n<p><br></p>\n<blockquote>引用文字<strong>123</strong><br>换行</blockquote>\n<p>hello&nbsp;<span style=\"color: rgb(235, 144, 58);\"><i><u>多样式&nbsp;</u></i></span><strong>word</strong>~😊😬</p>\n<p>\n  <span>span包裹1&nbsp;</span>\n  <span><strong>span包裹2&nbsp;</strong></span>\n  <span style=\"color: rgb(235, 144, 58);\"><strong><u>span包裹3</u></strong></span>\n</p>\n<hr>\n<p>\n  <a href=\"https://www.baidu.com/\" target=\"_blank\">百度</a>\n  &nbsp;\n  <code>var</code>\n</p>\n<p style=\"line-height: 2.5;\">行高文字 line-height</p>\n<p style=\"text-align: center;\">对齐方式 text-align</p>\n<p style=\"padding-left: 32px;\">增加缩进 indent 1</p>\n<p style=\"text-indent: 2em;\">增加缩进 indent 2</p>\n<p>\n  <img src=\"https://www.baidu.com/img/flexible/logo/pc/result.png\"\n    alt=\"google\"\n    data-href=\"https://www.google.com.hk/\"\n    style=\"width: 217.00px;height: 70.75px;\"\n  />\n</p>\n<p>\n  <span style=\"font-size: 19px;\">设置字号</span>\n  <span>&nbsp;</span>\n  <span style=\"font-family: 标楷体;\">设置字体</span>\n</p>\n<p><span style=\"color: rgb(235, 144, 58);\">颜色</span>&nbsp;<span style=\"background-color: rgb(255, 236, 61);\">颜色</span>&nbsp;<span style=\"color: rgb(217, 217, 217); background-color: rgb(54, 88, 226);\">颜色</span></p>\n<pre><code class=\"language-javascript\">const a = 10;\nfunction fn() {\n  // 代码1\n}</code></pre>\n<p><br></p>\n<pre><code class=\"language-javascript\"><span>const a = 10;\nfunction fn() {\n  // 代码2\n}</span></code></pre>\n<ul>\n  <li>项目A</li>\n  <li>项目B</li>\n  <li>项目C&nbsp;<strong>word</strong>~</li>\n</ul>\n<ol>\n  <li>项目1&nbsp;<a href=\"https://www.baidu.com/\" target=\"_blank\">百度</a></li>\n  <li>项目2</li>\n  <li>项目3</li>\n</ol>\n<div data-w-e-type=\"video\" data-w-e-is-void>\n  <iframe src=\"//player.bilibili.com/player.html?aid=250348909&bvid=BV1Pv411w7Xr&cid=401518678&page=1\" scrolling=\"no\" border=\"0\" frameborder=\"no\" framespacing=\"0\" allowfullscreen=\"true\"> </iframe>\n</div>\n<div data-w-e-type=\"video\" data-w-e-is-void>\n  <video controls=\"true\"><source src=\"https://www.w3school.com.cn/i/movie.ogg\" type=\"video/mp4\"/></video>\n</div>\n<p><br></p>\n<table style=\"width: 100%;\">\n  <tbody>\n    <tr><th colSpan=\"1\" rowSpan=\"1\">a<br>a1<br>a2</th><th colSpan=\"1\" rowSpan=\"1\"><span>b</span></th><th colSpan=\"1\" rowSpan=\"1\"><span>c</span></th></tr>\n    <tr><td colSpan=\"1\" rowSpan=\"1\"><span style=\"color: rgb(235, 144, 58);\"><strong>1<br>2<br>3<strong></span></td><td colSpan=\"1\" rowSpan=\"1\"><span>2</span></td><td colSpan=\"1\" rowSpan=\"1\"><span>3</span></td></tr>\n  </tbody>\n</table>\n<p><br></p>\n<div data-w-e-type=\"todo\"><input type=\"checkbox\" disabled >吃饭</div>\n<div data-w-e-type=\"todo\"><input type=\"checkbox\" disabled checked>睡觉</div>\n<div data-w-e-type=\"todo\"><input type=\"checkbox\" disabled checked>看电影</div>\n<div>\n  其他标签\n</div>\n`\n    })\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "packages/editor/examples/shadow-dom.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<meta charset=\"UTF-8\">\n<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>shadow dom</title>\n<link href=\"./css/view.css\" rel=\"stylesheet\">\n</head>\n\n<body>\n  <div style=\"width: 1000px; margin: 0 auto;\">\n    <p>\n      <button id=\"btn-create\">create editor</button>\n      <button id=\"btn-toggle-enable\">disable/enable</button>\n      <button id=\"btn-destroy\">destroy editor</button>\n      <button id=\"btn-wang-editor-dom\">remove wang-editor dom</button>\n    </p>\n\n    <!-- 编辑器 -->\n    <div id=\"editor-container\">\n      <wang-editor></wang-editor>\n    </div>\n\n    <!-- 内容状态 -->\n    <p style=\"background-color: #f1f1f1;\">\n      当前文字数量：<span id=\"total-length\"></span>；\n      选中文字数量：<span id=\"selected-length\"></span>；\n      选中文字：\"<span id=\"selected-text\"></span>\"\n    </p>\n\n    <!-- 显示内容 -->\n    <div id=\"editor-content-view\" class=\"editor-content-view\"></div>\n  </div>\n\n  <script src=\"js/init-content.js\"></script>\n  <script src=\"../dist/index.js\"></script>\n  <script>\n    let editor, toolbar\n    const editorContainer = document.querySelector('#editor-container')\n\n    const editorConfig = { MENU_CONF: {} }\n    // editorConfig.autoFocus = false\n    // editorConfig.readOnly = true\n    // editorConfig.scroll = false\n    editorConfig.placeholder = '请输入内容'\n    editorConfig.MENU_CONF['uploadImage'] = {\n      fieldName: 'your-fileName',\n      base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n    }\n    console.log('测试上传图片，请使用 upload-image.html')\n    editorConfig.onCreated = (editor) => {\n      console.log('on created', editor)\n    }\n    editorConfig.onChange = (editor) => {\n      const html = editor.getHtml()\n      document.getElementById('editor-content-view').innerHTML = html\n\n      // 选中文字\n      const selectionText = editor.getSelectionText()\n      document.getElementById('selected-text').innerHTML = selectionText\n      document.getElementById('selected-length').innerHTML = selectionText.length\n      // 全部文字\n      document.getElementById('total-length').innerHTML = editor.getText().length\n    }\n    editorConfig.onDestroyed = (editor) => {\n      console.log('on destroyed', editor)\n    }\n    editorConfig.onFocus = (editor) => {\n      console.log('onFocus', editor)\n    }\n    editorConfig.onBlur = (editor) => {\n      console.log('onBlur', editor)\n    }\n\n    class WangEditorElement extends HTMLElement {\n      constructor() {\n        super()\n        this.attachShadow({ mode: 'open' })\n        this.shadowRoot.innerHTML = `\n          <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n          <link href=\"./css/editor.css\" rel=\"stylesheet\">\n          <link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n          <div>\n            <div id=\"editor-toolbar\" class=\"editor-toolbar\"></div>\n            <div id=\"editor-text-area\" class=\"editor-text-area\"></div>\n          </div>\n        `\n      }\n      create() {\n        // 创建编辑器\n        editor = window.wangEditor.createEditor({\n          selector: this.shadowRoot.querySelector('#editor-text-area'),\n          config: editorConfig,\n          content: [], // 默认内容，下文有解释\n          mode: 'default' // 或者 'simple' ，下文有解释\n        })\n\n        // 创建工具栏\n        toolbar = window.wangEditor.createToolbar({\n          editor,\n          selector: this.shadowRoot.querySelector('#editor-toolbar'),\n          mode: 'default' // 或者 'simple' ，下文有解释\n        })\n\n        this.editor = editor\n        this.toolbar = toolbar\n      }\n\n      destroy() {\n        this.editor.destroy()\n      }\n    }\n\n    // customElements define\n    let wangEditorExample\n    customElements.define('wang-editor', class WangEditor extends WangEditorElement {\n      constructor() {\n        super()\n        wangEditorExample = this\n      }\n\n      connectedCallback() {\n        console.log('首次被插入文档DOM时调用。');\n        this.create()\n      }\n\n      disconnectedCallback() {\n        console.log('从文档DOM中删除时调用');\n        this.destroy()\n      }\n\n      adoptedCallback() {\n        console.log('移动到新的文档时调用');\n      }\n\n      attributeChangedCallback() {\n        console.log('增加、删除、修改自身属性被调用');\n      }\n    })\n\n\n    // create\n    document.getElementById('btn-create').addEventListener('click', () => {\n      // wangEditorExample.create()\n      if (editorContainer.children.length) {\n        wangEditorExample.create()\n      } else {\n        editorContainer.append(document.createElement('wang-editor'))\n      }\n    })\n    console.log(`如果页面没有编辑器，点击 'create' 按钮创建`)\n\n    // toggle enable\n    document.getElementById('btn-toggle-enable').addEventListener('mousedown', (e) => {\n      // 使用 mousedown ，且 preventDefault ，否则触发编辑器 blur ，导致无法正确 focus\n      // TODO 文档中说明（以及其他的 editState）\n      e.preventDefault()\n\n      if (editor.getConfig().readOnly) {\n        editor.enable()\n      } else {\n        editor.disable()\n      }\n    })\n\n    // destroy\n    document.getElementById('btn-destroy').addEventListener('click', () => {\n      editor.destroy()\n      editor = undefined\n    })\n\n    // btn-wang-editor-dom\n    document.getElementById('btn-wang-editor-dom').addEventListener('click', () => {\n      const editorElement = document.querySelector('wang-editor')\n      if (editorElement) {\n        editorElement.parentElement.removeChild(editorElement)\n      }\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/examples/simple-mode.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>simple mode</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <link href=\"./css/view.css\" rel=\"stylesheet\">\n  <link href=\"./css/editor.css\" rel=\"stylesheet\">\n  <link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n</head>\n<body>\n  <p>simple mode</p>\n  <p>\n    <b>bold</b>\n    <a href=\"./default-mode.html\">default mode</a>\n  </p>\n\n  <!-- 编辑器 -->\n  <div>\n    <div id=\"editor-toolbar\" class=\"editor-toolbar\"></div>\n    <div id=\"editor-text-area\" class=\"editor-text-area\"></div>\n  </div>\n\n  <div style=\"margin-top: 20px;\">\n    <textarea id=\"text-content\" readonly style=\"width: 100%; height: 500px;\"></textarea>\n  </div>\n\n  <script src=\"js/init-content.js\"></script>\n  <script src=\"../dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    // editor 配置\n    const editorConfig = {}\n    editorConfig.placeholder = '请输入内容123...'\n    editorConfig.onChange = (editor) => {\n      const contentStr = JSON.stringify(editor.children, null, 2)\n      document.getElementById('text-content').innerHTML = contentStr\n      console.log(editor.getHtml())\n    }\n\n    const editor = E.createEditor({\n      selector: '#editor-text-area',\n      config: editorConfig,\n      html: '<p>hello</p>',\n      mode: 'simple'\n    })\n\n    const toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      config: {},\n      mode: 'simple'\n    })\n  </script>\n</body>\n</html>"
  },
  {
    "path": "packages/editor/examples/theme.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Theme demo</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <link href=\"./css/editor.css\" rel=\"stylesheet\">\n  <link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n  <style>\n    html.dark  {\n      --w-e-textarea-bg-color: #999;\n    }\n  </style>\n</head>\n<body>\n  <p>\n    Theme\n    <button id=\"btn1\">切换主题</button>\n  </p>\n\n  <!-- 编辑器 -->\n  <div>\n    <div id=\"editor-toolbar\" class=\"editor-toolbar\"></div>\n    <div id=\"editor-text-area\" class=\"editor-text-area\"></div>\n  </div>\n\n  <script src=\"js/init-content.js\"></script>\n  <script src=\"../dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n    const editorConfig = {\n      placeholder: '请输入内容...',\n      MENU_CONF: {}\n    }\n    editorConfig.MENU_CONF['uploadImage'] = {\n      fieldName: 'your-fileName',\n      base64LimitSize: 10 * 1024 * 1024 // 10M 以下插入 base64\n    }\n    const editor = E.createEditor({\n      selector: '#editor-text-area',\n      config: editorConfig,\n      content: window.content1\n    })\n    const toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      config: {}\n    })\n\n    document.getElementById('btn1').addEventListener('mousedown', e => {\n      e.preventDefault()\n      e.stopPropagation()\n\n      const html = document.getElementsByTagName('html')[0]\n      if (html == null) return\n\n      const curClass = html.getAttribute('class')\n      if (curClass) {\n        html.removeAttribute('class')\n      } else {\n        html.setAttribute('class', 'dark')\n      }\n    })\n  </script>\n</body>\n</html>"
  },
  {
    "path": "packages/editor/examples/todo.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>todo demo</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <link href=\"./css/view.css\" rel=\"stylesheet\">\n  <link href=\"./css/editor.css\" rel=\"stylesheet\">\n  <link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n</head>\n\n<body>\n  <p>todo</p>\n\n  <!-- 编辑器 -->\n  <div>\n    <div id=\"editor-toolbar\" class=\"editor-toolbar\"></div>\n    <div id=\"editor-text-area\" class=\"editor-text-area\"></div>\n  </div>\n\n\n  <script src=\"https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js\"></script>\n  <script src=\"js/init-content.js\"></script>\n  <script src=\"../dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    const editorConfig = { MENU_CONF: {} }\n\n    const editor = E.createEditor({\n      selector: '#editor-text-area',\n      content: [\n        { type: 'paragraph', children: [{ text: 'hello' }] },\n        { type: 'todo', checked: false, children: [{ text: '吃饭' }] },\n        { type: 'todo', checked: false, children: [{ text: '睡觉' }] },\n        { type: 'todo', checked: true, children: [{ text: '看电影' }] },\n      ],\n      config: editorConfig\n    })\n\n    const toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      // config: {}\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/examples/upload-image.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>upload image demo</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <link href=\"./css/editor.css\" rel=\"stylesheet\">\n  <link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n</head>\n\n<body>\n  <p>\n    Upload image\n  </p>\n  <p>\n    本地下载、启动 <a href=\"https://github.com/wangeditor-team/server\" target=\"_blank\">server</a> 服务端\n    <code style=\"background-color: #f1f1f1;\">http://127.0.0.1:3000/api/upload-img</code>\n  </p>\n\n  <div>\n    <div id=\"editor-toolbar\" class=\"editor-toolbar\"></div>\n    <div id=\"editor-text-area\" class=\"editor-text-area\"></div>\n  </div>\n\n\n  <script src=\"https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js\"></script>\n  <script src=\"../dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    // Change language\n    const LANG = location.href.indexOf('lang=en') > 0 ? 'en' : 'zh-CN'\n    E.i18nChangeLanguage(LANG)\n\n    const editorConfig = { MENU_CONF: {} }\n    editorConfig.MENU_CONF['uploadImage'] = {\n      server: 'http://127.0.0.1:3000/api/upload-img',\n      // server: 'http://127.0.0.1:3000/api/upload-img-10s', // test timeout\n      // server: 'http://127.0.0.1:3000/api/upload-img-failed', // test failed\n      // server: 'http://127.0.0.1:3000/api/xxx', // test 404\n\n      timeout: 5 * 1000, // 5s\n\n      fieldName: 'custom-fileName',\n      meta: { token: 'xxx', a: 100 },\n      metaWithUrl: true, // join params to url\n      headers: { Accept: 'text/x-json' },\n\n      maxFileSize: 10 * 1024 * 1024, // 10M\n\n      base64LimitSize: 5 * 1024, // insert base64 format, if file's size less than 5kb\n\n      onBeforeUpload(file) {\n        console.log('onBeforeUpload', file)\n\n        return file // will upload this file\n        // return false // prevent upload\n      },\n      onProgress(progress) {\n        console.log('onProgress', progress)\n      },\n      onSuccess(file, res) {\n        console.log('onSuccess---', file, res)\n      },\n      onFailed(file, res) {\n        console.log('onFailed---', file, res)\n      },\n      onError(file, err, res) {\n        console.error('onError---', file, err, res)\n      },\n\n      // customInsert(res, insertFn) {\n      //   console.log('customInsert', res)\n      //   const imgInfo = res.data[0] || {}\n      //   const { url, alt, href } = imgInfo\n      //   if (!url) throw new Error(`Image url is empty`)\n\n      //   console.log('Your image url ', url)\n      //   insertFn(url, alt, href)\n      // },\n\n      // customUpload(file, insertFn) {\n      //   console.log('customUpload', file)\n\n      //   return new Promise((resolve) => {\n      //     // Simulate async insert image\n      //     setTimeout(() => {\n      //       const src = `https://www.baidu.com/img/flexible/logo/pc/result@2.png?r=${Math.random()}`\n      //       insertFn(src, 'baidu logo', src)\n      //       resolve('ok')\n      //     }, 500)\n      //   })\n      // },\n\n      // customBrowseAndUpload(insertFn) {\n      //   alert('Custom browse and upload')\n\n      //   // Simulate async insert image\n      //   setTimeout(() => {\n      //     const src = 'https://www.baidu.com/img/flexible/logo/pc/result@2.png'\n      //     insertFn(src, 'baidu logo', src) // insert a image\n      //   }, 500)\n      // },\n    }\n\n    const editor = E.createEditor({\n      selector: '#editor-text-area',\n      html: '<p>Upload image...</p><p><br></p>',\n      config: editorConfig\n    })\n\n    const toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      // config: {}\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/examples/upload-video.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>upload video demo</title>\n  <link href=\"https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css\" rel=\"stylesheet\">\n  <link href=\"./css/editor.css\" rel=\"stylesheet\">\n  <link href=\"../dist/css/style.css\" rel=\"stylesheet\">\n</head>\n\n<body>\n  <p>\n    Upload video\n  </p>\n  <p>\n    本地下载、启动 <a href=\"https://github.com/wangeditor-team/server\" target=\"_blank\">server</a> 服务端\n    <code style=\"background-color: #f1f1f1;\">http://127.0.0.1:3000/api/upload-video</code>\n  </p>\n\n  <div>\n    <div id=\"editor-toolbar\" class=\"editor-toolbar\"></div>\n    <div id=\"editor-text-area\" class=\"editor-text-area\"></div>\n  </div>\n\n\n  <script src=\"https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js\"></script>\n  <script src=\"../dist/index.js\"></script>\n  <script>\n    const E = window.wangEditor\n\n    // Change language\n    const LANG = location.href.indexOf('lang=en') > 0 ? 'en' : 'zh-CN'\n    E.i18nChangeLanguage(LANG)\n\n    const editorConfig = { MENU_CONF: {} }\n    editorConfig.MENU_CONF['insertVideo'] = {\n      onInsertedVideo(videoNode) {\n        console.log('inserted video', videoNode)\n      },\n    }\n    editorConfig.MENU_CONF['uploadVideo'] = {\n      server: 'http://127.0.0.1:3000/api/upload-video',\n      // server: 'http://127.0.0.1:3000/api/upload-img-failed', // test failed\n      // server: 'http://127.0.0.1:3000/api/xxx', // test 404\n\n      timeout: 5 * 1000, // 5s\n\n      fieldName: 'custom-fileName',\n      meta: { token: 'xxx', a: 100 },\n      metaWithUrl: true, // join params to url\n      headers: { Accept: 'text/x-json' },\n\n      maxFileSize: 10 * 1024 * 1024, // 10M\n\n      onBeforeUpload(file) {\n        console.log('onBeforeUpload', file)\n\n        return file // will upload this file\n        // return false // prevent upload\n      },\n      onProgress(progress) {\n        console.log('onProgress', progress)\n      },\n      onSuccess(file, res) {\n        console.log('onSuccess---', file, res)\n      },\n      onFailed(file, res) {\n        console.log('onFailed---', file, res)\n      },\n      onError(file, err, res) {\n        console.error('onError---', file, err, res)\n      },\n\n      // customInsert(res, insertFn) {\n      //   console.log('customInsert', res)\n      //   const videoInfo = res.data || {}\n      //   const { url, poster } = videoInfo\n      //   if (!url) throw new Error(`Video url is empty`)\n\n      //   console.log('Your video url ', url)\n      //   insertFn(url, poster)\n      // },\n\n      // customUpload(file, insertFn) {\n      //   console.log('customUpload', file)\n\n      //   return new Promise((resolve) => {\n      //     // Simulate async insert video\n      //     setTimeout(() => {\n      //       const src = `https://www.w3school.com.cn/i/movie.ogg`\n      //       const poster = 'https://www.baidu.com/img/flexible/logo/pc/result@2.png'\n      //       insertFn(src, poster)\n      //       resolve('ok')\n      //     }, 500)\n      //   })\n      // },\n\n      // customBrowseAndUpload(insertFn) {\n      //   alert('Custom browse and upload')\n\n      //   // Simulate async insert video\n      //   setTimeout(() => {\n      //     // const src = '<iframe src=\"//player.bilibili.com/player.html?aid=250348909&bvid=BV1Pv411w7Xr&cid=401518678&page=1\" scrolling=\"no\" border=\"0\" frameborder=\"no\" framespacing=\"0\" allowfullscreen=\"true\"> </iframe>'\n      //     const src = 'https://www.w3school.com.cn/i/movie.ogg'\n      //     const poster = 'https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png'\n      //     insertFn(src, poster)\n      //   }, 500)\n      // },\n    }\n\n    const editor = E.createEditor({\n      selector: '#editor-text-area',\n      html: '<p>Upload video...</p><p><br></p>',\n      config: editorConfig\n    })\n\n    const toolbar = E.createToolbar({\n      editor,\n      selector: '#editor-toolbar',\n      // config: {}\n    })\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "packages/editor/package.json",
    "content": "{\n  \"name\": \"@wangeditor/editor\",\n  \"version\": \"5.1.23\",\n  \"description\": \"Web rich text editor, Web 富文本编辑器\",\n  \"keywords\": [\n    \"wangeditor\",\n    \"rich text\",\n    \"editor\",\n    \"富文本\",\n    \"编辑器\"\n  ],\n  \"author\": \"wangfupeng1988 <wangfupeng1988@163.com>\",\n  \"contributors\": [],\n  \"homepage\": \"https://www.wangeditor.com/\",\n  \"license\": \"MIT\",\n  \"types\": \"dist/editor/src/index.d.ts\",\n  \"main\": \"dist/index.js\",\n  \"module\": \"dist/index.esm.js\",\n  \"browser\": {\n    \"./dist/index.js\": \"./dist/index.js\",\n    \"./dist/index.esm.js\": \"./dist/index.esm.js\"\n  },\n  \"directories\": {\n    \"lib\": \"dist\",\n    \"test\": \"__tests__\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.com/\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wangeditor-team/wangEditor.git\"\n  },\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"test-c\": \"jest --coverage\",\n    \"example\": \"concurrently \\\"yarn dev-watch\\\" \\\"http-server -p 8881 -c-1\\\" \",\n    \"dev\": \"cross-env NODE_ENV=development rollup -c rollup.config.js\",\n    \"dev-watch\": \"cross-env NODE_ENV=development rollup -c rollup.config.js -w\",\n    \"build\": \"cross-env NODE_ENV=production rollup -c rollup.config.js\",\n    \"dev-size-stats\": \"cross-env NODE_ENV=development:size_stats rollup -c rollup.config.js\",\n    \"size-stats\": \"cross-env NODE_ENV=production:size_stats rollup -c rollup.config.js\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/wangeditor-team/wangEditor/issues\"\n  },\n  \"dependencies\": {\n    \"@uppy/core\": \"^2.1.1\",\n    \"@uppy/xhr-upload\": \"^2.0.3\",\n    \"@wangeditor/basic-modules\": \"^1.1.7\",\n    \"@wangeditor/code-highlight\": \"^1.0.3\",\n    \"@wangeditor/core\": \"^1.1.19\",\n    \"@wangeditor/list-module\": \"^1.0.5\",\n    \"@wangeditor/table-module\": \"^1.1.4\",\n    \"@wangeditor/upload-image-module\": \"^1.0.2\",\n    \"@wangeditor/video-module\": \"^1.1.4\",\n    \"dom7\": \"^3.0.0\",\n    \"is-hotkey\": \"^0.2.0\",\n    \"lodash.camelcase\": \"^4.3.0\",\n    \"lodash.clonedeep\": \"^4.5.0\",\n    \"lodash.debounce\": \"^4.0.8\",\n    \"lodash.foreach\": \"^4.5.0\",\n    \"lodash.isequal\": \"^4.5.0\",\n    \"lodash.throttle\": \"^4.1.1\",\n    \"lodash.toarray\": \"^4.4.0\",\n    \"nanoid\": \"^3.2.0\",\n    \"slate\": \"^0.72.0\",\n    \"snabbdom\": \"^3.1.0\"\n  }\n}\n"
  },
  {
    "path": "packages/editor/rollup.config.js",
    "content": "import { createRollupConfig, IS_PRD, IS_DEV } from '../../build/create-rollup-config'\nimport pkg from './package.json'\n\nconst name = 'wangEditor'\n\nconst configList = []\n\n// umd\nconst umdConf = createRollupConfig({\n  output: {\n    file: pkg.main,\n    format: 'umd',\n    name,\n  },\n})\nconfigList.push(umdConf)\n\n// esm\nconst esmConf = createRollupConfig({\n  output: {\n    file: pkg.module,\n    format: 'esm',\n    name,\n  },\n})\nconfigList.push(esmConf)\n\nexport default configList\n"
  },
  {
    "path": "packages/editor/src/Boot.ts",
    "content": "/**\n * @description Editor View class\n * @author wangfupeng\n */\n\nimport {\n  IDomEditor,\n\n  // 配置\n  IEditorConfig,\n  IToolbarConfig,\n  IModuleConf,\n\n  // 注册菜单\n  IRegisterMenuConf,\n  registerMenu,\n\n  // 渲染 modal -> view\n  IRenderElemConf,\n  RenderStyleFnType,\n  registerStyleHandler,\n  registerRenderElemConf,\n\n  // to html\n  IElemToHtmlConf,\n  styleToHtmlFnType,\n  registerStyleToHtmlHandler,\n  registerElemToHtmlConf,\n\n  // parseHtml\n  PreParseHtmlFnType,\n  IPreParseHtmlConf,\n  registerPreParseHtmlConf,\n  ParseStyleHtmlFnType,\n  IParseElemHtmlConf,\n  registerParseElemHtmlConf,\n  registerParseStyleHtmlHandler,\n} from '@wangeditor/core'\nimport registerModule from './register-builtin-modules/register'\n\ntype PluginType = <T extends IDomEditor>(editor: T) => T\n\nclass Boot {\n  constructor() {\n    throw new Error('不能实例化\\nCan not construct a instance')\n  }\n\n  // editor 配置\n  static editorConfig: Partial<IEditorConfig> = {}\n  static setEditorConfig(newConfig: Partial<IEditorConfig> = {}) {\n    this.editorConfig = {\n      ...this.editorConfig,\n      ...newConfig,\n    }\n  }\n  static simpleEditorConfig: Partial<IEditorConfig> = {}\n  static setSimpleEditorConfig(newConfig: Partial<IEditorConfig> = {}) {\n    this.simpleEditorConfig = {\n      ...this.simpleEditorConfig,\n      ...newConfig,\n    }\n  }\n\n  //toolbar 配置\n  static toolbarConfig: Partial<IToolbarConfig> = {}\n  static setToolbarConfig(newConfig: Partial<IToolbarConfig> = {}) {\n    this.toolbarConfig = {\n      ...this.toolbarConfig,\n      ...newConfig,\n    }\n  }\n  static simpleToolbarConfig: Partial<IToolbarConfig> = {}\n  static setSimpleToolbarConfig(newConfig: Partial<IToolbarConfig> = {}) {\n    this.simpleToolbarConfig = {\n      ...this.simpleToolbarConfig,\n      ...newConfig,\n    }\n  }\n\n  // 注册插件\n  static plugins: PluginType[] = []\n  static registerPlugin(plugin: PluginType) {\n    this.plugins.push(plugin)\n  }\n\n  // 注册 menu\n  // TODO 可在注册时传入配置，在开发文档中说明\n  static registerMenu(menuConf: IRegisterMenuConf, customConfig?: { [key: string]: any }) {\n    registerMenu(menuConf, customConfig)\n  }\n\n  // 注册 renderElem\n  static registerRenderElem(renderElemConf: IRenderElemConf) {\n    registerRenderElemConf(renderElemConf)\n  }\n\n  // 注册 renderStyle\n  static registerRenderStyle(fn: RenderStyleFnType) {\n    registerStyleHandler(fn)\n  }\n\n  // 注册 elemToHtml\n  static registerElemToHtml(elemToHtmlConf: IElemToHtmlConf) {\n    registerElemToHtmlConf(elemToHtmlConf)\n  }\n\n  // 注册 styleToHtml\n  static registerStyleToHtml(fn: styleToHtmlFnType) {\n    registerStyleToHtmlHandler(fn)\n  }\n\n  // 注册 preParseHtml\n  static registerPreParseHtml(preParseHtmlConf: IPreParseHtmlConf) {\n    registerPreParseHtmlConf(preParseHtmlConf)\n  }\n\n  // 注册 parseElemHtml\n  static registerParseElemHtml(parseElemHtmlConf: IParseElemHtmlConf) {\n    registerParseElemHtmlConf(parseElemHtmlConf)\n  }\n\n  // 注册 parseStyleHtml\n  static registerParseStyleHtml(fn: ParseStyleHtmlFnType) {\n    registerParseStyleHtmlHandler(fn)\n  }\n\n  // 注册 module\n  static registerModule(module: Partial<IModuleConf>) {\n    registerModule(module)\n  }\n}\n\nexport default Boot\n"
  },
  {
    "path": "packages/editor/src/assets/index.less",
    "content": "// 集中定义 css vars ，否则会被重复定义多次\n:root, :host {\n  // textarea - css vars\n  --w-e-textarea-bg-color: #fff;\n  --w-e-textarea-color: #333;\n  --w-e-textarea-border-color: #ccc;\n  --w-e-textarea-slight-border-color: #e8e8e8;\n  --w-e-textarea-slight-color: #d4d4d4;\n  --w-e-textarea-slight-bg-color: #f5f2f0;\n  --w-e-textarea-selected-border-color: #B4D5FF; // 选中的元素，如选中了分割线\n  --w-e-textarea-handler-bg-color: #4290f7; // 工具，如图片拖拽按钮\n\n  // toolbar - css vars\n  --w-e-toolbar-color: #595959;\n  --w-e-toolbar-bg-color: #fff;\n  --w-e-toolbar-active-color: #333;\n  --w-e-toolbar-active-bg-color: #f1f1f1;\n  --w-e-toolbar-disabled-color: #999;\n  --w-e-toolbar-border-color: #e8e8e8;\n\n  // modal - css vars\n  --w-e-modal-button-bg-color: #fafafa;\n  --w-e-modal-button-border-color: #d9d9d9;\n}\n"
  },
  {
    "path": "packages/editor/src/constants/svg.ts",
    "content": "/**\n * @description svg tag\n * @author wangfupeng\n */\n\n/**\n * 【注意】svg 字符串的长度 ，否则会导致代码体积过大\n * 尽量选择 https://www.iconfont.cn/collections/detail?spm=a313x.7781069.0.da5a778a4&cid=20293\n * 找不到再从 iconfont.com 搜索\n */\n\n// 缩进 right\nexport const INDENT_RIGHT_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M0 64h1024v128H0z m384 192h640v128H384z m0 192h640v128H384z m0 192h640v128H384zM0 832h1024v128H0z m0-128V320l256 192z\"></path></svg>'\n\n// 左对齐\nexport const JUSTIFY_LEFT_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M768 793.6v102.4H51.2v-102.4h716.8z m204.8-230.4v102.4H51.2v-102.4h921.6z m-204.8-230.4v102.4H51.2v-102.4h716.8zM972.8 102.4v102.4H51.2V102.4h921.6z\"></path></svg>'\n\n// 图片\nexport const IMAGE_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M959.877 128l0.123 0.123v767.775l-0.123 0.122H64.102l-0.122-0.122V128.123l0.122-0.123h895.775zM960 64H64C28.795 64 0 92.795 0 128v768c0 35.205 28.795 64 64 64h896c35.205 0 64-28.795 64-64V128c0-35.205-28.795-64-64-64zM832 288.01c0 53.023-42.988 96.01-96.01 96.01s-96.01-42.987-96.01-96.01S682.967 192 735.99 192 832 234.988 832 288.01zM896 832H128V704l224.01-384 256 320h64l224.01-192z\"></path></svg>'\n\n// plus\nexport const MORE_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M204.8 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z\"></path><path d=\"M505.6 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z\"></path><path d=\"M806.4 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z\"></path></svg>'\n\n// 视频\nexport const VIDEO_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M981.184 160.096C837.568 139.456 678.848 128 512 128S186.432 139.456 42.816 160.096C15.296 267.808 0 386.848 0 512s15.264 244.16 42.816 351.904C186.464 884.544 345.152 896 512 896s325.568-11.456 469.184-32.096C1008.704 756.192 1024 637.152 1024 512s-15.264-244.16-42.816-351.904zM384 704V320l320 192-320 192z\"></path></svg>'\n"
  },
  {
    "path": "packages/editor/src/create.ts",
    "content": "/**\n * @description create\n * @author wangfupeng\n */\n\nimport { Descendant } from 'slate'\nimport Boot from './Boot'\nimport { DOMElement } from './utils/dom'\nimport {\n  IEditorConfig,\n  IDomEditor,\n  IToolbarConfig,\n  coreCreateEditor,\n  coreCreateToolbar,\n  Toolbar,\n} from '@wangeditor/core'\n\nexport interface ICreateEditorOption {\n  selector: string | DOMElement\n  config: Partial<IEditorConfig>\n  content?: Descendant[]\n  html?: string\n  mode: string\n}\n\nexport interface ICreateToolbarOption {\n  editor: IDomEditor | null\n  selector: string | DOMElement\n  config?: Partial<IToolbarConfig>\n  mode?: string\n}\n\n/**\n * 创建 editor 实例\n */\nexport function createEditor(option: Partial<ICreateEditorOption> = {}): IDomEditor {\n  const { selector = '', content = [], html, config = {}, mode = 'default' } = option\n\n  let globalConfig = mode === 'simple' ? Boot.simpleEditorConfig : Boot.editorConfig\n\n  // 单独处理 hoverbarKeys\n  const newHoverbarKeys = {\n    ...(globalConfig.hoverbarKeys || {}),\n    ...(config.hoverbarKeys || {}),\n  }\n\n  const editor = coreCreateEditor({\n    selector,\n    config: {\n      ...globalConfig, // 全局配置\n      ...config,\n      hoverbarKeys: newHoverbarKeys,\n    },\n    content,\n    html,\n    plugins: Boot.plugins,\n  })\n\n  return editor\n}\n\n/**\n * 创建 toolbar 实例\n */\nexport function createToolbar(option: ICreateToolbarOption): Toolbar {\n  const { selector, editor, config = {}, mode = 'default' } = option\n  if (!selector) {\n    throw new Error(`Cannot find 'selector' when create toolbar`)\n  }\n\n  let globalConfig = mode === 'simple' ? Boot.simpleToolbarConfig : Boot.toolbarConfig\n\n  const toolbar = coreCreateToolbar(editor, {\n    selector,\n    config: {\n      ...globalConfig, // 全局配置\n      ...config,\n    },\n  })\n\n  return toolbar\n}\n"
  },
  {
    "path": "packages/editor/src/index.ts",
    "content": "/**\n * @description editor entry\n * @author wangfupeng\n */\n\nimport './assets/index.less'\nimport '@wangeditor/core/dist/css/style.css'\n\n// 兼容性（要放在最开始就执行）\nimport './utils/browser-polyfill'\nimport './utils/node-polyfill'\n\n// 配置多语言\nimport './locale/index'\n\n// 注册内置模块\nimport './register-builtin-modules/index'\n// 初始化默认配置\nimport './init-default-config'\n\n// 全局注册\nimport Boot from './Boot'\nexport { Boot }\n\n// 导出 core API 和接口（注意，此处按需导出，不可直接用 `*` ）\nexport {\n  DomEditor,\n  IDomEditor,\n  IEditorConfig,\n  IToolbarConfig,\n  Toolbar,\n  // 第三方模块 - 接口\n  IModuleConf,\n  IButtonMenu,\n  ISelectMenu,\n  IDropPanelMenu,\n  IModalMenu,\n  // 第三方模块 - 多语言\n  i18nChangeLanguage,\n  i18nAddResources,\n  i18nGetResources,\n  t,\n  // 第三方模块 - modal 中用到的 API\n  genModalTextareaElems,\n  genModalInputElems,\n  genModalButtonElems,\n  // 第三方模块 - 上传时用到\n  createUploader,\n  IUploadConfig,\n} from '@wangeditor/core'\n\n// 导出 slate API 和接口 （需重命名，加 `Slate` 前缀）\nexport {\n  Transforms as SlateTransforms,\n  Descendant as SlateDescendant,\n  Editor as SlateEditor,\n  Node as SlateNode,\n  Element as SlateElement,\n  Text as SlateText,\n  Path as SlatePath,\n  Range as SlateRange,\n  Location as SlateLocation,\n  Point as SlatePoint,\n} from 'slate'\n\n// 导出 create 函数\nexport { createEditor, createToolbar } from './create'\n\nexport default {}\n"
  },
  {
    "path": "packages/editor/src/init-default-config/config/hoverbar.ts",
    "content": "/**\n * @description hoverbar 配置\n * @author wangfupeng\n */\n\nconst COMMON_HOVERBAR_KEYS = {\n  // key 即 element type\n  link: {\n    menuKeys: ['editLink', 'unLink', 'viewLink'],\n  },\n  image: {\n    menuKeys: [\n      'imageWidth30',\n      'imageWidth50',\n      'imageWidth100',\n      'editImage',\n      'viewImageLink',\n      'deleteImage',\n    ],\n  },\n  pre: {\n    menuKeys: ['enter', 'codeBlock', 'codeSelectLang'],\n  },\n  table: {\n    menuKeys: [\n      'enter',\n      'tableHeader',\n      'tableFullWidth',\n      'insertTableRow',\n      'deleteTableRow',\n      'insertTableCol',\n      'deleteTableCol',\n      'deleteTable',\n    ],\n  },\n  divider: {\n    menuKeys: ['enter'],\n  },\n  video: {\n    menuKeys: ['enter', 'editVideoSize'],\n  },\n}\n\nexport function genDefaultHoverbarKeys() {\n  return {\n    ...COMMON_HOVERBAR_KEYS,\n\n    // 也可以自定义 match 来匹配元素，此时 key 就随意了\n    text: {\n      menuKeys: [\n        'headerSelect',\n        'insertLink',\n        'bulletedList',\n        '|',\n        'bold',\n        'through',\n        'color',\n        'bgColor',\n        'clearStyle',\n      ],\n    },\n    // other hover bar ...\n  }\n}\n\nexport function genSimpleHoverbarKeys() {\n  return COMMON_HOVERBAR_KEYS\n}\n"
  },
  {
    "path": "packages/editor/src/init-default-config/config/index.ts",
    "content": "/**\n * @description 获取编辑器默认配置\n * @author wangfupeng\n */\n\nimport { genDefaultToolbarKeys, genSimpleToolbarKeys } from './toolbar'\nimport { genDefaultHoverbarKeys, genSimpleHoverbarKeys } from './hoverbar'\n\nexport function getDefaultEditorConfig() {\n  return {\n    hoverbarKeys: genDefaultHoverbarKeys(),\n  }\n}\n\nexport function getSimpleEditorConfig() {\n  return {\n    hoverbarKeys: genSimpleHoverbarKeys(),\n  }\n}\n\nexport function getDefaultToolbarConfig() {\n  return {\n    toolbarKeys: genDefaultToolbarKeys(),\n  }\n}\n\nexport function getSimpleToolbarConfig() {\n  return {\n    toolbarKeys: genSimpleToolbarKeys(),\n  }\n}\n"
  },
  {
    "path": "packages/editor/src/init-default-config/config/toolbar.ts",
    "content": "/**\n * @description toolbar 配置\n * @author wangfupeng\n */\n\nimport { t } from '@wangeditor/core'\nimport {\n  INDENT_RIGHT_SVG,\n  JUSTIFY_LEFT_SVG,\n  IMAGE_SVG,\n  MORE_SVG,\n  VIDEO_SVG,\n} from '../../constants/svg'\n\nexport function genDefaultToolbarKeys() {\n  return [\n    'headerSelect',\n    // 'header1',\n    // 'header2',\n    // 'header3',\n    'blockquote',\n    '|',\n    'bold',\n    'underline',\n    'italic',\n    {\n      key: 'group-more-style', // 以 group 开头\n      title: t('editor.more'),\n      iconSvg: MORE_SVG,\n      menuKeys: ['through', 'code', 'sup', 'sub', 'clearStyle'],\n    },\n    'color',\n    'bgColor',\n    '|',\n    'fontSize',\n    'fontFamily',\n    'lineHeight',\n    '|',\n    'bulletedList',\n    'numberedList',\n    'todo',\n    {\n      key: 'group-justify', // 以 group 开头\n      title: t('editor.justify'),\n      iconSvg: JUSTIFY_LEFT_SVG,\n      menuKeys: ['justifyLeft', 'justifyRight', 'justifyCenter', 'justifyJustify'],\n    },\n    {\n      key: 'group-indent', // 以 group 开头\n      title: t('editor.indent'),\n      iconSvg: INDENT_RIGHT_SVG,\n      menuKeys: ['indent', 'delIndent'],\n    },\n    '|',\n    'emotion',\n    'insertLink',\n    // 'editLink',\n    // 'unLink',\n    // 'viewLink',\n    {\n      key: 'group-image', // 以 group 开头\n      title: t('editor.image'),\n      iconSvg: IMAGE_SVG,\n      menuKeys: ['insertImage', 'uploadImage'],\n    },\n    // 'deleteImage',\n    // 'editImage',\n    // 'viewImageLink',\n    {\n      key: 'group-video', // 以 group 开头\n      title: t('editor.video'),\n      iconSvg: VIDEO_SVG,\n      menuKeys: ['insertVideo', 'uploadVideo'],\n    },\n    // 'deleteVideo',\n    'insertTable',\n    'codeBlock',\n    // 'codeSelectLang',\n    'divider',\n    // 'deleteTable',\n    '|',\n    'undo',\n    'redo',\n    '|',\n    'fullScreen',\n  ]\n}\n\nexport function genSimpleToolbarKeys() {\n  return [\n    'blockquote',\n    'header1',\n    'header2',\n    'header3',\n    '|',\n    'bold',\n    'underline',\n    'italic',\n    'through',\n    'color',\n    'bgColor',\n    'clearStyle',\n    '|',\n    'bulletedList',\n    'numberedList',\n    'todo',\n    'justifyLeft',\n    'justifyRight',\n    'justifyCenter',\n    '|',\n    'insertLink',\n    {\n      key: 'group-image', // 以 group 开头\n      title: t('editor.image'),\n      iconSvg: IMAGE_SVG,\n      menuKeys: ['insertImage', 'uploadImage'],\n    },\n    'insertVideo',\n    'insertTable',\n    'codeBlock',\n    '|',\n    'undo',\n    'redo',\n    '|',\n    'fullScreen',\n  ]\n}\n"
  },
  {
    "path": "packages/editor/src/init-default-config/index.ts",
    "content": "/**\n * @description set default config\n * @author wangfupeng\n */\n\nimport Boot from '../Boot'\nimport {\n  getDefaultEditorConfig,\n  getDefaultToolbarConfig,\n  getSimpleEditorConfig,\n  getSimpleToolbarConfig,\n} from './config'\n\nimport { wangEditorCodeHighLightDecorate } from '@wangeditor/code-highlight'\n\nconst defaultEditorConfig = getDefaultEditorConfig()\nBoot.setEditorConfig({\n  ...defaultEditorConfig,\n  decorate: wangEditorCodeHighLightDecorate, // 代码高亮\n})\n\nconst simpleEditorConfig = getSimpleEditorConfig()\nBoot.setSimpleEditorConfig({\n  ...simpleEditorConfig,\n  decorate: wangEditorCodeHighLightDecorate, // 代码高亮\n})\n\nconst defaultToolbarConfig = getDefaultToolbarConfig()\nBoot.setToolbarConfig(defaultToolbarConfig)\n\nconst simpleToolbarConfig = getSimpleToolbarConfig()\nBoot.setSimpleToolbarConfig(simpleToolbarConfig)\n"
  },
  {
    "path": "packages/editor/src/locale/en.ts",
    "content": "/**\n * @description i18n en\n * @author wangfupeng\n */\n\nexport default {\n  editor: {\n    more: 'More',\n    justify: 'Justify',\n    indent: 'Indent',\n    image: 'Image',\n    video: 'Video',\n  },\n}\n"
  },
  {
    "path": "packages/editor/src/locale/index.ts",
    "content": "/**\n * @description i18n entry\n * @author wangfupeng\n */\n\nimport { i18nAddResources } from '@wangeditor/core'\nimport enResources from './en'\nimport zhResources from './zh-CN'\n\ni18nAddResources('en', enResources)\ni18nAddResources('zh-CN', zhResources)\n"
  },
  {
    "path": "packages/editor/src/locale/zh-CN.ts",
    "content": "/**\n * @description i18n zh-CN\n * @author wangfupeng\n */\n\nexport default {\n  editor: {\n    more: '更多',\n    justify: '对齐',\n    indent: '缩进',\n    image: '图片',\n    video: '视频',\n  },\n}\n"
  },
  {
    "path": "packages/editor/src/register-builtin-modules/index.ts",
    "content": "/**\n * @description register builtin modules\n * @author wangfupeng\n */\n\n// basic-modules\nimport '@wangeditor/basic-modules/dist/css/style.css'\nimport basicModules from '@wangeditor/basic-modules'\n\nimport '@wangeditor/list-module/dist/css/style.css'\nimport wangEditorListModule from '@wangeditor/list-module'\n\n// table-module\nimport '@wangeditor/table-module/dist/css/style.css'\nimport wangEditorTableModule from '@wangeditor/table-module'\n\n// video-module\nimport '@wangeditor/video-module/dist/css/style.css'\nimport wangEditorVideoModule from '@wangeditor/video-module'\n\n// upload-image-module\nimport '@wangeditor/upload-image-module/dist/css/style.css'\nimport wangEditorUploadImageModule from '@wangeditor/upload-image-module'\n\n// code-highlight\nimport '@wangeditor/code-highlight/dist/css/style.css'\nimport { wangEditorCodeHighlightModule } from '@wangeditor/code-highlight'\n\nimport registerModule from './register'\n\nbasicModules.forEach(module => registerModule(module))\nregisterModule(wangEditorListModule)\nregisterModule(wangEditorTableModule)\nregisterModule(wangEditorVideoModule)\nregisterModule(wangEditorUploadImageModule)\nregisterModule(wangEditorCodeHighlightModule)\n"
  },
  {
    "path": "packages/editor/src/register-builtin-modules/register.ts",
    "content": "/**\n * @description 注册 module\n * @author wangfupeng\n */\n\nimport Boot from '../Boot'\nimport { IModuleConf } from '@wangeditor/core'\n\nfunction registerModule(module: Partial<IModuleConf>) {\n  const {\n    menus,\n    renderElems,\n    renderStyle,\n    elemsToHtml,\n    styleToHtml,\n    preParseHtml,\n    parseElemsHtml,\n    parseStyleHtml,\n    editorPlugin,\n  } = module\n\n  if (menus) {\n    menus.forEach(menu => Boot.registerMenu(menu))\n  }\n  if (renderElems) {\n    renderElems.forEach(renderElemConf => Boot.registerRenderElem(renderElemConf))\n  }\n  if (renderStyle) {\n    Boot.registerRenderStyle(renderStyle)\n  }\n  if (elemsToHtml) {\n    elemsToHtml.forEach(elemToHtmlConf => Boot.registerElemToHtml(elemToHtmlConf))\n  }\n  if (styleToHtml) {\n    Boot.registerStyleToHtml(styleToHtml)\n  }\n  if (preParseHtml) {\n    preParseHtml.forEach(conf => Boot.registerPreParseHtml(conf))\n  }\n  if (parseElemsHtml) {\n    parseElemsHtml.forEach(parseElemHtmlConf => Boot.registerParseElemHtml(parseElemHtmlConf))\n  }\n  if (parseStyleHtml) {\n    Boot.registerParseStyleHtml(parseStyleHtml)\n  }\n  if (editorPlugin) {\n    Boot.registerPlugin(editorPlugin)\n  }\n}\n\nexport default registerModule\n"
  },
  {
    "path": "packages/editor/src/utils/browser-polyfill.ts",
    "content": "/**\n * @description browser polyfill\n * @author wangfupeng\n */\n\n// @ts-nocheck\n\n// 必须是浏览器环境\nif (typeof global === 'undefined') {\n  // 检查 IE 浏览器\n  if ('ActiveXObject' in window) {\n    let info = '抱歉，wangEditor V5+ 版本开始，不在支持 IE 浏览器'\n    info += '\\n Sorry, wangEditor V5+ versions do not support IE browser.'\n    console.error(info)\n  }\n\n  globalThisPolyfill()\n  AggregateErrorPolyfill()\n} else if (global && global.navigator?.userAgent.match('QQBrowser')) {\n  // 兼容 QQ 浏览器 AggregateError 报错\n  globalThisPolyfill()\n  AggregateErrorPolyfill()\n}\n\nfunction globalThisPolyfill() {\n  // 部分浏览器不支持 globalThis\n  if (typeof globalThis === 'undefined') {\n    // @ts-ignore\n    window.globalThis = window\n  }\n}\n\nfunction AggregateErrorPolyfill() {\n  if (typeof AggregateError === 'undefined') {\n    window.AggregateError = function (errors, msg) {\n      const err = new Error(msg)\n      err.errors = errors\n      return err\n    }\n  }\n}\n"
  },
  {
    "path": "packages/editor/src/utils/dom.ts",
    "content": "/**\n * @description dom utils\n * @author wangfupeng\n */\n\nimport DOMElement = globalThis.Element\n\nexport { DOMElement }\n"
  },
  {
    "path": "packages/editor/src/utils/node-polyfill.ts",
    "content": "/**\n * @description node polyfill\n * @author wangfupeng\n */\n\n// @ts-nocheck\n\n// 必须是 node 环境\nif (typeof global === 'object') {\n  // 用于 nodejs ，避免报错\n  const globalProperty = Object.getOwnPropertyDescriptor(global, 'window')\n\n  // global.window 为空则直接写入\n  // 部分框架下已经定义了global.window且是不可写属性\n  if (!global.window || globalProperty.set) {\n    global.window = global\n    global.requestAnimationFrame = () => {}\n    global.navigator = {\n      userAgent: '',\n    }\n    global.location = {\n      hostname: '0.0.0.0',\n      port: 0,\n      protocol: 'http:',\n    }\n    global.btoa = () => {}\n    global.crypto = {\n      getRandomValues: function (buffer: any) {\n        return nodeCrypto.randomFillSync(buffer)\n      },\n    }\n  }\n\n  if (global.document != null) {\n    // SSR 环境下可能会报错 （issue 4409）\n    if (global.document.getElementsByTagName == null) {\n      global.document.getElementsByTagName = () => []\n    }\n  }\n}\n"
  },
  {
    "path": "packages/editor/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\n    \"./src/**/*\",\n    \"../custom-types.d.ts\"\n  ]\n}"
  },
  {
    "path": "packages/list-module/CHANGELOG.md",
    "content": "# Change Log\n\nAll notable changes to this project will be documented in this file.\nSee [Conventional Commits](https://conventionalcommits.org) for commit guidelines.\n\n## [1.0.5](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/list-module@1.0.4...@wangeditor/list-module@1.0.5) (2022-09-27)\n\n\n### Bug Fixes\n\n* list-item - 遇到 style 是 toHtml 出错 ([9854308](https://github.com/wangeditor-team/wangEditor/commit/98543083a1cb09207aceb2a4d8f3c1ce020b106d))\n\n\n\n\n\n## [1.0.4](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/list-module@1.0.3...@wangeditor/list-module@1.0.4) (2022-09-27)\n\n**Note:** Version bump only for package @wangeditor/list-module\n"
  },
  {
    "path": "packages/list-module/README.md",
    "content": "# wangEditor list-module\n\nList module built in [wangEditor](https://www.wangeditor.com/) by default.\n"
  },
  {
    "path": "packages/list-module/__tests__/elem-to-html.test.ts",
    "content": "/**\n * @description list toHtml test\n * @author wangfupeng\n */\n\nimport createEditor from '../../../tests/utils/create-editor'\nimport { ELEM_TO_EDITOR } from '../src/utils/maps'\nimport listItemToHtmlConf from '../src/module/elem-to-html'\n\ndescribe('module elem-to-html', () => {\n  const childrenHtml = '<span>hello</span>'\n\n  const orderedElem1 = { type: 'list-item', ordered: true, children: [{ text: '' }] }\n  const orderedElem2 = { type: 'list-item', ordered: true, children: [{ text: '' }] }\n  const unOrderedItem1 = { type: 'list-item', children: [{ text: '' }] }\n  const unOrderedItem2 = { type: 'list-item', children: [{ text: '' }] }\n  const unOrderedItem21 = { type: 'list-item', level: 1, children: [{ text: '' }] }\n\n  const editor = createEditor({\n    content: [orderedElem1, orderedElem2, unOrderedItem1, unOrderedItem2, unOrderedItem21],\n  })\n\n  // elem 绑定 editor\n  ELEM_TO_EDITOR.set(orderedElem1, editor)\n  ELEM_TO_EDITOR.set(orderedElem2, editor)\n  ELEM_TO_EDITOR.set(unOrderedItem1, editor)\n  ELEM_TO_EDITOR.set(unOrderedItem2, editor)\n  ELEM_TO_EDITOR.set(unOrderedItem21, editor)\n\n  test(`toHtml conf type`, () => {\n    expect(listItemToHtmlConf.type).toBe('list-item')\n  })\n\n  test(`ordered item toHtml`, () => {\n    const { elemToHtml } = listItemToHtmlConf\n\n    // first item\n    const firstHtml = elemToHtml(orderedElem1, childrenHtml)\n    expect(firstHtml).toEqual({\n      html: '<li><span>hello</span></li>',\n      prefix: '<ol>', // 第一个 item ，前面会有 <ol>\n      suffix: '',\n    })\n\n    // last item\n    const lastHtml = elemToHtml(orderedElem2, childrenHtml)\n    expect(lastHtml).toEqual({\n      html: '<li><span>hello</span></li>',\n      prefix: '',\n      suffix: '</ol>', // 最后一个 item ，后面会有 </ol>\n    })\n  })\n\n  test(`unOrdered item toHtml`, () => {\n    const { elemToHtml } = listItemToHtmlConf\n\n    // first item\n    const firstHtml = elemToHtml(unOrderedItem1, childrenHtml)\n    expect(firstHtml).toEqual({\n      html: '<li><span>hello</span></li>',\n      prefix: '<ul>', // 第一个 item ，前面会有 <ul>\n      suffix: '',\n    })\n\n    // second item\n    const secondHtml = elemToHtml(unOrderedItem2, childrenHtml)\n    expect(secondHtml).toEqual({\n      html: '<li><span>hello</span></li>', // 第二个 item ，不应该有 <ul>\n      prefix: '',\n      suffix: '',\n    })\n\n    // last item - leveled\n    const lastHtml = elemToHtml(unOrderedItem21, childrenHtml)\n    expect(lastHtml).toEqual({\n      html: '<li><span>hello</span></li>', // 最后一个 item ( leveled ) ，包裹 <ul>\n      prefix: '<ul>',\n      suffix: '</ul></ul>',\n    })\n  })\n})\n"
  },
  {
    "path": "packages/list-module/__tests__/menu/bulleted-list-menu.test.ts",
    "content": "/**\n * @description bulletedList menu test\n * @author wangfupeng\n */\n\nimport BulletedListMenu from '../../src/module/menu/BulletedListMenu'\nimport createEditor from '../../../../tests/utils/create-editor'\n\ndescribe('list BulletedListMenu', () => {\n  const menu = new BulletedListMenu()\n\n  it('getValue', () => {\n    const editor = createEditor()\n    expect(menu.getValue(editor)).toBe('')\n  })\n\n  it('isActive', () => {\n    const editor = createEditor({\n      content: [\n        { type: 'paragraph', children: [{ text: 'hello' }] },\n        { type: 'list-item', children: [{ text: 'a' }] },\n      ],\n    })\n\n    editor.deselect()\n    expect(menu.isActive(editor)).toBeFalsy()\n\n    editor.select({ path: [0, 0], offset: 0 }) // 选中 p\n    expect(menu.isActive(editor)).toBeFalsy()\n\n    editor.select({ path: [1, 0], offset: 0 }) // 选中 li\n    expect(menu.isActive(editor)).toBeTruthy()\n  })\n\n  it('isDisabled', () => {\n    const editor = createEditor({\n      content: [\n        { type: 'paragraph', children: [{ text: 'hello' }] },\n        { type: 'list-item', children: [{ text: 'a' }] },\n        {\n          type: 'table',\n          width: 'auto',\n          children: [\n            {\n              type: 'table-row',\n              children: [{ type: 'table-cell', children: [{ text: '' }], isHeader: true }],\n            },\n          ],\n        },\n        {\n          type: 'pre',\n          children: [{ type: 'code', language: '', children: [{ text: 'a' }] }],\n        },\n      ],\n    })\n\n    editor.deselect()\n    expect(menu.isDisabled(editor)).toBeTruthy()\n\n    editor.select({ path: [0, 0], offset: 0 }) // 选中 p\n    expect(menu.isDisabled(editor)).toBeFalsy()\n\n    editor.select({ path: [1, 0], offset: 0 }) // 选中 li\n    expect(menu.isDisabled(editor)).toBeFalsy()\n\n    editor.select({ path: [2, 0, 0, 0], offset: 0 }) // 选中 table 单元格\n    expect(menu.isDisabled(editor)).toBeTruthy()\n\n    editor.select({ path: [3, 0, 0], offset: 0 }) // 选中 code\n    expect(menu.isDisabled(editor)).toBeTruthy()\n  })\n\n  it('exec', () => {\n    const pElem = { type: 'paragraph', children: [{ text: 'hello' }] }\n    const editor = createEditor({\n      content: [pElem],\n    })\n    editor.select({ path: [0, 0], offset: 0 }) // 选中 p\n\n    menu.exec(editor, '') // p 转 li\n    expect(editor.children).toEqual([\n      {\n        type: 'list-item',\n        ordered: false,\n        children: [{ text: 'hello' }],\n      },\n    ])\n\n    menu.exec(editor, '') // li 转 p\n    expect(editor.children).toEqual([pElem])\n  })\n})\n"
  },
  {
    "path": "packages/list-module/__tests__/menu/numbered-list-menu.test.ts",
    "content": "/**\n * @description list NumberedListMenu test\n * @author wangfupeng\n */\n\nimport NumberedListMenu from '../../src/module/menu/NumberedListMenu'\nimport createEditor from '../../../../tests/utils/create-editor'\n\ndescribe('list NumberedListMenu', () => {\n  const menu = new NumberedListMenu()\n\n  it('getValue', () => {\n    const editor = createEditor()\n    expect(menu.getValue(editor)).toBe('')\n  })\n\n  it('isActive', () => {\n    const editor = createEditor({\n      content: [\n        { type: 'paragraph', children: [{ text: 'hello' }] },\n        { type: 'list-item', ordered: true, children: [{ text: 'a' }] },\n      ],\n    })\n\n    editor.deselect()\n    expect(menu.isActive(editor)).toBeFalsy()\n\n    editor.select({ path: [0, 0], offset: 0 }) // 选中 p\n    expect(menu.isActive(editor)).toBeFalsy()\n\n    editor.select({ path: [1, 0], offset: 0 }) // 选中 li\n    expect(menu.isActive(editor)).toBeTruthy()\n  })\n\n  it('isDisabled', () => {\n    const editor = createEditor({\n      content: [\n        { type: 'paragraph', children: [{ text: 'hello' }] },\n        { type: 'list-item', ordered: true, children: [{ text: 'a' }] },\n        {\n          type: 'table',\n          width: 'auto',\n          children: [\n            {\n              type: 'table-row',\n              children: [{ type: 'table-cell', children: [{ text: '' }], isHeader: true }],\n            },\n          ],\n        },\n        {\n          type: 'pre',\n          children: [{ type: 'code', language: '', children: [{ text: 'a' }] }],\n        },\n      ],\n    })\n\n    editor.deselect()\n    expect(menu.isDisabled(editor)).toBeTruthy()\n\n    editor.select({ path: [0, 0], offset: 0 }) // 选中 p\n    expect(menu.isDisabled(editor)).toBeFalsy()\n\n    editor.select({ path: [1, 0], offset: 0 }) // 选中 li\n    expect(menu.isDisabled(editor)).toBeFalsy()\n\n    editor.select({ path: [2, 0, 0, 0], offset: 0 }) // 选中 table 单元格\n    expect(menu.isDisabled(editor)).toBeTruthy()\n\n    editor.select({ path: [3, 0, 0], offset: 0 }) // 选中 code\n    expect(menu.isDisabled(editor)).toBeTruthy()\n  })\n\n  it('exec', () => {\n    const pElem = { type: 'paragraph', children: [{ text: 'hello' }] }\n    const editor = createEditor({\n      content: [pElem],\n    })\n    editor.select({ path: [0, 0], offset: 0 }) // 选中 p\n\n    menu.exec(editor, '') // p 转 li\n    expect(editor.children).toEqual([\n      {\n        type: 'list-item',\n        ordered: true,\n        children: [{ text: 'hello' }],\n      },\n    ])\n\n    menu.exec(editor, '') // li 转 p\n    expect(editor.children).toEqual([pElem])\n  })\n})\n"
  },
  {
    "path": "packages/list-module/__tests__/parse-html.test.ts",
    "content": "/**\n * @description parse html test\n * @author wangfupeng\n */\n\nimport { $ } from 'dom7'\nimport createEditor from '../../../tests/utils/create-editor'\nimport { parseItemHtmlConf, parseListHtmlConf } from '../src/module/parse-elem-html'\n\ndescribe('list - parse html', () => {\n  const editor = createEditor()\n\n  it('parse unOrdered list item', () => {\n    const $ul = $('<ul></ul>')\n    const $li = $('<li></li>')\n    $ul.append($li)\n    const children = [{ text: 'hello' }]\n\n    const elem = parseItemHtmlConf.parseElemHtml($li[0], children, editor)\n    expect(elem).toEqual({\n      type: 'list-item',\n      ordered: false,\n      level: 0,\n      children,\n    })\n  })\n\n  it('parse ordered list item', () => {\n    const $ol = $('<ol></ol>')\n    const $li = $('<li></li>')\n    $ol.append($li)\n    const children = [{ text: 'hello' }]\n\n    const elem = parseItemHtmlConf.parseElemHtml($li[0], children, editor)\n    expect(elem).toEqual({\n      type: 'list-item',\n      ordered: true,\n      level: 0,\n      children,\n    })\n  })\n\n  it('parse leveled list item', () => {\n    const $ul = $('<ul></ul>')\n    const $ol = $('<ol></ol>')\n    const $li = $('<li></li>')\n    $ul.append($ol)\n    $ol.append($li)\n    const children = [{ text: 'hello' }]\n\n    const elem = parseItemHtmlConf.parseElemHtml($li[0], children, editor)\n    expect(elem).toEqual({\n      type: 'list-item',\n      ordered: true,\n      level: 1,\n      children,\n    })\n  })\n\n  it('parse list', () => {\n    const $ol = $('<ol></ol>')\n    const children = [\n      {\n        type: 'list-item',\n        ordered: true,\n        children: [{ text: 'a' }],\n      },\n      {\n        type: 'list-item',\n        ordered: true,\n        children: [{ text: 'b' }],\n      },\n      // 嵌套列表\n      [\n        {\n          type: 'list-item',\n          level: 1,\n          children: [{ text: 'x' }],\n        },\n        {\n          type: 'list-item',\n          level: 1,\n          children: [{ text: 'y' }],\n        },\n      ],\n    ]\n    // @ts-ignore\n    const listElems = parseListHtmlConf.parseElemHtml($ol[0], children, editor)\n    expect(listElems.length).toBe(4) // parse list 时，会把输出的结果（数组）flatten ，把嵌套的平铺开\n  })\n})\n"
  },
  {
    "path": "packages/list-module/__tests__/plugin.test.ts",
    "content": "/**\n * @description list plugin test\n * @author wangfupeng\n */\n\nimport withList from '../src/module/plugin'\nimport createEditor from '../../../tests/utils/create-editor'\n\ndescribe('list plugin test', () => {\n  it('insert tab - increase level', () => {\n    const listItem = { type: 'list-item', children: [{ text: 'hello' }] }\n    let editor = createEditor({\n      content: [listItem],\n    })\n    editor = withList(editor) // 使用插件\n    editor.select({ path: [0, 0], offset: 0 }) // 选中 list-item 开头\n\n    editor.handleTab() // tab\n\n    const children = editor.children\n    expect(children).toEqual([\n      {\n        ...listItem,\n        level: 1, // 增加 level\n      },\n    ])\n  })\n\n  it('insert delete - decrease level', () => {\n    const listItem = { type: 'list-item', children: [{ text: 'hello' }], level: 2 }\n    let editor = createEditor({\n      content: [listItem],\n    })\n    editor = withList(editor) // 使用插件\n    editor.select({ path: [0, 0], offset: 0 }) // 选中 list-item 开头\n\n    editor.deleteBackward('character') // delete\n    expect(editor.children).toEqual([\n      {\n        ...listItem,\n        level: 1, // 减少 level\n      },\n    ])\n\n    editor.deleteBackward('character') // delete\n    expect(editor.children).toEqual([\n      {\n        ...listItem,\n        level: 0, // 减少 level\n      },\n    ])\n  })\n\n  it('兼容之前的 JSON 格式', () => {\n    const listItem = { type: 'list-item', children: [{ text: 'hello' }] }\n    let editor = createEditor({\n      // 之前的 JSON 格式\n      content: [\n        {\n          type: 'bulleted-list',\n          children: [listItem],\n        },\n      ],\n    })\n    editor = withList(editor) // 使用插件\n\n    expect(editor.children).toEqual([listItem])\n  })\n})\n"
  },
  {
    "path": "packages/list-module/__tests__/render-elem.test.ts",
    "content": "/**\n * @description list render elem test\n * @author wangfupeng\n */\n\nimport createEditor from '../../../tests/utils/create-editor'\nimport renderListItemConf from '../src/module/render-elem'\n\ndescribe('list module - render elem', () => {\n  const unOrderedItem = { type: 'list-item', children: [{ text: '' }] }\n  const orderedItem = { type: 'list-item', ordered: true, children: [{ text: '' }] }\n  const leveledItem = { type: 'list-item', level: 3, children: [{ text: '' }] }\n  const editor = createEditor({\n    content: [unOrderedItem, orderedItem, leveledItem],\n  })\n\n  it('render conf type', () => {\n    expect(renderListItemConf.type).toBe('list-item')\n  })\n\n  it('render ordered list item elem', () => {\n    const vnode: any = renderListItemConf.renderElem(orderedItem, null, editor)\n    expect(vnode.sel).toBe('div') // render-elem 使用 <div> 模拟 <li>\n\n    const prefixVnode = vnode.children[0] || {}\n    expect(prefixVnode.text).toBe('1.') // ordered list-item 有序号\n  })\n\n  it('render unOrdered list item elem', () => {\n    const vnode: any = renderListItemConf.renderElem(unOrderedItem, null, editor)\n    expect(vnode.sel).toBe('div') // render-elem 使用 <div> 模拟 <li>\n\n    const prefixVnode = vnode.children[0] || {}\n    expect(prefixVnode.text).toBe('•') // unOrdered list-item 点号\n  })\n\n  it('render leveled list item elem', () => {\n    const vnode: any = renderListItemConf.renderElem(leveledItem, null, editor)\n    const style = vnode.data.style\n    expect(style).toEqual({ margin: '5px 0 5px 60px' }) // margin-left 60px\n  })\n})\n"
  },
  {
    "path": "packages/list-module/package.json",
    "content": "{\n  \"name\": \"@wangeditor/list-module\",\n  \"version\": \"1.0.5\",\n  \"description\": \"wangEditor list module\",\n  \"author\": \"wangfupeng1988 <wangfupeng1988@163.com>\",\n  \"contributors\": [],\n  \"homepage\": \"https://github.com/wangeditor-team/wangEditor#readme\",\n  \"license\": \"MIT\",\n  \"types\": \"dist/list-module/src/index.d.ts\",\n  \"main\": \"dist/index.js\",\n  \"module\": \"dist/index.esm.js\",\n  \"browser\": {\n    \"./dist/index.js\": \"./dist/index.js\",\n    \"./dist/index.esm.js\": \"./dist/index.esm.js\"\n  },\n  \"directories\": {\n    \"lib\": \"dist\",\n    \"test\": \"__tests__\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.com/\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wangeditor-team/wangEditor.git\"\n  },\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"test-c\": \"jest --coverage\",\n    \"dev\": \"cross-env NODE_ENV=development rollup -c rollup.config.js\",\n    \"dev-watch\": \"cross-env NODE_ENV=development rollup -c rollup.config.js -w\",\n    \"build\": \"cross-env NODE_ENV=production rollup -c rollup.config.js\",\n    \"dev-size-stats\": \"cross-env NODE_ENV=development:size_stats rollup -c rollup.config.js\",\n    \"size-stats\": \"cross-env NODE_ENV=production:size_stats rollup -c rollup.config.js\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/wangeditor-team/wangEditor/issues\"\n  },\n  \"peerDependencies\": {\n    \"@wangeditor/core\": \"1.x\",\n    \"dom7\": \"^3.0.0\",\n    \"slate\": \"^0.72.0\",\n    \"snabbdom\": \"^3.1.0\"\n  }\n}\n"
  },
  {
    "path": "packages/list-module/rollup.config.js",
    "content": "import { createRollupConfig, IS_PRD } from '../../build/create-rollup-config'\nimport pkg from './package.json'\n\nconst name = 'WangEditorListModule'\n\nconst configList = []\n\n// esm - 开发环境不需要 CDN 引入，只需要 npm 引入，所以优先输出 esm\nconst esmConf = createRollupConfig({\n  output: {\n    file: pkg.module,\n    format: 'esm',\n    name,\n  },\n})\nconfigList.push(esmConf)\n\n// umd\nconst umdConf = createRollupConfig({\n  output: {\n    file: pkg.main,\n    format: 'umd',\n    name,\n  },\n})\nconfigList.push(umdConf)\n\nexport default configList\n"
  },
  {
    "path": "packages/list-module/src/assets/index.less",
    "content": ""
  },
  {
    "path": "packages/list-module/src/constants/svg.ts",
    "content": "/**\n * @description icon svg\n * @author wangfupeng\n */\n\n/**\n * 【注意】svg 字符串的长度 ，否则会导致代码体积过大\n * 尽量选择 https://www.iconfont.cn/collections/detail?spm=a313x.7781069.0.da5a778a4&cid=20293\n * 找不到再从 iconfont.com 搜索\n */\n\n// 无序列表\nexport const BULLETED_LIST_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M384 64h640v128H384V64z m0 384h640v128H384v-128z m0 384h640v128H384v-128zM0 128a128 128 0 1 1 256 0 128 128 0 0 1-256 0z m0 384a128 128 0 1 1 256 0 128 128 0 0 1-256 0z m0 384a128 128 0 1 1 256 0 128 128 0 0 1-256 0z\"></path></svg>'\n\n// 有序列表\nexport const NUMBERED_LIST_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M384 832h640v128H384z m0-384h640v128H384z m0-384h640v128H384zM192 0v256H128V64H64V0zM128 526.016v50.016h128v64H64v-146.016l128-60V384H64v-64h192v146.016zM256 704v320H64v-64h128v-64H64v-64h128v-64H64v-64z\"></path></svg>'\n"
  },
  {
    "path": "packages/list-module/src/index.ts",
    "content": "/**\n * @description list module\n * @author wangfupeng\n */\n\nimport './assets/index.less'\n\n// 配置多语言\nimport './locale/index'\n\n// 导出 module\nimport wangEditorListModule from './module/index'\nexport default wangEditorListModule\n"
  },
  {
    "path": "packages/list-module/src/locale/en.ts",
    "content": "/**\n * @description i18n en\n * @author wangfupeng\n */\n\nexport default {\n  listModule: {\n    unOrderedList: 'Unordered list',\n    orderedList: 'Ordered list',\n  },\n}\n"
  },
  {
    "path": "packages/list-module/src/locale/index.ts",
    "content": "/**\n * @description i18n entry\n * @author wangfupeng\n */\n\nimport { i18nAddResources } from '@wangeditor/core'\nimport enResources from './en'\nimport zhResources from './zh-CN'\n\ni18nAddResources('en', enResources)\ni18nAddResources('zh-CN', zhResources)\n"
  },
  {
    "path": "packages/list-module/src/locale/zh-CN.ts",
    "content": "/**\n * @description i18n zh-CN\n * @author wangfupeng\n */\n\nexport default {\n  listModule: {\n    unOrderedList: '无序列表',\n    orderedList: '有序列表',\n  },\n}\n"
  },
  {
    "path": "packages/list-module/src/module/custom-types.ts",
    "content": "/**\n * @description list element\n * @author wangfupeng\n */\n\nimport { Text } from 'slate'\n\n//【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts\n\nexport type ListItemElement = {\n  type: 'list-item'\n  ordered: boolean // 有序/无序\n  level: number // 层级：0 1 2 ...\n  children: Text[]\n}\n"
  },
  {
    "path": "packages/list-module/src/module/elem-to-html.ts",
    "content": "/**\n * @description to html\n * @author wangfupeng\n */\n\nimport { Element, Path, Editor } from 'slate'\nimport { DomEditor } from '@wangeditor/core'\nimport { ListItemElement } from './custom-types'\nimport { ELEM_TO_EDITOR } from '../utils/maps'\n\n/**\n * 当前 list-item 前面需要拼接几个 <ol> 或 <ul>\n * @param elem elem\n */\nfunction getStartContainerTagNumber(elem: Element): number {\n  const editor = ELEM_TO_EDITOR.get(elem)\n  if (editor == null) return 0\n\n  const { type, ordered = false, level = 0 } = elem as ListItemElement\n\n  const path = DomEditor.findPath(editor, elem)\n  if (path[0] === 0) {\n    // list-item 是第一个元素，再往前没有了。需要拼接 <ol> 或 <ul>\n    return level + 1\n  }\n\n  // 获取上一个 elem\n  const prevPath = Path.previous(path)\n  const prevEntry = Editor.node(editor, prevPath)\n  if (!prevEntry) return 0\n  const [prevElem] = prevEntry\n\n  const prevType = DomEditor.getNodeType(prevElem)\n  if (prevType !== type) {\n    // 上一个 elem 不是 list-item ，需要拼接 <ol> 或 <ul>\n    return level + 1\n  }\n\n  // 上一个 elem 是 list-item\n  const { ordered: prevOrdered = false, level: prevLevel = 0 } = prevElem as ListItemElement\n  if (prevLevel < level) {\n    // 上一个 level 小于当前 level ，需要拼接 <ol> 或 <ul>\n    return level - prevLevel\n  }\n  if (prevLevel > level) {\n    // 上一个 level 大于当前 level ，不需要拼接 <ol> 或 <ul>\n    return 0\n  }\n  if (prevLevel === level) {\n    // 上一个 level 等于当前 level\n    if (prevOrdered === ordered) {\n      // ordered 一致，则不需要拼接 <ol> 或 <ul>\n      return 0\n    } else {\n      /// ordered 不一致，则需要拼接 <ol> 或 <ul>\n      return 1\n    }\n  }\n\n  // 其他情况\n  return 0\n}\n\n/**\n * 当前 list-item 后面面需要拼接几个 </ol> 或 </ul>\n * @param elem elem\n */\nfunction getEndContainerTagNumber(elem: Element): number {\n  const editor = ELEM_TO_EDITOR.get(elem)\n  if (editor == null) return 0\n\n  const { type, ordered = false, level = 0 } = elem as ListItemElement\n\n  const path = DomEditor.findPath(editor, elem)\n  if (path[0] === editor.children.length - 1) {\n    // list-item 是最后一个元素，再往后没有了。需要拼接 </ol> 或 </ul>\n    return level + 1\n  }\n\n  // 获取下一个 elem\n  const nextPath = Path.next(path)\n  const nextEntry = Editor.node(editor, nextPath)\n  if (!nextEntry) return 0\n  const [nextElem] = nextEntry\n\n  const nextType = DomEditor.getNodeType(nextElem)\n  if (nextType !== type) {\n    // 下一个 elem 不是 list-item ，需要拼接 <ol> 或 <ul>\n    return level + 1\n  }\n\n  // 下一个 elem 是 list-item\n  const { ordered: nextOrdered = false, level: nextLevel = 0 } = nextElem as ListItemElement\n  if (nextLevel < level) {\n    // 下一个 level 小于当前 level ，需要拼接 </ol> 或 </ul>\n    return level - nextLevel\n  }\n  if (nextLevel > level) {\n    // 下一个 level 大于当前 level ，不需要拼接 </ol> 或 </ul>\n    return 0\n  }\n  if (nextLevel === level) {\n    // 下一个 level 等于当前 level\n    if (nextOrdered === ordered) {\n      // ordered 一致，则不需要拼接 </ol> 或 </ul>\n      return 0\n    } else {\n      /// ordered 不一致，则需要拼接 </ol> 或 </ul>\n      return 1\n    }\n  }\n\n  // 其他情况\n  return 0\n}\n\n// ol ul 栈\nconst CONTAINER_TAG_STACK: Array<string> = []\n\nfunction elemToHtml(\n  elem: Element,\n  childrenHtml: string\n): {\n  html: string\n  prefix?: string\n  suffix?: string\n} {\n  let startContainerStr = ''\n  let endContainerStr = ''\n\n  const { ordered = false } = elem as ListItemElement\n  const containerTag = ordered ? 'ol' : 'ul'\n\n  // 前面需要拼接几个 <ol> 或 <ul>\n  const startContainerTagNumber = getStartContainerTagNumber(elem)\n  if (startContainerTagNumber > 0) {\n    for (let i = 0; i < startContainerTagNumber; i++) {\n      startContainerStr += `<${containerTag}>` // 记录 start container tag ，如 `<ul>`\n      CONTAINER_TAG_STACK.push(containerTag) // tag 压栈\n    }\n  }\n\n  // 后面需要拼接几个 </ol> 或 </ul>\n  const endContainerTagNumber = getEndContainerTagNumber(elem)\n  if (endContainerTagNumber > 0) {\n    for (let i = 0; i < endContainerTagNumber; i++) {\n      const tag = CONTAINER_TAG_STACK.pop() // tag 从栈中获取\n      endContainerStr += `</${tag}>` // 记录 end container tag ，如 `</ul>`\n    }\n  }\n\n  return {\n    html: `<li>${childrenHtml}</li>`,\n    prefix: startContainerStr,\n    suffix: endContainerStr,\n  }\n}\n\nconst listItemToHtmlConf = {\n  type: 'list-item',\n  elemToHtml: elemToHtml,\n}\n\nexport default listItemToHtmlConf\n"
  },
  {
    "path": "packages/list-module/src/module/index.ts",
    "content": "/**\n * @description list module entry\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport renderListItemConf from './render-elem'\nimport withList from './plugin'\nimport { bulletedListMenuConf, numberedListMenuConf } from './menu/index'\nimport listItemToHtmlConf from './elem-to-html'\nimport { parseItemHtmlConf, parseListHtmlConf } from './parse-elem-html'\n\nconst list: Partial<IModuleConf> = {\n  renderElems: [renderListItemConf],\n  editorPlugin: withList,\n  menus: [bulletedListMenuConf, numberedListMenuConf],\n  elemsToHtml: [listItemToHtmlConf],\n  parseElemsHtml: [parseListHtmlConf, parseItemHtmlConf],\n}\n\nexport default list\n"
  },
  {
    "path": "packages/list-module/src/module/menu/BaseMenu.ts",
    "content": "/**\n * @description base menu\n * @author wangfupeng\n */\n\nimport { Editor, Node, Transforms, Element } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor } from '@wangeditor/core'\nimport { ListItemElement } from '../custom-types'\n\nabstract class BaseMenu implements IButtonMenu {\n  readonly type = 'list-item'\n  abstract readonly ordered: boolean\n  abstract readonly title: string\n  abstract readonly iconSvg: string\n  readonly tag = 'button'\n\n  private getListNode(editor: IDomEditor): Node | null {\n    const { type } = this\n    return DomEditor.getSelectedNodeByType(editor, type)\n  }\n\n  getValue(editor: IDomEditor): string | boolean {\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    const node = this.getListNode(editor)\n    if (node == null) return false\n    const { ordered = false } = node as ListItemElement\n    return ordered === this.ordered\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n\n    const selectedElems = DomEditor.getSelectedElems(editor)\n    const notMatch = selectedElems.some((elem: Element) => {\n      if (Editor.isVoid(editor, elem) && Editor.isBlock(editor, elem)) return true\n\n      const { type } = elem as Element\n      if (['pre', 'code', 'table'].includes(type)) return true\n    })\n    if (notMatch) return true\n\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean): void {\n    const active = this.isActive(editor)\n    if (active) {\n      // 如果当前 active ，则转换为 p 标签\n      Transforms.setNodes(editor, {\n        type: 'paragraph',\n        ordered: undefined,\n        level: undefined,\n      })\n    } else {\n      // 否则，转换为 list-item\n      Transforms.setNodes(editor, {\n        type: 'list-item',\n        ordered: this.ordered, // 有序/无序\n        indent: undefined,\n      })\n    }\n  }\n}\n\nexport default BaseMenu\n"
  },
  {
    "path": "packages/list-module/src/module/menu/BulletedListMenu.ts",
    "content": "/**\n * @description bulleted list menu\n * @author wangfupeng\n */\n\nimport { t } from '@wangeditor/core'\nimport BaseMenu from './BaseMenu'\nimport { BULLETED_LIST_SVG } from '../../constants/svg'\n\nclass BulletedListMenu extends BaseMenu {\n  readonly ordered = false\n  readonly title = t('listModule.unOrderedList')\n  readonly iconSvg = BULLETED_LIST_SVG\n}\n\nexport default BulletedListMenu\n"
  },
  {
    "path": "packages/list-module/src/module/menu/NumberedListMenu.ts",
    "content": "/**\n * @description numbered list menu\n * @author wangfupeng\n */\n\nimport { t } from '@wangeditor/core'\nimport BaseMenu from './BaseMenu'\nimport { NUMBERED_LIST_SVG } from '../../constants/svg'\n\nclass NumberedListMenu extends BaseMenu {\n  readonly ordered = true\n  readonly title = t('listModule.orderedList')\n  readonly iconSvg = NUMBERED_LIST_SVG\n}\n\nexport default NumberedListMenu\n"
  },
  {
    "path": "packages/list-module/src/module/menu/index.ts",
    "content": "/**\n * @description menu entry\n * @author wangfupeng\n */\n\nimport BulletedListMenu from './BulletedListMenu'\nimport NumberedListMenu from './NumberedListMenu'\n\nexport const bulletedListMenuConf = {\n  key: 'bulletedList',\n  factory() {\n    return new BulletedListMenu()\n  },\n}\n\nexport const numberedListMenuConf = {\n  key: 'numberedList',\n  factory() {\n    return new NumberedListMenu()\n  },\n}\n"
  },
  {
    "path": "packages/list-module/src/module/parse-elem-html.ts",
    "content": "/**\n * @description parse elem html\n * @author wangfupeng\n */\n\nimport { Dom7Array } from 'dom7'\nimport { Descendant, Text } from 'slate'\nimport $, { DOMElement, getTagName } from '../utils/dom'\nimport { IDomEditor } from '@wangeditor/core'\nimport { ListItemElement } from './custom-types'\n\n/**\n * 获取 ordered\n * @param $elem list $elem\n */\nfunction getOrdered($elem: Dom7Array): boolean {\n  const $list = $elem.parent()\n  const listTagName = getTagName($list)\n  if (listTagName === 'ol') return true\n  return false\n}\n\n/**\n * 获取 level\n * @param $elem list $elem\n */\nfunction getLevel($elem: Dom7Array): number {\n  let level = 0\n\n  let $cur: Dom7Array = $elem.parent()\n  let tagName: string = getTagName($cur)\n\n  while (tagName === 'ul' || tagName === 'ol') {\n    $cur = $cur.parent()\n    tagName = getTagName($cur)\n    level++\n  }\n\n  return level - 1\n}\n\nfunction parseItemHtml(\n  elem: DOMElement,\n  children: Descendant[],\n  editor: IDomEditor\n): ListItemElement {\n  const $elem = $(elem)\n\n  children = children.filter(child => {\n    if (Text.isText(child)) return true\n    if (editor.isInline(child)) return true\n    return false\n  })\n\n  // 无 children ，则用纯文本\n  if (children.length === 0) {\n    children = [{ text: $elem.text().replace(/\\s+/gm, ' ') }]\n  }\n\n  const ordered = getOrdered($elem)\n  const level = getLevel($elem)\n\n  return {\n    type: 'list-item',\n    ordered,\n    level,\n    // @ts-ignore\n    children,\n  }\n}\n\nexport const parseItemHtmlConf = {\n  selector: 'li:not([data-w-e-type])', // data-w-e-type 属性，留给自定义元素，保证扩展性\n  parseElemHtml: parseItemHtml,\n}\n\nfunction parseListHtml(\n  elem: DOMElement,\n  children: Descendant[],\n  editor: IDomEditor\n): ListItemElement[] {\n  // @ts-ignore flatten 因为可能有 ul/ol 嵌套，重要！！！\n  return children.flat(Infinity)\n}\n\nexport const parseListHtmlConf = {\n  selector: 'ul:not([data-w-e-type]),ol:not([data-w-e-type])', // data-w-e-type 属性，留给自定义元素，保证扩展性\n  parseElemHtml: parseListHtml,\n}\n"
  },
  {
    "path": "packages/list-module/src/module/plugin.ts",
    "content": "/**\n * @description editor 插件，重写 editor API\n * @author wangfupeng\n */\n\nimport { Editor, Transforms, Range } from 'slate'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\nimport { ListItemElement } from './custom-types'\n\n/**\n * 获取选中的 top elems\n * @param editor editor\n */\nfunction getTopSelectedElemsBySelection(editor: IDomEditor) {\n  return Editor.nodes(editor, {\n    at: editor.selection || undefined,\n    match: n => DomEditor.findPath(editor, n).length === 1, // 只匹配顶级元素\n  })\n}\n\nfunction withList<T extends IDomEditor>(editor: T): T {\n  const { deleteBackward, handleTab, normalizeNode } = editor\n  const newEditor = editor\n\n  // 重写 deleteBackward - 降低 level 或者转换为 p 元素\n  newEditor.deleteBackward = unit => {\n    const { selection } = newEditor\n    if (selection == null) {\n      deleteBackward(unit)\n      return\n    }\n\n    if (Range.isExpanded(selection)) {\n      deleteBackward(unit)\n      return\n    }\n\n    const listItemElem = DomEditor.getSelectedNodeByType(newEditor, 'list-item')\n    if (listItemElem == null) {\n      // 未匹配到 list-item\n      deleteBackward(unit)\n      return\n    }\n\n    if (selection.focus.offset === 0) {\n      // 选中了当前 list-item 文本的开头，此时按删除键，应该降低 level 或转换为 p 元素\n      const { level = 0 } = listItemElem as ListItemElement\n      if (level > 0) {\n        // 降低 level\n        Transforms.setNodes(newEditor, { level: level - 1 })\n      } else {\n        // 转换为 p 元素\n        Transforms.setNodes(newEditor, {\n          type: 'paragraph',\n          ordered: undefined,\n          level: undefined,\n        })\n      }\n      return\n    }\n\n    // 其他情况\n    deleteBackward(unit)\n  }\n\n  // 重写 tab - 当选中 list-item 文本开头时，增加 level\n  newEditor.handleTab = () => {\n    const { selection } = newEditor\n    if (selection == null) {\n      handleTab()\n      return\n    }\n\n    // 选区是合并的，判断单个 list-item 即可\n    if (Range.isCollapsed(selection)) {\n      const listItemElem = DomEditor.getSelectedNodeByType(newEditor, 'list-item')\n      if (listItemElem == null) {\n        // 未匹配到 list-item\n        handleTab()\n        return\n      }\n\n      if (selection.focus.offset === 0) {\n        // 选中了当前 list-item 文本的开头，此时按 tab 应该增加 level\n        const { level = 0 } = listItemElem as ListItemElement\n        Transforms.setNodes(newEditor, { level: level + 1 })\n        return\n      }\n    }\n\n    // 选区是展开的，要判断多个 list-item\n    if (Range.isExpanded(selection)) {\n      let listItemNum = 0 // 选中的 list-item 有几个\n      let hasOtherElem = false // 是否有其他元素\n\n      for (const entry of getTopSelectedElemsBySelection(newEditor)) {\n        const [elem] = entry\n        const type = DomEditor.getNodeType(elem)\n        if (type === 'list-item') listItemNum++\n        else hasOtherElem = true\n      }\n\n      if (hasOtherElem || listItemNum <= 1) {\n        // 选中了其他元素，或者只选中一个 list-item ，则执行默认行为\n        handleTab()\n        return\n      }\n\n      // 未选中其他元素，且选中多个 list-item ，则增加 level\n      for (const entry of getTopSelectedElemsBySelection(newEditor)) {\n        const [elem, path] = entry\n        const { level = 0 } = elem as ListItemElement\n        Transforms.setNodes(newEditor, { level: level + 1 }, { at: path })\n      }\n      return\n    }\n\n    // 其他情况\n    handleTab()\n  }\n\n  // 兼容之前的 JSON 格式 `numbered-list` 和 `bulleted-list` （之前的 list 没有嵌套功能）\n  newEditor.normalizeNode = ([node, path]) => {\n    const type = DomEditor.getNodeType(node)\n\n    if (type === 'bulleted-list' || type === 'numbered-list') {\n      Transforms.unwrapNodes(newEditor, { at: path })\n    }\n\n    // 执行默认行为\n    return normalizeNode([node, path])\n  }\n\n  return newEditor\n}\n\nexport default withList\n"
  },
  {
    "path": "packages/list-module/src/module/render-elem.tsx",
    "content": "/**\n * @description render list elem\n * @author wangfupeng\n */\n\nimport { Element as SlateElement, Path, Editor, Text } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\nimport { ListItemElement } from './custom-types'\nimport { ELEM_TO_EDITOR } from '../utils/maps'\n\n/**\n * 无序列表：根据 level 获取的前置符号\n * @param level 层级\n */\nfunction genPreSymbol(level = 0): string {\n  let s = ''\n  switch (level) {\n    case 0:\n      s = '•' // 第一层级\n      break\n    case 1:\n      s = '◦' // 第一层级\n      break\n    case 2:\n      s = '▪' // 第三层级\n      break\n    default:\n      s = '▪' // 其他层级\n  }\n  return s\n}\n\n/**\n * 有序列表：获取前缀 number\n * @param editor editor\n * @param elem listItem elem\n */\nfunction getOrderedItemNumber(editor: IDomEditor, elem: SlateElement): number {\n  const { type, level = 0, ordered = false } = elem as ListItemElement\n  if (!ordered) {\n    return -1 // 不是有序列表\n  }\n\n  let num = 1 // 默认值 1\n  let curElem = elem\n  let curPath = DomEditor.findPath(editor, curElem)\n\n  // 第一个元素，直接返回 1\n  if (curPath[0] === 0) return 1\n\n  while (curPath[0] > 0) {\n    const prevPath = Path.previous(curPath)\n    const prevEntry = Editor.node(editor, prevPath)\n    if (prevEntry == null) break\n    const prevElem = prevEntry[0] as ListItemElement // 上一个节点\n    const { level: prevLevel = 0, type: prevType, ordered: prevOrdered } = prevElem\n\n    // type 不一致，退出循环，不再累加 num\n    if (prevType !== type) break\n    // prevLevel 更小，退出循环，不再累加 num\n    if (prevLevel < level) break\n\n    if (prevLevel === level) {\n      // level 一样，如果 ordered 不一样，则退出循环，不再累加 num\n      if (prevOrdered !== ordered) break\n      // level 一样，order 一样，则累加 num\n      else num++\n    }\n\n    // prevLevel 更大，不累加 num ，继续向前\n    curElem = prevElem\n    curPath = prevPath\n  }\n\n  return num\n}\n\n/**\n * 获取第一个 text-node 的颜色\n * @param elem elem\n */\nfunction getListItemColor(elem: SlateElement): string {\n  const children = elem.children || []\n  const length = children.length\n  if (length === 0) return ''\n\n  let firstTextNode\n\n  for (let i = 0; i < length; i++) {\n    if (firstTextNode) break // 已找到第一个 text-node ，则退出\n    const child = children[i]\n    if (Text.isText(child)) firstTextNode = child\n  }\n\n  if (firstTextNode == null) return ''\n  return firstTextNode['color'] || ''\n}\n\nfunction renderListElem(\n  elemNode: SlateElement,\n  children: VNode[] | null,\n  editor: IDomEditor\n): VNode {\n  ELEM_TO_EDITOR.set(elemNode, editor) // 记录 elem 和 editor 关系，elem-to-html 时要用\n\n  const { level = 0, ordered = false } = elemNode as ListItemElement\n\n  // 根据 level 增加 margin-left\n  const listStyle = { margin: `5px 0 5px ${level * 20}px` }\n\n  // list-item 前缀\n  let prefix = ''\n  if (ordered) {\n    // 有序列表：获取前缀 number\n    const orderedNumber = getOrderedItemNumber(editor, elemNode)\n    prefix = `${orderedNumber}.`\n  } else {\n    // 无序列表：根据层级，使用不同的前缀符号\n    prefix = genPreSymbol(level)\n  }\n\n  // 获取前缀颜色\n  const prefixColor = getListItemColor(elemNode)\n\n  const vnode = (\n    <div style={listStyle}>\n      <span\n        contentEditable={false}\n        style={{ marginRight: '0.5em', color: prefixColor }}\n        data-w-e-reserve\n      >\n        {prefix}\n      </span>\n      <span>{children}</span>\n    </div>\n  )\n  return vnode\n}\n\nconst renderListItemConf = {\n  type: 'list-item',\n  renderElem: renderListElem,\n}\n\nexport default renderListItemConf\n"
  },
  {
    "path": "packages/list-module/src/utils/dom.ts",
    "content": "/**\n * @description DOM 操作\n * @author wangfupeng\n */\n\nimport $, { append, on, focus, attr, val, html, parent, hasClass, Dom7Array, empty } from 'dom7'\n\nif (append) $.fn.append = append\n// if (on) $.fn.on = on\n// if (focus) $.fn.focus = focus\nif (attr) $.fn.attr = attr\n// if (val) $.fn.val = val\n// if (html) $.fn.html = html\nif (parent) $.fn.parent = parent\n// if (hasClass) $.fn.hasClass = hasClass\n// if (empty) $.fn.empty = empty\n\nexport default $\n\n// COMPAT: This is required to prevent TypeScript aliases from doing some very\n// weird things for Slate's types with the same name as globals. (2019/11/27)\n// https://github.com/microsoft/TypeScript/issues/35002\nimport DOMNode = globalThis.Node\nimport DOMComment = globalThis.Comment\nimport DOMElement = globalThis.Element\nimport DOMText = globalThis.Text\nimport DOMRange = globalThis.Range\nimport DOMSelection = globalThis.Selection\nimport DOMStaticRange = globalThis.StaticRange\nexport { DOMNode, DOMComment, DOMElement, DOMText, DOMRange, DOMSelection, DOMStaticRange }\n\n/**\n * 获取 tagName lower-case\n * @param $elem $elem\n */\nexport function getTagName($elem: Dom7Array): string {\n  if ($elem.length) return $elem[0].tagName.toLowerCase()\n  return ''\n}\n"
  },
  {
    "path": "packages/list-module/src/utils/maps.ts",
    "content": "/**\n * @description maps\n * @author wangfupeng\n */\n\nimport { Element as SlateElement } from 'slate'\nimport { IDomEditor } from '@wangeditor/core'\n\nexport const ELEM_TO_EDITOR = new WeakMap<SlateElement, IDomEditor>()\n"
  },
  {
    "path": "packages/list-module/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {},\n  \"include\": [\n    \"./src/**/*\",\n    \"../custom-types.d.ts\"\n  ]\n}"
  },
  {
    "path": "packages/table-module/CHANGELOG.md",
    "content": "# Change Log\n\nAll notable changes to this project will be documented in this file.\nSee [Conventional Commits](https://conventionalcommits.org) for commit guidelines.\n\n## [1.1.4](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/table-module@1.1.3...@wangeditor/table-module@1.1.4) (2022-09-27)\n\n**Note:** Version bump only for package @wangeditor/table-module\n\n\n\n\n\n## [1.1.3](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/table-module@1.1.2...@wangeditor/table-module@1.1.3) (2022-09-15)\n\n\n### Bug Fixes\n\n* 插入表格会删掉去掉 issue 4711 ([d4fac4e](https://github.com/wangeditor-team/wangEditor/commit/d4fac4efd06480457a95c2b06e7472cf6204de58))\n\n\n\n\n\n## [1.1.2](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/table-module@1.1.1...@wangeditor/table-module@1.1.2) (2022-09-14)\n\n**Note:** Version bump only for package @wangeditor/table-module\n\n\n\n\n\n## [1.1.1](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/table-module@1.1.0...@wangeditor/table-module@1.1.1) (2022-07-11)\n\n\n### Bug Fixes\n\n* disabled 时，点击 table 会弹出菜单栏 ([9aa4b80](https://github.com/wangeditor-team/wangEditor/commit/9aa4b80a8c3cd29ca57dd62d69f5811868998f5c))\n\n\n\n\n\n# [1.1.0](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/table-module@1.0.1...@wangeditor/table-module@1.1.0) (2022-05-25)\n\n\n### Bug Fixes\n\n* 从表格后面删除，删除最后一个单元格 ([b327fcd](https://github.com/wangeditor-team/wangEditor/commit/b327fcd4669b1b1fad0e8b38b7d88db04c300e37))\n\n\n### Features\n\n* enter menu ([988fc31](https://github.com/wangeditor-team/wangEditor/commit/988fc31f31de3d37dffbf54abb784cceb8e6118d))\n* 表格拖拽列宽 ([46ea2c0](https://github.com/wangeditor-team/wangEditor/commit/46ea2c0f831b03ebca5fddfd59d682fed0b3476e))\n\n\n\n\n\n## 1.0.1 (2022-04-18)\n\n\n### Bug Fixes\n\n* 部分菜单 disabled ([87f1233](https://github.com/wangeditor-team/wangEditor/commit/87f12332a087072406c1988dc5cef2eae8335375))\n* 单元格内包含复杂样式内容时按tab未跳转到下一个单元格 ([db5e6f2](https://github.com/wangeditor-team/wangEditor/commit/db5e6f20c2c081d193fa80077f91d121be98c2a0))\n* 更新各包之间依赖版本 ([75c552c](https://github.com/wangeditor-team/wangEditor/commit/75c552cc8ed54765bebb86a7ec5329a7fc79e85f))\n* 两个表格不能紧挨着 ([5955b61](https://github.com/wangeditor-team/wangEditor/commit/5955b614cf92f65c9ebea47e6719047f3c0d27ea))\n* 修复 pnpm 安装 @wangeditor/editor 出现警告的问题 ([4087fbe](https://github.com/wangeditor-team/wangEditor/commit/4087fbee01c76bdd55e747a5e86c5e4a8d6a8353))\n* 移除了每个包下的 publishConfig directory 配置 ([16559f0](https://github.com/wangeditor-team/wangEditor/commit/16559f052545c111318be760e64291a521bdcc65))\n* 优化 custom-types.d.ts 中类型声明，修复测试文件 ts 报错 ([3a6c455](https://github.com/wangeditor-team/wangEditor/commit/3a6c4553245bc734dae1e17d605af389971782a2))\n* 优化表格 ([f240ca7](https://github.com/wangeditor-team/wangEditor/commit/f240ca71e31ccdea947233a767e3371434af0b6f))\n* parse html - 有些 elem children 需要过滤 ([63cbb80](https://github.com/wangeditor-team/wangEditor/commit/63cbb804c8c7a778a4ee1f4ba8717a11b4b6b5a3))\n* rename es module filename ([1821d4e](https://github.com/wangeditor-team/wangEditor/commit/1821d4eef49e64efcb41b848849ca7a5e6472044))\n* table - 粘贴合并单元格的表格 ([56ecb63](https://github.com/wangeditor-team/wangEditor/commit/56ecb6392510d433e092653f0f08183361778a3d))\n* table - disabled ([2b8717c](https://github.com/wangeditor-team/wangEditor/commit/2b8717c9a1c6853a3311fa6a667df6e0e75b61ee))\n* table - elemToHtml ([e36e609](https://github.com/wangeditor-team/wangEditor/commit/e36e6092ef721723169afc8bf0560a47ac9f4dfc))\n* table 不能是第一个元素 ([9407b79](https://github.com/wangeditor-team/wangEditor/commit/9407b79604163fece99dd96552487d21afd085e7))\n* table insertDOMElem ([6c89177](https://github.com/wangeditor-team/wangEditor/commit/6c89177878461fd59f128aa44ac175b2a49c3bd6))\n* table insertDOMElem ([3a42c37](https://github.com/wangeditor-team/wangEditor/commit/3a42c37c3bc38343e3a0b245d2bfb2abed0bd720))\n* table-cell 全选 ([1ef4872](https://github.com/wangeditor-team/wangEditor/commit/1ef48729e6d99e7414bc89bc4ef0d66c172fc566))\n* table内图片拖拽消失问题 ([a700a51](https://github.com/wangeditor-team/wangEditor/commit/a700a512fa7149da304f3d7c0ffaad8548a3def9))\n\n\n### Features\n\n* basic text paste ([f0a5b98](https://github.com/wangeditor-team/wangEditor/commit/f0a5b980c95fa1e2fc59a898c6e0d0723c276c28))\n* i18n ([c11b244](https://github.com/wangeditor-team/wangEditor/commit/c11b2440f91b99d40bca18b675c66a22b6e160c9))\n* parse html ([2a5eace](https://github.com/wangeditor-team/wangEditor/commit/2a5eace00f33cded50b68e8164748ec2480213fd))\n* table module ([a397116](https://github.com/wangeditor-team/wangEditor/commit/a397116de73e088232d9c41828f30f8d56a22dd4))\n* table module - header + fullWidth ([9a8a0e0](https://github.com/wangeditor-team/wangEditor/commit/9a8a0e093af944ee7deab674f47c2ec7baae0e63))\n* table内按tab光标换到下一个单元格 ([02421ad](https://github.com/wangeditor-team/wangEditor/commit/02421ad7603d20ce8e0d627a0f046c8992ba4934))\n* toHtml 机制 ([1c4d872](https://github.com/wangeditor-team/wangEditor/commit/1c4d8729f84aaab6a448f23064b34a20596305e9))\n* upload video ([ac8e6f8](https://github.com/wangeditor-team/wangEditor/commit/ac8e6f8b5258e593714676a6f6be359ba525833c))\n"
  },
  {
    "path": "packages/table-module/README.md",
    "content": "# wangEditor table-module\n\nTable module built in [wangEditor](https://www.wangeditor.com/) by default.\n"
  },
  {
    "path": "packages/table-module/__tests__/elem-to-html.test.ts",
    "content": "/**\n * @description table menu test\n * @author luochao\n */\n\nimport {\n  tableCellToHtmlConf,\n  tableToHtmlConf,\n  tableRowToHtmlConf,\n} from '../src/module/elem-to-html'\nimport * as core from '@wangeditor/core'\nimport { Ancestor } from 'slate'\n\ndescribe('TableModule module', () => {\n  describe('module elem-to-html', () => {\n    test('tableCellToHtmlConf should return object that include \"type\" and \"elemToHtml\" property', () => {\n      expect(tableCellToHtmlConf.type).toBe('table-cell')\n      expect(typeof tableCellToHtmlConf.elemToHtml).toBe('function')\n    })\n\n    test('tableCellToHtmlConf elemToHtml should throw Error if tableCell do not have parent', () => {\n      const element = {\n        type: 'table-cell',\n        children: [],\n      }\n\n      try {\n        tableCellToHtmlConf.elemToHtml(element, '<span>123</span>')\n      } catch (err) {\n        expect(err.message).toBe(\n          `Cannot get table row node by cell node ${JSON.stringify(element)}`\n        )\n      }\n    })\n\n    test('tableCellToHtmlConf elemToHtml should throw Error if tableRow do not have parent', () => {\n      const element = {\n        type: 'table-cell',\n        children: [],\n      }\n      jest\n        .spyOn(core.DomEditor, 'getParentNode')\n        .mockReturnValue({ type: 'table-row', children: [{ text: '' }] } as any)\n      try {\n        tableCellToHtmlConf.elemToHtml(element, '<span>123</span>')\n      } catch (err) {\n        expect(err.message).toBe(`Cannot get table node by cell node ${JSON.stringify(element)}`)\n      }\n    })\n\n    test('tableCellToHtmlConf elemToHtml should return html element td string', () => {\n      const element = {\n        type: 'table-cell',\n        children: [],\n      }\n      jest\n        .spyOn(core.DomEditor, 'getParentNode')\n        .mockReturnValueOnce({ type: 'table-row', children: [{ text: '' }] } as any)\n        .mockReturnValueOnce({ type: 'table', children: [{ text: '' }] } as Ancestor)\n\n      const res = tableCellToHtmlConf.elemToHtml(element, '<span>123</span>')\n      expect(res).toBe('<td colSpan=\"1\" rowSpan=\"1\" width=\"auto\"><span>123</span></td>')\n    })\n\n    test('tableRowToHtmlConf should return object that include \"type\" and \"elemToHtml\" property', () => {\n      expect(tableRowToHtmlConf.type).toBe('table-row')\n      expect(typeof tableRowToHtmlConf.elemToHtml).toBe('function')\n    })\n\n    test('tableRowToHtmlConf elemToHtml should return html table row string', () => {\n      const element = {\n        type: 'table-row',\n        children: [],\n      }\n      const res = tableRowToHtmlConf.elemToHtml(element, '<td>123</td>')\n      expect(res).toBe('<tr><td>123</td></tr>')\n    })\n\n    test('tableToHtmlConf should return object that include \"type\" and \"elemToHtml\" property', () => {\n      expect(tableToHtmlConf.type).toBe('table')\n      expect(typeof tableToHtmlConf.elemToHtml).toBe('function')\n    })\n\n    test('tableToHtmlConf should return html table string', () => {\n      const element = {\n        type: 'table',\n        children: [],\n      }\n      const res = tableToHtmlConf.elemToHtml(element, '<tr><td>123</td></tr>')\n      expect(res).toBe('<table style=\"width: auto;\"><tbody><tr><td>123</td></tr></tbody></table>')\n    })\n\n    test('tableToHtmlConf should return html table string with full width style if element is set fullWith value true', () => {\n      const element = {\n        type: 'table',\n        width: '100%',\n        children: [],\n      }\n      const res = tableToHtmlConf.elemToHtml(element, '<tr><td>123</td></tr>')\n      expect(res).toBe('<table style=\"width: 100%;\"><tbody><tr><td>123</td></tr></tbody></table>')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/table-module/__tests__/menu/delete-col.test.ts",
    "content": "import DeleteCol from '../../src/module/menu/DeleteCol'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { DEL_COL_SVG } from '../../src/constants/svg'\nimport locale from '../../src/locale/zh-CN'\nimport * as slate from 'slate'\nimport * as core from '@wangeditor/core'\n\nfunction setEditorSelection(\n  editor: core.IDomEditor,\n  selection: slate.Selection = {\n    anchor: { path: [0, 0], offset: 0 },\n    focus: { path: [0, 0], offset: 0 },\n  }\n) {\n  editor.selection = selection\n}\ndescribe('Table Module Delete Col Menu', () => {\n  test('it should create DeleteCol object', () => {\n    const deleteColMenu = new DeleteCol()\n    expect(typeof deleteColMenu).toBe('object')\n    expect(deleteColMenu.tag).toBe('button')\n    expect(deleteColMenu.iconSvg).toBe(DEL_COL_SVG)\n    expect(deleteColMenu.title).toBe(locale.tableModule.deleteCol)\n  })\n\n  test('it should get empty string if invoke getValue method', () => {\n    const deleteColMenu = new DeleteCol()\n    const editor = createEditor()\n    expect(deleteColMenu.getValue(editor)).toBe('')\n  })\n\n  test('it should get falsy value if invoke isActive method', () => {\n    const deleteColMenu = new DeleteCol()\n    const editor = createEditor()\n    expect(deleteColMenu.isActive(editor)).toBeFalsy()\n  })\n\n  test('isDisabled should get truthy value if editor selection is null', () => {\n    const deleteColMenu = new DeleteCol()\n    const editor = createEditor()\n    editor.selection = null\n    expect(deleteColMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get truthy value if editor selection is collapsed', () => {\n    const deleteColMenu = new DeleteCol()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => false)\n\n    expect(deleteColMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get truthy value if editor current selected node is not table cell', () => {\n    const deleteColMenu = new DeleteCol()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => true)\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => null)\n\n    expect(deleteColMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get falsy value if editor current selected node is table cell', () => {\n    const deleteColMenu = new DeleteCol()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => true)\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => ({} as any))\n\n    expect(deleteColMenu.isDisabled(editor)).toBeFalsy()\n  })\n\n  test('exec should return directly if menu is disabled', () => {\n    const deleteColMenu = new DeleteCol()\n    const editor = createEditor()\n    setEditorSelection(editor, null)\n\n    expect(deleteColMenu.exec(editor, '')).toBeUndefined()\n  })\n\n  test('exec should invoke removeNodes method to remove whole table if menu is not disabled and table col length less than 1', () => {\n    const deleteColMenu = new DeleteCol()\n    const editor = createEditor()\n\n    jest.spyOn(deleteColMenu, 'isDisabled').mockImplementation(() => false)\n    jest.spyOn(core.DomEditor, 'getParentNode').mockImplementation(() => ({\n      type: 'table-col',\n      children: [],\n    }))\n\n    const fn = function* a() {\n      yield [\n        {\n          type: 'table-cell',\n          children: [],\n        } as slate.Element,\n        [0, 1],\n      ] as slate.NodeEntry<slate.Element>\n    }\n    jest.spyOn(slate.Editor, 'nodes').mockReturnValue(fn())\n    const removeNodesFn = jest.fn()\n    jest.spyOn(slate.Transforms, 'removeNodes').mockImplementation(removeNodesFn)\n\n    deleteColMenu.exec(editor, '')\n    expect(removeNodesFn).toBeCalled()\n  })\n\n  test('exec should invoke removeNodes method to remove all table cells if menu is not disabled and table col length greater than 1', () => {\n    const deleteColMenu = new DeleteCol()\n    const editor = createEditor()\n\n    jest.spyOn(deleteColMenu, 'isDisabled').mockImplementation(() => false)\n    jest.spyOn(core.DomEditor, 'getParentNode').mockImplementation(() => ({\n      type: 'table-row',\n      children: [\n        {\n          type: 'table-col',\n          children: [{ type: 'table-cell', children: [] }],\n        },\n        {\n          type: 'table-col',\n          children: [{ type: 'table-cell', children: [] }],\n        },\n      ],\n    }))\n\n    const fn = function* a() {\n      yield [\n        {\n          type: 'table-cell',\n          children: [],\n        } as slate.Element,\n        [0, 1],\n      ] as slate.NodeEntry<slate.Element>\n    }\n    jest.spyOn(slate.Editor, 'nodes').mockReturnValue(fn())\n    jest.spyOn(core.DomEditor, 'findPath').mockImplementation(() => [0, 1] as slate.Path)\n    const removeNodesFn = jest.fn()\n    jest.spyOn(slate.Transforms, 'removeNodes').mockImplementation(removeNodesFn)\n\n    deleteColMenu.exec(editor, '')\n    expect(removeNodesFn).toBeCalledTimes(2)\n  })\n})\n"
  },
  {
    "path": "packages/table-module/__tests__/menu/delete-row.test.ts",
    "content": "import DeleteRow from '../../src/module/menu/DeleteRow'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { DEL_ROW_SVG } from '../../src/constants/svg'\nimport locale from '../../src/locale/zh-CN'\nimport * as slate from 'slate'\nimport * as core from '@wangeditor/core'\n\nfunction setEditorSelection(\n  editor: core.IDomEditor,\n  selection: slate.Selection = {\n    anchor: { path: [0, 0], offset: 0 },\n    focus: { path: [0, 0], offset: 0 },\n  }\n) {\n  editor.selection = selection\n}\ndescribe('Table Module Delete Row Menu', () => {\n  test('it should create DeleteRow object', () => {\n    const deleteRowMenu = new DeleteRow()\n    expect(typeof deleteRowMenu).toBe('object')\n    expect(deleteRowMenu.tag).toBe('button')\n    expect(deleteRowMenu.iconSvg).toBe(DEL_ROW_SVG)\n    expect(deleteRowMenu.title).toBe(locale.tableModule.deleteRow)\n  })\n\n  test('it should get empty string if invoke getValue method', () => {\n    const deleteRowMenu = new DeleteRow()\n    const editor = createEditor()\n    expect(deleteRowMenu.getValue(editor)).toBe('')\n  })\n\n  test('it should get falsy value if invoke isActive method', () => {\n    const deleteRowMenu = new DeleteRow()\n    const editor = createEditor()\n    expect(deleteRowMenu.isActive(editor)).toBeFalsy()\n  })\n\n  test('isDisabled should get truthy value if editor selection is null', () => {\n    const deleteRowMenu = new DeleteRow()\n    const editor = createEditor()\n    editor.selection = null\n    expect(deleteRowMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get truthy value if editor selection is collapsed', () => {\n    const deleteRowMenu = new DeleteRow()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => false)\n\n    expect(deleteRowMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get truthy value if editor current selected node is not table cell', () => {\n    const deleteRowMenu = new DeleteRow()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => true)\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => null)\n\n    expect(deleteRowMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get falsy value if editor current selected node is table cell', () => {\n    const deleteRowMenu = new DeleteRow()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => true)\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => ({} as any))\n\n    expect(deleteRowMenu.isDisabled(editor)).toBeFalsy()\n  })\n\n  test('exec should return directly if menu is disabled', () => {\n    const deleteRowMenu = new DeleteRow()\n    const editor = createEditor()\n    setEditorSelection(editor, null)\n\n    expect(deleteRowMenu.exec(editor, '')).toBeUndefined()\n  })\n\n  test('exec should invoke removeNodes method to remove whole table if menu is not disabled and table row length less than 1', () => {\n    const deleteRowMenu = new DeleteRow()\n    const editor = createEditor()\n\n    jest.spyOn(deleteRowMenu, 'isDisabled').mockImplementation(() => false)\n    jest.spyOn(core.DomEditor, 'getParentNode').mockImplementation(() => ({\n      type: 'table',\n      children: [\n        {\n          type: 'table-row',\n          children: [],\n        },\n      ],\n    }))\n\n    const path = [0, 1]\n    const fn = function* a() {\n      yield [\n        {\n          type: 'table-cell',\n          children: [],\n        } as slate.Element,\n        path,\n      ] as slate.NodeEntry<slate.Element>\n    }\n    jest.spyOn(slate.Editor, 'nodes').mockReturnValue(fn())\n    const removeNodesFn = jest.fn()\n    jest.spyOn(slate.Transforms, 'removeNodes').mockImplementation(removeNodesFn)\n\n    deleteRowMenu.exec(editor, '')\n    expect(removeNodesFn).toBeCalled()\n  })\n\n  test('exec should invoke removeNodes method to remove current row if menu is not disabled and table row length greater than 1', () => {\n    const deleteRowMenu = new DeleteRow()\n    const editor = createEditor()\n\n    jest.spyOn(deleteRowMenu, 'isDisabled').mockImplementation(() => false)\n    jest.spyOn(core.DomEditor, 'getParentNode').mockImplementation(() => ({\n      type: 'table',\n      children: [\n        {\n          type: 'table-row',\n          children: [],\n        },\n        {\n          type: 'table-row',\n          children: [],\n        },\n      ],\n    }))\n\n    const path = [0, 1]\n    const fn = function* a() {\n      yield [\n        {\n          type: 'table-cell',\n          children: [],\n        } as slate.Element,\n        path,\n      ] as slate.NodeEntry<slate.Element>\n    }\n    jest.spyOn(slate.Editor, 'nodes').mockReturnValue(fn())\n    const removeNodesFn = jest.fn()\n    jest.spyOn(slate.Transforms, 'removeNodes').mockImplementation(removeNodesFn)\n\n    deleteRowMenu.exec(editor, '')\n    expect(removeNodesFn).toBeCalledWith(editor, { at: path })\n  })\n})\n"
  },
  {
    "path": "packages/table-module/__tests__/menu/delete-table.test.ts",
    "content": "import DeleteTable from '../../src/module/menu/DeleteTable'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport locale from '../../src/locale/zh-CN'\nimport * as slate from 'slate'\nimport * as core from '@wangeditor/core'\n\nfunction setEditorSelection(\n  editor: core.IDomEditor,\n  selection: slate.Selection = {\n    anchor: { path: [0, 0], offset: 0 },\n    focus: { path: [0, 0], offset: 0 },\n  }\n) {\n  editor.selection = selection\n}\ndescribe('Table Module Delete Table Menu', () => {\n  test('it should create DeleteTable object', () => {\n    const deleteTableMenu = new DeleteTable()\n    expect(typeof deleteTableMenu).toBe('object')\n    expect(deleteTableMenu.tag).toBe('button')\n    expect(deleteTableMenu.title).toBe(locale.tableModule.deleteTable)\n  })\n\n  test('it should get empty string if invoke getValue method', () => {\n    const deleteTableMenu = new DeleteTable()\n    const editor = createEditor()\n    expect(deleteTableMenu.getValue(editor)).toBe('')\n  })\n\n  test('it should get falsy value if invoke isActive method', () => {\n    const deleteTableMenu = new DeleteTable()\n    const editor = createEditor()\n    expect(deleteTableMenu.isActive(editor)).toBeFalsy()\n  })\n\n  test('isDisabled should get truthy value if editor selection is null', () => {\n    const deleteTableMenu = new DeleteTable()\n    const editor = createEditor()\n    editor.selection = null\n    expect(deleteTableMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get truthy value if editor current selected node is not table cell', () => {\n    const deleteTableMenu = new DeleteTable()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => null)\n\n    expect(deleteTableMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get falsy value if editor current selected node is table cell', () => {\n    const deleteTableMenu = new DeleteTable()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => ({} as any))\n\n    expect(deleteTableMenu.isDisabled(editor)).toBeFalsy()\n  })\n\n  test('exec should return directly if menu is disabled', () => {\n    const deleteTableMenu = new DeleteTable()\n    const editor = createEditor()\n    setEditorSelection(editor, null)\n\n    expect(deleteTableMenu.exec(editor, '')).toBeUndefined()\n  })\n\n  test('exec should invoke removeNodes method to remove whole table if menu is not disabled', () => {\n    const deleteTableMenu = new DeleteTable()\n    const editor = createEditor()\n\n    jest.spyOn(deleteTableMenu, 'isDisabled').mockReturnValue(false)\n    const fn = jest.fn()\n\n    jest.spyOn(slate.Transforms, 'removeNodes').mockImplementation(fn)\n\n    deleteTableMenu.exec(editor, '')\n    expect(fn).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "packages/table-module/__tests__/menu/full-width.test.ts",
    "content": "import FullWidth from '../../src/module/menu/FullWidth'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { FULL_WIDTH_SVG } from '../../src/constants/svg'\nimport locale from '../../src/locale/zh-CN'\nimport * as slate from 'slate'\nimport * as core from '@wangeditor/core'\n\nfunction setEditorSelection(\n  editor: core.IDomEditor,\n  selection: slate.Selection = {\n    anchor: { path: [0, 0], offset: 0 },\n    focus: { path: [0, 0], offset: 0 },\n  }\n) {\n  editor.selection = selection\n}\ndescribe('Table Module Full Width Menu', () => {\n  test('it should create FullWidth object', () => {\n    const fullWidthMenu = new FullWidth()\n    expect(typeof fullWidthMenu).toBe('object')\n    expect(fullWidthMenu.tag).toBe('button')\n    expect(fullWidthMenu.iconSvg).toBe(FULL_WIDTH_SVG)\n    expect(fullWidthMenu.title).toBe(locale.tableModule.widthAuto)\n  })\n\n  test('getValue should get falsy value if editor selected node is not table', () => {\n    const fullWidthMenu = new FullWidth()\n    const editor = createEditor()\n\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => null)\n    expect(fullWidthMenu.getValue(editor)).toBeFalsy()\n  })\n\n  test(`getValue should get truthy value if editor selected table's width is 100%`, () => {\n    const fullWidthMenu = new FullWidth()\n    const editor = createEditor()\n\n    jest\n      .spyOn(core.DomEditor, 'getSelectedNodeByType')\n      .mockImplementation(() => ({ width: '100%' } as any))\n    expect(fullWidthMenu.getValue(editor)).toBeTruthy()\n  })\n\n  test('isActive should get falsy value if editor selected node is not table', () => {\n    const fullWidthMenu = new FullWidth()\n    const editor = createEditor()\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => null)\n\n    expect(fullWidthMenu.isActive(editor)).toBeFalsy()\n  })\n\n  test('isDisabled should get truthy value if editor selection is null', () => {\n    const fullWidthMenu = new FullWidth()\n    const editor = createEditor()\n    editor.selection = null\n    expect(fullWidthMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get truthy value if editor selection is collapsed', () => {\n    const fullWidthMenu = new FullWidth()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => false)\n\n    expect(fullWidthMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get truthy value if editor current selected node is not table cell', () => {\n    const fullWidthMenu = new FullWidth()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => true)\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => null)\n\n    expect(fullWidthMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get falsy value if editor current selected node is table cell', () => {\n    const fullWidthMenu = new FullWidth()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => true)\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => ({} as any))\n\n    expect(fullWidthMenu.isDisabled(editor)).toBeFalsy()\n  })\n\n  test('exec should return directly if menu is disabled', () => {\n    const fullWidthMenu = new FullWidth()\n    const editor = createEditor()\n    setEditorSelection(editor, null)\n\n    expect(fullWidthMenu.exec(editor, '')).toBeUndefined()\n  })\n\n  test('exec should invoke setNodes with props if menu is not disabled', () => {\n    const fullWidthMenu = new FullWidth()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => true)\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => ({} as any))\n\n    const fn = jest.fn()\n    jest.spyOn(slate.Transforms, 'setNodes').mockImplementation(fn)\n\n    fullWidthMenu.exec(editor, true)\n\n    expect(fn).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "packages/table-module/__tests__/menu/insert-col.test.ts",
    "content": "import InsertCol from '../../src/module/menu/InsertCol'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { ADD_COL_SVG } from '../../src/constants/svg'\nimport locale from '../../src/locale/zh-CN'\nimport * as slate from 'slate'\nimport * as core from '@wangeditor/core'\n\nfunction setEditorSelection(\n  editor: core.IDomEditor,\n  selection: slate.Selection = {\n    anchor: { path: [0, 0], offset: 0 },\n    focus: { path: [0, 0], offset: 0 },\n  }\n) {\n  editor.selection = selection\n}\ndescribe('Table Module Insert Col Menu', () => {\n  test('it should create InsertCol object', () => {\n    const insertColMenu = new InsertCol()\n    expect(typeof insertColMenu).toBe('object')\n    expect(insertColMenu.tag).toBe('button')\n    expect(insertColMenu.iconSvg).toBe(ADD_COL_SVG)\n    expect(insertColMenu.title).toBe(locale.tableModule.insertCol)\n  })\n\n  test('it should get empty string if invoke getValue method', () => {\n    const insertColMenu = new InsertCol()\n    const editor = createEditor()\n    expect(insertColMenu.getValue(editor)).toBe('')\n  })\n\n  test('it should get falsy value if invoke isActive method', () => {\n    const insertColMenu = new InsertCol()\n    const editor = createEditor()\n    expect(insertColMenu.isActive(editor)).toBeFalsy()\n  })\n\n  test('isDisabled should get truthy value if editor selection is null', () => {\n    const insertColMenu = new InsertCol()\n    const editor = createEditor()\n    editor.selection = null\n    expect(insertColMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get truthy value if editor selection is collapsed', () => {\n    const insertColMenu = new InsertCol()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => false)\n\n    expect(insertColMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get truthy value if editor current selected node is not table cell', () => {\n    const insertColMenu = new InsertCol()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => true)\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => null)\n\n    expect(insertColMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get falsy value if editor current selected node is table cell', () => {\n    const insertColMenu = new InsertCol()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => true)\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => ({} as any))\n\n    expect(insertColMenu.isDisabled(editor)).toBeFalsy()\n  })\n\n  test('exec should return directly if menu is disabled', () => {\n    const insertColMenu = new InsertCol()\n    const editor = createEditor()\n    setEditorSelection(editor, null)\n\n    expect(insertColMenu.exec(editor, '')).toBeUndefined()\n  })\n\n  test('exec should return directly if current selected node parent is null', () => {\n    const insertColMenu = new InsertCol()\n    const editor = createEditor()\n\n    jest.spyOn(insertColMenu, 'isDisabled').mockReturnValue(false)\n\n    const fn = function* a() {\n      yield [\n        {\n          type: 'table-cell',\n          children: [],\n        } as slate.Element,\n        [0, 1],\n      ] as slate.NodeEntry<slate.Element>\n    }\n    jest.spyOn(slate.Editor, 'nodes').mockReturnValue(fn())\n    jest.spyOn(core.DomEditor, 'getParentNode').mockReturnValue(null)\n\n    expect(insertColMenu.exec(editor, '')).toBeUndefined()\n  })\n\n  test('exec should return directly if current selected table row parent is null', () => {\n    const insertColMenu = new InsertCol()\n    const editor = createEditor()\n\n    jest.spyOn(insertColMenu, 'isDisabled').mockReturnValue(false)\n\n    const fn = function* a() {\n      yield [\n        {\n          type: 'table-cell',\n          children: [],\n        } as slate.Element,\n        [0, 1],\n      ] as slate.NodeEntry<slate.Element>\n    }\n    jest.spyOn(slate.Editor, 'nodes').mockReturnValue(fn())\n    jest\n      .spyOn(core.DomEditor, 'getParentNode')\n      .mockReturnValue({} as any)\n      .mockReturnValue(null)\n\n    expect(insertColMenu.exec(editor, '')).toBeUndefined()\n  })\n\n  test('exec should return directly if current selected table row parent is null', () => {\n    const insertColMenu = new InsertCol()\n    const editor = createEditor()\n\n    jest.spyOn(insertColMenu, 'isDisabled').mockReturnValue(false)\n\n    const fn = function* a() {\n      yield [\n        {\n          type: 'table-cell',\n          children: [],\n        } as slate.Element,\n        [0, 1],\n      ] as slate.NodeEntry<slate.Element>\n    }\n    jest.spyOn(slate.Editor, 'nodes').mockReturnValue(fn())\n    jest\n      .spyOn(core.DomEditor, 'getParentNode')\n      .mockReturnValue({} as any)\n      .mockReturnValue({\n        type: 'table',\n        children: [\n          {\n            type: 'table-row',\n            children: [\n              {\n                type: 'table-cell',\n                children: [],\n              },\n              {\n                type: 'table-cell',\n                children: [],\n              },\n            ],\n          },\n          {\n            type: 'table-row',\n            children: [\n              {\n                type: 'table-cell',\n                children: [],\n              },\n              {\n                type: 'table-cell',\n                children: [],\n              },\n            ],\n          },\n        ],\n      } as any)\n\n    jest.spyOn(core.DomEditor, 'findPath').mockReturnValue([0, 1])\n    const insertNodesFn = jest.fn()\n    jest.spyOn(slate.Transforms, 'insertNodes').mockImplementation(insertNodesFn)\n\n    insertColMenu.exec(editor, '')\n\n    expect(insertNodesFn).toBeCalledWith(\n      editor,\n      { type: 'table-cell', children: [{ text: '' }] },\n      { at: [0, 1] }\n    )\n  })\n})\n"
  },
  {
    "path": "packages/table-module/__tests__/menu/insert-row.test.ts",
    "content": "import InsertRow from '../../src/module/menu/InsertRow'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { ADD_ROW_SVG } from '../../src/constants/svg'\nimport locale from '../../src/locale/zh-CN'\nimport * as slate from 'slate'\nimport * as core from '@wangeditor/core'\n\nfunction setEditorSelection(\n  editor: core.IDomEditor,\n  selection: slate.Selection = {\n    anchor: { path: [0, 0], offset: 0 },\n    focus: { path: [0, 0], offset: 0 },\n  }\n) {\n  editor.selection = selection\n}\ndescribe('Table Module Insert Row Menu', () => {\n  test('it should create InsertRow object', () => {\n    const insertRowMenu = new InsertRow()\n    expect(typeof insertRowMenu).toBe('object')\n    expect(insertRowMenu.tag).toBe('button')\n    expect(insertRowMenu.iconSvg).toBe(ADD_ROW_SVG)\n    expect(insertRowMenu.title).toBe(locale.tableModule.insertRow)\n  })\n\n  test('it should get empty string if invoke getValue method', () => {\n    const insertRowMenu = new InsertRow()\n    const editor = createEditor()\n    expect(insertRowMenu.getValue(editor)).toBe('')\n  })\n\n  test('it should get falsy value if invoke isActive method', () => {\n    const insertRowMenu = new InsertRow()\n    const editor = createEditor()\n    expect(insertRowMenu.isActive(editor)).toBeFalsy()\n  })\n\n  test('isDisabled should get truthy value if editor selection is null', () => {\n    const insertRowMenu = new InsertRow()\n    const editor = createEditor()\n    editor.selection = null\n    expect(insertRowMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get truthy value if editor selection is collapsed', () => {\n    const insertRowMenu = new InsertRow()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => false)\n\n    expect(insertRowMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get truthy value if editor current selected node is not table cell', () => {\n    const insertRowMenu = new InsertRow()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => true)\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => null)\n\n    expect(insertRowMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get falsy value if editor current selected node is table cell', () => {\n    const insertRowMenu = new InsertRow()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => true)\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => ({} as any))\n\n    expect(insertRowMenu.isDisabled(editor)).toBeFalsy()\n  })\n\n  test('exec should return directly if menu is disabled', () => {\n    const insertRowMenu = new InsertRow()\n    const editor = createEditor()\n    setEditorSelection(editor, null)\n\n    expect(insertRowMenu.exec(editor, '')).toBeUndefined()\n  })\n\n  test('exec should invoke insertNodes method to remove whole table if menu is not disabled', () => {\n    const insertRowMenu = new InsertRow()\n    const editor = createEditor()\n\n    jest.spyOn(insertRowMenu, 'isDisabled').mockReturnValue(false)\n    jest.spyOn(core.DomEditor, 'getParentNode').mockImplementation(() => ({\n      type: 'table-row',\n      children: [\n        {\n          type: 'table-cell',\n          children: [],\n        },\n        {\n          type: 'table-cell',\n          children: [],\n        },\n      ],\n    }))\n    const fn = function* a() {\n      yield [\n        {\n          type: 'table-cell',\n          children: [],\n        } as slate.Element,\n        [0, 1],\n      ] as slate.NodeEntry<slate.Element>\n    }\n    jest.spyOn(slate.Editor, 'nodes').mockReturnValue(fn())\n    const insertNodesFn = jest.fn()\n    jest.spyOn(slate.Transforms, 'insertNodes').mockImplementation(insertNodesFn)\n\n    insertRowMenu.exec(editor, '')\n    expect(insertNodesFn).toBeCalled()\n  })\n\n  test('exec should return directly if current selected row that does not has children', () => {\n    const insertRowMenu = new InsertRow()\n    const editor = createEditor()\n\n    jest.spyOn(insertRowMenu, 'isDisabled').mockReturnValue(false)\n    jest.spyOn(core.DomEditor, 'getParentNode').mockImplementation(() => ({\n      type: 'table-row',\n      children: [],\n    }))\n    const fn = function* a() {\n      yield [\n        {\n          type: 'table-cell',\n          children: [],\n        } as slate.Element,\n        [0, 1],\n      ] as slate.NodeEntry<slate.Element>\n    }\n    jest.spyOn(slate.Editor, 'nodes').mockReturnValue(fn())\n    const insertNodesFn = jest.fn()\n    jest.spyOn(slate.Transforms, 'insertNodes').mockImplementation(insertNodesFn)\n\n    expect(insertRowMenu.exec(editor, '')).toBeUndefined()\n    expect(insertNodesFn).not.toBeCalled()\n  })\n})\n"
  },
  {
    "path": "packages/table-module/__tests__/menu/insert-table.test.ts",
    "content": "import InsertTable from '../../src/module/menu/InsertTable'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { TABLE_SVG } from '../../src/constants/svg'\nimport locale from '../../src/locale/zh-CN'\nimport * as slate from 'slate'\nimport * as core from '@wangeditor/core'\nimport $, { DOMElement } from '../../src/utils/dom'\n\nfunction setEditorSelection(\n  editor: core.IDomEditor,\n  selection: slate.Selection = {\n    anchor: { path: [0, 0], offset: 0 },\n    focus: { path: [0, 0], offset: 0 },\n  }\n) {\n  editor.selection = selection\n}\ndescribe('Table Module Insert Table Menu', () => {\n  test('it should create InsertTable object', () => {\n    const insertTableMenu = new InsertTable()\n    expect(typeof insertTableMenu).toBe('object')\n    expect(insertTableMenu.tag).toBe('button')\n    expect(insertTableMenu.iconSvg).toBe(TABLE_SVG)\n    expect(insertTableMenu.title).toBe(locale.tableModule.insertTable)\n  })\n\n  test('it should get empty string if invoke getValue method', () => {\n    const insertTableMenu = new InsertTable()\n    const editor = createEditor()\n    expect(insertTableMenu.getValue(editor)).toBe('')\n  })\n\n  test('it should get falsy value if invoke isActive method', () => {\n    const insertTableMenu = new InsertTable()\n    const editor = createEditor()\n    expect(insertTableMenu.isActive(editor)).toBeFalsy()\n  })\n\n  test('isDisabled should get truthy value if editor selection is null', () => {\n    const insertTableMenu = new InsertTable()\n    const editor = createEditor()\n    editor.selection = null\n    expect(insertTableMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get truthy value if editor selection is collapsed', () => {\n    const insertTableMenu = new InsertTable()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => false)\n\n    expect(insertTableMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get truthy value if editor current selected node is contains pre node', () => {\n    const insertTableMenu = new InsertTable()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => true)\n    jest\n      .spyOn(core.DomEditor, 'getSelectedElems')\n      .mockImplementation(() => [{ type: 'pre', children: [] }])\n\n    expect(insertTableMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get truthy value if editor current selected node is contains table node', () => {\n    const insertTableMenu = new InsertTable()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => true)\n    jest\n      .spyOn(core.DomEditor, 'getSelectedElems')\n      .mockImplementation(() => [{ type: 'table', children: [] }])\n\n    expect(insertTableMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get truthy value if editor current selected node is contains void node', () => {\n    const insertTableMenu = new InsertTable()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => true)\n    jest\n      .spyOn(core.DomEditor, 'getSelectedElems')\n      .mockImplementation(() => [{ type: 'image', children: [] }])\n\n    jest.spyOn(editor, 'isVoid').mockImplementation(() => true)\n\n    expect(insertTableMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get falsy value if editor current selected node is valid', () => {\n    const insertTableMenu = new InsertTable()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => true)\n    jest\n      .spyOn(core.DomEditor, 'getSelectedElems')\n      .mockImplementation(() => [{ type: 'paragraph', children: [] }])\n\n    expect(insertTableMenu.isDisabled(editor)).toBeFalsy()\n  })\n\n  test('getPanelContentElem should return table panel dom', () => {\n    const insertTableMenu = new InsertTable()\n    const editor = createEditor()\n\n    expect(insertTableMenu.getPanelContentElem(editor) instanceof DOMElement).toBeTruthy()\n    expect(insertTableMenu.getPanelContentElem(editor).className).toBe('w-e-panel-content-table')\n  })\n\n  test('it should invoke insertNodes method if click panel td node', () => {\n    const insertTableMenu = new InsertTable()\n    const editor = createEditor()\n\n    const tablePanel = insertTableMenu.getPanelContentElem(editor)\n    const tdEl = $(tablePanel).find('td')[0]\n\n    const fn = jest.fn()\n    jest.spyOn(slate.Transforms, 'insertNodes').mockImplementation(fn)\n\n    tdEl.dispatchEvent(\n      new MouseEvent('click', {\n        view: window,\n        bubbles: true,\n        cancelable: true,\n      })\n    )\n\n    expect(fn).toBeCalled()\n  })\n\n  test('it should add active class if mouse enter panel td node', () => {\n    const insertTableMenu = new InsertTable()\n    const editor = createEditor()\n\n    const tablePanel = insertTableMenu.getPanelContentElem(editor)\n    const tdEl = $(tablePanel).find('td')[0]\n\n    expect(tdEl.className).toBe('')\n\n    tdEl.dispatchEvent(\n      new MouseEvent('mouseenter', {\n        view: window,\n        bubbles: true,\n        cancelable: true,\n      })\n    )\n\n    expect(tdEl.className).toBe('active')\n  })\n})\n"
  },
  {
    "path": "packages/table-module/__tests__/menu/table-header.test.ts",
    "content": "import TableHeader from '../../src/module/menu/TableHeader'\nimport createEditor from '../../../../tests/utils/create-editor'\nimport { TABLE_HEADER_SVG } from '../../src/constants/svg'\nimport locale from '../../src/locale/zh-CN'\nimport * as slate from 'slate'\nimport * as core from '@wangeditor/core'\n\nfunction setEditorSelection(\n  editor: core.IDomEditor,\n  selection: slate.Selection = {\n    anchor: { path: [0, 0], offset: 0 },\n    focus: { path: [0, 0], offset: 0 },\n  }\n) {\n  editor.selection = selection\n}\ndescribe('Table Module Table Header Menu', () => {\n  test('it should create TableHeader object', () => {\n    const tableHeaderMenu = new TableHeader()\n    expect(typeof tableHeaderMenu).toBe('object')\n    expect(tableHeaderMenu.tag).toBe('button')\n    expect(tableHeaderMenu.iconSvg).toBe(TABLE_HEADER_SVG)\n    expect(tableHeaderMenu.title).toBe(locale.tableModule.header)\n  })\n\n  test('getValue should get falsy value if editor selected node is not table', () => {\n    const tableHeaderMenu = new TableHeader()\n    const editor = createEditor()\n\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => null)\n\n    expect(tableHeaderMenu.getValue(editor)).toBeFalsy()\n  })\n\n  test('isActive should get falsy value if editor selected node is not table', () => {\n    const tableHeaderMenu = new TableHeader()\n    const editor = createEditor()\n\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => null)\n\n    expect(tableHeaderMenu.isActive(editor)).toBeFalsy()\n  })\n\n  test('isDisabled should get truthy value if editor selection is null', () => {\n    const tableHeaderMenu = new TableHeader()\n    const editor = createEditor()\n    editor.selection = null\n    expect(tableHeaderMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get truthy value if editor selection is collapsed', () => {\n    const tableHeaderMenu = new TableHeader()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => false)\n\n    expect(tableHeaderMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get truthy value if editor current selected node is not table cell', () => {\n    const tableHeaderMenu = new TableHeader()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => true)\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => null)\n\n    expect(tableHeaderMenu.isDisabled(editor)).toBeTruthy()\n  })\n\n  test('isDisabled should get falsy value if editor current selected node is table cell', () => {\n    const tableHeaderMenu = new TableHeader()\n    const editor = createEditor()\n    setEditorSelection(editor)\n\n    jest.spyOn(slate.Range, 'isCollapsed').mockImplementation(() => true)\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => ({} as any))\n\n    expect(tableHeaderMenu.isDisabled(editor)).toBeFalsy()\n  })\n\n  test('exec should return directly if menu is disabled', () => {\n    const tableHeaderMenu = new TableHeader()\n    const editor = createEditor()\n    setEditorSelection(editor, null)\n\n    expect(tableHeaderMenu.exec(editor, '')).toBeUndefined()\n  })\n\n  test('exec should return directly if current selected node is not table', () => {\n    const tableHeaderMenu = new TableHeader()\n    const editor = createEditor()\n\n    jest.spyOn(tableHeaderMenu, 'isDisabled').mockReturnValue(false)\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => null)\n\n    expect(tableHeaderMenu.exec(editor, '')).toBeUndefined()\n  })\n\n  test('exec should invoke setNodes to set table header if current selected node table', () => {\n    const tableHeaderMenu = new TableHeader()\n    const editor = createEditor()\n\n    jest.spyOn(tableHeaderMenu, 'isDisabled').mockReturnValue(false)\n    jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockImplementation(() => ({\n      type: 'table',\n      children: [\n        {\n          type: 'table-row',\n          children: [\n            { type: 'table-cell', children: [] },\n            { type: 'table-cell', children: [] },\n          ],\n        },\n        {\n          type: 'table-row',\n          children: [\n            { type: 'table-cell', children: [] },\n            { type: 'table-cell', children: [] },\n          ],\n        },\n      ],\n    }))\n\n    const fn = jest.fn()\n    jest.spyOn(slate.Transforms, 'setNodes').mockImplementation(fn)\n    jest.spyOn(core.DomEditor, 'findPath').mockImplementation(() => [0, 1])\n\n    tableHeaderMenu.exec(editor, '')\n\n    expect(fn).toBeCalledTimes(2)\n  })\n})\n"
  },
  {
    "path": "packages/table-module/__tests__/parse-html.test.ts",
    "content": "/**\n * @description parse html test\n * @author wangfupeng\n */\n\nimport { $ } from 'dom7'\nimport createEditor from '../../../tests/utils/create-editor'\nimport { preParseTableHtmlConf } from '../src/module/pre-parse-html'\nimport {\n  parseCellHtmlConf,\n  parseRowHtmlConf,\n  parseTableHtmlConf,\n} from '../src/module/parse-elem-html'\n\ndescribe('table - pre parse html', () => {\n  it('pre parse', () => {\n    const $table = $('<table><tbody><tr><td>hello</td></tr></tbody></table>')\n\n    // match selector\n    expect($table[0].matches(preParseTableHtmlConf.selector)).toBeTruthy()\n\n    // pre parse\n    const res = preParseTableHtmlConf.preParseHtml($table[0])\n    expect(res.outerHTML).toBe('<table><tr><td>hello</td></tr></table>')\n  })\n\n  it('it should return fake element if pass fake table element', () => {\n    const fakeTable = $('<div>hello</div>')\n\n    // pre parse\n    const res = preParseTableHtmlConf.preParseHtml(fakeTable[0])\n    expect(res.outerHTML).toBe('<div>hello</div>')\n  })\n\n  it('it should return directly if pass table element without body', () => {\n    const table = $('<table><tr><td>hello</td></tr></table>')\n\n    // pre parse\n    const res = preParseTableHtmlConf.preParseHtml(table[0])\n    expect(res.outerHTML).toBe('<table><tr><td>hello</td></tr></table>')\n  })\n})\n\ndescribe('table - parse html', () => {\n  const editor = createEditor()\n\n  it('table cell', () => {\n    const $cell1 = $('<td>hello&nbsp;world</td>')\n    expect($cell1[0].matches(parseCellHtmlConf.selector)).toBeTruthy()\n    expect(parseCellHtmlConf.parseElemHtml($cell1[0], [], editor)).toEqual({\n      type: 'table-cell',\n      isHeader: false,\n      colSpan: 1,\n      rowSpan: 1,\n      width: 'auto',\n      children: [{ text: 'hello world' }],\n    })\n\n    const $cell2 = $('<th></th>')\n    const children = [{ text: 'hello ' }, { text: 'world', bold: true }]\n    expect($cell2[0].matches(parseCellHtmlConf.selector)).toBeTruthy()\n    expect(parseCellHtmlConf.parseElemHtml($cell2[0], children, editor)).toEqual({\n      type: 'table-cell',\n      isHeader: true,\n      colSpan: 1,\n      rowSpan: 1,\n      width: 'auto',\n      children,\n    })\n  })\n\n  it('table row', () => {\n    const $tr = $('<tr></tr>')\n    const children = [{ type: 'table-cell', children: [{ text: 'hello world' }] }]\n\n    expect($tr[0].matches(parseRowHtmlConf.selector)).toBeTruthy()\n\n    expect(parseRowHtmlConf.parseElemHtml($tr[0], children, editor)).toEqual({\n      type: 'table-row',\n      children,\n    })\n  })\n\n  it('table', () => {\n    const $table = $('<table style=\"width: 100%;\"></table>')\n    const children = [\n      {\n        type: 'table-row',\n        children: [{ type: 'table-cell', children: [{ text: 'hello world' }] }],\n      },\n    ]\n\n    expect($table[0].matches(parseTableHtmlConf.selector)).toBeTruthy()\n\n    expect(parseTableHtmlConf.parseElemHtml($table[0], children, editor)).toEqual({\n      type: 'table',\n      width: '100%',\n      children,\n    })\n  })\n})\n"
  },
  {
    "path": "packages/table-module/__tests__/plugin.test.ts",
    "content": "/**\n * @description table menu test\n * @author luochao\n */\n\nimport createEditor from '../../../tests/utils/create-editor'\nimport withTable from '../src/module/plugin'\nimport * as core from '@wangeditor/core'\nimport * as slate from 'slate'\n\ndescribe('TableModule module', () => {\n  describe('module plugin', () => {\n    test('use withTable plugin when break line not split node', () => {\n      const editor = createEditor()\n      const newEditor = withTable(editor)\n\n      jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockReturnValue({\n        type: 'table',\n        children: [{ text: '' }],\n      } as slate.Element)\n\n      const mockFn = jest.fn()\n      newEditor.insertText = mockFn\n\n      newEditor.insertBreak()\n\n      expect(mockFn).toBeCalledWith('\\n')\n    })\n\n    test('use withTable plugin when insertData should insertText to cell', () => {\n      const editor = createEditor()\n      const newEditor = withTable(editor)\n\n      jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockReturnValue({\n        type: 'table',\n        children: [{ text: '' }],\n      } as slate.Element)\n\n      const mockFn = jest.fn()\n      slate.Editor.insertText = mockFn\n\n      newEditor.insertData({ getData: () => 'test' } as unknown as DataTransfer)\n\n      expect(mockFn).toBeCalled()\n    })\n\n    test('use withTable plugin when insertData should invoke original insertData if selection not in table node', () => {\n      const editor = createEditor()\n      const mockInsertDataFn = jest.fn()\n      editor.insertData = mockInsertDataFn\n\n      const newEditor = withTable(editor)\n\n      jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockReturnValue(null)\n\n      newEditor.insertData({} as DataTransfer)\n\n      expect(mockInsertDataFn).toBeCalled()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/table-module/__tests__/render-elem.test.ts",
    "content": "import createEditor from '../../../tests/utils/create-editor'\nimport { renderTableConf, renderTableCellConf, renderTableRowConf } from '../src/module/render-elem'\n\ndescribe('table module - render elem', () => {\n  const editor = createEditor()\n\n  it('render table td elem', () => {\n    expect(renderTableCellConf.type).toBe('table-cell')\n\n    const elem = { type: 'table-cell', children: [] }\n    const vnode = renderTableCellConf.renderElem(elem, null, editor)\n    expect(vnode.sel).toBe('td')\n  })\n\n  // // isHeader 必须在第一行才能生效，该 case 运行报错，暂注释 - wangfupeng 2022.05.20\n  // it('render table th elem', () => {\n  //   const cell = { type: 'table-cell', children: [], isHeader: true }\n  //   const row = { type: 'table-row', children: [cell] }\n  //   const table = { type: 'table', children: [row] }\n  //   editor.insertNode(table)\n  //   const vnode = renderTableCellConf.renderElem(cell, null, editor)\n  //   expect(vnode.sel).toBe('th')\n  // })\n\n  it('render table row elem', () => {\n    expect(renderTableRowConf.type).toBe('table-row')\n\n    const elem = { type: 'table-row', children: [] }\n    const vnode = renderTableRowConf.renderElem(elem, null, editor)\n    expect(vnode.sel).toBe('tr')\n  })\n\n  it('render table elem', () => {\n    expect(renderTableConf.type).toBe('table')\n\n    const elem = { type: 'table', children: [] }\n    const containerVnode = renderTableConf.renderElem(elem, null, editor) as any\n    expect(containerVnode.sel).toBe('div')\n    const tableVnode = containerVnode.children[0] as any\n    expect(tableVnode.sel).toBe('table')\n  })\n\n  it('render table elem with full with', () => {\n    const elem = { type: 'table', children: [], width: '100%' }\n    const containerVnode = renderTableConf.renderElem(elem, null, editor) as any\n    const tableVnode = containerVnode.children[0] as any\n    expect(tableVnode.data.width).toBe('100%')\n  })\n})\n"
  },
  {
    "path": "packages/table-module/package.json",
    "content": "{\n  \"name\": \"@wangeditor/table-module\",\n  \"version\": \"1.1.4\",\n  \"description\": \"wangEditor table module\",\n  \"author\": \"wangfupeng1988 <wangfupeng1988@163.com>\",\n  \"contributors\": [],\n  \"homepage\": \"https://github.com/wangeditor-team/wangEditor#readme\",\n  \"license\": \"MIT\",\n  \"types\": \"dist/table-module/src/index.d.ts\",\n  \"main\": \"dist/index.js\",\n  \"module\": \"dist/index.esm.js\",\n  \"browser\": {\n    \"./dist/index.js\": \"./dist/index.js\",\n    \"./dist/index.esm.js\": \"./dist/index.esm.js\"\n  },\n  \"directories\": {\n    \"lib\": \"dist\",\n    \"test\": \"__tests__\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.com/\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wangeditor-team/wangEditor.git\"\n  },\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"test-c\": \"jest --coverage\",\n    \"dev\": \"cross-env NODE_ENV=development rollup -c rollup.config.js\",\n    \"dev-watch\": \"cross-env NODE_ENV=development rollup -c rollup.config.js -w\",\n    \"build\": \"cross-env NODE_ENV=production rollup -c rollup.config.js\",\n    \"dev-size-stats\": \"cross-env NODE_ENV=development:size_stats rollup -c rollup.config.js\",\n    \"size-stats\": \"cross-env NODE_ENV=production:size_stats rollup -c rollup.config.js\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/wangeditor-team/wangEditor/issues\"\n  },\n  \"peerDependencies\": {\n    \"@wangeditor/core\": \"1.x\",\n    \"dom7\": \"^3.0.0\",\n    \"lodash.isequal\": \"^4.5.0\",\n    \"lodash.throttle\": \"^4.1.1\",\n    \"nanoid\": \"^3.2.0\",\n    \"slate\": \"^0.72.0\",\n    \"snabbdom\": \"^3.1.0\"\n  }\n}\n"
  },
  {
    "path": "packages/table-module/rollup.config.js",
    "content": "import { createRollupConfig, IS_PRD } from '../../build/create-rollup-config'\nimport pkg from './package.json'\n\nconst name = 'WangEditorTableModule'\n\nconst configList = []\n\n// esm\nconst esmConf = createRollupConfig({\n  output: {\n    file: pkg.module,\n    format: 'esm',\n    name,\n  },\n})\nconfigList.push(esmConf)\n\n// umd\nconst umdConf = createRollupConfig({\n  output: {\n    file: pkg.main,\n    format: 'umd',\n    name,\n  },\n})\nconfigList.push(umdConf)\n\nexport default configList\n"
  },
  {
    "path": "packages/table-module/src/assets/index.less",
    "content": "@import \"../../../vars.less\";\n\n.w-e-text-container [data-slate-editor] {\n  .table-container {\n    width: 100%;\n    overflow-x: auto;\n    border: 1px dashed var(--w-e-textarea-border-color);\n    padding: 10px;\n    border-radius: 5px;\n    margin-top: 10px;\n  }\n\n  table {\n    border-collapse: collapse;\n\n    td,th {\n      border: 1px solid @textarea-border-color;\n      padding: 3px 5px;\n      min-width: 30px;\n      text-align: left;\n      line-height: 1.5;\n    }\n    th {\n      background-color: @textarea-slight-bg-color;\n      text-align: center;\n      font-weight: bold;\n    }\n  }\n}\n\n// --------------------------------- 分割线 ---------------------------------\n\n.w-e-panel-content-table {\n  background-color: @toolbar-bg-color;\n\n  table {\n    border-collapse: collapse;\n  }\n\n  td {\n    border: 1px solid @toolbar-border-color;\n    padding: 3px 5px;\n    width: 20px;\n    height: 15px;\n    cursor: pointer;\n  }\n  td.active {\n    background-color: @toolbar-active-bg-color;\n  }\n}\n"
  },
  {
    "path": "packages/table-module/src/constants/svg.ts",
    "content": "/**\n * @description icon svg\n * @author wangfupeng\n */\n\n/**\n * 【注意】svg 字符串的长度 ，否则会导致代码体积过大\n * 尽量选择 https://www.iconfont.cn/collections/detail?spm=a313x.7781069.0.da5a778a4&cid=20293\n * 找不到再从 iconfont.com 搜索\n */\n\n// 表格\nexport const TABLE_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M0 64v896h1024V64H0z m384 576v-192h256v192h-256z m256 64v192h-256v-192h256z m0-512v192h-256V192h256zM320 192v192H64V192h256z m-256 256h256v192H64v-192z m640 0h256v192h-256v-192z m0-64V192h256v192h-256zM64 704h256v192H64v-192z m640 192v-192h256v192h-256z\"></path></svg>'\n\n// 垃圾桶（删除）\nexport const TRASH_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M826.8032 356.5312c-19.328 0-36.3776 15.6928-36.3776 35.0464v524.2624c0 19.328-16 34.56-35.328 34.56H264.9344c-19.328 0-35.5072-15.3088-35.5072-34.56V390.0416c0-19.328-14.1568-35.0464-33.5104-35.0464s-33.5104 15.6928-33.5104 35.0464V915.712c0 57.9328 44.6208 108.288 102.528 108.288H755.2c57.9328 0 108.0832-50.4576 108.0832-108.288V391.4752c-0.1024-19.2512-17.1264-34.944-36.48-34.944z\" p-id=\"9577\"></path><path d=\"M437.1712 775.7568V390.6048c0-19.328-14.1568-35.0464-33.5104-35.0464s-33.5104 15.616-33.5104 35.0464v385.152c0 19.328 14.1568 35.0464 33.5104 35.0464s33.5104-15.7184 33.5104-35.0464zM649.7024 775.7568V390.6048c0-19.328-17.0496-35.0464-36.3776-35.0464s-36.3776 15.616-36.3776 35.0464v385.152c0 19.328 17.0496 35.0464 36.3776 35.0464s36.3776-15.7184 36.3776-35.0464zM965.0432 217.0368h-174.6176V145.5104c0-57.9328-47.2064-101.76-104.6528-101.76h-350.976c-57.8304 0-105.3952 43.8528-105.3952 101.76v71.5264H54.784c-19.4304 0-35.0464 14.1568-35.0464 33.5104 0 19.328 15.616 33.5104 35.0464 33.5104h910.3616c19.328 0 35.0464-14.1568 35.0464-33.5104 0-19.3536-15.6928-33.5104-35.1488-33.5104z m-247.3728 0H297.3952V145.5104c0-19.328 18.2016-34.7648 37.4272-34.7648h350.976c19.1488 0 31.872 15.1296 31.872 34.7648v71.5264z\"></path></svg>'\n\n// 表格 添加行\nexport const ADD_ROW_SVG =\n  '<svg viewBox=\"0 0 1048 1024\"><path d=\"M707.7888 521.0112h-147.456v-147.456H488.2432v147.456h-147.456v68.8128h147.456v147.456h72.0896v-147.456h147.456zM0 917.504V0h1048.576v917.504H0zM327.68 65.536H65.536v196.608H327.68V65.536z m327.68 0H393.216v196.608h262.144V65.536z m327.68 0h-262.144v196.608h262.144V65.536z m0 258.8672H65.536v462.0288H983.04V324.4032z\"></path></svg>'\n\n// 表格 删除行\nexport const DEL_ROW_SVG =\n  '<svg viewBox=\"0 0 1048 1024\"><path d=\"M907.6736 586.5472L747.1104 425.984l163.84-163.84-78.6432-78.6432-163.84 163.84L507.904 186.7776 429.2608 262.144l163.84 163.84-167.1168 167.1168 78.6432 78.6432 167.1168-167.1168 160.5632 160.5632 75.3664-78.6432zM0 917.504V0h1048.576v917.504H0z m983.04-327.68h-22.9376l-65.536-65.536H983.04V327.68h-91.7504l65.536-65.536h26.2144V65.536H65.536v196.608h317.8496l65.536 65.536H65.536v196.608h380.1088l-65.536 65.536H65.536v196.608H983.04v-196.608z\"></path></svg>'\n\n// 表格 添加列\nexport const ADD_COL_SVG =\n  '<svg viewBox=\"0 0 1048 1024\"><path d=\"M327.68 193.3312v186.7776H140.9024v91.7504H327.68v186.7776h88.4736V471.8592h190.0544V380.1088H416.1536V193.3312zM0 917.504V0h1048.576v917.504H0zM655.36 65.536H65.536v720.896H655.36V65.536z m327.68 0h-262.144v196.608h262.144V65.536z m0 262.144h-262.144v196.608h262.144V327.68z m0 262.144h-262.144v196.608h262.144v-196.608z\"></path></svg>'\n\n// 表格 删除列\nexport const DEL_COL_SVG =\n  '<svg viewBox=\"0 0 1048 1024\"><path d=\"M327.68 510.976L393.216 445.44v-13.1072L327.68 366.7968V510.976z m327.68-78.4384l65.536-65.536V507.904L655.36 442.368v-9.8304z m393.216 484.9664V0H0v917.504h1048.576z m-65.536-131.072h-262.144v-52.4288l-13.1072 13.1072-52.4288-52.4288v91.7504H393.216v-91.7504l-52.4288 52.4288-13.1072-13.1072v52.4288H65.536V65.536H327.68v121.2416l36.0448-36.0448 29.4912 29.4912V62.2592h262.144V180.224l49.152-49.152 16.384 16.384V62.2592h262.144V786.432z m-294.912-108.1344l-160.5632-160.5632-167.1168 167.1168-78.6432-78.6432 167.1168-167.1168L288.3584 278.528l78.6432-78.6432 160.5632 160.5632 163.84-163.84 78.6432 78.6432-163.84 163.84 160.5632 160.5632-78.6432 78.6432z\"></path></svg>'\n\n// 表头\nexport const TABLE_HEADER_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M704 128l-64 0L384 128 320 128 0 128l0 256 0 64 0 192 0 64 0 256 320 0 64 0 256 0 64 0 320 0 0-256 0-64L1024 448 1024 384 1024 128 704 128zM640 640 384 640 384 448l256 0L640 640zM64 448l256 0 0 192L64 640 64 448zM320 896 64 896l0-192 256 0L320 896zM640 896 384 896l0-192 256 0L640 896zM960 896l-256 0 0-192 256 0L960 896zM960 640l-256 0L704 448l256 0L960 640z\"></path></svg>'\n\n// 宽度\nexport const FULL_WIDTH_SVG =\n  '<svg viewBox=\"0 0 1228 1024\"><path d=\"M862.514337 563.200461H404.581995v121.753478a13.311987 13.311987 0 0 1-6.655993 11.468789 10.23999 10.23999 0 0 1-12.083188-1.433599l-204.799795-179.199821a13.721586 13.721586 0 0 1 0-20.479979l204.799795-179.302221a10.23999 10.23999 0 0 1 12.185588-1.535998 13.209587 13.209587 0 0 1 6.553593 11.673588v115.097485h457.932342V319.693504a11.571188 11.571188 0 0 1 18.841582-10.239989l204.799795 179.19982a13.721586 13.721586 0 0 1 0 20.47998l-204.799795 179.199821a10.23999 10.23999 0 0 1-12.185588 1.535998 13.311987 13.311987 0 0 1-6.655994-11.571188V563.200461zM136.499064 14.951409v993.893406a15.257585 15.257585 0 0 1-15.155185 15.052785H15.155185A15.155185 15.155185 0 0 1 0 1008.844815V14.951409a15.257585 15.257585 0 0 1 15.155185-15.052785h106.086294a15.155185 15.155185 0 0 1 15.257585 15.155185zM1228.798771 14.951409v993.893406a15.257585 15.257585 0 0 1-15.155185 15.052785h-106.188693a15.155185 15.155185 0 0 1-15.155185-15.052785V14.951409a15.257585 15.257585 0 0 1 15.155185-15.052785h106.086293A15.155185 15.155185 0 0 1 1228.798771 15.053809z\"></path></svg>'\n"
  },
  {
    "path": "packages/table-module/src/index.ts",
    "content": "/**\n * @description table entry\n * @author wangfupeng\n */\n\nimport './assets/index.less'\n\n// 配置多语言\nimport './locale/index'\n\nimport wangEditorTableModule from './module/index'\nexport default wangEditorTableModule\n"
  },
  {
    "path": "packages/table-module/src/locale/en.ts",
    "content": "/**\n * @description i18n en\n * @author wangfupeng\n */\n\nexport default {\n  tableModule: {\n    deleteCol: 'Delete column',\n    deleteRow: 'Delete row',\n    deleteTable: 'Delete table',\n    widthAuto: 'Width auto',\n    insertCol: 'Insert column',\n    insertRow: 'Insert row',\n    insertTable: 'Insert table',\n    header: 'Header',\n  },\n}\n"
  },
  {
    "path": "packages/table-module/src/locale/index.ts",
    "content": "/**\n * @description i18n entry\n * @author wangfupeng\n */\n\nimport { i18nAddResources } from '@wangeditor/core'\nimport enResources from './en'\nimport zhResources from './zh-CN'\n\ni18nAddResources('en', enResources)\ni18nAddResources('zh-CN', zhResources)\n"
  },
  {
    "path": "packages/table-module/src/locale/zh-CN.ts",
    "content": "/**\n * @description i18n zh-CN\n * @author wangfupeng\n */\n\nexport default {\n  tableModule: {\n    deleteCol: '删除列',\n    deleteRow: '删除行',\n    deleteTable: '删除表格',\n    widthAuto: '宽度自适应',\n    insertCol: '插入列',\n    insertRow: '插入行',\n    insertTable: '插入表格',\n    header: '表头',\n  },\n}\n"
  },
  {
    "path": "packages/table-module/src/module/custom-types.ts",
    "content": "/**\n * @description 自定义 element\n * @author wangfupeng\n */\n\nimport { Text } from 'slate'\n\n//【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts\n\nexport type TableCellElement = {\n  type: 'table-cell'\n  isHeader?: boolean // td/th 只作用于第一行\n  colSpan?: number\n  rowSpan?: number\n  width?: string // 只作用于第一行（尚未考虑单元格合并！）\n  children: Text[]\n}\n\nexport type TableRowElement = {\n  type: 'table-row'\n  children: TableCellElement[]\n}\n\nexport type TableElement = {\n  type: 'table'\n  width: string\n  children: TableRowElement[]\n}\n"
  },
  {
    "path": "packages/table-module/src/module/elem-to-html.ts",
    "content": "/**\n * @description to html\n * @author wangfupeng\n */\n\nimport { Element } from 'slate'\nimport { TableCellElement, TableRowElement, TableElement } from './custom-types'\n\nfunction tableToHtml(elemNode: Element, childrenHtml: string): string {\n  const { width = 'auto' } = elemNode as TableElement\n\n  return `<table style=\"width: ${width};\"><tbody>${childrenHtml}</tbody></table>`\n}\n\nfunction tableRowToHtml(elem: Element, childrenHtml: string): string {\n  return `<tr>${childrenHtml}</tr>`\n}\n\nfunction tableCellToHtml(cellNode: Element, childrenHtml: string): string {\n  const {\n    colSpan = 1,\n    rowSpan = 1,\n    isHeader = false,\n    width = 'auto',\n  } = cellNode as TableCellElement\n  const tag = isHeader ? 'th' : 'td'\n  return `<${tag} colSpan=\"${colSpan}\" rowSpan=\"${rowSpan}\" width=\"${width}\">${childrenHtml}</${tag}>`\n}\n\nexport const tableToHtmlConf = {\n  type: 'table',\n  elemToHtml: tableToHtml,\n}\n\nexport const tableRowToHtmlConf = {\n  type: 'table-row',\n  elemToHtml: tableRowToHtml,\n}\n\nexport const tableCellToHtmlConf = {\n  type: 'table-cell',\n  elemToHtml: tableCellToHtml,\n}\n"
  },
  {
    "path": "packages/table-module/src/module/helpers.ts",
    "content": "/**\n * @description table menu helpers\n * @author wangfupeng\n */\n\nimport { DomEditor, IDomEditor } from '@wangeditor/core'\nimport { TableElement, TableCellElement } from './custom-types'\n\n/**\n * 获取第一行所有 cells\n * @param tableNode table node\n */\nexport function getFirstRowCells(tableNode: TableElement): TableCellElement[] {\n  const rows = tableNode.children || [] // 所有行\n  if (rows.length === 0) return []\n  const firstRow = rows[0] || {} // 第一行\n  const cells = firstRow.children || [] // 第一行所有 cell\n  return cells\n}\n\n/**\n * 表格是否带有表头？\n * @param tableNode table node\n */\nexport function isTableWithHeader(tableNode: TableElement): boolean {\n  const firstRowCells = getFirstRowCells(tableNode)\n  return firstRowCells.every(cell => !!cell.isHeader)\n}\n\n/**\n * 单元格是否在第一行\n * @param editor editor\n * @param cellNode cell node\n */\nexport function isCellInFirstRow(editor: IDomEditor, cellNode: TableCellElement): boolean {\n  const rowNode = DomEditor.getParentNode(editor, cellNode)\n  if (rowNode == null) return false\n  const tableNode = DomEditor.getParentNode(editor, rowNode)\n  if (tableNode == null) return false\n\n  const firstRowCells = getFirstRowCells(tableNode as TableElement)\n  return firstRowCells.some(c => c === cellNode)\n}\n"
  },
  {
    "path": "packages/table-module/src/module/index.ts",
    "content": "/**\n * @description table module\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport withTable from './plugin'\nimport { renderTableConf, renderTableRowConf, renderTableCellConf } from './render-elem/index'\nimport { tableToHtmlConf, tableRowToHtmlConf, tableCellToHtmlConf } from './elem-to-html'\nimport { preParseTableHtmlConf } from './pre-parse-html'\nimport { parseCellHtmlConf, parseRowHtmlConf, parseTableHtmlConf } from './parse-elem-html'\nimport {\n  insertTableMenuConf,\n  deleteTableMenuConf,\n  insertTableRowConf,\n  deleteTableRowConf,\n  insertTableColConf,\n  deleteTableColConf,\n  tableHeaderMenuConf,\n  tableFullWidthMenuConf,\n} from './menu/index'\n\nconst table: Partial<IModuleConf> = {\n  renderElems: [renderTableConf, renderTableRowConf, renderTableCellConf],\n  elemsToHtml: [tableToHtmlConf, tableRowToHtmlConf, tableCellToHtmlConf],\n  preParseHtml: [preParseTableHtmlConf],\n  parseElemsHtml: [parseCellHtmlConf, parseRowHtmlConf, parseTableHtmlConf],\n  menus: [\n    insertTableMenuConf,\n    deleteTableMenuConf,\n    insertTableRowConf,\n    deleteTableRowConf,\n    insertTableColConf,\n    deleteTableColConf,\n    tableHeaderMenuConf,\n    tableFullWidthMenuConf,\n  ],\n  editorPlugin: withTable,\n}\n\nexport default table\n"
  },
  {
    "path": "packages/table-module/src/module/menu/DeleteCol.ts",
    "content": "/**\n * @description del col menu\n * @author wangfupeng\n */\n\nimport isEqual from 'lodash.isequal'\nimport { Editor, Element, Transforms, Range, Node } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport { DEL_COL_SVG } from '../../constants/svg'\n\nclass DeleteCol implements IButtonMenu {\n  readonly title = t('tableModule.deleteCol')\n  readonly iconSvg = DEL_COL_SVG\n  readonly tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 无需获取 val\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 无需 active\n    return false\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    const { selection } = editor\n    if (selection == null) return true\n    if (!Range.isCollapsed(selection)) return true\n\n    const cellNode = DomEditor.getSelectedNodeByType(editor, 'table-cell')\n    if (cellNode == null) {\n      // 选区未处于 table cell node ，则禁用\n      return true\n    }\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    if (this.isDisabled(editor)) return\n\n    const [cellEntry] = Editor.nodes(editor, {\n      match: n => DomEditor.checkNodeType(n, 'table-cell'),\n      universal: true,\n    })\n    const [selectedCellNode, selectedCellPath] = cellEntry\n\n    // 如果只有一列，则删除整个表格\n    const rowNode = DomEditor.getParentNode(editor, selectedCellNode)\n    const colLength = rowNode?.children.length || 0\n    if (!rowNode || colLength <= 1) {\n      Transforms.removeNodes(editor, { mode: 'highest' }) // 删除整个表格\n      return\n    }\n\n    // ------------------------- 不只有 1 列，则继续 -------------------------\n\n    const tableNode = DomEditor.getParentNode(editor, rowNode)\n    if (tableNode == null) return\n\n    // 遍历所有 rows ，挨个删除 cell\n    const rows = tableNode.children || []\n    rows.forEach(row => {\n      if (!Element.isElement(row)) return\n\n      const cells = row.children || []\n      // 遍历一个 row 的所有 cells\n      cells.forEach((cell: Node) => {\n        const path = DomEditor.findPath(editor, cell)\n        if (\n          path.length === selectedCellPath.length &&\n          isEqual(path.slice(-1), selectedCellPath.slice(-1)) // 俩数组，最后一位相同\n        ) {\n          // 如果当前 td 的 path 和选中 td 的 path ，最后一位相同，说明是同一列\n          // 删除当前的 cell\n          Transforms.removeNodes(editor, { at: path })\n        }\n      })\n    })\n  }\n}\n\nexport default DeleteCol\n"
  },
  {
    "path": "packages/table-module/src/module/menu/DeleteRow.ts",
    "content": "/**\n * @description del row menu\n * @author wangfupeng\n */\n\nimport { Editor, Transforms, Range } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport { DEL_ROW_SVG } from '../../constants/svg'\n\nclass DeleteRow implements IButtonMenu {\n  readonly title = t('tableModule.deleteRow')\n  readonly iconSvg = DEL_ROW_SVG\n  readonly tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 无需获取 val\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 无需 active\n    return false\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    const { selection } = editor\n    if (selection == null) return true\n    if (!Range.isCollapsed(selection)) return true\n\n    const rowNode = DomEditor.getSelectedNodeByType(editor, 'table-row')\n    if (rowNode == null) {\n      // 选区未处于 table row node ，则禁用\n      return true\n    }\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    if (this.isDisabled(editor)) return\n\n    const [rowEntry] = Editor.nodes(editor, {\n      match: n => DomEditor.checkNodeType(n, 'table-row'),\n      universal: true,\n    })\n    const [rowNode, rowPath] = rowEntry\n\n    const tableNode = DomEditor.getParentNode(editor, rowNode)\n    const rowsLength = tableNode?.children.length || 0\n    if (rowsLength <= 1) {\n      // row 只有一行，则删掉整个表格\n      Transforms.removeNodes(editor, { mode: 'highest' })\n      return\n    }\n\n    // row > 1 行，则删掉这一行\n    Transforms.removeNodes(editor, { at: rowPath })\n  }\n}\n\nexport default DeleteRow\n"
  },
  {
    "path": "packages/table-module/src/module/menu/DeleteTable.ts",
    "content": "/**\n * @description del table menu\n * @author wangfupeng\n */\n\nimport { Transforms } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport { TRASH_SVG } from '../../constants/svg'\n\nclass DeleteTable implements IButtonMenu {\n  readonly title = t('tableModule.deleteTable')\n  readonly iconSvg = TRASH_SVG\n  readonly tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 无需获取 val\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 无需 active\n    return false\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n\n    const tableNode = DomEditor.getSelectedNodeByType(editor, 'table')\n    if (tableNode == null) {\n      // 选区未处于 table node ，则禁用\n      return true\n    }\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    if (this.isDisabled(editor)) return\n\n    // 删除表格\n    Transforms.removeNodes(editor, { mode: 'highest' })\n  }\n}\n\nexport default DeleteTable\n"
  },
  {
    "path": "packages/table-module/src/module/menu/FullWidth.ts",
    "content": "/**\n * @description table full width menu\n * @author wangfupeng\n */\n\nimport { Transforms, Range } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport { FULL_WIDTH_SVG } from '../../constants/svg'\nimport { TableElement } from '../custom-types'\n\nclass TableFullWidth implements IButtonMenu {\n  readonly title = t('tableModule.widthAuto')\n  readonly iconSvg = FULL_WIDTH_SVG\n  readonly tag = 'button'\n\n  // 是否已设置 宽度自适应\n  getValue(editor: IDomEditor): string | boolean {\n    const tableNode = DomEditor.getSelectedNodeByType(editor, 'table')\n    if (tableNode == null) return false\n    return (tableNode as TableElement).width === '100%'\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    return !!this.getValue(editor)\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    const { selection } = editor\n    if (selection == null) return true\n    if (!Range.isCollapsed(selection)) return true\n\n    const tableNode = DomEditor.getSelectedNodeByType(editor, 'table')\n    if (tableNode == null) {\n      // 选区未处于 table node ，则禁用\n      return true\n    }\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    if (this.isDisabled(editor)) return\n\n    const props: Partial<TableElement> = {\n      width: value ? 'auto' : '100%', // 切换 'auto' 和 '100%'\n    }\n    Transforms.setNodes(editor, props, { mode: 'highest' })\n  }\n}\n\nexport default TableFullWidth\n"
  },
  {
    "path": "packages/table-module/src/module/menu/InsertCol.ts",
    "content": "/**\n * @description insert col menu\n * @author wangfupeng\n */\n\nimport isEqual from 'lodash.isequal'\nimport { Editor, Element, Transforms, Range, Node } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport { ADD_COL_SVG } from '../../constants/svg'\nimport { TableCellElement, TableElement } from '../custom-types'\nimport { isTableWithHeader } from '../helpers'\n\nclass InsertCol implements IButtonMenu {\n  readonly title = t('tableModule.insertCol')\n  readonly iconSvg = ADD_COL_SVG\n  readonly tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 无需获取 val\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 无需 active\n    return false\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    const { selection } = editor\n    if (selection == null) return true\n    if (!Range.isCollapsed(selection)) return true\n\n    const tableNode = DomEditor.getSelectedNodeByType(editor, 'table')\n    if (tableNode == null) {\n      // 选区未处于 table cell node ，则禁用\n      return true\n    }\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    if (this.isDisabled(editor)) return\n\n    const [cellEntry] = Editor.nodes(editor, {\n      match: n => DomEditor.checkNodeType(n, 'table-cell'),\n      universal: true,\n    })\n    const [selectedCellNode, selectedCellPath] = cellEntry\n\n    const rowNode = DomEditor.getParentNode(editor, selectedCellNode)\n    if (rowNode == null) return\n    const tableNode = DomEditor.getParentNode(editor, rowNode) as TableElement\n    if (tableNode == null) return\n\n    // 遍历所有 rows ，挨个添加 cell\n    const rows = tableNode.children || []\n    rows.forEach((row, rowIndex) => {\n      if (!Element.isElement(row)) return\n\n      const cells = row.children || []\n      // 遍历一个 row 的所有 cells\n      cells.forEach((cell: Node) => {\n        const path = DomEditor.findPath(editor, cell)\n        if (\n          path.length === selectedCellPath.length &&\n          isEqual(path.slice(-1), selectedCellPath.slice(-1)) // 俩数组，最后一位相同\n        ) {\n          // 如果当前 td 的 path 和选中 td 的 path ，最后一位相同，说明是同一列\n          // 则在其后插入一个 cell\n          const newCell: TableCellElement = { type: 'table-cell', children: [{ text: '' }] }\n          if (rowIndex === 0 && isTableWithHeader(tableNode)) {\n            newCell.isHeader = true\n          }\n          Transforms.insertNodes(editor, newCell, { at: path })\n        }\n      })\n    })\n  }\n}\n\nexport default InsertCol\n"
  },
  {
    "path": "packages/table-module/src/module/menu/InsertRow.ts",
    "content": "/**\n * @description insert row menu\n * @author wangfupeng\n */\n\nimport { Editor, Transforms, Range, Path } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport { ADD_ROW_SVG } from '../../constants/svg'\nimport { TableRowElement, TableCellElement } from '../custom-types'\n\nclass InsertRow implements IButtonMenu {\n  readonly title = t('tableModule.insertRow')\n  readonly iconSvg = ADD_ROW_SVG\n  readonly tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 无需获取 val\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 无需 active\n    return false\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    const { selection } = editor\n    if (selection == null) return true\n    if (!Range.isCollapsed(selection)) return true\n\n    const tableNode = DomEditor.getSelectedNodeByType(editor, 'table')\n    if (tableNode == null) {\n      // 选区未处于 table cell node ，则禁用\n      return true\n    }\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    if (this.isDisabled(editor)) return\n\n    const [cellEntry] = Editor.nodes(editor, {\n      match: n => DomEditor.checkNodeType(n, 'table-cell'),\n      universal: true,\n    })\n    const [cellNode, cellPath] = cellEntry\n\n    // 获取 cell length ，即多少列\n    const rowNode = DomEditor.getParentNode(editor, cellNode)\n    const cellsLength = rowNode?.children.length || 0\n    if (cellsLength === 0) return\n\n    // 拼接新的 row\n    const newRow: TableRowElement = { type: 'table-row', children: [] }\n    for (let i = 0; i < cellsLength; i++) {\n      const cell: TableCellElement = {\n        type: 'table-cell',\n        children: [{ text: '' }],\n      }\n      newRow.children.push(cell)\n    }\n\n    // 插入 row\n    const rowPath = Path.parent(cellPath) // 获取 tr 的 path\n    const newRowPath = Path.next(rowPath)\n    Transforms.insertNodes(editor, newRow, { at: newRowPath })\n  }\n}\n\nexport default InsertRow\n"
  },
  {
    "path": "packages/table-module/src/module/menu/InsertTable.ts",
    "content": "/**\n * @description insert table menu\n * @author wangfupeng\n */\n\nimport { Editor, Transforms, Range, Node } from 'slate'\nimport { IDropPanelMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport $, { Dom7Array, DOMElement } from '../../utils/dom'\nimport { genRandomStr } from '../../utils/util'\nimport { TABLE_SVG } from '../../constants/svg'\nimport { TableElement, TableCellElement, TableRowElement } from '../custom-types'\n\nfunction genTableNode(rowNum: number, colNum: number): TableElement {\n  // 拼接 rows\n  const rows: TableRowElement[] = []\n  for (let i = 0; i < rowNum; i++) {\n    // 拼接 cells\n    const cells: TableCellElement[] = []\n    for (let j = 0; j < colNum; j++) {\n      const cellNode: TableCellElement = {\n        type: 'table-cell',\n        children: [{ text: '' }],\n      }\n      if (i === 0) {\n        cellNode.isHeader = true // 第一行默认是 th\n      }\n      cells.push(cellNode)\n    }\n\n    // 生成 row\n    rows.push({\n      type: 'table-row',\n      children: cells,\n    })\n  }\n\n  return {\n    type: 'table',\n    width: 'auto',\n    children: rows,\n  }\n}\n\n/**\n * 生成唯一的 DOM ID\n */\nfunction genDomID(): string {\n  return genRandomStr('w-e-insert-table')\n}\n\nclass InsertTable implements IDropPanelMenu {\n  title = t('tableModule.insertTable')\n  iconSvg = TABLE_SVG\n  tag = 'button'\n  showDropPanel = true // 点击 button 时显示 dropPanel\n  private $content: Dom7Array | null = null\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 插入菜单，不需要 value\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 任何时候，都不用激活 menu\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    // 点击菜单时，弹出 modal 之前，不需要执行其他代码\n    // 此处空着即可\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    const { selection } = editor\n    if (selection == null) return true\n    if (!Range.isCollapsed(selection)) return true // 选区非折叠，禁用\n\n    const selectedElems = DomEditor.getSelectedElems(editor)\n    const hasVoidOrPreOrTable = selectedElems.some(elem => {\n      const type = DomEditor.getNodeType(elem)\n      if (type === 'pre') return true\n      if (type === 'table') return true\n      if (type === 'list-item') return true\n      if (editor.isVoid(elem)) return true\n      return false\n    })\n    if (hasVoidOrPreOrTable) return true // 匹配到，禁用\n\n    return false\n  }\n\n  /**\n   *  获取 panel 内容\n   * @param editor editor\n   */\n  getPanelContentElem(editor: IDomEditor): DOMElement {\n    // 已有，直接返回\n    if (this.$content) return this.$content[0]\n\n    // 初始化\n    const $content = $('<div class=\"w-e-panel-content-table\"></div>')\n    const $info = $('<span>0 &times; 0</span>') // 显示行列数量\n\n    // 渲染 10 * 10 table ，以快速创建表格\n    const $table = $('<table></table>')\n    for (let i = 0; i < 10; i++) {\n      const $tr = $('<tr></tr>')\n      for (let j = 0; j < 10; j++) {\n        const $td = $('<td></td>')\n        $td.attr('data-x', j.toString())\n        $td.attr('data-y', i.toString())\n        $tr.append($td)\n\n        // 绑定 mouseenter\n        $td.on('mouseenter', (e: Event) => {\n          const { target } = e\n          if (target == null) return\n          const $focusTd = $(target)\n          const { x: focusX, y: focusY } = $focusTd.dataset()\n\n          // 显示行列数量\n          $info[0].innerHTML = `${focusX + 1} &times; ${focusY + 1}`\n\n          // 修改 table td 样式\n          $table.children().each(tr => {\n            $(tr)\n              .children()\n              .each(td => {\n                const $td = $(td)\n                const { x, y } = $td.dataset()\n                if (x <= focusX && y <= focusY) {\n                  $td.addClass('active')\n                } else {\n                  $td.removeClass('active')\n                }\n              })\n          })\n        })\n\n        // 绑定 click\n        $td.on('click', (e: Event) => {\n          e.preventDefault()\n          const { target } = e\n          if (target == null) return\n          const $td = $(target)\n          const { x, y } = $td.dataset()\n          this.insertTable(editor, y + 1, x + 1)\n        })\n      }\n      $table.append($tr)\n    }\n    $content.append($table)\n    $content.append($info)\n\n    // 记录，并返回\n    this.$content = $content\n    return $content[0]\n  }\n\n  private insertTable(editor: IDomEditor, rowNumStr: string, colNumStr: string) {\n    const rowNum = parseInt(rowNumStr, 10)\n    const colNum = parseInt(colNumStr, 10)\n    if (!rowNum || !colNum) return\n    if (rowNum <= 0 || colNum <= 0) return\n\n    // 如果当前是空 p ，则删除该 p\n    if (DomEditor.isSelectedEmptyParagraph(editor)) {\n      Transforms.removeNodes(editor, { mode: 'highest' })\n    }\n\n    // 插入表格\n    const tableNode = genTableNode(rowNum, colNum)\n    Transforms.insertNodes(editor, tableNode, { mode: 'highest' })\n  }\n}\n\nexport default InsertTable\n"
  },
  {
    "path": "packages/table-module/src/module/menu/TableHeader.ts",
    "content": "/**\n * @description table header menu\n * @author wangfupeng\n */\n\nimport { Transforms, Range } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport { TABLE_HEADER_SVG } from '../../constants/svg'\nimport { TableElement } from '../custom-types'\nimport { getFirstRowCells, isTableWithHeader } from '../helpers'\n\nclass TableHeader implements IButtonMenu {\n  readonly title = t('tableModule.header')\n  readonly iconSvg = TABLE_HEADER_SVG\n  readonly tag = 'button'\n\n  // 是否已设置表头\n  getValue(editor: IDomEditor): string | boolean {\n    const tableNode = DomEditor.getSelectedNodeByType(editor, 'table') as TableElement\n    if (tableNode == null) return false\n\n    return isTableWithHeader(tableNode)\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    return !!this.getValue(editor)\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    const { selection } = editor\n    if (selection == null) return true\n    if (!Range.isCollapsed(selection)) return true\n\n    const tableNode = DomEditor.getSelectedNodeByType(editor, 'table')\n    if (tableNode == null) {\n      // 选区未处于 table node ，则禁用\n      return true\n    }\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    if (this.isDisabled(editor)) return\n\n    // 已经设置了表头，则取消。未设置表头，则设置\n    const newValue = value ? false : true\n\n    // 获取第一行所有 cell\n    const tableNode = DomEditor.getSelectedNodeByType(editor, 'table') as TableElement\n    if (tableNode == null) return\n    const firstRowCells = getFirstRowCells(tableNode)\n\n    // 设置 isHeader 属性\n    firstRowCells.forEach(cell =>\n      Transforms.setNodes(\n        editor,\n        { isHeader: newValue },\n        {\n          at: DomEditor.findPath(editor, cell),\n        }\n      )\n    )\n  }\n}\n\nexport default TableHeader\n"
  },
  {
    "path": "packages/table-module/src/module/menu/index.ts",
    "content": "/**\n * @description table menu\n * @author wangfupeng\n */\n\nimport InsertTable from './InsertTable'\nimport DeleteTable from './DeleteTable'\nimport InsertRow from './InsertRow'\nimport DeleteRow from './DeleteRow'\nimport InsertCol from './InsertCol'\nimport DeleteCol from './DeleteCol'\nimport TableHander from './TableHeader'\nimport FullWidth from './FullWidth'\n\nexport const insertTableMenuConf = {\n  key: 'insertTable',\n  factory() {\n    return new InsertTable()\n  },\n}\n\nexport const deleteTableMenuConf = {\n  key: 'deleteTable',\n  factory() {\n    return new DeleteTable()\n  },\n}\n\nexport const insertTableRowConf = {\n  key: 'insertTableRow',\n  factory() {\n    return new InsertRow()\n  },\n}\n\nexport const deleteTableRowConf = {\n  key: 'deleteTableRow',\n  factory() {\n    return new DeleteRow()\n  },\n}\n\nexport const insertTableColConf = {\n  key: 'insertTableCol',\n  factory() {\n    return new InsertCol()\n  },\n}\n\nexport const deleteTableColConf = {\n  key: 'deleteTableCol',\n  factory() {\n    return new DeleteCol()\n  },\n}\n\nexport const tableHeaderMenuConf = {\n  key: 'tableHeader',\n  factory() {\n    return new TableHander()\n  },\n}\n\nexport const tableFullWidthMenuConf = {\n  key: 'tableFullWidth',\n  factory() {\n    return new FullWidth()\n  },\n}\n"
  },
  {
    "path": "packages/table-module/src/module/parse-elem-html.ts",
    "content": "/**\n * @description parse html\n * @author wangfupeng\n */\n\nimport { Descendant, Text } from 'slate'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\nimport { TableCellElement, TableRowElement, TableElement } from './custom-types'\nimport $, { getTagName, getStyleValue, DOMElement } from '../utils/dom'\n\nfunction parseCellHtml(\n  elem: DOMElement,\n  children: Descendant[],\n  editor: IDomEditor\n): TableCellElement {\n  const $elem = $(elem)\n\n  children = children.filter(child => {\n    if (Text.isText(child)) return true\n    if (editor.isInline(child)) return true\n    return false\n  })\n\n  // 无 children ，则用纯文本\n  if (children.length === 0) {\n    children = [{ text: $elem.text().replace(/\\s+/gm, ' ') }]\n  }\n\n  const colSpan = parseInt($elem.attr('colSpan') || '1')\n  const rowSpan = parseInt($elem.attr('rowSpan') || '1')\n  const width = $elem.attr('width') || 'auto'\n\n  return {\n    type: 'table-cell',\n    isHeader: getTagName($elem) === 'th',\n    colSpan,\n    rowSpan,\n    width,\n    // @ts-ignore\n    children,\n  }\n}\n\nexport const parseCellHtmlConf = {\n  selector: 'td:not([data-w-e-type]),th:not([data-w-e-type])', // data-w-e-type 属性，留给自定义元素，保证扩展性\n  parseElemHtml: parseCellHtml,\n}\n\nfunction parseRowHtml(\n  elem: DOMElement,\n  children: Descendant[],\n  editor: IDomEditor\n): TableRowElement {\n  return {\n    type: 'table-row',\n    // @ts-ignore\n    children: children.filter(child => DomEditor.getNodeType(child) === 'table-cell'),\n  }\n}\n\nexport const parseRowHtmlConf = {\n  selector: 'tr:not([data-w-e-type])', // data-w-e-type 属性，留给自定义元素，保证扩展性\n  parseElemHtml: parseRowHtml,\n}\n\nfunction parseTableHtml(\n  elem: DOMElement,\n  children: Descendant[],\n  editor: IDomEditor\n): TableElement {\n  const $elem = $(elem)\n\n  // 计算宽度\n  let width = 'auto'\n  if (getStyleValue($elem, 'width') === '100%') width = '100%'\n  if ($elem.attr('width') === '100%') width = '100%' // 兼容 v4 格式\n\n  return {\n    type: 'table',\n    width,\n    // @ts-ignore\n    children: children.filter(child => DomEditor.getNodeType(child) === 'table-row'),\n  }\n}\n\nexport const parseTableHtmlConf = {\n  selector: 'table:not([data-w-e-type])', // data-w-e-type 属性，留给自定义元素，保证扩展性\n  parseElemHtml: parseTableHtml,\n}\n"
  },
  {
    "path": "packages/table-module/src/module/plugin.ts",
    "content": "/**\n * @description editor 插件，重写 editor API\n * @author wangfupeng\n */\n\nimport {\n  Editor,\n  Transforms,\n  Location,\n  Point,\n  Element as SlateElement,\n  Descendant,\n  NodeEntry,\n  Node,\n  BaseText,\n  Path,\n} from 'slate'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\n\n// table cell 内部的删除处理\nfunction deleteHandler(newEditor: IDomEditor): boolean {\n  const { selection } = newEditor\n  if (selection == null) return false\n\n  const [cellNodeEntry] = Editor.nodes(newEditor, {\n    match: n => DomEditor.checkNodeType(n, 'table-cell'),\n  })\n  if (cellNodeEntry) {\n    const [, cellPath] = cellNodeEntry\n    const start = Editor.start(newEditor, cellPath)\n\n    if (Point.equals(selection.anchor, start)) {\n      return true // 阻止删除 cell\n    }\n  }\n\n  return false\n}\n\n/**\n * 判断该 location 有没有命中 table\n * @param editor editor\n * @param location location\n */\nfunction isTableLocation(editor: IDomEditor, location: Location): boolean {\n  const tables = Editor.nodes(editor, {\n    at: location,\n    match: n => {\n      const type = DomEditor.getNodeType(n)\n      return type === 'table'\n    },\n  })\n  let hasTable = false\n  for (const table of tables) {\n    hasTable = true // 找到了 table\n  }\n  return hasTable\n}\n\nfunction withTable<T extends IDomEditor>(editor: T): T {\n  const {\n    insertBreak,\n    deleteBackward,\n    deleteForward,\n    normalizeNode,\n    insertData,\n    handleTab,\n    selectAll,\n  } = editor\n  const newEditor = editor\n\n  // 重写 insertBreak - cell 内换行，只换行文本，不拆分 node\n  newEditor.insertBreak = () => {\n    const selectedNode = DomEditor.getSelectedNodeByType(newEditor, 'table')\n    if (selectedNode != null) {\n      // 选中了 table ，则在 cell 内换行\n      newEditor.insertText('\\n')\n      return\n    }\n\n    // 未选中 table ，默认的换行\n    insertBreak()\n  }\n\n  // 重写 delete - cell 内删除，只删除文字，不删除 node\n  newEditor.deleteBackward = unit => {\n    const res = deleteHandler(newEditor)\n    if (res) return // 命中 table cell ，自己处理删除\n\n    // 防止从 table 后面的 p 删除时，删除最后一个 cell - issues/4221\n    const { selection } = newEditor\n    if (selection) {\n      const before = Editor.before(newEditor, selection) // 前一个 location\n      if (before) {\n        const isTableOnBeforeLocation = isTableLocation(newEditor, before) // before 是否是 table\n        const isTableOnCurSelection = isTableLocation(newEditor, selection) // 当前是否是 table\n        if (isTableOnBeforeLocation && !isTableOnCurSelection) {\n          return // 如果当前不是 table ，前面是 table ，则不执行删除。否则会删除 table 最后一个 cell\n        }\n      }\n    }\n\n    // 执行默认的删除\n    deleteBackward(unit)\n  }\n\n  // 重写 handleTab 在table内按tab时跳到下一个单元格\n  newEditor.handleTab = () => {\n    const selectedNode = DomEditor.getSelectedNodeByType(newEditor, 'table')\n    if (selectedNode) {\n      const above = Editor.above(editor) as NodeEntry<SlateElement>\n\n      // 常规情况下选中文字外层 table-cell 进行跳转\n      if (DomEditor.checkNodeType(above[0], 'table-cell')) {\n        Transforms.select(editor, above[1])\n      }\n\n      let next = Editor.next(editor)\n      if (next) {\n        if (next[0] && (next[0] as BaseText).text) {\n          // 多个单元格同时选中按 tab 导致错位修复\n          next = (Editor.above(editor, { at: next[1] }) as NodeEntry<Descendant>) ?? next\n        }\n        Transforms.select(editor, next[1])\n      } else {\n        const topLevelNodes = newEditor.children || []\n        const topLevelNodesLength = topLevelNodes.length\n        // 在最后一个单元格按tab时table末尾如果没有p则插入p后光标切到p上\n        if (DomEditor.checkNodeType(topLevelNodes[topLevelNodesLength - 1], 'table')) {\n          const p = DomEditor.genEmptyParagraph()\n          Transforms.insertNodes(newEditor, p, { at: [topLevelNodesLength] })\n          // 在表格末尾插入p后再次执行使光标切到p上\n          newEditor.handleTab()\n        }\n      }\n      return\n    }\n\n    handleTab()\n  }\n\n  newEditor.deleteForward = unit => {\n    const res = deleteHandler(newEditor)\n    if (res) return // 命中 table cell ，自己处理删除\n\n    // 执行默认的删除\n    deleteForward(unit)\n  }\n\n  // 重新 normalize\n  newEditor.normalizeNode = ([node, path]) => {\n    const type = DomEditor.getNodeType(node)\n    if (type !== 'table') {\n      // 未命中 table ，执行默认的 normalizeNode\n      return normalizeNode([node, path])\n    }\n\n    // -------------- table 是 editor 最后一个节点，需要后面插入 p --------------\n    const isLast = DomEditor.isLastNode(newEditor, node)\n    if (isLast) {\n      const p = DomEditor.genEmptyParagraph()\n      Transforms.insertNodes(newEditor, p, { at: [path[0] + 1] })\n    }\n  }\n\n  // 重写 insertData - 粘贴文本\n  newEditor.insertData = (data: DataTransfer) => {\n    const tableNode = DomEditor.getSelectedNodeByType(newEditor, 'table')\n    if (tableNode == null) {\n      insertData(data) // 执行默认的 insertData\n      return\n    }\n\n    // 获取文本，并插入到 cell\n    const text = data.getData('text/plain')\n\n    // 单图或图文 插入\n    if (text === '\\n' || /<img[^>]+>/.test(data.getData('text/html'))) {\n      insertData(data)\n      return\n    }\n\n    Editor.insertText(newEditor, text)\n  }\n\n  // 重写 table-cell 中的全选\n  newEditor.selectAll = () => {\n    const selection = newEditor.selection\n    if (selection == null) {\n      selectAll()\n      return\n    }\n\n    const cell = DomEditor.getSelectedNodeByType(newEditor, 'table-cell')\n    if (cell == null) {\n      selectAll()\n      return\n    }\n\n    const { anchor, focus } = selection\n    if (!Path.equals(anchor.path.slice(0, 3), focus.path.slice(0, 3))) {\n      // 选中了多个 cell ，忽略\n      selectAll()\n      return\n    }\n\n    const text = Node.string(cell)\n    const textLength = text.length\n    if (textLength === 0) {\n      selectAll()\n      return\n    }\n\n    const path = DomEditor.findPath(newEditor, cell)\n    const start = Editor.start(newEditor, path)\n    const end = Editor.end(newEditor, path)\n    const newSelection = {\n      anchor: start,\n      focus: end,\n    }\n    newEditor.select(newSelection) // 选中 table-cell 内部的全部文字\n  }\n\n  // 可继续修改其他 newEditor API ...\n\n  // 返回 editor ，重要！\n  return newEditor\n}\n\nexport default withTable\n"
  },
  {
    "path": "packages/table-module/src/module/pre-parse-html.ts",
    "content": "/**\n * @description pre parse html\n * @author wangfupeng\n */\n\nimport $, { getTagName, DOMElement } from '../utils/dom'\n\n/**\n * pre-prase table ，去掉 <tbody>\n * @param table table elem\n */\nfunction preParse(tableElem: DOMElement): DOMElement {\n  const $table = $(tableElem)\n  const tagName = getTagName($table)\n  if (tagName !== 'table') return tableElem\n\n  // 没有 <tbody> 则直接返回\n  const $tbody = $table.find('tbody')\n  if ($tbody.length === 0) return tableElem\n\n  // 去掉 <tbody> ，把 <tr> 移动到 <table> 下面\n  const $tr = $table.find('tr')\n  $table.append($tr)\n  $tbody.remove()\n\n  return $table[0]\n}\n\nexport const preParseTableHtmlConf = {\n  selector: 'table',\n  preParseHtml: preParse,\n}\n"
  },
  {
    "path": "packages/table-module/src/module/render-elem/index.ts",
    "content": "/**\n * @description render elem\n * @author wangfupeng\n */\n\nimport renderTable from './render-table'\nimport renderTableRow from './render-row'\nimport renderTableCell from './render-cell'\n\nexport const renderTableConf = {\n  type: 'table',\n  renderElem: renderTable,\n}\n\nexport const renderTableRowConf = {\n  type: 'table-row',\n  renderElem: renderTableRow,\n}\n\nexport const renderTableCellConf = {\n  type: 'table-cell',\n  renderElem: renderTableCell,\n}\n"
  },
  {
    "path": "packages/table-module/src/module/render-elem/render-cell.tsx",
    "content": "/**\n * @description render cell\n * @author wangfupeng\n */\n\nimport throttle from 'lodash.throttle'\nimport { Element as SlateElement, Transforms, Location } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\nimport { TableCellElement } from '../custom-types'\nimport { isCellInFirstRow } from '../helpers'\nimport $ from '../../utils/dom'\n\n// 拖拽列宽相关信息\nlet isMouseDownForResize = false\nlet clientXWhenMouseDown = 0\nlet cellWidthWhenMouseDown = 0\nlet cellPathWhenMouseDown: Location | null = null\nlet editorWhenMouseDown: IDomEditor | null = null\nconst $body = $('body')\n\nfunction onMouseDown(event: Event) {\n  const elem = event.target as HTMLElement\n  if (elem.tagName !== 'TH' && elem.tagName !== 'TD') return\n\n  if (elem.style.cursor !== 'col-resize') return\n  elem.style.cursor = 'auto'\n\n  event.preventDefault()\n\n  // 记录必要信息\n  isMouseDownForResize = true\n  const { clientX } = event as MouseEvent\n  clientXWhenMouseDown = clientX\n  const { width } = elem.getBoundingClientRect()\n  cellWidthWhenMouseDown = width\n\n  // 绑定事件\n  $body.on('mousemove', onMouseMove)\n  $body.on('mouseup', onMouseUp)\n}\n$body.on('mousedown', onMouseDown) // 绑定事件\n\nfunction onMouseUp(event: Event) {\n  isMouseDownForResize = false\n  editorWhenMouseDown = null\n  cellPathWhenMouseDown = null\n\n  // 解绑事件\n  $body.off('mousemove', onMouseMove)\n  $body.off('mouseup', onMouseUp)\n}\n\nconst onMouseMove = throttle(function (event: Event) {\n  if (!isMouseDownForResize) return\n  if (editorWhenMouseDown == null || cellPathWhenMouseDown == null) return\n  event.preventDefault()\n\n  const { clientX } = event as MouseEvent\n  let newWith = cellWidthWhenMouseDown + (clientX - clientXWhenMouseDown) // 计算新宽度\n  newWith = Math.floor(newWith * 100) / 100 // 保留小数点后两位\n  if (newWith < 30) newWith = 30 // 最小宽度\n\n  // 这是宽度\n  Transforms.setNodes(\n    editorWhenMouseDown,\n    { width: newWith.toString() },\n    {\n      at: cellPathWhenMouseDown,\n    }\n  )\n}, 100)\n\nfunction renderTableCell(\n  cellNode: SlateElement,\n  children: VNode[] | null,\n  editor: IDomEditor\n): VNode {\n  const isFirstRow = isCellInFirstRow(editor, cellNode as TableCellElement)\n  const { colSpan = 1, rowSpan = 1, isHeader = false } = cellNode as TableCellElement\n\n  // ------------------ 不是第一行，直接渲染 <td> ------------------\n  if (!isFirstRow) {\n    return (\n      <td colSpan={colSpan} rowSpan={rowSpan}>\n        {children}\n      </td>\n    )\n  }\n\n  // ------------------ 是第一行：1. 判断 th ；2. 拖拽列宽 ------------------\n  const Tag = isHeader ? 'th' : 'td'\n\n  const vnode = (\n    <Tag\n      colSpan={colSpan}\n      rowSpan={rowSpan}\n      style={{ borderRightWidth: '3px' }}\n      on={{\n        mousemove: throttle(function (this: VNode, event: MouseEvent) {\n          const elem = this.elm as HTMLElement\n          if (elem == null) return\n          const { left, width, top, height } = elem.getBoundingClientRect()\n          const { clientX, clientY } = event\n\n          if (isMouseDownForResize) return // 此时正在修改列宽\n\n          // 非 mousedown 状态，计算 cursor 样式\n          const matchX = clientX > left + width - 5 && clientX < left + width // X 轴，是否接近 cell 右侧？\n          const matchY = clientY > top && clientY < top + height // Y 轴，是否在 cell 之内\n          // X Y 轴都接近，则修改鼠标样式\n          if (matchX && matchY) {\n            elem.style.cursor = 'col-resize'\n            editorWhenMouseDown = editor\n            cellPathWhenMouseDown = DomEditor.findPath(editor, cellNode)\n          } else {\n            if (!isMouseDownForResize) {\n              elem.style.cursor = 'auto'\n              editorWhenMouseDown = null\n              cellPathWhenMouseDown = null\n            }\n          }\n        }, 100),\n      }}\n    >\n      {children}\n    </Tag>\n  )\n  return vnode\n}\n\nexport default renderTableCell\n"
  },
  {
    "path": "packages/table-module/src/module/render-elem/render-row.tsx",
    "content": "/**\n * @description render row\n * @author wangfupeng\n */\n\nimport { Element as SlateElement } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { IDomEditor } from '@wangeditor/core'\n\nfunction renderTableRow(\n  elemNode: SlateElement,\n  children: VNode[] | null,\n  editor: IDomEditor\n): VNode {\n  const vnode = <tr>{children}</tr>\n  return vnode\n}\n\nexport default renderTableRow\n"
  },
  {
    "path": "packages/table-module/src/module/render-elem/render-table.tsx",
    "content": "/**\n * @description render table\n * @author wangfupeng\n */\n\nimport { Editor, Element as SlateElement, Range, Point, Path } from 'slate'\nimport { jsx, VNode } from 'snabbdom'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\nimport { TableElement } from '../custom-types'\nimport { getFirstRowCells } from '../helpers'\n\n/**\n * 计算 table 是否可编辑。如果选区跨域 table 和外部内容，删除，会导致 table 结构打乱。所以，有时要让 table 不可编辑\n * @param editor editor\n * @param tableElem table elem\n */\nfunction getContentEditable(editor: IDomEditor, tableElem: SlateElement): boolean {\n  if (editor.isDisabled()) return false\n\n  const { selection } = editor\n  if (selection == null) return true\n  if (Range.isCollapsed(selection)) return true\n\n  const { anchor, focus } = selection\n  const tablePath = DomEditor.findPath(editor, tableElem)\n\n  const tableStart = Editor.start(editor, tablePath)\n  const tableEnd = Editor.end(editor, tablePath)\n  const isAnchorInTable =\n    Point.compare(anchor, tableEnd) <= 0 && Point.compare(anchor, tableStart) >= 0\n  const isFocusInTable =\n    Point.compare(focus, tableEnd) <= 0 && Point.compare(focus, tableStart) >= 0\n\n  // 选区在 table 内部，且选中了同一个单元格。表格可以编辑\n  if (isAnchorInTable && isFocusInTable) {\n    if (Path.equals(anchor.path.slice(0, 3), focus.path.slice(0, 3))) {\n      return true\n    }\n  }\n\n  return false\n}\n\nfunction renderTable(elemNode: SlateElement, children: VNode[] | null, editor: IDomEditor): VNode {\n  // 是否可编辑\n  const editable = getContentEditable(editor, elemNode)\n\n  // 宽度\n  const { width = 'auto' } = elemNode as TableElement\n\n  // 是否选中\n  const selected = DomEditor.isNodeSelected(editor, elemNode)\n\n  // 第一行的 cells ，以计算列宽\n  const firstRowCells = getFirstRowCells(elemNode as TableElement)\n\n  const vnode = (\n    <div\n      className=\"table-container\"\n      data-selected={selected}\n      on={{\n        mousedown: (e: MouseEvent) => {\n          // @ts-ignore 阻止光标定位到 table 后面\n          if (e.target.tagName === 'DIV') e.preventDefault()\n\n          if (editor.isDisabled()) return\n\n          // 是否需要定位到 table 内部\n          const tablePath = DomEditor.findPath(editor, elemNode)\n          const tableStart = Editor.start(editor, tablePath)\n          const { selection } = editor\n          if (selection == null) {\n            editor.select(tableStart) // 选中 table 内部\n            return\n          }\n          const { path } = selection.anchor\n          if (path[0] === tablePath[0]) return // 当前选区，就在 table 内部\n\n          editor.select(tableStart) // 选中 table 内部\n        },\n      }}\n    >\n      <table width={width} contentEditable={editable}>\n        <colgroup>\n          {firstRowCells.map(cell => {\n            const { width = 'auto' } = cell\n            return <col width={width}></col>\n          })}\n        </colgroup>\n        <tbody>{children}</tbody>\n      </table>\n    </div>\n  )\n  return vnode\n}\n\nexport default renderTable\n"
  },
  {
    "path": "packages/table-module/src/utils/dom.ts",
    "content": "/**\n * @description DOM 操作\n * @author wangfupeng\n */\n\nimport $, {\n  append,\n  on,\n  focus,\n  attr,\n  val,\n  html,\n  dataset,\n  addClass,\n  removeClass,\n  children,\n  each,\n  find,\n  Dom7Array,\n} from 'dom7'\nexport { Dom7Array } from 'dom7'\n\nif (append) $.fn.append = append\nif (on) $.fn.on = on\nif (focus) $.fn.focus = focus\nif (attr) $.fn.attr = attr\nif (val) $.fn.val = val\nif (html) $.fn.html = html\nif (dataset) $.fn.dataset = dataset\nif (addClass) $.fn.addClass = addClass\nif (removeClass) $.fn.removeClass = removeClass\nif (children) $.fn.children = children\nif (each) $.fn.each = each\nif (find) $.fn.find = find\n\nexport default $\n\n/**\n * 获取 tagName lower-case\n * @param $elem $elem\n */\nexport function getTagName($elem: Dom7Array): string {\n  if ($elem.length) return $elem[0].tagName.toLowerCase()\n  return ''\n}\n\n/**\n * 获取 $elem 某一个 style 值\n * @param $elem $elem\n * @param styleKey style key\n */\nexport function getStyleValue($elem: Dom7Array, styleKey: string): string {\n  let res = ''\n\n  const styleStr = $elem.attr('style') || '' // 如 'line-height: 2.5; color: red;'\n  const styleArr = styleStr.split(';') // 如 ['line-height: 2.5', ' color: red', '']\n  const length = styleArr.length\n  for (let i = 0; i < length; i++) {\n    const styleItemStr = styleArr[i] // 如 'line-height: 2.5'\n    if (styleItemStr) {\n      const arr = styleItemStr.split(':') // ['line-height', ' 2.5']\n      if (arr[0].trim() === styleKey) {\n        res = arr[1].trim()\n      }\n    }\n  }\n\n  return res\n}\n\n// COMPAT: This is required to prevent TypeScript aliases from doing some very\n// weird things for Slate's types with the same name as globals. (2019/11/27)\n// https://github.com/microsoft/TypeScript/issues/35002\nimport DOMNode = globalThis.Node\nimport DOMComment = globalThis.Comment\nimport DOMElement = globalThis.Element\nimport DOMText = globalThis.Text\nimport DOMRange = globalThis.Range\nimport DOMSelection = globalThis.Selection\nimport DOMStaticRange = globalThis.StaticRange\nexport { DOMNode, DOMComment, DOMElement, DOMText, DOMRange, DOMSelection, DOMStaticRange }\n"
  },
  {
    "path": "packages/table-module/src/utils/util.ts",
    "content": "/**\n * @description 工具函数\n * @author wangfupeng\n */\n\nimport { nanoid } from 'nanoid'\n\n/**\n * 获取随机数字符串\n * @param prefix 前缀\n * @returns 随机数字符串\n */\nexport function genRandomStr(prefix: string = 'r'): string {\n  return `${prefix}-${nanoid()}`\n}\n"
  },
  {
    "path": "packages/table-module/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {},\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\n    \"./src/**/*\",\n    \"../custom-types.d.ts\"\n  ]\n}"
  },
  {
    "path": "packages/upload-image-module/CHANGELOG.md",
    "content": "# Change Log\n\nAll notable changes to this project will be documented in this file.\nSee [Conventional Commits](https://conventionalcommits.org) for commit guidelines.\n\n## [1.0.2](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/upload-image-module@1.0.1...@wangeditor/upload-image-module@1.0.2) (2022-09-15)\n\n\n### Bug Fixes\n\n* customInsert 不触发 onSuccess ([d6f4a1b](https://github.com/wangeditor-team/wangEditor/commit/d6f4a1b1494864b116a1310cce2d9e8632c92c6f))\n\n\n\n\n\n## 1.0.1 (2022-04-18)\n\n\n### Bug Fixes\n\n* 多图片上传 ([53fe915](https://github.com/wangeditor-team/wangEditor/commit/53fe915aa7d40f05e1e9446c7f26606c46832ff3))\n* 更新各包之间依赖版本 ([75c552c](https://github.com/wangeditor-team/wangEditor/commit/75c552cc8ed54765bebb86a7ec5329a7fc79e85f))\n* 上传图片 - base64 仍触发上传 + 超出 maxSize 的报错提醒 ([a1d469a](https://github.com/wangeditor-team/wangEditor/commit/a1d469accb7f87f8ea0282a1699d002aaaa4e79a))\n* 图片上传，提示 ([3754012](https://github.com/wangeditor-team/wangEditor/commit/37540129dff1212c5ebfd4ca3f4d4e8def735e73))\n* 修复 pnpm 安装 @wangeditor/editor 出现警告的问题 ([4087fbe](https://github.com/wangeditor-team/wangEditor/commit/4087fbee01c76bdd55e747a5e86c5e4a8d6a8353))\n* 移除了每个包下的 publishConfig directory 配置 ([16559f0](https://github.com/wangeditor-team/wangEditor/commit/16559f052545c111318be760e64291a521bdcc65))\n* 粘贴 excel ([5382a6e](https://github.com/wangeditor-team/wangEditor/commit/5382a6edab2d362c7be143b62e7dd21bea8a15ab))\n* npm install error - basic-modules 相关 ([b85a0dc](https://github.com/wangeditor-team/wangEditor/commit/b85a0dcfaa15d69424d86a20255d6b9e8b28494f))\n* rename es module filename ([1821d4e](https://github.com/wangeditor-team/wangEditor/commit/1821d4eef49e64efcb41b848849ca7a5e6472044))\n\n\n### Features\n\n* 上传图片 metaWithUrl ([2485157](https://github.com/wangeditor-team/wangEditor/commit/24851576a1dcc07b1a8931d17a147c3640222e85))\n* 增加 enable disable API（删除 setConfig setMenuConfig API） ([984fc50](https://github.com/wangeditor-team/wangEditor/commit/984fc50520061fc34ea08f4136bdeb93dee46564))\n* editor.showProgressBar ([51761d4](https://github.com/wangeditor-team/wangEditor/commit/51761d466ab3ef7c99e872954d4724ab51d8e28c))\n* i18n ([c11b244](https://github.com/wangeditor-team/wangEditor/commit/c11b2440f91b99d40bca18b675c66a22b6e160c9))\n* image menu - width 50% 100% ([f9b4c68](https://github.com/wangeditor-team/wangEditor/commit/f9b4c68dff3232b50491b07949c20eb4c18baa6b))\n* image menu config ([bb18774](https://github.com/wangeditor-team/wangEditor/commit/bb187740e9703b4a76cde4f5e4d32ac714aa793a))\n* upload image ([0a0564b](https://github.com/wangeditor-team/wangEditor/commit/0a0564bf14edd4dea6eb958e653272a9a216cec1))\n* upload video ([ac8e6f8](https://github.com/wangeditor-team/wangEditor/commit/ac8e6f8b5258e593714676a6f6be359ba525833c))\n"
  },
  {
    "path": "packages/upload-image-module/README.md",
    "content": "# wangEditor upload-image-module\n\nUpload image module built in [wangEditor](https://www.wangeditor.com/) by default.\n"
  },
  {
    "path": "packages/upload-image-module/__tests__/config.test.ts",
    "content": "import { genUploadImageConfig } from '../src/module/menu/config'\n\ndescribe('Upload image default config', () => {\n  test('Upload image invoke genUploadImageConfig should generate default config', () => {\n    expect(typeof genUploadImageConfig()).toBe('object')\n  })\n\n  test('The option server is \"\" in default config', () => {\n    expect(genUploadImageConfig().server).toBe('')\n  })\n\n  test('The option fieldName is \"wangeditor-uploaded-image\" in default config', () => {\n    expect(genUploadImageConfig().fieldName).toBe('wangeditor-uploaded-image')\n  })\n\n  test('The option maxFileSize is \"2M\" in default config', () => {\n    expect(genUploadImageConfig().maxFileSize).toBe(2 * 1024 * 1024)\n  })\n\n  test('The option maxNumberOfFiles is \"100\" in default config', () => {\n    expect(genUploadImageConfig().maxNumberOfFiles).toBe(100)\n  })\n\n  test('The option allowedFileTypes is \"[image/*\"]\" in default config', () => {\n    expect(genUploadImageConfig().allowedFileTypes).toEqual(['image/*'])\n  })\n\n  test('The option metaWithUrl is \"false\" in default config', () => {\n    expect(genUploadImageConfig().metaWithUrl).toBe(false)\n  })\n\n  test('The option withCredentials is \"false\" in default config', () => {\n    expect(genUploadImageConfig().withCredentials).toBe(false)\n  })\n\n  test('The option timeout is \"10s\" in default config', () => {\n    expect(genUploadImageConfig().timeout).toBe(10 * 1000)\n  })\n\n  test('The option base64LimitSize is \"0\" in default config', () => {\n    expect(genUploadImageConfig().base64LimitSize).toBe(0)\n  })\n})\n"
  },
  {
    "path": "packages/upload-image-module/__tests__/plugin.test.ts",
    "content": "import { IDomEditor } from '@wangeditor/core'\nimport * as basicModule from '@wangeditor/basic-modules'\nimport createEditor from '../../../tests/utils/create-editor'\nimport withUploadImage from '../src/module/plugin'\nimport * as uploadImage from '../src/module/upload-images'\n\nlet editor: IDomEditor\n\ndescribe('withUploadImage plugin', () => {\n  beforeEach(() => {\n    editor = createEditor()\n\n    // mock isInsertImageMenuDisabled\n    jest.spyOn(basicModule, 'isInsertImageMenuDisabled').mockImplementation(() => false)\n  })\n\n  test('withUploadImage plugin should invoke insertData directly for insert transfer data if isInsertImageMenuDisabled return truthy value', () => {\n    jest.spyOn(basicModule, 'isInsertImageMenuDisabled').mockImplementation(() => true)\n\n    const fn = jest.fn()\n    editor.insertData = fn\n\n    const newEditor = withUploadImage(editor)\n    newEditor.insertData(new DataTransfer())\n\n    expect(fn).toBeCalled()\n  })\n\n  test('withUploadImage plugin should invoke insertData with text data if transfer data contains plain text ', () => {\n    const fn = jest.fn()\n    editor.insertData = fn\n\n    const newEditor = withUploadImage(editor)\n    jest.spyOn(DataTransfer.prototype, 'getData').mockImplementation(() => 'plain text')\n    const transfer = new DataTransfer()\n    newEditor.insertData(transfer)\n\n    expect(transfer.getData('text/plain')).toBe('plain text')\n    expect(fn).toBeCalledWith(transfer)\n\n    // 不影响后面的测试，需要重置\n    jest.spyOn(DataTransfer.prototype, 'getData').mockImplementation(() => '')\n  })\n\n  test('withUploadImage plugin should invoke insertData with transfer data if transfer data contains empty files', () => {\n    const fn = jest.fn()\n    editor.insertData = fn\n\n    const newEditor = withUploadImage(editor)\n    jest.spyOn(DataTransfer.prototype, 'files', 'get').mockReturnValue([] as any)\n    newEditor.insertData(new DataTransfer())\n\n    expect(fn).toBeCalled()\n  })\n\n  test('withUploadImage plugin should invoke uploadImage method with image files if transfer data contains file which mime type is image', () => {\n    const fn = jest.fn()\n    jest.spyOn(uploadImage, 'default').mockImplementation(fn)\n\n    const newEditor = withUploadImage(editor)\n    jest\n      .spyOn(DataTransfer.prototype, 'files', 'get')\n      .mockReturnValue([{ type: 'image/png', size: 10 }] as any)\n\n    newEditor.insertData(new DataTransfer())\n\n    expect(fn).toBeCalled()\n  })\n\n  test('withUploadImage plugin should invoke insertData method with transfer data if transfer data contains file which mime type is not image', () => {\n    const fn = jest.fn()\n    editor.insertData = fn\n\n    const newEditor = withUploadImage(editor)\n    jest\n      .spyOn(DataTransfer.prototype, 'files', 'get')\n      .mockReturnValue([{ type: 'text/html', size: 10 }] as any)\n\n    const transfer = new DataTransfer()\n    newEditor.insertData(transfer)\n\n    expect(fn).toBeCalledWith(transfer)\n  })\n})\n"
  },
  {
    "path": "packages/upload-image-module/__tests__/upload-files.test.ts",
    "content": "import uploadImages from '../src/module/upload-images'\nimport createEditor from '../../../tests/utils/create-editor'\nimport * as core from '@wangeditor/core'\n\nfunction mockFile(filename: string) {\n  const file = new File(['123'], filename)\n  return file\n}\n\ndescribe('Upload image menu upload files util', () => {\n  test('uploadImages should do nothing if give null value to fileList argument', async () => {\n    const editor = createEditor()\n    const res = await uploadImages(editor, null)\n    expect(res).toBeUndefined()\n  })\n\n  test('uploadImages should invoke customUpload if give customUpload to config', async () => {\n    const fn = jest.fn()\n    const editor = createEditor({\n      config: {\n        MENU_CONF: {\n          uploadImage: {\n            customUpload: fn,\n          },\n        },\n      },\n    })\n\n    await uploadImages(editor, [mockFile('test.jpg')] as unknown as FileList)\n\n    expect(fn).toBeCalled()\n  })\n\n  test('uploadImages should insert image with base64 string if file size less than base64LimitSize config', async () => {\n    const fn = jest.fn()\n    const editor = createEditor({\n      config: {\n        MENU_CONF: {\n          uploadImage: {\n            customUpload: fn,\n            base64LimitSize: 10,\n          },\n        },\n      },\n    })\n\n    const mockReadAsDataURL = jest.spyOn(FileReader.prototype, 'readAsDataURL')\n\n    await uploadImages(editor, [mockFile('test.jpg')] as unknown as FileList)\n\n    expect(mockReadAsDataURL).toBeCalled()\n  })\n\n  test('uploadImages should invoke core createUploader if not give customUpload to config', async () => {\n    const fn = jest.fn().mockImplementation(\n      () =>\n        // 这里需要返回一个 duck 类型的 uppy 对象，防止后面代码执行报错\n        ({\n          addFile: jest.fn(),\n          upload: jest.fn(),\n        } as any)\n    )\n    const editor = createEditor()\n\n    jest.spyOn(core, 'createUploader').mockImplementation(fn)\n\n    await uploadImages(editor, [mockFile('test.jpg')] as unknown as FileList)\n\n    expect(fn).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "packages/upload-image-module/__tests__/upload-image-menu.test.ts",
    "content": "import { IDomEditor } from '../../../packages/editor/src'\nimport UploadImageMenu from '../src/module/menu/UploadImageMenu'\nimport createEditor from '../../../tests/utils/create-editor'\n\nlet editor: IDomEditor\nlet menu: UploadImageMenu\n\ndescribe('Upload image menu', () => {\n  beforeEach(() => {\n    editor = createEditor()\n    menu = new UploadImageMenu()\n  })\n\n  test('UploadImageMenu instance title is \"上传图片\" for zhCn locale config', () => {\n    expect(menu.title).toBe('上传图片')\n  })\n\n  test('UploadImageMenu invoke getValue return \"\"', () => {\n    expect(menu.getValue(editor)).toBe('')\n  })\n\n  test('UploadImageMenu invoke isActive always return false', () => {\n    expect(menu.isActive(editor)).toBe(false)\n  })\n\n  test('UploadImageMenu invoke exec should exec customBrowseAndUpload if config has customBrowseAndUpload option', () => {\n    const jestFn = jest.fn()\n    const editor = createEditor({\n      config: {\n        MENU_CONF: {\n          uploadImage: {\n            customBrowseAndUpload: jestFn,\n          },\n        },\n      },\n    })\n    menu.exec(editor, 'test.jpg')\n    expect(jestFn).toBeCalled()\n  })\n\n  test('UploadImageMenu invoke exec should insert hidden input element to body', () => {\n    const editor = createEditor({\n      config: {\n        MENU_CONF: {\n          uploadImage: {\n            allowedFileTypes: ['jpg', 'png'],\n          },\n        },\n      },\n    })\n\n    // 防卫断言\n    expect(document.querySelector('input')).toBeNull()\n\n    menu.exec(editor, 'test.jpg')\n\n    expect(document.querySelector('input') instanceof HTMLInputElement).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "packages/upload-image-module/package.json",
    "content": "{\n  \"name\": \"@wangeditor/upload-image-module\",\n  \"version\": \"1.0.2\",\n  \"description\": \"wangEditor upload-image module\",\n  \"author\": \"wangfupeng1988 <wangfupeng1988@163.com>\",\n  \"contributors\": [],\n  \"homepage\": \"https://github.com/wangeditor-team/wangEditor#readme\",\n  \"license\": \"MIT\",\n  \"types\": \"dist/upload-image-module/src/index.d.ts\",\n  \"main\": \"dist/index.js\",\n  \"module\": \"dist/index.esm.js\",\n  \"browser\": {\n    \"./dist/index.js\": \"./dist/index.js\",\n    \"./dist/index.esm.js\": \"./dist/index.esm.js\"\n  },\n  \"directories\": {\n    \"lib\": \"dist\",\n    \"test\": \"__tests__\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.com/\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wangeditor-team/wangEditor.git\"\n  },\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"test-c\": \"jest --coverage\",\n    \"dev\": \"cross-env NODE_ENV=development rollup -c rollup.config.js\",\n    \"dev-watch\": \"cross-env NODE_ENV=development rollup -c rollup.config.js -w\",\n    \"build\": \"cross-env NODE_ENV=production rollup -c rollup.config.js\",\n    \"dev-size-stats\": \"cross-env NODE_ENV=development:size_stats rollup -c rollup.config.js\",\n    \"size-stats\": \"cross-env NODE_ENV=production:size_stats rollup -c rollup.config.js\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/wangeditor-team/wangEditor/issues\"\n  },\n  \"peerDependencies\": {\n    \"@uppy/core\": \"^2.0.3\",\n    \"@uppy/xhr-upload\": \"^2.0.3\",\n    \"@wangeditor/basic-modules\": \"1.x\",\n    \"@wangeditor/core\": \"1.x\",\n    \"dom7\": \"^3.0.0\",\n    \"lodash.foreach\": \"^4.5.0\",\n    \"slate\": \"^0.72.0\",\n    \"snabbdom\": \"^3.1.0\"\n  }\n}\n"
  },
  {
    "path": "packages/upload-image-module/rollup.config.js",
    "content": "import { createRollupConfig, IS_PRD } from '../../build/create-rollup-config'\nimport pkg from './package.json'\n\nconst name = 'WangEditorUploadImageModule'\n\nconst configList = []\n\n// esm\nconst esmConf = createRollupConfig({\n  output: {\n    file: pkg.module,\n    format: 'esm',\n    name,\n  },\n})\nconfigList.push(esmConf)\n\n// umd\nconst umdConf = createRollupConfig({\n  output: {\n    file: pkg.main,\n    format: 'umd',\n    name,\n  },\n})\nconfigList.push(umdConf)\n\nexport default configList\n"
  },
  {
    "path": "packages/upload-image-module/src/assets/index.less",
    "content": "// styles\n"
  },
  {
    "path": "packages/upload-image-module/src/constants/svg.ts",
    "content": "/**\n * @description icon svg\n * @author wangfupeng\n */\n\n/**\n * 【注意】svg 字符串的长度 ，否则会导致代码体积过大\n * 尽量选择 https://www.iconfont.cn/collections/detail?spm=a313x.7781069.0.da5a778a4&cid=20293\n * 找不到再从 iconfont.com 搜索\n */\n\n// 上传图片\nexport const UPLOAD_IMAGE_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M828.708571 585.045333a48.761905 48.761905 0 0 0-48.737523 48.761905v18.529524l-72.143238-72.167619a135.972571 135.972571 0 0 0-191.585524 0l-34.133334 34.133333-120.880762-120.953905a138.898286 138.898286 0 0 0-191.585523 0l-72.167619 72.167619V292.400762a48.786286 48.786286 0 0 1 48.761904-48.761905h341.23581a48.737524 48.737524 0 0 0 34.474667-83.285333 48.737524 48.737524 0 0 0-34.474667-14.287238H146.236952A146.212571 146.212571 0 0 0 0 292.400762v585.289143A146.358857 146.358857 0 0 0 146.236952 1024h584.996572a146.212571 146.212571 0 0 0 146.236952-146.310095V633.807238a48.786286 48.786286 0 0 0-48.761905-48.761905zM146.261333 926.45181a48.737524 48.737524 0 0 1-48.761904-48.761905v-174.128762l141.409523-141.458286a38.497524 38.497524 0 0 1 53.126096 0l154.526476 154.624 209.627428 209.724953H146.236952z m633.734096-48.761905c-0.073143 9.337905-3.145143 18.383238-8.777143 25.843809l-219.843048-220.94019 34.133333-34.133334a37.546667 37.546667 0 0 1 53.613715 0l140.873143 141.897143V877.714286zM1009.615238 160.231619L863.329524 13.897143a48.737524 48.737524 0 0 0-16.091429-10.24c-11.849143-4.87619-25.161143-4.87619-37.059047 0a48.761905 48.761905 0 0 0-16.067048 10.24l-146.236952 146.334476a49.005714 49.005714 0 0 0 69.217523 69.241905l62.902858-63.390476v272.627809a48.761905 48.761905 0 1 0 97.475047 0V166.083048l62.902857 63.390476a48.737524 48.737524 0 0 0 69.217524 0 48.761905 48.761905 0 0 0 0-69.241905z\"></path></svg>'\n"
  },
  {
    "path": "packages/upload-image-module/src/index.ts",
    "content": "/**\n * @description upload image\n * @author wangfupeng\n */\n\nimport './assets/index.less'\n\n// 配置多语言\nimport './locale/index'\n\nimport wangEditorUploadImageModule from './module/index'\nexport default wangEditorUploadImageModule\n"
  },
  {
    "path": "packages/upload-image-module/src/locale/en.ts",
    "content": "/**\n * @description i18n en\n * @author wangfupeng\n */\n\nexport default {\n  uploadImgModule: {\n    uploadImage: 'Upload Image',\n    uploadError: '{{fileName}} upload error',\n  },\n}\n"
  },
  {
    "path": "packages/upload-image-module/src/locale/index.ts",
    "content": "/**\n * @description i18n entry\n * @author wangfupeng\n */\n\nimport { i18nAddResources } from '@wangeditor/core'\nimport enResources from './en'\nimport zhResources from './zh-CN'\n\ni18nAddResources('en', enResources)\ni18nAddResources('zh-CN', zhResources)\n"
  },
  {
    "path": "packages/upload-image-module/src/locale/zh-CN.ts",
    "content": "/**\n * @description i18n zh-CN\n * @author wangfupeng\n */\n\nexport default {\n  uploadImgModule: {\n    uploadImage: '上传图片',\n    uploadError: '{{fileName}} 上传出错',\n  },\n}\n"
  },
  {
    "path": "packages/upload-image-module/src/module/index.ts",
    "content": "/**\n * @description uploadImage module\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport withUploadImage from './plugin'\nimport { uploadImageMenuConf } from './menu/index'\n\nconst uploadImage: Partial<IModuleConf> = {\n  menus: [uploadImageMenuConf],\n  editorPlugin: withUploadImage,\n}\n\nexport default uploadImage\n"
  },
  {
    "path": "packages/upload-image-module/src/module/menu/UploadImageMenu.ts",
    "content": "/**\n * @description upload image menu\n * @author wangfupeng\n */\n\nimport { IButtonMenu, IDomEditor, t } from '@wangeditor/core'\nimport { insertImageNode, isInsertImageMenuDisabled } from '@wangeditor/basic-modules'\nimport { UPLOAD_IMAGE_SVG } from '../../constants/svg'\nimport $ from '../../utils/dom'\nimport { IUploadConfigForImage } from './config'\nimport uploadImages from '../upload-images'\n\nclass UploadImage implements IButtonMenu {\n  readonly title = t('uploadImgModule.uploadImage')\n  readonly iconSvg = UPLOAD_IMAGE_SVG\n  readonly tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 插入菜单，不需要 value\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 任何时候，都不用激活 menu\n    return false\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    return isInsertImageMenuDisabled(editor)\n  }\n\n  private getMenuConfig(editor: IDomEditor): IUploadConfigForImage {\n    // 获取配置，见 `./config.js`\n    return editor.getMenuConfig('uploadImage') as IUploadConfigForImage\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    const { allowedFileTypes = [], customBrowseAndUpload } = this.getMenuConfig(editor)\n\n    // 自定义选择图片，并上传，如图床\n    if (customBrowseAndUpload) {\n      customBrowseAndUpload((src, alt, href) => insertImageNode(editor, src, alt, href))\n      return\n    }\n\n    // 设置选择文件的类型\n    let acceptAttr = ''\n    if (allowedFileTypes.length > 0) {\n      acceptAttr = `accept=\"${allowedFileTypes.join(', ')}\"`\n    }\n\n    // 添加 file input（每次重新创建 input）\n    const $body = $('body')\n    const $inputFile = $(`<input type=\"file\" ${acceptAttr} multiple/>`)\n    $inputFile.hide()\n    $body.append($inputFile)\n    $inputFile.click()\n    // 选中文件\n    $inputFile.on('change', () => {\n      const files = ($inputFile[0] as HTMLInputElement).files\n      uploadImages(editor, files) // 上传文件\n    })\n  }\n}\n\nexport default UploadImage\n"
  },
  {
    "path": "packages/upload-image-module/src/module/menu/config.ts",
    "content": "/**\n * @description upload image config\n * @author wangfupeng\n */\n\nimport { IUploadConfig } from '@wangeditor/core'\n\ntype InsertFn = (src: string, alt: string, href: string) => void\n\n// 在通用 uploadConfig 上，扩展 image 相关配置\nexport type IUploadConfigForImage = IUploadConfig & {\n  allowedFileTypes?: string[]\n  // 用户自定义插入图片\n  customInsert?: (res: any, insertFn: InsertFn) => void\n  // 用户自定义上传图片\n  customUpload?: (files: File, insertFn: InsertFn) => void\n  // base64 限制（单位 kb） - 小于 xxx 就插入 base64 格式\n  base64LimitSize: number\n  // 自定义选择图片，如图床\n  customBrowseAndUpload?: (insertFn: InsertFn) => void\n}\n\n// 生成默认配置\nexport function genUploadImageConfig(): IUploadConfigForImage {\n  return {\n    server: '', // server API 地址，需用户配置\n\n    fieldName: 'wangeditor-uploaded-image', // formData 中，文件的 key\n    maxFileSize: 2 * 1024 * 1024, // 2M\n    maxNumberOfFiles: 100, // 最多上传 xx 张图片\n    allowedFileTypes: ['image/*'],\n    meta: {\n      // 自定义上传参数，例如传递验证的 token 等。参数会被添加到 formData 中，一起上传到服务端。\n      // 例如：token: 'xxxxx', x: 100\n    },\n    metaWithUrl: false,\n    // headers: {\n    //   // 自定义 http headers\n    //   // 例如：Accept: 'text/x-json', a: 100,\n    // },\n    withCredentials: false,\n    timeout: 10 * 1000, // 10s\n\n    onBeforeUpload: (files: any) => files, // 返回 false 则终止上传\n    onProgress: (progress: number) => {\n      /* on progress */\n    },\n    onSuccess: (file: any, res: any) => {\n      /* on success */\n    },\n    onFailed: (file: any, res: any) => {\n      console.error(`'${file.name}' upload failed`, res)\n    },\n    onError: (file: any, err: any, res: any) => {\n      /* on error */\n      /* on timeout */\n      console.error(`'${file.name}' upload error`, res)\n    },\n\n    // 自定义插入图片，用户配置\n    // customInsert: (res, insertFn) => {},\n\n    // 自定义上传图片，用户配置\n    // customUpload: (file, insertFn) => {},\n\n    // 小于 xxx 就插入 base64\n    base64LimitSize: 0,\n\n    // 自定义选择，并上传图片，如：图床 （用户配置）\n    // customBrowseAndUpload: insertFn => {},\n  }\n}\n"
  },
  {
    "path": "packages/upload-image-module/src/module/menu/index.ts",
    "content": "/**\n * @description upload image menu\n * @author wangfupeng\n */\n\nimport UploadImageMenu from './UploadImageMenu'\nimport { genUploadImageConfig } from './config'\n\nexport const uploadImageMenuConf = {\n  key: 'uploadImage',\n  factory() {\n    return new UploadImageMenu()\n  },\n\n  // 默认的菜单菜单配置，将存储在 editorConfig.MENU_CONF[key] 中\n  // 创建编辑器时，可通过 editorConfig.MENU_CONF[key] = {...} 来修改\n  config: genUploadImageConfig(),\n}\n"
  },
  {
    "path": "packages/upload-image-module/src/module/plugin.ts",
    "content": "/**\n * @description editor 插件，重写 editor API\n * @author wangfupeng\n */\n\nimport { IDomEditor } from '@wangeditor/core'\nimport { isInsertImageMenuDisabled } from '@wangeditor/basic-modules'\nimport uploadImages from './upload-images'\n\nfunction withUploadImage<T extends IDomEditor>(editor: T): T {\n  const { insertData } = editor\n  const newEditor = editor\n\n  // 重写 insertData - 粘贴图片、拖拽上传图片\n  newEditor.insertData = (data: DataTransfer) => {\n    if (isInsertImageMenuDisabled(newEditor)) {\n      insertData(data)\n      return\n    }\n\n    // 如有 text ，则优先粘贴 text\n    const text = data.getData('text/plain')\n    if (text) {\n      insertData(data)\n      return\n    }\n\n    // 获取文件\n    const { files } = data\n    if (files.length <= 0) {\n      insertData(data)\n      return\n    }\n\n    // 判断是否有图片文件（可能是其他类型的文件）\n    const fileList = Array.prototype.slice.call(files)\n    let _hasImageFiles = fileList.some(file => {\n      const [mime] = file.type.split('/')\n      return mime === 'image'\n    })\n\n    if (_hasImageFiles) {\n      // 有图片文件，则上传图片\n      uploadImages(editor, files)\n    } else {\n      // 如果没有， 则继续 insertData\n      insertData(data)\n    }\n  }\n\n  // 返回 editor ，重要！\n  return newEditor\n}\n\nexport default withUploadImage\n"
  },
  {
    "path": "packages/upload-image-module/src/module/upload-images.ts",
    "content": "/**\n * @description 上传文件\n * @author wangfupeng\n */\n\nimport Uppy, { UppyFile } from '@uppy/core'\nimport { IDomEditor, createUploader } from '@wangeditor/core'\nimport { insertImageNode } from '@wangeditor/basic-modules'\nimport { IUploadConfigForImage } from './menu/config'\n\n// 存储 editor uppy 的关系 - 缓存 uppy ，不重复创建\nconst EDITOR_TO_UPPY_MAP = new WeakMap<IDomEditor, Uppy>()\n\n/**\n * 获取 uppy 实例（并通过 editor 缓存）\n * @param editor editor\n */\nfunction getUppy(editor: IDomEditor): Uppy {\n  // 从缓存中获取\n  let uppy = EDITOR_TO_UPPY_MAP.get(editor)\n  if (uppy != null) return uppy\n\n  const menuConfig = getMenuConfig(editor)\n  const { onSuccess, onProgress, onFailed, customInsert, onError } = menuConfig\n\n  // 上传完成之后\n  const successHandler = (file: UppyFile, res: any) => {\n    // 预期 res 格式：\n    // 成功：{ errno: 0, data: { url, alt, href } } —— 注意，旧版的 data 是数组，要兼容一下\n    // 失败：{ errno: !0, message: '失败信息' }\n\n    if (customInsert) {\n      // 用户自定义插入图片，此时 res 格式可能不符合预期\n      customInsert(res, (src, alt, href) => insertImageNode(editor, src, alt, href))\n      // success 回调\n      onSuccess(file, res)\n      return\n    }\n\n    let { errno = 1, data = {} } = res\n    if (errno !== 0) {\n      // failed 回调\n      onFailed(file, res)\n      return\n    }\n\n    if (Array.isArray(data)) {\n      // 返回的数组（旧版的，兼容一下）\n      data.forEach((item: { url: string; alt?: string; href?: string }) => {\n        const { url = '', alt = '', href = '' } = item\n        // 使用 basic-module 的 insertImageNode 方法插入图片，其中有用户配置的校验和 callback\n        insertImageNode(editor, url, alt, href)\n      })\n    } else {\n      // 返回的对象\n      const { url = '', alt = '', href = '' } = data\n      insertImageNode(editor, url, alt, href)\n    }\n\n    // success 回调\n    onSuccess(file, res)\n  }\n\n  // progress 显示进度条\n  const progressHandler = (progress: number) => {\n    editor.showProgressBar(progress)\n\n    // 回调函数\n    onProgress && onProgress(progress)\n  }\n\n  // onError 提示错误\n  const errorHandler = (file: any, err: any, res: any) => {\n    // 回调函数\n    onError(file, err, res)\n  }\n\n  // 创建 uppy\n  uppy = createUploader({\n    ...menuConfig,\n    onProgress: progressHandler,\n    onSuccess: successHandler,\n    onError: errorHandler,\n  })\n  // 缓存 uppy\n  EDITOR_TO_UPPY_MAP.set(editor, uppy)\n\n  return uppy\n}\n\nfunction getMenuConfig(editor: IDomEditor) {\n  return editor.getMenuConfig('uploadImage') as IUploadConfigForImage\n}\n\n/**\n * 插入 base64 格式\n * @param editor editor\n * @param file file\n */\nasync function insertBase64(editor: IDomEditor, file: File) {\n  return new Promise(resolve => {\n    const reader = new FileReader()\n    reader.readAsDataURL(file)\n    reader.onload = () => {\n      const { result } = reader\n      if (!result) return\n      const src = result.toString()\n      let href = src.indexOf('data:image') === 0 ? '' : src // base64 格式则不设置 href\n      insertImageNode(editor, src, file.name, href)\n\n      resolve('ok')\n    }\n  })\n}\n\n/**\n * 上传图片文件\n * @param editor editor\n * @param file file\n */\nasync function uploadFile(editor: IDomEditor, file: File) {\n  const uppy = getUppy(editor)\n\n  const { name, type, size } = file\n  uppy.addFile({\n    name,\n    type,\n    size,\n    data: file,\n  })\n  await uppy.upload()\n}\n\n/**\n * 上传图片\n * @param editor editor\n * @param files files\n */\nexport default async function (editor: IDomEditor, files: FileList | null) {\n  if (files == null) return\n  const fileList = Array.prototype.slice.call(files)\n\n  // 获取菜单配置\n  const { customUpload, base64LimitSize } = getMenuConfig(editor)\n\n  // 按顺序上传\n  for await (const file of fileList) {\n    const size = file.size // size kb\n    if (base64LimitSize && size <= base64LimitSize) {\n      // 允许 base64 ，而且 size 在 base64 限制之内，则插入 base64 格式\n      await insertBase64(editor, file)\n    } else {\n      // 上传\n      if (customUpload) {\n        // 自定义上传\n        await customUpload(file, (src, alt, href) => insertImageNode(editor, src, alt, href))\n      } else {\n        // 默认上传\n        await uploadFile(editor, file)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/upload-image-module/src/utils/dom.ts",
    "content": "/**\n * @description DOM 操作\n * @author wangfupeng\n */\n\nimport $, { append, on, remove, val, click, hide } from 'dom7'\nexport { Dom7Array } from 'dom7'\n\nif (append) $.fn.append = append\nif (on) $.fn.on = on\nif (remove) $.fn.remove = remove\nif (val) $.fn.val = val\nif (click) $.fn.click = click\nif (hide) $.fn.hide = hide\n\nexport default $\n"
  },
  {
    "path": "packages/upload-image-module/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {},\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\n    \"./src/**/*\",\n    \"../custom-types.d.ts\"\n  ]\n}"
  },
  {
    "path": "packages/vars.less",
    "content": "// 注意：css vars 全部都定义在 packages/editor/src/assets/index.less\n\n@size: 14px;\n\n// textarea - less vars\n@textarea-color: var(--w-e-textarea-color);\n@textarea-bg-color: var(--w-e-textarea-bg-color);\n@textarea-selected-border-color: var(--w-e-textarea-selected-border-color);\n@textarea-slight-color: var(--w-e-textarea-slight-color);\n@textarea-slight-bg-color: var(--w-e-textarea-slight-bg-color);\n@textarea-border-color: var(--w-e-textarea-border-color);\n@textarea-slight-border-color: var( --w-e-textarea-slight-border-color);\n@textarea-handler-bg-color: var(--w-e-textarea-handler-bg-color);\n\n// toolbar - less vars\n@toolbar-color: var(--w-e-toolbar-color);\n@toolbar-bg-color: var(--w-e-toolbar-bg-color);\n@toolbar-active-color: var(--w-e-toolbar-active-color);\n@toolbar-active-bg-color: var(--w-e-toolbar-active-bg-color);\n@toolbar-disabled-color: var(--w-e-toolbar-disabled-color);\n@toolbar-border-color: var(--w-e-toolbar-border-color);\n@toolbar-height: 40px;\n\n// modal - less vars\n@modal-button-bg-color: var(--w-e-modal-button-bg-color);\n@modal-button-border-color: var(--w-e-modal-button-border-color);\n\n// less mixins\n.shadowBordered(@shadowRadius: 5px) {\n  border: 1px solid @toolbar-border-color;\n  border-radius: 3px;\n  box-shadow: 0 2px @shadowRadius #0000001f;\n}\n"
  },
  {
    "path": "packages/video-module/CHANGELOG.md",
    "content": "# Change Log\n\nAll notable changes to this project will be documented in this file.\nSee [Conventional Commits](https://conventionalcommits.org) for commit guidelines.\n\n## [1.1.4](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/video-module@1.1.3...@wangeditor/video-module@1.1.4) (2022-09-27)\n\n**Note:** Version bump only for package @wangeditor/video-module\n\n\n\n\n\n## [1.1.3](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/video-module@1.1.2...@wangeditor/video-module@1.1.3) (2022-09-15)\n\n\n### Bug Fixes\n\n* customInsert 不触发 onSuccess ([d6f4a1b](https://github.com/wangeditor-team/wangEditor/commit/d6f4a1b1494864b116a1310cce2d9e8632c92c6f))\n* 上传视频 - customBrowseAndUpload 缺少 poster ([c24627a](https://github.com/wangeditor-team/wangEditor/commit/c24627aaa4c173c5d435e3077dfe8f6b4a9a87b1))\n\n\n\n\n\n## [1.1.2](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/video-module@1.1.1...@wangeditor/video-module@1.1.2) (2022-08-30)\n\n\n### Bug Fixes\n\n* checkVideo 增加 poster 参数 ([c0402e1](https://github.com/wangeditor-team/wangEditor/commit/c0402e155470233d256e037d863dab74c026b7f6))\n\n\n\n\n\n## [1.1.1](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/video-module@1.1.0...@wangeditor/video-module@1.1.1) (2022-07-14)\n\n\n### Bug Fixes\n\n* video poster（不想升级大版本，所有暂用 fix 不用 feature） ([5a2aff9](https://github.com/wangeditor-team/wangEditor/commit/5a2aff92bc23f240bd249a7294874940cfc9f717))\n\n\n\n\n\n# [1.1.0](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/video-module@1.0.1...@wangeditor/video-module@1.1.0) (2022-05-25)\n\n\n### Features\n\n* editVideoSize ([375eecb](https://github.com/wangeditor-team/wangEditor/commit/375eecba826eac681268c55c47bcd922f7157d63))\n* enter menu ([988fc31](https://github.com/wangeditor-team/wangEditor/commit/988fc31f31de3d37dffbf54abb784cceb8e6118d))\n* setHtml ([f4f91b8](https://github.com/wangeditor-team/wangEditor/commit/f4f91b883298091e3679ca6b206ae0d796003772))\n* 表格拖拽列宽 ([46ea2c0](https://github.com/wangeditor-team/wangEditor/commit/46ea2c0f831b03ebca5fddfd59d682fed0b3476e))\n\n\n\n\n\n## 1.0.1 (2022-04-18)\n\n\n### Bug Fixes\n\n* 部分菜单 disabled ([87f1233](https://github.com/wangeditor-team/wangEditor/commit/87f12332a087072406c1988dc5cef2eae8335375))\n* 插入图片的 < > 替换 ([5721560](https://github.com/wangeditor-team/wangEditor/commit/57215609ada8b9d15f5505d1ba52e49707b5b183))\n* 更新各包之间依赖版本 ([75c552c](https://github.com/wangeditor-team/wangEditor/commit/75c552cc8ed54765bebb86a7ec5329a7fc79e85f))\n* 修复 pnpm 安装 @wangeditor/editor 出现警告的问题 ([4087fbe](https://github.com/wangeditor-team/wangEditor/commit/4087fbee01c76bdd55e747a5e86c5e4a8d6a8353))\n* 修复视频无法被xml-formatter解析的问题 ([e081518](https://github.com/wangeditor-team/wangEditor/commit/e08151863628e0241fe4a3d5858cda4c8ea57949)), closes [#101](https://github.com/wangeditor-team/wangEditor/issues/101) [#95](https://github.com/wangeditor-team/wangEditor/issues/95) [#70](https://github.com/wangeditor-team/wangEditor/issues/70) [#69](https://github.com/wangeditor-team/wangEditor/issues/69)\n* 移除了每个包下的 publishConfig directory 配置 ([16559f0](https://github.com/wangeditor-team/wangEditor/commit/16559f052545c111318be760e64291a521bdcc65))\n* fix插入视频报错的问题 ([f78b06d](https://github.com/wangeditor-team/wangEditor/commit/f78b06d7f75c288f306f04fbfec1dfeb1332a861))\n* fix视频插入iframe时报错的问题 ([ad8f9ce](https://github.com/wangeditor-team/wangEditor/commit/ad8f9cea0f7eae1cb0bc51dba64585be05dfda2f))\n* menu active ([10829e2](https://github.com/wangeditor-team/wangEditor/commit/10829e2e9e1d864d4900821ee3d5fa516b8cca2a))\n* parse html - v4 video ([8dca822](https://github.com/wangeditor-team/wangEditor/commit/8dca822f9f1b52fd71dd6e17f0954d6aa016324b))\n* rename es module filename ([1821d4e](https://github.com/wangeditor-team/wangEditor/commit/1821d4eef49e64efcb41b848849ca7a5e6472044))\n* shadow dom 中 modal 输入框异常 ([ef3b199](https://github.com/wangeditor-team/wangEditor/commit/ef3b199a3e74c6b8ba61ed781e1aa13a1c5acfde))\n* table - elemToHtml ([e36e609](https://github.com/wangeditor-team/wangEditor/commit/e36e6092ef721723169afc8bf0560a47ac9f4dfc))\n* video - 键盘删除 ([5a6bedd](https://github.com/wangeditor-team/wangEditor/commit/5a6bedd80fa0d758270731f62115637ad7f313d0))\n\n\n### Features\n\n* 增加 enable disable API（删除 setConfig setMenuConfig API） ([984fc50](https://github.com/wangeditor-team/wangEditor/commit/984fc50520061fc34ea08f4136bdeb93dee46564))\n* i18n ([c11b244](https://github.com/wangeditor-team/wangEditor/commit/c11b2440f91b99d40bca18b675c66a22b6e160c9))\n* parse html ([2a5eace](https://github.com/wangeditor-team/wangEditor/commit/2a5eace00f33cded50b68e8164748ec2480213fd))\n* parse src (link image video) ([715a841](https://github.com/wangeditor-team/wangEditor/commit/715a841fc6c730ee2b448a1799a07ce778128aad))\n* toHtml 机制 ([1c4d872](https://github.com/wangeditor-team/wangEditor/commit/1c4d8729f84aaab6a448f23064b34a20596305e9))\n* upload video ([ac8e6f8](https://github.com/wangeditor-team/wangEditor/commit/ac8e6f8b5258e593714676a6f6be359ba525833c))\n* video menu config ([7fa3783](https://github.com/wangeditor-team/wangEditor/commit/7fa3783c42aa83f7d53c8be34be3c8b7c8a64754))\n"
  },
  {
    "path": "packages/video-module/README.md",
    "content": "# wangEditor video-module\n\nVideo module built in [wangEditor](https://www.wangeditor.com/) by default.\n"
  },
  {
    "path": "packages/video-module/__tests__/elem-to-html.test.ts",
    "content": "/**\n * @description video menu test\n * @author luochao\n */\n\nimport videoModule from '../src/'\n\nconst videoToHtmlConf = videoModule.elemsToHtml![0]\n\ndescribe('videoModule module', () => {\n  describe('module elem-to-html', () => {\n    test('videoToHtmlConf should return object that include \"type\" and \"elemToHtml\" property', () => {\n      expect(videoToHtmlConf.type).toBe('video')\n      expect(typeof videoToHtmlConf.elemToHtml).toBe('function')\n    })\n\n    test('videoToHtmlConf elemToHtml fn should return html video string', () => {\n      const element = {\n        type: 'video',\n        src: 'test.mp4',\n        poster: 'xxx.png',\n        children: [],\n      }\n      const res = videoToHtmlConf.elemToHtml(element, '')\n\n      expect(res).toEqual(\n        '<div data-w-e-type=\"video\" data-w-e-is-void>\\n<video poster=\"xxx.png\" controls=\"true\" width=\"auto\" height=\"auto\"><source src=\"test.mp4\" type=\"video/mp4\"/></video>\\n</div>'\n      )\n    })\n\n    test('videoToHtmlConf elemToHtml should return original string if src is a iframe html string', () => {\n      const element = {\n        type: 'video',\n        src: '<iframe src=\"test.mp4\"></iframe>',\n        poster: 'xxx.png',\n        width: '500',\n        height: '300',\n        children: [],\n      }\n      const res = videoToHtmlConf.elemToHtml(element, '')\n\n      expect(res).toEqual(\n        '<div data-w-e-type=\"video\" data-w-e-is-void>\\n<iframe src=\"test.mp4\" width=\"500\" height=\"300\"></iframe>\\n</div>'\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "packages/video-module/__tests__/helpler.test.ts",
    "content": "import createEditor from '../../../tests/utils/create-editor'\nimport insertVideo from '../src/module/helper/insert-video'\nimport uploadVideos from '../src/module/helper/upload-videos'\nimport * as slate from 'slate'\nimport nock from 'nock'\n\nconst server = 'https://fake-endpoint.wangeditor-v5.com'\n\nlet editor: ReturnType<typeof createEditor>\ndescribe('Video module helper', () => {\n  beforeEach(() => {\n    editor = createEditor()\n  })\n\n  describe('insert-video helper', () => {\n    test('it should return if give empty src', async () => {\n      expect(await insertVideo(editor, '', '')).toBeUndefined()\n    })\n\n    test('it should alert result if checkVideo return result that data type is string', async () => {\n      const editor = createEditor({\n        config: {\n          MENU_CONF: {\n            insertVideo: {\n              checkVideo: (_src: string, _poster: string) => 'check result',\n            },\n          },\n        },\n      })\n      const fn = jest.fn()\n      editor.alert = fn\n\n      await insertVideo(editor, 'test.mp4', 'xxx.png')\n\n      expect(fn).toBeCalledWith('check result', 'error')\n    })\n\n    test('it should return if checkVideo return null', async () => {\n      const editor = createEditor({\n        config: {\n          MENU_CONF: {\n            insertVideo: {\n              checkVideo: (_src: string, _poster: string) => null,\n            },\n          },\n        },\n      })\n\n      expect(await insertVideo(editor, 'test.mp4', 'xxx.png')).toBeUndefined()\n    })\n\n    test('it should invoke slate insertNodes method if give right src', done => {\n      const fn = jest.fn()\n      jest.spyOn(slate.Transforms, 'insertNodes').mockImplementation(fn)\n\n      insertVideo(editor, 'test.mp4', 'xxx.png').then(() => {\n        setTimeout(() => {\n          expect(fn).toBeCalled()\n          done()\n        })\n      })\n    })\n\n    test('it should invoke onInsertedVideo callback if pass the option when create editor', done => {\n      const fn = jest.fn()\n\n      const editor = createEditor({\n        config: {\n          MENU_CONF: {\n            insertVideo: {\n              onInsertedVideo: fn,\n            },\n          },\n        },\n      })\n\n      insertVideo(editor, 'test.mp4', 'xxx.png').then(() => {\n        expect(fn).toBeCalled()\n        done()\n      })\n    })\n\n    test('it should parse iframe if give iframe element', done => {\n      const fn = jest.fn()\n      jest.spyOn(slate.Transforms, 'insertNodes').mockImplementation(fn)\n\n      insertVideo(editor, '<iframe src=\"test.mp4\"></iframe>').then(() => {\n        setTimeout(() => {\n          expect(fn).toBeCalled()\n          done()\n        })\n      })\n    })\n  })\n\n  describe('upload-video helper', () => {\n    test('it should return if give null', async () => {\n      expect(await uploadVideos(editor, null)).toBeUndefined()\n    })\n\n    test('it should invoke customUpload if give the option when create editor', async () => {\n      const fn = jest.fn()\n      const editor = createEditor({\n        config: {\n          MENU_CONF: {\n            uploadVideo: {\n              customUpload: fn,\n            },\n          },\n        },\n      })\n\n      await uploadVideos(editor, [new File(['123'], 'test.png')] as unknown as FileList)\n\n      expect(fn).toBeCalled()\n    })\n\n    test('it should invoke onSuccess callback if give the option when create editor', async () => {\n      const fn = jest.fn()\n      nock(server)\n        .defaultReplyHeaders({\n          'access-control-allow-method': 'POST',\n          'access-control-allow-origin': '*',\n        })\n        .options('/')\n        .reply(200, {})\n        .post('/')\n        .reply(200, { errno: 0 })\n\n      const editor = createEditor({\n        config: {\n          MENU_CONF: {\n            uploadVideo: {\n              server,\n              onSuccess: fn,\n            },\n          },\n        },\n      })\n\n      await uploadVideos(editor, [new File(['test123'], 'foo.jpg')] as unknown as FileList)\n\n      expect(fn).toBeCalled()\n    })\n\n    test('it should invoke onProgress callback and show progress bar if uploading', async () => {\n      const mockOnProgress = jest.fn()\n      nock(server)\n        .defaultReplyHeaders({\n          'access-control-allow-method': 'POST',\n          'access-control-allow-origin': '*',\n        })\n        .options('/')\n        .reply(200, {})\n        .post('/')\n        .reply(200, { errno: 0 })\n\n      const editor = createEditor({\n        config: {\n          MENU_CONF: {\n            uploadVideo: {\n              server,\n              onProgress: mockOnProgress,\n            },\n          },\n        },\n      })\n\n      const mockShowProgressBar = jest.fn()\n      editor.showProgressBar = mockShowProgressBar\n\n      await uploadVideos(editor, [new File(['test123'], 'foo.jpg')] as unknown as FileList)\n\n      expect(mockOnProgress).toBeCalled()\n      expect(mockShowProgressBar).toBeCalled()\n    })\n\n    test('it should invoke onError callback if upload failed', () => {\n      const fn = jest.fn()\n      nock(server)\n        .defaultReplyHeaders({\n          'access-control-allow-method': 'POST',\n          'access-control-allow-origin': '*',\n        })\n        .options('/')\n        .reply(200, {})\n        .post('/')\n        .reply(400, {})\n\n      const editor = createEditor({\n        config: {\n          MENU_CONF: {\n            uploadVideo: {\n              server,\n              onError: fn,\n            },\n          },\n        },\n      })\n\n      uploadVideos(editor, [new File(['test123'], 'foo.jpg')] as unknown as FileList).catch(() => {\n        expect(fn).toBeCalled()\n      })\n    })\n\n    test('it should invoke onFail callback if upload result with error', async () => {\n      const fn = jest.fn()\n      nock(server)\n        .defaultReplyHeaders({\n          'access-control-allow-method': 'POST',\n          'access-control-allow-origin': '*',\n        })\n        .options('/')\n        .reply(200, {})\n        .post('/')\n        .reply(200, { error: 1 })\n\n      const editor = createEditor({\n        config: {\n          MENU_CONF: {\n            uploadVideo: {\n              server,\n              onFailed: fn,\n            },\n          },\n        },\n      })\n\n      await uploadVideos(editor, [new File(['test123'], 'foo.jpg')] as unknown as FileList)\n\n      expect(fn).toBeCalled()\n    })\n\n    test('it should invoke customInsert callback if upload successfully', async () => {\n      const fn = jest.fn()\n      nock(server)\n        .defaultReplyHeaders({\n          'access-control-allow-method': 'POST',\n          'access-control-allow-origin': '*',\n        })\n        .options('/')\n        .reply(200, {})\n        .post('/')\n        .reply(200, { error: 0 })\n\n      const editor = createEditor({\n        config: {\n          MENU_CONF: {\n            uploadVideo: {\n              server,\n              customInsert: fn,\n            },\n          },\n        },\n      })\n\n      await uploadVideos(editor, [new File(['test123'], 'foo.jpg')] as unknown as FileList)\n\n      expect(fn).toBeCalled()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/video-module/__tests__/menu/delete-video-menu.test.ts.bak",
    "content": "/**\n * @description video menu test\n * @author luochao\n */\n\nimport createEditor from '../../../../tests/utils/create-editor'\nimport DeleteVideoMenu from '../../src/module/menu'\nimport * as core from '@wangeditor/core'\nimport * as slate from 'slate'\n\nfunction setEditorSelection(\n  editor: core.IDomEditor,\n  selection: slate.Selection = {\n    anchor: { path: [0, 0], offset: 0 },\n    focus: { path: [0, 0], offset: 0 },\n  }\n) {\n  editor.selection = selection\n}\n\ndescribe('videoModule module', () => {\n  describe('module DeleteVideoMenu', () => {\n    const deleteVideoMenu = new DeleteVideoMenu()\n    const editor = createEditor()\n\n    test('DeleteVideoMenu invoke getValue function should be empty string', () => {\n      expect(deleteVideoMenu.getValue(editor)).toBe('')\n    })\n\n    test('DeleteVideoMenu invoke isActive function should be false', () => {\n      expect(deleteVideoMenu.isActive(editor)).toBe(false)\n    })\n\n    test('DeleteVideoMenu invoke isDisabled function if editor selected video element should be false', () => {\n      jest\n        .spyOn(core.DomEditor, 'getSelectedNodeByType')\n        .mockReturnValue({ type: 'video', children: [{ text: '' }] } as any)\n\n      setEditorSelection(editor)\n\n      expect(deleteVideoMenu.isDisabled(editor)).toBe(false)\n    })\n\n    test('DeleteVideoMenu invoke isDisabled function if editor do not selected video element should be true', () => {\n      jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockReturnValue(null)\n\n      setEditorSelection(editor)\n\n      expect(deleteVideoMenu.isDisabled(editor)).toBe(true)\n    })\n\n    test('DeleteVideoMenu invoke exec function if video menu is disabled should return directly', () => {\n      jest.spyOn(core.DomEditor, 'getSelectedNodeByType').mockReturnValue(null)\n      const fn = jest.spyOn(slate.Transforms, 'removeNodes')\n\n      setEditorSelection(editor)\n\n      deleteVideoMenu.exec(editor, '')\n\n      expect(fn).not.toBeCalled()\n    })\n\n    test('DeleteVideoMenu invoke exec function if video menu is disabled should execute transform removeNodes', () => {\n      jest\n        .spyOn(core.DomEditor, 'getSelectedNodeByType')\n        .mockReturnValue({ type: 'video', children: [{ text: '' }] } as any)\n\n      const fn = jest.spyOn(slate.Transforms, 'removeNodes')\n\n      setEditorSelection(editor)\n\n      deleteVideoMenu.exec(editor, '')\n\n      expect(fn).toBeCalled()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/video-module/__tests__/menu/insert-video-menu.test.ts",
    "content": "/**\n * @description video menu test\n * @author luochao\n */\n\nimport createEditor from '../../../../tests/utils/create-editor'\nimport InsertVideoMenu from '../../src/module/menu/InsertVideoMenu'\nimport * as core from '@wangeditor/core'\nimport * as slate from 'slate'\n\nfunction setEditorSelection(\n  editor: core.IDomEditor,\n  selection: slate.Selection = {\n    anchor: { path: [0, 0], offset: 0 },\n    focus: { path: [0, 0], offset: 0 },\n  }\n) {\n  editor.selection = selection\n}\n\ndescribe('videoModule module', () => {\n  describe('module InsertVideoMenu', () => {\n    const insertVideoMenu = new InsertVideoMenu()\n    const editor = createEditor()\n\n    test('InsertVideoMenu invoke getValue function should be empty string', () => {\n      expect(insertVideoMenu.getValue(editor)).toBe('')\n    })\n\n    test('InsertVideoMenu invoke isActive function should be false', () => {\n      expect(insertVideoMenu.isActive(editor)).toBe(false)\n    })\n\n    test('InsertVideoMenu invoke isDisabled if editor selection is null that the function return true', () => {\n      setEditorSelection(editor, null)\n      expect(insertVideoMenu.isDisabled(editor)).toBe(true)\n    })\n\n    test('InsertVideoMenu invoke isDisabled if editor selection is not collapsed that the function return true', () => {\n      setEditorSelection(editor)\n\n      jest.spyOn(slate.Range, 'isCollapsed').mockReturnValue(false)\n      expect(insertVideoMenu.isDisabled(editor)).toBe(true)\n    })\n\n    test('InsertVideoMenu invoke isDisabled if editor selection is not null and collapsed that the function return false', () => {\n      setEditorSelection(editor)\n\n      jest.spyOn(slate.Range, 'isCollapsed').mockReturnValue(true)\n      expect(insertVideoMenu.isDisabled(editor)).toBe(false)\n    })\n\n    test('InsertVideoMenu invoke getModalPositionNode should return null', () => {\n      expect(insertVideoMenu.getModalPositionNode(editor)).toBeNull()\n    })\n\n    test('InsertVideoMenu invoke getModalContentElem should return HTML element', () => {\n      expect(insertVideoMenu.getModalContentElem(editor) instanceof HTMLElement).toBe(true)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/video-module/__tests__/menu/upload-video-menu.test.ts",
    "content": "/**\n * @description video menu test\n * @author luochao\n */\n\nimport createEditor from '../../../../tests/utils/create-editor'\nimport UploadVideoMenu from '../../src/module/menu/UploadVideoMenu'\nimport * as core from '@wangeditor/core'\nimport * as slate from 'slate'\nimport $ from '../../src/utils/dom'\n\nfunction setEditorSelection(\n  editor: core.IDomEditor,\n  selection: slate.Selection = {\n    anchor: { path: [0, 0], offset: 0 },\n    focus: { path: [0, 0], offset: 0 },\n  }\n) {\n  editor.selection = selection\n}\n\ndescribe('videoModule module', () => {\n  describe('module UploadVideoMenu', () => {\n    const uploadVideoMenu = new UploadVideoMenu()\n    const editor = createEditor()\n\n    test('UploadVideoMenu invoke getValue function should be empty string', () => {\n      expect(uploadVideoMenu.getValue(editor)).toBe('')\n    })\n\n    test('UploadVideoMenu invoke isActive function should be false', () => {\n      expect(uploadVideoMenu.isActive(editor)).toBe(false)\n    })\n\n    test('UploadVideoMenu invoke isDisabled if editor selection is null that the function return true', () => {\n      setEditorSelection(editor, null)\n      expect(uploadVideoMenu.isDisabled(editor)).toBe(true)\n    })\n\n    test('UploadVideoMenu invoke isDisabled if editor selection is not collapsed that the function return true', () => {\n      setEditorSelection(editor)\n\n      jest.spyOn(slate.Range, 'isCollapsed').mockReturnValue(false)\n      expect(uploadVideoMenu.isDisabled(editor)).toBe(true)\n    })\n\n    test('UploadVideoMenu invoke isDisabled if editor selection is not null and collapsed that the function return false', () => {\n      setEditorSelection(editor)\n\n      jest.spyOn(slate.Range, 'isCollapsed').mockReturnValue(true)\n      expect(uploadVideoMenu.isDisabled(editor)).toBe(false)\n    })\n\n    test('UploadVideoMenu invoke customBrowseAndUpload if editor give customBrowseAndUpload option', () => {\n      const fn = jest.fn()\n      const editor = createEditor({\n        config: {\n          MENU_CONF: {\n            uploadVideo: {\n              customBrowseAndUpload: fn,\n            },\n          },\n        },\n      })\n\n      uploadVideoMenu.exec(editor, '')\n\n      expect(fn).toBeCalled()\n    })\n\n    test('it should insert input element to body if invoke exec method', () => {\n      const editor = createEditor()\n\n      expect($('input').length).toBe(0)\n\n      uploadVideoMenu.exec(editor, '')\n\n      expect($('input').length).toBeGreaterThan(0)\n    })\n\n    test('it should insert input element with accept attr if editor config allowedFileTypes', () => {\n      const editor = createEditor({\n        config: {\n          MENU_CONF: {\n            uploadVideo: {\n              allowedFileTypes: ['video/*'],\n            },\n          },\n        },\n      })\n\n      uploadVideoMenu.exec(editor, '')\n\n      expect($('input')[0].getAttribute('accept')).toBe('video/*')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/video-module/__tests__/parse-html.test.ts",
    "content": "/**\n * @description parse html test\n * @author wangfupeng\n */\n\nimport { $ } from 'dom7'\nimport createEditor from '../../../tests/utils/create-editor'\nimport videoModule from '../src'\n\nconst { parseElemsHtml, preParseHtml } = videoModule\nconst [parseHtmlConf] = parseElemsHtml!\nconst [preParseHtmlConf] = preParseHtml!\n\ndescribe('video - pre parse html', () => {\n  it('iframe', () => {\n    const $iframe = $('<iframe></iframe>')\n\n    // match selector\n    expect($iframe[0].matches(preParseHtmlConf.selector)).toBeTruthy()\n\n    // pre parse\n    const res = preParseHtmlConf.preParseHtml($iframe[0])\n    expect(res.outerHTML).toBe(\n      '<div data-w-e-type=\"video\" data-w-e-is-void=\"\"><iframe></iframe></div>'\n    )\n  })\n\n  it('video', () => {\n    const $video = $('<video></video>')\n\n    // match selector\n    expect($video[0].matches(preParseHtmlConf.selector)).toBeTruthy()\n\n    // pre parse\n    const res = preParseHtmlConf.preParseHtml($video[0])\n    expect(res.outerHTML).toBe(\n      '<div data-w-e-type=\"video\" data-w-e-is-void=\"\"><video></video></div>'\n    )\n  })\n\n  it('it should parse video element which is wrapped by p', () => {\n    const $video = $('<p><video></video></p>')\n\n    // match selector\n    expect($video[0].matches(preParseHtmlConf.selector)).toBeTruthy()\n\n    // pre parse\n    const res = preParseHtmlConf.preParseHtml($video[0])\n    expect(res.outerHTML).toBe(\n      '<div data-w-e-type=\"video\" data-w-e-is-void=\"\"><video></video></div>'\n    )\n  })\n})\n\ndescribe('video - parse html', () => {\n  const editor = createEditor()\n\n  it('iframe', () => {\n    const iframeHtml = '<iframe src=\"xxx\" width=\"500\" height=\"300\"></iframe>'\n    const $container = $(`<div data-w-e-type=\"video\" data-w-e-is-void>${iframeHtml}</div>`)\n\n    // match selector\n    expect($container[0].matches(parseHtmlConf.selector)).toBeTruthy()\n\n    // parse\n    expect(parseHtmlConf.parseElemHtml($container[0], [], editor)).toEqual({\n      type: 'video',\n      src: iframeHtml,\n      poster: '',\n      width: '500',\n      height: '300',\n      children: [{ text: '' }], // void 元素有一个空 text\n    })\n  })\n\n  it('video', () => {\n    const src = 'xxx.mp4'\n    const poster = 'xxx.png'\n    const videoHtml = `<video poster=\"${poster}\"><source src=\"${src}\"/></video>`\n    const $container = $(`<div data-w-e-type=\"video\" data-w-e-is-void>${videoHtml}</div>`)\n\n    // match selector\n    expect($container[0].matches(parseHtmlConf.selector)).toBeTruthy()\n\n    // parse\n    expect(parseHtmlConf.parseElemHtml($container[0], [], editor)).toEqual({\n      type: 'video',\n      src,\n      poster,\n      width: 'auto',\n      height: 'auto',\n      children: [{ text: '' }], // void 元素有一个空 text\n    })\n  })\n})\n"
  },
  {
    "path": "packages/video-module/__tests__/plugin.test.ts",
    "content": "/**\n * @description video menu test\n * @author luochao\n */\n\nimport withVideo from '../src/module/plugin'\nimport createEditor from '../../../tests/utils/create-editor'\n\ndescribe('videoModule module', () => {\n  describe('module plugin', () => {\n    test('withVideo should override editor \"isVoid\" and \"normalizeNode\" methods', () => {\n      const editor = createEditor()\n      const originalIsVoidFn = editor.isVoid\n      const originalNormalizeNode = editor.normalizeNode\n\n      const newEditor = withVideo(editor)\n\n      expect(originalIsVoidFn).not.toEqual(newEditor.isVoid)\n      expect(originalNormalizeNode).not.toEqual(newEditor.normalizeNode)\n    })\n\n    test('使用 withVideo 插件后，Editor 会将 Video 元素视为 void 元素', () => {\n      const editor = createEditor()\n      const newEditor = withVideo(editor)\n      const videoElem = {\n        type: 'video',\n        src: 'test.mp4',\n        children: [],\n      }\n      expect(newEditor.isVoid(videoElem)).toBeTruthy()\n    })\n\n    test('使用 withVideo 插件后，对于非 video 元素，直接调用 original isVoid 方法', () => {\n      const editor = createEditor()\n      const fn = jest.fn()\n      editor.isVoid = fn\n\n      const newEditor = withVideo(editor)\n      const videoElem = {\n        type: 'paragraph',\n        children: [{ text: '' }],\n      }\n      newEditor.isVoid(videoElem)\n\n      expect(fn).toBeCalled()\n    })\n\n    test('使用 withVideo 插件后，Editor 调用 normalizeNode 方法确保 Video 元素后面有 paragraph、block、header 等元素', () => {\n      const videoElem = {\n        type: 'video',\n        src: 'test.mp4',\n        children: [],\n      }\n      const editor = createEditor({\n        content: [videoElem],\n      })\n      const newEditor = withVideo(editor)\n\n      newEditor.normalizeNode([videoElem, [0]])\n      expect(newEditor.children).toEqual([\n        {\n          type: 'video',\n          src: 'test.mp4',\n          children: [{ text: '' }],\n        },\n        { type: 'paragraph', children: [{ text: '' }] },\n      ])\n    })\n  })\n})\n"
  },
  {
    "path": "packages/video-module/__tests__/render-elem.test.ts",
    "content": "/**\n * @description video render elem test\n * @author luochao\n */\n\nimport createEditor from '../../../tests/utils/create-editor'\nimport { renderVideoConf } from '../src/module/render-elem'\n\ndescribe('video module - render elem', () => {\n  const editor = createEditor()\n\n  it('render video elem', () => {\n    expect(renderVideoConf.type).toBe('video')\n\n    const elem = { type: 'video', src: 'test.mp4', poster: 'xxx.png', children: [] }\n    const vnode = renderVideoConf.renderElem(elem, null, editor)\n    expect(vnode.sel).toBe('div')\n  })\n\n  it('render video with iframe', () => {\n    expect(renderVideoConf.type).toBe('video')\n\n    const elem = { type: 'video', src: '<iframe src=\"test.mp4\"></iframe>', children: [] }\n    const vnode = renderVideoConf.renderElem(elem, null, editor)\n    expect(vnode.sel).toBe('div')\n  })\n})\n"
  },
  {
    "path": "packages/video-module/__tests__/util.test.ts",
    "content": "/**\n * @description video menu test\n * @author luochao\n */\n\nimport { genRandomStr } from '../src/utils/util'\n\ndescribe('videoModule util', () => {\n  describe('utils util', () => {\n    test('genRandomStr should generate a random string every time', () => {\n      const str1 = genRandomStr()\n      const str2 = genRandomStr()\n\n      expect(str1).not.toBe(str2)\n    })\n\n    test('genRandomStr should generate a random string that specify a prefix string', () => {\n      const str = genRandomStr('wangeditor')\n\n      expect(str.indexOf('wangeditor-')).toEqual(0)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/video-module/package.json",
    "content": "{\n  \"name\": \"@wangeditor/video-module\",\n  \"version\": \"1.1.4\",\n  \"description\": \"wangEditor video module\",\n  \"author\": \"wangfupeng1988 <wangfupeng1988@163.com>\",\n  \"contributors\": [],\n  \"homepage\": \"https://github.com/wangeditor-team/wangEditor#readme\",\n  \"license\": \"MIT\",\n  \"types\": \"dist/video-module/src/index.d.ts\",\n  \"main\": \"dist/index.js\",\n  \"module\": \"dist/index.esm.js\",\n  \"browser\": {\n    \"./dist/index.js\": \"./dist/index.js\",\n    \"./dist/index.esm.js\": \"./dist/index.esm.js\"\n  },\n  \"directories\": {\n    \"lib\": \"dist\",\n    \"test\": \"__tests__\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.com/\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wangeditor-team/wangEditor.git\"\n  },\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"test-c\": \"jest --coverage\",\n    \"dev\": \"cross-env NODE_ENV=development rollup -c rollup.config.js\",\n    \"dev-watch\": \"cross-env NODE_ENV=development rollup -c rollup.config.js -w\",\n    \"build\": \"cross-env NODE_ENV=production rollup -c rollup.config.js\",\n    \"dev-size-stats\": \"cross-env NODE_ENV=development:size_stats rollup -c rollup.config.js\",\n    \"size-stats\": \"cross-env NODE_ENV=production:size_stats rollup -c rollup.config.js\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/wangeditor-team/wangEditor/issues\"\n  },\n  \"peerDependencies\": {\n    \"@uppy/core\": \"^2.1.4\",\n    \"@uppy/xhr-upload\": \"^2.0.7\",\n    \"@wangeditor/core\": \"1.x\",\n    \"dom7\": \"^3.0.0\",\n    \"nanoid\": \"^3.2.0\",\n    \"slate\": \"^0.72.0\",\n    \"snabbdom\": \"^3.1.0\"\n  }\n}\n"
  },
  {
    "path": "packages/video-module/rollup.config.js",
    "content": "import { createRollupConfig, IS_PRD } from '../../build/create-rollup-config'\nimport pkg from './package.json'\n\nconst name = 'WangEditorVideoModule'\n\nconst configList = []\n\n// esm\nconst esmConf = createRollupConfig({\n  output: {\n    file: pkg.module,\n    format: 'esm',\n    name,\n  },\n})\nconfigList.push(esmConf)\n\n// umd\nconst umdConf = createRollupConfig({\n  output: {\n    file: pkg.main,\n    format: 'umd',\n    name,\n  },\n})\nconfigList.push(umdConf)\n\nexport default configList\n"
  },
  {
    "path": "packages/video-module/src/assets/index.less",
    "content": "@import \"../../../vars.less\";\n\n.w-e-textarea-video-container {\n  text-align: center;\n  border: 1px dashed @textarea-border-color;\n  padding: 10px 0;\n  margin: 0 auto;\n  margin-top: 10px;\n  border-radius: 5px;\n  background-position: 0px 0px, 10px 10px;\n  background-size: 20px 20px;\n  background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee 100%),linear-gradient(45deg, #eee 25%, white 25%, white 75%, #eee 75%, #eee 100%);\n}\n"
  },
  {
    "path": "packages/video-module/src/constants/svg.ts",
    "content": "/**\n * @description icon svg\n * @author wangfupeng\n */\n\n/**\n * 【注意】svg 字符串的长度 ，否则会导致代码体积过大\n * 尽量选择 https://www.iconfont.cn/collections/detail?spm=a313x.7781069.0.da5a778a4&cid=20293\n * 找不到再从 iconfont.com 搜索\n */\n\n// 视频\nexport const VIDEO_SVG =\n  '<svg viewBox=\"0 0 1024 1024\"><path d=\"M981.184 160.096C837.568 139.456 678.848 128 512 128S186.432 139.456 42.816 160.096C15.296 267.808 0 386.848 0 512s15.264 244.16 42.816 351.904C186.464 884.544 345.152 896 512 896s325.568-11.456 469.184-32.096C1008.704 756.192 1024 637.152 1024 512s-15.264-244.16-42.816-351.904zM384 704V320l320 192-320 192z\"></path></svg>'\n\n// 上传视频\nexport const UPLOAD_VIDEO_SVG =\n  '<svg viewBox=\"0 0 1056 1024\"><path d=\"M805.902261 521.819882a251.441452 251.441452 0 0 0-251.011972 246.600033 251.051015 251.051015 0 1 0 502.023944 8.823877 253.237463 253.237463 0 0 0-251.011972-255.42391z m59.463561 240.001647v129.898403h-116.701631v-129.898403h-44.041298l101.279368-103.504859 101.279368 103.504859z\" p-id=\"6802\"></path><path d=\"M788.254507 0.000781H99.094092A98.663439 98.663439 0 0 0 0.001171 99.093701v590.067495a98.663439 98.663439 0 0 0 99.092921 99.092921h411.7549a266.434235 266.434235 0 0 1-2.186448-41.815807 275.843767 275.843767 0 0 1 275.180024-270.729042 270.650955 270.650955 0 0 1 103.504859 19.834201V99.093701A101.51363 101.51363 0 0 0 788.254507 0.000781zM295.054441 640.747004V147.507894l394.146189 246.600033z\"></path></svg>'\n\n// // 垃圾桶（删除）\n// export const TRASH_SVG =\n//   '<svg viewBox=\"0 0 1024 1024\"><path d=\"M826.8032 356.5312c-19.328 0-36.3776 15.6928-36.3776 35.0464v524.2624c0 19.328-16 34.56-35.328 34.56H264.9344c-19.328 0-35.5072-15.3088-35.5072-34.56V390.0416c0-19.328-14.1568-35.0464-33.5104-35.0464s-33.5104 15.6928-33.5104 35.0464V915.712c0 57.9328 44.6208 108.288 102.528 108.288H755.2c57.9328 0 108.0832-50.4576 108.0832-108.288V391.4752c-0.1024-19.2512-17.1264-34.944-36.48-34.944z\" p-id=\"9577\"></path><path d=\"M437.1712 775.7568V390.6048c0-19.328-14.1568-35.0464-33.5104-35.0464s-33.5104 15.616-33.5104 35.0464v385.152c0 19.328 14.1568 35.0464 33.5104 35.0464s33.5104-15.7184 33.5104-35.0464zM649.7024 775.7568V390.6048c0-19.328-17.0496-35.0464-36.3776-35.0464s-36.3776 15.616-36.3776 35.0464v385.152c0 19.328 17.0496 35.0464 36.3776 35.0464s36.3776-15.7184 36.3776-35.0464zM965.0432 217.0368h-174.6176V145.5104c0-57.9328-47.2064-101.76-104.6528-101.76h-350.976c-57.8304 0-105.3952 43.8528-105.3952 101.76v71.5264H54.784c-19.4304 0-35.0464 14.1568-35.0464 33.5104 0 19.328 15.616 33.5104 35.0464 33.5104h910.3616c19.328 0 35.0464-14.1568 35.0464-33.5104 0-19.3536-15.6928-33.5104-35.1488-33.5104z m-247.3728 0H297.3952V145.5104c0-19.328 18.2016-34.7648 37.4272-34.7648h350.976c19.1488 0 31.872 15.1296 31.872 34.7648v71.5264z\"></path></svg>'\n"
  },
  {
    "path": "packages/video-module/src/index.ts",
    "content": "/**\n * @description video module\n * @author wangfupeng\n */\n\nimport './assets/index.less'\n\n// 配置多语言\nimport './locale/index'\n\nimport wangEditorVideoModule from './module/index'\nexport default wangEditorVideoModule\n"
  },
  {
    "path": "packages/video-module/src/locale/en.ts",
    "content": "/**\n * @description i18n en\n * @author wangfupeng\n */\n\nexport default {\n  videoModule: {\n    delete: 'Delete',\n    uploadVideo: 'Upload video',\n    insertVideo: 'Insert video',\n    videoSrc: 'Video source',\n    videoSrcPlaceHolder: 'Video file url, or third-party <iframe>',\n    videoPoster: 'Video poster',\n    videoPosterPlaceHolder: 'Poster image url',\n    ok: 'Ok',\n    editSize: 'Edit size',\n    width: 'Width',\n    height: 'Height',\n  },\n}\n"
  },
  {
    "path": "packages/video-module/src/locale/index.ts",
    "content": "/**\n * @description i18n entry\n * @author wangfupeng\n */\n\nimport { i18nAddResources } from '@wangeditor/core'\nimport enResources from './en'\nimport zhResources from './zh-CN'\n\ni18nAddResources('en', enResources)\ni18nAddResources('zh-CN', zhResources)\n"
  },
  {
    "path": "packages/video-module/src/locale/zh-CN.ts",
    "content": "/**\n * @description i18n zh-CN\n * @author wangfupeng\n */\n\nexport default {\n  videoModule: {\n    delete: '删除视频',\n    uploadVideo: '上传视频',\n    insertVideo: '插入视频',\n    videoSrc: '视频地址',\n    videoSrcPlaceHolder: '视频文件 url 或第三方 <iframe>',\n    videoPoster: '视频封面',\n    videoPosterPlaceHolder: '封面图片 url',\n    ok: '确定',\n    editSize: '修改尺寸',\n    width: '宽度',\n    height: '高度',\n  },\n}\n"
  },
  {
    "path": "packages/video-module/src/module/custom-types.ts",
    "content": "/**\n * @description video element\n * @author wangfupeng\n */\n\n//【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts\n\ntype EmptyText = {\n  text: ''\n}\n\nexport type VideoElement = {\n  type: 'video'\n  src: string\n  poster?: string\n  width?: string\n  height?: string\n  children: EmptyText[]\n}\n"
  },
  {
    "path": "packages/video-module/src/module/elem-to-html.ts",
    "content": "/**\n * @description to html\n * @author wangfupeng\n */\n\nimport { Element } from 'slate'\nimport { VideoElement } from './custom-types'\nimport { genSizeStyledIframeHtml } from '../utils/dom'\n\nfunction videoToHtml(elemNode: Element, childrenHtml?: string): string {\n  const { src = '', poster = '', width = 'auto', height = 'auto' } = elemNode as VideoElement\n  let res = '<div data-w-e-type=\"video\" data-w-e-is-void>\\n'\n\n  if (src.trim().indexOf('<iframe ') === 0) {\n    // iframe 形式\n    const iframeHtml = genSizeStyledIframeHtml(src, width, height)\n    res += iframeHtml\n  } else {\n    // 其他，mp4 等 url 格式\n    res += `<video poster=\"${poster}\" controls=\"true\" width=\"${width}\" height=\"${height}\"><source src=\"${src}\" type=\"video/mp4\"/></video>`\n  }\n  res += '\\n</div>'\n\n  return res\n}\n\nexport const videoToHtmlConf = {\n  type: 'video',\n  elemToHtml: videoToHtml,\n}\n"
  },
  {
    "path": "packages/video-module/src/module/helper/insert-video.ts",
    "content": "/**\n * @description insert video\n * @author wangfupeng\n */\n\nimport { Transforms } from 'slate'\nimport { IDomEditor } from '@wangeditor/core'\nimport { replaceSymbols } from '../../utils/util'\nimport { VideoElement } from '../custom-types'\n\n/**\n * 插入视频\n * @param editor editor\n * @param src video src\n * @param poster video poster\n */\nexport default async function (editor: IDomEditor, src: string, poster = '') {\n  if (!src) return\n\n  // 还原选区\n  editor.restoreSelection()\n\n  // 校验\n  const { onInsertedVideo, checkVideo, parseVideoSrc } = editor.getMenuConfig('insertVideo')\n  const checkRes = await checkVideo(src, poster)\n  if (typeof checkRes === 'string') {\n    // 校验失败，给出提示\n    editor.alert(checkRes, 'error')\n    return\n  }\n  if (checkRes == null) {\n    // 校验失败，不给提示\n    return\n  }\n\n  // 转换 src\n  let parsedSrc = await parseVideoSrc(src)\n\n  if (parsedSrc.trim().indexOf('<iframe ') !== 0) {\n    parsedSrc = replaceSymbols(parsedSrc)\n  }\n\n  // 新建一个 video node\n  const video: VideoElement = {\n    type: 'video',\n    src: parsedSrc,\n    poster,\n    children: [{ text: '' }], // 【注意】void node 需要一个空 text 作为 children\n  }\n\n  // 插入视频\n  // 不使用此方式会比正常的选区选取先执行\n  Promise.resolve().then(() => {\n    Transforms.insertNodes(editor, video)\n  })\n\n  // 调用 callback\n  onInsertedVideo(video)\n}\n"
  },
  {
    "path": "packages/video-module/src/module/helper/upload-videos.ts",
    "content": "/**\n * @description upload video\n * @author wangfupeng\n */\n\nimport Uppy, { UppyFile } from '@uppy/core'\nimport { IDomEditor, createUploader } from '@wangeditor/core'\nimport insertVideo from './insert-video'\nimport { IUploadConfigForVideo } from '../menu/config'\n\nfunction getMenuConfig(editor: IDomEditor): IUploadConfigForVideo {\n  // 获取配置，见 `./config.js`\n  return editor.getMenuConfig('uploadVideo') as IUploadConfigForVideo\n}\n\n// 存储 editor uppy 的关系 - 缓存 uppy ，不重复创建\nconst EDITOR_TO_UPPY_MAP = new WeakMap<IDomEditor, Uppy>()\n\n/**\n * 获取 uppy 实例（并通过 editor 缓存）\n * @param editor editor\n */\nfunction getUppy(editor: IDomEditor): Uppy {\n  // 从缓存中获取\n  let uppy = EDITOR_TO_UPPY_MAP.get(editor)\n  if (uppy != null) return uppy\n\n  const menuConfig = getMenuConfig(editor)\n  const { onSuccess, onProgress, onFailed, customInsert, onError } = menuConfig\n\n  // 上传完成之后\n  const successHandler = (file: UppyFile, res: any) => {\n    // 预期 res 格式：\n    // 成功：{ errno: 0, data: { url, poster } }\n    // 失败：{ errno: !0, message: '失败信息' }\n\n    if (customInsert) {\n      // 用户自定义插入视频，此时 res 格式可能不符合预期\n      customInsert(res, (src, poster) => insertVideo(editor, src, poster))\n      // success 回调\n      onSuccess(file, res)\n      return\n    }\n\n    let { errno = 1, data = {} } = res\n    if (errno !== 0) {\n      // failed 回调\n      onFailed(file, res)\n      return\n    }\n\n    const { url = '', poster = '' } = data\n    insertVideo(editor, url, poster)\n\n    // success 回调\n    onSuccess(file, res)\n  }\n\n  // progress 显示进度条\n  const progressHandler = (progress: number) => {\n    editor.showProgressBar(progress)\n\n    // 回调函数\n    onProgress && onProgress(progress)\n  }\n\n  // onError 提示错误\n  const errorHandler = (file: any, err: any, res: any) => {\n    onError(file, err, res)\n  }\n\n  // 创建 uppy\n  uppy = createUploader({\n    ...menuConfig,\n    onProgress: progressHandler,\n    onSuccess: successHandler,\n    onError: errorHandler,\n  })\n  // 缓存 uppy\n  EDITOR_TO_UPPY_MAP.set(editor, uppy)\n\n  return uppy\n}\n\n/**\n * 上传视频文件\n * @param editor editor\n * @param file file\n */\nasync function uploadFile(editor: IDomEditor, file: File) {\n  const uppy = getUppy(editor)\n\n  const { name, type, size } = file\n  uppy.addFile({\n    name,\n    type,\n    size,\n    data: file,\n  })\n  await uppy.upload()\n}\n\nexport default async function (editor: IDomEditor, files: FileList | null) {\n  if (files == null) return\n  const fileList = Array.prototype.slice.call(files)\n\n  // 获取菜单配置\n  const { customUpload } = getMenuConfig(editor)\n\n  // 按顺序上传\n  for await (const file of fileList) {\n    // 上传\n    if (customUpload) {\n      // 自定义上传\n      await customUpload(file, (src, poster) => insertVideo(editor, src, poster))\n    } else {\n      // 默认上传\n      await uploadFile(editor, file)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/video-module/src/module/index.ts",
    "content": "/**\n * @description video module\n * @author wangfupeng\n */\n\nimport { IModuleConf } from '@wangeditor/core'\nimport withVideo from './plugin'\nimport { renderVideoConf } from './render-elem'\nimport { videoToHtmlConf } from './elem-to-html'\nimport { preParseHtmlConf } from './pre-parse-html'\nimport { parseHtmlConf } from './parse-elem-html'\nimport { insertVideoMenuConf, uploadVideoMenuConf, editorVideSizeMenuConf } from './menu/index'\n\nconst video: Partial<IModuleConf> = {\n  renderElems: [renderVideoConf],\n  elemsToHtml: [videoToHtmlConf],\n  preParseHtml: [preParseHtmlConf],\n  parseElemsHtml: [parseHtmlConf],\n  menus: [insertVideoMenuConf, uploadVideoMenuConf, editorVideSizeMenuConf],\n  editorPlugin: withVideo,\n}\n\nexport default video\n"
  },
  {
    "path": "packages/video-module/src/module/menu/DeleteVideoMenu.ts.bak",
    "content": "/**\n * @description delete video menu\n * @author wangfupeng\n */\n\nimport { Transforms } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport { TRASH_SVG } from '../../constants/svg'\n\nclass DeleteVideoMenu implements IButtonMenu {\n  readonly title = t('videoModule.delete')\n  readonly iconSvg = TRASH_SVG\n  readonly tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 无需获取 val\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 无需 active\n    return false\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n\n    const videoNode = DomEditor.getSelectedNodeByType(editor, 'video')\n    if (videoNode == null) {\n      // 选区未处于 video node ，则禁用\n      return true\n    }\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    if (this.isDisabled(editor)) return\n\n    // 删除视频\n    Transforms.removeNodes(editor, {\n      match: n => DomEditor.checkNodeType(n, 'video'),\n    })\n  }\n}\n\nexport default DeleteVideoMenu\n"
  },
  {
    "path": "packages/video-module/src/module/menu/EditVideoSizeMenu.ts",
    "content": "/**\n * @description 修改视频尺寸\n * @author wangfupeng\n */\n\nimport { Node as SlateNode, Transforms } from 'slate'\nimport {\n  IModalMenu,\n  IDomEditor,\n  DomEditor,\n  genModalInputElems,\n  genModalButtonElems,\n  t,\n} from '@wangeditor/core'\nimport $, { Dom7Array, DOMElement } from '../../utils/dom'\nimport { genRandomStr } from '../../utils/util'\nimport { VideoElement } from '../custom-types'\n\n/**\n * 生成唯一的 DOM ID\n */\nfunction genDomID(): string {\n  return genRandomStr('w-e-insert-video')\n}\n\nclass EditorVideoSizeMenu implements IModalMenu {\n  readonly title = t('videoModule.editSize')\n  readonly tag = 'button'\n  readonly showModal = true // 点击 button 时显示 modal\n  readonly modalWidth = 320\n  private $content: Dom7Array | null = null\n  private readonly widthInputId = genDomID()\n  private readonly heightInputId = genDomID()\n  private readonly buttonId = genDomID()\n\n  private getSelectedVideoNode(editor: IDomEditor): SlateNode | null {\n    return DomEditor.getSelectedNodeByType(editor, 'video')\n  }\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 插入菜单，不需要 value\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 任何时候，都不用激活 menu\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    // 点击菜单时，弹出 modal 之前，不需要执行其他代码\n    // 此处空着即可\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    if (editor.selection == null) return true\n\n    const videoNode = this.getSelectedVideoNode(editor)\n    if (videoNode == null) {\n      // 选区未处于 video node ，则禁用\n      return true\n    }\n    return false\n  }\n\n  getModalPositionNode(editor: IDomEditor): SlateNode | null {\n    return this.getSelectedVideoNode(editor)\n  }\n\n  getModalContentElem(editor: IDomEditor): DOMElement {\n    // return $('<div><p>修改尺寸</p><p>修改尺寸</p><p>修改尺寸</p><p>修改尺寸</p></div>')[0]\n\n    const { widthInputId, heightInputId, buttonId } = this\n\n    const [widthContainerElem, inputWidthElem] = genModalInputElems(\n      t('videoModule.width'),\n      widthInputId,\n      'auto'\n    )\n    const $inputWidth = $(inputWidthElem)\n    const [heightContainerElem, inputHeightElem] = genModalInputElems(\n      t('videoModule.height'),\n      heightInputId,\n      'auto'\n    )\n    const $inputHeight = $(inputHeightElem)\n    const [buttonContainerElem] = genModalButtonElems(buttonId, t('videoModule.ok'))\n\n    if (this.$content == null) {\n      // 第一次渲染\n      const $content = $('<div></div>')\n\n      // 绑定事件（第一次渲染时绑定，不要重复绑定）\n      $content.on('click', `#${buttonId}`, e => {\n        e.preventDefault()\n\n        const rawWidth = $content.find(`#${widthInputId}`).val().trim()\n        const rawHeight = $content.find(`#${heightInputId}`).val().trim()\n        const numberWidth = parseInt(rawWidth)\n        const numberHeight = parseInt(rawHeight)\n        const width = numberWidth ? numberWidth.toString() : 'auto'\n        const height = numberHeight ? numberHeight.toString() : 'auto'\n\n        editor.restoreSelection()\n\n        // 修改尺寸\n        Transforms.setNodes(\n          editor,\n          { width, height },\n          {\n            match: n => DomEditor.checkNodeType(n, 'video'),\n          }\n        )\n\n        editor.hidePanelOrModal() // 隐藏 modal\n      })\n\n      this.$content = $content\n    }\n\n    const $content = this.$content\n\n    // 先清空，再重新添加 DOM 内容\n    $content.empty()\n    $content.append(widthContainerElem)\n    $content.append(heightContainerElem)\n    $content.append(buttonContainerElem)\n\n    const videoNode = this.getSelectedVideoNode(editor) as VideoElement\n    if (videoNode == null) return $content[0]\n\n    // 初始化 input 值\n    const { width = 'auto', height = 'auto' } = videoNode\n    $inputWidth.val(width)\n    $inputHeight.val(height)\n    setTimeout(() => {\n      $inputWidth.focus()\n    })\n\n    return $content[0]\n  }\n}\n\nexport default EditorVideoSizeMenu\n"
  },
  {
    "path": "packages/video-module/src/module/menu/InsertVideoMenu.ts",
    "content": "/**\n * @description insert video menu\n * @author wangfupeng\n */\n\nimport { Range, Node } from 'slate'\nimport {\n  IModalMenu,\n  IDomEditor,\n  DomEditor,\n  genModalInputElems,\n  genModalButtonElems,\n  t,\n} from '@wangeditor/core'\nimport $, { Dom7Array, DOMElement } from '../../utils/dom'\nimport { genRandomStr } from '../../utils/util'\nimport { VIDEO_SVG } from '../../constants/svg'\nimport insertVideo from '../helper/insert-video'\n\n/**\n * 生成唯一的 DOM ID\n */\nfunction genDomID(): string {\n  return genRandomStr('w-e-insert-video')\n}\n\nclass InsertVideoMenu implements IModalMenu {\n  readonly title = t('videoModule.insertVideo')\n  readonly iconSvg = VIDEO_SVG\n  readonly tag = 'button'\n  readonly showModal = true // 点击 button 时显示 modal\n  readonly modalWidth = 320\n  private $content: Dom7Array | null = null\n  private readonly srcInputId = genDomID()\n  private readonly posterInputId = genDomID()\n  private readonly buttonId = genDomID()\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 插入菜单，不需要 value\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 任何时候，都不用激活 menu\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    // 点击菜单时，弹出 modal 之前，不需要执行其他代码\n    // 此处空着即可\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    const { selection } = editor\n    if (selection == null) return true\n    if (!Range.isCollapsed(selection)) return true // 选区非折叠，禁用\n\n    const selectedElems = DomEditor.getSelectedElems(editor)\n    const hasVoidOrPre = selectedElems.some(elem => {\n      const type = DomEditor.getNodeType(elem)\n      if (type === 'pre') return true\n      if (type === 'list-item') return true\n      if (editor.isVoid(elem)) return true\n      return false\n    })\n    if (hasVoidOrPre) return true // void 或 pre ，禁用\n\n    return false\n  }\n\n  getModalPositionNode(editor: IDomEditor): Node | null {\n    return null // modal 依据选区定位\n  }\n\n  getModalContentElem(editor: IDomEditor): DOMElement {\n    const { srcInputId, posterInputId, buttonId } = this\n\n    // 获取 input button elem\n    const [srcContainerElem, inputSrcElem] = genModalInputElems(\n      t('videoModule.videoSrc'),\n      srcInputId,\n      t('videoModule.videoSrcPlaceHolder')\n    )\n    const [posterContainerElem, inputPosterElem] = genModalInputElems(\n      t('videoModule.videoPoster'),\n      posterInputId,\n      t('videoModule.videoPosterPlaceHolder')\n    )\n    const $inputSrc = $(inputSrcElem)\n    const $inputPoster = $(inputPosterElem)\n    const [buttonContainerElem] = genModalButtonElems(buttonId, t('videoModule.ok'))\n\n    if (this.$content == null) {\n      // 第一次渲染\n      const $content = $('<div></div>')\n\n      // 绑定事件（第一次渲染时绑定，不要重复绑定）\n      $content.on('click', `#${buttonId}`, async e => {\n        e.preventDefault()\n        const src = $content.find(`#${srcInputId}`).val().trim()\n        const poster = $content.find(`#${posterInputId}`).val().trim()\n        await insertVideo(editor, src, poster)\n        editor.hidePanelOrModal() // 隐藏 modal\n      })\n\n      // 记录属性，重要\n      this.$content = $content\n    }\n\n    const $content = this.$content\n    $content.empty() // 先清空内容\n\n    // append inputs and button\n    $content.append(srcContainerElem)\n    $content.append(posterContainerElem)\n    $content.append(buttonContainerElem)\n\n    // 设置 input val\n    $inputSrc.val('')\n    $inputPoster.val('')\n\n    // focus 一个 input（异步，此时 DOM 尚未渲染）\n    setTimeout(() => {\n      $inputSrc.focus()\n    })\n\n    return $content[0]\n  }\n}\n\nexport default InsertVideoMenu\n"
  },
  {
    "path": "packages/video-module/src/module/menu/UploadVideoMenu.ts",
    "content": "/**\n * @description upload video menu\n * @author wangfupeng\n */\n\nimport { Range } from 'slate'\nimport { IButtonMenu, IDomEditor, DomEditor, t } from '@wangeditor/core'\nimport $ from '../../utils/dom'\nimport { UPLOAD_VIDEO_SVG } from '../../constants/svg'\nimport { IUploadConfigForVideo } from './config'\nimport insertVideo from '../helper/insert-video'\nimport uploadVideos from '../helper/upload-videos'\n\nclass UploadVideoMenu implements IButtonMenu {\n  readonly title = t('videoModule.uploadVideo')\n  readonly iconSvg = UPLOAD_VIDEO_SVG\n  readonly tag = 'button'\n\n  getValue(editor: IDomEditor): string | boolean {\n    // 无需获取 val\n    return ''\n  }\n\n  isActive(editor: IDomEditor): boolean {\n    // 任何时候，都不用激活 menu\n    return false\n  }\n\n  exec(editor: IDomEditor, value: string | boolean) {\n    const { allowedFileTypes = [], customBrowseAndUpload } = this.getMenuConfig(editor)\n\n    // 自定义选择图片，并上传，如图床\n    if (customBrowseAndUpload) {\n      customBrowseAndUpload((src, poster) => insertVideo(editor, src, poster))\n      return\n    }\n\n    // 设置选择文件的类型\n    let acceptAttr = ''\n    if (allowedFileTypes.length > 0) {\n      acceptAttr = `accept=\"${allowedFileTypes.join(', ')}\"`\n    }\n\n    // 添加 file input（每次重新创建 input）\n    const $body = $('body')\n    const $inputFile = $(`<input type=\"file\" ${acceptAttr} multiple/>`)\n    $inputFile.hide()\n    $body.append($inputFile)\n    $inputFile.click()\n    // 选中文件\n    $inputFile.on('change', () => {\n      const files = ($inputFile[0] as HTMLInputElement).files\n      uploadVideos(editor, files) // 上传文件\n    })\n  }\n\n  isDisabled(editor: IDomEditor): boolean {\n    const { selection } = editor\n    if (selection == null) return true\n    if (!Range.isCollapsed(selection)) return true // 选区非折叠，禁用\n\n    const selectedElems = DomEditor.getSelectedElems(editor)\n    const hasVoidOrPre = selectedElems.some(elem => {\n      const type = DomEditor.getNodeType(elem)\n      if (type === 'pre') return true\n      if (type === 'list-item') return true\n      if (editor.isVoid(elem)) return true\n      return false\n    })\n    if (hasVoidOrPre) return true // void 或 pre ，禁用\n\n    return false\n  }\n\n  private getMenuConfig(editor: IDomEditor): IUploadConfigForVideo {\n    // 获取配置，见 `./config.js`\n    return editor.getMenuConfig('uploadVideo') as IUploadConfigForVideo\n  }\n}\n\nexport default UploadVideoMenu\n"
  },
  {
    "path": "packages/video-module/src/module/menu/config.ts",
    "content": "/**\n * @description video menu config\n * @author wangfupeng\n */\n\nimport { IUploadConfig } from '@wangeditor/core'\nimport { VideoElement } from '../custom-types'\n\ntype InsertFn = (src: string, poster: string) => void\n\n// 在通用 uploadConfig 上，扩展 video 相关配置\nexport type IUploadConfigForVideo = IUploadConfig & {\n  allowedFileTypes?: string[]\n  // 用户自定义插入视频\n  customInsert?: (res: any, insertFn: InsertFn) => void\n  // 用户自定义上传视频\n  customUpload?: (files: File, insertFn: InsertFn) => void\n  // 自定义选择视频，如图床\n  customBrowseAndUpload?: (insertFn: InsertFn) => void\n}\n\nexport function genUploadVideoMenuConfig(): IUploadConfigForVideo {\n  return {\n    server: '', // server API 地址，需用户配置\n\n    fieldName: 'wangeditor-uploaded-video', // formData 中，文件的 key\n    maxFileSize: 10 * 1024 * 1024, // 10M\n    maxNumberOfFiles: 5, // 最多上传 xx 个视频\n    allowedFileTypes: ['video/*'],\n    meta: {\n      // 自定义上传参数，例如传递验证的 token 等。参数会被添加到 formData 中，一起上传到服务端。\n      // 例如：token: 'xxxxx', x: 100\n    },\n    metaWithUrl: false,\n    // headers: {\n    //   // 自定义 http headers\n    //   // 例如：Accept: 'text/x-json', a: 100,\n    // },\n    withCredentials: false,\n    timeout: 30 * 1000, // 30s\n\n    onBeforeUpload: (files: any) => files, // 返回 false 则终止上传\n    onProgress: (progress: number) => {\n      /* on progress */\n    },\n    onSuccess: (file: any, res: any) => {\n      /* on success */\n    },\n    onFailed: (file: any, res: any) => {\n      /* on failed */\n      console.error(`'${file.name}' upload failed`, res)\n    },\n    onError: (file: any, err: any, res: any) => {\n      /* on error */\n      /* on timeout */\n      console.error(`'${file.name} upload error`, err, res)\n    },\n\n    // 自定义插入视频，用户配置\n    // customInsert: (res, insertFn) => {},\n\n    // 自定义上传视频，用户配置\n    // customUpload: (file, insertFn) => {},\n\n    // 自定义选择，并上传视频，如：图床 （用户配置）\n    // customBrowseAndUpload: insertFn => {},\n  }\n}\n\n/**\n * 生成插入网络视频的配置\n */\nexport function genInsertVideoMenuConfig() {\n  return {\n    onInsertedVideo(node: VideoElement) {\n      // 插入视频之后的 callback\n    },\n\n    /**\n     * 检查 video ，支持 async\n     * @param src src\n     * @param poster poster\n     */\n    checkVideo(src: string, poster: string): boolean | string | undefined {\n      // 1. 返回 true ，说明检查通过\n      // 2. 返回一个字符串，说明检查未通过，编辑器会阻止插入。会 alert 出错误信息（即返回的字符串）\n      // 3. 返回 undefined（即没有任何返回），说明检查未通过，编辑器会阻止插入\n      return true\n    },\n\n    /**\n     * 转换 video src\n     * @param src src\n     * @returns new src\n     */\n    parseVideoSrc(src: string): string {\n      return src\n    },\n  }\n}\n"
  },
  {
    "path": "packages/video-module/src/module/menu/index.ts",
    "content": "/**\n * @description video menu\n * @author wangfupeng\n */\n\nimport InsertVideoMenu from './InsertVideoMenu'\n// import DeleteVideoMenu from './DeleteVideoMenu'\nimport UploadVideoMenu from './UploadVideoMenu'\nimport EditorVideoSizeMenu from './EditVideoSizeMenu'\nimport { genInsertVideoMenuConfig, genUploadVideoMenuConfig } from './config'\n\nexport const insertVideoMenuConf = {\n  key: 'insertVideo',\n  factory() {\n    return new InsertVideoMenu()\n  },\n\n  // 默认的菜单菜单配置，将存储在 editorConfig.MENU_CONF[key] 中\n  // 创建编辑器时，可通过 editorConfig.MENU_CONF[key] = {...} 来修改\n  config: genInsertVideoMenuConfig(),\n}\n\nexport const uploadVideoMenuConf = {\n  key: 'uploadVideo',\n  factory() {\n    return new UploadVideoMenu()\n  },\n\n  // 默认的菜单菜单配置，将存储在 editorConfig.MENU_CONF[key] 中\n  // 创建编辑器时，可通过 editorConfig.MENU_CONF[key] = {...} 来修改\n  config: genUploadVideoMenuConfig(),\n}\n\nexport const editorVideSizeMenuConf = {\n  key: 'editVideoSize',\n  factory() {\n    return new EditorVideoSizeMenu()\n  },\n}\n\n// export const deleteVideoMenuConf = {\n//   key: 'deleteVideo',\n//   factory() {\n//     return new DeleteVideoMenu()\n//   },\n// }\n// 键盘能删除 video 了，注释掉这个菜单 wangfupeng 22.02.23\n"
  },
  {
    "path": "packages/video-module/src/module/parse-elem-html.ts",
    "content": "/**\n * @description parse html\n * @author wangfupeng\n */\n\nimport { Descendant } from 'slate'\nimport { IDomEditor } from '@wangeditor/core'\nimport { VideoElement } from './custom-types'\nimport $, { DOMElement } from '../utils/dom'\n\nfunction genVideoElem(\n  src: string,\n  poster: string = '',\n  width = 'auto',\n  height = 'auto'\n): VideoElement {\n  return {\n    type: 'video',\n    src,\n    poster,\n    width,\n    height,\n    children: [{ text: '' }], // void 元素有一个空 text\n  }\n}\n\nfunction parseHtml(elem: DOMElement, children: Descendant[], editor: IDomEditor): VideoElement {\n  const $elem = $(elem)\n  let src = ''\n  let poster = ''\n  let width = 'auto'\n  let height = 'auto'\n\n  // <iframe> 形式\n  const $iframe = $elem.find('iframe')\n  if ($iframe.length > 0) {\n    width = $iframe.attr('width') || 'auto'\n    height = $iframe.attr('height') || 'auto'\n    src = $iframe[0].outerHTML\n    return genVideoElem(src, poster, width, height)\n  }\n\n  // <video> 形式\n  const $video = $elem.find('video')\n  src = $video.attr('src') || ''\n  if (!src) {\n    if ($video.length > 0) {\n      const $source = $video.find('source')\n      src = $source.attr('src') || ''\n    }\n  }\n  width = $video.attr('width') || 'auto'\n  height = $video.attr('height') || 'auto'\n  poster = $video.attr('poster') || ''\n  return genVideoElem(src, poster, width, height)\n}\n\nexport const parseHtmlConf = {\n  selector: 'div[data-w-e-type=\"video\"]',\n  parseElemHtml: parseHtml,\n}\n"
  },
  {
    "path": "packages/video-module/src/module/plugin.ts",
    "content": "/**\n * @description editor 插件，重写 editor API\n * @author wangfupeng\n */\n\nimport { Transforms } from 'slate'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\nimport { CustomElement } from '../../../custom-types'\n\nfunction withVideo<T extends IDomEditor>(editor: T): T {\n  const { isVoid, normalizeNode } = editor\n  const newEditor = editor\n\n  // 重写 isVoid\n  newEditor.isVoid = (elem: CustomElement) => {\n    const { type } = elem\n\n    if (type === 'video') {\n      return true\n    }\n\n    return isVoid(elem)\n  }\n\n  // 重写 normalizeNode\n  newEditor.normalizeNode = ([node, path]) => {\n    const type = DomEditor.getNodeType(node)\n\n    // ----------------- video 后面必须跟一个 p header blockquote -----------------\n    if (type === 'video') {\n      // -------------- video 是 editor 最后一个节点，需要后面插入 p --------------\n      const isLast = DomEditor.isLastNode(newEditor, node)\n      if (isLast) {\n        Transforms.insertNodes(newEditor, DomEditor.genEmptyParagraph(), { at: [path[0] + 1] })\n      }\n    }\n\n    // 执行默认的 normalizeNode ，重要！！！\n    return normalizeNode([node, path])\n  }\n\n  // 返回 editor ，重要！\n  return newEditor\n}\n\nexport default withVideo\n"
  },
  {
    "path": "packages/video-module/src/module/pre-parse-html.ts",
    "content": "/**\n * @description pre parse html\n * @author wangfupeng\n */\n\nimport $, { getTagName, DOMElement } from '../utils/dom'\n\n/**\n * pre-prase video ，兼容 V4\n * @param elem elem\n */\nfunction preParse(elem: DOMElement): DOMElement {\n  const $elem = $(elem)\n  let $video = $elem\n\n  const elemTagName = getTagName($elem)\n  if (elemTagName === 'p') {\n    // v4 的 video 或 iframe 是被 p 包裹的\n    const children = $elem.children()\n    if (children.length === 1) {\n      const firstChild = children[0]\n      const firstChildTagName = firstChild.tagName.toLowerCase()\n      if (['iframe', 'video'].includes(firstChildTagName)) {\n        // p 下面包含 iframe 或 video\n        $video = $(firstChild)\n      }\n    }\n  }\n\n  const videoTagName = getTagName($video)\n  if (videoTagName !== 'iframe' && videoTagName !== 'video') return $video[0]\n\n  // 已经符合 V5 格式\n  const $parent = $video.parent()\n  if ($parent.attr('data-w-e-type') === 'video') return $video[0]\n\n  const $container = $(`<div data-w-e-type=\"video\" data-w-e-is-void></div>`)\n  $container.append($video)\n\n  return $container[0]\n}\n\nexport const preParseHtmlConf = {\n  selector: 'iframe,video,p',\n  preParseHtml: preParse,\n}\n"
  },
  {
    "path": "packages/video-module/src/module/render-elem.tsx",
    "content": "/**\n * @description video render elem\n * @author wangfupeng\n */\n\nimport { Element } from 'slate'\nimport { h, jsx, VNode } from 'snabbdom'\nimport { IDomEditor, DomEditor } from '@wangeditor/core'\nimport { VideoElement } from './custom-types'\nimport { genSizeStyledIframeHtml } from '../utils/dom'\n\nfunction renderVideo(elemNode: Element, children: VNode[] | null, editor: IDomEditor): VNode {\n  const { src = '', poster = '', width = 'auto', height = 'auto' } = elemNode as VideoElement\n\n  // 是否选中\n  const selected = DomEditor.isNodeSelected(editor, elemNode)\n\n  let vnode: VNode\n  if (src.trim().indexOf('<iframe ') === 0) {\n    // 增加尺寸样式\n    const iframeHtml = genSizeStyledIframeHtml(src, width, height)\n\n    // iframe 形式，第三方视频\n    vnode = (\n      <div\n        className=\"w-e-textarea-video-container\"\n        data-selected={selected ? 'true' : ''} // 标记为 选中\n        innerHTML={iframeHtml} // 内嵌第三方 iframe 视频\n      ></div>\n    )\n  } else {\n    // 其他，mp4 格式\n    const videoVnode = (\n      <video poster={poster} controls>\n        <source src={src} type=\"video/mp4\" />\n        {`Sorry, your browser doesn't support embedded videos.\\n 抱歉，浏览器不支持 video 视频`}\n      </video>\n    )\n    // @ts-ignore 添加尺寸\n    if (width !== 'auto') videoVnode.data.width = width\n    // @ts-ignore\n    if (height !== 'auto') videoVnode.data.height = height\n\n    vnode = (\n      <div\n        className=\"w-e-textarea-video-container\"\n        data-selected={selected ? 'true' : ''} // 标记为 选中\n      >\n        {videoVnode}\n      </div>\n    )\n  }\n\n  // 【注意】void node 中，renderElem 不用处理 children 。core 会统一处理。\n\n  const containerVnode = h(\n    'div',\n    {\n      props: {\n        contentEditable: false,\n      },\n      on: {\n        mousedown: e => e.preventDefault(),\n      },\n    },\n    vnode\n  )\n\n  return containerVnode\n}\n\nconst renderVideoConf = {\n  type: 'video', // 和 elemNode.type 一致\n  renderElem: renderVideo,\n}\n\nexport { renderVideoConf }\n"
  },
  {
    "path": "packages/video-module/src/utils/dom.ts",
    "content": "/**\n * @description DOM 操作\n * @author wangfupeng\n */\n\nimport $, { append, on, focus, attr, val, html, parent, hasClass, Dom7Array, empty } from 'dom7'\nexport { Dom7Array } from 'dom7'\n\nif (append) $.fn.append = append\nif (on) $.fn.on = on\nif (focus) $.fn.focus = focus\nif (attr) $.fn.attr = attr\nif (val) $.fn.val = val\nif (html) $.fn.html = html\nif (parent) $.fn.parent = parent\nif (hasClass) $.fn.hasClass = hasClass\nif (empty) $.fn.empty = empty\n\nexport default $\n\n/**\n * 获取 tagName lower-case\n * @param $elem $elem\n */\nexport function getTagName($elem: Dom7Array): string {\n  if ($elem.length) return $elem[0].tagName.toLowerCase()\n  return ''\n}\n\n/**\n * 生成带 size 样式的 iframe html\n * @param iframeHtml iframe html string\n * @param width width\n * @param height height\n * @returns iframe html string with size style\n */\nexport function genSizeStyledIframeHtml(\n  iframeHtml: string,\n  width: string = 'auto',\n  height: string = 'auto'\n): string {\n  const $iframe = $(iframeHtml)\n  $iframe.attr('width', width)\n  $iframe.attr('height', height)\n  return $iframe[0].outerHTML\n}\n\n// COMPAT: This is required to prevent TypeScript aliases from doing some very\n// weird things for Slate's types with the same name as globals. (2019/11/27)\n// https://github.com/microsoft/TypeScript/issues/35002\nimport DOMNode = globalThis.Node\nimport DOMComment = globalThis.Comment\nimport DOMElement = globalThis.Element\nimport DOMText = globalThis.Text\nimport DOMRange = globalThis.Range\nimport DOMSelection = globalThis.Selection\nimport DOMStaticRange = globalThis.StaticRange\nexport { DOMNode, DOMComment, DOMElement, DOMText, DOMRange, DOMSelection, DOMStaticRange }\n"
  },
  {
    "path": "packages/video-module/src/utils/util.ts",
    "content": "/**\n * @description 工具函数\n * @author wangfupeng\n */\n\nimport { nanoid } from 'nanoid'\n\n/**\n * 获取随机数字符串\n * @param prefix 前缀\n * @returns 随机数字符串\n */\nexport function genRandomStr(prefix: string = 'r'): string {\n  return `${prefix}-${nanoid()}`\n}\n\nexport function replaceSymbols(str: string) {\n  return str.replace(/</g, '&lt;').replace(/>/g, '&gt;')\n}\n"
  },
  {
    "path": "packages/video-module/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {},\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\n    \"./src/**/*\",\n    \"../custom-types.d.ts\"\n  ]\n}"
  },
  {
    "path": "scripts/release-tag.js",
    "content": "const util = require('util')\nconst exec = util.promisify(require('child_process').exec)\nconst DEFAULT_RELEASE_COMMIT_MESSAGE = 'chore: release tag'\n\nfunction command(command) {\n  return exec(command, { cwd: process.cwd() })\n    .then(resp => {\n      const data = resp.stdout.toString()\n      return Promise.resolve(data)\n    })\n    .catch(err => {\n      throw err\n    })\n}\n\nasync function run(commitMsg = DEFAULT_RELEASE_COMMIT_MESSAGE) {\n  const timestamp = Date.now()\n  const tagName = `v${timestamp}`\n  // 先打触发 publish ci 的标签\n  await command(`git tag -a ${tagName} -m\"${commitMsg}\"`)\n  // 推送标签到远程触发 ci\n  await command(`git push origin ${tagName}`)\n}\n\nrun()\n"
  },
  {
    "path": "tests/setup/index.ts",
    "content": "import '@testing-library/jest-dom'\nimport nodeCrypto from 'crypto'\n\n// @ts-ignore\nglobal.crypto = {\n  getRandomValues: function (buffer: any) {\n    return nodeCrypto.randomFillSync(buffer)\n  },\n}\n\n// Jest environment not contains DataTransfer object, so mock a DataTransfer class\n// @ts-ignore\nglobal.DataTransfer = class DataTransfer {\n  clearData() {}\n  getData(type: string) {\n    if (type === 'text/plain') return ''\n    return []\n  }\n  setData() {}\n  get files() {\n    return [new File(['124'], 'test.jpg')]\n  }\n}\n"
  },
  {
    "path": "tests/utils/create-editor.ts",
    "content": "/**\n * @description create editor for test\n * @author luochao\n */\nimport { createEditor as create } from '../../packages/editor/src'\n\nexport default function createEditor(options: any = {}) {\n  const container = document.createElement('div')\n  document.body.appendChild(container)\n\n  return create({\n    selector: container,\n    ...options,\n  })\n}\n\n// 【注意】packages/editor 中的 createEditor 不能用于 packages/core 的单元测试（因为从模块关系上，后者不能依赖于前者）！！！\n//        只能用于其他 package\n"
  },
  {
    "path": "tests/utils/create-toolbar.ts",
    "content": "/**\n * @description create toolbar for test\n * @author wangfupeng\n */\nimport { createToolbar as create } from '../../packages/editor/src'\n\nexport default function createToolbar(editor: any, config: any = {}) {\n  const container = document.createElement('div')\n  document.body.appendChild(container)\n\n  return create({\n    editor,\n    selector: container,\n    config,\n  })\n}\n"
  },
  {
    "path": "tests/utils/stylesMock.js",
    "content": "module.exports = {}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"module\": \"ES2015\",\n    \"lib\": [\n      \"es6\",\n      \"dom\",\n      \"esnext\"\n    ],\n    \"declaration\": true,\n    \"sourceMap\": true,\n    \"outDir\": \"dist\",\n    \"strict\": true,\n    \"noImplicitAny\": false,\n    \"resolveJsonModule\": true,\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"node\",\n    \"forceConsistentCasingInFileNames\": true,\n    \"jsx\": \"react\",\n    \"jsxFactory\": \"jsx\", /* snabbdom jsx */\n    \"downlevelIteration\": true,\n    \"allowJs\": true\n  },\n  \"exclude\": [\n    \"node_modules\",\n    \"build\",\n    \"__tests__\"\n  ],\n  \"include\": [\"./tests/setup/index.ts\", \"./packages/custom-types.d.ts\"]\n}"
  }
]