[
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n  root: true,\n  plugins: ['prettier', '@typescript-eslint'],\n  extends: ['tui/es6', 'plugin:prettier/recommended', 'plugin:@typescript-eslint/recommended'],\n  parser: '@typescript-eslint/parser',\n  parserOptions: {\n    parser: 'typescript-eslint-parser',\n  },\n  env: {\n    browser: true,\n    node: true,\n    jest: true,\n  },\n  globals: {\n    jest: true,\n  },\n  ignorePatterns: ['node_modules/*', 'dist'],\n  rules: {\n    '@typescript-eslint/no-non-null-assertion': 0,\n    '@typescript-eslint/explicit-function-return-type': 0,\n    '@typescript-eslint/explicit-module-boundary-types': 0,\n    '@typescript-eslint/no-explicit-any': 0,\n    '@typescript-eslint/ban-types': 0,\n    '@typescript-eslint/ban-ts-comment': 0,\n    '@typescript-eslint/no-useless-constructor': 2,\n    'lines-around-directive': 0,\n    'newline-before-return': 0,\n    'no-use-before-define': 0,\n    'no-useless-constructor': 0,\n    'padding-line-between-statements': [\n      2,\n      { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' },\n      { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] },\n    ],\n    'no-useless-rename': 'error',\n    'no-duplicate-imports': ['error', { includeExports: true }],\n    'dot-notation': ['error', { allowKeywords: true }],\n    'prefer-destructuring': [\n      'error',\n      {\n        VariableDeclarator: {\n          array: true,\n          object: true,\n        },\n        AssignmentExpression: {\n          array: false,\n          object: false,\n        },\n      },\n      {\n        enforceForRenamedProperties: false,\n      },\n    ],\n    'arrow-body-style': ['error', 'as-needed', { requireReturnForObjectLiteral: true }],\n    'object-property-newline': ['error', { allowMultiplePropertiesPerLine: true }],\n    'no-sync': 0,\n    complexity: 0,\n    'max-nested-callbacks': ['error', 4],\n    'no-cond-assign': 0,\n    'max-depth': ['error', 4],\n    'no-return-assign': 0,\n  },\n};\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: Bug\nassignees: ''\n---\n\n## Describe the bug\nA clear and concise description of what the bug is.\n\n## To Reproduce\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n## Expected behavior\nA clear and concise description of what you expected to happen.\n\n## Screenshots\nIf applicable, add screenshots to help explain your problem.\n\n## Desktop (please complete the following information):\n - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n## Smartphone (please complete the following information):\n - Device: [e.g. iPhone6]\n - OS: [e.g. iOS8.1]\n - Browser [e.g. stock browser, safari]\n - Version [e.g. 22]\n\n## Additional context\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: Enhancement, Need Discussion\nassignees: ''\n---\n\n<!--\nThank you for your contribution.\n\nWhen it comes to write an issue, please, use the template below.\nTo use the template is mandatory for submit new issue and we won't reply the issue that without the template.\n\nAnd you can write template's contents in Korean also.\n-->\n\n## Version\nWrite the version that you are currently using.\n\n## Development Environment\nWrite the browser type, OS and so on.\n\n## Current Behavior\nWrite a description of the current operation. You can add sample code, 'CodePen' or 'jsfiddle' links.\n\n```js\n// Write example code\n```\n\n## Expected Behavior\nWrite a description of the future action.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/question.md",
    "content": "---\nname: Question\nabout: Create a question about the Editor\ntitle: ''\nlabels: Question\nassignees: ''\n---\n\n<!--\n  To make it easier for us to help you, please include as much useful information as possible.\n\n  Useful Links:\n  - tutorial: https://github.com/nhn/tui.editor/tree/master/docs\n  - API/Example: https://nhn.github.io/tui.editor/latest/\n\n  Before opening a new issue, please search existing issues https://github.com/nhn/tui.editor/issues\n-->\n\n## Summary\nA clear and concise description of what the question is.\n\n## Screenshots\nIf applicable, add screenshots to help explain your question.\n\n## Version\nWrite the version of the Editor you are currently using.\n\n## Additional context\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: /\n    schedule:\n      interval: weekly\n  - package-ecosystem: npm\n    open-pull-requests-limit: 30\n    directory: /\n    schedule:\n      interval: weekly\n"
  },
  {
    "path": ".github/stale.yml",
    "content": "# Configuration for probot-stale - https://github.com/probot/stale\n\n# Number of days of inactivity before an Issue or Pull Request becomes stale\ndaysUntilStale: 30\n\n# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.\n# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.\ndaysUntilClose: 7\n\n# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable\nexemptLabels:\n  - Feature\n  - Enhancement\n  - Bug\n  - NHN Cloud\n\n# Label to use when marking as stale\nstaleLabel: inactive\n\n# Comment to post when marking as stale. Set to `false` to disable\nmarkComment: >\n  This issue has been automatically marked as inactive because there hasn’t been much going on it lately.\n  It is going to be closed after 7 days. Thanks!\n\n# Comment to post when closing a stale Issue or Pull Request.\ncloseComment: >\n  This issue will be closed due to inactivity. Thanks for your contribution!\n"
  },
  {
    "path": ".github/workflows/check-types.yml",
    "content": "name: Editor Check Types\non: pull_request\njobs: \n  check-types:\n    name: Check Types\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout branch\n        uses: actions/checkout@v2\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n      - name: Install\n        run: |\n          npm ci\n      - name: check types\n        run: |\n          npm run test:types:all"
  },
  {
    "path": ".github/workflows/examplePageTest.yml",
    "content": "name: detect runtime error\n\non:\n  schedule:\n    - cron: '0 22 * * *'\njobs:\n  makeUrl:\n    runs-on: ubuntu-latest\n    env:\n      WORKING_DIRECTORY: ./apps/editor\n    steps:\n      - name: checkout repository\n        uses: actions/checkout@v2\n      - name: create config variable\n        working-directory: ${{ env.WORKING_DIRECTORY }}\n        run: |\n          node scripts/createConfigVariable.js\n      - name: set global error variable\n        working-directory: ${{ env.WORKING_DIRECTORY }}\n        run: |\n          echo ::set-env name=ERROR_VARIABLE::$(head -n 1 ./errorVariable.txt)\n      - name: set url\n        working-directory: ${{ env.WORKING_DIRECTORY }}\n        run: |\n          echo ::set-env name=URLS::$(head -n 1 ./url.txt)\n      - name: detect runtime error\n        uses: nhn/toast-ui.detect-runtime-error-actions@master\n        with:\n          global-error-log-variable: ${{ env.ERROR_VARIABLE }}\n          urls: ${{ env.URLS }}\n          browserlist: ie11, safari, edge, firefox, chrome\n        env:\n          BROWSERSTACK_USERNAME: ${{secrets.BROWSERSTACK_USERNAME}}\n          BROWSERSTACK_ACCESS_KEY: ${{secrets.BROWSERSTACK_ACCESS_KEY}}\n"
  },
  {
    "path": ".github/workflows/linter.yml",
    "content": "name: Editor Lint Code Base\non: pull_request\njobs: \n  lint:\n    name: Lint Code Base\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout branch\n        uses: actions/checkout@v2\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n      - name: Install\n        run: |\n          npm ci\n      - name: eslint\n        run: |\n          npm run lint:all"
  },
  {
    "path": ".github/workflows/plugin-test.yml",
    "content": "name: Plugin Unit, Integration Test\non: pull_request\njobs: \n  plugin-test:\n    name: Unit, Integration Test\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout branch\n        uses: actions/checkout@v2\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n      - name: Install\n        run: |\n          npm install\n      - name: build toastmark\n        run: |\n          npm run build toastmark\n      - name: build editor\n        run: |\n          npm run build editor\n      - name: chart plugin unit, integration test\n        run: |\n          npm run test:ci chart\n      - name: color syntax plugin unit, integration test\n        run: |\n          npm run test:ci color\n      - name: code syntax highlighting plugin unit, integration test\n        run: |\n          npm run test:ci code\n      - name: table merged cell plugin unit, integration test\n        run: |\n          npm run test:ci table\n      - name: uml plugin unit, integration test\n        run: |\n          npm run test:ci uml"
  },
  {
    "path": ".github/workflows/publish-cdn.yml",
    "content": "name: Cdn Publish\non: [workflow_dispatch]\njobs:\n  pre-check:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout branch\n        uses: actions/checkout@v2\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n      - name: Install\n        run: |\n          npm ci\n      - name: Eslint\n        run: |\n          npm run lint:all\n      - name: Check types\n        run: |\n          npm run test:types:all\n\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout branch\n        uses: actions/checkout@v2\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n      - name: Install\n        run: |\n          npm ci\n      - name: Build\n        run: |\n          npm run build toastmark\n      - name: Toastmark unit, integration test\n        run: |\n          npm run test:ci toastmark\n      - name: Editor unit, integration test\n        run: |\n          npm run test:ci editor\n\n  plugin-test:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout branch\n        uses: actions/checkout@v2\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n      - name: Install\n        run: |\n          npm ci\n      - name: Build\n        run: |\n          npm run build toastmark\n          npm run build editor\n      - name: chart plugin unit, integration test\n        run: |\n          npm run test:ci chart\n      - name: color syntax plugin unit, integration test\n        run: |\n          npm run test:ci color\n      - name: code syntax highlighting plugin unit, integration test\n        run: |\n          npm run test:ci code\n      - name: table merged cell plugin unit, integration test\n        run: |\n          npm run test:ci table\n      - name: uml plugin unit, integration test\n        run: |\n          npm run test:ci uml\n\n  publish-cdn:\n    runs-on: ubuntu-latest\n    needs: [pre-check, test, plugin-test]\n    steps:\n      - uses: actions/checkout@v2\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n      - name: Install\n        run: |\n          npm ci\n      - name: Build\n        run: |\n          npm run build toastmark\n          npm run build editor\n      - name: Publish CDN\n        run: |\n          npm run publish:cdn\n        env:\n          TOAST_CLOUD_TENENTID: ${{ secrets.TOAST_CLOUD_TENENTID }}\n          TOAST_CLOUD_STORAGEID: ${{ secrets.TOAST_CLOUD_STORAGEID }}\n          TOAST_CLOUD_USERNAME: ${{ secrets.TOAST_CLOUD_USERNAME }}\n          TOAST_CLOUD_PASSWORD: ${{ secrets.TOAST_CLOUD_PASSWORD }}"
  },
  {
    "path": ".github/workflows/publish-doc.yml",
    "content": "name: Doc Publish\non: [workflow_dispatch]\njobs:\n  pre-check:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout branch\n        uses: actions/checkout@v2\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n      - name: Install\n        run: |\n          npm ci\n      - name: Eslint\n        run: |\n          npm run lint:all\n      - name: Check types\n        run: |\n          npm run test:types:all\n\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout branch\n        uses: actions/checkout@v2\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n      - name: Install\n        run: |\n          npm ci\n      - name: Build\n        run: |\n          npm run build toastmark\n      - name: Toastmark unit, integration test\n        run: |\n          npm run test:ci toastmark\n      - name: Editor unit, integration test\n        run: |\n          npm run test:ci editor\n\n  plugin-test:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout branch\n        uses: actions/checkout@v2\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n      - name: Install\n        run: |\n          npm ci\n      - name: Build\n        run: |\n          npm run build toastmark\n          npm run build editor\n      - name: chart plugin unit, integration test\n        run: |\n          npm run test:ci chart\n      - name: color syntax plugin unit, integration test\n        run: |\n          npm run test:ci color\n      - name: code syntax highlighting plugin unit, integration test\n        run: |\n          npm run test:ci code\n      - name: table merged cell plugin unit, integration test\n        run: |\n          npm run test:ci table\n      - name: uml plugin unit, integration test\n        run: |\n          npm run test:ci uml\n\n  doc:\n    runs-on: ubuntu-latest\n    needs: [pre-check, test, plugin-test]\n    steps:\n      - uses: actions/checkout@v2\n      - name: Check the package version\n        id: check\n        uses: PostHog/check-package-version@v2\n        with:\n          path: ./apps/editor/\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n      - name: Install\n        run: |\n          npm install\n      - name: Build\n        run: |\n          npm run build toastmark\n          npm run build editor\n      - name: Use Node.js 10.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '10.x'\n      - name: Install @toast-ui/doc\n        run: |\n          npm i -g @toast-ui/doc\n      - name: Run doc\n        run: |\n          npm run doc\n          mv apps/editor/_${{ steps.check.outputs.committed-version }} ${{ steps.check.outputs.committed-version }}\n          mv apps/editor/_latest latest\n          rm -rf apps/editor/tmpdoc\n          git checkout -- apps/editor/types/index.d.ts package-lock.json\n          git add ${{ steps.check.outputs.committed-version }}/dist -f\n          git add latest/dist -f\n          git stash --include-untracked\n      - name: Checkout gh-pages\n        uses: actions/checkout@v2\n        with:\n          ref: gh-pages\n      - name: Commit files\n        run: |\n          git config --local user.email 'jw.lee@nhn.com'\n          git config --local user.name 'jwlee1108'\n          rm -rf ${{ steps.check.outputs.committed-version }}\n          rm -rf latest\n          git add .\n          git stash pop\n          git add .\n          git commit -m '${{ steps.check.outputs.committed-version }}'\n      - name: Push changes\n        uses: ad-m/github-push-action@master\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          branch: gh-pages\n"
  },
  {
    "path": ".github/workflows/publish-npm-wrapper.yml",
    "content": "name: Wrapper Npm Publish\non: [workflow_dispatch]\njobs:\n  pre-check:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout branch\n        uses: actions/checkout@v2\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n      - name: Install\n        run: |\n          npm ci\n      - name: Eslint\n        run: |\n          npm run lint:all\n      - name: Check types\n        run: |\n          npm run test:types:all\n\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout branch\n        uses: actions/checkout@v2\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n      - name: Install\n        run: |\n          npm ci\n      - name: Build\n        run: |\n          npm run build toastmark\n      - name: Toastmark unit, integration test\n        run: |\n          npm run test:ci toastmark\n      - name: Editor unit, integration test\n        run: |\n          npm run test:ci editor\n\n  plugin-test:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout branch\n        uses: actions/checkout@v2\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n      - name: Install\n        run: |\n          npm ci\n      - name: Build\n        run: |\n          npm run build toastmark\n          npm run build editor\n      - name: chart plugin unit, integration test\n        run: |\n          npm run test:ci chart\n      - name: color syntax plugin unit, integration test\n        run: |\n          npm run test:ci color\n      - name: code syntax highlighting plugin unit, integration test\n        run: |\n          npm run test:ci code\n      - name: table merged cell plugin unit, integration test\n        run: |\n          npm run test:ci table\n      - name: uml plugin unit, integration test\n        run: |\n          npm run test:ci uml\n\n  publish:\n    runs-on: ubuntu-latest\n    needs: [pre-check, test, plugin-test]\n    steps:\n      - uses: actions/checkout@v2\n      - name: Check the package version\n        id: check\n        uses: PostHog/check-package-version@v2\n        with:\n          path: ./apps/editor/\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n          registry-url: https://registry.npmjs.org/\n      - name: Install\n        run: |\n          npm ci\n      - name: Build\n        run: |\n          npm run build toastmark\n          npm run build editor\n          npm run build react\n          npm run build vue\n      - name: Npm Publish(react)\n        working-directory: ./apps/react-editor\n        run: |\n          npm publish\n        env:\n          NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}}\n      - name: Npm Publish(vue)\n        working-directory: ./apps/vue-editor\n        run: |\n          npm publish\n        env:\n          NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}}\n"
  },
  {
    "path": ".github/workflows/publish-npm.yml",
    "content": "name: Npm Publish\non: [workflow_dispatch]\njobs:\n  checkVersion:\n    name: Check package version\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - name: Check package version\n        id: check\n        uses: PostHog/check-package-version@v2\n        with:\n          path: ./apps/editor/\n      - name: Cancel when unchanged\n        uses: andymckay/cancel-action@0.2\n        if: steps.check.outputs.is-new-version == 'false'\n\n  pre-check:\n    needs: [checkVersion]\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout branch\n        uses: actions/checkout@v2\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n      - name: Install\n        run: |\n          npm ci\n      - name: Eslint\n        run: |\n          npm run lint:all\n      - name: Check types\n        run: |\n          npm run test:types:all\n\n  test:\n    needs: [checkVersion]\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout branch\n        uses: actions/checkout@v2\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n      - name: Install\n        run: |\n          npm ci\n      - name: Build\n        run: |\n          npm run build toastmark\n      - name: Toastmark unit, integration test\n        run: |\n          npm run test:ci toastmark\n      - name: Editor unit, integration test\n        run: |\n          npm run test:ci editor\n\n  plugin-test:\n    needs: [checkVersion]\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout branch\n        uses: actions/checkout@v2\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n      - name: Install\n        run: |\n          npm ci\n      - name: Build\n        run: |\n          npm run build toastmark\n          npm run build editor\n      - name: chart plugin unit, integration test\n        run: |\n          npm run test:ci chart\n      - name: color syntax plugin unit, integration test\n        run: |\n          npm run test:ci color\n      - name: code syntax highlighting plugin unit, integration test\n        run: |\n          npm run test:ci code\n      - name: table merged cell plugin unit, integration test\n        run: |\n          npm run test:ci table\n      - name: uml plugin unit, integration test\n        run: |\n          npm run test:ci uml\n\n  publish:\n    runs-on: ubuntu-latest\n    needs: [pre-check, test, plugin-test]\n    steps:\n      - uses: actions/checkout@v2\n      - name: Check the package version\n        id: check\n        uses: PostHog/check-package-version@v2\n        with:\n          path: ./apps/editor/\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n          registry-url: https://registry.npmjs.org/\n      - name: Install\n        run: |\n          npm ci\n      - name: Build\n        run: |\n          npm run build toastmark\n          npm run build editor\n      - name: Create Tag\n        run: |\n          git config --local user.email 'jw.lee@nhn.com'\n          git config --local user.name 'jwlee1108'\n          git tag editor@${{ steps.check.outputs.committed-version }}\n      - name: Push Tag\n        run: |\n          git push origin editor@${{ steps.check.outputs.committed-version }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      - name: Npm Publish(editor)\n        working-directory: ./apps/editor\n        run: |\n          npm publish\n        env:\n          NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Editor Unit, Integration Test\non: pull_request\njobs: \n  test:\n    name: Unit, Integration Test\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout branch\n        uses: actions/checkout@v2\n      - name: Use Node.js 15.x\n        uses: actions/setup-node@v2.5.1\n        with:\n          node-version: '15.x'\n      - name: Install\n        run: |\n          npm install\n      - name: build toastmark\n        run: |\n          npm run build toastmark\n      - name: toastmark unit, integration test\n        run: |\n          npm run test:ci toastmark\n      - name: editor unit, integration test\n        run: |\n          npm run test:ci editor"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\nscreenshots\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directory\nnode_modules\n\n# Bower Components\nbower_components\nlib\n\n# IDEA\n.idea\n*.iml\n\n# Window\nThumbs.db\nDesktop.ini\n\n# MAC\n.DS_Store\n\n# SVN\n.svn\n\n# eclipse\n.project\n.metadata\n\n# build\nbuild\n\n# etc\n*.swp\netc\ntemp\napi\ndoc\nreport\nkarma.conf.local.js\n.tern-project\n.tern-port\n*.vim\n.\\#*\n.vscode/\ndist/"
  },
  {
    "path": ".prettierignore",
    "content": "*.md\n*.html"
  },
  {
    "path": ".prettierrc.js",
    "content": "module.exports = {\n  printWidth: 100,\n  singleQuote: true\n};\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, gender identity and expression, level of experience,\neducation, socio-economic status, nationality, personal appearance, race,\nreligion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n  advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at dl_javascript@nhn.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to TOAST UI\n\nFirst off, thanks for taking the time to contribute! 🎉 😘 ✨\n\nThe following is a set of guidelines for contributing to TOAST UI. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.\n\n## Reporting Bugs\nBugs are tracked as GitHub issues. Search the list and try reproduce on [demo][demo] before you create an issue. When you create an issue, please provide the following information by filling in the template.\n\nExplain the problem and include additional details to help maintainers reproduce the problem:\n\n* **Use a clear and descriptive title** for the issue to identify the problem.\n* **Describe the exact steps which reproduce the problem** in as many details as possible. Don't just say what you did, but explain how you did it. For example, if you moved the cursor to the end of a line, explain if you used a mouse or a keyboard.\n* **Provide specific examples to demonstrate the steps.** Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets on the issue, use Markdown code blocks.\n* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.\n* **Explain which behavior you expected to see instead and why.**\n* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem.\n\n## Suggesting Enhancements\nIn case you want to suggest for TOAST UI Editor, please follow this guideline to help maintainers and the community understand your suggestion.\nBefore creating suggestions, please check [issue list](https://github.com/nhn/tui.editor/labels/feature) if there's already a request.\n\nCreate an issue and provide the following information:\n\n* **Use a clear and descriptive title** for the issue to identify the suggestion.\n* **Provide a step-by-step description of the suggested enhancement** in as many details as possible.\n* **Provide specific examples to demonstrate the steps.** Include copy/pasteable snippets which you use in those examples, as Markdown code blocks.\n* **Include screenshots and animated GIFs** which helps demonstrate the steps or point out the part of TOAST UI Editor which the suggestion is related to.\n* **Explain why this enhancement would be useful** to most TOAST UI users.\n* **List some other text editors or applications where this enhancement exists.**\n\n## First Code Contribution\n\nUnsure where to begin contributing to TOAST UI? You can start by looking through these `document`, `good first issue` and `help wanted` issues:\n\n* **document issues**: issues which should be reviewed or improved.\n* **good first issues**: issues which should only require a few lines of code, and a test or two.\n* **help wanted issues**: issues which should be a bit more involved than beginner issues.\n\n## Pull Requests\n\n### Development WorkFlow\n- Set up your development environment\n- Make change from a right branch\n- Be sure the code passes `npm run lint:all`, `npm run test:types:all`, `npm run test:all`\n- Make a pull request\n\n### Development environment\n- Prepare your machine node and it's packages installed.\n- Checkout our repository\n- Install dependencies by `npm install`\n- Build toastmark by `npm run build toastmark`\n- Start snowpack-dev-server by `npm run serve`\n\n### Make changes\n#### Checkout a branch\n- **master**: PR Base branch.\n- **production**: lastest release branch with distribution files. never make a PR on this\n- **gh-pages**: API docs, examples and demo\n\n#### Check Code Style\nRun `npm run eslint` and make sure all the tests pass.\n\n#### Test\nRun `npm run test:all` and verify all the tests pass.\nIf you are adding new commands or features, they must include tests.\nIf you are changing functionality, update the tests if you need to.\n\n#### Commit\nFollow our [commit message conventions](./docs/COMMIT_MESSAGE_CONVENTION.md).\n\n### Yes! Pull request\nMake your pull request, then describe your changes.\n#### Title\nFollow other PR title format on below.\n```\n    <Type>: Short Description (fix #111)\n    <Type>: Short Description (fix #123, #111, #122)\n    <Type>: Short Description (ref #111)\n```\n* capitalize first letter of Type\n* use present tense: 'change' not 'changed' or 'changes'\n\n#### Description\nIf it has related to issues, add links to the issues(like `#123`) in the description.\nFill in the [Pull Request Template](./docs/PULL_REQUEST_TEMPLATE.md) by check your case.\n\n## Code of Conduct\nThis project and everyone participating in it is governed by the [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to dl_javascript@nhn.com.\n\n> This Guide is base on [atom contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md), [CocoaPods](http://guides.cocoapods.org/contributing/contribute-to-cocoapods.html) and [ESLint](http://eslint.org/docs/developer-guide/contributing/pull-requests)\n\n[demo]:https://nhn.github.io/tui.editor/\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 NHN Cloud Corp.\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\nall copies 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\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# ![TOAST UI Editor](https://uicdn.toast.com/toastui/img/tui-editor-bi.png)\n\n> GFM  Markdown and WYSIWYG Editor - Productive and Extensible\n\n[![github release version](https://img.shields.io/github/v/release/nhn/tui.editor.svg?include_prereleases)](https://github.com/nhn/tui.editor/releases/latest) [![npm version](https://img.shields.io/npm/v/@toast-ui/editor.svg)](https://www.npmjs.com/package/@toast-ui/editor) [![license](https://img.shields.io/github/license/nhn/tui.editor.svg)](https://github.com/nhn/tui.editor/blob/master/LICENSE) [![PRs welcome](https://img.shields.io/badge/PRs-welcome-ff69b4.svg)](https://github.com/nhn/tui.editor/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) [![code with hearth by NHN Cloud](https://img.shields.io/badge/%3C%2F%3E%20with%20%E2%99%A5%20by-NHN_Cloud-ff1414.svg)](https://github.com/nhn)\n\n<img src=\"https://user-images.githubusercontent.com/37766175/121809054-446bac80-cc96-11eb-9139-08c6d9ad2d88.png\" />\n\n\n## 🚩 Table of Contents\n\n- [Packages](#-packages)\n- [Why TOAST UI Editor?](#-why-toast-ui-editor)\n- [Features](#-features)\n- [Examples](#-examples)\n- [Browser Support](#-browser-support)\n- [Pull Request Steps](#-pull-request-steps)\n- [Contributing](#-contributing)\n- [TOAST UI Family](#-toast-ui-family)\n- [Used By](#-used-by)\n- [License](#-license)\n\n\n## 📦 Packages\n\n### TOAST UI Editor\n\n| Name | Description |\n| --- | --- |\n| [`@toast-ui/editor`](https://github.com/nhn/tui.editor/tree/master/apps/editor) | Plain JavaScript component |\n\n### TOAST UI Editor's Wrappers\n\n| Name | Description |\n| --- | --- |\n| [`@toast-ui/react-editor`](https://github.com/nhn/tui.editor/tree/master/apps/react-editor) | [React](https://reactjs.org/) wrapper component |\n| [`@toast-ui/vue-editor`](https://github.com/nhn/tui.editor/tree/master/apps/vue-editor) | [Vue](https://vuejs.org/) wrapper component |\n\n### TOAST UI Editor's Plugins\n\n| Name | Description |\n| --- | --- |\n| [`@toast-ui/editor-plugin-chart`](https://github.com/nhn/tui.editor/tree/master/plugins/chart) | Plugin to render chart |\n| [`@toast-ui/editor-plugin-code-syntax-highlight`](https://github.com/nhn/tui.editor/tree/master/plugins/code-syntax-highlight) | Plugin to highlight code syntax |\n| [`@toast-ui/editor-plugin-color-syntax`](https://github.com/nhn/tui.editor/tree/master/plugins/color-syntax) | Plugin to color editing text |\n| [`@toast-ui/editor-plugin-table-merged-cell`](https://github.com/nhn/tui.editor/tree/master/plugins/table-merged-cell) | Plugin to merge table columns |\n| [`@toast-ui/editor-plugin-uml`](https://github.com/nhn/tui.editor/tree/master/plugins/uml) | Plugin to render UML |\n\n\n## 🤖 Why TOAST UI Editor?\n\nTOAST UI Editor provides **Markdown mode** and **WYSIWYG mode**. Depending on the type of use you want like production of *Markdown* or maybe to just edit the *Markdown*. The TOAST UI Editor can be helpful for both the usage. It offers **Markdown mode** and **WYSIWYG mode**, which can be switched any point in time.\n\n### Productive Markdown Mode\n\n![markdown](https://user-images.githubusercontent.com/37766175/121464762-71e2fc80-c9ef-11eb-9a0a-7b06e08d3ccb.png)\n\n**CommonMark + GFM Specifications**\n\nToday *CommonMark* is the de-facto *Markdown* standard. *GFM (GitHub Flavored Markdown)* is another popular specification based on *CommonMark* - maintained by *GitHub*, which is the *Markdown* mostly used. TOAST UI Editor follows both [*CommonMark*](http://commonmark.org/) and [*GFM*](https://github.github.com/gfm/) specifications. Write documents with ease using productive tools provided by TOAST UI Editor and you can easily open the produced document wherever the specifications are supported.\n\n* **Live Preview** : Edit Markdown while keeping an eye on the rendered HTML. Your edits will be applied immediately.\n* **Scroll Sync** : Synchronous scrolling between Markdown and Preview. You don't need to scroll through each one separately.\n* **Syntax Highlight** : You can check broken Markdown syntax immediately.\n\n### Easy WYSIWYG Mode\n\n![wysiwyg](https://user-images.githubusercontent.com/37766175/121808381-251f5000-cc93-11eb-8c47-4f5a809de2b3.png)\n\n* **Table** : Through the context menu of the table, you can add or delete columns or rows of the table, and you can also arrange text in cells.\n* **Custom Block Editor** : The custom block area can be edited through the internal editor.\n* **Copy and Paste** : Paste anything from browser, screenshot, excel, powerpoint, etc.\n\n### UI\n* **Toolbar** : Through the toolbar, you can style or add elements to the document you are editing.\n![UI](https://user-images.githubusercontent.com/37766175/121808231-767b0f80-cc92-11eb-82a0-433123746982.png)\n\n* **Dark Theme** : You can use the dark theme.\n![UI](https://user-images.githubusercontent.com/37766175/121808649-8136a400-cc94-11eb-8674-812e170ccab5.png)\n\n### Use of Various Extended Functions - Plugins\n\n![plugin](https://user-images.githubusercontent.com/37766175/121808323-d8d41000-cc92-11eb-9117-b92a435c9b43.png)\n\nCommonMark and GFM are great, but we often need more abstraction. The TOAST UI Editor comes with powerful **Plugins** in compliance with the Markdown syntax.\n\n**Five basic plugins** are provided as follows, and can be downloaded and used with npm.\n\n* [**`chart`**](https://github.com/nhn/tui.editor/tree/master/plugins/chart) : A code block marked as a 'chart' will render [TOAST UI Chart](https://github.com/nhn/tui.chart).\n* [**`code-syntax-highlight`**](https://github.com/nhn/tui.editor/tree/master/plugins/code-syntax-highlight) : Highlight the code block area corresponding to the language provided by [Prism.js](https://prismjs.com/).\n* [**`color-syntax`**](https://github.com/nhn/tui.editor/tree/master/plugins/color-syntax) : \nUsing [TOAST UI ColorPicker](https://github.com/nhn/tui.color-picker), you can change the color of the editing text with the GUI.\n* [**`table-merged-cell`**](https://github.com/nhn/tui.editor/tree/master/plugins/table-merged-cell) : \nYou can merge columns of the table header and body area.\n* [**`uml`**](https://github.com/nhn/tui.editor/tree/master/plugins/uml) : A code block marked as an 'uml' will render [UML diagrams](http://plantuml.com/screenshot).\n\n## 🎨 Features\n\n* [Viewer](https://github.com/nhn/tui.editor/tree/master/docs/en/viewer.md) : Supports a mode to display only markdown data without an editing area.\n* [Internationalization (i18n)](https://github.com/nhn/tui.editor/tree/master/docs/en/i18n.md) : Supports English, Dutch, Korean, Japanese, Chinese, Spanish, German, Russian, French, Ukrainian, Turkish, Finnish, Czech, Arabic, Polish, Galician, Swedish, Italian, Norwegian, Croatian + language and you can extend.\n* [Widget](https://github.com/nhn/tui.editor/tree/master/docs/en/widget.md) : This feature allows you to configure the rules that replaces the string matching to a specific `RegExp` with the widget node.\n* [Custom Block](https://github.com/nhn/tui.editor/tree/master/docs/en/custom-block.md) : Nodes not supported by Markdown can be defined through custom block. You can display the node what you want through writing the parsing logic with custom block.\n\n## 🐾 Examples\n\n* [Basic](https://nhn.github.io/tui.editor/latest/tutorial-example01-editor-basic)\n* [Viewer](https://nhn.github.io/tui.editor/latest/tutorial-example04-viewer)\n* [Using All Plugins](https://nhn.github.io/tui.editor/latest/tutorial-example12-editor-with-all-plugins)\n* [Creating the User's Plugin](https://nhn.github.io/tui.editor/latest/tutorial-example13-creating-plugin)\n* [Customizing the Toobar Buttons](https://nhn.github.io/tui.editor/latest/tutorial-example15-customizing-toolbar-buttons)\n* [Internationalization (i18n)](https://nhn.github.io/tui.editor/latest/tutorial-example16-i18n)\n\nHere are more [examples](https://nhn.github.io/tui.editor/latest/tutorial-example01-editor-basic) and play with TOAST UI Editor!\n\n\n## 🌏 Browser Support\n\n| <img src=\"https://user-images.githubusercontent.com/1215767/34348387-a2e64588-ea4d-11e7-8267-a43365103afe.png\" alt=\"Chrome\" width=\"16px\" height=\"16px\" /> Chrome | <img src=\"https://user-images.githubusercontent.com/1215767/34348590-250b3ca2-ea4f-11e7-9efb-da953359321f.png\" alt=\"IE\" width=\"16px\" height=\"16px\" /> Internet Explorer | <img src=\"https://user-images.githubusercontent.com/1215767/34348380-93e77ae8-ea4d-11e7-8696-9a989ddbbbf5.png\" alt=\"Edge\" width=\"16px\" height=\"16px\" /> Edge | <img src=\"https://user-images.githubusercontent.com/1215767/34348394-a981f892-ea4d-11e7-9156-d128d58386b9.png\" alt=\"Safari\" width=\"16px\" height=\"16px\" /> Safari | <img src=\"https://user-images.githubusercontent.com/1215767/34348383-9e7ed492-ea4d-11e7-910c-03b39d52f496.png\" alt=\"Firefox\" width=\"16px\" height=\"16px\" /> Firefox |\n| :---------: | :---------: | :---------: | :---------: | :---------: |\n| Yes | 11+ | Yes | Yes | Yes |\n\n\n## 🔧 Pull Request Steps\n\nTOAST UI products are open source, so you can create a pull request(PR) after you fix issues. Run npm scripts and develop yourself with the following process.\n\n### Setup\n\nFork `main` branch into your personal repository. Clone it to local computer. Install node modules. Before starting development, you should check if there are any errors.\n\n```sh\n$ git clone https://github.com/{your-personal-repo}/tui.editor.git\n$ npm install\n$ npm run build toastmark\n$ npm run test editor\n```\n\n> TOAST UI Editor uses [npm workspace](https://docs.npmjs.com/cli/v7/using-npm/workspaces/), so you need to set the environment based on [npm7](https://github.blog/2021-02-02-npm-7-is-now-generally-available/). If subversion is used, dependencies must be installed by moving direct paths per package.\n\n### Develop\n\nYou can see your code reflected as soon as you save the code by running a server. Don't miss adding test cases and then make green rights.\n\n#### Run snowpack-dev-server\n[snowpack](https://www.snowpack.dev/) allows you to run a development server without bundling.\n\n``` sh\n$ npm run serve editor\n```\n\n#### Run webpack-dev-server\nIf testing of legacy browsers is required, the development server can still be run using a [webpack](https://webpack.js.org/).\n\n``` sh\n$ npm run serve:ie editor\n```\n\n#### Run test\n\n``` sh\n$ npm test editor\n```\n\n### Pull Request\n\nBefore uploading your PR, run test one last time to check if there are any errors. If it has no errors, commit and then push it!\n\nFor more information on PR's steps, please see links in the Contributing section.\n\n## 💬 Contributing\n\n* [Code of Conduct](https://github.com/nhn/tui.editor/blob/master/CODE_OF_CONDUCT.md)\n* [Contributing Guideline](https://github.com/nhn/tui.editor/blob/master/CONTRIBUTING.md)\n* [Commit Convention](https://github.com/nhn/tui.editor/blob/master/docs/COMMIT_MESSAGE_CONVENTION.md)\n* [Issue Guidelines](https://github.com/nhn/tui.editor/tree/master/.github/ISSUE_TEMPLATE)\n\n\n## 🍞 TOAST UI Family\n\n- [TOAST UI Calendar](https://github.com/nhn/tui.calendar)\n- [TOAST UI Chart](https://github.com/nhn/tui.chart)\n- [TOAST UI Grid](https://github.com/nhn/tui.grid)\n- [TOAST UI Image Editor](https://github.com/nhn/tui.image-editor)\n- [TOAST UI Components](https://github.com/nhn)\n\n\n## 🚀 Used By\n\n* [NHN Dooray! - Collaboration Service (Project, Messenger, Mail, Calendar, Drive, Wiki, Contacts)](https://dooray.com)\n* [UNOTES - Visual Studio Code Extension](https://marketplace.visualstudio.com/items?itemName=ryanmcalister.Unotes)\n\n\n## 📜 License\n\nThis software is licensed under the [MIT](https://github.com/nhn/tui.editor/blob/master/LICENSE) © [NHN Cloud](https://github.com/nhn).\n"
  },
  {
    "path": "__mocks__/cssMock.js",
    "content": "module.exports = {\n  process() {\n    return 'module.exports = {};';\n  },\n};\n"
  },
  {
    "path": "apps/editor/README.md",
    "content": "# ![TOAST UI Editor](https://uicdn.toast.com/toastui/img/tui-editor-bi.png)\n\n[![npm](https://img.shields.io/npm/v/@toast-ui/editor.svg)](https://www.npmjs.com/package/@toast-ui/editor)\n\n## 🚩 Table of Contents\n\n- [Collect Statistics on the Use of Open Source](#Collect-statistics-on-the-use-of-open-source)\n- [Documents](#-documents)\n- [Install](#-install)\n- [Usage](#-usage)\n- [Tutorials](#-tutorials)\n\n## Collect Statistics on the Use of Open Source\n\nTOAST UI products apply Google Analytics (GA) to collect statistics on the use of open source, in order to identify how widely TOAST UI Editor is used throughout the world. It also serves as important index to determine the future course of projects. `location.hostname` (e.g. ui.toast.com) is to be collected and the sole purpose is nothing but to measure statistics on the usage.\n\nTo disable GA, use the following `usageStatistics` option when creating the instance.\n\n```js\nconst options = {\n  // ...\n  usageStatistics: false\n};\n\nconst editor = new Editor(options);\n```\n\n## 📙 Documents\n\n- [Getting Started](https://github.com/nhn/tui.editor/blob/master/docs/en/getting-started.md)\n- [APIs](https://nhn.github.io/tui.editor/latest/)\n- v3.0 Migration Guide\n  - [English](https://github.com/nhn/tui.editor/blob/master/docs/v3.0-migration-guide.md)\n  - [한국어](https://github.com/nhn/tui.editor/blob/master/docs/v3.0-migration-guide-ko.md)\n\nYou can also see the older versions of API page on the [releases page](https://github.com/nhn/tui.editor/releases).\n\n## 💾 Install\n\nTOAST UI products can be used by using the package manager or downloading the source directly. However, we highly recommend using the package manager.\n\n### Via Package Manager\n\nTOAST UI products are registered in two package managers, [npm](https://www.npmjs.com/). You can conveniently install it using the commands provided by the package manager. When using npm, be sure to use it in the environment [Node.js](https://nodejs.org/en/) is installed.\n\n#### npm\n\n```sh\n$ npm install --save @toast-ui/editor # Latest Version\n$ npm install --save @toast-ui/editor@<version> # Specific Version\n```\n\n### Via Contents Delivery Network (CDN)\n\nTOAST UI products are available over the CDN powered by [NHN Cloud](https://www.toast.com).\n\nYou can use the CDN as below.\n\n```html\n...\n<body>\n  ...\n  <script src=\"https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js\"></script>\n</body>\n...\n```\n\nIf you want to use a specific version, use the tag name instead of `latest` in the url's path.\n\nThe CDN directory has the following structure:\n\n```\n- uicdn.toast.com/\n   ├─ editor/\n   │     ├─ latest/\n   │     │    ├─ toastui-editor-all.js\n   │     │    ├─ toastui-editor-all.min.js\n   │     │    ├─ toastui-editor-viewer.js\n   │     │    ├─ toastui-editor-viewer.min.js\n   │     │    ├─ toastui-editor.css\n   │     │    ├─ toastui-editor.min.css\n   │     │    ├─ toastui-editor-viewer.css\n   │     │    ├─ toastui-editor-viewer.min.css\n   │     │    ├─ toastui-editor-only.css\n   │     │    ├─ toastui-editor-only.min.css\n   │     │    └─ theme/\n   │     │         ├─ toastui-editor-dark.css\n   │     │         └─ toastui-editor-dark.min.css\n   │     │    └─ i18n/\n   │     │         └─ ...\n   │     ├─ 2.0.0/\n   │     │    └─ ...\n```\n\n## 🔨 Usage\n\nFirst, you need to add the container element where TOAST UI Editor (henceforth referred to as 'Editor') will be created.\n\n```html\n...\n<body>\n  <div id=\"editor\"></div>\n</body>\n...\n```\n\nThe editor can be used by creating an instance with the constructor function. To get the constructor function, you should import the module using one of the following ways depending on your environment.\n\n### Using Module Format in Node Environment\n\n- ES6 Modules\n\n```javascript\nimport Editor from '@toast-ui/editor';\n```\n\n- CommonJS\n\n```javascript\nconst Editor = require('@toast-ui/editor');\n```\n\n### Using Namespace in Browser Environment\n\n```javascript\nconst Editor = toastui.Editor;\n```\n\nThen, you need to add the CSS files needed for the Editor. Import CSS files in node environment, and add it to html file when using CDN.\n\n### Using in Node Environment\n\n```javascript\nimport '@toast-ui/editor/dist/toastui-editor.css'; // Editor's Style\n```\n\n### Using in Browser Environment by CDN\n\n```html\n...\n<head>\n  ...\n  <!-- Editor's Style -->\n  <link rel=\"stylesheet\" href=\"https://uicdn.toast.com/editor/latest/toastui-editor.min.css\" />\n</head>\n...\n```\n\nFinally you can create an instance with options and call various API after creating an instance.\n\n```javascript\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  height: '500px',\n  initialEditType: 'markdown',\n  previewStyle: 'vertical'\n});\n\neditor.getMarkdown();\n```\n\n### Default Options\n\n- `height`: Height in string or auto ex) `300px` | `auto`\n- `initialEditType`: Initial type to show `markdown` | `wysiwyg`\n- `initialValue`: Initial value. Set Markdown string\n- `previewStyle`: Preview style of Markdown mode `tab` | `vertical`\n- `usageStatistics`: Let us know the _hostname_. We want to learn from you how you are using the Editor. You are free to disable it. `true` | `false`\n\nFind out more options [here](https://nhn.github.io/tui.editor/latest/ToastUIEditor).\n\n## 🦄 Tutorials\n\n- [Viewer](https://github.com/nhn/tui.editor/blob/master/docs/en/viewer.md)\n- [Plugins](https://github.com/nhn/tui.editor/blob/master/docs/en/plugin.md)\n- [Internationalization (i18n)](https://github.com/nhn/tui.editor/blob/master/docs/en/i18n.md)\n"
  },
  {
    "path": "apps/editor/demo/esm/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>Demo</title>\n  </head>\n  <body>\n    <div id=\"editor\"></div>\n    <!-- Editor -->\n    <script type=\"module\">\n      import { Editor } from '/dist/index.js';\n\n      const content = [\n        '![image](https://uicdn.toast.com/toastui/img/tui-editor-bi.png)',\n        '',\n        '# Awesome Editor!',\n        '',\n        'It has been _released as opensource in 2018_ and has ~~continually~~ evolved to **receive 10k GitHub ⭐️ Stars**.',\n        '',\n        '## Create Instance',\n        '',\n        'You can create an instance with the following code and use `getHtml()` and `getMarkdown()` of the [Editor](https://github.com/nhn/tui.editor).',\n        '',\n        '```js',\n        'const editor = new Editor(options);',\n        '```',\n        '',\n        '> See the table below for default options',\n        '> > More API information can be found in the document',\n        '',\n        '| name | type | description |',\n        '| --- | --- | --- |',\n        '| el | `HTMLElement` | container element |',\n        '',\n        '## Features',\n        '',\n        '* CommonMark + GFM Specifications',\n        '   * Live Preview',\n        '   * Scroll Sync',\n        '   * Auto Indent',\n        '   * Syntax Highlight',\n        '        1. Markdown',\n        '        2. Preview',\n        '',\n        '## Support Wrappers',\n        '',\n        '> * Wrappers',\n        '>    1. [x] React',\n        '>    2. [x] Vue',\n        '>    3. [ ] Ember',\n      ].join('\\n');\n\n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '400px',\n        initialEditType: 'markdown',\n        useCommandShortcut: true,\n        extendedAutolinks: true,\n        frontMatter: true,\n        initialValue: content,\n      });\n\n      window.editor = editor;\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/editor/examples/css/tuidoc-example-style.css",
    "content": "body {\n  margin: 0;\n  padding: 0;\n}\n\n.tui-doc-description {\n  padding: 22px 52px;\n  background-color: rgba(81, 92, 230, 0.1);\n  line-height: 1.4em;\n}\n\n.tui-doc-description,\n.tui-doc-description a {\n  font-family: Arial;\n  font-size: 14px;\n  color: #515ce6;\n}\n\n.tui-doc-contents {\n  padding: 20px 52px;\n}\n\n.tui-doc-contents .btn {\n  display: inline-block;\n  margin-bottom: 10px;\n  padding: 0 14px 0 15px;\n  height: 28px;\n  font-size: 12px;\n  font-weight: bold;\n  color: #fff;\n  border: 0;\n  vertical-align: top;\n  line-height: 22px;\n  background: #777;\n  cursor: pointer;\n  border-radius: 5px;\n  outline: 0;\n}\n"
  },
  {
    "path": "apps/editor/examples/data/md-default.js",
    "content": "/* eslint-disable no-unused-vars */\n/* eslint-disable no-var */\nvar content = [\n  '![image](https://uicdn.toast.com/toastui/img/tui-editor-bi.png)',\n  '',\n  '# Awesome Editor!',\n  '',\n  'It has been _released as opensource in 2018_ and has ~~continually~~ evolved to **receive 10k GitHub ⭐️ Stars**.',\n  '',\n  '## Create Instance',\n  '',\n  'You can create an instance with the following code and use `getHtml()` and `getMarkdown()` of the [Editor](https://github.com/nhn/tui.editor).',\n  '',\n  '```js',\n  'const editor = new Editor(options);',\n  '```',\n  '',\n  '> See the table below for default options',\n  '> > More API information can be found in the document',\n  '',\n  '| name | type | description |',\n  '| --- | --- | --- |',\n  '| el | `HTMLElement` | container element |',\n  '',\n  '## Features',\n  '',\n  '* CommonMark + GFM Specifications',\n  '   * Live Preview',\n  '   * Scroll Sync',\n  '   * Auto Indent',\n  '   * Syntax Highlight',\n  '        1. Markdown',\n  '        2. Preview',\n  '',\n  '## Support Wrappers',\n  '',\n  '> * Wrappers',\n  '>    1. [x] React',\n  '>    2. [x] Vue',\n  '>    3. [ ] Ember',\n].join('\\n');\n"
  },
  {
    "path": "apps/editor/examples/data/md-plugins.js",
    "content": "/* eslint-disable no-unused-vars */\n/* eslint-disable no-var */\nvar chartContent = [\n  '$$chart',\n  ',category1,category2',\n  'Jan,21,23',\n  'Feb,31,17',\n  '',\n  'type: column',\n  'title: Monthly Revenue',\n  'x.title: Amount',\n  'y.title: Month',\n  'y.min: 1',\n  'y.max: 40',\n  'y.suffix: $',\n  '$$',\n].join('\\n');\n\nvar codeContent = [\n  '```js',\n  \"console.log('foo')\",\n  '```',\n  '```javascript',\n  \"console.log('bar')\",\n  '```',\n  '```html',\n  '<div id=\"editor\"><span>baz</span></div>',\n  '```',\n  '```wrong',\n  '[1 2 3]',\n  '```',\n  '```clojure',\n  '[1 2 3]',\n  '```',\n].join('\\n');\n\nvar tableContent = ['| @cols=2:merged |', '| --- | --- |', '| table | table2 |'].join('\\n');\n\nvar umlContent = [\n  '$$uml',\n  'partition Conductor {',\n  '  (*) --> \"Climbs on Platform\"',\n  '  --> === S1 ===',\n  '  --> Bows',\n  '}',\n  '',\n  'partition Audience #LightSkyBlue {',\n  '  === S1 === --> Applauds',\n  '}',\n  '',\n  'partition Conductor {',\n  '  Bows --> === S2 ===',\n  '  --> WavesArmes',\n  '  Applauds --> === S2 ===',\n  '}',\n  '',\n  'partition Orchestra #CCCCEE {',\n  '  WavesArmes --> Introduction',\n  '  --> \"Play music\"',\n  '}',\n  '$$',\n].join('\\n');\n\nvar allPluginsContent = [chartContent, codeContent, tableContent, umlContent].join('\\n');\n"
  },
  {
    "path": "apps/editor/examples/example01-editor-basic.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>1. Editor</title>\n    <link rel=\"stylesheet\" href=\"./css/tuidoc-example-style.css\" />\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor.css\" />\n  </head>\n  <body>\n    <div class=\"tui-doc-description\">\n      <strong\n        >The example code can be slower than your environment because the code is transpiled by\n        babel-standalone in runtime.</strong\n      >\n      <br />\n      You can see the tutorial\n      <a\n        href=\"https://github.com/nhn/tui.editor/blob/master/docs/en/getting-started.md\"\n        rel=\"noopener noreferrer\" target=\"_blank\"\n        >here</a>\n    </div>\n    <div class=\"code-html tui-doc-contents\">\n      <div id=\"editor\"></div>\n    </div>\n    <!-- Added to check demo page in Internet Explorer -->\n    <script src=\"https://unpkg.com/babel-standalone@6.26.0/babel.min.js\"></script>\n    <script src=\"./data/md-default.js\"></script>\n    <!-- Editor -->\n    <script src=\"../dist/cdn/toastui-editor-all.js\"></script>\n    <script type=\"text/babel\" class=\"code-js\">\n      const editor = new toastui.Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: content\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/editor/examples/example02-editor-with-horizontal-preview.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>2. Editor With Horizontal Preview</title>\n    <link rel=\"stylesheet\" href=\"./css/tuidoc-example-style.css\" />\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor.css\" />\n  </head>\n  <body>\n    <div class=\"tui-doc-description\">\n      <strong\n        >The example code can be slower than your environment because the code is transpiled by\n        babel-standalone in runtime.</strong\n      >\n      <br />\n      You can see the tutorial\n      <a\n        href=\"https://github.com/nhn/tui.editor/blob/master/docs/en/getting-started.md\"\n        rel=\"noopener noreferrer\" target=\"_blank\"\n         >here</a>\n    </div>\n    <div class=\"code-html tui-doc-contents\">\n      <div id=\"editor\"></div>\n    </div>\n    <!-- Added to check demo page in Internet Explorer -->\n    <script src=\"https://unpkg.com/babel-standalone@6.26.0/babel.min.js\"></script>\n    <script src=\"./data/md-default.js\"></script>\n    <!-- Editor -->\n    <script src=\"../dist/cdn/toastui-editor-all.js\"></script>\n    <script type=\"text/babel\" class=\"code-js\">\n      const editor = new toastui.Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'tab',\n        height: '500px',\n        initialValue: content\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/editor/examples/example03-editor-with-wysiwyg-mode.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>3. Editor With WYSIWYG Mode</title>\n    <link rel=\"stylesheet\" href=\"./css/tuidoc-example-style.css\" />\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor.css\" />\n  </head>\n  <body>\n    <div class=\"tui-doc-description\">\n      <strong\n        >The example code can be slower than your environment because the code is transpiled by\n        babel-standalone in runtime.</strong\n      >\n      <br />\n      You can see the tutorial\n      <a\n        href=\"https://github.com/nhn/tui.editor/blob/master/docs/en/getting-started.md\"\n        rel=\"noopener noreferrer\" target=\"_blank\"\n        >here</a>.\n    </div>\n    <div class=\"code-html tui-doc-contents\">\n      <div id=\"editor\"></div>\n    </div>\n    <!-- Added to check demo page in Internet Explorer -->\n    <script src=\"https://unpkg.com/babel-standalone@6.26.0/babel.min.js\"></script>\n    <script src=\"./data/md-default.js\"></script>\n    <!-- Editor -->\n    <script src=\"../dist/cdn/toastui-editor-all.js\"></script>\n    <script type=\"text/babel\" class=\"code-js\">\n      const editor = new toastui.Editor({\n        el: document.querySelector('#editor'),\n        height: '500px',\n        initialValue: content,\n        initialEditType: 'wysiwyg'\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/editor/examples/example04-viewer.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>4. Viewer</title>\n    <link rel=\"stylesheet\" href=\"./css/tuidoc-example-style.css\" />\n    <!-- Viewer -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor-viewer.css\" />\n  </head>\n  <body>\n    <div class=\"tui-doc-description\">\n      <strong\n        >The example code can be slower than your environment because the code is transpiled by\n        babel-standalone in runtime.</strong\n      >\n      <br />\n      You can see the tutorial\n      <a\n        href=\"https://github.com/nhn/tui.editor/blob/master/docs/en/viewer.md\"\n        rel=\"noopener noreferrer\" target=\"_blank\"\n        >here</a>.\n    </div>\n    <div class=\"code-html tui-doc-contents\">\n      <div id=\"viewer\">\n        <strong\n          >The example code can be slower than your environment because the code is transpiled by\n          babel-standalone in runtime.</strong\n        >\n      </div>\n    </div>\n    <!-- Added to check demo page in Internet Explorer -->\n    <script src=\"https://unpkg.com/babel-standalone@6.26.0/babel.min.js\"></script>\n    <script src=\"./data/md-default.js\"></script>\n    <!-- Viewer -->\n    <script src=\"../dist/cdn/toastui-editor-viewer.js\"></script>\n    <script type=\"text/babel\" class=\"code-js\">\n      const viewer = new toastui.Editor({\n        el: document.querySelector('#viewer'),\n        initialValue: content\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/editor/examples/example05-viewer-using-editor-factory.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>5. Viewer Using Editor's Factory</title>\n    <link rel=\"stylesheet\" href=\"./css/tuidoc-example-style.css\" />\n    <!-- Viewer -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor-viewer.css\" />\n  </head>\n  <body>\n    <div class=\"tui-doc-description\">\n      <strong\n        >The example code can be slower than your environment because the code is transpiled by\n        babel-standalone in runtime.</strong\n      >\n      <br />\n      You can see the tutorial\n      <a\n        href=\"https://github.com/nhn/tui.editor/blob/master/docs/en/viewer.md\"\n        rel=\"noopener noreferrer\" target=\"_blank\"\n        >here</a\n      >.\n    </div>\n    <div class=\"code-html tui-doc-contents\">\n      <div id=\"viewer\">\n        <strong\n          >The example code can be slower than your environment because the code is transpiled by\n          babel-standalone in runtime.</strong\n        >\n      </div>\n    </div>\n    <!-- Added to check demo page in Internet Explorer -->\n    <script src=\"https://unpkg.com/babel-standalone@6.26.0/babel.min.js\"></script>\n    <script src=\"./data/md-default.js\"></script>\n    <!-- Editor -->\n    <script src=\"../dist/cdn/toastui-editor-all.js\"></script>\n    <script type=\"text/babel\" class=\"code-js\">\n      const viewer = toastui.Editor.factory({\n        el: document.querySelector('#viewer'),\n        viewer: true,\n        initialValue: content\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/editor/examples/example06-dark-theme.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>6. Editor with Dark Theme</title>\n    <link rel=\"stylesheet\" href=\"./css/tuidoc-example-style.css\" />\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor.css\" />\n    <!-- Dark theme -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/theme/toastui-editor-dark.css\" />\n  </head>\n  <body style=\"background: #111\">\n    <div class=\"tui-doc-description\">\n      <strong\n        >The example code can be slower than your environment because the code is transpiled by\n        babel-standalone in runtime.</strong\n      >\n    </div>\n    <div class=\"code-html tui-doc-contents\">\n      <!-- Editor -->\n      <h2 style=\"color: #fff\">Editor</h2>\n      <div id=\"editor\"></div>\n      <!-- Viewer Using Editor -->\n      <h2 style=\"color: #fff\">Viewer</h2>\n      <div id=\"viewer\"></div>\n    </div>\n    <!-- Added to check demo page in Internet Explorer -->\n    <script src=\"https://unpkg.com/babel-standalone@6.26.0/babel.min.js\"></script>\n    <script src=\"./data/md-default.js\"></script>\n    <!-- Editor -->\n    <script src=\"../dist/cdn/toastui-editor-all.js\"></script>\n    <script type=\"text/babel\" class=\"code-js\">\n      const { Editor } = toastui;\n\n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: content,\n        theme: 'dark'\n      });\n\n      const viewer = Editor.factory({\n        el: document.querySelector('#viewer'),\n        viewer: true,\n        height: '500px',\n        initialValue: content,\n        theme: 'dark'\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/editor/examples/example07-editor-with-chart-plugin.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>7. Editor with Chart Plugin</title>\n    <link rel=\"stylesheet\" href=\"./css/tuidoc-example-style.css\" />\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor.css\" />\n    <!-- Chart -->\n    <link rel=\"stylesheet\" href=\"https://uicdn.toast.com/chart/v4.3.4/toastui-chart.css\" />\n  </head>\n  <body>\n    <div class=\"tui-doc-description\">\n      <strong\n        >The example code can be slower than your environment because the code is transpiled by\n        babel-standalone in runtime.</strong\n      >\n      <br />\n      You can see the tutorial\n      <a\n        href=\"https://github.com/nhn/tui.editor/blob/master/docs/en/plugins.md\"\n        rel=\"noopener noreferrer\" target=\"_blank\"\n        >here</a\n      >.\n    </div>\n    <div class=\"code-html tui-doc-contents\">\n      <!-- Editor -->\n      <h2>Editor</h2>\n      <div id=\"editor\"></div>\n      <!-- Viewer Using Editor -->\n      <h2>Viewer</h2>\n      <div id=\"viewer\"></div>\n    </div>\n    <!-- Added to check demo page in Internet Explorer -->\n    <script src=\"https://unpkg.com/babel-standalone@6.26.0/babel.min.js\"></script>\n    <script src=\"./data/md-plugins.js\"></script>\n    <!-- Editor -->\n    <script src=\"../dist/cdn/toastui-editor-all.js\"></script>\n    <!-- Chart -->\n    <script src=\"https://uicdn.toast.com/chart/v4.3.4/toastui-chart.js\"></script>\n    <!-- Editor's Plugin -->\n    <script src=\"https://uicdn.toast.com/editor-plugin-chart/3.0.0/toastui-editor-plugin-chart.min.js\"></script>\n    <script type=\"text/babel\" class=\"code-js\">\n      const { Editor } = toastui;\n      const { chart } = Editor.plugin;\n\n      const chartOptions = {\n        minWidth: 100,\n        maxWidth: 600,\n        minHeight: 100,\n        maxHeight: 300\n      };\n\n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: chartContent,\n        plugins: [[chart, chartOptions]]\n      });\n\n      const viewer = Editor.factory({\n        el: document.querySelector('#viewer'),\n        viewer: true,\n        height: '500px',\n        initialValue: chartContent,\n        plugins: [[chart, chartOptions]]\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/editor/examples/example08-editor-with-code-syntax-highlight-plugin.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>8. Editor with Code Syntax Highlight Plugin</title>\n    <link rel=\"stylesheet\" href=\"./css/tuidoc-example-style.css\" />\n    <!-- Editor's Dependencies -->\n    <link\n      rel=\"stylesheet\"\n      href=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/themes/prism.min.css\"\n    />\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor.css\" />\n    <!-- Editor's Plugin -->\n    <link\n      rel=\"stylesheet\"\n      href=\"https://uicdn.toast.com/editor-plugin-code-syntax-highlight/3.0.0/toastui-editor-plugin-code-syntax-highlight.min.css\"\n    />\n  </head>\n  <body>\n    <div class=\"tui-doc-description\">\n      <strong\n        >The example code can be slower than your environment because the code is transpiled by\n        babel-standalone in runtime.</strong\n      >\n      <br />\n      You can see the tutorial\n      <a\n        href=\"https://github.com/nhn/tui.editor/blob/master/docs/en/plugins.md\"\n         rel=\"noopener noreferrer\" target=\"_blank\"\n        >here</a\n      >.\n    </div>\n    <div class=\"code-html tui-doc-contents\">\n      <!-- Editor -->\n      <h2>Editor</h2>\n      <div id=\"editor\"></div>\n      <!-- Viewer Using Editor -->\n      <h2>Viewer</h2>\n      <div id=\"viewer\"></div>\n    </div>\n    <!-- Added to check demo page in Internet Explorer -->\n    <script src=\"https://unpkg.com/babel-standalone@6.26.0/babel.min.js\"></script>\n    <script src=\"./data/md-plugins.js\"></script>\n    <!-- Editor -->\n    <script src=\"../dist/cdn/toastui-editor-all.js\"></script>\n    <!-- Editor's Plugin -->\n    <script src=\"https://uicdn.toast.com/editor-plugin-code-syntax-highlight/3.0.0/toastui-editor-plugin-code-syntax-highlight-all.min.js\"></script>\n    <script type=\"text/babel\" class=\"code-js\">\n      const { Editor } = toastui;\n      const { codeSyntaxHighlight } = Editor.plugin;\n\n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: codeContent,\n        plugins: [[codeSyntaxHighlight, { highlighter: Prism }]]\n      });\n\n      const viewer = Editor.factory({\n        el: document.querySelector('#viewer'),\n        viewer: true,\n        height: '500px',\n        initialValue: codeContent,\n        plugins: [[codeSyntaxHighlight, { highlighter: Prism }]]\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/editor/examples/example09-editor-with-color-syntax-plugin.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>9. Editor with Color Syntax Plugin</title>\n    <link rel=\"stylesheet\" href=\"./css/tuidoc-example-style.css\" />\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor.css\" />\n    <!-- Editor's Plugin -->\n    <link\n      rel=\"stylesheet\"\n      href=\"https://uicdn.toast.com/tui-color-picker/v2.2.6/tui-color-picker.css\"\n    />\n    <link\n      rel=\"stylesheet\"\n      href=\"https://uicdn.toast.com/editor-plugin-color-syntax/3.0.0/toastui-editor-plugin-color-syntax.min.css\"\n    />\n  </head>\n  <body>\n    <div class=\"tui-doc-description\">\n      <strong\n        >The example code can be slower than your environment because the code is transpiled by\n        babel-standalone in runtime.</strong\n      >\n      <br />\n      You can see the tutorial\n      <a\n        href=\"https://github.com/nhn/tui.editor/blob/master/docs/en/plugins.md\"\n        rel=\"noopener noreferrer\"\n        target=\"_blank\"\n        >here</a\n      >.\n    </div>\n    <div class=\"code-html tui-doc-contents\">\n      <div id=\"editor\"></div>\n    </div>\n    <!-- Added to check demo page in Internet Explorer -->\n    <script src=\"https://unpkg.com/babel-standalone@6.26.0/babel.min.js\"></script>\n    <script src=\"./data/md-plugins.js\"></script>\n    <!-- Editor -->\n    <script src=\"../dist/cdn/toastui-editor-all.js\"></script>\n    <!-- Editor's Plugin -->\n    <script src=\"https://uicdn.toast.com/tui-color-picker/v2.2.6/tui-color-picker.min.js\"></script>\n    <script src=\"https://uicdn.toast.com/editor-plugin-color-syntax/3.0.0/toastui-editor-plugin-color-syntax.min.js\"></script>\n    <script type=\"text/babel\" class=\"code-js\">\n      const { Editor } = toastui;\n      const { colorSyntax } = Editor.plugin;\n\n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialEditType: 'wysiwyg',\n        initialValue: 'Select some text and choose a color from the toolbar.',\n        plugins: [colorSyntax]\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/editor/examples/example10-editor-with-table-merged-cell-plugin.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>10. Editor with Table Merged Cell Plugin</title>\n    <link rel=\"stylesheet\" href=\"./css/tuidoc-example-style.css\" />\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor.css\" />\n    <link rel=\"stylesheet\" href=\"https://uicdn.toast.com/editor-plugin-table-merged-cell/3.0.0/toastui-editor-plugin-table-merged-cell.min.css\" />\n  </head>\n  <body>\n    <div class=\"tui-doc-description\">\n      <strong\n        >The example code can be slower than your environment because the code is transpiled by\n        babel-standalone in runtime.</strong\n      >\n      <br />\n      You can see the tutorial\n      <a\n        href=\"https://github.com/nhn/tui.editor/blob/master/docs/en/plugins.md\"\n        rel=\"noopener noreferrer\"\n        target=\"_blank\"\n        >here</a\n      >.\n    </div>\n    <div class=\"code-html tui-doc-contents\">\n      <!-- Editor -->\n      <h2>Editor</h2>\n      <div id=\"editor\"></div>\n      <!-- Viewer Using Editor -->\n      <h2>Viewer</h2>\n      <div id=\"viewer\"></div>\n    </div>\n    <!-- Added to check demo page in Internet Explorer -->\n    <script src=\"https://unpkg.com/babel-standalone@6.26.0/babel.min.js\"></script>\n    <script src=\"./data/md-plugins.js\"></script>\n    <!-- Editor -->\n    <script src=\"../dist/cdn/toastui-editor-all.js\"></script>\n    <!-- Editor's Plugin -->\n    <script src=\"https://uicdn.toast.com/editor-plugin-table-merged-cell/3.0.0/toastui-editor-plugin-table-merged-cell.min.js\"></script>\n    <script type=\"text/babel\" class=\"code-js\">\n      const { Editor } = toastui;\n      const { tableMergedCell } = Editor.plugin;\n\n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: tableContent,\n        plugins: [tableMergedCell]\n      });\n\n      const viewer = Editor.factory({\n        el: document.querySelector('#viewer'),\n        viewer: true,\n        height: '500px',\n        initialValue: tableContent,\n        plugins: [tableMergedCell]\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/editor/examples/example11-editor-with-uml-plugin.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>11. Editor with UML Plugin</title>\n    <link rel=\"stylesheet\" href=\"./css/tuidoc-example-style.css\" />\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor.css\" />\n  </head>\n  <body>\n    <div class=\"tui-doc-description\">\n      <strong\n        >The example code can be slower than your environment because the code is transpiled by\n        babel-standalone in runtime.</strong\n      >\n      <br />\n      You can see the tutorial\n      <a\n        href=\"https://github.com/nhn/tui.editor/blob/master/docs/en/plugins.md\"\n        rel=\"noopener noreferrer\" target=\"_blank\"\n        >here</a\n      >.\n    </div>\n    <div class=\"code-html tui-doc-contents\">\n      <!-- Editor -->\n      <h2>Editor</h2>\n      <div id=\"editor\"></div>\n      <!-- Viewer Using Editor -->\n      <h2>Viewer</h2>\n      <div id=\"viewer\"></div>\n    </div>\n    <!-- Added to check demo page in Internet Explorer -->\n    <script src=\"https://unpkg.com/babel-standalone@6.26.0/babel.min.js\"></script>\n    <script src=\"./data/md-plugins.js\"></script>\n    <!-- Editor -->\n    <script src=\"../dist/cdn/toastui-editor-all.js\"></script>\n    <!-- Editor's Plugin -->\n    <script src=\"https://uicdn.toast.com/editor-plugin-uml/3.0.0/toastui-editor-plugin-uml.min.js\"></script>\n    <script type=\"text/babel\" class=\"code-js\">\n      const { Editor } = toastui;\n      const { uml } = Editor.plugin;\n\n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: umlContent,\n        plugins: [uml]\n      });\n\n      const viewer = Editor.factory({\n        el: document.querySelector('#viewer'),\n        viewer: true,\n        height: '500px',\n        initialValue: umlContent,\n        plugins: [uml]\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/editor/examples/example12-editor-with-all-plugins.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>12. Editor with All Plugins</title>\n    <link rel=\"stylesheet\" href=\"./css/tuidoc-example-style.css\" />\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor.css\" />\n    <!-- Chart -->\n    <link rel=\"stylesheet\" href=\"https://uicdn.toast.com/chart/v4.3.4/toastui-chart.css\" />\n    <!-- Code Highlight -->\n    <link\n      rel=\"stylesheet\"\n      href=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/themes/prism.min.css\"\n    />\n    <link\n      rel=\"stylesheet\"\n      href=\"https://uicdn.toast.com/editor-plugin-code-syntax-highlight/3.0.0/toastui-editor-plugin-code-syntax-highlight.min.css\"\n    />\n    <!-- Color syntax -->\n    <link\n      rel=\"stylesheet\"\n      href=\"https://uicdn.toast.com/tui-color-picker/v2.2.6/tui-color-picker.css\"\n    />\n    <link\n      rel=\"stylesheet\"\n      href=\"https://uicdn.toast.com/editor-plugin-color-syntax/3.0.0/toastui-editor-plugin-color-syntax.min.css\"\n    />\n    <!-- Merged Table -->\n    <link rel=\"stylesheet\" href=\"https://uicdn.toast.com/editor-plugin-table-merged-cell/3.0.0/toastui-editor-plugin-table-merged-cell.min.css\" />\n  </head>\n  <body>\n    <div class=\"tui-doc-description\">\n      <strong\n        >The example code can be slower than your environment because the code is transpiled by\n        babel-standalone in runtime.</strong\n      >\n      <br />\n      You can see the tutorial\n      <a\n        href=\"https://github.com/nhn/tui.editor/blob/master/docs/en/plugins.md\"\n        rel=\"noopener noreferrer\"\n        target=\"_blank\"\n        >here</a\n      >.\n    </div>\n    <div class=\"code-html tui-doc-contents\">\n      <!-- Editor -->\n      <h2>Editor</h2>\n      <div id=\"editor\"></div>\n      <!-- Viewer Using Editor -->\n      <h2>Viewer</h2>\n      <div id=\"viewer\"></div>\n    </div>\n    <!-- Added to check demo page in Internet Explorer -->\n    <script src=\"https://unpkg.com/babel-standalone@6.26.0/babel.min.js\"></script>\n    <script src=\"./data/md-plugins.js\"></script>\n    <!-- Editor -->\n    <script src=\"../dist/cdn/toastui-editor-all.js\"></script>\n    <!-- Chart -->\n    <script src=\"https://uicdn.toast.com/chart/v4.3.4/toastui-chart.js\"></script>\n    <!-- Color Picker -->\n    <script src=\"https://uicdn.toast.com/tui-color-picker/v2.2.6/tui-color-picker.min.js\"></script>\n    <!-- Editor's Plugin -->\n    <script src=\"https://uicdn.toast.com/editor-plugin-chart/3.0.0/toastui-editor-plugin-chart.min.js\"></script>\n    <script src=\"https://uicdn.toast.com/editor-plugin-code-syntax-highlight/3.0.0/toastui-editor-plugin-code-syntax-highlight-all.min.js\"></script>\n    <script src=\"https://uicdn.toast.com/editor-plugin-color-syntax/3.0.0/toastui-editor-plugin-color-syntax.min.js\"></script>\n    <script src=\"https://uicdn.toast.com/editor-plugin-table-merged-cell/3.0.0/toastui-editor-plugin-table-merged-cell.min.js\"></script>\n    <script src=\"https://uicdn.toast.com/editor-plugin-uml/3.0.0/toastui-editor-plugin-uml.min.js\"></script>\n    <script type=\"text/babel\" class=\"code-js\">\n      const { Editor } = toastui;\n      const { chart, codeSyntaxHighlight, colorSyntax, tableMergedCell, uml } = Editor.plugin;\n\n      const chartOptions = {\n        minWidth: 100,\n        maxWidth: 600,\n        minHeight: 100,\n        maxHeight: 300\n      };\n\n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: allPluginsContent,\n        plugins: [[chart, chartOptions], [codeSyntaxHighlight, { highlighter: Prism }], colorSyntax, tableMergedCell, uml]\n      });\n\n      const viewer = Editor.factory({\n        el: document.querySelector('#viewer'),\n        viewer: true,\n        height: '500px',\n        initialValue: allPluginsContent,\n        plugins: [[chart, chartOptions], [codeSyntaxHighlight, { highlighter: Prism }], tableMergedCell, uml]\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/editor/examples/example13-creating-plugin.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>13. Creating Plugin</title>\n    <link rel=\"stylesheet\" href=\"./css/tuidoc-example-style.css\" />\n    <link\n      rel=\"stylesheet\"\n      href=\"https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css\"\n      integrity=\"sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X\"\n      crossorigin=\"anonymous\"\n    />\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor.css\" />\n  </head>\n  <body>\n    <div class=\"tui-doc-description\">\n      <strong\n        >The example code can be slower than your environment because the code is transpiled by\n        babel-standalone in runtime.</strong\n      >\n      <br />\n      You can see the tutorial\n      <a\n        href=\"https://github.com/nhn/tui.editor/blob/master/docs/en/plugins.md\"\n        rel=\"noopener noreferrer\" target=\"_blank\"\n        >here</a\n      > and \n      <a\n        href=\"https://github.com/nhn/tui.editor/blob/master/docs/en/custom-block.md\"\n        rel=\"noopener noreferrer\" target=\"_blank\"\n        >here</a\n      >.\n    </div>\n    <div class=\"code-html tui-doc-contents\">\n      <p>Note: LaText doesn't support the ie11. Please check this example in Chrome.</p>\n      <div id=\"editor\"></div>\n    </div>\n    <!-- Added to check demo page in Internet Explorer -->\n    <script src=\"https://unpkg.com/babel-standalone@6.26.0/babel.min.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/npm/latex.js/dist/latex.js\"></script>\n    <!-- Editor -->\n    <script src=\"../dist/cdn/toastui-editor-all.js\"></script>\n    <script type=\"text/babel\" class=\"code-js\">\n      const { Editor } = toastui;\n\n      // Step 1: Define the user plugin function\n      function latexPlugin() {\n        const toHTMLRenderers = {\n          latex(node) {\n            const generator = new latexjs.HtmlGenerator({ hyphenate: false });\n            const { body } = latexjs.parse(node.literal, { generator }).htmlDocument();\n\n            return [\n              { type: 'openTag', tagName: 'div', outerNewLine: true },\n              { type: 'html', content: body.innerHTML },\n              { type: 'closeTag', tagName: 'div', outerNewLine: true }\n            ];\n          },\n        }\n\n        return { toHTMLRenderers }\n      }\n\n      const content = [\n        '$$latex',\n        '\\\\documentclass{article}',\n        '\\\\begin{document}',\n        '',\n        '$',\n        'f(x) = \\\\int_{-\\\\infty}^\\\\infty \\\\hat f(\\\\xi)\\,e^{2 \\\\pi i \\\\xi x} \\, d\\\\xi',\n        '$',\n        '\\\\end{document}',\n        '$$'\n      ].join('\\n');\n\n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: content,\n        // Step 2: Set the defined plugin function as an option value\n        plugins: [latexPlugin]\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/editor/examples/example14-using-command.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>14. Using Command</title>\n    <link rel=\"stylesheet\" href=\"./css/tuidoc-example-style.css\" />\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor.css\" />\n  </head>\n  <body>\n    <div class=\"tui-doc-description\">\n      <strong\n        >The example code can be slower than your environment because the code is transpiled by\n        babel-standalone in runtime.</strong\n      >\n    </div>\n    <div class=\"code-html tui-doc-contents\">\n      <button id=\"btn\" class=\"btn\">Execute the \"Bold\" command</button>\n      <div id=\"editor\"></div>\n    </div>\n    <!-- Added to check demo page in Internet Explorer -->\n    <script src=\"https://unpkg.com/babel-standalone@6.26.0/babel.min.js\"></script>\n    <!-- Editor -->\n    <script src=\"../dist/cdn/toastui-editor-all.js\"></script>\n    <script type=\"text/babel\" class=\"code-js\">\n      const editor = new toastui.Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: 'Select this line and click the button above.'\n      });\n\n      document.getElementById('btn').addEventListener('click', () => {\n        editor.exec('bold');\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/editor/examples/example15-customizing-toolbar-buttons.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>15. Customizing Toolbar Buttons</title>\n    <link rel=\"stylesheet\" href=\"./css/tuidoc-example-style.css\" />\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor.css\" />\n    <!-- Customizing Button's Style in Example -->\n    <style type=\"text/css\">\n      .toastui-editor-defaultUI button.first {\n        color: red;\n      }\n\n      .toastui-editor-defaultUI button.last {\n        color: orange;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"tui-doc-description\">\n      <strong\n        >The example code can be slower than your environment because the code is transpiled by\n        babel-standalone in runtime.</strong\n      >\n    </div>\n    <div class=\"code-html tui-doc-contents\">\n      <div id=\"editor\"></div>\n    </div>\n    <!-- Added to check demo page in Internet Explorer -->\n    <script src=\"https://unpkg.com/babel-standalone@6.26.0/babel.min.js\"></script>\n    <!-- Editor -->\n    <script src=\"../dist/cdn/toastui-editor-all.js\"></script>\n    <script type=\"text/babel\" class=\"code-js\">\n      function createLastButton() {\n        const button = document.createElement('button');\n\n        button.className = 'toastui-editor-toolbar-icons last';\n        button.style.backgroundImage = 'none';\n        button.style.margin = '0';\n        button.innerHTML = `<i>B</i>`;\n        button.addEventListener('click', () => {\n          editor.exec('bold');\n        });\n\n        return button;\n      }\n\n      const editor = new toastui.Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: 'The first and last buttons are customized.',\n        toolbarItems: [\n          ['heading', 'bold', 'italic', 'strike'],\n          ['hr', 'quote'],\n          ['ul', 'ol', 'task', 'indent', 'outdent'],\n          ['table', 'image', 'link'],\n          ['code', 'codeblock'],\n          // Using Option: Customize the last button\n          [{\n            el: createLastButton(),\n            command: 'bold',\n            tooltip: 'Custom Bold'\n          }]\n        ]\n      });\n\n      editor.insertToolbarItem({ groupIndex: 0, itemIndex: 0 }, {\n        name: 'myItem',\n        tooltip: 'Custom Button',\n        command: 'bold',\n        text: '@',\n        className: 'toastui-editor-toolbar-icons first',\n        style: { backgroundImage: 'none' }\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/editor/examples/example16-i18n.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>16. Internationalization (i18n)</title>\n    <link rel=\"stylesheet\" href=\"./css/tuidoc-example-style.css\" />\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor.css\" />\n  </head>\n  <body>\n    <div class=\"tui-doc-description\">\n      <strong\n        >The example code can be slower than your environment because the code is transpiled by\n        babel-standalone in runtime.</strong\n      >\n      <br />\n      You can see the tutorial\n      <a\n        href=\"https://github.com/nhn/tui.editor/blob/master/docs/en/i18n.md\"\n        rel=\"noopener noreferrer\" target=\"_blank\"\n        >here</a\n      >.\n    </div>\n    <div class=\"code-html tui-doc-contents\">\n      <div id=\"editor\"></div>\n    </div>\n    <!-- Added to check demo page in Internet Explorer -->\n    <script src=\"https://unpkg.com/babel-standalone@6.26.0/babel.min.js\"></script>\n    <!-- Editor -->\n    <script src=\"../dist/cdn/toastui-editor-all.js\"></script>\n    <script src=\"../dist/cdn/i18n/ko-kr.js\"></script>\n    <script type=\"text/babel\" class=\"code-js\">\n      // In order to set the desired multilingual in ESM environment, you should import our language module as below.\n      // import '@toast-ui/editor/dist/i18n/ko-kr';\n\n      const editor = new toastui.Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: 'Hover on item above show different language tooltips.',\n        language: 'ko'\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/editor/examples/example17-placeholder.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>17. Placeholder</title>\n    <link rel=\"stylesheet\" href=\"./css/tuidoc-example-style.css\" />\n    <!-- Editor's Dependencies -->\n    <link\n      rel=\"stylesheet\"\n      href=\"https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/codemirror.css\"\n    />\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor.css\" />\n  </head>\n  <body>\n    <div class=\"tui-doc-description\">\n      <strong\n        >The example code can be slower than your environment because the code is transpiled by\n        babel-standalone in runtime.</strong\n      >\n    </div>\n    <div class=\"code-html tui-doc-contents\">\n      <div id=\"editor\"></div>\n    </div>\n    <!-- Added to check demo page in Internet Explorer -->\n    <script src=\"https://unpkg.com/babel-standalone@6.26.0/babel.min.js\"></script>\n    <script src=\"./data/md-default.js\"></script>\n    <!-- Editor -->\n    <script src=\"../dist/cdn/toastui-editor-all.js\"></script>\n    <script type=\"text/babel\" class=\"code-js\">\n      const editor = new toastui.Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        placeholder: 'Please enter text.'\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/editor/jest.config.js",
    "content": "// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst base = require('../../jest.base.config');\n\nmodule.exports = {\n  ...base,\n  testEnvironment: 'jsdom',\n  moduleNameMapper: {\n    '^@/(.*)$': '<rootDir>/src/$1',\n  },\n};\n"
  },
  {
    "path": "apps/editor/package.json",
    "content": "{\n  \"name\": \"@toast-ui/editor\",\n  \"version\": \"3.2.2\",\n  \"description\": \"GFM  Markdown Wysiwyg Editor - Productive and Extensible\",\n  \"keywords\": [\n    \"nhn\",\n    \"nhn cloud\",\n    \"toast\",\n    \"toastui\",\n    \"toast-ui\",\n    \"markdown\",\n    \"wysiwyg\",\n    \"editor\",\n    \"preview\",\n    \"gfm\"\n  ],\n  \"main\": \"dist/toastui-editor.js\",\n  \"module\": \"dist/esm/\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/esm/index.js\",\n      \"require\": \"./dist/toastui-editor.js\"\n    },\n    \"./viewer\": {\n      \"import\": \"./dist/esm/indexViewer.js\",\n      \"require\": \"./dist/toastui-editor-viewer.js\"\n    },\n    \"./dist/i18n/*\": {\n      \"import\": \"./dist/esm/i18n/*.js\",\n      \"require\": \"./dist/i18n/*.js\"\n    },\n    \"./dist/toastui-editor-viewer\": \"./dist/toastui-editor-viewer.js\",\n    \"./dist/toastui-editor.css\": \"./dist/toastui-editor.css\",\n    \"./dist/toastui-editor-viewer.css\": \"./dist/toastui-editor-viewer.css\",\n    \"./dist/toastui-editor-only.css\": \"./dist/toastui-editor-only.css\",\n    \"./dist/theme/toastui-editor-dark.css\": \"./dist/theme/toastui-editor-dark.css\",\n    \"./toastui-editor.css\": \"./dist/toastui-editor.css\",\n    \"./toastui-editor-viewer.css\": \"./dist/toastui-editor-viewer.css\",\n    \"./toastui-editor-only.css\": \"./dist/toastui-editor-only.css\",\n    \"./toastui-editor-dark.css\": \"./dist/theme/toastui-editor-dark.css\"\n  },\n  \"types\": \"types/index.d.ts\",\n  \"files\": [\n    \"dist/*.js\",\n    \"dist/*.css\",\n    \"dist/theme\",\n    \"dist/esm\",\n    \"dist/i18n\",\n    \"types\"\n  ],\n  \"author\": \"NHN Cloud FE Development Lab <dl_javascript@nhn.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/nhn/tui.editor.git\",\n    \"directory\": \"apps/editor\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/nhn/tui.editor/issues\"\n  },\n  \"homepage\": \"https://ui.toast.com\",\n  \"browserslist\": \"last 2 versions, not ie <= 10\",\n  \"scripts\": {\n    \"lint\": \"eslint .\",\n    \"test:types\": \"tsc\",\n    \"test\": \"jest --watch\",\n    \"test:ci\": \"jest\",\n    \"serve\": \"snowpack dev\",\n    \"serve:ie\": \"webpack serve\",\n    \"build:i18n\": \"cross-env webpack --config scripts/webpack.config.i18n.js && webpack --config scripts/webpack.config.i18n.js --env minify\",\n    \"build:prod\": \"cross-env webpack build && webpack build --env minify && node tsBannerGenerator.js\",\n    \"build\": \"npm run build:esm && npm run build:i18n && npm run build:prod\",\n    \"build:esm\": \"rollup -c\",\n    \"note\": \"tui-note --tag=$(git describe --tags)\",\n    \"ts2js\": \"tsc --outDir tmpdoc --sourceMap false --target ES2015 --noEmit false\",\n    \"doc:dev\": \"npm run ts2js && tuidoc --serv\",\n    \"doc\": \"npm run ts2js && tuidoc\"\n  },\n  \"devDependencies\": {\n    \"@toast-ui/release-notes\": \"^2.0.1\",\n    \"@types/dompurify\": \"2.3.3\",\n    \"cross-env\": \"^6.0.3\"\n  },\n  \"dependencies\": {\n    \"dompurify\": \"^2.3.3\",\n    \"prosemirror-commands\": \"^1.1.9\",\n    \"prosemirror-history\": \"^1.1.3\",\n    \"prosemirror-inputrules\": \"^1.1.3\",\n    \"prosemirror-keymap\": \"^1.1.4\",\n    \"prosemirror-model\": \"^1.14.1\",\n    \"prosemirror-state\": \"^1.3.4\",\n    \"prosemirror-view\": \"^1.18.7\"\n  }\n}\n"
  },
  {
    "path": "apps/editor/rollup.config.js",
    "content": "import typescript from '@rollup/plugin-typescript';\nimport commonjs from '@rollup/plugin-commonjs';\nimport { nodeResolve } from '@rollup/plugin-node-resolve';\nimport fs from 'fs';\nimport banner from 'rollup-plugin-banner';\nimport { version, author, license } from './package.json';\n\nfunction i18nEditorImportPath() {\n  return {\n    name: 'i18nEditorImportPath',\n    transform(code) {\n      return code.replace('../editorCore', '@toast-ui/editor');\n    },\n  };\n}\n\nconst fileNames = fs.readdirSync('./src/i18n');\n\nfunction createBannerPlugin(type) {\n  return banner(\n    [\n      `@toast-ui/editor${type ? ` : ${type}` : ''}`,\n      `@version ${version} | ${new Date().toDateString()}`,\n      `@author ${author}`,\n      `@license ${license}`,\n    ].join('\\n')\n  );\n}\n\nexport default [\n  // editor\n  {\n    input: 'src/esm/index.ts',\n    output: {\n      dir: 'dist/esm',\n      format: 'es',\n      sourcemap: false,\n    },\n    plugins: [typescript(), commonjs(), nodeResolve(), createBannerPlugin()],\n    external: [/^prosemirror/],\n  },\n  // viewer\n  {\n    input: 'src/esm/indexViewer.ts',\n    output: {\n      dir: 'dist/esm',\n      format: 'es',\n      sourcemap: false,\n    },\n    plugins: [typescript(), commonjs(), nodeResolve(), createBannerPlugin('viewer')],\n    external: [/^prosemirror/],\n  },\n  // i18n\n  {\n    input: fileNames.map((fileName) => `src/i18n/${fileName}`),\n    output: {\n      dir: 'dist/esm/i18n',\n      format: 'es',\n      sourcemap: false,\n    },\n    external: ['@toast-ui/editor'],\n    plugins: [\n      typescript(),\n      commonjs(),\n      nodeResolve(),\n      i18nEditorImportPath(),\n      createBannerPlugin('i18n'),\n    ],\n  },\n];\n"
  },
  {
    "path": "apps/editor/scripts/createConfigVariable.js",
    "content": "/* eslint-disable @typescript-eslint/no-var-requires */\nconst fs = require('fs');\nconst path = require('path');\nconst config = require(path.resolve(__dirname, '../tuidoc.config.json'));\nconst examples = config.examples || {};\nconst { filePath, globalErrorLogVariable } = examples;\n\n/**\n * Get Examples Url\n */\nfunction getTestUrls() {\n  if (!filePath) {\n    throw Error('not exist examples path at tuidoc.config.json');\n  }\n\n  const urlPrefix = 'http://nhn.github.io/tui.editor/latest';\n\n  const testUrls = fs.readdirSync(filePath).reduce((urls, fileName) => {\n    if (/html$/.test(fileName)) {\n      urls.push(`${urlPrefix}/${filePath}/${fileName}`);\n    }\n    return urls;\n  }, []);\n\n  fs.writeFileSync('url.txt', testUrls.join(', '));\n}\n\nfunction getGlobalVariable() {\n  if (!globalErrorLogVariable) {\n    throw Error('not exist examples path at tuidoc.config.json');\n  }\n\n  fs.writeFileSync('errorVariable.txt', String(globalErrorLogVariable));\n}\n\ngetTestUrls();\ngetGlobalVariable();\n"
  },
  {
    "path": "apps/editor/scripts/createIndexPage.js",
    "content": "/* eslint-disable @typescript-eslint/no-var-requires */\n\nconst fs = require('fs');\nconst path = require('path');\nconst directory = path.resolve(__dirname, '../examples');\n\nconst fileName = 'index.html';\n\nconst style = `\n<style>\n.wrapper {\n  padding: 60px 50px;\n  font-size: 15px;\n}\nul {\n  padding: 0;\n  list-style: none;\n  overflow: hidden;\n}\n\nli {\n  display: inline-block;\n  width: 30%;\n  overflow: hidden;\n  line-height: 2;\n}\n\na {\n  color: #555;\n  text-decoration: none;\n}\n</style>\n`;\n\nfunction writeData(dir) {\n  let data = `\n    <!DOCTYPE html>\n    <html>\n      <head><meta charset=\"utf-8\"/>${style}</head>\n      <body>\n        <div class=\"wrapper\">\n          <h1>Examples</h1>\n  `;\n\n  data += '<ul>';\n  const files = fs.readdirSync(dir);\n\n  files.forEach((item) => {\n    const p = `${directory}/${item}`;\n\n    if (fs.statSync(p).isFile()) {\n      data += `<li><a href=\"./${item}\">${item}</a></li>`;\n    }\n  });\n\n  data += '</ul></div></body></html>';\n\n  return data;\n}\n\nconst data = writeData(directory);\n\nfs.writeFile(`snowpack/examples/${fileName}`, data, 'utf8', (err) => {\n  if (err) {\n    console.error(err);\n  } else {\n    console.log('index.html is created successfully');\n  }\n});\n"
  },
  {
    "path": "apps/editor/scripts/webpack.config.i18n.js",
    "content": "/* eslint-disable @typescript-eslint/no-var-requires */\nconst path = require('path');\nconst webpack = require('webpack');\nconst entry = require('webpack-glob-entry');\nconst pkg = require('../package.json');\n\nconst TerserPlugin = require('terser-webpack-plugin');\nconst FileManagerPlugin = require('filemanager-webpack-plugin');\nconst ESLintPlugin = require('eslint-webpack-plugin');\n\nfunction getOptimizationConfig(minify) {\n  const minimizer = [];\n\n  if (minify) {\n    minimizer.push(\n      new TerserPlugin({\n        parallel: true,\n        extractComments: false,\n      })\n    );\n  }\n\n  return { minimizer };\n}\n\nfunction getEntries() {\n  const entries = entry('./src/i18n/*.ts');\n\n  delete entries['en-us'];\n  delete entries.i18n;\n\n  return entries;\n}\n\nmodule.exports = (env) => {\n  const { minify = false } = env;\n\n  return {\n    mode: 'production',\n    entry: getEntries(),\n    output: {\n      library: {\n        type: 'umd',\n      },\n      path: path.resolve(__dirname, minify ? '../dist/cdn/i18n' : '../dist/i18n'),\n      filename: `[name]${minify ? '.min' : ''}.js`,\n    },\n    externals: [\n      {\n        '../editorCore': {\n          commonjs: '@toast-ui/editor',\n          commonjs2: '@toast-ui/editor',\n          amd: '@toast-ui/editor',\n          root: ['toastui', 'Editor'],\n        },\n      },\n    ],\n    module: {\n      rules: [\n        {\n          test: /\\.ts$|\\.js$/,\n          use: [\n            {\n              loader: 'ts-loader',\n              options: {\n                transpileOnly: true,\n              },\n            },\n          ],\n          exclude: /node_modules/,\n        },\n      ],\n    },\n    plugins: [\n      new webpack.BannerPlugin(\n        [\n          'TOAST UI Editor : i18n',\n          `@version ${pkg.version}`,\n          `@author ${pkg.author}`,\n          `@license ${pkg.license}`,\n        ].join('\\n')\n      ),\n      new FileManagerPlugin({\n        events: {\n          onEnd: {\n            copy: [{ source: './dist/i18n/*.js', destination: './dist/cdn/i18n' }],\n          },\n        },\n      }),\n      new ESLintPlugin({\n        extensions: ['js', 'ts'],\n        exclude: ['node_modules', 'dist'],\n        failOnError: true,\n      }),\n    ],\n    optimization: getOptimizationConfig(minify),\n  };\n};\n"
  },
  {
    "path": "apps/editor/snowpack.config.js",
    "content": "/** @type {import(\"snowpack\").SnowpackUserConfig } */\nmodule.exports = {\n  mount: {\n    'demo/esm': '/',\n    'src/img': '/img',\n    src: '/dist',\n  },\n  devOptions: {\n    port: 8080,\n  },\n  alias: {\n    '@': './src',\n    '@t': './types',\n  },\n  workspaceRoot: '../../',\n};\n"
  },
  {
    "path": "apps/editor/src/__test__/integration/ui/layout.spec.ts",
    "content": "import { cls } from '@/utils/dom';\nimport '@/i18n/en-us';\nimport { Editor } from '@/index';\nimport { Emitter } from '@t/event';\nimport { screen } from '@testing-library/dom';\n\nconst EDITOR_CLASS = 'toastui-editor';\n\nfunction getElement(selector: string) {\n  return document.querySelector<HTMLElement>(selector)!;\n}\n\nfunction getElements(selector: string) {\n  return document.querySelectorAll<HTMLElement>(selector)!;\n}\n\nfunction getEditorMain() {\n  return getElement(`.${cls('main')}`)!;\n}\n\nfunction getMdEditor() {\n  return getElement(`.${cls('md-container')} .${EDITOR_CLASS}`)!;\n}\n\nfunction getMdPreview() {\n  return getElement(`.${cls('md-container')} .${cls('md-preview')}`)!;\n}\n\nfunction getWwEditor() {\n  return getElement(`.${cls('ww-container')} .${EDITOR_CLASS}`)!;\n}\n\nfunction getMdSwitch() {\n  return screen.getByText('Markdown')!;\n}\n\nfunction getWwSwitch() {\n  return screen.getByText('WYSIWYG')!;\n}\n\nfunction clickMdSwitch() {\n  return getMdSwitch().click();\n}\n\nfunction clickWwSwitch() {\n  return getWwSwitch().click();\n}\n\nfunction getMdWriteTab() {\n  return getElement(`.${cls('md-tab-container')} .tab-item`)!;\n}\n\nfunction getMdPreviewTab() {\n  return document.querySelectorAll<HTMLElement>(`.${cls('md-tab-container')} .tab-item`)[1];\n}\n\nfunction getScrollSyncWrapper() {\n  const scrollSync = getElement('.scroll-sync');\n\n  return scrollSync ? scrollSync.parentElement : null;\n}\n\nfunction clickMdWriteTab() {\n  return getMdWriteTab().click();\n}\n\nfunction clickMdPreviewTab() {\n  return getMdPreviewTab().click();\n}\n\nfunction assertToContainElement(el: HTMLElement) {\n  expect(document.body).toContainElement(el);\n}\n\ndescribe('layout component', () => {\n  let container: HTMLElement;\n  let editor: Editor;\n  let em: Emitter;\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    editor = new Editor({\n      el: container,\n      previewStyle: 'vertical',\n      height: '400px',\n      initialEditType: 'markdown',\n    });\n    em = editor.eventEmitter;\n    document.body.appendChild(container);\n  });\n\n  afterEach(() => {\n    editor.destroy();\n    document.body.removeChild(container);\n  });\n\n  it('render default ui properly', () => {\n    assertToContainElement(getEditorMain());\n    assertToContainElement(getMdEditor());\n    assertToContainElement(getMdPreview());\n    assertToContainElement(getWwEditor());\n    assertToContainElement(getMdSwitch());\n    assertToContainElement(getWwSwitch());\n  });\n\n  it('show/hide editor', () => {\n    const layout = getElement(`.${cls('defaultUI')}`);\n\n    editor.hide();\n    expect(layout).toHaveClass('hidden');\n\n    editor.show();\n    expect(layout).not.toHaveClass('hidden');\n  });\n\n  describe('changing editor mode', () => {\n    it('should trigger needChangeMode when clicking the switch button', () => {\n      const spy = jest.fn();\n\n      em.listen('needChangeMode', spy);\n\n      clickWwSwitch();\n      expect(spy).toHaveBeenCalledWith('wysiwyg');\n\n      clickMdSwitch();\n      expect(spy).toHaveBeenCalledWith('markdown');\n    });\n\n    it('should switch the editor in layout when changeMode is triggered', () => {\n      const editorArea = getEditorMain();\n      const mdSwitch = getMdSwitch();\n      const wwSwitch = getWwSwitch();\n\n      em.emit('changeMode', 'wysiwyg');\n\n      expect(editorArea).toHaveClass(cls('ww-mode'));\n      expect(wwSwitch).toHaveClass('active');\n      expect(mdSwitch).not.toHaveClass('active');\n\n      em.emit('changeMode', 'markdown');\n\n      expect(editorArea).toHaveClass(cls('md-mode'));\n      expect(mdSwitch).toHaveClass('active');\n      expect(wwSwitch).not.toHaveClass('active');\n    });\n\n    it('should change layout when clicking the switch button', () => {\n      const editorArea = getEditorMain();\n      const mdSwitch = getMdSwitch();\n      const wwSwitch = getWwSwitch();\n\n      clickWwSwitch();\n      expect(editorArea).toHaveClass(cls('ww-mode'));\n      expect(wwSwitch).toHaveClass('active');\n      expect(mdSwitch).not.toHaveClass('active');\n\n      clickMdSwitch();\n      expect(editorArea).toHaveClass(cls('md-mode'));\n      expect(mdSwitch).toHaveClass('active');\n      expect(wwSwitch).not.toHaveClass('active');\n    });\n\n    it('should not render scrollSync when previewStyle is tab regardless of changing editor mode', () => {\n      editor = new Editor({\n        el: container,\n        previewStyle: 'tab',\n      });\n\n      const scrollSyncWrapper = getScrollSyncWrapper();\n\n      expect(scrollSyncWrapper).toBeNull();\n\n      em.emit('changeMode', 'wysiwyg');\n\n      expect(scrollSyncWrapper).toBeNull();\n    });\n\n    // @todo It needs to break test by each event (changePreviewStyle, changeMode)\n    it('should show scrollSync when previewStyle is vertical on only markdown mode', () => {\n      const scrollSyncWrapper = getScrollSyncWrapper();\n\n      em.emit('changePreviewStyle', 'vertical');\n      expect(scrollSyncWrapper).toHaveStyle({ display: 'inline-block' });\n\n      em.emit('changeMode', 'wysiwyg');\n      expect(getElement('.scroll-sync')).toBeNull();\n    });\n\n    it('should show scrollSync when previewStyle is changed on only markdown mode', () => {\n      const scrollSyncWrapper = getScrollSyncWrapper();\n\n      em.emit('changeMode', 'wysiwyg');\n      em.emit('changePreviewStyle', 'vertical');\n      expect(getElement('.scroll-sync')).toBeNull();\n\n      em.emit('changeMode', 'markdown');\n      expect(scrollSyncWrapper).toHaveStyle({ display: 'inline-block' });\n    });\n  });\n\n  describe('changing preview style', () => {\n    it('should hide markdown tab when changePreviewStyle is triggered', () => {\n      const tabSection = getElement(`.${cls('md-tab-container')}`)!;\n\n      expect(tabSection).toHaveStyle({ display: 'none' });\n\n      em.emit('changePreviewStyle', 'tab');\n      expect(tabSection).toHaveStyle({ display: 'block' });\n    });\n\n    it('should hide markdown tab when changeMode is triggered', () => {\n      editor = new Editor({\n        el: container,\n        previewStyle: 'tab',\n        initialEditType: 'markdown',\n      });\n      em = editor.eventEmitter;\n\n      const tabSection = getElement(`.${cls('md-tab-container')}`)!;\n\n      expect(tabSection).toHaveStyle({ display: 'block' });\n\n      em.emit('changeMode', 'wysiwyg');\n      expect(tabSection).toHaveStyle({ display: 'none' });\n    });\n\n    it('should display the markdown editor or preview by clicking markdown tab', () => {\n      expect(getMdWriteTab()).toHaveClass('active');\n      expect(getMdPreviewTab()).not.toHaveClass('active');\n\n      clickMdPreviewTab();\n\n      expect(getMdWriteTab()).not.toHaveClass('active');\n      expect(getMdPreviewTab()).toHaveClass('active');\n    });\n\n    it('should emit changePreviewTabWrite, changePreviewTabPreview events by clicking markdown tab', () => {\n      const spy1 = jest.fn();\n      const spy2 = jest.fn();\n\n      em.listen('changePreviewTabWrite', spy1);\n      em.listen('changePreviewTabPreview', spy2);\n\n      clickMdPreviewTab();\n      expect(spy2).toHaveBeenCalledTimes(1);\n\n      clickMdWriteTab();\n      expect(spy1).toHaveBeenCalledTimes(1);\n    });\n\n    it('should enable/disable the toolbar items by clicking markdown tab', () => {\n      editor = new Editor({\n        el: container,\n        previewStyle: 'tab',\n        initialEditType: 'markdown',\n      });\n      const scrollSyncWrapper = getScrollSyncWrapper();\n\n      clickMdPreviewTab();\n\n      expect(scrollSyncWrapper).toBeNull();\n      expect(getElement(`.${cls('toolbar-icons')}`)).toBeDisabled();\n\n      clickMdWriteTab();\n\n      expect(scrollSyncWrapper).toBeNull();\n      expect(getElement(`.${cls('toolbar-icons')}`)).not.toBeDisabled();\n    });\n\n    it('should enable the toolbar items when changeMode is triggered', () => {\n      editor = new Editor({\n        el: container,\n        previewStyle: 'tab',\n        initialEditType: 'markdown',\n      });\n      em = editor.eventEmitter;\n\n      clickMdPreviewTab();\n\n      em.emit('changeMode', 'wysiwyg');\n      expect(getElement(`.${cls('toolbar-icons')}`)).not.toBeDisabled();\n\n      em.emit('changeMode', 'markdown');\n      expect(getElement(`.${cls('toolbar-icons')}`)).not.toBeDisabled();\n      expect(getMdWriteTab()).toHaveClass('active');\n    });\n\n    it('should enable the toolbar items when changePreviewStyle is triggered', () => {\n      clickMdPreviewTab();\n      em.emit('changePreviewStyle', 'vertical');\n      expect(getElement(`.${cls('toolbar-icons')}`)).not.toBeDisabled();\n    });\n  });\n\n  describe('context menu', () => {\n    it('should be displayed when contextmenu event is triggered', () => {\n      const contextMenu = getElement(`.${cls('context-menu')}`);\n\n      expect(contextMenu).toHaveStyle({ display: 'none' });\n      em.emit('contextmenu', { pos: { left: 10, top: 10 }, menuGroups: [[{ label: 'test' }]] });\n      expect(contextMenu).toHaveStyle({ display: 'block' });\n    });\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/integration/ui/toolbar.spec.ts",
    "content": "import { cls } from '@/utils/dom';\nimport { fireEvent, getByLabelText, getByText, screen } from '@testing-library/dom';\nimport { Editor } from '@/index';\nimport '@/i18n/en-us';\n\nfunction getElement(selector: string) {\n  return document.querySelector<HTMLElement>(selector)!;\n}\n\nfunction getPopUpElement() {\n  return getElement(`.${cls('popup')}`);\n}\n\nfunction fireMousemoveEvent(el: HTMLElement, x: number, y: number) {\n  const event = new MouseEvent('mousemove', {\n    bubbles: true,\n    cancelable: true,\n  });\n\n  // @ts-ignore\n  event.pageX = x;\n  // @ts-ignore\n  event.pageY = y;\n\n  fireEvent(el, event);\n}\n\nfunction fireMouseoverEvent(el: HTMLElement) {\n  const mouseover = new MouseEvent('mouseover', {\n    bubbles: true,\n    cancelable: true,\n  });\n\n  fireEvent(el, mouseover);\n}\n\ndescribe('Default toolbar', () => {\n  let el: HTMLDivElement, editor: Editor;\n\n  beforeEach(() => {\n    el = document.createElement('div');\n\n    editor = new Editor({\n      el,\n      previewStyle: 'vertical',\n      height: '400px',\n      initialEditType: 'markdown',\n    });\n\n    document.body.appendChild(el);\n  });\n\n  afterEach(() => {\n    editor.destroy();\n    document.body.removeChild(el);\n  });\n\n  it('should be rendered properly', () => {\n    [\n      'Headings',\n      'Bold',\n      'Italic',\n      'Italic',\n      'Line',\n      'Blockquote',\n      'Unordered list',\n      'Ordered list',\n      'Task',\n      'Indent',\n      'Outdent',\n      'Insert table',\n      'Insert image',\n      'Insert link',\n      'Inline code',\n      'Insert codeBlock',\n    ].forEach((label) => {\n      expect(screen.queryByLabelText(label)).not.toBeNull();\n    });\n\n    expect(document.body).toContainElement(getElement('.scroll-sync'));\n  });\n\n  it('should trigger command event when clicking toolbar button', () => {\n    const spy = jest.fn();\n\n    editor.eventEmitter.listen('command', spy);\n    screen.getByLabelText('Bold').click();\n\n    // eslint-disable-next-line no-undefined\n    expect(spy).toHaveBeenCalledWith('bold', undefined);\n  });\n\n  it('should show tooltip when mouseover on toolbar button', () => {\n    fireMouseoverEvent(screen.getByLabelText('Headings'));\n\n    const tooltip = screen.getByText('Headings').parentElement;\n\n    expect(tooltip).toHaveStyle({ display: 'block' });\n    expect(tooltip).toHaveClass(cls('tooltip'));\n  });\n\n  describe('scroll sync button', () => {\n    it('should toggle active state when clicking scroll sync button', () => {\n      const scrollSyncSwitch = getElement('.scroll-sync');\n\n      expect(scrollSyncSwitch).toHaveClass('active');\n\n      scrollSyncSwitch.click();\n\n      expect(scrollSyncSwitch).not.toHaveClass('active');\n    });\n\n    it('should trigger command event with state', () => {\n      const spy = jest.fn();\n\n      editor.eventEmitter.listen('command', spy);\n\n      getElement('.scroll-sync').click();\n\n      expect(spy).toHaveBeenCalledWith('toggleScrollSync', { active: false });\n\n      getElement('.scroll-sync').click();\n\n      expect(spy).toHaveBeenCalledWith('toggleScrollSync', { active: true });\n    });\n  });\n\n  describe('Headings button', () => {\n    let headingPopup: HTMLElement;\n    let hedingButton: HTMLElement;\n\n    beforeEach(() => {\n      headingPopup = getPopUpElement();\n      hedingButton = screen.getByLabelText('Headings');\n\n      hedingButton.click();\n    });\n\n    it('should show the popup when clicking Headings button', () => {\n      expect(headingPopup).toHaveClass(cls('popup-add-heading'));\n      expect(headingPopup).toHaveStyle({ display: 'block' });\n    });\n\n    ['1', '2', '3', '4', '5', '6'].forEach((level) => {\n      const mdHeadingOfLevel = '#'.repeat(parseInt(level, 10));\n\n      it(`should active heading button when click heading level ${level}`, () => {\n        getByText(headingPopup, `Heading ${level}`).click();\n\n        expect(hedingButton).toHaveClass('active');\n      });\n\n      it(`should add heading to document when click heading level ${level}`, () => {\n        getByText(headingPopup, `Heading ${level}`).click();\n\n        expect(editor.getMarkdown()).toBe(`${mdHeadingOfLevel} `);\n      });\n    });\n  });\n\n  describe('link button', () => {\n    let linkPopup: HTMLElement;\n    let linkButton: HTMLElement;\n\n    beforeEach(() => {\n      linkPopup = getPopUpElement();\n      linkButton = screen.getByLabelText('Insert link');\n\n      linkButton.click();\n    });\n\n    it('should show the popup when clicking link button', () => {\n      expect(linkPopup).toHaveClass(cls('popup-add-link'));\n      expect(linkPopup).toHaveStyle({ display: 'block' });\n    });\n\n    it('should hide popup when clicking Cancel button', () => {\n      const closeBtn = getByText(linkPopup, 'Cancel');\n\n      closeBtn.click();\n\n      expect(linkPopup).toHaveStyle({ display: 'none' });\n    });\n\n    it('should add link to document when clicking OK button', () => {\n      const urlText = getByText(linkPopup, 'URL').nextElementSibling as HTMLInputElement;\n      const linkText = getByText(linkPopup, 'Link text').nextElementSibling as HTMLInputElement;\n      const OkBtn = getByText(linkPopup, 'OK');\n\n      urlText.value = 'https://ui.toast.com';\n      linkText.value = 'toastui';\n\n      OkBtn.click();\n\n      expect(editor.getMarkdown()).toBe('[toastui](https://ui.toast.com)');\n    });\n\n    it('should add wrong class when url or text are not filled out', () => {\n      const urlText = getByText(linkPopup, 'URL').nextElementSibling as HTMLInputElement;\n      const linkText = getByText(linkPopup, 'Link text').nextElementSibling as HTMLInputElement;\n      const OkBtn = getByText(linkPopup, 'OK');\n\n      OkBtn.click();\n\n      expect(urlText).toHaveClass('wrong');\n\n      urlText.value = 'https://ui.toast.com';\n      OkBtn.click();\n\n      expect(linkText).toHaveClass('wrong');\n    });\n  });\n\n  describe('image button', () => {\n    let imagePopup: HTMLElement;\n    let imageButton: HTMLElement;\n\n    beforeEach(() => {\n      imagePopup = getPopUpElement();\n      imageButton = screen.getByLabelText('Insert image');\n\n      imageButton.click();\n    });\n\n    it('should show the popup when clicking image button', () => {\n      expect(imagePopup).toHaveClass(cls('popup-add-image'));\n      expect(imagePopup).toHaveStyle({ display: 'block' });\n    });\n\n    it('should hide popup when clicking Cancel button', () => {\n      const closeBtn = getByText(imagePopup, 'Cancel');\n\n      closeBtn.click();\n\n      expect(imagePopup).toHaveStyle({ display: 'none' });\n    });\n\n    it('should toggle tab when clicking the file or url tab', () => {\n      const fileTabBtn = getByLabelText(imagePopup, 'File');\n      const urlTabBtn = getByLabelText(imagePopup, 'URL');\n\n      urlTabBtn.click();\n\n      expect(fileTabBtn).not.toHaveClass('active');\n      expect(urlTabBtn).toHaveClass('active');\n\n      fileTabBtn.click();\n\n      expect(fileTabBtn).toHaveClass('active');\n      expect(urlTabBtn).not.toHaveClass('active');\n    });\n\n    it('should add image to document when clicking OK button', () => {\n      getByLabelText(imagePopup, 'URL').click();\n\n      const urlText = getByText(imagePopup, 'Image URL').nextElementSibling as HTMLInputElement;\n      const descriptionText = getByText(imagePopup, 'Description')\n        .nextElementSibling as HTMLInputElement;\n      const OkBtn = getByText(imagePopup, 'OK');\n\n      urlText.value = 'myImageUrl';\n      descriptionText.value = 'image';\n\n      OkBtn.click();\n\n      expect(editor.getMarkdown()).toBe('![image](myImageUrl)');\n    });\n\n    it('should add wrong class when url or text are not filled out', () => {\n      const fileText = getByText(imagePopup, 'Select image file')\n        .nextElementSibling as HTMLInputElement;\n      const urlText = getByText(imagePopup, 'Image URL').nextElementSibling as HTMLInputElement;\n      const OkBtn = getByText(imagePopup, 'OK');\n\n      OkBtn.click();\n\n      expect(fileText).toHaveClass('wrong');\n\n      getByLabelText(imagePopup, 'URL').click();\n\n      OkBtn.click();\n\n      expect(urlText).toHaveClass('wrong');\n    });\n  });\n\n  describe('table button', () => {\n    let tablePopup: HTMLElement;\n    let tableButton: HTMLElement;\n\n    beforeEach(() => {\n      tablePopup = getPopUpElement();\n      tableButton = screen.getByLabelText('Insert table');\n\n      tableButton.click();\n    });\n\n    it('should show the popup when clicking table button', () => {\n      expect(tablePopup).toHaveClass(cls('popup-add-table'));\n      expect(tablePopup).toHaveStyle({ display: 'block' });\n    });\n\n    it('should add table to document when selecting the area and clicking it', () => {\n      const tableSelection = tablePopup.querySelector(`.${cls('table-selection')}`)! as HTMLElement;\n\n      fireMousemoveEvent(tableSelection, 100, 60);\n      tableSelection.click();\n\n      expect(editor.getMarkdown()).toBe(\n        '\\n|  |  |  |  |  |  |\\n| --- | --- | --- | --- | --- | --- |\\n|  |  |  |  |  |  |\\n|  |  |  |  |  |  |\\n|  |  |  |  |  |  |'\n      );\n    });\n  });\n\n  it('should active indent/outdent button when only ordered or bullet list actived', () => {\n    const bulletListBtn = screen.getByLabelText('Unordered list');\n    const orderedListBtn = screen.getByLabelText('Ordered list');\n    const indentBtn = screen.getByLabelText('Indent');\n    const outdentBtn = screen.getByLabelText('Outdent');\n\n    bulletListBtn.click();\n\n    expect(indentBtn).not.toBeDisabled();\n    expect(outdentBtn).not.toBeDisabled();\n\n    orderedListBtn.click();\n\n    expect(indentBtn).not.toBeDisabled();\n    expect(outdentBtn).not.toBeDisabled();\n\n    editor.reset();\n    expect(indentBtn).toBeDisabled();\n    expect(outdentBtn).toBeDisabled();\n  });\n\n  it('should change tab mode when changing markdown tab mode', () => {\n    editor.changePreviewStyle('tab');\n    const writeTab = screen.getByLabelText('Write');\n    const previewTab = screen.getByLabelText('Preview');\n\n    previewTab.click();\n\n    expect(writeTab).not.toHaveClass('active');\n    expect(previewTab).toHaveClass('active');\n\n    writeTab.click();\n\n    expect(writeTab).toHaveClass('active');\n    expect(previewTab).not.toHaveClass('active');\n  });\n});\n\ndescribe('Custom toobar button', () => {\n  let el: HTMLDivElement, editor: Editor;\n\n  function createCustomButtonWithPopup() {\n    const body = document.createElement('select');\n\n    body.innerHTML = `\n      <option value=\"1\">1</option>\n      <option value=\"2\">2</option>\n      <option value=\"3\">3</option>\n      <option value=\"4\">4</option>\n      <option value=\"5\">5</option>\n      <option value=\"6\">6</option>\n    `;\n\n    body.addEventListener('change', (ev) => {\n      editor.eventEmitter.emit('command', 'heading', {\n        level: Number((ev.target as HTMLSelectElement).value),\n      });\n      editor.eventEmitter.emit('closePopup');\n      (ev.target as HTMLSelectElement).value = '1';\n    });\n\n    return {\n      name: 'myToolbarWithPopup',\n      tooltip: 'L!',\n      className: 'my-toolbar-with-popup',\n      text: 'L!',\n      style: { color: '#fff', width: 30 },\n      popup: {\n        body,\n        className: 'my-popup',\n        style: { width: 'auto' },\n      },\n    };\n  }\n\n  const customButton = {\n    name: 'myToolbar',\n    tooltip: 'B!',\n    className: 'my-toolbar',\n    command: 'bold',\n    text: 'B!',\n    style: { color: '#222', width: 40 },\n  };\n  const customButtonWithPopup = createCustomButtonWithPopup();\n\n  beforeEach(() => {\n    el = document.createElement('div');\n\n    editor = new Editor({\n      el,\n      previewStyle: 'vertical',\n      height: '400px',\n      initialEditType: 'markdown',\n      toolbarItems: [[customButton, customButtonWithPopup]],\n    });\n\n    document.body.appendChild(el);\n  });\n\n  afterEach(() => {\n    editor.destroy();\n    document.body.removeChild(el);\n  });\n\n  it('should be rendered properly', () => {\n    const customToolbar1 = screen.getByLabelText('B!');\n    const customToolbar2 = screen.getByLabelText('L!');\n\n    expect(customToolbar1).toHaveTextContent('B!');\n    expect(customToolbar1).toHaveStyle({ color: '#222', width: '40px' });\n\n    expect(customToolbar2).toHaveTextContent('L!');\n    expect(customToolbar2).toHaveStyle({ color: '#fff', width: '30px' });\n  });\n\n  it('should show tooltip when mouseover on toolbar button', () => {\n    fireMouseoverEvent(screen.getByLabelText('B!'));\n\n    const tooltip = getElement(`.${cls('tooltip')}`);\n\n    expect(tooltip).toHaveStyle({ display: 'block' });\n    expect(tooltip).toHaveTextContent('B!');\n  });\n\n  it('should add text that matched command to document event when clicking button', () => {\n    screen.getByLabelText('B!').click();\n\n    expect(editor.getMarkdown()).toBe('****');\n  });\n\n  it('should show the popup when clicking button with popup option', () => {\n    screen.getByLabelText('L!').click();\n\n    const customPopup = getElement('.my-popup');\n\n    expect(customPopup).toHaveStyle({ display: 'block', width: 'auto' });\n    expect(customPopup).toHaveClass('my-popup');\n  });\n\n  it('should operate properly when event is triggered in popup', () => {\n    screen.getByLabelText('L!').click();\n\n    const customPopup = getElement('.my-popup');\n    const select = customPopup.querySelector('select')!;\n\n    select.value = '3';\n    fireEvent(select, new Event('change'));\n\n    expect(editor.getMarkdown()).toBe('### ');\n  });\n});\n\ndescribe('API', () => {\n  function getToolbarItems() {\n    return getElement(`.${cls('defaultUI-toolbar')}`).querySelectorAll('button:not(.more)');\n  }\n\n  let el: HTMLDivElement, editor: Editor;\n\n  beforeEach(() => {\n    const toolbarItems = [['heading', 'bold', 'italic', 'strike']];\n\n    el = document.createElement('div');\n\n    editor = new Editor({\n      el,\n      previewStyle: 'vertical',\n      height: '400px',\n      initialEditType: 'markdown',\n      toolbarItems,\n    });\n\n    document.body.appendChild(el);\n  });\n\n  afterEach(() => {\n    editor.destroy();\n    document.body.removeChild(el);\n  });\n\n  it('should insert item on calling insertToolbarItem', () => {\n    editor.insertToolbarItem({ groupIndex: 0, itemIndex: 1 }, 'ol');\n\n    const toolbarItems = getToolbarItems();\n\n    expect(toolbarItems[0]).toHaveClass('heading');\n    expect(toolbarItems[1]).toHaveClass('ordered-list');\n    expect(toolbarItems[2]).toHaveClass('bold');\n    expect(toolbarItems[3]).toHaveClass('italic');\n    expect(toolbarItems[4]).toHaveClass('strike');\n    // should have same parent because the toolbar is added to same group\n    expect(toolbarItems[1].parentElement).toEqual(toolbarItems[2].parentElement);\n  });\n\n  it('should add item on calling insertToolbarItem', () => {\n    editor.insertToolbarItem({ groupIndex: 1, itemIndex: 1 }, 'ol');\n\n    const toolbarItems = getToolbarItems();\n\n    expect(toolbarItems[0]).toHaveClass('heading');\n    expect(toolbarItems[1]).toHaveClass('bold');\n    expect(toolbarItems[2]).toHaveClass('italic');\n    expect(toolbarItems[3]).toHaveClass('strike');\n    expect(toolbarItems[4]).toHaveClass('ordered-list');\n    // should have different parent because the toolbar is added to another group\n    expect(toolbarItems[3].parentElement).not.toEqual(toolbarItems[4].parentElement);\n  });\n\n  it('should insert custom toolbar item on calling insertToolbarItem', () => {\n    const customButton = {\n      name: 'myToolbar',\n      tooltip: 'B!',\n      className: 'my-toolbar',\n      command: 'bold',\n      text: 'B!',\n      style: { color: '#222', width: 40 },\n    };\n\n    editor.insertToolbarItem({ groupIndex: 0, itemIndex: 1 }, customButton);\n\n    const toolbarItems = getToolbarItems();\n\n    expect(toolbarItems[0]).toHaveClass('heading');\n\n    expect(toolbarItems[1]).toHaveClass('my-toolbar');\n    expect(toolbarItems[1]).toHaveTextContent('B!');\n    expect(toolbarItems[1]).toHaveStyle({ color: '#222', width: '40px' });\n\n    expect(toolbarItems[2]).toHaveClass('bold');\n    expect(toolbarItems[3]).toHaveClass('italic');\n    expect(toolbarItems[4]).toHaveClass('strike');\n  });\n\n  it('should remove item on calling removeToolbarItem', () => {\n    editor.removeToolbarItem('bold');\n\n    expect(screen.queryByLabelText('Bold')).toBeNull();\n  });\n});\n\ndescribe('Event', () => {\n  let el: HTMLDivElement, editor: Editor;\n\n  beforeEach(() => {\n    el = document.createElement('div');\n\n    editor = new Editor({\n      el,\n      previewStyle: 'vertical',\n      height: '400px',\n      initialEditType: 'markdown',\n    });\n\n    document.body.appendChild(el);\n  });\n\n  afterEach(() => {\n    editor.destroy();\n    document.body.removeChild(el);\n  });\n\n  describe('openPopup, closePopup', () => {\n    it('should open and close popup corresponding to name', () => {\n      editor.eventEmitter.emit('openPopup', 'image');\n\n      const imagePopup = getElement(`.${cls('popup-add-image')}`);\n\n      expect(imagePopup).toHaveStyle({ display: 'block' });\n\n      editor.eventEmitter.emit('closePopup');\n\n      expect(imagePopup).toHaveStyle({ display: 'none' });\n    });\n\n    it('should render popup with initial values', () => {\n      const initialValues = { linkUrl: 'http://test.com', linkText: 'foo' };\n\n      editor.eventEmitter.emit('openPopup', 'link', initialValues);\n\n      const urlText = screen.getByText('URL').nextElementSibling as HTMLInputElement;\n      const linkText = screen.getByText('Link text').nextElementSibling as HTMLInputElement;\n\n      expect(urlText).toHaveValue('http://test.com');\n      expect(linkText).toHaveValue('foo');\n    });\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/integration/vdom/render.spec.ts",
    "content": "import { oneLineTrim } from 'common-tags';\nimport { render } from '@/ui/vdom/renderer';\nimport { Component } from '@/ui/vdom/component';\nimport { VNode } from '@/ui/vdom/vnode';\nimport html from '@/ui/vdom/template';\n\ninterface Props {\n  mounted?: jest.Mock;\n  updated?: jest.Mock;\n  beforeDestroy?: jest.Mock;\n  refDOM?: jest.Mock<any, [HTMLElement]>;\n}\n\ninterface State {\n  hide: boolean;\n  conditional: boolean;\n}\n\nclass TestComponent extends Component<Props, State> {\n  constructor(props: Props) {\n    super(props);\n    this.state = {\n      hide: false,\n      conditional: true,\n    };\n  }\n\n  show() {\n    this.setState({ hide: false });\n  }\n\n  hide() {\n    this.setState({ hide: true });\n  }\n\n  conditionalRender() {\n    this.setState({ conditional: false });\n  }\n\n  mounted() {\n    if (this.props.mounted) {\n      this.props.mounted();\n    }\n  }\n\n  updated() {\n    if (this.props.updated) {\n      this.props.updated();\n    }\n  }\n\n  beforeDestroy() {\n    if (this.props.beforeDestroy) {\n      this.props.beforeDestroy();\n    }\n  }\n\n  render() {\n    const style = {\n      display: this.state.hide ? 'none' : 'block',\n    };\n\n    return html`\n      <div\n        class=\"my-comp\"\n        ref=${(el: HTMLElement) => {\n          if (this.props.refDOM) {\n            this.props.refDOM(el);\n          }\n        }}\n      >\n        <div style=${style}>child</div>\n        <div>\n          ${this.state.conditional ? [1, 2, 3].map((num) => html`<span>${num}</span>`) : null}\n        </div>\n        ${this.state.conditional ? [1, 2, 3].map((num) => html`<span>${num}</span>`) : null}\n        <button onClick=${() => this.show()}>show</button>\n        <button onClick=${() => this.hide()}>hide</button>\n        <button onClick=${() => this.conditionalRender()}>conditional</button>\n      </div>\n    `;\n  }\n}\n\nlet container: HTMLElement, destroy: () => void;\n\ndescribe('html', () => {\n  it('should be rendered properly', () => {\n    const wrapper = document.createElement('div');\n\n    render(wrapper, html`<div class=\"my-comp\" data-id=\"my-comp\">test</div>` as VNode);\n\n    expect(wrapper).toContainHTML('<div class=\"my-comp\" data-id=\"my-comp\">test</div>');\n  });\n\n  it('list children should be rendered properly', () => {\n    const wrapper = document.createElement('div');\n    const expected = oneLineTrim`\n      <div>\n        <span>1</span>\n        <span>2</span>\n        <span>3</span>\n      </div>\n    `;\n\n    render(\n      wrapper,\n      html`<div>${[1, 2, 3].map((text) => html`<span>${text}</span>`)}</div>` as VNode\n    );\n\n    expect(wrapper).toContainHTML(expected);\n  });\n\n  it('nested vnode should be rendered properly', () => {\n    const wrapper = document.createElement('div');\n    const expected = oneLineTrim`\n      <div class=\"my-comp\" data-id=\"my-comp\">\n        <nav>\n          <ul>\n            <li>1</li>\n            <li>2</li>\n            <li>3</li>\n          </ul>\n        </nav>\n      </div>\n    `;\n\n    render(\n      wrapper,\n      html`\n        <div class=\"my-comp\" data-id=\"my-comp\">\n          <nav>\n            <ul>\n              ${['1', '2', '3'].map((text) => html`<li>${text}</li>`)}\n            </ul>\n          </nav>\n        </div>\n      ` as VNode\n    );\n\n    expect(wrapper).toContainHTML(expected);\n  });\n\n  it('should be rendered with style object properly', () => {\n    const wrapper = document.createElement('div');\n    const style = { display: 'inline-block', backgroundColor: '#ccc' };\n    const expected = oneLineTrim`\n      <div class=\"my-comp\" style=\"display: inline-block; background-color: rgb(204, 204, 204);\">test</div>\n    `;\n\n    render(wrapper, html`<div class=\"my-comp\" style=${style}>test</div>` as VNode);\n\n    expect(wrapper).toContainHTML(expected);\n  });\n\n  it('should be rendered with pixel added automatically', () => {\n    const wrapper = document.createElement('div');\n    const style = { position: 'absolute', top: 10, left: 10 };\n    const expected = oneLineTrim`\n      <div class=\"my-comp\" style=\"position: absolute; top: 10px; left: 10px;\">test</div>\n    `;\n\n    render(wrapper, html`<div class=\"my-comp\" style=${style}>test</div>` as VNode);\n\n    expect(wrapper).toContainHTML(expected);\n  });\n});\n\ndescribe('Class Component', () => {\n  function clickShowBtn() {\n    container.querySelector('button')!.click();\n  }\n\n  function clickHideBtn() {\n    container.querySelectorAll('button')[1].click();\n  }\n\n  function clickConditionalBtn() {\n    container.querySelectorAll('button')[2].click();\n  }\n\n  function renderComponent(spies?: Record<string, jest.Mock>) {\n    container = document.createElement('div');\n\n    destroy = render(container, html`<${TestComponent} ...${spies} />` as VNode);\n  }\n\n  it('should be rendered properly', () => {\n    renderComponent();\n\n    const expected = oneLineTrim`\n      <div class=\"my-comp\">\n        <div style=\"display: block;\">child</div>\n        <div>\n          <span>1</span>\n          <span>2</span>\n          <span>3</span>\n        </div>\n        <span>1</span>\n        <span>2</span>\n        <span>3</span>\n        <button>show</button>\n        <button>hide</button>\n        <button>conditional</button>\n      </div>\n    `;\n\n    expect(container).toContainHTML(expected);\n  });\n\n  it('should be updated by event', () => {\n    renderComponent();\n    clickHideBtn();\n\n    let expected = oneLineTrim`\n      <div class=\"my-comp\">\n        <div style=\"display: none;\">child</div>\n        <div>\n          <span>1</span>\n          <span>2</span>\n          <span>3</span>\n        </div>\n        <span>1</span>\n        <span>2</span>\n        <span>3</span>\n        <button>show</button>\n        <button>hide</button>\n        <button>conditional</button>\n      </div>\n    `;\n\n    expect(container).toContainHTML(expected);\n\n    clickShowBtn();\n\n    expected = oneLineTrim`\n      <div class=\"my-comp\">\n        <div style=\"display: block;\">child</div>\n        <div>\n          <span>1</span>\n          <span>2</span>\n          <span>3</span>\n        </div>\n        <span>1</span>\n        <span>2</span>\n        <span>3</span>\n        <button>show</button>\n        <button>hide</button>\n        <button>conditional</button>\n      </div>\n    `;\n\n    expect(container).toContainHTML(expected);\n  });\n\n  it('should call ref function with DOM after rendering the component', () => {\n    const spy = jest.fn();\n\n    renderComponent({ refDOM: spy });\n\n    expect(spy).toHaveBeenCalledWith(container.querySelector('.my-comp'));\n  });\n\n  it('should call ref function with component after rendering the component', () => {\n    const spy = jest.fn();\n\n    renderComponent({ ref: spy });\n\n    expect(spy).toHaveBeenCalledTimes(1);\n  });\n\n  it('should call mounted life cycle method ', () => {\n    const spy = jest.fn();\n\n    renderComponent({ mounted: spy });\n\n    expect(spy).toHaveBeenCalledTimes(1);\n  });\n\n  it('should call mounted life cycle method once', () => {\n    const spy = jest.fn();\n\n    renderComponent({ mounted: spy });\n    clickHideBtn();\n\n    expect(spy).toHaveBeenCalledTimes(1);\n  });\n\n  it('should call updated life cycle method after component is updated', () => {\n    const spy = jest.fn();\n\n    renderComponent({ updated: spy });\n\n    clickHideBtn();\n\n    expect(spy).toHaveBeenCalledTimes(1);\n  });\n\n  it('should call beforeDestroy life cycle method after component is destroyed', () => {\n    const spy = jest.fn();\n\n    renderComponent({ beforeDestroy: spy });\n\n    destroy();\n\n    expect(spy).toHaveBeenCalledTimes(1);\n    expect(container).toContainHTML('');\n  });\n\n  it('should render conditional children components', () => {\n    renderComponent();\n\n    let expected = oneLineTrim`\n      <div class=\"my-comp\">\n        <div style=\"display: block;\">child</div>\n        <div>\n          <span>1</span>\n          <span>2</span>\n          <span>3</span>\n        </div>\n        <span>1</span>\n        <span>2</span>\n        <span>3</span>\n        <button>show</button>\n        <button>hide</button>\n        <button>conditional</button>\n      </div>\n    `;\n\n    expect(container).toContainHTML(expected);\n\n    clickConditionalBtn();\n\n    expected = oneLineTrim`\n      <div class=\"my-comp\">\n        <div style=\"display: block;\">child</div>\n        <div></div>\n        <button>show</button>\n        <button>hide</button>\n        <button>conditional</button>\n      </div>\n    `;\n\n    expect(container).toContainHTML(expected);\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/integration/widget/widgetNode.spec.ts",
    "content": "import { oneLineTrim } from 'common-tags';\nimport Editor from '@/editorCore';\nimport { cls } from '@/utils/dom';\nimport { removeDataAttr } from '@/__test__/unit/markdown/util';\n\ndescribe('widgetNode', () => {\n  let container: HTMLElement,\n    mdEditor: HTMLElement,\n    mdPreview: HTMLElement,\n    wwEditor: HTMLElement,\n    editor: Editor;\n\n  function getPreviewHTML() {\n    return removeDataAttr(mdPreview.querySelector(`.${cls('contents')}`)!.innerHTML);\n  }\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    editor = new Editor({\n      el: container,\n      widgetRules: [\n        {\n          rule: /@\\S+/,\n          toDOM(text) {\n            const span = document.createElement('span');\n\n            span.innerHTML = `<a href=\"www.google.com\">${text}</a>`;\n            return span;\n          },\n        },\n        {\n          rule: /\\[(#\\S+)\\]\\((\\S+)\\)/,\n          toDOM: (text) => {\n            const rule = /\\[(#\\S+)\\]\\((\\S+)\\)/;\n            const matched = text.match(rule)!;\n            const span = document.createElement('span');\n\n            span.innerHTML = `<a href=\"${matched[2]}\">${matched[1]}</a>`;\n\n            return span;\n          },\n        },\n      ],\n      previewStyle: 'vertical',\n    });\n\n    const elements = editor.getEditorElements();\n\n    mdEditor = elements.mdEditor;\n    mdPreview = elements.mdPreview!;\n    wwEditor = elements.wwEditor!;\n\n    container.append(mdEditor);\n    container.append(mdPreview!);\n    container.append(wwEditor!);\n\n    document.body.appendChild(container);\n  });\n\n  afterEach(() => {\n    editor.destroy();\n    document.body.removeChild(container);\n  });\n\n  describe('in markdown', () => {\n    it('should render widget node in the editor and preview using replaceWithWidget API', () => {\n      editor.setMarkdown('abc');\n      editor.replaceWithWidget([1, 1], [1, 3], '@test');\n\n      const expectedEditor = oneLineTrim`\n        <div>\n          <span class=\"tui-widget\">\n            <span><a href=\"www.google.com\">@test</a></span>\n          </span>\n          c\n        </div>\n      `;\n      const expectedPreview = oneLineTrim`\n        <p>\n          <span class=\"tui-widget\">\n            <span><a href=\"www.google.com\">@test</a></span>\n          </span>\n          c\n        </p>\n      `;\n\n      expect(mdEditor).toContainHTML(expectedEditor);\n      expect(getPreviewHTML()).toBe(expectedPreview);\n    });\n\n    it('should render widget node in the editor and preview using setMarkdown API', () => {\n      editor.setMarkdown('@test1 @test2');\n\n      const expectedEditor = oneLineTrim`\n        <span class=\"tui-widget\">\n          <span><a href=\"www.google.com\">@test1</a></span>\n        </span> \n        <span class=\"tui-widget\">\n          <span>\n            <a href=\"www.google.com\">@test2</a>\n          </span>\n        </span>\n      `;\n      const expectedPreview = oneLineTrim`\n        <p>\n          <span class=\"tui-widget\">\n            <span>\n              <a href=\"www.google.com\">@test1</a>\n            </span>\n          </span> \n          <span class=\"tui-widget\">\n            <span><a href=\"www.google.com\">@test2</a></span>\n          </span>\n        </p>\n      `;\n\n      expect(mdEditor).toContainHTML(expectedEditor);\n      expect(getPreviewHTML()).toBe(expectedPreview);\n    });\n\n    it('should render widget node in the editor and preview using insertText API', () => {\n      editor.insertText('@test1 @test2');\n\n      const expectedEditor = oneLineTrim`\n        <span class=\"tui-widget\">\n          <span><a href=\"www.google.com\">@test1</a></span>\n        </span> \n        <span class=\"tui-widget\">\n          <span>\n            <a href=\"www.google.com\">@test2</a>\n          </span>\n        </span>\n      `;\n      const expectedPreview = oneLineTrim`\n        <p>\n          <span class=\"tui-widget\">\n            <span>\n              <a href=\"www.google.com\">@test1</a>\n            </span>\n          </span> \n          <span class=\"tui-widget\">\n            <span><a href=\"www.google.com\">@test2</a></span>\n          </span>\n        </p>\n      `;\n\n      expect(mdEditor).toContainHTML(expectedEditor);\n      expect(getPreviewHTML()).toBe(expectedPreview);\n    });\n\n    it('should render widget node with markdown text', () => {\n      editor.replaceWithWidget([1, 1], [1, 1], '[#toast](ui.toast.com)');\n\n      const expectedEditor = oneLineTrim`\n        <span class=\"tui-widget\">\n          <span><a href=\"ui.toast.com\">#toast</a></span>\n        </span>\n      `;\n      const expectedPreview = oneLineTrim`\n        <p>\n          <span class=\"tui-widget\">\n            <span><a href=\"ui.toast.com\">#toast</a></span>\n          </span>\n        </p>\n      `;\n\n      expect(mdEditor).toContainHTML(expectedEditor);\n      expect(getPreviewHTML()).toBe(expectedPreview);\n    });\n\n    it('should render widget node using all widget rules', () => {\n      editor.insertText('@test1 [#toast](ui.toast.com) @test2');\n\n      const expectedEditor = oneLineTrim`\n        <span class=\"tui-widget\">\n          <span><a href=\"www.google.com\">@test1</a></span>\n        </span> \n        <span class=\"tui-widget\">\n          <span><a href=\"ui.toast.com\">#toast</a></span>\n        </span> \n        <span class=\"tui-widget\">\n          <span>\n            <a href=\"www.google.com\">@test2</a>\n          </span>\n        </span>\n      `;\n      const expectedPreview = oneLineTrim`\n        <p>\n          <span class=\"tui-widget\">\n            <span>\n              <a href=\"www.google.com\">@test1</a>\n            </span>\n          </span> \n          <span class=\"tui-widget\">\n            <span><a href=\"ui.toast.com\">#toast</a></span>\n          </span> \n          <span class=\"tui-widget\">\n            <span><a href=\"www.google.com\">@test2</a></span>\n          </span>\n        </p>\n      `;\n\n      expect(mdEditor).toContainHTML(expectedEditor);\n      expect(getPreviewHTML()).toBe(expectedPreview);\n    });\n\n    it('should convert to wysiwyg properly', () => {\n      editor.setMarkdown('@test1 @test2');\n      editor.changeMode('wysiwyg');\n\n      const expectedEditor = oneLineTrim`\n        <span class=\"tui-widget\">\n          <span><a href=\"www.google.com\">@test1</a></span>\n        </span> \n        <span class=\"tui-widget\">\n          <span>\n            <a href=\"www.google.com\">@test2</a>\n          </span>\n        </span>\n      `;\n\n      expect(wwEditor).toContainHTML(expectedEditor);\n    });\n\n    it('should keep \"$\" character in case of plain text other than widget node', () => {\n      editor.setMarkdown('@test1 $$myText @test2');\n\n      const expectedEditor = oneLineTrim`\n        <span class=\"tui-widget\">\n          <span><a href=\"www.google.com\">@test1</a></span>\n        </span> \n        $$myText \n        <span class=\"tui-widget\">\n          <span>\n            <a href=\"www.google.com\">@test2</a>\n          </span>\n        </span>\n      `;\n      const expectedPreview = oneLineTrim`\n        <p>\n          <span class=\"tui-widget\">\n            <span>\n              <a href=\"www.google.com\">@test1</a>\n            </span>\n          </span> \n          $$myText \n          <span class=\"tui-widget\">\n            <span><a href=\"www.google.com\">@test2</a></span>\n          </span>\n        </p>\n      `;\n\n      expect(mdEditor).toContainHTML(expectedEditor);\n      expect(getPreviewHTML()).toBe(expectedPreview);\n    });\n\n    it('should render widget node in the editor and preview using replaceSelection API', () => {\n      editor.setMarkdown('widgetNode: ');\n\n      editor.replaceSelection('@test1 @test2', [1, 1], [1, 13]);\n\n      const expectedEditor = oneLineTrim`\n        <span class=\"tui-widget\">\n          <span>\n            <a href=\"www.google.com\">@test1</a>\n          </span>\n        </span> \n        <span class=\"tui-widget\">\n          <span>\n            <a href=\"www.google.com\">@test2</a>\n          </span>\n        </span>\n      `;\n      const expectedPreview = oneLineTrim`\n        <p>\n          <span class=\"tui-widget\">\n            <span>\n              <a href=\"www.google.com\">@test1</a>\n            </span>\n          </span> \n          <span class=\"tui-widget\">\n            <span>\n              <a href=\"www.google.com\">@test2</a>\n            </span>\n          </span>\n        </p>\n      `;\n\n      expect(mdEditor).toContainHTML(expectedEditor);\n      expect(getPreviewHTML()).toBe(expectedPreview);\n    });\n\n    it('should return the markdown text without widget syntax through calling getMarkdown() API', () => {\n      const markdownText = oneLineTrim`\n        Brand site: [#toast](https://ui.toast.com), editor: [#toastui-editor](https://github.com/nhn/tui.editor)\\n\n        The Toastui-editor...\n      `;\n\n      editor.setMarkdown(markdownText);\n\n      expect(editor.getMarkdown()).toBe(markdownText);\n    });\n  });\n\n  describe('in wysiwyg', () => {\n    it('should render widget node in the editor using replaceWithWidget API', () => {\n      editor.changeMode('wysiwyg');\n      editor.replaceWithWidget(1, 1, '@test');\n\n      const expectedEditor = oneLineTrim`\n        <span class=\"tui-widget\">\n          <span><a href=\"www.google.com\">@test</a></span>\n        </span>\n      `;\n\n      expect(wwEditor).toContainHTML(expectedEditor);\n    });\n\n    it('should render widget node with markdown text', () => {\n      editor.changeMode('wysiwyg');\n      editor.replaceWithWidget(1, 1, '[#toast](ui.toast.com)');\n\n      const expectedEditor = oneLineTrim`\n        <span class=\"tui-widget\">\n          <span><a href=\"ui.toast.com\">#toast</a></span>\n        </span>\n      `;\n\n      expect(wwEditor).toContainHTML(expectedEditor);\n    });\n\n    it('should convert to markdown properly', () => {\n      editor.changeMode('wysiwyg');\n      editor.replaceWithWidget(1, 1, '@test1 @test2');\n      editor.changeMode('markdown');\n\n      const expectedEditor = oneLineTrim`\n        <span class=\"tui-widget\">\n          <span><a href=\"www.google.com\">@test1</a></span>\n        </span> \n        <span class=\"tui-widget\">\n          <span>\n            <a href=\"www.google.com\">@test2</a>\n          </span>\n        </span>\n      `;\n      const expectedPreview = oneLineTrim`\n        <p>\n          <span class=\"tui-widget\">\n            <span>\n              <a href=\"www.google.com\">@test1</a>\n            </span>\n          </span> \n          <span class=\"tui-widget\">\n            <span><a href=\"www.google.com\">@test2</a></span>\n          </span>\n        </p>\n      `;\n\n      expect(mdEditor).toContainHTML(expectedEditor);\n      expect(getPreviewHTML()).toBe(expectedPreview);\n    });\n\n    it('should render widget node in the editor using replaceSelection API', () => {\n      editor.setMarkdown('widgetNode:');\n      editor.changeMode('wysiwyg');\n\n      editor.replaceSelection('@test1 @test2', 1, 12);\n\n      const expectedEditor = oneLineTrim`\n        <span class=\"tui-widget\">\n          <span>\n            <a href=\"www.google.com\">@test1</a>\n          </span>\n        </span> \n        <span class=\"tui-widget\">\n          <span>\n            <a href=\"www.google.com\">@test2</a>\n          </span>\n        </span>\n      `;\n\n      expect(wwEditor).toContainHTML(expectedEditor);\n    });\n\n    it('should render widget node in the editor using insertText API', () => {\n      editor.changeMode('wysiwyg');\n\n      editor.insertText('@test1 @test2');\n\n      const expectedEditor = oneLineTrim`\n        <span class=\"tui-widget\">\n          <span><a href=\"www.google.com\">@test1</a></span>\n        </span> \n        <span class=\"tui-widget\">\n          <span>\n            <a href=\"www.google.com\">@test2</a>\n          </span>\n        </span>\n      `;\n\n      expect(wwEditor).toContainHTML(expectedEditor);\n    });\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/convertor.spec.ts",
    "content": "import { source, oneLineTrim } from 'common-tags';\n\nimport { Context, MdNode, Parser, HTMLConvertorMap } from '@toast-ui/toastmark';\n\nimport { Node, Schema } from 'prosemirror-model';\nimport { createSpecs } from '@/wysiwyg/specCreator';\n\nimport Convertor from '@/convertors/convertor';\nimport { WwToDOMAdaptor } from '@/wysiwyg/adaptor/wwToDOMAdaptor';\nimport EventEmitter from '@/event/eventEmitter';\n\nimport { ToMdConvertorMap, ToMdConvertorContext, NodeInfo, MarkInfo } from '@t/convertor';\nimport { createHTMLSchemaMap } from '@/wysiwyg/nodes/html';\nimport { sanitizeHTML } from '@/sanitizer/htmlSanitizer';\nimport { createHTMLrenderer } from './markdown/util';\n\nfunction createSchema() {\n  const specs = createSpecs({});\n\n  return new Schema({\n    nodes: specs.nodes,\n    marks: specs.marks,\n  });\n}\n\ndescribe('Convertor', () => {\n  let convertor: Convertor;\n  let schema: Schema;\n\n  const parser = new Parser({\n    disallowedHtmlBlockTags: ['br', 'img'],\n  });\n\n  function assertConverting(markdown: string, expected: string) {\n    const mdNode = parser.parse(markdown);\n\n    const wwNode = convertor.toWysiwygModel(mdNode);\n    const result = convertor.toMarkdownText(wwNode!);\n\n    expect(result).toBe(expected);\n  }\n\n  beforeEach(() => {\n    schema = createSchema();\n    convertor = new Convertor(schema, {}, {}, new EventEmitter());\n  });\n\n  describe('should convert between markdown and wysiwyg node to', () => {\n    it('empty content', () => {\n      assertConverting('', '');\n    });\n\n    it('paragraph', () => {\n      const markdown = 'foo';\n\n      assertConverting(markdown, markdown);\n    });\n\n    it('headings', () => {\n      const markdown = source`\n        # heading1\n        ## heading2\n        ### heading3\n        #### heading4\n        ##### heading5\n        ###### heading6\n      `;\n      const expected = source`\n        # heading1\n        \n        ## heading2\n        \n        ### heading3\n        \n        #### heading4\n        \n        ##### heading5\n        \n        ###### heading6\n      `;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('codeBlock', () => {\n      const markdown = source`\n        \\`\\`\\`\n        foo\n        \\`\\`\\`\n      `;\n\n      assertConverting(markdown, markdown);\n    });\n\n    it('bullet list', () => {\n      const markdown = source`\n        * foo\n        * bar\n            * qux\n        * baz\n    \t`;\n\n      assertConverting(markdown, markdown);\n    });\n\n    it('ordered list', () => {\n      const markdown = source`\n        1. foo\n        2. bar\n        3. baz\n    \t`;\n\n      assertConverting(markdown, markdown);\n    });\n\n    it('blockQuote', () => {\n      const markdown = source`\n        > foo\n        > bar\n        >> baz\n        > > qux\n        > >> quxx\n      `;\n      const expected = source`\n        > foo\n        > bar\n        > > baz\n        > > qux\n        > > > quxx\n      `;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('thematicBreak', () => {\n      const markdown = source`\n        ---\n        ***\n        - - -\n        * * * *\n    \t`;\n      const expected = source`\n        ***\n        \n        ***\n        \n        ***\n        \n        ***\n      `;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('image', () => {\n      const markdown = source`\n        ![](imgUrl)\n        ![altText](imgUrl)\n        ![altText](img*Url)\n        ![altText](url?key=abc&attribute=abc)\n        `;\n      const expected = source`\n        ![](imgUrl)\n        ![altText](imgUrl)\n        ![altText](img*Url)\n        ![altText](url?key=abc&attribute=abc)\n    \t`;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('link', () => {\n      const markdown = source`\n        [](url)foo\n        [text](url)\n        [text](ur*l)\n        [Editor](https://github.com/nhn_test/tui.editor)\n        [this.is_a_test_link.com](this.is_a_test_link.com)\n        [text](url?key=abc&attribute=abc)\n        `;\n      const expected = source`\n        foo\n        [text](url)\n        [text](ur*l)\n        [Editor](https://github.com/nhn_test/tui.editor)\n        [this.is_a_test_link.com](this.is_a_test_link.com)\n        [text](url?key=abc&attribute=abc)\n    \t`;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('code', () => {\n      const markdown = '`foo bar baz`';\n\n      assertConverting(markdown, markdown);\n    });\n\n    it('emphasis (strong, italic) syntax', () => {\n      const markdown = source`\n        **foo**\n        __bar__\n        *baz*\n        _qux_\n    \t`;\n      const expected = source`\n        **foo**\n        **bar**\n        *baz*\n        *qux*\n    \t`;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('strike', () => {\n      const markdown = '~~strike~~';\n\n      assertConverting(markdown, markdown);\n    });\n\n    it('table', () => {\n      const markdown = source`\n        | thead | thead |\n        | --- | --- |\n        | tbody | tbody |\n\n        | thead |thead |\n        | -- | ----- |\n        | tbody|tbody|\n        | tbody|tbody|\n\n        |||\n        |-|-|\n        |||\n      `;\n      const expected = source`\n        | thead | thead |\n        | ----- | ----- |\n        | tbody | tbody |\n\n        | thead | thead |\n        | ----- | ----- |\n        | tbody | tbody |\n        | tbody | tbody |\n\n        |  |  |\n        | --- | --- |\n        |  |  |\n      `;\n\n      assertConverting(markdown, `${expected}\\n`);\n    });\n\n    it('table with column align syntax', () => {\n      const markdown = source`\n        | default | left | right | center |\n        | --- | :--- | ---: | :---: |\n        | tbody | tbody | tbody | tbody |\n\n        |  |  |  |  |\n        | --- | :--- | ---: | :---: |\n        | default | left | right | center |\n      `;\n      const expected = source`\n        | default | left | right | center |\n        | ------- | :--- | ----: | :----: |\n        | tbody | tbody | tbody | tbody |\n\n        |  |  |  |  |\n        | --- | :--- | ---: | :---: |\n        | default | left | right | center |\n      `;\n\n      assertConverting(markdown, `${expected}\\n`);\n    });\n\n    it('table with inline syntax', () => {\n      const markdown = source`\n        | ![altText](imgUrl) | foo ![altText](imgUrl) baz |\n        | ---- | ---- |\n        | [linkText](linkUrl) | foo [linkText](linkUrl) baz |\n        | **foo** _bar_ ~~baz~~ | **foo** *bar* ~~baz~~ [linkText](linkUrl) |\n      `;\n      const expected = source`\n        | ![altText](imgUrl) | foo ![altText](imgUrl) baz |\n        | --- | -------- |\n        | [linkText](linkUrl) | foo [linkText](linkUrl) baz |\n        | **foo** *bar* ~~baz~~ | **foo** *bar* ~~baz~~ [linkText](linkUrl) |\n      `;\n\n      assertConverting(markdown, `${expected}\\n`);\n    });\n\n    // @TODO: should normalize table cell\n    // it('should normalize wrong table syntax when converting', () => {\n    //   const markdown = source`\n    //     | col1 | col2 | col3 |\n    //     | --- | --- |\n    //     | cell1 | cell2 | cell3 |\n    //   `;\n    //   const expected = source`\n    //     | col1 | col2 | col3 |\n    //     | ---- | ---- | ---- |\n    //     | cell1 | cell2 |  |\n    //   `;\n\n    //   assertConverting(markdown, `${expected}\\n`);\n    // });\n\n    it('task', () => {\n      const markdown = source`\n        * [ ] foo\n            * [x] baz\n        * [x] bar\n        \n        1. [x] foo\n        2. [ ] bar\n      `;\n\n      assertConverting(markdown, markdown);\n    });\n\n    it('list in blockQuote', () => {\n      const markdown = source`\n        > * foo\n        >   * baz\n        > * bar\n        >> 1. qux\n        > > 2. quxx \n      `;\n      const expected = source`\n        > * foo\n        >     * baz\n        > * bar\n        > > 1. qux\n        > > 2. quxx \n      `;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('block nodes in list', () => {\n      const markdown = source`\n        1. foo\n\n            \\`\\`\\`\n            bar\n            \\`\\`\\`\n        \n            > bam\n      `;\n      const expected = source`\n        1. foo\n\n            \\`\\`\\`\n            bar\n            \\`\\`\\`\n        \n            > bam\n      `;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('soft break', () => {\n      const markdown = source`\n        foo\n        bar\n\n        baz\n\n\n\n        qux\n      `;\n\n      const expected = source`\n        foo\n        bar\n\n        baz\n\n        qux\n      `;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('<br>', () => {\n      const markdown = source`\n        foo\n        <br>\n        bar\n        <br>\n        <br>\n        baz\n        <br>\n        <br>\n        <br>\n        qux\n      `;\n      const expected = source`\n        foo\n        \n        bar\n\n        <br>\n        baz\n\n        <br>\n        <br>\n        qux\n      `;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('<br> with soft break', () => {\n      const markdown = source`\n        foo\n\n        <br>\n        bar\n        \n        <br>\n        <br>\n        baz\n        \n\n        <br>\n        qux\n        <br>\n\n        quux\n\n        <br>\n\n        quuz\n      `;\n      const expected = source`\n        foo\n        \n        <br>\n        bar\n\n        <br>\n        <br>\n        baz\n\n        <br>\n        qux\n\n        <br>\n        quux\n\n        <br>\n        <br>\n        quuz\n      `;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('<br> with html inline node', () => {\n      const markdown = source`\n        foo\n        bar\n        Para       <b>Word</b><br>\n      `;\n      const expected = source`\n        foo\n        bar\n        Para <b>Word</b>\n      `;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('<br> with following <br>', () => {\n      const markdown = source`\n        text1\n        <br>\n        text2<br>\n        <br>\n        text3\n      `;\n      const expected = source`\n        text1\n        \n        text2\n        \n        text3\n      `;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('<br> in the middle of the paragraph', () => {\n      const markdown = source`\n        text1\n        <br>\n        te<br>xt2<br>\n        <br>\n        text3\n      `;\n      const expected = source`\n        text1\n        \n        te\n        xt2\n        \n        text3\n      `;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('should convert html comment', () => {\n      const markdown = source`\n        <!--\n        foo\n\n        bar\n        baz\n        -->\n      `;\n\n      assertConverting(markdown, markdown);\n    });\n  });\n\n  describe('convert inline html', () => {\n    it('emphasis type', () => {\n      const markdown = source`\n        <b>foo</b>\n        <strong>foo</strong>\n\n        <i>foo</i>\n        <em>foo</em>\n        \n        <s>foo</s>\n        <del>foo</del>\n\n        <code>foo</code>\n      `;\n\n      assertConverting(markdown, markdown);\n    });\n\n    it('link type', () => {\n      const markdown = source`\n        <a href=\"#\">foo</a>\n\n        <img src=\"#\">\n\n        <img src=\"#\" alt=\"test\">\n      `;\n\n      assertConverting(markdown, markdown);\n    });\n\n    it('table with <br>', () => {\n      const markdown = source`\n        | thead<br>thead | thead |\n        | ----- | ----- |\n        | tbody<br>tbody | tbody |\n        | tbody | tbody<br>tbody<br>tbody |\n        | tbody | **tbody**<br>_tbody_<br>~~tbody~~<br>\\`tbody\\` |\n        | tbody | ![img](imgUrl)<br>[link](linkUrl) |\n      `;\n      const expected = source`\n        | thead<br>thead | thead |\n        | ---------- | ----- |\n        | tbody<br>tbody | tbody |\n        | tbody | tbody<br>tbody<br>tbody |\n        | tbody | **tbody**<br>*tbody*<br>~~tbody~~<br>\\`tbody\\` |\n        | tbody | ![img](imgUrl)<br>[link](linkUrl) |\n      `;\n\n      assertConverting(markdown, `${expected}\\n`);\n    });\n\n    it('table with list', () => {\n      const markdown = source`\n        | thead |\n        | ----- |\n        | <ul><li>bullet</li></ul> |\n        | <ol><li>ordered</li></ol> |\n        | <ul><li>nested<ul><li>nested</li></ul></li></ul> |\n        | <ul><li>nested<ul><li>nested</li><li>nested</li></ul></li></ul> |\n        | <ol><li>mix**ed**<ul><li>**mix**ed</li></ul></li></ol> |\n        | <ol><li>mix<i>ed</i><ul><li><strong>mix</strong>ed</li></ul></li></ol> |\n        | foo<ul><li>bar</li></ul>baz |\n        | ![altText](imgUrl) **mixed**<ul><li>[linkText](linkUrl) mixed</li></ul> |\n      `;\n\n      assertConverting(markdown, `${markdown}\\n`);\n    });\n\n    it('table with unmatched html list', () => {\n      const markdown = source`\n        | thead |\n        | ----- |\n        | <ul><li>bullet</li><ul> |\n        | <ol><li>ordered</li><ol> |\n        | <ul><li>nested<ul><li>nested</li><ul><li><ul> |\n      `;\n      const expected = source`\n        | thead |\n        | ----- |\n        | <ul><li>bullet</li></ul> |\n        | <ol><li>ordered</li></ol> |\n        | <ul><li>nested<ul><li>nested</li></ul></li></ul> |\n      `;\n\n      assertConverting(markdown, `${expected}\\n`);\n    });\n  });\n\n  describe('convert block html', () => {\n    it('paragraph and division are not converted to html block', () => {\n      const markdown = source`\n        <p>paragraph</p>\n\n        <div>division</div>\n      `;\n      const expected = source`\n        paragraph\n        division\n      `;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('heading', () => {\n      const markdown = source`\n        <h1>heading1</h1>\n        <h2>heading2</h2>\n        <h3>heading3</h3>\n        <h4>heading4</h4>\n        <h5>heading4</h5>\n        <h6>heading4</h6>\n      `;\n      const expected = oneLineTrim`\n        <h1>heading1</h1>\n        <h2>heading2</h2>\n        <h3>heading3</h3>\n        <h4>heading4</h4>\n        <h5>heading4</h5>\n        <h6>heading4</h6>\n      `;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('pre', () => {\n      const markdown = source`\n        <pre>code</pre>\n      `;\n\n      assertConverting(markdown, markdown);\n    });\n\n    it('blockquote', () => {\n      const markdown = source`\n        <blockquote>foo</blockquote>\n        <blockquote>foo<blockquote>foo</blockquote></blockquote>\n      `;\n      const expected = oneLineTrim`\n        <blockquote>foo</blockquote>\n        <blockquote>foo<blockquote>foo</blockquote></blockquote>\n      `;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('bullet list', () => {\n      const markdown = source`\n        <ul><li>foo</li></ul>\n        <ul><li>foo<ul><li>foo</li></ul></li></ul>\n      `;\n      const expected = oneLineTrim`\n        <ul><li>foo</li></ul>\n        <ul><li>foo<ul><li>foo</li></ul></li></ul>\n      `;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('ordered list', () => {\n      const markdown = source`\n        <ol><li>foo</li></ol>\n        <ol><li>foo<ol><li>foo</li></ol></li></ol>\n      `;\n      const expected = oneLineTrim`\n        <ol><li>foo</li></ol>\n        <ol><li>foo<ol><li>foo</li></ol></li></ol>\n      `;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('task', () => {\n      const markdown = source`\n        <ul><li class=\"task-list-item\" data-task>bullet task</li></ul>\n        <ul><li class=\"task-list-item checked\" data-task data-task-checked>ordered task</li></ul>\n      `;\n      const expected = oneLineTrim`\n        <ul><li class=\"task-list-item\" data-task>bullet task</li></ul>\n        <ul><li class=\"task-list-item checked\" data-task data-task-checked>ordered task</li></ul>\n      `;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('table', () => {\n      const markdown = source`\n        <table><thead><tr><th>foo</th></tr></thead><tbody><tr><td>bar</td></tr></tbody></table>\n      `;\n\n      assertConverting(markdown, markdown);\n    });\n\n    it('with html inline', () => {\n      const markdown = source`\n        <h1><b>foo</b></h1>\n        <ul><li>foo <i>bar</i></li></ul>\n        <blockquote><s>foo</s> bar</blockquote>\n      `;\n      const expected = oneLineTrim`\n        <h1><b>foo</b></h1>\n        <ul><li>foo <i>bar</i></li></ul>\n        <blockquote><s>foo</s> bar</blockquote>\n      `;\n\n      assertConverting(markdown, expected);\n    });\n  });\n\n  describe('convert custom inline', () => {\n    it('with info only', () => {\n      const markdown = source`$$custom$$`;\n      const expected = oneLineTrim`$$custom$$`;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('with info and text', () => {\n      const markdown = source`$$custom inline$$`;\n      const expected = oneLineTrim`$$custom inline$$`;\n\n      assertConverting(markdown, expected);\n    });\n  });\n\n  describe('sanitize when using html', () => {\n    it('href attribute with link', () => {\n      const markdown = source`\n        <a href=\"javascript:alert();\">xss</a>\n\n        <a href=\"  JaVaScRiPt: alert();\">xss</a>\n\n        <a href=\"vbscript:alert();\">xss</a>\n\n        <a href=\"  VBscript: alert(); \">xss</a>\n\n        <a href=\"livescript:alert();\">xss</a>\n\n        <a href=\"  LIVEScript: alert() ;\">xss</a>\n\n        123<a href=' javascript:alert();'>xss</a>\n      `;\n      const expected = source`\n        <a href=\"\">xss</a>\n\n        <a href=\"\">xss</a>\n\n        <a href=\"\">xss</a>\n\n        <a href=\"\">xss</a>\n\n        <a href=\"\">xss</a>\n\n        <a href=\"\">xss</a>\n\n        123<a href=\"\">xss</a>\n      `;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('src attribute with image', () => {\n      const markdown = source`\n        <img src=\"javascript:alert();\">\n\n        <img src=\"  JaVaScRiPt: alert();\">\n\n        <img src=\"vbscript:alert();\">\n\n        <img src=\"  VBscript: alert(); \">\n\n        <img src=\"  LIVEScript: alert() ;\">\n      `;\n      const expected = source`\n        <img src=\"\">\n\n        <img src=\"\">\n\n        <img src=\"\">\n\n        <img src=\"\">\n\n        <img src=\"\">\n      `;\n\n      assertConverting(markdown, expected);\n    });\n  });\n\n  describe('should custom convertor when converting from wysiwyg to markdown', () => {\n    function createCustomConvertor(customConvertor: ToMdConvertorMap) {\n      schema = createSchema();\n      convertor = new Convertor(schema, customConvertor, {}, new EventEmitter());\n    }\n\n    it('should change delimeter', () => {\n      const toMdCustomConvertor = {\n        thematicBreak() {\n          return {\n            delim: '- - -',\n          };\n        },\n      };\n\n      createCustomConvertor(toMdCustomConvertor);\n\n      assertConverting('***', '- - -');\n    });\n\n    it('should change raw html', () => {\n      const toMdCustomConvertor = {\n        thematicBreak() {\n          return {\n            rawHTML: '<hr class=\"foo\">',\n          };\n        },\n      };\n\n      createCustomConvertor(toMdCustomConvertor);\n\n      assertConverting('***', '<hr class=\"foo\">');\n    });\n\n    it('should not convert raw html when returning only delimiter', () => {\n      const toMdCustomConvertor = {\n        thematicBreak() {\n          return {\n            delim: '***',\n          };\n        },\n      };\n\n      createCustomConvertor(toMdCustomConvertor);\n\n      assertConverting('<hr>', '***');\n    });\n\n    it('should convert to original value', () => {\n      const toMdCustomConvertor = {\n        thematicBreak(_: NodeInfo | MarkInfo, { origin }: ToMdConvertorContext) {\n          return origin!();\n        },\n      };\n\n      createCustomConvertor(toMdCustomConvertor);\n\n      assertConverting('***', '***');\n    });\n\n    it('should convert by mixing return values', () => {\n      const toMdCustomConvertor = {\n        heading({ node }: NodeInfo | MarkInfo, { origin }: ToMdConvertorContext) {\n          const { level, headingType } = node.attrs;\n\n          if (headingType === 'setext') {\n            const delim = level === 1 ? '========' : '------';\n\n            return { delim };\n          }\n\n          return origin!();\n        },\n      };\n\n      createCustomConvertor(toMdCustomConvertor);\n\n      const markdown = source`\n        heading1\n        ===\n\n        heading2\n        ---\n\n        # heading1\n      `;\n      const expected = source`\n        heading1\n        ========\n\n        heading2\n        ------\n\n        # heading1\n      `;\n\n      assertConverting(markdown, expected);\n    });\n  });\n\n  describe('with front matter parser option', () => {\n    function assertFrontMatterConverting(markdown: string, expected: string) {\n      const useFrontMatterParser = new Parser({\n        disallowedHtmlBlockTags: ['br', 'img'],\n        frontMatter: true,\n      });\n      const mdNode = useFrontMatterParser.parse(markdown);\n\n      const wwNode = convertor.toWysiwygModel(mdNode);\n      const result = convertor.toMarkdownText(wwNode!);\n\n      expect(result).toBe(expected);\n    }\n\n    it('should convert front matter', () => {\n      const markdown = source`\n        ---\n        title: foo\n        desc: bar\n        ---\n      `;\n\n      assertFrontMatterConverting(markdown, markdown);\n    });\n  });\n\n  describe('should convert html block node which is not supported as default', () => {\n    function createConvertorWithHTMLRenderer() {\n      const customHTMLRenderer = createHTMLrenderer();\n      const adaptor = new WwToDOMAdaptor({}, customHTMLRenderer);\n      const htmlSchemaMap = createHTMLSchemaMap(customHTMLRenderer, sanitizeHTML, adaptor);\n      const specs = createSpecs({});\n\n      schema = new Schema({\n        nodes: { ...specs.nodes, ...htmlSchemaMap.nodes },\n        marks: { ...specs.marks, ...htmlSchemaMap.marks },\n      });\n      convertor = new Convertor(schema, {}, {}, new EventEmitter());\n    }\n\n    beforeEach(() => {\n      createConvertorWithHTMLRenderer();\n    });\n\n    it('should convert html block node to wysiwyg ignoring sanitizer tag', () => {\n      const markdown =\n        '<iframe src=\"https://www.youtube.com/embed/XyenY12fzAk\" height=\"315\" width=\"420\"></iframe>';\n      const expected =\n        '<iframe width=\"420\" height=\"315\" src=\"https://www.youtube.com/embed/XyenY12fzAk\"></iframe>';\n\n      assertConverting(markdown, expected);\n    });\n\n    it('should convert html block element which has \"=\" character as the attribute value', () => {\n      const markdown =\n        '<iframe src=\"//player.bilibili.com/player.html?aid=588782532&bvid=BV1hB4y1K7ro&cid=360826679&page=1\" height=\"315\" width=\"420\"></iframe>';\n      const expected =\n        '<iframe width=\"420\" height=\"315\" src=\"//player.bilibili.com/player.html?aid=588782532&amp;bvid=BV1hB4y1K7ro&amp;cid=360826679&amp;page=1\"></iframe>';\n\n      assertConverting(markdown, expected);\n    });\n\n    it('should convert html block node as the block node through inserting the blank line', () => {\n      const markdown = source`\n        para1\n\n        <iframe src=\"https://www.youtube.com/embed/XyenY12fzAk\" width=\"420\" height=\"315\"></iframe>\n\n        para2\n      `;\n      const expected = source`\n        para1\n\n        <iframe height=\"315\" width=\"420\" src=\"https://www.youtube.com/embed/XyenY12fzAk\"></iframe>\n\n        para2\n      `;\n\n      assertConverting(markdown, expected);\n    });\n\n    it('should convert html inline node', () => {\n      const markdown = 'inline <big class=\"my-big\">content</big>';\n\n      assertConverting(markdown, markdown);\n    });\n  });\n\n  describe('with custom convertor when converting from markdown to wysiwyg', () => {\n    function createCustomConvertor(customConvertor: HTMLConvertorMap) {\n      schema = createSchema();\n      convertor = new Convertor(schema, {}, customConvertor, new EventEmitter());\n    }\n\n    it('should convert markdown to wysiwyg', () => {\n      const toHTMLConvertor: HTMLConvertorMap = {\n        paragraph(_: MdNode, { entering, origin, options }: Context) {\n          if (options.nodeId) {\n            return {\n              type: entering ? 'openTag' : 'closeTag',\n              outerNewLine: true,\n              tagName: 'p',\n            };\n          }\n\n          return origin!();\n        },\n      };\n\n      createCustomConvertor(toHTMLConvertor);\n\n      const markdown = source`\n        > * Wrappers\n        >     1. [x] React\n        >     2. [x] Vue\n        >     3. [ ] Ember\n      `;\n\n      assertConverting(markdown, markdown);\n    });\n  });\n\n  describe('should escape markdown text in wysiwyg', () => {\n    it('with markdown text', () => {\n      const markdown = source`\n        \\\\# heading\n        \\\\> blockquote\n        \\\\*test\\\\*\n        \\\\* list\n      `;\n\n      assertConverting(markdown, markdown);\n    });\n\n    it('with html text', () => {\n      const markdown = source`\n        \\\\<div>block\\\\</div>\n        \\\\<strong>bold\\\\</strong>\n      `;\n\n      assertConverting(markdown, markdown);\n    });\n  });\n\n  it('should convert empty line between lists of wysiwig to <br>', () => {\n    const wwNodeJson = {\n      type: 'doc',\n      content: [\n        {\n          type: 'bulletList',\n          content: [\n            {\n              type: 'listItem',\n              content: [\n                { type: 'paragraph', content: [{ type: 'text', text: 'test_1' }] },\n                { type: 'paragraph', content: [] },\n              ],\n            },\n            {\n              type: 'listItem',\n              content: [{ type: 'paragraph', content: [{ type: 'text', text: 'test_2' }] }],\n            },\n          ],\n        },\n      ],\n    };\n\n    const wwNode = Node.fromJSON(schema, wwNodeJson);\n\n    const result = convertor.toMarkdownText(wwNode);\n\n    expect(result).toBe(`* test\\\\_1\\n<br>\\n* test\\\\_2`);\n  });\n\n  it('should escape the backslash, which is a plain chracter in the middle of a sentence', () => {\n    const markdown = source`\n      backslash \\\\in the middle of a sentence\n      `;\n    const expected = source`\n      backslash \\\\\\\\in the middle of a sentence\n      `;\n\n    assertConverting(markdown, expected);\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/dom.spec.ts",
    "content": "import toArray from 'tui-code-snippet/collection/toArray';\nimport {\n  isPositionInBox,\n  isElemNode,\n  findNodes,\n  appendNodes,\n  insertBeforeNode,\n  removeNode,\n  unwrapNode,\n  toggleClass,\n  createElementWith,\n  closest,\n  empty,\n  appendNode,\n  prependNode,\n} from '@/utils/dom';\n\ndescribe('dom utils', () => {\n  let container: HTMLElement;\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n  });\n\n  afterEach(() => {\n    container.parentNode!.removeChild(container);\n  });\n\n  it('isPositionInBox() returns state whether position is contained within box size', () => {\n    container.innerHTML = '<div class=\"test\">foo</div>';\n\n    const el = document.querySelector('.test') as HTMLElement;\n    const { style } = el;\n\n    style.left = '0';\n    style.top = '0';\n    style.width = '10px';\n    style.height = '10px';\n    style.paddingLeft = '0';\n    style.paddingRight = '0';\n    style.paddingTop = '0';\n    style.paddingBottom = '0';\n\n    expect(isPositionInBox(style, 5, 5)).toBe(true);\n    expect(isPositionInBox(style, 15, 15)).toBe(false);\n  });\n\n  describe('isElemNode', () => {\n    it('returns true if passed node is ELEMENT_NODE', () => {\n      container.innerHTML = '<p>hi</p>';\n\n      const target = container.querySelector('p') as HTMLElement;\n      const result = isElemNode(target);\n\n      expect(result).toBe(true);\n    });\n\n    it('returns false if passed node is not ELEMENT_NODE', () => {\n      container.innerHTML = 'text';\n\n      const result = isElemNode(container.firstChild!);\n\n      expect(result).toBe(false);\n    });\n  });\n\n  it('appendNodes() appends last child to parent using dom element', () => {\n    container.innerHTML = '<div>foo</div>';\n\n    const el = document.createElement('p');\n\n    el.innerHTML = 'bar';\n\n    const target = container.querySelector('div') as HTMLElement;\n\n    appendNodes(target, el);\n\n    expect(container.innerHTML).toBe('<div>foo<p>bar</p></div>');\n  });\n\n  it('insertBeforeNode() inserts node in front of target node', () => {\n    container.innerHTML = '<div>foo</div>';\n\n    const el = document.createElement('p');\n\n    el.innerHTML = 'bar';\n\n    const target = container.querySelector('div') as HTMLElement;\n\n    insertBeforeNode(el, target);\n\n    expect(container.innerHTML).toBe('<p>bar</p><div>foo</div>');\n  });\n\n  it('removeNode() removes target node', () => {\n    container.innerHTML = '<div><p>foo</p><p>bar</p></div>';\n\n    const target = container.querySelector('p') as HTMLElement;\n\n    removeNode(target);\n\n    expect(container.innerHTML).toBe('<div><p>bar</p></div>');\n  });\n\n  it('unwrapNode() removes given element and insert children at the same position', () => {\n    const childrenHTML = '<i>emph1</i> text <i>emph2</i>';\n\n    container.innerHTML = `<p><b>${childrenHTML}</b></p>`;\n\n    const target = container.querySelector('b') as HTMLElement;\n\n    unwrapNode(target);\n\n    expect(container.innerHTML).toBe(`<p>${childrenHTML}</p>`);\n  });\n\n  describe('findNodes() returns nodes matching by selector', () => {\n    beforeEach(() => {\n      container.innerHTML = '<div>foo</div><div>bar</div>';\n    });\n\n    it('to array when found', () => {\n      const result = findNodes(container, 'div');\n\n      expect(result.length).toBe(2);\n      expect(result[0].textContent).toBe('foo');\n      expect(result[1].textContent).toBe('bar');\n    });\n\n    it('to empty array when not found', () => {\n      const result = findNodes(container, '.test');\n\n      expect(result.length).toBe(0);\n    });\n  });\n\n  describe('toggleClass() adds or removes specific class name of element', () => {\n    beforeEach(() => {\n      container.innerHTML = '<div class=\"test\">foo</div>';\n    });\n\n    it('only toggle class', () => {\n      const target = container.querySelector('div')!;\n\n      toggleClass(target, 'active');\n      expect(target.className).toBe('test active');\n\n      toggleClass(target, 'active');\n      expect(target.className).toBe('test');\n    });\n\n    it('add or remove class by condition', () => {\n      const target = container.querySelector('div')!;\n\n      toggleClass(target, 'active', true);\n      expect(target.className).toBe('test active');\n\n      toggleClass(target, 'active1', false);\n      expect(target.className).toBe('test active');\n\n      toggleClass(target, 'active', false);\n      expect(target.className).toBe('test');\n    });\n  });\n\n  describe('createElementWith() returns created new element using', () => {\n    it('html string', () => {\n      const result = createElementWith('<p>foo</p>')!;\n\n      expect(result.textContent).toBe('foo');\n    });\n\n    it('dom element', () => {\n      const element = document.createElement('p');\n\n      element.innerHTML = 'foo';\n\n      const result = createElementWith(element)!;\n\n      expect(result.textContent).toBe('foo');\n    });\n\n    it('if there is target element, new element is appended to target', () => {\n      container.innerHTML = '<div></div>';\n\n      const target = container.querySelector('div')!;\n      const result = createElementWith('<p>foo</p>', target)!;\n\n      expect(result.parentNode).toBe(target);\n    });\n  });\n\n  describe('closest() finds node with', () => {\n    beforeEach(() => {\n      container.innerHTML = '<ul><li>foo</li><li class=\"test\">bar</li></ul>';\n    });\n\n    it('type selector from text node', () => {\n      const selector = 'li';\n      const [target] = toArray(container.querySelectorAll('li'));\n      const foundNode = closest(target.firstChild!, selector);\n      const result = container.querySelector(selector);\n\n      expect(foundNode).toBe(result);\n    });\n\n    it('attribute selector from text node', () => {\n      const selector = '.test';\n      const [, target] = toArray(container.querySelectorAll('li'));\n      const foundNode = closest(target.firstChild!, selector);\n      const result = container.querySelector(selector);\n\n      expect(foundNode).toBe(result);\n    });\n\n    it('type selector from element node', () => {\n      const selector = 'UL';\n      const target = container.querySelector('li')!;\n\n      const foundNode = closest(target, selector);\n      const result = container.querySelector(selector);\n\n      expect(foundNode).toBe(result);\n    });\n\n    it('wrong selector', () => {\n      const selector = 'wrong selector';\n      const target = container.querySelector('li')!;\n\n      const foundNode = closest(target, selector);\n\n      expect(foundNode).toBeNull();\n    });\n\n    it('dom element', () => {\n      const target = container.querySelector('li')!;\n      const selector = container.querySelector('ul')!;\n\n      const foundNode = closest(target, selector)!;\n\n      expect(foundNode).toEqual(selector);\n    });\n  });\n\n  it('empty() removes all children from target node', () => {\n    container.innerHTML = '<div><p>foo</p><p>bar</p></div>';\n\n    const target = container.querySelector('div')!;\n\n    empty(target);\n\n    expect(container.innerHTML).toBe('<div></div>');\n  });\n\n  describe('appendNode() appends last child to parent using', () => {\n    beforeEach(() => {\n      container.innerHTML = '<div>foo</div>';\n    });\n\n    it('html string', () => {\n      appendNode(container.querySelector('div')!, '<p>bar</p>');\n\n      expect(container.innerHTML).toBe('<div>foo<p>bar</p></div>');\n    });\n\n    it('dom element', () => {\n      const child = document.createElement('p');\n\n      child.innerHTML = 'bar';\n\n      appendNode(container.querySelector('div')!, child);\n\n      expect(container.innerHTML).toBe('<div>foo<p>bar</p></div>');\n    });\n  });\n\n  describe('prependNode() appends first child to parent using', () => {\n    beforeEach(() => {\n      container.innerHTML = '<div>foo</div>';\n    });\n\n    it('html string', () => {\n      prependNode(container.querySelector('div')!, '<p>bar</p>');\n\n      expect(container.innerHTML).toBe('<div><p>bar</p>foo</div>');\n    });\n\n    it('dom element', () => {\n      const child = document.createElement('p');\n\n      child.innerHTML = 'bar';\n\n      prependNode(container.querySelector('div')!, child);\n\n      expect(container.innerHTML).toBe('<div><p>bar</p>foo</div>');\n    });\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/editor.spec.ts",
    "content": "import '@/i18n/en-us';\nimport { oneLineTrim, stripIndents, source } from 'common-tags';\nimport { Emitter } from '@t/event';\nimport { EditorOptions } from '@t/editor';\nimport type { OpenTagToken } from '@toast-ui/toastmark';\nimport i18n from '@/i18n/i18n';\nimport Editor from '@/editor';\nimport Viewer from '@/viewer';\nimport * as commonUtil from '@/utils/common';\nimport { createHTMLrenderer } from './markdown/util';\nimport { cls } from '@/utils/dom';\nimport * as imageHelper from '@/helper/image';\n\nconst HEADING_CLS = `${cls('md-heading')} ${cls('md-heading1')}`;\nconst DELIM_CLS = cls('md-delimiter');\n\ndescribe('editor', () => {\n  let container: HTMLElement,\n    mdEditor: HTMLElement,\n    mdPreview: HTMLElement,\n    wwEditor: HTMLElement,\n    editor: Editor;\n\n  function getPreviewHTML() {\n    return mdPreview\n      .querySelector(`.${cls('contents')}`)!\n      .innerHTML.replace(/\\sdata-nodeid=\"\\d+\"|\\n/g, '')\n      .trim();\n  }\n\n  describe('instance API', () => {\n    beforeEach(() => {\n      container = document.createElement('div');\n      editor = new Editor({\n        el: container,\n        previewHighlight: false,\n        widgetRules: [\n          {\n            rule: /@\\S+/,\n            toDOM(text) {\n              const span = document.createElement('span');\n\n              span.innerHTML = `<a href=\"www.google.com\">${text}</a>`;\n              return span;\n            },\n          },\n        ],\n      });\n\n      const elements = editor.getEditorElements();\n\n      mdEditor = elements.mdEditor;\n      mdPreview = elements.mdPreview!;\n      wwEditor = elements.wwEditor!;\n\n      document.body.appendChild(container);\n    });\n\n    afterEach(() => {\n      editor.destroy();\n      document.body.removeChild(container);\n    });\n\n    describe('convertPosToMatchEditorMode', () => {\n      const mdPos: [number, number] = [2, 1];\n      const wwPos = 14;\n\n      it('should convert position to match editor mode', () => {\n        editor.setMarkdown('Hello World\\nwelcome to the world');\n\n        editor.changeMode('wysiwyg');\n        expect(editor.convertPosToMatchEditorMode(mdPos)).toEqual([wwPos, wwPos]);\n\n        editor.changeMode('markdown');\n        expect(editor.convertPosToMatchEditorMode(wwPos)).toEqual([mdPos, mdPos]);\n      });\n\n      it('should occurs error when types of parameters is not matched', () => {\n        expect(() => {\n          editor.convertPosToMatchEditorMode(mdPos, wwPos);\n        }).toThrowError();\n      });\n    });\n\n    it('setPlaceholder()', () => {\n      editor.setPlaceholder('Please input text');\n\n      const expected = '<span class=\"placeholder ProseMirror-widget\">Please input text</span>';\n\n      expect(mdEditor).toContainHTML(expected);\n      expect(wwEditor).toContainHTML(expected);\n    });\n\n    describe('getHTML()', () => {\n      it('basic', () => {\n        editor.setMarkdown('# heading\\n* bullet');\n\n        const result = oneLineTrim`\n          <h1>heading</h1>\n          <ul>\n            <li>\n              <p>bullet</p>\n            </li>\n          </ul>\n        `;\n\n        expect(editor.getHTML()).toBe(result);\n      });\n\n      it('should not trigger change event when the mode is wysiwyg', () => {\n        const spy = jest.fn();\n\n        editor.changeMode('wysiwyg');\n        editor.on('change', spy);\n        editor.getHTML();\n\n        expect(spy).not.toHaveBeenCalled();\n      });\n\n      it('should be the same as wysiwyg contents', () => {\n        const input = source`\n          <p>first line</p>\n          <p>second line</p>\n          <p><br>\\nthird line</p>\n          <p><br>\\n<br>\\nfourth line</p>\n        `;\n        const expected = oneLineTrim`\n          <p>first line</p>\n          <p>second line</p>\n          <p><br></p>\n          <p>third line</p>\n          <p><br></p>\n          <p><br></p>\n          <p>fourth line</p>\n        `;\n\n        editor.setHTML(input);\n\n        expect(editor.getHTML()).toBe(expected);\n      });\n\n      it('placeholder should be removed', () => {\n        editor.changeMode('wysiwyg');\n        editor.setPlaceholder('placeholder');\n\n        const result = oneLineTrim`\n          <p><br></p>\n        `;\n\n        expect(editor.getHTML()).toBe(result);\n      });\n    });\n\n    it('changeMode()', () => {\n      const spy = jest.fn();\n\n      expect(editor.isMarkdownMode()).toBe(true);\n      expect(editor.isWysiwygMode()).toBe(false);\n\n      editor.on('changeMode', spy);\n      editor.changeMode('wysiwyg');\n\n      expect(spy).toHaveBeenCalledWith('wysiwyg');\n      expect(editor.isMarkdownMode()).toBe(false);\n      expect(editor.isWysiwygMode()).toBe(true);\n    });\n\n    it('changePreviewStyle()', () => {\n      const spy = jest.fn();\n\n      expect(editor.getCurrentPreviewStyle()).toBe('tab');\n\n      editor.on('changePreviewStyle', spy);\n      editor.changePreviewStyle('vertical');\n\n      expect(spy).toHaveBeenCalledWith('vertical');\n      expect(editor.getCurrentPreviewStyle()).toBe('vertical');\n    });\n\n    describe('setMarkdown()', () => {\n      it('basic', () => {\n        editor.setMarkdown('# heading');\n\n        expect(mdEditor).toContainHTML(\n          `<div><span class=\"${HEADING_CLS}\"><span class=\"${DELIM_CLS}\">#</span> heading</span></div>`\n        );\n        expect(getPreviewHTML()).toBe('<h1>heading</h1>');\n      });\n\n      it('should parse the CRLF properly in markdown', () => {\n        editor.setMarkdown('# heading\\r\\nCRLF');\n\n        expect(mdEditor).toContainHTML(\n          `<div><span class=\"${HEADING_CLS}\"><span class=\"${DELIM_CLS}\">#</span> heading</span></div><div>CRLF</div>`\n        );\n        expect(getPreviewHTML()).toBe('<h1>heading</h1><p>CRLF</p>');\n      });\n    });\n\n    describe('setHTML()', () => {\n      it('basic', () => {\n        editor.setHTML('<h1>heading</h1>');\n\n        expect(mdEditor).toContainHTML(\n          `<div><span class=\"${HEADING_CLS}\"><span class=\"${DELIM_CLS}\">#</span> heading</span></div>`\n        );\n        expect(getPreviewHTML()).toBe('<h1>heading</h1>');\n      });\n\n      it('should parse the br tag as the empty block to separate between blocks', () => {\n        editor.setHTML('<p>a<br/>b</p>');\n\n        expect(mdEditor).toContainHTML('<div>a</div><div>b</div>');\n        expect(getPreviewHTML()).toBe('<p>a<br>b</p>');\n      });\n\n      it('should parse the br tag with the paragraph block to separate between blocks in wysiwyg', () => {\n        editor.setHTML(\n          '<h1>test title</h1><p><strong>test bold</strong><br><em>test italic</em><br>normal text</p>'\n        );\n        editor.changeMode('wysiwyg');\n\n        const expected = oneLineTrim`\n          <h1>test title</h1>\n          <p><strong>test bold</strong></p>\n          <p><em>test italic</em></p>\n          <p>normal text</p>\n        `;\n\n        expect(wwEditor).toContainHTML(expected);\n      });\n\n      it('should parse the br tag with the paragraph block to separate between blocks', () => {\n        const input = source`\n          <p>first line</p>\n          <p>second line</p>\n          <p><br>\\nthird line</p>\n          <p><br>\\n<br>\\nfourth line</p>\n        `;\n        const expected = oneLineTrim`\n          <p>first line<br>second line</p>\n          <p>third line</p>\n          <p><br>fourth line</p>\n        `;\n\n        editor.setHTML(input);\n\n        expect(getPreviewHTML()).toBe(expected);\n      });\n\n      it('should be parsed with the same content when calling setHTML() with getHTML() API result', () => {\n        const input = source`\n          <p>first line</p>\n          <p>second line</p>\n          <p><br>\\nthird line</p>\n          <p><br>\\n<br>\\nfourth line</p>\n        `;\n\n        editor.setHTML(input);\n\n        const mdEditorHTML = mdEditor.innerHTML;\n        const mdPreviewHTML = getPreviewHTML();\n\n        editor.setHTML(editor.getHTML());\n\n        expect(mdEditor).toContainHTML(mdEditorHTML);\n        expect(getPreviewHTML()).toBe(mdPreviewHTML);\n      });\n    });\n\n    it('reset()', () => {\n      editor.setMarkdown('# heading');\n      editor.reset();\n\n      expect(mdEditor).not.toContainHTML(\n        `<div><span class=\"${HEADING_CLS}\"><span class=\"${DELIM_CLS}\">#</span> heading</span></div>`\n      );\n      expect(getPreviewHTML()).toBe('');\n    });\n\n    describe('setMinHeight()', () => {\n      it('should set height with pixel option', () => {\n        editor.setMinHeight('200px');\n\n        expect(mdEditor).toHaveStyle({ minHeight: '200px' });\n        expect(mdPreview).toHaveStyle({ minHeight: '200px' });\n        expect(wwEditor).toHaveStyle({ minHeight: '200px' });\n      });\n\n      it('should be less than the editor height', () => {\n        editor.setMinHeight('400px');\n\n        expect(mdEditor).toHaveStyle({ minHeight: '225px' });\n        expect(mdPreview).toHaveStyle({ minHeight: '225px' });\n        expect(wwEditor).toHaveStyle({ minHeight: '225px' });\n      });\n    });\n\n    describe('setHeight()', () => {\n      it('should set height with pixel option', () => {\n        editor.setHeight('300px');\n\n        expect(container).not.toHaveClass('auto-height');\n        expect(container).toHaveStyle({ height: '300px' });\n        expect(mdEditor).toHaveStyle({ minHeight: '200px' });\n        expect(mdPreview).toHaveStyle({ minHeight: '200px' });\n        expect(wwEditor).toHaveStyle({ minHeight: '200px' });\n      });\n\n      it('should set height with auto option', () => {\n        editor.setHeight('auto');\n\n        expect(container).toHaveClass('auto-height');\n        expect(container).toHaveStyle({ height: 'auto' });\n        expect(mdEditor).toHaveStyle({ minHeight: '200px' });\n        expect(mdPreview).toHaveStyle({ minHeight: '200px' });\n        expect(wwEditor).toHaveStyle({ minHeight: '200px' });\n      });\n    });\n\n    it('addWidget()', () => {\n      const node = document.createElement('div');\n\n      node.innerHTML = 'widget';\n\n      editor.addWidget(node, 'top');\n\n      expect(document.body).toContainElement(node);\n\n      editor.changeMode('wysiwyg');\n\n      expect(document.body).not.toContainElement(node);\n    });\n\n    describe('replaceWithWidget()', () => {\n      it('in markdown', () => {\n        editor.replaceWithWidget([1, 1], [1, 1], '@test');\n\n        const expectedEditor = oneLineTrim`\n          <span class=\"tui-widget\">\n            <span><a href=\"www.google.com\">@test</a></span>\n          </span>\n        `;\n        const expectedPreview = oneLineTrim`\n          <p>\n            <span class=\"tui-widget\">\n              <span><a href=\"www.google.com\">@test</a></span>\n            </span>\n          </p>\n        `;\n\n        expect(mdEditor).toContainHTML(expectedEditor);\n        expect(getPreviewHTML()).toBe(expectedPreview);\n      });\n\n      it('in wysiwyg', () => {\n        editor.changeMode('wysiwyg');\n        editor.replaceWithWidget(1, 1, '@test');\n\n        const expected = oneLineTrim`\n          <span class=\"tui-widget\">\n            <span><a href=\"www.google.com\">@test</a></span>\n          </span>\n        `;\n\n        expect(wwEditor).toContainHTML(expected);\n      });\n    });\n\n    it('exec()', () => {\n      // @ts-ignore\n      jest.spyOn(editor.commandManager, 'exec');\n\n      editor.exec('bold');\n\n      // @ts-ignore\n      // eslint-disable-next-line no-undefined\n      expect(editor.commandManager.exec).toHaveBeenCalledWith('bold', undefined);\n    });\n\n    it('addCommand()', () => {\n      const spy = jest.fn();\n      // @ts-ignore\n      const { view } = editor.mdEditor;\n      const { state, dispatch } = view;\n\n      editor.addCommand('markdown', 'custom', spy);\n      editor.exec('custom', { prop: 'prop' });\n\n      expect(spy).toHaveBeenCalledWith({ prop: 'prop' }, state, dispatch, view);\n      expect(spy).toHaveBeenCalled();\n    });\n\n    it('should be triggered only once when the event registered by addHook()', () => {\n      const spy = jest.fn();\n      const { eventEmitter } = editor;\n\n      eventEmitter.addEventType('custom');\n\n      editor.addHook('custom', spy);\n      editor.addHook('custom', spy);\n\n      eventEmitter.emit('custom');\n\n      expect(spy).toHaveBeenCalledTimes(1);\n    });\n\n    describe('insertText()', () => {\n      it('in markdown', () => {\n        editor.insertText('test');\n\n        expect(mdEditor).toContainHTML('<div>test</div>');\n        expect(getPreviewHTML()).toBe('<p>test</p>');\n      });\n\n      it('in wysiwyg', () => {\n        editor.changeMode('wysiwyg');\n        editor.insertText('test');\n\n        expect(wwEditor).toContainHTML('<p>test</p>');\n      });\n    });\n\n    describe('setSelection(), getSelection()', () => {\n      it('in markdown', () => {\n        expect(editor.getSelection()).toEqual([\n          [1, 1],\n          [1, 1],\n        ]);\n\n        editor.setMarkdown('line1\\nline2');\n        editor.setSelection([1, 2], [2, 4]);\n\n        expect(editor.getSelection()).toEqual([\n          [1, 2],\n          [2, 4],\n        ]);\n      });\n\n      it('in wysiwyg', () => {\n        editor.changeMode('wysiwyg');\n\n        expect(editor.getSelection()).toEqual([1, 1]);\n\n        editor.setMarkdown('line1\\nline2');\n        editor.setSelection(2, 8);\n\n        expect(editor.getSelection()).toEqual([2, 8]);\n      });\n    });\n\n    describe('getSelectedText()', () => {\n      beforeEach(() => {\n        editor.setMarkdown('line1\\nline2');\n        editor.setSelection([1, 2], [2, 4]);\n      });\n\n      it('in markdown', () => {\n        expect(editor.getSelectedText()).toEqual('ine1\\nlin');\n        expect(editor.getSelectedText([1, 2], [2, 6])).toEqual('ine1\\nline2');\n      });\n\n      it('in wysiwyg', () => {\n        editor.changeMode('wysiwyg');\n        editor.setSelection(2, 11);\n\n        expect(editor.getSelectedText()).toEqual('ine1\\nlin');\n        expect(editor.getSelectedText(2, 13)).toEqual('ine1\\nline2');\n      });\n    });\n\n    describe('replaceSelection()', () => {\n      beforeEach(() => {\n        editor.setMarkdown('line1\\nline2');\n        editor.setSelection([1, 2], [2, 4]);\n      });\n\n      it('should replace current selection in markdown', () => {\n        editor.replaceSelection('Replaced');\n\n        expect(mdEditor).toContainHTML('<div>lReplacede2</div>');\n        expect(getPreviewHTML()).toBe('<p>lReplacede2</p>');\n      });\n\n      it('should replace current selection in wysiwyg', () => {\n        editor.changeMode('wysiwyg');\n        editor.setSelection(2, 11);\n        editor.replaceSelection('Replaced');\n\n        expect(wwEditor).toContainHTML('<p>lReplacede2</p>');\n      });\n\n      it('should replace given selection in markdown', () => {\n        editor.replaceSelection('Replaced', [1, 1], [2, 1]);\n\n        expect(mdEditor).toContainHTML('<div>Replacedline2</div>');\n        expect(getPreviewHTML()).toBe('<p>Replacedline2</p>');\n      });\n\n      it('should replace given selection in wysiwyg', () => {\n        editor.changeMode('wysiwyg');\n        editor.replaceSelection('Replaced', 1, 7);\n\n        expect(wwEditor).toContainHTML('<p>Replaced</p><p>line2</p>');\n      });\n\n      it('should parse the CRLF properly in markdown', () => {\n        editor.replaceSelection('text\\r\\nCRLF');\n\n        expect(mdEditor).toContainHTML('<div>ltext</div><div>CRLFe2</div>');\n        expect(getPreviewHTML()).toBe('<p>ltext<br>CRLFe2</p>');\n      });\n    });\n\n    describe('deleteSelection()', () => {\n      beforeEach(() => {\n        editor.setMarkdown('line1\\nline2');\n        editor.setSelection([1, 2], [2, 4]);\n      });\n\n      it('should delete current selection in markdown', () => {\n        editor.deleteSelection();\n\n        expect(mdEditor).toContainHTML('<div>le2</div>');\n        expect(getPreviewHTML()).toBe('<p>le2</p>');\n      });\n\n      it('should delete current selection in wysiwyg', () => {\n        editor.changeMode('wysiwyg');\n        editor.setSelection(2, 11);\n        editor.deleteSelection();\n\n        expect(wwEditor).toContainHTML('<p>le2</p>');\n      });\n\n      it('should delete given selection in markdown', () => {\n        editor.deleteSelection([1, 1], [2, 1]);\n\n        expect(mdEditor).toContainHTML('<div>line2</div>');\n        expect(getPreviewHTML()).toBe('<p>line2</p>');\n      });\n\n      it('should delete given selection in wysiwyg', () => {\n        editor.changeMode('wysiwyg');\n        editor.deleteSelection(1, 7);\n\n        expect(wwEditor).toContainHTML('<p>line2</p>');\n      });\n    });\n\n    describe('getRangeOfNode()', () => {\n      beforeEach(() => {\n        editor.setMarkdown('line1\\nline2 **strong**');\n        editor.setSelection([2, 10], [2, 12]);\n      });\n\n      it('should get the range of the current selected node in markdown', () => {\n        const rangeInfo = editor.getRangeInfoOfNode();\n        const [start, end] = rangeInfo.range;\n\n        expect(rangeInfo).toEqual({\n          range: [\n            [2, 7],\n            [2, 17],\n          ],\n          type: 'strong',\n        });\n\n        editor.replaceSelection('Replaced', start, end);\n\n        expect(getPreviewHTML()).toBe('<p>line1<br>line2 Replaced</p>');\n      });\n\n      it('should get the range of the current selected node in wysiwyg', () => {\n        editor.changeMode('wysiwyg');\n        editor.setSelection(15, 15);\n\n        const rangeInfo = editor.getRangeInfoOfNode();\n        const [start, end] = rangeInfo.range;\n\n        expect(rangeInfo).toEqual({ range: [14, 20], type: 'strong' });\n\n        editor.replaceSelection('Replaced', start, end);\n\n        expect(wwEditor).toContainHTML('<p>line1</p><p>line2 Replaced</p>');\n      });\n\n      it('should get the range of selection with given position in markdown', () => {\n        const rangeInfo = editor.getRangeInfoOfNode([2, 2]);\n        const [start, end] = rangeInfo.range;\n\n        expect(rangeInfo).toEqual({\n          range: [\n            [2, 1],\n            [2, 7],\n          ],\n          type: 'text',\n        });\n\n        editor.replaceSelection('Replaced', start, end);\n\n        expect(getPreviewHTML()).toBe('<p>line1<br>Replaced<strong>strong</strong></p>');\n      });\n\n      it('should get the range of selection with given position in wysiwyg', () => {\n        editor.changeMode('wysiwyg');\n\n        const rangeInfo = editor.getRangeInfoOfNode(10);\n        const [start, end] = rangeInfo.range;\n\n        expect(rangeInfo).toEqual({ range: [8, 14], type: 'text' });\n\n        editor.replaceSelection('Replaced', start, end);\n\n        expect(wwEditor).toContainHTML('<p>line1</p><p>Replaced<strong>strong</strong></p>');\n      });\n    });\n  });\n\n  describe('static API', () => {\n    it('factory()', () => {\n      const editorInst = Editor.factory({ el: document.createElement('div'), viewer: false });\n      const viewerInst = Editor.factory({ el: document.createElement('div'), viewer: true });\n\n      expect(editorInst).toBeInstanceOf(Editor);\n      expect(viewerInst).toBeInstanceOf(Viewer);\n    });\n\n    it('setLanguage()', () => {\n      const data = {};\n\n      jest.spyOn(i18n, 'setLanguage');\n\n      Editor.setLanguage('ko', data);\n\n      expect(i18n.setLanguage).toHaveBeenCalledWith('ko', data);\n    });\n  });\n\n  describe('options', () => {\n    beforeEach(() => {\n      container = document.createElement('div');\n\n      document.body.appendChild(container);\n    });\n\n    afterEach(() => {\n      editor.destroy();\n      document.body.removeChild(container);\n    });\n\n    function createEditor(options: EditorOptions) {\n      editor = new Editor(options);\n\n      const elements = editor.getEditorElements();\n\n      mdEditor = elements.mdEditor;\n      mdPreview = elements.mdPreview!;\n      wwEditor = elements.wwEditor!;\n    }\n\n    describe('plugins', () => {\n      it('should invoke plugin functions', () => {\n        const fooPlugin = jest.fn().mockReturnValue({});\n        const barPlugin = jest.fn().mockReturnValue({});\n\n        createEditor({ el: container, plugins: [fooPlugin, barPlugin] });\n\n        // @ts-ignore\n        const { eventEmitter } = editor;\n\n        expect(fooPlugin).toHaveBeenCalledWith(expect.objectContaining({ eventEmitter }));\n        expect(barPlugin).toHaveBeenCalledWith(expect.objectContaining({ eventEmitter }));\n      });\n\n      it('should invoke plugin function with options of plugin', () => {\n        const plugin = jest.fn().mockReturnValue({});\n        const options = {};\n\n        createEditor({ el: container, plugins: [[plugin, options]] });\n\n        // @ts-ignore\n        const { eventEmitter } = editor;\n\n        expect(plugin).toHaveBeenCalledWith(\n          expect.objectContaining({ eventEmitter }),\n          expect.objectContaining(options)\n        );\n      });\n\n      it(`should add command to command manager when plugin return 'markdownCommands' value`, () => {\n        const spy = jest.fn();\n        const plugin = () => {\n          return {\n            markdownCommands: {\n              foo: () => {\n                spy();\n                return true;\n              },\n            },\n          };\n        };\n\n        createEditor({ el: container, plugins: [plugin] });\n\n        editor.exec('foo');\n\n        expect(spy).toHaveBeenCalled();\n      });\n\n      it(`should add command to command manager when plugin return 'wysiwygCommands' value`, () => {\n        const spy = jest.fn();\n        const plugin = () => {\n          return {\n            wysiwygCommands: {\n              foo: () => {\n                spy();\n                return true;\n              },\n            },\n          };\n        };\n\n        createEditor({ el: container, plugins: [plugin] });\n\n        editor.changeMode('wysiwyg');\n        editor.exec('foo');\n\n        expect(spy).toHaveBeenCalled();\n      });\n\n      it(`should add toolbar item when plugin return 'toolbarItems' value`, () => {\n        const toolbarItem = {\n          name: 'color',\n          tooltip: 'Text color',\n          className: 'toastui-editor-toolbar-icons color',\n        };\n        const plugin = () => {\n          return {\n            toolbarItems: [{ groupIndex: 1, itemIndex: 2, item: toolbarItem }],\n          };\n        };\n\n        createEditor({ el: container, plugins: [plugin] });\n\n        const toolbar = document.querySelector(`.${cls('toolbar-icons.color')}`);\n\n        expect(toolbar).toBeInTheDocument();\n      });\n    });\n\n    describe('usageStatistics', () => {\n      it('should send request hostname in payload by default', () => {\n        spyOn(commonUtil, 'sendHostName');\n\n        createEditor({ el: container });\n\n        expect(commonUtil.sendHostName).toHaveBeenCalled();\n      });\n\n      it('should not send request if the option is set to false', () => {\n        spyOn(commonUtil, 'sendHostName');\n\n        createEditor({ el: container, usageStatistics: false });\n\n        expect(commonUtil.sendHostName).not.toHaveBeenCalled();\n      });\n    });\n\n    describe('hideModeSwitch', () => {\n      it('should hide mode switch if the option value is true', () => {\n        createEditor({ el: container, hideModeSwitch: true });\n\n        const modeSwitch = document.querySelector(`.${cls('mode-switch')}`);\n\n        expect(modeSwitch).not.toBeInTheDocument();\n      });\n    });\n\n    describe('extendedAutolinks option', () => {\n      it('should convert url-like strings to anchor tags', () => {\n        createEditor({\n          el: container,\n          initialValue: 'http://nhn.com',\n          extendedAutolinks: true,\n          previewHighlight: false,\n        });\n\n        expect(getPreviewHTML()).toBe('<p><a href=\"http://nhn.com\">http://nhn.com</a></p>');\n      });\n    });\n\n    describe('disallowDeepHeading internal parsing option', () => {\n      it('should disallow the nested seTextHeading in list', () => {\n        createEditor({\n          el: container,\n          initialValue: '- item1\\n\\t-',\n          previewHighlight: false,\n        });\n\n        const result = oneLineTrim`\n          <ul>\n            <li>\n              <p>item1<br>\n              -</p>\n            </li>\n          </ul>\n        `;\n\n        expect(getPreviewHTML()).toBe(result);\n      });\n\n      it('should disallow the nested atxHeading in list', () => {\n        createEditor({\n          el: container,\n          initialValue: '- # item1',\n          previewHighlight: false,\n        });\n\n        const result = oneLineTrim`\n          <ul>\n            <li>\n              <p># item1</p>\n            </li>\n          </ul>\n        `;\n\n        expect(getPreviewHTML()).toBe(result);\n      });\n\n      it('should disallow the nested seTextHeading in blockquote', () => {\n        createEditor({\n          el: container,\n          initialValue: '> item1\\n> -',\n          previewHighlight: false,\n        });\n\n        const result = oneLineTrim`\n          <blockquote>\n            <p>item1<br>\n            -</p>\n          </blockquote>\n        `;\n\n        expect(getPreviewHTML()).toBe(result);\n      });\n\n      it('should disallow the nested atxHeading in blockquote', () => {\n        createEditor({\n          el: container,\n          initialValue: '> # item1',\n          previewHighlight: false,\n        });\n\n        const result = oneLineTrim`\n          <blockquote>\n            <p># item1</p>\n          </blockquote>\n        `;\n\n        expect(getPreviewHTML()).toBe(result);\n      });\n    });\n\n    describe('frontMatter option', () => {\n      it('should parse the front matter as the paragraph in WYSIWYG', () => {\n        createEditor({\n          el: container,\n          frontMatter: true,\n          initialValue: '---\\ntitle: front matter\\n---',\n          initialEditType: 'wysiwyg',\n        });\n\n        const result = stripIndents`\n          <div data-front-matter=\"true\">---\n          title: front matter\n          ---</div>\n        `;\n\n        expect(wwEditor).toContainHTML(result);\n      });\n\n      it('should keep the front matter after changing the mode', () => {\n        createEditor({\n          el: container,\n          frontMatter: true,\n          initialEditType: 'wysiwyg',\n          initialValue: '---\\ntitle: front matter\\n---',\n        });\n\n        editor.changeMode('markdown');\n\n        expect(editor.getMarkdown()).toBe('---\\ntitle: front matter\\n---');\n      });\n    });\n\n    describe('customHTMLSanitizer option', () => {\n      it('should replace default sanitizer with custom sanitizer', () => {\n        const customHTMLSanitizer = jest.fn();\n\n        createEditor({ el: container, customHTMLSanitizer });\n\n        editor.changeMode('wysiwyg');\n\n        expect(customHTMLSanitizer).toHaveBeenCalled();\n      });\n    });\n\n    describe('customHTMLRenderer', () => {\n      it('should pass customHTMLRender option for creating convertor instance', () => {\n        createEditor({\n          el: container,\n          initialValue: 'Hello World',\n          previewHighlight: false,\n          customHTMLRenderer: {\n            paragraph(_, { entering, origin }) {\n              const result = origin!() as OpenTagToken;\n\n              if (entering) {\n                result.classNames = ['my-class'];\n              }\n\n              return result;\n            },\n          },\n        });\n\n        expect(getPreviewHTML()).toBe('<p class=\"my-class\">Hello World</p>');\n      });\n\n      it('linkAttributes option should be applied to original renderer', () => {\n        createEditor({\n          el: container,\n          initialValue: '[Hello](nhn.com)',\n          linkAttributes: { target: '_blank' },\n          previewHighlight: false,\n          customHTMLRenderer: {\n            link(_, { origin }) {\n              return origin!();\n            },\n          },\n        });\n\n        expect(getPreviewHTML()).toBe('<p><a target=\"_blank\" href=\"nhn.com\">Hello</a></p>');\n      });\n\n      it('should render html block node regardless of the sanitizer', () => {\n        createEditor({\n          el: container,\n          initialValue:\n            '<iframe width=\"420\" height=\"315\" src=\"https://www.youtube.com/embed/XyenY12fzAk\"></iframe>\\n\\ntest',\n          previewHighlight: false,\n          // add iframe html block renderer\n          customHTMLRenderer: createHTMLrenderer(),\n        });\n\n        const result = oneLineTrim`\n          <iframe src=\"https://www.youtube.com/embed/XyenY12fzAk\" height=\"315\" width=\"420\"></iframe>\n          <p>test</p>\n        `;\n\n        expect(getPreviewHTML()).toBe(result);\n      });\n\n      it('should keep the html block node after changing the mode', () => {\n        createEditor({\n          el: container,\n          initialValue:\n            '<iframe width=\"420\" height=\"315\" src=\"https://www.youtube.com/embed/XyenY12fzAk\"></iframe>\\n\\ntest',\n          previewHighlight: false,\n          // add iframe html block renderer\n          customHTMLRenderer: createHTMLrenderer(),\n        });\n\n        editor.changeMode('wysiwyg');\n\n        const result = oneLineTrim`\n          <iframe width=\"420\" height=\"315\" src=\"https://www.youtube.com/embed/XyenY12fzAk\" class=\"html-block\"></iframe>\n          <p>test</p>\n        `;\n\n        expect(wwEditor.innerHTML).toContain(result);\n      });\n\n      it('should keep the html attributes with an empty string after changing the mode', () => {\n        createEditor({\n          el: container,\n          initialValue: '<iframe width=\"\" height=\"\" src=\"\"></iframe>',\n          previewHighlight: false,\n          // add iframe html block renderer\n          customHTMLRenderer: createHTMLrenderer(),\n        });\n\n        editor.changeMode('wysiwyg');\n\n        const result = oneLineTrim`\n          <iframe width=\"\" height=\"\" src=\"\" class=\"html-block\"></iframe>\n        `;\n\n        expect(wwEditor.innerHTML).toContain(result);\n      });\n    });\n\n    describe('hooks option', () => {\n      const defaultImageBlobHookSpy = jest.fn();\n\n      function mockDefaultImageBlobHook() {\n        defaultImageBlobHookSpy.mockReset();\n\n        jest\n          .spyOn(imageHelper, 'addDefaultImageBlobHook')\n          .mockImplementation((emitter: Emitter) => {\n            emitter.listen('addImageBlobHook', defaultImageBlobHookSpy);\n          });\n      }\n\n      it('should remove default `addImageBlobHook` event handler after registering hook', () => {\n        const spy = jest.fn();\n\n        mockDefaultImageBlobHook();\n\n        createEditor({\n          el: container,\n          hooks: {\n            addImageBlobHook: spy,\n          },\n        });\n\n        editor.eventEmitter.emit('addImageBlobHook');\n\n        expect(spy).toHaveBeenCalled();\n        expect(defaultImageBlobHookSpy).not.toHaveBeenCalled();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/eventEmitter.spec.ts",
    "content": "import EventEmitter from '@/event/eventEmitter';\n\n/* eslint-disable @typescript-eslint/no-empty-function */\ndescribe('eventEmitter', () => {\n  let emitter: EventEmitter;\n\n  beforeEach(() => {\n    emitter = new EventEmitter();\n  });\n\n  describe('Event registration', () => {\n    it('should throw exception when it use not registered event type', () => {\n      const throwableListen = () => {\n        emitter.listen('testNoEvent', () => {});\n      };\n\n      expect(throwableListen).toThrow(new Error('There is no event type testNoEvent'));\n    });\n\n    it('should throw exception when it register event type that already have', () => {\n      emitter.addEventType('testAlreadyHaveEvent');\n\n      const throwableListen = () => {\n        emitter.addEventType('testAlreadyHaveEvent');\n      };\n\n      expect(throwableListen).toThrow(\n        new Error('There is already have event type testAlreadyHaveEvent')\n      );\n    });\n  });\n\n  describe('emit()', () => {\n    beforeEach(() => {\n      emitter.addEventType('testEvent');\n      emitter.addEventType('testEventHook');\n    });\n\n    it('should emit and listen event', () => {\n      const spy = jest.fn();\n\n      emitter.listen('testEvent', spy);\n      emitter.emit('testEvent');\n\n      expect(spy).toHaveBeenCalled();\n    });\n\n    it('should return value that returned by listener', () => {\n      let count = 0;\n\n      emitter.listen('testEventHook', () => count);\n      emitter.listen('testEventHook', () => {\n        count += 1;\n\n        return count;\n      });\n\n      const result = emitter.emit('testEventHook');\n\n      expect(result).toEqual([0, 1]);\n    });\n\n    it('should return the empty array if listener have not return value', () => {\n      emitter.listen('testEvent', jest.fn());\n\n      const result = emitter.emit('testEvent');\n\n      expect(result).toEqual([]);\n    });\n\n    it('should trigger the event handler added with namespace', () => {\n      const spy = jest.fn();\n\n      emitter.listen('testEvent.ns', spy);\n      emitter.emit('testEvent');\n\n      expect(spy).toHaveBeenCalled();\n    });\n  });\n\n  describe('emitReduce()', () => {\n    beforeEach(() => {\n      emitter.addEventType('reduceTest');\n    });\n\n    it('reduce the return value', () => {\n      emitter.listen('reduceTest', (data) => {\n        data += 1;\n\n        return data;\n      });\n\n      emitter.listen('reduceTest', (data) => {\n        data += 2;\n\n        return data;\n      });\n\n      expect(emitter.emitReduce('reduceTest', 1)).toBe(4);\n    });\n\n    it('can have additional parameter', () => {\n      emitter.listen('reduceTest', (data, addition) => {\n        data += addition;\n\n        return data;\n      });\n\n      emitter.listen('reduceTest', (data, addition) => {\n        data += addition + 1;\n\n        return data;\n      });\n      expect(emitter.emitReduce('reduceTest', 1, 2)).toBe(6);\n    });\n\n    it('skip the return value if the value is falsy', () => {\n      emitter.listen('reduceTest', () => {});\n\n      emitter.listen('reduceTest', (data, addition) => {\n        data += addition + 1;\n\n        return data;\n      });\n\n      expect(emitter.emitReduce('reduceTest', 1, 2)).toBe(4);\n    });\n  });\n\n  describe('remove handler', () => {\n    let handlerBeRemoved: jest.Mock, handlerBeRemained: jest.Mock;\n\n    beforeEach(() => {\n      handlerBeRemoved = jest.fn();\n      handlerBeRemained = jest.fn();\n\n      emitter.addEventType('myEvent');\n      emitter.addEventType('myEvent2');\n    });\n\n    it('remove all event handler by event', () => {\n      emitter.listen('myEvent', handlerBeRemoved);\n      emitter.listen('myEvent.ns', handlerBeRemoved);\n\n      emitter.removeEventHandler('myEvent');\n\n      emitter.emit('myEvent');\n\n      expect(handlerBeRemoved).not.toHaveBeenCalled();\n    });\n\n    it('remove all event handler by namespace', () => {\n      emitter.listen('myEvent.ns', handlerBeRemoved);\n      emitter.listen('myEvent2.ns', handlerBeRemoved);\n\n      emitter.removeEventHandler('.ns');\n\n      emitter.emit('myEvent');\n      emitter.emit('myEvent2');\n\n      expect(handlerBeRemoved).not.toHaveBeenCalled();\n    });\n\n    it('should remain the non-namespace handler when removing namespace', () => {\n      emitter.listen('myEvent', handlerBeRemained);\n\n      emitter.removeEventHandler('.ns');\n\n      emitter.emit('myEvent');\n\n      expect(handlerBeRemained).toHaveBeenCalled();\n    });\n\n    it('remove specific event handler using namespace and type', () => {\n      emitter.listen('myEvent.ns', handlerBeRemoved);\n\n      emitter.removeEventHandler('myEvent.ns');\n\n      emitter.emit('myEvent');\n\n      expect(handlerBeRemoved).not.toHaveBeenCalled();\n    });\n\n    it('should remain the non-related handler when removing specific namespace and type', () => {\n      emitter.listen('myEvent2.ns', handlerBeRemained);\n\n      emitter.removeEventHandler('myEvent.ns');\n\n      emitter.emit('myEvent2');\n\n      expect(handlerBeRemained).toHaveBeenCalled();\n    });\n\n    it('remove specific event handler using name and handler', () => {\n      emitter.listen('myEvent', handlerBeRemoved);\n\n      emitter.removeEventHandler('myEvent', handlerBeRemoved);\n\n      emitter.emit('myEvent');\n\n      expect(handlerBeRemoved).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('hold event', () => {\n    let handler: jest.Mock;\n\n    function triggerEvent(apiName: 'emit' | 'emitReduce') {\n      if (apiName === 'emit') {\n        emitter.emit('myEvent');\n      } else {\n        emitter.emitReduce('myEvent', 0);\n      }\n    }\n\n    beforeEach(() => {\n      handler = jest.fn();\n\n      emitter.addEventType('myEvent');\n\n      emitter.listen('myEvent', handler);\n    });\n\n    (['emit', 'emitReduce'] as const).forEach((apiName) => {\n      it(`should not call the holding event with ${apiName} API`, () => {\n        emitter.holdEventInvoke(() => triggerEvent(apiName));\n\n        expect(handler).not.toHaveBeenCalled();\n      });\n\n      it(`should call the event after holding the event with ${apiName} API`, () => {\n        emitter.holdEventInvoke(() => triggerEvent(apiName));\n\n        triggerEvent(apiName);\n\n        expect(handler).toHaveBeenCalledTimes(1);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/helper/common.spec.ts",
    "content": "import { deepCopy, deepCopyArray, deepMergedCopy, includes } from '@/utils/common';\n\nit('\"deepCopy\" should copy the object deeply', () => {\n  const obj = { foo: 1, bar: { baz: 1 } };\n\n  expect(deepCopy(obj)).toEqual(obj);\n});\n\nit('\"deepCopyArray\" should copy the array deeply', () => {\n  const arr = [1, 2, ['a', 'b', ['c']], 3, 4];\n\n  expect(deepCopyArray(arr)).toEqual(arr);\n});\n\nit('\"deepMergedCopy\" should merge the objects and copy them deeply', () => {\n  const obj1 = { a: 1, b: { c: 1, d: 'a', e: 'c', f: { g: 'd' } } };\n  const obj2 = { a: 1, b: { c: 1, d: 'b', h: 'e' } };\n\n  expect(deepMergedCopy(obj1, obj2)).toEqual({\n    a: 1,\n    b: { c: 1, d: 'b', e: 'c', f: { g: 'd' }, h: 'e' },\n  });\n});\n\nit('\"includes\" should check whether the specific element is inlcuded in array', () => {\n  expect(includes([1, 2, 3], 1)).toBe(true);\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/helper/image.spec.ts",
    "content": "import EventEmitter from '@/event/eventEmitter';\nimport { addDefaultImageBlobHook, emitImageBlobHook } from '@/helper/image';\n\ndescribe('image processor', () => {\n  let em: EventEmitter;\n\n  beforeEach(() => {\n    em = new EventEmitter();\n  });\n\n  function mockReadAsDataURL() {\n    jest\n      .spyOn(FileReader.prototype, 'readAsDataURL')\n      .mockImplementation(function (this: FileReader) {\n        const ev = { target: { result: '/file.jpg' } } as ProgressEvent<FileReader>;\n\n        this.onload!(ev);\n      });\n  }\n\n  it('should call addImageBlobHook hook on calling emitImageBlobHook function', () => {\n    const spy = jest.fn();\n    const file = new File([new ArrayBuffer(1)], 'file.jpg');\n\n    em.listen('addImageBlobHook', spy);\n    emitImageBlobHook(em, file, 'drop');\n\n    expect(spy).toHaveBeenCalledWith(file, expect.any(Function), 'drop');\n  });\n\n  it('should execute addImage command through hook callback function in default addImageBlobHook hook', () => {\n    addDefaultImageBlobHook(em);\n    mockReadAsDataURL();\n\n    const spy = jest.fn();\n    const file = new File([new ArrayBuffer(1)], 'file.jpg');\n\n    em.listen('command', spy);\n    emitImageBlobHook(em, file, 'drop');\n\n    expect(spy).toHaveBeenCalledWith('addImage', { altText: 'file.jpg', imageUrl: '/file.jpg' });\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/markdown/__snapshots__/syntaxHighlight.spec.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`markdown editor syntax highlight atx heading 1`] = `\n<div>\n  <span class=\"toastui-editor-md-heading toastui-editor-md-heading1\">\n    <span class=\"toastui-editor-md-delimiter\">\n      #\n    </span>\n    heading\n  </span>\n</div>\n`;\n\nexports[`markdown editor syntax highlight blockQuote basic 1`] = `\n<div>\n  <span class=\"toastui-editor-md-block-quote\">\n    &gt;\n    <span class=\"toastui-editor-md-marked-text\">\n      block quote\n    </span>\n  </span>\n</div>\n`;\n\nexports[`markdown editor syntax highlight blockQuote with list 1`] = `\n<div>\n  <span class=\"toastui-editor-md-block-quote\">\n    &gt;\n    <span class=\"toastui-editor-md-list-item toastui-editor-md-list-item-style toastui-editor-md-list-item-odd\">\n      *\n    </span>\n    <span class=\"toastui-editor-md-delimiter toastui-editor-md-list-item\">\n      [\n      <span class=\"toastui-editor-md-meta\">\n      </span>\n      ]\n    </span>\n    <span class=\"toastui-editor-md-marked-text\">\n      block quote\n    </span>\n  </span>\n</div>\n`;\n\nexports[`markdown editor syntax highlight bulletList 1`] = `\n<div>\n  <span class=\"toastui-editor-md-list-item toastui-editor-md-list-item-style toastui-editor-md-list-item-odd\">\n    *\n    <span class=\"toastui-editor-md-marked-text\">\n    </span>\n  </span>\n  <span class=\"toastui-editor-md-marked-text\">\n    bullet list\n  </span>\n</div>\n`;\n\nexports[`markdown editor syntax highlight code block 1`] = `\n<div class=\"toastui-editor-md-code-block-line-background start\">\n  <span class=\"toastui-editor-md-code-block\">\n    <span class=\"toastui-editor-md-delimiter\">\n      \\`\\`\\`\n    </span>\n    <span class=\"toastui-editor-md-meta\">\n      js\n    </span>\n  </span>\n</div>\n<div class=\"toastui-editor-md-code-block-line-background\">\n  <span class=\"toastui-editor-md-code-block\">\n    console.log(\"editor\")\n  </span>\n</div>\n<div class=\"toastui-editor-md-code-block-line-background\">\n  <span class=\"toastui-editor-md-code-block\">\n    <span class=\"toastui-editor-md-delimiter\">\n      \\`\\`\\`\n    </span>\n  </span>\n</div>\n`;\n\nexports[`markdown editor syntax highlight code block within list 1`] = `\n<div>\n  <span class=\"toastui-editor-md-list-item toastui-editor-md-list-item-style toastui-editor-md-list-item-odd\">\n    *\n    <span class=\"toastui-editor-md-marked-text\">\n    </span>\n  </span>\n  <span class=\"toastui-editor-md-marked-text\">\n    list\n  </span>\n</div>\n<div>\n  <span class=\"toastui-editor-md-code-block\">\n    <span class=\"toastui-editor-md-marked-text\">\n    </span>\n    <span class=\"toastui-editor-md-delimiter\">\n      <span class=\"toastui-editor-md-marked-text\">\n        \\`\\`\\`\n      </span>\n    </span>\n    <span class=\"toastui-editor-md-marked-text\">\n      <span class=\"toastui-editor-md-meta\">\n        js\n      </span>\n    </span>\n  </span>\n</div>\n<div>\n  <span class=\"toastui-editor-md-code-block\">\n    <span class=\"toastui-editor-md-marked-text\">\n      console.log(\"editor\")\n    </span>\n  </span>\n</div>\n<div>\n  <span class=\"toastui-editor-md-code-block\">\n    <span class=\"toastui-editor-md-delimiter\">\n      <span class=\"toastui-editor-md-marked-text\">\n        \\`\\`\\`\n      </span>\n    </span>\n  </span>\n</div>\n`;\n\nexports[`markdown editor syntax highlight custom block 1`] = `\n<div class=\"toastui-editor-md-custom-block-line-background start\">\n  <span class=\"toastui-editor-md-custom-block\">\n    <span class=\"toastui-editor-md-delimiter\">\n      $$\n    </span>\n    <span class=\"toastui-editor-md-meta\">\n      custom\n    </span>\n  </span>\n</div>\n<div class=\"toastui-editor-md-custom-block-line-background\">\n  <span class=\"toastui-editor-md-custom-block\">\n    my custom element\n  </span>\n</div>\n<div class=\"toastui-editor-md-custom-block-line-background\">\n  <span class=\"toastui-editor-md-custom-block\">\n    <span class=\"toastui-editor-md-delimiter\">\n      $$\n    </span>\n  </span>\n</div>\n`;\n\nexports[`markdown editor syntax highlight emph 1`] = `\n<div>\n  <span class=\"toastui-editor-md-delimiter\">\n    *\n  </span>\n  <span class=\"toastui-editor-md-emph\">\n    emph\n  </span>\n  <span class=\"toastui-editor-md-delimiter\">\n    *\n  </span>\n</div>\n`;\n\nexports[`markdown editor syntax highlight image 1`] = `\n<div>\n  <span class=\"toastui-editor-md-link\">\n    <span class=\"toastui-editor-md-meta\">\n      !\n    </span>\n    [\n  </span>\n  <span class=\"toastui-editor-md-link toastui-editor-md-link-desc toastui-editor-md-marked-text\">\n    Logo\n  </span>\n  <span class=\"toastui-editor-md-link\">\n    ](\n  </span>\n  <span class=\"toastui-editor-md-link toastui-editor-md-link-url toastui-editor-md-marked-text\">\n    https://picsum.photos/200\n  </span>\n  <span class=\"toastui-editor-md-link\">\n    )\n  </span>\n</div>\n`;\n\nexports[`markdown editor syntax highlight inline code 1`] = `\n<div>\n  <span class=\"toastui-editor-md-code toastui-editor-md-delimiter toastui-editor-md-start\">\n    \\`\n  </span>\n  <span class=\"toastui-editor-md-code toastui-editor-md-marked-text\">\n    inline code\n  </span>\n  <span class=\"toastui-editor-md-code toastui-editor-md-delimiter toastui-editor-md-end\">\n    \\`\n  </span>\n</div>\n`;\n\nexports[`markdown editor syntax highlight link 1`] = `\n<div>\n  <span class=\"toastui-editor-md-link\">\n    [\n  </span>\n  <span class=\"toastui-editor-md-link toastui-editor-md-link-desc toastui-editor-md-marked-text\">\n    TOAST UI\n  </span>\n  <span class=\"toastui-editor-md-link\">\n    ](\n  </span>\n  <span class=\"toastui-editor-md-link toastui-editor-md-link-url toastui-editor-md-marked-text\">\n    https://ui.toast.com\n  </span>\n  <span class=\"toastui-editor-md-link\">\n    )\n  </span>\n</div>\n`;\n\nexports[`markdown editor syntax highlight orderedList 1`] = `\n<div>\n  <span class=\"toastui-editor-md-list-item toastui-editor-md-list-item-style toastui-editor-md-list-item-odd\">\n    1.\n    <span class=\"toastui-editor-md-marked-text\">\n    </span>\n  </span>\n  <span class=\"toastui-editor-md-marked-text\">\n    ordered list\n  </span>\n</div>\n`;\n\nexports[`markdown editor syntax highlight seText heading 1`] = `\n<div>\n  <span class=\"toastui-editor-md-heading toastui-editor-md-heading2\">\n    heading\n  </span>\n</div>\n<div>\n  <span class=\"toastui-editor-md-heading toastui-editor-md-heading1 toastui-editor-md-delimiter toastui-editor-md-setext\">\n    ---\n  </span>\n</div>\n`;\n\nexports[`markdown editor syntax highlight strike 1`] = `\n<div>\n  <span class=\"toastui-editor-md-delimiter\">\n    ~~\n  </span>\n  <span class=\"toastui-editor-md-strike\">\n    strike\n  </span>\n  <span class=\"toastui-editor-md-delimiter\">\n    ~~\n  </span>\n</div>\n`;\n\nexports[`markdown editor syntax highlight strong 1`] = `\n<div>\n  <span class=\"toastui-editor-md-delimiter\">\n    **\n  </span>\n  <span class=\"toastui-editor-md-strong\">\n    strong\n  </span>\n  <span class=\"toastui-editor-md-delimiter\">\n    **\n  </span>\n</div>\n`;\n\nexports[`markdown editor syntax highlight table basic 1`] = `\n<div>\n  <span class=\"toastui-editor-md-table\">\n    |\n    <span class=\"toastui-editor-md-table-cell\">\n      col2\n    </span>\n    |\n    <span class=\"toastui-editor-md-table-cell\">\n      col2\n    </span>\n  </span>\n</div>\n<div>\n  <span class=\"toastui-editor-md-table\">\n    | --- | ---\n  </span>\n</div>\n<div>\n  <span class=\"toastui-editor-md-table\">\n    |\n    <span class=\"toastui-editor-md-table-cell\">\n      data1\n    </span>\n    |\n    <span class=\"toastui-editor-md-table-cell\">\n      data2\n    </span>\n    |\n  </span>\n</div>\n`;\n\nexports[`markdown editor syntax highlight table with mark 1`] = `\n<div>\n  <span class=\"toastui-editor-md-table\">\n    |\n    <span class=\"toastui-editor-md-table-cell\">\n      col2\n    </span>\n    |\n    <span class=\"toastui-editor-md-table-cell\">\n      col2\n    </span>\n  </span>\n</div>\n<div>\n  <span class=\"toastui-editor-md-table\">\n    | --- | ---\n  </span>\n</div>\n<div>\n  <span class=\"toastui-editor-md-table\">\n    |\n    <span class=\"toastui-editor-md-table-cell\">\n      data1\n    </span>\n    |\n    <span class=\"toastui-editor-md-table-cell\">\n      <span class=\"toastui-editor-md-delimiter\">\n        **\n      </span>\n      <span class=\"toastui-editor-md-strong\">\n        data2\n      </span>\n      <span class=\"toastui-editor-md-delimiter\">\n        **\n      </span>\n    </span>\n    |\n  </span>\n</div>\n`;\n\nexports[`markdown editor syntax highlight tastkList 1`] = `\n<div>\n  <span class=\"toastui-editor-md-list-item toastui-editor-md-list-item-style toastui-editor-md-list-item-odd\">\n    *\n  </span>\n  <span class=\"toastui-editor-md-delimiter toastui-editor-md-list-item\">\n    [\n    <span class=\"toastui-editor-md-meta\">\n      x\n    </span>\n    ]\n  </span>\n  <span class=\"toastui-editor-md-marked-text\">\n    task list\n  </span>\n</div>\n`;\n\nexports[`markdown editor syntax highlight thematicBreak 1`] = `\n<div>\n  <span class=\"toastui-editor-md-thematic-break\">\n    ---\n  </span>\n</div>\n`;\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/markdown/keymap.spec.ts",
    "content": "import { oneLineTrim, source, stripIndent } from 'common-tags';\nimport { redo, undo } from 'prosemirror-history';\nimport {\n  chainCommands,\n  deleteSelection,\n  joinBackward,\n  selectNodeBackward,\n} from 'prosemirror-commands';\nimport * as keymaps from 'prosemirror-keymap';\nimport { Sourcepos, ToastMark } from '@toast-ui/toastmark';\nimport MarkdownEditor from '@/markdown/mdEditor';\nimport MarkdownPreview from '@/markdown/mdPreview';\nimport EventEmitter from '@/event/eventEmitter';\nimport { sanitizeHTML } from '@/sanitizer/htmlSanitizer';\nimport { getTextContent, removeDataAttr, TestEditorWithNoneDelayHistory } from './util';\n\n// @TODO: all tests should move to e2e test\n\nfunction forceKeymapFn(type: string, methodName: string, args: any[] = []) {\n  const { specs, view } = mde;\n  // @ts-ignore\n  const [keymapFn] = specs.specs.filter((spec) => spec.name === type);\n\n  // @ts-ignore\n  keymapFn[methodName](...args)(view.state, view.dispatch);\n}\n\nfunction forceBackspaceKeymap() {\n  const { state, dispatch } = mde.view;\n\n  chainCommands(deleteSelection, joinBackward, selectNodeBackward)(state, dispatch, mde.view);\n}\n\nlet mde: MarkdownEditor, em: EventEmitter, preview: MarkdownPreview;\n\nfunction getPreviewHTML() {\n  return oneLineTrim`${removeDataAttr(preview.getHTML())}`;\n}\n\nfunction assertSelection(mdPos: Sourcepos) {\n  expect(mde.getSelection()).toEqual(mdPos);\n}\n\nfunction execUndo() {\n  const { state, dispatch } = mde.view;\n\n  undo(state, dispatch);\n}\n\nbeforeEach(() => {\n  em = new EventEmitter();\n  mde = new TestEditorWithNoneDelayHistory(em, { toastMark: new ToastMark() });\n\n  const options = {\n    linkAttributes: null,\n    customHTMLRenderer: {},\n    isViewer: false,\n    highlight: false,\n    sanitizer: sanitizeHTML,\n  };\n\n  preview = new MarkdownPreview(em, options);\n});\n\n// @TODO: should add test case after developing the markdown editor API\n// describe('move table cell keymap', () => {\n// });\n\ndescribe('extend table keymap', () => {\n  it('should extend the table', () => {\n    const input = source`\n      | head1 | head2 |\n      | --- | --- |\n      | row1 | row1 |\n      | row2 | row2 |\n    `;\n    const result = source`\n      | head1 | head2 |\n      | --- | --- |\n      | row1 | row1 |\n      |  |  |\n      | row2 | row2 |\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([3, 2], [3, 2]);\n\n    forceKeymapFn('table', 'extendTable');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should delete the row in case of empty table content', () => {\n    const input = source`\n      | head1 | head2 |\n      | --- | --- |\n      | row1 | row1 |\n      |  |  |\n    `;\n    const result = source`\n      | head1 | head2 |\n      | --- | --- |\n      | row1 | row1 |\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([4, 2], [4, 2]);\n\n    forceKeymapFn('table', 'extendTable');\n\n    expect(getTextContent(mde)).toBe(`${result}\\n\\n`);\n  });\n\n  it('should not extend table list on multi line selection', () => {\n    const input = source`\n      | head1 | head2 |\n      | --- | --- |\n      | row1 | row1 |\n      | row2 | row2 |\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([2, 14], [4, 5]);\n\n    forceKeymapFn('table', 'extendTable');\n\n    expect(getTextContent(mde)).toBe(input);\n  });\n\n  it('should not extend the table out of table range', () => {\n    const input = source`\n      | head1 | head2 |\n      | --- | --- |\n      | row1 | row1 |\n      | row2 | row2 |\n    `;\n    const result = source`\n      | head1 | head2 |\n      | --- | --- |\n      | row1 | row1 |\n      | row2 | row2 |\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([4, 15], [4, 15]);\n\n    forceKeymapFn('table', 'extendTable');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should undo extend the table properly', () => {\n    const input = source`\n      | head1 | head2 |\n      | --- | --- |\n      | row1 | row1 |\n      | row2 | row2 |\n      text\n    `;\n    const result = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th>head1</th>\n            <th>head2</th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td>row1</td>\n            <td>row1</td>\n          </tr>\n          <tr>\n            <td>row2</td>\n            <td>row2</td>\n          </tr>\n        </tbody>\n      </table>\n      <p>text</p>\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([4, 2], [4, 2]);\n\n    forceKeymapFn('table', 'extendTable');\n\n    execUndo();\n\n    expect(getPreviewHTML()).toBe(result);\n  });\n});\n\ndescribe('extend block quote keymap', () => {\n  it('should extend the block quote', () => {\n    mde.setMarkdown('> block');\n    mde.setSelection([1, 8], [1, 8]);\n\n    forceKeymapFn('blockQuote', 'extendBlockQuote');\n\n    expect(getTextContent(mde)).toBe('> block\\n> ');\n    assertSelection([\n      [2, 3],\n      [2, 3],\n    ]);\n  });\n\n  it('should extend the block quote with sliced text', () => {\n    mde.setMarkdown('> block');\n    mde.setSelection([1, 6], [1, 6]);\n\n    forceKeymapFn('blockQuote', 'extendBlockQuote');\n\n    expect(getTextContent(mde)).toBe('> blo\\n> ck');\n    assertSelection([\n      [2, 3],\n      [2, 3],\n    ]);\n  });\n\n  it('should not extend the block quote on multi line selection', () => {\n    const input = '> block1\\n> block2';\n\n    mde.setMarkdown(input);\n    mde.setSelection([1, 2], [2, 4]);\n\n    forceKeymapFn('blockQuote', 'extendBlockQuote');\n\n    expect(getTextContent(mde)).toBe(input);\n  });\n\n  it('should delete the row in case of empty block quote content', () => {\n    mde.setMarkdown('> block\\n> ');\n    mde.setSelection([2, 2], [2, 2]);\n\n    forceKeymapFn('blockQuote', 'extendBlockQuote');\n\n    expect(getTextContent(mde)).toBe('> block\\n\\n');\n  });\n\n  it('should delete the row in case of empty block quote content with next content', () => {\n    mde.setMarkdown('> block\\n>\\nparagraph');\n    mde.setSelection([2, 2], [2, 2]);\n\n    forceKeymapFn('blockQuote', 'extendBlockQuote');\n\n    expect(getTextContent(mde)).toBe('> block\\n\\n\\nparagraph');\n  });\n\n  it('should not extend block quote when position is start offset', () => {\n    mde.setMarkdown('> block');\n    mde.setSelection([1, 1], [1, 1]);\n\n    forceKeymapFn('blockQuote', 'extendBlockQuote');\n\n    expect(getTextContent(mde)).toBe('> block');\n  });\n\n  it('should undo extend the block quote properly', () => {\n    const input = '> block\\nparagraph';\n    const result = '<blockquote><p>block<br>paragraph</p></blockquote>';\n\n    mde.setMarkdown(input);\n    mde.setSelection([1, 6], [1, 6]);\n\n    forceKeymapFn('blockQuote', 'extendBlockQuote');\n\n    execUndo();\n\n    expect(getPreviewHTML()).toBe(result);\n  });\n});\n\ndescribe('extend list keymap', () => {\n  describe('bullet list', () => {\n    it('should extend the bullet list', () => {\n      const input = source`\n        * bullet\n      `;\n      const result = `${source`\n        * bullet\n        * \n      `} `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([1, 9], [1, 9]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(result);\n      assertSelection([\n        [2, 3],\n        [2, 3],\n      ]);\n    });\n\n    it('should extend the bullet list with sliced text', () => {\n      const input = source`\n        * bullet\n      `;\n      const result = source`\n        * bull\n        * et\n      `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([1, 7], [1, 7]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(result);\n      assertSelection([\n        [2, 3],\n        [2, 3],\n      ]);\n    });\n\n    it('should extend the nested bullet list', () => {\n      const input = stripIndent`\n        * bullet\n          * sub\n      `;\n      const result = `${source`\n        * bullet\n          * sub\n          * \n      `} `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([2, 8], [2, 8]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(result);\n    });\n\n    it('should extend the bullet list excluding blank line', () => {\n      const input = `${source`\n        * bullet1\n        * bullet2\n      `}\\n\\n`;\n      const result = `${source`\n        * bullet1\n        * bullet2\n        * \n      `} \\n\\n`;\n\n      mde.setMarkdown(input);\n      mde.setSelection([2, 10], [2, 10]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(result);\n    });\n\n    it('should extend the bullet list with task', () => {\n      const input = source`\n        * [ ] bullet\n      `;\n      const result = `${source`\n        * [ ] bullet\n        * [ ] \n      `} `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([1, 13], [1, 13]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(result);\n      assertSelection([\n        [2, 7],\n        [2, 7],\n      ]);\n    });\n\n    it('should not extend the bullet list on multi line selection', () => {\n      const input = source`\n        * bullet1\n        * bullet2\n      `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([1, 2], [2, 4]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(input);\n    });\n\n    it('should delete the row in case of empty bullet list content', () => {\n      const input = `${stripIndent`\n        * bullet1\n        * \n      `} `;\n      const result = `${source`\n        * bullet1\n      `}\\n\\n`;\n\n      mde.setMarkdown(input);\n      mde.setSelection([2, 2], [2, 2]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(result);\n    });\n\n    it('should delete the row in case of empty bullet task list content', () => {\n      const input = `${stripIndent`\n        * [ ] bullet1\n        * [ ]\n      `} `;\n      const result = `${source`\n        * [ ] bullet1\n      `}\\n\\n`;\n\n      mde.setMarkdown(input);\n      mde.setSelection([2, 5], [2, 5]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(result);\n    });\n\n    it('should not extend list when paragraph includes `* `', () => {\n      const input = source`\n        just paragraph* bullet\n      `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([1, 9], [1, 9]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(input);\n    });\n\n    it('should delete the row in case of empty bullet list content with next content', () => {\n      mde.setMarkdown('* bullet1\\n* \\nparagraph');\n      mde.setSelection([2, 3], [2, 3]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe('* bullet1\\n\\n\\nparagraph');\n    });\n\n    it('should undo extend the bullet list properly', () => {\n      const input = source`\n        * bullet\n        paragraph\n      `;\n      const result = oneLineTrim`\n        <ul>\n          <li>\n            <p>bullet<br>\n            paragraph</p>\n          </li>\n        </ul>\n      `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([1, 9], [1, 9]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      execUndo();\n\n      expect(getPreviewHTML()).toBe(result);\n    });\n  });\n\n  describe('ordered list', () => {\n    it('should extend the ordered list', () => {\n      const input = source`\n        1. ordered\n      `;\n      const result = `${source`\n        1. ordered\n        2. \n      `} `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([1, 11], [1, 11]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(result);\n      assertSelection([\n        [2, 4],\n        [2, 4],\n      ]);\n    });\n\n    it('should extend the ordered list with sliced text', () => {\n      const input = source`\n        1. ordered\n      `;\n      const result = source`\n        1. ord\n        2. ered\n      `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([1, 7], [1, 7]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(result);\n      assertSelection([\n        [2, 4],\n        [2, 4],\n      ]);\n    });\n\n    it('should reorder the list list in the middle of ordered list', () => {\n      const input = source`\n        1. ordered1\n        2. ordered2\n        3. ordered3\n      `;\n      const result = source`\n        1. ordered1\n        2. \n        3. ordered2\n        4. ordered3\n      `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([1, 12], [1, 12]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(result);\n    });\n\n    it('should extend the nested ordered list', () => {\n      const input = stripIndent`\n        1. ordered1\n            1. sub1\n            2. sub2\n        2. ordered2\n        3. ordered3\n      `;\n      const result = stripIndent`\n        1. ordered1\n            1. sub1\n            2. \n            3. sub2\n        2. ordered2\n        3. ordered3\n      `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([2, 12], [2, 12]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(result);\n    });\n\n    it('should extend the ordered list on ordered paragraph(not ordered list)', () => {\n      const input = stripIndent`\n        1. ordered1\n            2. sub1\n            3. sub2\n        2. ordered2\n        3. ordered3\n      `;\n      const result = stripIndent`\n        1. ordered1\n            2. sub1\n            3. \n            4. sub2\n        2. ordered2\n        3. ordered3\n      `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([2, 12], [2, 12]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(result);\n    });\n\n    it('should extend the ordered list with task', () => {\n      const input = source`\n        1. [ ] ordered\n      `;\n      const result = `${source`\n        1. [ ] ordered\n        2. [ ]\n      `} `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([1, 15], [1, 15]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(result);\n      assertSelection([\n        [2, 8],\n        [2, 8],\n      ]);\n    });\n\n    it('should not extend the ordered list on multi line selection', () => {\n      const input = source`\n        1. ordered1\n        2. ordered2\n      `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([1, 2], [2, 5]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(input);\n    });\n\n    it('should extend the ordered list excluding blank line', () => {\n      const input = `${source`\n        1. ordered1\n        2. ordered2\n      `}\\n\\n`;\n      const result = `${source`\n        1. ordered1\n        2. ordered2\n        3.  \n      `} \\n\\n`;\n\n      mde.setMarkdown(input);\n      mde.setSelection([2, 12], [2, 12]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(result);\n    });\n\n    it('should delete the row in case of empty ordered list content', () => {\n      const input = `${source`\n        1. ordered1\n        2. \n      `} `;\n      const result = `${source`\n        1. ordered1 \n      `}\\n\\n`;\n\n      mde.setMarkdown(input);\n      mde.setSelection([2, 2], [2, 2]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(result);\n    });\n\n    it('should delete the row in case of empty ordered task list content', () => {\n      const input = `${stripIndent`\n        1. [ ] ordered1\n        2. [ ]\n      `} `;\n      const result = `${source`\n        1. [ ] ordered1\n      `}\\n\\n`;\n\n      mde.setMarkdown(input);\n      mde.setSelection([2, 6], [2, 6]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(result);\n    });\n\n    it('should extend the ordered list with below bullet list', () => {\n      const input = source`\n        1. ordered1\n        2. ordered2\n\n        * bullet1\n        * bullet2\n      `;\n      const result = source`\n        1. ordered1\n        2. ordered2\n        3. \n\n        * bullet1\n        * bullet2\n      `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([2, 12], [2, 12]);\n\n      forceKeymapFn('listItem', 'extendList');\n\n      expect(getTextContent(mde)).toBe(result);\n    });\n  });\n});\n\ndescribe('toggle task list keymap', () => {\n  it('should toggle single bullet task list state', () => {\n    const input = source`\n      * [ ] task1\n    `;\n    const result = source`\n      * [x] task1\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([1, 6], [1, 6]);\n\n    forceKeymapFn('listItem', 'toggleTask');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should toggle multi bullet task list state', () => {\n    const input = source`\n      * [ ] task1\n      * [x] task2\n    `;\n    const result = source`\n      * [x] task1\n      * [ ] task2\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([1, 6], [2, 2]);\n\n    forceKeymapFn('listItem', 'toggleTask');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should toggle single ordered task list state', () => {\n    const input = source`\n      1. [ ] task1\n    `;\n    const result = source`\n      1. [x] task1\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([1, 6], [1, 6]);\n\n    forceKeymapFn('listItem', 'toggleTask');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should toggle multi ordered task list state', () => {\n    const input = source`\n      1. [ ] task1\n      2. [x] task2\n    `;\n    const result = source`\n      1. [x] task1\n      2. [ ] task2\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([1, 6], [2, 2]);\n\n    forceKeymapFn('listItem', 'toggleTask');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should toggle nested task list state', () => {\n    const input = stripIndent`\n      1. [x] task1\n      2. [ ] task2\n        * [x] sub-task1\n        * [x] sub-task2\n          1. [ ] sub-task3\n\n    `;\n    const result = stripIndent`\n      1. [ ] task1\n      2. [x] task2\n        * [ ] sub-task1\n        * [ ] sub-task2\n          1. [x] sub-task3\n\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([1, 6], [5, 6]);\n\n    forceKeymapFn('listItem', 'toggleTask');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should remain unchanged on non task list', () => {\n    const input = source`\n      1. task1\n        * sub-task1\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([1, 6], [2, 2]);\n\n    forceKeymapFn('listItem', 'toggleTask');\n\n    expect(getTextContent(mde)).toBe(input);\n  });\n});\n\ndescribe('delete lines keymap', () => {\n  it('should delete the single line', () => {\n    const input = stripIndent`\n      aaaa\n      bbbb\n    `;\n    const result = '\\nbbbb';\n\n    mde.setMarkdown(input);\n    mde.setSelection([1, 1], [1, 1]);\n\n    forceKeymapFn('paragraph', 'deleteLines');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should delete the multi lines', () => {\n    const input = stripIndent`\n      aaaa\n      bbbb\n      cccc\n    `;\n    const result = '\\ncccc';\n\n    mde.setMarkdown(input);\n    mde.setSelection([1, 1], [2, 1]);\n\n    forceKeymapFn('paragraph', 'deleteLines');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n});\n\ndescribe('move lines keymap', () => {\n  it('should move down the single line', () => {\n    const input = stripIndent`\n      aaaa\n      bbbb\n      cccc\n    `;\n    const result = stripIndent`\n      bbbb\n      aaaa\n      cccc\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([1, 1], [1, 1]);\n\n    forceKeymapFn('paragraph', 'moveDown');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should move down the multi lines', () => {\n    const input = stripIndent`\n      aaaa\n      bbbb\n      cccc\n    `;\n    const result = stripIndent`\n      cccc\n      aaaa\n      bbbb\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([1, 1], [2, 1]);\n\n    forceKeymapFn('paragraph', 'moveDown');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should not move lines when the selection includes last line', () => {\n    const input = stripIndent`\n      aaaa\n      bbbb\n      cccc\n    `;\n    const result = stripIndent`\n      aaaa\n      bbbb\n      cccc\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([2, 1], [3, 1]);\n\n    forceKeymapFn('paragraph', 'moveDown');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should move up the single line', () => {\n    const input = stripIndent`\n      aaaa\n      bbbb\n      cccc\n    `;\n    const result = stripIndent`\n      bbbb\n      aaaa\n      cccc\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([2, 1], [2, 1]);\n\n    forceKeymapFn('paragraph', 'moveUp');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should move up the multi lines', () => {\n    const input = stripIndent`\n      aaaa\n      bbbb\n      cccc\n    `;\n    const result = stripIndent`\n      bbbb\n      cccc\n      aaaa\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([2, 1], [3, 1]);\n\n    forceKeymapFn('paragraph', 'moveUp');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should not move lines when the selection includes first line', () => {\n    const input = stripIndent`\n      aaaa\n      bbbb\n      cccc\n    `;\n    const result = stripIndent`\n      aaaa\n      bbbb\n      cccc\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([1, 1], [2, 1]);\n\n    forceKeymapFn('paragraph', 'moveUp');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n});\n\n/* eslint-disable no-irregular-whitespace */\ndescribe('keep indentation in code block', () => {\n  it('should keep indentation in next new line', () => {\n    const input = stripIndent`\n    \\`\\`\\`js\n    console.log('line1');\n        console.log('line2');\n    \\`\\`\\`\n    `;\n\n    const result = stripIndent`\n    \\`\\`\\`js\n    console.log('line1');\n        console.log('line2');\n        \n    \\`\\`\\`\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([3, 26], [3, 26]);\n\n    forceKeymapFn('codeBlock', 'keepIndentation');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should keep indentation with sliced text', () => {\n    const input = stripIndent`\n      \\`\\`\\`js\n      console.log('line1');\n          console.log('line2');\n      \\`\\`\\`\n    `;\n    const result = stripIndent`\n      \\`\\`\\`js\n      console.log('line1');\n          console.log('li\n          ne2');\n      \\`\\`\\`\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([3, 20], [3, 20]);\n\n    forceKeymapFn('codeBlock', 'keepIndentation');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should remain unchanged on multi selection', () => {\n    const input = stripIndent`\n      \\`\\`\\`js\n      console.log('line1');\n          console.log('line2');\n      \\`\\`\\`\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([2, 3], [3, 10]);\n\n    forceKeymapFn('codeBlock', 'keepIndentation');\n\n    expect(getTextContent(mde)).toBe(input);\n  });\n\n  it('should undo extend the code block properly', () => {\n    const input = stripIndent`\n      \\`\\`\\`js\n      console.log('line1');\n          console.log('line2');\n      \\`\\`\\`\n    `;\n    const result = oneLineTrim`\n      <pre class=\"lang-js\">\n        <code data-language=\"js\">\n          console.log('line1');\n              console.log('line2');\n        </code>\n      </pre>\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([3, 20], [3, 20]);\n\n    forceKeymapFn('codeBlock', 'keepIndentation');\n\n    execUndo();\n\n    expect(getPreviewHTML()).toBe(result);\n  });\n});\n/* eslint-enable no-irregular-whitespace */\n\n// @TODO: should move key event test case to e2e test\ndescribe('default keymap', () => {\n  it('should delete the blank line properly when pressing the backspace key', () => {\n    mde.setMarkdown('# myText\\n\\ntest');\n    mde.setSelection([3, 1], [3, 1]);\n\n    forceBackspaceKeymap();\n\n    expect(getPreviewHTML()).toBe('<h1>myText</h1><p>test</p>');\n  });\n});\n\ndescribe('useCommandShortcut option', () => {\n  it('should not make keymaps with history command when the value is false', () => {\n    const spy = jest.spyOn(keymaps, 'keymap');\n\n    const useCommandShortcut = false;\n    const history = {\n      'Mod-z': undo,\n      'Shift-Mod-z': redo,\n    };\n\n    mde.createKeymaps(useCommandShortcut);\n\n    expect(spy).not.toHaveBeenCalledWith(history);\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/markdown/mdCommand.spec.ts",
    "content": "import { oneLineTrim, source, stripIndent } from 'common-tags';\nimport { undo } from 'prosemirror-history';\nimport { ToastMark } from '@toast-ui/toastmark';\nimport MarkdownEditor from '@/markdown/mdEditor';\nimport MarkdownPreview from '@/markdown/mdPreview';\nimport EventEmitter from '@/event/eventEmitter';\nimport { sanitizeHTML } from '@/sanitizer/htmlSanitizer';\nimport CommandManager from '@/commands/commandManager';\nimport { getTextContent, TestEditorWithNoneDelayHistory, removeDataAttr } from './util';\n\nlet mde: MarkdownEditor, em: EventEmitter, cmd: CommandManager, preview: MarkdownPreview;\n\nfunction execUndo() {\n  const { state, dispatch } = mde.view;\n\n  undo(state, dispatch);\n}\n\nfunction getPreviewHTML() {\n  return oneLineTrim`${removeDataAttr(preview.getHTML())}`;\n}\n\nbeforeEach(() => {\n  em = new EventEmitter();\n  mde = new TestEditorWithNoneDelayHistory(em, { toastMark: new ToastMark() });\n  cmd = new CommandManager(em, mde.commands, {}, () => 'markdown');\n\n  const options = {\n    linkAttributes: null,\n    customHTMLRenderer: {},\n    isViewer: false,\n    highlight: false,\n    sanitizer: sanitizeHTML,\n  };\n\n  preview = new MarkdownPreview(em, options);\n});\n\nafterEach(() => {\n  mde.destroy();\n  preview.destroy();\n});\n\ndescribe('bold command', () => {\n  it('should add bold syntax', () => {\n    mde.setMarkdown('bold');\n\n    cmd.exec('selectAll');\n    cmd.exec('bold');\n\n    expect(getTextContent(mde)).toBe('**bold**');\n  });\n\n  it('should remove bold syntax', () => {\n    mde.setMarkdown('**bold**');\n    mde.setSelection([1, 3], [1, 7]);\n\n    cmd.exec('bold');\n\n    expect(getTextContent(mde)).toBe('bold');\n  });\n\n  it('should remove bold syntax with empty text', () => {\n    mde.setMarkdown('****');\n    mde.setSelection([1, 3], [1, 3]);\n\n    cmd.exec('bold');\n\n    expect(getTextContent(mde)).toBe('');\n  });\n});\n\ndescribe('italic command', () => {\n  it('should add italic syntax', () => {\n    mde.setMarkdown('italic');\n\n    cmd.exec('selectAll');\n    cmd.exec('italic');\n\n    expect(getTextContent(mde)).toBe('*italic*');\n  });\n\n  it('should remove italic syntax', () => {\n    mde.setMarkdown('ab*italic*cd');\n    mde.setSelection([1, 4], [1, 10]);\n\n    cmd.exec('italic');\n\n    expect(getTextContent(mde)).toBe('abitaliccd');\n  });\n\n  it('should remove italic syntax with empty text', () => {\n    mde.setMarkdown('**');\n    mde.setSelection([1, 2], [1, 2]);\n\n    cmd.exec('italic');\n\n    expect(getTextContent(mde)).toBe('');\n  });\n});\n\ndescribe('strike command', () => {\n  it('should add strike syntax', () => {\n    mde.setMarkdown('strike');\n\n    cmd.exec('selectAll');\n    cmd.exec('strike');\n\n    expect(getTextContent(mde)).toBe('~~strike~~');\n  });\n\n  it('should remove strike syntax', () => {\n    mde.setMarkdown('~~strike~~');\n    mde.setSelection([1, 3], [1, 9]);\n\n    cmd.exec('strike');\n\n    expect(getTextContent(mde)).toBe('strike');\n  });\n\n  it('should remove strike syntax with empty text', () => {\n    mde.setMarkdown('~~~~');\n    mde.setSelection([1, 3], [1, 3]);\n\n    cmd.exec('strike');\n\n    expect(getTextContent(mde)).toBe('');\n  });\n});\n\ndescribe('code command', () => {\n  it('should add code syntax', () => {\n    mde.setMarkdown('code');\n\n    cmd.exec('selectAll');\n    cmd.exec('code');\n\n    expect(getTextContent(mde)).toBe('`code`');\n  });\n\n  it('should remove code syntax', () => {\n    mde.setMarkdown('`code`');\n    mde.setSelection([1, 2], [1, 6]);\n\n    cmd.exec('code');\n\n    expect(getTextContent(mde)).toBe('code');\n  });\n\n  it('should remove code syntax with empty text', () => {\n    mde.setMarkdown('``');\n    mde.setSelection([1, 2], [1, 2]);\n\n    cmd.exec('code');\n\n    expect(getTextContent(mde)).toBe('');\n  });\n});\n\ndescribe('blockQuote command', () => {\n  it('should add blockQuote syntax', () => {\n    mde.setMarkdown('blockQuote');\n\n    cmd.exec('selectAll');\n    cmd.exec('blockQuote');\n\n    expect(getTextContent(mde)).toBe('> blockQuote');\n  });\n\n  it('should add blockQuote syntax on empty node', () => {\n    cmd.exec('blockQuote');\n\n    expect(getTextContent(mde)).toBe('> ');\n  });\n\n  it('should remove blockQuote syntax', () => {\n    mde.setMarkdown('> blockQuote');\n\n    cmd.exec('selectAll');\n    cmd.exec('blockQuote');\n\n    expect(getTextContent(mde)).toBe('blockQuote');\n  });\n\n  it('should add blockQuote syntax on multi line', () => {\n    mde.setMarkdown('blockQuote\\ntext');\n\n    cmd.exec('selectAll');\n    cmd.exec('blockQuote');\n\n    expect(getTextContent(mde)).toBe('> blockQuote\\n> text');\n  });\n\n  it('should remove unnecessary space when adding the blockQuote syntax', () => {\n    mde.setMarkdown('  blockQuote');\n\n    cmd.exec('selectAll');\n    cmd.exec('blockQuote');\n\n    expect(getTextContent(mde)).toBe('> blockQuote');\n  });\n\n  it('should remove unnecessary space when removing the blockQuote syntax', () => {\n    mde.setMarkdown('>   blockQuote');\n\n    cmd.exec('selectAll');\n    cmd.exec('blockQuote');\n\n    expect(getTextContent(mde)).toBe('blockQuote');\n  });\n\n  it('should select last position of the line when adding the blockQuote syntax', () => {\n    mde.setMarkdown('\\ntest');\n\n    mde.setSelection([1, 1], [1, 1]);\n    cmd.exec('blockQuote');\n\n    expect(getTextContent(mde)).toBe('> \\ntest');\n    expect(mde.getSelection()).toEqual([\n      [1, 3],\n      [1, 3],\n    ]);\n  });\n\n  it('should undo blockQuote command properly', () => {\n    const input = 'test\\nparagraph';\n    const result = '<p>test<br>paragraph</p>';\n\n    mde.setMarkdown(input);\n\n    mde.setSelection([1, 1], [1, 1]);\n    cmd.exec('blockQuote');\n\n    execUndo();\n\n    expect(getPreviewHTML()).toBe(result);\n  });\n});\n\ndescribe('hr command', () => {\n  it('should add thematicBreak(hr) syntax', () => {\n    cmd.exec('hr');\n\n    expect(getTextContent(mde)).toBe('\\n***\\n');\n  });\n\n  it('should split the paragraph when adding thematicBreak(hr) syntax', () => {\n    mde.setMarkdown('paragraph');\n\n    mde.setSelection([1, 2], [1, 4]);\n    cmd.exec('hr');\n\n    expect(getTextContent(mde)).toBe('p\\n***\\nagraph');\n  });\n\n  it('should undo hr command properly', () => {\n    const input = 'test\\nparagraph';\n    const result = '<p>test<br>paragraph</p>';\n\n    mde.setMarkdown(input);\n\n    mde.setSelection([1, 5], [1, 5]);\n    cmd.exec('hr');\n\n    execUndo();\n\n    expect(getPreviewHTML()).toBe(result);\n  });\n});\n\ndescribe('addImage command', () => {\n  it('should add image syntax', () => {\n    cmd.exec('addImage', { altText: 'image', imageUrl: 'https://picsum.photos/200' });\n\n    expect(getTextContent(mde)).toBe('![image](https://picsum.photos/200)');\n  });\n\n  it('should escape image altText', () => {\n    cmd.exec('addImage', {\n      altText: 'mytext ()[]<>',\n      imageUrl: 'https://picsum.photos/200',\n    });\n\n    expect(getTextContent(mde)).toBe('![mytext ()\\\\[\\\\]<>](https://picsum.photos/200)');\n  });\n\n  it('should encode image url', () => {\n    cmd.exec('addImage', {\n      altText: 'image',\n      imageUrl: 'myurl ()[]<>',\n    });\n\n    expect(getTextContent(mde)).toBe('![image](myurl ()[]<>)');\n  });\n\n  it('should not decode url which is already encoded', () => {\n    cmd.exec('addImage', {\n      altText: 'image',\n      imageUrl: 'https://firebasestorage.googleapis.com/images%2Fimage.png?alt=media',\n    });\n\n    expect(getTextContent(mde)).toBe(\n      '![image](https://firebasestorage.googleapis.com/images%2Fimage.png?alt=media)'\n    );\n  });\n});\n\ndescribe('addLink command', () => {\n  it('should add link syntax', () => {\n    cmd.exec('addLink', { linkText: 'TOAST UI', linkUrl: 'https://ui.toast.com' });\n\n    expect(getTextContent(mde)).toBe('[TOAST UI](https://ui.toast.com)');\n  });\n\n  it('should escape link Text', () => {\n    cmd.exec('addLink', {\n      linkText: 'mytext ()[]<>',\n      linkUrl: 'https://ui.toast.com',\n    });\n\n    expect(getTextContent(mde)).toBe('[mytext ()\\\\[\\\\]<>](https://ui.toast.com)');\n  });\n\n  it('should not decode url which is already encoded', () => {\n    cmd.exec('addLink', {\n      linkText: 'TOAST UI',\n      linkUrl: 'https://firebasestorage.googleapis.com/links%2Fimage.png?alt=media',\n    });\n\n    expect(getTextContent(mde)).toBe(\n      '[TOAST UI](https://firebasestorage.googleapis.com/links%2Fimage.png?alt=media)'\n    );\n  });\n});\n\ndescribe('heading command', () => {\n  it('should add heading syntax', () => {\n    mde.setMarkdown('heading');\n    cmd.exec('heading', { level: 1 });\n\n    expect(getTextContent(mde)).toBe('# heading');\n  });\n\n  it('should add heading syntax on empty node', () => {\n    cmd.exec('heading', { level: 1 });\n\n    expect(getTextContent(mde)).toBe('# ');\n  });\n\n  it('should maintain the heading syntax on same heading level', () => {\n    mde.setMarkdown('## heading2');\n\n    cmd.exec('selectAll');\n    cmd.exec('heading', { level: 2 });\n\n    expect(getTextContent(mde)).toBe('## heading2');\n  });\n\n  it('should change the heading syntax on different heading level', () => {\n    mde.setMarkdown('## heading2');\n\n    cmd.exec('selectAll');\n    cmd.exec('heading', { level: 1 });\n\n    expect(getTextContent(mde)).toBe('# heading2');\n  });\n\n  it('should add heading syntax on multi line', () => {\n    mde.setMarkdown('heading1\\n# heading2');\n\n    cmd.exec('selectAll');\n    cmd.exec('heading', { level: 2 });\n\n    expect(getTextContent(mde)).toBe('## heading1\\n## heading2');\n  });\n\n  it('should select last position of the line when adding the heading syntax', () => {\n    mde.setMarkdown('\\ntest');\n\n    mde.setSelection([1, 1], [1, 1]);\n    cmd.exec('heading', { level: 1 });\n\n    expect(getTextContent(mde)).toBe('# \\ntest');\n    expect(mde.getSelection()).toEqual([\n      [1, 3],\n      [1, 3],\n    ]);\n  });\n});\n\ndescribe('codeBlock command', () => {\n  it('should add code block syntax', () => {\n    const result = source`\n      \\`\\`\\`\n\n      \\`\\`\\`\n    `;\n\n    cmd.exec('codeBlock');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should wrap the selection with code block syntax', () => {\n    const result = source`\n      \\`\\`\\`\n      console.log('codeBlock');\n      \\`\\`\\`\n    `;\n\n    mde.setMarkdown(`console.log('codeBlock');`);\n\n    cmd.exec('selectAll');\n    cmd.exec('codeBlock');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n});\n\ndescribe('bulletList command', () => {\n  it('should add bullet list syntax', () => {\n    cmd.exec('bulletList');\n\n    expect(getTextContent(mde)).toBe('* ');\n  });\n\n  it('should add bullet list syntax to empty line', () => {\n    mde.setMarkdown('\\n');\n\n    mde.setSelection([2, 1], [2, 1]);\n    cmd.exec('bulletList');\n\n    expect(getTextContent(mde)).toBe('\\n* ');\n  });\n\n  it('should add bullet list syntax on multi line', () => {\n    const input = source`\n      bullet1\n      bullet2\n    `;\n    const result = source`\n      * bullet1\n      * bullet2\n    `;\n\n    mde.setMarkdown(input);\n\n    cmd.exec('selectAll');\n    cmd.exec('bulletList');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should change ordered list to bullet list', () => {\n    const input = source`\n      1. ordered1\n      2. ordered2\n      3. ordered3\n    `;\n    const result = source`\n      * ordered1\n      * ordered2\n      * ordered3\n    `;\n\n    mde.setMarkdown(input);\n\n    mde.setSelection([2, 1], [2, 1]);\n    cmd.exec('bulletList');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should change ordered list to bullet list with depth', () => {\n    const input = source`\n      1. ordered1\n      2. ordered2\n      3. ordered3\n         1. sub1\n         2. sub2\n    `;\n    const result = source`\n      * ordered1\n      * ordered2\n      * ordered3\n         * sub1\n         * sub2\n    `;\n\n    mde.setMarkdown(input);\n\n    cmd.exec('selectAll');\n    cmd.exec('bulletList');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should undo bullet list command properly', () => {\n    const input = source`\n      1. ordered1\n      2. ordered2\n      3. ordered3\n         1. sub1\n         2. sub2\n    `;\n    const result = oneLineTrim`\n      <ol>\n        <li><p>ordered1</p></li>\n        <li><p>ordered2</p></li>\n        <li>\n          <p>ordered3</p>\n          <ol>\n            <li><p>sub1</p></li>\n            <li><p>sub2</p></li>\n          </ol>\n        </li>\n      </ol>\n    `;\n\n    mde.setMarkdown(input);\n\n    mde.setSelection([1, 2], [1, 2]);\n    cmd.exec('bulletList');\n\n    execUndo();\n\n    expect(getPreviewHTML()).toBe(result);\n  });\n\n  it('should add bullet list syntax to empty line after heading node', () => {\n    mde.setMarkdown('# heading\\n');\n\n    mde.setSelection([2, 1], [2, 1]);\n    cmd.exec('bulletList');\n\n    expect(getTextContent(mde)).toBe('# heading\\n* ');\n  });\n});\n\ndescribe('orderedList command', () => {\n  it('should add ordered list syntax', () => {\n    cmd.exec('orderedList');\n\n    expect(getTextContent(mde)).toBe('1. ');\n  });\n\n  it('should add ordered list syntax to empty line', () => {\n    mde.setMarkdown('\\n');\n\n    mde.setSelection([2, 1], [2, 1]);\n    cmd.exec('orderedList');\n\n    expect(getTextContent(mde)).toBe('\\n1. ');\n  });\n\n  it('should add ordered list syntax on multi line', () => {\n    const input = source`\n      ordered1\n      ordered2\n    `;\n    const result = source`\n      1. ordered1\n      2. ordered2\n    `;\n\n    mde.setMarkdown(input);\n\n    cmd.exec('selectAll');\n    cmd.exec('orderedList');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should change bullet list to ordered list', () => {\n    const input = source`\n      * bullet1\n      * bullet2\n      * bullet3\n    `;\n    const result = source`\n      1. bullet1\n      2. bullet2\n      3. bullet3\n    `;\n\n    mde.setMarkdown(input);\n\n    mde.setSelection([2, 1], [2, 1]);\n    cmd.exec('orderedList');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should change bullet list to ordered list with depth', () => {\n    const input = source`\n      * bullet1\n         * sub1\n         * sub2\n      * bullet2\n      * bullet3\n    `;\n    const result = source`\n      1. bullet1\n         1. sub1\n         2. sub2\n      2. bullet2\n      3. bullet3\n    `;\n\n    mde.setMarkdown(input);\n\n    cmd.exec('selectAll');\n    cmd.exec('orderedList');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should change paragraph to ordered list with prev bullet list', () => {\n    const input = source`\n      * bullet1\n\n      ordered1\n      ordered2\n    `;\n    const result = source`\n    * bullet1\n\n    1. ordered1\n    2. ordered2\n    `;\n\n    mde.setMarkdown(input);\n\n    mde.setSelection([3, 2], [4, 2]);\n    cmd.exec('orderedList');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should change bullet list to ordered list partially', () => {\n    const input = source`\n      * bullet1\n      * bullet2\n      * bullet3\n         * bullet4\n         * bullet5\n    `;\n    const firstResult = source`\n      1. bullet1\n      2. bullet2\n      3. bullet3\n         * bullet4\n         * bullet5\n    `;\n    const secondResult = source`\n      1. bullet1\n      2. bullet2\n      3. bullet3\n         1. bullet4\n         2. bullet5\n    `;\n\n    mde.setMarkdown(input);\n\n    mde.setSelection([1, 2], [1, 2]);\n    cmd.exec('orderedList');\n\n    expect(getTextContent(mde)).toBe(firstResult);\n\n    mde.setSelection([4, 2], [4, 2]);\n    cmd.exec('orderedList');\n\n    expect(getTextContent(mde)).toBe(secondResult);\n  });\n\n  it('should change bullet list to ordered list with extended ranges', () => {\n    const input = source`\n      * bullet1\n      * bullet2\n      * bullet3\n         * bullet4\n         * bullet5\n      * bullet6\n    `;\n    const result = source`\n      1. bullet1\n      2. bullet2\n      3. bullet3\n         * bullet4\n         * bullet5\n      4. bullet6\n    `;\n\n    mde.setMarkdown(input);\n\n    mde.setSelection([1, 2], [1, 2]);\n    cmd.exec('orderedList');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should undo ordered list command properly', () => {\n    const input = source`\n      * bullet1\n      * bullet2\n      * bullet3\n         * bullet4\n         * bullet5\n      * bullet6\n    `;\n    const result = oneLineTrim`\n      <ul>\n        <li><p>bullet1</p></li>\n        <li><p>bullet2</p></li>\n        <li>\n          <p>bullet3</p>\n          <ul>\n            <li><p>bullet4</p></li>\n            <li><p>bullet5</p></li>\n          </ul>\n        </li>\n        <li><p>bullet6</p></li>\n      </ul>\n    `;\n\n    mde.setMarkdown(input);\n\n    mde.setSelection([1, 2], [1, 2]);\n    cmd.exec('orderedList');\n\n    execUndo();\n\n    expect(getPreviewHTML()).toBe(result);\n  });\n\n  it('should add ordered list syntax to empty line after heading node', () => {\n    mde.setMarkdown('# heading\\n');\n\n    mde.setSelection([2, 1], [2, 1]);\n    cmd.exec('orderedList');\n\n    expect(getTextContent(mde)).toBe('# heading\\n1. ');\n  });\n});\n\ndescribe('taskList command', () => {\n  it('should add task list syntax', () => {\n    cmd.exec('taskList');\n\n    expect(getTextContent(mde)).toBe('* [ ] ');\n  });\n\n  it('should add task list syntax on multi line', () => {\n    const input = source`\n      task1\n      task2\n    `;\n    const result = source`\n      * [ ] task1\n      * [ ] task2\n    `;\n\n    mde.setMarkdown(input);\n\n    cmd.exec('selectAll');\n    cmd.exec('taskList');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should add task syntax to ordered list', () => {\n    const input = source`\n      1. ordered1\n      2. ordered2\n      3. ordered3\n    `;\n    const result = source`\n      1. [ ] ordered1\n      2. [ ] ordered2\n      3. [ ] ordered3\n    `;\n\n    mde.setMarkdown(input);\n\n    cmd.exec('selectAll');\n    cmd.exec('taskList');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should add task syntax to bullet list', () => {\n    const input = source`\n      * bullet1\n      * bullet2\n      * bullet3\n    `;\n    const result = source`\n      * [ ] bullet1\n      * [ ] bullet2\n      * [ ] bullet3\n    `;\n\n    mde.setMarkdown(input);\n\n    cmd.exec('selectAll');\n    cmd.exec('taskList');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should remove task syntax on ordered task list', () => {\n    const input = source`\n      1. [ ] ordered1\n      2. [ ] ordered2\n      3. [ ] ordered3\n    `;\n    const result = source`\n      1. ordered1\n      2. ordered2\n      3. ordered3\n    `;\n\n    mde.setMarkdown(input);\n\n    cmd.exec('selectAll');\n    cmd.exec('taskList');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should remove task syntax on bullet task list', () => {\n    const input = source`\n      * [ ] bullet1\n      * [ ] bullet2\n      * [ ] bullet3\n    `;\n    const result = source`\n      * bullet1\n      * bullet2\n      * bullet3\n    `;\n\n    mde.setMarkdown(input);\n\n    cmd.exec('selectAll');\n    cmd.exec('taskList');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n});\n\ndescribe('addTable command', () => {\n  it('should add table syntax', () => {\n    const result = `\\n${source`\n      |  |  |\n      | --- | --- |\n      |  |  |\n      |  |  |\n    `}`;\n\n    cmd.exec('addTable', { columnCount: 2, rowCount: 3 });\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should add table syntax to next line', () => {\n    const result = source`\n      text\n      |  |  |\n      | --- | --- |\n      |  |  |\n      |  |  |\n    `;\n\n    mde.setMarkdown('text');\n\n    cmd.exec('selectAll');\n    cmd.exec('addTable', { columnCount: 2, rowCount: 3 });\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should undo table command properly', () => {\n    mde.setMarkdown('text');\n\n    cmd.exec('selectAll');\n    cmd.exec('addTable', { columnCount: 2, rowCount: 3 });\n\n    execUndo();\n\n    expect(getPreviewHTML()).toBe('<p>text</p>');\n  });\n});\n\ndescribe('indent command', () => {\n  it('should not operate if not a list', () => {\n    mde.setMarkdown('text');\n    mde.setSelection([1, 3], [1, 3]);\n\n    cmd.exec('indent');\n\n    expect(getTextContent(mde)).toBe('text');\n  });\n\n  it('should add soft-tab indentation to first offset on multi line selection', () => {\n    const input = source`\n      * line1\n      * line2\n      * line3\n      * line4\n    `;\n    const result = stripIndent`\n      * line1\n          * line2\n          * line3\n      * line4\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([2, 3], [3, 2]);\n\n    cmd.exec('indent');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should undo indent command properly', () => {\n    const input = source`\n      * line1\n      * line2\n      * line3\n      * line4\n    `;\n    const result = oneLineTrim`\n      <ul>\n        <li><p>line1</p></li>\n        <li><p>line2</p></li>\n        <li><p>line3</p></li>\n        <li><p>line4</p></li>\n      </ul>\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([2, 3], [3, 2]);\n\n    cmd.exec('indent');\n\n    execUndo();\n\n    expect(getPreviewHTML()).toBe(result);\n  });\n\n  describe('ordered list', () => {\n    it('should reorder ordered list after adding soft-tab indentation based on caret position', () => {\n      const input = source`\n        1. line1\n        2. line2\n        3. line3\n        4. line4\n      `;\n      const result = stripIndent`\n        1. line1\n            1. line2\n        2. line3\n        3. line4\n      `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([2, 1], [2, 1]);\n\n      cmd.exec('indent');\n\n      expect(getTextContent(mde)).toBe(result);\n    });\n\n    it('should reorder ordered list after adding soft-tab indentation based on multi line selection', () => {\n      const input = source`\n        1. line1\n        2. line2\n        3. line3\n        4. line4\n      `;\n      const result = stripIndent`\n        1. line1\n            1. line2\n            2. line3\n        2. line4\n      `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([2, 3], [3, 2]);\n\n      cmd.exec('indent');\n\n      expect(getTextContent(mde)).toBe(result);\n    });\n\n    it('should reorder ordered list with empty list item', () => {\n      const input = source`\n        1. line1\n        2. line2\n        3. \n        4. line4\n      `;\n      const result = stripIndent`\n        1. line1\n        2. line2\n            1. \n        3. line4\n      `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([3, 2], [3, 3]);\n\n      cmd.exec('indent');\n\n      expect(getTextContent(mde)).toBe(result);\n    });\n\n    it('should change ordered list to paragraph properly', () => {\n      const input = stripIndent`\n      1. ordered1\n      2. ordered2\n          * sub1\n          * sub2\n            1. sub-ordered1\n            2. sub-ordered1\n            3. sub-ordered1\n    `;\n      const result = stripIndent`\n        1. ordered1\n        2. ordered2\n            * sub1\n            * sub2\n                  1. sub-ordered1\n              2. sub-ordered1\n              3. sub-ordered1\n      `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([5, 10], [5, 10]);\n\n      cmd.exec('indent');\n\n      expect(getTextContent(mde)).toBe(result);\n    });\n  });\n});\n\ndescribe('outdent command', () => {\n  it('should not operate if not a list', () => {\n    mde.setMarkdown('    text');\n    mde.setSelection([1, 5], [1, 5]);\n\n    cmd.exec('outdent');\n\n    expect(getTextContent(mde)).toBe('    text');\n  });\n\n  it('should remove soft-tab indentation from first offset on multi line selection', () => {\n    const input = stripIndent`\n      * line1\n          * line2\n          * line3\n      * line4\n    `;\n    const result = source`\n      * line1\n      * line2\n      * line3\n      * line4\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([2, 3], [3, 2]);\n\n    cmd.exec('outdent');\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should undo outdent command properly', () => {\n    const input = stripIndent`\n      * line1\n          * line2\n          * line3\n      * line4\n    `;\n    const result = oneLineTrim`\n      <ul>\n        <li>\n          <p>line1</p>\n          <ul>\n            <li><p>line2</p></li>\n            <li><p>line3</p></li>\n          </ul>\n        </li>\n        <li><p>line4</p></li>\n      </ul>\n    `;\n\n    mde.setMarkdown(input);\n    mde.setSelection([2, 3], [3, 2]);\n\n    cmd.exec('outdent');\n\n    execUndo();\n\n    expect(getPreviewHTML()).toBe(result);\n  });\n\n  describe('ordered list', () => {\n    it('should reorder ordered list after removing soft-tab indentation based on caret position', () => {\n      const input = stripIndent`\n        1. line1\n          1. line2\n        2. line3\n        3. line4\n      `;\n      const result = source`\n        1. line1\n        2. line2\n        3. line3\n        4. line4\n      `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([2, 1], [2, 1]);\n\n      cmd.exec('outdent');\n\n      expect(getTextContent(mde)).toBe(result);\n    });\n\n    it('should reorder ordered list after removing soft-tab indentation based on multi line selection', () => {\n      const input = stripIndent`\n        1. line1\n            1. line2\n            2. line3\n        2. line4\n      `;\n      const result = source`\n        1. line1\n        2. line2\n        3. line3\n        4. line4\n      `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([2, 3], [3, 2]);\n\n      cmd.exec('outdent');\n\n      expect(getTextContent(mde)).toBe(result);\n    });\n\n    it('should reorder ordered list with empty list item', () => {\n      const input = stripIndent`\n        1. line1\n        2. line2\n          1. \n        3. line4\n      `;\n      const result = source`\n        1. line1\n        2. line2\n        3. \n        4. line4\n      `;\n\n      mde.setMarkdown(input);\n      mde.setSelection([3, 2], [3, 3]);\n\n      cmd.exec('outdent');\n\n      expect(getTextContent(mde)).toBe(result);\n    });\n\n    it('should not throw error on line which has no indentation', () => {\n      const result = stripIndent`\n        1. line1\n        2. line2\n        3. line3\n        4. line4\n      `;\n\n      mde.setMarkdown(result);\n      mde.setSelection([1, 2], [3, 3]);\n\n      cmd.exec('outdent');\n\n      expect(getTextContent(mde)).toBe(result);\n    });\n  });\n});\n\ndescribe('customBlock command', () => {\n  it('should add custom block syntax', () => {\n    const result = source`\n      $$myCustom\n\n      $$\n    `;\n\n    cmd.exec('customBlock', { info: 'myCustom' });\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n\n  it('should wrap the selection with custom block syntax', () => {\n    const result = source`\n      $$myCustom\n      console.log('customBlock');\n      $$\n    `;\n\n    mde.setMarkdown(`console.log('customBlock');`);\n\n    cmd.exec('selectAll');\n    cmd.exec('customBlock', { info: 'myCustom' });\n\n    expect(getTextContent(mde)).toBe(result);\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/markdown/mdEditor.spec.ts",
    "content": "import { ToastMark } from '@toast-ui/toastmark';\nimport MarkdownEditor from '@/markdown/mdEditor';\nimport EventEmitter from '@/event/eventEmitter';\nimport { getTextContent } from './util';\n\nfunction getSelectedText() {\n  return document.getSelection()!.toString();\n}\n\nfunction getEditorHTML(editor: MarkdownEditor) {\n  return editor.view.dom.innerHTML;\n}\n\njest.useFakeTimers();\n\ndescribe('MarkdownEditor', () => {\n  let mde: MarkdownEditor, em: EventEmitter, el: HTMLElement;\n\n  beforeEach(() => {\n    em = new EventEmitter();\n    mde = new MarkdownEditor(em, { toastMark: new ToastMark() });\n    el = mde.el;\n    document.body.appendChild(el);\n  });\n\n  afterEach(() => {\n    jest.clearAllTimers();\n\n    mde.destroy();\n    document.body.removeChild(el);\n  });\n\n  it('should emit updatePreview event when editing the content', () => {\n    const spy = jest.fn();\n\n    em.listen('updatePreview', spy);\n\n    mde.setMarkdown('# myText');\n\n    expect(spy).toHaveBeenCalled();\n  });\n\n  it('setMarkdown API', () => {\n    mde.setMarkdown('# myText');\n\n    expect(getTextContent(mde)).toBe('# myText');\n  });\n\n  it('getMarkdown API', () => {\n    mde.setMarkdown('# myText');\n\n    const markdown = mde.getMarkdown();\n\n    expect(markdown).toBe('# myText');\n  });\n\n  it('setSelection API', () => {\n    mde.setMarkdown('# myText');\n    mde.setSelection([1, 1], [1, 2]);\n\n    // run setTimeout function when focusing the editor\n    jest.runAllTimers();\n\n    expect(getSelectedText()).toBe('#');\n  });\n\n  it('getSelection API', () => {\n    mde.setMarkdown('# myText');\n    mde.setSelection([1, 1], [1, 2]);\n\n    const selection = mde.getSelection();\n\n    expect(selection).toEqual([\n      [1, 1],\n      [1, 2],\n    ]);\n  });\n\n  it('setPlaceholder API', () => {\n    mde.setPlaceholder('Write something');\n\n    expect(getEditorHTML(mde)).toContain(\n      '<span class=\"placeholder ProseMirror-widget\">Write something</span>'\n    );\n  });\n\n  it('replaceSelection API', () => {\n    mde.setMarkdown('# myText');\n\n    mde.setSelection([1, 1], [1, 2]);\n    mde.replaceSelection('# newText\\n#newLine');\n\n    expect(getTextContent(mde)).toBe('# newText\\n#newLine myText');\n  });\n\n  it('focus API', () => {\n    mde.focus();\n\n    // run setTimeout function when focusing the editor\n    jest.runAllTimers();\n\n    expect(document.activeElement).toEqual(mde.view.dom);\n  });\n\n  it('blur API', () => {\n    mde.focus();\n    mde.blur();\n\n    expect(document.activeElement).not.toEqual(mde.view.dom);\n  });\n\n  it('setHeight API', () => {\n    mde.setHeight(100);\n\n    const { height } = mde.el.style;\n\n    expect(height).toBe('100px');\n  });\n\n  it('setMinHeight API', () => {\n    mde.setMinHeight(100);\n\n    const { minHeight } = mde.el.style;\n\n    expect(minHeight).toBe('100px');\n  });\n\n  it('addWidget API', () => {\n    const ul = document.createElement('ul');\n\n    ul.innerHTML = `\n      <li>Ryu</li>\n      <li>Lee</li>\n    `;\n\n    mde.addWidget(ul, 'top');\n\n    expect(document.body).toContainElement(ul);\n\n    mde.blur();\n\n    expect(document.body).not.toContainElement(ul);\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/markdown/mdPreview.spec.ts",
    "content": "import { MdPos, ToastMark } from '@toast-ui/toastmark';\nimport MarkdownPreview, { CLASS_HIGHLIGHT } from '@/markdown/mdPreview';\nimport MarkdownEditor from '@/markdown/mdEditor';\nimport EventEmitter from '@/event/eventEmitter';\nimport * as sanitizer from '@/sanitizer/htmlSanitizer';\nimport { createHTMLrenderer, removeDataAttr } from './util';\n\nfunction getHTML(preview: MarkdownPreview) {\n  return removeDataAttr(preview.getHTML());\n}\n\njest.useFakeTimers();\n\ndescribe('Preview', () => {\n  let eventEmitter: EventEmitter, preview: MarkdownPreview;\n\n  beforeEach(() => {\n    jest.spyOn(sanitizer, 'sanitizeHTML');\n\n    const options = {\n      linkAttributes: null,\n      customHTMLRenderer: {},\n      isViewer: false,\n      highlight: true,\n      sanitizer: sanitizer.sanitizeHTML,\n    };\n\n    eventEmitter = new EventEmitter();\n\n    preview = new MarkdownPreview(eventEmitter, options);\n  });\n\n  afterEach(() => {\n    jest.restoreAllMocks();\n    preview.destroy();\n  });\n\n  it('listen to updatePreview and update the preview', () => {\n    const doc = new ToastMark();\n    const editResult = doc.editMarkdown([1, 7], [1, 7], 'changed');\n\n    eventEmitter.emit('updatePreview', editResult);\n\n    expect(getHTML(preview)).toBe('<p>changed</p>');\n  });\n\n  it('should call sanitizeHTML', () => {\n    const doc = new ToastMark();\n    const editResult = doc.editMarkdown(\n      [1, 1],\n      [1, 1],\n      `<TABLE BACKGROUND=\"javascript:alert('XSS')\">`\n    );\n\n    eventEmitter.emit('updatePreview', editResult);\n\n    expect(sanitizer.sanitizeHTML).toHaveBeenCalledTimes(1);\n  });\n});\n\ndescribe('preview highlight', () => {\n  let eventEmitter: EventEmitter,\n    preview: MarkdownPreview,\n    editor: MarkdownEditor,\n    editorEl: HTMLElement;\n\n  function init(highlight: boolean) {\n    const options = {\n      linkAttributes: null,\n      customHTMLRenderer: {},\n      isViewer: false,\n      highlight,\n      sanitizer: sanitizer.sanitizeHTML,\n    };\n\n    eventEmitter = new EventEmitter();\n    editor = new MarkdownEditor(eventEmitter, { toastMark: new ToastMark() });\n    preview = new MarkdownPreview(eventEmitter, options);\n    editorEl = editor.getElement();\n\n    document.body.appendChild(editorEl);\n    document.body.appendChild(preview.getElement()!);\n  }\n\n  function setMarkdown(markdown: string) {\n    editor.setMarkdown(markdown);\n  }\n\n  function setCursor(caret: MdPos) {\n    editor.setSelection(caret, caret);\n  }\n\n  function blur() {\n    editor.blur();\n  }\n\n  function getHighlightedCount() {\n    return preview.el!.querySelectorAll(`.${CLASS_HIGHLIGHT}`).length;\n  }\n\n  function assertHighlighted(tagName: string, html: string) {\n    const el = preview.el!.querySelector(`.${CLASS_HIGHLIGHT}`)!;\n\n    expect(el.tagName).toBe(tagName);\n    expect(el.innerHTML).toBe(html);\n  }\n\n  afterEach(() => {\n    jest.clearAllTimers();\n    document.body.removeChild(editorEl);\n    editor.destroy();\n    preview.destroy();\n  });\n\n  it('highlighted element should be one', () => {\n    init(true);\n    setMarkdown('# Hello\\n\\nWorld');\n    setCursor([1, 1]);\n\n    expect(getHighlightedCount()).toBe(1);\n    assertHighlighted('H1', 'Hello');\n\n    setCursor([3, 1]);\n\n    expect(getHighlightedCount()).toBe(1);\n    assertHighlighted('P', 'World');\n  });\n\n  it('highlighted element is not displayed when highlight option is false', () => {\n    init(false);\n    setMarkdown('# Hello\\n\\nWorld');\n    setCursor([1, 1]);\n\n    expect(getHighlightedCount()).toBe(0);\n\n    setCursor([3, 1]);\n\n    expect(getHighlightedCount()).toBe(0);\n  });\n\n  it('paragraph inside tight list item should not be removed', () => {\n    init(true);\n    setMarkdown('- Item1\\n- Item2');\n    setCursor([1, 4]);\n\n    expect(assertHighlighted('P', 'Item1'));\n\n    setCursor([2, 4]);\n\n    expect(assertHighlighted('P', 'Item2'));\n  });\n\n  describe('table cell', () => {\n    beforeEach(() => {\n      init(true);\n      setMarkdown('| a | b |\\n| - | - |\\n| c | d |\\n\\n');\n    });\n\n    it('whitespace and delimiter should be considered as a table cell', () => {\n      setCursor([1, 2]);\n      assertHighlighted('TH', 'a');\n\n      setCursor([1, 5]);\n      assertHighlighted('TH', 'a');\n\n      setCursor([1, 6]);\n      assertHighlighted('TH', 'b');\n\n      setCursor([1, 8]);\n      assertHighlighted('TH', 'b');\n\n      setCursor([3, 1]);\n      assertHighlighted('TD', 'c');\n\n      setCursor([3, 5]);\n      assertHighlighted('TD', 'c');\n\n      setCursor([3, 6]);\n      assertHighlighted('TD', 'd');\n\n      setCursor([3, 8]);\n      assertHighlighted('TD', 'd');\n    });\n\n    it('delimiter row should not highlight any element', () => {\n      setCursor([2, 2]);\n      expect(getHighlightedCount()).toBe(0);\n\n      setCursor([2, 4]);\n      expect(getHighlightedCount()).toBe(0);\n\n      setCursor([2, 6]);\n      expect(getHighlightedCount()).toBe(0);\n    });\n\n    it('empty line next to table should not highlight any element ', () => {\n      setCursor([4, 1]);\n\n      expect(getHighlightedCount()).toBe(0);\n    });\n  });\n\n  it('the highlighted element disappears when blur event is triggered', () => {\n    init(true);\n\n    setMarkdown('# Heading');\n    setCursor([1, 1]);\n\n    // run setTimeout function when focusing the editor\n    jest.runAllTimers();\n\n    expect(getHighlightedCount()).toBe(1);\n\n    blur();\n\n    expect(getHighlightedCount()).toBe(0);\n  });\n});\n\ndescribe('Preview with html renderer', () => {\n  let eventEmitter: EventEmitter, preview: MarkdownPreview;\n\n  function createPreviewWithHTMLRenderer() {\n    const options = {\n      linkAttributes: null,\n      customHTMLRenderer: createHTMLrenderer(),\n      isViewer: false,\n      highlight: true,\n      sanitizer: sanitizer.sanitizeHTML,\n    };\n\n    sanitizer.registerTagWhitelistIfPossible('iframe');\n    eventEmitter = new EventEmitter();\n    preview = new MarkdownPreview(eventEmitter, options);\n  }\n\n  beforeEach(() => {\n    createPreviewWithHTMLRenderer();\n  });\n\n  it('should render iframe node to preview ignoring sanitizer tag', () => {\n    const doc = new ToastMark();\n    const editResult = doc.editMarkdown(\n      [1, 1],\n      [1, 1],\n      '<iframe width=\"420\" height=\"315\" src=\"https://www.youtube.com/embed/XyenY12fzAk\"></iframe>'\n    );\n\n    eventEmitter.emit('updatePreview', editResult);\n\n    expect(getHTML(preview)).toBe(\n      '<iframe src=\"https://www.youtube.com/embed/XyenY12fzAk\" height=\"315\" width=\"420\"></iframe>'\n    );\n  });\n\n  it('should render html inline node', () => {\n    const doc = new ToastMark();\n    const editResult = doc.editMarkdown([1, 1], [1, 1], '<big class=\"my-big\">content</big>');\n\n    eventEmitter.emit('updatePreview', editResult);\n\n    expect(getHTML(preview)).toBe('<p><big class=\"my-big\">content</big></p>');\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/markdown/smartTask.spec.ts",
    "content": "import { ToastMark } from '@toast-ui/toastmark';\nimport MarkdownEditor from '@/markdown/mdEditor';\nimport EventEmitter from '@/event/eventEmitter';\nimport { getTextContent } from './util';\n\nlet mde: MarkdownEditor, em: EventEmitter;\n\nfunction dispatchKeyup() {\n  const event = new KeyboardEvent('keyup', {\n    key: 'backspace',\n    bubbles: true,\n    cancelable: true,\n  });\n\n  mde.view.dom.dispatchEvent(event);\n}\n\nbeforeEach(() => {\n  em = new EventEmitter();\n  mde = new MarkdownEditor(em, { toastMark: new ToastMark() });\n});\n\nafterEach(() => {\n  mde.destroy();\n});\n\ndescribe('smart task', () => {\n  it('should add space between task brackets when collapsed', () => {\n    mde.setMarkdown('* [] aaa');\n    mde.setSelection([1, 4], [1, 4]);\n\n    dispatchKeyup();\n\n    expect(getTextContent(mde)).toBe('* [ ] aaa');\n  });\n\n  it('should remove spaces between task brackets when unnecessary spaces are included', () => {\n    mde.setMarkdown('* [ x ] aaa');\n    mde.setSelection([1, 4], [1, 4]);\n\n    dispatchKeyup();\n\n    expect(getTextContent(mde)).toBe('* [x] aaa');\n  });\n\n  it('should not emit script error and apply smart task when cursor position is not in the task list', () => {\n    mde.setMarkdown('*  *aaa*');\n    mde.setSelection([1, 4], [1, 4]);\n\n    dispatchKeyup();\n\n    expect(getTextContent(mde)).toBe('*  *aaa*');\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/markdown/syntaxHighlight.spec.ts",
    "content": "import { ToastMark } from '@toast-ui/toastmark';\nimport MarkdownEditor from '@/markdown/mdEditor';\nimport EventEmitter from '@/event/eventEmitter';\nimport { source } from 'common-tags';\n\nfunction getEditorHTML(editor: MarkdownEditor) {\n  return editor.view.dom.innerHTML;\n}\n\nlet mde: MarkdownEditor, em: EventEmitter;\n\nbeforeEach(() => {\n  em = new EventEmitter();\n  mde = new MarkdownEditor(em, { toastMark: new ToastMark() });\n});\n\nafterEach(() => {\n  mde.destroy();\n});\n\ndescribe('markdown editor syntax highlight', () => {\n  it('atx heading', () => {\n    mde.setMarkdown('# heading');\n\n    const html = getEditorHTML(mde);\n\n    expect(html).toMatchSnapshot();\n  });\n\n  it('seText heading', () => {\n    mde.setMarkdown('heading\\n---');\n\n    const html = getEditorHTML(mde);\n\n    expect(html).toMatchSnapshot();\n  });\n\n  describe('blockQuote', () => {\n    it('basic', () => {\n      mde.setMarkdown('> block quote');\n\n      const html = getEditorHTML(mde);\n\n      expect(html).toMatchSnapshot();\n    });\n\n    it('with list', () => {\n      mde.setMarkdown('> * [ ] block quote');\n\n      const html = getEditorHTML(mde);\n\n      expect(html).toMatchSnapshot();\n    });\n  });\n\n  it('bulletList', () => {\n    mde.setMarkdown('* bullet list');\n\n    const html = getEditorHTML(mde);\n\n    expect(html).toMatchSnapshot();\n  });\n\n  it('orderedList', () => {\n    mde.setMarkdown('1. ordered list');\n\n    const html = getEditorHTML(mde);\n\n    expect(html).toMatchSnapshot();\n  });\n\n  it('tastkList', () => {\n    mde.setMarkdown('* [x] task list');\n\n    const html = getEditorHTML(mde);\n\n    expect(html).toMatchSnapshot();\n  });\n\n  describe('table', () => {\n    it('basic', () => {\n      const input = source`\n          | col2 | col2\n          | --- | ---\n          | data1 | data2 |\n        `;\n\n      mde.setMarkdown(input);\n\n      const html = getEditorHTML(mde);\n\n      expect(html).toMatchSnapshot();\n    });\n\n    it('with mark', () => {\n      const input = source`\n          | col2 | col2\n          | --- | ---\n          | data1 | **data2** |\n        `;\n\n      mde.setMarkdown(input);\n\n      const html = getEditorHTML(mde);\n\n      expect(html).toMatchSnapshot();\n    });\n  });\n\n  it('thematicBreak', () => {\n    mde.setMarkdown('---');\n\n    const html = getEditorHTML(mde);\n\n    expect(html).toMatchSnapshot();\n  });\n\n  it('emph', () => {\n    mde.setMarkdown('*emph*');\n\n    const html = getEditorHTML(mde);\n\n    expect(html).toMatchSnapshot();\n  });\n\n  it('strong', () => {\n    mde.setMarkdown('**strong**');\n\n    const html = getEditorHTML(mde);\n\n    expect(html).toMatchSnapshot();\n  });\n\n  it('strike', () => {\n    mde.setMarkdown('~~strike~~');\n\n    const html = getEditorHTML(mde);\n\n    expect(html).toMatchSnapshot();\n  });\n\n  it('inline code', () => {\n    mde.setMarkdown('`inline code`');\n\n    const html = getEditorHTML(mde);\n\n    expect(html).toMatchSnapshot();\n  });\n\n  it('link', () => {\n    mde.setMarkdown('[TOAST UI](https://ui.toast.com)');\n\n    const html = getEditorHTML(mde);\n\n    expect(html).toMatchSnapshot();\n  });\n\n  it('image', () => {\n    mde.setMarkdown('![Logo](https://picsum.photos/200)');\n\n    const html = getEditorHTML(mde);\n\n    expect(html).toMatchSnapshot();\n  });\n\n  it('code block', () => {\n    mde.setMarkdown('```js\\nconsole.log(\"editor\")\\n```');\n\n    const html = getEditorHTML(mde);\n\n    expect(html).toMatchSnapshot();\n  });\n\n  it('code block within list', () => {\n    mde.setMarkdown('* list\\n  ```js\\n  console.log(\"editor\")\\n  ```');\n\n    const html = getEditorHTML(mde);\n\n    expect(html).toMatchSnapshot();\n  });\n\n  it('custom block', () => {\n    mde.setMarkdown('$$custom\\nmy custom element\\n$$');\n\n    const html = getEditorHTML(mde);\n\n    expect(html).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/markdown/util.ts",
    "content": "import { HTMLConvertorMap } from '@toast-ui/toastmark';\nimport { history } from 'prosemirror-history';\nimport MarkdownEditor from '@/markdown/mdEditor';\n\nexport function getTextContent(editor: MarkdownEditor) {\n  const { doc } = editor.view.state;\n  const docSize = doc.content.size;\n  let text = '';\n\n  doc.nodesBetween(0, docSize, (node, pos) => {\n    if (node.isText) {\n      text += node.text!.slice(Math.max(0, pos) - pos, docSize - pos);\n    } else if (node.isBlock && pos > 0) {\n      text += '\\n';\n    }\n  });\n\n  return text;\n}\n\nexport function removeDataAttr(html: string) {\n  return html.replace(/\\sdata-nodeid=\"\\d{1,}\"/g, '').trim();\n}\n\nexport function createHTMLrenderer() {\n  const customHTMLRenderer: HTMLConvertorMap = {\n    htmlBlock: {\n      // @ts-ignore\n      iframe(node: MdLikeNode) {\n        return [\n          { type: 'openTag', tagName: 'iframe', outerNewLine: true, attributes: node.attrs },\n          { type: 'html', content: node.childrenHTML },\n          { type: 'closeTag', tagName: 'iframe', outerNewLine: true },\n        ];\n      },\n    },\n    htmlInline: {\n      // @ts-ignore\n      big(node: MdLikeNode, { entering }: Context) {\n        return entering\n          ? { type: 'openTag', tagName: 'big', attributes: node.attrs }\n          : { type: 'closeTag', tagName: 'big' };\n      },\n    },\n  };\n\n  return customHTMLRenderer;\n}\n\nexport class TestEditorWithNoneDelayHistory extends MarkdownEditor {\n  get defaultPlugins() {\n    return [...this.keymaps, history({ newGroupDelay: -1 })];\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/sanitizer.spec.ts",
    "content": "import { registerTagWhitelistIfPossible, sanitizeHTML } from '@/sanitizer/htmlSanitizer';\n\ndescribe('sanitizeHTML', () => {\n  it('removes unnecessary tags', () => {\n    expect(sanitizeHTML('<script>alert(\"test\");</script>')).toBe('');\n    expect(sanitizeHTML('<embed type=\"image/jpg\" src=\"\">')).toBe('');\n    expect(sanitizeHTML('<object>child die</object>')).toBe('child die');\n    expect(sanitizeHTML('<input type=\"image\" />')).toBe('');\n    expect(sanitizeHTML('<base href=https://avocadot0ast.free.beeceptor.com>')).toBe('');\n  });\n\n  describe('attributes', () => {\n    describe('removes attributes with invalid value including xss script', () => {\n      it('table', () => {\n        expect(sanitizeHTML(`<TABLE BACKGROUND=\"javascript:alert('XSS')\">`)).toBe(\n          '<table></table>'\n        );\n        expect(sanitizeHTML(`<TABLE><TD BACKGROUND=\"javascript:alert('XSS')\"></TD>`)).toBe(\n          '<table><tbody><tr><td></td></tr></tbody></table>'\n        );\n      });\n\n      it('href attribute with a tag', () => {\n        expect(sanitizeHTML('<a href=\"javascript:alert();\">xss</a>')).toBe('<a>xss</a>');\n        expect(sanitizeHTML('<a href=\"  JaVaScRiPt: alert();\">xss</a>')).toBe('<a>xss</a>');\n        expect(sanitizeHTML('<a href=\"vbscript:alert();\">xss</a>')).toBe('<a>xss</a>');\n        expect(sanitizeHTML('<a href=\"  VBscript: alert(); \">xss</a>')).toBe('<a>xss</a>');\n        expect(sanitizeHTML('<a href=\"livescript:alert();\">xss</a>')).toBe('<a>xss</a>');\n        expect(sanitizeHTML('<a href=\"  LIVEScript: alert() ;\">xss</a>')).toBe('<a>xss</a>');\n        expect(sanitizeHTML(`123<a href=' javascript:alert();'>xss</a>`)).toBe('123<a>xss</a>');\n        expect(sanitizeHTML(`<a href='javas cript:alert()'>xss</a>`)).toBe('<a>xss</a>');\n      });\n\n      it('src attribute with img tag', () => {\n        expect(sanitizeHTML('<img src=\"javascript:alert();\">')).toBe('<img>');\n        expect(sanitizeHTML('<img src=\"  JaVaScRiPt: alert();\">')).toBe('<img>');\n        expect(sanitizeHTML('<img src=\"vbscript:alert();\">')).toBe('<img>');\n        expect(sanitizeHTML('<img src=\"  VBscript: alert(); \">')).toBe('<img>');\n        expect(sanitizeHTML('<img src=\"  LIVEScript: alert() ;\">')).toBe('<img>');\n        expect(sanitizeHTML('<img src=\"java script:alert();\">')).toBe('<img>');\n      });\n\n      it('src and onerror attribute with img tag', () => {\n        expect(\n          sanitizeHTML('<img src = x onerror = \"javascript: window.onerror = alert; throw XSS\">')\n        ).toBe('<img src=\"x\">');\n        expect(sanitizeHTML('\"><img src=\"x:x\" onerror=\"alert(XSS)\">')).toBe('\"&gt;<img>');\n        expect(sanitizeHTML('<img src=x:alert(alt) onerror=eval(src) alt=0>')).toBe(\n          '<img alt=\"0\">'\n        );\n      });\n\n      it('should remove onload attribute in svg', () => {\n        expect(sanitizeHTML('<svg><svg onload=alert(111)> </svg></svg>')).toBe(\n          '<svg><svg> </svg></svg>'\n        );\n        expect(sanitizeHTML('<svg><svg onLOad=alert(111)> </svg></svg>')).toBe(\n          '<svg><svg> </svg></svg>'\n        );\n        expect(sanitizeHTML('<svg><svg onLOad=\"alert(111)\"> </svg></svg>')).toBe(\n          '<svg><svg> </svg></svg>'\n        );\n        expect(sanitizeHTML(`<svg><svg onLOad='alert(111)'> </svg></svg>`)).toBe(\n          '<svg><svg> </svg></svg>'\n        );\n        expect(sanitizeHTML('<svg><svg onload=alert(1) onload=alert(2)>')).toBe(\n          '<svg><svg></svg></svg>'\n        );\n        expect(sanitizeHTML('<svg><svg x=\">\" onload=alert(1)>')).toBe(\n          '<svg><svg x=\">\"></svg></svg>'\n        );\n        expect(sanitizeHTML('<p><svg><svg onload=onload=alert(1)></svg></svg></p>')).toBe(\n          '<p><svg><svg></svg></svg></p>'\n        );\n      });\n\n      it('should remove <use> tag and href attribute in svg', () => {\n        expect(\n          sanitizeHTML(\n            '<svg><use href=\"data:image/svg+xml;base64,PHN2ZyBpZD0neCcgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJyAKICAgIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJyB3aWR0aD0nMTAwJyBoZWlnaHQ9JzEwMCc+PGEgeGxpbms6aHJlZj0namF2YXNjcmlwdDphbGVydCgxKSc+PHJlY3QgeD0nMCcgeT0nMCcgd2lkdGg9JzEwMCcgaGVpZ2h0PScxMDAnIC8+PC9hPjwvc3ZnPg#x\"></use></svg>'\n          )\n        ).toBe('<svg></svg>');\n        expect(\n          sanitizeHTML(\n            `<svg><use href=\"data:image/svg+xml;charset=ISO-2022-JP,<svg id='x' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='100' height='100'><a xlink:href='javas%1B%28Bcript:alert(1)'><rect x='0' y='0' width='100' height='100' /></a></svg>#x\"></use></svg>`\n          )\n        ).toBe('<svg></svg>');\n      });\n\n      it('should remove ontoggle attribute in details', () => {\n        expect(sanitizeHTML('<details open ontoggle=alert(1)>')).toBe(\n          '<details open=\"\"></details>'\n        );\n      });\n    });\n\n    describe('registerTagWhitelistIfPossible', () => {\n      it('if possible, should keep the tags when registered in the white tag list', () => {\n        registerTagWhitelistIfPossible('embed');\n        registerTagWhitelistIfPossible('iframe');\n\n        expect(sanitizeHTML('<iframe src=\"\"></iframe>')).toBe('<iframe src=\"\"></iframe>');\n        expect(sanitizeHTML('<embed type=\"image/jpg\" src=\"\">')).toBe(\n          '<embed src=\"\" type=\"image/jpg\">'\n        );\n      });\n\n      it('should remove the tags in case that the tag name cannot be white list', () => {\n        registerTagWhitelistIfPossible('sript');\n        registerTagWhitelistIfPossible('input');\n\n        expect(sanitizeHTML('<script>alert(\"test\");</script>')).toBe('');\n        expect(sanitizeHTML('<input type=\"image\" />')).toBe('');\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/vdom/template.spec.ts",
    "content": "import { VNode } from '@t/ui';\nimport html from '@/ui/vdom/template';\nimport { Component } from '@/ui/vdom/component';\n\nclass TestComponent extends Component {\n  render() {\n    return html`<div class=\"my-comp\">test</div>`;\n  }\n}\n\ndescribe('lit-html syntax', () => {\n  it('should be converted as vnode', () => {\n    const style = { position: 'absolute', top: 10, marginLeft: 10 };\n    const expected = {\n      type: 'div',\n      props: {\n        class: 'my-class',\n        style: { position: 'absolute', top: 10, marginLeft: 10 },\n      },\n      children: [\n        {\n          type: 'TEXT_NODE',\n          props: { nodeValue: 'test' },\n          children: [],\n        },\n      ],\n    };\n\n    const vnode = html`<div class=\"my-class\" style=${style}>test</div>` as VNode;\n\n    expect(vnode).toMatchObject(expected);\n  });\n\n  it('should be converted  with children array as vnode', () => {\n    const expected = {\n      type: 'div',\n      props: {\n        class: 'my-class',\n      },\n      children: [\n        {\n          type: 'span',\n          props: {},\n          children: [\n            {\n              type: 'TEXT_NODE',\n              props: { nodeValue: '1' },\n              children: [],\n            },\n          ],\n        },\n        {\n          type: 'span',\n          props: {},\n          children: [\n            {\n              type: 'TEXT_NODE',\n              props: { nodeValue: '2' },\n              children: [],\n            },\n          ],\n        },\n        {\n          type: 'span',\n          props: {},\n          children: [\n            {\n              type: 'TEXT_NODE',\n              props: { nodeValue: '3' },\n              children: [],\n            },\n          ],\n        },\n      ],\n    };\n\n    const vnode = html`\n      <div class=\"my-class\">${[1, 2, 3].map((num) => html`<span>${num}</span>`)}</div>\n    `;\n\n    expect(vnode).toMatchObject(expected);\n  });\n\n  it('should be not converted with null, undefined, false value', () => {\n    const expected = {\n      type: 'div',\n      props: {\n        class: 'my-class',\n      },\n      children: [\n        {\n          type: 'TEXT_NODE',\n          props: { nodeValue: 'test' },\n          children: [],\n        },\n      ],\n    };\n\n    const vnode = html`\n      <div class=\"my-class\">\n        ${null && html`<span>123</span>`}\n        ${\n          // eslint-disable-next-line no-undefined\n          undefined && html`<span>123</span>`\n        }\n        ${false && html`<span>123</span>`}test\n      </div>\n    ` as VNode;\n\n    expect(vnode).toMatchObject(expected);\n  });\n\n  it('should be converted with Component as vnode', () => {\n    const expected = {\n      type: TestComponent,\n      props: {\n        class: 'my-comp',\n        'data-id': 'my-comp',\n      },\n      children: [],\n    };\n\n    const vnode = html`<${TestComponent} class=\"my-comp\" data-id=\"my-comp\" />` as VNode;\n\n    expect(vnode).toMatchObject(expected);\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/viewer.spec.ts",
    "content": "import { oneLineTrim } from 'common-tags';\nimport Viewer from '@/viewer';\nimport { createHTMLrenderer, removeDataAttr } from './markdown/util';\n\ndescribe('Viewer', () => {\n  let viewer: Viewer, container: HTMLElement;\n\n  function getViewerHTML() {\n    return oneLineTrim`${removeDataAttr(\n      container.querySelector('.toastui-editor-contents')!.innerHTML\n    )}`;\n  }\n\n  beforeEach(() => {\n    container = document.createElement('div');\n\n    viewer = new Viewer({\n      el: container,\n      extendedAutolinks: true,\n      frontMatter: true,\n      initialValue: '# test\\n* list1\\n* list2',\n      customHTMLRenderer: createHTMLrenderer(),\n    });\n\n    document.body.appendChild(container);\n  });\n\n  afterEach(() => {\n    viewer.destroy();\n    document.body.removeChild(container);\n  });\n\n  it('should render properly', () => {\n    const expected = oneLineTrim`\n      <h1>test</h1>\n      <ul>\n        <li>\n          <p>list1</p>\n        </li>\n        <li>\n          <p>list2</p>\n        </li>\n      </ul>\n    `;\n\n    expect(getViewerHTML()).toBe(expected);\n  });\n\n  it('should update preview by setMarkdown API', () => {\n    viewer.setMarkdown('> block quote\\n# heading *emph*');\n\n    const expected = oneLineTrim`\n      <blockquote><p>block quote</p></blockquote>\n      <h1>\n        heading <em>emph</em>\n      </h1>\n    `;\n\n    expect(getViewerHTML()).toBe(expected);\n  });\n\n  it('should render htmlBlock properly', () => {\n    viewer.setMarkdown(\n      '<iframe src=\"https://www.youtube.com/embed/XyenY12fzAk\" height=\"315\" width=\"420\"></iframe>'\n    );\n\n    const expected =\n      '<iframe width=\"420\" height=\"315\" src=\"https://www.youtube.com/embed/XyenY12fzAk\"></iframe>';\n\n    expect(getViewerHTML()).toBe(expected);\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/wysiwyg/customBlock.spec.ts",
    "content": "import { oneLineTrim } from 'common-tags';\nimport { HTMLConvertorMap } from '@toast-ui/toastmark';\nimport { ToDOMAdaptor } from '@t/convertor';\nimport { WwToDOMAdaptor } from '@/wysiwyg/adaptor/wwToDOMAdaptor';\nimport WysiwygEditor from '@/wysiwyg/wwEditor';\nimport EventEmitter from '@/event/eventEmitter';\n\nlet wwe: WysiwygEditor, em: EventEmitter, toDOMAdaptor: ToDOMAdaptor;\n\nfunction createCustomBlockNode() {\n  const customBlock = wwe.schema.nodes.customBlock.create(\n    { info: 'myCustom' },\n    wwe.schema.text('myCustom Node!!')\n  );\n  const doc = wwe.schema.nodes.doc.create(null, customBlock);\n\n  return doc;\n}\n\nbeforeEach(() => {\n  const convertors: HTMLConvertorMap = {\n    myCustom(node) {\n      const span = document.createElement('span');\n\n      span.innerHTML = node.literal!;\n\n      return [\n        { type: 'openTag', tagName: 'div', attributes: { 'data-custom': 'myCustom' } },\n        { type: 'html', content: span.outerHTML },\n        { type: 'closeTag', tagName: 'div' },\n      ];\n    },\n  };\n\n  toDOMAdaptor = new WwToDOMAdaptor({}, convertors);\n  em = new EventEmitter();\n  wwe = new WysiwygEditor(em, { toDOMAdaptor });\n  wwe.setModel(createCustomBlockNode());\n});\n\nafterEach(() => {\n  wwe.destroy();\n});\n\nit('custom block node should be rendered in wysiwyg editor properly', () => {\n  const expected = oneLineTrim`\n    <div data-custom=\"myCustom\">\n      <span>myCustom Node!!</span>\n    </div>\n  `;\n\n  expect(wwe.getHTML()).toContain(expected);\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/wysiwyg/helper/pasteMsoList.spec.ts",
    "content": "import { oneLineTrim } from 'common-tags';\n\nimport { convertMsoParagraphsToList } from '@/wysiwyg/clipboard/pasteMsoList';\n\ndescribe('pasteMsoList helper', () => {\n  let container: HTMLElement;\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n  });\n\n  afterEach(() => {\n    container.parentNode!.removeChild(container);\n  });\n\n  describe('convertMsoParagraphsToList() convert paragraphs copied from ms office ', () => {\n    it('bullet list', () => {\n      const inputHTML = oneLineTrim`\n        <p class=\"MsoListParagraph\" style=\"margin-left:40.0pt;mso-para-margin-left:0gd;text-indent:-20.0pt;mso-list:l0 level1 lfo1\">\n          <span class=\"font\" style=\"font-family:Wingdings\">\n            <span style=\"mso-list:Ignore\">l\n              <span class=\"font\" style=\"font-family:&quot;Times New Roman&quot;\">\n                <span class=\"size\" style=\"font-size:7pt\">&nbsp; </span>\n              </span>\n            </span>\n          </span>\n          <span lang=\"KO\">foo</span>\n        </p>\n        <p class=\"MsoListParagraph\" style=\"margin-left:40.0pt;mso-para-margin-left:0gd;text-indent:-20.0pt;mso-list:l0 level1 lfo1\">\n          <span class=\"font\" style=\"font-family:Wingdings\">\n            <span style=\"mso-list:Ignore\">l\n              <span class=\"font\" style=\"font-family:&quot;Times New Roman&quot;\">\n                <span class=\"size\" style=\"font-size:7pt\">&nbsp; </span>\n              </span>\n            </span>\n          </span>\n          <span lang=\"KO\">bar</span>\n        </p>\n      `;\n\n      const result = convertMsoParagraphsToList(inputHTML);\n      const expected = oneLineTrim`\n        <p></p>\n        <ul>\n          <li><span lang=\"KO\">foo</span></li>\n          <li><span lang=\"KO\">bar</span></li>\n        </ul>\n      `;\n\n      expect(result).toBe(expected);\n    });\n\n    it('ordered list', () => {\n      const inputHTML = oneLineTrim`\n        <p class=\"MsoListParagraph\" style=\"margin-left:40.0pt;mso-para-margin-left:0gd;text-indent:-20.0pt;mso-list:l1 level1 lfo2\">\n          <span lang=\"KO\" style=\"mso-fareast-font-family:&quot;맑은 고딕&quot;;mso-fareast-theme-font:minor-latin;mso-bidi-font-family:&quot;맑은 고딕&quot;;mso-bidi-theme-font:minor-latin\">\n            <span style=\"mso-list:Ignore\">1.\n              <span class=\"font\" style=\"font-family:&quot;Times New Roman&quot;\">\n                <span class=\"size\" style=\"font-size:7pt\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span>\n              </span>\n            </span>\n          </span>\n          <span lang=\"KO\">가</span>\n        </p>\n        <p class=\"MsoListParagraph\" style=\"margin-left:40.0pt;mso-para-margin-left:0gd;\n        text-indent:-20.0pt;mso-list:l1 level1 lfo2\">\n          <span lang=\"KO\" style=\"mso-fareast-font-family:&quot;맑은 고딕&quot;;mso-fareast-theme-font:minor-latin;\n        mso-bidi-font-family:&quot;맑은 고딕&quot;;mso-bidi-theme-font:minor-latin\">\n            <span style=\"mso-list:Ignore\">2.\n              <span class=\"font\" style=\"font-family:&quot;Times New Roman&quot;\">\n                <span class=\"size\" style=\"font-size:7pt\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span>\n              </span>\n            </span>\n          </span>\n          <span lang=\"KO\">나</span>\n        </p>\n      `;\n\n      const result = convertMsoParagraphsToList(inputHTML);\n      const expected = oneLineTrim`\n        <p></p>\n        <ol>\n          <li><span lang=\"KO\">가</span></li>\n          <li><span lang=\"KO\">나</span></li>\n        </ol>\n      `;\n\n      expect(result).toBe(expected);\n    });\n\n    it('nested list', () => {\n      const inputHTML = oneLineTrim`\n        <p class=\"MsoListParagraph\" style=\"margin-left:40.0pt;mso-para-margin-left:0gd;\n        text-indent:-20.0pt;mso-list:l0 level1 lfo1\">\n          <span class=\"font\" style=\"font-family:Wingdings\">\n            <span style=\"mso-list:Ignore\">l\n              <span class=\"font\" style=\"font-family:&quot;Times New Roman&quot;\">\n                <span class=\"size\" style=\"font-size:7pt\">&nbsp; </span>\n              </span>\n            </span>\n          </span>\n          <span lang=\"KO\">foo</span>\n        </p>\n        <p class=\"MsoListParagraph\" style=\"margin-left:60.0pt;mso-para-margin-left:0gd;\ntext-indent:-20.0pt;mso-list:l0 level2 lfo1\">\n          <span lang=\"KO\" style=\"mso-fareast-font-family:&quot;맑은 고딕&quot;;mso-fareast-theme-font:minor-latin;\nmso-bidi-font-family:&quot;맑은 고딕&quot;;mso-bidi-theme-font:minor-latin\">\n            <span style=\"mso-list:Ignore\">1.\n              <span class=\"font\" style=\"font-family:&quot;Times New Roman&quot;\">\n                <span class=\"size\" style=\"font-size:7pt\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>\n              </span>\n            </span>\n          </span>\n          <span lang=\"KO\">가나다</span>\n        </p>\n      `;\n\n      const result = convertMsoParagraphsToList(inputHTML);\n      const expected = oneLineTrim`\n        <p></p>\n        <ul>\n          <li><span lang=\"KO\">foo</span></li>\n          <ol>\n            <li><span lang=\"KO\">가나다</span></li>\n          </ol>\n        </ul>\n      `;\n\n      expect(result).toBe(expected);\n    });\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/wysiwyg/keymap.spec.ts",
    "content": "import { oneLineTrim } from 'common-tags';\n\nimport { DOMParser } from 'prosemirror-model';\nimport {\n  chainCommands,\n  deleteSelection,\n  joinBackward,\n  selectNodeBackward,\n} from 'prosemirror-commands';\n\nimport WysiwygEditor from '@/wysiwyg/wwEditor';\nimport EventEmitter from '@/event/eventEmitter';\nimport { WwToDOMAdaptor } from '@/wysiwyg/adaptor/wwToDOMAdaptor';\nimport CellSelection from '@/wysiwyg/plugins/selection/cellSelection';\nimport { cls } from '@/utils/dom';\n\nconst CELL_SELECTION_CLS = cls('cell-selected');\nconst CODE_BLOCK_CLS = cls('ww-code-block');\n\ndescribe('keymap', () => {\n  let wwe: WysiwygEditor, em: EventEmitter;\n  let html;\n\n  function setContent(content: string) {\n    const wrapper = document.createElement('div');\n\n    wrapper.innerHTML = content;\n\n    const nodes = DOMParser.fromSchema(wwe.schema).parse(wrapper);\n\n    wwe.setModel(nodes);\n  }\n\n  function forceKeymapFn(type: string, methodName: string, args: any[] = []) {\n    const { specs, view } = wwe;\n    // @ts-ignore\n    const [keymapFn] = specs.specs.filter((spec) => spec.name === type);\n\n    // @ts-ignore\n    keymapFn[methodName](...args)(view.state, view.dispatch);\n  }\n\n  function selectCells(from: number, to: number) {\n    const { state, dispatch } = wwe.view;\n    const { doc, tr } = state;\n\n    const startCellPos = doc.resolve(from);\n    const endCellPos = doc.resolve(to);\n    const selection = new CellSelection(startCellPos, endCellPos);\n\n    dispatch!(tr.setSelection(selection));\n  }\n\n  beforeEach(() => {\n    const toDOMAdaptor = new WwToDOMAdaptor({}, {});\n\n    em = new EventEmitter();\n    wwe = new WysiwygEditor(em, { toDOMAdaptor });\n  });\n\n  afterEach(() => {\n    wwe.destroy();\n  });\n\n  describe('table', () => {\n    beforeEach(() => {\n      html = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p>foo</p></th>\n              <th><p>bar</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p>baz</p></td>\n              <td><p>qux</p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      setContent(html);\n    });\n\n    describe('moveToCell keymap with right (tab key)', () => {\n      it('should move to start of right cell', () => {\n        wwe.setSelection(7, 7); // in 'foo' cell\n\n        forceKeymapFn('table', 'moveToCell', ['right']);\n\n        expect(wwe.getSelection()).toEqual([12, 12]);\n      });\n\n      it('should move to first cell of next line', () => {\n        wwe.setSelection(13, 13); // in 'bar' cell\n\n        forceKeymapFn('table', 'moveToCell', ['right']);\n\n        expect(wwe.getSelection()).toEqual([23, 23]);\n      });\n    });\n\n    describe('moveToCell keymap with left (shift + tab key)', () => {\n      it('should move to end of left cell', () => {\n        wwe.setSelection(13, 13); // in 'bar' cell\n\n        forceKeymapFn('table', 'moveToCell', ['left']);\n\n        expect(wwe.getSelection()).toEqual([8, 8]);\n      });\n\n      it('should move to last cell of previous line', () => {\n        wwe.setSelection(24, 24); // in 'baz' cell\n\n        forceKeymapFn('table', 'moveToCell', ['left']);\n\n        expect(wwe.getSelection()).toEqual([15, 15]);\n      });\n    });\n\n    describe('moveInCell keymap with up', () => {\n      it('should move to end of up cell', () => {\n        wwe.setSelection(26, 26); // in 'baz' cell\n\n        forceKeymapFn('table', 'moveInCell', ['up']);\n\n        expect(wwe.getSelection()).toEqual([8, 8]);\n      });\n\n      it('should add paragraph when there is no content before table and cursor is in first row', () => {\n        wwe.setSelection(13, 13); // in 'bar' cell\n\n        forceKeymapFn('table', 'moveInCell', ['up']);\n\n        const expected = oneLineTrim`\n          <p><br></p>\n          <table>\n            <thead>\n              <tr>\n                <th><p>foo</p></th>\n                <th><p>bar</p></th>\n              </tr>\n            </thead>\n            <tbody>\n              <tr>\n                <td><p>baz</p></td>\n                <td><p>qux</p></td>\n              </tr>\n            </tbody>\n          </table>\n        `;\n\n        expect(wwe.getHTML()).toBe(expected);\n      });\n\n      it('should move to before table content when cursor is in first row', () => {\n        html = oneLineTrim`\n          <p>before</p>\n          <table>\n            <thead>\n              <tr>\n                <th><p>foo</p></th>\n                <th><p>bar</p></th>\n              </tr>\n            </thead>\n            <tbody>\n              <tr>\n                <td><p>baz</p></td>\n                <td><p>qux</p></td>\n              </tr>\n            </tbody>\n          </table>\n        `;\n\n        setContent(html);\n\n        wwe.setSelection(15, 15); // in 'foo' cell\n        forceKeymapFn('table', 'moveInCell', ['up']);\n\n        expect(wwe.getSelection()).toEqual([7, 7]); // 'before' paragraph\n      });\n    });\n\n    describe('moveInCell keymap with down', () => {\n      it('should move to start of down cell', () => {\n        wwe.setSelection(7, 7); // in 'foo' cell\n\n        forceKeymapFn('table', 'moveInCell', ['down']);\n\n        expect(wwe.getSelection()).toEqual([23, 23]);\n      });\n\n      it('should add paragraph when there is no content after table and cursor is in last row', () => {\n        wwe.setSelection(26, 26); // in 'baz' cell\n\n        forceKeymapFn('table', 'moveInCell', ['down']);\n\n        const expected = oneLineTrim`\n          <table>\n            <thead>\n              <tr>\n                <th><p>foo</p></th>\n                <th><p>bar</p></th>\n              </tr>\n            </thead>\n            <tbody>\n              <tr>\n                <td><p>baz</p></td>\n                <td><p>qux</p></td>\n              </tr>\n            </tbody>\n          </table>\n          <p><br></p>\n        `;\n\n        expect(wwe.getHTML()).toBe(expected);\n      });\n\n      it('should move to after table content when cursor is in last row', () => {\n        html = oneLineTrim`\n          <table>\n            <thead>\n              <tr>\n                <th><p>foo</p></th>\n                <th><p>bar</p></th>\n              </tr>\n            </thead>\n            <tbody>\n              <tr>\n                <td><p>baz</p></td>\n                <td><p>qux</p></td>\n              </tr>\n            </tbody>\n          </table>\n          <p>after</p>\n        `;\n\n        setContent(html);\n\n        wwe.setSelection(32, 32); // in 'qux' cell\n        forceKeymapFn('table', 'moveInCell', ['down']);\n\n        expect(wwe.getSelection()).toEqual([39, 39]); // 'after' paragraph\n      });\n    });\n\n    describe('moveInCell keymap with left and right', () => {\n      let expected: string;\n\n      beforeEach(() => {\n        expected = oneLineTrim`\n          <table class=\"ProseMirror-selectednode\" draggable=\"true\">\n            <thead>\n              <tr>\n                <th><p>foo</p></th>\n                <th><p>bar</p></th>\n              </tr>\n            </thead>\n            <tbody>\n              <tr>\n                <td><p>baz</p></td>\n                <td><p>qux</p></td>\n              </tr>\n            </tbody>\n          </table>\n        `;\n      });\n\n      it('should select table when cursor is in start of first cell', () => {\n        wwe.setSelection(5, 5); // in 'foo' cell\n\n        forceKeymapFn('table', 'moveInCell', ['left']);\n\n        expect(wwe.getHTML()).toBe(expected);\n      });\n\n      it('should select table when cursor is in end of last cell', () => {\n        wwe.setSelection(33, 33); // in 'qux' cell\n\n        forceKeymapFn('table', 'moveInCell', ['right']);\n\n        expect(wwe.getHTML()).toBe(expected);\n      });\n    });\n\n    it('deleteCells keymap should delete cells in selection', () => {\n      selectCells(3, 28);\n\n      forceKeymapFn('table', 'deleteCells');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th class=\"${CELL_SELECTION_CLS}\"><p><br></p></th>\n              <th class=\"${CELL_SELECTION_CLS}\"><p><br></p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td class=\"${CELL_SELECTION_CLS}\"><p><br></p></td>\n              <td class=\"${CELL_SELECTION_CLS}\"><p><br></p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    describe('exitTable keymap', () => {\n      it('should exit the table node and add paragraph', () => {\n        wwe.setSelection(5, 5); // in 'foo' cell\n\n        forceKeymapFn('table', 'exitTable');\n\n        const expected = oneLineTrim`\n          <table>\n            <thead>\n              <tr>\n                <th><p>foo</p></th>\n                <th><p>bar</p></th>\n              </tr>\n            </thead>\n            <tbody>\n              <tr>\n                <td><p>baz</p></td>\n                <td><p>qux</p></td>\n              </tr>\n            </tbody>\n          </table>\n          <p><br></p>\n        `;\n\n        expect(wwe.getHTML()).toBe(expected);\n        expect(wwe.getSelection()).toEqual([39, 39]); // in added paragraph\n      });\n    });\n  });\n\n  describe('table with list and multiple lines', () => {\n    beforeEach(() => {\n      html = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th>\n                <p>foo</p>\n                <p>bar</p>\n              </th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td>\n                <ul>\n                  <li><p>baz</p></li>\n                  <li><p>qux</p></li>\n                </ul>\n              </td>\n            </tr>\n            <tr>\n              <td>\n                <ul>\n                  <li>\n                    <p>quux</p>\n                    <ul>\n                      <li><p>quuz</p></li>\n                    </ul>\n                  </li>\n                </ul>\n              </td>\n            </tr>\n            <tr>\n              <td>\n                <p>corge</p>\n              </td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      setContent(html);\n    });\n\n    describe('moveInCell keymap with up', () => {\n      it('should move from first paragraph to end list item of up cell', () => {\n        wwe.setSelection(65, 65); // in 'corge' cell\n\n        forceKeymapFn('table', 'moveInCell', ['up']);\n\n        expect(wwe.getSelection()).toEqual([55, 55]); // in 'quux'\n      });\n\n      it('should move from first list item to end list item of up cell', () => {\n        wwe.setSelection(44, 44); // in 'quux' cell\n\n        forceKeymapFn('table', 'moveInCell', ['up']);\n\n        expect(wwe.getSelection()).toEqual([33, 33]); // in 'qux'\n      });\n    });\n\n    describe('moveInCell keymap with down', () => {\n      it('should move from last paragraph to start list item of down cell', () => {\n        wwe.setSelection(10, 10); // in 'bar'\n\n        forceKeymapFn('table', 'moveInCell', ['down']);\n\n        expect(wwe.getSelection()).toEqual([23, 23]); // in 'baz'\n      });\n\n      it('should move from last list item to start list item of down cell', () => {\n        wwe.setSelection(30, 30); // in 'qux'\n\n        forceKeymapFn('table', 'moveInCell', ['down']);\n\n        expect(wwe.getSelection()).toEqual([43, 43]); // in 'quux'\n      });\n    });\n  });\n\n  describe('code block', () => {\n    beforeEach(() => {\n      html = oneLineTrim`\n        <div data-language=\"text\" class=\"${CODE_BLOCK_CLS}\">\n          <pre>\n            <code>foo\\nbar\\nbaz</code>\n          </pre>\n        </div>\n      `;\n\n      setContent(html);\n    });\n\n    describe('moveCursor keymap with up', () => {\n      it('should add paragraph when there is no content before code block and cursor is in first line', () => {\n        wwe.setSelection(4, 4); // in 'foo' text\n\n        forceKeymapFn('codeBlock', 'moveCursor', ['up']);\n\n        const expected = oneLineTrim`\n          <p><br></p>\n          <div data-language=\"text\" class=\"${CODE_BLOCK_CLS}\">\n            <pre>\n              <code>foo\\nbar\\nbaz</code>\n            </pre>\n          </div>\n        `;\n\n        expect(wwe.getHTML()).toBe(expected);\n      });\n    });\n\n    describe('moveCursor keymap with down', () => {\n      it('should add paragraph when there is no content after code block and cursor is in last line', () => {\n        wwe.setSelection(10, 10); // in 'baz' text\n\n        forceKeymapFn('codeBlock', 'moveCursor', ['down']);\n\n        const expected = oneLineTrim`\n          <div data-language=\"text\" class=\"${CODE_BLOCK_CLS}\">\n            <pre>\n              <code>foo\\nbar\\nbaz</code>\n            </pre>\n          </div>\n          <p><br></p>\n        `;\n\n        expect(wwe.getHTML()).toBe(expected);\n      });\n    });\n  });\n\n  describe('list item', () => {\n    function forceBackspaceKeymap() {\n      const { view } = wwe;\n      const { state, dispatch } = view;\n\n      chainCommands(deleteSelection, joinBackward, selectNodeBackward)(state, dispatch, view);\n    }\n\n    it('should remove list item and lift up to previous list item by backspace keymap ', () => {\n      html = oneLineTrim`\n        <ul>\n          <li>item1</li>\n          <li></li>\n        </ul>\n      `;\n\n      setContent(html);\n      wwe.setSelection(9, 10); // in second list item\n\n      forceBackspaceKeymap();\n      forceKeymapFn('listItem', 'liftToPrevListItem');\n\n      const expected = oneLineTrim`\n        <ul>\n          <li><p>item1</p></li>\n        </ul>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should remove list item and lift up to parent list item by backspace keymap ', () => {\n      html = oneLineTrim`\n        <ul>\n          <li>item1</li>\n          <li>\n            item2\n            <ul>\n              <li></li>\n            </ul>\n          </li>\n        </ul>\n      `;\n\n      setContent(html);\n      wwe.setSelection(19, 20); // in nested last child list item\n\n      forceBackspaceKeymap();\n      forceKeymapFn('listItem', 'liftToPrevListItem');\n\n      const expected = oneLineTrim`\n        <ul>\n          <li><p>item1</p></li>\n          <li><p>item2</p></li>\n        </ul>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/wysiwyg/wwCommand.spec.ts",
    "content": "import { oneLineTrim } from 'common-tags';\n\nimport { DOMParser } from 'prosemirror-model';\n\nimport WysiwygEditor from '@/wysiwyg/wwEditor';\nimport EventEmitter from '@/event/eventEmitter';\nimport CommandManager from '@/commands/commandManager';\nimport { WwToDOMAdaptor } from '@/wysiwyg/adaptor/wwToDOMAdaptor';\nimport { cls } from '@/utils/dom';\n\nimport type { HTMLConvertorMap } from '@toast-ui/toastmark';\n\nconst CODE_BLOCK_CLS = cls('ww-code-block');\n\ndescribe('wysiwyg commands', () => {\n  let wwe: WysiwygEditor, em: EventEmitter, cmd: CommandManager;\n\n  function setTextToEditor(text: string) {\n    const { state, dispatch } = wwe.view;\n    const { tr, doc } = state;\n    const lines = text.split('\\n');\n    const node = lines.map((lineText) =>\n      wwe.schema.nodes.paragraph.create(null, wwe.schema.text(lineText))\n    );\n\n    dispatch(tr.replaceWith(0, doc.content.size, node));\n  }\n\n  function setContent(content: string) {\n    const wrapper = document.createElement('div');\n\n    wrapper.innerHTML = content;\n\n    const nodes = DOMParser.fromSchema(wwe.schema).parse(wrapper);\n\n    wwe.setModel(nodes);\n  }\n\n  beforeEach(() => {\n    const customHTMLRenderer: HTMLConvertorMap = {\n      myCustom(node) {\n        const span = document.createElement('span');\n\n        span.innerHTML = node.literal!;\n\n        return [\n          { type: 'openTag', tagName: 'div', attributes: { 'data-custom': 'myCustom' } },\n          { type: 'html', content: span.outerHTML },\n          { type: 'closeTag', tagName: 'div' },\n        ];\n      },\n    };\n    const toDOMAdaptor = new WwToDOMAdaptor({}, customHTMLRenderer);\n\n    em = new EventEmitter();\n    wwe = new WysiwygEditor(em, { toDOMAdaptor });\n    cmd = new CommandManager(em, {}, wwe.commands, () => 'wysiwyg');\n  });\n\n  afterEach(() => {\n    wwe.destroy();\n  });\n\n  describe('heading command', () => {\n    it('should add empty heading element', () => {\n      cmd.exec('heading', { level: 1 });\n\n      expect(wwe.getHTML()).toBe('<h1><br></h1>');\n    });\n\n    it('should add heading element to selection', () => {\n      setTextToEditor('foo');\n\n      cmd.exec('selectAll');\n      cmd.exec('heading', { level: 2 });\n\n      expect(wwe.getHTML()).toBe('<h2>foo</h2>');\n    });\n\n    it('should change heading element by level', () => {\n      setTextToEditor('foo');\n\n      cmd.exec('selectAll');\n      cmd.exec('heading', { level: 3 });\n\n      expect(wwe.getHTML()).toBe('<h3>foo</h3>');\n\n      cmd.exec('selectAll');\n      cmd.exec('heading', { level: 4 });\n\n      expect(wwe.getHTML()).toBe('<h4>foo</h4>');\n\n      cmd.exec('selectAll');\n      cmd.exec('heading', { level: 5 });\n\n      expect(wwe.getHTML()).toBe('<h5>foo</h5>');\n\n      cmd.exec('selectAll');\n      cmd.exec('heading', { level: 6 });\n\n      expect(wwe.getHTML()).toBe('<h6>foo</h6>');\n    });\n\n    it('should change heading element to paragraph with level 0', () => {\n      setTextToEditor('foo');\n\n      cmd.exec('selectAll');\n      cmd.exec('heading', { level: 0 });\n\n      expect(wwe.getHTML()).toBe('<p>foo</p>');\n    });\n  });\n\n  describe('hr command', () => {\n    it('should add hr element with empty paragraphs in empty document', () => {\n      cmd.exec('hr');\n\n      expect(wwe.getHTML()).toBe(oneLineTrim`\n        <p><br></p>\n        <div><hr></div>\n        <p><br></p>\n      `);\n    });\n\n    it('should add hr element with after empty paragraph', () => {\n      setTextToEditor('foo');\n      wwe.setSelection(2, 2);\n      cmd.exec('hr');\n\n      expect(wwe.getHTML()).toBe(oneLineTrim`\n        <p>foo</p>\n        <div><hr></div>\n        <p><br></p>\n      `);\n    });\n\n    it('should add only hr element', () => {\n      setTextToEditor('foo\\nbar');\n      wwe.setSelection(2, 2);\n      cmd.exec('hr');\n\n      expect(wwe.getHTML()).toBe(oneLineTrim`\n        <p>foo</p>\n        <div><hr></div>\n        <p>bar</p>\n      `);\n    });\n\n    it('should not add hr element when there is selection', () => {\n      setTextToEditor('foo');\n\n      cmd.exec('selectAll');\n      cmd.exec('hr');\n\n      expect(wwe.getHTML()).toBe('<p>foo</p>');\n    });\n  });\n\n  describe('blockQuote command', () => {\n    it('should add blockquote element including empty paragraph', () => {\n      cmd.exec('blockQuote');\n\n      expect(wwe.getHTML()).toBe('<blockquote><p><br></p></blockquote>');\n    });\n\n    it('should change blockquote element to selection', () => {\n      setTextToEditor('foo');\n\n      cmd.exec('selectAll');\n      cmd.exec('blockQuote');\n\n      expect(wwe.getHTML()).toBe('<blockquote><p>foo</p></blockquote>');\n    });\n\n    it('should wrap with blockquote element', () => {\n      setTextToEditor('foo');\n\n      cmd.exec('selectAll');\n      cmd.exec('blockQuote');\n      cmd.exec('blockQuote');\n\n      const expected = oneLineTrim`\n        <blockquote>\n          <blockquote><p>foo</p></blockquote>\n        </blockquote>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n  });\n\n  describe('codeBlock command', () => {\n    it('should add pre element including code element', () => {\n      cmd.exec('codeBlock');\n\n      expect(wwe.getHTML()).toBe(oneLineTrim`\n        <div data-language=\"text\" class=\"${CODE_BLOCK_CLS}\">\n          <pre>\n            <code><br></code>\n          </pre>\n        </div>\n      `);\n    });\n\n    it('should change pre element to selection', () => {\n      setTextToEditor('foo');\n\n      cmd.exec('selectAll');\n      cmd.exec('codeBlock');\n\n      expect(wwe.getHTML()).toBe(oneLineTrim`\n        <div data-language=\"text\" class=\"${CODE_BLOCK_CLS}\">\n          <pre>\n            <code>foo</code>\n          </pre>\n        </div>\n      `);\n    });\n  });\n\n  describe('bulletList command', () => {\n    it('should add ul element having empty list item', () => {\n      cmd.exec('bulletList');\n\n      const expected = oneLineTrim`\n        <ul>\n          <li><p><br></p></li>\n        </ul>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should change to bullet list item in selection', () => {\n      setTextToEditor('foo\\nbar\\nbaz');\n\n      cmd.exec('selectAll');\n      cmd.exec('bulletList');\n\n      const expected = oneLineTrim`\n        <ul>\n          <li><p>foo</p></li>\n          <li><p>bar</p></li>\n          <li><p>baz</p></li>\n        </ul>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n  });\n\n  describe('orderedList command', () => {\n    it('should add ol element having empty list item', () => {\n      cmd.exec('orderedList');\n\n      const expected = oneLineTrim`\n        <ol>\n          <li><p><br></p></li>\n        </ol>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should change to ordered list item in selection', () => {\n      setTextToEditor('foo\\nbar\\nbaz');\n\n      cmd.exec('selectAll');\n      cmd.exec('orderedList');\n\n      const expected = oneLineTrim`\n        <ol>\n          <li><p>foo</p></li>\n          <li><p>bar</p></li>\n          <li><p>baz</p></li>\n        </ol>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n  });\n\n  it('bulletList and orderedList command should change parent list to other list when in list item', () => {\n    setTextToEditor('foo\\nbar\\nbaz');\n\n    cmd.exec('selectAll');\n    cmd.exec('bulletList');\n\n    wwe.setSelection(3, 3); // in 'foo'\n    cmd.exec('orderedList');\n\n    let expected = oneLineTrim`\n      <ol>\n        <li><p>foo</p></li>\n        <li><p>bar</p></li>\n        <li><p>baz</p></li>\n      </ol>\n    `;\n\n    expect(wwe.getHTML()).toBe(expected);\n\n    wwe.setSelection(11, 11); // in 'bar'\n    cmd.exec('bulletList');\n\n    expected = oneLineTrim`\n      <ul>\n        <li><p>foo</p></li>\n        <li><p>bar</p></li>\n        <li><p>baz</p></li>\n      </ul>\n    `;\n\n    expect(wwe.getHTML()).toBe(expected);\n  });\n\n  describe('taskList command', () => {\n    it('should add task to ul element ', () => {\n      cmd.exec('taskList');\n\n      const expected = oneLineTrim`\n        <ul>\n          <li class=\"task-list-item\" data-task=\"true\">\n            <p><br></p>\n          </li>\n        </ul>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should change to task item in selection', () => {\n      setTextToEditor('foo\\nbar\\nbaz');\n\n      cmd.exec('selectAll');\n      cmd.exec('taskList');\n\n      const expected = oneLineTrim`\n        <ul>\n          <li class=\"task-list-item\" data-task=\"true\">\n            <p>foo</p>\n          </li>\n          <li class=\"task-list-item\" data-task=\"true\">\n            <p>bar</p>\n          </li>\n          <li class=\"task-list-item\" data-task=\"true\">\n            <p>baz</p>\n          </li>\n        </ul>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should toggle task list item', () => {\n      setTextToEditor('foo\\nbar\\nbaz');\n\n      cmd.exec('selectAll');\n      cmd.exec('taskList');\n\n      wwe.setSelection(3, 3); // from 'foo'\n      cmd.exec('bulletList');\n\n      let expected = oneLineTrim`\n        <ul>\n          <li>\n            <p>foo</p>\n          </li>\n          <li class=\"task-list-item\" data-task=\"true\">\n            <p>bar</p>\n          </li>\n          <li class=\"task-list-item\" data-task=\"true\">\n            <p>baz</p>\n          </li>\n        </ul>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n\n      wwe.setSelection(3, 12); // from 'foo' to 'bar'\n      cmd.exec('taskList');\n\n      expected = oneLineTrim`\n        <ul>\n          <li class=\"task-list-item\" data-task=\"true\">\n            <p>foo</p>\n          </li>\n          <li>\n            <p>bar</p>\n          </li>\n          <li class=\"task-list-item\" data-task=\"true\">\n            <p>baz</p>\n          </li>\n        </ul>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n  });\n\n  describe('bold command', () => {\n    beforeEach(() => setTextToEditor('foo'));\n\n    it('should add strong element to selection', () => {\n      cmd.exec('selectAll');\n      cmd.exec('bold');\n\n      expect(wwe.getHTML()).toBe('<p><strong>foo</strong></p>');\n    });\n\n    it('should toggle and remove strong element', () => {\n      cmd.exec('selectAll');\n      cmd.exec('bold');\n\n      cmd.exec('selectAll');\n      cmd.exec('bold');\n\n      expect(wwe.getHTML()).toBe('<p>foo</p>');\n    });\n  });\n\n  describe('italic command', () => {\n    beforeEach(() => setTextToEditor('foo'));\n\n    it('should add emphasis element to selection', () => {\n      cmd.exec('selectAll');\n      cmd.exec('italic');\n\n      expect(wwe.getHTML()).toBe('<p><em>foo</em></p>');\n    });\n\n    it('should toggle and remove emphasis element', () => {\n      cmd.exec('selectAll');\n      cmd.exec('bold');\n\n      cmd.exec('selectAll');\n      cmd.exec('bold');\n\n      expect(wwe.getHTML()).toBe('<p>foo</p>');\n    });\n  });\n\n  describe('strike command', () => {\n    beforeEach(() => setTextToEditor('foo'));\n\n    it('should add del element to selection', () => {\n      cmd.exec('selectAll');\n      cmd.exec('strike');\n\n      expect(wwe.getHTML()).toBe('<p><del>foo</del></p>');\n    });\n\n    it('should toggle and remove del element', () => {\n      cmd.exec('selectAll');\n      cmd.exec('strike');\n\n      cmd.exec('selectAll');\n      cmd.exec('strike');\n\n      expect(wwe.getHTML()).toBe('<p>foo</p>');\n    });\n  });\n\n  describe('code command', () => {\n    beforeEach(() => setTextToEditor('foo'));\n\n    it('should add code element to selection', () => {\n      cmd.exec('selectAll');\n      cmd.exec('code');\n\n      expect(wwe.getHTML()).toBe('<p><code>foo</code></p>');\n    });\n\n    it('should toggle and remove code element', () => {\n      cmd.exec('selectAll');\n      cmd.exec('code');\n\n      cmd.exec('selectAll');\n      cmd.exec('code');\n\n      expect(wwe.getHTML()).toBe('<p>foo</p>');\n    });\n  });\n\n  describe('addImage command', () => {\n    it('should add image element', () => {\n      cmd.exec('addImage', {\n        imageUrl: '#',\n      });\n\n      expect(wwe.getHTML()).toBe('<p><img src=\"#\"><br></p>');\n    });\n\n    it('should add image element with enabled attirbute', () => {\n      cmd.exec('addImage', {\n        imageUrl: '#',\n        altText: 'foo',\n        foo: 'test',\n      });\n\n      expect(wwe.getHTML()).toBe('<p><img src=\"#\" alt=\"foo\"><br></p>');\n    });\n\n    it('should not add image element when not having imageUrl attribute', () => {\n      cmd.exec('addImage', {\n        altText: 'foo',\n      });\n\n      expect(wwe.getHTML()).toBe('<p><br></p>');\n    });\n\n    it('should not decode url which is already encoded', () => {\n      cmd.exec('addImage', {\n        imageUrl: 'https://firebasestorage.googleapis.com/images%2Fimage.png?alt=media',\n        altText: 'foo',\n      });\n\n      expect(wwe.getHTML()).toBe(\n        '<p><img src=\"https://firebasestorage.googleapis.com/images%2Fimage.png?alt=media\" alt=\"foo\"><br></p>'\n      );\n    });\n  });\n\n  describe('addLink command', () => {\n    it('should add link element', () => {\n      cmd.exec('addLink', {\n        linkUrl: '#',\n        linkText: 'foo',\n      });\n\n      expect(wwe.getHTML()).toBe('<p><a href=\"#\">foo</a></p>');\n    });\n\n    it('should not add link element when no selection and attributes are missing', () => {\n      cmd.exec('addLink', {\n        linkText: 'foo',\n      });\n\n      expect(wwe.getHTML()).toBe('<p><br></p>');\n\n      cmd.exec('addLink', {\n        linkUrl: '#',\n      });\n\n      expect(wwe.getHTML()).toBe('<p><br></p>');\n    });\n\n    it('should change link url in selection', () => {\n      cmd.exec('addLink', {\n        linkUrl: '#',\n        linkText: 'foo bar baz',\n      });\n\n      wwe.setSelection(5, 8);\n\n      cmd.exec('addLink', {\n        linkUrl: 'http://test.com',\n        linkText: 'bar',\n      });\n\n      const expected = oneLineTrim`\n        <p>\n          <a href=\"#\">foo </a>\n          <a href=\"http://test.com\">bar</a>\n          <a href=\"#\"> baz</a>\n        </p>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should not decode url which is already encoded', () => {\n      cmd.exec('addLink', {\n        linkUrl: 'https://firebasestorage.googleapis.com/links%2Fimage.png?alt=media',\n        linkText: 'foo',\n      });\n\n      expect(wwe.getHTML()).toBe(\n        '<p><a href=\"https://firebasestorage.googleapis.com/links%2Fimage.png?alt=media\">foo</a></p>'\n      );\n    });\n  });\n\n  describe(`addLink command with 'linkAttributes' option`, () => {\n    beforeEach(() => {\n      const linkAttributes = {\n        target: '_blank',\n        rel: 'noopener noreferrer',\n      };\n      const toDOMAdaptor = new WwToDOMAdaptor({}, {});\n\n      em = new EventEmitter();\n      wwe = new WysiwygEditor(em, { toDOMAdaptor, linkAttributes });\n      cmd = new CommandManager(em, {}, wwe.commands, () => 'wysiwyg');\n    });\n\n    it('should add link element with link attributes', () => {\n      cmd.exec('addLink', {\n        linkUrl: '#',\n        linkText: 'foo',\n      });\n\n      expect(wwe.getHTML()).toBe(\n        '<p><a href=\"#\" target=\"_blank\" rel=\"noopener noreferrer\">foo</a></p>'\n      );\n    });\n  });\n\n  describe('toggleLink command', () => {\n    beforeEach(() => setTextToEditor('foo'));\n\n    it('should add link element to selection', () => {\n      cmd.exec('selectAll');\n      cmd.exec('toggleLink', {\n        linkUrl: 'linkUrl',\n      });\n\n      expect(wwe.getHTML()).toBe('<p><a href=\"linkUrl\">foo</a></p>');\n    });\n\n    it('should toggle link element to selection', () => {\n      cmd.exec('selectAll');\n      cmd.exec('toggleLink', {\n        linkUrl: 'linkUrl',\n      });\n\n      cmd.exec('selectAll');\n      cmd.exec('toggleLink');\n\n      expect(wwe.getHTML()).toBe('<p>foo</p>');\n    });\n  });\n\n  describe('history command', () => {\n    beforeEach(() => {\n      setTextToEditor('foo');\n\n      cmd.exec('selectAll');\n      cmd.exec('bold');\n      cmd.exec('italic');\n    });\n\n    it('undo go back to before previous action', () => {\n      cmd.exec('undo');\n      expect(wwe.getHTML()).toBe('<p><strong>foo</strong></p>');\n\n      cmd.exec('undo');\n      expect(wwe.getHTML()).toBe('<p>foo</p>');\n    });\n\n    it('redo cancel undo action', () => {\n      cmd.exec('undo');\n      cmd.exec('undo');\n      cmd.exec('redo');\n\n      expect(wwe.getHTML()).toBe('<p><strong>foo</strong></p>');\n    });\n  });\n\n  describe('indent command', () => {\n    let html;\n\n    beforeEach(() => {\n      html = oneLineTrim`\n        <ul>\n          <li>\n            <p>foo</p>\n            <ol>\n              <li><p>bar</p></li>\n              <li><p>baz</p></li>\n              <li><p>qux</p></li>\n            </ol>\n          </li>\n        </ul>\n      `;\n\n      setContent(html);\n    });\n\n    // @TODO move to 'tab' key event test\n    // it('should add spaces for tab when it is not in list', () => {\n    //   setContent('<p>foo</p>');\n\n    //   wwe.setSelection(1, 1);\n    //   cmd.exec( 'indent');\n\n    //   expect(wwe.getHTML()).toBe('<p>    foo</p>');\n\n    //   wwe.setSelection(1, 8);\n    //   cmd.exec( 'indent');\n\n    //   expect(wwe.getHTML()).toBe('<p>    </p>');\n    // });\n\n    it('should indent to list items at cursor position', () => {\n      wwe.setSelection(18, 18);\n      cmd.exec('indent');\n\n      const expected = oneLineTrim`\n        <ul>\n          <li>\n            <p>foo</p>\n            <ol>\n              <li>\n                <p>bar</p>\n                <ol>\n                  <li><p>baz</p></li>\n                </ol>\n              </li>\n              <li><p>qux</p></li>\n            </ol>\n          </li>\n        </ul>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should indent to list items as selection', () => {\n      wwe.setSelection(18, 26);\n      cmd.exec('indent');\n\n      const expected = oneLineTrim`\n        <ul>\n          <li>\n            <p>foo</p>\n            <ol>\n              <li>\n                <p>bar</p>\n                <ol>\n                  <li><p>baz</p></li>\n                  <li><p>qux</p></li>\n                </ol>\n              </li>\n            </ol>\n          </li>\n        </ul>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n  });\n\n  describe('outdent command', () => {\n    let html;\n\n    beforeEach(() => {\n      html = oneLineTrim`\n        <ul>\n          <li>\n            <p>foo</p>\n            <ol>\n              <li>\n                <p>bar</p>\n                <ul>\n                  <li><p>baz</p></li>\n                </ul>\n              </li>\n            </ol>\n          </li>\n        </ul>\n      `;\n\n      setContent(html);\n    });\n\n    // @TODO move to 'shift + tab' key event test\n    // it('should remove spaces for tab when it is not in list', () => {\n    //   setContent('<p> &nbsp; &nbsp;foo</p>');\n\n    //   wwe.setSelection(4, 4);\n    //   cmd.exec( 'outdent');\n\n    //   expect(wwe.getHTML()).toBe('<p>foo</p>');\n\n    //   setContent('<p>foo &nbsp; &nbsp;bar</p>');\n\n    //   wwe.setSelection(6, 6);\n    //   cmd.exec( 'outdent');\n\n    //   expect(wwe.getHTML()).toBe('<p>foo &nbsp;bar</p>');\n\n    //   wwe.setSelection(6, 8);\n    //   cmd.exec( 'outdent');\n\n    //   expect(wwe.getHTML()).toBe('<p>foobar</p>');\n    // });\n\n    it('should outdent to list items at cursor position', () => {\n      wwe.setSelection(19, 19);\n      cmd.exec('outdent');\n\n      const expected = oneLineTrim`\n        <ul>\n          <li>\n            <p>foo</p>\n            <ol>\n              <li><p>bar</p></li>\n              <li><p>baz</p></li>\n            </ol>\n          </li>\n        </ul>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should outdent to list items as selection', () => {\n      wwe.setSelection(10, 20);\n      cmd.exec('outdent');\n\n      const expected = oneLineTrim`\n        <ul>\n          <li><p>foo</p></li>\n          <li>\n            <p>bar</p>\n            <ul>\n              <li><p>baz</p></li>\n            </ul>\n          </li>\n        </ul>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should change list item of 1 depth into paragraph ', () => {\n      wwe.setSelection(3, 5);\n      cmd.exec('outdent');\n\n      const expected = oneLineTrim`\n        <p>foo</p>\n        <ol>\n          <li>\n            <p>bar</p>\n            <ul>\n              <li><p>baz</p></li>\n            </ul>\n          </li>\n        </ol>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n  });\n\n  describe('customBlock command', () => {\n    it('should add customBlock element', () => {\n      cmd.exec('customBlock', { info: 'myCustom' });\n\n      expect(wwe.getHTML()).toBe(oneLineTrim`\n        <div class=\"toastui-editor-custom-block\">\n          <div class=\"toastui-editor-custom-block-editor\" style=\"display: none;\"></div>\n          <div class=\"toastui-editor-custom-block-view\">\n            <div data-custom=\"myCustom\">\n              <span></span>\n            </div>\n            <div class=\"tool\">\n              <span class=\"info\">myCustom</span>\n              <button type=\"button\"></button>\n            </div>\n          </div>\n        </div>\n      `);\n    });\n\n    it('should change customBlock element to selection', () => {\n      setTextToEditor('foo');\n\n      cmd.exec('selectAll');\n      cmd.exec('customBlock', { info: 'myCustom' });\n\n      expect(wwe.getHTML()).toBe(oneLineTrim`\n        <div class=\"toastui-editor-custom-block\">\n          <div class=\"toastui-editor-custom-block-editor\" style=\"display: none;\"></div>\n          <div class=\"toastui-editor-custom-block-view\">\n            <div data-custom=\"myCustom\">\n              <span>foo</span>\n            </div>\n            <div class=\"tool\">\n              <span class=\"info\">myCustom</span>\n              <button type=\"button\"></button>\n            </div>\n          </div>\n        </div>\n      `);\n    });\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/wysiwyg/wwEditor.spec.ts",
    "content": "import { oneLineTrim } from 'common-tags';\n\nimport { DOMParser } from 'prosemirror-model';\n\nimport WysiwygEditor from '@/wysiwyg/wwEditor';\nimport EventEmitter from '@/event/eventEmitter';\nimport { WwToDOMAdaptor } from '@/wysiwyg/adaptor/wwToDOMAdaptor';\nimport { createHTMLSchemaMap } from '@/wysiwyg/nodes/html';\nimport { sanitizeHTML } from '@/sanitizer/htmlSanitizer';\nimport { createHTMLrenderer } from '../markdown/util';\n\njest.useFakeTimers();\n\ndescribe('WysiwygEditor', () => {\n  let wwe: WysiwygEditor, em: EventEmitter, el: HTMLElement;\n\n  function assertToContainHTML(html: string) {\n    expect(wwe.view.dom.innerHTML).toContain(html);\n  }\n\n  function setContent(content: string) {\n    const wrapper = document.createElement('div');\n\n    wrapper.innerHTML = content;\n\n    const nodes = DOMParser.fromSchema(wwe.schema).parse(wrapper);\n\n    wwe.setModel(nodes);\n  }\n\n  beforeEach(() => {\n    const htmlRenderer = createHTMLrenderer();\n    const toDOMAdaptor = new WwToDOMAdaptor({}, htmlRenderer);\n    const htmlSchemaMap = createHTMLSchemaMap(htmlRenderer, sanitizeHTML, toDOMAdaptor);\n\n    em = new EventEmitter();\n    wwe = new WysiwygEditor(em, { toDOMAdaptor, htmlSchemaMap });\n    el = wwe.el;\n    document.body.appendChild(el);\n  });\n\n  afterEach(() => {\n    jest.clearAllTimers();\n    if (Object.keys(wwe).length) {\n      wwe.destroy();\n    }\n    document.body.removeChild(el);\n  });\n\n  describe('API', () => {\n    it('destroy() initialize instance object', () => {\n      wwe.destroy();\n\n      expect(wwe).toEqual({});\n    });\n\n    it(`focus() enable editor's dom selection state`, () => {\n      wwe.focus();\n\n      // run setTimeout function when focusing the editor\n      jest.runAllTimers();\n\n      expect(document.activeElement).toEqual(wwe.view.dom);\n    });\n\n    it(`blur() disable editor's dom selection state`, () => {\n      wwe.focus();\n      wwe.blur();\n\n      expect(document.activeElement).not.toEqual(wwe.view.dom);\n    });\n\n    it('setHeight() change height of editor', () => {\n      wwe.setHeight(50);\n\n      expect(wwe.el.style.height).toBe('50px');\n    });\n\n    it('setMinHeight() change minimum height of editor', () => {\n      wwe.setMinHeight(50);\n\n      expect(wwe.el.style.minHeight).toBe('50px');\n    });\n\n    it('setPlaceholder() attach placeholder element', () => {\n      wwe.setPlaceholder('placeholder text');\n\n      assertToContainHTML(oneLineTrim`\n\n          <span class=\"placeholder ProseMirror-widget\">placeholder text</span>\n      `);\n    });\n\n    it('scrollTo() move scroll position', () => {\n      setContent(oneLineTrim`\n        <p>foo</p>\n        <p><br></p>\n        <p><br></p>\n        <p><br></p>\n        <p><br></p>\n        <p><br></p>\n        <p><br></p>\n        <p><br></p>\n      `);\n\n      wwe.setHeight(50);\n      wwe.setScrollTop(30);\n\n      expect(wwe.getScrollTop()).toBe(30);\n    });\n\n    it('getSelection() return selection range as array', () => {\n      setContent(oneLineTrim`\n        <p>foo</p>\n        <p>bar</p>\n        <p>baz</p>\n      `);\n\n      wwe.setSelection(13, 2);\n\n      expect(wwe.getSelection()).toEqual([2, 13]);\n    });\n\n    it('replaceSelection() change text of selection range', () => {\n      setContent(oneLineTrim`\n        <p>foo</p>\n        <p>bar</p>\n      `);\n\n      wwe.setSelection(3, 7);\n      wwe.replaceSelection('new foo\\nnew bar');\n\n      assertToContainHTML(oneLineTrim`\n        <p>fonew foo</p>\n        <p>new barar</p>\n      `);\n    });\n\n    it('addWidget API', () => {\n      const ul = document.createElement('ul');\n\n      ul.innerHTML = `\n        <li>Ryu</li>\n        <li>Lee</li>\n      `;\n\n      wwe.addWidget(ul, 'top');\n\n      expect(document.body).toContainElement(ul);\n\n      wwe.blur();\n\n      expect(document.body).not.toContainElement(ul);\n    });\n  });\n\n  it(`should emit 'changeToolbarState' event when changing cursor`, () => {\n    setContent(oneLineTrim`\n      <p>foo</p>\n      <p>bar</p>\n    `);\n\n    const spy = jest.fn();\n\n    em.listen('changeToolbarState', spy);\n\n    wwe.setSelection(3, 3);\n\n    expect(spy).toHaveBeenCalled();\n  });\n\n  it('should display html block element properly', () => {\n    setContent(\n      '<iframe width=\"420\" height=\"315\" src=\"https://www.youtube.com/embed/XyenY12fzAk\"></iframe>'\n    );\n\n    assertToContainHTML(\n      '<iframe src=\"https://www.youtube.com/embed/XyenY12fzAk\" height=\"315\" width=\"420\" class=\"html-block ProseMirror-selectednode\" draggable=\"true\"></iframe>'\n    );\n  });\n\n  it('should display html inline element properly', () => {\n    setContent('<big class=\"my-inline\">text</big>');\n\n    assertToContainHTML('<p><big class=\"my-inline\">text</big></p>');\n  });\n\n  it('should sanitize html element', () => {\n    setContent('<iframe width=\"420\" height=\"315\" src=\"javascript: alert(1);\"></iframe>');\n\n    assertToContainHTML(\n      '<iframe height=\"315\" width=\"420\" class=\"html-block ProseMirror-selectednode\" draggable=\"true\"></iframe>'\n    );\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/wysiwyg/wwTableCommand.spec.ts",
    "content": "import { oneLineTrim } from 'common-tags';\n\nimport WysiwygEditor from '@/wysiwyg/wwEditor';\nimport EventEmitter from '@/event/eventEmitter';\nimport CommandManager from '@/commands/commandManager';\nimport CellSelection from '@/wysiwyg/plugins/selection/cellSelection';\n\nimport { WwToDOMAdaptor } from '@/wysiwyg/adaptor/wwToDOMAdaptor';\nimport { TableOffsetMap } from '@/wysiwyg/helper/tableOffsetMap';\nimport { cls } from '@/utils/dom';\n\nconst CELL_SELECTION_CLS = cls('cell-selected');\n\ndescribe('wysiwyg table commands', () => {\n  let wwe: WysiwygEditor, em: EventEmitter, cmd: CommandManager;\n\n  function selectCells(from: number, to: number) {\n    const { state, dispatch } = wwe.view;\n    const { doc, tr } = state;\n\n    const startCellPos = doc.resolve(from);\n    const endCellPos = doc.resolve(to);\n    const selection = new CellSelection(startCellPos, endCellPos);\n\n    dispatch!(tr.setSelection(selection));\n  }\n\n  function setCellSelection(\n    [startRowIdx, startColIdx]: number[],\n    [endRowIdx, endColIdx]: number[],\n    cellSelection = true\n  ) {\n    const doc = wwe.getModel();\n    const map = TableOffsetMap.create(doc.resolve(1))!;\n\n    const startCellOffset = map.getCellInfo(startRowIdx, startColIdx).offset;\n    const endCellOffset = map.getCellInfo(endRowIdx, endColIdx).offset;\n\n    if (startCellOffset === endCellOffset && !cellSelection) {\n      const from = startCellOffset + 1;\n\n      wwe.setSelection(from, from);\n    } else {\n      selectCells(startCellOffset, endCellOffset);\n    }\n  }\n\n  beforeEach(() => {\n    const toDOMAdaptor = new WwToDOMAdaptor({}, {});\n\n    em = new EventEmitter();\n    wwe = new WysiwygEditor(em, { toDOMAdaptor });\n    cmd = new CommandManager(em, {}, wwe.commands, () => 'wysiwyg');\n  });\n\n  afterEach(() => {\n    wwe.destroy();\n  });\n\n  describe('addTable command', () => {\n    it('should create one by one table', () => {\n      cmd.exec('addTable');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p><br></p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p><br></p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should create table with column and row count', () => {\n      cmd.exec('addTable', { rowCount: 4, columnCount: 2 });\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p><br></p></th>\n              <th><p><br></p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p><br></p></td>\n              <td><p><br></p></td>\n            </tr>\n            <tr>\n              <td><p><br></p></td>\n              <td><p><br></p></td>\n            </tr>\n            <tr>\n              <td><p><br></p></td>\n              <td><p><br></p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should create table with data', () => {\n      cmd.exec('addTable', {\n        rowCount: 2,\n        columnCount: 2,\n        data: ['foo', 'bar', 'baz', 'qux'],\n      });\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p>foo</p></th>\n              <th><p>bar</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p>baz</p></td>\n              <td><p>qux</p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n  });\n\n  describe('removeTable command', () => {\n    beforeEach(() => {\n      cmd.exec('addTable');\n    });\n\n    it('should remove table when cursor is in table hedaer', () => {\n      setCellSelection([0, 0], [0, 0], false);\n\n      cmd.exec('removeTable');\n\n      expect(wwe.getHTML()).toBe('<p><br></p>');\n    });\n\n    it('should remove table when cursor is in table body', () => {\n      setCellSelection([1, 0], [1, 0], false);\n\n      cmd.exec('removeTable');\n\n      expect(wwe.getHTML()).toBe('<p><br></p>');\n    });\n\n    it('should remove table when selected cells', () => {\n      setCellSelection([0, 0], [1, 0]);\n\n      cmd.exec('removeTable');\n\n      expect(wwe.getHTML()).toBe('<p><br></p>');\n    });\n  });\n\n  describe('addRowToDown command', () => {\n    beforeEach(() => {\n      cmd.exec('addTable', {\n        rowCount: 3,\n        columnCount: 2,\n        data: ['foo', 'bar', 'baz', 'qux', 'quux', 'quuz'],\n      });\n    });\n\n    it('should add a row to next row of current cursor cell', () => {\n      setCellSelection([1, 1], [1, 1], false); // select 'baz' cell\n\n      cmd.exec('addRowToDown');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p>foo</p></th>\n              <th><p>bar</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p>baz</p></td>\n              <td><p>qux</p></td>\n            </tr>\n            <tr>\n              <td><p><br></p></td>\n              <td><p><br></p></td>\n            </tr>\n            <tr>\n              <td><p>quux</p></td>\n              <td><p>quuz</p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should add rows as selected row count after selection', () => {\n      setCellSelection([0, 0], [1, 1]); // select from 'foo' to 'qux' cells\n\n      cmd.exec('addRowToDown');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th class=\"${CELL_SELECTION_CLS}\"><p>foo</p></th>\n              <th class=\"${CELL_SELECTION_CLS}\"><p>bar</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td class=\"${CELL_SELECTION_CLS}\"><p>baz</p></td>\n              <td class=\"${CELL_SELECTION_CLS}\"><p>qux</p></td>\n            </tr>\n            <tr>\n              <td><p><br></p></td>\n              <td><p><br></p></td>\n            </tr>\n            <tr>\n              <td><p><br></p></td>\n              <td><p><br></p></td>\n            </tr>\n            <tr>\n              <td><p>quux</p></td>\n              <td><p>quuz</p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should not add a row when selection is only at table head', () => {\n      setCellSelection([0, 0], [0, 1]); // select from 'foo' to 'bar' cells\n\n      cmd.exec('addRowToDown');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th class=\"${CELL_SELECTION_CLS}\"><p>foo</p></th>\n              <th class=\"${CELL_SELECTION_CLS}\"><p>bar</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p>baz</p></td>\n              <td><p>qux</p></td>\n            </tr>\n            <tr>\n              <td><p>quux</p></td>\n              <td><p>quuz</p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n  });\n\n  describe('addRowToUp command', () => {\n    beforeEach(() => {\n      cmd.exec('addTable', {\n        rowCount: 3,\n        columnCount: 2,\n        data: ['foo', 'bar', 'baz', 'qux', 'quux', 'quuz'],\n      });\n    });\n\n    it('should add a row to previous row of current cursor cell', () => {\n      setCellSelection([1, 1], [1, 1], false); // select 'baz' cell\n\n      cmd.exec('addRowToUp');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p>foo</p></th>\n              <th><p>bar</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p><br></p></td>\n              <td><p><br></p></td>\n            </tr>\n            <tr>\n              <td><p>baz</p></td>\n              <td><p>qux</p></td>\n            </tr>\n            <tr>\n              <td><p>quux</p></td>\n              <td><p>quuz</p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should add rows as selected row count before selection', () => {\n      setCellSelection([1, 1], [2, 1]); // select from 'qux' to 'quuz' cells\n\n      cmd.exec('addRowToUp');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p>foo</p></th>\n              <th><p>bar</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p><br></p></td>\n              <td><p><br></p></td>\n            </tr>\n            <tr>\n              <td><p><br></p></td>\n              <td><p><br></p></td>\n            </tr>\n            <tr>\n              <td><p>baz</p></td>\n              <td class=\"${CELL_SELECTION_CLS}\"><p>qux</p></td>\n            </tr>\n            <tr>\n              <td><p>quux</p></td>\n              <td class=\"${CELL_SELECTION_CLS}\"><p>quuz</p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should not add a row when selection include table head', () => {\n      setCellSelection([0, 0], [1, 0]); // select from 'foo' to 'baz' cells\n\n      cmd.exec('addRowToUp');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th class=\"${CELL_SELECTION_CLS}\"><p>foo</p></th>\n              <th><p>bar</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td class=\"${CELL_SELECTION_CLS}\"><p>baz</p></td>\n              <td><p>qux</p></td>\n            </tr>\n            <tr>\n              <td><p>quux</p></td>\n              <td><p>quuz</p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n  });\n\n  describe('removeRow command', () => {\n    beforeEach(() => {\n      cmd.exec('addTable', {\n        rowCount: 4,\n        columnCount: 2,\n        data: ['foo', 'bar', 'baz', 'qux', 'quux', 'quuz', 'corge', ''],\n      });\n    });\n\n    it('should remove a row where current cursor cell is located', () => {\n      setCellSelection([1, 1], [1, 1], false); // select from 'qux' cell\n\n      cmd.exec('removeRow');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p>foo</p></th>\n              <th><p>bar</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p>quux</p></td>\n              <td><p>quuz</p></td>\n            </tr>\n            <tr>\n              <td><p>corge</p></td>\n              <td><p><br></p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should remove columns as selected column count in selection', () => {\n      setCellSelection([3, 1], [2, 1]); // select from last to 'quuz' cells\n\n      cmd.exec('removeRow');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p>foo</p></th>\n              <th><p>bar</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p>baz</p></td>\n              <td><p>qux</p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should not remove rows when selection include table head', () => {\n      setCellSelection([0, 1], [2, 1]); // select from 'bar' to 'qux' cells\n\n      cmd.exec('removeRow');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p>foo</p></th>\n              <th class=\"${CELL_SELECTION_CLS}\"><p>bar</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p>baz</p></td>\n              <td class=\"${CELL_SELECTION_CLS}\"><p>qux</p></td>\n            </tr>\n            <tr>\n              <td><p>quux</p></td>\n              <td class=\"${CELL_SELECTION_CLS}\"><p>quuz</p></td>\n            </tr>\n            <tr>\n              <td><p>corge</p></td>\n              <td><p><br></p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should not remove rows when all rows of table body are selected', () => {\n      setCellSelection([1, 0], [3, 0]); // select from 'baz' to 'corge' cells\n\n      cmd.exec('removeRow');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p>foo</p></th>\n              <th><p>bar</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td class=\"${CELL_SELECTION_CLS}\"><p>baz</p></td>\n              <td><p>qux</p></td>\n            </tr>\n            <tr>\n              <td class=\"${CELL_SELECTION_CLS}\"><p>quux</p></td>\n              <td><p>quuz</p></td>\n            </tr>\n            <tr>\n              <td class=\"${CELL_SELECTION_CLS}\"><p>corge</p></td>\n              <td><p><br></p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n  });\n\n  describe('addColumnToRight command', () => {\n    beforeEach(() => {\n      cmd.exec('addTable', {\n        rowCount: 3,\n        columnCount: 3,\n        data: ['foo', 'bar', 'baz', 'qux', 'quux', 'quuz', 'corge', 'grault', ''],\n      });\n    });\n\n    it('should add a column to next column of current cursor cell', () => {\n      setCellSelection([1, 1], [1, 1], false); // select 'quux' cell\n\n      cmd.exec('addColumnToRight');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p>foo</p></th>\n              <th><p>bar</p></th>\n              <th><p><br></p></th>\n              <th><p>baz</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p>qux</p></td>\n              <td><p>quux</p></td>\n              <td><p><br></p></td>\n              <td><p>quuz</p></td>\n            </tr>\n            <tr>\n              <td><p>corge</p></td>\n              <td><p>grault</p></td>\n              <td><p><br></p></td>\n              <td><p><br></p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should add columns as selected column count to right of selection', () => {\n      setCellSelection([0, 0], [1, 1]); // select from 'foo' to 'quux' cells\n\n      cmd.exec('addColumnToRight');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th class=\"${CELL_SELECTION_CLS}\"><p>foo</p></th>\n              <th class=\"${CELL_SELECTION_CLS}\"><p>bar</p></th>\n              <th><p><br></p></th>\n              <th><p><br></p></th>\n              <th><p>baz</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td class=\"${CELL_SELECTION_CLS}\"><p>qux</p></td>\n              <td class=\"${CELL_SELECTION_CLS}\"><p>quux</p></td>\n              <td><p><br></p></td>\n              <td><p><br></p></td>\n              <td><p>quuz</p></td>\n            </tr>\n            <tr>\n              <td><p>corge</p></td>\n              <td><p>grault</p></td>\n              <td><p><br></p></td>\n              <td><p><br></p></td>\n              <td><p><br></p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n  });\n\n  describe('addColumnToLeft command', () => {\n    beforeEach(() => {\n      cmd.exec('addTable', {\n        rowCount: 3,\n        columnCount: 3,\n        data: ['foo', 'bar', 'baz', 'qux', 'quux', 'quuz', 'corge', 'grault', ''],\n      });\n    });\n\n    it('should add a column to previous column of current cursor cell', () => {\n      setCellSelection([1, 1], [1, 1], false); // select 'quux' cell\n\n      cmd.exec('addColumnToLeft');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p>foo</p></th>\n              <th><p><br></p></th>\n              <th><p>bar</p></th>\n              <th><p>baz</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p>qux</p></td>\n              <td><p><br></p></td>\n              <td><p>quux</p></td>\n              <td><p>quuz</p></td>\n            </tr>\n            <tr>\n              <td><p>corge</p></td>\n              <td><p><br></p></td>\n              <td><p>grault</p></td>\n              <td><p><br></p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should add columns as selected column count to right of selection', () => {\n      setCellSelection([0, 1], [2, 2]); // select from 'bar' to last cells\n\n      cmd.exec('addColumnToLeft');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p>foo</p></th>\n              <th><p><br></p></th>\n              <th><p><br></p></th>\n              <th class=\"${CELL_SELECTION_CLS}\"><p>bar</p></th>\n              <th class=\"${CELL_SELECTION_CLS}\"><p>baz</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p>qux</p></td>\n              <td><p><br></p></td>\n              <td><p><br></p></td>\n              <td class=\"${CELL_SELECTION_CLS}\"><p>quux</p></td>\n              <td class=\"${CELL_SELECTION_CLS}\"><p>quuz</p></td>\n            </tr>\n            <tr>\n              <td><p>corge</p></td>\n              <td><p><br></p></td>\n              <td><p><br></p></td>\n              <td class=\"${CELL_SELECTION_CLS}\"><p>grault</p></td>\n              <td class=\"${CELL_SELECTION_CLS}\"><p><br></p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n  });\n\n  describe('removeColumn command', () => {\n    beforeEach(() => {\n      cmd.exec('addTable', {\n        rowCount: 3,\n        columnCount: 3,\n        data: ['foo', 'bar', 'baz', 'qux', 'quux', 'quuz', 'corge', 'grault', ''],\n      });\n    });\n\n    it('should remove a column where current cursor cell is located', () => {\n      setCellSelection([1, 1], [1, 1], false); // select 'quux' cell\n\n      cmd.exec('removeColumn');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p>foo</p></th>\n              <th><p>baz</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p>qux</p></td>\n              <td><p>quuz</p></td>\n            </tr>\n            <tr>\n              <td><p>corge</p></td>\n              <td><p><br></p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should remove columns as selected column count in selection', () => {\n      setCellSelection([0, 1], [2, 2]); // select from 'bar' to last cells\n\n      cmd.exec('removeColumn');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p>foo</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p>qux</p></td>\n            </tr>\n            <tr>\n              <td><p>corge</p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should not remove columns when all columns are selected', () => {\n      setCellSelection([0, 0], [1, 2]); // select from 'foo' to 'quuz' cells\n\n      cmd.exec('removeRow');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th class=\"${CELL_SELECTION_CLS}\"><p>foo</p></th>\n              <th class=\"${CELL_SELECTION_CLS}\"><p>bar</p></th>\n              <th class=\"${CELL_SELECTION_CLS}\"><p>baz</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td class=\"${CELL_SELECTION_CLS}\"><p>qux</p></td>\n              <td class=\"${CELL_SELECTION_CLS}\"><p>quux</p></td>\n              <td class=\"${CELL_SELECTION_CLS}\"><p>quuz</p></td>\n            </tr>\n            <tr>\n              <td><p>corge</p></td>\n              <td><p>grault</p></td>\n              <td><p><br></p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n  });\n\n  describe('alignColumn command', () => {\n    beforeEach(() => {\n      cmd.exec('addTable', {\n        rowCount: 3,\n        columnCount: 2,\n        data: ['foo', 'bar', 'baz', 'qux', 'quux', ''],\n      });\n    });\n\n    it('should add center align attribute to columns by no option', () => {\n      setCellSelection([1, 0], [1, 0], false); // select 'baz' cell\n\n      cmd.exec('alignColumn');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th align=\"center\"><p>foo</p></th>\n              <th><p>bar</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td align=\"center\"><p>baz</p></td>\n              <td><p>qux</p></td>\n            </tr>\n            <tr>\n              <td align=\"center\"><p>quux</p></td>\n              <td><p><br></p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should change align attribute to columns by option', () => {\n      setCellSelection([2, 1], [2, 1], false); // select last cell\n\n      cmd.exec('alignColumn', { align: 'left' });\n\n      let expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p>foo</p></th>\n              <th align=\"left\"><p>bar</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p>baz</p></td>\n              <td align=\"left\"><p>qux</p></td>\n            </tr>\n            <tr>\n              <td><p>quux</p></td>\n              <td align=\"left\"><p><br></p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n\n      cmd.exec('alignColumn', { align: 'right' });\n\n      expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p>foo</p></th>\n              <th align=\"right\"><p>bar</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p>baz</p></td>\n              <td align=\"right\"><p>qux</p></td>\n            </tr>\n            <tr>\n              <td><p>quux</p></td>\n              <td align=\"right\"><p><br></p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should add align attribute to columns with cursor in table hedaer', () => {\n      setCellSelection([0, 0], [0, 0], false); // select 'foo' cell\n\n      cmd.exec('alignColumn', { align: 'left' });\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th align=\"left\"><p>foo</p></th>\n              <th><p>bar</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td align=\"left\"><p>baz</p></td>\n              <td><p>qux</p></td>\n            </tr>\n            <tr>\n              <td align=\"left\"><p>quux</p></td>\n              <td><p><br></p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n\n    it('should add align attribute to selected columns in selection', () => {\n      setCellSelection([1, 0], [1, 1]); // select from 'baz' to 'qux' cell\n\n      cmd.exec('alignColumn', { align: 'left' });\n\n      let expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th align=\"left\"><p>foo</p></th>\n              <th align=\"left\"><p>bar</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td align=\"left\" class=\"${CELL_SELECTION_CLS}\"><p>baz</p></td>\n              <td align=\"left\" class=\"${CELL_SELECTION_CLS}\"><p>qux</p></td>\n            </tr>\n            <tr>\n              <td align=\"left\"><p>quux</p></td>\n              <td align=\"left\"><p><br></p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n\n      setCellSelection([0, 1], [0, 0]); // select from 'bar' to 'foo' cell\n\n      cmd.exec('alignColumn', { align: 'right' });\n\n      expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th align=\"right\" class=\"${CELL_SELECTION_CLS}\"><p>foo</p></th>\n              <th align=\"right\" class=\"${CELL_SELECTION_CLS}\"><p>bar</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td align=\"right\"><p>baz</p></td>\n              <td align=\"right\"><p>qux</p></td>\n            </tr>\n            <tr>\n              <td align=\"right\"><p>quux</p></td>\n              <td align=\"right\"><p><br></p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      expect(wwe.getHTML()).toBe(expected);\n    });\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/__test__/unit/wysiwyg/wwToDOMAdaptor.spec.ts",
    "content": "import { Fragment, ProsemirrorNode } from 'prosemirror-model';\nimport { oneLineTrim } from 'common-tags';\nimport { HeadingMdNode, CodeBlockMdNode, HTMLConvertorMap } from '@toast-ui/toastmark';\nimport { ToDOMAdaptor } from '@t/convertor';\nimport { WwToDOMAdaptor } from '@/wysiwyg/adaptor/wwToDOMAdaptor';\nimport EventEmitter from '@/event/eventEmitter';\nimport WysiwygEditor from '@/wysiwyg/wwEditor';\nimport { createMdLikeNode } from '@/wysiwyg/adaptor/mdLikeNode';\nimport { createHTMLSchemaMap } from '@/wysiwyg/nodes/html';\nimport { sanitizeHTML } from '@/sanitizer/htmlSanitizer';\n\nlet wwe: WysiwygEditor, em: EventEmitter, toDOMAdaptor: ToDOMAdaptor;\n\nfunction createText(text: string) {\n  return wwe.schema.text(text);\n}\n\nfunction createNode(\n  type: string,\n  attrs?: { [key: string]: any } | null,\n  content?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>\n) {\n  return wwe.schema.nodes[type].create(attrs, content);\n}\n\nfunction createMark(type: string, attrs?: { [key: string]: any } | null) {\n  return wwe.schema.marks[type].create(attrs!);\n}\n\nbeforeEach(() => {\n  const convertors: HTMLConvertorMap = {\n    code() {\n      return [\n        { type: 'openTag', tagName: 'code' },\n        { type: 'html', content: '<span>123</span>' },\n        { type: 'closeTag', tagName: 'code' },\n      ];\n    },\n    heading(node, { entering }) {\n      return {\n        type: entering ? 'openTag' : 'closeTag',\n        tagName: `h${(node as HeadingMdNode).level}`,\n        attributes: { 'data-custom': 'customAttr' },\n        classNames: ['custom-heading'],\n      };\n    },\n    codeBlock(node) {\n      return [\n        {\n          type: 'openTag',\n          tagName: 'pre',\n          attributes: { 'data-custom': (node as CodeBlockMdNode).info || '' },\n          classNames: ['custom-pre'],\n        },\n        { type: 'openTag', tagName: 'code', classNames: ['custom-code'] },\n        { type: 'openTag', tagName: 'span' },\n        { type: 'text', content: node.literal! },\n        { type: 'closeTag', tagName: 'span' },\n        { type: 'closeTag', tagName: 'code' },\n        { type: 'closeTag', tagName: 'pre' },\n      ];\n    },\n    emph(_, { entering }) {\n      return {\n        type: entering ? 'openTag' : 'closeTag',\n        tagName: `em`,\n        attributes: { 'data-custom': 'customAttr' },\n        classNames: ['custom-emph'],\n      };\n    },\n    htmlBlock: {\n      // @ts-ignore\n      nav(node) {\n        return [\n          { type: 'openTag', tagName: 'nav', outerNewLine: true, attributes: node.attrs },\n          { type: 'html', content: node.childrenHTML },\n          { type: 'closeTag', tagName: 'nav', outerNewLine: true },\n        ];\n      },\n    },\n    htmlInline: {\n      // @ts-ignore\n      big(node: MdLikeNode, { entering }: Context) {\n        return entering\n          ? { type: 'openTag', tagName: 'big', attributes: { class: node.attrs.class } }\n          : { type: 'closeTag', tagName: 'big' };\n      },\n    },\n  };\n\n  toDOMAdaptor = new WwToDOMAdaptor({}, convertors);\n  em = new EventEmitter();\n\n  const htmlSchemaMap = createHTMLSchemaMap(convertors, sanitizeHTML, toDOMAdaptor);\n\n  wwe = new WysiwygEditor(em, { toDOMAdaptor, htmlSchemaMap });\n});\n\nafterEach(() => {\n  wwe.destroy();\n});\n\ndescribe('mdLikeNode', () => {\n  it('heading node should be changed to markdown-like-node', () => {\n    const headingNode = createMdLikeNode(\n      createNode('heading', { level: 2 }, createText('myHeading'))\n    );\n\n    expect(headingNode).toEqual({ type: 'heading', literal: null, wysiwygNode: true, level: 2 });\n  });\n\n  it('image node should be changed to markdown-like-node', () => {\n    const imageNode = createMdLikeNode(createNode('image', { imageUrl: 'myImageUrl' }));\n\n    expect(imageNode).toEqual({\n      type: 'image',\n      literal: null,\n      wysiwygNode: true,\n      destination: 'myImageUrl',\n    });\n  });\n\n  it('codeBlock node should be changed to markdown-like-node', () => {\n    const codeBlockNode = createMdLikeNode(\n      createNode('codeBlock', { language: 'myLang' }, createText('myCode'))\n    );\n\n    expect(codeBlockNode).toEqual({\n      type: 'codeBlock',\n      literal: 'myCode',\n      wysiwygNode: true,\n      info: 'myLang',\n    });\n  });\n\n  it('bulletList node should be changed to markdown-like-node', () => {\n    const bulletListNode = createMdLikeNode(createNode('bulletList'));\n\n    expect(bulletListNode).toEqual({\n      type: 'list',\n      literal: null,\n      wysiwygNode: true,\n      listData: { type: 'bullet' },\n    });\n  });\n\n  it('orderedList node should be changed to markdown-like-node', () => {\n    const orderedListNode = createMdLikeNode(createNode('orderedList'));\n\n    expect(orderedListNode).toEqual({\n      type: 'list',\n      literal: null,\n      wysiwygNode: true,\n      listData: { start: 1, type: 'ordered' },\n    });\n  });\n\n  it('listItem node should be changed to markdown-like-node', () => {\n    const listItemNode = createMdLikeNode(createNode('listItem', { task: true }));\n\n    expect(listItemNode).toEqual({\n      type: 'item',\n      literal: null,\n      wysiwygNode: true,\n      listData: { task: true, checked: false },\n    });\n  });\n\n  it('tableHeadCell node should be changed to markdown-like-node', () => {\n    const tableHeadCellNode = createMdLikeNode(createNode('tableHeadCell', { align: 'left' }));\n\n    expect(tableHeadCellNode).toEqual({\n      type: 'tableCell',\n      cellType: 'head',\n      align: 'left',\n      literal: null,\n      wysiwygNode: true,\n    });\n  });\n\n  it('tableBodyCell node should be changed to markdown-like-node', () => {\n    const tableBodyCellNode = createMdLikeNode(createNode('tableBodyCell', { align: 'left' }));\n\n    expect(tableBodyCellNode).toEqual({\n      type: 'tableCell',\n      cellType: 'body',\n      align: 'left',\n      literal: null,\n      wysiwygNode: true,\n    });\n  });\n\n  it('customBlock node should be changed to markdown-like-node', () => {\n    const customBlockNode = createMdLikeNode(\n      createNode('customBlock', { info: 'myCustom' }, createText('myCustom'))\n    );\n\n    expect(customBlockNode).toEqual({\n      type: 'customBlock',\n      info: 'myCustom',\n      literal: 'myCustom',\n      wysiwygNode: true,\n    });\n  });\n\n  it('link mark should be changed to markdown-like-node', () => {\n    const linkNode = createMdLikeNode(\n      createMark('link', { linkText: 'myLinkText', linkUrl: 'myLinkUrl' })\n    );\n\n    expect(linkNode).toEqual({\n      type: 'link',\n      literal: null,\n      wysiwygNode: true,\n      destination: 'myLinkUrl',\n      title: null,\n    });\n  });\n\n  it('html block should be changed to markdown-like-node', () => {\n    const navNode = createMdLikeNode(\n      createNode('nav', {\n        htmlAttrs: { class: 'my-nav', 'data-my-nav': 'my-nav' },\n        childrenHTML: 'text',\n      })\n    );\n\n    expect(navNode).toEqual({\n      type: 'nav',\n      literal: '',\n      wysiwygNode: true,\n      attrs: { class: 'my-nav', 'data-my-nav': 'my-nav' },\n      childrenHTML: 'text',\n    });\n  });\n\n  it('html inline should be changed to markdown-like-node', () => {\n    const bigNode = createMdLikeNode(createMark('big', { htmlAttrs: { class: 'my-big' } }));\n\n    expect(bigNode).toEqual({\n      type: 'big',\n      wysiwygNode: true,\n      literal: null,\n      attrs: { class: 'my-big' },\n    });\n  });\n});\n\ndescribe('wysiwyg adaptor toDOMNode using custom renderer', () => {\n  function getHTML(node: Node) {\n    return (node as HTMLElement).outerHTML;\n  }\n\n  it('toDOMNode should be parsed with renderer tokens for wysiwyg node schema', () => {\n    const toDOMNode = toDOMAdaptor.getToDOMNode('heading')!;\n    const headingNode = createNode('heading', { level: 2 }, createText('myHeading'));\n    const expected = oneLineTrim`\n        <h2 class=\"custom-heading\" data-custom=\"customAttr\">\n          myHeading\n        </h2>\n      `;\n\n    expect(getHTML(toDOMNode(headingNode))).toBe(expected);\n  });\n\n  it('toDOMNode should be parsed with the nested renderer tokens', () => {\n    const toDOMNode = toDOMAdaptor.getToDOMNode('codeBlock')!;\n    const codeBlockNode = createNode('codeBlock', { language: 'myLan' }, createText('codeBlock'));\n\n    const expected = oneLineTrim`\n        <pre class=\"custom-pre\" data-custom=\"myLan\">\n          <code class=\"custom-code\">\n            <span>codeBlock</span>\n          </code>\n        </pre>\n      `;\n\n    expect(getHTML(toDOMNode(codeBlockNode))).toBe(expected);\n  });\n\n  it('html token should be parsed in DOMNode', () => {\n    const toDOMNode = toDOMAdaptor.getToDOMNode('code')!;\n    const codeNode = createMark('code');\n\n    const expected = oneLineTrim`\n        <code>\n          <span>123</span>\n        </code>\n      `;\n\n    expect(getHTML(toDOMNode(codeNode))).toBe(expected);\n  });\n\n  it('should get toDOM for only registered renderer', () => {\n    const toDOMNode = toDOMAdaptor.getToDOMNode('blockQuote');\n\n    expect(toDOMNode).toBe(null);\n  });\n\n  it('toDOMNode should be parsed with the html block renderer tokens', () => {\n    const toDOMNode = toDOMAdaptor.getToDOMNode('nav')!;\n    const navNode = createNode('nav', {\n      htmlAttrs: { class: 'my-nav' },\n      childrenHTML: 'text',\n    });\n\n    const expected = oneLineTrim`\n      <nav class=\"my-nav\">\n        text\n      </nav>\n    `;\n\n    expect(getHTML(toDOMNode(navNode))).toBe(expected);\n  });\n\n  it('toDOMNode should be parsed with the html inline renderer tokens', () => {\n    const toDOMNode = toDOMAdaptor.getToDOMNode('big')!;\n    const bigNode = createMark('big', {\n      htmlAttrs: { class: 'my-big', 'data-my-attr': 'my-attr' },\n    });\n\n    const expected = oneLineTrim`\n      <big class=\"my-big\"></big>\n    `;\n\n    expect(getHTML(toDOMNode(bigNode))).toBe(expected);\n  });\n});\n"
  },
  {
    "path": "apps/editor/src/base.ts",
    "content": "import { Schema } from 'prosemirror-model';\nimport { EditorState, Plugin, Transaction } from 'prosemirror-state';\nimport { EditorView } from 'prosemirror-view';\nimport { keymap } from 'prosemirror-keymap';\nimport { baseKeymap } from 'prosemirror-commands';\nimport { InputRule, inputRules } from 'prosemirror-inputrules';\nimport { history } from 'prosemirror-history';\nimport { Sourcepos } from '@toast-ui/toastmark';\nimport css from 'tui-code-snippet/domUtil/css';\nimport { WidgetStyle, EditorType, EditorPos, Base, NodeRangeInfo } from '@t/editor';\nimport { Emitter } from '@t/event';\nimport { Context, EditorAllCommandMap } from '@t/spec';\nimport SpecManager from './spec/specManager';\nimport { createTextSelection } from './helper/manipulation';\nimport { createNodesWithWidget, getWidgetRules } from './widget/rules';\nimport { getDefaultCommands } from './commands/defaultCommands';\nimport { placeholder } from './plugins/placeholder';\nimport { addWidget } from './plugins/popupWidget';\nimport { dropImage } from './plugins/dropImage';\nimport { isWidgetNode } from './widget/widgetNode';\nimport { last } from './utils/common';\nimport { PluginProp } from '@t/plugin';\n\nexport default abstract class EditorBase implements Base {\n  el: HTMLElement;\n\n  editorType!: EditorType;\n\n  eventEmitter: Emitter;\n\n  context!: Context;\n\n  schema!: Schema;\n\n  keymaps!: Plugin[];\n\n  view!: EditorView;\n\n  commands!: EditorAllCommandMap;\n\n  specs!: SpecManager;\n\n  placeholder: { text: string };\n\n  extraPlugins!: PluginProp[];\n\n  timer: NodeJS.Timeout | null = null;\n\n  constructor(eventEmitter: Emitter) {\n    this.el = document.createElement('div');\n    this.el.className = 'toastui-editor';\n\n    this.eventEmitter = eventEmitter;\n    this.placeholder = { text: '' };\n  }\n\n  abstract createSpecs(): SpecManager;\n\n  abstract createContext(): Context;\n\n  abstract createView(): EditorView;\n\n  createState() {\n    return EditorState.create({\n      schema: this.schema,\n      plugins: this.createPlugins(),\n    });\n  }\n\n  protected initEvent() {\n    const { eventEmitter, view, editorType } = this;\n\n    view.dom.addEventListener('focus', () => eventEmitter.emit('focus', editorType));\n    view.dom.addEventListener('blur', () => eventEmitter.emit('blur', editorType));\n  }\n\n  protected emitChangeEvent(tr: Transaction) {\n    this.eventEmitter.emit('caretChange', this.editorType);\n    if (tr.docChanged) {\n      this.eventEmitter.emit('change', this.editorType);\n    }\n  }\n\n  get defaultPlugins() {\n    const rules = this.createInputRules();\n    const plugins = [\n      ...this.keymaps,\n      keymap({\n        'Shift-Enter': baseKeymap.Enter,\n        ...baseKeymap,\n      }),\n      history(),\n      placeholder(this.placeholder),\n      addWidget(this.eventEmitter),\n      dropImage(this.context),\n    ];\n\n    return rules ? plugins.concat(rules) : plugins;\n  }\n\n  private createInputRules() {\n    const widgetRules = getWidgetRules();\n    const rules = widgetRules.map(\n      ({ rule }) =>\n        new InputRule(rule, (state, match: RegExpMatchArray, start, end) => {\n          const { schema, tr, doc } = state;\n          const allMatched = match.input!.match(new RegExp(rule, 'g'))!;\n          const pos = doc.resolve(start);\n          let { parent } = pos;\n          let count = 0;\n\n          if (isWidgetNode(parent)) {\n            parent = pos.node(pos.depth - 1);\n          }\n\n          parent.forEach((child) => isWidgetNode(child) && (count += 1));\n\n          // replace the content only if the count of matched rules in whole text is greater than current widget node count\n          if (allMatched.length > count) {\n            const content = last(allMatched);\n            const nodes = createNodesWithWidget(content, schema);\n\n            // adjust start position based on widget content\n            return tr.replaceWith(end - content.length + 1, end, nodes);\n          }\n          return null;\n        })\n    );\n\n    return rules.length ? inputRules({ rules }) : null;\n  }\n\n  private clearTimer() {\n    if (this.timer) {\n      clearTimeout(this.timer);\n      this.timer = null;\n    }\n  }\n\n  createSchema() {\n    return new Schema({\n      nodes: this.specs.nodes,\n      marks: this.specs.marks,\n    });\n  }\n\n  createKeymaps(useCommandShortcut: boolean) {\n    const { undo, redo } = getDefaultCommands();\n    const allKeymaps = this.specs.keymaps(useCommandShortcut);\n    const historyKeymap = {\n      'Mod-z': undo(),\n      'Shift-Mod-z': redo(),\n    };\n\n    return useCommandShortcut ? allKeymaps.concat(keymap(historyKeymap)) : allKeymaps;\n  }\n\n  createCommands() {\n    return this.specs.commands(this.view);\n  }\n\n  createPluginProps() {\n    return this.extraPlugins.map((plugin) => plugin(this.eventEmitter));\n  }\n\n  focus() {\n    this.clearTimer();\n    // prevent the error for IE11\n    this.timer = setTimeout(() => {\n      this.view.focus();\n      this.view.dispatch(this.view.state.tr.scrollIntoView());\n    });\n  }\n\n  blur() {\n    (this.view.dom as HTMLElement).blur();\n  }\n\n  destroy() {\n    this.clearTimer();\n    this.view.destroy();\n    Object.keys(this).forEach((prop) => {\n      delete this[prop as keyof this];\n    });\n  }\n\n  moveCursorToStart(focus: boolean) {\n    const { tr } = this.view.state;\n\n    this.view.dispatch(tr.setSelection(createTextSelection(tr, 1)).scrollIntoView());\n    if (focus) {\n      this.focus();\n    }\n  }\n\n  moveCursorToEnd(focus: boolean) {\n    const { tr } = this.view.state;\n\n    this.view.dispatch(\n      tr.setSelection(createTextSelection(tr, tr.doc.content.size - 1)).scrollIntoView()\n    );\n\n    if (focus) {\n      this.focus();\n    }\n  }\n\n  setScrollTop(top: number) {\n    this.view.dom.scrollTop = top;\n  }\n\n  getScrollTop() {\n    return this.view.dom.scrollTop;\n  }\n\n  setPlaceholder(text: string) {\n    this.placeholder.text = text;\n    this.view.dispatch(this.view.state.tr.scrollIntoView());\n  }\n\n  setHeight(height: number) {\n    css(this.el, { height: `${height}px` });\n  }\n\n  setMinHeight(minHeight: number) {\n    css(this.el, { minHeight: `${minHeight}px` });\n  }\n\n  getElement() {\n    return this.el;\n  }\n\n  abstract createPlugins(): Plugin[];\n\n  abstract replaceWithWidget(start: EditorPos, end: EditorPos, text: string): void;\n\n  abstract addWidget(node: Node, style: WidgetStyle, pos?: EditorPos): void;\n\n  abstract setSelection(start?: EditorPos, end?: EditorPos): void;\n\n  abstract replaceSelection(text: string, start?: EditorPos, end?: EditorPos): void;\n\n  abstract deleteSelection(start?: EditorPos, end?: EditorPos): void;\n\n  abstract getSelectedText(start?: EditorPos, end?: EditorPos): string;\n\n  abstract getSelection(): Sourcepos | [number, number];\n\n  abstract getRangeInfoOfNode(pos?: EditorPos): NodeRangeInfo;\n}\n"
  },
  {
    "path": "apps/editor/src/commands/commandManager.ts",
    "content": "import { EditorType } from '@t/editor';\nimport { EditorAllCommandMap, EditorCommandFn } from '@t/spec';\nimport { Emitter } from '@t/event';\n\ntype GetEditorType = () => EditorType;\nexport default class CommandManager {\n  private eventEmitter: Emitter;\n\n  private mdCommands: EditorAllCommandMap;\n\n  private wwCommands: EditorAllCommandMap;\n\n  private getEditorType: GetEditorType;\n\n  constructor(\n    eventEmitter: Emitter,\n    mdCommands: EditorAllCommandMap,\n    wwCommands: EditorAllCommandMap,\n    getEditorType: GetEditorType\n  ) {\n    this.eventEmitter = eventEmitter;\n    this.mdCommands = mdCommands;\n    this.wwCommands = wwCommands;\n    this.getEditorType = getEditorType;\n    this.initEvent();\n  }\n\n  private initEvent() {\n    this.eventEmitter.listen('command', (command, payload) => {\n      this.exec(command, payload);\n    });\n  }\n\n  addCommand(type: EditorType, name: string, command: EditorCommandFn) {\n    if (type === 'markdown') {\n      this.mdCommands[name] = command;\n    } else {\n      this.wwCommands[name] = command;\n    }\n  }\n\n  deleteCommand(type: EditorType, name: string) {\n    if (type === 'markdown') {\n      delete this.mdCommands[name];\n    } else {\n      delete this.wwCommands[name];\n    }\n  }\n\n  exec(name: string, payload?: Record<string, any>) {\n    const type = this.getEditorType();\n\n    if (type === 'markdown') {\n      this.mdCommands[name](payload);\n    } else {\n      this.wwCommands[name](payload);\n    }\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/commands/defaultCommands.ts",
    "content": "import { deleteSelection, selectAll } from 'prosemirror-commands';\nimport { undo, redo } from 'prosemirror-history';\n\nimport { EditorCommand } from '@t/spec';\n\nexport function getDefaultCommands(): Record<string, EditorCommand> {\n  return {\n    deleteSelection: () => deleteSelection,\n    selectAll: () => selectAll,\n    undo: () => undo,\n    redo: () => redo,\n  };\n}\n"
  },
  {
    "path": "apps/editor/src/commands/wwCommands.ts",
    "content": "import { isInListNode } from '@/wysiwyg/helper/node';\nimport { sinkListItem, liftListItem } from '@/wysiwyg/command/list';\n\nimport { EditorCommand } from '@t/spec';\n\nfunction indent(): EditorCommand {\n  return () => (state, dispatch) => {\n    const { selection, schema } = state;\n    const { $from, $to } = selection;\n    const range = $from.blockRange($to);\n\n    if (range && isInListNode($from)) {\n      return sinkListItem(schema.nodes.listItem)(state, dispatch);\n    }\n\n    return false;\n  };\n}\n\nfunction outdent(): EditorCommand {\n  return () => (state, dispatch) => {\n    const { selection, schema } = state;\n    const { $from, $to } = selection;\n    const range = $from.blockRange($to);\n\n    if (range && isInListNode($from)) {\n      return liftListItem(schema.nodes.listItem)(state, dispatch);\n    }\n\n    return false;\n  };\n}\n\nexport function getWwCommands(): Record<string, EditorCommand> {\n  return {\n    indent: indent(),\n    outdent: outdent(),\n  };\n}\n"
  },
  {
    "path": "apps/editor/src/convertors/convertor.ts",
    "content": "import { Node as ProsemirrorNode, Schema } from 'prosemirror-model';\nimport { HTMLConvertorMap, MdNode, MdPos } from '@toast-ui/toastmark';\n\nimport { ToWwConvertorMap, ToMdConvertors, ToMdConvertorMap } from '@t/convertor';\nimport { Emitter } from '@t/event';\n\nimport { createWwConvertors } from './toWysiwyg/toWwConvertors';\nimport ToWwConvertorState from './toWysiwyg/toWwConvertorState';\n\nimport { createMdConvertors } from './toMarkdown/toMdConvertors';\nimport ToMdConvertorState from './toMarkdown/toMdConvertorState';\n\nexport default class Convertor {\n  private readonly schema: Schema;\n\n  private readonly toWwConvertors: ToWwConvertorMap;\n\n  private readonly toMdConvertors: ToMdConvertors;\n\n  private readonly eventEmitter: Emitter;\n\n  private focusedNode: ProsemirrorNode | MdNode | null;\n\n  private mappedPosWhenConverting: number | MdPos | null;\n\n  constructor(\n    schema: Schema,\n    toMdConvertors: ToMdConvertorMap,\n    toHTMLConvertors: HTMLConvertorMap,\n    eventEmitter: Emitter\n  ) {\n    this.schema = schema;\n    this.eventEmitter = eventEmitter;\n    this.focusedNode = null;\n    this.mappedPosWhenConverting = null;\n    this.toWwConvertors = createWwConvertors(toHTMLConvertors);\n    this.toMdConvertors = createMdConvertors(toMdConvertors || {});\n\n    this.eventEmitter.listen(\n      'setFocusedNode',\n      (node: ProsemirrorNode | MdNode) => (this.focusedNode = node)\n    );\n  }\n\n  getMappedPos() {\n    return this.mappedPosWhenConverting;\n  }\n\n  setMappedPos = (pos: number | MdPos) => {\n    this.mappedPosWhenConverting = pos;\n  };\n\n  private getInfoForPosSync() {\n    return { node: this.focusedNode, setMappedPos: this.setMappedPos };\n  }\n\n  toWysiwygModel(mdNode: MdNode) {\n    const state = new ToWwConvertorState(this.schema, this.toWwConvertors);\n\n    return state.convertNode(mdNode, this.getInfoForPosSync());\n  }\n\n  toMarkdownText(wwNode: ProsemirrorNode) {\n    const state = new ToMdConvertorState(this.toMdConvertors);\n    let markdownText = state.convertNode(wwNode, this.getInfoForPosSync());\n\n    markdownText = this.eventEmitter.emitReduce('beforeConvertWysiwygToMarkdown', markdownText);\n\n    return markdownText;\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/convertors/toMarkdown/toMdConvertorState.ts",
    "content": "import { Node, Mark } from 'prosemirror-model';\n\nimport { includes, escape, last } from '@/utils/common';\n\nimport { WwNodeType, WwMarkType } from '@t/wysiwyg';\nimport {\n  ToMdConvertors,\n  ToMdNodeTypeConvertorMap,\n  ToMdMarkTypeConvertorMap,\n  FirstDelimFn,\n  InfoForPosSync,\n} from '@t/convertor';\n\nexport default class ToMdConvertorState {\n  private readonly nodeTypeConvertors: ToMdNodeTypeConvertorMap;\n\n  private readonly markTypeConvertors: ToMdMarkTypeConvertorMap;\n\n  private delim: string;\n\n  private result: string;\n\n  private closed: boolean | Node;\n\n  private tightList: boolean;\n\n  public stopNewline: boolean;\n\n  public inTable: boolean;\n\n  constructor({ nodeTypeConvertors, markTypeConvertors }: ToMdConvertors) {\n    this.nodeTypeConvertors = nodeTypeConvertors;\n    this.markTypeConvertors = markTypeConvertors;\n    this.delim = '';\n    this.result = '';\n    this.closed = false;\n    this.tightList = false;\n    this.stopNewline = false;\n    this.inTable = false;\n  }\n\n  private getMarkConvertor(mark: Mark) {\n    const type = mark.attrs.htmlInline ? 'html' : (mark.type.name as WwMarkType);\n\n    return this.markTypeConvertors[type];\n  }\n\n  private isInBlank() {\n    return /(^|\\n)$/.test(this.result);\n  }\n\n  private markText(mark: Mark, entering: boolean, parent: Node, index: number) {\n    const convertor = this.getMarkConvertor(mark);\n\n    if (convertor) {\n      const { delim, rawHTML } = convertor({ node: mark, parent, index }, entering);\n\n      return (rawHTML as string) || (delim as string);\n    }\n\n    return '';\n  }\n\n  setDelim(delim: string) {\n    this.delim = delim;\n  }\n\n  getDelim() {\n    return this.delim;\n  }\n\n  flushClose(size?: number) {\n    if (!this.stopNewline && this.closed) {\n      if (!this.isInBlank()) {\n        this.result += '\\n';\n      }\n\n      if (!size) {\n        size = 2;\n      }\n\n      if (size > 1) {\n        let delimMin = this.delim;\n        const trim = /\\s+$/.exec(delimMin);\n\n        if (trim) {\n          delimMin = delimMin.slice(0, delimMin.length - trim[0].length);\n        }\n\n        for (let i = 1; i < size; i += 1) {\n          this.result += `${delimMin}\\n`;\n        }\n      }\n\n      this.closed = false;\n    }\n  }\n\n  wrapBlock(delim: string, firstDelim: string | null, node: Node, fn: () => void) {\n    const old = this.getDelim();\n\n    this.write(firstDelim || delim);\n    this.setDelim(this.getDelim() + delim);\n    fn();\n    this.setDelim(old);\n    this.closeBlock(node);\n  }\n\n  ensureNewLine() {\n    if (!this.isInBlank()) {\n      this.result += '\\n';\n    }\n  }\n\n  write(content = '') {\n    this.flushClose();\n\n    if (this.delim && this.isInBlank()) {\n      this.result += this.delim;\n    }\n\n    if (content) {\n      this.result += content;\n    }\n  }\n\n  closeBlock(node: Node) {\n    this.closed = node;\n  }\n\n  text(text: string, escaped = true) {\n    const lines = text.split('\\n');\n\n    for (let i = 0; i < lines.length; i += 1) {\n      this.write();\n      this.result += escaped ? escape(lines[i]) : lines[i];\n\n      if (i !== lines.length - 1) {\n        this.result += '\\n';\n      }\n    }\n  }\n\n  convertBlock(node: Node, parent: Node, index: number) {\n    const type = node.type.name as WwNodeType;\n    const convertor = this.nodeTypeConvertors[type];\n    const nodeInfo = { node, parent, index };\n\n    if (node.attrs.htmlBlock) {\n      this.nodeTypeConvertors.html!(this, nodeInfo);\n    } else if (convertor) {\n      convertor(this, nodeInfo);\n    }\n  }\n\n  convertInline(parent: Node) {\n    const active: Mark[] = [];\n    let trailing = '';\n\n    const progress = (node: Node | null, _: number | null, index: number) => {\n      let marks = node ? (node.marks as Mark[]) : [];\n      let leading = trailing;\n\n      trailing = '';\n\n      // If whitespace has to be expelled from the node, adjust\n      // leading and trailing accordingly.\n      const removedWhitespace =\n        node &&\n        node.isText &&\n        marks.some((mark: Mark) => {\n          const markConvertor = this.getMarkConvertor(mark);\n          const info = markConvertor && markConvertor();\n\n          return info && info.removedEnclosingWhitespace;\n        });\n\n      if (removedWhitespace && node && node.text) {\n        const [, lead, mark, trail] = /^(\\s*)(.*?)(\\s*)$/m.exec(node.text)!;\n\n        leading += lead;\n        trailing = trail;\n\n        if (lead || trail) {\n          // @ts-ignore\n          // type is not defined for \"withText\" in prosemirror-model\n          node = mark ? node.withText(mark) : null;\n\n          if (!node) {\n            marks = active;\n          }\n        }\n      }\n\n      const lastMark = marks.length && last(marks);\n      const markConvertor = lastMark && this.getMarkConvertor(lastMark);\n      const markType = markConvertor && markConvertor();\n\n      const noEscape = markType && markType.escape === false;\n      const len = marks.length - (noEscape ? 1 : 0);\n\n      // Try to reorder 'mixable' marks, such as em and strong, which\n      // in Markdown may be opened and closed in different order, so\n      // that order of the marks for the token matches the order in\n      // active.\n      for (let i = 0; i < len; i += 1) {\n        const mark = marks[i];\n\n        if (markType && !markType.mixable) {\n          break;\n        }\n\n        for (let j = 0; j < active.length; j += 1) {\n          const other = active[j];\n\n          if (markType && !markType.mixable) {\n            break;\n          }\n\n          if (mark.eq(other)) {\n            // eslint-disable-next-line max-depth\n            if (i > j) {\n              marks = marks\n                .slice(0, j)\n                .concat(mark)\n                .concat(marks.slice(j, i))\n                .concat(marks.slice(i + 1, len));\n            } else if (j > i) {\n              marks = marks\n                .slice(0, i)\n                .concat(marks.slice(i + 1, j))\n                .concat(mark)\n                .concat(marks.slice(j, len));\n            }\n\n            break;\n          }\n        }\n      }\n\n      // Find the prefix of the mark set that didn't change\n      let keep = 0;\n\n      while (keep < Math.min(active.length, len) && marks[keep].eq(active[keep])) {\n        keep += 1;\n      }\n\n      // Close the marks that need to be closed\n      while (keep < active.length) {\n        const activedMark = active.pop();\n\n        if (activedMark) {\n          this.text(this.markText(activedMark, false, parent, index), false);\n        }\n      }\n\n      // Output any previously expelled trailing whitespace outside the marks\n      if (leading) {\n        this.text(leading);\n      }\n\n      // Open the marks that need to be opened\n      if (node) {\n        while (active.length < len) {\n          const mark = marks[active.length];\n\n          active.push(mark);\n          this.text(this.markText(mark, true, parent, index), false);\n        }\n\n        // Render the node. Special case code marks, since their content\n        // may not be escaped.\n        if (noEscape && node.isText) {\n          this.text(\n            this.markText(lastMark as Mark, true, parent, index) +\n              node.text +\n              this.markText(lastMark as Mark, false, parent, index + 1),\n            false\n          );\n        } else {\n          this.convertBlock(node, parent, index);\n        }\n      }\n    };\n\n    parent.forEach(progress);\n\n    progress(null, null, parent.childCount);\n  }\n\n  // Render a node's content as a list. `delim` should be the extra\n  // indentation added to all lines except the first in an item,\n  // `firstDelimFn` is a function going from an item index to a\n  // delimiter for the first line of the item.\n  convertList(node: Node, delim: string, firstDelimFn: FirstDelimFn) {\n    if (this.closed && (this.closed as Node).type === node.type) {\n      this.flushClose(3);\n    } else if (this.tightList) {\n      this.flushClose(1);\n    }\n\n    const tight = node.attrs.tight ?? true;\n    const prevTight = this.tightList;\n\n    this.tightList = tight;\n\n    node.forEach((child, _, index) => {\n      if (index && tight) {\n        this.flushClose(1);\n      }\n\n      this.wrapBlock(delim, firstDelimFn(index), node, () => this.convertBlock(child, node, index));\n    });\n\n    this.tightList = prevTight;\n  }\n\n  convertTableCell(node: Node) {\n    this.stopNewline = true;\n    this.inTable = true;\n\n    node.forEach((child, _, index) => {\n      if (includes(['bulletList', 'orderedList'], child.type.name)) {\n        this.convertBlock(child, node, index);\n        this.closed = false;\n      } else {\n        this.convertInline(child);\n\n        if (index < node.childCount - 1) {\n          const nextChild = node.child(index + 1);\n\n          if (nextChild.type.name === 'paragraph') {\n            this.write('<br>');\n          }\n        }\n      }\n    });\n\n    this.stopNewline = false;\n    this.inTable = false;\n  }\n\n  convertNode(parent: Node, infoForPosSync?: InfoForPosSync | null) {\n    parent.forEach((node, _, index) => {\n      this.convertBlock(node, parent, index);\n\n      if (infoForPosSync?.node === node) {\n        const lineTexts = this.result.split('\\n');\n\n        infoForPosSync.setMappedPos([lineTexts.length, last(lineTexts).length + 1]);\n      }\n    });\n\n    return this.result;\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/convertors/toMarkdown/toMdConvertors.ts",
    "content": "import { ProsemirrorNode } from 'prosemirror-model';\n\nimport isUndefined from 'tui-code-snippet/type/isUndefined';\n\nimport { nodeTypeWriters, write } from './toMdNodeTypeWriters';\n\nimport { repeat, quote, escapeXml, escapeTextForLink } from '@/utils/common';\n\nimport {\n  ToMdConvertorMap,\n  ToMdNodeTypeConvertorMap,\n  ToMdMarkTypeConvertorMap,\n  ToMdMarkTypeOptions,\n  NodeInfo,\n  MarkInfo,\n} from '@t/convertor';\nimport { WwNodeType, WwMarkType } from '@t/wysiwyg';\n\nfunction addBackticks(node: ProsemirrorNode, side: number) {\n  const { text } = node;\n  const ticks = /`+/g;\n  let len = 0;\n\n  if (node.isText && text) {\n    let matched = ticks.exec(text);\n\n    while (matched) {\n      len = Math.max(len, matched[0].length);\n      matched = ticks.exec(text);\n    }\n  }\n\n  let result = len > 0 && side > 0 ? ' `' : '`';\n\n  for (let i = 0; i < len; i += 1) {\n    result += '`';\n  }\n\n  if (len > 0 && side < 0) {\n    result += ' ';\n  }\n\n  return result;\n}\n\nfunction getPairRawHTML(rawHTML?: string[]) {\n  return rawHTML ? [`<${rawHTML}>`, `</${rawHTML}>`] : null;\n}\n\nfunction getOpenRawHTML(rawHTML?: string) {\n  return rawHTML ? `<${rawHTML}>` : null;\n}\n\nfunction getCloseRawHTML(rawHTML?: string) {\n  return rawHTML ? `</${rawHTML}>` : null;\n}\n\nexport const toMdConvertors: ToMdConvertorMap = {\n  heading({ node }) {\n    const { attrs } = node;\n    const { level } = attrs;\n    let delim = repeat('#', level);\n\n    if (attrs.headingType === 'setext') {\n      delim = level === 1 ? '===' : '---';\n    }\n\n    return {\n      delim,\n      rawHTML: getPairRawHTML(attrs.rawHTML),\n    };\n  },\n\n  codeBlock({ node }) {\n    const { attrs, textContent } = node as ProsemirrorNode;\n\n    return {\n      delim: [`\\`\\`\\`${attrs.language || ''}`, '```'],\n      rawHTML: getPairRawHTML(attrs.rawHTML),\n      text: textContent,\n    };\n  },\n\n  blockQuote({ node }) {\n    return {\n      delim: '> ',\n      rawHTML: getPairRawHTML(node.attrs.rawHTML),\n    };\n  },\n\n  bulletList({ node }, { inTable }) {\n    let { rawHTML } = node.attrs;\n\n    if (inTable) {\n      rawHTML = rawHTML || 'ul';\n    }\n\n    return {\n      delim: '*',\n      rawHTML: getPairRawHTML(rawHTML),\n    };\n  },\n\n  orderedList({ node }, { inTable }) {\n    let { rawHTML } = node.attrs;\n\n    if (inTable) {\n      rawHTML = rawHTML || 'ol';\n    }\n\n    return {\n      rawHTML: getPairRawHTML(rawHTML),\n    };\n  },\n\n  listItem({ node }, { inTable }) {\n    const { task, checked } = node.attrs;\n    let { rawHTML } = node.attrs;\n\n    if (inTable) {\n      rawHTML = rawHTML || 'li';\n    }\n\n    const className = task ? ` class=\"task-list-item${checked ? ' checked' : ''}\"` : '';\n    const dataset = task ? ` data-task${checked ? ` data-task-checked` : ''}` : '';\n\n    return {\n      rawHTML: rawHTML ? [`<${rawHTML}${className}${dataset}>`, `</${rawHTML}>`] : null,\n    };\n  },\n\n  table({ node }) {\n    return {\n      rawHTML: getPairRawHTML(node.attrs.rawHTML),\n    };\n  },\n\n  tableHead({ node }) {\n    return {\n      rawHTML: getPairRawHTML(node.attrs.rawHTML),\n    };\n  },\n\n  tableBody({ node }) {\n    return {\n      rawHTML: getPairRawHTML(node.attrs.rawHTML),\n    };\n  },\n\n  tableRow({ node }) {\n    return {\n      rawHTML: getPairRawHTML(node.attrs.rawHTML),\n    };\n  },\n\n  tableHeadCell({ node }) {\n    return {\n      rawHTML: getPairRawHTML(node.attrs.rawHTML),\n    };\n  },\n\n  tableBodyCell({ node }) {\n    return {\n      rawHTML: getPairRawHTML(node.attrs.rawHTML),\n    };\n  },\n\n  image({ node }) {\n    const { attrs } = node;\n    const { rawHTML, altText } = attrs;\n    const imageUrl = attrs.imageUrl.replace(/&amp;/g, '&');\n    const altAttr = altText ? ` alt=\"${escapeXml(altText)}\"` : '';\n\n    return {\n      rawHTML: rawHTML ? `<${rawHTML} src=\"${escapeXml(imageUrl)}\"${altAttr}>` : null,\n      attrs: {\n        altText: escapeTextForLink(altText || ''),\n        imageUrl,\n      },\n    };\n  },\n\n  thematicBreak({ node }) {\n    return {\n      delim: '***',\n      rawHTML: getOpenRawHTML(node.attrs.rawHTML),\n    };\n  },\n\n  customBlock({ node }) {\n    const { attrs, textContent } = node as ProsemirrorNode;\n\n    return {\n      delim: [`$$${attrs.info}`, '$$'],\n      text: textContent,\n    };\n  },\n\n  frontMatter({ node }) {\n    return {\n      text: (node as ProsemirrorNode).textContent,\n    };\n  },\n\n  widget({ node }) {\n    return {\n      text: (node as ProsemirrorNode).textContent,\n    };\n  },\n\n  strong({ node }, { entering }) {\n    const { rawHTML } = node.attrs;\n\n    return {\n      delim: '**',\n      rawHTML: entering ? getOpenRawHTML(rawHTML) : getCloseRawHTML(rawHTML),\n    };\n  },\n\n  emph({ node }, { entering }) {\n    const { rawHTML } = node.attrs;\n\n    return {\n      delim: '*',\n      rawHTML: entering ? getOpenRawHTML(rawHTML) : getCloseRawHTML(rawHTML),\n    };\n  },\n\n  strike({ node }, { entering }) {\n    const { rawHTML } = node.attrs;\n\n    return {\n      delim: '~~',\n      rawHTML: entering ? getOpenRawHTML(rawHTML) : getCloseRawHTML(rawHTML),\n    };\n  },\n\n  link({ node }, { entering }) {\n    const { attrs } = node;\n    const { title, rawHTML } = attrs;\n    const linkUrl = attrs.linkUrl.replace(/&amp;/g, '&');\n    const titleAttr = title ? ` title=\"${escapeXml(title)}\"` : '';\n\n    if (entering) {\n      return {\n        delim: '[',\n        rawHTML: rawHTML ? `<${rawHTML} href=\"${escapeXml(linkUrl)}\"${titleAttr}>` : null,\n      };\n    }\n\n    return {\n      delim: `](${linkUrl}${title ? ` ${quote(escapeTextForLink(title))}` : ''})`,\n      rawHTML: getCloseRawHTML(rawHTML),\n    };\n  },\n\n  code({ node, parent, index = 0 }, { entering }) {\n    const delim = entering\n      ? addBackticks(parent!.child(index), -1)\n      : addBackticks(parent!.child(index - 1), 1);\n    const rawHTML = entering\n      ? getOpenRawHTML(node.attrs.rawHTML)\n      : getCloseRawHTML(node.attrs.rawHTML);\n\n    return {\n      delim,\n      rawHTML,\n    };\n  },\n\n  htmlComment({ node }) {\n    return {\n      text: (node as ProsemirrorNode).textContent,\n    };\n  },\n\n  // html inline node, html block node\n  html({ node }, { entering }) {\n    const tagName = node.type.name;\n    const attrs = node.attrs.htmlAttrs;\n    let openTag = `<${tagName}`;\n    const closeTag = `</${tagName}>`;\n\n    Object.keys(attrs).forEach((attrName) => {\n      // To prevent broken converting when attributes has double quote string\n      openTag += ` ${attrName}=\"${attrs[attrName].replace(/\"/g, \"'\")}\"`;\n    });\n    openTag += '>';\n\n    if (node.attrs.htmlInline) {\n      return {\n        rawHTML: entering ? openTag : closeTag,\n      };\n    }\n\n    return {\n      text: `${openTag}${node.attrs.childrenHTML}${closeTag}`,\n    };\n  },\n};\n\nconst markTypeOptions: ToMdMarkTypeOptions = {\n  strong: {\n    mixable: true,\n    removedEnclosingWhitespace: true,\n  },\n\n  emph: {\n    mixable: true,\n    removedEnclosingWhitespace: true,\n  },\n\n  strike: {\n    mixable: true,\n    removedEnclosingWhitespace: true,\n  },\n\n  code: {\n    escape: false,\n  },\n\n  link: null,\n\n  html: null,\n};\n\nfunction createNodeTypeConvertors(convertors: ToMdConvertorMap) {\n  const nodeTypeConvertors: ToMdNodeTypeConvertorMap = {};\n  const nodeTypes = Object.keys(nodeTypeWriters) as WwNodeType[];\n\n  nodeTypes.forEach((type) => {\n    nodeTypeConvertors[type] = (state, nodeInfo) => {\n      const writer = nodeTypeWriters[type];\n\n      if (writer) {\n        const convertor = convertors[type];\n        const params = convertor\n          ? convertor(nodeInfo as NodeInfo, {\n              inTable: state.inTable,\n            })\n          : {};\n\n        write(type, { state, nodeInfo, params });\n      }\n    };\n  });\n\n  return nodeTypeConvertors;\n}\n\nfunction createMarkTypeConvertors(convertors: ToMdConvertorMap) {\n  const markTypeConvertors: ToMdMarkTypeConvertorMap = {};\n  const markTypes = Object.keys(markTypeOptions) as WwMarkType[];\n\n  markTypes.forEach((type) => {\n    markTypeConvertors[type] = (nodeInfo, entering) => {\n      const markOption = markTypeOptions[type];\n      const convertor = convertors[type];\n\n      // There are two ways to call the mark type converter\n      // in the `toMdConvertorState` module.\n      // When calling the converter without using `delim` and `rawHTML` values,\n      // the converter is called without parameters.\n      const runConvertor = convertor && nodeInfo && !isUndefined(entering);\n      const params = runConvertor ? convertor!(nodeInfo as MarkInfo, { entering }) : {};\n\n      return { ...params, ...markOption };\n    };\n  });\n\n  return markTypeConvertors;\n}\n\n// Step 1: Create the converter by overriding the custom converter\n//         to the original converter defined in the `toMdConvertors` module.\n//         If the node type is defined in the original converter,\n//         the `origin()` function is exported to the paramter of the converter.\n// Step 2: Create a converter for the node type of ProseMirror by combining the converter\n//         created in Step 1 with the writers defined in the`toMdNodeTypeWriters` module.\n//         Each writer converts the ProseMirror's node to a string with the value returned\n//         by the converter, and then stores the state in the`toMdConverterState` class.\n// Step 3: Create a converter for the mark type of ProseMirror by combining the converter\n//         created in Step 1 with `markTypeOptions`.\n// Step 4: The created node type converter and mark type converter are injected\n//         when creating an instance of the`toMdConverterState` class.\nexport function createMdConvertors(customConvertors: ToMdConvertorMap) {\n  const customConvertorTypes = Object.keys(customConvertors) as (WwNodeType | WwMarkType)[];\n\n  customConvertorTypes.forEach((type) => {\n    const baseConvertor = toMdConvertors[type];\n    const customConvertor = customConvertors[type]!;\n\n    if (baseConvertor) {\n      toMdConvertors[type] = (nodeInfo, context) => {\n        context.origin = () => baseConvertor(nodeInfo, context);\n\n        return customConvertor(nodeInfo, context);\n      };\n    } else {\n      toMdConvertors[type] = customConvertor;\n    }\n\n    delete customConvertors[type];\n  });\n\n  const nodeTypeConvertors = createNodeTypeConvertors(toMdConvertors);\n  const markTypeConvertors = createMarkTypeConvertors(toMdConvertors);\n\n  return {\n    nodeTypeConvertors,\n    markTypeConvertors,\n  };\n}\n"
  },
  {
    "path": "apps/editor/src/convertors/toMarkdown/toMdNodeTypeWriters.ts",
    "content": "import { ProsemirrorNode } from 'prosemirror-model';\n\nimport inArray from 'tui-code-snippet/array/inArray';\n\nimport { escapeTextForLink, repeat } from '@/utils/common';\n\nimport {\n  ToMdNodeTypeWriterMap,\n  ToMdConvertorState,\n  NodeInfo,\n  ToMdConvertorReturnValues,\n} from '@t/convertor';\nimport { WwNodeType, ColumnAlign } from '@t/wysiwyg';\n\nfunction convertToRawHTMLHavingInlines(\n  state: ToMdConvertorState,\n  node: ProsemirrorNode,\n  [openTag, closeTag]: string[]\n) {\n  state.write(openTag);\n  state.convertInline(node);\n  state.write(closeTag);\n}\n\nfunction convertToRawHTMLHavingBlocks(\n  state: ToMdConvertorState,\n  { node, parent }: NodeInfo,\n  [openTag, closeTag]: string[]\n) {\n  state.stopNewline = true;\n  state.write(openTag);\n  state.convertNode(node);\n  state.write(closeTag);\n\n  if (parent?.type.name === 'doc') {\n    state.closeBlock(node);\n    state.stopNewline = false;\n  }\n}\n\nfunction createTableHeadDelim(textContent: string, columnAlign: ColumnAlign) {\n  let textLen = textContent.length;\n  let leftDelim = '';\n  let rightDelim = '';\n\n  if (columnAlign === 'left') {\n    leftDelim = ':';\n    textLen -= 1;\n  } else if (columnAlign === 'right') {\n    rightDelim = ':';\n    textLen -= 1;\n  } else if (columnAlign === 'center') {\n    leftDelim = ':';\n    rightDelim = ':';\n    textLen -= 2;\n  }\n\n  return `${leftDelim}${repeat('-', Math.max(textLen, 3))}${rightDelim}`;\n}\n\nexport const nodeTypeWriters: ToMdNodeTypeWriterMap = {\n  text(state, { node }) {\n    const text = node.text ?? '';\n\n    if ((node.marks || []).some((mark) => mark.type.name === 'link')) {\n      state.text(escapeTextForLink(text), false);\n    } else {\n      state.text(text);\n    }\n  },\n\n  paragraph(state, { node, parent, index = 0 }) {\n    if (state.stopNewline) {\n      state.convertInline(node);\n    } else {\n      const firstChildNode = index === 0;\n      const prevNode = !firstChildNode && parent!.child(index - 1);\n      const prevEmptyNode = prevNode && prevNode.childCount === 0;\n      const nextNode = index < parent!.childCount - 1 && parent!.child(index + 1);\n      const nextParaNode = nextNode && nextNode.type.name === 'paragraph';\n      const emptyNode = node.childCount === 0;\n\n      if (emptyNode && prevEmptyNode) {\n        state.write('<br>\\n');\n      } else if (emptyNode && !prevEmptyNode && !firstChildNode) {\n        if (parent?.type.name === 'listItem') {\n          const prevDelim = state.getDelim();\n\n          state.setDelim('');\n          state.write('<br>');\n\n          state.setDelim(prevDelim);\n        }\n        state.write('\\n');\n      } else {\n        state.convertInline(node);\n\n        if (nextParaNode) {\n          state.write('\\n');\n        } else {\n          state.closeBlock(node);\n        }\n      }\n    }\n  },\n\n  heading(state, { node }, { delim }) {\n    const { headingType } = node.attrs;\n\n    if (headingType === 'atx') {\n      state.write(`${delim} `);\n      state.convertInline(node);\n      state.closeBlock(node);\n    } else {\n      state.convertInline(node);\n      state.ensureNewLine();\n      state.write(delim as string);\n      state.closeBlock(node);\n    }\n  },\n\n  codeBlock(state, { node }, { delim, text }) {\n    const [openDelim, closeDelim] = delim as string[];\n\n    state.write(openDelim);\n    state.ensureNewLine();\n    state.text(text!, false);\n    state.ensureNewLine();\n    state.write(closeDelim);\n    state.closeBlock(node);\n  },\n\n  blockQuote(state, { node, parent }, { delim }) {\n    if (parent?.type.name === node.type.name) {\n      state.flushClose(1);\n    }\n\n    state.wrapBlock(delim as string, null, node, () => state.convertNode(node));\n  },\n\n  bulletList(state, { node }, { delim }) {\n    // soft-tab(4)\n    state.convertList(node, repeat(' ', 4), () => `${delim} `);\n  },\n\n  orderedList(state, { node }) {\n    const start = node.attrs.order || 1;\n\n    // soft-tab(4)\n    state.convertList(node, repeat(' ', 4), (index: number) => {\n      const orderedNum = String(start + index);\n\n      return `${orderedNum}. `;\n    });\n  },\n\n  listItem(state, { node }) {\n    const { task, checked } = node.attrs;\n\n    if (task) {\n      state.write(`[${checked ? 'x' : ' '}] `);\n    }\n\n    state.convertNode(node);\n  },\n\n  image(state, _, { attrs }) {\n    state.write(`![${attrs?.altText}](${attrs?.imageUrl})`);\n  },\n\n  thematicBreak(state, { node }, { delim }) {\n    state.write(delim as string);\n    state.closeBlock(node);\n  },\n\n  table(state, { node }) {\n    state.convertNode(node);\n    state.closeBlock(node);\n  },\n\n  tableHead(state, { node }, { delim }) {\n    const row = node.firstChild;\n\n    state.convertNode(node);\n\n    let result = delim ?? '';\n\n    if (!delim && row) {\n      row.forEach(({ textContent, attrs }) => {\n        const headDelim = createTableHeadDelim(textContent, attrs.align);\n\n        result += `| ${headDelim} `;\n      });\n    }\n\n    state.write(`${result}|`);\n    state.ensureNewLine();\n  },\n\n  tableBody(state, { node }) {\n    state.convertNode(node);\n  },\n\n  tableRow(state, { node }) {\n    state.convertNode(node);\n    state.write('|');\n    state.ensureNewLine();\n  },\n\n  tableHeadCell(state, { node }, { delim = '| ' }) {\n    state.write(delim as string);\n    state.convertTableCell(node);\n    state.write(' ');\n  },\n\n  tableBodyCell(state, { node }, { delim = '| ' }) {\n    state.write(delim as string);\n    state.convertTableCell(node);\n    state.write(' ');\n  },\n\n  customBlock(state, { node }, { delim, text }) {\n    const [openDelim, closeDelim] = delim as string[];\n\n    state.write(openDelim);\n    state.ensureNewLine();\n    state.text(text!, false);\n    state.ensureNewLine();\n    state.write(closeDelim);\n    state.closeBlock(node);\n  },\n\n  frontMatter(state, { node }, { text }) {\n    state.text(text!, false);\n    state.closeBlock(node);\n  },\n\n  widget(state, _, { text }) {\n    state.write(text);\n  },\n\n  html(state, { node }, { text }) {\n    state.write(text);\n\n    if (node.attrs.htmlBlock) {\n      state.closeBlock(node);\n    }\n  },\n\n  htmlComment(state, { node }, { text }) {\n    state.write(text);\n    state.closeBlock(node);\n  },\n};\n\nexport function write(\n  type: WwNodeType,\n  {\n    state,\n    nodeInfo,\n    params,\n  }: {\n    state: ToMdConvertorState;\n    nodeInfo: NodeInfo;\n    params: ToMdConvertorReturnValues;\n  }\n) {\n  const { rawHTML } = params;\n\n  if (rawHTML) {\n    if (inArray(type, ['heading', 'codeBlock']) > -1) {\n      convertToRawHTMLHavingInlines(state, nodeInfo.node, rawHTML as string[]);\n    } else if (inArray(type, ['image', 'thematicBreak']) > -1) {\n      state.write(rawHTML as string);\n    } else {\n      convertToRawHTMLHavingBlocks(state, nodeInfo, rawHTML as string[]);\n    }\n  } else {\n    nodeTypeWriters[type]!(state, nodeInfo, params);\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/convertors/toWysiwyg/htmlToWwConvertors.ts",
    "content": "import { MdNode } from '@toast-ui/toastmark';\nimport { sanitizeHTML } from '@/sanitizer/htmlSanitizer';\n\nimport {\n  HTMLToWwConvertorMap,\n  FlattenHTMLToWwConvertorMap,\n  ToWwConvertorState,\n} from '@t/convertor';\nimport { includes } from '@/utils/common';\nimport { reHTMLTag } from '@/utils/constants';\n\nexport function getTextWithoutTrailingNewline(text: string) {\n  return text[text.length - 1] === '\\n' ? text.slice(0, text.length - 1) : text;\n}\n\nexport function isCustomHTMLInlineNode({ schema }: ToWwConvertorState, node: MdNode) {\n  const html = node.literal!;\n  const matched = html.match(reHTMLTag);\n\n  if (matched) {\n    const [, openTagName, , closeTagName] = matched;\n    const typeName = (openTagName || closeTagName).toLowerCase();\n\n    return node.type === 'htmlInline' && !!(schema.marks[typeName] || schema.nodes[typeName]);\n  }\n  return false;\n}\n\nexport function isInlineNode({ type }: MdNode) {\n  return includes(['text', 'strong', 'emph', 'strike', 'image', 'link', 'code'], type);\n}\n\nfunction isSoftbreak(mdNode: MdNode | null) {\n  return mdNode?.type === 'softbreak';\n}\n\nfunction isListNode({ type, literal }: MdNode) {\n  const matched = type === 'htmlInline' && literal!.match(reHTMLTag);\n\n  if (matched) {\n    const [, openTagName, , closeTagName] = matched;\n    const tagName = openTagName || closeTagName;\n\n    if (tagName) {\n      return includes(['ul', 'ol', 'li'], tagName.toLowerCase());\n    }\n  }\n\n  return false;\n}\n\nfunction getListItemAttrs({ literal }: MdNode) {\n  const task = /data-task/.test(literal!);\n  const checked = /data-task-checked/.test(literal!);\n\n  return { task, checked };\n}\n\nfunction getMatchedAttributeValue(rawHTML: string, ...attrNames: string[]) {\n  const wrapper = document.createElement('div');\n\n  wrapper.innerHTML = sanitizeHTML(rawHTML);\n\n  const el = wrapper.firstChild as HTMLElement;\n\n  return attrNames.map((attrName) => el.getAttribute(attrName) || '');\n}\n\nfunction createConvertors(convertors: HTMLToWwConvertorMap) {\n  const convertorMap: FlattenHTMLToWwConvertorMap = {};\n\n  Object.keys(convertors).forEach((key) => {\n    const tagNames = key.split(', ');\n\n    tagNames.forEach((tagName) => {\n      const name = tagName.toLowerCase();\n\n      convertorMap[name] = convertors[key]!;\n    });\n  });\n\n  return convertorMap;\n}\n\nconst convertors: HTMLToWwConvertorMap = {\n  'b, strong': (state, _, openTagName) => {\n    const { strong } = state.schema.marks;\n\n    if (openTagName) {\n      state.openMark(strong.create({ rawHTML: openTagName }));\n    } else {\n      state.closeMark(strong);\n    }\n  },\n\n  'i, em': (state, _, openTagName) => {\n    const { emph } = state.schema.marks;\n\n    if (openTagName) {\n      state.openMark(emph.create({ rawHTML: openTagName }));\n    } else {\n      state.closeMark(emph);\n    }\n  },\n\n  's, del': (state, _, openTagName) => {\n    const { strike } = state.schema.marks;\n\n    if (openTagName) {\n      state.openMark(strike.create({ rawHTML: openTagName }));\n    } else {\n      state.closeMark(strike);\n    }\n  },\n\n  code: (state, _, openTagName) => {\n    const { code } = state.schema.marks;\n\n    if (openTagName) {\n      state.openMark(code.create({ rawHTML: openTagName }));\n    } else {\n      state.closeMark(code);\n    }\n  },\n\n  a: (state, node, openTagName) => {\n    const tag = node.literal!;\n    const { link } = state.schema.marks;\n\n    if (openTagName) {\n      const [linkUrl] = getMatchedAttributeValue(tag, 'href');\n\n      state.openMark(\n        link.create({\n          linkUrl,\n          rawHTML: openTagName,\n        })\n      );\n    } else {\n      state.closeMark(link);\n    }\n  },\n\n  img: (state, node, openTagName) => {\n    const tag = node.literal!;\n\n    if (openTagName) {\n      const [imageUrl, altText] = getMatchedAttributeValue(tag, 'src', 'alt');\n      const { image } = state.schema.nodes;\n\n      state.addNode(image, {\n        rawHTML: openTagName,\n        imageUrl,\n        ...(altText && { altText }),\n      });\n    }\n  },\n\n  hr: (state, _, openTagName) => {\n    state.addNode(state.schema.nodes.thematicBreak, { rawHTML: openTagName });\n  },\n\n  br: (state, node) => {\n    const { paragraph } = state.schema.nodes;\n    const { parent, prev, next } = node;\n\n    if (parent?.type === 'paragraph') {\n      // should open a paragraph node when line text has only <br> tag\n      // ex) first line\\n\\n<br>\\nfourth line\n      if (isSoftbreak(prev)) {\n        state.openNode(paragraph);\n      }\n\n      // should close a paragraph node when line text has only <br> tag\n      // ex) first line\\n\\n<br>\\nfourth line\n      if (isSoftbreak(next)) {\n        state.closeNode();\n        // should close a paragraph node and open a paragraph node to separate between blocks\n        // when <br> tag is in the middle of the paragraph\n        // ex) first <br>line\\nthird line\n      } else if (next) {\n        state.closeNode();\n        state.openNode(paragraph);\n      }\n    } else if (parent?.type === 'tableCell') {\n      if (prev && (isInlineNode(prev) || isCustomHTMLInlineNode(state, prev))) {\n        state.closeNode();\n      }\n\n      if (next && (isInlineNode(next) || isCustomHTMLInlineNode(state, next))) {\n        state.openNode(paragraph);\n      }\n    }\n  },\n\n  pre: (state, node, openTagName) => {\n    const container = document.createElement('div');\n\n    container.innerHTML = node.literal!;\n\n    const literal = container.firstChild?.firstChild?.textContent;\n\n    state.openNode(state.schema.nodes.codeBlock, { rawHTML: openTagName });\n    state.addText(getTextWithoutTrailingNewline(literal!));\n    state.closeNode();\n  },\n\n  'ul, ol': (state, node, openTagName) => {\n    // in the table cell, '<ul>', '<ol>' is parsed as 'htmlInline' node\n    if (node.parent!.type === 'tableCell') {\n      const { bulletList, orderedList, paragraph } = state.schema.nodes;\n      const list = openTagName === 'ul' ? bulletList : orderedList;\n\n      if (openTagName) {\n        if (node.prev && !isListNode(node.prev)) {\n          state.closeNode();\n        }\n\n        state.openNode(list, { rawHTML: openTagName });\n      } else {\n        state.closeNode();\n\n        if (node.next && !isListNode(node.next)) {\n          state.openNode(paragraph);\n        }\n      }\n    }\n  },\n\n  li: (state, node, openTagName) => {\n    // in the table cell, '<li>' is parsed as 'htmlInline' node\n    if (node.parent?.type === 'tableCell') {\n      const { listItem, paragraph } = state.schema.nodes;\n\n      if (openTagName) {\n        const attrs = getListItemAttrs(node);\n\n        if (node.prev && !isListNode(node.prev)) {\n          state.closeNode();\n        }\n\n        state.openNode(listItem, { rawHTML: openTagName, ...attrs });\n\n        if (node.next && !isListNode(node.next)) {\n          state.openNode(paragraph);\n        }\n      } else {\n        if (node.prev && !isListNode(node.prev)) {\n          state.closeNode();\n        }\n\n        state.closeNode();\n      }\n    }\n  },\n};\n\nexport const htmlToWwConvertors = createConvertors(convertors);\n"
  },
  {
    "path": "apps/editor/src/convertors/toWysiwyg/toWwConvertorState.ts",
    "content": "import { Schema, Node, NodeType, Mark, MarkType, DOMParser } from 'prosemirror-model';\nimport { MdNode } from '@toast-ui/toastmark';\n\nimport { ToWwConvertorMap, StackItem, Attrs, InfoForPosSync } from '@t/convertor';\nimport { last } from '@/utils/common';\nimport { isContainer, getChildrenText } from '@/utils/markdown';\n\nexport function mergeMarkText(a: Node, b: Node) {\n  if (a.isText && b.isText && Mark.sameSet(a.marks, b.marks)) {\n    // @ts-ignore\n    // type is not defined for \"withText\" in prosemirror-model\n    return a.withText(a.text! + b.text);\n  }\n\n  return false;\n}\n\nexport default class ToWwConvertorState {\n  public readonly schema: Schema;\n\n  private readonly convertors: ToWwConvertorMap;\n\n  private stack: StackItem[];\n\n  private marks: Mark[];\n\n  constructor(schema: Schema, convertors: ToWwConvertorMap) {\n    this.schema = schema;\n    this.convertors = convertors;\n    this.stack = [{ type: this.schema.topNodeType, attrs: null, content: [] }];\n    this.marks = Mark.none as Mark[];\n  }\n\n  top() {\n    return last(this.stack);\n  }\n\n  push(node: Node) {\n    if (this.stack.length) {\n      this.top().content.push(node);\n    }\n  }\n\n  addText(text: string) {\n    if (text) {\n      const nodes = this.top().content;\n      const lastNode = last(nodes);\n      const node = this.schema.text(text, this.marks);\n      const merged = lastNode && mergeMarkText(lastNode, node);\n\n      if (merged) {\n        nodes[nodes.length - 1] = merged;\n      } else {\n        nodes.push(node);\n      }\n    }\n  }\n\n  openMark(mark: Mark) {\n    this.marks = mark.addToSet(this.marks) as Mark[];\n  }\n\n  closeMark(mark: MarkType) {\n    this.marks = mark.removeFromSet(this.marks) as Mark[];\n  }\n\n  addNode(type: NodeType, attrs: Attrs, content: Node[]) {\n    const node = type.createAndFill(attrs, content, this.marks);\n\n    if (node) {\n      this.push(node);\n\n      return node;\n    }\n\n    return null;\n  }\n\n  openNode(type: NodeType, attrs: Attrs) {\n    this.stack.push({ type, attrs, content: [] });\n  }\n\n  closeNode() {\n    if (this.marks.length) {\n      this.marks = Mark.none as Mark[];\n    }\n\n    const { type, attrs, content } = this.stack.pop() as StackItem;\n\n    return this.addNode(type, attrs, content);\n  }\n\n  convertByDOMParser(root: HTMLElement) {\n    const doc = DOMParser.fromSchema(this.schema).parse(root);\n\n    doc.content.forEach((node) => this.push(node));\n  }\n\n  private closeUnmatchedHTMLInline(node: MdNode, entering: boolean) {\n    if (!entering && node.type !== 'htmlInline') {\n      const length = this.stack.length - 1;\n\n      for (let i = length; i >= 0; i -= 1) {\n        const nodeInfo = this.stack[i];\n\n        if (nodeInfo.attrs?.rawHTML) {\n          if (nodeInfo.content.length) {\n            this.closeNode();\n          } else {\n            // just pop useless unmatched html inline node\n            this.stack.pop();\n          }\n        } else {\n          break;\n        }\n      }\n    }\n  }\n\n  private convert(mdNode: MdNode, infoForPosSync?: InfoForPosSync) {\n    const walker = mdNode.walker();\n    let event = walker.next();\n\n    while (event) {\n      const { node, entering } = event;\n      const convertor = this.convertors[node.type];\n\n      let skipped = false;\n\n      if (convertor) {\n        const context = {\n          entering,\n          leaf: !isContainer(node),\n          getChildrenText,\n          options: { gfm: true, nodeId: false, tagFilter: false, softbreak: '\\n' },\n          skipChildren: () => {\n            skipped = true;\n          },\n        };\n\n        this.closeUnmatchedHTMLInline(node, entering);\n        convertor(this, node, context);\n\n        if (infoForPosSync?.node === node) {\n          const pos =\n            this.stack.reduce(\n              (nodeSize, stackItem) =>\n                nodeSize +\n                stackItem.content.reduce((contentSize, pmNode) => contentSize + pmNode.nodeSize, 0),\n              0\n            ) + 1;\n\n          infoForPosSync.setMappedPos(pos);\n        }\n      }\n\n      if (skipped) {\n        walker.resumeAt(node, false);\n        walker.next();\n      }\n\n      event = walker.next();\n    }\n  }\n\n  convertNode(mdNode: MdNode, infoForPosSync?: InfoForPosSync) {\n    this.convert(mdNode, infoForPosSync);\n\n    if (this.stack.length) {\n      return this.closeNode();\n    }\n\n    return null;\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/convertors/toWysiwyg/toWwConvertors.ts",
    "content": "import {\n  MdNode,\n  HeadingMdNode,\n  CodeBlockMdNode,\n  ListItemMdNode,\n  LinkMdNode,\n  TableCellMdNode,\n  CustomBlockMdNode,\n  CustomInlineMdNode,\n  TableMdNode,\n  HTMLConvertorMap,\n  OpenTagToken,\n  Renderer,\n} from '@toast-ui/toastmark';\nimport toArray from 'tui-code-snippet/collection/toArray';\n\nimport { isElemNode } from '@/utils/dom';\n\nimport {\n  htmlToWwConvertors,\n  getTextWithoutTrailingNewline,\n  isInlineNode,\n  isCustomHTMLInlineNode,\n} from './htmlToWwConvertors';\n\nimport { ToWwConvertorMap } from '@t/convertor';\nimport { createWidgetContent, getWidgetContent } from '@/widget/rules';\nimport { getChildrenHTML, getHTMLAttrsByHTMLString } from '@/wysiwyg/nodes/html';\nimport { includes } from '@/utils/common';\nimport { reBR, reHTMLTag, reHTMLComment } from '@/utils/constants';\nimport { sanitizeHTML } from '@/sanitizer/htmlSanitizer';\n\nfunction isBRTag(node: MdNode) {\n  return node.type === 'htmlInline' && reBR.test(node.literal!);\n}\n\nfunction addRawHTMLAttributeToDOM(parent: Node) {\n  toArray(parent.childNodes).forEach((child) => {\n    if (isElemNode(child)) {\n      const openTagName = child.nodeName.toLowerCase();\n\n      (child as HTMLElement).setAttribute('data-raw-html', openTagName);\n\n      if (child.childNodes) {\n        addRawHTMLAttributeToDOM(child);\n      }\n    }\n  });\n}\n\nconst toWwConvertors: ToWwConvertorMap = {\n  text(state, node) {\n    state.addText(node.literal || '');\n  },\n\n  paragraph(state, node, { entering }, customAttrs) {\n    if (entering) {\n      const { paragraph } = state.schema.nodes;\n\n      // The `\\n\\n` entered in markdown separates the paragraph.\n      // When changing to wysiwyg, a newline is added between the two paragraphs.\n      if (node.prev?.type === 'paragraph') {\n        state.openNode(paragraph, customAttrs);\n        state.closeNode();\n      }\n\n      state.openNode(paragraph, customAttrs);\n    } else {\n      state.closeNode();\n    }\n  },\n\n  heading(state, node, { entering }, customAttrs) {\n    if (entering) {\n      const { level, headingType } = node as HeadingMdNode;\n\n      state.openNode(state.schema.nodes.heading, { level, headingType, ...customAttrs });\n    } else {\n      state.closeNode();\n    }\n  },\n\n  codeBlock(state, node, customAttrs) {\n    const { codeBlock } = state.schema.nodes;\n    const { info, literal } = node as CodeBlockMdNode;\n\n    state.openNode(codeBlock, { language: info, ...customAttrs });\n    state.addText(getTextWithoutTrailingNewline(literal || ''));\n    state.closeNode();\n  },\n\n  list(state, node, { entering }, customAttrs) {\n    if (entering) {\n      const { bulletList, orderedList } = state.schema.nodes;\n      const { type, start } = (node as ListItemMdNode).listData;\n\n      if (type === 'bullet') {\n        state.openNode(bulletList, customAttrs);\n      } else {\n        state.openNode(orderedList, { order: start, ...customAttrs });\n      }\n    } else {\n      state.closeNode();\n    }\n  },\n\n  item(state, node, { entering }, customAttrs) {\n    const { listItem } = state.schema.nodes;\n    const { task, checked } = (node as ListItemMdNode).listData;\n\n    if (entering) {\n      const attrs = {\n        ...(task && { task }),\n        ...(checked && { checked }),\n        ...customAttrs,\n      };\n\n      state.openNode(listItem, attrs);\n    } else {\n      state.closeNode();\n    }\n  },\n\n  blockQuote(state, _, { entering }, customAttrs) {\n    if (entering) {\n      state.openNode(state.schema.nodes.blockQuote, customAttrs);\n    } else {\n      state.closeNode();\n    }\n  },\n\n  image(state, node, { entering, skipChildren }, customAttrs) {\n    const { image } = state.schema.nodes;\n    const { destination, firstChild } = node as LinkMdNode;\n\n    if (entering && skipChildren) {\n      skipChildren();\n    }\n\n    state.addNode(image, {\n      imageUrl: destination,\n      ...(firstChild && { altText: firstChild.literal }),\n      ...customAttrs,\n    });\n  },\n\n  thematicBreak(state, node, _, customAttrs) {\n    state.addNode(state.schema.nodes.thematicBreak, customAttrs);\n  },\n\n  strong(state, _, { entering }, customAttrs) {\n    const { strong } = state.schema.marks;\n\n    if (entering) {\n      state.openMark(strong.create(customAttrs));\n    } else {\n      state.closeMark(strong);\n    }\n  },\n\n  emph(state, _, { entering }, customAttrs) {\n    const { emph } = state.schema.marks;\n\n    if (entering) {\n      state.openMark(emph.create(customAttrs));\n    } else {\n      state.closeMark(emph);\n    }\n  },\n\n  link(state, node, { entering }, customAttrs) {\n    const { link } = state.schema.marks;\n    const { destination, title } = node as LinkMdNode;\n\n    if (entering) {\n      const attrs = {\n        linkUrl: destination,\n        title,\n        ...customAttrs,\n      };\n\n      state.openMark(link.create(attrs));\n    } else {\n      state.closeMark(link);\n    }\n  },\n\n  softbreak(state, node) {\n    if (node.parent!.type === 'paragraph') {\n      const { prev, next } = node;\n\n      if (prev && !isBRTag(prev)) {\n        state.closeNode();\n      }\n\n      if (next && !isBRTag(next)) {\n        state.openNode(state.schema.nodes.paragraph);\n      }\n    }\n  },\n\n  // GFM specifications node\n  table(state, _, { entering }, customAttrs) {\n    if (entering) {\n      state.openNode(state.schema.nodes.table, customAttrs);\n    } else {\n      state.closeNode();\n    }\n  },\n\n  tableHead(state, _, { entering }, customAttrs) {\n    if (entering) {\n      state.openNode(state.schema.nodes.tableHead, customAttrs);\n    } else {\n      state.closeNode();\n    }\n  },\n\n  tableBody(state, _, { entering }, customAttrs) {\n    if (entering) {\n      state.openNode(state.schema.nodes.tableBody, customAttrs);\n    } else {\n      state.closeNode();\n    }\n  },\n\n  tableRow(state, _, { entering }, customAttrs) {\n    if (entering) {\n      state.openNode(state.schema.nodes.tableRow, customAttrs);\n    } else {\n      state.closeNode();\n    }\n  },\n\n  tableCell(state, node, { entering }) {\n    if (!(node as TableCellMdNode).ignored) {\n      const hasParaNode = (childNode: MdNode | null) =>\n        childNode && (isInlineNode(childNode) || isCustomHTMLInlineNode(state, childNode));\n\n      if (entering) {\n        const { tableHeadCell, tableBodyCell, paragraph } = state.schema.nodes;\n        const tablePart = node.parent!.parent!;\n        const cell = tablePart.type === 'tableHead' ? tableHeadCell : tableBodyCell;\n\n        const table = tablePart.parent as TableMdNode;\n        const { align } = table.columns[(node as TableCellMdNode).startIdx] || {};\n        const attrs: Record<string, string | number> = { ...(node as TableCellMdNode).attrs };\n\n        if (align) {\n          attrs.align = align;\n        }\n\n        state.openNode(cell, attrs);\n\n        if (hasParaNode(node.firstChild)) {\n          state.openNode(paragraph);\n        }\n      } else {\n        if (hasParaNode(node.lastChild)) {\n          state.closeNode();\n        }\n        state.closeNode();\n      }\n    }\n  },\n\n  strike(state, _, { entering }, customAttrs) {\n    const { strike } = state.schema.marks;\n\n    if (entering) {\n      state.openMark(strike.create(customAttrs));\n    } else {\n      state.closeMark(strike);\n    }\n  },\n\n  code(state, node, _, customAttrs) {\n    const { code } = state.schema.marks;\n\n    state.openMark(code.create(customAttrs));\n    state.addText(getTextWithoutTrailingNewline(node.literal || ''));\n    state.closeMark(code);\n  },\n\n  customBlock(state, node) {\n    const { customBlock, paragraph } = state.schema.nodes;\n    const { info, literal } = node as CustomBlockMdNode;\n\n    state.openNode(customBlock, { info });\n    state.addText(getTextWithoutTrailingNewline(literal || ''));\n    state.closeNode();\n    // add empty line to edit the content in next line\n    if (!node.next) {\n      state.openNode(paragraph);\n      state.closeNode();\n    }\n  },\n\n  frontMatter(state, node) {\n    state.openNode(state.schema.nodes.frontMatter);\n    state.addText(node.literal!);\n    state.closeNode();\n  },\n\n  htmlInline(state, node) {\n    const html = node.literal!;\n    const matched = html.match(reHTMLTag)!;\n    const [, openTagName, , closeTagName] = matched;\n    const typeName = (openTagName || closeTagName).toLowerCase();\n    const markType = state.schema.marks[typeName];\n    const sanitizedHTML = sanitizeHTML(html);\n\n    // for user defined html schema\n    if (markType?.spec.attrs!.htmlInline) {\n      if (openTagName) {\n        const htmlAttrs = getHTMLAttrsByHTMLString(sanitizedHTML);\n\n        state.openMark(markType.create({ htmlAttrs }));\n      } else {\n        state.closeMark(markType);\n      }\n    } else {\n      const htmlToWwConvertor = htmlToWwConvertors[typeName];\n\n      if (htmlToWwConvertor) {\n        htmlToWwConvertor(state, node, openTagName);\n      }\n    }\n  },\n\n  htmlBlock(state, node) {\n    const html = node.literal!;\n    const container = document.createElement('div');\n    const isHTMLComment = reHTMLComment.test(html);\n\n    if (isHTMLComment) {\n      state.openNode(state.schema.nodes.htmlComment);\n      state.addText(node.literal!);\n      state.closeNode();\n    } else {\n      const matched = html.match(reHTMLTag)!;\n      const [, openTagName, , closeTagName] = matched;\n\n      const typeName = (openTagName || closeTagName).toLowerCase();\n      const nodeType = state.schema.nodes[typeName];\n      const sanitizedHTML = sanitizeHTML(html);\n\n      // for user defined html schema\n      if (nodeType?.spec.attrs!.htmlBlock) {\n        const htmlAttrs = getHTMLAttrsByHTMLString(sanitizedHTML);\n        const childrenHTML = getChildrenHTML(node, typeName);\n\n        state.addNode(nodeType, { htmlAttrs, childrenHTML });\n      } else {\n        container.innerHTML = sanitizedHTML;\n        addRawHTMLAttributeToDOM(container);\n\n        state.convertByDOMParser(container as HTMLElement);\n      }\n    }\n  },\n\n  customInline(state, node, { entering, skipChildren }) {\n    const { info, firstChild } = node as CustomInlineMdNode;\n    const { schema } = state;\n\n    if (info.indexOf('widget') !== -1 && entering) {\n      const content = getWidgetContent(node as CustomInlineMdNode);\n\n      skipChildren();\n\n      state.addNode(schema.nodes.widget, { info }, [\n        schema.text(createWidgetContent(info, content)),\n      ]);\n    } else {\n      let text = '$$';\n\n      if (entering) {\n        text += firstChild ? `${info} ` : info;\n      }\n\n      state.addText(text);\n    }\n  },\n};\n\nexport function createWwConvertors(customConvertors: HTMLConvertorMap) {\n  const customConvertorTypes = Object.keys(customConvertors);\n  const convertors = { ...toWwConvertors };\n  const renderer = new Renderer({\n    gfm: true,\n    nodeId: true,\n    convertors: customConvertors,\n  });\n  const orgConvertors = renderer.getConvertors();\n\n  customConvertorTypes.forEach((type) => {\n    const wwConvertor = toWwConvertors[type];\n\n    if (wwConvertor && !includes(['htmlBlock', 'htmlInline'], type)) {\n      convertors[type] = (state, node, context) => {\n        context.origin = () => orgConvertors[type]!(node, context, orgConvertors);\n        const tokens = customConvertors[type]!(node, context) as OpenTagToken;\n        let attrs;\n\n        if (tokens) {\n          const { attributes: htmlAttrs, classNames } = Array.isArray(tokens) ? tokens[0] : tokens;\n\n          attrs = { htmlAttrs, classNames };\n        }\n\n        wwConvertor(state, node, context, attrs);\n      };\n    }\n  });\n\n  return convertors;\n}\n"
  },
  {
    "path": "apps/editor/src/css/contents.css",
    "content": "/* \n  z-index basis\n  -1: pseudo element\n  20 - preview, wysiwyg\n  30 - wysiwyg code block language editor, popup, context menu\n  40 - tooltip\n*/\n.ProseMirror {\n  font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', 'Arial', '나눔바른고딕',\n    'Nanum Barun Gothic', '맑은고딕', 'Malgun Gothic', sans-serif;\n  color: #222;\n  font-size: 13px;\n  overflow-y: auto;\n  overflow-X: hidden;\n  height: calc(100% - 36px);\n}\n\n.ProseMirror .placeholder {\n  color: #999;\n}\n\n.ProseMirror:focus {\n  outline: none;\n}\n\n.ProseMirror-selectednode {\n  outline: none;\n}\n\ntable.ProseMirror-selectednode {\n  border-radius: 2px;\n  outline: 2px solid #00a9ff;\n}\n\n.html-block.ProseMirror-selectednode {\n  border-radius: 2px;\n  outline: 2px solid #00a9ff;\n}\n\n.toastui-editor-contents {\n  margin: 0;\n  padding: 0;\n  font-size: 13px;\n  font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', 'Arial', '나눔바른고딕',\n    'Nanum Barun Gothic', '맑은고딕', 'Malgun Gothic', sans-serif;\n  z-index: 20;\n}\n\n.toastui-editor-contents *:not(table) {\n  line-height: 160%;\n  box-sizing: content-box;\n}\n\n.toastui-editor-contents i,\n.toastui-editor-contents cite,\n.toastui-editor-contents em,\n.toastui-editor-contents var,\n.toastui-editor-contents address,\n.toastui-editor-contents dfn {\n  font-style: italic;\n}\n\n.toastui-editor-contents strong {\n  font-weight: bold;\n}\n\n.toastui-editor-contents p {\n  margin: 10px 0;\n  color: #222;\n}\n\n.toastui-editor-contents > h1:first-of-type,\n.toastui-editor-contents > div > div:first-of-type h1 {\n  margin-top: 14px;\n}\n\n.toastui-editor-contents h1,\n.toastui-editor-contents h2,\n.toastui-editor-contents h3,\n.toastui-editor-contents h4,\n.toastui-editor-contents h5,\n.toastui-editor-contents h6 {\n  font-weight: bold;\n  color: #222;\n}\n\n.toastui-editor-contents h1 {\n  font-size: 24px;\n  line-height: 28px;\n  border-bottom: 3px double #999;\n  margin: 52px 0 15px 0;\n  padding-bottom: 7px;\n}\n\n.toastui-editor-contents h2 {\n  font-size: 22px;\n  line-height: 23px;\n  border-bottom: 1px solid #dbdbdb;\n  margin: 20px 0 13px 0;\n  padding-bottom: 7px;\n}\n\n.toastui-editor-contents h3 {\n  font-size: 20px;\n  margin: 18px 0 2px;\n}\n\n.toastui-editor-contents h4 {\n  font-size: 18px;\n  margin: 10px 0 2px;\n}\n\n.toastui-editor-contents h3,\n.toastui-editor-contents h4 {\n  line-height: 18px;\n}\n\n.toastui-editor-contents h5 {\n  font-size: 16px;\n}\n\n.toastui-editor-contents h6 {\n  font-size: 14px;\n}\n\n.toastui-editor-contents h5,\n.toastui-editor-contents h6 {\n  line-height: 17px;\n  margin: 9px 0 -4px;\n}\n\n.toastui-editor-contents del {\n  color: #999;\n}\n\n.toastui-editor-contents blockquote {\n  margin: 14px 0;\n  border-left: 4px solid #e5e5e5;\n  padding: 0 16px;\n  color: #999;\n}\n\n.toastui-editor-contents blockquote p,\n.toastui-editor-contents blockquote ul,\n.toastui-editor-contents blockquote ol {\n  color: #999;\n}\n\n.toastui-editor-contents blockquote > :first-child {\n  margin-top: 0;\n}\n\n.toastui-editor-contents blockquote > :last-child {\n  margin-bottom: 0;\n}\n\n.toastui-editor-contents pre,\n.toastui-editor-contents code {\n  font-family: Consolas, Courier, 'Apple SD 산돌고딕 Neo', -apple-system, 'Lucida Grande',\n    'Apple SD Gothic Neo', '맑은 고딕', 'Malgun Gothic', 'Segoe UI', '돋움', dotum, sans-serif;\n  border: 0;\n  border-radius: 0;\n}\n\n.toastui-editor-contents pre {\n  margin: 2px 0 8px;\n  padding: 18px;\n  background-color: #f4f7f8;\n}\n\n.toastui-editor-contents code {\n  color: #c1798b;\n  background-color: #f9f2f4;\n  padding: 2px 3px;\n  letter-spacing: -0.3px;\n  border-radius: 2px;\n}\n\n.toastui-editor-contents pre code {\n  padding: 0;\n  color: inherit;\n  white-space: pre-wrap;\n  background-color: transparent;\n}\n\n.toastui-editor-contents img {\n  margin: 4px 0 10px;\n  box-sizing: border-box;\n  vertical-align: top;\n  max-width: 100%;\n}\n\n.toastui-editor-contents table {\n  border: 1px solid rgba(0, 0, 0, 0.1);\n  margin: 12px 0 14px;\n  color: #222;\n  width: auto;\n  border-collapse: collapse;\n  box-sizing: border-box;\n}\n\n.toastui-editor-contents table th,\n.toastui-editor-contents table td {\n  border: 1px solid rgba(0, 0, 0, 0.1);\n  padding: 5px 14px 5px 12px;\n  height: 32px;\n}\n\n.toastui-editor-contents table th {\n  background-color: #555;\n  font-weight: 300;\n  color: #fff;\n  padding-top: 6px;\n}\n\n.toastui-editor-contents th p {\n  margin: 0;\n  color: #fff;\n}\n\n.toastui-editor-contents td p {\n  margin: 0;\n  padding: 0 2px;\n}\n\n.toastui-editor-contents td.toastui-editor-cell-selected {\n  background-color: #d8dfec;\n}\n\n.toastui-editor-contents th.toastui-editor-cell-selected {\n  background-color: #908f8f;\n}\n\n.toastui-editor-contents ul,\n.toastui-editor-contents menu,\n.toastui-editor-contents ol,\n.toastui-editor-contents dir {\n  display: block;\n  list-style-type: none;\n  padding-left: 24px;\n  margin: 6px 0 10px;\n  color: #222;\n}\n\n.toastui-editor-contents ol {\n  list-style-type: none;\n  counter-reset: li;\n}\n\n.toastui-editor-contents ol > li {\n  counter-increment: li;\n}\n\n.toastui-editor-contents ul > li::before,\n.toastui-editor-contents ol > li::before {\n  display: inline-block;\n  position: absolute;\n}\n\n.toastui-editor-contents ul > li::before {\n  content: '';\n  margin-top: 6px;\n  margin-left: -17px;\n  width: 5px;\n  height: 5px;\n  border-radius: 50%;\n  background-color: #ccc;\n}\n\n.toastui-editor-contents ol > li::before {\n  content: '.' counter(li);\n  margin-left: -28px;\n  width: 24px;\n  text-align: right;\n  direction: rtl;\n  color: #aaa;\n}\n\n.toastui-editor-contents ul ul,\n.toastui-editor-contents ul ol,\n.toastui-editor-contents ol ol,\n.toastui-editor-contents ol ul {\n  margin-top: 0 !important;\n  margin-bottom: 0 !important;\n}\n\n.toastui-editor-contents ul li,\n.toastui-editor-contents ol li {\n  position: relative;\n}\n\n.toastui-editor-contents ul p,\n.toastui-editor-contents ol p {\n  margin: 0;\n}\n\n.toastui-editor-contents hr {\n  border-top: 1px solid #eee;\n  margin: 16px 0;\n}\n\n.toastui-editor-contents a {\n  text-decoration: underline;\n  color: #4b96e6;\n}\n\n.toastui-editor-contents a:hover {\n  color: #1f70de;\n}\n\n.toastui-editor-contents .image-link {\n  position: relative;\n}\n\n.toastui-editor-contents .image-link:hover::before {\n  content: '';\n  position: absolute;\n  width: 30px;\n  height: 30px;\n  right: 0px;\n  border-radius: 50%;\n  border: 1px solid #c9ccd5;\n  background: #fff url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgdmlld0JveD0iMCAwIDIwIDIwIj4KICAgIDxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIj4KICAgICAgICA8ZyBzdHJva2U9IiM1NTUiIHN0cm9rZS13aWR0aD0iMS41Ij4KICAgICAgICAgICAgPGc+CiAgICAgICAgICAgICAgICA8Zz4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNNy42NjUgMTUuMDdsLTEuODE5LS4wMDJjLTEuNDg2IDAtMi42OTItMS4yMjgtMi42OTItMi43NDR2LS4xOTJjMC0xLjUxNSAxLjIwNi0yLjc0NCAyLjY5Mi0yLjc0NGgzLjg0NmMxLjQ4NyAwIDIuNjkyIDEuMjI5IDIuNjkyIDIuNzQ0di4xOTIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMDAwIC00NTgxKSB0cmFuc2xhdGUoOTk1IDQ1NzYpIHRyYW5zbGF0ZSg1IDUpIHNjYWxlKDEgLTEpIHJvdGF0ZSg0NSAzNy4yOTMgMCkiLz4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTIuMzI2IDQuOTM0bDEuODIyLjAwMmMxLjQ4NyAwIDIuNjkzIDEuMjI4IDIuNjkzIDIuNzQ0di4xOTJjMCAxLjUxNS0xLjIwNiAyLjc0NC0yLjY5MyAyLjc0NGgtMy44NDVjLTEuNDg3IDAtMi42OTItMS4yMjktMi42OTItMi43NDRWNy42OCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTEwMDAgLTQ1ODEpIHRyYW5zbGF0ZSg5OTUgNDU3NikgdHJhbnNsYXRlKDUgNSkgc2NhbGUoMSAtMSkgcm90YXRlKDQ1IDMwLjk5NiAwKSIvPgogICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICA8L2c+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4K') no-repeat;\n  background-position: center;\n  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.08);\n  cursor: pointer;\n}\n\n.toastui-editor-contents .task-list-item {\n  border: 0;\n  list-style: none;\n  padding-left: 24px;\n  margin-left: -24px;\n}\n\n.toastui-editor-contents .task-list-item::before {\n  background-repeat: no-repeat;\n  background-size: 18px 18px;\n  background-position: center;\n  content: '';\n  margin-left: 0;\n  margin-top: 0;\n  border-radius: 2px;\n  height: 18px;\n  width: 18px;\n  position: absolute;\n  left: 0;\n  top: 1px;\n  cursor: pointer;\n  background: transparent url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxOCIgdmlld0JveD0iMCAwIDE4IDE4Ij4KICAgIDxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgZmlsbD0iI0ZGRiIgc3Ryb2tlPSIjQ0NDIj4KICAgICAgICAgICAgPGc+CiAgICAgICAgICAgICAgICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTAzMCAtMjk2KSB0cmFuc2xhdGUoNzg4IDE5MikgdHJhbnNsYXRlKDI0MiAxMDQpIj4KICAgICAgICAgICAgICAgICAgICA8cmVjdCB3aWR0aD0iMTciIGhlaWdodD0iMTciIHg9Ii41IiB5PSIuNSIgcng9IjIiLz4KICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+Cg==');\n}\n\n.toastui-editor-contents .task-list-item.checked::before {\n  background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxOCIgdmlld0JveD0iMCAwIDE4IDE4Ij4KICAgIDxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgZmlsbD0iIzRCOTZFNiI+CiAgICAgICAgICAgIDxnPgogICAgICAgICAgICAgICAgPGc+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTE2IDBjMS4xMDUgMCAyIC44OTUgMiAydjE0YzAgMS4xMDUtLjg5NSAyLTIgMkgyYy0xLjEwNSAwLTItLjg5NS0yLTJWMkMwIC44OTUuODk1IDAgMiAwaDE0em0tMS43OTMgNS4yOTNjLS4zOS0uMzktMS4wMjQtLjM5LTEuNDE0IDBMNy41IDEwLjU4NSA1LjIwNyA4LjI5M2wtLjA5NC0uMDgzYy0uMzkyLS4zMDUtLjk2LS4yNzgtMS4zMi4wODMtLjM5LjM5LS4zOSAxLjAyNCAwIDEuNDE0bDMgMyAuMDk0LjA4M2MuMzkyLjMwNS45Ni4yNzggMS4zMi0uMDgzbDYtNiAuMDgzLS4wOTRjLjMwNS0uMzkyLjI3OC0uOTYtLjA4My0xLjMyeiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTEwNTAgLTI5NikgdHJhbnNsYXRlKDc4OCAxOTIpIHRyYW5zbGF0ZSgyNjIgMTA0KSIvPgogICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICA8L2c+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4K');\n}\n\n.toastui-editor-custom-block .toastui-editor-custom-block-editor {\n  background: #f9f7fd;\n  color: #452d6b;\n  border: solid 1px #dbd4ea;\n}\n\n.toastui-editor-custom-block .toastui-editor-custom-block-view {\n  position: relative;\n  padding: 9px 13px 8px 12px;\n}\n\n.toastui-editor-custom-block.ProseMirror-selectednode .toastui-editor-custom-block-view {\n  border: solid 1px #dbd4ea;\n  border-radius: 2px;\n}\n\n.toastui-editor-custom-block .toastui-editor-custom-block-view .tool {\n  position: absolute;\n  right: 10px;\n  top: 7px;\n  display: none;\n}\n\n.toastui-editor-custom-block.ProseMirror-selectednode .toastui-editor-custom-block-view .tool {\n  display: block;\n}\n\n.toastui-editor-custom-block-view button {\n  vertical-align: middle;\n  width: 15px;\n  height: 15px;\n  margin-left: 8px;\n  padding: 3px;\n  border: solid 1px #cccccc;\n  background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI1LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IuugiOydtOyWtF8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiCgkgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMzAgMzAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMwIDMwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGwtcnVsZTpldmVub2RkO2NsaXAtcnVsZTpldmVub2RkO2ZpbGw6IzU1NTU1NTt9Cjwvc3R5bGU+CjxnPgoJPGc+CgkJPGc+CgkJCTxnPgoJCQkJPGc+CgkJCQkJPHBhdGggY2xhc3M9InN0MCIgZD0iTTE1LjUsMTIuNWwyLDJMMTIsMjBoLTJ2LTJMMTUuNSwxMi41eiBNMTgsMTBsMiwybC0xLjUsMS41bC0yLTJMMTgsMTB6Ii8+CgkJCQk8L2c+CgkJCTwvZz4KCQk8L2c+Cgk8L2c+CjwvZz4KPC9zdmc+Cg==')\n    no-repeat;\n  background-position: center;\n  background-size: 30px 30px;\n}\n\n.toastui-editor-custom-block-view .info {\n  font-size: 13px;\n  font-weight: bold;\n  color: #5200d0;\n  vertical-align: middle;\n}\n\n.toastui-editor-contents .toastui-editor-ww-code-block {\n  position: relative;\n}\n\n.toastui-editor-contents .toastui-editor-ww-code-block:after {\n  content: attr(data-language);\n  position: absolute;\n  display: inline-block;\n  top: 10px;\n  right: 10px;\n  height: 24px;\n  padding: 3px 35px 0 10px;\n  font-weight: bold;\n  font-size: 13px;\n  color: #333;\n  background: #e5e9ea url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI1LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IuugiOydtOyWtF8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiCgkgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMzAgMzAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMwIDMwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGwtcnVsZTpldmVub2RkO2NsaXAtcnVsZTpldmVub2RkO2ZpbGw6IzU1NTU1NTt9Cjwvc3R5bGU+CjxnPgoJPGc+CgkJPGc+CgkJCTxnPgoJCQkJPGc+CgkJCQkJPHBhdGggY2xhc3M9InN0MCIgZD0iTTE1LjUsMTIuNWwyLDJMMTIsMjBoLTJ2LTJMMTUuNSwxMi41eiBNMTgsMTBsMiwybC0xLjUsMS41bC0yLTJMMTgsMTB6Ii8+CgkJCQk8L2c+CgkJCTwvZz4KCQk8L2c+Cgk8L2c+CjwvZz4KPC9zdmc+Cg==') no-repeat;\n  background-position: right;\n  border-radius: 2px;\n  background-size: 30px 30px;\n  cursor: pointer;\n}\n\n.toastui-editor-ww-code-block-language {\n  position: fixed;\n  display: inline-block;\n  width: 100px;\n  height: 27px;\n  right: 35px;\n  border: 1px solid #ccc;\n  border-radius: 2px;\n  background-color: #fff;\n  z-index: 30;\n}\n\n.toastui-editor-ww-code-block-language input {\n  box-sizing: border-box;\n  margin: 0;\n  padding: 0 10px;\n  height: 100%;\n  width: 100%;\n  background-color: transparent;\n  border: none;\n  outline: none;\n}\n\n.toastui-editor-contents-placeholder::before {\n  content: attr(data-placeholder);\n  color: grey;\n  line-height: 160%;\n  position: absolute;\n}\n\n.toastui-editor-md-preview .toastui-editor-contents h1 {\n  min-height: 28px;\n}\n\n.toastui-editor-md-preview .toastui-editor-contents h2 {\n  min-height: 23px;\n}\n\n.toastui-editor-md-preview .toastui-editor-contents blockquote {\n  min-height: 20px;\n}\n\n.toastui-editor-md-preview .toastui-editor-contents li {\n  min-height: 22px;\n}\n\n.toastui-editor-pseudo-clipboard {\n  position: fixed;\n  opacity: 0;\n  width: 0;\n  height: 0;\n  left: -1000px;\n  top: -1000px;\n  z-index: -1;\n}\n"
  },
  {
    "path": "apps/editor/src/css/editor.css",
    "content": "/* height */\n.auto-height,\n.auto-height .toastui-editor-defaultUI {\n  height: auto;\n}\n\n.auto-height .toastui-editor-md-container {\n  position: relative;\n}\n\n:not(.auto-height) > .toastui-editor-defaultUI,\n:not(.auto-height) > .toastui-editor-defaultUI > .toastui-editor-main {\n  display: -ms-flexbox;\n  display: flex;\n  -ms-flex-direction: column;\n  flex-direction: column;\n}\n\n:not(.auto-height) > .toastui-editor-defaultUI > .toastui-editor-main {\n  -ms-flex: 1;\n  flex: 1;\n}\n\n/* toastui editor */\n.toastui-editor-md-container::after,\n.toastui-editor-defaultUI-toolbar::after {\n  content: '';\n  display: block;\n  height: 0;\n  clear: both;\n}\n\n\n.toastui-editor-main {\n  min-height: 0px;\n  position: relative;\n  height: inherit;\n  box-sizing: border-box;\n}\n\n.toastui-editor-md-container {\n  display: none;\n  overflow: hidden;\n  height: 100%;\n}\n\n.toastui-editor-md-container .toastui-editor {\n  line-height: 1.5;\n  position: relative;\n}\n\n.toastui-editor-md-container .toastui-editor,\n.toastui-editor-md-container .toastui-editor-md-preview {\n  box-sizing: border-box;\n  padding: 0;\n  height: inherit;\n}\n\n.toastui-editor-md-container .toastui-editor-md-preview {\n  overflow: auto;\n  padding: 0 25px;\n  height: 100%;\n}\n\n.toastui-editor-md-container .toastui-editor-md-preview > p:first-child {\n  margin-top: 0 !important;\n}\n\n.toastui-editor-md-container .toastui-editor-md-preview .toastui-editor-contents {\n  padding-top: 8px;\n}\n\n.toastui-editor-main .toastui-editor-md-tab-style > .toastui-editor,\n.toastui-editor-main .toastui-editor-md-tab-style > .toastui-editor-md-preview {\n  width: 100%;\n  display: none;\n}\n\n.toastui-editor-main .toastui-editor-md-tab-style > .active {\n  display: block;\n}\n\n.toastui-editor-main .toastui-editor-md-vertical-style > .toastui-editor-tabs {\n  display: none;\n}\n\n.toastui-editor-main .toastui-editor-md-tab-style > .toastui-editor-tabs {\n  display: block;\n}\n\n.toastui-editor-main .toastui-editor-md-vertical-style .toastui-editor {\n  width: 50%;\n}\n\n.toastui-editor-main .toastui-editor-md-vertical-style .toastui-editor-md-preview {\n  width: 50%;\n}\n\n.toastui-editor-main .toastui-editor-md-splitter {\n  display: none;\n  height: 100%;\n  width: 1px;\n  background-color: #ebedf2;\n  position: absolute;\n  left: 50%;\n}\n\n.toastui-editor-main .toastui-editor-md-vertical-style .toastui-editor-md-splitter {\n  display: block;\n}\n\n.toastui-editor-ww-container {\n  display: none;\n  overflow: hidden;\n  height: inherit;\n  background-color: #fff;\n}\n\n.auto-height .toastui-editor-main-container {\n  position: relative;\n}\n\n.toastui-editor-main-container {\n  position: absolute;\n  line-height: 1;\n  color: #222;\n  width: 100%;\n  height: inherit;\n}\n\n.toastui-editor-ww-container > .toastui-editor {\n  height: inherit;\n  position: relative;\n  width: 100%;\n}\n\n.toastui-editor-ww-container .toastui-editor-contents {\n  overflow: auto;\n  box-sizing: border-box;\n  margin: 0px;\n  padding: 16px 25px 0px 25px;\n  height: inherit;\n}\n\n.toastui-editor-ww-container .toastui-editor-contents p {\n  margin: 0;\n}\n\n.toastui-editor-md-mode .toastui-editor-md-container,\n.toastui-editor-ww-mode .toastui-editor-ww-container {\n  display: block;\n  z-index: 20;\n}\n\n.toastui-editor-md-mode .toastui-editor-md-vertical-style {\n  display: -ms-flexbox;\n  display: flex;\n}\n\n.toastui-editor-main.hidden,\n.toastui-editor-defaultUI.hidden {\n  display: none;\n}\n\n/* default UI Styles */\n.toastui-editor-defaultUI .ProseMirror {\n  padding: 18px 25px;\n}\n\n.toastui-editor-defaultUI {\n  position: relative;\n  border: 1px solid #dadde6;\n  height: 100%;\n  font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', 'Arial', '나눔바른고딕',\n    'Nanum Barun Gothic', '맑은고딕', 'Malgun Gothic', sans-serif;\n  border-radius: 4px;\n}\n\n.toastui-editor-defaultUI button {\n  color: #333;\n  height: 28px;\n  font-size: 13px;\n  cursor: pointer;\n  border: none;\n  border-radius: 2px;\n}\n\n.toastui-editor-defaultUI .toastui-editor-ok-button {\n  min-width: 63px;\n  height: 32px;\n  background-color: #00a9ff;\n  color: #fff;\n  outline-color: #009bf2;\n}\n\n.toastui-editor-defaultUI .toastui-editor-ok-button:hover {\n  background-color: #009bf2;\n}\n\n.toastui-editor-defaultUI .toastui-editor-close-button {\n  min-width: 63px;\n  height: 32px;\n  background-color: #f7f9fc;\n  border: 1px solid #dadde6;\n  margin-right: 5px;\n  outline-color: #cbcfdb;\n}\n\n.toastui-editor-defaultUI .toastui-editor-close-button:hover {\n  border-color: #cbcfdb;\n}\n\n/* mode switch tab */\n.toastui-editor-mode-switch {\n  background-color: #fff;\n  border-top: 1px solid #dadde6;\n  font-size: 12px;\n  text-align: right;\n  height: 28px;\n  padding-right: 10px;\n  border-radius: 0 0 3px 3px;\n}\n\n.toastui-editor-mode-switch .tab-item {\n  display: inline-block;\n  width: 96px;\n  height: 24px;\n  line-height: 24px;\n  text-align: center;\n  background: #f7f9fc;\n  color: #969aa5;\n  margin-top: -1px;\n  margin-right: -1px;\n  cursor: pointer;\n  border: 1px solid #dadde6;\n  border-radius: 0 0 4px 4px;\n  font-weight: 500;\n  box-sizing: border-box;\n}\n\n.toastui-editor-mode-switch .tab-item.active {\n  border-top: 1px solid #fff;\n  background-color: #fff;\n  color: #555;\n}\n\n/* markdown tab */\n.toastui-editor-defaultUI .toastui-editor-md-tab-container {\n  float: left;\n  height: 45px;\n  font-size: 13px;\n  background: #f7f9fc;\n  border-bottom: 1px solid #ebedf2;\n  border-top-left-radius: 3px;\n}\n\n.toastui-editor-md-tab-container .toastui-editor-tabs {\n  margin-left: 15px;\n  height: 100%;\n}\n\n.toastui-editor-md-tab-container .tab-item {\n  display: inline-block;\n  width: 70px;\n  height: 33px;\n  line-height: 33px;\n  font-size: 12px;\n  font-weight: 500;\n  text-align: center;\n  background: #eaedf1;\n  color: #969aa5;\n  cursor: pointer;\n  border: 1px solid #dadde6;\n  border-radius: 4px 4px 0 0;\n  box-sizing: border-box;\n  margin-top: 13px;\n}\n\n.toastui-editor-md-tab-container .tab-item.active {\n  border-bottom: 1px solid #fff;\n  background-color: #fff;\n  color: #555;\n}\n\n.toastui-editor-md-tab-container .tab-item:last-child {\n  margin-left: -1px;\n}\n\n/* toolbar */\n.toastui-editor-defaultUI-toolbar {\n  display: -ms-flexbox;\n  display: flex;\n  padding: 0 25px;\n  height: 45px;\n  background-color: #f7f9fc;\n  border-bottom: 1px solid #ebedf2;\n  border-radius: 3px 3px 0 0;\n}\n\n.toastui-editor-toolbar {\n  height: 46px;\n  box-sizing: border-box;\n}\n\n.toastui-editor-toolbar-divider {\n  display: inline-block;\n  width: 1px;\n  height: 18px;\n  background-color: #e1e3e9;\n  margin: 14px 12px;\n}\n\n.toastui-editor-toolbar-group {\n  display: -ms-flexbox;\n  display: flex;\n}\n\n.toastui-editor-defaultUI-toolbar button {\n  box-sizing: border-box;\n  cursor: pointer;\n  width: 32px;\n  height: 32px;\n  padding: 0;\n  border-radius: 3px;\n  margin: 7px 5px;\n  border: 1px solid #f7f9fc;\n}\n\n.toastui-editor-defaultUI-toolbar button:not(:disabled):hover {\n  border: 1px solid #e4e7ee;\n  background-color: #fff;\n}\n\n.toastui-editor-defaultUI-toolbar .scroll-sync {\n  display: inline-block;\n  position: relative;\n  width: 70px;\n  height: 10px;\n  text-align: center;\n  line-height: 10px;\n  color: #81858f;\n  cursor: pointer;\n}\n\n.toastui-editor-defaultUI-toolbar .scroll-sync::before {\n  content: 'Scroll';\n  position: absolute;\n  left: 0;\n  font-size: 14px;\n}\n\n.toastui-editor-defaultUI-toolbar .scroll-sync.active::before {\n  color: #00a9ff;\n}\n\n.toastui-editor-defaultUI-toolbar .scroll-sync input {\n  opacity: 0;\n  width: 0;\n  height: 0;\n}\n\n.toastui-editor-defaultUI-toolbar .switch {\n  position: absolute;\n  top: 0;\n  left: 45px;\n  right: 0;\n  bottom: 0;\n  background-color: #d6d8de;\n  -webkit-transition: .4s;\n  transition: .4s;\n  border-radius: 50px;\n}\n\n.toastui-editor-defaultUI-toolbar input:checked + .switch {\n  background-color: #acddfa;\n}\n\n.toastui-editor-defaultUI-toolbar .switch::before {\n  position: absolute;\n  content: '';\n  height: 14px;\n  width: 14px;\n  left: 0px;\n  bottom: -2px;\n  background-color: #94979f;\n  -webkit-transition: .4s;\n  transition: .4s;\n  border-radius: 50%;\n}\n\n.toastui-editor-defaultUI-toolbar input:checked + .switch::before {\n  background-color: #00a9ff;\n  -webkit-transform: translateX(12px);\n  -moz-transform: translateX(12px);\n  -ms-transform: translateX(12px);\n  transform: translateX(12px);\n}\n\n.toastui-editor-dropdown-toolbar .scroll-sync {\n  margin: 0 5px;\n}\n\n.toastui-editor-dropdown-toolbar {\n  position: absolute;\n  height: 46px;\n  z-index: 30;\n  border-radius: 2px;\n  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.08);\n  border: 1px solid #dadde6;\n  background-color: #f7f9fc;\n  display: -ms-flexbox;\n  display: flex;\n}\n\n.toastui-editor-toolbar-item-wrapper {\n  margin: 7px 5px;\n  height: 32px;\n  line-height: 32px;\n}\n\n/* toolbar popup */\n.toastui-editor-popup {\n  width: 400px;\n  margin-right: auto;\n  background: #fff;\n  z-index: 30;\n  position: absolute;\n  border-radius: 2px;\n  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.08);\n  border: 1px solid #dadde6;\n}\n\n.toastui-editor-popup-body {\n  padding: 15px;\n  font-size: 12px;\n}\n\n.toastui-editor-popup-body label {\n  font-weight: 600;\n  color: #555;\n  display: block;\n  margin: 20px 0 5px;\n}\n\n.toastui-editor-popup-body .toastui-editor-button-container {\n  text-align: right;\n  margin-top: 20px;\n}\n\n.toastui-editor-popup-body input[type='text'] {\n  width: calc(100% - 26px);\n  height: 30px;\n  padding: 0 12px;\n  border-radius: 2px;\n  border: 1px solid #e1e3e9;\n  color: #333;\n}\n\n.toastui-editor-popup-body input[type='text']:focus {\n  outline: 1px solid #00a9ff;\n  border-color: transparent;\n}\n\n.toastui-editor-popup-body input[type='text'].disabled {\n  background-color: #f7f9fc;\n  border-color: #e1e3e9;\n  color: #969aa5;\n}\n\n.toastui-editor-popup-body input[type='file'] {\n  opacity: 0;\n  border: none;\n  width: 1px;\n  height: 1px;\n  position: absolute;\n  top: 0;\n  left: 0;\n}\n\n.toastui-editor-popup-body input.wrong,\n.toastui-editor-popup-body span.wrong {\n  border-color: #fa2828;\n}\n\n.toastui-editor-popup-add-link .toastui-editor-popup-body,\n.toastui-editor-popup-add-image .toastui-editor-popup-body {\n  padding: 0 20px 20px;\n}\n\n.toastui-editor-popup-add-image .toastui-editor-tabs {\n  margin: 5px 0 10px;\n}\n\n.toastui-editor-popup-add-image .toastui-editor-tabs .tab-item {\n  display: inline-block;\n  width: 60px;\n  height: 40px;\n  line-height: 40px;\n  border-bottom: 1px solid #dadde6;\n  color: #333;\n  font-size: 13px;\n  font-weight: 600;\n  text-align: center;\n  cursor: pointer;\n  box-sizing: border-box;\n}\n\n.toastui-editor-popup-add-image .toastui-editor-tabs .tab-item:hover {\n  border-bottom: 1px solid #cbcfdb;\n}\n\n.toastui-editor-popup-add-image .toastui-editor-tabs .tab-item.active {\n  color: #00a9ff;\n  border-bottom: 2px solid #00a9ff;\n}\n\n.toastui-editor-popup-add-image .toastui-editor-file-name {\n  width: 58%;\n  display: inline-block;\n  border-radius: 2px;\n  border: 1px solid #e1e3e9;\n  color: #dadde6;\n  height: 30px;\n  line-height: 30px;\n  padding: 0 12px;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  cursor: pointer;\n}\n\n.toastui-editor-popup-add-image .toastui-editor-file-name.has-file {\n  color: #333;\n}\n\n.toastui-editor-popup-add-image .toastui-editor-file-select-button {\n  width: 33%;\n  margin-left: 5px;\n  height: 32px;\n  border-radius: 2px;\n  border: 1px solid #dadde6;\n  background-color: #f7f9fc;\n  vertical-align: top;\n}\n\n.toastui-editor-popup-add-image .toastui-editor-file-select-button:hover {\n  border-color: #cbcfdb;\n}\n\n.toastui-editor-popup-add-table {\n  width: auto;\n}\n\n.toastui-editor-popup-add-table .toastui-editor-table-selection {\n  position: relative;\n}\n\n.toastui-editor-popup-add-table .toastui-editor-table-cell {\n  display: table-cell;\n  width: 20px;\n  height: 20px;\n  border: 1px solid #e1e3e9;\n  background: #fff;\n  box-sizing: border-box;\n}\n\n.toastui-editor-popup-add-table .toastui-editor-table-cell.header {\n  background: #f7f9fc;\n}\n\n.toastui-editor-popup-add-table .toastui-editor-table-row {\n  display: table-row;\n}\n\n.toastui-editor-popup-add-table .toastui-editor-table {\n  display: table;\n  border-collapse: collapse;\n}\n\n.toastui-editor-popup-add-table .toastui-editor-table-selection-layer {\n  position: absolute;\n  top: 0;\n  left: 0;\n  border: 1px solid #00a9ff;\n  background: rgba(0, 169, 255, 0.1);\n  z-index: 30;\n}\n\n.toastui-editor-popup-add-table .toastui-editor-table-description {\n  margin: 5px 0 0;\n  text-align: center;\n  color: #333\n}\n\n.toastui-editor-popup-add-heading {\n  width: auto;\n}\n\n.toastui-editor-popup-add-heading .toastui-editor-popup-body {\n  padding: 0;\n}\n\n.toastui-editor-popup-add-heading h1,\n.toastui-editor-popup-add-heading h2,\n.toastui-editor-popup-add-heading h3,\n.toastui-editor-popup-add-heading h4,\n.toastui-editor-popup-add-heading h5,\n.toastui-editor-popup-add-heading h6,\n.toastui-editor-popup-add-heading ul,\n.toastui-editor-popup-add-heading p {\n  padding: 0;\n  margin: 0;\n}\n\n.toastui-editor-popup-add-heading ul {\n  padding: 5px 0;\n  list-style: none;\n}\n\n.toastui-editor-popup-add-heading ul li {\n  padding: 4px 12px;\n  cursor: pointer;\n}\n\n.toastui-editor-popup-add-heading ul li:hover {\n  background-color: #dff4ff;\n}\n\n.toastui-editor-popup-add-heading h1 {\n  font-size: 24px;\n}\n\n.toastui-editor-popup-add-heading h2 {\n  font-size: 22px;\n}\n\n.toastui-editor-popup-add-heading h3 {\n  font-size: 20px;\n}\n\n.toastui-editor-popup-add-heading h4 {\n  font-size: 18px;\n}\n\n.toastui-editor-popup-add-heading h5 {\n  font-size: 16px;\n}\n\n.toastui-editor-popup-add-heading h6 {\n  font-size: 14px;\n}\n\n/* table context menu */\n.toastui-editor-context-menu {\n  position: absolute;\n  width: auto;\n  min-width: 197px;\n  color: #333;\n  border-radius: 2px;\n  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.08);\n  border: 1px solid #dadde6;\n  z-index: 30;\n  padding: 5px 0;\n  background-color: #fff;\n}\n\n.toastui-editor-context-menu .menu-group {\n  list-style: none;\n  border-bottom: 1px solid #ebedf2;\n  padding: 0;\n  margin: 0;\n  font-size: 13px;\n}\n\n.toastui-editor-context-menu .menu-group:last-child {\n  border-bottom: none !important;\n}\n\n.toastui-editor-context-menu .menu-item {\n  height: 32px;\n  line-height: 32px;\n  padding: 0 14px;\n  cursor: pointer;\n}\n\n.toastui-editor-context-menu span {\n  display: inline-block;\n}\n\n.toastui-editor-context-menu span::before {\n  background: url('../img/toastui-editor.png') no-repeat;\n  background-size: 466px 146px;\n  content: '';\n  width: 20px;\n  height: 20px;\n  display: inline-block;\n  vertical-align: middle;\n  margin-right: 10px;\n}\n\n.toastui-editor-context-menu .add-row-up::before {\n  background-position: 3px -104px;\n}\n\n.toastui-editor-context-menu .add-row-down::before {\n  background-position: -19px -104px;\n}\n\n.toastui-editor-context-menu .remove-row::before {\n  background-position: -41px -104px;\n}\n\n.toastui-editor-context-menu .add-column-left::before {\n  background-position: -63px -104px;\n}\n\n.toastui-editor-context-menu .add-column-right::before {\n  background-position: -85px -104px;\n}\n\n.toastui-editor-context-menu .remove-column::before {\n  background-position: -111px -104px;\n}\n\n.toastui-editor-context-menu .align-column-left::before {\n  background-position: -129px -104px;\n}\n\n.toastui-editor-context-menu .align-column-center::before {\n  background-position: -151px -104px;\n}\n\n.toastui-editor-context-menu .align-column-right::before {\n  background-position: -173px -104px;\n}\n\n.toastui-editor-context-menu .remove-table::before {\n  background-position: -197px -104px;\n}\n\n.toastui-editor-context-menu .disabled span::before {\n  opacity: 0.3;\n}\n\n.toastui-editor-context-menu li:not(.disabled):hover {\n  background-color: #dff4ff;\n}\n\n.toastui-editor-context-menu li.disabled {\n  color: #c9ccd5;\n}\n\n.toastui-editor-tooltip {\n  position: absolute;\n  background-color: #444;\n  z-index: 40;\n  padding: 4px 7px;\n  font-size: 12px;\n  border-radius: 3px;\n  color: #fff;\n  font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', 'Arial', '나눔바른고딕',\n    'Nanum Barun Gothic', '맑은고딕', 'Malgun Gothic', sans-serif;\n}\n\n.toastui-editor-tooltip .arrow {\n  content: '';\n  display: inline-block;\n  width: 10px;\n  height: 10px;\n  background-color: #444;\n  -webkit-transform: rotate(45deg);\n  -moz-transform: rotate(45deg);\n  -ms-transform: rotate(45deg);\n  -o-transform: rotate(45deg);\n  transform: rotate(45deg);\n  position: absolute;\n  top: -3px;\n  left: 6px;\n  z-index: -1;\n}\n\n.toastui-editor-toolbar-icons {\n  background: url('../img/toastui-editor.png') no-repeat;\n  background-size: 466px 146px;\n}\n\n@media only screen and (-webkit-min-device-pixel-ratio: 2),\n  only screen and (min--moz-device-pixel-ratio: 2),\n  only screen and (-o-min-device-pixel-ratio: 2/1),\n  only screen and (min-device-pixel-ratio: 2),\n  only screen and (min-resolution: 192dpi),\n  only screen and (min-resolution: 2dppx) {\n  .toastui-editor-toolbar-icons,\n  .toastui-editor-context-menu span::before {\n    background: url('../img/toastui-editor-2x.png') no-repeat;\n    background-size: 466px 146px;\n  }\n}\n\n.toastui-editor-toolbar-icons {\n  background-position-y: 3px;\n}\n\n.toastui-editor-toolbar-icons:disabled {\n  opacity: 0.3;\n}\n\n.toastui-editor-toolbar-icons.heading {\n  background-position-x: 3px;\n}\n\n.toastui-editor-toolbar-icons.bold {\n  background-position-x: -23px;\n}\n\n.toastui-editor-toolbar-icons.italic {\n  background-position-x: -49px;\n}\n\n.toastui-editor-toolbar-icons.strike {\n  background-position-x: -75px;\n}\n\n.toastui-editor-toolbar-icons.hrline {\n  background-position-x: -101px;\n}\n\n.toastui-editor-toolbar-icons.quote {\n  background-position-x: -127px;\n}\n\n.toastui-editor-toolbar-icons.bullet-list {\n  background-position-x: -153px;\n}\n\n.toastui-editor-toolbar-icons.ordered-list {\n  background-position-x: -179px;\n}\n\n.toastui-editor-toolbar-icons.task-list {\n  background-position-x: -205px;\n}\n\n.toastui-editor-toolbar-icons.indent {\n  background-position-x: -231px;\n}\n\n.toastui-editor-toolbar-icons.outdent {\n  background-position-x: -257px;\n}\n\n.toastui-editor-toolbar-icons.table {\n  background-position-x: -283px;\n}\n\n.toastui-editor-toolbar-icons.image {\n  background-position-x: -309px;\n}\n\n.toastui-editor-toolbar-icons.link {\n  background-position-x: -334px;\n}\n\n.toastui-editor-toolbar-icons.code {\n  background-position-x: -361px;\n}\n\n.toastui-editor-toolbar-icons.codeblock {\n  background-position-x: -388px;\n}\n\n.toastui-editor-toolbar-icons.more {\n  background-position-x: -412px;\n}\n\n.toastui-editor-toolbar-icons:not(:disabled).active {\n  background-position-y: -23px;\n}\n\n@media only screen and (max-width: 480px) {\n  .toastui-editor-popup {\n    max-width: 300px;\n    margin-left: -150px;\n  }\n\n  .toastui-editor-dropdown-toolbar {\n    max-width: none;\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/css/md-syntax-highlighting.css",
    "content": ".toastui-editor-md-heading1 {\n  font-size: 24px;\n}\n\n.toastui-editor-md-heading2 {\n  font-size: 22px;\n}\n\n.toastui-editor-md-heading3 {\n  font-size: 20px;\n}\n\n.toastui-editor-md-heading4 {\n  font-size: 18px;\n}\n\n.toastui-editor-md-heading5 {\n  font-size: 16px;\n}\n\n.toastui-editor-md-heading6 {\n  font-size: 14px;\n}\n\n.toastui-editor-md-heading.toastui-editor-md-delimiter.setext {\n  line-height: 15px;\n}\n\n.toastui-editor-md-strong,\n.toastui-editor-md-heading,\n.toastui-editor-md-list-item-style,\n.toastui-editor-md-list-item .toastui-editor-md-meta {\n  font-weight: bold;\n}\n\n.toastui-editor-md-emph {\n  font-style: italic;\n}\n\n.toastui-editor-md-strike {\n  text-decoration: line-through;\n}\n\n.toastui-editor-md-strike.toastui-editor-md-delimiter {\n  text-decoration: none;\n}\n\n.toastui-editor-md-delimiter,\n.toastui-editor-md-thematic-break,\n.toastui-editor-md-link,\n.toastui-editor-md-table,\n.toastui-editor-md-block-quote {\n  color: #ccc;\n}\n\n.toastui-editor-md-code.toastui-editor-md-delimiter {\n  color: #aaa;\n}\n\n.toastui-editor-md-meta,\n.toastui-editor-md-html,\n.toastui-editor-md-link.toastui-editor-md-link-url.toastui-editor-md-marked-text {\n  color: #999;\n}\n\n.toastui-editor-md-block-quote .toastui-editor-md-marked-text,\n.toastui-editor-md-list-item .toastui-editor-md-meta {\n  color: #555;\n}\n\n.toastui-editor-md-table .toastui-editor-md-table-cell {\n  color: #222;\n}\n\n.toastui-editor-md-link.toastui-editor-md-link-desc.toastui-editor-md-marked-text,\n.toastui-editor-md-list-item-style.toastui-editor-md-list-item-odd {\n  color: #4b96e6;\n}\n\n.toastui-editor-md-list-item-style.toastui-editor-md-list-item-even {\n  color: #cb4848;\n}\n\n.toastui-editor-md-code.toastui-editor-md-marked-text {\n  color: #c1798b;\n}\n\n.toastui-editor-md-code {\n  background-color: rgba(243, 229, 233, 0.5);\n  padding: 2px 0;\n  letter-spacing: -0.3px;\n}\n\n.toastui-editor-md-code.toastui-editor-md-start {\n  padding-left: 2px;\n  border-top-left-radius: 2px;\n  border-bottom-left-radius: 2px;\n}\n\n.toastui-editor-md-code.toastui-editor-md-end {\n  padding-right: 2px;\n  border-top-right-radius: 2px;\n  border-bottom-right-radius: 2px;\n}\n\n.toastui-editor-md-code-block-line-background {\n  background-color: #f5f7f8;\n}\n\n.toastui-editor-md-code-block-line-background.start,\n.toastui-editor-md-custom-block-line-background.start {\n  margin-top: 2px;\n}\n\n.toastui-editor-md-code,\n.toastui-editor-md-code-block {\n  font-family: Consolas, Courier, 'Lucida Grande', '나눔바른고딕', 'Nanum Barun Gothic', '맑은고딕',\n    'Malgun Gothic', sans-serif;\n}\n\n.toastui-editor-md-custom-block {\n  color: #452d6b;\n}\n.toastui-editor-md-custom-block-line-background {\n  background-color: #f9f7fd;\n}\n.toastui-editor-md-custom-block .toastui-editor-md-delimiter {\n  color: #b8b3c0;\n}\n.toastui-editor-md-custom-block .toastui-editor-md-meta {\n  color: #5200d0;\n}"
  },
  {
    "path": "apps/editor/src/css/preview-highlighting.css",
    "content": ".toastui-editor-contents .toastui-editor-md-preview-highlight {\n  position: relative;\n  z-index: 0;\n}\n\n.toastui-editor-contents .toastui-editor-md-preview-highlight::after {\n  content: '';\n  background-color: rgba(255, 245, 131, 0.5);\n  border-radius: 4px;\n  z-index: -1;\n  position: absolute;\n  top: -4px;\n  right: -4px;\n  left: -4px;\n  bottom: -4px;\n}\n\n.toastui-editor-contents h1.toastui-editor-md-preview-highlight::after,\n.toastui-editor-contents h2.toastui-editor-md-preview-highlight::after {\n  bottom: 0;\n}\n\n.toastui-editor-contents td.toastui-editor-md-preview-highlight::after,\n.toastui-editor-contents th.toastui-editor-md-preview-highlight::after {\n  display: none;\n}\n\n.toastui-editor-contents th.toastui-editor-md-preview-highlight,\n.toastui-editor-contents td.toastui-editor-md-preview-highlight {\n  background-color: rgba(255, 245, 131, 0.5);\n}\n\n.toastui-editor-contents th.toastui-editor-md-preview-highlight {\n  color: #222;\n}\n"
  },
  {
    "path": "apps/editor/src/css/theme/dark.css",
    "content": "@charset \"utf-8\";\n.toastui-editor-dark.toastui-editor-defaultUI {\n  border-color: #494c56;\n  color: #eee;\n}\n\n.toastui-editor-dark .toastui-editor-md-container,\n.toastui-editor-dark .toastui-editor-ww-container {\n  background-color: #121212;\n}\n\n.toastui-editor-dark .toastui-editor-defaultUI-toolbar {\n  background-color: #232428;\n  border-bottom-color: #303238;\n}\n\n.toastui-editor-dark .toastui-editor-toolbar-icons {\n  background-position-y: -49px;\n  border-color: #232428;\n}\n\n.toastui-editor-dark .toastui-editor-toolbar-icons:not(:disabled):hover {\n  background-color: #36383f;\n  border-color: #36383f;\n}\n\n.toastui-editor-dark .toastui-editor-toolbar-divider {\n  background-color: #303238;\n}\n\n.toastui-editor-dark .toastui-editor-tooltip {\n  background-color: #535662;\n}\n\n.toastui-editor-dark .toastui-editor-tooltip .arrow {\n  background-color: #535662;\n}\n\n.toastui-editor-dark .toastui-editor-defaultUI-toolbar .scroll-sync::before {\n  color: #8f939f;\n}\n\n.toastui-editor-dark .toastui-editor-defaultUI-toolbar .scroll-sync.active::before {\n  color: #67ccff;\n}\n\n.toastui-editor-dark .toastui-editor-defaultUI-toolbar .switch {\n  background-color: #2b4455;\n}\n\n.toastui-editor-dark .toastui-editor-defaultUI-toolbar input:checked + .switch {\n  background-color: #2b4455;\n}\n\n.toastui-editor-dark .toastui-editor-defaultUI-toolbar .switch::before {\n  background-color: #8f939f;\n}\n\n.toastui-editor-dark .toastui-editor-defaultUI-toolbar input:checked + .switch::before {\n  background-color: #67ccff;\n}\n\n.toastui-editor-dark .toastui-editor-main .toastui-editor-md-splitter {\n  background-color: #303238;\n}\n\n.toastui-editor-dark .toastui-editor-mode-switch {\n  border-top-color: #393b42;\n  background-color: #121212;\n}\n\n.toastui-editor-dark .toastui-editor-mode-switch .tab-item {\n  border-color: #393b42;\n  background-color: #232428;\n  color: #757a86;\n}\n\n.toastui-editor-dark .toastui-editor-mode-switch .tab-item.active {\n  border-top-color: #121212;\n  background-color: #121212;\n  color: #eee;\n}\n\n.toastui-editor-dark .toastui-editor-popup,\n.toastui-editor-dark .toastui-editor-context-menu {\n  background-color: #121212;\n  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.08);\n  border-color: #494c56;\n}\n\n.toastui-editor-dark .toastui-editor-popup-add-heading ul li:hover {\n  background-color: #36383f;\n}\n\n.toastui-editor-dark .toastui-editor-popup-body label {\n  color: #9a9da3;\n}\n\n.toastui-editor-dark .toastui-editor-popup-body input[type='text'] {\n  background-color: transparent;\n  color: #eee;\n  border-color: #303238;\n}\n\n.toastui-editor-dark .toastui-editor-popup-body input[type='text']:focus {\n  outline-color: #67ccff;\n}\n\n.toastui-editor-dark .toastui-editor-popup-body input[type='text'].disabled {\n  color: #969aa5;\n  border-color: #303238;\n  background-color: rgba(48, 50, 56, 0.4);\n}\n\n.toastui-editor-dark .toastui-editor-popup-add-image .toastui-editor-tabs .tab-item {\n  border-bottom-color: #292e37;\n  color: #eee;\n}\n\n.toastui-editor-dark .toastui-editor-popup-add-image .toastui-editor-tabs .tab-item:hover {\n  border-bottom-color: #3c424d;\n}\n\n.toastui-editor-dark .toastui-editor-popup-add-image .toastui-editor-tabs .tab-item.active {\n  color: #67ccff;\n  border-bottom-color: #67ccff;\n}\n\n.toastui-editor-dark .toastui-editor-popup-body .toastui-editor-file-name {\n  border-color: #303238;\n  color: #eee;\n}\n\n.toastui-editor-dark .toastui-editor-popup-body .toastui-editor-file-select-button {\n  border-color: #303238;\n  background-color: #232428;\n  color: #eee;\n}\n\n.toastui-editor-dark .toastui-editor-popup-body .toastui-editor-file-select-button:hover {\n  border-color: #494c56;\n}\n\n.toastui-editor-dark.toastui-editor-defaultUI .toastui-editor-close-button {\n  color: #eee;\n  border-color: #303238;\n  background-color: #232428;\n}\n\n.toastui-editor-dark.toastui-editor-defaultUI .toastui-editor-close-button:hover {\n  border-color: #494c56;\n}\n\n.toastui-editor-dark.toastui-editor-defaultUI .toastui-editor-ok-button {\n  color: #121212;\n  background-color: #67ccff;\n}\n\n.toastui-editor-dark.toastui-editor-defaultUI .toastui-editor-ok-button:hover {\n  color: #121212;\n  background-color: #32baff;\n}\n\n.toastui-editor-dark .toastui-editor-popup-add-table .toastui-editor-table-cell {\n  border-color: #303238;\n  background-color: #121212;\n}\n\n.toastui-editor-dark .toastui-editor-popup-add-table .toastui-editor-table-cell.header {\n  border-color: #303238;\n  background-color: #232428;\n}\n\n.toastui-editor-dark .toastui-editor-popup-add-table .toastui-editor-table-selection-layer {\n  border-color: rgba(103, 204, 255, 0.4);\n  background-color: rgba(103, 204, 255, 0.1);\n}\n\n.toastui-editor-dark .toastui-editor-popup-add-table .toastui-editor-table-description {\n  color: #eee\n}\n\n.toastui-editor-dark .toastui-editor-md-tab-container {\n  background-color: #232428;\n  border-bottom-color: #303238;\n}\n\n.toastui-editor-dark .toastui-editor-md-tab-container .tab-item {\n  border-color: #393b42;\n  background-color: #2d2f34;\n  color: #757a86;\n}\n\n.toastui-editor-dark .toastui-editor-md-tab-container .tab-item.active {\n  border-bottom-color: #121212;\n  background-color: #121212;\n  color: #eee;\n}\n\n\n.toastui-editor-dark .toastui-editor-context-menu .menu-group {\n  border-bottom-color: #303238;\n  color: #eee;\n}\n\n.toastui-editor-dark .toastui-editor-context-menu .menu-item span::before {\n  background-position-y: -126px;\n}\n\n.toastui-editor-dark .toastui-editor-context-menu li:not(.disabled):hover {\n  background-color: #36383f;\n}\n\n.toastui-editor-dark .toastui-editor-context-menu li.disabled {\n  color: #969aa5;\n}\n\n.toastui-editor-dark .toastui-editor-dropdown-toolbar {\n  border-color: #494c56;\n  background-color: #232428;\n}\n\n.toastui-editor-dark .ProseMirror,\n.toastui-editor-dark .toastui-editor-contents p,\n.toastui-editor-dark .toastui-editor-contents h1,\n.toastui-editor-dark .toastui-editor-contents h2,\n.toastui-editor-dark .toastui-editor-contents h3,\n.toastui-editor-dark .toastui-editor-contents h4,\n.toastui-editor-dark .toastui-editor-contents h5,\n.toastui-editor-dark .toastui-editor-contents h6 {\n  color: #fff;\n}\n\n.toastui-editor-dark .toastui-editor-contents h1,\n.toastui-editor-dark .toastui-editor-contents h2 {\n  border-color: #fff;\n}\n\n.toastui-editor-dark .toastui-editor-contents del {\n  color: #777980;\n}\n\n.toastui-editor-dark .toastui-editor-contents blockquote {\n  border-color: #303135;\n}\n\n.toastui-editor-dark .toastui-editor-contents blockquote p,\n.toastui-editor-dark .toastui-editor-contents blockquote ul,\n.toastui-editor-dark .toastui-editor-contents blockquote ol {\n  color: #777980;\n}\n\n.toastui-editor-dark .toastui-editor-contents pre {\n  background-color: #232428;\n}\n\n.toastui-editor-dark .toastui-editor-contents pre code {\n  background-color: transparent;\n  color: #fff;\n}\n\n.toastui-editor-dark .toastui-editor-contents code {\n  color: #c1798b;\n  background-color: #35262a;\n}\n\n.toastui-editor-dark .toastui-editor-contents div {\n  color: #fff;\n}\n\n.toastui-editor-dark .toastui-editor-ww-code-block-language {\n  border-color: #303238;\n  background-color: #121212;\n}\n\n.toastui-editor-dark .toastui-editor-ww-code-block-language input {\n  color: #fff;\n}\n\n.toastui-editor-dark .toastui-editor-contents .toastui-editor-ww-code-block:after {\n  background-color: #232428;\n  border: 1px solid #393b42;\n  color: #eee;\n  background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI1LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IuugiOydtOyWtF8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiCgkgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMzAgMzAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMwIDMwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGwtcnVsZTpldmVub2RkO2NsaXAtcnVsZTpldmVub2RkO2ZpbGw6I2ZmZjt9Cjwvc3R5bGU+CjxnPgoJPGc+CgkJPGc+CgkJCTxnPgoJCQkJPGc+CgkJCQkJPHBhdGggY2xhc3M9InN0MCIgZD0iTTE1LjUsMTIuNWwyLDJMMTIsMjBoLTJ2LTJMMTUuNSwxMi41eiBNMTgsMTBsMiwybC0xLjUsMS41bC0yLTJMMTgsMTB6Ii8+CgkJCQk8L2c+CgkJCTwvZz4KCQk8L2c+Cgk8L2c+CjwvZz4KPC9zdmc+Cg==');\n}\n\n.toastui-editor-dark .toastui-editor-contents .toastui-editor-custom-block-editor {\n  background: #392d31;\n  color: #fff;\n  border-color: #327491;\n}\n\n.toastui-editor-dark .toastui-editor-custom-block.ProseMirror-selectednode .toastui-editor-custom-block-view {\n  color: #fff;\n  border-color: #327491;\n}\n\n.toastui-editor-dark .toastui-editor-custom-block-view button {\n  background-color: #232428;\n  border-color: #393b42;\n  background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI1LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IuugiOydtOyWtF8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiCgkgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMzAgMzAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMwIDMwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGwtcnVsZTpldmVub2RkO2NsaXAtcnVsZTpldmVub2RkO2ZpbGw6I2ZmZjt9Cjwvc3R5bGU+CjxnPgoJPGc+CgkJPGc+CgkJCTxnPgoJCQkJPGc+CgkJCQkJPHBhdGggY2xhc3M9InN0MCIgZD0iTTE1LjUsMTIuNWwyLDJMMTIsMjBoLTJ2LTJMMTUuNSwxMi41eiBNMTgsMTBsMiwybC0xLjUsMS41bC0yLTJMMTgsMTB6Ii8+CgkJCQk8L2c+CgkJCTwvZz4KCQk8L2c+Cgk8L2c+CjwvZz4KPC9zdmc+Cg==');\n}\n\n.toastui-editor-dark .toastui-editor-custom-block-view button:hover {\n  background-color: #232428;\n  border-color: #595c68;\n}\n\n.toastui-editor-dark .toastui-editor-custom-block-view .info {\n  color: #65acca;\n}\n\n.toastui-editor-dark .toastui-editor-contents table {\n  border-color: #303238;\n}\n\n.toastui-editor-dark .toastui-editor-contents table th,\n.toastui-editor-dark .toastui-editor-contents table td {\n  border-color: #303238;\n}\n\n.toastui-editor-dark .toastui-editor-contents table th {\n  background-color: #3a3c42;\n}\n\n.toastui-editor-dark .toastui-editor-contents table td,\n.toastui-editor-dark .toastui-editor-contents table td p {\n  color: #fff;\n}\n\n.toastui-editor-dark .toastui-editor-contents td.toastui-editor-cell-selected {\n  background-color: rgba(103, 204, 255, 0.5);\n}\n\n.toastui-editor-dark .toastui-editor-contents th.toastui-editor-cell-selected {\n  background-color: rgba(103, 204, 255, 0.3);\n}\n\n.toastui-editor-dark table.ProseMirror-selectednode {\n  outline-color: #67ccff;\n}\n\n.toastui-editor-dark .html-block.ProseMirror-selectednode {\n  outline-color: #67ccff;\n}\n\n.toastui-editor-dark .toastui-editor-contents ul,\n.toastui-editor-dark .toastui-editor-contents menu,\n.toastui-editor-dark .toastui-editor-contents ol,\n.toastui-editor-dark .toastui-editor-contents dir {\n  color: #55575f;\n}\n\n.toastui-editor-dark .toastui-editor-contents ul > li::before {\n  background-color: #55575f;\n}\n\n.toastui-editor-dark .toastui-editor-contents hr {\n  border-color: #55575f;\n}\n\n.toastui-editor-dark .toastui-editor-contents a {\n  color: #4b96e6;\n}\n\n.toastui-editor-dark .toastui-editor-contents a:hover {\n  color: #1f70de;\n}\n\n.toastui-editor-dark .toastui-editor-contents .image-link:hover::before {\n  border-color: #393b42;\n  background-color: #232428;\n  background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgdmlld0JveD0iMCAwIDIwIDIwIj4KICAgIDxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIj4KICAgICAgICA8ZyBzdHJva2U9IiNFRUUiIHN0cm9rZS13aWR0aD0iMS41Ij4KICAgICAgICAgICAgPGc+CiAgICAgICAgICAgICAgICA8Zz4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNNy42NjUgMTUuMDdsLTEuODE5LS4wMDJjLTEuNDg2IDAtMi42OTItMS4yMjgtMi42OTItMi43NDR2LS4xOTJjMC0xLjUxNSAxLjIwNi0yLjc0NCAyLjY5Mi0yLjc0NGgzLjg0NmMxLjQ4NyAwIDIuNjkyIDEuMjI5IDIuNjkyIDIuNzQ0di4xOTIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMDQ1IC0xNzQzKSB0cmFuc2xhdGUoMTA0MCAxNzM4KSB0cmFuc2xhdGUoNSA1KSBzY2FsZSgxIC0xKSByb3RhdGUoNDUgMzcuMjkzIDApIi8+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTEyLjMyNiA0LjkzNGwxLjgyMi4wMDJjMS40ODcgMCAyLjY5MyAxLjIyOCAyLjY5MyAyLjc0NHYuMTkyYzAgMS41MTUtMS4yMDYgMi43NDQtMi42OTMgMi43NDRoLTMuODQ1Yy0xLjQ4NyAwLTIuNjkyLTEuMjI5LTIuNjkyLTIuNzQ0VjcuNjgiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMDQ1IC0xNzQzKSB0cmFuc2xhdGUoMTA0MCAxNzM4KSB0cmFuc2xhdGUoNSA1KSBzY2FsZSgxIC0xKSByb3RhdGUoNDUgMzAuOTk2IDApIi8+CiAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPgo=');\n  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.08);\n}\n\n.toastui-editor-dark .toastui-editor-contents .task-list-item::before {\n  background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxOCIgdmlld0JveD0iMCAwIDE4IDE4Ij4KICAgIDxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgc3Ryb2tlPSIjNTU1NzVGIj4KICAgICAgICAgICAgPGc+CiAgICAgICAgICAgICAgICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTAzMCAtMzE2KSB0cmFuc2xhdGUoNzg4IDE5MikgdHJhbnNsYXRlKDI0MiAxMjQpIj4KICAgICAgICAgICAgICAgICAgICA8cmVjdCB3aWR0aD0iMTciIGhlaWdodD0iMTciIHg9Ii41IiB5PSIuNSIgcng9IjIiLz4KICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+Cg==');\n  background-color: transparent;\n}\n\n.toastui-editor-dark .toastui-editor-contents .task-list-item.checked::before {\n  background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxOCIgdmlld0JveD0iMCAwIDE4IDE4Ij4KICAgIDxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgZmlsbD0iIzRCOTZFNiI+CiAgICAgICAgICAgIDxnPgogICAgICAgICAgICAgICAgPGc+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTE2IDBjMS4xMDUgMCAyIC44OTUgMiAydjE0YzAgMS4xMDUtLjg5NSAyLTIgMkgyYy0xLjEwNSAwLTItLjg5NS0yLTJWMkMwIC44OTUuODk1IDAgMiAwaDE0em0tMS43OTMgNS4yOTNjLS4zOS0uMzktMS4wMjQtLjM5LTEuNDE0IDBMNy41IDEwLjU4NSA1LjIwNyA4LjI5M2wtLjA5NC0uMDgzYy0uMzkyLS4zMDUtLjk2LS4yNzgtMS4zMi4wODMtLjM5LjM5LS4zOSAxLjAyNCAwIDEuNDE0bDMgMyAuMDk0LjA4M2MuMzkyLjMwNS45Ni4yNzggMS4zMi0uMDgzbDYtNiAuMDgzLS4wOTRjLjMwNS0uMzkyLjI3OC0uOTYtLjA4My0xLjMyeiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTEwNTAgLTI5NikgdHJhbnNsYXRlKDc4OCAxOTIpIHRyYW5zbGF0ZSgyNjIgMTA0KSIvPgogICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICA8L2c+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4K');\n}\n\n.toastui-editor-dark .toastui-editor-md-delimiter,\n.toastui-editor-dark .toastui-editor-md-code.toastui-editor-md-delimiter,\n.toastui-editor-dark .toastui-editor-md-thematic-break,\n.toastui-editor-dark .toastui-editor-md-link,\n.toastui-editor-dark .toastui-editor-md-table,\n.toastui-editor-dark .toastui-editor-md-block-quote {\n  color: #55575f;\n}\n\n.toastui-editor-dark .toastui-editor-md-meta,\n.toastui-editor-dark .toastui-editor-md-html {\n  color: #55575f;\n}\n\n.toastui-editor-dark .toastui-editor-md-link.toastui-editor-md-link-url.toastui-editor-md-marked-text {\n  color: #777980;\n}\n\n.toastui-editor-dark .toastui-editor-md-block-quote .toastui-editor-md-marked-text,\n.toastui-editor-dark .toastui-editor-md-list-item .toastui-editor-md-meta {\n  color: #b3b5bc;\n}\n\n.toastui-editor-dark .toastui-editor-md-link.toastui-editor-md-link-desc.toastui-editor-md-marked-text,\n.toastui-editor-dark .toastui-editor-md-list-item-style.toastui-editor-md-list-item-odd {\n  color: #4b96e6;\n}\n\n.toastui-editor-dark .toastui-editor-md-list-item-style.toastui-editor-md-list-item-even {\n  color: #ef6767;\n}\n\n.toastui-editor-dark .toastui-editor-md-table .toastui-editor-md-table-cell {\n  color: #fff;\n}\n\n.toastui-editor-dark .toastui-editor-md-code.toastui-editor-md-marked-text {\n  color: #c1798b;\n}\n\n.toastui-editor-dark .toastui-editor-md-code {\n  background-color: #35262a;\n}\n\n.toastui-editor-dark .toastui-editor-md-code-block-line-background {\n  background-color: #232428;\n}\n\n.toastui-editor-dark .toastui-editor-md-code-block .toastui-editor-md-meta {\n  color: #aaa;\n}\n\n.toastui-editor-dark .toastui-editor-md-custom-block {\n  color: #fff;\n}\n\n.toastui-editor-dark .toastui-editor-md-custom-block-line-background {\n  background-color: #392d31;\n}\n\n.toastui-editor-dark .toastui-editor-md-custom-block .toastui-editor-md-delimiter {\n  color: #327491;\n}\n\n.toastui-editor-dark .toastui-editor-md-custom-block .toastui-editor-md-meta {\n  color: #65acca;\n}\n\n.toastui-editor-dark .toastui-editor-contents .toastui-editor-md-preview-highlight::after {\n  background-color: rgba(255, 250, 193, 0.5);\n}\n\n.toastui-editor-dark .toastui-editor-contents th.toastui-editor-md-preview-highlight,\n.toastui-editor-dark .toastui-editor-contents td.toastui-editor-md-preview-highlight {\n  background-color: rgba(255, 250, 193, 0.5);\n}\n\n.toastui-editor-dark .toastui-editor-contents th.toastui-editor-md-preview-highlight {\n  color: #fff;\n}\n\n.toastui-editor-dark .toastui-editor-contents th.toastui-editor-md-preview-highlight,\n.toastui-editor-dark .toastui-editor-contents td.toastui-editor-md-preview-highlight {\n  background-color: rgba(255, 250, 193, 0.25);\n}\n\n.toastui-editor-dark .toastui-editor-contents .toastui-editor-md-preview-highlight::after {\n  background-color: rgba(255, 250, 193, 0.25);\n}"
  },
  {
    "path": "apps/editor/src/editor.ts",
    "content": "import { EditorOptions, ViewerOptions } from '@t/editor';\nimport { DefaultUI, VNode, IndexList, ToolbarItemOptions } from '@t/ui';\nimport EditorCore from './editorCore';\nimport Viewer from './viewer';\nimport html from './ui/vdom/template';\nimport { Layout } from './ui/components/layout';\nimport { render } from './ui/vdom/renderer';\n\n/**\n * ToastUI Editor\n * @extends ToastUIEditorCore\n */\nclass ToastUIEditor extends EditorCore {\n  private defaultUI!: DefaultUI;\n\n  constructor(options: EditorOptions) {\n    super(options);\n\n    let layoutComp!: Layout;\n    const destroy = render(\n      this.options.el,\n      html`\n        <${Layout}\n          ref=${(layout: Layout) => (layoutComp = layout)}\n          eventEmitter=${this.eventEmitter}\n          slots=${this.getEditorElements()}\n          hideModeSwitch=${this.options.hideModeSwitch}\n          toolbarItems=${this.options.toolbarItems}\n          previewStyle=${this.options.previewStyle}\n          editorType=${this.options.initialEditType}\n          theme=${this.options.theme}\n        />\n      ` as VNode\n    );\n\n    this.setMinHeight(this.options.minHeight);\n    this.setHeight(this.options.height);\n    this.defaultUI = {\n      insertToolbarItem: layoutComp.insertToolbarItem.bind(layoutComp),\n      removeToolbarItem: layoutComp.removeToolbarItem.bind(layoutComp),\n      destroy,\n    };\n\n    this.pluginInfo.toolbarItems?.forEach((toolbarItem) => {\n      const { groupIndex, itemIndex, item } = toolbarItem;\n\n      this.defaultUI.insertToolbarItem({ groupIndex, itemIndex }, item);\n    });\n    this.eventEmitter.emit('loadUI', this);\n  }\n\n  /**\n   * Factory method for Editor\n   * @param {object} options Option for initialize TUIEditor\n   * @returns {object} ToastUIEditor or ToastUIEditorViewer\n   */\n  static factory(options: (EditorOptions | ViewerOptions) & { viewer?: boolean }) {\n    return options.viewer ? new Viewer(options) : new ToastUIEditor(options as EditorOptions);\n  }\n\n  /**\n   * add toolbar item\n   * @param {Object} indexInfo group index and item index of the toolbar item\n   * @param {string|Object} item toolbar item\n   */\n  insertToolbarItem(indexInfo: IndexList, item: string | ToolbarItemOptions) {\n    this.defaultUI.insertToolbarItem(indexInfo, item);\n  }\n\n  /**\n   * Remove toolbar item\n   * @param {string} itemName toolbar item name\n   */\n  removeToolbarItem(itemName: string) {\n    this.defaultUI.removeToolbarItem(itemName);\n  }\n\n  /**\n   * Destroy TUIEditor from document\n   */\n  destroy() {\n    super.destroy();\n    this.defaultUI.destroy();\n  }\n}\n\nexport default ToastUIEditor;\n"
  },
  {
    "path": "apps/editor/src/editorCore.ts",
    "content": "import { DOMParser } from 'prosemirror-model';\nimport forEachOwnProperties from 'tui-code-snippet/collection/forEachOwnProperties';\nimport extend from 'tui-code-snippet/object/extend';\nimport css from 'tui-code-snippet/domUtil/css';\nimport addClass from 'tui-code-snippet/domUtil/addClass';\nimport removeClass from 'tui-code-snippet/domUtil/removeClass';\nimport isString from 'tui-code-snippet/type/isString';\nimport isNumber from 'tui-code-snippet/type/isNumber';\n\nimport { Emitter, Handler } from '@t/event';\nimport {\n  Base,\n  EditorOptions,\n  EditorPos,\n  EditorType,\n  PreviewStyle,\n  ViewerOptions,\n  WidgetStyle,\n} from '@t/editor';\nimport { PluginCommandMap, PluginInfoResult, CommandFn } from '@t/plugin';\n\nimport { sendHostName, sanitizeLinkAttribute, deepMergedCopy } from './utils/common';\n\nimport MarkdownEditor from './markdown/mdEditor';\nimport MarkdownPreview from './markdown/mdPreview';\n\nimport WysiwygEditor from './wysiwyg/wwEditor';\n\nimport EventEmitter from './event/eventEmitter';\nimport CommandManager from './commands/commandManager';\nimport Convertor from './convertors/convertor';\nimport Viewer from './viewer';\nimport i18n, { I18n } from './i18n/i18n';\nimport { getPluginInfo } from './helper/plugin';\n\nimport { ToastMark } from '@toast-ui/toastmark';\nimport { WwToDOMAdaptor } from './wysiwyg/adaptor/wwToDOMAdaptor';\nimport { ScrollSync } from './markdown/scroll/scrollSync';\nimport { addDefaultImageBlobHook } from './helper/image';\nimport { setWidgetRules } from './widget/rules';\nimport { cls, removeProseMirrorHackNodes, replaceBRWithEmptyBlock } from './utils/dom';\nimport { sanitizeHTML } from './sanitizer/htmlSanitizer';\nimport { createHTMLSchemaMap } from './wysiwyg/nodes/html';\nimport { getHTMLRenderConvertors } from './markdown/htmlRenderConvertors';\nimport { buildQuery } from './queries/queryManager';\nimport { getEditorToMdPos, getMdToEditorPos } from './markdown/helper/pos';\nimport { Pos } from '@t/toastmark';\n\n/**\n * ToastUIEditorCore\n * @param {Object} options Option object\n *     @param {HTMLElement} options.el - container element\n *     @param {string} [options.height='300px'] - Editor's height style value. Height is applied as border-box ex) '300px', '100%', 'auto'\n *     @param {string} [options.minHeight='200px'] - Editor's min-height style value in pixel ex) '300px'\n *     @param {string} [options.initialValue] - Editor's initial value\n *     @param {string} [options.previewStyle] - Markdown editor's preview style (tab, vertical)\n *     @param {boolean} [options.previewHighlight = true] - Highlight a preview element corresponds to the cursor position in the markdown editor\n *     @param {string} [options.initialEditType] - Initial editor type (markdown, wysiwyg)\n *     @param {Object} [options.events] - Events\n *         @param {function} [options.events.load] - It would be emitted when editor fully load\n *         @param {function} [options.events.change] - It would be emitted when content changed\n *         @param {function} [options.events.caretChange] - It would be emitted when format change by cursor position\n *         @param {function} [options.events.focus] - It would be emitted when editor get focus\n *         @param {function} [options.events.blur] - It would be emitted when editor loose focus\n *         @param {function} [options.events.keydown] - It would be emitted when the key is pressed in editor\n *         @param {function} [options.events.keyup] - It would be emitted when the key is released in editor\n *         @param {function} [options.events.beforePreviewRender] - It would be emitted before rendering the markdown preview with html string\n *         @param {function} [options.events.beforeConvertWysiwygToMarkdown] - It would be emitted before converting wysiwyg to markdown with markdown text\n *     @param {Object} [options.hooks] - Hooks\n *         @param {addImageBlobHook} [options.hooks.addImageBlobHook] - hook for image upload\n *     @param {string} [options.language='en-US'] - language\n *     @param {boolean} [options.useCommandShortcut=true] - whether use keyboard shortcuts to perform commands\n *     @param {boolean} [options.usageStatistics=true] - send hostname to google analytics\n *     @param {Array.<string|toolbarItemsValue>} [options.toolbarItems] - toolbar items.\n *     @param {boolean} [options.hideModeSwitch=false] - hide mode switch tab bar\n *     @param {Array.<function|Array>} [options.plugins] - Array of plugins. A plugin can be either a function or an array in the form of [function, options].\n *     @param {Object} [options.extendedAutolinks] - Using extended Autolinks specified in GFM spec\n *     @param {string} [options.placeholder] - The placeholder text of the editable element.\n *     @param {Object} [options.linkAttributes] - Attributes of anchor element that should be rel, target, hreflang, type\n *     @param {Object} [options.customHTMLRenderer=null] - Object containing custom renderer functions correspond to change markdown node to preview HTML or wysiwyg node\n *     @param {Object} [options.customMarkdownRenderer=null] - Object containing custom renderer functions correspond to change wysiwyg node to markdown text\n *     @param {boolean} [options.referenceDefinition=false] - whether use the specification of link reference definition\n *     @param {function} [options.customHTMLSanitizer=null] - custom HTML sanitizer\n *     @param {boolean} [options.previewHighlight=false] - whether highlight preview area\n *     @param {boolean} [options.frontMatter=false] - whether use the front matter\n *     @param {Array.<object>} [options.widgetRules=[]] - The rules for replacing the text with widget node\n *     @param {string} [options.theme] - The theme to style the editor with. The default is included in toastui-editor.css.\n *     @param {autofocus} [options.autofocus=true] - automatically focus the editor on creation.\n */\nclass ToastUIEditorCore {\n  private initialHTML: string;\n\n  private toastMark: ToastMark;\n\n  private mdEditor: MarkdownEditor;\n\n  private wwEditor: WysiwygEditor;\n\n  private preview: MarkdownPreview;\n\n  private convertor: Convertor;\n\n  private commandManager: CommandManager;\n\n  private height!: string;\n\n  private minHeight!: string;\n\n  private mode!: EditorType;\n\n  private mdPreviewStyle: PreviewStyle;\n\n  private i18n: I18n;\n\n  private scrollSync: ScrollSync;\n\n  private placeholder?: string;\n\n  eventEmitter: Emitter;\n\n  protected options: Required<EditorOptions>;\n\n  protected pluginInfo: PluginInfoResult;\n\n  constructor(options: EditorOptions) {\n    this.initialHTML = options.el.innerHTML;\n    options.el.innerHTML = '';\n\n    this.options = extend(\n      {\n        previewStyle: 'tab',\n        previewHighlight: true,\n        initialEditType: 'markdown',\n        height: '300px',\n        minHeight: '200px',\n        language: 'en-US',\n        useCommandShortcut: true,\n        usageStatistics: true,\n        toolbarItems: [\n          ['heading', 'bold', 'italic', 'strike'],\n          ['hr', 'quote'],\n          ['ul', 'ol', 'task', 'indent', 'outdent'],\n          ['table', 'image', 'link'],\n          ['code', 'codeblock'],\n          ['scrollSync'],\n        ],\n        hideModeSwitch: false,\n        linkAttributes: null,\n        extendedAutolinks: false,\n        customHTMLRenderer: null,\n        customMarkdownRenderer: null,\n        referenceDefinition: false,\n        customHTMLSanitizer: null,\n        frontMatter: false,\n        widgetRules: [],\n        theme: 'light',\n        autofocus: true,\n      },\n      options\n    );\n\n    const {\n      customHTMLRenderer,\n      extendedAutolinks,\n      referenceDefinition,\n      frontMatter,\n      customMarkdownRenderer,\n      useCommandShortcut,\n      initialEditType,\n      widgetRules,\n      customHTMLSanitizer,\n    } = this.options;\n\n    this.mode = initialEditType || 'markdown';\n    this.mdPreviewStyle = this.options.previewStyle;\n\n    this.i18n = i18n;\n    this.i18n.setCode(this.options.language);\n\n    this.eventEmitter = new EventEmitter();\n\n    setWidgetRules(widgetRules);\n\n    const linkAttributes = sanitizeLinkAttribute(this.options.linkAttributes);\n\n    this.pluginInfo = getPluginInfo({\n      plugins: this.options.plugins,\n      eventEmitter: this.eventEmitter,\n      usageStatistics: this.options.usageStatistics,\n      instance: this,\n    });\n    const {\n      toHTMLRenderers,\n      toMarkdownRenderers,\n      mdPlugins,\n      wwPlugins,\n      wwNodeViews,\n      mdCommands,\n      wwCommands,\n      markdownParsers,\n    } = this.pluginInfo;\n    const rendererOptions = {\n      linkAttributes,\n      customHTMLRenderer: deepMergedCopy(toHTMLRenderers, customHTMLRenderer),\n      extendedAutolinks,\n      referenceDefinition,\n      frontMatter,\n      sanitizer: customHTMLSanitizer || sanitizeHTML,\n    };\n    const wwToDOMAdaptor = new WwToDOMAdaptor(linkAttributes, rendererOptions.customHTMLRenderer);\n    const htmlSchemaMap = createHTMLSchemaMap(\n      rendererOptions.customHTMLRenderer,\n      rendererOptions.sanitizer,\n      wwToDOMAdaptor\n    );\n\n    this.toastMark = new ToastMark('', {\n      disallowedHtmlBlockTags: ['br', 'img'],\n      extendedAutolinks,\n      referenceDefinition,\n      disallowDeepHeading: true,\n      frontMatter,\n      customParser: markdownParsers,\n    });\n\n    this.mdEditor = new MarkdownEditor(this.eventEmitter, {\n      toastMark: this.toastMark,\n      useCommandShortcut,\n      mdPlugins,\n    });\n\n    this.preview = new MarkdownPreview(this.eventEmitter, {\n      ...rendererOptions,\n      isViewer: false,\n      highlight: this.options.previewHighlight,\n    });\n\n    this.wwEditor = new WysiwygEditor(this.eventEmitter, {\n      toDOMAdaptor: wwToDOMAdaptor,\n      useCommandShortcut,\n      htmlSchemaMap,\n      linkAttributes,\n      wwPlugins,\n      wwNodeViews,\n    });\n\n    this.convertor = new Convertor(\n      this.wwEditor.getSchema(),\n      { ...toMarkdownRenderers, ...customMarkdownRenderer },\n      getHTMLRenderConvertors(linkAttributes, rendererOptions.customHTMLRenderer),\n      this.eventEmitter\n    );\n\n    this.setMinHeight(this.options.minHeight);\n\n    this.setHeight(this.options.height);\n\n    this.setMarkdown(this.options.initialValue, false);\n\n    if (this.options.placeholder) {\n      this.setPlaceholder(this.options.placeholder);\n    }\n\n    if (!this.options.initialValue) {\n      this.setHTML(this.initialHTML, false);\n    }\n\n    this.commandManager = new CommandManager(\n      this.eventEmitter,\n      this.mdEditor.commands,\n      this.wwEditor.commands,\n      () => this.mode\n    );\n\n    if (this.options.usageStatistics) {\n      sendHostName();\n    }\n\n    this.scrollSync = new ScrollSync(this.mdEditor, this.preview, this.eventEmitter);\n    this.addInitEvent();\n    this.addInitCommand(mdCommands, wwCommands);\n    buildQuery(this);\n\n    if (this.options.hooks) {\n      forEachOwnProperties(this.options.hooks, (fn, key) => this.addHook(key, fn));\n    }\n\n    if (this.options.events) {\n      forEachOwnProperties(this.options.events, (fn, key) => this.on(key, fn));\n    }\n\n    this.eventEmitter.emit('load', this);\n    this.moveCursorToStart(this.options.autofocus);\n  }\n\n  private addInitEvent() {\n    this.on('needChangeMode', this.changeMode.bind(this));\n    this.on('loadUI', () => {\n      if (this.height !== 'auto') {\n        // 75px equals default editor ui height - the editing area height\n        const minHeight = `${Math.min(\n          parseInt(this.minHeight, 10),\n          parseInt(this.height, 10) - 75\n        )}px`;\n\n        this.setMinHeight(minHeight);\n      }\n    });\n    addDefaultImageBlobHook(this.eventEmitter);\n  }\n\n  private addInitCommand(mdCommands: PluginCommandMap, wwCommands: PluginCommandMap) {\n    const addPluginCommands = (type: EditorType, commandMap: PluginCommandMap) => {\n      Object.keys(commandMap).forEach((name) => {\n        this.addCommand(type, name, commandMap[name]);\n      });\n    };\n\n    this.addCommand('markdown', 'toggleScrollSync', (payload) => {\n      this.eventEmitter.emit('toggleScrollSync', payload!.active);\n      return true;\n    });\n    addPluginCommands('markdown', mdCommands);\n    addPluginCommands('wysiwyg', wwCommands);\n  }\n\n  private getCurrentModeEditor() {\n    return (this.isMarkdownMode() ? this.mdEditor : this.wwEditor) as Base;\n  }\n\n  /**\n   * Factory method for Editor\n   * @param {object} options Option for initialize TUIEditor\n   * @returns {object} ToastUIEditorCore or ToastUIEditorViewer\n   */\n  static factory(options: (EditorOptions | ViewerOptions) & { viewer?: boolean }) {\n    return options.viewer ? new Viewer(options) : new ToastUIEditorCore(options as EditorOptions);\n  }\n\n  /**\n   * Set language\n   * @param {string|string[]} code - code for I18N language\n   * @param {object} data - language set\n   */\n  static setLanguage(code: string | string[], data: Record<string, string>) {\n    i18n.setLanguage(code, data);\n  }\n\n  /**\n   * change preview style\n   * @param {string} style - 'tab'|'vertical'\n   */\n  changePreviewStyle(style: PreviewStyle) {\n    if (this.mdPreviewStyle !== style) {\n      this.mdPreviewStyle = style;\n      this.eventEmitter.emit('changePreviewStyle', style);\n    }\n  }\n\n  /**\n   * execute editor command\n   * @param {string} name - command name\n   * @param {object} [payload] - payload for command\n   */\n  exec(name: string, payload?: Record<string, any>) {\n    this.commandManager.exec(name, payload);\n  }\n\n  /**\n   * @param {string} type - editor type\n   * @param {string} name - command name\n   * @param {function} command - command handler\n   */\n  addCommand(type: EditorType, name: string, command: CommandFn) {\n    const commandHoc = (paylaod: Record<string, any> = {}) => {\n      const { view } = type === 'markdown' ? this.mdEditor : this.wwEditor;\n\n      command(paylaod, view.state, view.dispatch, view);\n    };\n\n    this.commandManager.addCommand(type, name, commandHoc);\n  }\n\n  /**\n   * Bind eventHandler to event type\n   * @param {string} type Event type\n   * @param {function} handler Event handler\n   */\n  on(type: string, handler: Handler) {\n    this.eventEmitter.listen(type, handler);\n  }\n\n  /**\n   * Unbind eventHandler from event type\n   * @param {string} type Event type\n   */\n  off(type: string) {\n    this.eventEmitter.removeEventHandler(type);\n  }\n\n  /**\n   * Add hook to TUIEditor event\n   * @param {string} type Event type\n   * @param {function} handler Event handler\n   */\n  addHook(type: string, handler: Handler) {\n    this.eventEmitter.removeEventHandler(type);\n    this.eventEmitter.listen(type, handler);\n  }\n\n  /**\n   * Remove hook from TUIEditor event\n   * @param {string} type Event type\n   */\n  removeHook(type: string) {\n    this.eventEmitter.removeEventHandler(type);\n  }\n\n  /**\n   * Set focus to current Editor\n   */\n  focus() {\n    this.getCurrentModeEditor().focus();\n  }\n\n  /**\n   * Remove focus of current Editor\n   */\n  blur() {\n    this.getCurrentModeEditor().blur();\n  }\n\n  /**\n   * Set cursor position to end\n   * @param {boolean} [focus] - automatically focus the editor\n   */\n  moveCursorToEnd(focus = true) {\n    this.getCurrentModeEditor().moveCursorToEnd(focus);\n  }\n\n  /**\n   * Set cursor position to start\n   * @param {boolean} [focus] - automatically focus the editor\n   */\n  moveCursorToStart(focus = true) {\n    this.getCurrentModeEditor().moveCursorToStart(focus);\n  }\n\n  /**\n   * Set markdown syntax text.\n   * @param {string} markdown - markdown syntax text.\n   * @param {boolean} [cursorToEnd=true] - move cursor to contents end\n   */\n  setMarkdown(markdown = '', cursorToEnd = true) {\n    this.mdEditor.setMarkdown(markdown, cursorToEnd);\n\n    if (this.isWysiwygMode()) {\n      const mdNode = this.toastMark.getRootNode();\n      const wwNode = this.convertor.toWysiwygModel(mdNode);\n\n      this.wwEditor.setModel(wwNode!, cursorToEnd);\n    }\n  }\n\n  /**\n   * Set html value.\n   * @param {string} html - html syntax text\n   * @param {boolean} [cursorToEnd=true] - move cursor to contents end\n   */\n  setHTML(html = '', cursorToEnd = true) {\n    const container = document.createElement('div');\n\n    // the `br` tag should be replaced with empty block to separate between blocks\n    container.innerHTML = replaceBRWithEmptyBlock(html);\n    const wwNode = DOMParser.fromSchema(this.wwEditor.schema).parse(container);\n\n    if (this.isMarkdownMode()) {\n      this.mdEditor.setMarkdown(this.convertor.toMarkdownText(wwNode), cursorToEnd);\n    } else {\n      this.wwEditor.setModel(wwNode, cursorToEnd);\n    }\n  }\n\n  /**\n   * Get content to markdown\n   * @returns {string} markdown text\n   */\n  getMarkdown() {\n    if (this.isMarkdownMode()) {\n      return this.mdEditor.getMarkdown();\n    }\n\n    return this.convertor.toMarkdownText(this.wwEditor.getModel());\n  }\n\n  /**\n   * Get content to html\n   * @returns {string} html string\n   */\n  getHTML() {\n    this.eventEmitter.holdEventInvoke(() => {\n      if (this.isMarkdownMode()) {\n        const mdNode = this.toastMark.getRootNode();\n        const wwNode = this.convertor.toWysiwygModel(mdNode);\n\n        this.wwEditor.setModel(wwNode!);\n      }\n    });\n    const html = removeProseMirrorHackNodes(this.wwEditor.view.dom.innerHTML);\n\n    if (this.placeholder) {\n      const rePlaceholder = new RegExp(\n        `<span class=\"placeholder[^>]+>${this.placeholder}</span>`,\n        'i'\n      );\n\n      return html.replace(rePlaceholder, '');\n    }\n\n    return html;\n  }\n\n  /**\n   * Insert text\n   * @param {string} text - text content\n   */\n  insertText(text: string) {\n    this.getCurrentModeEditor().replaceSelection(text);\n  }\n\n  /**\n   * Set selection range\n   * @param {number|Array.<number>} start - start position\n   * @param {number|Array.<number>} end - end position\n   */\n  setSelection(start: EditorPos, end?: EditorPos) {\n    this.getCurrentModeEditor().setSelection(start, end);\n  }\n\n  /**\n   * Replace selection range with given text content\n   * @param {string} text - text content\n   * @param {number|Array.<number>} [start] - start position\n   * @param {number|Array.<number>} [end] - end position\n   */\n  replaceSelection(text: string, start?: EditorPos, end?: EditorPos) {\n    this.getCurrentModeEditor().replaceSelection(text, start, end);\n  }\n\n  /**\n   * Delete the content of selection range\n   * @param {number|Array.<number>} [start] - start position\n   * @param {number|Array.<number>} [end] - end position\n   */\n  deleteSelection(start?: EditorPos, end?: EditorPos) {\n    this.getCurrentModeEditor().deleteSelection(start, end);\n  }\n\n  /**\n   * Get selected text content\n   * @param {number|Array.<number>} [start] - start position\n   * @param {number|Array.<number>} [end] - end position\n   * @returns {string} - selected text content\n   */\n  getSelectedText(start?: EditorPos, end?: EditorPos) {\n    return this.getCurrentModeEditor().getSelectedText(start, end);\n  }\n\n  /**\n   * Get range of the node\n   * @param {number|Array.<number>} [pos] - position\n   * @returns {Array.<number[]>|Array.<number>} - node [start, end] range\n   * @example\n   * // Markdown mode\n   * const rangeInfo = editor.getRangeInfoOfNode();\n   *\n   * console.log(rangeInfo); // { range: [[startLineOffset, startCurorOffset], [endLineOffset, endCurorOffset]], type: 'emph' }\n   *\n   * // WYSIWYG mode\n   * const rangeInfo = editor.getRangeInfoOfNode();\n   *\n   * console.log(rangeInfo); // { range: [startCursorOffset, endCursorOffset], type: 'emph' }\n   */\n  getRangeInfoOfNode(pos?: EditorPos) {\n    return this.getCurrentModeEditor().getRangeInfoOfNode(pos);\n  }\n\n  /**\n   * Add widget to selection\n   * @param {Node} node - widget node\n   * @param {string} style - Adding style \"top\" or \"bottom\"\n   * @param {number|Array.<number>} [pos] - position\n   */\n  addWidget(node: Node, style: WidgetStyle, pos?: EditorPos) {\n    this.getCurrentModeEditor().addWidget(node, style, pos);\n  }\n\n  /**\n   * Replace node with widget to range\n   * @param {number|Array.<number>} start - start position\n   * @param {number|Array.<number>} end - end position\n   * @param {string} text - widget text content\n   */\n  replaceWithWidget(start: EditorPos, end: EditorPos, text: string) {\n    this.getCurrentModeEditor().replaceWithWidget(start, end, text);\n  }\n\n  /**\n   * Set editor height\n   * @param {string} height - editor height in pixel\n   */\n  setHeight(height: string) {\n    const { el } = this.options;\n\n    if (isString(height)) {\n      if (height === 'auto') {\n        addClass(el, 'auto-height');\n      } else {\n        removeClass(el, 'auto-height');\n      }\n      this.setMinHeight(this.getMinHeight());\n    }\n\n    css(el, { height });\n    this.height = height;\n  }\n\n  /**\n   * Get editor height\n   * @returns {string} editor height in pixel\n   */\n  getHeight() {\n    return this.height;\n  }\n\n  /**\n   * Set minimum height to editor content\n   * @param {string} minHeight - min content height in pixel\n   */\n  setMinHeight(minHeight: string) {\n    if (minHeight !== this.minHeight) {\n      const height = this.height || this.options.height;\n\n      if (height !== 'auto' && this.options.el.querySelector(`.${cls('main')}`)) {\n        // 75px equals default editor ui height - the editing area height\n        minHeight = `${Math.min(parseInt(minHeight, 10), parseInt(height, 10) - 75)}px`;\n      }\n\n      const minHeightNum = parseInt(minHeight, 10);\n\n      this.minHeight = minHeight;\n\n      this.wwEditor.setMinHeight(minHeightNum);\n      this.mdEditor.setMinHeight(minHeightNum);\n      this.preview.setMinHeight(minHeightNum);\n    }\n  }\n\n  /**\n   * Get minimum height of editor content\n   * @returns {string} min height in pixel\n   */\n  getMinHeight() {\n    return this.minHeight;\n  }\n\n  /**\n   * Return true if current editor mode is Markdown\n   * @returns {boolean}\n   */\n  isMarkdownMode() {\n    return this.mode === 'markdown';\n  }\n\n  /**\n   * Return true if current editor mode is WYSIWYG\n   * @returns {boolean}\n   */\n  isWysiwygMode() {\n    return this.mode === 'wysiwyg';\n  }\n\n  /**\n   * Return false\n   * @returns {boolean}\n   */\n  isViewer() {\n    return false;\n  }\n\n  /**\n   * Get current Markdown editor's preview style\n   * @returns {string}\n   */\n  getCurrentPreviewStyle() {\n    return this.mdPreviewStyle;\n  }\n\n  /**\n   * Change editor's mode to given mode string\n   * @param {string} mode - Editor mode name of want to change\n   * @param {boolean} [withoutFocus] - Change mode without focus\n   */\n  changeMode(mode: EditorType, withoutFocus?: boolean) {\n    if (this.mode === mode) {\n      return;\n    }\n\n    this.mode = mode;\n\n    if (this.isWysiwygMode()) {\n      const mdNode = this.toastMark.getRootNode();\n      const wwNode = this.convertor.toWysiwygModel(mdNode);\n\n      this.wwEditor.setModel(wwNode!);\n    } else {\n      const wwNode = this.wwEditor.getModel();\n\n      this.mdEditor.setMarkdown(this.convertor.toMarkdownText(wwNode), !withoutFocus);\n    }\n\n    this.eventEmitter.emit('removePopupWidget');\n    this.eventEmitter.emit('changeMode', mode);\n\n    if (!withoutFocus) {\n      const pos = this.convertor.getMappedPos();\n\n      this.focus();\n\n      if (this.isWysiwygMode() && isNumber(pos)) {\n        this.wwEditor.setSelection(pos);\n      } else if (Array.isArray(pos)) {\n        this.mdEditor.setSelection(pos);\n      }\n    }\n  }\n\n  /**\n   * Destroy TUIEditor from document\n   */\n  destroy() {\n    this.wwEditor.destroy();\n    this.mdEditor.destroy();\n    this.preview.destroy();\n    this.scrollSync.destroy();\n    this.eventEmitter.emit('destroy');\n    this.eventEmitter.getEvents().forEach((_, type: string) => this.off(type));\n  }\n\n  /**\n   * Hide TUIEditor\n   */\n  hide() {\n    this.eventEmitter.emit('hide');\n  }\n\n  /**\n   * Show TUIEditor\n   */\n  show() {\n    this.eventEmitter.emit('show');\n  }\n\n  /**\n   * Move on scroll position of the editor container\n   * @param {number} value scrollTop value of editor container\n   */\n  setScrollTop(value: number) {\n    this.getCurrentModeEditor().setScrollTop(value);\n  }\n\n  /**\n   * Get scroll position value of editor container\n   * @returns {number} scrollTop value of editor container\n   */\n  getScrollTop() {\n    return this.getCurrentModeEditor().getScrollTop();\n  }\n\n  /**\n   * Reset TUIEditor\n   */\n  reset() {\n    this.wwEditor.setModel([]);\n    this.mdEditor.setMarkdown('');\n  }\n\n  /**\n   * Get current selection range\n   * @returns {Array.<number[]>|Array.<number>} Returns the range of the selection depending on the editor mode\n   * @example\n   * // Markdown mode\n   * const mdSelection = editor.getSelection();\n   *\n   * console.log(mdSelection); // [[startLineOffset, startCurorOffset], [endLineOffset, endCurorOffset]]\n   *\n   * // WYSIWYG mode\n   * const wwSelection = editor.getSelection();\n   *\n   * console.log(wwSelection); // [startCursorOffset, endCursorOffset]\n   */\n  getSelection() {\n    return this.getCurrentModeEditor().getSelection();\n  }\n\n  /**\n   * Set the placeholder on all editors\n   * @param {string} placeholder - placeholder to set\n   */\n  setPlaceholder(placeholder: string) {\n    this.placeholder = placeholder;\n    this.mdEditor.setPlaceholder(placeholder);\n    this.wwEditor.setPlaceholder(placeholder);\n  }\n\n  /**\n   * Get markdown editor, preview, wysiwyg editor DOM elements\n   */\n  getEditorElements() {\n    return {\n      mdEditor: this.mdEditor.getElement(),\n      mdPreview: this.preview.getElement(),\n      wwEditor: this.wwEditor.getElement(),\n    };\n  }\n\n  /**\n   * Convert position to match editor mode\n   * @param {number|Array.<number>} start - start position\n   * @param {number|Array.<number>} end - end position\n   * @param {string} mode - Editor mode name of want to match converted position to\n   */\n  convertPosToMatchEditorMode(start: EditorPos, end = start, mode = this.mode) {\n    const { doc } = this.mdEditor.view.state;\n    const isFromArray = Array.isArray(start);\n    const isToArray = Array.isArray(end);\n\n    let convertedFrom = start;\n    let convertedTo = end;\n\n    if (isFromArray !== isToArray) {\n      throw new Error('Types of arguments must be same');\n    }\n\n    if (mode === 'markdown' && !isFromArray && !isToArray) {\n      [convertedFrom, convertedTo] = getEditorToMdPos(doc, start as number, end as number);\n    } else if (mode === 'wysiwyg' && isFromArray && isToArray) {\n      [convertedFrom, convertedTo] = getMdToEditorPos(doc, start as Pos, end as Pos);\n    }\n\n    return [convertedFrom, convertedTo];\n  }\n}\n\n// // (Not an official API)\n// // Create a function converting markdown to HTML using the internal parser and renderer.\n// ToastUIEditor._createMarkdownToHTML = createMarkdownToHTML;\n\nexport default ToastUIEditorCore;\n"
  },
  {
    "path": "apps/editor/src/esm/index.ts",
    "content": "import EditorCore from '@/editorCore';\nimport Editor from '@/editor';\n\nimport '@/i18n/en-us';\n\nexport default Editor;\nexport { Editor, EditorCore };\n"
  },
  {
    "path": "apps/editor/src/esm/indexViewer.ts",
    "content": "import Viewer from '@/viewer';\n\nexport default Viewer;\n"
  },
  {
    "path": "apps/editor/src/event/eventEmitter.ts",
    "content": "import isUndefined from 'tui-code-snippet/type/isUndefined';\nimport isFalsy from 'tui-code-snippet/type/isFalsy';\nimport { Emitter, EventTypes, Handler } from '@t/event';\nimport Map from '@/utils/map';\n\nconst eventTypeList: EventTypes[] = [\n  'afterPreviewRender',\n  'updatePreview',\n  'changeMode',\n  'needChangeMode',\n  'command',\n  'changePreviewStyle',\n  'changePreviewTabPreview',\n  'changePreviewTabWrite',\n  'scroll',\n  'contextmenu',\n  'show',\n  'hide',\n  'changeLanguage',\n  'changeToolbarState',\n  'toggleScrollSync',\n  'mixinTableOffsetMapPrototype',\n  'setFocusedNode',\n  'removePopupWidget',\n  'query',\n  // provide event for user\n  'openPopup',\n  'closePopup',\n  'addImageBlobHook',\n  'beforePreviewRender',\n  'beforeConvertWysiwygToMarkdown',\n  'load',\n  'loadUI',\n  'change',\n  'caretChange',\n  'destroy',\n  'focus',\n  'blur',\n  'keydown',\n  'keyup',\n];\n\n/**\n * Class EventEmitter\n * @ignore\n */\nclass EventEmitter implements Emitter {\n  private events: Map<string, Handler[] | undefined>;\n\n  private eventTypes: Record<string, string>;\n\n  private hold: boolean;\n\n  constructor() {\n    this.events = new Map();\n    this.eventTypes = eventTypeList.reduce((types, type) => {\n      return { ...types, type };\n    }, {});\n    this.hold = false;\n\n    eventTypeList.forEach((eventType) => {\n      this.addEventType(eventType);\n    });\n  }\n\n  /**\n   * Listen event and bind event handler\n   * @param {string} type Event type string\n   * @param {function} handler Event handler\n   */\n  listen(type: string, handler: Handler) {\n    const typeInfo = this.getTypeInfo(type);\n    const eventHandlers = this.events.get(typeInfo.type) || [];\n\n    if (!this.hasEventType(typeInfo.type)) {\n      throw new Error(`There is no event type ${typeInfo.type}`);\n    }\n\n    if (typeInfo.namespace) {\n      handler.namespace = typeInfo.namespace;\n    }\n\n    eventHandlers.push(handler);\n\n    this.events.set(typeInfo.type, eventHandlers);\n  }\n\n  /**\n   * Emit event\n   * @param {string} eventName Event name to emit\n   * @returns {Array}\n   */\n  emit(type: string, ...args: any[]) {\n    const typeInfo = this.getTypeInfo(type);\n    const eventHandlers = this.events.get(typeInfo.type);\n    const results: any[] = [];\n\n    if (!this.hold && eventHandlers) {\n      eventHandlers.forEach((handler) => {\n        const result = handler(...args);\n\n        if (!isUndefined(result)) {\n          results.push(result);\n        }\n      });\n    }\n\n    return results;\n  }\n\n  /**\n   * Emit given event and return result\n   * @param {string} eventName Event name to emit\n   * @param {any} source Source to change\n   * @returns {string}\n   */\n  emitReduce(type: string, source: any, ...args: any[]) {\n    const eventHandlers = this.events.get(type);\n\n    if (!this.hold && eventHandlers) {\n      eventHandlers.forEach((handler) => {\n        const result = handler(source, ...args);\n\n        if (!isFalsy(result)) {\n          source = result;\n        }\n      });\n    }\n\n    return source;\n  }\n\n  /**\n   * Get event type and namespace\n   * @param {string} type Event type name\n   * @returns {{type: string, namespace: string}}\n   * @private\n   */\n  private getTypeInfo(type: string) {\n    const splited = type.split('.');\n\n    return {\n      type: splited[0],\n      namespace: splited[1],\n    };\n  }\n\n  /**\n   * Check whether event type exists or not\n   * @param {string} type Event type name\n   * @returns {boolean}\n   * @private\n   */\n  private hasEventType(type: string) {\n    return !isUndefined(this.eventTypes[this.getTypeInfo(type).type]);\n  }\n\n  /**\n   * Add event type when given event not exists\n   * @param {string} type Event type name\n   */\n  addEventType(type: string) {\n    if (this.hasEventType(type)) {\n      throw new Error(`There is already have event type ${type}`);\n    }\n\n    this.eventTypes[type] = type;\n  }\n\n  /**\n   * Remove event handler from given event type\n   * @param {string} eventType Event type name\n   * @param {function} [handler] - registered event handler\n   */\n  removeEventHandler(eventType: string, handler?: Handler) {\n    const { type, namespace } = this.getTypeInfo(eventType);\n\n    if (type && handler) {\n      this.removeEventHandlerWithHandler(type, handler);\n    } else if (type && !namespace) {\n      this.events.delete(type);\n    } else if (!type && namespace) {\n      this.events.forEach((_, evtType) => {\n        this.removeEventHandlerWithTypeInfo(evtType, namespace);\n      });\n    } else if (type && namespace) {\n      this.removeEventHandlerWithTypeInfo(type, namespace);\n    }\n  }\n\n  /**\n   * Remove event handler with event handler\n   * @param {string} type - event type name\n   * @param {function} handler - event handler\n   * @private\n   */\n  private removeEventHandlerWithHandler(type: string, handler: Handler) {\n    const eventHandlers = this.events.get(type);\n\n    if (eventHandlers) {\n      const handlerIndex = eventHandlers.indexOf(handler);\n\n      if (eventHandlers.indexOf(handler) >= 0) {\n        eventHandlers.splice(handlerIndex, 1);\n      }\n    }\n  }\n\n  /**\n   * Remove event handler with event type information\n   * @param {string} type Event type name\n   * @param {string} namespace Event namespace\n   * @private\n   */\n  private removeEventHandlerWithTypeInfo(type: string, namespace: string) {\n    const handlersToSurvive: Handler[] = [];\n    const eventHandlers = this.events.get(type);\n\n    if (!eventHandlers) {\n      return;\n    }\n\n    eventHandlers.map((handler: Handler) => {\n      if (handler.namespace !== namespace) {\n        handlersToSurvive.push(handler);\n      }\n\n      return null;\n    });\n\n    this.events.set(type, handlersToSurvive);\n  }\n\n  getEvents() {\n    return this.events;\n  }\n\n  holdEventInvoke(fn: Function) {\n    this.hold = true;\n    fn();\n    this.hold = false;\n  }\n}\n\nexport default EventEmitter;\n"
  },
  {
    "path": "apps/editor/src/helper/image.ts",
    "content": "import toArray from 'tui-code-snippet/collection/toArray';\n\nimport { HookCallback } from '@t/editor';\nimport { Emitter } from '@t/event';\n\nexport function addDefaultImageBlobHook(eventEmitter: Emitter) {\n  eventEmitter.listen('addImageBlobHook', (blob: File, callback: HookCallback) => {\n    const reader = new FileReader();\n\n    reader.onload = ({ target }) => callback(target!.result as string);\n    reader.readAsDataURL(blob);\n  });\n}\n\nexport function emitImageBlobHook(eventEmitter: Emitter, blob: File, type: string) {\n  const hook: HookCallback = (imageUrl, altText) => {\n    eventEmitter.emit('command', 'addImage', {\n      imageUrl,\n      altText: altText || blob.name || 'image',\n    });\n  };\n\n  eventEmitter.emit('addImageBlobHook', blob, hook, type);\n}\n\nexport function pasteImageOnly(items: DataTransferItemList) {\n  const images = toArray(items).filter(({ type }) => type.indexOf('image') !== -1);\n\n  if (images.length === 1) {\n    const [item] = images;\n\n    if (item) {\n      return item.getAsFile();\n    }\n  }\n\n  return null;\n}\n"
  },
  {
    "path": "apps/editor/src/helper/manipulation.ts",
    "content": "import { TextSelection, Transaction, EditorState } from 'prosemirror-state';\nimport { ProsemirrorNode, Schema, Mark, ResolvedPos, Fragment } from 'prosemirror-model';\n\nimport isString from 'tui-code-snippet/type/isString';\n\ninterface ReplacePayload {\n  state: EditorState;\n  from: number;\n  startIndex: number;\n  endIndex: number;\n  createText: (textContent: string) => string;\n}\n\nexport function createParagraph(schema: Schema, content?: string | ProsemirrorNode[]) {\n  const { paragraph } = schema.nodes;\n\n  if (!content) {\n    return paragraph.createAndFill()!;\n  }\n  return paragraph.create(null, isString(content) ? schema.text(content) : content);\n}\n\nexport function createTextNode(schema: Schema, text: string, marks?: Mark[]) {\n  return schema.text(text, marks);\n}\n\nexport function createTextSelection(tr: Transaction, from: number, to = from) {\n  const contentSize = tr.doc.content.size;\n  const size = contentSize > 0 ? contentSize - 1 : 1;\n\n  return TextSelection.create(tr.doc, Math.min(from, size), Math.min(to, size));\n}\n\nexport function addParagraph(tr: Transaction, { pos }: ResolvedPos, schema: Schema) {\n  tr.replaceWith(pos, pos, createParagraph(schema));\n\n  return tr.setSelection(createTextSelection(tr, pos + 1));\n}\n\nexport function replaceTextNode({ state, from, startIndex, endIndex, createText }: ReplacePayload) {\n  const { tr, doc, schema } = state;\n\n  for (let i = startIndex; i <= endIndex; i += 1) {\n    const { nodeSize, textContent, content } = doc.child(i);\n    const text = createText(textContent);\n    const node = text ? createTextNode(schema, text) : Fragment.empty;\n    const mappedFrom = tr.mapping.map(from);\n    const mappedTo = mappedFrom + content.size;\n\n    tr.replaceWith(mappedFrom, mappedTo, node);\n    from += nodeSize;\n  }\n  return tr;\n}\n\nexport function splitAndExtendBlock(\n  tr: Transaction,\n  pos: number,\n  text: string,\n  node: ProsemirrorNode\n) {\n  const textLen = text.length;\n\n  (tr.split(pos) as Transaction)\n    .delete(pos - textLen, pos)\n    .insert(tr.mapping.map(pos), node)\n    .setSelection(createTextSelection(tr, tr.mapping.map(pos) - textLen));\n}\n"
  },
  {
    "path": "apps/editor/src/helper/plugin.ts",
    "content": "import isArray from 'tui-code-snippet/type/isArray';\nimport { Plugin, PluginKey, Selection, TextSelection } from 'prosemirror-state';\nimport { inputRules, InputRule, undoInputRule } from 'prosemirror-inputrules';\nimport { Decoration, DecorationSet } from 'prosemirror-view';\nimport { keymap } from 'prosemirror-keymap';\nimport { Fragment } from 'prosemirror-model';\nimport i18n from '@/i18n/i18n';\nimport { deepMergedCopy } from '@/utils/common';\n\nimport { EditorPluginInfo, EditorPluginsInfo } from '@t/editor';\nimport { PluginInfoResult } from '@t/plugin';\nimport { mixinTableOffsetMapPrototype } from '@/wysiwyg/helper/tableOffsetMap';\n\nfunction execPlugin(pluginInfo: EditorPluginInfo) {\n  const { plugin, eventEmitter, usageStatistics, instance } = pluginInfo;\n\n  const pmState = { Plugin, PluginKey, Selection, TextSelection };\n  const pmView = { Decoration, DecorationSet };\n  const pmModel = { Fragment };\n  const pmRules = { InputRule, inputRules, undoInputRule };\n  const pmKeymap = { keymap };\n  const context = {\n    eventEmitter,\n    usageStatistics,\n    instance,\n    pmState,\n    pmView,\n    pmModel,\n    pmRules,\n    pmKeymap,\n    i18n,\n  };\n\n  if (isArray(plugin)) {\n    const [pluginFn, options = {}] = plugin;\n\n    return pluginFn(context, options);\n  }\n\n  return plugin(context);\n}\n\nexport function getPluginInfo(pluginsInfo: EditorPluginsInfo) {\n  const { plugins, eventEmitter, usageStatistics, instance } = pluginsInfo;\n\n  eventEmitter.listen('mixinTableOffsetMapPrototype', mixinTableOffsetMapPrototype);\n\n  return (plugins ?? []).reduce<PluginInfoResult>(\n    (acc, plugin) => {\n      const pluginInfoResult = execPlugin({\n        plugin,\n        eventEmitter,\n        usageStatistics,\n        instance,\n      });\n\n      if (!pluginInfoResult) {\n        throw new Error('The return value of the executed plugin is empty.');\n      }\n\n      const {\n        markdownParsers,\n        toHTMLRenderers,\n        toMarkdownRenderers,\n        markdownPlugins,\n        wysiwygPlugins,\n        wysiwygNodeViews,\n        markdownCommands,\n        wysiwygCommands,\n        toolbarItems,\n      } = pluginInfoResult;\n\n      if (toHTMLRenderers) {\n        acc.toHTMLRenderers = deepMergedCopy(acc.toHTMLRenderers, toHTMLRenderers);\n      }\n\n      if (toMarkdownRenderers) {\n        acc.toMarkdownRenderers = deepMergedCopy(acc.toMarkdownRenderers, toMarkdownRenderers);\n      }\n\n      if (markdownPlugins) {\n        acc.mdPlugins = acc.mdPlugins!.concat(markdownPlugins);\n      }\n\n      if (wysiwygPlugins) {\n        acc.wwPlugins = acc.wwPlugins!.concat(wysiwygPlugins);\n      }\n\n      if (wysiwygNodeViews) {\n        acc.wwNodeViews = { ...acc.wwNodeViews, ...wysiwygNodeViews };\n      }\n\n      if (markdownCommands) {\n        acc.mdCommands = { ...acc.mdCommands, ...markdownCommands };\n      }\n\n      if (wysiwygCommands) {\n        acc.wwCommands = { ...acc.wwCommands, ...wysiwygCommands };\n      }\n\n      if (toolbarItems) {\n        acc.toolbarItems = acc.toolbarItems!.concat(toolbarItems);\n      }\n\n      if (markdownParsers) {\n        acc.markdownParsers = { ...acc.markdownParsers, ...markdownParsers };\n      }\n\n      return acc;\n    },\n    {\n      toHTMLRenderers: {},\n      toMarkdownRenderers: {},\n      mdPlugins: [],\n      wwPlugins: [],\n      wwNodeViews: {},\n      mdCommands: {},\n      wwCommands: {},\n      toolbarItems: [],\n      markdownParsers: {},\n    }\n  );\n}\n"
  },
  {
    "path": "apps/editor/src/i18n/ar.ts",
    "content": "/**\n * @fileoverview I18N for Arabic\n * @author Amira Salah <amira.salah@itworx.com>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage('ar', {\n  Markdown: 'لغة ترميز',\n  WYSIWYG: 'ما تراه هو ما تحصل عليه',\n  Write: 'يكتب',\n  Preview: 'عرض مسبق',\n  Headings: 'العناوين',\n  Paragraph: 'فقرة',\n  Bold: 'خط عريض',\n  Italic: 'خط مائل',\n  Strike: 'إضراب',\n  Code: 'رمز',\n  Line: 'خط',\n  Blockquote: 'فقرة مقتبسة',\n  'Unordered list': 'قائمة غير مرتبة',\n  'Ordered list': 'قائمة مرتبة',\n  Task: 'مهمة',\n  Indent: 'المسافة البادئة',\n  Outdent: 'المسافة الخارجة',\n  'Insert link': 'أدخل الرابط',\n  'Insert CodeBlock': 'أدخل الكود',\n  'Insert table': 'أدخل جدول',\n  'Insert image': 'أدخل صورة',\n  Heading: 'عنوان',\n  'Image URL': 'رابط الصورة',\n  'Select image file': 'حدد ملف الصورة',\n  'Choose a file': 'اختيار الملف',\n  'No file': 'لا ملف',\n  Description: 'وصف',\n  OK: 'موافقة',\n  More: 'أكثر',\n  Cancel: 'إلغاء',\n  File: 'ملف',\n  URL: 'رابط',\n  'Link text': 'نص الرابط',\n  'Add row to up': 'أضف صفًا لأعلى',\n  'Add row to down': 'أضف صفًا إلى أسفل',\n  'Add column to left': 'أضف العمود على اليسار',\n  'Add column to right': 'أضف عمودًا إلى اليمين',\n  'Remove row': 'حذف سطر',\n  'Remove column': 'حذف عمود',\n  'Align column to left': 'محاذاة اليسار',\n  'Align column to center': 'محاذاة الوسط',\n  'Align column to right': 'محاذاة اليمين',\n  'Remove table': 'حذف الجدول',\n  'Would you like to paste as table?': 'هل تريد اللصق كجدول',\n  'Text color': 'لون النص',\n  'Auto scroll enabled': 'التحريك التلقائي ممكّن',\n  'Auto scroll disabled': 'التحريك التلقائي معطّل',\n  'Choose language': 'اختر اللغة',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/cs-cz.ts",
    "content": "/**\n * @fileoverview I18N for Czech\n * @author Dmitrij Tkačenko <dmitrij.tkacenko@scalesoft.cz>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage(['cs', 'cs-CZ'], {\n  Markdown: 'Markdown',\n  WYSIWYG: 'WYSIWYG',\n  Write: 'Napsat',\n  Preview: 'Náhled',\n  Headings: 'Nadpisy',\n  Paragraph: 'Odstavec',\n  Bold: 'Tučné',\n  Italic: 'Kurzíva',\n  Strike: 'Přeškrtnuté',\n  Code: 'Kód',\n  Line: 'Vodorovná čára',\n  Blockquote: 'Citace',\n  'Unordered list': 'Seznam s odrážkami',\n  'Ordered list': 'Číslovaný seznam',\n  Task: 'Úkol',\n  Indent: 'Zvětšit odsazení',\n  Outdent: 'Zmenšit odsazení',\n  'Insert link': 'Vložit odkaz',\n  'Insert CodeBlock': 'Vložit blok kódu',\n  'Insert table': 'Vložit tabulku',\n  'Insert image': 'Vložit obrázek',\n  Heading: 'Nadpis',\n  'Image URL': 'URL obrázku',\n  'Select image file': 'Vybrat obrázek',\n  'Choose a file': 'Vyberte soubor',\n  'No file': 'Žádný soubor',\n  Description: 'Popis',\n  OK: 'OK',\n  More: 'Více',\n  Cancel: 'Zrušit',\n  File: 'Soubor',\n  URL: 'URL',\n  'Link text': 'Text odkazu',\n  'Add row to up': 'Přidejte řádek nahoru',\n  'Add row to down': 'Přidejte řádek dolů',\n  'Add column to left': 'Přidat sloupec vlevo',\n  'Add column to right': 'Přidat sloupec doprava',\n  'Remove row': 'Odebrat řádek',\n  'Remove column': 'Odebrat sloupec',\n  'Align column to left': 'Zarovnat vlevo',\n  'Align column to center': 'Zarovnat na střed',\n  'Align column to right': 'Zarovnat vpravo',\n  'Remove table': 'Odstranit tabulku',\n  'Would you like to paste as table?': 'Chcete vložit jako tabulku?',\n  'Text color': 'Barva textu',\n  'Auto scroll enabled': 'Automatické rolování zapnuto',\n  'Auto scroll disabled': 'Automatické rolování vypnuto',\n  'Choose language': 'Vybrat jazyk',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/de-de.ts",
    "content": "/**\n * @fileoverview I18N for German\n * @author Jann-Niklas Kiepert <jannkiepert@vivaldi.net>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage(['de', 'de-DE'], {\n  Markdown: 'Markdown',\n  WYSIWYG: 'WYSIWYG',\n  Write: 'Verfassen',\n  Preview: 'Vorschau',\n  Headings: 'Überschriften',\n  Paragraph: 'Text',\n  Bold: 'Fett',\n  Italic: 'Kursiv',\n  Strike: 'Durchgestrichen',\n  Code: 'Code',\n  Line: 'Trennlinie',\n  Blockquote: 'Blocktext',\n  'Unordered list': 'Aufzählung',\n  'Ordered list': 'Nummerierte Aufzählung',\n  Task: 'Aufgabe',\n  Indent: 'Einrücken',\n  Outdent: 'Ausrücken',\n  'Insert link': 'Link einfügen',\n  'Insert CodeBlock': 'Codeblock einfügen',\n  'Insert table': 'Tabelle einfügen',\n  'Insert image': 'Grafik einfügen',\n  Heading: 'Titel',\n  'Image URL': 'Bild URL',\n  'Select image file': 'Grafik auswählen',\n  'Choose a file': 'Wähle eine Datei',\n  'No file': 'Keine Datei',\n  Description: 'Beschreibung',\n  OK: 'OK',\n  More: 'Mehr',\n  Cancel: 'Abbrechen',\n  File: 'Datei',\n  URL: 'URL',\n  'Link text': 'Anzuzeigender Text',\n  'Add row to up': 'Zeile nach oben hinzufügen',\n  'Add row to down': 'Zeile nach unten hinzufügen',\n  'Add column to left': 'Spalte links hinzufügen',\n  'Add column to right': 'Spalte rechts hinzufügen',\n  'Remove row': 'Zeile entfernen',\n  'Remove column': 'Spalte entfernen',\n  'Align column to left': 'Links ausrichten',\n  'Align column to center': 'Zentrieren',\n  'Align column to right': 'Rechts ausrichten',\n  'Remove table': 'Tabelle entfernen',\n  'Would you like to paste as table?': 'Möchten Sie eine Tabelle einfügen?',\n  'Text color': 'Textfarbe',\n  'Auto scroll enabled': 'Autoscrollen aktiviert',\n  'Auto scroll disabled': 'Autoscrollen deaktiviert',\n  'Choose language': 'Sprache auswählen',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/en-us.ts",
    "content": "/**\n * @fileoverview I18N for English\n * @author NHN Cloud FE Development Lab <dl_javascript@nhn.com>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage(['en', 'en-US'], {\n  Markdown: 'Markdown',\n  WYSIWYG: 'WYSIWYG',\n  Write: 'Write',\n  Preview: 'Preview',\n  Headings: 'Headings',\n  Paragraph: 'Paragraph',\n  Bold: 'Bold',\n  Italic: 'Italic',\n  Strike: 'Strike',\n  Code: 'Inline code',\n  Line: 'Line',\n  Blockquote: 'Blockquote',\n  'Unordered list': 'Unordered list',\n  'Ordered list': 'Ordered list',\n  Task: 'Task',\n  Indent: 'Indent',\n  Outdent: 'Outdent',\n  'Insert link': 'Insert link',\n  'Insert CodeBlock': 'Insert codeBlock',\n  'Insert table': 'Insert table',\n  'Insert image': 'Insert image',\n  Heading: 'Heading',\n  'Image URL': 'Image URL',\n  'Select image file': 'Select image file',\n  'Choose a file': 'Choose a file',\n  'No file': 'No file',\n  Description: 'Description',\n  OK: 'OK',\n  More: 'More',\n  Cancel: 'Cancel',\n  File: 'File',\n  URL: 'URL',\n  'Link text': 'Link text',\n  'Add row to up': 'Add row to up',\n  'Add row to down': 'Add row to down',\n  'Add column to left': 'Add column to left',\n  'Add column to right': 'Add column to right',\n  'Remove row': 'Remove row',\n  'Remove column': 'Remove column',\n  'Align column to left': 'Align column to left',\n  'Align column to center': 'Align column to center',\n  'Align column to right': 'Align column to right',\n  'Remove table': 'Remove table',\n  'Would you like to paste as table?': 'Would you like to paste as table?',\n  'Text color': 'Text color',\n  'Auto scroll enabled': 'Auto scroll enabled',\n  'Auto scroll disabled': 'Auto scroll disabled',\n  'Choose language': 'Choose language',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/es-es.ts",
    "content": "/**\n * @fileoverview I18N for Spanish\n * @author Enrico Lamperti <oss@elamperti.com>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage(['es', 'es-ES'], {\n  Markdown: 'Markdown',\n  WYSIWYG: 'WYSIWYG',\n  Write: 'Escribir',\n  Preview: 'Vista previa',\n  Headings: 'Encabezados',\n  Paragraph: 'Párrafo',\n  Bold: 'Negrita',\n  Italic: 'Itálica',\n  Strike: 'Tachado',\n  Code: 'Código',\n  Line: 'Línea',\n  Blockquote: 'Cita',\n  'Unordered list': 'Lista desordenada',\n  'Ordered list': 'Lista ordenada',\n  Task: 'Tarea',\n  Indent: 'Sangría',\n  Outdent: 'Saliendo',\n  'Insert link': 'Insertar enlace',\n  'Insert CodeBlock': 'Insertar bloque de código',\n  'Insert table': 'Insertar tabla',\n  'Insert image': 'Insertar imagen',\n  Heading: 'Encabezado',\n  'Image URL': 'URL de la imagen',\n  'Select image file': 'Seleccionar archivo de imagen',\n  'Choose a file': 'Escoge un archivo',\n  'No file': 'Ningún archivo',\n  Description: 'Descripción',\n  OK: 'Aceptar',\n  More: 'Más',\n  Cancel: 'Cancelar',\n  File: 'Archivo',\n  URL: 'URL',\n  'Link text': 'Texto del enlace',\n  'Add row to up': 'Agregar fila para subir',\n  'Add row to down': 'Agregar fila hacia abajo',\n  'Add column to left': 'Agregar columna a la izquierda',\n  'Add column to right': 'Agregar columna a la derecha',\n  'Remove row': 'Eliminar fila',\n  'Remove column': 'Eliminar columna',\n  'Align column to left': 'Alinear a la izquierda',\n  'Align column to center': 'Centrar',\n  'Align column to right': 'Alinear a la derecha',\n  'Remove table': 'Eliminar tabla',\n  'Would you like to paste as table?': '¿Desea pegar como tabla?',\n  'Text color': 'Color del texto',\n  'Auto scroll enabled': 'Desplazamiento automático habilitado',\n  'Auto scroll disabled': 'Desplazamiento automático deshabilitado',\n  'Choose language': 'Elegir idioma',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/fi-fi.ts",
    "content": "/**\n * @fileoverview I18N for Finnish\n * @author Tomi Mynttinen <pikseli@iki.fi>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage(['fi', 'fi-FI'], {\n  Markdown: 'Markdown',\n  WYSIWYG: 'WYSIWYG',\n  Write: 'Kirjoita',\n  Preview: 'Esikatselu',\n  Headings: 'Otsikot',\n  Paragraph: 'Kappale',\n  Bold: 'Lihavointi',\n  Italic: 'Kursivointi',\n  Strike: 'Yliviivaus',\n  Code: 'Koodi',\n  Line: 'Vaakaviiva',\n  Blockquote: 'Lainaus',\n  'Unordered list': 'Luettelo',\n  'Ordered list': 'Numeroitu luettelo',\n  Task: 'Tehtävä',\n  Indent: 'Suurenna sisennystä',\n  Outdent: 'Pienennä sisennystä',\n  'Insert link': 'Lisää linkki',\n  'Insert CodeBlock': 'Lisää koodia',\n  'Insert table': 'Lisää taulukko',\n  'Insert image': 'Lisää kuva',\n  Heading: 'Otsikko',\n  'Image URL': 'Kuvan URL',\n  'Select image file': 'Valitse kuvatiedosto',\n  'Choose a file': 'Valitse tiedosto',\n  'No file': 'Ei tiedosto',\n  Description: 'Kuvaus',\n  OK: 'OK',\n  More: 'Lisää',\n  Cancel: 'Peruuta',\n  File: 'Tiedosto',\n  URL: 'URL',\n  'Link text': 'Linkkiteksti',\n  'Add row to up': 'Lisää rivi ylöspäin',\n  'Add row to down': 'Lisää rivi alaspäin',\n  'Add column to left': 'Lisää sarake vasemmalla',\n  'Add column to right': 'Lisää sarake oikealle',\n  'Remove row': 'Poista rivi',\n  'Remove column': 'Poista sarake',\n  'Align column to left': 'Tasaus vasemmalle',\n  'Align column to center': 'Keskitä',\n  'Align column to right': 'Tasaus oikealle',\n  'Remove table': 'Poista taulukko',\n  'Would you like to paste as table?': 'Haluatko liittää taulukkomuodossa?',\n  'Text color': 'Tekstin väri',\n  'Auto scroll enabled': 'Automaattinen skrollaus käytössä',\n  'Auto scroll disabled': 'Automaattinen skrollaus pois käytöstä',\n  'Choose language': 'Valitse kieli',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/fr-fr.ts",
    "content": "/**\n * @fileoverview I18N for French\n * @author Stanislas Michalak <stanislas.michalak@gmail.com>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage(['fr', 'fr-FR'], {\n  Markdown: 'Markdown',\n  WYSIWYG: 'WYSIWYG',\n  Write: 'Écrire',\n  Preview: 'Aperçu',\n  Headings: 'En-têtes',\n  Paragraph: 'Paragraphe',\n  Bold: 'Gras',\n  Italic: 'Italique',\n  Strike: 'Barré',\n  Code: 'Code en ligne',\n  Line: 'Ligne',\n  Blockquote: 'Citation',\n  'Unordered list': 'Liste non-ordonnée',\n  'Ordered list': 'Liste ordonnée',\n  Task: 'Tâche',\n  Indent: 'Retrait',\n  Outdent: 'Sortir',\n  'Insert link': 'Insérer un lien',\n  'Insert CodeBlock': 'Insérer un bloc de code',\n  'Insert table': 'Insérer un tableau',\n  'Insert image': 'Insérer une image',\n  Heading: 'En-tête',\n  'Image URL': \"URL de l'image\",\n  'Select image file': 'Sélectionnez un fichier image',\n  'Choose a file': 'Choisissez un fichier',\n  'No file': 'Pas de fichier',\n  Description: 'Description',\n  OK: 'OK',\n  More: 'de plus',\n  Cancel: 'Annuler',\n  File: 'Fichier',\n  URL: 'URL',\n  'Link text': 'Texte du lien',\n  'Add row to up': 'Ajouter une ligne vers le haut',\n  'Add row to down': 'Ajouter une ligne vers le bas',\n  'Add column to left': 'Ajouter une colonne à gauche',\n  'Add column to right': 'Ajouter une colonne à droite',\n  'Remove row': 'Supprimer une ligne',\n  'Remove column': 'Supprimer une colonne',\n  'Align column to left': 'Aligner à gauche',\n  'Align column to center': 'Aligner au centre',\n  'Align column to right': 'Aligner à droite',\n  'Remove table': 'Supprimer le tableau',\n  'Would you like to paste as table?': 'Voulez-vous coller ce contenu en tant que tableau ?',\n  'Text color': 'Couleur du texte',\n  'Auto scroll enabled': 'Défilement automatique activé',\n  'Auto scroll disabled': 'Défilement automatique désactivé',\n  'Choose language': 'Choix de la langue',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/gl-es.ts",
    "content": "/**\n * @fileoverview I18N for Spanish\n * @author Aida Vidal <avidal@emapic.es>\n */\n\nimport Editor from '../editorCore';\n\nEditor.setLanguage(['gl', 'gl-ES'], {\n  Markdown: 'Markdown',\n  WYSIWYG: 'WYSIWYG',\n  Write: 'Escribir',\n  Preview: 'Vista previa',\n  Headings: 'Encabezados',\n  Paragraph: 'Parágrafo',\n  Bold: 'Negriña',\n  Italic: 'Cursiva',\n  Strike: 'Riscado',\n  Code: 'Código',\n  Line: 'Liña',\n  Blockquote: 'Cita',\n  'Unordered list': 'Lista desordenada',\n  'Ordered list': 'Lista ordenada',\n  Task: 'Tarefa',\n  Indent: 'Sangría',\n  Outdent: 'Anular sangría',\n  'Insert link': 'Inserir enlace',\n  'Insert CodeBlock': 'Inserir bloque de código',\n  'Insert table': 'Inserir táboa',\n  'Insert image': 'Inserir imaxe',\n  Heading: 'Encabezado',\n  'Image URL': 'URL da imaxe',\n  'Select image file': 'Seleccionar arquivo da imaxe',\n  'Choose a file': 'Escoge un archivo',\n  'No file': 'Ningún archivo',\n  Description: 'Descrición',\n  OK: 'Aceptar',\n  More: 'Máis',\n  Cancel: 'Cancelar',\n  File: 'Arquivo',\n  URL: 'URL',\n  'Link text': 'Texto do enlace',\n  'Add row to up': 'Engade fila para arriba',\n  'Add row to down': 'Engade fila para abaixo',\n  'Add column to left': 'Engade columna á esquerda',\n  'Add column to right': 'Engade columna á dereita',\n  'Remove row': 'Eliminar fila',\n  'Remove column': 'Eliminar columna',\n  'Align column to left': 'Aliñar á esquerda',\n  'Align column to center': 'Centrar',\n  'Align column to right': 'Aliñar á dereita',\n  'Remove table': 'Eliminar táboa',\n  'Would you like to paste as table?': 'Desexa pegar como táboa?',\n  'Text color': 'Cor do texto',\n  'Auto scroll enabled': 'Desprazamento automático habilitado',\n  'Auto scroll disabled': 'Desprazamento automático deshabilitado',\n  'Choose language': 'Elixir idioma',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/hr-hr.ts",
    "content": "/**\n * @fileoverview I18N for Croatian\n * @author Hrvoje A. <hrvoj3e@gmail.com>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage(['hr', 'hr-HR'], {\n  Markdown: 'Markdown',\n  WYSIWYG: 'WYSIWYG',\n  Write: 'Piši',\n  Preview: 'Pregled',\n  Headings: 'Naslovi',\n  Paragraph: 'Paragraf',\n  Bold: 'podebljano',\n  Italic: 'kurziv',\n  Strike: 'prcrtano',\n  Code: 'Uklopljeni kôd',\n  Line: 'Linija',\n  Blockquote: 'Blok citat',\n  'Unordered list': 'Neporedana lista',\n  'Ordered list': 'Poredana lista',\n  Task: 'Task',\n  Indent: 'Povećaj uvlaku',\n  Outdent: 'Smanji uvlaku',\n  'Insert link': 'Umetni link',\n  'Insert CodeBlock': 'Umetni blok kôda',\n  'Insert table': 'Umetni tablicu',\n  'Insert image': 'Umetni sliku',\n  Heading: 'Naslov',\n  'Image URL': 'URL slike',\n  'Select image file': 'Odaberi slikovnu datoteku',\n  'Choose a file': 'Odaberite datoteka',\n  'No file': 'Nema datoteka',\n  Description: 'Opis',\n  OK: 'OK',\n  More: 'Više',\n  Cancel: 'Odustani',\n  File: 'Datoteka',\n  URL: 'URL',\n  'Link text': 'Tekst linka',\n  'Add row to up': 'Dodaj redak prema gore',\n  'Add row to down': 'Dodaj redak prema dolje',\n  'Add column to left': 'Dodaj stupac s lijeve strane',\n  'Add column to right': 'Dodajte stupac s desne strane',\n  'Remove row': 'Ukloni redak',\n  'Remove column': 'Remove stupac',\n  'Align column to left': 'Poravnaj lijevo',\n  'Align column to center': 'Poravnaj centrirano',\n  'Align column to right': 'Poravnaj desno',\n  'Remove table': 'Ukloni tablicu',\n  'Would you like to paste as table?': 'Zalite li zalijepiti kao tablicu?',\n  'Text color': 'Boja teksta',\n  'Auto scroll enabled': 'Omogući auto klizanje',\n  'Auto scroll disabled': 'Onemogući auto klizanje',\n  'Choose language': 'Odabir jezika',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/i18n.ts",
    "content": "/**\n * @fileoverview Implements i18n\n * @author NHN Cloud FE Development Lab <dl_javascript@nhn.com>\n */\nimport extend from 'tui-code-snippet/object/extend';\nimport Map from '../utils/map';\n\nconst DEFAULT_CODE = 'en-US';\n\n/**\n * Class I18n\n * @ignore\n */\nclass I18n {\n  private code: string;\n\n  private langs: Map<string, Record<string, string>>;\n\n  constructor() {\n    this.code = DEFAULT_CODE;\n    this.langs = new Map();\n  }\n\n  setCode(code?: string) {\n    this.code = code || DEFAULT_CODE;\n  }\n\n  /**\n   * Set language set\n   * @param {string|string[]} codes locale code\n   * @param {object} data language set\n   */\n  setLanguage(codes: string | string[], data: Record<string, string>) {\n    codes = ([] as string[]).concat(codes);\n\n    codes.forEach((code) => {\n      if (!this.langs.has(code)) {\n        this.langs.set(code, data);\n      } else {\n        const langData = this.langs.get(code)!;\n\n        this.langs.set(code, extend(langData, data));\n      }\n    });\n  }\n\n  get(key: string, code?: string) {\n    if (!code) {\n      code = this.code;\n    }\n\n    let langSet = this.langs.get(code);\n\n    if (!langSet) {\n      langSet = this.langs.get(DEFAULT_CODE)!;\n    }\n\n    const text = langSet[key];\n\n    if (!text) {\n      throw new Error(`There is no text key \"${key}\" in ${code}`);\n    }\n\n    return text;\n  }\n}\n\nexport { I18n };\nexport default new I18n();\n"
  },
  {
    "path": "apps/editor/src/i18n/it-it.ts",
    "content": "/**\n * @fileoverview I18N for Italian\n * @author Massimo Redaelli <massimo@typish.io>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage(['it', 'it-IT'], {\n  Markdown: 'Markdown',\n  WYSIWYG: 'WYSIWYG',\n  Write: 'Scrivere',\n  Preview: 'Anteprima',\n  Headings: 'Intestazioni',\n  Paragraph: 'Paragrafo',\n  Bold: 'Grassetto',\n  Italic: 'Corsivo',\n  Strike: 'Barrato',\n  Code: 'Codice',\n  Line: 'Linea',\n  Blockquote: 'Blocco citazione',\n  'Unordered list': 'Lista puntata',\n  'Ordered list': 'Lista numerata',\n  Task: 'Attività',\n  Indent: 'Aggiungi indentazione',\n  Outdent: 'Rimuovi indentazione',\n  'Insert link': 'Inserisci link',\n  'Insert CodeBlock': 'Inserisci blocco di codice',\n  'Insert table': 'Inserisci tabella',\n  'Insert image': 'Inserisci immagine',\n  Heading: 'Intestazione',\n  'Image URL': 'URL immagine',\n  'Select image file': 'Seleziona file immagine',\n  'Choose a file': 'Scegli un file',\n  'No file': 'Nessun file',\n  Description: 'Descrizione',\n  OK: 'OK',\n  More: 'Più',\n  Cancel: 'Cancella',\n  File: 'File',\n  URL: 'URL',\n  'Link text': 'Testo del collegamento',\n  'Add row to up': 'Aggiungi riga in alto',\n  'Add row to down': 'Aggiungi riga in basso',\n  'Add column to left': 'Aggiungi colonna a sinistra',\n  'Add column to right': 'Aggiungi colonna a destra',\n  'Remove row': 'Rimuovi riga',\n  'Remove column': 'Rimuovi colonna',\n  'Align column to left': 'Allinea a sinistra',\n  'Align column to center': 'Allinea al centro',\n  'Align column to right': 'Allinea a destra',\n  'Remove table': 'Rimuovi tabella',\n  'Would you like to paste as table?': 'Desideri incollare sotto forma di tabella?',\n  'Text color': 'Colore del testo',\n  'Auto scroll enabled': 'Scrolling automatico abilitato',\n  'Auto scroll disabled': 'Scrolling automatico disabilitato',\n  'Choose language': 'Scegli la lingua',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/ja-jp.ts",
    "content": "/**\n * @fileoverview I18N for Japanese\n * @author NHN Cloud FE Development Lab <dl_javascript@nhn.com>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage(['ja', 'ja-JP'], {\n  Markdown: 'マークダウン',\n  WYSIWYG: 'WYSIWYG',\n  Write: '編集する',\n  Preview: 'プレビュー',\n  Headings: '見出し',\n  Paragraph: '本文',\n  Bold: '太字',\n  Italic: 'イタリック',\n  Strike: 'ストライク',\n  Code: 'インラインコード',\n  Line: 'ライン',\n  Blockquote: '引用',\n  'Unordered list': '番号なしリスト',\n  'Ordered list': '順序付きリスト',\n  Task: 'タスク',\n  Indent: 'インデント',\n  Outdent: 'アウトデント',\n  'Insert link': 'リンク挿入',\n  'Insert CodeBlock': 'コードブロック挿入',\n  'Insert table': 'テーブル挿入',\n  'Insert image': '画像挿入',\n  Heading: '見出し',\n  'Image URL': 'イメージURL',\n  'Select image file': '画像ファイル選択',\n  'Choose a file': 'ファイルの選択',\n  'No file': 'ファイルがない',\n  Description: 'ディスクリプション ',\n  OK: 'はい',\n  More: 'もっと',\n  Cancel: 'キャンセル',\n  File: 'ファイル',\n  URL: 'URL',\n  'Link text': 'リンクテキスト',\n  'Add row to up': '行を上に追加',\n  'Add row to down': '下に行を追加',\n  'Add column to left': '左側に列を追加',\n  'Add column to right': '右側に列を追加',\n  'Remove row': '行削除',\n  'Remove column': '列削除',\n  'Align column to left': '左揃え',\n  'Align column to center': '中央揃え',\n  'Align column to right': '右揃え',\n  'Remove table': 'テーブル削除',\n  'Would you like to paste as table?': 'テーブルを貼り付けますか?',\n  'Text color': '文字色相',\n  'Auto scroll enabled': '自動スクロールが有効',\n  'Auto scroll disabled': '自動スクロールを無効に',\n  'Choose language': '言語選択',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/ko-kr.ts",
    "content": "/**\n * @fileoverview I18N for Korean\n * @author NHN Cloud FE Development Lab <dl_javascript@nhn.com>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage(['ko', 'ko-KR'], {\n  Markdown: '마크다운',\n  WYSIWYG: '위지윅',\n  Write: '편집하기',\n  Preview: '미리보기',\n  Headings: '제목크기',\n  Paragraph: '본문',\n  Bold: '굵게',\n  Italic: '기울임꼴',\n  Strike: '취소선',\n  Code: '인라인 코드',\n  Line: '문단나눔',\n  Blockquote: '인용구',\n  'Unordered list': '글머리 기호',\n  'Ordered list': '번호 매기기',\n  Task: '체크박스',\n  Indent: '들여쓰기',\n  Outdent: '내어쓰기',\n  'Insert link': '링크 삽입',\n  'Insert CodeBlock': '코드블럭 삽입',\n  'Insert table': '표 삽입',\n  'Insert image': '이미지 삽입',\n  Heading: '제목',\n  'Image URL': '이미지 주소',\n  'Select image file': '이미지 파일을 선택하세요.',\n  'Choose a file': '파일 선택',\n  'No file': '선택된 파일 없음',\n  Description: '설명',\n  OK: '확인',\n  More: '더 보기',\n  Cancel: '취소',\n  File: '파일',\n  URL: '주소',\n  'Link text': '링크 텍스트',\n  'Add row to up': '위에 행 추가',\n  'Add row to down': '아래에 행 추가',\n  'Add column to left': '왼쪽에 열 추가',\n  'Add column to right': '오른쪽에 열 추가',\n  'Remove row': '행 삭제',\n  'Remove column': '열 삭제',\n  'Align column to left': '열 왼쪽 정렬',\n  'Align column to center': '열 가운데 정렬',\n  'Align column to right': '열 오른쪽 정렬',\n  'Remove table': '표 삭제',\n  'Would you like to paste as table?': '표형태로 붙여 넣겠습니까?',\n  'Text color': '글자 색상',\n  'Auto scroll enabled': '자동 스크롤 켜짐',\n  'Auto scroll disabled': '자동 스크롤 꺼짐',\n  'Choose language': '언어 선택',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/nb-no.ts",
    "content": "/**\n * @fileoverview I18N for Norwegian\n * @author Anton Reytarovskiy <reitarovskii.toh@gmail.com>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage(['nb', 'nb-NO'], {\n  Markdown: 'Funksjonaliteter',\n  WYSIWYG: 'WYSIWYG',\n  Write: 'Skriv',\n  Preview: 'Forhåndsvisning',\n  Headings: 'Overskrift',\n  Paragraph: 'Paragraf',\n  Bold: 'Fet skrift',\n  Italic: 'Italic',\n  Strike: 'Strike',\n  Code: 'Kode',\n  Line: 'Linje',\n  Blockquote: 'Blokksitat',\n  'Unordered list': 'Usortert liste',\n  'Ordered list': 'Sortert liste',\n  Task: 'Task',\n  Indent: 'Indent',\n  Outdent: 'Outdent',\n  'Insert link': 'Sett inn lenke',\n  'Insert CodeBlock': 'Sett inn CodeStreng',\n  'Insert table': 'Sett inn diagram',\n  'Insert image': 'Sett inn bilde',\n  Heading: 'Overskrift',\n  'Image URL': 'BildeURL',\n  'Select image file': 'Velg bildefil',\n  'Choose a file': 'Velg en fil',\n  'No file': 'Ingen fil',\n  Description: 'Beskrivelse',\n  OK: 'OK',\n  More: 'Mer',\n  Cancel: 'Angre',\n  File: 'Fil',\n  URL: 'URL',\n  'Link text': 'Lenketekst',\n  'Add row to up': 'Legg rad til opp',\n  'Add row to down': 'Legg rad til ned',\n  'Add column to left': 'Legg til kolonne til venstre',\n  'Add column to right': 'Legg til kolonne til høyre',\n  'Remove row': 'Fjern rad',\n  'Remove column': 'Fjern kolonne',\n  'Align column to left': 'Venstreorienter',\n  'Align column to center': 'Senterorienter',\n  'Align column to right': 'Høyreorienter',\n  'Remove table': 'Fjern diagram',\n  'Would you like to paste as table?': 'Ønsker du å lime inn som et diagram?',\n  'Text color': 'Tekstfarge',\n  'Auto scroll enabled': 'Auto-scroll aktivert',\n  'Auto scroll disabled': 'Auto-scroll deaktivert',\n  'Choose language': 'Velg språl',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/nl-nl.ts",
    "content": "/**\n * @fileoverview I18N for Dutch\n * @author NHN Cloud FE Development Lab <dl_javascript@nhn.com>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage(['nl', 'nl-NL'], {\n  Markdown: 'Markdown',\n  WYSIWYG: 'WYSIWYG',\n  Write: 'Opslaan',\n  Preview: 'Voorbeeld',\n  Headings: 'Koppen',\n  Paragraph: 'Alinea',\n  Bold: 'Vet',\n  Italic: 'Cursief',\n  Strike: 'Doorhalen',\n  Code: 'Inline code',\n  Line: 'Regel',\n  Blockquote: 'Citaatblok',\n  'Unordered list': 'Opsomming',\n  'Ordered list': 'Genummerde opsomming',\n  Task: 'Taak',\n  Indent: 'Niveau verhogen',\n  Outdent: 'Niveau verlagen',\n  'Insert link': 'Link invoegen',\n  'Insert CodeBlock': 'Codeblok toevoegen',\n  'Insert table': 'Tabel invoegen',\n  'Insert image': 'Afbeelding invoegen',\n  Heading: 'Kop',\n  'Image URL': 'Afbeelding URL',\n  'Select image file': 'Selecteer een afbeelding',\n  'Choose a file': 'Kies een bestand',\n  'No file': 'Geen bestand',\n  Description: 'Omschrijving',\n  OK: 'OK',\n  More: 'Meer',\n  Cancel: 'Annuleren',\n  File: 'Bestand',\n  URL: 'URL',\n  'Link text': 'Link tekst',\n  'Add row to up': 'Voeg rij toe aan omhoog',\n  'Add row to down': 'Rij naar beneden toevoegen',\n  'Add column to left': 'Voeg kolom aan de linkerkant toe',\n  'Add column to right': 'Voeg een kolom aan de rechterkant toe',\n  'Remove row': 'Rij verwijderen',\n  'Remove column': 'Kolom verwijderen',\n  'Align column to left': 'Links uitlijnen',\n  'Align column to center': 'Centreren',\n  'Align column to right': 'Rechts uitlijnen',\n  'Remove table': 'Verwijder tabel',\n  'Would you like to paste as table?': 'Wil je dit als tabel plakken?',\n  'Text color': 'Tekstkleur',\n  'Auto scroll enabled': 'Autoscroll ingeschakeld',\n  'Auto scroll disabled': 'Autoscroll uitgeschakeld',\n  'Choose language': 'Kies een taal',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/pl-pl.ts",
    "content": "/**\n * @fileoverview I18N for Polish\n * @author Marcin Mikołajczak <me@m4sk.in>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage(['pl', 'pl-PL'], {\n  Markdown: 'Markdown',\n  WYSIWYG: 'WYSIWYG',\n  Write: 'Napisz',\n  Preview: 'Podgląd',\n  Headings: 'Nagłówki',\n  Paragraph: 'Akapit',\n  Bold: 'Pogrubienie',\n  Italic: 'Kursywa',\n  Strike: 'Przekreślenie',\n  Code: 'Fragment kodu',\n  Line: 'Linia',\n  Blockquote: 'Cytat',\n  'Unordered list': 'Lista nieuporządkowana',\n  'Ordered list': 'Lista uporządkowana',\n  Task: 'Zadanie',\n  Indent: 'Utwórz wcięcie',\n  Outdent: 'Usuń wcięcie',\n  'Insert link': 'Umieść odnośnik',\n  'Insert CodeBlock': 'Umieść blok kodu',\n  'Insert table': 'Umieść tabelę',\n  'Insert image': 'Umieść obraz',\n  Heading: 'Nagłówek',\n  'Image URL': 'Adres URL obrazu',\n  'Select image file': 'Wybierz plik obrazu',\n  'Choose a file': 'Wybierz plik',\n  'No file': 'Brak plik',\n  Description: 'Opis',\n  OK: 'OK',\n  More: 'Więcej',\n  Cancel: 'Anuluj',\n  File: 'Plik',\n  URL: 'URL',\n  'Link text': 'Tekst odnośnika',\n  'Add row to up': 'Dodaj wiersz do góry',\n  'Add row to down': 'Dodaj wiersz w dół',\n  'Add column to left': 'Dodaj kolumnę po lewej stronie',\n  'Add column to right': 'Dodaj kolumnę po prawej stronie',\n  'Remove row': 'Usuń rząd',\n  'Remove column': 'Usuń kolumnę',\n  'Align column to left': 'Wyrównaj do lewej',\n  'Align column to center': 'Wyśrodkuj',\n  'Align column to right': 'Wyrównaj do prawej',\n  'Remove table': 'Usuń tabelę',\n  'Would you like to paste as table?': 'Czy chcesz wkleić tekst jako tabelę?',\n  'Text color': 'Kolor tekstu',\n  'Auto scroll enabled': 'Włączono automatyczne przewijanie',\n  'Auto scroll disabled': 'Wyłączono automatyczne przewijanie',\n  'Choose language': 'Wybierz język',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/pt-br.ts",
    "content": "/**\n * @fileoverview I18N for Português\n * @author Nícolas Huber <nicolasluishuber@gmail.com>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage(['pt', 'pt-BR'], {\n  Markdown: 'Markdown',\n  WYSIWYG: 'WYSIWYG',\n  Write: 'Escrever',\n  Preview: 'Pré-visualizar',\n  Headings: 'Cabeçalhos',\n  Paragraph: 'Parágrafo',\n  Bold: 'Negrito',\n  Italic: 'Itálico',\n  Strike: 'Traçado',\n  Code: 'Código',\n  Line: 'Linha',\n  Blockquote: 'Bloco de citação',\n  'Unordered list': 'Lista não ordenada',\n  'Ordered list': 'Lista ordenada',\n  Task: 'Tarefa',\n  Indent: 'Recuo à esquerda',\n  Outdent: 'Recuo à direita',\n  'Insert link': 'Inserir link',\n  'Insert CodeBlock': 'Inserir bloco de código',\n  'Insert table': 'Inserir tabela',\n  'Insert image': 'Inserir imagem',\n  Heading: 'Título',\n  'Image URL': 'URL da imagem',\n  'Select image file': 'Selecione um arquivo de imagem',\n  'Choose a file': 'Escolha um arquivo',\n  'No file': 'Nenhum arquivo',\n  Description: 'Descrição',\n  OK: 'OK',\n  More: 'Mais',\n  Cancel: 'Cancelar',\n  File: 'Arquivo',\n  URL: 'URL',\n  'Link text': 'Link de texto',\n  'Add row to up': 'Adicionar linha para cima',\n  'Add row to down': 'Adicionar linha para baixo',\n  'Add column to left': 'Adicionar coluna à esquerda',\n  'Add column to right': 'Adicionar coluna à direita',\n  'Remove row': 'Remover linha',\n  'Remove column': 'Remover coluna',\n  'Align column to left': 'Alinhar à esquerda',\n  'Align column to center': 'Alinhar ao centro',\n  'Align column to right': 'Alinhar à direita',\n  'Remove table': 'Remover tabela',\n  'Would you like to paste as table?': 'Você gostaria de colar como mesa?',\n  'Text color': 'Cor do texto',\n  'Auto scroll enabled': 'Rolagem automática habilitada',\n  'Auto scroll disabled': 'Rolagem automática desabilitada',\n  'Choose language': 'Escolher linguagem',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/ru-ru.ts",
    "content": "/**\n * @fileoverview I18N for Russian\n * @author Stepan Samko <stpnsamko@gmail.com>\n * @author Veaceslav Grimalschi <grimalschi@yandex.ru>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage(['ru', 'ru-RU'], {\n  Markdown: 'Markdown',\n  WYSIWYG: 'WYSIWYG',\n  Write: 'Редактор',\n  Preview: 'Просмотр',\n  Headings: 'Заголовки',\n  Paragraph: 'Абзац',\n  Bold: 'Жирный',\n  Italic: 'Курсив',\n  Strike: 'Зачеркнутый',\n  Code: 'Код',\n  Line: 'Линия',\n  Blockquote: 'Цитата',\n  'Unordered list': 'Неупорядоченный список',\n  'Ordered list': 'Упорядоченный список',\n  Task: 'Галочка',\n  Indent: 'Увеличить отступ',\n  Outdent: 'Уменьшить отступ',\n  'Insert link': 'Вставить ссылку',\n  'Insert CodeBlock': 'Вставить блок кода',\n  'Insert table': 'Вставить таблицу',\n  'Insert image': 'Вставить изображение',\n  Heading: 'Заголовок',\n  'Image URL': 'URL изображения',\n  'Select image file': 'Выбрать файл изображения',\n  'Choose a file': 'Выбрать',\n  'No file': 'Нет файла',\n  Description: 'Описание',\n  OK: 'Хорошо',\n  More: 'Еще',\n  Cancel: 'Отмена',\n  File: 'Файл',\n  URL: 'URL',\n  'Link text': 'Текст ссылки',\n  'Add row to up': 'Добавить строку вверх',\n  'Add row to down': 'Добавить строку вниз',\n  'Add column to left': 'Добавить столбец слева',\n  'Add column to right': 'Добавить столбец справа',\n  'Remove row': 'Удалить ряд',\n  'Remove column': 'Удалить столбец',\n  'Align column to left': 'Выровнять по левому краю',\n  'Align column to center': 'Выровнять по центру',\n  'Align column to right': 'Выровнять по правому краю',\n  'Remove table': 'Удалить таблицу',\n  'Would you like to paste as table?': 'Вы хотите вставить в виде таблицы?',\n  'Text color': 'Цвет текста',\n  'Auto scroll enabled': 'Автопрокрутка включена',\n  'Auto scroll disabled': 'Автопрокрутка отключена',\n  'Choose language': 'Выбрать язык',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/sv-se.ts",
    "content": "/**\n * @fileoverview I18N for Swedish\n * @author Magnus Aspling <magnus@yug.se>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage(['sv', 'sv-SE'], {\n  Markdown: 'Markdown',\n  WYSIWYG: 'WYSIWYG',\n  Write: 'Skriv',\n  Preview: 'Förhandsgranska',\n  Headings: 'Överskrifter',\n  Paragraph: 'Paragraf',\n  Bold: 'Fet',\n  Italic: 'Kursiv',\n  Strike: 'Genomstruken',\n  Code: 'Kodrad',\n  Line: 'Linje',\n  Blockquote: 'Citatblock',\n  'Unordered list': 'Punktlista',\n  'Ordered list': 'Numrerad lista',\n  Task: 'Att göra',\n  Indent: 'Öka indrag',\n  Outdent: 'Minska indrag',\n  'Insert link': 'Infoga länk',\n  'Insert CodeBlock': 'Infoga kodblock',\n  'Insert table': 'Infoga tabell',\n  'Insert image': 'Infoga bild',\n  Heading: 'Överskrift',\n  'Image URL': 'Bildadress',\n  'Select image file': 'Välj en bildfil',\n  'Choose a file': 'Välj en fil',\n  'No file': 'Ingen fil',\n  Description: 'Beskrivning',\n  OK: 'OK',\n  More: 'Mer',\n  Cancel: 'Avbryt',\n  File: 'Fil',\n  URL: 'Adress',\n  'Link text': 'Länktext',\n  'Add row to up': 'Lägg till rad till upp',\n  'Add row to down': 'Lägg till rad till ner',\n  'Add column to left': 'Lägg till kolumn till vänster',\n  'Add column to right': 'Lägg till kolumn till höger',\n  'Remove row': 'Radera rad',\n  'Remove column': 'Radera kolumn',\n  'Align column to left': 'Vänsterjustera',\n  'Align column to center': 'Centrera',\n  'Align column to right': 'Högerjustera',\n  'Remove table': 'Radera tabell',\n  'Would you like to paste as table?': 'Vill du klistra in som en tabell?',\n  'Text color': 'Textfärg',\n  'Auto scroll enabled': 'Automatisk scroll aktiverad',\n  'Auto scroll disabled': 'Automatisk scroll inaktiverad',\n  'Choose language': 'Välj språk',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/tr-tr.ts",
    "content": "/**\n * @fileoverview I18N for Turkish\n * @author Mesut Gölcük <mesutgolcuk@gmail.com>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage(['tr', 'tr-TR'], {\n  Markdown: 'Markdown',\n  WYSIWYG: 'WYSIWYG',\n  Write: 'Düzenle',\n  Preview: 'Ön izleme',\n  Headings: 'Başlıklar',\n  Paragraph: 'Paragraf',\n  Bold: 'Kalın',\n  Italic: 'İtalik',\n  Strike: 'Altı çizgili',\n  Code: 'Satır içi kod',\n  Line: 'Çizgi',\n  Blockquote: 'Alıntı',\n  'Unordered list': 'Sıralanmamış liste',\n  'Ordered list': 'Sıralı liste',\n  Task: 'Görev kutusu',\n  Indent: 'Girintiyi arttır',\n  Outdent: 'Girintiyi azalt',\n  'Insert link': 'Bağlantı ekle',\n  'Insert CodeBlock': 'Kod bloku ekle',\n  'Insert table': 'Tablo ekle',\n  'Insert image': 'İmaj ekle',\n  Heading: 'Başlık',\n  'Image URL': 'İmaj URL',\n  'Select image file': 'İmaj dosyası seç',\n  'Choose a file': 'Bir dosya seçin',\n  'No file': 'Dosya yok',\n  Description: 'Açıklama',\n  OK: 'Onay',\n  More: 'Daha Fazla',\n  Cancel: 'İptal',\n  File: 'Dosya',\n  URL: 'URL',\n  'Link text': 'Bağlantı yazısı',\n  'Add row to up': 'Yukarı satır ekle',\n  'Add row to down': 'Aşağı satır ekle',\n  'Add column to left': 'Sola sütun ekleyin',\n  'Add column to right': 'Sağa sütun ekle',\n  'Remove row': 'Satır sil',\n  'Remove column': 'Sütun sil',\n  'Align column to left': 'Sola hizala',\n  'Align column to center': 'Merkeze hizala',\n  'Align column to right': 'Sağa hizala',\n  'Remove table': 'Tabloyu kaldır',\n  'Would you like to paste as table?': 'Tablo olarak yapıştırmak ister misiniz?',\n  'Text color': 'Metin rengi',\n  'Auto scroll enabled': 'Otomatik kaydırma açık',\n  'Auto scroll disabled': 'Otomatik kaydırma kapalı',\n  'Choose language': 'Dil seçiniz',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/uk-ua.ts",
    "content": "/**\n * @fileoverview I18N for Ukrainian\n * @author Nikolya <k_m_i@i.ua>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage(['uk', 'uk-UA'], {\n  Markdown: 'Markdown',\n  WYSIWYG: 'WYSIWYG',\n  Write: 'Написати',\n  Preview: 'Попередній перегляд',\n  Headings: 'Заголовки',\n  Paragraph: 'Абзац',\n  Bold: 'Жирний',\n  Italic: 'Курсив',\n  Strike: 'Закреслений',\n  Code: 'Вбудований код',\n  Line: 'Лінія',\n  Blockquote: 'Блок цитування',\n  'Unordered list': 'Невпорядкований список',\n  'Ordered list': 'Упорядкований список',\n  Task: 'Завдання',\n  Indent: 'відступ',\n  Outdent: 'застарілий',\n  'Insert link': 'Вставити посилання',\n  'Insert CodeBlock': 'Вставити код',\n  'Insert table': 'Вставити таблицю',\n  'Insert image': 'Вставити зображення',\n  Heading: 'Заголовок',\n  'Image URL': 'URL зображення',\n  'Select image file': 'Вибрати файл зображення',\n  'Choose a file': 'Виберіть файл',\n  'No file': 'Немає файлу',\n  Description: 'Опис',\n  OK: 'OK',\n  More: 'ще',\n  Cancel: 'Скасувати',\n  File: 'Файл',\n  URL: 'URL',\n  'Link text': 'Текст посилання',\n  'Add row to up': 'Додати рядок вгору',\n  'Add row to down': 'Додати рядок вниз',\n  'Add column to left': 'Додайте стовпець зліва',\n  'Add column to right': 'Додайте стовпець праворуч',\n  'Remove row': 'Видалити ряд',\n  'Remove column': 'Видалити стовпчик',\n  'Align column to left': 'Вирівняти по лівому краю',\n  'Align column to center': 'Вирівняти по центру',\n  'Align column to right': 'Вирівняти по правому краю',\n  'Remove table': 'Видалити таблицю',\n  'Would you like to paste as table?': 'Ви хочете вставити у вигляді таблиці?',\n  'Text color': 'Колір тексту',\n  'Auto scroll enabled': 'Автоматична прокрутка включена',\n  'Auto scroll disabled': 'Автоматична прокрутка відключена',\n  'Choose language': 'Вибрати мову',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/zh-cn.ts",
    "content": "/**\n * @fileoverview I18N for Chinese\n * @author NHN Cloud FE Development Lab <dl_javascript@nhn.com>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage('zh-CN', {\n  Markdown: 'Markdown',\n  WYSIWYG: '所见即所得',\n  Write: '编辑',\n  Preview: '预览',\n  Headings: '标题',\n  Paragraph: '文本',\n  Bold: '加粗',\n  Italic: '斜体字',\n  Strike: '删除线',\n  Code: '内嵌代码',\n  Line: '水平线',\n  Blockquote: '引用块',\n  'Unordered list': '无序列表',\n  'Ordered list': '有序列表',\n  Task: '任务',\n  Indent: '缩进',\n  Outdent: '减少缩进',\n  'Insert link': '插入链接',\n  'Insert CodeBlock': '插入代码块',\n  'Insert table': '插入表格',\n  'Insert image': '插入图片',\n  Heading: '标题',\n  'Image URL': '图片网址',\n  'Select image file': '选择图片文件',\n  'Choose a file': '选择一个文件',\n  'No file': '没有文件',\n  Description: '说明',\n  OK: '确认',\n  More: '更多',\n  Cancel: '取消',\n  File: '文件',\n  URL: 'URL',\n  'Link text': '链接文本',\n  'Add row to up': '向上添加行',\n  'Add row to down': '在下方添加行',\n  'Add column to left': '在左侧添加列',\n  'Add column to right': '在右侧添加列',\n  'Remove row': '删除行',\n  'Remove column': '删除列',\n  'Align column to left': '左对齐',\n  'Align column to center': '居中对齐',\n  'Align column to right': '右对齐',\n  'Remove table': '删除表格',\n  'Would you like to paste as table?': '需要粘贴为表格吗?',\n  'Text color': '文字颜色',\n  'Auto scroll enabled': '自动滚动已启用',\n  'Auto scroll disabled': '自动滚动已禁用',\n  'Choose language': '选择语言',\n});\n"
  },
  {
    "path": "apps/editor/src/i18n/zh-tw.ts",
    "content": "/**\n * @fileoverview I18N for Traditional Chinese\n * @author Tzu-Ray Su <raysu3329@gmail.com>\n */\nimport Editor from '../editorCore';\n\nEditor.setLanguage('zh-TW', {\n  Markdown: 'Markdown',\n  WYSIWYG: '所見即所得',\n  Write: '編輯',\n  Preview: '預覽',\n  Headings: '標題',\n  Paragraph: '內文',\n  Bold: '粗體',\n  Italic: '斜體',\n  Strike: '刪除線',\n  Code: '內嵌程式碼',\n  Line: '分隔線',\n  Blockquote: '引言',\n  'Unordered list': '項目符號清單',\n  'Ordered list': '編號清單',\n  Task: '核取方塊清單',\n  Indent: '增加縮排',\n  Outdent: '減少縮排',\n  'Insert link': '插入超連結',\n  'Insert CodeBlock': '插入程式碼區塊',\n  'Insert table': '插入表格',\n  'Insert image': '插入圖片',\n  Heading: '標題',\n  'Image URL': '圖片網址',\n  'Select image file': '選擇圖片檔案',\n  'Choose a file': '選擇一個文件',\n  'No file': '沒有文件',\n  Description: '描述',\n  OK: '確認',\n  More: '更多',\n  Cancel: '取消',\n  File: '檔案',\n  URL: 'URL',\n  'Link text': '超連結文字',\n  'Add row to up': '向上添加行',\n  'Add row to down': '在下方添加行',\n  'Add column to left': '在左側添加列',\n  'Add column to right': '在右側添加列',\n  'Remove row': '刪除行',\n  'Remove column': '刪除列',\n  'Align column to left': '靠左對齊',\n  'Align column to center': '置中',\n  'Align column to right': '靠右對齊',\n  'Remove table': '刪除表格',\n  'Would you like to paste as table?': '您要以表格貼上嗎？',\n  'Text color': '文字顏色',\n  'Auto scroll enabled': '已啟用自動滾動',\n  'Auto scroll disabled': '已停用自動滾動',\n  'Choose language': '選擇語言',\n});\n"
  },
  {
    "path": "apps/editor/src/index.ts",
    "content": "import EditorCore from './editorCore';\nimport Editor from './editor';\n\nimport 'prosemirror-view/style/prosemirror.css';\nimport '@/css/editor.css';\nimport '@/css/contents.css';\nimport '@/css/preview-highlighting.css';\nimport '@/css/md-syntax-highlighting.css';\n\nimport './i18n/en-us';\n\nexport default Editor;\nexport { Editor, EditorCore };\n"
  },
  {
    "path": "apps/editor/src/indexEditorOnlyStyle.ts",
    "content": "import '@/css/editor.css';\nimport '@/css/preview-highlighting.css';\nimport '@/css/md-syntax-highlighting.css';\n"
  },
  {
    "path": "apps/editor/src/indexViewer.ts",
    "content": "import Viewer from './viewer';\n\nimport '@/css/contents.css';\n\nexport default Viewer;\n"
  },
  {
    "path": "apps/editor/src/markdown/helper/list.ts",
    "content": "import { ProsemirrorNode, Schema } from 'prosemirror-model';\nimport { ListItemMdNode, MdNode, ToastMark } from '@toast-ui/toastmark';\nimport { findClosestNode, isListNode, isOrderedListNode } from '@/utils/markdown';\nimport { createTextNode } from '@/helper/manipulation';\nimport { getTextByMdLine } from './query';\n\nexport interface ToListContext<T = ListItemMdNode> {\n  mdNode: T;\n  line: number;\n  toastMark: ToastMark;\n  doc: ProsemirrorNode;\n  startLine: number;\n}\n\nexport type ExtendListContext = Omit<ToListContext, 'startLine'>;\n\nexport interface ChangedListInfo {\n  line: number;\n  text: string;\n}\n\ninterface ToListResult {\n  changedResults: ChangedListInfo[];\n  firstIndex?: number;\n  lastIndex?: number;\n}\n\ntype ExtendedResult = {\n  listSyntax: string;\n  changedResults?: ChangedListInfo[];\n  lastIndex?: number;\n};\n\ntype ListType = 'bullet' | 'ordered';\ntype ListToListFn = (context: ToListContext) => ToListResult;\ntype NodeToListFn = (context: ToListContext<MdNode>) => ToListResult;\ntype ExtendListFn = (context: ExtendListContext) => ExtendedResult;\n\ninterface ItemInfo {\n  line: number;\n  depth: number;\n  mdNode: ListItemMdNode;\n}\n\ninterface ListToList {\n  bullet: ListToListFn;\n  ordered: ListToListFn;\n  task: ListToListFn;\n}\n\ninterface NodeToList {\n  bullet: NodeToListFn;\n  ordered: NodeToListFn;\n  task: NodeToListFn;\n}\n\ninterface ExtendList {\n  bullet: ExtendListFn;\n  ordered: ExtendListFn;\n}\n\nexport const reList = /(^\\s*)([-*+] |[\\d]+\\. )/;\nexport const reOrderedList = /(^\\s*)([\\d])+\\.( \\[[ xX]])? /;\nexport const reOrderedListGroup = /^(\\s*)((\\d+)([.)]\\s(?:\\[(?:x|\\s)\\]\\s)?))(.*)/;\nexport const reCanBeTaskList = /(^\\s*)([-*+]|[\\d]+\\.)( \\[[ xX]])? /;\nconst reBulletListGroup = /^(\\s*)([-*+]+(\\s(?:\\[(?:x|\\s)\\]\\s)?))(.*)/;\nconst reTaskList = /(^\\s*)([-*+] |[\\d]+\\. )(\\[[ xX]] )/;\nconst reBulletTaskList = /(^\\s*)([-*+])( \\[[ xX]]) /;\n\nexport function getListType(text: string): ListType {\n  return reOrderedList.test(text) ? 'ordered' : 'bullet';\n}\n\nfunction getListDepth(mdNode: MdNode) {\n  let depth = 0;\n\n  while (mdNode && mdNode.type !== 'document') {\n    if (mdNode.type === 'list') {\n      depth += 1;\n    }\n    mdNode = mdNode.parent!;\n  }\n  return depth;\n}\n\nfunction findSameDepthList(\n  toastMark: ToastMark,\n  currentLine: number,\n  depth: number,\n  backward: boolean\n): ItemInfo[] {\n  const lineTexts = toastMark.getLineTexts();\n  const lineLen = lineTexts.length;\n  const result = [];\n  let line = currentLine;\n\n  while (backward ? line < lineLen : line > 1) {\n    line = backward ? line + 1 : line - 1;\n    const mdNode = toastMark.findFirstNodeAtLine(line) as ListItemMdNode;\n    const currentListDepth = getListDepth(mdNode);\n\n    if (currentListDepth === depth) {\n      result.push({ line, depth, mdNode });\n    } else if (currentListDepth < depth) {\n      break;\n    }\n  }\n\n  return result;\n}\n\nfunction getSameDepthItems({ toastMark, mdNode, line }: ToListContext) {\n  const depth = getListDepth(mdNode);\n  const forwardList = findSameDepthList(toastMark, line, depth, false).reverse();\n  const backwardList = findSameDepthList(toastMark, line, depth, true);\n\n  return forwardList.concat([{ line, depth, mdNode }]).concat(backwardList);\n}\n\nfunction textToBullet(text: string) {\n  if (!reList.test(text)) {\n    return `* ${text}`;\n  }\n  const type = getListType(text);\n\n  if (type === 'bullet' && reCanBeTaskList.test(text)) {\n    text = text.replace(reBulletTaskList, '$1$2 ');\n  } else if (type === 'ordered') {\n    text = text.replace(reOrderedList, '$1* ');\n  }\n\n  return text;\n}\n\nfunction textToOrdered(text: string, ordinalNum: number) {\n  if (!reList.test(text)) {\n    return `${ordinalNum}. ${text}`;\n  }\n  const type = getListType(text);\n\n  if (type === 'bullet' || (type === 'ordered' && reCanBeTaskList.test(text))) {\n    text = text.replace(reCanBeTaskList, `$1${ordinalNum}. `);\n  } else if (type === 'ordered') {\n    // eslint-disable-next-line prefer-destructuring\n    const start = reOrderedListGroup.exec(text)![3];\n\n    if (Number(start) !== ordinalNum) {\n      text = text.replace(reOrderedList, `$1${ordinalNum}. `);\n    }\n  }\n\n  return text;\n}\n\nfunction getChangedInfo(\n  doc: ProsemirrorNode,\n  sameDepthItems: ItemInfo[],\n  type: ListType,\n  start = 0\n): ToListResult {\n  let firstIndex = Number.MAX_VALUE;\n  let lastIndex = 0;\n\n  const changedResults = sameDepthItems.map(({ line }, index) => {\n    firstIndex = Math.min(line - 1, firstIndex);\n    lastIndex = Math.max(line - 1, lastIndex);\n\n    let text = getTextByMdLine(doc, line);\n\n    text = type === 'bullet' ? textToBullet(text) : textToOrdered(text, index + 1 + start);\n\n    return { text, line };\n  });\n\n  return { changedResults, firstIndex, lastIndex };\n}\n\nfunction getBulletOrOrdered(type: ListType, context: ToListContext) {\n  const sameDepthListInfo = getSameDepthItems(context);\n\n  return getChangedInfo(context.doc, sameDepthListInfo, type);\n}\n\nexport const otherListToList: ListToList = {\n  bullet(context) {\n    return getBulletOrOrdered('bullet', context);\n  },\n  ordered(context) {\n    return getBulletOrOrdered('ordered', context);\n  },\n  task({ mdNode, doc, line }) {\n    let text = getTextByMdLine(doc, line);\n\n    if (mdNode.listData.task) {\n      text = text.replace(reTaskList, '$1$2');\n    } else if (isListNode(mdNode)) {\n      text = text.replace(reList, '$1$2[ ] ');\n    }\n\n    return { changedResults: [{ text, line }] };\n  },\n};\n\nexport const otherNodeToList: NodeToList = {\n  bullet({ doc, line }) {\n    const lineText = getTextByMdLine(doc, line);\n    const changedResults = [{ text: `* ${lineText}`, line }];\n\n    return { changedResults };\n  },\n  ordered({ toastMark, doc, line, startLine }) {\n    const lineText = getTextByMdLine(doc, line);\n    let firstOrderedListNum = 1;\n    let firstOrderedListLine = startLine;\n    let skipped = 0;\n\n    for (let i = startLine - 1; i > 0; i -= 1) {\n      const mdNode = toastMark.findFirstNodeAtLine(i)!;\n      const text = getTextByMdLine(doc, i);\n      const canBeListNode =\n        text && !!findClosestNode(mdNode, (targetNode) => isListNode(targetNode));\n      const searchResult = reOrderedListGroup.exec(getTextByMdLine(doc, i));\n\n      if (!searchResult && !canBeListNode) {\n        break;\n      }\n      if (!searchResult && canBeListNode) {\n        skipped += 1;\n        continue;\n      }\n      const [, indent, , start] = searchResult!;\n\n      // basis on one depth list\n      if (!indent) {\n        firstOrderedListNum = Number(start);\n        firstOrderedListLine = i;\n        break;\n      }\n    }\n    const ordinalNum = firstOrderedListNum + line - firstOrderedListLine - skipped;\n    const changedResults = [{ text: `${ordinalNum}. ${lineText}`, line }];\n\n    return { changedResults };\n  },\n  task({ doc, line }) {\n    const lineText = getTextByMdLine(doc, line);\n    const changedResults = [{ text: `* [ ] ${lineText}`, line }];\n\n    return { changedResults };\n  },\n};\n\nexport const extendList: ExtendList = {\n  bullet({ line, doc }: ExtendListContext) {\n    const lineText = getTextByMdLine(doc, line);\n    const [, indent, delimiter] = reBulletListGroup.exec(lineText)!;\n\n    return { listSyntax: `${indent}${delimiter}` };\n  },\n  ordered({ toastMark, line, mdNode, doc }: ExtendListContext) {\n    const depth = getListDepth(mdNode);\n    const lineText = getTextByMdLine(doc, line);\n\n    const [, indent, , start, delimiter] = reOrderedListGroup.exec(lineText)!;\n    const ordinalNum = Number(start) + 1;\n    const listSyntax = `${indent}${ordinalNum}${delimiter}`;\n\n    const backwardList = findSameDepthList(toastMark, line, depth, true);\n    const filteredList = backwardList.filter((info) => {\n      const searchResult = reOrderedListGroup.exec(getTextByMdLine(doc, info.line));\n\n      return (\n        searchResult &&\n        searchResult[1].length === indent.length &&\n        !!findClosestNode(info.mdNode, (targetNode) => isOrderedListNode(targetNode))\n      );\n    });\n\n    return { listSyntax, ...getChangedInfo(doc, filteredList, 'ordered', ordinalNum) };\n  },\n};\n\nexport function getReorderedListInfo(\n  doc: ProsemirrorNode,\n  schema: Schema,\n  line: number,\n  ordinalNum: number,\n  prevIndentLength: number\n) {\n  let nodes: ProsemirrorNode[] = [];\n  let lineText = getTextByMdLine(doc, line);\n  let searchResult = reOrderedListGroup.exec(lineText);\n\n  while (searchResult) {\n    const [, indent, , , delimiter, text] = searchResult;\n    const indentLength = indent.length;\n\n    if (indentLength === prevIndentLength) {\n      nodes.push(createTextNode(schema, `${indent}${ordinalNum}${delimiter}${text}`));\n      ordinalNum += 1;\n      line += 1;\n    } else if (indentLength > prevIndentLength) {\n      const nestedListInfo = getReorderedListInfo(doc, schema, line, 1, indentLength);\n\n      line = nestedListInfo.line;\n      nodes = nodes.concat(nestedListInfo.nodes);\n    }\n\n    if (indentLength < prevIndentLength || line > doc.childCount) {\n      break;\n    }\n\n    lineText = getTextByMdLine(doc, line);\n    searchResult = reOrderedListGroup.exec(lineText);\n  }\n\n  return { nodes, line };\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/helper/mdCommand.ts",
    "content": "import isFunction from 'tui-code-snippet/type/isFunction';\nimport { EditorCommand } from '@t/spec';\nimport { createTextSelection } from '@/helper/manipulation';\nimport { resolveSelectionPos } from './pos';\n\ntype ConditionFn = (text: string) => boolean;\ntype Condition = RegExp | ConditionFn;\n\nexport function toggleMark(condition: Condition, syntax: string): EditorCommand {\n  return () => ({ tr, selection }, dispatch) => {\n    const conditionFn: ConditionFn = !isFunction(condition)\n      ? (text) => condition.test(text)\n      : condition;\n    const syntaxLen = syntax.length;\n    const { doc } = tr;\n\n    const [from, to] = resolveSelectionPos(selection);\n    const prevPos = Math.max(from - syntaxLen, 1);\n    const nextPos = Math.min(to + syntaxLen, doc.content.size - 1);\n    const slice = selection.content();\n\n    let textContent = slice.content.textBetween(0, slice.content.size, '\\n');\n    const prevText = doc.textBetween(prevPos, from, '\\n');\n    const nextText = doc.textBetween(to, nextPos, '\\n');\n\n    textContent = `${prevText}${textContent}${nextText}`;\n\n    if (prevText && nextText && conditionFn(textContent)) {\n      tr.delete(nextPos - syntaxLen, nextPos).delete(prevPos, prevPos + syntaxLen);\n    } else {\n      tr.insertText(syntax, to).insertText(syntax, from);\n      const newSelection = selection.empty\n        ? createTextSelection(tr, from + syntaxLen)\n        : createTextSelection(tr, from + syntaxLen, to + syntaxLen);\n\n      tr.setSelection(newSelection);\n    }\n    dispatch!(tr);\n\n    return true;\n  };\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/helper/pos.ts",
    "content": "import { AllSelection, Selection } from 'prosemirror-state';\nimport { ProsemirrorNode, ResolvedPos } from 'prosemirror-model';\nimport { Sourcepos, MdPos } from '@toast-ui/toastmark';\nimport { isWidgetNode } from '@/widget/widgetNode';\n\nexport function resolveSelectionPos(selection: Selection) {\n  const { from, to } = selection;\n\n  if (selection instanceof AllSelection) {\n    return [from + 1, to - 1];\n  }\n  return [from, to];\n}\n\nfunction getMdLine(resolvedPos: ResolvedPos) {\n  return resolvedPos.index(0) + 1;\n}\n\nexport function getWidgetNodePos(node: ProsemirrorNode, chPos: number, direction: 1 | -1 = 1) {\n  let additionalPos = 0;\n\n  node.forEach((child, pos) => {\n    // add or subtract widget node tag\n    if (isWidgetNode(child) && pos + 2 < chPos) {\n      additionalPos += 2 * direction;\n    }\n  });\n\n  return additionalPos;\n}\n\nexport function getEditorToMdPos(doc: ProsemirrorNode, from: number, to = from): Sourcepos {\n  const collapsed = from === to;\n  const startResolvedPos = doc.resolve(from);\n  const startLine = getMdLine(startResolvedPos);\n  let endLine = startLine;\n\n  const startOffset = startResolvedPos.start(1);\n  let endOffset = startOffset;\n\n  if (!collapsed) {\n    // prevent the end offset from pointing to the root document position\n    const endResolvedPos = doc.resolve(to === doc.content.size ? to - 1 : to);\n\n    endOffset = endResolvedPos.start(1);\n    endLine = getMdLine(endResolvedPos);\n\n    // To resolve the end offset excluding document tag size\n    if (endResolvedPos.pos === doc.content.size) {\n      to = doc.content.size - 2;\n    }\n  }\n\n  const startCh = Math.max(from - startOffset + 1, 1);\n  const endCh = Math.max(to - endOffset + 1, 1);\n\n  return [\n    [startLine, startCh + getWidgetNodePos(doc.child(startLine - 1), startCh, -1)],\n    [endLine, endCh + getWidgetNodePos(doc.child(endLine - 1), endCh, -1)],\n  ];\n}\n\nexport function getStartPosListPerLine(doc: ProsemirrorNode, endIndex: number) {\n  const startPosListPerLine: number[] = [];\n\n  for (let i = 0, pos = 0; i < endIndex; i += 1) {\n    const child = doc.child(i);\n\n    startPosListPerLine[i] = pos;\n    pos += child.nodeSize;\n  }\n\n  return startPosListPerLine;\n}\n\nexport function getMdToEditorPos(doc: ProsemirrorNode, startPos: MdPos, endPos: MdPos) {\n  const startPosListPerLine = getStartPosListPerLine(doc, endPos[0]);\n  const startIndex = startPos[0] - 1;\n  const endIndex = endPos[0] - 1;\n  const startNode = doc.child(startIndex);\n  const endNode = doc.child(endIndex);\n\n  // calculate the position corresponding to the line\n  let from = startPosListPerLine[startIndex];\n  let to = startPosListPerLine[endIndex];\n\n  // calculate the position corresponding to the character offset of the line\n  from += startPos[1] + getWidgetNodePos(startNode, startPos[1] - 1);\n  to += endPos[1] + getWidgetNodePos(endNode, endPos[1] - 1);\n\n  return [from, Math.min(to, doc.content.size)];\n}\n\nexport function getRangeInfo(selection: Selection) {\n  let { $from, $to } = selection;\n  const { from, to } = selection;\n  const { doc } = $from;\n\n  if (selection instanceof AllSelection) {\n    $from = doc.resolve(from + 1);\n    $to = doc.resolve(to - 1);\n  }\n  if ($from.depth === 0) {\n    $from = doc.resolve(from - 1);\n    $to = $from;\n  }\n\n  return {\n    startFromOffset: $from.start(1),\n    endFromOffset: $to.start(1),\n    startToOffset: $from.end(1),\n    endToOffset: $to.end(1),\n    startIndex: $from.index(0),\n    endIndex: $to.index(0),\n    from: $from.pos,\n    to: $to.pos,\n  };\n}\n\nexport function getNodeContentOffsetRange(doc: ProsemirrorNode, targetIndex: number) {\n  let startOffset = 1;\n  let endOffset = 1;\n\n  for (let i = 0, offset = 0; i < doc.childCount; i += 1) {\n    const { nodeSize } = doc.child(i);\n\n    // calculate content start, end offset(not node offset)\n    startOffset = offset + 1;\n    endOffset = offset + nodeSize - 1;\n\n    if (i === targetIndex) {\n      break;\n    }\n\n    offset += nodeSize;\n  }\n  return { startOffset, endOffset };\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/helper/query.ts",
    "content": "import { ProsemirrorNode } from 'prosemirror-model';\n\nexport function getTextByMdLine(doc: ProsemirrorNode, mdLine: number) {\n  return getTextContent(doc, mdLine - 1);\n}\n\nexport function getTextContent(doc: ProsemirrorNode, index: number) {\n  return doc.child(index).textContent;\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/htmlRenderConvertors.ts",
    "content": "import isFunction from 'tui-code-snippet/type/isFunction';\nimport {\n  HTMLConvertorMap,\n  MdNode,\n  ListItemMdNode,\n  CodeMdNode,\n  CodeBlockMdNode,\n  CustomInlineMdNode,\n  OpenTagToken,\n  Context,\n  HTMLConvertor,\n} from '@t/toastmark';\nimport { LinkAttributes, CustomHTMLRenderer } from '@t/editor';\nimport { HTMLMdNode } from '@t/markdown';\nimport { getWidgetContent, widgetToDOM } from '@/widget/rules';\nimport { getChildrenHTML, getHTMLAttrsByHTMLString } from '@/wysiwyg/nodes/html';\nimport { includes } from '@/utils/common';\nimport { reHTMLTag } from '@/utils/constants';\n\ntype TokenAttrs = Record<string, any>;\n\nconst reCloseTag = /^\\s*<\\s*\\//;\n\nconst baseConvertors: HTMLConvertorMap = {\n  paragraph(_, { entering, origin, options }: Context) {\n    if (options.nodeId) {\n      return {\n        type: entering ? 'openTag' : 'closeTag',\n        outerNewLine: true,\n        tagName: 'p',\n      };\n    }\n\n    return origin!();\n  },\n\n  softbreak(node: MdNode) {\n    const isPrevNodeHTML = node.prev && node.prev.type === 'htmlInline';\n    const isPrevBR = isPrevNodeHTML && /<br ?\\/?>/.test(node.prev!.literal!);\n    const content = isPrevBR ? '\\n' : '<br>\\n';\n\n    return { type: 'html', content };\n  },\n\n  item(node: MdNode, { entering }: Context) {\n    if (entering) {\n      const attributes: TokenAttrs = {};\n      const classNames = [];\n\n      if ((node as ListItemMdNode).listData.task) {\n        attributes['data-task'] = '';\n        classNames.push('task-list-item');\n        if ((node as ListItemMdNode).listData.checked) {\n          classNames.push('checked');\n          attributes['data-task-checked'] = '';\n        }\n      }\n\n      return {\n        type: 'openTag',\n        tagName: 'li',\n        classNames,\n        attributes,\n        outerNewLine: true,\n      };\n    }\n\n    return {\n      type: 'closeTag',\n      tagName: 'li',\n      outerNewLine: true,\n    };\n  },\n\n  code(node: MdNode) {\n    const attributes = { 'data-backticks': String((node as CodeMdNode).tickCount) };\n\n    return [\n      { type: 'openTag', tagName: 'code', attributes },\n      { type: 'text', content: node.literal! },\n      { type: 'closeTag', tagName: 'code' },\n    ];\n  },\n\n  codeBlock(node: MdNode) {\n    const { fenceLength, info } = node as CodeBlockMdNode;\n    const infoWords = info ? info.split(/\\s+/) : [];\n    const preClasses = [];\n    const codeAttrs: TokenAttrs = {};\n\n    if (fenceLength > 3) {\n      codeAttrs['data-backticks'] = fenceLength;\n    }\n    if (infoWords.length > 0 && infoWords[0].length > 0) {\n      const [lang] = infoWords;\n\n      preClasses.push(`lang-${lang}`);\n      codeAttrs['data-language'] = lang;\n    }\n\n    return [\n      { type: 'openTag', tagName: 'pre', classNames: preClasses },\n      { type: 'openTag', tagName: 'code', attributes: codeAttrs },\n      { type: 'text', content: node.literal! },\n      { type: 'closeTag', tagName: 'code' },\n      { type: 'closeTag', tagName: 'pre' },\n    ];\n  },\n\n  customInline(node: MdNode, { origin, entering, skipChildren }: Context) {\n    const { info } = node as CustomInlineMdNode;\n\n    if (info.indexOf('widget') !== -1 && entering) {\n      skipChildren();\n      const content = getWidgetContent(node as CustomInlineMdNode);\n      const htmlInline = widgetToDOM(info, content).outerHTML;\n\n      return [\n        { type: 'openTag', tagName: 'span', classNames: ['tui-widget'] },\n        { type: 'html', content: htmlInline },\n        { type: 'closeTag', tagName: 'span' },\n      ];\n    }\n    return origin!();\n  },\n};\n\nexport function getHTMLRenderConvertors(\n  linkAttributes: LinkAttributes | null,\n  customConvertors: CustomHTMLRenderer\n) {\n  const convertors = { ...baseConvertors };\n\n  if (linkAttributes) {\n    convertors.link = (_, { entering, origin }: Context) => {\n      const result = origin!();\n\n      if (entering) {\n        (result as OpenTagToken).attributes = {\n          ...(result as OpenTagToken).attributes,\n          ...linkAttributes,\n        } as TokenAttrs;\n      }\n      return result;\n    };\n  }\n\n  if (customConvertors) {\n    Object.keys(customConvertors).forEach((nodeType: string) => {\n      const orgConvertor = convertors[nodeType];\n      const customConvertor = customConvertors[nodeType]!;\n\n      if (orgConvertor && isFunction(customConvertor)) {\n        convertors[nodeType] = (node, context) => {\n          const newContext = { ...context };\n\n          newContext.origin = () => orgConvertor(node, context);\n          return customConvertor(node, newContext);\n        };\n      } else if (includes(['htmlBlock', 'htmlInline'], nodeType) && !isFunction(customConvertor)) {\n        convertors[nodeType] = (node, context) => {\n          const matched = node.literal!.match(reHTMLTag);\n\n          if (matched) {\n            const [rootHTML, openTagName, , closeTagName] = matched;\n            const typeName = (openTagName || closeTagName).toLowerCase();\n            const htmlConvertor = customConvertor[typeName];\n            const childrenHTML = getChildrenHTML(node, typeName);\n\n            if (htmlConvertor) {\n              // copy for preventing to overwrite the originial property\n              const newNode: HTMLMdNode = { ...node };\n\n              newNode.attrs = getHTMLAttrsByHTMLString(rootHTML);\n              newNode.childrenHTML = childrenHTML;\n              newNode.type = typeName;\n              context.entering = !reCloseTag.test(node.literal!);\n\n              return htmlConvertor(newNode, context);\n            }\n          }\n          return context.origin!();\n        };\n      } else {\n        convertors[nodeType] = customConvertor as HTMLConvertor;\n      }\n    });\n  }\n\n  return convertors;\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/marks/blockQuote.ts",
    "content": "import { DOMOutputSpec } from 'prosemirror-model';\nimport { Command } from 'prosemirror-commands';\nimport { EditorCommand } from '@t/spec';\nimport { clsWithMdPrefix } from '@/utils/dom';\nimport Mark from '@/spec/mark';\nimport {\n  createTextNode,\n  createTextSelection,\n  replaceTextNode,\n  splitAndExtendBlock,\n} from '@/helper/manipulation';\nimport { getRangeInfo } from '../helper/pos';\nimport { getTextContent } from '../helper/query';\n\nexport const reBlockQuote = /^\\s*> ?/;\n\nexport class BlockQuote extends Mark {\n  get name() {\n    return 'blockQuote';\n  }\n\n  get schema() {\n    return {\n      toDOM(): DOMOutputSpec {\n        return ['span', { class: clsWithMdPrefix('block-quote') }, 0];\n      },\n    };\n  }\n\n  private createBlockQuoteText(text: string, isBlockQuote?: boolean) {\n    return isBlockQuote ? text.replace(reBlockQuote, '').trim() : `> ${text.trim()}`;\n  }\n\n  private extendBlockQuote(): Command {\n    return ({ selection, doc, tr, schema }, dispatch) => {\n      const { endFromOffset, endToOffset, endIndex, to } = getRangeInfo(selection);\n      const textContent = getTextContent(doc, endIndex);\n      const isBlockQuote = reBlockQuote.test(textContent);\n\n      if (isBlockQuote && to > endFromOffset && selection.empty) {\n        const isEmpty = !textContent.replace(reBlockQuote, '').trim();\n\n        if (isEmpty) {\n          tr.deleteRange(endFromOffset, endToOffset).split(tr.mapping.map(endToOffset));\n        } else {\n          const slicedText = textContent.slice(to - endFromOffset).trim();\n          const node = createTextNode(schema, this.createBlockQuoteText(slicedText));\n\n          splitAndExtendBlock(tr, endToOffset, slicedText, node);\n        }\n        dispatch!(tr);\n        return true;\n      }\n\n      return false;\n    };\n  }\n\n  commands(): EditorCommand {\n    return () => (state, dispatch) => {\n      const { selection, doc } = state;\n      const { startFromOffset, endToOffset, startIndex, endIndex } = getRangeInfo(selection);\n      const isBlockQuote = reBlockQuote.test(getTextContent(doc, startIndex));\n      const tr = replaceTextNode({\n        state,\n        startIndex,\n        endIndex,\n        from: startFromOffset,\n        createText: (textContent) => this.createBlockQuoteText(textContent, isBlockQuote),\n      });\n\n      dispatch!(tr.setSelection(createTextSelection(tr, tr.mapping.map(endToOffset))));\n      return true;\n    };\n  }\n\n  keymaps() {\n    const blockQuoteCommand = this.commands()();\n\n    return {\n      'alt-q': blockQuoteCommand,\n      'alt-Q': blockQuoteCommand,\n      Enter: this.extendBlockQuote(),\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/marks/code.ts",
    "content": "import { DOMOutputSpec, Mark as ProsemirrorMark } from 'prosemirror-model';\nimport { EditorCommand } from '@t/spec';\nimport { clsWithMdPrefix } from '@/utils/dom';\nimport Mark from '@/spec/mark';\nimport { toggleMark } from '../helper/mdCommand';\n\nconst reCode = /^(`).*([\\s\\S]*)\\1$/m;\nconst codeSyntax = '`';\n\nexport class Code extends Mark {\n  get name() {\n    return 'code';\n  }\n\n  get schema() {\n    return {\n      attrs: {\n        start: { default: false },\n        end: { default: false },\n        marked: { default: false },\n      },\n      toDOM(mark: ProsemirrorMark): DOMOutputSpec {\n        const { start, end, marked } = mark.attrs;\n        let classNames = 'code';\n\n        if (start) {\n          classNames += '|delimiter|start';\n        }\n        if (end) {\n          classNames += '|delimiter|end';\n        }\n        if (marked) {\n          classNames += '|marked-text';\n        }\n\n        return ['span', { class: clsWithMdPrefix(...classNames.split('|')) }, 0];\n      },\n    };\n  }\n\n  commands(): EditorCommand {\n    return toggleMark(reCode, codeSyntax);\n  }\n\n  keymaps() {\n    const codeCommand = this.commands()();\n\n    return { 'Shift-Mod-c': codeCommand, 'Shift-Mod-C': codeCommand };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/marks/codeBlock.ts",
    "content": "import { DOMOutputSpec } from 'prosemirror-model';\nimport { Command } from 'prosemirror-commands';\nimport { EditorCommand, MdSpecContext } from '@t/spec';\nimport { clsWithMdPrefix } from '@/utils/dom';\nimport Mark from '@/spec/mark';\nimport { createTextNode, createTextSelection, splitAndExtendBlock } from '@/helper/manipulation';\nimport { isCodeBlockNode } from '@/utils/markdown';\nimport { getRangeInfo } from '../helper/pos';\nimport { getTextContent } from '../helper/query';\n\nconst fencedCodeBlockSyntax = '```';\n\nexport class CodeBlock extends Mark {\n  context!: MdSpecContext;\n\n  get name() {\n    return 'codeBlock';\n  }\n\n  get schema() {\n    return {\n      toDOM(): DOMOutputSpec {\n        return ['span', { class: clsWithMdPrefix('code-block') }, 0];\n      },\n    };\n  }\n\n  commands(): EditorCommand {\n    return () => (state, dispatch) => {\n      const { selection, schema, tr } = state;\n      const { startFromOffset, endToOffset } = getRangeInfo(selection);\n      const fencedNode = createTextNode(schema, fencedCodeBlockSyntax);\n\n      // add fenced start block\n      tr.insert(startFromOffset, fencedNode).split(startFromOffset + fencedCodeBlockSyntax.length);\n      // add fenced end block\n      tr.split(tr.mapping.map(endToOffset)).insert(tr.mapping.map(endToOffset), fencedNode);\n\n      dispatch!(\n        tr.setSelection(\n          // subtract fenced syntax length and open, close tag(2)\n          createTextSelection(tr, tr.mapping.map(endToOffset) - (fencedCodeBlockSyntax.length + 2))\n        )\n      );\n\n      return true;\n    };\n  }\n\n  private keepIndentation(): Command {\n    return ({ selection, tr, doc, schema }, dispatch) => {\n      const { toastMark } = this.context;\n      const { startFromOffset, endToOffset, endIndex, from, to } = getRangeInfo(selection);\n      const textContent = getTextContent(doc, endIndex);\n\n      if (from === to && textContent.trim()) {\n        const matched = textContent.match(/^\\s+/);\n        const mdNode = toastMark.findFirstNodeAtLine(endIndex + 1)!;\n\n        if (isCodeBlockNode(mdNode) && matched) {\n          const [spaces] = matched;\n          const slicedText = textContent.slice(to - startFromOffset);\n          const node = createTextNode(schema, spaces + slicedText);\n\n          splitAndExtendBlock(tr, endToOffset, slicedText, node);\n\n          dispatch!(tr);\n\n          return true;\n        }\n      }\n      return false;\n    };\n  }\n\n  keymaps() {\n    const codeBlockCommand = this.commands()();\n\n    return {\n      'Shift-Mod-p': codeBlockCommand,\n      'Shift-Mod-P': codeBlockCommand,\n      Enter: this.keepIndentation(),\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/marks/customBlock.ts",
    "content": "import { DOMOutputSpec } from 'prosemirror-model';\nimport { clsWithMdPrefix } from '@/utils/dom';\nimport Mark from '@/spec/mark';\nimport { EditorCommand } from '@t/spec';\nimport { getRangeInfo } from '../helper/pos';\nimport { createTextNode, createTextSelection } from '@/helper/manipulation';\n\nconst customBlockSyntax = '$$';\n\nexport class CustomBlock extends Mark {\n  get name() {\n    return 'customBlock';\n  }\n\n  get schema() {\n    return {\n      toDOM(): DOMOutputSpec {\n        return ['span', { class: clsWithMdPrefix('custom-block') }, 0];\n      },\n    };\n  }\n\n  commands(): EditorCommand {\n    return (payload) => (state, dispatch) => {\n      const { selection, schema, tr } = state;\n      const { startFromOffset, endToOffset } = getRangeInfo(selection);\n\n      if (!payload?.info) {\n        return false;\n      }\n\n      const customBlock = `${customBlockSyntax}${payload.info}`;\n      const startNode = createTextNode(schema, customBlock);\n      const endNode = createTextNode(schema, customBlockSyntax);\n\n      tr.insert(startFromOffset, startNode).split(startFromOffset + customBlock.length);\n      tr.split(tr.mapping.map(endToOffset)).insert(tr.mapping.map(endToOffset), endNode);\n\n      dispatch!(\n        tr.setSelection(\n          createTextSelection(tr, tr.mapping.map(endToOffset) - (customBlockSyntax.length + 2))\n        )\n      );\n\n      return true;\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/marks/emph.ts",
    "content": "import { DOMOutputSpec } from 'prosemirror-model';\nimport { EditorCommand } from '@t/spec';\nimport { clsWithMdPrefix } from '@/utils/dom';\nimport Mark from '@/spec/mark';\nimport { toggleMark } from '../helper/mdCommand';\n\nconst reEmph = /^(\\*|_).*([\\s\\S]*)\\1$/m;\nconst emphSyntax = '*';\n\nexport class Emph extends Mark {\n  get name() {\n    return 'emph';\n  }\n\n  get schema() {\n    return {\n      toDOM(): DOMOutputSpec {\n        return ['span', { class: clsWithMdPrefix('emph') }, 0];\n      },\n    };\n  }\n\n  private italic(): EditorCommand {\n    return toggleMark(reEmph, emphSyntax);\n  }\n\n  commands() {\n    return { italic: this.italic() };\n  }\n\n  keymaps() {\n    const italicCommand = this.italic()();\n\n    return { 'Mod-i': italicCommand, 'Mod-I': italicCommand };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/marks/heading.ts",
    "content": "import { DOMOutputSpec, Mark as ProsemirrorMark } from 'prosemirror-model';\nimport { EditorCommand } from '@t/spec';\nimport { clsWithMdPrefix } from '@/utils/dom';\nimport Mark from '@/spec/mark';\nimport { createTextSelection, replaceTextNode } from '@/helper/manipulation';\nimport { getRangeInfo } from '../helper/pos';\n\nconst reHeading = /^#{1,6}\\s/;\n\ninterface Payload {\n  level: number;\n}\n\nexport class Heading extends Mark {\n  get name() {\n    return 'heading';\n  }\n\n  get schema() {\n    return {\n      attrs: {\n        level: { default: 1 },\n        seText: { default: false },\n      },\n      toDOM({ attrs }: ProsemirrorMark): DOMOutputSpec {\n        const { level, seText } = attrs;\n        let classNames = `heading|heading${level}`;\n\n        if (seText) {\n          classNames += '|delimiter|setext';\n        }\n        return ['span', { class: clsWithMdPrefix(...classNames.split('|')) }, 0];\n      },\n    };\n  }\n\n  private createHeadingText(level: number, text: string, curHeadingSyntax: string) {\n    const textContent = text.replace(curHeadingSyntax, '').trim();\n    let headingText = '';\n\n    while (level > 0) {\n      headingText += '#';\n      level -= 1;\n    }\n\n    return `${headingText} ${textContent}`;\n  }\n\n  commands(): EditorCommand<Payload> {\n    return (payload) => (state, dispatch) => {\n      const { level } = payload!;\n      const { startFromOffset, endToOffset, startIndex, endIndex } = getRangeInfo(state.selection);\n\n      const tr = replaceTextNode({\n        state,\n        from: startFromOffset,\n        startIndex,\n        endIndex,\n        createText: (textContent) => {\n          const matchedHeading = textContent.match(reHeading);\n          const curHeadingSyntax = matchedHeading ? matchedHeading[0] : '';\n\n          return this.createHeadingText(level, textContent, curHeadingSyntax);\n        },\n      });\n\n      dispatch!(tr.setSelection(createTextSelection(tr, tr.mapping.map(endToOffset))));\n      return true;\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/marks/html.ts",
    "content": "import { DOMOutputSpec } from 'prosemirror-model';\nimport { clsWithMdPrefix } from '@/utils/dom';\nimport Mark from '@/spec/mark';\n\nexport class Html extends Mark {\n  get name() {\n    return 'html';\n  }\n\n  get schema() {\n    return {\n      toDOM(): DOMOutputSpec {\n        return ['span', { class: clsWithMdPrefix('html') }, 0];\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/marks/link.ts",
    "content": "import { DOMOutputSpec, Mark as ProsemirrorMark } from 'prosemirror-model';\nimport { EditorCommand } from '@t/spec';\nimport { clsWithMdPrefix } from '@/utils/dom';\nimport { escapeTextForLink } from '@/utils/common';\nimport Mark from '@/spec/mark';\nimport { createTextNode } from '@/helper/manipulation';\nimport { resolveSelectionPos } from '../helper/pos';\n\ntype CommandType = 'image' | 'link';\n\ninterface Payload {\n  linkText: string;\n  altText: string;\n  linkUrl: string;\n  imageUrl: string;\n}\n\nexport class Link extends Mark {\n  get name() {\n    return 'link';\n  }\n\n  get schema() {\n    return {\n      attrs: {\n        url: { default: false },\n        desc: { default: false },\n      },\n      toDOM({ attrs }: ProsemirrorMark): DOMOutputSpec {\n        const { url, desc } = attrs;\n        let classNames = 'link';\n\n        if (url) {\n          classNames += '|link-url|marked-text';\n        }\n        if (desc) {\n          classNames += '|link-desc|marked-text';\n        }\n\n        return ['span', { class: clsWithMdPrefix(...classNames.split('|')) }, 0];\n      },\n    };\n  }\n\n  private addLinkOrImage(commandType: CommandType): EditorCommand<Payload> {\n    return (payload) => ({ selection, tr, schema }, dispatch) => {\n      const [from, to] = resolveSelectionPos(selection);\n      const { linkText, altText, linkUrl, imageUrl } = payload!;\n      let text = linkText;\n      let url = linkUrl;\n      let syntax = '';\n\n      if (commandType === 'image') {\n        text = altText;\n        url = imageUrl;\n        syntax = '!';\n      }\n\n      text = escapeTextForLink(text);\n      syntax += `[${text}](${url})`;\n\n      dispatch!(tr.replaceWith(from, to, createTextNode(schema, syntax)));\n\n      return true;\n    };\n  }\n\n  commands() {\n    return {\n      addImage: this.addLinkOrImage('image'),\n      addLink: this.addLinkOrImage('link'),\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/marks/listItem.ts",
    "content": "import { DOMOutputSpec, Mark as ProsemirrorMark } from 'prosemirror-model';\nimport { Transaction } from 'prosemirror-state';\nimport { Command } from 'prosemirror-commands';\nimport { ListItemMdNode, MdNode } from '@toast-ui/toastmark';\nimport { EditorCommand, MdSpecContext } from '@t/spec';\nimport { clsWithMdPrefix } from '@/utils/dom';\nimport Mark from '@/spec/mark';\nimport { isListNode } from '@/utils/markdown';\nimport { createTextNode, createTextSelection, splitAndExtendBlock } from '@/helper/manipulation';\nimport { last } from '@/utils/common';\nimport {\n  ChangedListInfo,\n  extendList,\n  ExtendListContext,\n  getListType,\n  otherListToList,\n  otherNodeToList,\n  reCanBeTaskList,\n  reList,\n  ToListContext,\n} from '../helper/list';\nimport { getRangeInfo, getNodeContentOffsetRange } from '../helper/pos';\nimport { getTextContent } from '../helper/query';\n\ntype CommandType = 'bullet' | 'ordered' | 'task';\n\nfunction cannotBeListNode({ type, sourcepos }: MdNode, line: number) {\n  // eslint-disable-next-line prefer-destructuring\n  const startLine = sourcepos![0][0];\n\n  return line <= startLine && (type === 'codeBlock' || type === 'heading' || type.match('table'));\n}\n\ninterface RangeInfo {\n  from: number;\n  startLine: number;\n  endLine: number;\n  indexDiff?: number;\n}\n\nexport class ListItem extends Mark {\n  context!: MdSpecContext;\n\n  get name() {\n    return 'listItem';\n  }\n\n  get schema() {\n    return {\n      attrs: {\n        odd: { default: false },\n        even: { default: false },\n        listStyle: { default: false },\n      },\n      toDOM({ attrs }: ProsemirrorMark): DOMOutputSpec {\n        const { odd, even, listStyle } = attrs;\n        let classNames = 'list-item';\n\n        if (listStyle) {\n          classNames += '|list-item-style';\n        }\n        if (odd) {\n          classNames += '|list-item-odd';\n        }\n        if (even) {\n          classNames += '|list-item-even';\n        }\n        return ['span', { class: clsWithMdPrefix(...classNames.split('|')) }, 0];\n      },\n    };\n  }\n\n  private extendList(): Command {\n    return ({ selection, doc, schema, tr }, dispatch) => {\n      const { toastMark } = this.context;\n      const { to, startFromOffset, endFromOffset, endIndex, endToOffset } = getRangeInfo(selection);\n      const textContent = getTextContent(doc, endIndex);\n      const isList = reList.test(textContent);\n\n      if (!isList || selection.from === startFromOffset || !selection.empty) {\n        return false;\n      }\n\n      const isEmpty = !textContent.replace(reCanBeTaskList, '').trim();\n\n      if (isEmpty) {\n        tr.deleteRange(endFromOffset, endToOffset).split(tr.mapping.map(endToOffset));\n      } else {\n        const commandType = getListType(textContent);\n        // should add `1` to line for the markdown parser\n        // because markdown parser has `1`(not zero) as the start number\n        const mdNode = toastMark.findFirstNodeAtLine(endIndex + 1) as ListItemMdNode;\n        const slicedText = textContent.slice(to - endFromOffset);\n        const context: ExtendListContext = { toastMark, mdNode, doc, line: endIndex + 1 };\n        const { listSyntax, changedResults } = extendList[commandType](context);\n\n        // change ordinal number of backward ordered list\n        if (changedResults?.length) {\n          // split the block\n          tr.split(to);\n\n          // set first ordered list info\n          changedResults.unshift({ text: listSyntax + slicedText, line: endIndex + 1 });\n\n          this.changeToListPerLine(tr, changedResults, {\n            from: to,\n            // don't subtract 1 because the line has increased through 'split' command.\n            startLine: changedResults[0].line,\n            endLine: last(changedResults).line,\n          });\n\n          const pos = tr.mapping.map(endToOffset) - slicedText.length;\n\n          tr.setSelection(createTextSelection(tr, pos));\n        } else {\n          const node = createTextNode(schema, listSyntax + slicedText);\n\n          splitAndExtendBlock(tr, endToOffset, slicedText, node);\n        }\n      }\n      dispatch!(tr);\n      return true;\n    };\n  }\n\n  private toList(commandType: CommandType): EditorCommand {\n    return () => ({ doc, tr, selection }, dispatch) => {\n      const { toastMark } = this.context;\n      const rangeInfo = getRangeInfo(selection);\n      // should add `1` to line for the markdown parser\n      // because markdown parser has `1`(not zero) as the start number\n      const startLine = rangeInfo.startIndex + 1;\n      const endLine = rangeInfo.endIndex + 1;\n      let { endToOffset } = rangeInfo;\n\n      let skipLines: number[] = [];\n\n      for (let line = startLine; line <= endLine; line += 1) {\n        const mdNode: MdNode = toastMark.findFirstNodeAtLine(line)!;\n\n        if (mdNode && cannotBeListNode(mdNode, line)) {\n          break;\n        }\n\n        // to skip unnecessary processing\n        if (skipLines.indexOf(line) !== -1) {\n          continue;\n        }\n\n        const context: ToListContext<MdNode> = { toastMark, mdNode, doc, line, startLine };\n        const { changedResults } = isListNode(mdNode)\n          ? otherListToList[commandType](context as ToListContext)\n          : otherNodeToList[commandType](context);\n\n        const endOffset = this.changeToListPerLine(tr, changedResults, {\n          from: getNodeContentOffsetRange(doc, changedResults[0].line - 1).startOffset,\n          startLine: changedResults[0].line,\n          endLine: last(changedResults).line,\n          indexDiff: 1,\n        });\n\n        endToOffset = Math.max(endOffset, endToOffset);\n\n        if (changedResults) {\n          skipLines = skipLines.concat(changedResults.map((info) => info.line));\n        }\n      }\n\n      dispatch!(tr.setSelection(createTextSelection(tr, tr.mapping.map(endToOffset))));\n      return true;\n    };\n  }\n\n  private changeToListPerLine(\n    tr: Transaction,\n    changedResults: ChangedListInfo[],\n    { from, startLine, endLine, indexDiff = 0 }: RangeInfo\n  ) {\n    let maxEndOffset = 0;\n\n    for (let i = startLine - indexDiff; i <= endLine - indexDiff; i += 1) {\n      const { nodeSize, content } = tr.doc.child(i);\n      const mappedFrom = tr.mapping.map(from);\n      const mappedTo = mappedFrom + content.size;\n      const [changedResult] = changedResults.filter((result) => result.line - indexDiff === i);\n\n      if (changedResult) {\n        tr.replaceWith(\n          mappedFrom,\n          mappedTo,\n          createTextNode(this.context.schema, changedResult.text)\n        );\n        maxEndOffset = Math.max(maxEndOffset, from + content.size);\n      }\n      from += nodeSize;\n    }\n\n    return maxEndOffset;\n  }\n\n  private toggleTask(): Command {\n    return ({ selection, tr, doc, schema }, dispatch) => {\n      const { toastMark } = this.context;\n      const { startIndex, endIndex } = getRangeInfo(selection);\n      let newTr: Transaction | null = null;\n\n      for (let i = startIndex; i <= endIndex; i += 1) {\n        const mdNode = toastMark.findFirstNodeAtLine(i + 1)!;\n\n        if (isListNode(mdNode) && mdNode.listData.task) {\n          const { checked, padding } = mdNode.listData;\n          const stateChar = checked ? ' ' : 'x';\n          const [mdPos] = mdNode.sourcepos!;\n          let { startOffset } = getNodeContentOffsetRange(doc, mdPos[0] - 1);\n\n          startOffset += mdPos[1] + padding;\n\n          newTr = tr.replaceWith(startOffset, startOffset + 1, schema.text(stateChar));\n        }\n      }\n      if (newTr) {\n        dispatch!(newTr);\n        return true;\n      }\n      return false;\n    };\n  }\n\n  commands() {\n    return {\n      bulletList: this.toList('bullet'),\n      orderedList: this.toList('ordered'),\n      taskList: this.toList('task'),\n    };\n  }\n\n  keymaps() {\n    const bulletCommand = this.toList('bullet')();\n    const orderedCommand = this.toList('ordered')();\n    const taskCommand = this.toList('task')();\n    const togleTaskCommand = this.toggleTask();\n\n    return {\n      'Mod-u': bulletCommand,\n      'Mod-U': bulletCommand,\n      'Mod-o': orderedCommand,\n      'Mod-O': orderedCommand,\n      'alt-t': taskCommand,\n      'alt-T': taskCommand,\n      'Shift-Ctrl-x': togleTaskCommand,\n      'Shift-Ctrl-X': togleTaskCommand,\n      Enter: this.extendList(),\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/marks/simpleMark.ts",
    "content": "import { DOMOutputSpec } from 'prosemirror-model';\nimport { clsWithMdPrefix } from '@/utils/dom';\nimport Mark from '@/spec/mark';\n\nexport class TaskDelimiter extends Mark {\n  get name() {\n    return 'taskDelimiter';\n  }\n\n  get schema() {\n    return {\n      toDOM(): DOMOutputSpec {\n        return ['span', { class: clsWithMdPrefix('delimiter', 'list-item') }, 0];\n      },\n    };\n  }\n}\n\nexport class Delimiter extends Mark {\n  get name() {\n    return 'delimiter';\n  }\n\n  get schema() {\n    return {\n      toDOM(): DOMOutputSpec {\n        return ['span', { class: clsWithMdPrefix('delimiter') }, 0];\n      },\n    };\n  }\n}\n\nexport class Meta extends Mark {\n  get name() {\n    return 'meta';\n  }\n\n  get schema() {\n    return {\n      toDOM(): DOMOutputSpec {\n        return ['span', { class: clsWithMdPrefix('meta') }, 0];\n      },\n    };\n  }\n}\n\nexport class MarkedText extends Mark {\n  get name() {\n    return 'markedText';\n  }\n\n  get schema() {\n    return {\n      toDOM(): DOMOutputSpec {\n        return ['span', { class: clsWithMdPrefix('marked-text') }, 0];\n      },\n    };\n  }\n}\n\nexport class TableCell extends Mark {\n  get name() {\n    return 'tableCell';\n  }\n\n  get schema() {\n    return {\n      toDOM(): DOMOutputSpec {\n        return ['span', { class: clsWithMdPrefix('table-cell') }, 0];\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/marks/strike.ts",
    "content": "import { DOMOutputSpec } from 'prosemirror-model';\nimport { EditorCommand } from '@t/spec';\nimport { clsWithMdPrefix } from '@/utils/dom';\nimport Mark from '@/spec/mark';\nimport { toggleMark } from '../helper/mdCommand';\n\nconst reStrike = /^(~{2}).*([\\s\\S]*)\\1$/m;\nconst strikeSyntax = '~~';\n\nexport class Strike extends Mark {\n  get name() {\n    return 'strike';\n  }\n\n  get schema() {\n    return {\n      toDOM(): DOMOutputSpec {\n        return ['span', { class: clsWithMdPrefix('strike') }, 0];\n      },\n    };\n  }\n\n  commands(): EditorCommand {\n    return toggleMark(reStrike, strikeSyntax);\n  }\n\n  keymaps() {\n    const strikeCommand = this.commands()();\n\n    return { 'Mod-s': strikeCommand, 'Mod-S': strikeCommand };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/marks/strong.ts",
    "content": "import { DOMOutputSpec } from 'prosemirror-model';\nimport { EditorCommand } from '@t/spec';\nimport { clsWithMdPrefix } from '@/utils/dom';\nimport Mark from '@/spec/mark';\nimport { toggleMark } from '../helper/mdCommand';\n\nexport const reStrong = /^(\\*{2}|_{2}).*([\\s\\S]*)\\1$/m;\nconst strongSyntax = '**';\n\nexport class Strong extends Mark {\n  get name() {\n    return 'strong';\n  }\n\n  get schema() {\n    return {\n      toDOM(): DOMOutputSpec {\n        return ['span', { class: clsWithMdPrefix('strong') }, 0];\n      },\n    };\n  }\n\n  private bold(): EditorCommand {\n    return toggleMark(reStrong, strongSyntax);\n  }\n\n  commands() {\n    return { bold: this.bold() };\n  }\n\n  keymaps() {\n    const boldCommand = this.bold()();\n\n    return { 'Mod-b': boldCommand, 'Mod-B': boldCommand };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/marks/table.ts",
    "content": "import { DOMOutputSpec } from 'prosemirror-model';\nimport { Command } from 'prosemirror-commands';\nimport type { Transaction } from 'prosemirror-state';\nimport { TableCellMdNode, MdNode, MdPos } from '@toast-ui/toastmark';\nimport { EditorCommand, MdSpecContext } from '@t/spec';\nimport { TableRowMdNode } from '@t/markdown';\nimport { clsWithMdPrefix } from '@/utils/dom';\nimport { findClosestNode, getMdEndCh, isTableCellNode } from '@/utils/markdown';\nimport Mark from '@/spec/mark';\nimport { getRangeInfo } from '../helper/pos';\nimport { createTextNode, createTextSelection } from '@/helper/manipulation';\nimport { getTextContent } from '../helper/query';\n\ninterface Payload {\n  columnCount: number;\n  rowCount: number;\n}\n\ninterface MovingTypeInfo {\n  type: 'next' | 'prev';\n  parentType: 'tableHead' | 'tableBody';\n  childType: 'firstChild' | 'lastChild';\n}\n\nconst reEmptyTable = /\\||\\s/g;\n\nfunction createTableHeader(columnCount: number) {\n  return [createTableRow(columnCount), createTableRow(columnCount, true)];\n}\n\nfunction createTableBody(columnCount: number, rowCount: number) {\n  const bodyRows = [];\n\n  for (let i = 0; i < rowCount; i += 1) {\n    bodyRows.push(createTableRow(columnCount));\n  }\n\n  return bodyRows;\n}\n\nfunction createTableRow(columnCount: number, delim?: boolean) {\n  let row = '|';\n\n  for (let i = 0; i < columnCount; i += 1) {\n    row += delim ? ' --- |' : '  |';\n  }\n  return row;\n}\n\nfunction createTargetTypes(moveNext: boolean): MovingTypeInfo {\n  return moveNext\n    ? { type: 'next', parentType: 'tableHead', childType: 'firstChild' }\n    : { type: 'prev', parentType: 'tableBody', childType: 'lastChild' };\n}\n\nexport class Table extends Mark {\n  context!: MdSpecContext;\n\n  get name() {\n    return 'table';\n  }\n\n  get schema() {\n    return {\n      toDOM(): DOMOutputSpec {\n        return ['span', { class: clsWithMdPrefix('table') }, 0];\n      },\n    };\n  }\n\n  private extendTable(): Command {\n    return ({ selection, doc, tr, schema }, dispatch) => {\n      if (!selection.empty) {\n        return false;\n      }\n\n      const { endFromOffset, endToOffset, endIndex, to } = getRangeInfo(selection);\n      const textContent = getTextContent(doc, endIndex);\n      // should add `1` to line for the markdown parser\n      // because markdown parser has `1`(not zero) as the start number\n      const mdPos: MdPos = [endIndex + 1, to - endFromOffset + 1];\n      const mdNode: MdNode = this.context.toastMark.findNodeAtPosition(mdPos)!;\n      const cellNode = findClosestNode(\n        mdNode,\n        (node) =>\n          isTableCellNode(node) &&\n          (node.parent!.type === 'tableDelimRow' || node.parent!.parent!.type === 'tableBody')\n      ) as TableCellMdNode;\n\n      if (cellNode) {\n        const isEmpty = !textContent.replace(reEmptyTable, '').trim();\n        const parent = cellNode.parent as TableRowMdNode;\n        const columnCount = parent.parent.parent.columns.length;\n        const row = createTableRow(columnCount);\n\n        if (isEmpty) {\n          tr.deleteRange(endFromOffset, endToOffset).split(tr.mapping.map(endToOffset));\n        } else {\n          (tr\n            .split(endToOffset)\n            .insert(tr.mapping.map(endToOffset), createTextNode(schema, row)) as Transaction)\n            // should subtract `2` to selection end position considering ` |` text\n            .setSelection(createTextSelection(tr, tr.mapping.map(endToOffset) - 2));\n        }\n        dispatch!(tr);\n        return true;\n      }\n      return false;\n    };\n  }\n\n  private moveTableCell(moveNext: boolean): Command {\n    return ({ selection, tr }, dispatch) => {\n      const { endFromOffset, endIndex, to } = getRangeInfo(selection);\n      const mdPos: MdPos = [endIndex + 1, to - endFromOffset];\n      const mdNode: MdNode = this.context.toastMark.findNodeAtPosition(mdPos)!;\n      const cellNode = findClosestNode(mdNode, (node) => isTableCellNode(node)) as TableCellMdNode;\n\n      if (cellNode) {\n        const parent = cellNode.parent as TableRowMdNode;\n        const { type, parentType, childType } = createTargetTypes(moveNext);\n        let chOffset = getMdEndCh(cellNode);\n\n        if (cellNode[type]) {\n          chOffset = getMdEndCh(cellNode[type]!) - 1;\n        } else {\n          const row =\n            !parent[type] && parent.parent.type === parentType\n              ? parent.parent[type]![childType]\n              : parent[type];\n\n          if (type === 'next') {\n            // if there is next row, the base offset would be end position of the next row's first child.\n            // Otherwise, the base offset is zero.\n            const baseOffset = row ? getMdEndCh(row[childType]!) : 0;\n\n            // calculate tag(open, close) position('2') for selection\n            chOffset += baseOffset + 2;\n          } else if (type === 'prev') {\n            // if there is prev row, the target position would be '-4' for calculating ' |' characters and tag(open, close)\n            // Otherwise, the target position is zero.\n            chOffset = row ? -4 : 0;\n          }\n        }\n\n        dispatch!(tr.setSelection(createTextSelection(tr, endFromOffset + chOffset)));\n\n        return true;\n      }\n      return false;\n    };\n  }\n\n  private addTable(): EditorCommand<Payload> {\n    return (payload) => ({ selection, tr, schema }, dispatch) => {\n      const { columnCount, rowCount } = payload!;\n      const { endToOffset } = getRangeInfo(selection);\n\n      const headerRows = createTableHeader(columnCount);\n      const bodyRows = createTableBody(columnCount, rowCount - 1);\n      const rows = [...headerRows, ...bodyRows];\n\n      rows.forEach((row) => {\n        tr.split(tr.mapping.map(endToOffset)).insert(\n          tr.mapping.map(endToOffset),\n          createTextNode(schema, row)\n        );\n      });\n      // should add `4` to selection position considering `| ` text and start block tag length\n      dispatch!(tr.setSelection(createTextSelection(tr, endToOffset + 4)));\n      return true;\n    };\n  }\n\n  commands() {\n    return { addTable: this.addTable() };\n  }\n\n  keymaps() {\n    return {\n      Enter: this.extendTable(),\n      Tab: this.moveTableCell(true),\n      'Shift-Tab': this.moveTableCell(false),\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/marks/thematicBreak.ts",
    "content": "import { DOMOutputSpec } from 'prosemirror-model';\nimport type { Transaction } from 'prosemirror-state';\nimport { EditorCommand } from '@t/spec';\nimport { clsWithMdPrefix } from '@/utils/dom';\nimport Mark from '@/spec/mark';\nimport { createTextNode, createTextSelection } from '@/helper/manipulation';\nimport { getRangeInfo } from '../helper/pos';\n\nconst thematicBreakSyntax = '***';\n\nexport class ThematicBreak extends Mark {\n  get name() {\n    return 'thematicBreak';\n  }\n\n  get schema() {\n    return {\n      toDOM(): DOMOutputSpec {\n        return ['span', { class: clsWithMdPrefix('thematic-break') }, 0];\n      },\n    };\n  }\n\n  private hr(): EditorCommand {\n    return () => (state, dispatch) => {\n      const { selection, schema, tr } = state;\n      const { from, to, endToOffset } = getRangeInfo(selection);\n      const node = createTextNode(schema, thematicBreakSyntax);\n\n      (tr\n        .split(from)\n        .replaceWith(tr.mapping.map(from), tr.mapping.map(to), node)\n        .split(tr.mapping.map(to)) as Transaction).setSelection(\n        createTextSelection(tr, tr.mapping.map(endToOffset))\n      );\n\n      dispatch!(tr);\n      return true;\n    };\n  }\n\n  commands() {\n    return { hr: this.hr() };\n  }\n\n  keymaps() {\n    const lineCommand = this.hr()();\n\n    return { 'Mod-l': lineCommand, 'Mod-L': lineCommand };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/mdEditor.ts",
    "content": "import { Transaction } from 'prosemirror-state';\nimport { EditorView } from 'prosemirror-view';\nimport { Fragment, Slice } from 'prosemirror-model';\nimport { ReplaceAroundStep } from 'prosemirror-transform';\nimport { MdPos, ToastMark } from '@toast-ui/toastmark';\n\nimport toArray from 'tui-code-snippet/collection/toArray';\n\nimport { MdContext } from '@t/spec';\nimport { Emitter } from '@t/event';\nimport { WidgetStyle } from '@t/editor';\nimport EditorBase from '@/base';\nimport SpecManager from '@/spec/specManager';\nimport { cls, toggleClass } from '@/utils/dom';\nimport { emitImageBlobHook, pasteImageOnly } from '@/helper/image';\nimport { createParagraph, createTextSelection } from '@/helper/manipulation';\nimport { syntaxHighlight } from './plugins/syntaxHighlight';\nimport { previewHighlight } from './plugins/previewHighlight';\nimport { Doc } from './nodes/doc';\nimport { Paragraph } from './nodes/paragraph';\nimport { Text } from './nodes/text';\nimport { Heading } from './marks/heading';\nimport { BlockQuote } from './marks/blockQuote';\nimport { CodeBlock } from './marks/codeBlock';\nimport { Table } from './marks/table';\nimport { ThematicBreak } from './marks/thematicBreak';\nimport { ListItem } from './marks/listItem';\nimport { Strong } from './marks/strong';\nimport { Strike } from './marks/strike';\nimport { Emph } from './marks/emph';\nimport { Code } from './marks/code';\nimport { Link } from './marks/link';\nimport { Delimiter, TaskDelimiter, MarkedText, Meta, TableCell } from './marks/simpleMark';\nimport { Html } from './marks/html';\nimport { CustomBlock } from './marks/customBlock';\nimport { getEditorToMdPos, getMdToEditorPos } from './helper/pos';\nimport { smartTask } from './plugins/smartTask';\nimport { createNodesWithWidget, unwrapWidgetSyntax } from '@/widget/rules';\nimport { Widget, widgetNodeView } from '@/widget/widgetNode';\nimport { PluginProp } from '@t/plugin';\n\ninterface WindowWithClipboard extends Window {\n  clipboardData?: DataTransfer | null;\n}\n\ninterface MarkdownOptions {\n  toastMark: ToastMark;\n  useCommandShortcut?: boolean;\n  mdPlugins?: PluginProp[];\n}\n\nconst EVENT_TYPE = 'cut';\nconst reLineEnding = /\\r\\n|\\n|\\r/;\n\nexport default class MdEditor extends EditorBase {\n  private toastMark: ToastMark;\n\n  private clipboard!: HTMLTextAreaElement;\n\n  context!: MdContext;\n\n  constructor(eventEmitter: Emitter, options: MarkdownOptions) {\n    super(eventEmitter);\n\n    const { toastMark, useCommandShortcut = true, mdPlugins = [] } = options;\n\n    this.editorType = 'markdown';\n    this.el.classList.add('md-mode');\n    this.toastMark = toastMark;\n    this.extraPlugins = mdPlugins;\n    this.specs = this.createSpecs();\n    this.schema = this.createSchema();\n    this.context = this.createContext();\n    this.keymaps = this.createKeymaps(useCommandShortcut);\n    this.view = this.createView();\n    this.commands = this.createCommands();\n    this.specs.setContext({ ...this.context, view: this.view });\n    this.createClipboard();\n    // To prevent unnecessary focus setting during initial rendering\n    this.eventEmitter.listen('changePreviewTabWrite', (isMarkdownTabMounted?: boolean) =>\n      this.toggleActive(true, isMarkdownTabMounted)\n    );\n    this.eventEmitter.listen('changePreviewTabPreview', () => this.toggleActive(false));\n    this.initEvent();\n  }\n\n  private toggleActive(active: boolean, isMarkdownTabMounted?: boolean) {\n    toggleClass(this.el!, 'active', active);\n    if (active) {\n      if (!isMarkdownTabMounted) {\n        this.focus();\n      }\n    } else {\n      this.blur();\n    }\n  }\n\n  private createClipboard() {\n    this.clipboard = document.createElement('textarea');\n    this.clipboard.className = cls('pseudo-clipboard');\n    this.clipboard.addEventListener('paste', (ev: ClipboardEvent) => {\n      const clipboardData =\n        (ev as ClipboardEvent).clipboardData || (window as WindowWithClipboard).clipboardData;\n      const items = clipboardData && clipboardData.items;\n\n      if (items) {\n        const containRtfItem = toArray(items).some(\n          (item) => item.kind === 'string' && item.type === 'text/rtf'\n        );\n\n        // if it contains rtf, it's most likely copy paste from office -> no image\n        if (!containRtfItem) {\n          const imageBlob = pasteImageOnly(items);\n\n          if (imageBlob) {\n            ev.preventDefault();\n            emitImageBlobHook(this.eventEmitter, imageBlob, ev.type);\n          }\n        }\n      }\n    });\n    // process the pasted data in input event for IE11\n    this.clipboard.addEventListener('input', (ev) => {\n      const text = (ev.target as HTMLTextAreaElement).value;\n\n      this.replaceSelection(text);\n      ev.preventDefault();\n      (ev.target as HTMLTextAreaElement).value = '';\n    });\n    this.el.insertBefore(this.clipboard, this.view.dom);\n  }\n\n  createContext() {\n    return {\n      toastMark: this.toastMark,\n      schema: this.schema,\n      eventEmitter: this.eventEmitter,\n    };\n  }\n\n  createSpecs() {\n    return new SpecManager([\n      new Doc(),\n      new Paragraph(),\n      new Widget(),\n      new Text(),\n      new Heading(),\n      new BlockQuote(),\n      new CodeBlock(),\n      new CustomBlock(),\n      new Table(),\n      new TableCell(),\n      new ThematicBreak(),\n      new ListItem(),\n      new Strong(),\n      new Strike(),\n      new Emph(),\n      new Code(),\n      new Link(),\n      new Delimiter(),\n      new TaskDelimiter(),\n      new MarkedText(),\n      new Meta(),\n      new Html(),\n    ]);\n  }\n\n  createPlugins() {\n    return [\n      syntaxHighlight(this.context),\n      previewHighlight(this.context),\n      smartTask(this.context),\n      ...this.createPluginProps(),\n    ].concat(this.defaultPlugins);\n  }\n\n  createView() {\n    return new EditorView(this.el, {\n      state: this.createState(),\n      dispatchTransaction: (tr) => {\n        this.updateMarkdown(tr);\n\n        const { state } = this.view.state.applyTransaction(tr);\n\n        this.view.updateState(state);\n        this.emitChangeEvent(tr);\n      },\n      handleKeyDown: (_, ev) => {\n        if ((ev.metaKey || ev.ctrlKey) && ev.key.toUpperCase() === 'V') {\n          this.clipboard.focus();\n        }\n        this.eventEmitter.emit('keydown', this.editorType, ev);\n        return false;\n      },\n      handleDOMEvents: {\n        copy: (_, ev) => this.captureCopy(ev),\n        cut: (_, ev) => this.captureCopy(ev, EVENT_TYPE),\n        scroll: () => {\n          this.eventEmitter.emit('scroll', 'editor');\n          return true;\n        },\n        keyup: (_, ev: KeyboardEvent) => {\n          this.eventEmitter.emit('keyup', this.editorType, ev);\n          return false;\n        },\n      },\n      nodeViews: {\n        widget: widgetNodeView,\n      },\n    });\n  }\n\n  createCommands() {\n    return this.specs.commands(this.view);\n  }\n\n  private captureCopy(ev: ClipboardEvent, type?: string) {\n    ev.preventDefault();\n\n    const { selection, tr } = this.view.state;\n\n    if (selection.empty) {\n      return true;\n    }\n\n    const text = this.getChanged(selection.content());\n\n    if (ev.clipboardData) {\n      ev.clipboardData.setData('text/plain', text);\n    } else {\n      (window as WindowWithClipboard).clipboardData!.setData('Text', text);\n    }\n\n    if (type === EVENT_TYPE) {\n      this.view.dispatch(tr.deleteSelection().scrollIntoView().setMeta('uiEvent', EVENT_TYPE));\n    }\n    return true;\n  }\n\n  private updateMarkdown(tr: Transaction) {\n    if (tr.docChanged) {\n      tr.steps.forEach((step, index) => {\n        if (step.slice && !(step instanceof ReplaceAroundStep)) {\n          const doc = tr.docs[index];\n          const [from, to] = [step.from, step.to];\n          const [startPos, endPos] = getEditorToMdPos(doc, from, to);\n          let changed = this.getChanged(step.slice);\n\n          if (startPos[0] === endPos[0] && startPos[1] === endPos[1] && changed === '') {\n            changed = '\\n';\n          }\n          const editResult = this.toastMark.editMarkdown(startPos, endPos, changed);\n\n          this.eventEmitter.emit('updatePreview', editResult);\n\n          tr.setMeta('editResult', editResult).scrollIntoView();\n        }\n      });\n    }\n  }\n\n  private getChanged(slice: Slice) {\n    let changed = '';\n    const from = 0;\n    const to = slice.content.size;\n\n    slice.content.nodesBetween(from, to, (node, pos) => {\n      if (node.isText) {\n        changed += node.text!.slice(Math.max(from, pos) - pos, to - pos);\n      } else if (node.isBlock && pos > 0) {\n        changed += '\\n';\n      }\n    });\n\n    return changed;\n  }\n\n  setSelection(start: MdPos, end = start) {\n    const { tr } = this.view.state;\n    const [from, to] = getMdToEditorPos(tr.doc, start, end);\n\n    this.view.dispatch(tr.setSelection(createTextSelection(tr, from, to)).scrollIntoView());\n  }\n\n  replaceSelection(text: string, start?: MdPos, end?: MdPos) {\n    let newTr;\n    const { tr, schema, doc } = this.view.state;\n    const lineTexts = text.split(reLineEnding);\n    const nodes = lineTexts.map((lineText) =>\n      createParagraph(schema, createNodesWithWidget(lineText, schema))\n    );\n    const slice = new Slice(Fragment.from(nodes), 1, 1);\n\n    this.focus();\n\n    if (start && end) {\n      const [from, to] = getMdToEditorPos(doc, start, end);\n\n      newTr = tr.replaceRange(from, to, slice);\n    } else {\n      newTr = tr.replaceSelection(slice);\n    }\n    this.view.dispatch(newTr.scrollIntoView());\n  }\n\n  deleteSelection(start?: MdPos, end?: MdPos) {\n    let newTr;\n    const { tr, doc } = this.view.state;\n\n    if (start && end) {\n      const [from, to] = getMdToEditorPos(doc, start, end);\n\n      newTr = tr.deleteRange(from, to);\n    } else {\n      newTr = tr.deleteSelection();\n    }\n    this.view.dispatch(newTr.scrollIntoView());\n  }\n\n  getSelectedText(start?: MdPos, end?: MdPos) {\n    const { doc, selection } = this.view.state;\n    let { from, to } = selection;\n\n    if (start && end) {\n      const pos = getMdToEditorPos(doc, start, end);\n\n      from = pos[0];\n      to = pos[1];\n    }\n\n    return doc.textBetween(from, to, '\\n');\n  }\n\n  getSelection() {\n    const { from, to } = this.view.state.selection;\n\n    return getEditorToMdPos(this.view.state.tr.doc, from, to);\n  }\n\n  setMarkdown(markdown: string, cursorToEnd = true) {\n    const lineTexts = markdown.split(reLineEnding);\n    const { tr, doc, schema } = this.view.state;\n    const nodes = lineTexts.map((lineText) =>\n      createParagraph(schema, createNodesWithWidget(lineText, schema))\n    );\n\n    this.view.dispatch(tr.replaceWith(0, doc.content.size, nodes));\n\n    if (cursorToEnd) {\n      this.moveCursorToEnd(true);\n    }\n  }\n\n  addWidget(node: Node, style: WidgetStyle, mdPos?: MdPos) {\n    const { tr, doc, selection } = this.view.state;\n    const pos = mdPos ? getMdToEditorPos(doc, mdPos, mdPos)[0] : selection.to;\n\n    this.view.dispatch(tr.setMeta('widget', { pos, node, style }));\n  }\n\n  replaceWithWidget(start: MdPos, end: MdPos, text: string) {\n    const { tr, schema, doc } = this.view.state;\n    const pos = getMdToEditorPos(doc, start, end);\n    const nodes = createNodesWithWidget(text, schema);\n\n    this.view.dispatch(tr.replaceWith(pos[0], pos[1], nodes));\n  }\n\n  getRangeInfoOfNode(pos?: MdPos) {\n    const { doc, selection } = this.view.state;\n    const mdPos = pos || getEditorToMdPos(doc, selection.from)[0];\n    let mdNode = this.toastMark.findNodeAtPosition(mdPos)!;\n\n    if (mdNode.type === 'text' && mdNode.parent!.type !== 'paragraph') {\n      mdNode = mdNode.parent!;\n    }\n\n    // add 1 sync for prosemirror position\n    mdNode.sourcepos![1][1] += 1;\n\n    return { range: mdNode.sourcepos!, type: mdNode.type };\n  }\n\n  getMarkdown() {\n    return this.toastMark\n      .getLineTexts()\n      .map((lineText: string) => unwrapWidgetSyntax(lineText))\n      .join('\\n');\n  }\n\n  getToastMark() {\n    return this.toastMark;\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/mdPreview.ts",
    "content": "import off from 'tui-code-snippet/domEvent/off';\nimport addClass from 'tui-code-snippet/domUtil/addClass';\nimport removeClass from 'tui-code-snippet/domUtil/removeClass';\nimport on from 'tui-code-snippet/domEvent/on';\nimport css from 'tui-code-snippet/domUtil/css';\nimport { EditResult, MdNode, MdPos, Renderer } from '@toast-ui/toastmark';\n\nimport { Emitter } from '@t/event';\nimport { CustomHTMLRenderer, LinkAttributes } from '@t/editor';\nimport {\n  cls,\n  createElementWith,\n  removeNode,\n  removeProseMirrorHackNodes,\n  toggleClass,\n} from '@/utils/dom';\nimport { getHTMLRenderConvertors } from '@/markdown/htmlRenderConvertors';\nimport { isInlineNode, findClosestNode, getMdStartCh } from '@/utils/markdown';\nimport { findAdjacentElementToScrollTop } from './scroll/dom';\nimport { removeOffsetInfoByNode } from './scroll/offset';\n\nexport const CLASS_HIGHLIGHT = cls('md-preview-highlight');\n\nfunction findTableCell(tableRow: MdNode, chOffset: number) {\n  let cell = tableRow.firstChild;\n\n  while (cell && cell.next) {\n    if (getMdStartCh(cell.next) > chOffset + 1) {\n      break;\n    }\n    cell = cell.next;\n  }\n\n  return cell;\n}\n\ntype Sanitizer = (html: string) => string;\n\ninterface Options {\n  linkAttributes: LinkAttributes | null;\n  customHTMLRenderer: CustomHTMLRenderer;\n  isViewer: boolean;\n  highlight?: boolean;\n  sanitizer: Sanitizer;\n}\n\n/**\n * Class Markdown Preview\n * @param {HTMLElement} el - base element\n * @param {eventEmitter} eventEmitter - event manager\n * @param {object} options\n * @param {boolean} options.isViewer - true for view-only mode\n * @param {boolean} options.highlight - true for using live-highlight feature\n * @param {object} opitons.linkAttributes - attributes for link element\n * @param {object} opitons.customHTMLRenderer - map of custom HTML render functions\n *\n * @ignore\n */\nclass MarkdownPreview {\n  el: HTMLElement | null;\n\n  previewContent!: HTMLElement;\n\n  private eventEmitter: Emitter;\n\n  private isViewer: boolean;\n\n  private cursorNodeId!: number | null;\n\n  private renderer: Renderer;\n\n  private sanitizer: Sanitizer;\n\n  constructor(eventEmitter: Emitter, options: Options) {\n    const el = document.createElement('div');\n\n    this.el = el;\n    this.eventEmitter = eventEmitter;\n    this.isViewer = !!options.isViewer;\n\n    this.el.className = cls('md-preview');\n\n    const { linkAttributes, customHTMLRenderer, sanitizer, highlight = false } = options;\n\n    this.renderer = new Renderer({\n      gfm: true,\n      nodeId: true,\n      convertors: getHTMLRenderConvertors(linkAttributes, customHTMLRenderer),\n    });\n\n    this.cursorNodeId = null;\n    this.sanitizer = sanitizer;\n\n    this.initEvent(highlight);\n    this.initContentSection();\n\n    // To prevent overflowing contents in the viewer\n    if (this.isViewer) {\n      this.previewContent.style.overflowWrap = 'break-word';\n    }\n  }\n\n  private initContentSection() {\n    this.previewContent = createElementWith(\n      `<div class=\"${cls('contents')}\"></div>`\n    ) as HTMLElement;\n\n    if (!this.isViewer) {\n      this.el!.appendChild(this.previewContent);\n    }\n  }\n\n  private toggleActive(active: boolean) {\n    toggleClass(this.el!, 'active', active);\n  }\n\n  private initEvent(highlight: boolean) {\n    this.eventEmitter.listen('updatePreview', this.update.bind(this));\n\n    if (this.isViewer) {\n      return;\n    }\n\n    if (highlight) {\n      this.eventEmitter.listen('changeToolbarState', ({ mdNode, cursorPos }) => {\n        this.updateCursorNode(mdNode, cursorPos);\n      });\n\n      this.eventEmitter.listen('blur', () => {\n        this.removeHighlight();\n      });\n    }\n\n    on(this.el!, 'scroll', (event) => {\n      this.eventEmitter.emit(\n        'scroll',\n        'preview',\n        findAdjacentElementToScrollTop(event.target.scrollTop, this.previewContent)\n      );\n    });\n    this.eventEmitter.listen('changePreviewTabPreview', () => this.toggleActive(true));\n    this.eventEmitter.listen('changePreviewTabWrite', () => this.toggleActive(false));\n  }\n\n  private removeHighlight() {\n    if (this.cursorNodeId) {\n      const currentEl = this.getElementByNodeId(this.cursorNodeId);\n\n      if (currentEl) {\n        removeClass(currentEl, CLASS_HIGHLIGHT);\n      }\n    }\n  }\n\n  private updateCursorNode(cursorNode: MdNode | null, cursorPos: MdPos) {\n    if (cursorNode) {\n      cursorNode = findClosestNode(cursorNode, (mdNode) => !isInlineNode(mdNode))!;\n\n      if (cursorNode.type === 'tableRow') {\n        cursorNode = findTableCell(cursorNode, cursorPos[1])!;\n      } else if (cursorNode.type === 'tableBody') {\n        // empty line next to table\n        cursorNode = null;\n      }\n    }\n\n    const cursorNodeId = cursorNode ? cursorNode.id : null;\n\n    if (this.cursorNodeId === cursorNodeId) {\n      return;\n    }\n\n    const oldEL = this.getElementByNodeId(this.cursorNodeId);\n    const newEL = this.getElementByNodeId(cursorNodeId);\n\n    if (oldEL) {\n      removeClass(oldEL, CLASS_HIGHLIGHT);\n    }\n    if (newEL) {\n      addClass(newEL, CLASS_HIGHLIGHT);\n    }\n\n    this.cursorNodeId = cursorNodeId;\n  }\n\n  private getElementByNodeId(nodeId: number | null) {\n    return nodeId\n      ? this.previewContent.querySelector<HTMLElement>(`[data-nodeid=\"${nodeId}\"]`)\n      : null;\n  }\n\n  update(changed: EditResult[]) {\n    changed.forEach((editResult) => this.replaceRangeNodes(editResult));\n    this.eventEmitter.emit('afterPreviewRender', this);\n  }\n\n  replaceRangeNodes(editResult: EditResult) {\n    const { nodes, removedNodeRange } = editResult;\n    const contentEl = this.previewContent;\n    const newHtml = this.eventEmitter.emitReduce(\n      'beforePreviewRender',\n      this.sanitizer(nodes.map((node) => this.renderer.render(node)).join(''))\n    );\n\n    if (!removedNodeRange) {\n      contentEl.insertAdjacentHTML('afterbegin', newHtml);\n    } else {\n      const [startNodeId, endNodeId] = removedNodeRange.id;\n      const startEl = this.getElementByNodeId(startNodeId);\n      const endEl = this.getElementByNodeId(endNodeId);\n\n      if (startEl) {\n        startEl.insertAdjacentHTML('beforebegin', newHtml);\n        let el = startEl;\n\n        while (el && el !== endEl) {\n          const nextEl = el.nextElementSibling as HTMLElement;\n\n          removeNode(el);\n          removeOffsetInfoByNode(el);\n          el = nextEl;\n        }\n        if (el?.parentNode) {\n          removeNode(el);\n          removeOffsetInfoByNode(el);\n        }\n      }\n    }\n  }\n\n  getRenderer() {\n    return this.renderer;\n  }\n\n  destroy() {\n    off(this.el!, 'scroll');\n    this.el = null;\n  }\n\n  getElement() {\n    return this.el!;\n  }\n\n  getHTML() {\n    return removeProseMirrorHackNodes(this.previewContent.innerHTML);\n  }\n\n  setHTML(html: string) {\n    this.previewContent.innerHTML = html;\n  }\n\n  setHeight(height: number) {\n    css(this.el!, { height: `${height}px` });\n  }\n\n  setMinHeight(minHeight: number) {\n    css(this.el!, { minHeight: `${minHeight}px` });\n  }\n}\n\nexport default MarkdownPreview;\n"
  },
  {
    "path": "apps/editor/src/markdown/nodes/doc.ts",
    "content": "import Node from '@/spec/node';\n\nexport class Doc extends Node {\n  get name() {\n    return 'doc';\n  }\n\n  get schema() {\n    return {\n      content: 'block+',\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/nodes/paragraph.ts",
    "content": "import { DOMOutputSpec, ProsemirrorNode, Schema } from 'prosemirror-model';\nimport { Transaction, Selection } from 'prosemirror-state';\nimport { chainCommands, joinForward, Command } from 'prosemirror-commands';\nimport { EditorCommand, MdSpecContext } from '@t/spec';\nimport { clsWithMdPrefix } from '@/utils/dom';\nimport Node from '@/spec/node';\nimport { isBulletListNode, isOrderedListNode } from '@/utils/markdown';\nimport { createTextNode, createTextSelection, replaceTextNode } from '@/helper/manipulation';\nimport { reBlockQuote } from '../marks/blockQuote';\nimport { getRangeInfo, getNodeContentOffsetRange } from '../helper/pos';\nimport { getReorderedListInfo, reList, reOrderedListGroup } from '../helper/list';\nimport { getTextByMdLine, getTextContent } from '../helper/query';\n\ninterface SelectionInfo {\n  from: number;\n  to: number;\n}\n\ninterface IndentSelectionInfo extends SelectionInfo {\n  type: 'indent';\n  lineLen: number;\n}\n\ninterface OutdentSelectionInfo extends SelectionInfo {\n  type: 'outdent';\n  spaceLenList: number[];\n}\n\nconst reStartSpace = /(^\\s{1,4})(.*)/;\n\nfunction isBlockUnit(from: number, to: number, text: string) {\n  return from < to || reList.test(text) || reBlockQuote.test(text);\n}\n\nfunction isInTableCellNode(doc: ProsemirrorNode, schema: Schema, selection: Selection) {\n  let $pos = selection.$from;\n\n  if ($pos.depth === 0) {\n    $pos = doc.resolve($pos.pos - 1);\n  }\n  const node = $pos.node(1);\n  const startOffset = $pos.start(1);\n  const contentSize = node.content.size;\n\n  return (\n    node.rangeHasMark(0, contentSize, schema.marks.table) &&\n    $pos.pos - startOffset !== contentSize &&\n    $pos.pos !== startOffset\n  );\n}\n\nfunction createSelection(tr: Transaction, posInfo: IndentSelectionInfo | OutdentSelectionInfo) {\n  let { from, to } = posInfo;\n\n  if (posInfo.type === 'indent') {\n    const softTabLen = 4;\n\n    from += softTabLen;\n    to += (posInfo.lineLen + 1) * softTabLen;\n  } else {\n    const { spaceLenList } = posInfo;\n\n    from -= spaceLenList[0];\n    for (let i = 0; i < spaceLenList.length; i += 1) {\n      to -= spaceLenList[i];\n    }\n  }\n\n  return createTextSelection(tr, from, to);\n}\n\nexport class Paragraph extends Node {\n  context!: MdSpecContext;\n\n  get name() {\n    return 'paragraph';\n  }\n\n  get schema() {\n    return {\n      content: 'inline*',\n      attrs: {\n        className: { default: null },\n        codeStart: { default: null },\n        codeEnd: { default: null },\n      },\n      selectable: false,\n      group: 'block',\n      parseDOM: [{ tag: 'div' }],\n      toDOM({ attrs }: ProsemirrorNode): DOMOutputSpec {\n        return attrs.className\n          ? ['div', { class: clsWithMdPrefix(attrs.className) }, 0]\n          : ['div', 0];\n      },\n    };\n  }\n\n  private reorderList(startLine: number, endLine: number) {\n    const { view, toastMark, schema } = this.context;\n    const { tr, selection, doc } = view.state;\n\n    let mdNode = toastMark.findFirstNodeAtLine(startLine);\n    let topListNode = mdNode;\n\n    while (mdNode && !isBulletListNode(mdNode!) && mdNode.parent!.type !== 'document') {\n      mdNode = mdNode.parent!;\n      if (isOrderedListNode(mdNode!)) {\n        topListNode = mdNode;\n        break;\n      }\n    }\n\n    if (topListNode) {\n      startLine = topListNode.sourcepos![0][0];\n    }\n\n    const [, indent, , start] = reOrderedListGroup.exec(getTextByMdLine(doc, startLine))!;\n    const indentLen = indent.length;\n    const { line, nodes } = getReorderedListInfo(doc, schema, startLine, Number(start), indentLen);\n\n    endLine = Math.max(endLine, line - 1);\n\n    let { startOffset } = getNodeContentOffsetRange(doc, startLine - 1);\n\n    for (let i = startLine - 1; i <= endLine - 1; i += 1) {\n      const { nodeSize, content } = doc.child(i);\n      const mappedFrom = tr.mapping.map(startOffset);\n      const mappedTo = mappedFrom + content.size;\n\n      tr.replaceWith(mappedFrom, mappedTo, nodes[i - startLine + 1]);\n\n      startOffset += nodeSize;\n    }\n\n    const newSelection = createTextSelection(tr, selection.from, selection.to);\n\n    view.dispatch!(tr.setSelection(newSelection));\n  }\n\n  private indent(tabKey = false): EditorCommand {\n    return () => (state, dispatch) => {\n      const { schema, selection, doc } = state;\n      const { from, to, startFromOffset, startIndex, endIndex } = getRangeInfo(selection);\n\n      if (tabKey && isInTableCellNode(doc, schema, selection)) {\n        return false;\n      }\n\n      const startLineText = getTextContent(doc, startIndex);\n\n      if (\n        (tabKey && isBlockUnit(from, to, startLineText)) ||\n        (!tabKey && reList.test(startLineText))\n      ) {\n        const tr = replaceTextNode({\n          state,\n          from: startFromOffset,\n          startIndex,\n          endIndex,\n          createText: (textContent) => `    ${textContent}`,\n        });\n        const posInfo: IndentSelectionInfo = {\n          type: 'indent',\n          from,\n          to,\n          lineLen: endIndex - startIndex,\n        };\n\n        dispatch!(tr.setSelection(createSelection(tr, posInfo)));\n\n        if (reOrderedListGroup.test(startLineText)) {\n          this.reorderList(startIndex + 1, endIndex + 1);\n        }\n      } else if (tabKey) {\n        dispatch!(state.tr.insert(to, createTextNode(schema, '    ')));\n      }\n\n      return true;\n    };\n  }\n\n  private outdent(tabKey = false): EditorCommand {\n    return () => (state, dispatch) => {\n      const { selection, doc, schema } = state;\n      const { from, to, startFromOffset, startIndex, endIndex } = getRangeInfo(selection);\n\n      if (tabKey && isInTableCellNode(doc, schema, selection)) {\n        return false;\n      }\n\n      const startLineText = getTextContent(doc, startIndex);\n\n      if (\n        (tabKey && isBlockUnit(from, to, startLineText)) ||\n        (!tabKey && reList.test(startLineText))\n      ) {\n        const spaceLenList: number[] = [];\n        const tr = replaceTextNode({\n          state,\n          from: startFromOffset,\n          startIndex,\n          endIndex,\n          createText: (textContent) => {\n            const searchResult = reStartSpace.exec(textContent);\n\n            spaceLenList.push(searchResult ? searchResult[1].length : 0);\n\n            return textContent.replace(reStartSpace, '$2');\n          },\n        });\n\n        const posInfo: OutdentSelectionInfo = { type: 'outdent', from, to, spaceLenList };\n\n        dispatch!(tr.setSelection(createSelection(tr, posInfo)));\n\n        if (reOrderedListGroup.test(startLineText)) {\n          this.reorderList(startIndex + 1, endIndex + 1);\n        }\n      } else if (tabKey) {\n        const startText = startLineText.slice(0, to - startFromOffset);\n        const startTextWithoutSpace = startText.replace(/\\s{1,4}$/, '');\n        const deletStart = to - (startText.length - startTextWithoutSpace.length);\n\n        dispatch!(state.tr.delete(deletStart, to));\n      }\n\n      return true;\n    };\n  }\n\n  private deleteLines(): Command {\n    return (state, dispatch) => {\n      const { view } = this.context;\n      const { startFromOffset, endToOffset } = getRangeInfo(state.selection);\n\n      const deleteRange: Command = () => {\n        dispatch!(state.tr.deleteRange(startFromOffset, endToOffset));\n\n        return true;\n      };\n\n      return chainCommands(deleteRange, joinForward)(state, dispatch, view);\n    };\n  }\n\n  private moveDown(): Command {\n    return (state, dispatch) => {\n      const { doc, tr, selection, schema } = state;\n      const { startFromOffset, endToOffset, endIndex } = getRangeInfo(selection);\n\n      if (endIndex < doc.content.childCount - 1) {\n        const { nodeSize, textContent } = doc.child(endIndex + 1);\n\n        tr.delete(endToOffset, endToOffset + nodeSize)\n          .split(startFromOffset)\n          // subtract 2(start, end tag length) to insert prev line\n          .insert(tr.mapping.map(startFromOffset) - 2, createTextNode(schema, textContent));\n\n        dispatch!(tr);\n        return true;\n      }\n      return false;\n    };\n  }\n\n  private moveUp(): Command {\n    return (state, dispatch) => {\n      const { tr, doc, selection, schema } = state;\n      const { startFromOffset, endToOffset, startIndex } = getRangeInfo(selection);\n\n      if (startIndex > 0) {\n        const { nodeSize, textContent } = doc.child(startIndex - 1);\n\n        tr.delete(startFromOffset - nodeSize, startFromOffset)\n          .split(tr.mapping.map(endToOffset))\n          .insert(tr.mapping.map(endToOffset), createTextNode(schema, textContent));\n\n        dispatch!(tr);\n\n        return true;\n      }\n      return false;\n    };\n  }\n\n  commands() {\n    return {\n      indent: this.indent(),\n      outdent: this.outdent(),\n    };\n  }\n\n  keymaps() {\n    return {\n      Tab: this.indent(true)(),\n      'Shift-Tab': this.outdent(true)(),\n      'Mod-d': this.deleteLines(),\n      'Mod-D': this.deleteLines(),\n      'Alt-ArrowUp': this.moveUp(),\n      'Alt-ArrowDown': this.moveDown(),\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/nodes/text.ts",
    "content": "import Node from '@/spec/node';\n\nexport class Text extends Node {\n  get name() {\n    return 'text';\n  }\n\n  get schema() {\n    return {\n      group: 'inline',\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/plugins/helper/markInfo.ts",
    "content": "import {\n  MdNodeType,\n  MdPos,\n  HeadingMdNode,\n  LinkMdNode,\n  CodeMdNode,\n  MdNode,\n  CodeBlockMdNode,\n  CustomBlockMdNode,\n  ListItemMdNode,\n} from '@toast-ui/toastmark';\nimport isFunction from 'tui-code-snippet/type/isFunction';\nimport {\n  getMdStartLine,\n  getMdStartCh,\n  getMdEndLine,\n  getMdEndCh,\n  addOffsetPos,\n  setOffsetPos,\n} from '@/utils/markdown';\n\nconst HEADING = 'heading';\nconst BLOCK_QUOTE = 'blockQuote';\nconst LIST_ITEM = 'listItem';\nconst TABLE = 'table';\nconst TABLE_CELL = 'tableCell';\nconst CODE_BLOCK = 'codeBlock';\nconst THEMATIC_BREAK = 'thematicBreak';\nconst LINK = 'link';\nconst CODE = 'code';\nconst META = 'meta';\nconst DELIM = 'delimiter';\nconst TASK_DELIM = 'taskDelimiter';\nconst TEXT = 'markedText';\nconst HTML = 'html';\nconst CUSTOM_BLOCK = 'customBlock';\n\nconst delimSize = {\n  strong: 2,\n  emph: 1,\n  strike: 2,\n};\n\ntype MarkType =\n  | MdNodeType\n  | typeof LIST_ITEM\n  | typeof DELIM\n  | typeof TASK_DELIM\n  | typeof TEXT\n  | typeof HTML\n  | typeof META;\n\nexport interface MarkInfo {\n  start: MdPos;\n  end: MdPos;\n  spec?: { type?: MarkType; attrs?: Record<string, any> };\n  lineBackground?: boolean;\n}\n\nfunction markInfo(start: MdPos, end: MdPos, type: MarkType, attrs?: Record<string, any>): MarkInfo {\n  return { start, end, spec: { type, attrs } };\n}\n\nfunction heading({ level, headingType }: HeadingMdNode, start: MdPos, end: MdPos) {\n  const marks = [markInfo(start, end, HEADING, { level })];\n\n  if (headingType === 'atx') {\n    marks.push(markInfo(start, addOffsetPos(start, level), DELIM));\n  } else {\n    marks.push(markInfo(setOffsetPos(end, 0), end, HEADING, { seText: true }));\n  }\n\n  return marks;\n}\n\nfunction emphasisAndStrikethrough(\n  { type }: { type: keyof typeof delimSize },\n  start: MdPos,\n  end: MdPos\n) {\n  const startDelimPos = addOffsetPos(start, delimSize[type]);\n  const endDelimPos = addOffsetPos(end, -delimSize[type]);\n\n  return [\n    markInfo(startDelimPos, endDelimPos, type),\n    markInfo(start, startDelimPos, DELIM),\n    markInfo(endDelimPos, end, DELIM),\n  ];\n}\n\nfunction markLink(start: MdPos, end: MdPos, linkTextStart: MdPos, lastChildCh: number) {\n  return [\n    markInfo(start, end, LINK),\n    markInfo(setOffsetPos(start, linkTextStart[1] + 1), setOffsetPos(end, lastChildCh), LINK, {\n      desc: true,\n    }),\n    markInfo(setOffsetPos(end, lastChildCh + 2), addOffsetPos(end, -1), LINK, { url: true }),\n  ];\n}\n\nfunction image({ lastChild }: LinkMdNode, start: MdPos, end: MdPos) {\n  const lastChildCh = lastChild ? getMdEndCh(lastChild) + 1 : 3; // 3: length of '![]'\n  const linkTextEnd = addOffsetPos(start, 1);\n\n  return [markInfo(start, linkTextEnd, META), ...markLink(start, end, linkTextEnd, lastChildCh)];\n}\n\nfunction link({ lastChild, extendedAutolink }: LinkMdNode, start: MdPos, end: MdPos) {\n  const lastChildCh = lastChild ? getMdEndCh(lastChild) + 1 : 2; // 2: length of '[]'\n\n  return extendedAutolink\n    ? [markInfo(start, end, LINK, { desc: true })]\n    : markLink(start, end, start, lastChildCh);\n}\n\nfunction code({ tickCount }: CodeMdNode, start: MdPos, end: MdPos) {\n  const openDelimEnd = addOffsetPos(start, tickCount);\n  const closeDelimStart = addOffsetPos(end, -tickCount);\n\n  return [\n    markInfo(start, end, CODE),\n    markInfo(start, openDelimEnd, CODE, { start: true }),\n    markInfo(openDelimEnd, closeDelimStart, CODE, { marked: true }),\n    markInfo(closeDelimStart, end, CODE, { end: true }),\n  ];\n}\n\nfunction lineBackground(parent: MdNode, start: MdPos, end: MdPos, prefix: string) {\n  const defaultBackground = {\n    start,\n    end,\n    spec: {\n      attrs: { className: `${prefix}-line-background`, codeStart: start[0], codeEnd: end[0] },\n    },\n    lineBackground: true,\n  };\n\n  return parent!.type !== 'item' && parent!.type !== 'blockQuote'\n    ? [\n        {\n          ...defaultBackground,\n          end: start,\n          spec: { attrs: { className: `${prefix}-line-background start` } },\n        },\n        {\n          ...defaultBackground,\n          start: [Math.min(start[0] + 1, end[0]), start[1]] as MdPos,\n        },\n      ]\n    : null;\n}\n\nfunction codeBlock(node: CodeBlockMdNode, start: MdPos, end: MdPos, endLine: string) {\n  const { fenceOffset, fenceLength, fenceChar, info, infoPadding, parent } = node;\n  const fenceEnd = fenceOffset + fenceLength;\n  const marks = [markInfo(setOffsetPos(start, 1), end, CODE_BLOCK)];\n\n  if (fenceChar) {\n    marks.push(markInfo(start, addOffsetPos(start, fenceEnd), DELIM));\n  }\n\n  if (info) {\n    marks.push(\n      markInfo(\n        addOffsetPos(start, fenceLength),\n        addOffsetPos(start, fenceLength + infoPadding + info.length),\n        META\n      )\n    );\n  }\n\n  const codeBlockEnd = `^(\\\\s{0,4})(${fenceChar}{${fenceLength},})`;\n  const reCodeBlockEnd = new RegExp(codeBlockEnd);\n\n  if (reCodeBlockEnd.test(endLine)) {\n    marks.push(markInfo(setOffsetPos(end, 1), end, DELIM));\n  }\n\n  const lineBackgroundMarkInfo = lineBackground(parent!, start, end, 'code-block');\n\n  return lineBackgroundMarkInfo ? marks.concat(lineBackgroundMarkInfo) : marks;\n}\n\nfunction customBlock(node: MdNode, start: MdPos, end: MdPos) {\n  const { offset, syntaxLength, info, parent } = node as CustomBlockMdNode;\n  const syntaxEnd = offset + syntaxLength;\n  const marks = [markInfo(setOffsetPos(start, 1), end, CUSTOM_BLOCK)];\n\n  marks.push(markInfo(start, addOffsetPos(start, syntaxEnd), DELIM));\n\n  if (info) {\n    marks.push(\n      markInfo(\n        addOffsetPos(start, syntaxEnd),\n        addOffsetPos(start, syntaxLength + info.length),\n        META\n      )\n    );\n  }\n\n  marks.push(markInfo(setOffsetPos(end, 1), end, DELIM));\n\n  const lineBackgroundMarkInfo = lineBackground(parent!, start, end, 'custom-block');\n\n  return lineBackgroundMarkInfo ? marks.concat(lineBackgroundMarkInfo) : marks;\n}\n\nfunction markListItemChildren(node: MdNode, markType: MarkType) {\n  const marks: MarkInfo[] = [];\n\n  while (node) {\n    const { type } = node;\n\n    if (type === 'paragraph' || type === 'codeBlock') {\n      marks.push(\n        markInfo(\n          [getMdStartLine(node), getMdStartCh(node) - 1],\n          [getMdEndLine(node), getMdEndCh(node) + 1],\n          markType\n        )\n      );\n    }\n    node = node.next!;\n  }\n\n  return marks;\n}\n\nfunction markParagraphInBlockQuote(node: MdNode) {\n  const marks = [];\n\n  while (node) {\n    marks.push(\n      markInfo(\n        [getMdStartLine(node), getMdStartCh(node)],\n        [getMdEndLine(node), getMdEndCh(node) + 1],\n        TEXT\n      )\n    );\n    node = node.next!;\n  }\n\n  return marks;\n}\n\nfunction blockQuote(node: MdNode, start: MdPos, end: MdPos) {\n  let marks =\n    node.parent && node.parent.type !== 'blockQuote' ? [markInfo(start, end, BLOCK_QUOTE)] : [];\n\n  if (node.firstChild) {\n    let childMarks: MarkInfo[] = [];\n\n    if (node.firstChild.type === 'paragraph') {\n      childMarks = markParagraphInBlockQuote(node.firstChild.firstChild!);\n    } else if (node.firstChild.type === 'list') {\n      childMarks = markListItemChildren(node.firstChild, TEXT);\n    }\n\n    marks = [...marks, ...childMarks];\n  }\n\n  return marks;\n}\n\nfunction getSpecOfListItemStyle(node: MdNode): [MarkType, Record<string, any>] {\n  let depth = 0;\n\n  while (node.parent!.parent && node.parent!.parent.type === 'item') {\n    node = node.parent!.parent;\n    depth += 1;\n  }\n\n  const attrs = [{ odd: true }, { even: true }][depth % 2];\n\n  return [LIST_ITEM, { ...attrs, listStyle: true }];\n}\n\nfunction item(node: ListItemMdNode, start: MdPos) {\n  const { padding, task } = node.listData;\n  const spec = getSpecOfListItemStyle(node);\n  const marks = [markInfo(start, addOffsetPos(start, padding), ...spec)];\n\n  if (task) {\n    marks.push(\n      markInfo(addOffsetPos(start, padding), addOffsetPos(start, padding + 3), TASK_DELIM)\n    );\n    marks.push(markInfo(addOffsetPos(start, padding + 1), addOffsetPos(start, padding + 2), META));\n  }\n\n  return marks.concat(markListItemChildren(node.firstChild!, TEXT));\n}\n\nconst markNodeFuncMap = {\n  heading,\n  strong: emphasisAndStrikethrough,\n  emph: emphasisAndStrikethrough,\n  strike: emphasisAndStrikethrough,\n  link,\n  image,\n  code,\n  codeBlock,\n  blockQuote,\n  item,\n  customBlock,\n};\n\nconst simpleMarkClassNameMap = {\n  thematicBreak: THEMATIC_BREAK,\n  table: TABLE,\n  tableCell: TABLE_CELL,\n  htmlInline: HTML,\n} as const;\n\ntype MarkNodeFuncMapKey = keyof typeof markNodeFuncMap;\ntype SimpleNodeFuncMapKey = keyof typeof simpleMarkClassNameMap;\n\nexport function getMarkInfo(node: MdNode, start: MdPos, end: MdPos, endLine: string) {\n  const { type } = node;\n\n  if (isFunction(markNodeFuncMap[type as MarkNodeFuncMapKey])) {\n    // @ts-ignore\n    return markNodeFuncMap[type as MarkNodeFuncMapKey](node, start, end, endLine);\n  }\n\n  if (simpleMarkClassNameMap[type as SimpleNodeFuncMapKey]) {\n    return [markInfo(start, end, simpleMarkClassNameMap[type as SimpleNodeFuncMapKey])];\n  }\n\n  return null;\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/plugins/previewHighlight.ts",
    "content": "import { MdNode, MdPos } from '@toast-ui/toastmark';\nimport { Plugin } from 'prosemirror-state';\nimport { MdContext } from '@t/spec';\nimport { ToolbarStateMap, ToolbarStateKeys } from '@t/ui';\nimport { traverseParentNodes, isListNode } from '@/utils/markdown';\nimport { includes } from '@/utils/common';\n\nconst defaultToolbarStateKeys: ToolbarStateKeys[] = [\n  'taskList',\n  'orderedList',\n  'bulletList',\n  'table',\n  'strong',\n  'emph',\n  'strike',\n  'heading',\n  'thematicBreak',\n  'blockQuote',\n  'code',\n  'codeBlock',\n  'indent',\n  'outdent',\n];\n\nfunction getToolbarStateType(mdNode: MdNode) {\n  const { type } = mdNode;\n\n  if (isListNode(mdNode)) {\n    if (mdNode.listData.task) {\n      return 'taskList';\n    }\n    return mdNode.listData.type === 'ordered' ? 'orderedList' : 'bulletList';\n  }\n\n  if (type.indexOf('table') !== -1) {\n    return 'table';\n  }\n\n  if (!includes(defaultToolbarStateKeys, type)) {\n    return null;\n  }\n\n  return type as ToolbarStateKeys;\n}\n\nfunction getToolbarState(targetNode: MdNode) {\n  const toolbarState = {\n    indent: { active: false, disabled: true },\n    outdent: { active: false, disabled: true },\n  } as ToolbarStateMap;\n\n  let listEnabled = true;\n\n  traverseParentNodes(targetNode, (mdNode) => {\n    const type = getToolbarStateType(mdNode);\n\n    if (!type) {\n      return;\n    }\n\n    if (type === 'bulletList' || type === 'orderedList') {\n      // to apply the nearlist list state in the nested list\n      if (listEnabled) {\n        toolbarState[type] = { active: true };\n\n        toolbarState.indent.disabled = false;\n        toolbarState.outdent.disabled = false;\n\n        listEnabled = false;\n      }\n    } else {\n      toolbarState[type as ToolbarStateKeys] = { active: true };\n    }\n  });\n\n  return toolbarState;\n}\n\nexport function previewHighlight({ toastMark, eventEmitter }: MdContext) {\n  return new Plugin({\n    view() {\n      return {\n        update(view, prevState) {\n          const { state } = view;\n          const { doc, selection } = state;\n\n          if (prevState && prevState.doc.eq(doc) && prevState.selection.eq(selection)) {\n            return;\n          }\n          const { from } = selection;\n          const startChOffset = state.doc.resolve(from).start();\n          const line = state.doc.content.findIndex(from).index + 1;\n          let ch = from - startChOffset;\n\n          if (from === startChOffset) {\n            ch += 1;\n          }\n          const cursorPos: MdPos = [line, ch];\n          const mdNode = toastMark.findNodeAtPosition(cursorPos)!;\n          const toolbarState = getToolbarState(mdNode);\n\n          eventEmitter.emit('changeToolbarState', {\n            cursorPos,\n            mdNode,\n            toolbarState,\n          });\n          eventEmitter.emit('setFocusedNode', mdNode);\n        },\n      };\n    },\n  });\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/plugins/smartTask.ts",
    "content": "import { Plugin } from 'prosemirror-state';\nimport { EditorView } from 'prosemirror-view';\nimport { MdPos } from '@toast-ui/toastmark';\nimport { MdContext } from '@t/spec';\nimport { findClosestNode } from '@/utils/markdown';\nimport { getRangeInfo, getNodeContentOffsetRange } from '../helper/pos';\n\nconst reTaskMarkerKey = /x|backspace/i;\nconst reTaskMarker = /^\\[(\\s*)(x?)(\\s*)\\](?:\\s+)/i;\n\nexport function smartTask({ schema, toastMark }: MdContext) {\n  return new Plugin({\n    props: {\n      handleDOMEvents: {\n        keyup: (view: EditorView, ev: KeyboardEvent) => {\n          const { doc, tr, selection } = view.state;\n\n          if (selection.empty && reTaskMarkerKey.test(ev.key)) {\n            const { startIndex, startFromOffset, from } = getRangeInfo(selection);\n            // should add `1` to line for the markdown parser\n            // because markdown parser has `1`(not zero) as the start number\n            const mdPos: MdPos = [startIndex + 1, from - startFromOffset + 1];\n            const mdNode = toastMark.findNodeAtPosition(mdPos)!;\n            const paraNode = findClosestNode(\n              mdNode,\n              (node) => node!.type === 'paragraph' && node.parent?.type === 'item'\n            );\n\n            if (paraNode?.firstChild?.literal) {\n              const { firstChild } = paraNode;\n              const matched = firstChild.literal!.match(reTaskMarker);\n\n              if (matched) {\n                const [startMdPos] = firstChild.sourcepos!;\n                const [, startSpaces, stateChar, lastSpaces] = matched;\n                const spaces = startSpaces.length + lastSpaces.length;\n                const { startOffset } = getNodeContentOffsetRange(doc, startMdPos[0] - 1);\n                const startPos = startMdPos[1] + startOffset;\n\n                if (stateChar) {\n                  const addedPos = spaces ? spaces + 1 : 0;\n\n                  tr.replaceWith(startPos, addedPos + startPos, schema.text(stateChar));\n                  view.dispatch(tr);\n                } else if (!spaces) {\n                  tr.insertText(' ', startPos);\n                  view.dispatch(tr);\n                }\n              }\n            }\n          }\n          return false;\n        },\n      },\n    },\n  });\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/plugins/syntaxHighlight.ts",
    "content": "import { MdNode, MdPos, EditResult, ToastMark } from '@toast-ui/toastmark';\nimport { Plugin, Transaction } from 'prosemirror-state';\nimport { NodeType, ProsemirrorNode, Schema } from 'prosemirror-model';\nimport { MdContext } from '@t/spec';\nimport { getMdStartLine, getMdEndLine, getMdStartCh, getMdEndCh } from '@/utils/markdown';\nimport { includes, last } from '@/utils/common';\nimport { getStartPosListPerLine, getWidgetNodePos } from '@/markdown/helper/pos';\nimport { getMarkInfo, MarkInfo } from './helper/markInfo';\n\ninterface CodeBlockPos {\n  codeStart: number;\n  codeEnd: number;\n}\n\ninterface BlockPosInfo {\n  from: number;\n  to: number;\n  startIndex: number;\n  endIndex: number;\n}\n\nlet removingBackgroundIndexMap: Record<number, boolean> = {};\n\nexport function syntaxHighlight({ schema, toastMark }: MdContext) {\n  return new Plugin({\n    appendTransaction(transactions, _, newState) {\n      const [tr] = transactions;\n      const newTr = newState.tr;\n\n      if (tr.docChanged) {\n        let markInfo: MarkInfo[] = [];\n        const editResult: EditResult[] = tr.getMeta('editResult');\n\n        editResult.forEach((result) => {\n          const { nodes, removedNodeRange } = result;\n\n          if (nodes.length) {\n            markInfo = markInfo.concat(getMarkForRemoving(newTr, nodes));\n\n            for (const parent of nodes) {\n              const walker = parent.walker();\n              let event = walker.next();\n\n              while (event) {\n                const { node, entering } = event;\n\n                if (entering) {\n                  markInfo = markInfo.concat(getMarkForAdding(node, toastMark));\n                }\n                event = walker.next();\n              }\n            }\n          } else if (removedNodeRange) {\n            const maxIndex = newTr.doc.childCount - 1;\n            const [startLine, endLine] = removedNodeRange.line;\n            const startIndex = Math.min(startLine, maxIndex);\n            const endIndex = Math.min(endLine, maxIndex);\n\n            // cache the index to remove code block, custom block background when there are no adding nodes\n            for (let i = startIndex; i <= endIndex; i += 1) {\n              removingBackgroundIndexMap[i] = true;\n            }\n          }\n        });\n        appendMarkTr(newTr, schema, markInfo);\n      }\n      return newTr.setMeta('widget', tr.getMeta('widget'));\n    },\n  });\n}\n\nfunction isDifferentBlock(doc: ProsemirrorNode, index: number, attrs: Record<string, any>) {\n  return Object.keys(attrs).some((name) => attrs[name] !== doc.child(index).attrs[name]);\n}\n\nfunction addLineBackground(\n  tr: Transaction,\n  doc: ProsemirrorNode,\n  paragraph: NodeType,\n  blockPosInfo: BlockPosInfo,\n  attrs: Record<string, any> = {}\n) {\n  const { startIndex, endIndex, from, to } = blockPosInfo;\n  let shouldChangeBlockType = false;\n\n  for (let i = startIndex; i <= endIndex; i += 1) {\n    // prevent to remove background of the node that need to have background\n    delete removingBackgroundIndexMap[i];\n    shouldChangeBlockType = isDifferentBlock(doc, i, attrs);\n  }\n\n  if (shouldChangeBlockType) {\n    tr.setBlockType(from, to, paragraph, attrs);\n  }\n}\n\nfunction appendMarkTr(tr: Transaction, schema: Schema, marks: MarkInfo[]) {\n  const { doc } = tr;\n  const { paragraph } = schema.nodes;\n\n  // get start position per line for lazy calculation\n  const startPosListPerLine = getStartPosListPerLine(doc, doc.childCount);\n\n  marks.forEach(({ start, end, spec, lineBackground }) => {\n    const startIndex = Math.min(start[0], doc.childCount) - 1;\n    const endIndex = Math.min(end[0], doc.childCount) - 1;\n    const startNode = doc.child(startIndex);\n    const endNode = doc.child(endIndex);\n\n    // calculate the position corresponding to the line\n    let from = startPosListPerLine[startIndex];\n    let to = startPosListPerLine[endIndex];\n\n    // calculate the position corresponding to the character offset of the line\n    from += start[1] + getWidgetNodePos(startNode, start[1] - 1);\n    to += end[1] + getWidgetNodePos(endNode, end[1] - 1);\n\n    if (spec) {\n      if (lineBackground) {\n        const posInfo = { from, to, startIndex, endIndex };\n\n        addLineBackground(tr, doc, paragraph, posInfo, spec.attrs);\n      } else {\n        tr.addMark(from, to, schema.mark(spec.type!, spec.attrs));\n      }\n    } else {\n      tr.removeMark(from, to);\n    }\n  });\n\n  removeBlockBackground(tr, startPosListPerLine, paragraph);\n}\n\nfunction removeBlockBackground(\n  tr: Transaction,\n  startPosListPerLine: number[],\n  paragraph: NodeType\n) {\n  Object.keys(removingBackgroundIndexMap).forEach((index) => {\n    const startIndex = Number(index);\n    // get the end position of the current line with the next node start position.\n    const endIndex = Math.min(Number(index) + 1, tr.doc.childCount - 1);\n\n    const from = startPosListPerLine[startIndex];\n    // subtract '1' for getting end position of the line\n    let to = startPosListPerLine[endIndex] - 1;\n\n    if (startIndex === endIndex) {\n      to += 2;\n    }\n\n    tr.setBlockType(from, to, paragraph);\n  });\n}\n\nfunction cacheIndexToRemoveBackground(doc: ProsemirrorNode, start: MdPos, end: MdPos) {\n  const skipLines: number[] = [];\n\n  removingBackgroundIndexMap = {};\n\n  for (let i = start[0] - 1; i < end[0]; i += 1) {\n    const node = doc.child(i);\n    let { codeEnd } = node.attrs as CodeBlockPos;\n    const { codeStart } = node.attrs as CodeBlockPos;\n\n    if (codeStart && codeEnd && !includes(skipLines, codeStart)) {\n      skipLines.push(codeStart);\n      codeEnd = Math.min(codeEnd, doc.childCount);\n\n      // should subtract '1' to markdown line position\n      // because markdown parser has '1'(not zero) as the start number\n      const startIndex = codeStart - 1;\n      const [endIndex] = end;\n\n      for (let index = startIndex; index < endIndex; index += 1) {\n        removingBackgroundIndexMap[index] = true;\n      }\n    }\n  }\n}\n\nfunction getMarkForRemoving({ doc }: Transaction, nodes: MdNode[]) {\n  const [start] = nodes[0].sourcepos!;\n  const [, end] = last(nodes).sourcepos!;\n  const startPos: MdPos = [start[0], start[1]];\n  const endPos: MdPos = [end[0], end[1] + 1];\n  const marks: MarkInfo[] = [];\n\n  cacheIndexToRemoveBackground(doc, start, end);\n  marks.push({ start: startPos, end: endPos });\n\n  return marks;\n}\n\nfunction getMarkForAdding(node: MdNode, toastMark: ToastMark) {\n  const lineTexts = toastMark.getLineTexts();\n  const startPos: MdPos = [getMdStartLine(node), getMdStartCh(node)];\n  const endPos: MdPos = [getMdEndLine(node), getMdEndCh(node) + 1];\n  const markInfo = getMarkInfo(node, startPos, endPos, lineTexts[endPos[0] - 1]);\n\n  return markInfo ?? [];\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/scroll/animation.ts",
    "content": "import { SyncCallbacks } from './scrollSync';\n\n// @TODO: apply bezier and raq\ntype WinSetTimeout = typeof window.setTimeout;\n\nconst ANIMATION_TIME = 100;\nconst SCROLL_BLOCKING_RESET_DELAY = 15;\nlet currentTimeoutId: number | null = null;\nlet releaseTimer: number | null = null;\n\nfunction run(deltaScrollTop: number, { syncScrollTop, releaseEventBlock }: SyncCallbacks) {\n  if (releaseTimer) {\n    clearTimeout(releaseTimer);\n  }\n\n  syncScrollTop(deltaScrollTop);\n\n  releaseTimer = (setTimeout as WinSetTimeout)(() => {\n    releaseEventBlock();\n  }, SCROLL_BLOCKING_RESET_DELAY);\n}\n\nexport function animate(\n  curScrollTop: number,\n  targetScrollTop: number,\n  syncCallbacks: SyncCallbacks\n) {\n  const diff = targetScrollTop - curScrollTop;\n  const startTime = Date.now();\n\n  const step = () => {\n    const stepTime = Date.now();\n    const progress = (stepTime - startTime) / ANIMATION_TIME;\n    let deltaValue;\n\n    if (currentTimeoutId) {\n      clearTimeout(currentTimeoutId);\n    }\n\n    if (progress < 1) {\n      deltaValue = curScrollTop + diff * Math.cos(((1 - progress) * Math.PI) / 2);\n      run(Math.ceil(deltaValue), syncCallbacks);\n      currentTimeoutId = (setTimeout as WinSetTimeout)(step, 1);\n    } else {\n      run(targetScrollTop, syncCallbacks);\n      currentTimeoutId = null;\n    }\n  };\n\n  step();\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/scroll/dom.ts",
    "content": "import { ProsemirrorNode } from 'prosemirror-model';\nimport { MdNode } from '@toast-ui/toastmark';\nimport { includes } from '@/utils/common';\nimport { isStyledInlineNode, getMdEndLine, getMdStartLine } from '@/utils/markdown';\n\ntype El = HTMLElement | null;\n\nconst nestableTypes = ['list', 'item', 'blockQuote'];\nconst nestableTagNames = ['UL', 'OL', 'BLOCKQUOTE'];\n\nfunction isBlankLine(doc: ProsemirrorNode, index: number) {\n  const pmNode = doc.child(index);\n\n  return !pmNode.childCount || (pmNode.childCount === 1 && !pmNode.firstChild!.text?.trim());\n}\n\nexport function getEditorRangeHeightInfo(\n  doc: ProsemirrorNode,\n  mdNode: MdNode,\n  children: HTMLCollection\n) {\n  const start = getMdStartLine(mdNode) - 1;\n  const end = getMdEndLine(mdNode) - 1;\n  const rect = (children[start] as HTMLElement).getBoundingClientRect();\n\n  const height =\n    (children[end] as HTMLElement).offsetTop -\n    (children[start] as HTMLElement).offsetTop +\n    children[end].clientHeight;\n\n  return {\n    height:\n      height <= 0\n        ? children[start].clientHeight\n        : height + getBlankLinesHeight(doc, children, Math.min(end + 1, doc.childCount - 1)),\n    rect,\n  };\n}\n\nfunction getBlankLinesHeight(doc: ProsemirrorNode, children: HTMLCollection, start: number) {\n  const end = doc.childCount - 1;\n  let height = 0;\n\n  while (start <= end && isBlankLine(doc, start)) {\n    height += children[start].clientHeight;\n    start += 1;\n  }\n  return height;\n}\n\nexport function findAncestorHavingId(el: HTMLElement, root: HTMLElement) {\n  while (!el.getAttribute('data-nodeid') && el.parentElement !== root) {\n    el = el.parentElement!;\n  }\n  return el;\n}\n\nexport function getTotalOffsetTop(el: El, root: HTMLElement) {\n  let offsetTop = 0;\n\n  while (el && el !== root) {\n    if (!includes(nestableTagNames, el.tagName)) {\n      offsetTop += el.offsetTop;\n    }\n    if (el.offsetParent === root.offsetParent) {\n      break;\n    }\n    el = el.parentElement;\n  }\n  return offsetTop;\n}\n\nexport function findAdjacentElementToScrollTop(scrollTop: number, root: HTMLElement) {\n  let el: El = root;\n  let prev = null;\n\n  while (el) {\n    const { firstElementChild } = el;\n\n    if (!firstElementChild) {\n      break;\n    }\n    const lastSibling = findLastSiblingElementToScrollTop(\n      firstElementChild as El,\n      scrollTop,\n      getTotalOffsetTop(el, root)\n    );\n\n    prev = el;\n    el = lastSibling;\n  }\n\n  const adjacentEl = el || prev;\n\n  return adjacentEl === root ? null : adjacentEl;\n}\n\nfunction findLastSiblingElementToScrollTop(el: El, scrollTop: number, offsetTop: number): El {\n  if (el && scrollTop > offsetTop + el.offsetTop) {\n    return (\n      findLastSiblingElementToScrollTop(el.nextElementSibling as El, scrollTop, offsetTop) || el\n    );\n  }\n\n  return null;\n}\n\nexport function getAdditionalPos(\n  scrollTop: number,\n  offsetTop: number,\n  height: number,\n  targetNodeHeight: number\n) {\n  const ratio = Math.min((scrollTop - offsetTop) / height, 1);\n\n  return ratio * targetNodeHeight;\n}\n\nexport function getParentNodeObj(previewContent: HTMLElement, mdNode: MdNode) {\n  let el = previewContent.querySelector<HTMLElement>(`[data-nodeid=\"${mdNode.id}\"]`);\n\n  while (!el || isStyledInlineNode(mdNode)) {\n    mdNode = mdNode.parent!;\n    el = previewContent.querySelector<HTMLElement>(`[data-nodeid=\"${mdNode.id}\"]`);\n  }\n  return getNonNestableNodeObj({ mdNode, el });\n}\n\nfunction getNonNestableNodeObj({ mdNode, el }: { mdNode: MdNode; el: HTMLElement }) {\n  while ((includes(nestableTypes, mdNode.type) || mdNode.type === 'table') && mdNode.firstChild) {\n    mdNode = mdNode.firstChild;\n    el = el.firstElementChild as HTMLElement;\n  }\n  return { mdNode, el };\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/scroll/offset.ts",
    "content": "import toArray from 'tui-code-snippet/collection/toArray';\nimport { getTotalOffsetTop } from './dom';\n\nconst offsetInfoMap: { [key: number]: { height: number; offsetTop: number } } = {};\n\nexport function setHeight(id: number, height: number) {\n  offsetInfoMap[id] = offsetInfoMap[id] || {};\n  offsetInfoMap[id].height = height;\n}\n\nexport function setOffsetTop(id: number, offsetTop: number) {\n  offsetInfoMap[id] = offsetInfoMap[id] || {};\n  offsetInfoMap[id].offsetTop = offsetTop;\n}\n\nexport function getHeight(id: number) {\n  return offsetInfoMap[id] && offsetInfoMap[id].height;\n}\n\nexport function getOffsetTop(id: number) {\n  return offsetInfoMap[id] && offsetInfoMap[id].offsetTop;\n}\n\nexport function removeOffsetInfoByNode(node: HTMLElement) {\n  if (node) {\n    delete offsetInfoMap[Number(node.getAttribute('data-nodeid'))];\n    toArray(node.children).forEach((child) => {\n      removeOffsetInfoByNode(child as HTMLElement);\n    });\n  }\n}\n\nexport function getAndSaveOffsetInfo(node: HTMLElement, root: HTMLElement, mdNodeId: number) {\n  const cachedHeight = getHeight(mdNodeId);\n  const cachedTop = getOffsetTop(mdNodeId);\n  const nodeHeight = cachedHeight || node.clientHeight;\n  const offsetTop = cachedTop || getTotalOffsetTop(node, root) || node.offsetTop;\n\n  if (!cachedHeight) {\n    setHeight(mdNodeId, nodeHeight);\n  }\n\n  if (!cachedTop) {\n    setOffsetTop(mdNodeId, offsetTop);\n  }\n\n  return { nodeHeight, offsetTop };\n}\n"
  },
  {
    "path": "apps/editor/src/markdown/scroll/scrollSync.ts",
    "content": "import { ProsemirrorNode } from 'prosemirror-model';\nimport { EditorView } from 'prosemirror-view';\nimport { ToastMark } from '@toast-ui/toastmark';\nimport { Emitter } from '@t/event';\nimport { isHTMLNode, getMdStartLine } from '@/utils/markdown';\nimport MarkdownPreview from '../mdPreview';\nimport MdEditor from '../mdEditor';\nimport { animate } from './animation';\nimport { getAndSaveOffsetInfo } from './offset';\nimport {\n  getAdditionalPos,\n  findAncestorHavingId,\n  getEditorRangeHeightInfo,\n  getParentNodeObj,\n  getTotalOffsetTop,\n} from './dom';\n\nconst EDITOR_BOTTOM_PADDING = 18;\n\nexport interface SyncCallbacks {\n  syncScrollTop: (scrollTop: number) => void;\n  releaseEventBlock: () => void;\n}\n\ninterface PosInfo {\n  pos: number;\n  inside: number;\n}\n\ntype ScrollFrom = 'editor' | 'preview';\n\nexport class ScrollSync {\n  private previewRoot: HTMLElement;\n\n  private previewEl: HTMLElement;\n\n  private editorView: EditorView;\n\n  private toastMark: ToastMark;\n\n  private eventEmitter: Emitter;\n\n  private latestEditorScrollTop: number | null = null;\n\n  private latestPreviewScrollTop: number | null = null;\n\n  private blockedScroll: ScrollFrom | null = null;\n\n  private active = true;\n\n  private mdEditor: MdEditor;\n\n  private timer: NodeJS.Timeout | null = null;\n\n  constructor(mdEditor: MdEditor, preview: MarkdownPreview, eventEmitter: Emitter) {\n    const { previewContent: previewRoot, el: previewEl } = preview;\n\n    this.previewRoot = previewRoot;\n    this.previewEl = previewEl!;\n    this.mdEditor = mdEditor;\n    this.editorView = mdEditor.view;\n    this.toastMark = mdEditor.getToastMark();\n    this.eventEmitter = eventEmitter;\n    this.addScrollSyncEvent();\n  }\n\n  private addScrollSyncEvent() {\n    this.eventEmitter.listen('afterPreviewRender', () => {\n      this.clearTimer();\n      // Immediately after the 'afterPreviewRender' event has occurred,\n      // browser rendering is not yet complete.\n      // So the size of elements can not be accurately measured.\n      this.timer = setTimeout(() => {\n        this.syncPreviewScrollTop(true);\n      }, 200);\n    });\n    this.eventEmitter.listen('scroll', (type, data) => {\n      if (this.active) {\n        if (type === 'editor' && this.blockedScroll !== 'editor') {\n          this.syncPreviewScrollTop();\n        } else if (type === 'preview' && this.blockedScroll !== 'preview') {\n          this.syncEditorScrollTop(data);\n        }\n      }\n    });\n    this.eventEmitter.listen('toggleScrollSync', (active: boolean) => {\n      this.active = active;\n    });\n  }\n\n  private getMdNodeAtPos(doc: ProsemirrorNode, posInfo: PosInfo) {\n    const indexInfo = doc.content.findIndex(posInfo.pos);\n    const line = indexInfo.index;\n\n    return this.toastMark.findFirstNodeAtLine(line + 1);\n  }\n\n  private getScrollTopByCaretPos() {\n    const pos = this.mdEditor.getSelection();\n    const firstMdNode = this.toastMark.findFirstNodeAtLine(pos[0][0])!;\n    const previewHeight = this.previewEl.clientHeight;\n    const { el } = getParentNodeObj(this.previewRoot, firstMdNode);\n    const totalOffsetTop = getTotalOffsetTop(el, this.previewRoot) || el.offsetTop;\n    const nodeHeight = el.clientHeight;\n    // multiply 0.5 for calculating the position in the middle of preview area\n    const targetScrollTop = totalOffsetTop + nodeHeight - previewHeight * 0.5;\n\n    this.latestEditorScrollTop = null;\n\n    const diff = el.getBoundingClientRect().top - this.previewEl.getBoundingClientRect().top;\n\n    return diff < previewHeight ? null : targetScrollTop;\n  }\n\n  private syncPreviewScrollTop(editing = false) {\n    const { editorView, previewEl, previewRoot } = this;\n    const { left, top } = editorView.dom.getBoundingClientRect();\n    const posInfo = editorView.posAtCoords({ left, top })!;\n    const { doc } = editorView.state;\n    const firstMdNode = this.getMdNodeAtPos(doc, posInfo);\n\n    if (!firstMdNode || isHTMLNode(firstMdNode)) {\n      return;\n    }\n\n    const curScrollTop = previewEl.scrollTop;\n    const { scrollTop, scrollHeight, clientHeight, children } = editorView.dom;\n    const isBottomPos = scrollHeight - scrollTop <= clientHeight + EDITOR_BOTTOM_PADDING;\n\n    let targetScrollTop = isBottomPos ? previewEl.scrollHeight : 0;\n\n    if (scrollTop && !isBottomPos) {\n      if (editing) {\n        const scrollTopByEditing = this.getScrollTopByCaretPos();\n\n        if (!scrollTopByEditing) {\n          return;\n        }\n        targetScrollTop = scrollTopByEditing;\n      } else {\n        const { el, mdNode } = getParentNodeObj(this.previewRoot, firstMdNode);\n        const { height, rect } = getEditorRangeHeightInfo(doc, mdNode, children);\n        const totalOffsetTop = getTotalOffsetTop(el, previewRoot) || el.offsetTop;\n        const nodeHeight = el.clientHeight;\n        const ratio = top > rect.top ? Math.min((top - rect.top) / height, 1) : 0;\n\n        targetScrollTop = totalOffsetTop + nodeHeight * ratio;\n      }\n      targetScrollTop = this.getResolvedScrollTop(\n        'editor',\n        scrollTop,\n        targetScrollTop,\n        curScrollTop\n      );\n      this.latestEditorScrollTop = scrollTop;\n    }\n\n    if (targetScrollTop !== curScrollTop) {\n      this.run('editor', targetScrollTop, curScrollTop);\n    }\n  }\n\n  syncEditorScrollTop(targetNode: HTMLElement) {\n    const { toastMark, editorView, previewRoot, previewEl } = this;\n    const { dom, state } = editorView;\n    const { scrollTop, clientHeight, scrollHeight } = previewEl;\n    const isBottomPos = scrollHeight - scrollTop <= clientHeight;\n\n    const curScrollTop = dom.scrollTop;\n    let targetScrollTop = isBottomPos ? dom.scrollHeight : 0;\n\n    if (scrollTop && targetNode && !isBottomPos) {\n      targetNode = findAncestorHavingId(targetNode, previewRoot);\n\n      if (!targetNode.getAttribute('data-nodeid')) {\n        return;\n      }\n\n      const { children } = dom;\n      const mdNodeId = Number(targetNode.getAttribute('data-nodeid'));\n      const { mdNode, el } = getParentNodeObj(this.previewRoot, toastMark.findNodeById(mdNodeId)!);\n      const mdNodeStartLine = getMdStartLine(mdNode);\n\n      targetScrollTop = (children[mdNodeStartLine - 1] as HTMLElement).offsetTop;\n\n      const { height } = getEditorRangeHeightInfo(state.doc, mdNode, children);\n      const { nodeHeight, offsetTop } = getAndSaveOffsetInfo(el, previewRoot, mdNodeId);\n\n      targetScrollTop += getAdditionalPos(scrollTop, offsetTop, nodeHeight, height);\n      targetScrollTop = this.getResolvedScrollTop(\n        'preview',\n        scrollTop,\n        targetScrollTop,\n        curScrollTop\n      );\n      this.latestPreviewScrollTop = scrollTop;\n    }\n\n    if (targetScrollTop !== curScrollTop) {\n      this.run('preview', targetScrollTop, curScrollTop);\n    }\n  }\n\n  private getResolvedScrollTop(\n    from: ScrollFrom,\n    scrollTop: number,\n    targetScrollTop: number,\n    curScrollTop: number\n  ) {\n    const latestScrollTop =\n      from === 'editor' ? this.latestEditorScrollTop : this.latestPreviewScrollTop;\n\n    if (latestScrollTop === null) {\n      return targetScrollTop;\n    }\n\n    return latestScrollTop < scrollTop\n      ? Math.max(targetScrollTop, curScrollTop)\n      : Math.min(targetScrollTop, curScrollTop);\n  }\n\n  private run(from: ScrollFrom, targetScrollTop: number, curScrollTop: number) {\n    let scrollTarget: Element;\n\n    if (from === 'editor') {\n      scrollTarget = this.previewEl;\n      this.blockedScroll = 'preview';\n    } else {\n      scrollTarget = this.editorView.dom;\n      this.blockedScroll = 'editor';\n    }\n\n    const syncCallbacks: SyncCallbacks = {\n      syncScrollTop: (scrollTop) => (scrollTarget.scrollTop = scrollTop),\n      releaseEventBlock: () => (this.blockedScroll = null),\n    };\n\n    animate(curScrollTop, targetScrollTop, syncCallbacks);\n  }\n\n  clearTimer() {\n    if (this.timer) {\n      clearTimeout(this.timer);\n      this.timer = null;\n    }\n  }\n\n  destroy() {\n    this.clearTimer();\n    this.eventEmitter.removeEventHandler('scroll');\n    this.eventEmitter.removeEventHandler('afterPreviewRender');\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/plugins/dropImage.ts",
    "content": "import { Plugin } from 'prosemirror-state';\nimport forEachArray from 'tui-code-snippet/collection/forEachArray';\nimport { Context } from '@t/spec';\nimport { emitImageBlobHook } from '@/helper/image';\n\nexport function dropImage({ eventEmitter }: Context) {\n  return new Plugin({\n    props: {\n      handleDOMEvents: {\n        drop: (_, ev) => {\n          const items = (ev as DragEvent).dataTransfer?.files;\n\n          if (items) {\n            forEachArray(items, (item) => {\n              if (item.type.indexOf('image') !== -1) {\n                ev.preventDefault();\n                ev.stopPropagation();\n                emitImageBlobHook(eventEmitter, item, ev.type);\n\n                return false;\n              }\n              return true;\n            });\n          }\n          return true;\n        },\n      },\n    },\n  });\n}\n"
  },
  {
    "path": "apps/editor/src/plugins/placeholder.ts",
    "content": "import { Plugin } from 'prosemirror-state';\nimport { DecorationSet, Decoration } from 'prosemirror-view';\nimport addClass from 'tui-code-snippet/domUtil/addClass';\n\ninterface Options {\n  text?: string;\n  className?: string;\n}\n\nexport function placeholder(options: Options) {\n  return new Plugin({\n    props: {\n      decorations(state) {\n        const { doc } = state;\n\n        if (\n          options.text &&\n          doc.childCount === 1 &&\n          doc.firstChild!.isTextblock &&\n          doc.firstChild!.content.size === 0\n        ) {\n          const placeHolder = document.createElement('span');\n\n          addClass(placeHolder, 'placeholder');\n\n          if (options.className) {\n            addClass(placeHolder, options.className);\n          }\n          placeHolder.textContent = options.text;\n\n          return DecorationSet.create(doc, [Decoration.widget(1, placeHolder)]);\n        }\n        return null;\n      },\n    },\n  });\n}\n"
  },
  {
    "path": "apps/editor/src/plugins/popupWidget.ts",
    "content": "import { EditorView } from 'prosemirror-view';\nimport { Plugin, PluginKey } from 'prosemirror-state';\nimport css from 'tui-code-snippet/domUtil/css';\nimport { closest, cls } from '@/utils/dom';\nimport { WidgetStyle } from '@t/editor';\nimport { Emitter } from '@t/event';\ninterface Widget {\n  node: HTMLElement;\n  style: WidgetStyle;\n  pos: number;\n}\n\nconst pluginKey = new PluginKey('widget');\nconst MARGIN = 5;\n\nclass PopupWidget {\n  private popup: HTMLElement | null = null;\n\n  private eventEmitter: Emitter;\n\n  private rootEl!: HTMLElement;\n\n  constructor(view: EditorView, eventEmitter: Emitter) {\n    this.rootEl = view.dom.parentElement!;\n    this.eventEmitter = eventEmitter;\n    this.eventEmitter.listen('blur', this.removeWidget);\n    this.eventEmitter.listen('loadUI', () => {\n      this.rootEl = closest(view.dom.parentElement!, `.${cls('defaultUI')}`) as HTMLElement;\n    });\n    this.eventEmitter.listen('removePopupWidget', this.removeWidget);\n  }\n\n  private removeWidget = () => {\n    if (this.popup) {\n      this.rootEl.removeChild(this.popup);\n      this.popup = null;\n    }\n  };\n\n  update(view: EditorView) {\n    const widget: Widget | null = pluginKey.getState(view.state);\n\n    this.removeWidget();\n\n    if (widget) {\n      const { node, style } = widget;\n      const { top, left, bottom } = view.coordsAtPos(widget.pos);\n      const height = bottom - top;\n      const rect = this.rootEl.getBoundingClientRect();\n      const relTopPos = top - rect.top;\n\n      css(node, { opacity: '0' });\n      this.rootEl.appendChild(node);\n      css(node, {\n        position: 'absolute',\n        left: `${left - rect.left + MARGIN}px`,\n        top: `${style === 'bottom' ? relTopPos + height - MARGIN : relTopPos - height}px`,\n        opacity: '1',\n      });\n      this.popup = node;\n      view.focus();\n    }\n  }\n\n  destroy() {\n    this.eventEmitter.removeEventHandler('blur', this.removeWidget);\n  }\n}\n\nexport function addWidget(eventEmitter: Emitter) {\n  return new Plugin({\n    key: pluginKey,\n    state: {\n      init() {\n        return null;\n      },\n      apply(tr) {\n        return tr.getMeta('widget');\n      },\n    },\n    view(editorView) {\n      return new PopupWidget(editorView, eventEmitter);\n    },\n  });\n}\n"
  },
  {
    "path": "apps/editor/src/queries/queryManager.ts",
    "content": "import type { EditorCore as Editor } from '@t/editor';\n\ntype QueryFn = (editor: Editor, payload?: Record<string, any>) => any;\n\nconst queryMap: Record<string, QueryFn> = {\n  getPopupInitialValues(editor, payload) {\n    const { popupName } = payload!;\n\n    return popupName === 'link' ? { linkText: editor.getSelectedText() } : {};\n  },\n};\n\nexport function buildQuery(editor: Editor) {\n  editor.eventEmitter.listen('query', (query: string, payload?: Record<string, any>) =>\n    queryMap[query](editor, payload)\n  );\n}\n"
  },
  {
    "path": "apps/editor/src/sanitizer/htmlSanitizer.ts",
    "content": "import DOMPurify from 'dompurify';\nimport { includes } from '@/utils/common';\n\nconst CAN_BE_WHITE_TAG_LIST = ['iframe', 'embed'];\nconst whiteTagList: string[] = [];\n\nexport function registerTagWhitelistIfPossible(tagName: string) {\n  if (includes(CAN_BE_WHITE_TAG_LIST, tagName)) {\n    whiteTagList.push(tagName.toLowerCase());\n  }\n}\n\nexport function sanitizeHTML<T extends string | HTMLElement | DocumentFragment = string>(\n  html: string | Node,\n  options?: DOMPurify.Config\n) {\n  return DOMPurify.sanitize(html, {\n    ADD_TAGS: whiteTagList,\n    ADD_ATTR: ['rel', 'target', 'hreflang', 'type'],\n    FORBID_TAGS: [\n      'input',\n      'script',\n      'textarea',\n      'form',\n      'button',\n      'select',\n      'meta',\n      'style',\n      'link',\n      'title',\n      'object',\n      'base',\n    ],\n    ...options,\n  }) as T;\n}\n"
  },
  {
    "path": "apps/editor/src/spec/mark.ts",
    "content": "import { Keymap } from 'prosemirror-commands';\nimport { MarkSpec } from 'prosemirror-model';\nimport { SpecContext, EditorCommand, EditorCommandMap } from '@t/spec';\n\nexport default abstract class Mark {\n  context!: SpecContext;\n\n  get type() {\n    return 'mark';\n  }\n\n  setContext(context: SpecContext) {\n    this.context = context;\n  }\n\n  abstract get name(): string;\n\n  abstract get schema(): MarkSpec;\n\n  commands?(): EditorCommand | EditorCommandMap;\n\n  keymaps?(): Keymap<any>;\n}\n"
  },
  {
    "path": "apps/editor/src/spec/node.ts",
    "content": "import { Keymap } from 'prosemirror-commands';\nimport { NodeSpec } from 'prosemirror-model';\nimport { SpecContext, EditorCommand, EditorCommandMap } from '@t/spec';\n\nexport default abstract class Node {\n  context!: SpecContext;\n\n  get type() {\n    return 'node';\n  }\n\n  setContext(context: SpecContext) {\n    this.context = context;\n  }\n\n  abstract get name(): string;\n\n  abstract get schema(): NodeSpec;\n\n  commands?(): EditorCommand | EditorCommandMap;\n\n  keymaps?(): Keymap<any>;\n}\n"
  },
  {
    "path": "apps/editor/src/spec/specManager.ts",
    "content": "import { EditorView } from 'prosemirror-view';\nimport { keymap } from 'prosemirror-keymap';\nimport { EditorAllCommandMap, SpecContext, EditorCommand } from '@t/spec';\nimport isFunction from 'tui-code-snippet/type/isFunction';\nimport { getDefaultCommands } from '@/commands/defaultCommands';\nimport { includes } from '@/utils/common';\n\nimport Mark from '@/spec/mark';\nimport Node from '@/spec/node';\n\ntype Spec = Node | Mark;\n\nconst defaultCommandShortcuts = [\n  'Enter',\n  'Shift-Enter',\n  'Mod-Enter',\n  'Tab',\n  'Shift-Tab',\n  'Delete',\n  'Backspace',\n  'Mod-Delete',\n  'Mod-Backspace',\n  'ArrowUp',\n  'ArrowDown',\n  'ArrowLeft',\n  'ArrowRight',\n  'Mod-d',\n  'Mod-D',\n  'Alt-ArrowUp',\n  'Alt-ArrowDown',\n];\n\nexport function execCommand(\n  view: EditorView,\n  command: EditorCommand,\n  payload?: Record<string, any>\n) {\n  view.focus();\n  return command(payload)(view.state, view.dispatch, view);\n}\n\nexport default class SpecManager {\n  private specs: Spec[];\n\n  constructor(specs: Spec[]) {\n    this.specs = specs;\n  }\n\n  get nodes() {\n    return this.specs\n      .filter((spec) => spec.type === 'node')\n      .reduce((nodes, { name, schema }) => {\n        return {\n          ...nodes,\n          [name]: schema,\n        };\n      }, {});\n  }\n\n  get marks() {\n    return this.specs\n      .filter((spec) => spec.type === 'mark')\n      .reduce((marks, { name, schema }) => {\n        return {\n          ...marks,\n          [name]: schema,\n        };\n      }, {});\n  }\n\n  commands(view: EditorView, addedCommands?: Record<string, EditorCommand>) {\n    const specCommands: EditorAllCommandMap = this.specs\n      .filter(({ commands }) => commands)\n      .reduce((allCommands, spec) => {\n        const commands: EditorAllCommandMap = {};\n        const specCommand = spec.commands!();\n\n        if (isFunction(specCommand)) {\n          commands[spec.name] = (payload) => execCommand(view, specCommand, payload);\n        } else {\n          Object.keys(specCommand).forEach((name) => {\n            commands[name] = (payload) => execCommand(view, specCommand[name], payload);\n          });\n        }\n\n        return {\n          ...allCommands,\n          ...commands,\n        };\n      }, {});\n\n    const defaultCommands = getDefaultCommands();\n\n    Object.keys(defaultCommands).forEach((name) => {\n      specCommands[name] = (payload) => execCommand(view, defaultCommands[name], payload);\n    });\n\n    if (addedCommands) {\n      Object.keys(addedCommands).forEach((name) => {\n        specCommands[name] = (payload) => execCommand(view, addedCommands[name], payload);\n      });\n    }\n\n    return specCommands;\n  }\n\n  keymaps(useCommandShortcut: boolean) {\n    const specKeymaps = this.specs.filter((spec) => spec.keymaps).map((spec) => spec.keymaps!());\n\n    return specKeymaps.map((keys) => {\n      if (!useCommandShortcut) {\n        Object.keys(keys).forEach((key) => {\n          if (!includes(defaultCommandShortcuts, key)) {\n            delete keys[key];\n          }\n        });\n      }\n      return keymap(keys);\n    });\n  }\n\n  setContext(context: SpecContext) {\n    this.specs.forEach((spec) => {\n      spec.setContext(context);\n    });\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/ui/components/contextMenu.ts",
    "content": "import { ContextMenuItem, ExecCommand, Pos, VNode } from '@t/ui';\nimport { Emitter } from '@t/event';\nimport { closest, cls } from '@/utils/dom';\nimport html from '../vdom/template';\nimport { Component } from '../vdom/component';\n\ninterface State {\n  pos: Pos | null;\n  menuGroups: ContextMenuItem[][];\n}\n\ninterface Props {\n  eventEmitter: Emitter;\n  execCommand: ExecCommand;\n}\n\nexport class ContextMenu extends Component<Props, State> {\n  constructor(props: Props) {\n    super(props);\n    this.state = {\n      pos: null,\n      menuGroups: [],\n    };\n    this.addEvent();\n  }\n\n  addEvent() {\n    this.props.eventEmitter.listen('contextmenu', ({ pos, menuGroups }) => {\n      this.setState({ pos, menuGroups });\n    });\n  }\n\n  mounted() {\n    document.addEventListener('click', this.handleClickDocument);\n  }\n\n  beforeDestroy() {\n    document.removeEventListener('click', this.handleClickDocument);\n  }\n\n  private handleClickDocument = (ev: MouseEvent) => {\n    if (!closest(ev.target as HTMLElement, `.${cls('context-menu')}`)) {\n      this.setState({ pos: null });\n    }\n  };\n\n  private getMenuGroupElements() {\n    const { pos, menuGroups } = this.state;\n\n    return pos\n      ? menuGroups.reduce((acc, group) => {\n          const menuItem: VNode[] = [];\n\n          group.forEach(({ label, className = false, disabled, onClick }) => {\n            const handleClick = () => {\n              if (!disabled) {\n                onClick!();\n                this.setState({ pos: null });\n              }\n            };\n\n            menuItem.push(\n              html`\n                <li\n                  onClick=${handleClick}\n                  class=\"menu-item${disabled ? ' disabled' : ''}\"\n                  aria-role=\"menuitem\"\n                >\n                  <span class=\"${className}\">${label}</span>\n                </li>\n              `\n            );\n          });\n\n          acc.push(\n            html`<ul class=\"menu-group\">\n              ${menuItem}\n            </ul>`\n          );\n          return acc;\n        }, [] as VNode[])\n      : [];\n  }\n\n  render() {\n    const style = { display: this.state.pos ? 'block' : 'none', ...this.state.pos };\n\n    return html`<div class=\"${cls('context-menu')}\" style=${style} aria-role=\"menu\">\n      ${this.getMenuGroupElements()}\n    </div>`;\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/ui/components/layout.ts",
    "content": "import { EditorType, PreviewStyle } from '@t/editor';\nimport { Emitter } from '@t/event';\nimport { IndexList, ToolbarItem, ToolbarItemOptions } from '@t/ui';\nimport { cls } from '@/utils/dom';\nimport html from '../vdom/template';\nimport { Component } from '../vdom/component';\nimport { Switch } from './switch';\nimport { Toolbar } from './toolbar/toolbar';\nimport { ContextMenu } from './contextMenu';\n\ninterface Props {\n  eventEmitter: Emitter;\n  hideModeSwitch: boolean;\n  slots: {\n    mdEditor: HTMLElement;\n    mdPreview: HTMLElement;\n    wwEditor: HTMLElement;\n  };\n  previewStyle: PreviewStyle;\n  editorType: EditorType;\n  toolbarItems: ToolbarItem[];\n  theme: string;\n}\n\ninterface State {\n  editorType: EditorType;\n  previewStyle: PreviewStyle;\n  hide: boolean;\n}\n\nexport class Layout extends Component<Props, State> {\n  private toolbar!: Toolbar;\n\n  constructor(props: Props) {\n    super(props);\n    const { editorType, previewStyle } = props;\n\n    this.state = {\n      editorType,\n      previewStyle,\n      hide: false,\n    };\n    this.addEvent();\n  }\n\n  mounted() {\n    const { wwEditor, mdEditor, mdPreview } = this.props.slots;\n\n    this.refs.wwContainer.appendChild(wwEditor);\n    this.refs.mdContainer.insertAdjacentElement('afterbegin', mdEditor);\n    this.refs.mdContainer.appendChild(mdPreview);\n  }\n\n  insertToolbarItem(indexList: IndexList, item: string | ToolbarItemOptions) {\n    this.toolbar.insertToolbarItem(indexList, item);\n  }\n\n  removeToolbarItem(name: string) {\n    this.toolbar.removeToolbarItem(name);\n  }\n\n  render() {\n    const { eventEmitter, hideModeSwitch, toolbarItems, theme } = this.props;\n    const { hide, previewStyle, editorType } = this.state;\n    const displayClassName = hide ? ' hidden' : '';\n    const editorTypeClassName = cls(editorType === 'markdown' ? 'md-mode' : 'ww-mode');\n    const previewClassName = `${cls('md')}-${previewStyle}-style`;\n    const themeClassName = cls([theme !== 'light', `${theme} `]);\n\n    return html`\n      <div\n        class=\"${themeClassName}${cls('defaultUI')}${displayClassName}\"\n        ref=${(el: HTMLElement) => (this.refs.el = el)}\n      >\n        <${Toolbar}\n          ref=${(toolbar: Toolbar) => (this.toolbar = toolbar)}\n          eventEmitter=${eventEmitter}\n          previewStyle=${previewStyle}\n          toolbarItems=${toolbarItems}\n          editorType=${editorType}\n        />\n        <div\n          class=\"${cls('main')} ${editorTypeClassName}\"\n          ref=${(el: HTMLElement) => (this.refs.editorSection = el)}\n        >\n          <div class=\"${cls('main-container')}\">\n            <div\n              class=\"${cls('md-container')} ${previewClassName}\"\n              ref=${(el: HTMLElement) => (this.refs.mdContainer = el)}\n            >\n              <div class=\"${cls('md-splitter')}\"></div>\n            </div>\n            <div\n              class=\"${cls('ww-container')}\"\n              ref=${(el: HTMLElement) => (this.refs.wwContainer = el)}\n            />\n          </div>\n        </div>\n        ${!hideModeSwitch &&\n        html`<${Switch} eventEmitter=${eventEmitter} editorType=${editorType} />`}\n        <${ContextMenu} eventEmitter=${eventEmitter} />\n      </div>\n    `;\n  }\n\n  addEvent() {\n    const { eventEmitter } = this.props;\n\n    eventEmitter.listen('hide', this.hide);\n    eventEmitter.listen('show', this.show);\n    eventEmitter.listen('changeMode', this.changeMode);\n    eventEmitter.listen('changePreviewStyle', this.changePreviewStyle);\n  }\n\n  private changeMode = (editorType: EditorType) => {\n    if (editorType !== this.state.editorType) {\n      this.setState({ editorType });\n    }\n  };\n\n  private changePreviewStyle = (previewStyle: PreviewStyle) => {\n    if (previewStyle !== this.state.previewStyle) {\n      this.setState({ previewStyle });\n    }\n  };\n\n  private hide = () => {\n    this.setState({ hide: true });\n  };\n\n  private show = () => {\n    this.setState({ hide: false });\n  };\n}\n"
  },
  {
    "path": "apps/editor/src/ui/components/popup.ts",
    "content": "import { ExecCommand, HidePopup, PopupInfo, Pos } from '@t/ui';\nimport { Emitter } from '@t/event';\nimport { closest, cls } from '@/utils/dom';\nimport { shallowEqual } from '@/utils/common';\nimport html from '../vdom/template';\nimport { Component } from '../vdom/component';\n\ntype PopupStyle = {\n  display: 'none' | 'block';\n} & Partial<Pos>;\n\ninterface Props {\n  show: boolean;\n  info: PopupInfo;\n  eventEmitter: Emitter;\n  hidePopup: HidePopup;\n  execCommand: ExecCommand;\n}\n\ninterface State {\n  popupPos: Pos | null;\n}\n\nconst MARGIN_FROM_RIGHT_SIDE = 20;\n\nexport class Popup extends Component<Props, State> {\n  private handleMousedown = (ev: MouseEvent) => {\n    if (\n      !closest(ev.target as HTMLElement, `.${cls('popup')}`) &&\n      !closest(ev.target as HTMLElement, this.props.info.fromEl)\n    ) {\n      this.props.hidePopup();\n    }\n  };\n\n  mounted() {\n    document.addEventListener('mousedown', this.handleMousedown);\n    this.props.eventEmitter.listen('closePopup', this.props.hidePopup);\n  }\n\n  beforeDestroy() {\n    document.removeEventListener('mousedown', this.handleMousedown);\n  }\n\n  updated(prevProps: Props) {\n    const { show, info } = this.props;\n\n    if (show && info.pos && prevProps.show !== show) {\n      const popupPos = { ...info.pos };\n      const { offsetWidth } = this.refs.el;\n      const toolbarEl = closest(this.refs.el, `.${cls('toolbar')}`) as HTMLElement;\n      const { offsetWidth: toolbarOffsetWidth } = toolbarEl;\n\n      if (popupPos.left + offsetWidth >= toolbarOffsetWidth) {\n        popupPos.left = toolbarOffsetWidth - offsetWidth - MARGIN_FROM_RIGHT_SIDE;\n      }\n      if (!shallowEqual(this.state.popupPos, popupPos)) {\n        this.setState({ popupPos });\n      }\n    }\n  }\n\n  render() {\n    const { info, show, hidePopup, eventEmitter, execCommand } = this.props;\n    const { className = '', style, render, initialValues = {} } = info || {};\n    const popupStyle: PopupStyle = {\n      display: show ? 'block' : 'none',\n      ...style,\n      ...this.state.popupPos,\n    };\n\n    return html`\n      <div\n        class=\"${cls('popup')} ${className}\"\n        style=${popupStyle}\n        ref=${(el: HTMLElement) => (this.refs.el = el)}\n        aria-role=\"dialog\"\n      >\n        <div class=\"${cls('popup-body')}\">\n          ${render && render({ eventEmitter, show, hidePopup, execCommand, initialValues })}\n        </div>\n      </div>\n    `;\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/ui/components/switch.ts",
    "content": "import { Emitter } from '@t/event';\nimport { EditorType } from '@t/editor';\nimport i18n from '@/i18n/i18n';\nimport { cls } from '@/utils/dom';\nimport html from '../vdom/template';\nimport { Component } from '../vdom/component';\n\ninterface Props {\n  editorType: EditorType;\n  eventEmitter: Emitter;\n}\n\ninterface State {\n  hide: boolean;\n}\n\nexport class Switch extends Component<Props, State> {\n  constructor(props: Props) {\n    super(props);\n    this.state = {\n      hide: false,\n    };\n  }\n\n  show() {\n    this.setState({ hide: false });\n  }\n\n  hide() {\n    this.setState({ hide: true });\n  }\n\n  render() {\n    const { editorType, eventEmitter } = this.props;\n\n    return html`\n      <div class=\"${cls('mode-switch')}\" style=\"display: ${this.state.hide ? 'none' : 'block'}\">\n        <div\n          class=\"tab-item${editorType === 'markdown' ? ' active' : ''}\"\n          onClick=${() => {\n            eventEmitter.emit('needChangeMode', 'markdown');\n          }}\n        >\n          ${i18n.get('Markdown')}\n        </div>\n        <div\n          class=\"tab-item${editorType === 'wysiwyg' ? ' active' : ''}\"\n          onClick=${() => {\n            eventEmitter.emit('needChangeMode', 'wysiwyg');\n          }}\n        >\n          ${i18n.get('WYSIWYG')}\n        </div>\n      </div>\n    `;\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/ui/components/tabs.ts",
    "content": "import { TabInfo } from '@t/ui';\nimport i18n from '@/i18n/i18n';\nimport { cls } from '@/utils/dom';\nimport html from '../vdom/template';\nimport { Component } from '../vdom/component';\n\ninterface Props {\n  tabs: TabInfo[];\n  activeTab: string;\n  onClick: (ev: MouseEvent, activeTab: string) => void;\n}\n\nexport class Tabs extends Component<Props> {\n  private toggleTab(ev: MouseEvent, activeTab: string) {\n    this.props.onClick(ev, activeTab);\n  }\n\n  render() {\n    return html`\n      <div class=\"${cls('tabs')}\" aria-role=\"tabpanel\">\n        ${this.props.tabs.map(({ name, text }) => {\n          const isActive = this.props.activeTab === name;\n\n          return html`\n            <div\n              class=\"tab-item${isActive ? ' active' : ''}\"\n              onClick=${(ev: MouseEvent) => this.toggleTab(ev, name)}\n              aria-role=\"tab\"\n              aria-label=\"${i18n.get(text)}\"\n              aria-selected=\"${isActive ? 'true' : 'false'}\"\n              tabindex=\"${isActive ? '0' : '-1'}\"\n            >\n              ${i18n.get(text)}\n            </div>\n          `;\n        })}\n      </div>\n    `;\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/ui/components/toolbar/buttonHoc.ts",
    "content": "import css from 'tui-code-snippet/domUtil/css';\nimport {\n  ExecCommand,\n  SetPopupInfo,\n  ToolbarItemInfo,\n  SetItemWidth,\n  ComponentClass,\n  ToolbarButtonInfo,\n  ToolbarStateMap,\n} from '@t/ui';\nimport { Emitter } from '@t/event';\nimport html from '@/ui/vdom/template';\nimport { Component } from '@/ui/vdom/component';\nimport { closest, cls, getTotalOffset } from '@/utils/dom';\n\ninterface Props {\n  tooltipRef: { current: HTMLElement };\n  disabled: boolean;\n  eventEmitter: Emitter;\n  item: ToolbarItemInfo;\n  execCommand: ExecCommand;\n  setPopupInfo: SetPopupInfo;\n  setItemWidth?: SetItemWidth;\n}\n\ninterface Payload {\n  toolbarState: ToolbarStateMap;\n}\n\ninterface State {\n  active: boolean;\n  disabled: boolean;\n}\n\nconst TOOLTIP_INDENT = 6;\n\nexport function connectHOC(WrappedComponent: ComponentClass) {\n  return class ButtonHOC extends Component<Props, State> {\n    constructor(props: Props) {\n      super(props);\n      this.state = { active: false, disabled: props.disabled };\n      this.addEvent();\n    }\n\n    private addEvent() {\n      const { item, eventEmitter } = this.props;\n\n      if (item.state) {\n        eventEmitter.listen('changeToolbarState', ({ toolbarState }: Payload) => {\n          const { active, disabled } = toolbarState[item.state!] ?? {};\n\n          this.setState({ active: !!active, disabled: disabled ?? this.props.disabled });\n        });\n      }\n    }\n\n    private getBound(el: HTMLElement) {\n      const { offsetLeft, offsetTop } = getTotalOffset(\n        el,\n        closest(el, `.${cls('toolbar')}`) as HTMLElement\n      );\n\n      return { left: offsetLeft, top: el.offsetHeight + offsetTop };\n    }\n\n    private showTooltip = (el: HTMLElement) => {\n      const { tooltip } = this.props.item as ToolbarButtonInfo;\n\n      if (!this.props.disabled && tooltip) {\n        const bound = this.getBound(el);\n        const left = `${bound.left + TOOLTIP_INDENT}px`;\n        const top = `${bound.top + TOOLTIP_INDENT}px`;\n\n        css(this.props.tooltipRef.current, { display: 'block', left, top });\n        this.props.tooltipRef.current.querySelector<HTMLElement>('.text')!.textContent = tooltip;\n      }\n    };\n\n    private hideTooltip = () => {\n      css(this.props.tooltipRef.current, 'display', 'none');\n    };\n\n    render() {\n      return html`\n        <${WrappedComponent}\n          ...${this.props}\n          active=${this.state.active}\n          showTooltip=${this.showTooltip}\n          hideTooltip=${this.hideTooltip}\n          getBound=${this.getBound}\n          disabled=${this.state.disabled || this.props.disabled}\n        />\n      `;\n    }\n  };\n}\n"
  },
  {
    "path": "apps/editor/src/ui/components/toolbar/customPopupBody.ts",
    "content": "import { ExecCommand, HidePopup } from '@t/ui';\nimport { Emitter } from '@t/event';\nimport html from '@/ui/vdom/template';\nimport { Component } from '@/ui/vdom/component';\n\ninterface Props {\n  body: HTMLElement;\n  show: boolean;\n  eventEmitter: Emitter;\n  execCommand: ExecCommand;\n  hidePopup: HidePopup;\n}\n\nexport class CustomPopupBody extends Component<Props> {\n  mounted() {\n    // append the custom popup body element\n    this.refs.el.appendChild(this.props.body);\n  }\n\n  updated(prevProps: Props) {\n    // update custom popup element\n    this.refs.el.replaceChild(this.props.body, prevProps.body);\n  }\n\n  render() {\n    return html`<div ref=${(el: HTMLElement) => (this.refs.el = el)}></div>`;\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/ui/components/toolbar/customToolbarItem.ts",
    "content": "import {\n  ExecCommand,\n  SetPopupInfo,\n  SetItemWidth,\n  GetBound,\n  HideTooltip,\n  ShowTooltip,\n  ToolbarCustomOptions,\n} from '@t/ui';\nimport { Emitter } from '@t/event';\nimport html from '@/ui/vdom/template';\nimport { Component } from '@/ui/vdom/component';\nimport { cls, getOuterWidth } from '@/utils/dom';\nimport { createPopupInfo } from '@/ui/toolbarItemFactory';\nimport { connectHOC } from './buttonHoc';\n\ninterface Props {\n  disabled: boolean;\n  eventEmitter: Emitter;\n  item: ToolbarCustomOptions;\n  active: boolean;\n  execCommand: ExecCommand;\n  setPopupInfo: SetPopupInfo;\n  showTooltip: ShowTooltip;\n  hideTooltip: HideTooltip;\n  getBound: GetBound;\n  setItemWidth?: SetItemWidth;\n}\n\nclass CustomToolbarItemComp extends Component<Props> {\n  mounted() {\n    const { setItemWidth, item } = this.props;\n\n    // append the custom html element\n    this.refs.el.appendChild(item.el!);\n    // set width only if it is not a dropdown toolbar\n    if (setItemWidth) {\n      setItemWidth(item.name, getOuterWidth(this.refs.el));\n    }\n\n    if (item.onMounted) {\n      item.onMounted(this.props.execCommand);\n    }\n  }\n\n  updated(prevProps: Props) {\n    const { item, active, disabled } = this.props;\n\n    if (prevProps.active !== active || prevProps.disabled !== disabled) {\n      item.onUpdated?.({ active, disabled });\n    }\n  }\n\n  private showTooltip = () => {\n    this.props.showTooltip(this.refs.el);\n  };\n\n  private showPopup = () => {\n    const info = createPopupInfo('customPopupBody', {\n      el: this.refs.el,\n      pos: this.props.getBound(this.refs.el),\n      popup: this.props.item.popup!,\n    });\n\n    if (info) {\n      this.props.setPopupInfo(info);\n    }\n  };\n\n  render() {\n    const { disabled, item } = this.props;\n    const style = { display: item.hidden ? 'none' : 'inline-block' };\n    const getListener = (listener: Function) => (disabled ? null : listener);\n\n    return html`\n      <div\n        ref=${(el: HTMLElement) => (this.refs.el = el)}\n        style=${style}\n        class=${cls('toolbar-item-wrapper')}\n        onClick=${getListener(this.showPopup)}\n        onMouseover=${getListener(this.showTooltip)}\n        onMouseout=${getListener(this.props.hideTooltip)}\n      ></div>\n    `;\n  }\n}\nexport const CustomToolbarItem = connectHOC(CustomToolbarItemComp);\n"
  },
  {
    "path": "apps/editor/src/ui/components/toolbar/dropdownToolbarButton.ts",
    "content": "import {\n  ExecCommand,\n  SetPopupInfo,\n  ToolbarItemInfo,\n  GetBound,\n  HideTooltip,\n  ShowTooltip,\n  ToolbarButtonInfo,\n} from '@t/ui';\nimport { Emitter } from '@t/event';\nimport { closest, cls } from '@/utils/dom';\nimport html from '@/ui/vdom/template';\nimport { Component } from '@/ui/vdom/component';\nimport { ToolbarGroup } from './toolbarGroup';\nimport { connectHOC } from './buttonHoc';\n\ninterface Props {\n  disabled: boolean;\n  eventEmitter: Emitter;\n  item: ToolbarButtonInfo;\n  items: ToolbarItemInfo[];\n  execCommand: ExecCommand;\n  setPopupInfo: SetPopupInfo;\n  showTooltip: ShowTooltip;\n  hideTooltip: HideTooltip;\n  getBound: GetBound;\n}\n\ninterface State {\n  dropdownPos: { right: number; top: number } | null;\n  showDropdown: boolean;\n}\n\nconst POPUP_INDENT = 4;\n\nclass DropdownToolbarButtonComp extends Component<Props, State> {\n  constructor(props: Props) {\n    super(props);\n    this.state = { showDropdown: false, dropdownPos: null };\n  }\n\n  private getBound() {\n    const rect = this.props.getBound(this.refs.el);\n\n    rect.top += POPUP_INDENT;\n\n    return { ...rect, left: null, right: 10 };\n  }\n\n  private handleClickDocument = ({ target }: MouseEvent) => {\n    if (\n      !closest(target as HTMLElement, `.${cls('dropdown-toolbar')}`) &&\n      !closest(target as HTMLElement, '.more')\n    ) {\n      this.setState({ showDropdown: false, dropdownPos: null });\n    }\n  };\n\n  mounted() {\n    document.addEventListener('click', this.handleClickDocument);\n  }\n\n  updated() {\n    if (this.state.showDropdown && !this.state.dropdownPos) {\n      this.setState({ dropdownPos: this.getBound() });\n    }\n  }\n\n  beforeDestroy() {\n    document.removeEventListener('click', this.handleClickDocument);\n  }\n\n  private showTooltip = () => {\n    this.props.showTooltip(this.refs.el);\n  };\n\n  render() {\n    const { showDropdown, dropdownPos } = this.state;\n    const { disabled, item, items, hideTooltip } = this.props;\n    const visibleItems = items.filter((dropdownItem) => !dropdownItem.hidden);\n    const groupStyle = visibleItems.length ? null : { display: 'none' };\n    const dropdownStyle = showDropdown ? null : { display: 'none' };\n\n    return html`\n      <div class=\"${cls('toolbar-group')}\" style=${groupStyle}>\n        <button\n          ref=${(el: HTMLElement) => (this.refs.el = el)}\n          type=\"button\"\n          class=${item.className}\n          onClick=${() => this.setState({ showDropdown: true })}\n          onMouseover=${this.showTooltip}\n          onMouseout=${hideTooltip}\n          disabled=${disabled}\n        ></button>\n        <div\n          class=\"${cls('dropdown-toolbar')}\"\n          style=${{ ...dropdownStyle, ...dropdownPos }}\n          ref=${(el: HTMLElement) => (this.refs.dropdownEl = el)}\n        >\n          ${visibleItems.length\n            ? visibleItems.map(\n                (group, index) => html`\n                  <${ToolbarGroup}\n                    group=${group}\n                    hiddenDivider=${index === visibleItems.length - 1 ||\n                    (visibleItems as ToolbarButtonInfo[])[index + 1]?.hidden}\n                    ...${this.props}\n                  />\n                `\n              )\n            : null}\n        </div>\n      </div>\n    `;\n  }\n}\nexport const DropdownToolbarButton = connectHOC(DropdownToolbarButtonComp);\n"
  },
  {
    "path": "apps/editor/src/ui/components/toolbar/headingPopupBody.ts",
    "content": "import { Emitter } from '@t/event';\nimport { ExecCommand } from '@t/ui';\nimport { closest } from '@/utils/dom';\nimport i18n from '@/i18n/i18n';\nimport html from '@/ui/vdom/template';\nimport { Component } from '@/ui/vdom/component';\n\ninterface Props {\n  eventEmitter: Emitter;\n  execCommand: ExecCommand;\n}\n\nexport class HeadingPopupBody extends Component<Props> {\n  execCommand(ev: MouseEvent) {\n    const el = closest(ev.target as HTMLElement, 'li')! as HTMLElement;\n\n    this.props.execCommand('heading', {\n      level: Number(el.getAttribute('data-level')),\n    });\n  }\n\n  render() {\n    return html`\n      <ul\n        onClick=${(ev: MouseEvent) => this.execCommand(ev)}\n        aria-role=\"menu\"\n        aria-label=\"${i18n.get('Headings')}\"\n      >\n        ${[1, 2, 3, 4, 5, 6].map(\n          (level) =>\n            html`\n              <li data-level=\"${level}\" data-type=\"Heading\" aria-role=\"menuitem\">\n                <${`h${level}`}>${i18n.get('Heading')} ${level}</$>\n              </li>\n            `\n        )}\n        <li data-type=\"Paragraph\" aria-role=\"menuitem\">\n          <div>${i18n.get('Paragraph')}</div>\n        </li>\n      </ul>\n    `;\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/ui/components/toolbar/imagePopupBody.ts",
    "content": "import removeClass from 'tui-code-snippet/domUtil/removeClass';\nimport addClass from 'tui-code-snippet/domUtil/addClass';\nimport { HookCallback } from '@t/editor';\nimport { Emitter } from '@t/event';\nimport { ExecCommand, HidePopup, TabInfo } from '@t/ui';\nimport i18n from '@/i18n/i18n';\nimport { cls } from '@/utils/dom';\nimport { Component } from '@/ui/vdom/component';\nimport html from '@/ui/vdom/template';\nimport { Tabs } from '../tabs';\n\nconst TYPE_UI = 'ui';\n\ntype TabType = 'url' | 'file';\n\ninterface Props {\n  show: boolean;\n  eventEmitter: Emitter;\n  execCommand: ExecCommand;\n  hidePopup: HidePopup;\n}\n\ninterface State {\n  activeTab: TabType;\n  file: File | null;\n  fileNameElClassName: string;\n}\n\nexport class ImagePopupBody extends Component<Props, State> {\n  private tabs: TabInfo[];\n\n  constructor(props: Props) {\n    super(props);\n    this.state = { activeTab: 'file', file: null, fileNameElClassName: '' };\n    this.tabs = [\n      { name: 'file', text: 'File' },\n      { name: 'url', text: 'URL' },\n    ];\n  }\n\n  private initialize = (activeTab: TabType = 'file') => {\n    const urlEl = this.refs.url as HTMLInputElement;\n\n    urlEl.value = '';\n    (this.refs.altText as HTMLInputElement).value = '';\n    (this.refs.file as HTMLInputElement).value = '';\n\n    removeClass(urlEl, 'wrong');\n\n    this.setState({ activeTab, file: null, fileNameElClassName: '' });\n  };\n\n  private emitAddImageBlob() {\n    const { files } = this.refs.file as HTMLInputElement;\n    const altTextEl = this.refs.altText as HTMLInputElement;\n    let fileNameElClassName = ' wrong';\n\n    if (files?.length) {\n      fileNameElClassName = '';\n      const imageFile = files.item(0)!;\n      const hookCallback: HookCallback = (url, text) =>\n        this.props.execCommand('addImage', { imageUrl: url, altText: text || altTextEl.value });\n\n      this.props.eventEmitter.emit('addImageBlobHook', imageFile, hookCallback, TYPE_UI);\n    }\n    this.setState({ fileNameElClassName });\n  }\n\n  private emitAddImage() {\n    const imageUrlEl = this.refs.url as HTMLInputElement;\n    const altTextEl = this.refs.altText as HTMLInputElement;\n    const imageUrl = imageUrlEl.value;\n    const altText = altTextEl.value || 'image';\n\n    removeClass(imageUrlEl, 'wrong');\n\n    if (!imageUrl.length) {\n      addClass(imageUrlEl, 'wrong');\n      return;\n    }\n\n    if (imageUrl) {\n      this.props.execCommand('addImage', { imageUrl, altText });\n    }\n  }\n\n  private execCommand = () => {\n    if (this.state.activeTab === 'file') {\n      this.emitAddImageBlob();\n    } else {\n      this.emitAddImage();\n    }\n  };\n\n  private toggleTab = (_: MouseEvent, activeTab: TabType) => {\n    if (activeTab !== this.state.activeTab) {\n      this.initialize(activeTab);\n    }\n  };\n\n  private showFileSelectBox = () => {\n    this.refs.file.click();\n  };\n\n  private changeFile = (ev: Event) => {\n    const { files } = ev.target as HTMLInputElement;\n\n    if (files?.length) {\n      this.setState({ file: files[0] });\n    }\n  };\n\n  private preventSelectStart(ev: Event) {\n    ev.preventDefault();\n  }\n\n  updated() {\n    if (!this.props.show) {\n      this.initialize();\n    }\n  }\n\n  render() {\n    const { activeTab, file, fileNameElClassName } = this.state;\n\n    return html`\n      <div aria-label=\"${i18n.get('Insert image')}\">\n        <${Tabs} tabs=${this.tabs} activeTab=${activeTab} onClick=${this.toggleTab} />\n        <div style=\"display:${activeTab === 'url' ? 'block' : 'none'}\">\n          <label for=\"toastuiImageUrlInput\">${i18n.get('Image URL')}</label>\n          <input\n            id=\"toastuiImageUrlInput\"\n            type=\"text\"\n            ref=${(el: HTMLInputElement) => (this.refs.url = el)}\n          />\n        </div>\n        <div style=\"display:${activeTab === 'file' ? 'block' : 'none'};position: relative;\">\n          <label for=\"toastuiImageFileInput\">${i18n.get('Select image file')}</label>\n          <span\n            class=\"${cls('file-name')}${file ? ' has-file' : fileNameElClassName}\"\n            onClick=${this.showFileSelectBox}\n            onSelectstart=${this.preventSelectStart}\n          >\n            ${file ? file.name : i18n.get('No file')}\n          </span>\n          <button\n            type=\"button\"\n            class=\"${cls('file-select-button')}\"\n            onClick=${this.showFileSelectBox}\n          >\n            ${i18n.get('Choose a file')}\n          </button>\n          <input\n            id=\"toastuiImageFileInput\"\n            type=\"file\"\n            accept=\"image/*\"\n            onChange=${this.changeFile}\n            ref=${(el: HTMLInputElement) => (this.refs.file = el)}\n          />\n        </div>\n        <label for=\"toastuiAltTextInput\">${i18n.get('Description')}</label>\n        <input\n          id=\"toastuiAltTextInput\"\n          type=\"text\"\n          ref=${(el: HTMLInputElement) => (this.refs.altText = el)}\n        />\n        <div class=\"${cls('button-container')}\">\n          <button type=\"button\" class=\"${cls('close-button')}\" onClick=${this.props.hidePopup}>\n            ${i18n.get('Cancel')}\n          </button>\n          <button type=\"button\" class=\"${cls('ok-button')}\" onClick=${this.execCommand}>\n            ${i18n.get('OK')}\n          </button>\n        </div>\n      </div>\n    `;\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/ui/components/toolbar/linkPopupBody.ts",
    "content": "import addClass from 'tui-code-snippet/domUtil/addClass';\nimport removeClass from 'tui-code-snippet/domUtil/removeClass';\nimport isUndefined from 'tui-code-snippet/type/isUndefined';\n\nimport { Emitter } from '@t/event';\nimport { ExecCommand, HidePopup, PopupInitialValues } from '@t/ui';\nimport i18n from '@/i18n/i18n';\nimport { cls } from '@/utils/dom';\nimport html from '@/ui/vdom/template';\nimport { Component } from '@/ui/vdom/component';\n\ninterface Props {\n  eventEmitter: Emitter;\n  execCommand: ExecCommand;\n  hidePopup: HidePopup;\n  show: boolean;\n  initialValues: PopupInitialValues;\n}\n\nexport class LinkPopupBody extends Component<Props> {\n  private initialize() {\n    const { linkUrl, linkText } = this.props.initialValues;\n\n    const linkUrlEl = this.refs.url as HTMLInputElement;\n    const linkTextEl = this.refs.text as HTMLInputElement;\n\n    removeClass(linkUrlEl, 'wrong');\n    removeClass(linkTextEl, 'wrong', 'disabled');\n    linkTextEl.removeAttribute('disabled');\n\n    if (linkUrl) {\n      addClass(linkTextEl, 'disabled');\n      linkTextEl.setAttribute('disabled', 'disabled');\n    }\n\n    linkUrlEl.value = linkUrl || '';\n    linkTextEl.value = linkText || '';\n  }\n\n  private execCommand = () => {\n    const linkUrlEl = this.refs.url as HTMLInputElement;\n    const linkTextEl = this.refs.text as HTMLInputElement;\n\n    removeClass(linkUrlEl, 'wrong');\n    removeClass(linkTextEl, 'wrong');\n\n    if (linkUrlEl.value.length < 1) {\n      addClass(linkUrlEl, 'wrong');\n      return;\n    }\n\n    const checkLinkText = isUndefined(this.props.initialValues.linkUrl);\n\n    if (checkLinkText && linkTextEl.value.length < 1) {\n      addClass(linkTextEl, 'wrong');\n      return;\n    }\n\n    this.props.execCommand('addLink', {\n      linkUrl: linkUrlEl.value,\n      linkText: linkTextEl.value,\n    });\n  };\n\n  mounted() {\n    this.initialize();\n  }\n\n  updated(prevProps: Props) {\n    if (!prevProps.show && this.props.show) {\n      this.initialize();\n    }\n  }\n\n  render() {\n    return html`\n      <div aria-label=\"${i18n.get('Insert link')}\">\n        <label for=\"toastuiLinkUrlInput\">${i18n.get('URL')}</label>\n        <input\n          id=\"toastuiLinkUrlInput\"\n          type=\"text\"\n          ref=${(el: HTMLInputElement) => (this.refs.url = el)}\n        />\n        <label for=\"toastuiLinkTextInput\">${i18n.get('Link text')}</label>\n        <input\n          id=\"toastuiLinkTextInput\"\n          type=\"text\"\n          ref=${(el: HTMLInputElement) => (this.refs.text = el)}\n        />\n        <div class=\"${cls('button-container')}\">\n          <button type=\"button\" class=\"${cls('close-button')}\" onClick=${this.props.hidePopup}>\n            ${i18n.get('Cancel')}\n          </button>\n          <button type=\"button\" class=\"${cls('ok-button')}\" onClick=${this.execCommand}>\n            ${i18n.get('OK')}\n          </button>\n        </div>\n      </div>\n    `;\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/ui/components/toolbar/tablePopupBody.ts",
    "content": "import { Emitter } from '@t/event';\nimport { ExecCommand, Pos } from '@t/ui';\nimport { cls } from '@/utils/dom';\nimport html from '@/ui/vdom/template';\nimport { Component } from '@/ui/vdom/component';\nimport i18n from '@/i18n/i18n';\n\ninterface Range {\n  rowIdx: number;\n  colIdx: number;\n}\n\ninterface Props {\n  eventEmitter: Emitter;\n  execCommand: ExecCommand;\n  show: boolean;\n}\n\ntype State = Range;\n\nconst CELL_WIDTH = 20;\nconst CELL_HEIGHT = 20;\nconst MIN_ROW_INDEX = 5;\nconst MAX_ROW_INDEX = 14;\nconst MIN_COL_INDEX = 5;\nconst MAX_COL_INDEX = 9;\nconst MIN_ROW_SELECTION_INDEX = 1;\nconst MIN_COL_SELECTION_INDEX = 1;\nconst BORDER_WIDTH = 1;\n\nexport class TablePopupBody extends Component<Props, State> {\n  private offsetRect!: Pos;\n\n  constructor(props: Props) {\n    super(props);\n    this.state = {\n      rowIdx: -1,\n      colIdx: -1,\n    };\n  }\n\n  private extendSelectionRange = ({ pageX, pageY }: MouseEvent) => {\n    const x = pageX - this.offsetRect.left;\n    const y = pageY - this.offsetRect.top;\n    const range = this.getSelectionRangeByOffset(x, y);\n\n    this.setState({ ...range });\n  };\n\n  private execCommand = () => {\n    this.props.execCommand('addTable', {\n      rowCount: this.state.rowIdx + 1,\n      columnCount: this.state.colIdx + 1,\n    });\n  };\n\n  private getDescription() {\n    return this.state.colIdx === -1 ? '' : `${this.state.colIdx + 1} x ${this.state.rowIdx + 1}`;\n  }\n\n  private getBoundByRange(colIdx: number, rowIdx: number) {\n    return {\n      width: (colIdx + 1) * CELL_WIDTH,\n      height: (rowIdx + 1) * CELL_HEIGHT,\n    };\n  }\n\n  private getRangeByOffset(x: number, y: number) {\n    return {\n      colIdx: Math.floor(x / CELL_WIDTH),\n      rowIdx: Math.floor(y / CELL_HEIGHT),\n    };\n  }\n\n  private getTableRange() {\n    const { colIdx: orgColIdx, rowIdx: orgRowIdx } = this.state;\n    let colIdx = Math.max(orgColIdx, MIN_COL_INDEX);\n    let rowIdx = Math.max(orgRowIdx, MIN_ROW_INDEX);\n\n    if (orgColIdx >= MIN_COL_INDEX && colIdx < MAX_COL_INDEX) {\n      colIdx += 1;\n    }\n    if (orgRowIdx >= MIN_ROW_INDEX && rowIdx < MAX_ROW_INDEX) {\n      rowIdx += 1;\n    }\n\n    return { colIdx: colIdx + 1, rowIdx: rowIdx + 1 };\n  }\n\n  private getSelectionAreaBound() {\n    const { width, height } = this.getBoundByRange(this.state.colIdx, this.state.rowIdx);\n\n    if (!width && !height) {\n      return { display: 'none' };\n    }\n\n    return { width: width - BORDER_WIDTH, height: height - BORDER_WIDTH, display: 'block' };\n  }\n\n  private getSelectionRangeByOffset(x: number, y: number) {\n    const range = this.getRangeByOffset(x, y);\n\n    range.rowIdx = Math.min(Math.max(range.rowIdx, MIN_ROW_SELECTION_INDEX), MAX_ROW_INDEX);\n    range.colIdx = Math.min(Math.max(range.colIdx, MIN_COL_SELECTION_INDEX), MAX_COL_INDEX);\n\n    return range;\n  }\n\n  updated() {\n    if (!this.props.show) {\n      this.setState({ colIdx: -1, rowIdx: -1 });\n    } else if (this.state.colIdx === -1 && this.state.rowIdx === -1) {\n      const { left, top } = this.refs.tableEl.getBoundingClientRect();\n\n      this.offsetRect = {\n        left: window.pageXOffset + left,\n        top: window.pageYOffset + top,\n      };\n    }\n  }\n\n  private createTableArea(tableRange: Range) {\n    const { colIdx, rowIdx } = tableRange;\n    const rows = [];\n\n    for (let i = 0; i < rowIdx; i += 1) {\n      const cells = [];\n\n      for (let j = 0; j < colIdx; j += 1) {\n        const cellClassNames = `${cls('table-cell')}${i > 0 ? '' : ' header'}`;\n\n        cells.push(html`<div class=\"${cellClassNames}\"></div>`);\n      }\n      rows.push(html`<div class=\"${cls('table-row')}\">${cells}</div>`);\n    }\n\n    return html`<div class=\"${cls('table')}\">${rows}</div>`;\n  }\n\n  render() {\n    const tableRange = this.getTableRange();\n    const selectionAreaBound = this.getSelectionAreaBound();\n\n    return html`\n      <div aria-label=\"${i18n.get('Insert table')}\">\n        <div\n          class=\"${cls('table-selection')}\"\n          ref=${(el: HTMLElement) => (this.refs.tableEl = el)}\n          onMousemove=${this.extendSelectionRange}\n          onClick=${this.execCommand}\n        >\n          ${this.createTableArea(tableRange)}\n          <div class=\"${cls('table-selection-layer')}\" style=${selectionAreaBound}></div>\n        </div>\n        <p class=\"${cls('table-description')}\">${this.getDescription()}</p>\n      </div>\n    `;\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/ui/components/toolbar/toolbar.ts",
    "content": "import throttle from 'tui-code-snippet/tricks/throttle';\nimport forEachArray from 'tui-code-snippet/collection/forEachArray';\nimport ResizeObserver from 'resize-observer-polyfill';\nimport { EditorType, PreviewStyle } from '@t/editor';\nimport { Emitter } from '@t/event';\nimport {\n  IndexList,\n  PopupInfo,\n  TabInfo,\n  ToolbarGroupInfo,\n  ToolbarItem,\n  ToolbarItemOptions,\n} from '@t/ui';\nimport html from '@/ui/vdom/template';\nimport { Component } from '@/ui/vdom/component';\nimport {\n  createElementWith,\n  getOuterWidth,\n  closest,\n  getTotalOffset,\n  cls,\n  removeNode,\n} from '@/utils/dom';\nimport { last } from '@/utils/common';\nimport {\n  createToolbarItemInfo,\n  toggleScrollSync,\n  groupToolbarItems,\n  setGroupState,\n  createPopupInfo,\n} from '@/ui/toolbarItemFactory';\nimport { Popup } from '../popup';\nimport { Tabs } from '../tabs';\nimport { ToolbarGroup } from './toolbarGroup';\nimport { DropdownToolbarButton } from './dropdownToolbarButton';\n\ntype TabType = 'write' | 'preview';\n\ninterface Props {\n  eventEmitter: Emitter;\n  previewStyle: PreviewStyle;\n  toolbarItems: ToolbarItem[];\n  editorType: EditorType;\n}\n\ninterface State {\n  showPopup: boolean;\n  popupInfo: PopupInfo;\n  activeTab: TabType;\n  items: ToolbarGroupInfo[];\n  dropdownItems: ToolbarGroupInfo[];\n}\n\ninterface ItemWidthMap {\n  [key: string]: number;\n}\n\nconst INLINE_PADDING = 50;\n\nexport class Toolbar extends Component<Props, State> {\n  private tabs: TabInfo[];\n\n  private itemWidthMap: ItemWidthMap;\n\n  private tooltipRef!: { current: HTMLElement | null };\n\n  private initialItems: ToolbarGroupInfo[];\n\n  private resizeObserver!: ResizeObserver;\n\n  private handleResize!: () => void;\n\n  constructor(props: Props) {\n    super(props);\n    this.tabs = [\n      { name: 'write', text: 'Write' },\n      { name: 'preview', text: 'Preview' },\n    ];\n    this.itemWidthMap = {};\n    this.initialItems = groupToolbarItems(props.toolbarItems || [], this.hiddenScrollSync());\n\n    this.state = {\n      items: this.initialItems,\n      dropdownItems: [],\n      showPopup: false,\n      popupInfo: {} as PopupInfo,\n      activeTab: 'write',\n    };\n    this.tooltipRef = { current: null };\n    this.resizeObserver = new ResizeObserver(() => this.handleResize());\n    this.addEvent();\n  }\n\n  insertToolbarItem(indexList: IndexList, item: string | ToolbarItemOptions) {\n    const { groupIndex, itemIndex } = indexList;\n    const group = this.initialItems[groupIndex];\n\n    item = createToolbarItemInfo(item);\n\n    if (group) {\n      group.splice(itemIndex, 0, item);\n    } else {\n      this.initialItems.push([item]);\n    }\n    this.setState(this.classifyToolbarItems());\n  }\n\n  removeToolbarItem(name: string) {\n    forEachArray(this.initialItems, (group) => {\n      let found = false;\n\n      forEachArray(group, (item, index) => {\n        if (item.name === name) {\n          found = true;\n          group.splice(index, 1);\n          this.setState(this.classifyToolbarItems());\n          return false;\n        }\n        return true;\n      });\n      return !found;\n    });\n  }\n\n  addEvent() {\n    const { eventEmitter } = this.props;\n\n    this.handleResize = throttle(() => {\n      // reset toolbar items to re-layout toolbar items with each clientWidth\n      this.setState({ items: this.initialItems, dropdownItems: [] });\n      this.setState(this.classifyToolbarItems());\n    }, 200);\n    eventEmitter.listen('openPopup', this.openPopup);\n  }\n\n  private appendTooltipToRoot() {\n    const tooltip = `<div class=\"${cls('tooltip')}\" style=\"display:none\">\n        <div class=\"arrow\"></div>\n        <span class=\"text\"></span>\n      </div>`;\n\n    this.tooltipRef.current = createElementWith(tooltip, this.refs.el) as HTMLElement;\n  }\n\n  private hiddenScrollSync() {\n    return this.props.editorType === 'wysiwyg' || this.props.previewStyle === 'tab';\n  }\n\n  private toggleTab = (_: MouseEvent, activeTab: TabType) => {\n    const { eventEmitter } = this.props;\n\n    if (this.state.activeTab !== activeTab) {\n      const event = activeTab === 'write' ? 'changePreviewTabWrite' : 'changePreviewTabPreview';\n\n      eventEmitter.emit(event);\n      this.setState({ activeTab });\n    }\n  };\n\n  private setItemWidth = (name: string, width: number) => {\n    this.itemWidthMap[name] = width;\n  };\n\n  private setPopupInfo = (popupInfo: PopupInfo) => {\n    this.setState({ showPopup: true, popupInfo });\n  };\n\n  private openPopup = (popupName: string, initialValues = {}) => {\n    const el = this.refs.el.querySelector<HTMLElement>(`.${cls('toolbar-group')} .${popupName}`)!;\n\n    if (el) {\n      const { offsetLeft, offsetTop } = getTotalOffset(\n        el,\n        closest(el, `.${cls('toolbar')}`) as HTMLElement\n      );\n      const info = createPopupInfo(popupName, {\n        el,\n        pos: { left: offsetLeft, top: el.offsetHeight + offsetTop },\n        initialValues,\n      });\n\n      if (info) {\n        this.setPopupInfo(info);\n      }\n    }\n  };\n\n  private hidePopup = () => {\n    if (this.state.showPopup) {\n      this.setState({ showPopup: false });\n    }\n  };\n\n  private execCommand = (command: string, payload?: Record<string, any>) => {\n    const { eventEmitter } = this.props;\n\n    eventEmitter.emit('command', command, payload);\n    this.hidePopup();\n  };\n\n  private movePrevItemToDropdownToolbar(\n    itemIndex: number,\n    items: ToolbarGroupInfo[],\n    group: ToolbarGroupInfo,\n    dropdownGroup: ToolbarGroupInfo\n  ) {\n    const moveItem = (targetGroup: ToolbarGroupInfo) => {\n      const item = targetGroup.pop();\n\n      if (item) {\n        dropdownGroup.push(item);\n      }\n    };\n\n    if (itemIndex > 1) {\n      moveItem(group);\n    } else {\n      const prevGroup = last(items);\n\n      if (prevGroup) {\n        moveItem(prevGroup);\n      }\n    }\n  }\n\n  private classifyToolbarItems() {\n    let totalWidth = 0;\n    const { clientWidth } = this.refs.el;\n    const divider = this.refs.el.querySelector<HTMLElement>(`.${cls('toolbar-divider')}`);\n    const dividerWidth = divider ? getOuterWidth(divider) : 0;\n    const items: ToolbarGroupInfo[] = [];\n    const dropdownItems: ToolbarGroupInfo[] = [];\n    let moved = false;\n\n    this.initialItems.forEach((initialGroup, groupIndex) => {\n      const group: ToolbarGroupInfo = [];\n      const dropdownGroup: ToolbarGroupInfo = [];\n\n      initialGroup.forEach((item, itemIndex) => {\n        if (!item.hidden) {\n          totalWidth += this.itemWidthMap[item.name];\n\n          if (totalWidth > clientWidth - INLINE_PADDING) {\n            // should move the prev item to dropdown toolbar for placing the more button\n            if (!moved) {\n              this.movePrevItemToDropdownToolbar(itemIndex, items, group, dropdownGroup);\n              moved = true;\n            }\n            dropdownGroup.push(item);\n          } else {\n            group.push(item);\n          }\n        }\n      });\n\n      if (group.length) {\n        setGroupState(group);\n        items.push(group);\n      }\n      if (dropdownGroup.length) {\n        setGroupState(dropdownGroup);\n        dropdownItems.push(dropdownGroup);\n      }\n      // add divider width\n      if (groupIndex < this.state.items.length - 1) {\n        totalWidth += dividerWidth;\n      }\n    });\n    return { items, dropdownItems };\n  }\n\n  mounted() {\n    if (this.props.previewStyle === 'tab') {\n      this.props.eventEmitter.emit('changePreviewTabWrite', true);\n    }\n    // classify toolbar and dropdown toolbar after DOM has been rendered\n    this.setState(this.classifyToolbarItems());\n    this.appendTooltipToRoot();\n    this.resizeObserver.observe(this.refs.el);\n  }\n\n  updated(prevProps: Props) {\n    const { editorType, previewStyle, eventEmitter } = this.props;\n    const changedStyle = previewStyle !== prevProps.previewStyle;\n    const changedType = editorType !== prevProps.editorType;\n\n    if (changedStyle || changedType) {\n      // show or hide scrollSync button\n      toggleScrollSync(this.initialItems, this.hiddenScrollSync());\n      const newState = this.classifyToolbarItems() as State;\n\n      if (changedStyle || (previewStyle === 'tab' && editorType === 'markdown')) {\n        eventEmitter.emit('changePreviewTabWrite');\n        newState.activeTab = 'write';\n      }\n      this.setState(newState);\n    }\n  }\n\n  beforeDestroy() {\n    window.removeEventListener('resize', this.handleResize);\n    this.resizeObserver.disconnect();\n    removeNode(this.tooltipRef.current!);\n  }\n\n  render() {\n    const { previewStyle, eventEmitter, editorType } = this.props;\n    const { popupInfo, showPopup, activeTab, items, dropdownItems } = this.state;\n    const props = {\n      eventEmitter,\n      tooltipRef: this.tooltipRef,\n      disabled: editorType === 'markdown' && previewStyle === 'tab' && activeTab === 'preview',\n      execCommand: this.execCommand,\n      setPopupInfo: this.setPopupInfo,\n    };\n    const toolbarStyle = previewStyle === 'tab' ? { borderTopLeftRadius: 0 } : null;\n\n    return html`\n      <div class=\"${cls('toolbar')}\">\n        <div\n          class=\"${cls('md-tab-container')}\"\n          style=\"display: ${editorType === 'wysiwyg' || previewStyle === 'vertical'\n            ? 'none'\n            : 'block'}\"\n        >\n          <${Tabs} tabs=${this.tabs} activeTab=${activeTab} onClick=${this.toggleTab} />\n        </div>\n        <div\n          class=\"${cls('defaultUI-toolbar')}\"\n          ref=${(el: HTMLElement) => (this.refs.el = el)}\n          style=${toolbarStyle}\n        >\n          ${items.map(\n            (group, index) => html`\n              <${ToolbarGroup}\n                group=${group}\n                hiddenDivider=${index === items.length - 1 || items[index + 1]?.hidden}\n                setItemWidth=${this.setItemWidth}\n                ...${props}\n              />\n            `\n          )}\n          <${DropdownToolbarButton}\n            item=${createToolbarItemInfo('more')}\n            items=${dropdownItems}\n            ...${props}\n          />\n        </div>\n        <${Popup}\n          info=${popupInfo}\n          show=${showPopup}\n          eventEmitter=${eventEmitter}\n          hidePopup=${this.hidePopup}\n          execCommand=${this.execCommand}\n        />\n      </div>\n    `;\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/ui/components/toolbar/toolbarButton.ts",
    "content": "import {\n  ExecCommand,\n  SetPopupInfo,\n  SetItemWidth,\n  GetBound,\n  HideTooltip,\n  ShowTooltip,\n  ToolbarButtonInfo,\n} from '@t/ui';\nimport { Emitter } from '@t/event';\nimport html from '@/ui/vdom/template';\nimport { Component } from '@/ui/vdom/component';\nimport { createPopupInfo } from '@/ui/toolbarItemFactory';\nimport { getOuterWidth } from '@/utils/dom';\nimport { connectHOC } from './buttonHoc';\n\ninterface Props {\n  disabled: boolean;\n  eventEmitter: Emitter;\n  item: ToolbarButtonInfo;\n  active: boolean;\n  execCommand: ExecCommand;\n  setPopupInfo: SetPopupInfo;\n  showTooltip: ShowTooltip;\n  hideTooltip: HideTooltip;\n  getBound: GetBound;\n  setItemWidth?: SetItemWidth;\n}\n\nconst DEFAULT_WIDTH = 80;\n\nexport class ToolbarButtonComp extends Component<Props> {\n  mounted() {\n    this.setItemWidth();\n  }\n\n  updated(prevProps: Props) {\n    if (prevProps.item.name !== this.props.item.name) {\n      this.setItemWidth();\n    }\n  }\n\n  private setItemWidth() {\n    const { setItemWidth, item } = this.props;\n\n    // set width only if it is not a dropdown toolbar\n    if (setItemWidth) {\n      setItemWidth(item.name, getOuterWidth(this.refs.el) + (item.hidden ? DEFAULT_WIDTH : 0));\n    }\n  }\n\n  private showTooltip = () => {\n    this.props.showTooltip(this.refs.el);\n  };\n\n  private execCommand = () => {\n    const { item, execCommand, setPopupInfo, getBound, eventEmitter } = this.props;\n    const { command, name, popup } = item;\n\n    if (command) {\n      execCommand(command);\n    } else {\n      const popupName = popup ? 'customPopupBody' : name;\n      const [initialValues] = eventEmitter.emit('query', 'getPopupInitialValues', { popupName });\n\n      const info = createPopupInfo(popupName, {\n        el: this.refs.el,\n        pos: getBound(this.refs.el),\n        popup,\n        initialValues,\n      });\n\n      if (info) {\n        setPopupInfo(info);\n      }\n    }\n  };\n\n  render() {\n    const { hideTooltip, disabled, item, active } = this.props;\n    const style = { display: item.hidden ? 'none' : null, ...item.style };\n    const classNames = `${item.className || ''}${active ? ' active' : ''}`;\n\n    return html`\n      <button\n        ref=${(el: HTMLElement) => (this.refs.el = el)}\n        type=\"button\"\n        style=${style}\n        class=${classNames}\n        onClick=${this.execCommand}\n        onMouseover=${this.showTooltip}\n        onMouseout=${hideTooltip}\n        disabled=${!!disabled}\n        aria-label=${item.text || item.tooltip || ''}\n      >\n        ${item.text || ''}\n      </button>\n    `;\n  }\n}\nexport const ToolbarButton = connectHOC(ToolbarButtonComp);\n"
  },
  {
    "path": "apps/editor/src/ui/components/toolbar/toolbarGroup.ts",
    "content": "import {\n  ExecCommand,\n  SetPopupInfo,\n  ToolbarGroupInfo,\n  SetItemWidth,\n  GetBound,\n  HideTooltip,\n  ShowTooltip,\n  ToolbarCustomOptions,\n} from '@t/ui';\nimport { Emitter } from '@t/event';\nimport { cls } from '@/utils/dom';\nimport html from '@/ui/vdom/template';\nimport { Component } from '@/ui/vdom/component';\nimport { ToolbarButton } from './toolbarButton';\nimport { CustomToolbarItem } from './customToolbarItem';\n\ninterface Props {\n  tooltipEl: HTMLElement;\n  disabled: boolean;\n  group: ToolbarGroupInfo;\n  hidden: boolean;\n  hiddenDivider: boolean;\n  eventEmitter: Emitter;\n  execCommand: ExecCommand;\n  setPopupInfo: SetPopupInfo;\n  showTooltip: ShowTooltip;\n  hideTooltip: HideTooltip;\n  getBound: GetBound;\n  setItemWidth?: SetItemWidth;\n}\n\nexport class ToolbarGroup extends Component<Props> {\n  render() {\n    const { group, hiddenDivider } = this.props;\n    const groupStyle = group.hidden ? { display: 'none' } : null;\n    const dividerStyle = hiddenDivider ? { display: 'none' } : null;\n\n    return html`\n      <div class=\"${cls('toolbar-group')}\" style=${groupStyle}>\n        ${group.map((item: ToolbarCustomOptions) => {\n          const Comp = item.el ? CustomToolbarItem : ToolbarButton;\n\n          return html`<${Comp} key=${item.name} ...${this.props} item=${item} />`;\n        })}\n        <div class=\"${cls('toolbar-divider')}\" style=${dividerStyle}></div>\n      </div>\n    `;\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/ui/toolbarItemFactory.ts",
    "content": "import isString from 'tui-code-snippet/type/isString';\nimport addClass from 'tui-code-snippet/domUtil/addClass';\nimport removeClass from 'tui-code-snippet/domUtil/removeClass';\nimport {\n  PopupInfo,\n  PopupOptions,\n  Pos,\n  ToolbarButtonInfo,\n  ToolbarGroupInfo,\n  ToolbarItem,\n  ToolbarItemInfo,\n  ToolbarItemOptions,\n  PopupInitialValues,\n  ToolbarCustomOptions,\n  ExecCommand,\n} from '@t/ui';\nimport i18n from '@/i18n/i18n';\nimport { cls } from '@/utils/dom';\nimport html from './vdom/template';\nimport { HeadingPopupBody } from './components/toolbar/headingPopupBody';\nimport { ImagePopupBody } from './components/toolbar/imagePopupBody';\nimport { LinkPopupBody } from './components/toolbar/linkPopupBody';\nimport { TablePopupBody } from './components/toolbar/tablePopupBody';\nimport { CustomPopupBody } from './components/toolbar/customPopupBody';\n\nexport function createToolbarItemInfo(type: string | ToolbarItemOptions): ToolbarItemInfo {\n  return isString(type) ? createDefaultToolbarItemInfo(type) : type;\n}\n\nfunction createScrollSyncToolbarItem(): ToolbarItemInfo {\n  const label = document.createElement('label');\n  const checkbox = document.createElement('input');\n  const toggleSwitch = document.createElement('span');\n\n  label.className = 'scroll-sync active';\n  checkbox.type = 'checkbox';\n  checkbox.checked = true;\n  toggleSwitch.className = 'switch';\n\n  const onMounted = (execCommand: ExecCommand) =>\n    checkbox.addEventListener('change', (ev: Event) => {\n      const { checked } = ev.target as HTMLInputElement;\n\n      if (checked) {\n        addClass(label, 'active');\n      } else {\n        removeClass(label, 'active');\n      }\n      execCommand('toggleScrollSync', { active: checked });\n    });\n\n  label.appendChild(checkbox);\n  label.appendChild(toggleSwitch);\n\n  return {\n    name: 'scrollSync',\n    el: label,\n    onMounted,\n  };\n}\n\nfunction createDefaultToolbarItemInfo(type: string) {\n  let info!: ToolbarButtonInfo | ToolbarCustomOptions;\n\n  switch (type) {\n    case 'heading':\n      info = {\n        name: 'heading',\n        className: 'heading',\n        tooltip: i18n.get('Headings'),\n        state: 'heading',\n      };\n      break;\n    case 'bold':\n      info = {\n        name: 'bold',\n        className: 'bold',\n        command: 'bold',\n        tooltip: i18n.get('Bold'),\n        state: 'strong',\n      };\n      break;\n    case 'italic':\n      info = {\n        name: 'italic',\n        className: 'italic',\n        command: 'italic',\n        tooltip: i18n.get('Italic'),\n        state: 'emph',\n      };\n      break;\n    case 'strike':\n      info = {\n        name: 'strike',\n        className: 'strike',\n        command: 'strike',\n        tooltip: i18n.get('Strike'),\n        state: 'strike',\n      };\n      break;\n    case 'hr':\n      info = {\n        name: 'hr',\n        className: 'hrline',\n        command: 'hr',\n        tooltip: i18n.get('Line'),\n        state: 'thematicBreak',\n      };\n      break;\n    case 'quote':\n      info = {\n        name: 'quote',\n        className: 'quote',\n        command: 'blockQuote',\n        tooltip: i18n.get('Blockquote'),\n        state: 'blockQuote',\n      };\n      break;\n    case 'ul':\n      info = {\n        name: 'ul',\n        className: 'bullet-list',\n        command: 'bulletList',\n        tooltip: i18n.get('Unordered list'),\n        state: 'bulletList',\n      };\n      break;\n    case 'ol':\n      info = {\n        name: 'ol',\n        className: 'ordered-list',\n        command: 'orderedList',\n        tooltip: i18n.get('Ordered list'),\n        state: 'orderedList',\n      };\n      break;\n    case 'task':\n      info = {\n        name: 'task',\n        className: 'task-list',\n        command: 'taskList',\n        tooltip: i18n.get('Task'),\n        state: 'taskList',\n      };\n      break;\n    case 'table':\n      info = {\n        name: 'table',\n        className: 'table',\n        tooltip: i18n.get('Insert table'),\n        state: 'table',\n      };\n      break;\n\n    case 'image':\n      info = {\n        name: 'image',\n        className: 'image',\n        tooltip: i18n.get('Insert image'),\n      };\n      break;\n    case 'link':\n      info = {\n        name: 'link',\n        className: 'link',\n        tooltip: i18n.get('Insert link'),\n      };\n      break;\n    case 'code':\n      info = {\n        name: 'code',\n        className: 'code',\n        command: 'code',\n        tooltip: i18n.get('Code'),\n        state: 'code',\n      };\n      break;\n    case 'codeblock':\n      info = {\n        name: 'codeblock',\n        className: 'codeblock',\n        command: 'codeBlock',\n        tooltip: i18n.get('Insert CodeBlock'),\n        state: 'codeBlock',\n      };\n      break;\n    case 'indent':\n      info = {\n        name: 'indent',\n        className: 'indent',\n        command: 'indent',\n        tooltip: i18n.get('Indent'),\n        state: 'indent',\n      };\n      break;\n    case 'outdent':\n      info = {\n        name: 'outdent',\n        className: 'outdent',\n        command: 'outdent',\n        tooltip: i18n.get('Outdent'),\n        state: 'outdent',\n      };\n      break;\n    case 'scrollSync':\n      info = createScrollSyncToolbarItem();\n      break;\n    case 'more':\n      info = {\n        name: 'more',\n        className: 'more',\n        tooltip: i18n.get('More'),\n      };\n      break;\n\n    default:\n    // do nothing\n  }\n\n  if (info.name !== 'scrollSync') {\n    (info as ToolbarButtonInfo).className += ` ${cls('toolbar-icons')}`;\n  }\n\n  return info;\n}\n\ninterface Payload {\n  el: HTMLElement;\n  pos: Pos;\n  popup?: PopupOptions;\n  initialValues?: PopupInitialValues;\n}\n\nexport function createPopupInfo(type: string, payload: Payload): PopupInfo | null {\n  const { el, pos, popup, initialValues } = payload;\n\n  switch (type) {\n    case 'heading':\n      return {\n        render: (props) => html`<${HeadingPopupBody} ...${props} />`,\n        className: cls('popup-add-heading'),\n        fromEl: el,\n        pos,\n      };\n    case 'link':\n      return {\n        render: (props) => html`<${LinkPopupBody} ...${props} />`,\n        className: cls('popup-add-link'),\n        fromEl: el,\n        pos,\n        initialValues,\n      };\n    case 'image':\n      return {\n        render: (props) => html`<${ImagePopupBody} ...${props} />`,\n        className: cls('popup-add-image'),\n        fromEl: el,\n        pos,\n      };\n    case 'table':\n      return {\n        render: (props) => html`<${TablePopupBody} ...${props} />`,\n        className: cls('popup-add-table'),\n        fromEl: el,\n        pos,\n      };\n    case 'customPopupBody':\n      if (!popup) {\n        return null;\n      }\n      return {\n        render: (props) => html`<${CustomPopupBody} ...${props} body=${popup!.body} />`,\n        fromEl: el,\n        pos,\n        ...popup!,\n      };\n    default:\n      return null;\n  }\n}\n\nexport function setGroupState(group: ToolbarGroupInfo) {\n  group.hidden = group.length === group.filter((info: ToolbarButtonInfo) => info.hidden).length;\n}\n\nexport function groupToolbarItems(toolbarItems: ToolbarItem[], hiddenScrollSync: boolean) {\n  const toggleScrollSyncState = (item: ToolbarButtonInfo) => {\n    item.hidden = item.name === 'scrollSync' && hiddenScrollSync;\n    return item;\n  };\n\n  return toolbarItems.reduce((acc: ToolbarGroupInfo[], item) => {\n    acc.push(item.map((type) => toggleScrollSyncState(createToolbarItemInfo(type))));\n    const group = acc[(acc.length || 1) - 1];\n\n    if (group) {\n      setGroupState(group);\n    }\n    return acc;\n  }, []);\n}\n\nexport function toggleScrollSync(toolbarItems: ToolbarGroupInfo[], hiddenScrollSync: boolean) {\n  toolbarItems.forEach((group) => {\n    group.forEach(\n      (item: ToolbarButtonInfo) => (item.hidden = item.name === 'scrollSync' && hiddenScrollSync)\n    );\n    setGroupState(group);\n  });\n}\n"
  },
  {
    "path": "apps/editor/src/ui/vdom/commit.ts",
    "content": "import isFunction from 'tui-code-snippet/type/isFunction';\nimport { innerDiff, removeNode } from './dom';\nimport { VNode } from './vnode';\n\nexport function commit(vnode?: VNode) {\n  VNode.removalNodes.forEach((removalNode) => diff(removalNode));\n\n  if (vnode) {\n    let next;\n    const walker = vnode.walker();\n\n    while ((next = walker.walk())) {\n      vnode = next.vnode!;\n      if (next.entering) {\n        diff(vnode);\n      } else if (isFunction(vnode.type)) {\n        const comp = vnode.component!;\n\n        // lifecycle method\n        if (!vnode.old && comp.mounted) {\n          comp.mounted();\n        }\n        if (vnode.old && comp.updated) {\n          const prevProps = comp.prevProps || {};\n\n          comp.updated(prevProps);\n        }\n      }\n    }\n  }\n}\n\nfunction getParentNode(vnode: VNode) {\n  let { parent } = vnode;\n\n  while (!parent!.node) {\n    parent = parent!.parent!;\n  }\n\n  return parent!.node;\n}\n\nfunction diff(vnode: VNode | null) {\n  if (!vnode || !vnode.parent) {\n    return;\n  }\n\n  if (vnode.node) {\n    const parentNode = getParentNode(vnode);\n\n    if (vnode.effect === 'A') {\n      parentNode.appendChild(vnode.node);\n    } else if (vnode.effect === 'U') {\n      innerDiff(vnode.node!, vnode.old!.props, vnode.props);\n    }\n  }\n\n  if (vnode.effect === 'D') {\n    let next;\n    const walker = vnode.walker();\n\n    while ((next = walker.walk())) {\n      vnode = next.vnode!;\n      if (!next.entering) {\n        if (isFunction(vnode.type)) {\n          const comp = vnode.component!;\n\n          // lifecycle method\n          if (comp.beforeDestroy) {\n            comp.beforeDestroy();\n          }\n        } else {\n          const parentNode = getParentNode(vnode);\n\n          removeNode(vnode, parentNode);\n        }\n      }\n    }\n  }\n\n  // apply ref\n  if (vnode.ref) {\n    if (vnode.component) {\n      vnode.ref(vnode.component);\n    } else if (vnode.node) {\n      vnode.ref(vnode.node);\n    }\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/ui/vdom/component.ts",
    "content": "import { Component as IComponent, VNode } from '@t/ui';\nimport { shallowEqual } from '@/utils/common';\nimport { rerender } from './renderer';\n\nexport abstract class Component<T = {}, R = {}> implements IComponent<T, R> {\n  props: T;\n\n  state: R;\n\n  refs: Record<string, HTMLElement>;\n\n  vnode!: VNode;\n\n  constructor(props: T) {\n    this.props = props;\n    this.state = {} as R;\n    this.refs = {};\n  }\n\n  setState(state: Partial<R>) {\n    const newState = { ...this.state, ...state };\n\n    if (!shallowEqual(this.state, newState)) {\n      this.state = newState;\n      rerender(this);\n    }\n  }\n\n  abstract render(): VNode;\n}\n"
  },
  {
    "path": "apps/editor/src/ui/vdom/dom.ts",
    "content": "import isObject from 'tui-code-snippet/type/isObject';\nimport isNumber from 'tui-code-snippet/type/isNumber';\nimport { shallowEqual } from '@/utils/common';\nimport { isTextNode } from '@/utils/dom';\nimport { VNode } from './vnode';\n\ntype ConditionFn = (propName: string) => boolean;\ntype Props = Record<string, any>;\n\n// @TODO: clearfy the type definition for CSSDeclaration\n\nexport function createNode(vnode: VNode) {\n  let node: Node;\n\n  if (vnode.type === 'TEXT_NODE') {\n    node = document.createTextNode(vnode.props.nodeValue);\n  } else {\n    node = document.createElement(vnode.type as string);\n    setProps(node, {}, vnode.props);\n  }\n\n  return node;\n}\n\nexport function removeNode(vnode: VNode, parentNode: Node) {\n  if (vnode.node) {\n    parentNode.removeChild(vnode.node);\n  } else {\n    removeNode(vnode.firstChild!, parentNode);\n  }\n}\n\nexport function innerDiff(node: Node, prevProps: Props, nextProps: Props) {\n  Object.keys(prevProps).forEach((propName) => {\n    if (/^on/.test(propName)) {\n      if (!nextProps[propName] || prevProps[propName] !== nextProps[propName]) {\n        const eventName = propName.slice(2).toLowerCase();\n\n        node.removeEventListener(eventName, prevProps[propName]);\n      }\n    } else if (propName !== 'children' && !nextProps[propName] && !isTextNode(node)) {\n      (node as Element).removeAttribute(propName);\n    }\n  });\n\n  setProps(\n    node,\n    prevProps,\n    nextProps,\n    (propName) => !shallowEqual(prevProps[propName], nextProps[propName])\n  );\n}\n\nconst reNonDimension = /acit|ex(?:s|g|n|p|$)|rph|ows|mnc|ntw|ine[ch]|zoo|^ord/i;\n\nfunction setProps(node: Node, prevProps: Props, props: Props, condition?: ConditionFn) {\n  Object.keys(props).forEach((propName) => {\n    if (!condition || condition(propName)) {\n      if (/^on/.test(propName)) {\n        const eventName = propName.slice(2).toLowerCase();\n\n        node.addEventListener(eventName, props[propName]);\n      } else if (propName === 'nodeValue') {\n        node[propName] = props[propName];\n      } else if (propName === 'style' && isObject(props[propName])) {\n        setStyleProps(node as HTMLElement, prevProps[propName], props[propName]);\n      } else if (propName !== 'children') {\n        if (props[propName] === false) {\n          (node as HTMLElement).removeAttribute(propName);\n        } else {\n          (node as HTMLElement).setAttribute(propName, props[propName]);\n        }\n      }\n    }\n  });\n}\nfunction setStyleProps(node: HTMLElement, prevStyleProps: Props | null, styleProps: Props) {\n  if (prevStyleProps) {\n    Object.keys(prevStyleProps).forEach((styleProp) => {\n      // @ts-ignore\n      node.style[styleProp] = '';\n    });\n  }\n\n  Object.keys(styleProps).forEach((styleProp) => {\n    const value = styleProps[styleProp];\n\n    // @ts-ignore\n    node.style[styleProp] =\n      isNumber(value) && !reNonDimension.test(styleProp) ? `${value}px` : value;\n  });\n}\n"
  },
  {
    "path": "apps/editor/src/ui/vdom/htm.js",
    "content": "import { assign } from '@/utils/common';\n\n// @TODO: change syntax with our convention\n/* eslint-disable */\nexport default function (n) {\n  for (\n    var l,\n      e,\n      s = arguments,\n      t = 1,\n      r = '',\n      u = '',\n      a = [0],\n      c = function (n) {\n        t === 1 && (n || (r = r.replace(/^\\s*\\n\\s*|\\s*\\n\\s*$/g, '')))\n          ? a.push(n ? s[n] : r)\n          : t === 3 && (n || r)\n          ? ((a[1] = n ? s[n] : r), (t = 2))\n          : t === 2 && r === '...' && n\n          ? (a[2] = assign(a[2] || {}, s[n]))\n          : t === 2 && r && !n\n          ? ((a[2] = a[2] || {})[r] = !0)\n          : t >= 5 &&\n            (t === 5\n              ? (((a[2] = a[2] || {})[e] = n ? (r ? r + s[n] : s[n]) : r), (t = 6))\n              : (n || r) && (a[2][e] += n ? r + s[n] : r)),\n          (r = '');\n      },\n      h = 0;\n    h < n.length;\n    h++\n  ) {\n    h && (t === 1 && c(), c(h));\n    for (let i = 0; i < n[h].length; i++)\n      (l = n[h][i]),\n        t === 1\n          ? l === '<'\n            ? (c(), (a = [a, '', null]), (t = 3))\n            : (r += l)\n          : t === 4\n          ? r === '--' && l === '>'\n            ? ((t = 1), (r = ''))\n            : (r = l + r[0])\n          : u\n          ? l === u\n            ? (u = '')\n            : (r += l)\n          : l === '\"' || l === \"'\"\n          ? (u = l)\n          : l === '>'\n          ? (c(), (t = 1))\n          : t &&\n            (l === '='\n              ? ((t = 5), (e = r), (r = ''))\n              : l === '/' && (t < 5 || n[h][i + 1] === '>')\n              ? (c(),\n                t === 3 && (a = a[0]),\n                (t = a),\n                (a = a[0]).push(this.apply(null, t.slice(1))),\n                (t = 0))\n              : l === ' ' || l === '\\t' || l === '\\n' || l === '\\r'\n              ? (c(), (t = 2))\n              : (r += l)),\n        t === 3 && r === '!--' && ((t = 4), (a = a[0]));\n  }\n  return c(), a.length > 2 ? a.slice(1) : a[1];\n}\n"
  },
  {
    "path": "apps/editor/src/ui/vdom/render.ts",
    "content": "import isFunction from 'tui-code-snippet/type/isFunction';\nimport { ComponentClass } from '@t/ui';\nimport { VNode } from './vnode';\nimport { createNode } from './dom';\nimport { last } from '@/utils/common';\n\nexport function createComponent(Comp: ComponentClass, vnode: VNode) {\n  const { props, component } = vnode;\n\n  if (component) {\n    component.prevProps = component.props;\n    component.props = vnode.props;\n    return component;\n  }\n\n  return new Comp(props);\n}\n\nexport function buildVNode(vnode: VNode | null) {\n  const root = vnode;\n\n  while (vnode && !vnode.skip) {\n    if (isFunction(vnode.type)) {\n      const instance = createComponent(vnode.type, vnode);\n\n      instance.vnode = vnode;\n      vnode.component = instance;\n      vnode.props.children = vnode.children = [instance.render()];\n      buildChildrenVNode(vnode);\n    } else {\n      if (!vnode.node) {\n        vnode.node = createNode(vnode);\n      }\n      buildChildrenVNode(vnode);\n    }\n\n    if (vnode.firstChild) {\n      vnode = vnode.firstChild;\n    } else {\n      while (vnode && vnode.parent && !vnode.next) {\n        vnode = vnode.parent!;\n        if (vnode === root) {\n          break;\n        }\n      }\n      vnode = vnode.next;\n    }\n  }\n}\n\nfunction isSameType(old: VNode | null, vnode: VNode) {\n  return old && vnode && vnode.type === old.type && (!vnode.key || vnode.key === old.key);\n}\n\n// @TODO: add key diff algorithm\nfunction buildChildrenVNode(parent: VNode) {\n  const { children } = parent;\n  let old = parent.old ? parent.old.firstChild : null;\n  let prev: VNode | null = null;\n\n  children.forEach((vnode, index) => {\n    const sameType = isSameType(old, vnode);\n\n    if (sameType) {\n      vnode.old = old!;\n      vnode.parent = parent;\n      vnode.node = old!.node;\n      vnode.component = old!.component;\n      vnode.effect = 'U';\n    }\n\n    if (vnode && !sameType) {\n      vnode.old = null;\n      vnode.parent = parent;\n      vnode.node = null;\n      vnode.effect = 'A';\n    }\n\n    if (old && !sameType) {\n      VNode.removalNodes.push(old);\n      old.effect = 'D';\n    }\n\n    if (old) {\n      old = old.next;\n    }\n\n    if (index === 0) {\n      parent.firstChild = vnode;\n    } else if (vnode) {\n      prev!.next = vnode;\n    }\n\n    prev = vnode;\n  });\n\n  const lastChild = last(children);\n\n  if (!children.length) {\n    while (old) {\n      VNode.removalNodes.push(old);\n      old.effect = 'D';\n      old = old.next;\n    }\n  }\n\n  while (old && lastChild) {\n    if (old && lastChild.old !== old) {\n      VNode.removalNodes.push(old);\n      old.effect = 'D';\n      old = old.next;\n    }\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/ui/vdom/renderer.ts",
    "content": "import { Component } from '@t/ui';\nimport { commit } from './commit';\nimport { buildVNode } from './render';\nimport { VNode } from './vnode';\n\nfunction destroy(vnode: VNode) {\n  vnode.effect = 'D';\n  VNode.removalNodes = [vnode];\n  commit();\n  VNode.removalNodes = [];\n}\n\nexport function rerender(comp: Component) {\n  const root = comp.vnode;\n\n  root.effect = 'U';\n  root.old = root;\n\n  // skip for unnecessary reconciliation\n  if (root.next) {\n    root.next.skip = true;\n  }\n  VNode.removalNodes = [];\n\n  buildVNode(root);\n  commit(root);\n\n  if (root.next) {\n    root.next.skip = false;\n  }\n}\n\nexport function render(container: HTMLElement, vnode: VNode) {\n  const root = new VNode(container.tagName.toLowerCase(), {}, [vnode]);\n\n  root.node = container;\n  VNode.removalNodes = [];\n\n  buildVNode(root);\n  commit(root);\n\n  return () => destroy(root.firstChild!);\n}\n"
  },
  {
    "path": "apps/editor/src/ui/vdom/template.ts",
    "content": "import html from './htm';\nimport isBoolean from 'tui-code-snippet/type/isBoolean';\nimport isString from 'tui-code-snippet/type/isString';\nimport isNumber from 'tui-code-snippet/type/isNumber';\nimport { ComponentClass } from '@t/ui';\nimport { VNode } from './vnode';\n\nfunction createTextNode(text: string) {\n  return new VNode('TEXT_NODE', { nodeValue: text }, []);\n}\n\nfunction excludeUnnecessaryChild(child: VNode, flatted: VNode[]) {\n  let vnode: VNode | null = child;\n\n  // eslint-disable-next-line no-eq-null,eqeqeq\n  if (isBoolean(child) || child == null) {\n    vnode = null;\n  } else if (isString(child) || isNumber(child)) {\n    vnode = createTextNode(String(child));\n  }\n  if (vnode) {\n    flatted.push(vnode);\n  }\n}\n\nfunction h(type: string | ComponentClass, props: Record<string, any>, ...children: VNode[]) {\n  const flatted: VNode[] = [];\n\n  children.forEach((child) => {\n    if (Array.isArray(child)) {\n      child.forEach((vnode) => {\n        excludeUnnecessaryChild(vnode, flatted);\n      });\n    } else {\n      excludeUnnecessaryChild(child, flatted);\n    }\n  });\n\n  return new VNode(type, props || {}, flatted);\n}\n\n// @ts-ignore\nexport default html.bind(h) as (strings: TemplateStringsArray, ...values: any[]) => VNode;\n"
  },
  {
    "path": "apps/editor/src/ui/vdom/vnode.ts",
    "content": "import { Component, ComponentClass } from '@t/ui';\n\nclass VNodeWalker {\n  current: VNode | null;\n\n  root: VNode | null;\n\n  entering: boolean;\n\n  constructor(current: VNode | null) {\n    this.current = current;\n    this.root = current;\n    this.entering = true;\n  }\n\n  walk() {\n    const { entering, current: cur } = this;\n\n    if (!cur) {\n      return null;\n    }\n\n    if (entering) {\n      if (cur.firstChild) {\n        this.current = cur.firstChild;\n        this.entering = true;\n      } else {\n        this.entering = false;\n      }\n    } else if (cur === this.root) {\n      this.current = null;\n    } else if (cur.next) {\n      this.current = cur.next;\n      this.entering = true;\n    } else {\n      this.current = cur.parent;\n      this.entering = false;\n    }\n\n    return { vnode: cur, entering };\n  }\n}\n\nexport class VNode {\n  static removalNodes: VNode[] = [];\n\n  type: string | ComponentClass;\n\n  props: Record<string, any>;\n\n  children: VNode[];\n\n  parent: VNode | null = null;\n\n  old: VNode | null = null;\n\n  firstChild: VNode | null = null;\n\n  next: VNode | null = null;\n\n  ref?: (node: Node | Component) => void | Node | Component;\n\n  node!: Node | null;\n\n  // A: append, U: update, D: delete\n  effect!: 'A' | 'U' | 'D';\n\n  component?: Component;\n\n  key?: string;\n\n  skip = false;\n\n  constructor(type: string | ComponentClass, props: Record<string, any>, children: VNode[]) {\n    this.type = type;\n    this.props = props;\n    this.children = children;\n    this.props.children = children;\n    if (props.ref) {\n      this.ref = props.ref;\n      delete props.ref;\n    }\n    if (props.key) {\n      this.key = props.key;\n      delete props.key;\n    }\n  }\n\n  walker() {\n    return new VNodeWalker(this);\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/utils/common.ts",
    "content": "import isUndefined from 'tui-code-snippet/type/isUndefined';\nimport isNull from 'tui-code-snippet/type/isNull';\nimport sendHostname from 'tui-code-snippet/request/sendHostname';\nimport forEachOwnProperties from 'tui-code-snippet/collection/forEachOwnProperties';\n\nimport { LinkAttributeNames, LinkAttributes } from '@t/editor';\n\nexport const isMac = /Mac/.test(navigator.platform);\nconst reSpaceMoreThanOne = /[\\u0020]+/g;\nconst reEscapeChars = /[>(){}[\\]+-.!#|]/g;\nconst reEscapeHTML = /<([a-zA-Z_][a-zA-Z0-9\\-._]*)(\\s|[^\\\\>])*\\/?>|<(\\/)([a-zA-Z_][a-zA-Z0-9\\-._]*)\\s*\\/?>|<!--[^-]+-->|<([a-zA-Z_][a-zA-Z0-9\\-.:/]*)>/g;\nconst reEscapeBackSlash = /\\\\[!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~\\\\]/g;\nconst reEscapePairedChars = /[*_~`]/g;\nconst reMdImageSyntax = /!\\[.*\\]\\(.*\\)/g;\nconst reEscapedCharInLinkSyntax = /[[\\]]/g;\nconst reEscapeBackSlashInSentence = /(?:^|[^\\\\])\\\\(?!\\\\)/g;\n\nconst XMLSPECIAL = '[&<>\"]';\nconst reXmlSpecial = new RegExp(XMLSPECIAL, 'g');\n\nfunction replaceUnsafeChar(char: string) {\n  switch (char) {\n    case '&':\n      return '&amp;';\n    case '<':\n      return '&lt;';\n    case '>':\n      return '&gt;';\n    case '\"':\n      return '&quot;';\n    default:\n      return char;\n  }\n}\n\nexport function escapeXml(text: string) {\n  if (reXmlSpecial.test(text)) {\n    return text.replace(reXmlSpecial, replaceUnsafeChar);\n  }\n  return text;\n}\n\nexport function sendHostName() {\n  sendHostname('editor', 'UA-129966929-1');\n}\n\nexport function includes<T>(arr: T[], targetItem: T) {\n  return arr.indexOf(targetItem) !== -1;\n}\n\nconst availableLinkAttributes: LinkAttributeNames[] = ['rel', 'target', 'hreflang', 'type'];\nconst reMarkdownTextToEscapeMap = {\n  codeblock: /(^ {4}[^\\n]+\\n*)+/,\n  thematicBreak: /^ *((\\* *){3,}|(- *){3,} *|(_ *){3,}) */,\n  atxHeading: /^(#{1,6}) +[\\s\\S]+/,\n  seTextheading: /^([^\\n]+)\\n *(=|-){2,} */,\n  blockquote: /^( *>[^\\n]+.*)+/,\n  list: /^ *(\\*+|-+|\\d+\\.) [\\s\\S]+/,\n  def: /^ *\\[([^\\]]+)\\]: *<?([^\\s>]+)>?(?: +[\"(]([^\\n]+)[\")])? */,\n  link: /!?\\[.*\\]\\(.*\\)/,\n  reflink: /!?\\[.*\\]\\s*\\[([^\\]]*)\\]/,\n  verticalBar: /\\u007C/,\n  fencedCodeblock: /^((`|~){3,})/,\n};\n\nexport function sanitizeLinkAttribute(attribute: LinkAttributes) {\n  if (!attribute) {\n    return null;\n  }\n\n  const linkAttributes: LinkAttributes = {};\n\n  availableLinkAttributes.forEach((key) => {\n    if (!isUndefined(attribute[key])) {\n      linkAttributes[key] = attribute[key];\n    }\n  });\n\n  return linkAttributes;\n}\n\nexport function repeat(text: string, count: number) {\n  let result = '';\n\n  for (let i = 0; i < count; i += 1) {\n    result += text;\n  }\n\n  return result;\n}\n\nfunction isNeedEscapeText(text: string) {\n  let needEscape = false;\n\n  forEachOwnProperties(reMarkdownTextToEscapeMap, (reMarkdownTextToEscape) => {\n    if (reMarkdownTextToEscape.test(text)) {\n      needEscape = true;\n    }\n    return !needEscape;\n  });\n\n  return needEscape;\n}\n\nexport function escapeTextForLink(text: string) {\n  const imageSyntaxRanges: [number, number][] = [];\n  let result = reMdImageSyntax.exec(text);\n\n  while (result) {\n    imageSyntaxRanges.push([result.index, result.index + result[0].length]);\n    result = reMdImageSyntax.exec(text);\n  }\n\n  return text.replace(reEscapedCharInLinkSyntax, (matched, offset) => {\n    const isDelimiter = imageSyntaxRanges.some((range) => offset > range[0] && offset < range[1]);\n\n    return isDelimiter ? matched : `\\\\${matched}`;\n  });\n}\n\nexport function escape(text: string) {\n  const aheadReplacer = (matched: string) => `\\\\${matched}`;\n  const behindReplacer = (matched: string) => `${matched}\\\\`;\n\n  let escapedText = text.replace(reSpaceMoreThanOne, ' ');\n\n  if (reEscapeBackSlash.test(escapedText)) {\n    escapedText = escapedText.replace(reEscapeBackSlash, aheadReplacer);\n  }\n\n  if (reEscapeBackSlashInSentence.test(escapedText)) {\n    escapedText = escapedText.replace(reEscapeBackSlashInSentence, behindReplacer);\n  }\n\n  escapedText = escapedText.replace(reEscapePairedChars, aheadReplacer);\n\n  if (reEscapeHTML.test(escapedText)) {\n    escapedText = escapedText.replace(reEscapeHTML, aheadReplacer);\n  }\n\n  if (isNeedEscapeText(escapedText)) {\n    escapedText = escapedText.replace(reEscapeChars, aheadReplacer);\n  }\n\n  return escapedText;\n}\n\nexport function quote(text: string) {\n  let result;\n\n  if (text.indexOf('\"') === -1) {\n    result = '\"\"';\n  } else {\n    result = text.indexOf(\"'\") === -1 ? \"''\" : '()';\n  }\n\n  return result[0] + text + result[1];\n}\n\nexport function isNil(value: unknown): value is null | undefined {\n  return isNull(value) || isUndefined(value);\n}\n\nexport function shallowEqual(o1: Record<string, any> | null, o2: Record<string, any> | null) {\n  if (o1 === null && o1 === o2) {\n    return true;\n  }\n  if (typeof o1 !== 'object' || typeof o2 !== 'object' || isNil(o1) || isNil(o2)) {\n    return o1 === o2;\n  }\n  for (const key in o1) {\n    if (o1[key] !== o2[key]) {\n      return false;\n    }\n  }\n  for (const key in o2) {\n    if (!(key in o1)) {\n      return false;\n    }\n  }\n\n  return true;\n}\n\nexport function last<T>(arr: T[]) {\n  return arr[arr.length - 1];\n}\n\nexport function between(value: number, min: number, max: number) {\n  return value >= min && value <= max;\n}\n\nfunction isObject(obj: unknown): obj is object {\n  return typeof obj === 'object' && obj !== null;\n}\n\nexport function deepMergedCopy<T1 extends Record<string, any>, T2 extends Record<string, any>>(\n  targetObj: T1,\n  obj: T2\n) {\n  const resultObj = { ...targetObj } as T1 & T2;\n\n  if (targetObj && obj) {\n    Object.keys(obj).forEach((prop: keyof T2) => {\n      if (isObject(resultObj[prop])) {\n        if (Array.isArray(obj[prop])) {\n          resultObj[prop as keyof T1 & T2] = deepCopyArray(obj[prop]);\n        } else if (resultObj.hasOwnProperty(prop)) {\n          resultObj[prop] = deepMergedCopy(resultObj[prop], obj[prop]);\n        } else {\n          resultObj[prop as keyof T1 & T2] = deepCopy(obj[prop]);\n        }\n      } else {\n        resultObj[prop as keyof T1 & T2] = obj[prop];\n      }\n    });\n  }\n\n  return resultObj;\n}\n\nexport function deepCopyArray<T extends Array<any>>(items: T): T {\n  return items.map((item) => {\n    if (isObject(item)) {\n      return Array.isArray(item) ? deepCopyArray(item) : deepCopy(item);\n    }\n    return item;\n  }) as T;\n}\n\nexport function deepCopy<T extends Record<string, any>>(obj: T) {\n  const keys = Object.keys(obj);\n\n  if (!keys.length) {\n    return obj;\n  }\n\n  return keys.reduce((acc, prop: keyof T) => {\n    if (isObject(obj[prop])) {\n      acc[prop] = Array.isArray(obj[prop]) ? deepCopyArray(obj[prop]) : deepCopy(obj[prop]);\n    } else {\n      acc[prop] = obj[prop];\n    }\n    return acc;\n  }, {} as T);\n}\n\nexport function assign(targetObj: Record<string, any>, obj: Record<string, any> = {}) {\n  Object.keys(obj).forEach((prop) => {\n    if (targetObj.hasOwnProperty(prop) && typeof targetObj[prop] === 'object') {\n      if (Array.isArray(obj[prop])) {\n        targetObj[prop] = obj[prop];\n      } else {\n        assign(targetObj[prop], obj[prop]);\n      }\n    } else {\n      targetObj[prop] = obj[prop];\n    }\n  });\n  return targetObj;\n}\n\nexport function getSortedNumPair(valueA: number, valueB: number) {\n  return valueA > valueB ? [valueB, valueA] : [valueA, valueB];\n}\n"
  },
  {
    "path": "apps/editor/src/utils/constants.ts",
    "content": "const TAG_NAME = '[A-Za-z][A-Za-z0-9-]*';\nconst ATTRIBUTE_NAME = '[a-zA-Z_:][a-zA-Z0-9:._-]*';\nconst UNQUOTED_VALUE = '[^\"\\'=<>`\\\\x00-\\\\x20]+';\n\nconst SINGLE_QUOTED_VALUE = \"'[^']*'\";\nconst DOUBLE_QUOTED_VALUE = '\"[^\"]*\"';\n\nconst ATTRIBUTE_VALUE = `(?:${UNQUOTED_VALUE}|${SINGLE_QUOTED_VALUE}|${DOUBLE_QUOTED_VALUE})`;\nconst ATTRIBUTE_VALUE_SPEC = `${'(?:\\\\s*=\\\\s*'}${ATTRIBUTE_VALUE})`;\n\nexport const ATTRIBUTE = `${'(?:\\\\s+'}${ATTRIBUTE_NAME}${ATTRIBUTE_VALUE_SPEC}?)`;\n\nexport const OPEN_TAG = `<(${TAG_NAME})(${ATTRIBUTE})*\\\\s*/?>`;\nexport const CLOSE_TAG = `</(${TAG_NAME})\\\\s*[>]`;\n\nexport const HTML_TAG = `(?:${OPEN_TAG}|${CLOSE_TAG})`;\n\nexport const reHTMLTag = new RegExp(`^${HTML_TAG}`, 'i');\nexport const reBR = /<br\\s*\\/*>/i;\nexport const reHTMLComment = /<! ---->|<!--(?:-?[^>-])(?:-?[^-])*-->/;\n\nexport const ALTERNATIVE_TAG_FOR_BR = '</p><p>';\n"
  },
  {
    "path": "apps/editor/src/utils/dom.ts",
    "content": "import toArray from 'tui-code-snippet/collection/toArray';\nimport isArray from 'tui-code-snippet/type/isArray';\nimport isString from 'tui-code-snippet/type/isString';\nimport isUndefined from 'tui-code-snippet/type/isUndefined';\nimport hasClass from 'tui-code-snippet/domUtil/hasClass';\nimport addClass from 'tui-code-snippet/domUtil/addClass';\nimport removeClass from 'tui-code-snippet/domUtil/removeClass';\nimport matches from 'tui-code-snippet/domUtil/matches';\nimport { ALTERNATIVE_TAG_FOR_BR, HTML_TAG, OPEN_TAG, reBR } from './constants';\nimport { isNil } from './common';\n\nexport function isPositionInBox(style: CSSStyleDeclaration, offsetX: number, offsetY: number) {\n  const left = parseInt(style.left, 10);\n  const top = parseInt(style.top, 10);\n  const width =\n    parseInt(style.width, 10) + parseInt(style.paddingLeft, 10) + parseInt(style.paddingRight, 10);\n  const height =\n    parseInt(style.height, 10) + parseInt(style.paddingTop, 10) + parseInt(style.paddingBottom, 10);\n\n  return offsetX >= left && offsetX <= left + width && offsetY >= top && offsetY <= top + height;\n}\n\nconst CLS_PREFIX = 'toastui-editor-';\n\nexport function cls(...names: (string | [boolean, string])[]) {\n  const result = [];\n\n  for (const name of names) {\n    let className: string | null;\n\n    if (Array.isArray(name)) {\n      className = name[0] ? name[1] : null;\n    } else {\n      className = name;\n    }\n\n    if (className) {\n      result.push(`${CLS_PREFIX}${className}`);\n    }\n  }\n\n  return result.join(' ');\n}\n\nexport function clsWithMdPrefix(...names: string[]) {\n  return names.map((className) => `${CLS_PREFIX}md-${className}`).join(' ');\n}\n\nexport function isTextNode(node: Node) {\n  return node?.nodeType === Node.TEXT_NODE;\n}\n\nexport function isElemNode(node: Node) {\n  return node && node.nodeType === Node.ELEMENT_NODE;\n}\n\nexport function findNodes(element: Element, selector: string) {\n  const nodeList = toArray(element.querySelectorAll(selector));\n\n  if (nodeList.length) {\n    return nodeList;\n  }\n\n  return [];\n}\n\nexport function appendNodes(node: Node, nodesToAppend: Node | Node[]) {\n  nodesToAppend = isArray(nodesToAppend) ? toArray(nodesToAppend) : [nodesToAppend];\n\n  nodesToAppend.forEach((nodeToAppend) => {\n    node.appendChild(nodeToAppend);\n  });\n}\n\nexport function insertBeforeNode(insertedNode: Node, node: Node) {\n  if (node.parentNode) {\n    node.parentNode.insertBefore(insertedNode, node);\n  }\n}\n\nexport function removeNode(node: Node) {\n  if (node.parentNode) {\n    node.parentNode.removeChild(node);\n  }\n}\n\nexport function unwrapNode(node: Node) {\n  const result = [];\n\n  while (node.firstChild) {\n    result.push(node.firstChild);\n\n    if (node.parentNode) {\n      node.parentNode.insertBefore(node.firstChild, node);\n    }\n  }\n\n  removeNode(node);\n\n  return result;\n}\n\nexport function toggleClass(element: Element, className: string, state?: boolean) {\n  if (isUndefined(state)) {\n    state = !hasClass(element, className);\n  }\n  const toggleFn = state ? addClass : removeClass;\n\n  toggleFn(element, className);\n}\n\nexport function createElementWith(contents: string | HTMLElement, target?: HTMLElement) {\n  const container = document.createElement('div');\n\n  if (isString(contents)) {\n    container.innerHTML = contents;\n  } else {\n    container.appendChild(contents);\n  }\n\n  const { firstChild } = container;\n\n  if (target) {\n    target.appendChild(firstChild!);\n  }\n\n  return firstChild;\n}\n\nexport function getOuterWidth(el: HTMLElement) {\n  const computed = window.getComputedStyle(el);\n\n  return (\n    ['margin-left', 'margin-right'].reduce(\n      (acc, type) => acc + parseInt(computed.getPropertyValue(type), 10),\n      0\n    ) + el.offsetWidth\n  );\n}\n\nexport function closest(node: Node, found: string | Node) {\n  let condition;\n\n  if (isString(found)) {\n    condition = (target: Node) => matches(target as Element, found);\n  } else {\n    condition = (target: Node) => target === found;\n  }\n\n  while (node && node !== document) {\n    if (isElemNode(node) && condition(node)) {\n      return node;\n    }\n\n    node = node.parentNode!;\n  }\n\n  return null;\n}\n\nexport function getTotalOffset(el: HTMLElement, root: HTMLElement) {\n  let offsetTop = 0;\n  let offsetLeft = 0;\n\n  while (el && el !== root) {\n    const { offsetTop: top, offsetLeft: left, offsetParent } = el;\n\n    offsetTop += top;\n    offsetLeft += left;\n    if (offsetParent === root.offsetParent) {\n      break;\n    }\n    el = el.offsetParent as HTMLElement;\n  }\n  return { offsetTop, offsetLeft };\n}\n\nexport function finalizeHtml(html: Element, needHtmlText: boolean) {\n  let result;\n\n  if (needHtmlText) {\n    result = html.innerHTML;\n  } else {\n    const frag = document.createDocumentFragment();\n    const childNodes = toArray(html.childNodes);\n    const { length } = childNodes;\n\n    for (let i = 0; i < length; i += 1) {\n      frag.appendChild(childNodes[i]);\n    }\n    result = frag;\n  }\n\n  return result;\n}\n\nexport function empty(node: Node) {\n  while (node.firstChild) {\n    node.removeChild(node.firstChild);\n  }\n}\n\nexport function appendNode(node: Element, appended: string | ArrayLike<Element> | Element) {\n  if (isString(appended)) {\n    node.insertAdjacentHTML('beforeend', appended);\n  } else {\n    const nodes: Element[] = (appended as ArrayLike<Element>).length\n      ? toArray(appended as ArrayLike<Element>)\n      : [appended as Element];\n\n    for (let i = 0, len = nodes.length; i < len; i += 1) {\n      node.appendChild(nodes[i]);\n    }\n  }\n}\n\nexport function prependNode(node: Element, appended: string | ArrayLike<Element> | Element) {\n  if (isString(appended)) {\n    node.insertAdjacentHTML('afterbegin', appended);\n  } else {\n    const nodes: Element[] = (appended as ArrayLike<Element>).length\n      ? toArray(appended as ArrayLike<Element>)\n      : [appended as Element];\n\n    for (let i = nodes.length - 1, len = 0; i >= len; i -= 1) {\n      node.insertBefore(nodes[i], node.firstChild);\n    }\n  }\n}\n\nexport function setAttributes(attributes: Record<string, any>, element: HTMLElement) {\n  Object.keys(attributes).forEach((attrName) => {\n    if (isNil(attributes[attrName])) {\n      element.removeAttribute(attrName);\n    } else {\n      element.setAttribute(attrName, attributes[attrName]);\n    }\n  });\n}\n\nexport function replaceBRWithEmptyBlock(html: string) {\n  // remove br in paragraph to compatible with markdown\n  let replacedHTML = html.replace(/<p><br\\s*\\/*><\\/p>/gi, '<p></p>');\n  const reHTMLTag = new RegExp(HTML_TAG, 'ig');\n  const htmlTagMatched = replacedHTML.match(reHTMLTag);\n\n  htmlTagMatched?.forEach((htmlTag, index) => {\n    if (reBR.test(htmlTag)) {\n      let alternativeTag = ALTERNATIVE_TAG_FOR_BR;\n\n      if (index) {\n        const prevTag = htmlTagMatched[index - 1];\n        const openTagMatched = prevTag.match(OPEN_TAG);\n\n        if (openTagMatched && !/br/i.test(openTagMatched[1])) {\n          const [, tagName] = openTagMatched;\n\n          alternativeTag = `</${tagName}><${tagName}>`;\n        }\n      }\n      replacedHTML = replacedHTML.replace(reBR, alternativeTag);\n    }\n  });\n\n  return replacedHTML;\n}\n\nexport function removeProseMirrorHackNodes(html: string) {\n  const reProseMirrorImage = /<img class=\"ProseMirror-separator\" alt=\"\">/g;\n  const reProseMirrorTrailingBreak = / class=\"ProseMirror-trailingBreak\"/g;\n\n  let resultHTML = html;\n\n  resultHTML = resultHTML.replace(reProseMirrorImage, '');\n  resultHTML = resultHTML.replace(reProseMirrorTrailingBreak, '');\n\n  return resultHTML;\n}\n"
  },
  {
    "path": "apps/editor/src/utils/map.ts",
    "content": "import inArray from 'tui-code-snippet/array/inArray';\nimport { Mapable } from '@t/map';\n\n/**\n * @class\n * @ignore\n * @classdesc ES6 Map\n */\nclass Map<K, V> implements Mapable<K, V> {\n  private keys: K[];\n\n  private values: V[];\n\n  constructor() {\n    this.keys = [];\n    this.values = [];\n  }\n\n  private getKeyIndex(key: K) {\n    return inArray(key, this.keys);\n  }\n\n  get(key: K): V {\n    return this.values[this.getKeyIndex(key)];\n  }\n\n  set(key: K, value: V) {\n    const keyIndex = this.getKeyIndex(key);\n\n    if (keyIndex > -1) {\n      this.values[keyIndex] = value;\n    } else {\n      this.keys.push(key);\n      this.values.push(value);\n    }\n    return this;\n  }\n\n  has(key: K) {\n    return this.getKeyIndex(key) > -1;\n  }\n\n  delete(key: K) {\n    const keyIndex = this.getKeyIndex(key);\n\n    if (keyIndex > -1) {\n      this.keys.splice(keyIndex, 1);\n      this.values.splice(keyIndex, 1);\n\n      return true;\n    }\n    return false;\n  }\n\n  forEach(callback: (value: V, key: K, map: Mapable<K, V>) => void, thisArg = this) {\n    this.values.forEach((value, index) => {\n      if (value && this.keys[index]) {\n        callback.call(thisArg, value, this.keys[index], this);\n      }\n    });\n  }\n\n  clear() {\n    this.keys = [];\n    this.values = [];\n  }\n}\n\nexport default Map;\n"
  },
  {
    "path": "apps/editor/src/utils/markdown.ts",
    "content": "import {\n  CodeBlockMdNode,\n  CustomBlockMdNode,\n  LinkMdNode,\n  ListItemMdNode,\n  MdNode,\n  MdNodeType,\n  TableCellMdNode,\n  MdPos,\n} from '@toast-ui/toastmark';\nimport { includes } from './common';\n\nexport function hasSpecificTypeAncestor(mdNode: MdNode, ...types: MdNodeType[]) {\n  while (mdNode && mdNode.parent && mdNode.parent.type !== 'document') {\n    if (includes(types, mdNode.parent.type)) {\n      return true;\n    }\n    mdNode = mdNode.parent;\n  }\n  return false;\n}\n\nexport function getMdStartLine(mdNode: MdNode) {\n  return mdNode.sourcepos![0][0];\n}\n\nexport function getMdEndLine(mdNode: MdNode) {\n  return mdNode.sourcepos![1][0];\n}\n\nexport function getMdStartCh(mdNode: MdNode) {\n  return mdNode.sourcepos![0][1];\n}\n\nexport function getMdEndCh(mdNode: MdNode) {\n  return mdNode.sourcepos![1][1];\n}\n\nexport function isMultiLineNode(mdNode: MdNode) {\n  const { type } = mdNode;\n\n  return type === 'codeBlock' || type === 'paragraph';\n}\n\nexport function isHTMLNode(mdNode: MdNode) {\n  const { type } = mdNode;\n\n  return type === 'htmlBlock' || type === 'htmlInline';\n}\n\nexport function isStyledInlineNode(mdNode: MdNode) {\n  const { type } = mdNode;\n\n  return (\n    type === 'strike' ||\n    type === 'strong' ||\n    type === 'emph' ||\n    type === 'code' ||\n    type === 'link' ||\n    type === 'image'\n  );\n}\n\nexport function isCodeBlockNode(mdNode: MdNode): mdNode is CodeBlockMdNode {\n  return mdNode && mdNode.type === 'codeBlock';\n}\n\nexport function isCustomBlockNode(mdNode: MdNode): mdNode is CustomBlockMdNode {\n  return mdNode && mdNode.type === 'customBlock';\n}\n\nexport function isListNode(mdNode: MdNode): mdNode is ListItemMdNode {\n  return mdNode && (mdNode.type === 'item' || mdNode.type === 'list');\n}\n\nexport function isOrderedListNode(mdNode: MdNode): mdNode is ListItemMdNode {\n  return isListNode(mdNode) && mdNode.listData.type === 'ordered';\n}\n\nexport function isBulletListNode(mdNode: MdNode): mdNode is ListItemMdNode {\n  return isListNode(mdNode) && mdNode.listData.type !== 'ordered';\n}\n\nexport function isTableCellNode(mdNode: MdNode): mdNode is TableCellMdNode {\n  return mdNode && (mdNode.type === 'tableCell' || mdNode.type === 'tableDelimCell');\n}\n\nexport function isInlineNode(mdNode: MdNode) {\n  switch (mdNode.type) {\n    case 'code':\n    case 'text':\n    case 'emph':\n    case 'strong':\n    case 'strike':\n    case 'link':\n    case 'image':\n    case 'htmlInline':\n    case 'linebreak':\n    case 'softbreak':\n    case 'customInline':\n      return true;\n    default:\n      return false;\n  }\n}\n\nexport function findClosestNode(\n  mdNode: MdNode,\n  condition: (targetMdNode: MdNode) => boolean,\n  includeSelf = true\n) {\n  mdNode = includeSelf ? mdNode : mdNode.parent!;\n\n  while (mdNode && mdNode.type !== 'document') {\n    if (condition(mdNode)) {\n      return mdNode;\n    }\n    mdNode = mdNode.parent!;\n  }\n  return null;\n}\n\nexport function traverseParentNodes(\n  mdNode: MdNode,\n  iteratee: (targetNode: MdNode) => void,\n  includeSelf = true\n) {\n  mdNode = includeSelf ? mdNode! : mdNode.parent!;\n\n  while (mdNode && mdNode.type !== 'document') {\n    iteratee(mdNode);\n    mdNode = mdNode.parent!;\n  }\n}\n\nexport function addOffsetPos(originPos: MdPos, offset: number): MdPos {\n  return [originPos[0], originPos[1] + offset];\n}\n\nexport function setOffsetPos(originPos: MdPos, newOffset: number): MdPos {\n  return [originPos[0], newOffset];\n}\n\nexport function getInlineMarkdownText(mdNode: MdNode) {\n  const text = mdNode.firstChild!.literal;\n\n  switch (mdNode.type) {\n    case 'emph':\n      return `*${text}*`;\n    case 'strong':\n      return `**${text}**`;\n    case 'strike':\n      return `~~${text}~~`;\n    case 'code':\n      return `\\`${text}\\``;\n    case 'link':\n    case 'image':\n      /* eslint-disable no-case-declarations */\n      const { destination, title } = mdNode as LinkMdNode;\n      const delim = mdNode.type === 'link' ? '' : '!';\n\n      return `${delim}[${text}](${destination}${title ? ` \"${title}\"` : ''})`;\n    default:\n      return null;\n  }\n}\n\nexport function isContainer(node: MdNode) {\n  switch (node.type) {\n    case 'document':\n    case 'blockQuote':\n    case 'list':\n    case 'item':\n    case 'paragraph':\n    case 'heading':\n    case 'emph':\n    case 'strong':\n    case 'strike':\n    case 'link':\n    case 'image':\n    case 'table':\n    case 'tableHead':\n    case 'tableBody':\n    case 'tableRow':\n    case 'tableCell':\n    case 'tableDelimRow':\n    case 'customInline':\n      return true;\n    default:\n      return false;\n  }\n}\n\nexport function getChildrenText(node: MdNode) {\n  const buffer: string[] = [];\n  const walker = node.walker();\n  let event: ReturnType<typeof walker.next> = null;\n\n  while ((event = walker.next())) {\n    const { node: childNode } = event;\n\n    if (childNode.type === 'text') {\n      buffer.push(childNode.literal!);\n    }\n  }\n  return buffer.join('');\n}\n"
  },
  {
    "path": "apps/editor/src/viewer.ts",
    "content": "import { ToastMark } from '@toast-ui/toastmark';\nimport forEachOwnProperties from 'tui-code-snippet/collection/forEachOwnProperties';\nimport extend from 'tui-code-snippet/object/extend';\nimport on from 'tui-code-snippet/domEvent/on';\nimport off from 'tui-code-snippet/domEvent/off';\n\nimport { CustomHTMLRenderer, ViewerOptions } from '@t/editor';\nimport { Emitter, Handler } from '@t/event';\nimport MarkdownPreview from './markdown/mdPreview';\nimport { getPluginInfo } from './helper/plugin';\nimport { last, sanitizeLinkAttribute } from './utils/common';\nimport EventEmitter from './event/eventEmitter';\nimport { cls, isPositionInBox, toggleClass } from './utils/dom';\nimport { registerTagWhitelistIfPossible, sanitizeHTML } from './sanitizer/htmlSanitizer';\n\nconst TASK_ATTR_NAME = 'data-task';\nconst DISABLED_TASK_ATTR_NAME = 'data-task-disabled';\nconst TASK_CHECKED_CLASS_NAME = 'checked';\n\nfunction registerHTMLTagToWhitelist(convertorMap: CustomHTMLRenderer) {\n  ['htmlBlock', 'htmlInline'].forEach((htmlType) => {\n    if (convertorMap[htmlType]) {\n      // register tag white list for preventing to remove the html in sanitizer\n      Object.keys(convertorMap[htmlType]!).forEach((type) => registerTagWhitelistIfPossible(type));\n    }\n  });\n}\n\n/**\n * Class ToastUIEditorViewer\n * @param {object} options Option object\n *     @param {HTMLElement} options.el - container element\n *     @param {string} [options.initialValue] Editor's initial value\n *     @param {Object} [options.events] - Events\n *         @param {function} [options.events.load] - It would be emitted when editor fully load\n *         @param {function} [options.events.change] - It would be emitted when content changed\n *         @param {function} [options.events.caretChange] - It would be emitted when format change by cursor position\n *         @param {function} [options.events.focus] - It would be emitted when editor get focus\n *         @param {function} [options.events.blur] - It would be emitted when editor loose focus\n *     @param {Array.<function|Array>} [options.plugins] - Array of plugins. A plugin can be either a function or an array in the form of [function, options].\n *     @param {Object} [options.extendedAutolinks] - Using extended Autolinks specified in GFM spec\n *     @param {Object} [options.linkAttributes] - Attributes of anchor element that should be rel, target, hreflang, type\n *     @param {Object} [options.customHTMLRenderer=null] - Object containing custom renderer functions correspond to change markdown node to preview HTML or wysiwyg node\n *     @param {boolean} [options.referenceDefinition=false] - whether use the specification of link reference definition\n *     @param {function} [options.customHTMLSanitizer=null] - custom HTML sanitizer\n *     @param {boolean} [options.frontMatter=false] - whether use the front matter\n *     @param {string} [options.theme] - The theme to style the viewer with. The default is included in toastui-editor.css.\n */\nclass ToastUIEditorViewer {\n  private options: Required<ViewerOptions>;\n\n  private toastMark: ToastMark;\n\n  private eventEmitter: Emitter;\n\n  private preview: MarkdownPreview;\n\n  constructor(options: ViewerOptions) {\n    this.options = extend(\n      {\n        linkAttributes: null,\n        extendedAutolinks: false,\n        customHTMLRenderer: null,\n        referenceDefinition: false,\n        customHTMLSanitizer: null,\n        frontMatter: false,\n        usageStatistics: true,\n        theme: 'light',\n      },\n      options\n    );\n    this.eventEmitter = new EventEmitter();\n\n    const linkAttributes = sanitizeLinkAttribute(this.options.linkAttributes);\n    const { toHTMLRenderers, markdownParsers } =\n      getPluginInfo({\n        plugins: this.options.plugins,\n        eventEmitter: this.eventEmitter,\n        usageStatistics: this.options.usageStatistics,\n        instance: this,\n      }) || {};\n    const {\n      customHTMLRenderer,\n      extendedAutolinks,\n      referenceDefinition,\n      frontMatter,\n      customHTMLSanitizer,\n    } = this.options;\n\n    const rendererOptions = {\n      linkAttributes,\n      customHTMLRenderer: { ...toHTMLRenderers, ...customHTMLRenderer },\n      extendedAutolinks,\n      referenceDefinition,\n      frontMatter,\n      sanitizer: customHTMLSanitizer || sanitizeHTML,\n    };\n\n    registerHTMLTagToWhitelist(rendererOptions.customHTMLRenderer);\n\n    if (this.options.events) {\n      forEachOwnProperties(this.options.events, (fn, key) => {\n        this.on(key, fn);\n      });\n    }\n\n    const { el, initialValue, theme } = this.options;\n    const existingHTML = el.innerHTML;\n\n    if (theme !== 'light') {\n      el.classList.add(cls(theme));\n    }\n    el.innerHTML = '';\n\n    this.toastMark = new ToastMark('', {\n      disallowedHtmlBlockTags: ['br', 'img'],\n      extendedAutolinks,\n      referenceDefinition,\n      disallowDeepHeading: true,\n      frontMatter,\n      customParser: markdownParsers,\n    });\n    this.preview = new MarkdownPreview(this.eventEmitter, {\n      ...rendererOptions,\n      isViewer: true,\n    });\n\n    on(this.preview.previewContent!, 'mousedown', this.toggleTask.bind(this));\n\n    if (initialValue) {\n      this.setMarkdown(initialValue);\n    } else if (existingHTML) {\n      this.preview.setHTML(existingHTML);\n    }\n\n    el.appendChild(this.preview.previewContent);\n    this.eventEmitter.emit('load', this);\n  }\n\n  /**\n   * Toggle task by detecting mousedown event.\n   * @param {MouseEvent} ev - event\n   * @private\n   */\n  private toggleTask(ev: MouseEvent) {\n    const element = ev.target as HTMLElement;\n    const style = getComputedStyle(element, ':before');\n\n    if (\n      !element.hasAttribute(DISABLED_TASK_ATTR_NAME) &&\n      element.hasAttribute(TASK_ATTR_NAME) &&\n      isPositionInBox(style, ev.offsetX, ev.offsetY)\n    ) {\n      toggleClass(element, TASK_CHECKED_CLASS_NAME);\n      this.eventEmitter.emit('change', {\n        source: 'viewer',\n        date: ev,\n      });\n    }\n  }\n\n  /**\n   * Set content for preview\n   * @param {string} markdown Markdown text\n   */\n  setMarkdown(markdown: string) {\n    const lineTexts: string[] = this.toastMark.getLineTexts();\n    const { length } = lineTexts;\n    const lastLine = last(lineTexts);\n    const endSourcepos: [number, number] = [length, lastLine.length + 1];\n    const editResult = this.toastMark.editMarkdown([1, 1], endSourcepos, markdown || '');\n\n    this.eventEmitter.emit('updatePreview', editResult);\n  }\n\n  /**\n   * Bind eventHandler to event type\n   * @param {string} type Event type\n   * @param {function} handler Event handler\n   */\n  on(type: string, handler: Handler) {\n    this.eventEmitter.listen(type, handler);\n  }\n\n  /**\n   * Unbind eventHandler from event type\n   * @param {string} type Event type\n   */\n  off(type: string) {\n    this.eventEmitter.removeEventHandler(type);\n  }\n\n  /**\n   * Add hook to TUIEditor event\n   * @param {string} type Event type\n   * @param {function} handler Event handler\n   */\n  addHook(type: string, handler: Handler) {\n    this.eventEmitter.removeEventHandler(type);\n    this.eventEmitter.listen(type, handler);\n  }\n\n  /**\n   * Remove Viewer preview from document\n   */\n  destroy() {\n    off(this.preview.el!, 'mousedown', this.toggleTask.bind(this));\n    this.preview.destroy();\n    this.eventEmitter.emit('destroy');\n  }\n\n  /**\n   * Return true\n   * @returns {boolean}\n   */\n  isViewer() {\n    return true;\n  }\n\n  /**\n   * Return false\n   * @returns {boolean}\n   */\n  isMarkdownMode() {\n    return false;\n  }\n\n  /**\n   * Return false\n   * @returns {boolean}\n   */\n  isWysiwygMode() {\n    return false;\n  }\n}\n\nexport default ToastUIEditorViewer;\n"
  },
  {
    "path": "apps/editor/src/widget/rules.ts",
    "content": "import { Schema, ProsemirrorNode } from 'prosemirror-model';\nimport { CustomInlineMdNode } from '@toast-ui/toastmark';\nimport { WidgetRule, WidgetRuleMap } from '@t/editor';\nimport { getInlineMarkdownText } from '@/utils/markdown';\n\nlet widgetRules: WidgetRule[] = [];\n\nconst widgetRuleMap: WidgetRuleMap = {};\n\nconst reWidgetPrefix = /\\$\\$widget\\d+\\s/;\n\nexport function unwrapWidgetSyntax(text: string) {\n  const index = text.search(reWidgetPrefix);\n\n  if (index !== -1) {\n    const rest = text.substring(index);\n    const replaced = rest.replace(reWidgetPrefix, '').replace('$$', '');\n\n    text = text.substring(0, index);\n    text += unwrapWidgetSyntax(replaced);\n  }\n  return text;\n}\n\nexport function createWidgetContent(info: string, text: string) {\n  return `$$${info} ${text}$$`;\n}\n\nexport function widgetToDOM(info: string, text: string) {\n  const { rule, toDOM } = widgetRuleMap[info];\n\n  const matches = unwrapWidgetSyntax(text).match(rule);\n\n  if (matches) {\n    text = matches[0];\n  }\n\n  return toDOM(text);\n}\n\nexport function getWidgetRules() {\n  return widgetRules;\n}\n\nexport function setWidgetRules(rules: WidgetRule[]) {\n  widgetRules = rules;\n  widgetRules.forEach((rule, index) => {\n    widgetRuleMap[`widget${index}`] = rule;\n  });\n}\n\nfunction mergeNodes(nodes: ProsemirrorNode[], text: string, schema: Schema, ruleIndex: number) {\n  return nodes.concat(createNodesWithWidget(text, schema, ruleIndex));\n}\n\n/**\n * create nodes with plain text and replace text matched to the widget rules with the widget node\n * For example, in case the text and widget rules as below\n *\n * text: $test plain text #test\n * widget rules: [{ rule: /$.+/ }, { rule: /#.+/ }]\n *\n * The creating node process is recursive and is as follows.\n *\n * in first widget rule(/$.+/)\n *  $test -> widget node\n *  plain text -> match with next widget rule\n *  #test -> match with next widget rule\n *\n * in second widget rule(/#.+/)\n *  plain text -> text node(no rule for matching)\n *  #test -> widget node\n */\nexport function createNodesWithWidget(text: string, schema: Schema, ruleIndex = 0) {\n  let nodes: ProsemirrorNode[] = [];\n  const { rule } = widgetRules[ruleIndex] || {};\n  const nextRuleIndex = ruleIndex + 1;\n\n  text = unwrapWidgetSyntax(text);\n\n  if (rule && rule.test(text)) {\n    let index;\n\n    while ((index = text.search(rule)) !== -1) {\n      const prev = text.substring(0, index);\n\n      // get widget node on first splitted text using next widget rule\n      if (prev) {\n        nodes = mergeNodes(nodes, prev, schema, nextRuleIndex);\n      }\n\n      // build widget node using current widget rule\n      text = text.substring(index);\n\n      const [literal] = text.match(rule)!;\n      const info = `widget${ruleIndex}`;\n\n      nodes.push(\n        schema.nodes.widget.create({ info }, schema.text(createWidgetContent(info, literal)))\n      );\n      text = text.substring(literal.length);\n    }\n    // get widget node on last splitted text using next widget rule\n    if (text) {\n      nodes = mergeNodes(nodes, text, schema, nextRuleIndex);\n    }\n  } else if (text) {\n    nodes =\n      ruleIndex < widgetRules.length - 1\n        ? mergeNodes(nodes, text, schema, nextRuleIndex)\n        : [schema.text(text)];\n  }\n\n  return nodes;\n}\n\nexport function getWidgetContent(widgetNode: CustomInlineMdNode) {\n  let event;\n  let text = '';\n  const walker = widgetNode.walker();\n\n  while ((event = walker.next())) {\n    const { node, entering } = event;\n\n    if (entering) {\n      if (node !== widgetNode && node.type !== 'text') {\n        text += getInlineMarkdownText(node);\n        // skip the children\n        walker.resumeAt(widgetNode, false);\n        walker.next();\n      } else if (node.type === 'text') {\n        text += node.literal;\n      }\n    }\n  }\n\n  return text;\n}\n"
  },
  {
    "path": "apps/editor/src/widget/widgetNode.ts",
    "content": "import { DOMOutputSpec, ProsemirrorNode } from 'prosemirror-model';\nimport SpecNode from '@/spec/node';\nimport { widgetToDOM } from './rules';\n\nexport function widgetNodeView(pmNode: ProsemirrorNode) {\n  const dom = document.createElement('span');\n  const node = widgetToDOM(pmNode.attrs.info, pmNode.textContent);\n\n  dom.className = 'tui-widget';\n  dom.appendChild(node);\n\n  return { dom };\n}\n\nexport function isWidgetNode(pmNode: ProsemirrorNode) {\n  return pmNode.type.name === 'widget';\n}\n\nexport class Widget extends SpecNode {\n  get name() {\n    return 'widget';\n  }\n\n  get schema() {\n    return {\n      attrs: {\n        info: { default: null },\n      },\n      group: 'inline',\n      inline: true,\n      content: 'text*',\n      selectable: false,\n      atom: true,\n      toDOM(): DOMOutputSpec {\n        return ['span', { class: 'tui-widget' }, 0];\n      },\n      parseDOM: [\n        {\n          tag: 'span.tui-widget',\n          getAttrs(dom: Node | string) {\n            const text = (dom as HTMLElement).textContent!;\n            const [, info] = text.match(/\\$\\$(widget\\d+)/)!;\n\n            return { info };\n          },\n        },\n      ],\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/adaptor/mdLikeNode.ts",
    "content": "import { MdNodeType } from '@toast-ui/toastmark';\nimport { Mark, Node as ProsemirrorNode } from 'prosemirror-model';\nimport { MdLikeNode } from '@t/markdown';\nimport { includes } from '@/utils/common';\n\nexport function isPmNode(node: ProsemirrorNode | Mark): node is ProsemirrorNode {\n  return node instanceof ProsemirrorNode;\n}\n\nexport function isContainer(type: string) {\n  const containerTypes = [\n    'document',\n    'blockQuote',\n    'bulletList',\n    'orderedList',\n    'listItem',\n    'paragraph',\n    'heading',\n    'emph',\n    'strong',\n    'strike',\n    'link',\n    'image',\n    'table',\n    'tableHead',\n    'tableBody',\n    'tableRow',\n    'tableHeadCell',\n    'tableBodyCell',\n  ];\n\n  return includes(containerTypes, type);\n}\n\nexport function createMdLikeNode(node: ProsemirrorNode | Mark): MdLikeNode {\n  const { attrs, type } = node;\n  const nodeType = type.name;\n  const mdLikeNode: MdLikeNode = {\n    type: nodeType as MdNodeType,\n    wysiwygNode: true,\n    literal: !isContainer(nodeType) && isPmNode(node) ? node.textContent : null,\n  };\n\n  const nodeTypeMap = {\n    heading: { level: attrs.level },\n    link: { destination: attrs.linkUrl, title: attrs.title },\n    image: { destination: attrs.imageUrl },\n    codeBlock: { info: attrs.language },\n    bulletList: { type: 'list', listData: { type: 'bullet' } },\n    orderedList: { type: 'list', listData: { type: 'ordered', start: attrs.order } },\n    listItem: { type: 'item', listData: { task: attrs.task, checked: attrs.checked } },\n    tableHeadCell: { type: 'tableCell', cellType: 'head', align: attrs.align },\n    tableBodyCell: { type: 'tableCell', cellType: 'body', align: attrs.align },\n    customBlock: { info: attrs.info },\n  } as const;\n  const nodeInfo = nodeTypeMap[nodeType as keyof typeof nodeTypeMap];\n  const attributes = { ...mdLikeNode, ...nodeInfo };\n\n  // html block, inline node\n  const { htmlAttrs, childrenHTML } = node.attrs;\n\n  if (htmlAttrs) {\n    return {\n      ...attributes,\n      attrs: htmlAttrs,\n      childrenHTML,\n    };\n  }\n\n  return attributes;\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/adaptor/wwToDOMAdaptor.ts",
    "content": "import {\n  Context,\n  HTMLConvertorMap,\n  HTMLToken,\n  MdNode,\n  MdNodeType,\n  OpenTagToken,\n  RawHTMLToken,\n  Renderer,\n  TextToken,\n} from '@toast-ui/toastmark';\nimport { ProsemirrorNode, Mark } from 'prosemirror-model';\nimport isArray from 'tui-code-snippet/type/isArray';\nimport { getHTMLRenderConvertors } from '@/markdown/htmlRenderConvertors';\nimport { ToDOMAdaptor } from '@t/convertor';\nimport { includes, last } from '@/utils/common';\nimport { CustomHTMLRenderer, LinkAttributes } from '@t/editor';\nimport { setAttributes } from '@/utils/dom';\nimport { createMdLikeNode, isContainer, isPmNode } from './mdLikeNode';\n\ninterface TokenToDOM<T> {\n  openTag: (token: HTMLToken, stack: T[]) => void;\n  closeTag: (token: HTMLToken, stack: T[]) => void;\n  html: (token: HTMLToken, stack: T[]) => void;\n  text: (token: HTMLToken, stack: T[]) => void;\n}\n\nconst tokenToDOMNode: TokenToDOM<HTMLElement> = {\n  openTag(token, stack) {\n    const { tagName, classNames, attributes } = token as OpenTagToken;\n    const el = document.createElement(tagName);\n    let attrs: Record<string, any> = {};\n\n    if (classNames) {\n      el.className = classNames.join(' ');\n    }\n    if (attributes) {\n      attrs = { ...attrs, ...attributes };\n    }\n    setAttributes(attrs, el);\n\n    stack.push(el);\n  },\n  closeTag(_, stack) {\n    if (stack.length > 1) {\n      const el = stack.pop();\n\n      last(stack).appendChild(el!);\n    }\n  },\n  html(token, stack) {\n    last(stack).insertAdjacentHTML('beforeend', (token as RawHTMLToken).content);\n  },\n  text(token, stack) {\n    const textNode = document.createTextNode((token as TextToken).content);\n\n    last(stack).appendChild(textNode);\n  },\n};\n\nexport class WwToDOMAdaptor implements ToDOMAdaptor {\n  private customConvertorKeys: string[];\n\n  renderer: Renderer;\n\n  convertors: HTMLConvertorMap;\n\n  constructor(linkAttributes: LinkAttributes | null, customRenderer: CustomHTMLRenderer) {\n    const convertors = getHTMLRenderConvertors(linkAttributes, customRenderer);\n    const customHTMLConvertor = { ...customRenderer.htmlBlock, ...customRenderer.htmlInline };\n\n    // flatten the html block, inline convertor to other custom convertors\n    this.customConvertorKeys = Object.keys(customRenderer).concat(Object.keys(customHTMLConvertor));\n    this.renderer = new Renderer({\n      gfm: true,\n      convertors: { ...convertors, ...customHTMLConvertor },\n    });\n    this.convertors = this.renderer.getConvertors();\n  }\n\n  private generateTokens(node: ProsemirrorNode | Mark) {\n    const mdLikeNode = createMdLikeNode(node);\n    const context: Context = {\n      entering: true,\n      leaf: isPmNode(node) ? node.isLeaf : false,\n      options: this.renderer.getOptions(),\n      getChildrenText: () => (isPmNode(node) ? node.textContent : ''),\n      skipChildren: () => false,\n    };\n\n    const convertor = this.convertors[node.type.name as MdNodeType]!;\n    const converted = convertor(mdLikeNode as MdNode, context, this.convertors)!;\n    let tokens: HTMLToken[] = isArray(converted) ? converted : [converted];\n\n    if (isContainer(node.type.name) || node.attrs.htmlInline) {\n      context.entering = false;\n\n      tokens.push({ type: 'text', content: isPmNode(node) ? node.textContent : '' });\n      tokens = tokens.concat(convertor(mdLikeNode as MdNode, context, this.convertors)!);\n    }\n\n    return tokens;\n  }\n\n  private toDOMNode(node: ProsemirrorNode | Mark) {\n    const tokens = this.generateTokens(node);\n    const stack: HTMLElement[] = [];\n\n    tokens.forEach((token) => tokenToDOMNode[token.type](token, stack));\n\n    return stack[0];\n  }\n\n  getToDOMNode(name: string) {\n    if (includes(this.customConvertorKeys, name)) {\n      return this.toDOMNode.bind(this);\n    }\n    return null;\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/clipboard/paste.ts",
    "content": "import { Schema, Node, Slice, Fragment, NodeType } from 'prosemirror-model';\n\nimport { isFromMso, convertMsoParagraphsToList } from '@/wysiwyg/clipboard/pasteMsoList';\nimport { getTableContentFromSlice } from '@/wysiwyg/helper/table';\nimport { ALTERNATIVE_TAG_FOR_BR } from '@/utils/constants';\n\nconst START_FRAGMENT_COMMENT = '<!--StartFragment-->';\nconst END_FRAGMENT_COMMENT = '<!--EndFragment-->';\n\nfunction getContentBetweenFragmentComments(html: string) {\n  const startFragmentIndex = html.indexOf(START_FRAGMENT_COMMENT);\n  const endFragmentIndex = html.lastIndexOf(END_FRAGMENT_COMMENT);\n\n  if (startFragmentIndex > -1 && endFragmentIndex > -1) {\n    html = html.slice(startFragmentIndex + START_FRAGMENT_COMMENT.length, endFragmentIndex);\n  }\n\n  return html.replace(/<br[^>]*>/g, ALTERNATIVE_TAG_FOR_BR);\n}\n\nfunction convertMsoTableToCompletedTable(html: string) {\n  // wrap with <tr> if html contains dangling <td> tags\n  // dangling <td> tag is that tag does not have <tr> as parent node\n  if (/<\\/td>((?!<\\/tr>)[\\s\\S])*$/i.test(html)) {\n    html = `<tr>${html}</tr>`;\n  }\n  // wrap with <table> if html contains dangling <tr> tags\n  // dangling <tr> tag is that tag does not have <table> as parent node\n  if (/<\\/tr>((?!<\\/table>)[\\s\\S])*$/i.test(html)) {\n    html = `<table>${html}</table>`;\n  }\n\n  return html;\n}\n\nexport function changePastedHTML(html: string) {\n  html = getContentBetweenFragmentComments(html);\n  html = convertMsoTableToCompletedTable(html);\n\n  if (isFromMso(html)) {\n    html = convertMsoParagraphsToList(html);\n  }\n\n  return html;\n}\n\nfunction getMaxColumnCount(rows: Node[]) {\n  const row = rows.reduce((prevRow, currentRow) =>\n    prevRow.childCount > currentRow.childCount ? prevRow : currentRow\n  );\n\n  return row.childCount;\n}\n\nfunction createCells(orgRow: Node, maxColumnCount: number, cell: NodeType) {\n  const cells = [];\n  const cellCount = orgRow.childCount;\n\n  for (let colIdx = 0; colIdx < cellCount; colIdx += 1) {\n    if (!orgRow.child(colIdx).attrs.extended) {\n      const copiedCell =\n        colIdx < cellCount\n          ? cell.create(orgRow.child(colIdx).attrs, orgRow.child(colIdx).content)\n          : cell.createAndFill()!;\n\n      cells.push(copiedCell);\n    }\n  }\n\n  return cells;\n}\n\nexport function copyTableHeadRow(orgRow: Node, maxColumnCount: number, schema: Schema) {\n  const { tableRow, tableHeadCell } = schema.nodes;\n  const cells = createCells(orgRow, maxColumnCount, tableHeadCell);\n\n  return tableRow.create(null, cells);\n}\n\nexport function copyTableBodyRow(orgRow: Node, maxColumnCount: number, schema: Schema) {\n  const { tableRow, tableBodyCell } = schema.nodes;\n  const cells = createCells(orgRow, maxColumnCount, tableBodyCell);\n\n  return tableRow.create(null, cells);\n}\n\nfunction creatTableBodyDummyRow(columnCount: number, schema: Schema) {\n  const { tableRow, tableBodyCell } = schema.nodes;\n  const cells = [];\n\n  for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {\n    const dummyCell = tableBodyCell.createAndFill()!;\n\n    cells.push(dummyCell);\n  }\n\n  return tableRow.create({ dummyRowForPasting: true }, cells);\n}\n\nexport function createRowsFromPastingTable(tableContent: Fragment) {\n  const tableHeadRows: Node[] = [];\n  const tableBodyRows: Node[] = [];\n\n  if (tableContent.firstChild!.type.name === 'tableHead') {\n    const tableHead = tableContent.firstChild!;\n\n    tableHead.forEach((row) => tableHeadRows.push(row));\n  }\n\n  if (tableContent.lastChild!.type.name === 'tableBody') {\n    const tableBody = tableContent.lastChild!;\n\n    tableBody.forEach((row) => tableBodyRows.push(row));\n  }\n\n  return [...tableHeadRows, ...tableBodyRows];\n}\n\nfunction createTableHead(tableHeadRow: Node, maxColumnCount: number, schema: Schema) {\n  const copiedRow = copyTableHeadRow(tableHeadRow, maxColumnCount, schema);\n\n  return schema.nodes.tableHead.create(null, copiedRow);\n}\n\nfunction createTableBody(tableBodyRows: Node[], maxColumnCount: number, schema: Schema) {\n  const copiedRows = tableBodyRows.map((tableBodyRow) =>\n    copyTableBodyRow(tableBodyRow, maxColumnCount, schema)\n  );\n\n  if (!tableBodyRows.length) {\n    const dummyTableRow = creatTableBodyDummyRow(maxColumnCount, schema);\n\n    copiedRows.push(dummyTableRow);\n  }\n\n  return schema.nodes.tableBody.create(null, copiedRows);\n}\n\nfunction createTableFromPastingTable(\n  rows: Node[],\n  schema: Schema,\n  startFromBody: boolean,\n  isInTable: boolean\n) {\n  const columnCount = getMaxColumnCount(rows);\n\n  if (startFromBody && isInTable) {\n    return schema.nodes.table.create(null, [createTableBody(rows, columnCount, schema)]);\n  }\n\n  const [tableHeadRow] = rows;\n  const tableBodyRows = rows.slice(1);\n\n  const nodes = [createTableHead(tableHeadRow, columnCount, schema)];\n\n  if (tableBodyRows.length) {\n    nodes.push(createTableBody(tableBodyRows, columnCount, schema));\n  }\n\n  return schema.nodes.table.create(null, nodes);\n}\n\nexport function changePastedSlice(slice: Slice, schema: Schema, isInTable: boolean) {\n  const nodes: Node[] = [];\n  const { content, openStart, openEnd } = slice;\n\n  content.forEach((node) => {\n    if (node.type.name === 'table') {\n      const tableContent = getTableContentFromSlice(new Slice(Fragment.from(node), 0, 0));\n\n      if (tableContent) {\n        const rows = createRowsFromPastingTable(tableContent);\n        const startFromBody = tableContent.firstChild!.type.name === 'tableBody';\n        const table = createTableFromPastingTable(rows, schema, startFromBody, isInTable);\n\n        nodes.push(table);\n      }\n    } else {\n      nodes.push(node);\n    }\n  });\n\n  return new Slice(Fragment.from(nodes), openStart, openEnd);\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/clipboard/pasteMsoList.ts",
    "content": "import {\n  isElemNode,\n  findNodes,\n  removeNode,\n  unwrapNode,\n  insertBeforeNode,\n  appendNodes,\n} from '@/utils/dom';\n\nconst reMSOListClassName = /MsoListParagraph/;\nconst reMSOStylePrefix = /style=(.|\\n)*mso-/;\nconst reMSOListStyle = /mso-list:(.*)/;\nconst reMSOTagName = /O:P/;\nconst reMSOListBullet = /^(n|u|l)/;\n\nconst MSO_CLASS_NAME_LIST_PARA = 'p.MsoListParagraph';\n\ninterface ListItemData {\n  id: number;\n  level: number;\n  prev: ListItemData | null;\n  parent: ListItemData | null;\n  children: ListItemData[];\n  unordered: boolean;\n  contents: string;\n}\n\nexport function isFromMso(html: string) {\n  return reMSOStylePrefix.test(html);\n}\n\nfunction getListItemContents(para: HTMLElement) {\n  const removedNodes = [];\n  const walker = document.createTreeWalker(para, 1, null, false);\n\n  while (walker.nextNode()) {\n    const node = walker.currentNode;\n\n    if (isElemNode(node)) {\n      const { outerHTML, textContent } = node as HTMLElement;\n      const msoSpan = reMSOStylePrefix.test(outerHTML);\n      const bulletSpan = reMSOListStyle.test(outerHTML);\n\n      if (msoSpan && !bulletSpan && textContent) {\n        removedNodes.push([node, true]);\n      } else if (reMSOTagName.test(node.nodeName) || (msoSpan && !textContent) || bulletSpan) {\n        removedNodes.push([node, false]);\n      }\n    }\n  }\n\n  removedNodes.forEach(([node, isUnwrap]) => {\n    if (isUnwrap) {\n      unwrapNode(node as HTMLElement);\n    } else {\n      removeNode(node as HTMLElement);\n    }\n  });\n\n  return para.innerHTML.trim();\n}\n\nfunction createListItemDataFromParagraph(para: HTMLElement, index: number) {\n  const styleAttr = para.getAttribute('style');\n\n  if (styleAttr) {\n    const [, listItemInfo] = styleAttr.match(reMSOListStyle)!;\n    const [, levelStr] = listItemInfo.trim().split(' ');\n    const level = parseInt(levelStr.replace('level', ''), 10);\n    const unordered = reMSOListBullet.test(para.textContent || '');\n\n    return {\n      id: index,\n      level,\n      prev: null,\n      parent: null,\n      children: [],\n      unordered,\n      contents: getListItemContents(para),\n    };\n  }\n\n  return null;\n}\n\nfunction addListItemDetailData(data: ListItemData, prevData: ListItemData) {\n  if (prevData.level < data.level) {\n    prevData.children.push(data);\n    data.parent = prevData;\n  } else {\n    while (prevData) {\n      if (prevData.level === data.level) {\n        break;\n      }\n      prevData = prevData.parent!;\n    }\n\n    if (prevData) {\n      data.prev = prevData;\n      data.parent = prevData.parent;\n\n      if (data.parent) {\n        data.parent.children.push(data);\n      }\n    }\n  }\n}\n\nfunction createListData(paras: HTMLElement[]) {\n  const listData: ListItemData[] = [];\n\n  paras.forEach((para, index) => {\n    const prevListItemData = listData[index - 1];\n    const listItemData = createListItemDataFromParagraph(para, index);\n\n    if (listItemData) {\n      if (prevListItemData) {\n        addListItemDetailData(listItemData, prevListItemData);\n      }\n\n      listData.push(listItemData);\n    }\n  });\n\n  return listData;\n}\n\nfunction makeList(listData: ListItemData[]) {\n  const listTagName = listData[0].unordered ? 'ul' : 'ol';\n  const list = document.createElement(listTagName);\n\n  listData.forEach((data) => {\n    const { children, contents } = data;\n    const listItem = document.createElement('li');\n\n    listItem.innerHTML = contents;\n    list.appendChild(listItem);\n\n    if (children.length) {\n      list.appendChild(makeList(children));\n    }\n  });\n\n  return list;\n}\n\nfunction makeListFromParagraphs(paras: HTMLElement[]) {\n  const listData = createListData(paras);\n  const rootChildren = listData.filter(({ parent }) => !parent);\n\n  return makeList(rootChildren);\n}\n\nfunction isMsoListParagraphEnd(node: HTMLElement) {\n  while (node) {\n    if (isElemNode(node)) {\n      break;\n    }\n    node = node.nextSibling as HTMLElement;\n  }\n\n  return node ? !reMSOListClassName.test(node.className) : true;\n}\n\nexport function convertMsoParagraphsToList(html: string) {\n  const container = document.createElement('div') as HTMLElement;\n\n  container.innerHTML = html;\n\n  let paras: HTMLElement[] = [];\n\n  const foundParas = findNodes(container, MSO_CLASS_NAME_LIST_PARA);\n\n  foundParas.forEach((para) => {\n    const msoListParaEnd = isMsoListParagraphEnd(para.nextSibling as HTMLElement);\n\n    paras.push(para as HTMLElement);\n\n    if (msoListParaEnd) {\n      const list = makeListFromParagraphs(paras);\n      const { nextSibling } = para;\n\n      if (nextSibling) {\n        insertBeforeNode(list, nextSibling);\n      } else {\n        appendNodes(container, list);\n      }\n\n      paras = [];\n    }\n\n    removeNode(para);\n  });\n\n  // without `<p></p>`, the list string was parsed as a paragraph node and added\n  const extraHTML = foundParas.length ? '<p></p>' : '';\n\n  return `${extraHTML}${container.innerHTML}`;\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/clipboard/pasteToTable.ts",
    "content": "import { Schema, Slice, Fragment } from 'prosemirror-model';\nimport { Transaction } from 'prosemirror-state';\nimport { EditorView } from 'prosemirror-view';\n\nimport {\n  getResolvedSelection,\n  createDummyCells,\n  createTableBodyRows,\n  getTableContentFromSlice,\n  getRowAndColumnCount,\n} from '@/wysiwyg/helper/table';\n\nimport {\n  createRowsFromPastingTable,\n  copyTableHeadRow,\n  copyTableBodyRow,\n} from '@/wysiwyg/clipboard/paste';\nimport CellSelection from '@/wysiwyg/plugins/selection/cellSelection';\nimport { last } from '@/utils/common';\nimport { SelectionInfo, TableOffsetMap } from '@/wysiwyg/helper/tableOffsetMap';\n\ninterface PastingRangeInfo {\n  addedRowCount: number;\n  addedColumnCount: number;\n  startRowIdx: number;\n  startColIdx: number;\n  endColIdx: number;\n  endRowIdx: number;\n}\n\ninterface ReplacedCellsOffsets {\n  rowIdx: number;\n  startColIdx: number;\n  endColIdx: number;\n  dummyOffsets?: [startCellOffset: number, endCellOffset: number];\n}\n\nconst DUMMY_CELL_SIZE = 4;\nconst TR_NODES_SIZE = 2;\n\nfunction getDummyCellSize(dummyCellCount: number) {\n  return dummyCellCount * DUMMY_CELL_SIZE;\n}\n\nfunction createPastingCells(\n  tableContent: Fragment,\n  curSelectionInfo: SelectionInfo,\n  schema: Schema\n) {\n  const pastingRows: Fragment[] = [];\n  const pastingTableRows = createRowsFromPastingTable(tableContent);\n  const columnCount = pastingTableRows[0].childCount;\n  const rowCount = pastingTableRows.length;\n\n  const startToTableHead = curSelectionInfo.startRowIdx === 0;\n  const slicedRows = pastingTableRows.slice(0, rowCount);\n\n  if (startToTableHead) {\n    const tableHeadRow = slicedRows.shift();\n\n    if (tableHeadRow) {\n      const { content } = copyTableHeadRow(tableHeadRow, columnCount, schema);\n\n      pastingRows.push(content);\n    }\n  }\n\n  slicedRows.forEach((tableBodyRow) => {\n    if (!tableBodyRow.attrs.dummyRowForPasting) {\n      const { content } = copyTableBodyRow(tableBodyRow, columnCount, schema);\n\n      pastingRows.push(content);\n    }\n  });\n\n  return pastingRows;\n}\n\nfunction getPastingRangeInfo(\n  map: TableOffsetMap,\n  { startRowIdx, startColIdx }: SelectionInfo,\n  pastingCells: Fragment[]\n): PastingRangeInfo {\n  const pastingRowCount = pastingCells.length;\n  let pastingColumnCount = 0;\n\n  for (let i = 0; i < pastingRowCount; i += 1) {\n    let columnCount = pastingCells[i].childCount;\n\n    pastingCells[i].forEach(({ attrs }) => {\n      const { colspan } = attrs;\n\n      if (colspan > 1) {\n        columnCount += colspan - 1;\n      }\n    });\n    pastingColumnCount = Math.max(pastingColumnCount, columnCount);\n  }\n\n  const endRowIdx = startRowIdx + pastingRowCount - 1;\n  const endColIdx = startColIdx + pastingColumnCount - 1;\n  const addedRowCount = Math.max(endRowIdx + 1 - map.totalRowCount, 0);\n  const addedColumnCount = Math.max(endColIdx + 1 - map.totalColumnCount, 0);\n\n  return {\n    startRowIdx,\n    startColIdx,\n    endRowIdx,\n    endColIdx,\n    addedRowCount,\n    addedColumnCount,\n  };\n}\n\nfunction addReplacedOffsets(\n  {\n    startRowIdx,\n    startColIdx,\n    endRowIdx,\n    endColIdx,\n    addedRowCount,\n    addedColumnCount,\n  }: PastingRangeInfo,\n  cellsOffsets: ReplacedCellsOffsets[]\n) {\n  for (let rowIdx = startRowIdx; rowIdx <= endRowIdx - addedRowCount; rowIdx += 1) {\n    cellsOffsets.push({\n      rowIdx,\n      startColIdx,\n      endColIdx: endColIdx - addedColumnCount,\n    });\n  }\n}\n\nfunction expandColumns(\n  tr: Transaction,\n  schema: Schema,\n  map: TableOffsetMap,\n  {\n    startRowIdx,\n    startColIdx,\n    endRowIdx,\n    endColIdx,\n    addedRowCount,\n    addedColumnCount,\n  }: PastingRangeInfo,\n  cellsOffsets: ReplacedCellsOffsets[]\n) {\n  const { totalRowCount } = map;\n  let index = 0;\n\n  for (let rowIdx = 0; rowIdx < totalRowCount; rowIdx += 1) {\n    const { offset, nodeSize } = map.getCellInfo(rowIdx, endColIdx - addedColumnCount);\n    const insertOffset = tr.mapping.map(offset + nodeSize);\n    const cells = createDummyCells(addedColumnCount, rowIdx, schema);\n\n    tr.insert(insertOffset, cells);\n\n    if (rowIdx >= startRowIdx && rowIdx <= endRowIdx - addedRowCount) {\n      const cellInfo = map.getCellInfo(rowIdx, endColIdx - addedColumnCount);\n      const startCellOffset = tr.mapping.map(cellInfo.offset);\n      const endCellOffset = insertOffset + getDummyCellSize(addedColumnCount);\n\n      cellsOffsets[index] = {\n        rowIdx,\n        startColIdx,\n        endColIdx,\n        dummyOffsets: [startCellOffset, endCellOffset],\n      };\n      index += 1;\n    }\n  }\n}\n\nfunction expandRows(\n  tr: Transaction,\n  schema: Schema,\n  map: TableOffsetMap,\n  { addedRowCount, addedColumnCount, startColIdx, endColIdx }: PastingRangeInfo,\n  cellsOffsets: ReplacedCellsOffsets[]\n) {\n  const mapStart = tr.mapping.maps.length;\n  const tableEndPos = map.tableEndOffset - 2;\n  const rows = createTableBodyRows(addedRowCount, map.totalColumnCount + addedColumnCount, schema);\n  let startOffset = tableEndPos;\n\n  tr.insert(tr.mapping.slice(mapStart).map(startOffset), rows);\n\n  for (let rowIndex = 0; rowIndex < addedRowCount; rowIndex += 1) {\n    const startCellOffset = startOffset + getDummyCellSize(startColIdx) + 1;\n    const endCellOffset = startOffset + getDummyCellSize(endColIdx + 1) + 1;\n    const nextCellOffset =\n      startOffset + getDummyCellSize(map.totalColumnCount + addedColumnCount) + TR_NODES_SIZE;\n\n    cellsOffsets.push({\n      rowIdx: rowIndex + map.totalRowCount,\n      startColIdx,\n      endColIdx,\n      dummyOffsets: [startCellOffset, endCellOffset],\n    });\n    startOffset = nextCellOffset;\n  }\n}\n\nfunction replaceCells(\n  tr: Transaction,\n  pastingRows: Fragment[],\n  cellsOffsets: ReplacedCellsOffsets[],\n  map: TableOffsetMap\n) {\n  const mapStart = tr.mapping.maps.length;\n\n  cellsOffsets.forEach((offsets, index) => {\n    const { rowIdx, startColIdx, endColIdx, dummyOffsets } = offsets;\n    const mapping = tr.mapping.slice(mapStart);\n    const cells = new Slice(pastingRows[index], 0, 0);\n    const from = dummyOffsets ? dummyOffsets[0] : map.getCellStartOffset(rowIdx, startColIdx);\n    const to = dummyOffsets ? dummyOffsets[1] : map.getCellEndOffset(rowIdx, endColIdx);\n\n    tr.replace(mapping.map(from), mapping.map(to), cells);\n  });\n}\n\nexport function pasteToTable(view: EditorView, slice: Slice) {\n  const { selection, schema, tr } = view.state;\n  const { anchor, head } = getResolvedSelection(selection);\n\n  if (anchor && head) {\n    const tableContent = getTableContentFromSlice(slice);\n\n    if (!tableContent) {\n      return false;\n    }\n\n    const map = TableOffsetMap.create(anchor)!;\n    const curSelectionInfo = map.getRectOffsets(anchor, head);\n\n    const pastingCells = createPastingCells(tableContent, curSelectionInfo, schema);\n    const pastingInfo = getPastingRangeInfo(map, curSelectionInfo, pastingCells);\n\n    const cellsOffsets: ReplacedCellsOffsets[] = [];\n\n    // @TODO: unmerge the span and paste the cell\n    if (canMerge(map, pastingInfo)) {\n      addReplacedOffsets(pastingInfo, cellsOffsets);\n\n      if (pastingInfo.addedColumnCount) {\n        expandColumns(tr, schema, map, pastingInfo, cellsOffsets);\n      }\n      if (pastingInfo.addedRowCount) {\n        expandRows(tr, schema, map, pastingInfo, cellsOffsets);\n      }\n      replaceCells(tr, pastingCells, cellsOffsets, map);\n\n      view.dispatch!(tr);\n\n      setSelection(view, cellsOffsets, map.getCellInfo(0, 0).offset);\n    }\n    return true;\n  }\n  return false;\n}\n\nfunction setSelection(view: EditorView, cellsOffsets: ReplacedCellsOffsets[], pos: number) {\n  const { tr, doc } = view.state;\n  // get changed cell offsets\n  const map = TableOffsetMap.create(doc.resolve(pos))!;\n\n  // eslint-disable-next-line prefer-destructuring\n  const { rowIdx: startRowIdx, startColIdx } = cellsOffsets[0];\n  const { rowIdx: endRowIdx, endColIdx } = last(cellsOffsets);\n\n  const { offset: startOffset } = map.getCellInfo(startRowIdx, startColIdx);\n  const { offset: endOffset } = map.getCellInfo(endRowIdx, endColIdx);\n\n  view.dispatch!(\n    tr.setSelection(new CellSelection(doc.resolve(startOffset), doc.resolve(endOffset)))\n  );\n}\n\nfunction canMerge(map: TableOffsetMap, pastingInfo: PastingRangeInfo) {\n  const ranges = map.getSpannedOffsets(pastingInfo);\n\n  const { rowCount, columnCount } = getRowAndColumnCount(ranges);\n  const { rowCount: pastingRowCount, columnCount: pastingColumnCount } = getRowAndColumnCount(\n    pastingInfo\n  );\n\n  return rowCount === pastingRowCount && columnCount === pastingColumnCount;\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/command/list.ts",
    "content": "import { ProsemirrorNode, NodeType, NodeRange, Fragment, Slice } from 'prosemirror-model';\nimport { ReplaceAroundStep, canSplit, liftTarget } from 'prosemirror-transform';\nimport { Transaction, Selection, EditorState } from 'prosemirror-state';\nimport { Command } from 'prosemirror-commands';\n\nimport { findListItem, isInListNode } from '@/wysiwyg/helper/node';\n\ninterface Attrs {\n  [key: string]: any;\n}\n\ninterface WrapperInfo {\n  type: NodeType;\n  attrs?: Attrs;\n}\n\nfunction findWrappingOutside(range: NodeRange, type: NodeType) {\n  const { parent, startIndex, endIndex } = range;\n  const around = parent.contentMatchAt(startIndex).findWrapping(type);\n\n  if (around) {\n    const outer = around.length ? around[0] : type;\n\n    return parent.canReplaceWith(startIndex, endIndex, outer) ? around : null;\n  }\n\n  return null;\n}\n\nfunction findWrappingInside(range: NodeRange, type: NodeType) {\n  const { parent, startIndex, endIndex } = range;\n  const inner = parent.child(startIndex);\n  const inside = type.contentMatch.findWrapping(inner.type);\n\n  if (inside) {\n    const lastType = inside.length ? inside[inside.length - 1] : type;\n    let innerMatch = lastType.contentMatch;\n\n    for (let i = startIndex; innerMatch && i < endIndex; i += 1) {\n      innerMatch = innerMatch.matchType(parent.child(i).type)!;\n    }\n\n    if (innerMatch && innerMatch.validEnd) {\n      return inside;\n    }\n  }\n\n  return null;\n}\n\nfunction findWrappers(range: NodeRange, innerRange: NodeRange, nodeType: NodeType, attrs?: Attrs) {\n  const around = findWrappingOutside(range, nodeType);\n  const inner = findWrappingInside(innerRange, nodeType);\n\n  if (around && inner) {\n    const aroundNodes = around.map((type) => {\n      return { type };\n    });\n\n    const innerNodes = inner.map((type) => {\n      return { type, attrs };\n    });\n\n    return aroundNodes.concat({ type: nodeType }).concat(innerNodes);\n  }\n\n  return null;\n}\n\nfunction wrapInList(\n  tr: Transaction,\n  { start, end, startIndex, endIndex, parent }: NodeRange,\n  wrappers: WrapperInfo[],\n  joinBefore: boolean,\n  list: NodeType\n) {\n  let content = Fragment.empty;\n\n  for (let i = wrappers.length - 1; i >= 0; i -= 1) {\n    content = Fragment.from(wrappers[i].type.create(wrappers[i].attrs, content));\n  }\n\n  tr.step(\n    new ReplaceAroundStep(\n      start - (joinBefore ? 2 : 0),\n      end,\n      start,\n      end,\n      new Slice(content, 0, 0),\n      wrappers.length,\n      true\n    )\n  );\n\n  let foundListIndex = 0;\n\n  for (let i = 0; i < wrappers.length; i += 1) {\n    if (wrappers[i].type === list) {\n      foundListIndex = i + 1;\n      break;\n    }\n  }\n\n  const splitDepth = wrappers.length - foundListIndex;\n\n  let splitPos = start + wrappers.length - (joinBefore ? 2 : 0);\n\n  for (let i = startIndex, len = endIndex; i < len; i += 1) {\n    const first = i === startIndex;\n\n    if (!first && canSplit(tr.doc, splitPos, splitDepth)) {\n      tr.split(splitPos, splitDepth);\n      splitPos += splitDepth * 2;\n    }\n\n    splitPos += parent.child(i).nodeSize;\n  }\n\n  return tr;\n}\n\nfunction changeToList(tr: Transaction, range: NodeRange, list: NodeType, attrs?: Attrs) {\n  const { $from, $to, depth } = range;\n\n  let outerRange = range;\n  let joinBefore = false;\n\n  if (\n    depth >= 2 &&\n    $from.node(depth - 1).type.compatibleContent(list) &&\n    range.startIndex === 0 &&\n    $from.index(depth - 1)\n  ) {\n    const start = tr.doc.resolve(range.start - 2);\n\n    outerRange = new NodeRange(start, start, depth);\n\n    if (range.endIndex < range.parent.childCount) {\n      range = new NodeRange($from, tr.doc.resolve($to.end(depth)), depth);\n    }\n\n    joinBefore = true;\n  }\n\n  const wrappers = findWrappers(outerRange, range, list, attrs);\n\n  if (wrappers) {\n    return wrapInList(tr, range, wrappers, joinBefore, list);\n  }\n\n  return tr;\n}\n\nfunction getBeforeLineListItem(doc: ProsemirrorNode, offset: number) {\n  let endListItemPos = doc.resolve(offset);\n\n  while (endListItemPos.node().type.name !== 'paragraph') {\n    offset -= 2; // The position value of </li></ul>\n    endListItemPos = doc.resolve(offset);\n  }\n\n  return findListItem(endListItemPos);\n}\n\nfunction toggleTaskListItems(tr: Transaction, { $from, $to }: NodeRange) {\n  const startListItem = findListItem($from);\n  let endListItem = findListItem($to);\n\n  if (startListItem && endListItem) {\n    while (endListItem) {\n      const { offset, node } = endListItem;\n\n      const attrs = { task: !node.attrs.task, checked: false };\n\n      tr.setNodeMarkup(offset, null, attrs);\n\n      if (offset === startListItem.offset) {\n        break;\n      }\n\n      endListItem = getBeforeLineListItem(tr.doc, offset);\n    }\n  }\n\n  return tr;\n}\n\nfunction changeListType(tr: Transaction, { $from, $to }: NodeRange, list: NodeType) {\n  const startListItem = findListItem($from);\n  let endListItem = findListItem($to);\n\n  if (startListItem && endListItem) {\n    while (endListItem) {\n      const { offset, node, depth } = endListItem;\n\n      if (node.attrs.task) {\n        tr.setNodeMarkup(offset, null, { task: false, checked: false });\n      }\n\n      const resolvedPos = tr.doc.resolve(offset);\n\n      if (resolvedPos.parent!.type !== list) {\n        const parentOffset = resolvedPos.before(depth - 1);\n\n        tr.setNodeMarkup(parentOffset, list);\n      }\n\n      if (offset === startListItem.offset) {\n        break;\n      }\n\n      endListItem = getBeforeLineListItem(tr.doc, offset);\n    }\n  }\n\n  return tr;\n}\n\nexport function changeList(list: NodeType): Command {\n  return ({ selection, tr }, dispatch) => {\n    const { $from, $to } = selection;\n    const range = $from.blockRange($to);\n\n    if (range) {\n      const newTr = isInListNode($from)\n        ? changeListType(tr, range, list)\n        : changeToList(tr, range, list);\n\n      dispatch!(newTr);\n\n      return true;\n    }\n\n    return false;\n  };\n}\n\nexport function toggleTask(): Command {\n  return ({ selection, tr, schema }, dispatch) => {\n    const { $from, $to } = selection;\n    const range = $from.blockRange($to);\n\n    if (range) {\n      const newTr = isInListNode($from)\n        ? toggleTaskListItems(tr, range)\n        : changeToList(tr, range, schema.nodes.bulletList, { task: true });\n\n      dispatch!(newTr);\n\n      return true;\n    }\n\n    return false;\n  };\n}\n\nexport function sinkListItem(listItem: NodeType): Command {\n  return ({ tr, selection }: EditorState, dispatch) => {\n    const { $from, $to } = selection;\n    const range = $from.blockRange(\n      $to,\n      ({ childCount, firstChild }) => !!childCount && firstChild!.type === listItem\n    );\n\n    if (range && range.startIndex > 0) {\n      const { parent } = range;\n      const nodeBefore = parent.child(range.startIndex - 1);\n\n      if (nodeBefore.type !== listItem) {\n        return false;\n      }\n\n      const nestedBefore = nodeBefore.lastChild && nodeBefore.lastChild.type === parent.type;\n      const inner = nestedBefore ? Fragment.from(listItem.create()) : null;\n      const slice = new Slice(\n        Fragment.from(listItem.create(null, Fragment.from(parent.type.create(null, inner!)))),\n        nestedBefore ? 3 : 1,\n        0\n      );\n\n      const before = range.start;\n      const after = range.end;\n\n      tr.step(\n        new ReplaceAroundStep(before - (nestedBefore ? 3 : 1), after, before, after, slice, 1, true)\n      );\n\n      dispatch!(tr);\n\n      return true;\n    }\n\n    return false;\n  };\n}\n\nfunction liftToOuterList(tr: Transaction, range: NodeRange, listItem: NodeType) {\n  const { $from, $to, end, depth, parent } = range;\n  const endOfList = $to.end(depth);\n\n  if (end < endOfList) {\n    // There are siblings after the lifted items, which must become\n    // children of the last item\n    tr.step(\n      new ReplaceAroundStep(\n        end - 1,\n        endOfList,\n        end,\n        endOfList,\n        new Slice(Fragment.from(listItem.create(null, parent.copy())), 1, 0),\n        1,\n        true\n      )\n    );\n\n    range = new NodeRange(tr.doc.resolve($from.pos), tr.doc.resolve(endOfList), depth);\n  }\n\n  tr.lift(range, liftTarget(range)!);\n\n  return tr;\n}\n\nfunction liftOutOfList(tr: Transaction, range: NodeRange) {\n  const list = range.parent;\n\n  let pos = range.end;\n\n  // Merge the list items into a single big item\n  for (let i = range.endIndex - 1, len = range.startIndex; i > len; i -= 1) {\n    pos -= list.child(i).nodeSize;\n    tr.delete(pos - 1, pos + 1);\n  }\n\n  const startPos = tr.doc.resolve(range.start);\n  const listItem = startPos.nodeAfter;\n\n  const atStart = range.startIndex === 0;\n  const atEnd = range.endIndex === list.childCount;\n\n  const parent = startPos.node(-1);\n  const indexBefore = startPos.index(-1);\n  const canReplaceParent = parent.canReplace(\n    indexBefore + (atStart ? 0 : 1),\n    indexBefore + 1,\n    listItem?.content.append(atEnd ? Fragment.empty : Fragment.from(list))\n  );\n\n  if (listItem && canReplaceParent) {\n    const start = startPos.pos;\n    const end = start + listItem.nodeSize;\n\n    // Strip off the surrounding list. At the sides where we're not at\n    // the end of the list, the existing list is closed. At sides where\n    // this is the end, it is overwritten to its end.\n    tr.step(\n      new ReplaceAroundStep(\n        start - (atStart ? 1 : 0),\n        end + (atEnd ? 1 : 0),\n        start + 1,\n        end - 1,\n        new Slice(\n          (atStart ? Fragment.empty : Fragment.from(list.copy(Fragment.empty))).append(\n            atEnd ? Fragment.empty : Fragment.from(list.copy(Fragment.empty))\n          ),\n          atStart ? 0 : 1,\n          atEnd ? 0 : 1\n        ),\n        atStart ? 0 : 1\n      )\n    );\n  }\n\n  return tr;\n}\n\nexport function liftListItem(listItem: NodeType): Command {\n  return ({ tr, selection }: EditorState, dispatch) => {\n    const { $from, $to } = selection;\n    const range = $from.blockRange(\n      $to,\n      ({ childCount, firstChild }) => !!childCount && firstChild!.type === listItem\n    );\n\n    if (range) {\n      const topListItem = $from.node(range.depth - 1).type === listItem;\n      const newTr = topListItem ? liftToOuterList(tr, range, listItem) : liftOutOfList(tr, range);\n\n      dispatch!(newTr);\n\n      return true;\n    }\n\n    return false;\n  };\n}\n\nexport function splitListItem(listItem: NodeType): Command {\n  return ({ tr, selection }, dispatch) => {\n    const { $from, $to } = selection;\n\n    if ($from.depth < 2 || !$from.sameParent($to)) {\n      return false;\n    }\n\n    const grandParent = $from.node(-1);\n\n    if (grandParent.type !== listItem) {\n      return false;\n    }\n\n    if ($from.parent.content.size === 0 && $from.node(-1).childCount === $from.indexAfter(-1)) {\n      // In an empty block. If this is a nested list, the wrapping\n      // list item should be split. Otherwise, bail out and let next\n      // command handle lifting.\n      if (\n        $from.depth === 2 ||\n        $from.node(-3).type !== listItem ||\n        $from.index(-2) !== $from.node(-2).childCount - 1\n      ) {\n        return false;\n      }\n\n      const keepItem = $from.index(-1) > 0;\n\n      let wrapper = Fragment.empty;\n\n      // Build a fragment containing empty versions of the structure\n      // from the outer list item to the parent node of the cursor\n      for (let depth = $from.depth - (keepItem ? 1 : 2); depth >= $from.depth - 3; depth -= 1) {\n        wrapper = Fragment.from($from.node(depth).copy(wrapper));\n      }\n\n      // Add a second list item with an empty default start node\n      wrapper = wrapper.append(Fragment.from(listItem.createAndFill()!));\n\n      tr.replace(\n        keepItem ? $from.before() : $from.before(-1),\n        $from.after(-3),\n        new Slice(wrapper, keepItem ? 3 : 2, 2)\n      );\n\n      tr.setSelection(Selection.near(tr.doc.resolve($from.pos + (keepItem ? 3 : 2))));\n\n      dispatch!(tr);\n\n      return true;\n    }\n\n    const nextType = $to.pos === $from.end() ? grandParent.contentMatchAt(0).defaultType : null;\n    const types = nextType && [null, { type: nextType }];\n\n    tr.delete($from.pos, $to.pos);\n\n    if (canSplit(tr.doc, $from.pos, 2, types!)) {\n      tr.split($from.pos, 2, types!);\n\n      dispatch!(tr);\n\n      return true;\n    }\n\n    return false;\n  };\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/command/table.ts",
    "content": "import { ProsemirrorNode, ResolvedPos, Schema } from 'prosemirror-model';\nimport { Selection, Transaction, NodeSelection } from 'prosemirror-state';\n\nimport { addParagraph } from '@/helper/manipulation';\n\nimport { TableOffsetMap } from '../helper/tableOffsetMap';\nimport { Direction } from '../nodes/table';\n\nexport type CellPosition = [rowIdx: number, colIdx: number];\n\ntype CellOffsetFn = ([rowIdx, colIdx]: CellPosition, map: TableOffsetMap) => number | null;\n\ntype CellOffsetFnMap = {\n  [key in Direction]: CellOffsetFn;\n};\n\nconst cellOffsetFnMap: CellOffsetFnMap = {\n  left: getLeftCellOffset,\n  right: getRightCellOffset,\n  up: getUpCellOffset,\n  down: getDownCellOffset,\n};\n\nfunction isInFirstListItem(\n  pos: ResolvedPos,\n  doc: ProsemirrorNode,\n  [paraDepth, listDepth]: number[]\n) {\n  const listItemNode = doc.resolve(pos.before(paraDepth - 1));\n\n  return listDepth === paraDepth && !listItemNode.nodeBefore;\n}\n\nfunction isInLastListItem(pos: ResolvedPos) {\n  let { depth } = pos;\n  let parentNode;\n\n  while (depth) {\n    parentNode = pos.node(depth);\n\n    if (parentNode.type.name === 'tableBodyCell') {\n      break;\n    }\n\n    if (parentNode.type.name === 'listItem') {\n      const grandParent = pos.node(depth - 1);\n      const lastListItem = grandParent.lastChild === parentNode;\n      const hasChildren = parentNode.lastChild?.type.name !== 'paragraph';\n\n      if (!lastListItem) {\n        return false;\n      }\n\n      return !hasChildren;\n    }\n\n    depth -= 1;\n  }\n\n  return false;\n}\n\nfunction canMoveToBeforeCell(\n  direction: Direction,\n  [paraDepth, listDepth, curDepth]: number[],\n  from: ResolvedPos,\n  doc: ProsemirrorNode,\n  inList: boolean\n) {\n  if (direction === Direction.LEFT || direction === Direction.UP) {\n    if (inList && !isInFirstListItem(from, doc, [paraDepth, listDepth])) {\n      return false;\n    }\n\n    const endOffset = from.before(curDepth);\n    const { nodeBefore } = doc.resolve(endOffset);\n\n    if (nodeBefore) {\n      return false;\n    }\n  }\n\n  return true;\n}\n\nfunction canMoveToAfterCell(\n  direction: Direction,\n  curDepth: number,\n  from: ResolvedPos,\n  doc: ProsemirrorNode,\n  inList: boolean\n) {\n  if (direction === Direction.RIGHT || direction === Direction.DOWN) {\n    if (inList && !isInLastListItem(from)) {\n      return false;\n    }\n\n    const endOffset = from.after(curDepth);\n    const { nodeAfter } = doc.resolve(endOffset);\n\n    if (nodeAfter) {\n      return false;\n    }\n  }\n\n  return true;\n}\n\nexport function canMoveBetweenCells(\n  direction: Direction,\n  [cellDepth, paraDepth]: number[],\n  from: ResolvedPos,\n  doc: ProsemirrorNode\n) {\n  const listDepth = cellDepth + 3; // 3 is position of <ul><li><p>\n  const inList = paraDepth >= listDepth;\n  const curDepth = inList ? cellDepth + 1 : paraDepth;\n\n  const moveBeforeCell = canMoveToBeforeCell(\n    direction,\n    [paraDepth, listDepth, curDepth],\n    from,\n    doc,\n    inList\n  );\n  const moveAfterCell = canMoveToAfterCell(direction, curDepth, from, doc, inList);\n\n  return moveBeforeCell && moveAfterCell;\n}\n\nexport function canBeOutOfTable(\n  direction: Direction,\n  map: TableOffsetMap,\n  [rowIdx, colIdx]: CellPosition\n) {\n  const rowspanInfo = map.getRowspanStartInfo(rowIdx, colIdx)!;\n  const inFirstRow = direction === Direction.UP && rowIdx === 0;\n  const inLastRow =\n    direction === Direction.DOWN &&\n    (rowspanInfo?.count > 1 ? rowIdx + rowspanInfo!.count - 1 : rowIdx) === map.totalRowCount - 1;\n\n  return inFirstRow || inLastRow;\n}\n\nexport function addParagraphBeforeTable(tr: Transaction, map: TableOffsetMap, schema: Schema) {\n  const tableStartPos = tr.doc.resolve(map.tableStartOffset - 1);\n\n  if (!tableStartPos.nodeBefore) {\n    return addParagraph(tr, tableStartPos, schema);\n  }\n  return tr.setSelection(Selection.near(tableStartPos, -1));\n}\n\nexport function addParagraphAfterTable(\n  tr: Transaction,\n  map: TableOffsetMap,\n  schema: Schema,\n  forcedAddtion = false\n) {\n  const tableEndPos = tr.doc.resolve(map.tableEndOffset);\n\n  if (forcedAddtion || !tableEndPos.nodeAfter) {\n    return addParagraph(tr, tableEndPos, schema);\n  }\n  return tr.setSelection(Selection.near(tableEndPos, 1));\n}\n\nexport function getRightCellOffset([rowIdx, colIdx]: CellPosition, map: TableOffsetMap) {\n  const { totalRowCount, totalColumnCount } = map;\n\n  const lastCellInRow = colIdx === totalColumnCount - 1;\n  const lastCellInTable = rowIdx === totalRowCount - 1 && lastCellInRow;\n\n  if (!lastCellInTable) {\n    let nextColIdx = colIdx + 1;\n    const colspanInfo = map.getColspanStartInfo(rowIdx, colIdx)!;\n\n    if (colspanInfo?.count > 1) {\n      nextColIdx += colspanInfo.count - 1;\n    }\n\n    if (lastCellInRow || nextColIdx === totalColumnCount) {\n      rowIdx += 1;\n      nextColIdx = 0;\n    }\n    const { offset } = map.getCellInfo(rowIdx, nextColIdx);\n\n    return offset + 2;\n  }\n\n  return null;\n}\n\nexport function getLeftCellOffset([rowIdx, colIdx]: CellPosition, map: TableOffsetMap) {\n  const { totalColumnCount } = map;\n\n  const firstCellInRow = colIdx === 0;\n  const firstCellInTable = rowIdx === 0 && firstCellInRow;\n\n  if (!firstCellInTable) {\n    colIdx -= 1;\n\n    if (firstCellInRow) {\n      rowIdx -= 1;\n      colIdx = totalColumnCount - 1;\n    }\n\n    const { offset, nodeSize } = map.getCellInfo(rowIdx, colIdx);\n\n    return offset + nodeSize - 2;\n  }\n\n  return null;\n}\n\nexport function getUpCellOffset([rowIdx, colIdx]: CellPosition, map: TableOffsetMap) {\n  if (rowIdx > 0) {\n    const { offset, nodeSize } = map.getCellInfo(rowIdx - 1, colIdx);\n\n    return offset + nodeSize - 2;\n  }\n\n  return null;\n}\n\nexport function getDownCellOffset([rowIdx, colIdx]: CellPosition, map: TableOffsetMap) {\n  const { totalRowCount } = map;\n\n  if (rowIdx < totalRowCount - 1) {\n    let nextRowIdx = rowIdx + 1;\n    const rowspanInfo = map.getRowspanStartInfo(rowIdx, colIdx)!;\n\n    if (rowspanInfo?.count > 1) {\n      nextRowIdx += rowspanInfo.count - 1;\n    }\n    const { offset } = map.getCellInfo(nextRowIdx, colIdx);\n\n    return offset + 2;\n  }\n\n  return null;\n}\n\nexport function moveToCell(\n  direction: Direction,\n  tr: Transaction,\n  cellIndex: CellPosition,\n  map: TableOffsetMap\n) {\n  const cellOffsetFn = cellOffsetFnMap[direction];\n  const offset = cellOffsetFn(cellIndex, map);\n\n  if (offset) {\n    const dir = direction === Direction.RIGHT || direction === Direction.DOWN ? 1 : -1;\n\n    return tr.setSelection(Selection.near(tr.doc.resolve(offset), dir));\n  }\n\n  return null;\n}\n\nexport function canSelectTableNode(\n  direction: Direction,\n  map: TableOffsetMap,\n  [rowIdx, colIdx]: CellPosition\n) {\n  if (direction === Direction.UP || direction === Direction.DOWN) {\n    return false;\n  }\n  const { tableStartOffset, tableEndOffset } = map;\n  const { offset, nodeSize } = map.getCellInfo(rowIdx, colIdx);\n\n  const pos = direction === Direction.LEFT ? tableStartOffset : tableEndOffset;\n  const curPos = direction === Direction.LEFT ? offset - 2 : offset + nodeSize + 3;\n\n  return pos === curPos;\n}\n\nexport function selectNode(tr: Transaction, pos: ResolvedPos, depth: number) {\n  const tablePos = tr.doc.resolve(pos.before(depth - 3));\n\n  return tr.setSelection(new NodeSelection(tablePos));\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/helper/node.ts",
    "content": "import { ProsemirrorNode, ResolvedPos } from 'prosemirror-model';\nimport { includes } from '@/utils/common';\n\ntype NodeAttrs = Record<string, any>;\n\ninterface CustomAttrs {\n  htmlAttrs: { default: any };\n  classNames: { default: null | string[] };\n}\n\nexport function findNodeBy(\n  pos: ResolvedPos,\n  condition: (node: ProsemirrorNode, depth: number) => boolean\n) {\n  let { depth } = pos;\n\n  while (depth) {\n    const node = pos.node(depth);\n\n    if (condition(node, depth)) {\n      return {\n        node,\n        depth,\n        offset: depth > 0 ? pos.before(depth) : 0,\n      };\n    }\n\n    depth -= 1;\n  }\n\n  return null;\n}\n\nexport function isListNode({ type }: ProsemirrorNode) {\n  return type.name === 'bulletList' || type.name === 'orderedList';\n}\n\nexport function isInListNode(pos: ResolvedPos) {\n  return !!findNodeBy(\n    pos,\n    ({ type }: ProsemirrorNode) =>\n      type.name === 'listItem' || type.name === 'bulletList' || type.name === 'orderedList'\n  );\n}\n\nexport function isInTableNode(pos: ResolvedPos) {\n  return !!findNodeBy(\n    pos,\n    ({ type }: ProsemirrorNode) => type.name === 'tableHeadCell' || type.name === 'tableBodyCell'\n  );\n}\n\nexport function findListItem(pos: ResolvedPos) {\n  return findNodeBy(pos, ({ type }: ProsemirrorNode) => type.name === 'listItem');\n}\n\nexport function createDOMInfoParsedRawHTML(tag: string) {\n  return {\n    tag,\n    getAttrs(dom: Node | string) {\n      const rawHTML = (dom as HTMLElement).getAttribute('data-raw-html');\n\n      return {\n        ...(rawHTML && { rawHTML }),\n      };\n    },\n  };\n}\n\nexport function createCellAttrs(attrs: NodeAttrs) {\n  return Object.keys(attrs).reduce<NodeAttrs>((acc, attrName) => {\n    if (attrName !== 'rawHTML' && attrs[attrName]) {\n      attrName = attrName === 'className' ? 'class' : attrName;\n      acc[attrName] = attrs[attrName];\n    }\n    return acc;\n  }, {});\n}\n\nexport function createParsedCellDOM(tag: string) {\n  return {\n    tag,\n    getAttrs(dom: Node | string) {\n      return ['rawHTML', 'colspan', 'rowspan', 'extended'].reduce<NodeAttrs>((acc, attrName) => {\n        const attrNameInDOM = attrName === 'rawHTML' ? 'data-raw-html' : attrName;\n        const attrValue = (dom as HTMLElement).getAttribute(attrNameInDOM);\n\n        if (attrValue) {\n          acc[attrName] = includes(['rawHTML', 'extended'], attrName)\n            ? attrValue\n            : Number(attrValue);\n        }\n        return acc;\n      }, {});\n    },\n  };\n}\n\nexport function getDefaultCustomAttrs(): CustomAttrs {\n  return {\n    htmlAttrs: { default: null },\n    classNames: { default: null },\n  };\n}\n\nexport function getCustomAttrs(attrs: Record<string, any>) {\n  const { htmlAttrs, classNames } = attrs;\n\n  return { ...htmlAttrs, class: classNames ? classNames.join(' ') : null };\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/helper/table.ts",
    "content": "import { Node, Schema, ResolvedPos, Slice, ProsemirrorNode } from 'prosemirror-model';\nimport { Selection, TextSelection } from 'prosemirror-state';\n\nimport { findNodeBy } from '@/wysiwyg/helper/node';\n\nimport { CellSelection } from '@t/wysiwyg';\nimport type { SelectionInfo } from './tableOffsetMap';\n\nexport function createTableHeadRow(columnCount: number, schema: Schema, data?: string[]) {\n  const { tableRow, tableHeadCell, paragraph } = schema.nodes;\n  const cells = [];\n\n  for (let index = 0; index < columnCount; index += 1) {\n    const text = data && data[index];\n    const para = paragraph.create(null, text ? schema.text(text) : []);\n\n    cells.push(tableHeadCell.create(null, para));\n  }\n\n  return [tableRow.create(null, cells)];\n}\n\nexport function createTableBodyRows(\n  rowCount: number,\n  columnCount: number,\n  schema: Schema,\n  data?: string[]\n) {\n  const { tableRow, tableBodyCell, paragraph } = schema.nodes;\n  const tableRows = [];\n\n  for (let rowIdx = 0; rowIdx < rowCount; rowIdx += 1) {\n    const cells = [];\n\n    for (let colIdx = 0; colIdx < columnCount; colIdx += 1) {\n      const text = data && data[rowIdx * columnCount + colIdx];\n      const para = paragraph.create(null, text ? schema.text(text) : []);\n\n      cells.push(tableBodyCell.create(null, para));\n    }\n\n    tableRows.push(tableRow.create(null, cells));\n  }\n\n  return tableRows;\n}\n\nexport function createDummyCells(\n  columnCount: number,\n  rowIdx: number,\n  schema: Schema,\n  attrs: Record<string, any> | null = null\n) {\n  const { tableHeadCell, tableBodyCell, paragraph } = schema.nodes;\n  const cell = rowIdx === 0 ? tableHeadCell : tableBodyCell;\n  const cells = [];\n\n  for (let index = 0; index < columnCount; index += 1) {\n    cells.push(cell.create(attrs, paragraph.create()));\n  }\n\n  return cells;\n}\n\nexport function findCellElement(node: HTMLElement, root: Element) {\n  while (node && node !== root) {\n    if (node.nodeName === 'TD' || node.nodeName === 'TH') {\n      return node;\n    }\n\n    node = node.parentNode as HTMLElement;\n  }\n\n  return null;\n}\n\nexport function findCell(pos: ResolvedPos) {\n  return findNodeBy(\n    pos,\n    ({ type }: Node) => type.name === 'tableHeadCell' || type.name === 'tableBodyCell'\n  );\n}\n\nexport function getResolvedSelection(selection: Selection | CellSelection) {\n  if (selection instanceof TextSelection) {\n    const { $anchor } = selection;\n    const foundCell = findCell($anchor);\n\n    if (foundCell) {\n      const anchor = $anchor.node(0).resolve($anchor.before(foundCell.depth));\n\n      return { anchor, head: anchor };\n    }\n  }\n\n  const { startCell, endCell } = selection as CellSelection;\n\n  return { anchor: startCell, head: endCell };\n}\n\nexport function getTableContentFromSlice(slice: Slice) {\n  if (slice.size) {\n    let { content, openStart, openEnd } = slice;\n\n    if (content.childCount !== 1) {\n      return null;\n    }\n\n    while (\n      content.childCount === 1 &&\n      ((openStart > 0 && openEnd > 0) || content.firstChild?.type.name === 'table')\n    ) {\n      openStart -= 1;\n      openEnd -= 1;\n      content = content.firstChild!.content;\n    }\n\n    if (\n      content.firstChild!.type.name === 'tableHead' ||\n      content.firstChild!.type.name === 'tableBody'\n    ) {\n      return content;\n    }\n  }\n\n  return null;\n}\n\nexport function getRowAndColumnCount({\n  startRowIdx,\n  startColIdx,\n  endRowIdx,\n  endColIdx,\n}: SelectionInfo) {\n  const rowCount = endRowIdx - startRowIdx + 1;\n  const columnCount = endColIdx - startColIdx + 1;\n\n  return { rowCount, columnCount };\n}\n\nexport function setAttrs(cell: ProsemirrorNode, attrs: Record<string, any>) {\n  return { ...cell.attrs, ...attrs };\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/helper/tableOffsetMap.ts",
    "content": "import type { Node, ResolvedPos } from 'prosemirror-model';\nimport { findNodeBy } from '@/wysiwyg/helper/node';\nimport { assign, getSortedNumPair } from '@/utils/common';\n\nexport interface CellInfo {\n  offset: number;\n  nodeSize: number;\n  extended?: boolean;\n}\nexport interface SelectionInfo {\n  startRowIdx: number;\n  startColIdx: number;\n  endRowIdx: number;\n  endColIdx: number;\n}\n\ninterface SpanMap {\n  [key: number]: { count: number; startSpanIdx: number };\n}\n\nexport interface RowInfo {\n  [key: number]: CellInfo;\n  length: number;\n  rowspanMap: SpanMap;\n  colspanMap: SpanMap;\n}\n\ninterface SpanInfo {\n  node: Node;\n  pos: number;\n  count: number;\n  startSpanIdx: number;\n}\n\ninterface OffsetMap {\n  rowInfo: RowInfo[];\n  table: Node;\n  totalRowCount: number;\n  totalColumnCount: number;\n  tableStartOffset: number;\n  tableEndOffset: number;\n  getCellInfo(rowIdx: number, colIdx: number): CellInfo;\n  posAt(rowIdx: number, colIdx: number): number;\n  getNodeAndPos(rowIdx: number, colIdx: number): { node: Node; pos: number };\n  extendedRowspan(rowIdx: number, colIdx: number): boolean;\n  extendedColspan(rowIdx: number, colIdx: number): boolean;\n  getRowspanCount(rowIdx: number, colIdx: number): number;\n  getColspanCount(rowIdx: number, colIdx: number): number;\n  decreaseColspanCount(rowIdx: number, colIdx: number): number;\n  decreaseRowspanCount(rowIdx: number, colIdx: number): number;\n  getColspanStartInfo(rowIdx: number, colIdx: number): SpanInfo | null;\n  getRowspanStartInfo(rowIdx: number, colIdx: number): SpanInfo | null;\n  getRectOffsets(startCellPos: ResolvedPos, endCellPos?: ResolvedPos): SelectionInfo;\n  getSpannedOffsets(selectionInfo: SelectionInfo): SelectionInfo;\n}\n\ntype CreateOffsetMapMixin = (\n  headOrBody: Node,\n  startOffset: number,\n  startFromBody?: boolean\n) => RowInfo[];\n\nconst cache = new Map();\n\n/* eslint-disable @typescript-eslint/no-unused-vars */\nexport class TableOffsetMap {\n  private table: Node;\n\n  private tableRows: Node[];\n\n  private tableStartPos: number;\n\n  private rowInfo: RowInfo[];\n\n  constructor(table: Node, tableRows: Node[], tableStartPos: number, rowInfo: RowInfo[]) {\n    this.table = table;\n    this.tableRows = tableRows;\n    this.tableStartPos = tableStartPos;\n    this.rowInfo = rowInfo;\n  }\n\n  static create(cellPos: ResolvedPos): TableOffsetMap | null {\n    const table = findNodeBy(cellPos, ({ type }: Node) => type.name === 'table');\n\n    if (table) {\n      const { node, depth, offset } = table;\n      const cached = cache.get(node);\n\n      if (cached?.tableStartPos === offset + 1) {\n        return cached;\n      }\n\n      const rows: Node[] = [];\n      const tablePos = cellPos.start(depth);\n\n      const thead = node.child(0);\n      const tbody = node.child(1);\n\n      const theadCellInfo = createOffsetMap(thead, tablePos);\n      const tbodyCellInfo = createOffsetMap(tbody, tablePos + thead.nodeSize);\n\n      thead.forEach((row) => rows.push(row));\n      tbody.forEach((row) => rows.push(row));\n\n      const map = new TableOffsetMap(node, rows, tablePos, theadCellInfo.concat(tbodyCellInfo));\n\n      cache.set(node, map);\n\n      return map;\n    }\n\n    return null;\n  }\n\n  get totalRowCount() {\n    return this.rowInfo.length;\n  }\n\n  get totalColumnCount() {\n    return this.rowInfo[0].length;\n  }\n\n  get tableStartOffset() {\n    return this.tableStartPos;\n  }\n\n  get tableEndOffset() {\n    return this.tableStartPos + this.table.nodeSize - 1;\n  }\n\n  getCellInfo(rowIdx: number, colIdx: number) {\n    return this.rowInfo[rowIdx][colIdx];\n  }\n\n  posAt(rowIdx: number, colIdx: number): number {\n    for (let i = 0, rowStart = this.tableStartPos; ; i += 1) {\n      const rowEnd = rowStart + this.tableRows[i].nodeSize;\n\n      if (i === rowIdx) {\n        let index = colIdx;\n\n        // Skip the cells from previous row(via rowspan)\n        while (index < this.totalColumnCount && this.rowInfo[i][index].offset < rowStart) {\n          index += 1;\n        }\n        return index === this.totalColumnCount ? rowEnd : this.rowInfo[i][index].offset;\n      }\n      rowStart = rowEnd;\n    }\n  }\n\n  getNodeAndPos(rowIdx: number, colIdx: number) {\n    const cellInfo = this.rowInfo[rowIdx][colIdx];\n\n    return {\n      node: this.table.nodeAt(cellInfo.offset - this.tableStartOffset)!,\n      pos: cellInfo.offset,\n    };\n  }\n\n  extendedRowspan(rowIdx: number, colIdx: number) {\n    return false;\n  }\n\n  extendedColspan(rowIdx: number, colIdx: number) {\n    return false;\n  }\n\n  getRowspanCount(rowIdx: number, colIdx: number) {\n    return 0;\n  }\n\n  getColspanCount(rowIdx: number, colIdx: number) {\n    return 0;\n  }\n\n  decreaseColspanCount(rowIdx: number, colIdx: number) {\n    return 0;\n  }\n\n  decreaseRowspanCount(rowIdx: number, colIdx: number) {\n    return 0;\n  }\n\n  getColspanStartInfo(rowIdx: number, colIdx: number): SpanInfo | null {\n    return null;\n  }\n\n  getRowspanStartInfo(rowIdx: number, colIdx: number): SpanInfo | null {\n    return null;\n  }\n\n  getCellStartOffset(rowIdx: number, colIdx: number) {\n    const { offset } = this.rowInfo[rowIdx][colIdx];\n\n    return this.extendedRowspan(rowIdx, colIdx) ? this.posAt(rowIdx, colIdx) : offset;\n  }\n\n  getCellEndOffset(rowIdx: number, colIdx: number) {\n    const { offset, nodeSize } = this.rowInfo[rowIdx][colIdx];\n\n    return this.extendedRowspan(rowIdx, colIdx) ? this.posAt(rowIdx, colIdx) : offset + nodeSize;\n  }\n\n  getCellIndex(cellPos: ResolvedPos): [rowIdx: number, colIdx: number] {\n    for (let rowIdx = 0; rowIdx < this.totalRowCount; rowIdx += 1) {\n      const rowInfo = this.rowInfo[rowIdx];\n\n      for (let colIdx = 0; colIdx < this.totalColumnCount; colIdx += 1) {\n        if (rowInfo[colIdx].offset + 1 > cellPos.pos) {\n          return [rowIdx, colIdx];\n        }\n      }\n    }\n    return [0, 0];\n  }\n\n  getRectOffsets(startCellPos: ResolvedPos, endCellPos = startCellPos) {\n    if (startCellPos.pos > endCellPos.pos) {\n      [startCellPos, endCellPos] = [endCellPos, startCellPos];\n    }\n    let [startRowIdx, startColIdx] = this.getCellIndex(startCellPos);\n    let [endRowIdx, endColIdx] = this.getCellIndex(endCellPos);\n\n    [startRowIdx, endRowIdx] = getSortedNumPair(startRowIdx, endRowIdx);\n    [startColIdx, endColIdx] = getSortedNumPair(startColIdx, endColIdx);\n\n    return this.getSpannedOffsets({ startRowIdx, startColIdx, endRowIdx, endColIdx });\n  }\n\n  getSpannedOffsets(selectionInfo: SelectionInfo): SelectionInfo {\n    return selectionInfo;\n  }\n}\n/* eslint-enable @typescript-eslint/no-unused-vars */\n\nlet createOffsetMap = (headOrBody: Node, startOffset: number) => {\n  const cellInfoMatrix: RowInfo[] = [];\n\n  headOrBody.forEach((row: Node, rowOffset: number) => {\n    // get row index based on table(not table head or table body)\n    const rowInfo: RowInfo = { rowspanMap: {}, colspanMap: {}, length: 0 };\n\n    row.forEach(({ nodeSize }: Node, cellOffset: number) => {\n      let colIdx = 0;\n\n      while (rowInfo[colIdx]) {\n        colIdx += 1;\n      }\n\n      rowInfo[colIdx] = {\n        // 2 is the sum of the front and back positions of the tag\n        offset: startOffset + rowOffset + cellOffset + 2,\n        nodeSize,\n      };\n      rowInfo.length += 1;\n    });\n    cellInfoMatrix.push(rowInfo);\n  });\n\n  return cellInfoMatrix;\n};\n\nexport function mixinTableOffsetMapPrototype(\n  offsetMapMixin: OffsetMap,\n  createOffsetMapMixin: CreateOffsetMapMixin\n) {\n  assign(TableOffsetMap.prototype, offsetMapMixin);\n  createOffsetMap = createOffsetMapMixin;\n\n  return TableOffsetMap;\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/marks/code.ts",
    "content": "import { Mark as ProsemirrorMark, DOMOutputSpec } from 'prosemirror-model';\nimport { toggleMark } from 'prosemirror-commands';\n\nimport Mark from '@/spec/mark';\nimport { getCustomAttrs, getDefaultCustomAttrs } from '@/wysiwyg/helper/node';\n\nimport { EditorCommand } from '@t/spec';\n\nexport class Code extends Mark {\n  get name() {\n    return 'code';\n  }\n\n  get schema() {\n    return {\n      attrs: {\n        rawHTML: { default: null },\n        ...getDefaultCustomAttrs(),\n      },\n      parseDOM: [\n        {\n          tag: 'code',\n          getAttrs(dom: Node | string) {\n            const rawHTML = (dom as HTMLElement).getAttribute('data-raw-html');\n\n            return {\n              ...(rawHTML && { rawHTML }),\n            };\n          },\n        },\n      ],\n      toDOM({ attrs }: ProsemirrorMark): DOMOutputSpec {\n        return [attrs.rawHTML || 'code', getCustomAttrs(attrs)];\n      },\n    };\n  }\n\n  commands(): EditorCommand {\n    return () => (state, dispatch) => toggleMark(state.schema.marks.code)(state, dispatch);\n  }\n\n  keymaps() {\n    const codeCommand = this.commands()();\n\n    return {\n      'Shift-Mod-c': codeCommand,\n      'Shift-Mod-C': codeCommand,\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/marks/emph.ts",
    "content": "import { Mark as ProsemirrorMark, DOMOutputSpec } from 'prosemirror-model';\nimport { toggleMark } from 'prosemirror-commands';\n\nimport Mark from '@/spec/mark';\nimport { getCustomAttrs, getDefaultCustomAttrs } from '@/wysiwyg/helper/node';\n\nimport { EditorCommand } from '@t/spec';\n\nexport class Emph extends Mark {\n  get name() {\n    return 'emph';\n  }\n\n  get schema() {\n    const parseDOM = ['i', 'em'].map((tag) => {\n      return {\n        tag,\n        getAttrs(dom: Node | string) {\n          const rawHTML = (dom as HTMLElement).getAttribute('data-raw-html');\n\n          return {\n            ...(rawHTML && { rawHTML }),\n          };\n        },\n      };\n    });\n\n    return {\n      attrs: {\n        rawHTML: { default: null },\n        ...getDefaultCustomAttrs(),\n      },\n      parseDOM,\n      toDOM({ attrs }: ProsemirrorMark): DOMOutputSpec {\n        return [attrs.rawHTML || 'em', getCustomAttrs(attrs)];\n      },\n    };\n  }\n\n  private italic(): EditorCommand {\n    return () => (state, dispatch) => toggleMark(state.schema.marks.emph)(state, dispatch);\n  }\n\n  commands() {\n    return { italic: this.italic() };\n  }\n\n  keymaps() {\n    const italicCommand = this.italic()();\n\n    return {\n      'Mod-i': italicCommand,\n      'Mod-I': italicCommand,\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/marks/link.ts",
    "content": "import { Mark as ProsemirrorMark, DOMOutputSpec } from 'prosemirror-model';\nimport { toggleMark } from 'prosemirror-commands';\n\nimport Mark from '@/spec/mark';\nimport { escapeXml } from '@/utils/common';\nimport { sanitizeHTML } from '@/sanitizer/htmlSanitizer';\nimport { createTextNode } from '@/helper/manipulation';\nimport { getCustomAttrs, getDefaultCustomAttrs } from '@/wysiwyg/helper/node';\n\nimport { EditorCommand } from '@t/spec';\nimport { LinkAttributes } from '@t/editor';\n\nexport class Link extends Mark {\n  private linkAttributes: LinkAttributes;\n\n  constructor(linkAttributes: LinkAttributes) {\n    super();\n    this.linkAttributes = linkAttributes;\n  }\n\n  get name() {\n    return 'link';\n  }\n\n  get schema() {\n    return {\n      attrs: {\n        linkUrl: { default: '' },\n        title: { default: null },\n        rawHTML: { default: null },\n        ...getDefaultCustomAttrs(),\n      },\n      inclusive: false,\n      parseDOM: [\n        {\n          tag: 'a[href]',\n          getAttrs(dom: Node | string) {\n            const sanitizedDOM = sanitizeHTML<DocumentFragment>(dom, { RETURN_DOM_FRAGMENT: true })\n              .firstChild as HTMLElement;\n            const href = sanitizedDOM.getAttribute('href') || '';\n            const title = sanitizedDOM.getAttribute('title') || '';\n            const rawHTML = sanitizedDOM.getAttribute('data-raw-html');\n\n            return {\n              linkUrl: href,\n              title,\n              ...(rawHTML && { rawHTML }),\n            };\n          },\n        },\n      ],\n      toDOM: ({ attrs }: ProsemirrorMark): DOMOutputSpec => [\n        attrs.rawHTML || 'a',\n        {\n          href: escapeXml(attrs.linkUrl),\n          ...this.linkAttributes,\n          ...getCustomAttrs(attrs),\n        },\n      ],\n    };\n  }\n\n  private addLink(): EditorCommand {\n    return (payload) => (state, dispatch) => {\n      const { linkUrl, linkText = '' } = payload!;\n      const { schema, tr, selection } = state;\n      const { empty, from, to } = selection;\n\n      if (from && to && linkUrl) {\n        const attrs = { linkUrl };\n        const mark = schema.mark('link', attrs);\n\n        if (empty && linkText) {\n          const node = createTextNode(schema, linkText, mark);\n\n          tr.replaceRangeWith(from, to, node);\n        } else {\n          tr.addMark(from, to, mark);\n        }\n\n        dispatch!(tr.scrollIntoView());\n\n        return true;\n      }\n\n      return false;\n    };\n  }\n\n  private toggleLink(): EditorCommand {\n    return (payload) => (state, dispatch) =>\n      toggleMark(state.schema.marks.link, payload)(state, dispatch);\n  }\n\n  commands() {\n    return {\n      addLink: this.addLink(),\n      toggleLink: this.toggleLink(),\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/marks/strike.ts",
    "content": "import { Mark as ProsemirrorMark, DOMOutputSpec } from 'prosemirror-model';\nimport { toggleMark } from 'prosemirror-commands';\n\nimport Mark from '@/spec/mark';\nimport { getCustomAttrs, getDefaultCustomAttrs } from '@/wysiwyg/helper/node';\n\nimport { EditorCommand } from '@t/spec';\n\nexport class Strike extends Mark {\n  get name() {\n    return 'strike';\n  }\n\n  get schema() {\n    const parseDOM = ['s', 'del'].map((tag) => {\n      return {\n        tag,\n        getAttrs(dom: Node | string) {\n          const rawHTML = (dom as HTMLElement).getAttribute('data-raw-html');\n\n          return {\n            ...(rawHTML && { rawHTML }),\n          };\n        },\n      };\n    });\n\n    return {\n      attrs: {\n        rawHTML: { default: null },\n        ...getDefaultCustomAttrs(),\n      },\n      parseDOM,\n      toDOM({ attrs }: ProsemirrorMark): DOMOutputSpec {\n        return [attrs.rawHTML || 'del', getCustomAttrs(attrs)];\n      },\n    };\n  }\n\n  commands(): EditorCommand {\n    return () => (state, dispatch) => toggleMark(state.schema.marks.strike)(state, dispatch);\n  }\n\n  keymaps() {\n    const strikeCommand = this.commands()();\n\n    return {\n      'Mod-s': strikeCommand,\n      'Mod-S': strikeCommand,\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/marks/strong.ts",
    "content": "import { Mark as ProsemirrorMark, DOMOutputSpec } from 'prosemirror-model';\nimport { toggleMark } from 'prosemirror-commands';\n\nimport Mark from '@/spec/mark';\nimport { getCustomAttrs, getDefaultCustomAttrs } from '@/wysiwyg/helper/node';\n\nimport { EditorCommand } from '@t/spec';\n\nexport class Strong extends Mark {\n  get name() {\n    return 'strong';\n  }\n\n  get schema() {\n    const parseDOM = ['b', 'strong'].map((tag) => {\n      return {\n        tag,\n        getAttrs(dom: Node | string) {\n          const rawHTML = (dom as HTMLElement).getAttribute('data-raw-html');\n\n          return {\n            ...(rawHTML && { rawHTML }),\n          };\n        },\n      };\n    });\n\n    return {\n      attrs: {\n        rawHTML: { default: null },\n        ...getDefaultCustomAttrs(),\n      },\n      parseDOM,\n      toDOM({ attrs }: ProsemirrorMark): DOMOutputSpec {\n        return [attrs.rawHTML || 'strong', getCustomAttrs(attrs)];\n      },\n    };\n  }\n\n  private bold(): EditorCommand {\n    return () => (state, dispatch) => toggleMark(state.schema.marks.strong)(state, dispatch);\n  }\n\n  commands() {\n    return { bold: this.bold() };\n  }\n\n  keymaps() {\n    const boldCommand = this.bold()();\n\n    return {\n      'Mod-b': boldCommand,\n      'Mod-B': boldCommand,\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/blockQuote.ts",
    "content": "import { DOMOutputSpec, ProsemirrorNode } from 'prosemirror-model';\nimport { wrapIn } from 'prosemirror-commands';\n\nimport NodeSchema from '@/spec/node';\nimport {\n  createDOMInfoParsedRawHTML,\n  getCustomAttrs,\n  getDefaultCustomAttrs,\n} from '@/wysiwyg/helper/node';\n\nimport { EditorCommand } from '@t/spec';\n\nexport class BlockQuote extends NodeSchema {\n  get name() {\n    return 'blockQuote';\n  }\n\n  get schema() {\n    return {\n      attrs: {\n        rawHTML: { default: null },\n        ...getDefaultCustomAttrs(),\n      },\n      content: 'block+',\n      group: 'block',\n      parseDOM: [createDOMInfoParsedRawHTML('blockquote')],\n      toDOM({ attrs }: ProsemirrorNode): DOMOutputSpec {\n        return ['blockquote', getCustomAttrs(attrs), 0];\n      },\n    };\n  }\n\n  commands(): EditorCommand {\n    return () => (state, dispatch) => wrapIn(state.schema.nodes.blockQuote)(state, dispatch);\n  }\n\n  keymaps() {\n    const blockQutoeCommand = this.commands()();\n\n    return {\n      'Alt-q': blockQutoeCommand,\n      'Alt-Q': blockQutoeCommand,\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/bulletList.ts",
    "content": "import { DOMOutputSpec, ProsemirrorNode } from 'prosemirror-model';\n\nimport NodeSchema from '@/spec/node';\nimport { getWwCommands } from '@/commands/wwCommands';\nimport {\n  createDOMInfoParsedRawHTML,\n  getCustomAttrs,\n  getDefaultCustomAttrs,\n} from '@/wysiwyg/helper/node';\nimport { changeList, toggleTask } from '@/wysiwyg/command/list';\n\nimport { Command } from 'prosemirror-commands';\n\nexport class BulletList extends NodeSchema {\n  get name() {\n    return 'bulletList';\n  }\n\n  get schema() {\n    return {\n      content: 'listItem+',\n      group: 'block',\n      attrs: {\n        rawHTML: { default: null },\n        ...getDefaultCustomAttrs(),\n      },\n      parseDOM: [createDOMInfoParsedRawHTML('ul')],\n      toDOM({ attrs }: ProsemirrorNode): DOMOutputSpec {\n        return ['ul', getCustomAttrs(attrs), 0];\n      },\n    };\n  }\n\n  private changeList(): Command {\n    return (state, dispatch) => changeList(state.schema.nodes.bulletList)(state, dispatch);\n  }\n\n  commands() {\n    return {\n      bulletList: this.changeList,\n      taskList: toggleTask,\n    };\n  }\n\n  keymaps() {\n    const bulletListCommand = this.changeList();\n    const { indent, outdent } = getWwCommands();\n\n    return {\n      'Mod-u': bulletListCommand,\n      'Mod-U': bulletListCommand,\n      Tab: indent(),\n      'Shift-Tab': outdent(),\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/codeBlock.ts",
    "content": "import { ProsemirrorNode, DOMOutputSpec } from 'prosemirror-model';\nimport { setBlockType, Command } from 'prosemirror-commands';\n\nimport { addParagraph } from '@/helper/manipulation';\nimport { between, last } from '@/utils/common';\nimport NodeSchema from '@/spec/node';\nimport { getCustomAttrs, getDefaultCustomAttrs } from '@/wysiwyg/helper/node';\n\nimport { EditorCommand } from '@t/spec';\n\nexport class CodeBlock extends NodeSchema {\n  get name() {\n    return 'codeBlock';\n  }\n\n  get schema() {\n    return {\n      content: 'text*',\n      group: 'block',\n      attrs: {\n        language: { default: null },\n        rawHTML: { default: null },\n        ...getDefaultCustomAttrs(),\n      },\n      code: true,\n      defining: true,\n      marks: '',\n      parseDOM: [\n        {\n          tag: 'pre',\n          preserveWhitespace: 'full' as const,\n          getAttrs(dom: Node | string) {\n            const rawHTML = (dom as HTMLElement).getAttribute('data-raw-html');\n            const child = (dom as HTMLElement).firstElementChild;\n\n            return {\n              language: child?.getAttribute('data-language') || null,\n              ...(rawHTML && { rawHTML }),\n            };\n          },\n        },\n      ],\n      toDOM({ attrs }: ProsemirrorNode): DOMOutputSpec {\n        return [\n          attrs.rawHTML || 'pre',\n          ['code', { 'data-language': attrs.language, ...getCustomAttrs(attrs) }, 0],\n        ];\n      },\n    };\n  }\n\n  commands(): EditorCommand {\n    return () => (state, dispatch) => setBlockType(state.schema.nodes.codeBlock)(state, dispatch);\n  }\n\n  moveCursor(direction: 'up' | 'down'): Command {\n    return (state, dispatch) => {\n      const { tr, doc, schema } = state;\n      const { $from } = state.selection;\n      const { view } = this.context;\n\n      if (view!.endOfTextblock(direction) && $from.node().type.name === 'codeBlock') {\n        const lines: string[] = $from.parent.textContent.split('\\n');\n\n        const offset = direction === 'up' ? $from.start() : $from.end();\n        const range =\n          direction === 'up'\n            ? [offset, lines[0].length + offset]\n            : [offset - last(lines).length, offset];\n        const pos = doc.resolve(direction === 'up' ? $from.before() : $from.after());\n        const node = direction === 'up' ? pos.nodeBefore : pos.nodeAfter;\n\n        if (between($from.pos, range[0], range[1]) && !node) {\n          const newTr = addParagraph(tr, pos, schema);\n\n          if (newTr) {\n            dispatch!(newTr);\n            return true;\n          }\n        }\n      }\n\n      return false;\n    };\n  }\n\n  keymaps() {\n    const codeCommand = this.commands()();\n\n    return {\n      'Shift-Mod-p': codeCommand,\n      'Shift-Mod-P': codeCommand,\n      ArrowUp: this.moveCursor('up'),\n      ArrowDown: this.moveCursor('down'),\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/customBlock.ts",
    "content": "import { DOMOutputSpec, ProsemirrorNode } from 'prosemirror-model';\nimport { setBlockType } from 'prosemirror-commands';\nimport NodeSchema from '@/spec/node';\nimport { EditorCommand } from '@t/spec';\n\nexport class CustomBlock extends NodeSchema {\n  get name() {\n    return 'customBlock';\n  }\n\n  get schema() {\n    return {\n      content: 'text*',\n      group: 'block',\n      attrs: {\n        info: { default: null },\n      },\n      atom: true,\n      code: true,\n      defining: true,\n      parseDOM: [\n        {\n          tag: 'div[data-custom-info]',\n          getAttrs(dom: Node | string) {\n            const info = (dom as HTMLElement).getAttribute('data-custom-info');\n\n            return { info };\n          },\n        },\n      ],\n      toDOM({ attrs }: ProsemirrorNode): DOMOutputSpec {\n        return ['div', { 'data-custom-info': attrs.info || null }, 0];\n      },\n    };\n  }\n\n  commands(): EditorCommand {\n    return (payload) => (state, dispatch) =>\n      payload?.info\n        ? setBlockType(state.schema.nodes.customBlock, payload)(state, dispatch)\n        : false;\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/doc.ts",
    "content": "import Node from '@/spec/node';\n\nexport class Doc extends Node {\n  get name() {\n    return 'doc';\n  }\n\n  get schema() {\n    return {\n      content: 'block+',\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/frontMatter.ts",
    "content": "import { DOMOutputSpec } from 'prosemirror-model';\nimport { exitCode } from 'prosemirror-commands';\n\nimport NodeSchema from '@/spec/node';\n\nimport { EditorCommand } from '@t/spec';\n\nexport class FrontMatter extends NodeSchema {\n  get name() {\n    return 'frontMatter';\n  }\n\n  get schema() {\n    return {\n      content: 'text*',\n      group: 'block',\n      code: true,\n      defining: true,\n      parseDOM: [\n        {\n          preserveWhitespace: 'full' as const,\n          tag: 'div[data-front-matter]',\n        },\n      ],\n      toDOM(): DOMOutputSpec {\n        return ['div', { 'data-front-matter': 'true' }, 0];\n      },\n    };\n  }\n\n  commands(): EditorCommand {\n    return () => (state, dispatch, view) => {\n      const { $from } = state.selection;\n\n      if (view!.endOfTextblock('down') && $from.node().type.name === 'frontMatter') {\n        return exitCode(state, dispatch);\n      }\n\n      return false;\n    };\n  }\n\n  keymaps() {\n    return {\n      Enter: this.commands()(),\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/heading.ts",
    "content": "import { ProsemirrorNode, DOMOutputSpec } from 'prosemirror-model';\nimport { setBlockType } from 'prosemirror-commands';\n\nimport NodeSchema from '@/spec/node';\nimport { getCustomAttrs, getDefaultCustomAttrs } from '@/wysiwyg/helper/node';\n\nimport { EditorCommand } from '@t/spec';\n\nexport class Heading extends NodeSchema {\n  get name() {\n    return 'heading';\n  }\n\n  get levels() {\n    return [1, 2, 3, 4, 5, 6];\n  }\n\n  get schema() {\n    const parseDOM = this.levels.map((level) => {\n      return {\n        tag: `h${level}`,\n        getAttrs(dom: Node | string) {\n          const rawHTML = (dom as HTMLElement).getAttribute('data-raw-html');\n\n          return {\n            level,\n            ...(rawHTML && { rawHTML }),\n          };\n        },\n      };\n    });\n\n    return {\n      attrs: {\n        level: { default: 1 },\n        headingType: { default: 'atx' },\n        rawHTML: { default: null },\n        ...getDefaultCustomAttrs(),\n      },\n      content: 'inline*',\n      group: 'block',\n      defining: true,\n      parseDOM,\n      toDOM({ attrs }: ProsemirrorNode): DOMOutputSpec {\n        return [`h${attrs.level}`, getCustomAttrs(attrs), 0];\n      },\n    };\n  }\n\n  commands(): EditorCommand {\n    return (payload) => (state, dispatch) => {\n      const nodeType = state.schema.nodes[payload!.level ? 'heading' : 'paragraph'];\n\n      return setBlockType(nodeType, payload)(state, dispatch);\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/html.ts",
    "content": "import {\n  ProsemirrorNode,\n  Mark as ProsemirrorMark,\n  DOMOutputSpec,\n  NodeSpec,\n  MarkSpec,\n} from 'prosemirror-model';\nimport { MdNode } from '@toast-ui/toastmark';\nimport toArray from 'tui-code-snippet/collection/toArray';\nimport { Sanitizer, HTMLSchemaMap, CustomHTMLRenderer } from '@t/editor';\nimport { ToDOMAdaptor } from '@t/convertor';\nimport { registerTagWhitelistIfPossible } from '@/sanitizer/htmlSanitizer';\nimport { reHTMLTag, ATTRIBUTE } from '@/utils/constants';\n\nexport function getChildrenHTML(node: MdNode, typeName: string) {\n  return node\n    .literal!.replace(new RegExp(`(<\\\\s*${typeName}[^>]*>)|(</${typeName}\\\\s*[>])`, 'ig'), '')\n    .trim();\n}\n\nexport function getHTMLAttrsByHTMLString(html: string) {\n  html = html.match(reHTMLTag)![0];\n  const attrs = html.match(new RegExp(ATTRIBUTE, 'g'));\n\n  return attrs\n    ? attrs.reduce<Record<string, string | null>>((acc, attr) => {\n        const [name, ...values] = attr.trim().split('=');\n\n        if (values.length) {\n          acc[name] = values.join('=').replace(/'|\"/g, '').trim();\n        }\n\n        return acc;\n      }, {})\n    : {};\n}\n\nfunction getHTMLAttrs(dom: HTMLElement) {\n  return toArray(dom.attributes).reduce<Record<string, string | null>>((acc, attr) => {\n    acc[attr.nodeName] = attr.nodeValue;\n    return acc;\n  }, {});\n}\n\nexport function sanitizeDOM(\n  node: ProsemirrorNode | ProsemirrorMark,\n  typeName: string,\n  sanitizer: Sanitizer,\n  wwToDOMAdaptor: ToDOMAdaptor\n) {\n  let dom = wwToDOMAdaptor.getToDOMNode(typeName)!(node) as HTMLElement;\n  const html = sanitizer(dom.outerHTML);\n  const container = document.createElement('div');\n\n  container.innerHTML = html;\n  dom = container.firstChild as HTMLElement;\n\n  const htmlAttrs = getHTMLAttrs(dom);\n\n  return { dom, htmlAttrs };\n}\n\nconst schemaFactory = {\n  htmlBlock(typeName: string, sanitizeHTML: Sanitizer, wwToDOMAdaptor: ToDOMAdaptor): NodeSpec {\n    return {\n      atom: true,\n      content: 'block+',\n      group: 'block',\n      attrs: {\n        htmlAttrs: { default: {} },\n        childrenHTML: { default: '' },\n        htmlBlock: { default: true },\n      },\n      parseDOM: [\n        {\n          tag: typeName,\n          getAttrs(dom: Node | string) {\n            return {\n              htmlAttrs: getHTMLAttrs(dom as HTMLElement),\n              childrenHTML: (dom as HTMLElement).innerHTML,\n            };\n          },\n        },\n      ],\n      toDOM(node: ProsemirrorNode): DOMOutputSpec {\n        const { dom, htmlAttrs } = sanitizeDOM(node, typeName, sanitizeHTML, wwToDOMAdaptor);\n\n        htmlAttrs.class = htmlAttrs.class ? `${htmlAttrs.class} html-block` : 'html-block';\n\n        return [typeName, htmlAttrs, ...toArray(dom.childNodes)];\n      },\n    };\n  },\n  htmlInline(typeName: string, sanitizeHTML: Sanitizer, wwToDOMAdaptor: ToDOMAdaptor): MarkSpec {\n    return {\n      attrs: {\n        htmlAttrs: { default: {} },\n        htmlInline: { default: true },\n      },\n      parseDOM: [\n        {\n          tag: typeName,\n          getAttrs(dom: Node | string) {\n            return {\n              htmlAttrs: getHTMLAttrs(dom as HTMLElement),\n            };\n          },\n        },\n      ],\n      toDOM(node: ProsemirrorMark): DOMOutputSpec {\n        const { htmlAttrs } = sanitizeDOM(node, typeName, sanitizeHTML, wwToDOMAdaptor);\n\n        return [typeName, htmlAttrs, 0];\n      },\n    };\n  },\n};\n\nexport function createHTMLSchemaMap(\n  convertorMap: CustomHTMLRenderer,\n  sanitizeHTML: Sanitizer,\n  wwToDOMAdaptor: ToDOMAdaptor\n): HTMLSchemaMap {\n  const htmlSchemaMap: HTMLSchemaMap = { nodes: {}, marks: {} };\n\n  (['htmlBlock', 'htmlInline'] as const).forEach((htmlType) => {\n    if (convertorMap[htmlType]) {\n      Object.keys(convertorMap[htmlType]!).forEach((type) => {\n        const targetType = htmlType === 'htmlBlock' ? 'nodes' : 'marks';\n\n        // register tag white list for preventing to remove the html in sanitizer\n        registerTagWhitelistIfPossible(type);\n        htmlSchemaMap[targetType][type] = schemaFactory[htmlType](\n          type,\n          sanitizeHTML,\n          wwToDOMAdaptor\n        );\n      });\n    }\n  });\n\n  return htmlSchemaMap;\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/htmlComment.ts",
    "content": "import { DOMOutputSpec } from 'prosemirror-model';\nimport { exitCode } from 'prosemirror-commands';\n\nimport NodeSchema from '@/spec/node';\n\nimport { EditorCommand } from '@t/spec';\n\nexport class HTMLComment extends NodeSchema {\n  get name() {\n    return 'htmlComment';\n  }\n\n  get schema() {\n    return {\n      content: 'text*',\n      group: 'block',\n      code: true,\n      defining: true,\n      parseDOM: [{ preserveWhitespace: 'full' as const, tag: 'div[data-html-comment]' }],\n      toDOM(): DOMOutputSpec {\n        return ['div', { 'data-html-comment': 'true' }, 0];\n      },\n    };\n  }\n\n  commands(): EditorCommand {\n    return () => (state, dispatch, view) => {\n      const { $from } = state.selection;\n\n      if (view!.endOfTextblock('down') && $from.node().type.name === 'htmlComment') {\n        return exitCode(state, dispatch);\n      }\n\n      return false;\n    };\n  }\n\n  keymaps() {\n    return {\n      Enter: this.commands()(),\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/image.ts",
    "content": "import { ProsemirrorNode, DOMOutputSpec } from 'prosemirror-model';\n\nimport NodeSchema from '@/spec/node';\nimport { escapeXml } from '@/utils/common';\nimport { sanitizeHTML } from '@/sanitizer/htmlSanitizer';\n\nimport { EditorCommand } from '@t/spec';\nimport { getCustomAttrs, getDefaultCustomAttrs } from '../helper/node';\n\nexport class Image extends NodeSchema {\n  get name() {\n    return 'image';\n  }\n\n  get schema() {\n    return {\n      inline: true,\n      attrs: {\n        imageUrl: { default: '' },\n        altText: { default: null },\n        rawHTML: { default: null },\n        ...getDefaultCustomAttrs(),\n      },\n      group: 'inline',\n      selectable: false,\n      parseDOM: [\n        {\n          tag: 'img[src]',\n          getAttrs(dom: Node | string) {\n            const sanitizedDOM = sanitizeHTML<DocumentFragment>(dom, { RETURN_DOM_FRAGMENT: true })\n              .firstChild as HTMLElement;\n            const imageUrl = sanitizedDOM.getAttribute('src') || '';\n            const rawHTML = sanitizedDOM.getAttribute('data-raw-html');\n            const altText = sanitizedDOM.getAttribute('alt');\n\n            return {\n              imageUrl,\n              altText,\n              ...(rawHTML && { rawHTML }),\n            };\n          },\n        },\n      ],\n      toDOM({ attrs }: ProsemirrorNode): DOMOutputSpec {\n        return [\n          attrs.rawHTML || 'img',\n          {\n            src: escapeXml(attrs.imageUrl),\n            ...(attrs.altText && { alt: attrs.altText }),\n            ...getCustomAttrs(attrs),\n          },\n        ];\n      },\n    };\n  }\n\n  private addImage(): EditorCommand {\n    return (payload) => ({ schema, tr }, dispatch) => {\n      const { imageUrl, altText } = payload!;\n\n      if (!imageUrl) {\n        return false;\n      }\n\n      const node = schema.nodes.image.createAndFill({\n        imageUrl,\n        ...(altText && { altText }),\n      });\n\n      dispatch!(tr.replaceSelectionWith(node!).scrollIntoView());\n\n      return true;\n    };\n  }\n\n  commands() {\n    return {\n      addImage: this.addImage(),\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/listItem.ts",
    "content": "import type { Command } from 'prosemirror-commands';\nimport type { ProsemirrorNode, DOMOutputSpec } from 'prosemirror-model';\n\nimport NodeSchema from '@/spec/node';\nimport { splitListItem } from '@/wysiwyg/command/list';\n\nexport class ListItem extends NodeSchema {\n  get name() {\n    return 'listItem';\n  }\n\n  get schema() {\n    return {\n      content: 'paragraph block*',\n      selectable: false,\n      attrs: {\n        task: { default: false },\n        checked: { default: false },\n        rawHTML: { default: null },\n      },\n      defining: true,\n      parseDOM: [\n        {\n          tag: 'li',\n          getAttrs(dom: Node | string) {\n            const rawHTML = (dom as HTMLElement).getAttribute('data-raw-html');\n\n            return {\n              task: (dom as HTMLElement).hasAttribute('data-task'),\n              checked: (dom as HTMLElement).hasAttribute('data-task-checked'),\n              ...(rawHTML && { rawHTML }),\n            };\n          },\n        },\n      ],\n      toDOM({ attrs }: ProsemirrorNode): DOMOutputSpec {\n        const { task, checked } = attrs;\n\n        if (!task) {\n          return [attrs.rawHTML || 'li', 0];\n        }\n\n        const classNames = ['task-list-item'];\n\n        if (checked) {\n          classNames.push('checked');\n        }\n\n        return [\n          attrs.rawHTML || 'li',\n          {\n            class: classNames.join(' '),\n            'data-task': task,\n            ...(checked && { 'data-task-checked': checked }),\n          },\n          0,\n        ];\n      },\n    };\n  }\n\n  private liftToPrevListItem(): Command {\n    return (state, dispatch) => {\n      const { selection, tr, schema } = state;\n      const { $from, empty } = selection;\n      const { listItem } = schema.nodes;\n      const { parent } = $from;\n      const listItemParent = $from.node(-1);\n\n      if (empty && !parent.childCount && listItemParent.type === listItem) {\n        // move to previous sibling list item when the current list item is not top list item\n        if ($from.index(-2) >= 1) {\n          // should subtract '1' for considering tag length(<li>)\n          tr.delete($from.start(-1) - 1, $from.end(-1));\n          dispatch!(tr);\n          return true;\n        }\n\n        const grandParentListItem = $from.node(-3);\n\n        // move to parent list item when the current list item is top list item\n        if (grandParentListItem.type === listItem) {\n          // should subtract '1' for considering tag length(<ul>)\n          tr.delete($from.start(-2) - 1, $from.end(-1));\n          dispatch!(tr);\n          return true;\n        }\n      }\n      return false;\n    };\n  }\n\n  keymaps() {\n    const split: Command = (state, dispatch) =>\n      splitListItem(state.schema.nodes.listItem)(state, dispatch);\n\n    return {\n      Backspace: this.liftToPrevListItem(),\n      Enter: split,\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/orderedList.ts",
    "content": "import { ProsemirrorNode, DOMOutputSpec } from 'prosemirror-model';\n\nimport NodeSchema from '@/spec/node';\nimport { getWwCommands } from '@/commands/wwCommands';\nimport { changeList } from '@/wysiwyg/command/list';\n\nimport { EditorCommand } from '@t/spec';\nimport { getDefaultCustomAttrs, getCustomAttrs } from '@/wysiwyg/helper/node';\n\nexport class OrderedList extends NodeSchema {\n  get name() {\n    return 'orderedList';\n  }\n\n  get schema() {\n    return {\n      content: 'listItem+',\n      group: 'block',\n      attrs: {\n        order: { default: 1 },\n        rawHTML: { default: null },\n        ...getDefaultCustomAttrs(),\n      },\n      parseDOM: [\n        {\n          tag: 'ol',\n          getAttrs(dom: Node | string) {\n            const start = (dom as HTMLElement).getAttribute('start');\n            const rawHTML = (dom as HTMLElement).getAttribute('data-raw-html');\n\n            return {\n              order: (dom as HTMLElement).hasAttribute('start') ? Number(start) : 1,\n              ...(rawHTML && { rawHTML }),\n            };\n          },\n        },\n      ],\n      toDOM({ attrs }: ProsemirrorNode): DOMOutputSpec {\n        return [\n          attrs.rawHTML || 'ol',\n          { start: attrs.order === 1 ? null : attrs.order, ...getCustomAttrs(attrs) },\n          0,\n        ];\n      },\n    };\n  }\n\n  commands(): EditorCommand {\n    return () => (state, dispatch) => changeList(state.schema.nodes.orderedList)(state, dispatch);\n  }\n\n  keymaps() {\n    const orderedListCommand = this.commands()();\n    const { indent, outdent } = getWwCommands();\n\n    return {\n      'Mod-o': orderedListCommand,\n      'Mod-O': orderedListCommand,\n      Tab: indent(),\n      'Shift-Tab': outdent(),\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/paragraph.ts",
    "content": "import { DOMOutputSpec, ProsemirrorNode } from 'prosemirror-model';\n\nimport NodeSchema from '@/spec/node';\nimport { getDefaultCustomAttrs, getCustomAttrs } from '@/wysiwyg/helper/node';\n\nexport class Paragraph extends NodeSchema {\n  get name() {\n    return 'paragraph';\n  }\n\n  get schema() {\n    return {\n      content: 'inline*',\n      group: 'block',\n      attrs: {\n        ...getDefaultCustomAttrs(),\n      },\n      parseDOM: [{ tag: 'p' }],\n      toDOM({ attrs }: ProsemirrorNode): DOMOutputSpec {\n        return ['p', getCustomAttrs(attrs), 0];\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/table.ts",
    "content": "import { DOMOutputSpec, ProsemirrorNode } from 'prosemirror-model';\nimport { TextSelection, Transaction } from 'prosemirror-state';\nimport { Command } from 'prosemirror-commands';\n\nimport NodeSchema from '@/spec/node';\nimport {\n  isInTableNode,\n  findNodeBy,\n  createDOMInfoParsedRawHTML,\n  getCustomAttrs,\n  getDefaultCustomAttrs,\n} from '@/wysiwyg/helper/node';\n\nimport {\n  createTableHeadRow,\n  createTableBodyRows,\n  createDummyCells,\n  getResolvedSelection,\n  getRowAndColumnCount,\n  setAttrs,\n} from '@/wysiwyg/helper/table';\nimport {\n  canBeOutOfTable,\n  canMoveBetweenCells,\n  canSelectTableNode,\n  selectNode,\n  addParagraphBeforeTable,\n  addParagraphAfterTable,\n  moveToCell,\n} from '@/wysiwyg/command/table';\n\nimport { createTextSelection } from '@/helper/manipulation';\n\nimport { EditorCommand } from '@t/spec';\nimport { ColumnAlign } from '@t/wysiwyg';\nimport { SelectionInfo, TableOffsetMap } from '@/wysiwyg/helper/tableOffsetMap';\n\ninterface AddTablePayload {\n  rowCount: number;\n  columnCount: number;\n  data: string[];\n}\n\ninterface AlignColumnPayload {\n  align: ColumnAlign;\n}\n\n// eslint-disable-next-line no-shadow\nexport const enum Direction {\n  LEFT = 'left',\n  RIGHT = 'right',\n  UP = 'up',\n  DOWN = 'down',\n}\n\ntype ColDirection = Direction.LEFT | Direction.RIGHT;\ntype RowDirection = Direction.UP | Direction.DOWN;\n\nfunction getTargetRowInfo(\n  direction: RowDirection,\n  map: TableOffsetMap,\n  selectionInfo: SelectionInfo\n) {\n  let targetRowIdx: number;\n  let insertColIdx: number;\n  let nodeSize: number;\n\n  if (direction === Direction.UP) {\n    targetRowIdx = selectionInfo.startRowIdx;\n    insertColIdx = 0;\n    nodeSize = -1;\n  } else {\n    targetRowIdx = selectionInfo.endRowIdx;\n    insertColIdx = map.totalColumnCount - 1;\n    nodeSize = map.getCellInfo(targetRowIdx, insertColIdx).nodeSize + 1;\n  }\n  return { targetRowIdx, insertColIdx, nodeSize };\n}\n\nfunction getRowRanges(map: TableOffsetMap, rowIdx: number, totalColumnCount: number) {\n  const { offset: startOffset } = map.getCellInfo(rowIdx, 0);\n  const { offset, nodeSize } = map.getCellInfo(rowIdx, totalColumnCount - 1);\n\n  return { from: startOffset, to: offset + nodeSize };\n}\n\nexport class Table extends NodeSchema {\n  get name() {\n    return 'table';\n  }\n\n  get schema() {\n    return {\n      content: 'tableHead{1} tableBody{1}',\n      group: 'block',\n      attrs: {\n        rawHTML: { default: null },\n        ...getDefaultCustomAttrs(),\n      },\n      parseDOM: [createDOMInfoParsedRawHTML('table')],\n      toDOM({ attrs }: ProsemirrorNode): DOMOutputSpec {\n        return ['table', getCustomAttrs(attrs), 0];\n      },\n    };\n  }\n\n  private addTable(): EditorCommand<AddTablePayload> {\n    return (payload = { rowCount: 2, columnCount: 1, data: [] }) => (state, dispatch) => {\n      const { rowCount, columnCount, data } = payload;\n      const { schema, selection, tr } = state;\n      const { from, to, $from } = selection;\n      const collapsed = from === to;\n\n      if (collapsed && !isInTableNode($from)) {\n        const { tableHead, tableBody } = schema.nodes;\n\n        const theadData = data?.slice(0, columnCount);\n        const tbodyData = data?.slice(columnCount, data.length);\n        const tableHeadRow = createTableHeadRow(columnCount, schema, theadData);\n        const tableBodyRows = createTableBodyRows(rowCount - 1, columnCount, schema, tbodyData);\n        const table = schema.nodes.table.create(null, [\n          tableHead.create(null, tableHeadRow),\n          tableBody.create(null, tableBodyRows),\n        ]);\n\n        dispatch!(tr.replaceSelectionWith(table));\n\n        return true;\n      }\n\n      return false;\n    };\n  }\n\n  private removeTable(): EditorCommand {\n    return () => (state, dispatch) => {\n      const { selection, tr } = state;\n      const map = TableOffsetMap.create(selection.$anchor)!;\n\n      if (map) {\n        const { tableStartOffset, tableEndOffset } = map;\n        const startOffset = tableStartOffset - 1;\n        const cursorPos = createTextSelection(tr.delete(startOffset, tableEndOffset), startOffset);\n\n        dispatch!(tr.setSelection(cursorPos));\n        return true;\n      }\n      return false;\n    };\n  }\n\n  private addColumn(direction: ColDirection): EditorCommand {\n    return () => (state, dispatch) => {\n      const { selection, tr, schema } = state;\n      const { anchor, head } = getResolvedSelection(selection);\n\n      if (anchor && head) {\n        const map = TableOffsetMap.create(anchor)!;\n        const selectionInfo = map.getRectOffsets(anchor, head);\n\n        const targetColIdx =\n          direction === Direction.LEFT ? selectionInfo.startColIdx : selectionInfo.endColIdx + 1;\n\n        const { columnCount } = getRowAndColumnCount(selectionInfo);\n        const { totalRowCount } = map;\n\n        for (let rowIdx = 0; rowIdx < totalRowCount; rowIdx += 1) {\n          const cells = createDummyCells(columnCount, rowIdx, schema);\n\n          tr.insert(tr.mapping.map(map.posAt(rowIdx, targetColIdx)), cells);\n        }\n        dispatch!(tr);\n        return true;\n      }\n      return false;\n    };\n  }\n\n  private removeColumn(): EditorCommand {\n    return () => (state, dispatch) => {\n      const { selection, tr } = state;\n      const { anchor, head } = getResolvedSelection(selection);\n\n      if (anchor && head) {\n        const map = TableOffsetMap.create(anchor)!;\n        const selectionInfo = map.getRectOffsets(anchor, head);\n\n        const { totalColumnCount, totalRowCount } = map;\n        const { columnCount } = getRowAndColumnCount(selectionInfo);\n        const selectedAllColumn = columnCount === totalColumnCount;\n\n        if (selectedAllColumn) {\n          return false;\n        }\n\n        const { startColIdx, endColIdx } = selectionInfo;\n        const mapStart = tr.mapping.maps.length;\n\n        for (let rowIdx = 0; rowIdx < totalRowCount; rowIdx += 1) {\n          for (let colIdx = endColIdx; colIdx >= startColIdx; colIdx -= 1) {\n            const { offset, nodeSize } = map.getCellInfo(rowIdx, colIdx);\n\n            const from = tr.mapping.slice(mapStart).map(offset);\n            const to = from + nodeSize;\n\n            tr.delete(from, to);\n          }\n        }\n        dispatch!(tr);\n        return true;\n      }\n      return false;\n    };\n  }\n\n  private addRow(direction: Direction.UP | Direction.DOWN): EditorCommand {\n    return () => (state, dispatch) => {\n      const { selection, schema, tr } = state;\n      const { anchor, head } = getResolvedSelection(selection);\n\n      if (anchor && head) {\n        const map = TableOffsetMap.create(anchor)!;\n        const { totalColumnCount } = map;\n        const selectionInfo = map.getRectOffsets(anchor, head);\n        const { rowCount } = getRowAndColumnCount(selectionInfo);\n        const { targetRowIdx, insertColIdx, nodeSize } = getTargetRowInfo(\n          direction,\n          map,\n          selectionInfo\n        );\n        const selectedThead = targetRowIdx === 0;\n\n        if (!selectedThead) {\n          const rows: ProsemirrorNode[] = [];\n          const from = tr.mapping.map(map.posAt(targetRowIdx, insertColIdx)) + nodeSize;\n          let cells: ProsemirrorNode[] = [];\n\n          for (let colIdx = 0; colIdx < totalColumnCount; colIdx += 1) {\n            cells = cells.concat(createDummyCells(1, targetRowIdx, schema));\n          }\n          for (let i = 0; i < rowCount; i += 1) {\n            rows.push(schema.nodes.tableRow.create(null, cells));\n          }\n          dispatch!(tr.insert(from, rows));\n          return true;\n        }\n      }\n      return false;\n    };\n  }\n\n  private removeRow(): EditorCommand {\n    return () => (state, dispatch) => {\n      const { selection, tr } = state;\n      const { anchor, head } = getResolvedSelection(selection);\n\n      if (anchor && head) {\n        const map = TableOffsetMap.create(anchor)!;\n        const { totalRowCount, totalColumnCount } = map;\n        const selectionInfo = map.getRectOffsets(anchor, head);\n        const { rowCount } = getRowAndColumnCount(selectionInfo);\n        const { startRowIdx, endRowIdx } = selectionInfo;\n\n        const selectedThead = startRowIdx === 0;\n        const selectedAllTbodyRow = rowCount === totalRowCount - 1;\n\n        if (selectedAllTbodyRow || selectedThead) {\n          return false;\n        }\n\n        for (let rowIdx = endRowIdx; rowIdx >= startRowIdx; rowIdx -= 1) {\n          const { from, to } = getRowRanges(map, rowIdx, totalColumnCount);\n\n          // delete table row\n          tr.delete(from - 1, to + 1);\n        }\n        dispatch!(tr);\n        return true;\n      }\n\n      return false;\n    };\n  }\n\n  private alignColumn(): EditorCommand<AlignColumnPayload> {\n    return (payload = { align: 'center' }) => (state, dispatch) => {\n      const { align } = payload;\n      const { selection, tr } = state;\n      const { anchor, head } = getResolvedSelection(selection);\n\n      if (anchor && head) {\n        const map = TableOffsetMap.create(anchor)!;\n        const { totalRowCount } = map;\n        const selectionInfo = map.getRectOffsets(anchor, head);\n        const { startColIdx, endColIdx } = selectionInfo;\n\n        for (let rowIdx = 0; rowIdx < totalRowCount; rowIdx += 1) {\n          for (let colIdx = startColIdx; colIdx <= endColIdx; colIdx += 1) {\n            if (!map.extendedRowspan(rowIdx, colIdx) && !map.extendedColspan(rowIdx, colIdx)) {\n              const { node, pos } = map.getNodeAndPos(rowIdx, colIdx);\n              const attrs = setAttrs(node, { align });\n\n              tr.setNodeMarkup(pos, null, attrs);\n            }\n          }\n        }\n        dispatch!(tr);\n        return true;\n      }\n      return false;\n    };\n  }\n\n  private moveToCell(direction: Direction): Command {\n    return (state, dispatch) => {\n      const { selection, tr, schema } = state;\n      const { anchor, head } = getResolvedSelection(selection);\n\n      if (anchor && head) {\n        const map = TableOffsetMap.create(anchor)!;\n        const cellIndex = map.getCellIndex(anchor);\n        let newTr: Transaction | null;\n\n        if (canBeOutOfTable(direction, map, cellIndex)) {\n          // When there is no content before or after the table,\n          // an empty line('paragraph') is created by pressing the arrow keys.\n          newTr = addParagraphAfterTable(tr, map, schema);\n        } else {\n          newTr = moveToCell(direction, tr, cellIndex, map);\n        }\n\n        if (newTr) {\n          dispatch!(newTr);\n          return true;\n        }\n      }\n\n      return false;\n    };\n  }\n\n  private moveInCell(direction: Direction): Command {\n    return (state, dispatch) => {\n      const { selection, tr, doc, schema } = state;\n      const { $from } = selection;\n      const { view } = this.context;\n\n      if (!view.endOfTextblock(direction)) {\n        return false;\n      }\n\n      const cell = findNodeBy(\n        $from,\n        ({ type }) => type.name === 'tableHeadCell' || type.name === 'tableBodyCell'\n      );\n\n      if (cell) {\n        const para = findNodeBy($from, ({ type }) => type.name === 'paragraph');\n        const { depth: cellDepth } = cell;\n\n        if (para && canMoveBetweenCells(direction, [cellDepth, para.depth], $from, doc)) {\n          const { anchor } = getResolvedSelection(selection);\n          const map = TableOffsetMap.create(anchor)!;\n          const cellIndex = map.getCellIndex(anchor);\n\n          let newTr;\n\n          if (canSelectTableNode(direction, map, cellIndex)) {\n            // When the cursor position is at the end of the cell,\n            // the table is selected when the left / right arrow keys are pressed.\n            newTr = selectNode(tr, $from, cellDepth);\n          } else if (canBeOutOfTable(direction, map, cellIndex)) {\n            // When there is no content before or after the table,\n            // an empty line('paragraph') is created by pressing the arrow keys.\n            if (direction === Direction.UP) {\n              newTr = addParagraphBeforeTable(tr, map, schema);\n            } else if (direction === Direction.DOWN) {\n              newTr = addParagraphAfterTable(tr, map, schema);\n            }\n          } else {\n            newTr = moveToCell(direction, tr, cellIndex, map);\n          }\n\n          if (newTr) {\n            dispatch!(newTr);\n\n            return true;\n          }\n        }\n      }\n\n      return false;\n    };\n  }\n\n  private deleteCells(): Command {\n    return (state, dispatch) => {\n      const { schema, selection, tr } = state;\n      const { anchor, head } = getResolvedSelection(selection);\n      const textSelection = selection instanceof TextSelection;\n\n      if (anchor && head && !textSelection) {\n        const map = TableOffsetMap.create(anchor)!;\n        const { startRowIdx, startColIdx, endRowIdx, endColIdx } = map.getRectOffsets(anchor, head);\n\n        for (let rowIdx = startRowIdx; rowIdx <= endRowIdx; rowIdx += 1) {\n          for (let colIdx = startColIdx; colIdx <= endColIdx; colIdx += 1) {\n            if (!map.extendedRowspan(rowIdx, colIdx) && !map.extendedColspan(rowIdx, colIdx)) {\n              const { node, pos } = map.getNodeAndPos(rowIdx, colIdx);\n              const cells = createDummyCells(1, rowIdx, schema, node.attrs);\n\n              tr.replaceWith(tr.mapping.map(pos), tr.mapping.map(pos + node.nodeSize), cells);\n            }\n          }\n        }\n        dispatch!(tr);\n        return true;\n      }\n      return false;\n    };\n  }\n\n  private exitTable(): Command {\n    return (state, dispatch) => {\n      const { selection, tr, schema } = state;\n      const { $from } = selection;\n      const cell = findNodeBy(\n        $from,\n        ({ type }) => type.name === 'tableHeadCell' || type.name === 'tableBodyCell'\n      );\n\n      if (cell) {\n        const para = findNodeBy($from, ({ type }) => type.name === 'paragraph');\n\n        if (para) {\n          const { anchor } = getResolvedSelection(selection);\n          const map = TableOffsetMap.create(anchor)!;\n\n          dispatch!(addParagraphAfterTable(tr, map, schema, true));\n          return true;\n        }\n      }\n      return false;\n    };\n  }\n\n  commands() {\n    return {\n      addTable: this.addTable(),\n      removeTable: this.removeTable(),\n      addColumnToLeft: this.addColumn(Direction.LEFT),\n      addColumnToRight: this.addColumn(Direction.RIGHT),\n      removeColumn: this.removeColumn(),\n      addRowToUp: this.addRow(Direction.UP),\n      addRowToDown: this.addRow(Direction.DOWN),\n      removeRow: this.removeRow(),\n      alignColumn: this.alignColumn(),\n    };\n  }\n\n  keymaps() {\n    const deleteCellContent = this.deleteCells();\n\n    return {\n      Tab: this.moveToCell(Direction.RIGHT),\n      'Shift-Tab': this.moveToCell(Direction.LEFT),\n\n      ArrowUp: this.moveInCell(Direction.UP),\n      ArrowDown: this.moveInCell(Direction.DOWN),\n\n      ArrowLeft: this.moveInCell(Direction.LEFT),\n      ArrowRight: this.moveInCell(Direction.RIGHT),\n\n      Backspace: deleteCellContent,\n      'Mod-Backspace': deleteCellContent,\n      Delete: deleteCellContent,\n      'Mod-Delete': deleteCellContent,\n\n      'Mod-Enter': this.exitTable(),\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/tableBody.ts",
    "content": "import { DOMOutputSpec, ProsemirrorNode } from 'prosemirror-model';\n\nimport NodeSchema from '@/spec/node';\nimport { getCustomAttrs, getDefaultCustomAttrs } from '@/wysiwyg/helper/node';\n\nexport class TableBody extends NodeSchema {\n  get name() {\n    return 'tableBody';\n  }\n\n  get schema() {\n    return {\n      content: 'tableRow+',\n      attrs: {\n        rawHTML: { default: null },\n        ...getDefaultCustomAttrs(),\n      },\n      parseDOM: [\n        {\n          tag: 'tbody',\n          getAttrs(dom: Node | string) {\n            const rows = (dom as HTMLElement).querySelectorAll('tr');\n            const columns = rows[0].children.length;\n            const rawHTML = (dom as HTMLElement).getAttribute('data-raw-html');\n\n            if (!columns) {\n              return false;\n            }\n\n            return { ...(rawHTML && { rawHTML }) };\n          },\n        },\n      ],\n      toDOM({ attrs }: ProsemirrorNode): DOMOutputSpec {\n        return ['tbody', getCustomAttrs(attrs), 0];\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/tableBodyCell.ts",
    "content": "import { DOMOutputSpec, ProsemirrorNode } from 'prosemirror-model';\n\nimport NodeSchema from '@/spec/node';\nimport { createCellAttrs, createParsedCellDOM } from '@/wysiwyg/helper/node';\n\nexport class TableBodyCell extends NodeSchema {\n  get name() {\n    return 'tableBodyCell';\n  }\n\n  get schema() {\n    return {\n      content: '(paragraph | bulletList | orderedList)+',\n      attrs: {\n        align: { default: null },\n        className: { default: null },\n        rawHTML: { default: null },\n        colspan: { default: null },\n        rowspan: { default: null },\n        extended: { default: null },\n      },\n      isolating: true,\n      parseDOM: [createParsedCellDOM('td')],\n      toDOM({ attrs }: ProsemirrorNode): DOMOutputSpec {\n        const cellAttrs = createCellAttrs(attrs);\n\n        return ['td', cellAttrs, 0];\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/tableHead.ts",
    "content": "import { DOMOutputSpec, ProsemirrorNode } from 'prosemirror-model';\n\nimport NodeSchema from '@/spec/node';\nimport {\n  createDOMInfoParsedRawHTML,\n  getCustomAttrs,\n  getDefaultCustomAttrs,\n} from '@/wysiwyg/helper/node';\n\nexport class TableHead extends NodeSchema {\n  get name() {\n    return 'tableHead';\n  }\n\n  get schema() {\n    return {\n      content: 'tableRow{1}',\n      attrs: {\n        rawHTML: { default: null },\n        ...getDefaultCustomAttrs(),\n      },\n      parseDOM: [createDOMInfoParsedRawHTML('thead')],\n      toDOM({ attrs }: ProsemirrorNode): DOMOutputSpec {\n        return ['thead', getCustomAttrs(attrs), 0];\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/tableHeadCell.ts",
    "content": "import { DOMOutputSpec, ProsemirrorNode } from 'prosemirror-model';\n\nimport NodeSchema from '@/spec/node';\nimport {\n  createCellAttrs,\n  createParsedCellDOM,\n  getCustomAttrs,\n  getDefaultCustomAttrs,\n} from '@/wysiwyg/helper/node';\n\nexport class TableHeadCell extends NodeSchema {\n  get name() {\n    return 'tableHeadCell';\n  }\n\n  get schema() {\n    return {\n      content: 'paragraph+',\n      attrs: {\n        align: { default: null },\n        className: { default: null },\n        rawHTML: { default: null },\n        colspan: { default: null },\n        extended: { default: null },\n        ...getDefaultCustomAttrs(),\n      },\n      isolating: true,\n      parseDOM: [createParsedCellDOM('th')],\n      toDOM({ attrs }: ProsemirrorNode): DOMOutputSpec {\n        const cellAttrs = createCellAttrs(attrs);\n\n        return ['th', { ...cellAttrs, ...getCustomAttrs(attrs) }, 0];\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/tableRow.ts",
    "content": "import { DOMOutputSpec, ProsemirrorNode } from 'prosemirror-model';\n\nimport NodeSchema from '@/spec/node';\nimport { getDefaultCustomAttrs, getCustomAttrs } from '@/wysiwyg/helper/node';\n\nexport class TableRow extends NodeSchema {\n  get name() {\n    return 'tableRow';\n  }\n\n  get schema() {\n    return {\n      content: '(tableHeadCell | tableBodyCell)*',\n      attrs: {\n        rawHTML: { default: null },\n        ...getDefaultCustomAttrs(),\n      },\n      parseDOM: [\n        {\n          tag: 'tr',\n          getAttrs: (dom: Node | string) => {\n            const columns = (dom as HTMLElement).children.length;\n            const rawHTML = (dom as HTMLElement).getAttribute('data-raw-html');\n\n            if (!columns) {\n              return false;\n            }\n\n            return { ...(rawHTML && { rawHTML }) };\n          },\n        },\n      ],\n      toDOM({ attrs }: ProsemirrorNode): DOMOutputSpec {\n        return ['tr', getCustomAttrs(attrs), 0];\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/text.ts",
    "content": "import { Command } from 'prosemirror-commands';\n\nimport Node from '@/spec/node';\nimport { isInListNode, isInTableNode } from '../helper/node';\n\nconst reSoftTabLen = /\\s{1,4}$/;\n\nexport class Text extends Node {\n  get name() {\n    return 'text';\n  }\n\n  get schema() {\n    return {\n      group: 'inline',\n    };\n  }\n\n  private addSpaces(): Command {\n    return ({ selection, tr }, dispatch) => {\n      const { $from, $to } = selection;\n      const range = $from.blockRange($to);\n\n      if (range && !isInListNode($from) && !isInTableNode($from)) {\n        dispatch!(tr.insertText('    ', $from.pos, $to.pos));\n        return true;\n      }\n\n      return false;\n    };\n  }\n\n  private removeSpaces(): Command {\n    return ({ selection, tr }, dispatch) => {\n      const { $from, $to, from } = selection;\n      const range = $from.blockRange($to);\n\n      if (range && !isInListNode($from) && !isInTableNode($from)) {\n        const { nodeBefore } = $from;\n\n        if (nodeBefore && nodeBefore.isText) {\n          const text = nodeBefore.text!;\n          const removedSpaceText = text.replace(reSoftTabLen, '');\n          const spaces = text.length - removedSpaceText.length;\n\n          dispatch!(tr.delete(from - spaces, from));\n\n          return true;\n        }\n      }\n\n      return false;\n    };\n  }\n\n  keymaps() {\n    return {\n      Tab: this.addSpaces(),\n      'Shift-Tab': this.removeSpaces(),\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodes/thematicBreak.ts",
    "content": "import { ProsemirrorNode, DOMOutputSpec } from 'prosemirror-model';\n\nimport Node from '@/spec/node';\n\nimport { EditorCommand } from '@t/spec';\nimport { getDefaultCustomAttrs, getCustomAttrs } from '@/wysiwyg/helper/node';\n\nconst ROOT_BLOCK_DEPTH = 1;\n\nexport class ThematicBreak extends Node {\n  get name() {\n    return 'thematicBreak';\n  }\n\n  get schema() {\n    return {\n      attrs: {\n        rawHTML: { default: null },\n        ...getDefaultCustomAttrs(),\n      },\n      group: 'block',\n      parseDOM: [{ tag: 'hr' }],\n      selectable: false,\n      toDOM({ attrs }: ProsemirrorNode): DOMOutputSpec {\n        return ['div', getCustomAttrs(attrs), [attrs.rawHTML || 'hr']];\n      },\n    };\n  }\n\n  private hr(): EditorCommand {\n    return () => (state, dispatch) => {\n      const { $from, $to } = state.selection;\n\n      if ($from === $to) {\n        const { doc } = state;\n        const { thematicBreak, paragraph } = state.schema.nodes;\n\n        const nodes: ProsemirrorNode[] = [thematicBreak.create()];\n\n        const rootBlock = $from.node(ROOT_BLOCK_DEPTH);\n        const lastBlock = doc.child(doc.childCount - 1) === rootBlock;\n        const blockEnd = doc.resolve($from.after(ROOT_BLOCK_DEPTH));\n        const nextHr = $from.nodeAfter?.type.name === this.name;\n\n        if (lastBlock || nextHr) {\n          nodes.push(paragraph.create());\n        }\n\n        dispatch!(state.tr.insert(blockEnd.pos, nodes).scrollIntoView());\n\n        return true;\n      }\n\n      return false;\n    };\n  }\n\n  commands() {\n    return { hr: this.hr() };\n  }\n\n  keymaps() {\n    const hrCommand = this.hr()();\n\n    return {\n      'Mod-l': hrCommand,\n      'Mod-L': hrCommand,\n    };\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodeview/codeBlockView.ts",
    "content": "import { EditorView, NodeView } from 'prosemirror-view';\nimport { ProsemirrorNode } from 'prosemirror-model';\n\nimport isFunction from 'tui-code-snippet/type/isFunction';\nimport css from 'tui-code-snippet/domUtil/css';\n\nimport { removeNode, setAttributes } from '@/utils/dom';\nimport { getCustomAttrs } from '@/wysiwyg/helper/node';\n\nimport { Emitter } from '@t/event';\n\ntype GetPos = (() => number) | boolean;\n\ntype InputPos = {\n  top: number;\n  right: number;\n};\n\nconst WRAPPER_CLASS_NAME = 'toastui-editor-ww-code-block';\nconst CODE_BLOCK_LANG_CLASS_NAME = 'toastui-editor-ww-code-block-language';\n\nexport class CodeBlockView implements NodeView {\n  dom!: HTMLElement;\n\n  contentDOM: HTMLElement | null = null;\n\n  private node: ProsemirrorNode;\n\n  private view: EditorView;\n\n  private getPos: GetPos;\n\n  private eventEmitter: Emitter;\n\n  private input: HTMLElement | null = null;\n\n  private timer: NodeJS.Timeout | null = null;\n\n  constructor(node: ProsemirrorNode, view: EditorView, getPos: GetPos, eventEmitter: Emitter) {\n    this.node = node;\n    this.view = view;\n    this.getPos = getPos;\n    this.eventEmitter = eventEmitter;\n\n    this.createElement();\n    this.bindDOMEvent();\n    this.bindEvent();\n  }\n\n  private createElement() {\n    const { language } = this.node.attrs;\n    const wrapper = document.createElement('div');\n\n    wrapper.setAttribute('data-language', language || 'text');\n    wrapper.className = WRAPPER_CLASS_NAME;\n\n    const pre = this.createCodeBlockElement();\n    const code = pre.firstChild as HTMLElement;\n\n    wrapper.appendChild(pre);\n\n    this.dom = wrapper;\n    this.contentDOM = code;\n  }\n\n  private createCodeBlockElement() {\n    const pre = document.createElement('pre');\n    const code = document.createElement('code');\n    const { language } = this.node.attrs;\n    const attrs = getCustomAttrs(this.node.attrs);\n\n    if (language) {\n      code.setAttribute('data-language', language);\n    }\n    setAttributes(attrs, pre);\n\n    pre.appendChild(code);\n\n    return pre;\n  }\n\n  private createLanguageEditor({ top, right }: InputPos) {\n    const wrapper = document.createElement('span');\n\n    wrapper.className = CODE_BLOCK_LANG_CLASS_NAME;\n\n    const input = document.createElement('input');\n\n    input.type = 'text';\n    input.value = this.node.attrs.language;\n\n    wrapper.appendChild(input);\n    this.view.dom.parentElement!.appendChild(wrapper);\n    const wrpperWidth = wrapper.clientWidth;\n\n    css(wrapper, {\n      top: `${top + 10}px`,\n      left: `${right - wrpperWidth - 10}px`,\n      width: `${wrpperWidth}px`,\n    });\n\n    this.input = input;\n    this.input.addEventListener('blur', () => this.changeLanguage());\n    this.input.addEventListener('keydown', this.handleKeydown);\n\n    this.clearTimer();\n    this.timer = setTimeout(() => {\n      this.input!.focus();\n    });\n  }\n\n  private bindDOMEvent() {\n    if (this.dom) {\n      this.dom.addEventListener('click', this.handleMousedown);\n    }\n  }\n\n  private bindEvent() {\n    this.eventEmitter.listen('scroll', () => {\n      if (this.input) {\n        this.reset();\n      }\n    });\n  }\n\n  private handleMousedown = (ev: MouseEvent) => {\n    const target = ev.target as HTMLElement;\n    const style = getComputedStyle(target, ':after');\n\n    // judge to click pseudo element with background image for IE11\n    if (style.backgroundImage !== 'none' && isFunction(this.getPos)) {\n      const { top, right } = this.view.coordsAtPos(this.getPos());\n\n      this.createLanguageEditor({ top, right });\n    }\n  };\n\n  private handleKeydown = (ev: KeyboardEvent) => {\n    if (ev.key === 'Enter' && this.input) {\n      ev.preventDefault();\n      this.changeLanguage();\n    }\n  };\n\n  private changeLanguage() {\n    if (this.input && isFunction(this.getPos)) {\n      const { value } = this.input as HTMLInputElement;\n\n      this.reset();\n\n      const pos = this.getPos();\n      const { tr } = this.view.state;\n\n      tr.setNodeMarkup(pos, null, { language: value });\n      this.view.dispatch(tr);\n    }\n  }\n\n  private reset() {\n    if (this.input?.parentElement) {\n      const parent = this.input.parentElement;\n\n      this.input = null;\n      removeNode(parent);\n    }\n  }\n\n  private clearTimer() {\n    if (this.timer) {\n      clearTimeout(this.timer);\n      this.timer = null;\n    }\n  }\n\n  stopEvent() {\n    return true;\n  }\n\n  update(node: ProsemirrorNode) {\n    if (!node.sameMarkup(this.node)) {\n      return false;\n    }\n\n    this.node = node;\n\n    return true;\n  }\n\n  destroy() {\n    this.reset();\n    this.clearTimer();\n\n    if (this.dom) {\n      this.dom.removeEventListener('click', this.handleMousedown);\n    }\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodeview/customBlockView.ts",
    "content": "import { EditorView, NodeView } from 'prosemirror-view';\nimport { ProsemirrorNode } from 'prosemirror-model';\nimport { StepMap } from 'prosemirror-transform';\nimport { EditorState, TextSelection, Transaction } from 'prosemirror-state';\nimport { newlineInCode } from 'prosemirror-commands';\nimport { redo, undo, undoDepth, history } from 'prosemirror-history';\nimport { keymap } from 'prosemirror-keymap';\nimport isFunction from 'tui-code-snippet/type/isFunction';\nimport { ToDOMAdaptor } from '@t/convertor';\nimport { createTextSelection } from '@/helper/manipulation';\nimport { cls } from '@/utils/dom';\n\ntype GetPos = (() => number) | boolean;\n\nexport class CustomBlockView implements NodeView {\n  dom: HTMLElement;\n\n  private node: ProsemirrorNode;\n\n  private toDOMAdaptor: ToDOMAdaptor;\n\n  private editorView: EditorView;\n\n  private innerEditorView: EditorView | null;\n\n  private wrapper: HTMLElement;\n\n  private innerViewContainer!: HTMLElement;\n\n  private getPos: GetPos;\n\n  private canceled: boolean;\n\n  constructor(node: ProsemirrorNode, view: EditorView, getPos: GetPos, toDOMAdaptor: ToDOMAdaptor) {\n    this.node = node;\n    this.editorView = view;\n    this.getPos = getPos;\n    this.toDOMAdaptor = toDOMAdaptor;\n    this.innerEditorView = null;\n    this.canceled = false;\n\n    this.dom = document.createElement('div');\n    this.dom.className = cls('custom-block');\n    this.wrapper = document.createElement('div');\n    this.wrapper.className = cls('custom-block-view');\n\n    this.createInnerViewContainer();\n    this.renderCustomBlock();\n\n    this.dom.appendChild(this.innerViewContainer);\n    this.dom.appendChild(this.wrapper);\n  }\n\n  private renderToolArea() {\n    const tool = document.createElement('div');\n    const span = document.createElement('span');\n    const button = document.createElement('button');\n\n    tool.className = 'tool';\n    span.textContent = this.node.attrs.info;\n    span.className = 'info';\n    button.type = 'button';\n    button.addEventListener('click', () => this.openEditor());\n\n    tool.appendChild(span);\n    tool.appendChild(button);\n    this.wrapper.appendChild(tool);\n  }\n\n  private renderCustomBlock() {\n    const toDOMNode = this.toDOMAdaptor.getToDOMNode(this.node.attrs.info);\n\n    if (toDOMNode) {\n      const node = toDOMNode(this.node);\n\n      while (this.wrapper.hasChildNodes()) {\n        this.wrapper.removeChild(this.wrapper.lastChild!);\n      }\n\n      if (node) {\n        this.wrapper.appendChild(node);\n      }\n      this.renderToolArea();\n    }\n  }\n\n  private createInnerViewContainer() {\n    this.innerViewContainer = document.createElement('div');\n    this.innerViewContainer.className = cls('custom-block-editor');\n    this.innerViewContainer.style.display = 'none';\n  }\n\n  private openEditor = () => {\n    if (this.innerEditorView) {\n      throw new Error('The editor is already opened.');\n    }\n\n    this.dom.draggable = false;\n    this.wrapper.style.display = 'none';\n    this.innerViewContainer.style.display = 'block';\n\n    this.innerEditorView = new EditorView(this.innerViewContainer, {\n      state: EditorState.create({\n        doc: this.node,\n        plugins: [\n          keymap({\n            'Mod-z': () => undo(this.innerEditorView!.state, this.innerEditorView!.dispatch),\n            'Shift-Mod-z': () => redo(this.innerEditorView!.state, this.innerEditorView!.dispatch),\n            Tab: (state, dispatch) => {\n              dispatch!(state.tr.insertText('\\t'));\n              return true;\n            },\n            Enter: newlineInCode,\n            Escape: () => {\n              this.cancelEditing();\n              return true;\n            },\n            'Ctrl-Enter': () => {\n              this.saveAndFinishEditing();\n              return true;\n            },\n          }),\n          history(),\n        ],\n      }),\n      dispatchTransaction: (tr: Transaction) => this.dispatchInner(tr),\n      handleDOMEvents: {\n        mousedown: () => {\n          if (this.editorView.hasFocus()) {\n            this.innerEditorView!.focus();\n          }\n          return true;\n        },\n        blur: () => {\n          this.saveAndFinishEditing();\n          return true;\n        },\n      },\n    });\n    this.innerEditorView!.focus();\n  };\n\n  private closeEditor() {\n    if (this.innerEditorView) {\n      this.innerEditorView.destroy();\n      this.innerEditorView = null;\n      this.innerViewContainer.style.display = 'none';\n    }\n    this.wrapper.style.display = 'block';\n  }\n\n  private saveAndFinishEditing() {\n    const { to } = this.editorView.state.selection;\n    const outerState: EditorState = this.editorView.state;\n\n    this.editorView.dispatch(outerState.tr.setSelection(createTextSelection(outerState.tr, to)));\n    this.editorView.focus();\n\n    this.renderCustomBlock();\n    this.closeEditor();\n  }\n\n  private cancelEditing() {\n    let undoableCount = undoDepth(this.innerEditorView!.state);\n\n    this.canceled = true;\n\n    // should undo editing result\n    // eslint-disable-next-line no-plusplus\n    while (undoableCount--) {\n      undo(this.innerEditorView!.state, this.innerEditorView!.dispatch);\n      undo(this.editorView.state, this.editorView.dispatch);\n    }\n    this.canceled = false;\n\n    const { to } = this.editorView.state.selection;\n    const outerState: EditorState = this.editorView.state;\n\n    this.editorView.dispatch(outerState.tr.setSelection(TextSelection.create(outerState.doc, to)));\n    this.editorView.focus();\n\n    this.closeEditor();\n  }\n\n  private dispatchInner(tr: Transaction) {\n    const { state, transactions } = this.innerEditorView!.state.applyTransaction(tr);\n\n    this.innerEditorView!.updateState(state);\n\n    if (!this.canceled && isFunction(this.getPos)) {\n      const outerTr = this.editorView.state.tr;\n      const offsetMap = StepMap.offset(this.getPos() + 1);\n\n      for (let i = 0; i < transactions.length; i += 1) {\n        const { steps } = transactions[i];\n\n        for (let j = 0; j < steps.length; j += 1) {\n          outerTr.step(steps[j].map(offsetMap)!);\n        }\n      }\n      if (outerTr.docChanged) {\n        this.editorView.dispatch(outerTr);\n      }\n    }\n  }\n\n  update(node: ProsemirrorNode) {\n    if (!node.sameMarkup(this.node)) {\n      return false;\n    }\n\n    this.node = node;\n\n    if (!this.innerEditorView) {\n      this.renderCustomBlock();\n    }\n\n    return true;\n  }\n\n  stopEvent(event: Event): boolean {\n    return (\n      !!this.innerEditorView &&\n      !!event.target &&\n      this.innerEditorView.dom.contains(event.target as Node)\n    );\n  }\n\n  ignoreMutation() {\n    return true;\n  }\n\n  destroy() {\n    this.dom.removeEventListener('dblclick', this.openEditor);\n    this.closeEditor();\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/nodeview/imageView.ts",
    "content": "import { EditorView, NodeView } from 'prosemirror-view';\nimport { Node as ProsemirrorNode, Mark } from 'prosemirror-model';\n\nimport hasClass from 'tui-code-snippet/domUtil/hasClass';\nimport isFunction from 'tui-code-snippet/type/isFunction';\n\nimport { isPositionInBox, setAttributes } from '@/utils/dom';\nimport { createTextSelection } from '@/helper/manipulation';\nimport { getCustomAttrs } from '@/wysiwyg/helper/node';\n\nimport { Emitter } from '@t/event';\n\ntype GetPos = (() => number) | boolean;\n\nconst IMAGE_LINK_CLASS_NAME = 'image-link';\n\nexport class ImageView implements NodeView {\n  dom: HTMLElement;\n\n  private node: ProsemirrorNode;\n\n  private view: EditorView;\n\n  private getPos: GetPos;\n\n  private eventEmitter: Emitter;\n\n  private imageLink: Mark | null;\n\n  constructor(node: ProsemirrorNode, view: EditorView, getPos: GetPos, eventEmitter: Emitter) {\n    this.node = node;\n    this.view = view;\n    this.getPos = getPos;\n    this.eventEmitter = eventEmitter;\n    this.imageLink = node.marks.filter(({ type }) => type.name === 'link')[0] ?? null;\n    this.dom = this.createElement();\n\n    this.bindEvent();\n  }\n\n  private createElement() {\n    const image = this.createImageElement(this.node);\n\n    if (this.imageLink) {\n      const wrapper = document.createElement('span');\n\n      wrapper.className = IMAGE_LINK_CLASS_NAME;\n      wrapper.appendChild(image);\n\n      return wrapper;\n    }\n\n    return image;\n  }\n\n  private createImageElement(node: ProsemirrorNode) {\n    const image = document.createElement('img');\n    const { imageUrl, altText } = node.attrs;\n    const attrs = getCustomAttrs(node.attrs);\n\n    image.src = imageUrl;\n\n    if (altText) {\n      image.alt = altText;\n    }\n    setAttributes(attrs, image);\n\n    return image;\n  }\n\n  private bindEvent() {\n    if (this.imageLink) {\n      this.dom.addEventListener('mousedown', this.handleMousedown);\n    }\n  }\n\n  private handleMousedown = (ev: MouseEvent) => {\n    ev.preventDefault();\n\n    const { target, offsetX, offsetY } = ev;\n\n    if (\n      this.imageLink &&\n      isFunction(this.getPos) &&\n      hasClass(target as HTMLElement, IMAGE_LINK_CLASS_NAME)\n    ) {\n      const style = getComputedStyle(target as HTMLElement, ':before');\n\n      ev.stopPropagation();\n\n      if (isPositionInBox(style, offsetX, offsetY)) {\n        const { tr } = this.view.state;\n        const pos = this.getPos();\n\n        tr.setSelection(createTextSelection(tr, pos, pos + 1));\n        this.view.dispatch(tr);\n        this.eventEmitter.emit('openPopup', 'link', this.imageLink.attrs);\n      }\n    }\n  };\n\n  stopEvent() {\n    return true;\n  }\n\n  destroy() {\n    if (this.imageLink) {\n      this.dom.removeEventListener('mousedown', this.handleMousedown);\n    }\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/plugins/selection/cellSelection.ts",
    "content": "import { Node, ResolvedPos, Slice, Fragment } from 'prosemirror-model';\nimport { Selection, SelectionRange, TextSelection } from 'prosemirror-state';\nimport { Mappable } from 'prosemirror-transform';\n\nimport { TableOffsetMap, SelectionInfo } from '@/wysiwyg/helper/tableOffsetMap';\n\nfunction getSelectionRanges(\n  doc: Node,\n  map: TableOffsetMap,\n  { startRowIdx, startColIdx, endRowIdx, endColIdx }: SelectionInfo\n) {\n  const ranges = [];\n\n  for (let rowIdx = startRowIdx; rowIdx <= endRowIdx; rowIdx += 1) {\n    for (let colIdx = startColIdx; colIdx <= endColIdx; colIdx += 1) {\n      const { offset, nodeSize } = map.getCellInfo(rowIdx, colIdx);\n\n      ranges.push(new SelectionRange(doc.resolve(offset + 1), doc.resolve(offset + nodeSize - 1)));\n    }\n  }\n  return ranges;\n}\n\nfunction createTableFragment(tableHead: Node, tableBody: Node) {\n  const fragment: Node[] = [];\n\n  if (tableHead.childCount) {\n    fragment.push(tableHead);\n  }\n  if (tableBody.childCount) {\n    fragment.push(tableBody);\n  }\n  return Fragment.from(fragment);\n}\n\nexport default class CellSelection extends Selection {\n  private offsetMap: TableOffsetMap;\n\n  startCell: ResolvedPos;\n\n  endCell: ResolvedPos;\n\n  isCellSelection: boolean;\n\n  constructor(startCellPos: ResolvedPos, endCellPos = startCellPos) {\n    const doc = startCellPos.node(0);\n\n    const map = TableOffsetMap.create(startCellPos)!;\n    const selectionInfo = map.getRectOffsets(startCellPos, endCellPos);\n    const ranges = getSelectionRanges(doc, map, selectionInfo);\n\n    super(ranges[0].$from, ranges[0].$to, ranges);\n\n    this.startCell = startCellPos;\n    this.endCell = endCellPos;\n    this.offsetMap = map;\n    this.isCellSelection = true;\n\n    // This property is the api of the 'Selection' in prosemirror,\n    // and is used to disable the text selection.\n    this.visible = false;\n  }\n\n  map(doc: Node, mapping: Mappable) {\n    const startPos = this.startCell.pos;\n    const endPos = this.endCell.pos;\n    const startCell = doc.resolve(mapping.map(startPos));\n    const endCell = doc.resolve(mapping.map(endPos));\n    const map = TableOffsetMap.create(startCell)!;\n\n    // text selection when rows or columns are deleted\n    if (\n      this.offsetMap.totalColumnCount > map.totalColumnCount ||\n      this.offsetMap.totalRowCount > map.totalRowCount\n    ) {\n      const depthMap = { tableBody: 1, tableRow: 2, tableCell: 3, paragraph: 4 };\n      const depthFromTable = depthMap[endCell.parent.type.name as keyof typeof depthMap];\n      const tableEndPos = endCell.end(endCell.depth - depthFromTable);\n      // subtract 4(</td></tr></tbody></table> tag length)\n      const from = Math.min(tableEndPos - 4, endCell.pos);\n\n      return TextSelection.create(doc, from);\n    }\n    return new CellSelection(startCell, endCell);\n  }\n\n  eq(cell: CellSelection) {\n    return (\n      cell instanceof CellSelection &&\n      cell.startCell.pos === this.startCell.pos &&\n      cell.endCell.pos === this.endCell.pos\n    );\n  }\n\n  content() {\n    const table = this.startCell.node(-2);\n    const tableOffset = this.startCell.start(-2);\n    const row = table.child(1).firstChild!;\n    const tableHead = table.child(0).type.create()!;\n    const tableBody = table.child(1).type.create()!;\n\n    const map = TableOffsetMap.create(this.startCell)!;\n    const selectionInfo = map.getRectOffsets(this.startCell, this.endCell);\n    const { startRowIdx, startColIdx, endRowIdx, endColIdx } = selectionInfo;\n\n    let isTableHeadCell = false;\n\n    for (let rowIdx = startRowIdx; rowIdx <= endRowIdx; rowIdx += 1) {\n      const cells = [];\n\n      for (let colIdx = startColIdx; colIdx <= endColIdx; colIdx += 1) {\n        const { offset } = map.getCellInfo(rowIdx, colIdx);\n        const cell = table.nodeAt(offset - tableOffset);\n\n        if (cell) {\n          isTableHeadCell = cell.type.name === 'tableHeadCell';\n          // mark the extended cell for pasting\n          if (map.extendedRowspan(rowIdx, colIdx) || map.extendedColspan(rowIdx, colIdx)) {\n            cells.push(cell.type.create({ extended: true }));\n          } else {\n            cells.push(cell.copy(cell.content));\n          }\n        }\n      }\n\n      const copiedRow = row.copy(Fragment.from(cells));\n      const targetNode = isTableHeadCell ? tableHead : tableBody;\n\n      // @ts-ignore\n      targetNode.content = targetNode.content.append(Fragment.from(copiedRow));\n    }\n    return new Slice(createTableFragment(tableHead, tableBody), 1, 1);\n  }\n\n  toJSON() {\n    return JSON.stringify(this);\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/plugins/selection/tableSelection.ts",
    "content": "import { EditorState, Plugin, SelectionRange } from 'prosemirror-state';\nimport { EditorView, Decoration, DecorationSet } from 'prosemirror-view';\n\nimport isNull from 'tui-code-snippet/type/isNull';\n\nimport { cls } from '@/utils/dom';\nimport CellSelection from './cellSelection';\nimport TableSelection, { pluginKey } from './tableSelectionView';\n\nconst SELECTED_CELL_CLASS_NAME = cls('cell-selected');\n\nfunction drawCellSelection({ selection, doc }: EditorState) {\n  if (selection instanceof CellSelection) {\n    const cells: Decoration[] = [];\n    const { ranges } = selection;\n\n    ranges.forEach(({ $from, $to }: SelectionRange) => {\n      cells.push(Decoration.node($from.pos - 1, $to.pos + 1, { class: SELECTED_CELL_CLASS_NAME }));\n    });\n\n    return DecorationSet.create(doc, cells);\n  }\n\n  return null;\n}\n\nexport function tableSelection() {\n  return new Plugin({\n    key: pluginKey,\n    state: {\n      init() {\n        return null;\n      },\n      apply(tr, value) {\n        const cellOffset = tr.getMeta(pluginKey);\n\n        if (cellOffset) {\n          return cellOffset === -1 ? null : cellOffset;\n        }\n\n        if (isNull(value) || !tr.docChanged) {\n          return value;\n        }\n\n        const { deleted, pos } = tr.mapping.mapResult(value);\n\n        return deleted ? null : pos;\n      },\n    },\n    props: {\n      decorations: drawCellSelection,\n      createSelectionBetween({ state }) {\n        if (!isNull(pluginKey.getState(state))) {\n          return state.selection;\n        }\n\n        return null;\n      },\n    },\n    view(editorView: EditorView) {\n      return new TableSelection(editorView);\n    },\n  });\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/plugins/selection/tableSelectionView.ts",
    "content": "import { ResolvedPos } from 'prosemirror-model';\nimport { EditorView } from 'prosemirror-view';\nimport { PluginKey } from 'prosemirror-state';\n\nimport { findCell, findCellElement } from '@/wysiwyg/helper/table';\n\nimport CellSelection from './cellSelection';\n\ninterface EventHandlers {\n  mousedown: (ev: Event) => void;\n  mousemove: (ev: Event) => void;\n  mouseup: () => void;\n}\n\nexport const pluginKey = new PluginKey('cellSelection');\n\nconst MOUSE_RIGHT_BUTTON = 2;\n\nexport default class TableSelection {\n  private view: EditorView;\n\n  private handlers: EventHandlers;\n\n  private startCellPos: ResolvedPos | null;\n\n  constructor(view: EditorView) {\n    this.view = view;\n\n    this.handlers = {\n      mousedown: this.handleMousedown.bind(this),\n      mousemove: this.handleMousemove.bind(this),\n      mouseup: this.handleMouseup.bind(this),\n    };\n\n    this.startCellPos = null;\n\n    this.init();\n  }\n\n  init() {\n    this.view.dom.addEventListener('mousedown', this.handlers.mousedown);\n  }\n\n  handleMousedown(ev: Event) {\n    const foundCell = findCellElement(ev.target as HTMLElement, this.view.dom);\n\n    if ((ev as MouseEvent).button === MOUSE_RIGHT_BUTTON) {\n      ev.preventDefault();\n      return;\n    }\n\n    if (foundCell) {\n      const startCellPos = this.getCellPos(ev as MouseEvent);\n\n      if (startCellPos) {\n        this.startCellPos = startCellPos;\n      }\n\n      this.bindEvent();\n    }\n  }\n\n  handleMousemove(ev: Event) {\n    const prevEndCellOffset = pluginKey.getState(this.view.state);\n    const endCellPos = this.getCellPos(ev as MouseEvent);\n    const { startCellPos } = this;\n\n    let prevEndCellPos;\n\n    if (prevEndCellOffset) {\n      prevEndCellPos = this.view.state.doc.resolve(prevEndCellOffset);\n    } else if (startCellPos !== endCellPos) {\n      prevEndCellPos = startCellPos;\n    }\n\n    if (prevEndCellPos && startCellPos && endCellPos) {\n      this.setCellSelection(startCellPos, endCellPos);\n    }\n  }\n\n  handleMouseup() {\n    this.startCellPos = null;\n\n    this.unbindEvent();\n\n    if (pluginKey.getState(this.view.state) !== null) {\n      this.view.dispatch(this.view.state.tr.setMeta(pluginKey, -1));\n    }\n  }\n\n  bindEvent() {\n    const { dom } = this.view;\n\n    dom.addEventListener('mousemove', this.handlers.mousemove);\n    dom.addEventListener('mouseup', this.handlers.mouseup);\n  }\n\n  unbindEvent() {\n    const { dom } = this.view;\n\n    dom.removeEventListener('mousemove', this.handlers.mousemove);\n    dom.removeEventListener('mouseup', this.handlers.mouseup);\n  }\n\n  getCellPos({ clientX, clientY }: MouseEvent) {\n    const mousePos = this.view.posAtCoords({ left: clientX, top: clientY });\n\n    if (mousePos) {\n      const { doc } = this.view.state;\n      const currentPos = doc.resolve(mousePos.pos);\n      const foundCell = findCell(currentPos);\n\n      if (foundCell) {\n        const cellOffset = currentPos.before(foundCell.depth);\n\n        return doc.resolve(cellOffset);\n      }\n    }\n\n    return null;\n  }\n\n  setCellSelection(startCellPos: ResolvedPos, endCellPos: ResolvedPos) {\n    const { selection, tr } = this.view.state;\n    const starting = pluginKey.getState(this.view.state) === null;\n    const cellSelection = new CellSelection(startCellPos, endCellPos);\n\n    if (starting || !selection.eq(cellSelection)) {\n      const newTr = tr.setSelection(cellSelection);\n\n      if (starting) {\n        newTr.setMeta(pluginKey, endCellPos.pos);\n      }\n\n      this.view.dispatch!(newTr);\n    }\n  }\n\n  destroy() {\n    this.view.dom.removeEventListener('mousedown', this.handlers.mousedown);\n  }\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/plugins/tableContextMenu.ts",
    "content": "import { Plugin } from 'prosemirror-state';\nimport { EditorView } from 'prosemirror-view';\n\nimport { findCellElement } from '@/wysiwyg/helper/table';\nimport i18n from '@/i18n/i18n';\n\nimport { Emitter } from '@t/event';\n\ninterface ContextMenuInfo {\n  action: string;\n  command: string;\n  payload?: {\n    align: string;\n  };\n  className: string;\n  disableInThead?: boolean;\n}\n\nconst contextMenuGroups: ContextMenuInfo[][] = [\n  [\n    {\n      action: 'Add row to up',\n      command: 'addRowToUp',\n      disableInThead: true,\n      className: 'add-row-up',\n    },\n    {\n      action: 'Add row to down',\n      command: 'addRowToDown',\n      disableInThead: true,\n      className: 'add-row-down',\n    },\n    { action: 'Remove row', command: 'removeRow', disableInThead: true, className: 'remove-row' },\n  ],\n  [\n    { action: 'Add column to left', command: 'addColumnToLeft', className: 'add-column-left' },\n    { action: 'Add column to right', command: 'addColumnToRight', className: 'add-column-right' },\n    { action: 'Remove column', command: 'removeColumn', className: 'remove-column' },\n  ],\n  [\n    {\n      action: 'Align column to left',\n      command: 'alignColumn',\n      payload: { align: 'left' },\n      className: 'align-column-left',\n    },\n    {\n      action: 'Align column to center',\n      command: 'alignColumn',\n      payload: { align: 'center' },\n      className: 'align-column-center',\n    },\n    {\n      action: 'Align column to right',\n      command: 'alignColumn',\n      payload: { align: 'right' },\n      className: 'align-column-right',\n    },\n  ],\n  [{ action: 'Remove table', command: 'removeTable', className: 'remove-table' }],\n];\n\nfunction getContextMenuGroups(eventEmitter: Emitter, inTableHead: boolean) {\n  return contextMenuGroups\n    .map((contextMenuGroup) =>\n      contextMenuGroup.map(({ action, command, payload, disableInThead, className }) => {\n        return {\n          label: i18n.get(action),\n          onClick: () => {\n            eventEmitter.emit('command', command, payload);\n          },\n          disabled: inTableHead && !!disableInThead,\n          className,\n        };\n      })\n    )\n    .concat();\n}\n\nexport function tableContextMenu(eventEmitter: Emitter) {\n  return new Plugin({\n    props: {\n      handleDOMEvents: {\n        contextmenu: (view: EditorView, ev: Event) => {\n          const tableCell = findCellElement(ev.target as HTMLElement, view.dom);\n\n          if (tableCell) {\n            ev.preventDefault();\n\n            const { clientX, clientY } = ev as MouseEvent;\n            const { left, top } = (view.dom.parentNode as HTMLElement).getBoundingClientRect();\n            const inTableHead = tableCell.nodeName === 'TH';\n\n            eventEmitter.emit('contextmenu', {\n              pos: { left: `${clientX - left + 10}px`, top: `${clientY - top + 30}px` },\n              menuGroups: getContextMenuGroups(eventEmitter, inTableHead),\n              tableCell,\n            });\n\n            return true;\n          }\n\n          return false;\n        },\n      },\n    },\n  });\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/plugins/task.ts",
    "content": "import { Plugin } from 'prosemirror-state';\nimport { EditorView } from 'prosemirror-view';\n\nimport { isPositionInBox } from '@/utils/dom';\nimport { findListItem } from '@/wysiwyg/helper/node';\n\nexport function task() {\n  return new Plugin({\n    props: {\n      handleDOMEvents: {\n        mousedown: (view: EditorView, ev: Event) => {\n          const { clientX, clientY } = ev as MouseEvent;\n          const mousePos = view.posAtCoords({ left: clientX, top: clientY });\n\n          if (mousePos) {\n            const { doc, tr } = view.state;\n            const currentPos = doc.resolve(mousePos.pos);\n            const listItem = findListItem(currentPos);\n\n            const target = ev.target as HTMLElement;\n            const style = getComputedStyle(target, ':before');\n            const { offsetX, offsetY } = ev as MouseEvent;\n\n            if (!listItem || !isPositionInBox(style, offsetX, offsetY)) {\n              return false;\n            }\n\n            ev.preventDefault();\n\n            const offset = currentPos.before(listItem.depth);\n            const { attrs } = listItem.node;\n\n            tr.setNodeMarkup(offset, null, { ...attrs, ...{ checked: !attrs.checked } });\n            view.dispatch!(tr);\n\n            return true;\n          }\n\n          return false;\n        },\n      },\n    },\n  });\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/plugins/toolbarState.ts",
    "content": "import { Node, ResolvedPos, Schema } from 'prosemirror-model';\nimport { Plugin, Selection } from 'prosemirror-state';\n\nimport { includes } from '@/utils/common';\n\nimport { ToolbarStateMap, ToolbarStateKeys } from '@t/ui';\nimport { Emitter } from '@t/event';\n\ntype ListType = 'bulletList' | 'orderedList' | 'taskList';\n\nconst EXCEPT_TYPES = ['image', 'link', 'customBlock', 'frontMatter'];\nconst MARK_TYPES = ['strong', 'strike', 'emph', 'code'];\nconst LIST_TYPES: ListType[] = ['bulletList', 'orderedList', 'taskList'];\n\nfunction getToolbarStateType(node: Node, parentNode: Node) {\n  const type = node.type.name;\n\n  if (type === 'listItem') {\n    return node.attrs.task ? 'taskList' : parentNode.type.name;\n  }\n\n  if (type.indexOf('table') !== -1) {\n    return 'table';\n  }\n\n  return type;\n}\n\nfunction setListNodeToolbarState(type: ToolbarStateKeys, nodeTypeState: ToolbarStateMap) {\n  nodeTypeState[type] = { active: true };\n\n  LIST_TYPES.filter((listName) => listName !== type).forEach((listType) => {\n    if (nodeTypeState[listType]) {\n      delete nodeTypeState[listType];\n    }\n  });\n}\n\nfunction setMarkTypeStates(\n  from: ResolvedPos,\n  to: ResolvedPos,\n  schema: Schema,\n  toolbarState: ToolbarStateMap\n) {\n  MARK_TYPES.forEach((type) => {\n    const mark = schema.marks[type];\n    const marksAtPos = from.marksAcross(to) || [];\n    const foundMark = !!mark.isInSet(marksAtPos);\n\n    if (foundMark) {\n      toolbarState[type as ToolbarStateKeys] = { active: true };\n    }\n  });\n}\n\nfunction getToolbarState(selection: Selection, doc: Node, schema: Schema) {\n  const { $from, $to, from, to } = selection;\n  const toolbarState = {\n    indent: { active: false, disabled: true },\n    outdent: { active: false, disabled: true },\n  } as ToolbarStateMap;\n\n  doc.nodesBetween(from, to, (node, _, parentNode) => {\n    const type = getToolbarStateType(node, parentNode!);\n\n    if (includes(EXCEPT_TYPES, type)) {\n      return;\n    }\n\n    if (includes(LIST_TYPES, type)) {\n      setListNodeToolbarState(type as ToolbarStateKeys, toolbarState);\n\n      toolbarState.indent.disabled = false;\n      toolbarState.outdent.disabled = false;\n    } else if (type === 'paragraph' || type === 'text') {\n      setMarkTypeStates($from, $to, schema, toolbarState);\n    } else {\n      toolbarState[type as ToolbarStateKeys] = { active: true };\n    }\n  });\n  return toolbarState;\n}\n\nexport function toolbarStateHighlight(eventEmitter: Emitter) {\n  return new Plugin({\n    view() {\n      return {\n        update(view) {\n          const { selection, doc, schema } = view.state;\n\n          eventEmitter.emit('changeToolbarState', {\n            toolbarState: getToolbarState(selection, doc, schema),\n          });\n        },\n      };\n    },\n  });\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/specCreator.ts",
    "content": "import SpecManager from '@/spec/specManager';\n\nimport { Doc } from './nodes/doc';\nimport { Paragraph } from './nodes/paragraph';\nimport { Text } from './nodes/text';\nimport { Heading } from './nodes/heading';\nimport { CodeBlock } from './nodes/codeBlock';\nimport { BulletList } from './nodes/bulletList';\nimport { OrderedList } from './nodes/orderedList';\nimport { ListItem } from './nodes/listItem';\nimport { BlockQuote } from './nodes/blockQuote';\nimport { Table } from './nodes/table';\nimport { TableHead } from './nodes/tableHead';\nimport { TableBody } from './nodes/tableBody';\nimport { TableRow } from './nodes/tableRow';\nimport { TableHeadCell } from './nodes/tableHeadCell';\nimport { TableBodyCell } from './nodes/tableBodyCell';\nimport { Image } from './nodes/image';\nimport { ThematicBreak } from './nodes/thematicBreak';\n\nimport { Strong } from './marks/strong';\nimport { Emph } from './marks/emph';\nimport { Strike } from './marks/strike';\nimport { Link } from './marks/link';\nimport { Code } from './marks/code';\nimport { CustomBlock } from './nodes/customBlock';\nimport { FrontMatter } from './nodes/frontMatter';\nimport { LinkAttributes } from '@t/editor';\nimport { Widget } from '@/widget/widgetNode';\nimport { HTMLComment } from './nodes/htmlComment';\n\nexport function createSpecs(linkAttributes: LinkAttributes) {\n  return new SpecManager([\n    new Doc(),\n    new Paragraph(),\n    new Text(),\n    new Heading(),\n    new CodeBlock(),\n    new BulletList(),\n    new OrderedList(),\n    new ListItem(),\n    new BlockQuote(),\n    new Table(),\n    new TableHead(),\n    new TableBody(),\n    new TableRow(),\n    new TableHeadCell(),\n    new TableBodyCell(),\n    new Image(),\n    new ThematicBreak(),\n    new Strong(),\n    new Emph(),\n    new Strike(),\n    new Link(linkAttributes),\n    new Code(),\n    new CustomBlock(),\n    new FrontMatter(),\n    new Widget(),\n    new HTMLComment(),\n  ]);\n}\n"
  },
  {
    "path": "apps/editor/src/wysiwyg/wwEditor.ts",
    "content": "import { EditorView, NodeView } from 'prosemirror-view';\nimport { ProsemirrorNode, Slice, Fragment, Mark, Schema } from 'prosemirror-model';\nimport isNumber from 'tui-code-snippet/type/isNumber';\nimport toArray from 'tui-code-snippet/collection/toArray';\n\nimport EditorBase from '@/base';\nimport { getWwCommands } from '@/commands/wwCommands';\n\nimport { createParagraph, createTextSelection } from '@/helper/manipulation';\nimport { emitImageBlobHook, pasteImageOnly } from '@/helper/image';\n\nimport { tableSelection } from './plugins/selection/tableSelection';\nimport { tableContextMenu } from './plugins/tableContextMenu';\nimport { task } from './plugins/task';\nimport { toolbarStateHighlight } from './plugins/toolbarState';\n\nimport { CustomBlockView } from './nodeview/customBlockView';\nimport { ImageView } from './nodeview/imageView';\nimport { CodeBlockView } from './nodeview/codeBlockView';\n\nimport { changePastedHTML, changePastedSlice } from './clipboard/paste';\nimport { pasteToTable } from './clipboard/pasteToTable';\nimport { createSpecs } from './specCreator';\n\nimport { Emitter } from '@t/event';\nimport { ToDOMAdaptor } from '@t/convertor';\nimport { HTMLSchemaMap, LinkAttributes, WidgetStyle } from '@t/editor';\nimport { NodeViewPropMap, PluginProp } from '@t/plugin';\nimport { createNodesWithWidget } from '@/widget/rules';\nimport { widgetNodeView } from '@/widget/widgetNode';\nimport { cls, removeProseMirrorHackNodes } from '@/utils/dom';\nimport { includes } from '@/utils/common';\nimport { isInTableNode } from '@/wysiwyg/helper/node';\n\ninterface WindowWithClipboard extends Window {\n  clipboardData?: DataTransfer | null;\n}\n\ninterface WysiwygOptions {\n  toDOMAdaptor: ToDOMAdaptor;\n  useCommandShortcut?: boolean;\n  htmlSchemaMap?: HTMLSchemaMap;\n  linkAttributes?: LinkAttributes | null;\n  wwPlugins?: PluginProp[];\n  wwNodeViews?: NodeViewPropMap;\n}\n\ntype PluginNodeVeiwFn = (node: ProsemirrorNode, view: EditorView, getPos: () => number) => NodeView;\n\ninterface PluginNodeViews {\n  [k: string]: PluginNodeVeiwFn;\n}\n\nconst CONTENTS_CLASS_NAME = cls('contents');\n\nexport default class WysiwygEditor extends EditorBase {\n  private toDOMAdaptor: ToDOMAdaptor;\n\n  private linkAttributes: LinkAttributes;\n\n  private pluginNodeViews: NodeViewPropMap;\n\n  constructor(eventEmitter: Emitter, options: WysiwygOptions) {\n    super(eventEmitter);\n\n    const {\n      toDOMAdaptor,\n      htmlSchemaMap = {} as HTMLSchemaMap,\n      linkAttributes = {},\n      useCommandShortcut = true,\n      wwPlugins = [],\n      wwNodeViews = {},\n    } = options;\n\n    this.editorType = 'wysiwyg';\n    this.el.classList.add('ww-mode');\n    this.toDOMAdaptor = toDOMAdaptor;\n    this.linkAttributes = linkAttributes!;\n    this.extraPlugins = wwPlugins;\n    this.pluginNodeViews = wwNodeViews;\n    this.specs = this.createSpecs();\n    this.schema = this.createSchema(htmlSchemaMap);\n    this.context = this.createContext();\n    this.keymaps = this.createKeymaps(useCommandShortcut);\n    this.view = this.createView();\n    this.commands = this.createCommands();\n    this.specs.setContext({ ...this.context, view: this.view });\n    this.initEvent();\n  }\n\n  createSpecs() {\n    return createSpecs(this.linkAttributes);\n  }\n\n  createContext() {\n    return {\n      schema: this.schema,\n      eventEmitter: this.eventEmitter,\n    };\n  }\n\n  createSchema(htmlSchemaMap?: HTMLSchemaMap) {\n    return new Schema({\n      nodes: { ...this.specs.nodes, ...htmlSchemaMap!.nodes },\n      marks: { ...this.specs.marks, ...htmlSchemaMap!.marks },\n    });\n  }\n\n  createPlugins() {\n    return [\n      tableSelection(),\n      tableContextMenu(this.eventEmitter),\n      task(),\n      toolbarStateHighlight(this.eventEmitter),\n      ...this.createPluginProps(),\n    ].concat(this.defaultPlugins);\n  }\n\n  createPluginNodeViews() {\n    const { eventEmitter, pluginNodeViews } = this;\n    const pluginNodeViewMap: PluginNodeViews = {};\n\n    if (pluginNodeViews) {\n      Object.keys(pluginNodeViews).forEach((key) => {\n        pluginNodeViewMap[key] = (node, view, getPos) =>\n          pluginNodeViews[key](node, view, getPos, eventEmitter);\n      });\n    }\n\n    return pluginNodeViewMap;\n  }\n\n  createView() {\n    const { toDOMAdaptor, eventEmitter } = this;\n\n    return new EditorView(this.el, {\n      state: this.createState(),\n      attributes: {\n        class: CONTENTS_CLASS_NAME,\n      },\n      nodeViews: {\n        customBlock(node, view, getPos) {\n          return new CustomBlockView(node, view, getPos, toDOMAdaptor);\n        },\n        image(node, view, getPos) {\n          return new ImageView(node, view, getPos, eventEmitter);\n        },\n        codeBlock(node, view, getPos) {\n          return new CodeBlockView(node, view, getPos, eventEmitter);\n        },\n        widget: widgetNodeView,\n        ...this.createPluginNodeViews(),\n      },\n      dispatchTransaction: (tr) => {\n        const { state } = this.view.state.applyTransaction(tr);\n\n        this.view.updateState(state);\n        this.emitChangeEvent(tr.scrollIntoView());\n        this.eventEmitter.emit('setFocusedNode', state.selection.$from.node(1));\n      },\n      transformPastedHTML: changePastedHTML,\n      transformPasted: (slice: Slice) =>\n        changePastedSlice(slice, this.schema, isInTableNode(this.view.state.selection.$from)),\n      handlePaste: (view: EditorView, _: ClipboardEvent, slice: Slice) => pasteToTable(view, slice),\n      handleKeyDown: (_, ev) => {\n        this.eventEmitter.emit('keydown', this.editorType, ev);\n        return false;\n      },\n      handleDOMEvents: {\n        paste: (_, ev) => {\n          const clipboardData =\n            (ev as ClipboardEvent).clipboardData || (window as WindowWithClipboard).clipboardData;\n          const items = clipboardData?.items;\n\n          if (items) {\n            const containRtfItem = toArray(items).some(\n              (item) => item.kind === 'string' && item.type === 'text/rtf'\n            );\n\n            // if it contains rtf, it's most likely copy paste from office -> no image\n            if (!containRtfItem) {\n              const imageBlob = pasteImageOnly(items);\n\n              if (imageBlob) {\n                ev.preventDefault();\n\n                emitImageBlobHook(this.eventEmitter, imageBlob, ev.type);\n              }\n            }\n          }\n          return false;\n        },\n        keyup: (_, ev: KeyboardEvent) => {\n          this.eventEmitter.emit('keyup', this.editorType, ev);\n          return false;\n        },\n        scroll: () => {\n          this.eventEmitter.emit('scroll', 'editor');\n          return true;\n        },\n      },\n    });\n  }\n\n  createCommands() {\n    return this.specs.commands(this.view, getWwCommands());\n  }\n\n  getHTML() {\n    return removeProseMirrorHackNodes(this.view.dom.innerHTML);\n  }\n\n  getModel() {\n    return this.view.state.doc;\n  }\n\n  getSelection(): [number, number] {\n    const { from, to } = this.view.state.selection;\n\n    return [from, to];\n  }\n\n  getSchema() {\n    return this.view.state.schema;\n  }\n\n  replaceSelection(text: string, start?: number, end?: number) {\n    const { schema, tr } = this.view.state;\n    const lineTexts = text.split('\\n');\n    const paras = lineTexts.map((lineText) =>\n      createParagraph(schema, createNodesWithWidget(lineText, schema))\n    );\n    const slice = new Slice(Fragment.from(paras), 1, 1);\n    const newTr =\n      isNumber(start) && isNumber(end)\n        ? tr.replaceRange(start, end, slice)\n        : tr.replaceSelection(slice);\n\n    this.view.dispatch(newTr);\n    this.focus();\n  }\n\n  deleteSelection(start?: number, end?: number) {\n    const { tr } = this.view.state;\n    const newTr =\n      isNumber(start) && isNumber(end) ? tr.deleteRange(start, end) : tr.deleteSelection();\n\n    this.view.dispatch(newTr.scrollIntoView());\n  }\n\n  getSelectedText(start?: number, end?: number) {\n    const { doc, selection } = this.view.state;\n    let { from, to } = selection;\n\n    if (isNumber(start) && isNumber(end)) {\n      from = start;\n      to = end;\n    }\n    return doc.textBetween(from, to, '\\n');\n  }\n\n  setModel(newDoc: ProsemirrorNode | [], cursorToEnd = false) {\n    const { tr, doc } = this.view.state;\n\n    this.view.dispatch(tr.replaceWith(0, doc.content.size, newDoc));\n\n    if (cursorToEnd) {\n      this.moveCursorToEnd(true);\n    }\n  }\n\n  setSelection(start: number, end = start) {\n    const { tr } = this.view.state;\n    const selection = createTextSelection(tr, start, end);\n\n    this.view.dispatch(tr.setSelection(selection).scrollIntoView());\n  }\n\n  addWidget(node: Node, style: WidgetStyle, pos?: number) {\n    const { dispatch, state } = this.view;\n\n    dispatch(state.tr.setMeta('widget', { pos: pos ?? state.selection.to, node, style }));\n  }\n\n  replaceWithWidget(start: number, end: number, text: string) {\n    const { tr, schema } = this.view.state;\n    const nodes = createNodesWithWidget(text, schema);\n\n    this.view.dispatch(tr.replaceWith(start, end, nodes));\n  }\n\n  getRangeInfoOfNode(pos?: number) {\n    const { doc, selection } = this.view.state;\n    const $pos = pos ? doc.resolve(pos) : selection.$from;\n    const marks = $pos.marks();\n    const node = $pos.node();\n    let start = $pos.start();\n    let end = $pos.end();\n    let type = node.type.name;\n\n    if (marks.length || type === 'paragraph') {\n      const mark = marks[marks.length - 1];\n      const maybeHasMark = (nodeMarks: Mark[]) =>\n        nodeMarks.length ? includes(nodeMarks, mark) : true;\n\n      type = mark ? mark.type.name : 'text';\n\n      node.forEach((child, offset) => {\n        const { isText, nodeSize, marks: nodeMarks } = child;\n        const startOffset = $pos.pos - start;\n\n        if (\n          isText &&\n          offset <= startOffset &&\n          offset + nodeSize >= startOffset &&\n          maybeHasMark(nodeMarks as Mark[])\n        ) {\n          start = start + offset;\n          end = start + nodeSize;\n        }\n      });\n    }\n    return { range: [start, end] as [number, number], type };\n  }\n}\n"
  },
  {
    "path": "apps/editor/tsBannerGenerator.js",
    "content": "/*eslint-disable*/\nconst fs = require('fs');\nconst path = require('path');\nconst pkg = require('./package.json');\nconst rootPkg = require('../../package.json');\n\nconst tsVersion = /[0-9.]+/.exec(rootPkg.devDependencies.typescript)[0];\nconst declareFilePath = path.join(__dirname, './types/index.d.ts');\nconst TS_BANNER = [\n  '// Type definitions for TOAST UI Editor v' + pkg.version,\n  '// TypeScript Version: ' + tsVersion,\n].join('\\n');\nlet declareRows = [];\n\nfs.readFile(declareFilePath, 'utf8', (error, data) => {\n  if (error) {\n    throw error;\n  }\n\n  declareRows = data.toString().split('\\n');\n  declareRows.splice(0, 2, TS_BANNER);\n\n  fs.writeFile(declareFilePath, declareRows.join('\\n'), 'utf8', (error, data) => {\n    if (error) {\n      throw error;\n    }\n\n    console.log('Completed Write Banner for Typescript!');\n  });\n});\n"
  },
  {
    "path": "apps/editor/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"src/**/*.ts\", \"src/**/*.js\", \"types/**/*\", \"../../types/**/*\"],\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"],\n      \"@t/*\": [\"types/*\"]\n    },\n    \"lib\": [\"esnext\", \"dom\", \"dom.iterable\"]\n  }\n}"
  },
  {
    "path": "apps/editor/tuidoc.config.json",
    "content": "{\n  \"header\": {\n    \"logo\": {\n      \"src\": \"https://uicdn.toast.com/toastui/img/tui-editor-bi-white.png\",\n      \"linkUrl\": \"/\"\n    },\n    \"title\": {\n      \"text\": \"github\",\n      \"linkUrl\": \"https://github.com/nhn/tui.editor\"\n    },\n    \"version\": true\n  },\n  \"footer\": [\n    {\n      \"title\": \"NHN Cloud\",\n      \"linkUrl\": \"https://github.com/nhn\"\n    },\n    {\n      \"title\": \"FE Development Lab\",\n      \"linkUrl\": \"https://ui.toast.com\"\n    }\n  ],\n  \"main\": {\n    \"filePath\": \"README.md\"\n  },\n  \"api\": {\n    \"filePath\": [\n      \"tmpdoc/editorCore.js\",\n      \"tmpdoc/editor.js\",\n      \"tmpdoc/viewer.js\"\n    ],\n    \"permalink\": false\n  },\n  \"examples\": {\n    \"filePath\": \"examples\",\n    \"titles\": {\n      \"example01-editor-basic\": \"1. Editor\",\n      \"example02-editor-with-horizontal-preview\": \"2. Editor With Horizontal Preview\",\n      \"example03-editor-with-wysiwyg-mode\": \"3. Editor With WYSIWYG Mode\",\n      \"example04-viewer\": \"4. Viewer\",\n      \"example05-viewer-using-editor-factory\": \"5. Viewer Using Editor's Factory\",\n      \"example06-dark-theme\": \"6. Editor with Dark Theme\",\n      \"example07-editor-with-chart-plugin\": \"7. Editor with Chart Plugin\",\n      \"example08-editor-with-code-syntax-highlight-plugin\": \"8. Editor with Code Syntax Highlight Plugin\",\n      \"example09-editor-with-color-syntax-plugin\": \"9. Editor with Color Syntax Plugin\",\n      \"example10-editor-with-table-merged-cell-plugin\": \"10. Editor with Table Merged Cell Plugin\",\n      \"example11-editor-with-uml-plugin\": \"11. Editor with UML Plugin\",\n      \"example12-editor-with-all-plugins\": \"12. Editor with All Plugins\",\n      \"example13-creating-plugin\": \"13. Creating Plugin\",\n      \"example14-using-command\": \"14. Using Command\",\n      \"example15-customizing-toolbar-buttons\": \"15. Customizing Toolbar Buttons\",\n      \"example16-i18n\": \"16. Internationalization (i18n)\",\n      \"example17-placeholder\": \"17. Placeholder\"\n    },\n    \"globalErrorLogVariable\": true\n  },\n  \"pathPrefix\": \"tui.editor\"\n}\n"
  },
  {
    "path": "apps/editor/types/convertor.d.ts",
    "content": "import { NodeType, MarkType, Schema, ProsemirrorNode, Mark } from 'prosemirror-model';\nimport { MdNode, MdNodeType, RendererOptions, HTMLToken, MdPos } from './toastmark';\nimport { WwNodeType, WwMarkType } from './wysiwyg';\n\nexport type Attrs = { [name: string]: any } | null;\n\nexport interface StackItem {\n  type: NodeType;\n  attrs: Attrs | null;\n  content: ProsemirrorNode[];\n}\n\nexport interface ToWwConvertorState {\n  schema: Schema;\n  top(): StackItem;\n  push(node: ProsemirrorNode): void;\n  addText(text: string): void;\n  openMark(mark: Mark): void;\n  closeMark(mark: MarkType): void;\n  addNode(type: NodeType, attrs?: Attrs, content?: ProsemirrorNode[]): ProsemirrorNode | null;\n  openNode(type: NodeType, attrs?: Attrs): void;\n  closeNode(): ProsemirrorNode | null;\n  convertNode(mdNode: MdNode, infoForPosSync: InfoForPosSync): ProsemirrorNode | null;\n  convertByDOMParser(root: HTMLElement): void;\n}\n\ntype ToWwConvertor = (\n  state: ToWwConvertorState,\n  node: MdNode,\n  context: {\n    entering: boolean;\n    skipChildren: () => void;\n    leaf: boolean;\n    options: Omit<RendererOptions, 'convertors'>;\n    getChildrenText: (mdNode: MdNode) => string;\n    origin?: () => HTMLToken | HTMLToken[] | null;\n  },\n  customAttrs?: { htmlAttrs?: Record<string, any>; classNames?: string[] }\n) => void;\n\nexport type ToWwConvertorMap = Partial<Record<string, ToWwConvertor>>;\n\nexport type FirstDelimFn = (index: number) => string;\n\nexport interface ToMdConvertorState {\n  stopNewline: boolean;\n  inTable: boolean;\n  getDelim(): string;\n  setDelim(delim: string): void;\n  flushClose(size?: number): void;\n  wrapBlock(delim: string, firstDelim: string | null, node: ProsemirrorNode, fn: () => void): void;\n  ensureNewLine(): void;\n  write(content?: string): void;\n  closeBlock(node: ProsemirrorNode): void;\n  text(text: string, escaped?: boolean): void;\n  convertBlock(node: ProsemirrorNode, parent: ProsemirrorNode, index: number): void;\n  convertInline(parent: ProsemirrorNode): void;\n  convertList(node: ProsemirrorNode, delim: string, firstDelimFn: FirstDelimFn): void;\n  convertTableCell(node: ProsemirrorNode): void;\n  convertNode(parent: ProsemirrorNode, infoForPosSync?: InfoForPosSync): string;\n}\n\nexport interface ToDOMAdaptor {\n  getToDOMNode(type: string): ((node: ProsemirrorNode | Mark) => Node) | null;\n}\n\ntype HTMLToWwConvertor = (state: ToWwConvertorState, node: MdNode, openTagName: string) => void;\n\nexport type HTMLToWwConvertorMap = Partial<Record<string, HTMLToWwConvertor>>;\n\nexport interface FlattenHTMLToWwConvertorMap {\n  [k: string]: HTMLToWwConvertor;\n}\n\nexport interface NodeInfo {\n  node: ProsemirrorNode;\n  parent?: ProsemirrorNode;\n  index?: number;\n}\n\nexport interface MarkInfo {\n  node: Mark;\n  parent?: ProsemirrorNode;\n  index?: number;\n}\n\ninterface ToMdConvertorReturnValues {\n  delim?: string | string[];\n  rawHTML?: string | string[] | null;\n  text?: string;\n  attrs?: Attrs;\n}\n\ntype ToMdNodeTypeWriter = (\n  state: ToMdConvertorState,\n  nodeInfo: NodeInfo,\n  params: ToMdConvertorReturnValues\n) => void;\n\nexport type ToMdNodeTypeWriterMap = Partial<Record<WwNodeType, ToMdNodeTypeWriter>>;\n\ninterface ToMdMarkTypeOption {\n  mixable?: boolean;\n  removedEnclosingWhitespace?: boolean;\n  escape?: boolean;\n}\n\nexport type ToMdMarkTypeOptions = Partial<Record<WwMarkType, ToMdMarkTypeOption | null>>;\n\ntype ToMdNodeTypeConvertor = (state: ToMdConvertorState, nodeInfo: NodeInfo) => void;\n\nexport type ToMdNodeTypeConvertorMap = Partial<Record<WwNodeType, ToMdNodeTypeConvertor>>;\n\ntype ToMdMarkTypeConvertor = (\n  nodeInfo?: MarkInfo,\n  entering?: boolean\n) => ToMdConvertorReturnValues & ToMdMarkTypeOption;\n\nexport type ToMdMarkTypeConvertorMap = Partial<Record<WwMarkType, ToMdMarkTypeConvertor>>;\n\ninterface ToMdConvertorContext {\n  origin?: () => ReturnType<ToMdConvertor>;\n  entering?: boolean;\n  inTable?: boolean;\n}\n\ntype ToMdConvertor = (\n  nodeInfo: NodeInfo | MarkInfo,\n  context: ToMdConvertorContext\n) => ToMdConvertorReturnValues;\n\nexport type ToMdConvertorMap = Partial<Record<WwNodeType | MdNodeType, ToMdConvertor>>;\n\nexport interface ToMdConvertors {\n  nodeTypeConvertors: ToMdNodeTypeConvertorMap;\n  markTypeConvertors: ToMdMarkTypeConvertorMap;\n}\n\nexport interface InfoForPosSync {\n  node: MdNode | ProsemirrorNode | null;\n  setMappedPos: (pos: MdPos | number) => void;\n}\n"
  },
  {
    "path": "apps/editor/types/editor.d.ts",
    "content": "import { Schema, NodeSpec, MarkSpec, Fragment } from 'prosemirror-model';\nimport { EditorView, Decoration, DecorationSet } from 'prosemirror-view';\nimport { EditorState, Plugin, PluginKey, Selection, TextSelection } from 'prosemirror-state';\nimport { undoInputRule, InputRule, inputRules } from 'prosemirror-inputrules';\nimport { keymap } from 'prosemirror-keymap';\nimport { Editor } from '@t/index';\n\nimport {\n  HTMLConvertor,\n  MdPos,\n  Sourcepos,\n  Context as MdContext,\n  HTMLToken,\n  HTMLConvertorMap,\n} from './toastmark';\nimport { Emitter, Handler } from './event';\nimport { Context, EditorAllCommandMap, EditorCommandFn, SpecManager } from './spec';\nimport { ToMdConvertorMap } from './convertor';\nimport { ToolbarItemOptions, IndexList } from './ui';\nimport { CommandFn, PluginInfo } from './plugin';\nimport { HTMLMdNode } from './markdown';\n\nexport type PreviewStyle = 'tab' | 'vertical';\nexport type EditorType = 'markdown' | 'wysiwyg';\nexport type WidgetStyle = 'top' | 'bottom';\nexport interface WidgetRule {\n  rule: RegExp;\n  toDOM: (text: string) => HTMLElement;\n}\n\nexport type WidgetRuleMap = Record<string, WidgetRule>;\n\nexport interface EventMap {\n  load?: (param: Editor) => void;\n  change?: (editorType: EditorType) => void;\n  caretChange?: (editorType: EditorType) => void;\n  focus?: (editorType: EditorType) => void;\n  blur?: (editorType: EditorType) => void;\n  keydown?: (editorType: EditorType, ev: KeyboardEvent) => void;\n  keyup?: (editorType: EditorType, ev: KeyboardEvent) => void;\n  beforePreviewRender?: (html: string) => string;\n  beforeConvertWysiwygToMarkdown?: (markdownText: string) => string;\n}\n\ntype HookCallback = (url: string, text?: string) => void;\n\nexport type HookMap = {\n  addImageBlobHook?: (blob: Blob | File, callback: HookCallback) => void;\n};\n\nexport type AutolinkParser = (\n  content: string\n) => {\n  url: string;\n  text: string;\n  range: [number, number];\n}[];\n\nexport type ExtendedAutolinks = boolean | AutolinkParser;\n\nexport type LinkAttributeNames = 'rel' | 'target' | 'hreflang' | 'type';\n\n// @TODO change option and type name from singular to plural\nexport type LinkAttributes = Partial<Record<LinkAttributeNames, string>>;\n\nexport type Sanitizer = (content: string) => string;\n\nexport type HTMLMdNodeConvertor = (\n  node: HTMLMdNode,\n  context: MdContext,\n  convertors?: HTMLConvertorMap\n) => HTMLToken | HTMLToken[] | null;\n\nexport type HTMLMdNodeConvertorMap = Record<string, HTMLMdNodeConvertor>;\n\nexport type CustomHTMLRenderer = Partial<Record<string, HTMLConvertor | HTMLMdNodeConvertorMap>>;\n\nexport interface ViewerOptions {\n  el: HTMLElement;\n  initialValue?: string;\n  events?: EventMap;\n  plugins?: EditorPlugin[];\n  extendedAutolinks?: ExtendedAutolinks;\n  linkAttributes?: LinkAttributes;\n  customHTMLRenderer?: CustomHTMLRenderer;\n  referenceDefinition?: boolean;\n  customHTMLSanitizer?: Sanitizer;\n  frontMatter?: boolean;\n  usageStatistics?: boolean;\n  theme?: string;\n}\n\nexport class Viewer {\n  static isViewer: boolean;\n\n  constructor(options: ViewerOptions);\n\n  setMarkdown(markdown: string): void;\n\n  on(type: string, handler: Handler): void;\n\n  off(type: string): void;\n\n  destroy(): void;\n\n  isViewer(): boolean;\n\n  isMarkdownMode(): boolean;\n\n  isWysiwygMode(): boolean;\n\n  addHook(type: string, handler: Handler): void;\n}\n\nexport interface I18n {\n  setCode(code?: string): void;\n\n  setLanguage(codes: string | string[], data: Record<string, string>): void;\n\n  get(key: string, code?: string): string;\n}\n\nexport interface PluginContext {\n  eventEmitter: Emitter;\n  usageStatistics?: boolean;\n  i18n: I18n;\n  instance: Editor | Viewer;\n  pmState: {\n    Plugin: typeof Plugin;\n    PluginKey: typeof PluginKey;\n    Selection: typeof Selection;\n    TextSelection: typeof TextSelection;\n  };\n  pmView: { Decoration: typeof Decoration; DecorationSet: typeof DecorationSet };\n  pmModel: { Fragment: typeof Fragment };\n  pmRules: {\n    inputRules: typeof inputRules;\n    InputRule: typeof InputRule;\n    undoInputRule: typeof undoInputRule;\n  };\n  pmKeymap: {\n    keymap: typeof keymap;\n  };\n}\n\nexport type PluginFn = (context: PluginContext, options?: any) => PluginInfo | null;\nexport type EditorPlugin = PluginFn | [PluginFn, any];\ntype ContextInfo = {\n  eventEmitter: Emitter;\n  usageStatistics: boolean;\n  instance: Editor | Viewer;\n};\n\nexport type EditorPluginInfo = ContextInfo & {\n  plugin: EditorPlugin;\n};\n\nexport type EditorPluginsInfo = ContextInfo & {\n  plugins: EditorPlugin[];\n};\n\nexport interface EditorOptions {\n  el: HTMLElement;\n  height?: string;\n  minHeight?: string;\n  initialValue?: string;\n  previewStyle?: PreviewStyle;\n  initialEditType?: EditorType;\n  events?: EventMap;\n  hooks?: HookMap;\n  language?: string;\n  useCommandShortcut?: boolean;\n  usageStatistics?: boolean;\n  toolbarItems?: (string | ToolbarItemOptions)[][];\n  hideModeSwitch?: boolean;\n  plugins?: EditorPlugin[];\n  extendedAutolinks?: ExtendedAutolinks;\n  placeholder?: string;\n  linkAttributes?: LinkAttributes;\n  customHTMLRenderer?: CustomHTMLRenderer;\n  customMarkdownRenderer?: ToMdConvertorMap;\n  referenceDefinition?: boolean;\n  customHTMLSanitizer?: Sanitizer;\n  previewHighlight?: boolean;\n  frontMatter?: boolean;\n  widgetRules?: WidgetRule[];\n  theme?: string;\n  autofocus?: boolean;\n  viewer?: boolean;\n}\n\ninterface Slots {\n  mdEditor: HTMLElement;\n  mdPreview: HTMLElement;\n  wwEditor: HTMLElement;\n}\n\nexport class EditorCore {\n  constructor(options: EditorOptions);\n\n  public eventEmitter: Emitter;\n\n  public static factory(options: EditorOptions): EditorCore | Viewer;\n\n  public static setLanguage(code: string, data: Record<string, string>): void;\n\n  changePreviewStyle(style: PreviewStyle): void;\n\n  exec(name: string, payload?: Record<string, any>): void;\n\n  addCommand(type: EditorType, name: string, command: CommandFn): void;\n\n  on(type: string, handler: Handler): void;\n\n  off(type: string): void;\n\n  addHook(type: string, handler: Handler): void;\n\n  removeHook(type: string): void;\n\n  focus(): void;\n\n  blur(): void;\n\n  moveCursorToEnd(focus?: boolean): void;\n\n  moveCursorToStart(focus?: boolean): void;\n\n  setMarkdown(markdown: string, cursorToEnd?: boolean): void;\n\n  setHTML(html: string, cursorToEnd?: boolean): void;\n\n  getMarkdown(): string;\n\n  getHTML(): string;\n\n  insertText(text: string): void;\n\n  setSelection(start: EditorPos, end?: EditorPos): void;\n\n  replaceSelection(text: string, start?: EditorPos, end?: EditorPos): void;\n\n  deleteSelection(start?: EditorPos, end?: EditorPos): void;\n\n  getSelectedText(start?: EditorPos, end?: EditorPos): string;\n\n  getRangeInfoOfNode(pos?: EditorPos): NodeRangeInfo;\n\n  addWidget(node: Node, style: WidgetStyle, pos?: EditorPos): void;\n\n  replaceWithWidget(start: EditorPos, end: EditorPos, text: string): void;\n\n  setHeight(height: string): void;\n\n  getHeight(): string;\n\n  setMinHeight(minHeight: string): void;\n\n  getMinHeight(): string;\n\n  isMarkdownMode(): boolean;\n\n  isWysiwygMode(): boolean;\n\n  isViewer(): boolean;\n\n  getCurrentPreviewStyle(): PreviewStyle;\n\n  changeMode(mode: EditorType, isWithoutFocus?: boolean): void;\n\n  destroy(): void;\n\n  hide(): void;\n\n  show(): void;\n\n  setScrollTop(value: number): void;\n\n  getScrollTop(): number;\n\n  reset(): void;\n\n  getSelection(): SelectionPos;\n\n  setPlaceholder(placeholder: string): void;\n\n  getEditorElements(): Slots;\n\n  convertPosToMatchEditorMode(start: EditorPos, end?: EditorPos, mode?: EditorType): EditorPos[];\n}\n\nexport class Editor extends EditorCore {\n  insertToolbarItem({ groupIndex, itemIndex }: IndexList, item: string | ToolbarItemOptions): void;\n\n  removeToolbarItem(itemName: string): void;\n}\n\nexport type SelectionPos = Sourcepos | [from: number, to: number];\nexport type EditorPos = MdPos | number;\nexport interface NodeRangeInfo {\n  range: SelectionPos;\n  type: string;\n}\n\nexport interface Base {\n  el: HTMLElement;\n\n  editorType: EditorType;\n\n  eventEmitter: Emitter;\n\n  context: Context;\n\n  schema: Schema;\n\n  keymaps: Plugin[];\n\n  view: EditorView;\n\n  commands: EditorAllCommandMap;\n\n  specs: SpecManager;\n\n  placeholder: { text: string };\n\n  createSpecs(): SpecManager;\n\n  createContext(): Context;\n\n  createState(): EditorState;\n\n  createView(): EditorView;\n\n  createSchema(): Schema;\n\n  createKeymaps(useCommandShortcut: boolean): Plugin<any, any>[];\n\n  createCommands(): Record<string, EditorCommandFn<Record<string, any>>>;\n\n  focus(): void;\n\n  blur(): void;\n\n  destroy(): void;\n\n  moveCursorToStart(focus: boolean): void;\n\n  moveCursorToEnd(focus: boolean): void;\n\n  setScrollTop(top: number): void;\n\n  getScrollTop(): number;\n\n  setPlaceholder(text: string): void;\n\n  setHeight(height: number): void;\n\n  setMinHeight(minHeight: number): void;\n\n  getElement(): HTMLElement;\n\n  setSelection(start: EditorPos, end?: EditorPos): void;\n\n  replaceWithWidget(start: EditorPos, end: EditorPos, text: string): void;\n\n  addWidget(node: Node, style: WidgetStyle, pos?: EditorPos): void;\n\n  replaceSelection(text: string, start?: EditorPos, end?: EditorPos): void;\n\n  deleteSelection(start?: EditorPos, end?: EditorPos): void;\n\n  getSelectedText(start?: EditorPos, end?: EditorPos): string;\n\n  getSelection(): SelectionPos;\n\n  getRangeInfoOfNode(pos?: EditorPos): NodeRangeInfo;\n}\n\nexport type SchemaMap = Record<string, NodeSpec | MarkSpec>;\nexport interface HTMLSchemaMap {\n  nodes: SchemaMap;\n  marks: SchemaMap;\n}\n"
  },
  {
    "path": "apps/editor/types/event.d.ts",
    "content": "import { Mapable } from './map';\n\nexport interface Handler {\n  (...args: any[]): any;\n  namespace?: string;\n}\n\nexport interface Emitter {\n  listen(type: string, handler: Handler): void;\n  emit(type: string, ...args: any[]): any[];\n  emitReduce(type: string, source: any, ...args: any[]): any;\n  addEventType(type: string): void;\n  removeEventHandler(type: string, handler?: Handler): void;\n  getEvents(): Mapable<string, Handler[] | undefined>;\n  holdEventInvoke(fn: Function): void;\n}\n\nexport interface EmitterConstructor {\n  new (): Emitter;\n}\n\nexport type EventTypes =\n  | 'afterPreviewRender'\n  | 'updatePreview'\n  | 'changeMode'\n  | 'needChangeMode'\n  | 'command'\n  | 'changePreviewStyle'\n  | 'changePreviewTabPreview'\n  | 'changePreviewTabWrite'\n  | 'scroll'\n  | 'contextmenu'\n  | 'show'\n  | 'hide'\n  | 'changeLanguage'\n  | 'changeToolbarState'\n  | 'toggleScrollSync'\n  | 'mixinTableOffsetMapPrototype'\n  | 'setFocusedNode'\n  | 'removePopupWidget'\n  | 'query'\n  // provide event for user\n  | 'openPopup'\n  | 'closePopup'\n  | 'addImageBlobHook'\n  | 'beforePreviewRender'\n  | 'beforeConvertWysiwygToMarkdown'\n  | 'load'\n  | 'loadUI'\n  | 'change'\n  | 'caretChange'\n  | 'destroy'\n  | 'focus'\n  | 'blur'\n  | 'keydown'\n  | 'keyup';\n"
  },
  {
    "path": "apps/editor/types/index.d.ts",
    "content": "// Type definitions for TOAST UI Editor v3.2.2\n// TypeScript Version: 4.2.3\nimport {\n  EditorCore,\n  Editor,\n  Viewer,\n  EditorOptions,\n  ViewerOptions,\n  ExtendedAutolinks,\n  LinkAttributes,\n  Sanitizer,\n  EditorType,\n  PreviewStyle,\n  EventMap,\n  HookMap,\n  WidgetStyle,\n  WidgetRuleMap,\n  WidgetRule,\n  PluginContext,\n  I18n,\n  CustomHTMLRenderer,\n  HTMLMdNodeConvertor,\n  HTMLMdNodeConvertorMap,\n} from './editor';\nimport './toastui-editor-viewer';\n\nexport {\n  MdNode,\n  MdNodeType,\n  ListMdNode,\n  ListItemMdNode,\n  TableMdNode,\n  TableCellMdNode,\n  CodeBlockMdNode,\n  LinkMdNode,\n  ListData,\n  HeadingMdNode,\n  CodeMdNode,\n  HTMLConvertorMap,\n} from './toastmark';\nexport { ToMdConvertorMap } from './convertor';\nexport { Emitter, Handler } from './event';\nexport {\n  EditorOptions,\n  ViewerOptions,\n  ExtendedAutolinks,\n  LinkAttributes,\n  Sanitizer,\n  EditorType,\n  PreviewStyle,\n  EventMap,\n  HookMap,\n  WidgetStyle,\n  WidgetRuleMap,\n  WidgetRule,\n  PluginContext,\n  I18n,\n  CustomHTMLRenderer,\n  HTMLMdNodeConvertor,\n  HTMLMdNodeConvertorMap,\n};\nexport { Dispatch } from './spec';\nexport { PluginInfo, PluginNodeViews, CommandFn, PluginCommandMap } from './plugin';\nexport { MdLikeNode, HTMLMdNode } from './markdown';\nexport { Editor, EditorCore, Viewer };\nexport default Editor;\n\nexport declare namespace toastui {\n  export { Editor };\n}\n"
  },
  {
    "path": "apps/editor/types/map.d.ts",
    "content": "export interface Mapable<K, V> {\n  clear(): void;\n  delete(key: K): boolean;\n  forEach(callbackfn: (value: V, key: K, map: Mapable<K, V>) => void, thisArg?: any): void;\n  get(key: K): V | undefined;\n  has(key: K): boolean;\n  set(key: K, value: V): this;\n}\n"
  },
  {
    "path": "apps/editor/types/markdown.d.ts",
    "content": "import { MdNode, TableMdNode, Sourcepos, NodeWalker } from './toastmark';\n\nexport interface TableRowMdNode extends MdNode {\n  parent: TableBodyMdNode | TableHeadMdNode;\n}\n\nexport interface TableBodyMdNode extends MdNode {\n  parent: TableMdNode;\n}\n\nexport interface TableHeadMdNode extends MdNode {\n  parent: TableMdNode;\n  firstChild: TableRowMdNode;\n  lastChild: TableRowMdNode;\n  next: TableBodyMdNode;\n}\n\nexport interface MdLikeNode {\n  type: string;\n  literal: string | null;\n  wysiwygNode?: boolean;\n  level?: number;\n  destination?: string;\n  title?: string;\n  info?: string;\n  cellType?: 'head' | 'body';\n  align?: 'left' | 'center' | 'right';\n  listData?: {\n    type?: 'bullet' | 'ordered';\n    start?: number;\n    task?: boolean;\n    checked?: boolean;\n  };\n  attrs?: Record<string, string | null>;\n  childrenHTML?: string;\n}\n\nexport interface HTMLMdNode {\n  type: string;\n  id: number;\n  parent: MdNode | null;\n  prev: MdNode | null;\n  next: MdNode | null;\n  sourcepos?: Sourcepos;\n  firstChild: MdNode | null;\n  lastChild: MdNode | null;\n  literal: string | null;\n\n  isContainer(): boolean;\n  unlink(): void;\n  replaceWith(node: MdNode): void;\n  insertAfter(node: MdNode): void;\n  insertBefore(node: MdNode): void;\n  appendChild(child: MdNode): void;\n  prependChild(child: MdNode): void;\n  walker(): NodeWalker;\n\n  attrs?: Record<string, string | null>;\n  childrenHTML?: string;\n}\n"
  },
  {
    "path": "apps/editor/types/plugin.d.ts",
    "content": "import { Plugin, EditorState } from 'prosemirror-state';\nimport { EditorView, NodeView } from 'prosemirror-view';\nimport { Node } from 'prosemirror-model';\n\nimport { CustomParserMap } from './toastmark';\nimport { CustomHTMLRenderer } from './editor';\nimport { Emitter } from './event';\nimport { ToMdConvertorMap } from './convertor';\nimport { Dispatch, Payload, DefaultPayload } from './spec';\nimport { ToolbarItemOptions } from './ui';\n\nexport type PluginProp = (eventEmitter?: Emitter) => Plugin;\n\nexport type PluginNodeViews = (\n  node: Node,\n  view: EditorView,\n  getPos: () => number,\n  eventEmitter: Emitter\n) => NodeView;\n\ntype NodeViewPropMap = Record<string, PluginNodeViews>;\n\nexport type CommandFn<T = DefaultPayload> = (\n  payload: Payload<T>,\n  state: EditorState,\n  dispatch: Dispatch,\n  view: EditorView\n) => boolean;\nexport type PluginCommandMap = Record<string, CommandFn>;\n\ninterface PluginToolbarItem {\n  groupIndex: number;\n  itemIndex: number;\n  item: string | ToolbarItemOptions;\n}\n\nexport interface PluginInfo {\n  toHTMLRenderers?: CustomHTMLRenderer;\n  toMarkdownRenderers?: ToMdConvertorMap;\n  markdownPlugins?: PluginProp[];\n  wysiwygPlugins?: PluginProp[];\n  wysiwygNodeViews?: NodeViewPropMap;\n  markdownCommands?: PluginCommandMap;\n  wysiwygCommands?: PluginCommandMap;\n  toolbarItems?: PluginToolbarItem[];\n  markdownParsers?: CustomParserMap;\n}\n\nexport interface PluginInfoResult {\n  toHTMLRenderers: CustomHTMLRenderer;\n  toMarkdownRenderers: ToMdConvertorMap;\n  mdPlugins: PluginProp[];\n  wwPlugins: PluginProp[];\n  wwNodeViews: NodeViewPropMap;\n  mdCommands: PluginCommandMap;\n  wwCommands: PluginCommandMap;\n  toolbarItems: PluginToolbarItem[];\n  markdownParsers: CustomParserMap;\n}\n"
  },
  {
    "path": "apps/editor/types/prosemirror-commands.d.ts",
    "content": "import { EditorState, Transaction } from 'prosemirror-state';\nimport { EditorView } from 'prosemirror-view';\nimport { Schema } from 'prosemirror-model';\n\nimport 'prosemirror-commands';\n\ndeclare module 'prosemirror-commands' {\n  export interface Command<S extends Schema = any> {\n    (state: EditorState<S>, dispatch?: (tr: Transaction<S>) => void, view?: EditorView<S>): boolean;\n  }\n\n  export interface Keymap<S extends Schema = any> {\n    [key: string]: Command<S>;\n  }\n}\n"
  },
  {
    "path": "apps/editor/types/prosemirror-model.d.ts",
    "content": "import * as Model from 'prosemirror-model';\n\ndeclare module 'prosemirror-model' {\n  export interface Fragment {\n    textBetween(from: number, to: number, blockSeparator?: string, leafText?: string): string;\n    findIndex(pos: number, round?: number): { index: number; offset: number };\n    findDiffEnd(other: ProsemirrorNode | Fragment): { a: number; b: number } | null | undefined;\n  }\n\n  export type ProsemirrorNode = Model.Node;\n\n  export interface NodeType {\n    compatibleContent(node: NodeType): boolean;\n  }\n}\n"
  },
  {
    "path": "apps/editor/types/prosemirror-transform.d.ts",
    "content": "import { Slice, Node, Mark, NodeType } from 'prosemirror-model';\nimport 'prosemirror-transform';\n\ndeclare module 'prosemirror-transform' {\n  export interface Step {\n    slice: Slice;\n    from: number;\n    to: number;\n  }\n\n  export interface Transform {\n    setNodeMarkup(\n      pos: number,\n      type: Node | null,\n      attrs?: { [key: string]: any },\n      marks?: Mark[]\n    ): Transform;\n\n    split(\n      pos: number,\n      depth?: number | undefined,\n      typesAfter?:\n        | ({\n            type: NodeType;\n            attrs?:\n              | {\n                  [key: string]: any;\n                }\n              | null\n              | undefined;\n          } | null)[]\n        | undefined\n    ): Transform;\n  }\n}\n"
  },
  {
    "path": "apps/editor/types/spec.d.ts",
    "content": "import { Schema } from 'prosemirror-model';\nimport { Transaction, Plugin } from 'prosemirror-state';\nimport { EditorView } from 'prosemirror-view';\nimport { Command } from 'prosemirror-commands';\nimport { ToastMark } from './toastmark';\nimport { Emitter } from './event';\n\nexport interface Context {\n  schema: Schema;\n  eventEmitter: Emitter;\n}\n\nexport interface MdContext extends Context {\n  toastMark: ToastMark;\n}\n\nexport interface SpecContext extends Context {\n  view: EditorView;\n}\n\nexport interface MdSpecContext extends SpecContext {\n  toastMark: ToastMark;\n}\n\nexport type DefaultPayload = Record<string, any>;\nexport type Payload<T> = T extends infer P ? P : any;\n\nexport type Dispatch = (tr: Transaction) => void;\nexport type EditorCommand<T = DefaultPayload> = (payload?: Payload<T>) => Command;\nexport type EditorCommandMap<T = DefaultPayload> = Record<string, EditorCommand<T>>;\nexport type EditorCommandFn<T = DefaultPayload> = (payload?: Payload<T>) => boolean | void;\nexport type EditorAllCommandMap<T = DefaultPayload> = Record<string, EditorCommandFn<T>>;\n\nexport interface SpecManager {\n  commands(\n    view: EditorView,\n    addedCommands?: Record<string, EditorCommand>\n  ): EditorAllCommandMap<DefaultPayload>;\n\n  keymaps(useCommandShortcut: boolean): Plugin<any, any>[];\n\n  setContext(context: SpecContext): void;\n}\n"
  },
  {
    "path": "apps/editor/types/toastmark.d.ts",
    "content": "// @TODO replace these definition for Definitely Type\nexport type BlockNodeType =\n  | 'document'\n  | 'list'\n  | 'blockQuote'\n  | 'item'\n  | 'heading'\n  | 'thematicBreak'\n  | 'paragraph'\n  | 'codeBlock'\n  | 'htmlBlock'\n  | 'table'\n  | 'tableHead'\n  | 'tableBody'\n  | 'tableRow'\n  | 'tableCell'\n  | 'tableDelimRow'\n  | 'tableDelimCell'\n  | 'refDef'\n  | 'customBlock'\n  | 'frontMatter';\n\nexport type InlineNodeType =\n  | 'code'\n  | 'text'\n  | 'emph'\n  | 'strong'\n  | 'strike'\n  | 'link'\n  | 'image'\n  | 'htmlInline'\n  | 'linebreak'\n  | 'softbreak'\n  | 'customInline';\n\nexport type MdNodeType = BlockNodeType | InlineNodeType;\n\nexport type Pos = [number, number];\nexport type MdPos = Pos;\nexport type Sourcepos = [Pos, Pos];\n\nexport interface NodeWalker {\n  current: MdNode | null;\n  root: MdNode;\n  entering: boolean;\n\n  next(): { entering: boolean; node: MdNode } | null;\n  resumeAt(node: MdNode, entering: boolean): void;\n}\n\nexport interface MdNode {\n  type: MdNodeType;\n  id: number;\n  parent: MdNode | null;\n  prev: MdNode | null;\n  next: MdNode | null;\n  sourcepos?: Sourcepos;\n  firstChild: MdNode | null;\n  lastChild: MdNode | null;\n  literal: string | null;\n\n  isContainer(): boolean;\n  unlink(): void;\n  replaceWith(node: MdNode): void;\n  insertAfter(node: MdNode): void;\n  insertBefore(node: MdNode): void;\n  appendChild(child: MdNode): void;\n  prependChild(child: MdNode): void;\n  walker(): NodeWalker;\n}\n\nexport interface BlockMdNode extends MdNode {\n  type: BlockNodeType;\n\n  // temporal data (for parsing)\n  open: boolean;\n  lineOffsets: number[] | null;\n  stringContent: string | null;\n  lastLineBlank: boolean;\n  lastLineChecked: boolean;\n}\n\nexport interface ListData {\n  type: 'ordered' | 'bullet';\n  tight: boolean;\n  start: number;\n  bulletChar: string;\n  delimiter: string;\n  markerOffset: number;\n  padding: number;\n  task: boolean;\n  checked: boolean;\n}\n\nexport interface ListMdNode extends BlockMdNode {\n  listData: ListData | null;\n}\n\nexport interface ListItemMdNode extends BlockMdNode {\n  parent: MdNode;\n  listData: ListData;\n}\n\nexport interface HeadingMdNode extends BlockMdNode {\n  level: number;\n  headingType: 'atx' | 'setext';\n}\n\nexport interface CodeBlockMdNode extends BlockMdNode {\n  fenceOffset: number;\n  fenceLength: number;\n  fenceChar: string | null;\n  info: string | null;\n  infoPadding: number;\n}\n\nexport interface TableColumn {\n  align: 'left' | 'center' | 'right' | null;\n}\n\nexport interface TableMdNode extends BlockMdNode {\n  columns: TableColumn[];\n}\n\nexport interface TableCellMdNode extends BlockMdNode {\n  startIdx: number;\n  endIdx: number;\n  paddingLeft: number;\n  paddingRight: number;\n  ignored: boolean;\n  attrs?: Record<string, any>;\n}\n\nexport interface RefDefMdNode extends BlockMdNode {\n  title: string;\n  dest: string;\n  label: string;\n}\n\nexport interface CustomBlockMdNode extends BlockMdNode {\n  syntaxLength: number;\n  offset: number;\n  info: string;\n}\n\nexport interface HtmlBlockMdNode extends BlockMdNode {\n  htmlBlockType: number;\n}\n\nexport interface LinkMdNode extends MdNode {\n  destination: string | null;\n  title: string | null;\n  extendedAutolink: boolean;\n  lastChild: MdNode;\n}\n\nexport interface CodeMdNode extends MdNode {\n  tickCount: number;\n}\n\nexport interface CustomInlineMdNode extends MdNode {\n  info: string;\n}\n\nexport type AutolinkParser = (\n  content: string\n) => {\n  url: string;\n  text: string;\n  range: [number, number];\n}[];\n\nexport type CustomParser = (\n  node: MdNode,\n  context: { entering: boolean; options: ParserOptions }\n) => void;\nexport type CustomParserMap = Partial<Record<MdNodeType, CustomParser>>;\n\ntype RefDefState = {\n  id: number;\n  destination: string;\n  title: string;\n  unlinked: boolean;\n  sourcepos: Sourcepos;\n};\n\nexport type RefMap = {\n  [k: string]: RefDefState;\n};\n\nexport type RefLinkCandidateMap = {\n  [k: number]: {\n    node: BlockMdNode;\n    refLabel: string;\n  };\n};\n\nexport type RefDefCandidateMap = {\n  [k: number]: RefDefMdNode;\n};\n\nexport interface ParserOptions {\n  smart: boolean;\n  tagFilter: boolean;\n  extendedAutolinks: boolean | AutolinkParser;\n  disallowedHtmlBlockTags: string[];\n  referenceDefinition: boolean;\n  disallowDeepHeading: boolean;\n  frontMatter: boolean;\n  customParser: CustomParserMap | null;\n}\n\nexport class Parser {\n  constructor(options?: Partial<ParserOptions>);\n\n  advanceOffset(count: number, columns?: boolean): void;\n\n  advanceNextNonspace(): void;\n\n  findNextNonspace(): void;\n\n  addLine(): void;\n\n  addChild(tag: BlockNodeType, offset: number): BlockMdNode;\n\n  closeUnmatchedBlocks(): void;\n\n  finalize(block: BlockMdNode, lineNumber: number): void;\n\n  processInlines(block: BlockMdNode): void;\n\n  incorporateLine(ln: string): void;\n\n  // The main parsing function.  Returns a parsed document AST.\n  parse(input: string, lineTexts?: string[]): MdNode;\n\n  partialParseStart(lineNumber: number, lines: string[]): MdNode;\n\n  partialParseExtends(lines: string[]): void;\n\n  partialParseFinish(): void;\n\n  setRefMaps(\n    refMap: RefMap,\n    refLinkCandidateMap: RefLinkCandidateMap,\n    refDefCandidateMap: RefDefCandidateMap\n  ): void;\n\n  clearRefMaps(): void;\n}\n\nexport type HTMLConvertor = (\n  node: MdNode,\n  context: Context,\n  convertors?: HTMLConvertorMap\n) => HTMLToken | HTMLToken[] | null;\n\nexport type HTMLConvertorMap = Partial<Record<string, HTMLConvertor>>;\n\ninterface RendererOptions {\n  gfm: boolean;\n  softbreak: string;\n  nodeId: boolean;\n  tagFilter: boolean;\n  convertors?: HTMLConvertorMap;\n}\n\ninterface Context {\n  entering: boolean;\n  leaf: boolean;\n  options: Omit<RendererOptions, 'convertors'>;\n  getChildrenText: (node: MdNode) => string;\n  skipChildren: () => void;\n  origin?: () => ReturnType<HTMLConvertor>;\n}\n\ninterface TagToken {\n  tagName: string;\n  outerNewLine?: boolean;\n  innerNewLine?: boolean;\n}\n\nexport interface OpenTagToken extends TagToken {\n  type: 'openTag';\n  classNames?: string[];\n  attributes?: Record<string, any>;\n  selfClose?: boolean;\n}\n\nexport interface CloseTagToken extends TagToken {\n  type: 'closeTag';\n}\n\nexport interface TextToken {\n  type: 'text';\n  content: string;\n}\n\nexport interface RawHTMLToken {\n  type: 'html';\n  content: string;\n  outerNewLine?: boolean;\n}\n\nexport type HTMLToken = OpenTagToken | CloseTagToken | TextToken | RawHTMLToken;\n\nexport class Renderer {\n  constructor(customOptions?: Partial<RendererOptions>);\n\n  getConvertors(): HTMLConvertorMap;\n\n  getOptions(): RendererOptions;\n\n  render(rootNode: MdNode): string;\n\n  renderHTMLNode(node: HTMLToken): void;\n}\n\nexport interface RemovedNodeRange {\n  id: [number, number];\n  line: [number, number];\n}\n\nexport interface EditResult {\n  nodes: MdNode[];\n  removedNodeRange: RemovedNodeRange | null;\n}\n\ntype EventName = 'change';\n\ntype EventHandlerMap = {\n  [key in EventName]: Function[];\n};\n\nexport class ToastMark {\n  constructor(contents?: string, options?: Partial<ParserOptions>);\n\n  lineTexts: string[];\n\n  editMarkdown(startPos: Pos, endPos: Pos, newText: string): EditResult[];\n\n  getLineTexts(): string[];\n\n  getRootNode(): MdNode;\n\n  findNodeAtPosition(pos: Pos): MdNode | null;\n\n  findFirstNodeAtLine(line: number): MdNode | null;\n\n  on(eventName: EventName, callback: () => void): void;\n\n  off(eventName: EventName, callback: () => void): void;\n\n  findNodeById(id: number): MdNode | null;\n\n  removeAllNode(): void;\n}\n"
  },
  {
    "path": "apps/editor/types/toastui-editor-viewer.d.ts",
    "content": "declare module '@toast-ui/editor/dist/toastui-editor-viewer' {\n  import {\n    Viewer,\n    ViewerOptions,\n    ExtendedAutolinks,\n    LinkAttributes,\n    Sanitizer,\n    EventMap,\n    WidgetRuleMap,\n    WidgetRule,\n    PluginContext,\n    I18n,\n    CustomHTMLRenderer,\n    HTMLMdNodeConvertor,\n    HTMLMdNodeConvertorMap,\n    PluginInfo,\n    PluginNodeViews,\n    PluginCommandMap,\n  } from '@toast-ui/editor';\n\n  export {\n    ViewerOptions,\n    ExtendedAutolinks,\n    LinkAttributes,\n    Sanitizer,\n    EventMap,\n    WidgetRuleMap,\n    WidgetRule,\n    PluginContext,\n    I18n,\n    CustomHTMLRenderer,\n    HTMLMdNodeConvertor,\n    HTMLMdNodeConvertorMap,\n    PluginInfo,\n    PluginNodeViews,\n    PluginCommandMap,\n  };\n  export default Viewer;\n}\n"
  },
  {
    "path": "apps/editor/types/ui.d.ts",
    "content": "export interface PopupOptions {\n  body: HTMLElement;\n  className?: string;\n  style?: Record<string, any>;\n}\n\nexport interface ToolbarButtonOptions {\n  name: string;\n  tooltip?: string;\n  className?: string;\n  command?: string;\n  text?: string;\n  style?: Record<string, any>;\n  popup?: PopupOptions;\n  state?: ToolbarStateKeys;\n}\n\nexport interface ToolbarCustomOptions {\n  name: string;\n  tooltip?: string;\n  el?: HTMLElement;\n  popup?: PopupOptions;\n  hidden?: boolean;\n  state?: ToolbarStateKeys;\n  onMounted?: (execCommand: ExecCommand) => void;\n  onUpdated?: (toolbarState: ToolbarItemState) => void;\n}\n\nexport type ToolbarButtonInfo = {\n  hidden?: boolean;\n} & ToolbarButtonOptions;\n\nexport interface Component<T = {}, R = {}> {\n  props: T;\n  prevProps?: T;\n  state: R;\n  vnode: VNode;\n  refs: Record<string, HTMLElement>;\n  render(): VNode;\n  addEvent?(): void;\n  mounted?(): void;\n  updated?(prevProps: T): void;\n  beforeDestroy?(): void;\n}\n\nexport interface VNodeWalker {\n  current: VNode | null;\n\n  root: VNode | null;\n\n  entering: boolean;\n\n  walk: () => { vnode: VNode; entering: boolean } | null;\n}\n\nexport interface VNode {\n  type: string | ComponentClass;\n\n  props: Record<string, any>;\n\n  children: VNode[];\n\n  parent: VNode | null;\n\n  old: VNode | null;\n\n  firstChild: VNode | null;\n\n  next: VNode | null;\n\n  ref?: (node: Node | Component) => void | Node | Component;\n\n  node: Node | null;\n\n  effect: 'A' | 'U' | 'D';\n\n  component?: Component;\n\n  skip: boolean;\n\n  walker: () => VNodeWalker;\n}\n\nexport interface ComponentClass {\n  new (props?: any): Component;\n}\n\nexport interface Pos {\n  left: number;\n  top: number;\n}\n\nexport type TooltipStyle = {\n  display: 'none' | 'block';\n} & Partial<Pos>;\n\nexport interface PopupInfo {\n  className?: string;\n  style?: Record<string, any>;\n  fromEl: HTMLElement;\n  pos: Pos;\n  render: (props: Record<string, any>) => VNode | VNode[];\n  initialValues?: PopupInitialValues;\n}\n\nexport type PopupInitialValues = Record<string, any>;\n\nexport interface TabInfo {\n  name: string;\n  text: string;\n}\n\ninterface ToolbarItemState {\n  active: boolean;\n  disabled?: boolean;\n}\n\ninterface ToolbarStateMap {\n  taskList: ToolbarItemState;\n  orderedList: ToolbarItemState;\n  bulletList: ToolbarItemState;\n  table: ToolbarItemState;\n  strong: ToolbarItemState;\n  emph: ToolbarItemState;\n  strike: ToolbarItemState;\n  heading: ToolbarItemState;\n  thematicBreak: ToolbarItemState;\n  blockQuote: ToolbarItemState;\n  code: ToolbarItemState;\n  codeBlock: ToolbarItemState;\n  indent: ToolbarItemState;\n  outdent: ToolbarItemState;\n}\nexport type ToolbarStateKeys = keyof ToolbarStateMap;\n\nexport type ToolbarItemInfo = ToolbarCustomOptions | ToolbarButtonInfo;\nexport type ToolbarGroupInfo = ToolbarItemInfo[] & { hidden?: boolean };\nexport type ToolbarItemOptions = ToolbarCustomOptions | ToolbarButtonOptions;\nexport type ToolbarItem = (string | ToolbarItemOptions)[];\n\nexport type ExecCommand = (command: string, payload?: Record<string, any>) => void;\nexport type HidePopup = () => void;\nexport type SetPopupInfo = (info: PopupInfo) => void;\nexport type SetItemWidth = (name: string, width: number) => void;\nexport type ShowTooltip = (el: HTMLElement) => void;\nexport type HideTooltip = () => void;\nexport type GetBound = (el: HTMLElement, active?: boolean) => Pos;\n\nexport interface ContextMenuItem {\n  label: string;\n  className?: string;\n  disabled?: boolean;\n  onClick?: () => void;\n}\n\nexport interface IndexList {\n  groupIndex: number;\n  itemIndex: number;\n}\n\nexport interface DefaultUI {\n  destroy: () => void;\n  insertToolbarItem: (indexList: IndexList, item: string | ToolbarItemOptions) => void;\n  removeToolbarItem: (name: string) => void;\n}\n"
  },
  {
    "path": "apps/editor/types/wysiwyg.d.ts",
    "content": "import { ResolvedPos } from 'prosemirror-model';\nimport { Selection } from 'prosemirror-state';\n\nexport type WwNodeType =\n  | 'text'\n  | 'paragraph'\n  | 'heading'\n  | 'codeBlock'\n  | 'bulletList'\n  | 'orderedList'\n  | 'listItem'\n  | 'table'\n  | 'tableHead'\n  | 'tableBody'\n  | 'tableRow'\n  | 'tableHeadCell'\n  | 'tableBodyCell'\n  | 'blockQuote'\n  | 'thematicBreak'\n  | 'image'\n  | 'hardBreak'\n  | 'lineBreak'\n  | 'customBlock'\n  | 'frontMatter'\n  | 'widget'\n  | 'html'\n  | 'htmlComment';\n\nexport type WwMarkType = 'strong' | 'emph' | 'strike' | 'link' | 'code' | 'html';\n\nexport interface CellSelection extends Selection {\n  startCell: ResolvedPos;\n  endCell: ResolvedPos;\n}\n\nexport type ColumnAlign = 'left' | 'right' | 'center';\n"
  },
  {
    "path": "apps/editor/webpack.config.js",
    "content": "/* eslint-disable @typescript-eslint/no-var-requires */\nconst path = require('path');\nconst webpack = require('webpack');\nconst pkg = require('./package.json');\n\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin');\nconst CssMinimizerPlugin = require('css-minimizer-webpack-plugin');\nconst TerserPlugin = require('terser-webpack-plugin');\nconst { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');\nconst FileManagerPlugin = require('filemanager-webpack-plugin');\nconst ESLintPlugin = require('eslint-webpack-plugin');\nconst CopyPlugin = require('copy-webpack-plugin');\n\nconst ENTRY_EDITOR = './src/index.ts';\nconst ENTRY_ONLY_STYLE = './src/indexEditorOnlyStyle.ts';\nconst ENTRY_VIEWER = './src/indexViewer.ts';\n\nlet isProduction;\nlet minify;\n\nfunction addFileManagerPlugin(config) {\n  // When an entry option's value is set to a CSS file,\n  // empty JavaScript files are created. (e.g. toastui-editor-only.js)\n  // These files are unnecessary, so use the FileManager plugin to delete them.\n  const options = minify\n    ? {\n        delete: ['./dist/cdn/toastui-editor-only.min.js'],\n      }\n    : {\n        delete: ['./dist/toastui-editor-only.js'],\n        copy: [{ source: './dist/*.{js,css}', destination: './dist/cdn' }],\n      };\n\n  config.plugins.push(new FileManagerPlugin({ events: { onEnd: options } }));\n}\n\nfunction addCopyPluginForThemeCss(config) {\n  const options = minify\n    ? {\n        patterns: [{ from: './src/css/theme/*.css', to: './theme/toastui-editor-[name].min.css' }],\n      }\n    : {\n        patterns: [{ from: './src/css/theme/*.css', to: './theme/toastui-editor-[name].css' }],\n      };\n\n  config.plugins.push(new CopyPlugin(options));\n}\n\nfunction addMinifyPlugin(config) {\n  config.optimization = {\n    minimize: true,\n    minimizer: [\n      new TerserPlugin({\n        parallel: true,\n        extractComments: false,\n      }),\n      new CssMinimizerPlugin(),\n    ],\n  };\n}\n\nfunction addAnalyzerPlugin(config, type) {\n  config.plugins.push(\n    new BundleAnalyzerPlugin({\n      analyzerMode: 'static',\n      reportFilename: `../../report/webpack/stats-${pkg.version}-${type}.html`,\n    })\n  );\n}\n\nfunction setDevelopConfig(config) {\n  // check in examples\n  config.entry = { 'editor-all': ENTRY_EDITOR };\n  config.output.publicPath = '/dist/cdn';\n\n  config.plugins.pop();\n  config.externals = [];\n\n  config.devtool = 'inline-source-map';\n  config.devServer = {\n    // https://github.com/webpack/webpack-dev-server/issues/2484\n    injectClient: false,\n    inline: true,\n    host: '0.0.0.0',\n    port: 8080,\n    disableHostCheck: true,\n  };\n}\n\nfunction setProductionConfig(config) {\n  config.entry = {\n    editor: ENTRY_EDITOR,\n    'editor-only': ENTRY_ONLY_STYLE,\n    'editor-viewer': ENTRY_VIEWER,\n  };\n\n  addFileManagerPlugin(config);\n  addCopyPluginForThemeCss(config);\n\n  if (minify) {\n    addMinifyPlugin(config);\n    addAnalyzerPlugin(config, 'normal');\n  }\n}\n\nfunction setProductionConfigForAll(config) {\n  config.entry = { 'editor-all': ENTRY_EDITOR };\n  config.output.path = path.resolve(__dirname, 'dist/cdn');\n  config.externals = [];\n\n  addCopyPluginForThemeCss(config);\n\n  if (minify) {\n    addMinifyPlugin(config);\n    addAnalyzerPlugin(config, 'all');\n  }\n}\n\nmodule.exports = (env) => {\n  minify = !!env.minify;\n  isProduction = env.WEBPACK_BUILD;\n\n  const configs = Array(isProduction ? 2 : 1)\n    .fill(0)\n    .map(() => {\n      return {\n        mode: isProduction ? 'production' : 'development',\n        cache: false,\n        output: {\n          environment: {\n            arrowFunction: false,\n            const: false,\n          },\n          library: {\n            name: ['toastui', 'Editor'],\n            type: 'umd',\n            export: 'default',\n          },\n          path: path.resolve(__dirname, minify ? 'dist/cdn' : 'dist'),\n          filename: `toastui-[name]${minify ? '.min' : ''}.js`,\n        },\n        module: {\n          rules: [\n            {\n              test: /\\.ts$|\\.js$/,\n              use: [\n                {\n                  loader: 'ts-loader',\n                  options: {\n                    transpileOnly: true,\n                  },\n                },\n              ],\n              exclude: /node_modules/,\n            },\n            {\n              test: /\\.css$/,\n              use: [MiniCssExtractPlugin.loader, 'css-loader'],\n            },\n            {\n              test: /\\.png$/i,\n              type: 'asset/inline',\n            },\n          ],\n        },\n        resolve: {\n          extensions: ['.ts', '.js'],\n          alias: {\n            '@': path.resolve('src'),\n            '@t': path.resolve('types'),\n          },\n        },\n        plugins: [\n          new MiniCssExtractPlugin({\n            filename: ({ chunk }) =>\n              `toastui-${chunk.name.replace('-all', '')}${minify ? '.min' : ''}.css`,\n          }),\n          new webpack.BannerPlugin({\n            banner: [\n              pkg.name,\n              `@version ${pkg.version} | ${new Date().toDateString()}`,\n              `@author ${pkg.author}`,\n              `@license ${pkg.license}`,\n            ].join('\\n'),\n            raw: false,\n            entryOnly: true,\n          }),\n          new ESLintPlugin({\n            extensions: ['js', 'ts'],\n            exclude: ['node_modules', 'dist'],\n            failOnError: isProduction,\n          }),\n        ],\n        externals: [\n          {\n            'prosemirror-commands': {\n              commonjs: 'prosemirror-commands',\n              commonjs2: 'prosemirror-commands',\n              amd: 'prosemirror-commands',\n            },\n            'prosemirror-history': {\n              commonjs: 'prosemirror-history',\n              commonjs2: 'prosemirror-history',\n              amd: 'prosemirror-history',\n            },\n            'prosemirror-inputrules': {\n              commonjs: 'prosemirror-inputrules',\n              commonjs2: 'prosemirror-inputrules',\n              amd: 'prosemirror-inputrules',\n            },\n            'prosemirror-keymap': {\n              commonjs: 'prosemirror-keymap',\n              commonjs2: 'prosemirror-keymap',\n              amd: 'prosemirror-keymap',\n            },\n            'prosemirror-model': {\n              commonjs: 'prosemirror-model',\n              commonjs2: 'prosemirror-model',\n              amd: 'prosemirror-model',\n            },\n            'prosemirror-state': {\n              commonjs: 'prosemirror-state',\n              commonjs2: 'prosemirror-state',\n              amd: 'prosemirror-state',\n            },\n            'prosemirror-view': {\n              commonjs: 'prosemirror-view',\n              commonjs2: 'prosemirror-view',\n              amd: 'prosemirror-view',\n            },\n            'prosemirror-transform': {\n              commonjs: 'prosemirror-transform',\n              commonjs2: 'prosemirror-transform',\n              amd: 'prosemirror-transform',\n            },\n          },\n        ],\n        optimization: {\n          minimize: false,\n        },\n        performance: {\n          hints: false,\n        },\n      };\n    });\n\n  if (isProduction) {\n    setProductionConfig(configs[0]);\n    setProductionConfigForAll(configs[1]);\n  } else {\n    setDevelopConfig(configs[0]);\n  }\n\n  return configs;\n};\n"
  },
  {
    "path": "apps/react-editor/.eslintrc.js",
    "content": "module.exports = {\n  plugins: ['react'],\n  extends: ['plugin:react/recommended'],\n  rules: {\n    'react/prop-types': 0,\n  },\n  settings: {\n    react: {\n      version: 'detect',\n    },\n  },\n};\n"
  },
  {
    "path": "apps/react-editor/README.md",
    "content": "# TOAST UI Editor for React\n\n> This is a [React](https://reactjs.org/) component wrapping [TOAST UI Editor](https://github.com/nhn/tui.editor/tree/master/apps/editor).\n\n[![npm version](https://img.shields.io/npm/v/@toast-ui/react-editor.svg)](https://www.npmjs.com/package/@toast-ui/react-editor)\n\n## 🚩 Table of Contents\n\n- [Collect Statistics on the Use of Open Source](#collect-statistics-on-the-use-of-open-source)\n- [Install](#-install)\n- [Usage](#-usage)\n\n## Collect Statistics on the Use of Open Source\n\nReact Wrapper of TOAST UI Editor applies Google Analytics (GA) to collect statistics on the use of open source, in order to identify how widely TOAST UI Editor is used throughout the world. It also serves as important index to determine the future course of projects. location.hostname (e.g. ui.toast.com) is to be collected and the sole purpose is nothing but to measure statistics on the usage. To disable GA, use the `usageStatistics` props like the example below.\n\n```js\n<Editor\n  ...\n  usageStatistics={false}\n/>\n```\n\n## 💾 Install\n\n### Using npm\n\n```sh\nnpm install --save @toast-ui/react-editor\n```\n\n## 📝 Usage\n\n### Import\n\nYou can use TOAST UI Editor for React as a ECMAScript module or a CommonJS module. As this module does not contain CSS files, you should import `toastui-editor.css` from `@toast-ui/editor` in the script.\n\n- ES Modules\n\n```js\nimport '@toast-ui/editor/dist/toastui-editor.css';\n\nimport { Editor } from '@toast-ui/react-editor';\n```\n\n- CommonJS\n\n```js\nrequire('@toast-ui/editor/dist/toastui-editor.css');\n\nconst { Editor } = require('@toast-ui/react-editor');\n```\n\n### Props\n\n[All the options of the TOAST UI Editor](https://nhn.github.io/tui.editor/latest/ToastUIEditor) are supported in the form of props.\n\n```js\nimport '@toast-ui/editor/dist/toastui-editor.css';\n\nimport { Editor } from '@toast-ui/react-editor';\n\nconst MyComponent = () => (\n  <Editor\n    initialValue=\"hello react editor world!\"\n    previewStyle=\"vertical\"\n    height=\"600px\"\n    initialEditType=\"markdown\"\n    useCommandShortcut={true}\n  />\n);\n```\n\n### Instance Methods\n\nFor using [instance methods of TOAST UI Editor](https://nhn.github.io/tui.editor/latest/ToastUIEditor#addHook), first thing to do is creating Refs of wrapper component using [`createRef()`](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs). But the wrapper component does not provide a way to call instance methods of TOAST UI Editor directly. Instead, you can call `getInstance()` method of the wrapper component to get the instance, and call the methods on it.\n\n```js\nimport '@toast-ui/editor/dist/toastui-editor.css';\n\nimport { Editor } from '@toast-ui/react-editor';\n\nclass MyComponent extends React.Component {\n  editorRef = React.createRef();\n\n  handleClick = () => {\n    this.editorRef.current.getInstance().exec('bold');\n  };\n\n  render() {\n    return (\n      <>\n        <Editor\n          previewStyle=\"vertical\"\n          height=\"400px\"\n          initialEditType=\"markdown\"\n          initialValue=\"hello\"\n          ref={this.editorRef}\n        />\n        <button onClick={this.handleClick}>make bold</button>\n      </>\n    );\n  }\n}\n```\n\n#### Getting the Root Element\n\nAn instance of the wrapper component also provides a handy method for getting the root element. If you want to manipulate the root element directly, you can call `getRootElement` to get the element.\n\n```js\nimport '@toast-ui/editor/dist/toastui-editor.css';\n\nimport { Editor } from '@toast-ui/react-editor';\n\nclass MyComponent extends React.Component {\n  editorRef = React.createRef();\n\n  handleClickButton = () => {\n    this.editorRef.current.getRootElement().classList.add('my-editor-root');\n  };\n\n  render() {\n    return (\n      <>\n        <Editor\n          previewStyle=\"vertical\"\n          height=\"400px\"\n          initialEditType=\"markdown\"\n          initialValue=\"hello\"\n          ref={this.editorRef}\n        />\n        <button onClick={this.handleClickButton}>Click!</button>\n      </>\n    );\n  }\n}\n```\n\n### Events\n\n[All the events of TOAST UI Editor](https://nhn.github.io/tui.editor/latest/ToastUIEditor#focus) are supported in the form of `on[EventName]` props. The first letter of each event name should be capitalized. For example, for using `focus` event you can use `onFocus` prop like the example below.\n\n```js\nimport '@toast-ui/editor/dist/toastui-editor.css';\n\nimport { Editor } from '@toast-ui/react-editor';\n\nclass MyComponent extends React.Component {\n  handleFocus = () => {\n    console.log('focus!!');\n  };\n\n  render() {\n    return (\n      <Editor\n        previewStyle=\"vertical\"\n        height=\"400px\"\n        initialEditType=\"markdown\"\n        initialValue=\"hello\"\n        ref={this.editorRef}\n        onFocus={this.handleFocus}\n      />\n    );\n  }\n}\n```\n"
  },
  {
    "path": "apps/react-editor/demo/esm/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>Demo</title>\n  </head>\n  <body>\n    <div id=\"editor\"></div>\n    <!-- Editor -->\n    <script type=\"module\" src=\"./index.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/react-editor/demo/esm/index.jsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport { Editor } from '/dist/index.js';\nimport '@toast-ui/editor/dist/toastui-editor.css';\n\nconst content = [\n  '![image](https://uicdn.toast.com/toastui/img/tui-editor-bi.png)',\n  '',\n  '# Awesome Editor!',\n  '',\n  'It has been _released as opensource in 2018_ and has ~~continually~~ evolved to **receive 10k GitHub ⭐️ Stars**.',\n  '',\n  '## Create Instance',\n  '',\n  'You can create an instance with the following code and use `getHtml()` and `getMarkdown()` of the [Editor](https://github.com/nhn/tui.editor).',\n  '',\n  '```js',\n  'const editor = new Editor(options);',\n  '```',\n  '',\n  '> See the table below for default options',\n  '> > More API information can be found in the document',\n  '',\n  '| name | type | description |',\n  '| --- | --- | --- |',\n  '| el | `HTMLElement` | container element |',\n  '',\n  '## Features',\n  '',\n  '* CommonMark + GFM Specifications',\n  '   * Live Preview',\n  '   * Scroll Sync',\n  '   * Auto Indent',\n  '   * Syntax Highlight',\n  '        1. Markdown',\n  '        2. Preview',\n  '',\n  '## Support Wrappers',\n  '',\n  '> * Wrappers',\n  '>    1. [x] React',\n  '>    2. [x] Vue',\n  '>    3. [ ] Ember',\n].join('\\n');\n\nReactDOM.render(\n  <>\n    <Editor previewStyle=\"vertical\" initialValue={content} height=\"400px\" />\n  </>,\n  document.getElementById('editor')\n);\n"
  },
  {
    "path": "apps/react-editor/index.d.ts",
    "content": "import { Component } from 'react';\nimport ToastuiEditor, { EditorOptions, ViewerOptions, EventMap } from '@toast-ui/editor';\nimport ToastuiEditorViewer from '@toast-ui/editor/dist/toastui-editor-viewer';\n\nexport interface EventMapping {\n  onLoad: EventMap['load'];\n  onChange: EventMap['change'];\n  onCaretChange: EventMap['caretChange'];\n  onFocus: EventMap['focus'];\n  onBlur: EventMap['blur'];\n  onKeydown: EventMap['keydown'];\n  onKeyup: EventMap['keyup'];\n  onBeforePreviewRender: EventMap['beforePreviewRender'];\n  onBeforeConvertWysiwygToMarkdown: EventMap['beforeConvertWysiwygToMarkdown'];\n}\n\nexport type EventNames = keyof EventMapping;\n\nexport type EditorProps = Omit<EditorOptions, 'el'> & Partial<EventMapping>;\nexport type ViewerProps = Omit<ViewerOptions, 'el'> & Partial<EventMapping>;\n\nexport class Editor extends Component<EditorProps> {\n  getInstance(): ToastuiEditor;\n\n  getRootElement(): HTMLElement;\n}\n\nexport class Viewer extends Component<ViewerProps> {\n  getInstance(): ToastuiEditorViewer;\n\n  getRootElement(): HTMLElement;\n}\n"
  },
  {
    "path": "apps/react-editor/package.json",
    "content": "{\n  \"name\": \"@toast-ui/react-editor\",\n  \"version\": \"3.2.3\",\n  \"description\": \"TOAST UI Editor for React\",\n  \"files\": [\n    \"dist\",\n    \"index.d.ts\"\n  ],\n  \"main\": \"dist/toastui-react-editor.js\",\n  \"module\": \"dist/esm/index.js\",\n  \"scripts\": {\n    \"test:types\": \"tsc\",\n    \"lint\": \"eslint .\",\n    \"serve\": \"snowpack dev\",\n    \"build\": \"webpack build && rollup -c\"\n  },\n  \"homepage\": \"https://ui.toast.com\",\n  \"bugs\": {\n    \"url\": \"https://github.com/nhn/tui.editor/issues\"\n  },\n  \"author\": \"NHN Cloud FE Development Lab <dl_javascript@nhn.com>\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/nhn/tui.editor.git\",\n    \"directory\": \"apps/react-editor\"\n  },\n  \"license\": \"MIT\",\n  \"browserslist\": \"last 2 versions, ie 11\",\n  \"peerDependencies\": {\n    \"react\": \"^17.0.1\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^17.0.3\",\n    \"react\": \"^17.0.1\",\n    \"react-dom\": \"^17.0.1\"\n  },\n  \"dependencies\": {\n    \"@toast-ui/editor\": \"^3.2.2\"\n  }\n}\n"
  },
  {
    "path": "apps/react-editor/rollup.config.js",
    "content": "import typescript from '@rollup/plugin-typescript';\nimport commonjs from '@rollup/plugin-commonjs';\nimport { nodeResolve } from '@rollup/plugin-node-resolve';\nimport banner from 'rollup-plugin-banner';\nimport { version, author, license } from './package.json';\n\nconst bannerText = [\n  'TOAST UI Editor : React Wrapper',\n  `@version ${version} | ${new Date().toDateString()}`,\n  `@author ${author}`,\n  `@license ${license}`,\n].join('\\n');\n\nexport default [\n  {\n    input: 'src/index.ts',\n    output: {\n      dir: 'dist/esm',\n      format: 'es',\n      sourcemap: false,\n    },\n    plugins: [typescript(), commonjs(), nodeResolve(), banner(bannerText)],\n    external: ['react', '@toast-ui/editor', '@toast-ui/editor/dist/toastui-editor-viewer'],\n  },\n];\n"
  },
  {
    "path": "apps/react-editor/snowpack.config.js",
    "content": "/** @type {import(\"snowpack\").SnowpackUserConfig } */\nmodule.exports = {\n  mount: {\n    'demo/esm': '/',\n    src: '/dist',\n  },\n  devOptions: {\n    port: 8080,\n  },\n  alias: {\n    '@': './src',\n    '@t': './types',\n  },\n};\n"
  },
  {
    "path": "apps/react-editor/src/editor.tsx",
    "content": "import React from 'react';\nimport Editor, { EventMap } from '@toast-ui/editor';\nimport type { EditorProps, EventNames } from '../index';\n\nexport default class extends React.Component<EditorProps> {\n  rootEl = React.createRef<HTMLDivElement>();\n\n  editorInst!: Editor;\n\n  getRootElement() {\n    return this.rootEl.current;\n  }\n\n  getInstance() {\n    return this.editorInst;\n  }\n\n  getBindingEventNames() {\n    return Object.keys(this.props)\n      .filter((key) => /^on[A-Z][a-zA-Z]+/.test(key))\n      .filter((key) => this.props[key as EventNames]);\n  }\n\n  bindEventHandlers(props: EditorProps) {\n    this.getBindingEventNames().forEach((key) => {\n      const eventName = key[2].toLowerCase() + key.slice(3);\n\n      this.editorInst.off(eventName);\n      this.editorInst.on(eventName, props[key as EventNames]!);\n    });\n  }\n\n  getInitEvents() {\n    return this.getBindingEventNames().reduce(\n      (acc: Record<string, EventMap[keyof EventMap]>, key) => {\n        const eventName = (key[2].toLowerCase() + key.slice(3)) as keyof EventMap;\n\n        acc[eventName] = this.props[key as EventNames];\n\n        return acc;\n      },\n      {}\n    );\n  }\n\n  componentDidMount() {\n    this.editorInst = new Editor({\n      el: this.rootEl.current!,\n      ...this.props,\n      events: this.getInitEvents(),\n    });\n  }\n\n  shouldComponentUpdate(nextProps: EditorProps) {\n    const instance = this.getInstance();\n    const { height, previewStyle } = nextProps;\n\n    if (height && this.props.height !== height) {\n      instance.setHeight(height);\n    }\n\n    if (previewStyle && this.props.previewStyle !== previewStyle) {\n      instance.changePreviewStyle(previewStyle);\n    }\n\n    this.bindEventHandlers(nextProps);\n\n    return false;\n  }\n\n  render() {\n    return <div ref={this.rootEl} />;\n  }\n}\n"
  },
  {
    "path": "apps/react-editor/src/index.ts",
    "content": "import Editor from './editor';\nimport Viewer from './viewer';\n\nexport { Editor, Viewer };\n"
  },
  {
    "path": "apps/react-editor/src/viewer.tsx",
    "content": "import React from 'react';\nimport Viewer, { EventMap } from '@toast-ui/editor/dist/toastui-editor-viewer';\nimport { ViewerProps, EventNames } from '../index';\n\nexport default class ViewerComponent extends React.Component<ViewerProps> {\n  rootEl = React.createRef<HTMLDivElement>();\n\n  viewerInst!: Viewer;\n\n  getRootElement() {\n    return this.rootEl.current;\n  }\n\n  getInstance() {\n    return this.viewerInst;\n  }\n\n  getBindingEventNames() {\n    return Object.keys(this.props)\n      .filter((key) => /^on[A-Z][a-zA-Z]+/.test(key))\n      .filter((key) => this.props[key as EventNames]);\n  }\n\n  bindEventHandlers(props: ViewerProps) {\n    this.getBindingEventNames().forEach((key) => {\n      const eventName = key[2].toLowerCase() + key.slice(3);\n\n      this.viewerInst.off(eventName);\n      this.viewerInst.on(eventName, props[key as EventNames]!);\n    });\n  }\n\n  getInitEvents() {\n    return this.getBindingEventNames().reduce(\n      (acc: Record<string, EventMap[keyof EventMap]>, key) => {\n        const eventName = (key[2].toLowerCase() + key.slice(3)) as keyof EventMap;\n\n        acc[eventName] = this.props[key as EventNames];\n\n        return acc;\n      },\n      {}\n    );\n  }\n\n  componentDidMount() {\n    this.viewerInst = new Viewer({\n      el: this.rootEl.current!,\n      ...this.props,\n      events: this.getInitEvents(),\n    });\n  }\n\n  shouldComponentUpdate(nextProps: ViewerProps) {\n    this.bindEventHandlers(nextProps);\n\n    return false;\n  }\n\n  render() {\n    return <div ref={this.rootEl} />;\n  }\n}\n"
  },
  {
    "path": "apps/react-editor/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"src/**/*.ts\", \"src/**/*.tsx\", \"index.d.ts\"],\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"jsx\": \"react\",\n    \"lib\": [\"esnext\", \"dom\", \"dom.iterable\"]\n  }\n}\n"
  },
  {
    "path": "apps/react-editor/webpack.config.js",
    "content": "/* eslint-disable @typescript-eslint/no-var-requires */\nconst path = require('path');\nconst webpack = require('webpack');\nconst { version, author, license } = require('./package.json');\n\nconst config = {\n  entry: './src/index.ts',\n  output: {\n    filename: 'toastui-react-editor.js',\n    path: path.resolve(__dirname, 'dist'),\n    library: {\n      type: 'commonjs2',\n    },\n  },\n  externals: {\n    '@toast-ui/editor': {\n      commonjs: '@toast-ui/editor',\n      commonjs2: '@toast-ui/editor',\n    },\n    '@toast-ui/editor/dist/toastui-editor-viewer': {\n      commonjs: '@toast-ui/editor/dist/toastui-editor-viewer',\n      commonjs2: '@toast-ui/editor/dist/toastui-editor-viewer',\n    },\n    react: {\n      commonjs: 'react',\n      commonjs2: 'react',\n    },\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.tsx?$/,\n        use: [\n          {\n            loader: 'ts-loader',\n            options: {\n              transpileOnly: true,\n            },\n          },\n        ],\n        exclude: /node_modules/,\n      },\n    ],\n  },\n  resolve: {\n    extensions: ['.tsx', '.ts', '.js'],\n  },\n  plugins: [\n    new webpack.BannerPlugin({\n      banner: [\n        'TOAST UI Editor : React Wrapper',\n        `@version ${version} | ${new Date().toDateString()}`,\n        `@author ${author}`,\n        `@license ${license}`,\n      ].join('\\n'),\n    }),\n  ],\n};\n\nmodule.exports = () => config;\n"
  },
  {
    "path": "apps/vue-editor/.eslintrc.js",
    "content": "module.exports = {\n  parser: 'vue-eslint-parser',\n  parserOptions: {\n    parser: '@typescript-eslint/parser',\n  },\n  extends: ['plugin:vue/base'],\n  plugins: ['vue'],\n};\n"
  },
  {
    "path": "apps/vue-editor/README.md",
    "content": "# TOAST UI Editor for Vue\n\n> This is [Vue](https://vuejs.org/) component wrapping [TOAST UI Editor](https://github.com/nhn/tui.editor/tree/master/apps/editor).\n\n[![npm version](https://img.shields.io/npm/v/@toast-ui/vue-editor.svg)](https://www.npmjs.com/package/@toast-ui/vue-editor)\n\n## 🚩 Table of Contents\n\n- [Collect Statistics on the Use of Open Source](#collect-statistics-on-the-use-of-open-source)\n- [Install](#-install)\n- [Editor Usage](#-editor-usage)\n- [Viewer Usage](#-viewer-usage)\n\n## Collect Statistics on the Use of Open Source\n\nVue Wrapper of TOAST UI Editor applies Google Analytics (GA) to collect statistics on the use of open source, in order to identify how widely TOAST UI Editor is used throughout the world. It also serves as important index to determine the future course of projects. location.hostname (e.g. ui.toast.com) is to be collected and the sole purpose is nothing but to measure statistics on the usage. To disable GA, use the following `usageStatistics` options when declare Vue Wrapper component.\n\n```js\nconst options = {\n  ...\n  usageStatistics: false\n}\n```\n\n## 💾 Install\n\n### Using npm\n\n```sh\nnpm install --save @toast-ui/vue-editor\n```\n\n## 📝 Editor Usage\n\n### Import\n\nYou can use Toast UI Editor for Vue as a ECMAScript module or a CommonJS module. As this module does not contain CSS files, you should import `toastui-editor.css` from `@toast-ui/editor` in the script.\n\n- ES Modules\n\n```js\nimport '@toast-ui/editor/dist/toastui-editor.css';\n\nimport { Editor } from '@toast-ui/vue-editor';\n```\n\n- CommonJS\n\n```js\nrequire('@toast-ui/editor/dist/toastui-editor.css');\n\nconst { Editor } = require('@toast-ui/vue-editor');\n```\n\n### Creating Component\n\nFirst implement `<editor/>` in the template.\n\n```html\n<template>\n  <editor />\n</template>\n```\n\nAnd then add `Editor` to the `components` in your component or Vue instance like this:\n\n```js\nimport '@toast-ui/editor/dist/toastui-editor.css';\n\nimport { Editor } from '@toast-ui/vue-editor';\n\nexport default {\n  components: {\n    editor: Editor\n  }\n};\n```\n\nor\n\n```js\nimport '@toast-ui/editor/dist/toastui-editor.css';\n\nimport { Editor } from '@toast-ui/vue-editor';\n\nnew Vue({\n  el: '#app',\n  components: {\n    editor: Editor\n  }\n});\n```\n\n### Props\n\n| Name            | Type   | Default                    | Description                                               |\n| --------------- | ------ | -------------------------- | --------------------------------------------------------- |\n| initialValue    | String | ''                         | Editor's initial value       .                             |\n| initialEditType | String | 'markdown'                 | Initial editor type (markdown, wysiwyg).                   |\n| options         | Object | following `defaultOptions` | Options of tui.editor. This is for initailize tui.editor. |\n| height          | String | '300px'                    | This prop can control the height of the editor.           |\n| previewStyle          | String | 'vertical'           | Markdown editor's preview style (tab, vertical).           |\n\n```js\nconst defaultOptions = {\n  minHeight: '200px',\n  language: 'en-US',\n  useCommandShortcut: true,\n  usageStatistics: true,\n  hideModeSwitch: false,\n  toolbarItems: [\n    ['heading', 'bold', 'italic', 'strike'],\n    ['hr', 'quote'],\n    ['ul', 'ol', 'task', 'indent', 'outdent'],\n    ['table', 'image', 'link'],\n    ['code', 'codeblock'],\n    ['scrollSync'],\n  ]\n};\n```\n\n```html\n<template>\n  <editor\n    :initialValue=\"editorText\"\n    :options=\"editorOptions\"\n    height=\"500px\"\n    initialEditType=\"wysiwyg\"\n    previewStyle=\"vertical\"\n  />\n</template>\n<script>\n  import '@toast-ui/editor/dist/toastui-editor.css';\n\n  import { Editor } from '@toast-ui/vue-editor';\n\n  export default {\n    components: {\n      editor: Editor\n    },\n    data() {\n      return {\n        editorText: 'This is initialValue.',\n        editorOptions: {\n          hideModeSwitch: true\n        }\n      };\n    }\n  };\n</script>\n```\n\n### Instance Methods\n\nIf you want to more manipulate the Editor, you can use `invoke` method to call the method of toastui.editor. For more information of method, see [instance methods of TOAST UI Editor](https://nhn.github.io/tui.editor/latest/ToastUIEditor#addHook).\n\nFirst, you need to assign `ref` attribute of `<editor/>` and then you can use `invoke` method through `this.$refs` like this:\n\n```html\n<template>\n  <editor ref=\"toastuiEditor\" />\n</template>\n<script>\n  import '@toast-ui/editor/dist/toastui-editor.css';\n\n  import { Editor } from '@toast-ui/vue-editor';\n\n  export default {\n    components: {\n      editor: Editor\n    },\n    methods: {\n      scroll() {\n        this.$refs.toastuiEditor.invoke('setScrollTop', 10);\n      },\n      moveTop() {\n        this.$refs.toastuiEditor.invoke('moveCursorToStart');\n      },\n      getHTML() {\n        let html = this.$refs.toastuiEditor.invoke('getHTML');\n      }\n    }\n  };\n</script>\n```\n\n### Events\n\n- load : It would be emitted when editor fully load\n- change : It would be emitted when content changed\n- caretChange : It would be emitted when format change by cursor position\n- focus : It would be emitted when editor get focus\n- blur : It would be emitted when editor loose focus\n\n```html\n<template>\n  <editor\n    @load=\"onEditorLoad\"\n    @focus=\"onEditorFocus\"\n    @blur=\"onEditorBlur\"\n    @change=\"onEditorChange\"\n    @caretChange=\"onEditorCaretChange\"\n  />\n</template>\n<script>\n  import { Editor } from '@toast-ui/vue-editor';\n\n  export default {\n    components: {\n      editor: Editor\n    },\n    methods: {\n      onEditorLoad() {\n        // implement your code\n      },\n      onEditorFocus() {\n        // implement your code\n      },\n      onEditorBlur() {\n        // implement your code\n      },\n      onEditorChange() {\n        // implement your code\n      },\n      onEditorCaretChange() {\n        // implement your code\n      }\n    }\n  };\n</script>\n```\n\n## 📃 Viewer Usage\n\n### Import\n\n- ES Modules\n\n```js\nimport '@toast-ui/editor/dist/toastui-editor-viewer.css';\n\nimport { Viewer } from '@toast-ui/vue-editor';\n```\n\n- CommonJS\n\n```js\nrequire('@toast-ui/editor/dist/toastui-editor-viewer.css');\n\nconst { Viewer } = require('@toast-ui/vue-editor');\n```\n\n### Creating Component\n\nFirst implement `<viewer />` in the template.\n\n```html\n<template>\n  <viewer />\n</template>\n```\n\nAnd then add `Viewer` to the `components` in your component or Vue instance like this:\n\n```js\nimport '@toast-ui/editor/dist/toastui-editor-viewer.css';\n\nimport { Viewer } from '@toast-ui/vue-editor';\n\nexport default {\n  components: {\n    viewer: Viewer\n  }\n};\n```\n\nor\n\n```js\nimport '@toast-ui/editor/dist/toastui-editor-viewer.css';\n\nimport { Viewer } from '@toast-ui/vue-editor';\n\nnew Vue({\n  el: '#app',\n  components: {\n    viewer: Viewer\n  }\n});\n```\n\n### Props\n\n| Name         | Type   | Default | Description                                     |\n| ------------ | ------ | ------- | ----------------------------------------------- |\n| initialValue | String | ''      | Viewer's initial value                          |\n| height       | String | '300px' | This prop can control the height of the viewer. |\n| options      | Object | above `defaultOptions` | Options of tui.editor. This is for initailize tui.editor. |\n\n```html\n<template>\n  <viewer :initialValue=\"viewerText\" height=\"500px\" />\n</template>\n<script>\n  import '@toast-ui/editor/dist/toastui-editor-viewer.css';\n\n  import { Viewer } from '@toast-ui/vue-editor';\n\n  export default {\n    components: {\n      viewer: Viewer\n    },\n    data() {\n      return {\n        viewerText: '# This is Viewer.\\n Hello World.'\n      };\n    }\n  };\n</script>\n```"
  },
  {
    "path": "apps/vue-editor/demo/esm/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>Demo</title>\n  </head>\n  <body>\n    <div id=\"editor\"></div>\n    <!-- Editor -->\n    <script type=\"module\" src=\"./index.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/vue-editor/demo/esm/index.js",
    "content": "import Vue from 'vue/dist/vue.esm.browser';\nimport { Editor } from '/dist/index.js';\nimport '@toast-ui/editor/dist/toastui-editor.css';\n\nVue.component('editor', Editor);\n\nconst content = [\n  '![image](https://uicdn.toast.com/toastui/img/tui-editor-bi.png)',\n  '',\n  '# Awesome Editor!',\n  '',\n  'It has been _released as opensource in 2018_ and has ~~continually~~ evolved to **receive 10k GitHub ⭐️ Stars**.',\n  '',\n  '## Create Instance',\n  '',\n  'You can create an instance with the following code and use `getHtml()` and `getMarkdown()` of the [Editor](https://github.com/nhn/tui.editor).',\n  '',\n  '```js',\n  'const editor = new Editor(options);',\n  '```',\n  '',\n  '> See the table below for default options',\n  '> > More API information can be found in the document',\n  '',\n  '| name | type | description |',\n  '| --- | --- | --- |',\n  '| el | `HTMLElement` | container element |',\n  '',\n  '## Features',\n  '',\n  '* CommonMark + GFM Specifications',\n  '   * Live Preview',\n  '   * Scroll Sync',\n  '   * Auto Indent',\n  '   * Syntax Highlight',\n  '        1. Markdown',\n  '        2. Preview',\n  '',\n  '## Support Wrappers',\n  '',\n  '> * Wrappers',\n  '>    1. [x] React',\n  '>    2. [x] Vue',\n  '>    3. [ ] Ember',\n].join('\\n');\n\nnew Vue({\n  data() {\n    return {\n      initialValue: content,\n    };\n  },\n  template: '<editor :initialValue=\"initialValue\" height=\"400px\" />',\n}).$mount('#editor');\n"
  },
  {
    "path": "apps/vue-editor/index.d.ts",
    "content": "import Vue from 'vue';\nimport ToastuiEditor from '@toast-ui/editor';\nimport ToastuiEditorViewer from '@toast-ui/editor/dist/toastui-editor-viewer';\n\ntype FunctionKeys<T extends object> = {\n  [K in keyof T]: T[K] extends Function ? K : never;\n}[keyof T];\n\ntype EditorFnKeys = FunctionKeys<ToastuiEditor>;\ntype ViewerFnKeys = FunctionKeys<ToastuiEditorViewer>;\n\nexport class Editor extends Vue {\n  invoke<T extends EditorFnKeys>(\n    fname: T,\n    ...args: Parameters<ToastuiEditor[T]>\n  ): ReturnType<ToastuiEditor[T]>;\n\n  getRootElement(): HTMLElement;\n}\n\nexport class Viewer extends Vue {\n  invoke<T extends ViewerFnKeys>(\n    fname: T,\n    ...args: Parameters<ToastuiEditorViewer[T]>\n  ): ReturnType<ToastuiEditorViewer[T]>;\n\n  getRootElement(): HTMLElement;\n}\n"
  },
  {
    "path": "apps/vue-editor/package.json",
    "content": "{\n  \"name\": \"@toast-ui/vue-editor\",\n  \"version\": \"3.2.3\",\n  \"description\": \"TOAST UI Editor for Vue\",\n  \"main\": \"dist/toastui-vue-editor.js\",\n  \"module\": \"dist/esm/index.js\",\n  \"files\": [\n    \"dist\",\n    \"index.d.ts\"\n  ],\n  \"scripts\": {\n    \"lint\": \"eslint .\",\n    \"serve\": \"snowpack dev\",\n    \"build\": \"webpack build && rollup -c\"\n  },\n  \"homepage\": \"https://ui.toast.com\",\n  \"bugs\": {\n    \"url\": \"https://github.com/nhn/tui.editor/issues\"\n  },\n  \"author\": \"NHN Cloud FE Development Lab <dl_javascript@nhn.com>\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/nhn/tui.editor.git\",\n    \"directory\": \"apps/vue-editor\"\n  },\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"vue\": \"^2.5.0\",\n    \"vue-loader\": \"^15.9.8\",\n    \"vue-template-compiler\": \"^2.6.12\",\n    \"@morgul/snowpack-plugin-vue2\": \"^0.4.0\"\n  },\n  \"peerDependencies\": {\n    \"vue\": \"^2.5.0\"\n  },\n  \"dependencies\": {\n    \"@toast-ui/editor\": \"^3.2.2\"\n  }\n}\n"
  },
  {
    "path": "apps/vue-editor/rollup.config.js",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport { nodeResolve } from '@rollup/plugin-node-resolve';\nimport vue from 'rollup-plugin-vue';\nimport banner from 'rollup-plugin-banner';\nimport * as ts from 'typescript';\nimport { version, author, license } from './package.json';\n\nfunction transpile() {\n  return {\n    name: 'transpile',\n    transform(code) {\n      const result = ts.transpileModule(code, {\n        compilerOptions: {\n          target: 'es5',\n          module: 'es6',\n          importHelpers: true,\n        },\n      });\n\n      return result.outputText;\n    },\n  };\n}\n\nconst bannerText = [\n  'TOAST UI Editor : Vue Wrapper',\n  `@version ${version} | ${new Date().toDateString()}`,\n  `@author ${author}`,\n  `@license ${license}`,\n].join('\\n');\n\nexport default [\n  {\n    input: 'src/index.js',\n    output: {\n      dir: 'dist/esm',\n      format: 'es',\n      sourcemap: false,\n    },\n    plugins: [vue({}), commonjs(), nodeResolve(), transpile(), banner(bannerText)],\n    external: ['vue', '@toast-ui/editor', '@toast-ui/editor/dist/toastui-editor-viewer'],\n  },\n];\n"
  },
  {
    "path": "apps/vue-editor/snowpack.config.js",
    "content": "/** @type {import(\"snowpack\").SnowpackUserConfig } */\nmodule.exports = {\n  mount: {\n    'demo/esm': '/',\n    src: '/dist',\n  },\n  devOptions: {\n    port: 8080,\n  },\n  plugins: ['@morgul/snowpack-plugin-vue2'],\n};\n"
  },
  {
    "path": "apps/vue-editor/src/Editor.vue",
    "content": "<template>\n  <div ref=\"toastuiEditor\"></div>\n</template>\n<script>\nimport Editor from '@toast-ui/editor';\nimport { optionsMixin } from './mixin/option';\n\nexport default {\n  name: 'ToastuiEditor',\n  mixins: [optionsMixin],\n  props: {\n    previewStyle: {\n      type: String,\n    },\n    height: {\n      type: String,\n    },\n    initialEditType: {\n      type: String,\n    },\n    initialValue: {\n      type: String,\n    },\n    options: {\n      type: Object,\n    },\n  },\n  watch: {\n    previewStyle(newValue) {\n      this.editor.changePreviewStyle(newValue);\n    },\n    height(newValue) {\n      this.editor.height(newValue);\n    },\n  },\n  mounted() {\n    const options = { ...this.computedOptions, el: this.$refs.toastuiEditor };\n\n    this.editor = new Editor(options);\n  },\n  methods: {\n    getRootElement() {\n      return this.$refs.toastuiEditor;\n    },\n  },\n};\n</script>\n"
  },
  {
    "path": "apps/vue-editor/src/Viewer.vue",
    "content": "<template>\n  <div ref=\"toastuiEditorViewer\"></div>\n</template>\n<script>\nimport Viewer from '@toast-ui/editor/dist/toastui-editor-viewer';\nimport { optionsMixin } from './mixin/option';\n\nexport default {\n  name: 'ToastuiEditorViewer',\n  mixins: [optionsMixin],\n  props: {\n    height: {\n      type: String,\n    },\n    initialValue: {\n      type: String,\n    },\n    options: {\n      type: Object,\n    },\n  },\n  mounted() {\n    const options = { ...this.computedOptions, el: this.$refs.toastuiEditorViewer };\n\n    this.editor = new Viewer(options);\n  },\n  methods: {\n    getRootElement() {\n      return this.$refs.toastuiEditorViewer;\n    },\n  },\n};\n</script>\n"
  },
  {
    "path": "apps/vue-editor/src/index.js",
    "content": "import Editor from './Editor.vue';\nimport Viewer from './Viewer.vue';\n\nexport { Editor, Viewer };\n"
  },
  {
    "path": "apps/vue-editor/src/mixin/option.js",
    "content": "const editorEvents = [\n  'load',\n  'change',\n  'caretChange',\n  'focus',\n  'blur',\n  'keydown',\n  'keyup',\n  'beforePreviewRender',\n  'beforeConvertWysiwygToMarkdown',\n];\nconst defaultValueMap = {\n  initialEditType: 'markdown',\n  initialValue: '',\n  height: '300px',\n  previewStyle: 'vertical',\n};\n\nexport const optionsMixin = {\n  data() {\n    const eventOptions = {};\n\n    editorEvents.forEach((event) => {\n      eventOptions[event] = (...args) => {\n        this.$emit(event, ...args);\n      };\n    });\n    const options = {\n      ...this.options,\n      initialEditType: this.initialEditType,\n      initialValue: this.initialValue,\n      height: this.height,\n      previewStyle: this.previewStyle,\n      events: eventOptions,\n    };\n\n    Object.keys(defaultValueMap).forEach((key) => {\n      if (!options[key]) {\n        options[key] = defaultValueMap[key];\n      }\n    });\n\n    return { editor: null, computedOptions: options };\n  },\n  methods: {\n    invoke(methodName, ...args) {\n      let result = null;\n\n      if (this.editor[methodName]) {\n        result = this.editor[methodName](...args);\n      }\n\n      return result;\n    },\n  },\n  destroyed() {\n    editorEvents.forEach((event) => {\n      this.editor.off(event);\n    });\n    this.editor.destroy();\n  },\n};\n"
  },
  {
    "path": "apps/vue-editor/webpack.config.js",
    "content": "/* eslint-disable @typescript-eslint/no-var-requires */\nconst path = require('path');\nconst VueLoaderPlugin = require('vue-loader/lib/plugin');\nconst webpack = require('webpack');\nconst { version, author, license } = require('./package.json');\n\nmodule.exports = {\n  entry: './src/index.js',\n  output: {\n    filename: 'toastui-vue-editor.js',\n    path: path.resolve(__dirname, 'dist'),\n    library: {\n      type: 'commonjs2',\n    },\n    environment: {\n      arrowFunction: false,\n      const: false,\n    },\n  },\n  externals: {\n    '@toast-ui/editor': {\n      commonjs: '@toast-ui/editor',\n      commonjs2: '@toast-ui/editor',\n    },\n    '@toast-ui/editor/dist/toastui-editor-viewer': {\n      commonjs: '@toast-ui/editor/dist/toastui-editor-viewer',\n      commonjs2: '@toast-ui/editor/dist/toastui-editor-viewer',\n    },\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.vue$/,\n        loader: 'vue-loader',\n        exclude: /node_modules/,\n      },\n      {\n        test: /\\.js$/,\n        use: [\n          {\n            loader: 'ts-loader',\n            options: {\n              transpileOnly: true,\n            },\n          },\n        ],\n        exclude: /node_modules/,\n      },\n    ],\n  },\n  plugins: [\n    new VueLoaderPlugin(),\n    new webpack.BannerPlugin({\n      banner: [\n        'TOAST UI Editor : Vue Wrapper',\n        `@version ${version} | ${new Date().toDateString()}`,\n        `@author ${author}`,\n        `@license ${license}`,\n      ].join('\\n'),\n    }),\n  ],\n};\n"
  },
  {
    "path": "docs/COMMIT_MESSAGE_CONVENTION.md",
    "content": "# Commit Message Convention\n\n## Commit Message Format\n\n```\n<Type>: Short description (fix #1234)\n\nLonger description here if necessary\n\nBREAKING CHANGE: only contain breaking change\n```\n* Any line of the commit message cannot be longer 100 characters!\n\n## Revert\n```\nrevert: commit <short-hash>\n\nThis reverts commit <full-hash>\nMore description if needed\n```\n\n## Type\nMust be one of the following:\n\n* **feat**: A new feature\n* **fix**: A bug fix\n* **docs**: Documentation only changes\n* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)\n* **refactor**: A code change that neither fixes a bug nor adds a feature\n* **perf**: A code change that improves performance\n* **test**: Adding missing or correcting existing tests\n* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation\n\n## Subject\n* use the imperative, __present__ tense: \"change\" not \"changed\" nor \"changes\"\n* don't capitalize the first letter\n* no dot (.) at the end\n* reference GitHub issues at the end. If the commit doesn’t completely fix the issue, then use `(refs #1234)` instead of `(fixes #1234)`.\n\n## Body\n\n* use the imperative, __present__ tense: \"change\" not \"changed\" nor \"changes\".\n* the motivation for the change and contrast this with previous behavior.\n\n## BREAKING CHANGE\n* This commit contains breaking change(s).\n* start with the word BREAKING CHANGE: with a space or two newlines. The rest of the commit message is then used for this.\n\nThis convention is based on [AngularJS](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits) and [ESLint](https://eslint.org/docs/developer-guide/contributing/pull-requests#step2)\n"
  },
  {
    "path": "docs/ISSUE_TEMPLATE.md",
    "content": "<!--\nThank you for your contribution.\n\nWhen writing an issue, please, use the template below.\nIt's mandatory to use the template for submitting the new issue.\nWe don't reply to the issue not following the template.\n-->\n\n<!-- BUG ISSUE TEMPLATE -->\n## Version\n<!-- Write the version of the @toast-ui/editor you are currently using. -->\n\n## Test Environment\n<!-- Write the browser type, OS and so on -->\n\n## Current Behavior\n<!-- Write steps to reproduce the current behaviour in detail.\nYou can add sample code, 'CodePen' or 'jsfiddle' links. -->\n\n```js\n// Write example code\n```\n\n## Expected Behavior\n<!-- Write a description for future action. -->\n"
  },
  {
    "path": "docs/PULL_REQUEST_TEMPLATE.md",
    "content": "<!-- EDIT TITLE PLEASE -->\n<!-- It should be one of them\n  <ISSUE TYPE>: Short Description (<CLOSING TYPE> #<ISSUE NUMBERS>)\n  ex)\n  feat: add new feature (close #111)\n  fix: wrong behavior (fix #111)\n  chore: change build tool (ref #111)\n-->\n\n<!-- SPECIFY A ISSUE TYPE AT HEAD\n  feat: A new feature\n  fix: A bug fix\n  docs: Documentation only changes\n  style: Changes that do not affect the meaning of the code (white-space, formatting etc)\n  refactor: A code change that neither fixes a bug or adds a feature\n  perf: A code change that improves performance\n  test: Adding missing tests\n  chore: Changes to the build process or auxiliary tools and libraries such as documentation generation\n-->\n\n<!-- ADD CLOSING TYPE AND ISSUE NUMBER AT TAIL\n  (<CLOSING TYPE> #<ISSUE NUMBERS>)\n  close: resolve not a bug(feature, docs, etc) completely\n  fix: resolve a bug completely\n  ref: not fully resolved or related to\n-->\n\n### Please check if the PR fulfills these requirements\n- [ ] It's the right issue type on the title\n- [ ] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where \"xxx\" is the issue number)\n- [ ] The commit message follows our guidelines\n- [ ] Tests for the changes have been added (for bug fixes/features)\n- [ ] Docs have been added/updated (for bug fixes/features)\n- [ ] It does not introduce a breaking change or has a description of the breaking change\n\n### Description\n\n\n\n---\nThank you for your contribution to TOAST UI product. 🎉 😘 ✨\n"
  },
  {
    "path": "docs/README.md",
    "content": "# 📄 Documents\n\n* [한글 가이드](https://github.com/nhn/tui.editor/blob/master/docs/ko/README.md)\n\n## Tutorials\n\n- [🚀 Getting Started](https://github.com/nhn/tui.editor/blob/master/docs/en/getting-started.md)\n- [👀 Viewer](https://github.com/nhn/tui.editor/blob/master/docs/en/viewer.md)\n- [🧩 Plugins](https://github.com/nhn/tui.editor/blob/master/docs/en/plugin.md)\n- [🌏 Internationalization (i18n)](https://github.com/nhn/tui.editor/blob/master/docs/en/i18n.md)\n- [🎨 Custom HTML Renderer](https://github.com/nhn/tui.editor/blob/master/docs/en/custom-html-renderer.md)\n- [🔩 Custom Block](https://github.com/nhn/tui.editor/blob/master/docs/en/custom-block.md)\n- [🔗 Extended Autolinks](https://github.com/nhn/tui.editor/blob/master/docs/en/extended-autolinks.md)\n- [🛠 Toolbar](https://github.com/nhn/tui.editor/blob/master/docs/en/toolbar.md)\n- [📱 Widget](https://github.com/nhn/tui.editor/blob/master/docs/en/widget.md)\n\n### Migration Guide\n\n- [✈️ v3.0 Migration Guide](https://github.com/nhn/tui.editor/blob/master/docs/v3.0-migration-guide.md)\n\n## Etc\n\n- [📌 Commit Message Convention](https://github.com/nhn/tui.editor/blob/master/docs/COMMIT_MESSAGE_CONVENTION.md)\n- [📌 Contributing](https://github.com/nhn/tui.editor/blob/master/CONTRIBUTING.md)\n- [📌 Code of conduct](https://github.com/nhn/tui.editor/blob/master/CODE_OF_CONDUCT.md)"
  },
  {
    "path": "docs/en/custom-block.md",
    "content": "# 🔩 Custom Block Node And HTML Node\n\nThe TOAST UI Editor (henceforth referred to as 'Editor') follows the [CommonMark](https://spec.commonmark.org/0.29/) specification, and also supports the [GFM](https://github.github.com/gfm/) specification. But what if you want to use a specific syntax that is not supported by CommonMark or GFM? For example, you might want to use [LaTeX](https://www.latex-project.org/) syntax or render elements such as charts in Markdown. The editor provides the option to define a **custom block node** for this usability.\n\n## Custom Block Node\n\nThe editor provides the `customHTMLRenderer` option that can be customized when converting Markdown Abstract Syntax Tree(AST) to HTML text. Using `customHTMLRenderer` option, rendering results of nodes supported by CommonMark or GFM can be customized like `table` and `heading`. Custom block nodes can also be defined using this `customHTMLRenderer` option.\n\nThe following code defines a custom block node that renders math typesetting using KaTeX, a library that supports LaTex syntax.\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  customHTMLRenderer: {\n    latex(node) {\n      const generator = new latexjs.HtmlGenerator({ hyphenate: false });\n      const { body } = latexjs.parse(node.literal, { generator }).htmlDocument();\n\n      return [\n        { type: 'openTag', tagName: 'div', outerNewLine: true },\n        { type: 'html', content: body.innerHTML },\n        { type: 'closeTag', tagName: 'div', outerNewLine: true }\n      ];\n    },\n  }\n});\n```\n\nThe `latex` function property was written in the `customHTMLRenderer` option, which returns HTML to be rendered in token format. It is easy to use because it configures options in almost the same form as when customizing a markdown node. The code above is rendered in the Markdown Editor as follows.\n\n![image](https://user-images.githubusercontent.com/37766175/120983159-65bf2b00-c7b4-11eb-84af-30c38e832585.png)\n\nAs you can see in the image above, in order to use a custom block node in a markdown editor, text must be entered within a block enclosed by the `$$` symbol. Blocks wrapped with `$$` symbols are parsed from editor to custom block nodes. In addition, to indicate which custom block node it is, the node name defined by the `customHTMLRenderer` option must be written next to the `$$` symbol.\n\n```js\n// The node name must be written next to the $$ symbol.\n$$latex\n\\documentclass{article}\n\\begin{document}\n\n$\nf(x) = \\int_{-\\infty}^\\infty \\hat f(\\xi)\\,e^{2 \\pi i \\xi x} \\, d\\xi\n$\n\\end{document}\n$$\n```\n\n### WYSIWYG\n\nThe custom block node in the WYSIWYG Editor works like the image below.\n\n![image](https://user-images.githubusercontent.com/37766175/120984395-96539480-c7b5-11eb-8e57-2f43082f345f.gif)\n\nIn WYSIWYG Editor, the custom block node is rendered in the same result as a markdown preview, and can be changed by clicking on the node and using the edit button that appears when selected. Because the custom block node are eventually parsed based on specific text, editing in the WYSIWYG Editor is also based on text. This operation is different from general WYSIWYG editors, but it is more ideal because the **TOAST UI Editor supports WYSIWYG editors based on markdown**.\n\n## HTML Node\n\nCommonMark uses `<` and `>` characters to write nodes that are not supported by default in HTML text.\n([CommonMark Raw HTML Spec](https://spec.commonmark.org/0.29/#raw-html))\n\nBecause Markdown Editor also follows these specifications, HTML text are rendered correctly in the Markdown preview.\n\n![image](https://user-images.githubusercontent.com/37766175/120987131-44f8d480-c7b8-11eb-971f-0b4ecb59e112.png)\n\n### WYSIWYG\nUnfortunately, WYSIWYG Editor cannot render HTML nodes properly. The editor internally manages nodes supported by the WYSIWYG Editor as abstracted model object. Nodes that are supported by WYSIWYG Editor are nodes that are supported by CommonMark and GFM (such as `heading`, `list`, `strike` and others) and custom block node.\n\n![image](https://user-images.githubusercontent.com/37766175/120989247-4c20e200-c7ba-11eb-8420-7ff5726592cf.gif)\n\nThe `iframe` node in the example image above is not a node supported by WYSIWYG Editor. Therefore, if you want to use `iframe` node in WYSIWYG Editor, you need to set it up using `customHTMLRenderer` option.\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  customHTMLRenderer: {\n    htmlBlock: {\n      iframe(node) {\n        return [\n          { type: 'openTag', tagName: 'iframe', outerNewLine: true, attributes: node.attrs },\n          { type: 'html', content: node.childrenHTML },\n          { type: 'closeTag', tagName: 'iframe', outerNewLine: true },\n        ];\n      },\n    }\n  },\n});\n```\n\nHTML nodes are defined in the `customHTMLRenderer.htmlBlock` property. To distinguish it from the custom block nodes described above, it should be configured within the `htmlBlock` property. If you run the example code, `iframe` node will be rendered correctly in WYSIWYG as shown in the image below.\n\n![image](https://user-images.githubusercontent.com/37766175/120989209-40352000-c7ba-11eb-9112-047a0af4f9d6.gif)\n\nIf you want to use an inline HTML node, it should be configured in the `customHTMLRenderer.htmlInline` property.\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  customHTMLRenderer: {\n    htmlBlock: {\n      iframe(node) {\n        return [\n          { type: 'openTag', tagName: 'iframe', outerNewLine: true, attributes: node.attrs },\n          { type: 'html', content: node.childrenHTML },\n          { type: 'closeTag', tagName: 'iframe', outerNewLine: true },\n        ];\n      },\n    },\n    htmlInline: {\n      big(node, { entering }) {\n        return entering\n          ? { type: 'openTag', tagName: 'big', attributes: node.attrs }\n          : { type: 'closeTag', tagName: 'big' };\n      },\n    },\n  },\n});\n```"
  },
  {
    "path": "docs/en/custom-html-renderer.md",
    "content": "# 🎨 Custom HTML Renderer\n\nThe TOAST UI Editor (henceforth referred to as 'Editor') provides a way to customize the final HTML contents.\n\nThe Editor uses its own markdown parser called `ToastMark`, which has two steps for converting markdown text to HTML text. The first step is converting markdown text into AST(Abstract Syntax Tree), and the second step is generating HTML text from the AST. Although it's tricky to customize the first step, the second step can be easily customized by providing a set of functions that convert a certain type of node to HTML string.\n\n## Basic Usage\n\nThe Editor accepts the `customHTMLRenderer` option, which is a key-value object. The keys of the object is types of node of the AST, and the values are convertor functions to be used for converting a node to a list of tokens.\n\nThe following code is a basic example of using `customHTMLRenderer` option.\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  customHTMLRenderer: {\n    heading(node, context) {\n      return {\n        type: context.entering ? 'openTag' : 'closeTag',\n        tagName: 'div',\n        classNames: [`heading-${node.level}`]\n      };\n    },\n    text(node, context) {\n      const strongContent = node.parent.type === 'strong';\n      return {\n        type: 'text',\n        content: strongContent ? node.literal.toUpperCase() : node.literal\n      };\n    },\n    linebreak(node, context) {\n      return {\n        type: 'html',\n        content: '\\n<br />\\n'\n      };\n    }\n  }\n});\n```\n\nIf we set the following markdown content,\n\n```markdown\n## Heading\n\nHello\nWorld\n```\n\nThe final HTML content will be like below.\n\n```html\n<div class=\"heading-2\">HEADING</div>\n<p>Hello<br /><br />World</p>\n```\n\n## Tokens\n\nAs you can see in the basic example above, each convertor function returns a token object instead of returning HTML string directly. The token objects are converted to HTML string automatically by internal module. The reason we use tokens instead of HTML string is that tokens are much easier to reuse as they contain structural information which can be used by overriding functions.\n\nThere are four token types available for the token objects, which are `openTag`, `closeTag`, `text`, and `html`.\n\n### openTag\n\nThe `openTag` type token represents an opening tag string. A `openTag` type token has `tagName`, `attributes`, `classNames` properties to specify the data for generating HTML string. For example, following token object,\n\n```js\n{\n  type: 'openTag',\n  tagName: 'a',\n  classNames: ['my-class1', 'my-class2']\n  attributes: {\n    target: '_blank',\n    href: 'http://ui.toast.com'\n  }\n}\n```\n\nis converted to the HTML string below.\n\n```html\n<a class=\"my-class1 my-class2\" href=\"http://ui.toast.com\" target=\"_blank\"></a>\n```\n\nTo specify self-closing tags like `<br />`, and `<hr />` , you can use `selfClose` options like below.\n\n```js\n{\n  type: 'openTag',\n  tagName: 'br',\n  classNames: ['my-class'],\n  selfClose: true\n}\n```\n\n```html\n<br class=\"my-class\" />\n```\n\n### closeTag\n\nThe `closeTag` type token represents a closing tag string. A `closeTag` type token does not contain additional information other than `tagName`.\n\n```js\n{\n  type: 'closeTag',\n  tagName: 'a'\n}\n```\n\n```html\n</a>\n```\n\n### text\n\nThe `text` type token represents a plain text string. This token only has a `content` property and HTML characters in the value are escaped in the converted string.\n\n```js\n{\n  type: 'text',\n  content: '<br />'\n}\n```\n\n```html\n&lt;br /&gt;\n```\n\n### html\n\nThe `html` type token represents a raw HTML string. Like the `text` type token, this token also has `content` property and the value is used as is without modification.\n\n```js\n{\n  type: 'html',\n  content: '<br />'\n}\n```\n\n```html\n<br />\n```\n\n## Node\n\nThe first parameter of a convertor function is a `Node` type object which is the main element of the AST(Abstract Syntax Tree) constructed by the ToastMark. Every node has common properties for constructing a tree, such as `parent`, `firstChild`, `lastChild`, `prev`, and `next`.\n\nIn addition, each node has its own properties based on its type. For example, a `heading` type node has `level` property to represent the level of heading, and a `link` type node has a `destination` property to represent the URL of the link.\n\nThe following markdown text and AST tree object will help you understand the structure of AST generated by the ToastMark.\n\n```md\n## TOAST UI\n\n**Hello** World!\n```\n\n```js\n{\n  type: 'document',\n  firstChild: {\n    type: 'heading',\n    level: 2,\n    parent: //[document node],\n    firstChild:\n      type: 'text',\n      parent: //[heading node],\n      literal: 'TOAST UI'\n    },\n    next: {\n      type: 'paragraph',\n      parent: //[document node],\n      firstChild: {\n        type: 'strong',\n        parent: //[paragraph node],\n        firstChild: {\n          type: 'text',\n          parent: //[strong node],\n          literal: 'Hello'\n        },\n        next: {\n          type: 'text',\n          parent: //[paragraph node],\n          literal: 'World !'\n        }\n      }\n    }\n  }\n}\n```\n\nThe type definition of each node can be found in the [source code](https://github.com/nhn/tui.editor/blob/master/libs/toastmark/src/commonmark/node.ts).\n\n## Context\n\nWhen the Editor tries to generate HTML string using an AST, every node in the AST is traversed in pre-order fashion. Whenever a node is visited, a convertor function of which the key is the same as the type of the node is invoked. At this point, a context object is given to the convertor function as a second parameter.\n\n### entering\n\nEvery node in an AST except leaf nodes is visited twice during a traversal. The fisrt time when the node is visited, and the second time after all the children of the node are visited. We can determine in which pace the convertor is invoked using `entering` property of the context object.\n\nThe following code is a typical example using `entering` property.\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  customHTMLRenderer: {\n    heading({ level }, { entering }) {\n      return {\n        type: entering ? 'openTag' : 'closeTag',\n        tagName: `h${level}`\n      };\n    },\n    text({ literal }) {\n      return {\n        type: 'text',\n        content: node.literal\n      };\n    }\n  }\n});\n```\n\nThe `heading` convertor function is using `context.entering` to determin the type of returning token object. The type is `openTag` when the value is `true`, otherwise is `closeTag`. The `text` convertor function doens't need to use `entering` property as it is invoked only once for the first visit.\n\nNow, if we set the following markdown text to the editor,\n\n```markdown\n# TOAST UI\n```\n\nThe AST genereted by ToastMark will be like below. (only essential properties are specified)\n\n```js\n{\n  type: 'document',\n  firstChild: {\n    type: 'heading',\n    level: 1,\n    firstChild: {\n      type: 'text',\n      literal: 'TOAST UI'\n    }\n  }\n}\n```\n\nAfter finishing a traversal, tokens returned by convertor functions are stored in an array like below.\n\n```js\n[\n  { type: 'openTag', tagName: 'h1' },\n  { type: 'text', content: 'TOAST UI' },\n  { type: 'closeTag', tagName: 'h1' }\n];\n```\n\nFinally, the array of token is converted to HTML string.\n\n```html\n<h1>TOAST UI</h1>\n```\n\n### origin()\n\nIf we want to use original convertor function inside the overriding function, we can use `origin()` function.\n\nFor example, if the return value of original convertor function for `link` node is like below,\n\n#### entering: true\n\n```js\n{\n  type: 'openTag',\n  tagName: 'a',\n  attributes: {\n    href: 'http://ui.toast.com',\n    title: 'TOAST UI'\n  }\n}\n```\n\n#### entering: false\n\n```js\n{\n  type: 'closeTag',\n  tagName: 'a'\n}\n```\n\nThe following code will set `target=\"_blank\"` attribute to the result object only when `entering` state is `true`.\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  customHTMLRenderer: {\n    link(node, context) {\n      const { origin, entering } = context;\n      const result = origin();\n      if (entering) {\n        result.attributes.target = '_blank';\n      }\n      return result;\n    }\n  },\n}\n```\n\n#### entering: true\n\n```js\n{\n  type: 'openTag',\n  tagName: 'a',\n  attributes: {\n    href: 'http://ui.toast.com',\n    target: '_blank',\n    title: 'TOAST UI'\n  }\n}\n```\n\n## Advanced Usage\n\n### getChildrenText()\n\nIn a normal situation, a node doesn't need to care about it's children as their content will be handled by their own convertor functions. However, sometimes a node needs to get the children content to set the value of it's attribute. For this use case, a `context` object provides the `getChildrenText()` function.\n\nFor example, if a heading element wants to set it's `id` based on its children content, we can use the `getChildrenText()` function like the code below.\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  customHTMLRenderer: {\n    heading({ level }, { entering, getChildrenText }) {\n      const tagName = `h${level}`;\n\n      if (entering) {\n        return {\n          type: 'openTag',\n          tagName,\n          attributes: {\n            id: getChildrenText(node)\n              .trim()\n              .replace(/\\s+/g, '-')\n          }\n        };\n      }\n      return { type: 'closeTag', tagName };\n    }\n  }\n});\n```\n\nNow, if we set the markdown text below,\n\n```markdown\n# Hello _World_\n```\n\nThe return value of `getChildrenText()` inside the `heading` convertor function will be `Hello World`. As we are replacing white spaces into `-`, the final HTML string through the custom renderer will be like below.\n\n```html\n<h1 id=\"Hello-World\">Hello <em>World</em></h1>\n```\n\n### skipChildren()\n\nThe `skipChildren()` function skips traversal of child nodes. This function is useful when we want to use the content of children only for the attribute of current node, instead of generating child elements.\n\nFor example, `image` node has children which represents the description of the image. However, if we want to use an `img` element for representing a `image` node, we can't use child elements as an `img` element cannot have children. In this case, we need to invoke `skipChildren()` to prevent child nodes from being converted to additional HTML string. Instead, we can use `getChildrenText()` to get the text content of children, and set it to the `alt` attribute.\n\nThe following code example is an simplified version of built-in convertor function for an `image` type node.\n\n```js\nfunction image(node, context) {\n  const { destination } = node;\n  const { getChildrenText, skipChildren } = context;\n\n  skipChildren();\n\n  return {\n    type: 'openTag',\n    tagName: 'img',\n    selfClose: true,\n    attributes: {\n      src: destination,\n      alt: getChildrenText(node)\n    }\n  };\n}\n```\n\n### Using Multiple Tags for a Node\n\nA convertor function can also returns an array of token object. This is useful when we want to convert a node to nested elements. The following code example shows how to convert a `codeBlock` node to `<pre><code>...</code></pre>` tag string.\n\n```js\nfunction codeBlock(node) {\n  return [\n    { type: 'openTag', tagName: 'pre', classNames: ['code-block'] },\n    { type: 'openTag', tagName: 'code' },\n    { type: 'text', content: node.literal },\n    { type: 'closeTag', tagName: 'code' },\n    { type: 'closeTag', tagName: 'pre' }\n  ];\n}\n```\n\n### Controlling Newlines\n\nIn a normal situation, we don't need to care about formatting of converted HTML string. However, as the ToastMark support [CommonMark Spec](https://spec.commonmark.org/0.29/), the renderer supports an option to control new-lines to pass the [official test cases](https://spec.commonmark.org/0.29/spec.json).\n\nThe `outerNewline` and `innerNewline` property can be added to token objects to control white spaces. The following example will help you understand how to use these properties.\n\n#### Token Array\n\n```js\n[\n  {\n    type: 'text',\n    content: 'Hello'\n  },\n  {\n    type: 'openTag',\n    tagName: 'p',\n    outerNewLine: true,\n    innerNewLine: true\n  },\n  {\n    type: 'html',\n    content: '<strong>My</strong>'\n    outerNewLine: true,\n  },\n  {\n    type: 'closeTag',\n    tagName: 'p',\n    innerNewLine: true\n  },\n  {\n    type: 'text',\n    content: 'World'\n  }\n]\n```\n\n#### Converted HTML string\n\n```html\nHello\n<p>\n  <strong>My</strong>\n</p>\nWorld\n```\n\nAs you can see in the example above, `outerNewLine` of `openTag` adds `\\n` before the tag string, whereas one of `closeTag` adds `\\n` after the tag string. In contrast, `innerNewLine` of `openTag` adds `\\n` after the tag string, whereas one of `closeTag` adds `\\n` before the tag string. In addition, consecutive newlines are merged into one newline to prevent duplication.\n"
  },
  {
    "path": "docs/en/extended-autolinks.md",
    "content": "# 🔗 Extending Autolinks\n\n## What Is Autolinks?\n\n### Autolinks\n\nThe [Autolinks](https://spec.commonmark.org/0.29/#autolinks) is the CommonMark specification like as below. (If you want to know the detail specification of the Autolinks, refer to examples in the above link.)\n> Autolinks are absolute URIs and email addresses inside `<` and `>`. They are parsed as `>` links, with the URL or email address as the link label.\n\nThe functionality is available in TOAST UI Editor (henceforth referred to as 'Editor') without any configuration, because the Editor follows the CommonMark specification. \n\n![image](https://user-images.githubusercontent.com/37766175/120604939-7ad04d00-c488-11eb-82c1-f9f05891039e.png)\n\n### Extended Autolinks\n\nThe Extended Autolinks is the specification which is supported by [GFM](https://github.github.com/gfm). The specification makes the Autolinks be recognized in a greater number of conditions. For example, if the text has `www.` with a valid domain, it will be recognized as the Autolinks like as below.\n\n![image](https://user-images.githubusercontent.com/37766175/120605112-a5baa100-c488-11eb-9b72-75eaa9324080.png)\n\nMore examples related with the Extended Autolinks can be found [here](https://github.github.com/gfm/#autolinks-extension-).\n\n\n## Extended Autolinks Configuration\nThe Extended Autolinks on Editor can be used by configuring the `extendedAutolinks` option. If the `extendedAutolinks` option is not otherwise defined, Editor will automatically configure the `false` value to make the Extended Autolinks be not worked internally.\n\nWhen we set the `extendedAutolinks` value to explicitly declare it `true` value, the nodes which follow the Extended Autolinks specification can be parsed as the link node on Editor.\n\n```js\nconst editor = new toastui.Editor({\n  // ...\n  extendedAutolinks: true\n});\n```\n\n## Customizing the Extended Autolinks\nEditor enables users to define their own Extended Autolinks by providing the callback function option. This option can be useful when you want to support the specific link format.\n\nTo customize the Extended Autolinks, `extendedAutolinks` option should be `function`. The following is a simple example snippet for configuring the option.\n\n```js\nconst reToastuiEditorRepo = /tui\\.editor/g;\n\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  extendedAutolinks: (content) => {\n    const matched = content.match(reToastuiEditorRepo);\n    if (matched) {\n      return matched.map(m =>\n        ({\n          text: 'toastui-editor',\n          url: 'https://github.com/nhn/tui.editor',\n          range: [0, 1]\n        })\n      );\n    }\n    return null;\n  }\n});\n```\nAs the code above demonstrates, the `content` parameter which has the editing content is passed to the `extendedAutolinks` callback function. If the desired link formats are found in the content, the result should be the array, and each element has `text`, `url` and `range` properties for the information of link.\n\n* `text`: The link label\n* `url`: The link destination\n* `range`: The link range for calculating source position internally\n\nHere is the result of the aforementioned example.\n\n![image](https://user-images.githubusercontent.com/37766175/120606618-55444300-c48a-11eb-8376-859fc6ffcf07.gif)"
  },
  {
    "path": "docs/en/getting-started.md",
    "content": "# 🚀 Getting Started\n\n## The Project Setup\n\nTOAST UI Editor can be used by using the package manager or downloading the source directly. However, we highly recommend using the package manager.\n\n### Via Package Manager (npm)\n\nYou can conveniently install it using the commands provided by each package manager. When using npm, be sure to use it in the environment [Node.js](https://nodejs.org/en/) is installed.\n\n```sh\n$ npm install --save @toast-ui/editor # Latest Version\n$ npm install --save @toast-ui/editor@<version> # Specific Version\n```\n\nWhen installed and used with npm, the list of files that can be imported is as follows:\n\n```\n- node_modules/\n   ├─ @toast-ui/editor/\n   │     ├─ dist/\n   │     │    ├─ toastui-editor.js\n   │     │    ├─ toastui-editor-viewer.js\n   │     │    ├─ toastui-editor.css\n   │     │    ├─ toastui-editor-viewer.css\n   │     │    └─ toastui-editor-only.css\n```\n\n### Via Contents Delivery Network (CDN)\n\nTOAST UI Editor is available over the CDN powered by [NHN Cloud](https://www.toast.com). You can use the CDN as below.\n\n```html\n...\n<body>\n  ...\n  <script src=\"https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js\"></script>\n</body>\n...\n```\n\nIf you want to use a specific version, use the tag name instead of `latest` in the url's path.\n\nThe CDN directory has the following structure:\n\n```\n- uicdn.toast.com/\n   ├─ editor/\n   │     ├─ latest/\n   │     │    ├─ toastui-editor-all.js\n   │     │    ├─ toastui-editor-all.min.js\n   │     │    ├─ toastui-editor-viewer.js\n   │     │    ├─ toastui-editor-viewer.min.js\n   │     │    ├─ toastui-editor-editor.js\n   │     │    ├─ toastui-editor-editor.min.js\n   │     │    ├─ toastui-editor-editor.css\n   │     │    ├─ toastui-editor-editor.min.css\n   │     │    ├─ toastui-editor-viewer.css\n   │     │    └─ toastui-editor-viewer.min.css\n   │     ├─ 2.0.0/\n   │     │    └─ ...\n```\n\n## Create Your First Editor\n\n### Adding the Wrapper Element\n\nYou need to add the container element where TOAST UI Editor (henceforth referred to as 'Editor') will be created.\n\n```html\n...\n<body>\n  <div id=\"editor\"></div>\n</body>\n...\n```\n\n### Importing the Editor's Constructor Function\n\nThe editor can be used by creating an instance with the constructor function. To get the constructor function, you should import the module using one of the following ways depending on your environment.\n\n#### Using Module Format in Node Environment\n\n- ES6 Modules\n\n```javascript\nimport Editor from '@toast-ui/editor';\n```\n\n- CommonJS\n\n```javascript\nconst Editor = require('@toast-ui/editor');\n```\n\n#### Using Namespace in Browser Environment\n\n```javascript\nconst Editor = toastui.Editor;\n```\n\n### Adding CSS Files\n\nYou need to add the CSS files needed for the Editor. Import CSS files in node environment, and add it to html file when using CDN.\n\n#### Using in Node Environment\n\n- ES6 Modules\n\n```javascript\nimport '@toast-ui/editor/dist/toastui-editor.css'; // Editor's Style\n```\n\n- CommonJS\n\n```javascript\nrequire('@toast-ui/editor/dist/toastui-editor.css');\n```\n\n#### Using in Browser Environment by CDN\n\n```html\n...\n<head>\n  ...\n  <!-- Editor's Style -->\n  <link rel=\"stylesheet\" href=\"https://uicdn.toast.com/editor/latest/toastui-editor.min.css\" />\n</head>\n...\n```\n\n### Creating Instance\n\nYou can create an instance with options and call various API after creating an instance.\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor')\n});\n```\n\n![getting-started-01](https://user-images.githubusercontent.com/37766175/121855586-7d576000-cd2e-11eb-9196-0c20270d1221.png)\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  height: '600px',\n  initialEditType: 'markdown',\n  previewStyle: 'vertical'\n});\n```\n\n![getting-started-02](https://user-images.githubusercontent.com/37766175/121464762-71e2fc80-c9ef-11eb-9a0a-7b06e08d3ccb.png)\n\nThe basic options available are:\n\n- `height`: Height in string or auto ex) `300px` | `auto`\n- `initialEditType`: Initial type to show `markdown` | `wysiwyg`\n- `initialValue`: Initial value. Set Markdown string\n- `previewStyle`: Preview style of Markdown mode `tab` | `vertical`\n- `usageStatistics`: Let us know the _hostname_. We want to learn from you how you are using the editor. You are free to disable it. `true` | `false`\n\nFind out more options [here](https://nhn.github.io/tui.editor/latest/ToastUIEditor).\n\n## Example\n\nYou can see the example [here](https://nhn.github.io/tui.editor/latest/tutorial-example01-editor-basic).\n"
  },
  {
    "path": "docs/en/i18n.md",
    "content": "# 🌏 Internationalization (i18n)\n\nTOAST UI Editor provides the ability to set the text set in UI in various languages. There are language files provided by default, you can import each language file and set the code ​​for the language you want to use when you create an instance.\n\n## Files Structure\n\n### Source File (For Contributors)\n\nIf you want to contribute language files in addition to the [languages ​​supported by default](#supported-languages), add them to the path below. See the [Contributing](#contributing) section for a more detailed contributing process.\n\n```\n- tui.editor/apps/editor/src/\n  - i18n/\n    - en-us.ts\n    - ko-kr.ts\n    - ...\n```\n\n### Build (For Maintainers)\n\n```\n- tui.editor/apps/editor/dist/\n  - i18n/\n    - ko-kr.js\n    - ...\n```\n\n### Files Distributed on npm\n\n```\n- node_modules/@toast-ui/editor/dist/\n  - i18n/\n    - ko-kr.js\n    - ...\n```\n\n### Files Distributed on CDN\n\n```\n- uicdn.toast.com/editor/latest/\n  - i18n/\n    - ko-kr.js\n    - ko-kr.min.js\n    - ...\n```\n\n## Supported Languages\n\nBelow is a table of valid language codes for i18n files provided by the TOAST UI Editor. This language code is based on the [IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag). When you import a language file, the language code is registered and you can set the code as the option.\n\n> Note : The default language is English. The production language file(`en-us.js`) for English is not provided and you don't need to import this file.\n\n| Language Name              | i18n File | Registered Code |\n| -------------------------- | --------- | --------------- |\n| Arabic                     | ar.js     | `ar`            |\n| Chinese (S)                | zh-cn.js  | `zh-CN`         |\n| Chinese (T)                | zh-tw.js  | `zh-TW`         |\n| Croatian (Croatia)         | hr-hr.js  | `hr` \\| `hr-HR` |\n| Czech (Czech Republic)     | cs-cz.js  | `cs` \\| `cs-CZ` |\n| Dutch (Netherlands)        | nl-nl.js  | `nl` \\| `nl-NL` |\n| English (United States)    | en-us.js  | `en` \\| `en-US` |\n| Finnish (Finland)          | fi-fi.js  | `fi` \\| `fi-FI` |\n| French (France)            | fr-fr.js  | `fr` \\| `fr-FR` |\n| Galician (Spain)           | gl-es.js  | `gl` \\| `gl-ES` |\n| German (Germany)           | de-de.js  | `de` \\| `de-DE` |\n| Italian (Italy)            | it-it.js  | `it` \\| `it-IT` |\n| Japanese (Japan)           | ja-jp.js  | `ja` \\| `ja-JP` |\n| Korean (Korea)             | ko-kr.js  | `ko` \\| `ko-KR` |\n| Norwegian Bokmål (Norway)  | nb-no.js  | `nb` \\| `nb-NO` |\n| Polish (Poland)            | pl-pl.js  | `pl` \\| `pl-PL` |\n| Portuguese (Brazil)        | pt-br.js  | `pt` \\| `pt-BR` |\n| Russian (Russia)           | ru-ru.js  | `ru` \\| `ru-RU` |\n| Spanish (Castilian, Spain) | es-es.js  | `es` \\| `es-ES` |\n| Swedish (Sweden)           | sv-se.js  | `sv` \\| `sv-SE` |\n| Turkish (Turkey)           | tr-tr.js  | `tr` \\| `tr-TR` |\n| Ukrainian (Ukraine)        | uk-ua.js  | `uk` \\| `uk-UA` |\n\n## Importing Language Files\n\nYou must register the language by importing the language file you want to use. The `${fileName}` is corresponding to the 'i18n File' column in [Supported Languages](#supported-languages) (Can be used without an extension).\n\n### ES Modules\n\n```js\nimport '@toast-ui/editor/dist/i18n/${fileName}';\n```\n\n### CommonJS\n\n```js\nrequire('@toast-ui/editor/dist/i18n/${fileName}');\n```\n\n### Usage CDN\n\nYou must register the language by including the language file you want to use. The `${fileName}` is corresponding to the 'i18n File' column in [Supported Languages](#supported-languages). It also provides a\nminified version.\n\n```html\n<script src=\"https://uicdn.toast.com/editor/latest/i18n/${fileName}\"></script>\n```\n\n## How to Use\n\n> Note : The following examples are based on npm usage.\n\n### Use Case 1 : Basic Usage\n\nIf you want to set a specific language, you can use the `language` option to create an instance. The value of this option corresponds to the 'Registered Code' column in [Supported Languages](#supported-languages). The default value is `en` and `en-US`.\n\n```js\nimport Editor from '@toast-ui/editor';\n\n// Step 1 : Import language file\nimport '@toast-ui/editor/dist/i18n/ko-kr';\n\n// Step 2 : Set language each editor\nconst foo = new Editor({\n  // Use default language in English\n  // ...\n});\n\nconst bar = new Editor({\n  // Use other language\n  // ...\n  language: 'ko-KR',\n});\n```\n\n### Use Case 2 : Some Value Overrides\n\nUse the `setLanguage` static method to override the value for a specific language code. See [here](https://github.com/nhn/tui.editor/tree/master/apps/editor/src/i18n/en-us.ts) for default values.\n\n```js\nimport Editor from '@toast-ui/editor';\n\n// Step 1 : Import language file\nimport '@toast-ui/editor/dist/i18n/ko-kr';\n\n// Step 2 : Override values of language\nEditor.setLanguage('en-US', {\n  'Add row': '[Add Row]', // Default value is 'Add row'\n});\n\nEditor.setLanguage('ko-KR', {\n  'Add row': '[로우 추가]', // Default value is '행 추가'\n});\n\n// Step 3 : Set language each editor\nconst foo = new Editor({\n  // Use default language in English\n  // ...\n});\n\nconst bar = new Editor({\n  // Use other language\n  // ...\n  language: 'ko-KR',\n});\n```\n\n### Use Case 3 : New Language Registration\n\nIf the language you want to use is not provided by default, you can register it with the `setLanguage` static method.\n\n```js\nimport Editor from '@toast-ui/editor';\n\n// Step 1 : Register new language\nEditor.setLanguage('en-GB', {\n  Markdown: '...',\n  WYSIWYG: '...',\n  // ...\n});\n\n// Step 2 : Set language with new registration code\nconst bar = new Editor({\n  // ...\n  language: 'en-GB',\n});\n```\n\n## Contributing\n\nIf you want to contribute to providing a different language file, follow this process:\n\n### Step 1\n\nFork the repository and add the language file to the path below. The name of the language file follows the `${languageCode}-${countryCode}.js` convention. `languageCode` and `countryCode` must be written in lowercase. (e.g. `en-gb.ts`)\n\n> Reference : [Nominatim/Country Codes](https://wiki.openstreetmap.org/wiki/Nominatim/Country_Codes)\n\n```\n- tui.editor/apps/editor/src/\n  - i18n/\n    - en-us.ts\n    - ko-kr.ts\n    - ...\n```\n\n### Step 2\n\nRefer to [this file](https://github.com/nhn/tui.editor/tree/master/src/i18n/en-us.ts) and write each parameter value used when calling the `setLanguage` method.\n\nThe first parameter is a code value that maps to the language file to register. Code values ​​follow the [`${languageCode}-${countryCode}` convention](https://en.wikipedia.org/wiki/IETF_language_tag). `languageCode` must be in lowercase and `countryCode` in uppercase.\n\n```js\n// th-th.js\n// ...\n\nEditor.setLanguage('th-TH', {\n  Markdown: '...',\n  WYSIWYG: '...',\n  // ...\n});\n```\n\nIf the following conditions are satisfied, the language code except for the country code can be added.\n\n> IETF Language Tag's Reference : Optional script and region subtags are preferred to be omitted when they add no distinguishing information to a language tag. For example, es is preferred over es-Latn, as Spanish is fully expected to be written in the Latin script; ja is preferred over ja-JP, as Japanese as used in Japan does not differ markedly from Japanese as used elsewhere.\n>\n> Not all linguistic regions can be represented with a valid region subtag: the subnational regional dialects of a primary language are registered as variant subtags. For example, the valencia variant subtag for the Valencian dialect of Catalan is registered in the Language Subtag Registry with the prefix ca. As this dialect is spoken almost exclusively in Spain, the region subtag ES can normally be omitted.\n\n```js\n// th-th.js\n// ...\n\nEditor.setLanguage(['th', 'th-TH'], {\n  Markdown: '...',\n  WYSIWYG: '...',\n  // ...\n});\n```\n\n## Example\n\nYou can see the example [here](https://nhn.github.io/tui.editor/latest/tutorial-example16-i18n).\n"
  },
  {
    "path": "docs/en/plugin.md",
    "content": "# 🧩 Plugins\n\n## What Is Plugin?\n\nTOAST UI Editor (henceforth referred to as 'Editor') provides a plugin. Plugin is an extension that can be added as needed. There are a total of 5 plugins provided by the Editor.\n\n| Plugin Name | Package Name | Description |\n| --- | --- | --- |\n| [`chart`](https://github.com/nhn/tui.editor/tree/master/plugins/chart) | [`@toast-ui/editor-plugin-chart`](https://www.npmjs.com/package/@toast-ui/editor-plugin-chart) | Plugin to render chart |\n| [`code-syntax-highlight`](https://github.com/nhn/tui.editor/tree/master/plugins/code-syntax-highlight) | [`@toast-ui/editor-plugin-code-syntax-highlight`](https://www.npmjs.com/package/@toast-ui/editor-plugin-code-syntax-highlight) | Plugin to highlight code syntax |\n| [`color-syntax`](https://github.com/nhn/tui.editor/tree/master/plugins/color-syntax) | [`@toast-ui/editor-plugin-color-syntax`](https://www.npmjs.com/package/@toast-ui/editor-plugin-color-syntax) | Plugin to color editing text |\n| [`table-merged-cell`](https://github.com/nhn/tui.editor/tree/master/plugins/table-merged-cell) | [`@toast-ui/editor-plugin-table-merged-cell`](https://www.npmjs.com/package/@toast-ui/editor-plugin-table-merged-cell) | Plugin to merge table cells |\n| [`uml`](https://github.com/nhn/tui.editor/tree/master/plugins/uml) | [`@toast-ui/editor-plugin-uml`](https://www.npmjs.com/package/@toast-ui/editor-plugin-uml) | Plugin to render UML |\n\n## How to Use Plugin\n\nEach plugin can be installed and used with npm, or it can be used as provided CDN files.\n\n### Via Package Manager (npm)\n\nYou can install each plugin using the command, and add the name of the plugin you want to install to `${pluginName}` below. For example, if you install the `chart` plugin, install it as`npm install @toast-ui/editor-plugin-chart`.\n\n```sh\n$ npm install --save @toast-ui/editor-plugin-${pluginName} # Latest Version\n$ npm install --save @toast-ui/editor-plugin-${pluginName}@<version> # Specific Version\n```\n\nWhen installed and used with npm, the list of files that can be imported is as follows:\n\n```\n- node_modules/\n   ├─ @toast-ui/editor-plugin-${pluginName}\n   │     ├─ dist/\n   │     │    ├─ toastui-editor-plugin-${pluginName}.js\n   │     │    ├─ ...\n```\n\nInstalled plugins can be imported as shown below depending on the environment.\n\n- ES Module\n\n```js\nimport pluginFn from '@toast-ui/editor-plugin-${pluginName}';\n```\n\n- CommonJS\n\n```js\nconst pluginFn = require('@toast-ui/editor-plugin-${pluginName}');\n```\n\nFor example, `chart` plugin can be imported as follows:\n\n```js\nimport chart from '@toast-ui/editor-plugin-chart';\n```\n### Via Contents Delivery Network (CDN)\n\nEach plugin is available over the CDN powered by [NHN Cloud](https://www.toast.com). \n\n```html\n...\n<body>\n  ...\n  <script src=\"https://uicdn.toast.com/editor-plugin-${pluginName}/latest/toastui-editor-plugin-${pluginName}.min.js\"></script>\n</body>\n...\n```\n\nIf you want to use a specific version, use the tag name instead of `latest` in the url's path.\n\nThe CDN directory has the following structure:\n\n```\n- uicdn.toast.com/\n   ├─ editor-plugin-${pluginName}/\n   │     ├─ latest/\n   │     │    ├─ toastui-editor-plugin-${pluginName}.js\n   │     │    └─ ...\n   │     ├─ 3.0.0/\n   │     │    └─ ...\n```\n\n> Note: Each plugin's CDN file contains all dependencies depending on the situation, or provides different types of bundled files. For more information, please check the each plugin repository.\n\nWhen importing the plugin into the namespace, use the plugin's namespace registered under `toastui.Editor.plugin`.\n\n```js\nconst pluginFn = toastui.Editor.plugin[${pluginName}];\n```\n\nFor example, the `chart` plugin imports as follows:\n\n```js\nconst { chart } = toastui.Editor.plugin;\n```\n\n### Using the Plugin in Editor\n\nTo use a plugin imported from the Editor, use an editor's `plugins` option. You can add each plugin function you imported to this option. The type of `plugins` option is `Array.<function>`.\n\n```js\nconst editor = new Editor({\n  // ...\n  plugins: [plugin]\n});\n```\n\nFor example, if you add the `chart` and `uml` plugin, you can do something like this:\n\n- ES Module\n\n```js\nimport Editor from '@toast-ui/editor';\nimport chart from '@toast-ui/editor-plugin-chart';\nimport uml from '@toast-ui/editor-plugin-uml';\n\nconst editor = new Editor({\n  // ...\n  plugins: [chart, uml]\n});\n```\n\n- CDN\n\n```js\nconst { Editor } = toastui;\nconst { chart, uml } = Editor.plugin;\n\nconst editor = new Editor({\n  // ...\n  plugins: [chart, uml]\n});\n```\n\nIf you need an option to use in a plugin function, you can add an array value of the `plugins` option as a tuple.\n\n```js\nconst pluginOptions = {\n  // ...\n};\n\nconst editor = new Editor({\n  // ...\n  plugins: [[plugin, pluginOptions]]\n});\n```\n\n## Creating the User Plugin\n\nIn addition to the plugins provided by default, users can create and use plugin functions themselves.\nThe method is very easy.\n\nIt defines the plugin function as shown below to return objects having specified properties.\n\n\n```ts\ninterface PluginInfo {\n  toHTMLRenderers?: HTMLConvertorMap;\n  toMarkdownRenderers?: ToMdConvertorMap;\n  markdownPlugins?: PluginProp[];\n  wysiwygPlugins?: PluginProp[];\n  wysiwygNodeViews?: NodeViewPropMap;\n  markdownCommands?: PluginCommandMap;\n  wysiwygCommands?: PluginCommandMap;\n  toolbarItems?: PluginToolbarItem[];\n}\n\nconst pluginResult: PluginInfo = {\n  // ...\n}\n\nfunction customPlugin() {\n  // ...\n  return pluginResult;\n}\n```\n\nLike other plugins, it can be used by adding plugin functions defined through the `plugins` option.\n\n```js\nconst editor = new Editor({\n  // ...\n  plugins: [customPlugin]\n});\n```\n\n### Plugin Return Object\n\nLet's find out the properties of the objects returned by the plugin. There are a total of 8 properties, as shown below, and user can define only the desired properties for customization.\n\n\n```ts\ninterface PluginInfo {\n  toHTMLRenderers?: HTMLConvertorMap;\n  toMarkdownRenderers?: ToMdConvertorMap;\n  markdownCommands?: PluginCommandMap;\n  wysiwygCommands?: PluginCommandMap;\n  toolbarItems?: PluginToolbarItem[];\n  markdownPlugins?: PluginProp[];\n  wysiwygPlugins?: PluginProp[];\n  wysiwygNodeViews?: NodeViewPropMap;\n}\n```\n\n#### toHTMLRenderers\n\n`toHTMLRenderers` object can change the rendering results of elements when rendered in the Markdown Preview or when converted from Markdown Editor to WYSIWYG Editor. It is same as the [customHTMLRenderer](https://github.com/nhn/tui.editor/blob/master/docs/en/custom-html-renderer.md) option in the editor.\n\n**toMarkdownRenderers**\n\n`toMarkdownRenderers` object can override markdown text that is converted from WYSIWYG editor to Markdown editor. The function defined in `toMarkdownRenderers` object has 2 parameters: `nodeInfo` and `context`.\n\n* `nodeInfo`: WYSIWYG node information when converting from WYSIWYG editor to Markdown editor.\n  * `node`: The information about the target node.\n  * `parent`: The parent node information of the target node.\n  * `index`: The index as child.\n* `context`: The information needed for converting except `nodeInfo`.\n  * `entering`: This can be seen whether it is a first visit to that node or if it is a visit after traversing all of the child nodes.\n  * `origin`: The function that executes the operation of an original converting function.\n\nThe function defined in `toMarkdownRenderers` returns the token information needed to convert the result to markdown text.\n\n```ts\ninterface ToMdConvertorReturnValues {\n  delim?: string | string[];\n  rawHTML?: string | string[] | null;\n  text?: string;\n  attrs?: Attrs;\n}\n```\n\n* `delim`: Defines symbols to be used in markdown text. It is used when it can be converted to multiple symbols, such as `*` and `-` on a list of markdown bullet list.\n* `rawHTML`: This text is required when converting a node to an HTML node (HTML text) in Markdown.\n* `text`: Text to be shown in Markdown.\n* `attrs`: Properties to use when converting a node to markdown text. For example, whether the task list is checked or not, or the url of the image node.\n\n**Example**\n```ts\nreturn {\n  toHTMLRenderers: {\n    // ...\n    tableCell(node: MergedTableCellMdNode, { entering, origin }) {\n      const result = origin!();\n\n      // ...\n      \n      return result;\n    },\n  },\n  toMarkdownRenderers: {\n    // ...\n    tableHead(nodeInfo) {\n      const row = (nodeInfo.node as ProsemirrorNode).firstChild;\n\n      let delim = '';\n\n      if (row) {\n        row.forEach(({ textContent, attrs }) => {\n          const headDelim = createTableHeadDelim(textContent, attrs.align);\n\n          delim += `| ${headDelim} `;\n\n          // ...\n        });\n      }\n      return { delim };\n    },\n  },\n};\n```\n\nThe code above is an example of a merge table cell plugin. The return result of `tableCell` node defined in `toHTMLRenderers` is used for converting to Markdown Preview and WYSIWYG Editor, while result of `tableHead` nodes defined in `toMarkdownRenderers` are used for converting to Markdown Editor.\n\n![image](https://user-images.githubusercontent.com/37766175/121026660-4c80a380-c7e1-11eb-9d36-65425b6944da.gif)\n\n#### markdownCommands, wysiwygCommands\n\nPlugin allows adding of markdown and WYSIWYG commands using `markdownCommands` and `wysiwygCommands` options.\n\nEach command function has 3 parameters: `payload`, `state`, and `dispatch`, which can be used to control the internal operation of the editor based on [Prosemirror](https://prosemirror.net/).\n\n* `payload`: This is a `payload` that is needed to execute commands.\n* `state`: An instance indicating the editor's internal state, which is the same as [prosemirror-state](https://prosemirror.net/docs/ref/#state).\n* `dispatch`: If you want to change the contents of an editor through command execution, you have to run the `dispatch` function. It is same as [dispatch](https://prosemirror.net/docs/ref/#view.EditorView.dispatch) function of Prosemirror.\n\nIf a change occurs in the editor's content by executing a command function, it must return `true`. In the opposite case, `false` must be returned.\n\n```js\nreturn {\n  markdownCommands: {\n    myCommand: (payload, state, dispatch) => {\n      // ...\n      return true;\n    },\n  },\n  wysiwygCommands: {\n    myCommand: (payload, state, dispatch) => {\n      // ...\n      return true;\n    },\n  },\n};\n```\n\nIf you define and return a command function in a plugin, as shown in the example code above, that command can be used in the editor.\n\n#### toolbarItems\n\nYou can also register an editor's toolbar item from the plugin.\n\n```js\nreturn {\n  // ...\n  toolbarItems: [\n    {\n      groupIndex: 0,\n      itemIndex: 3,\n      item: toolbarItem,\n    },\n  ],\n};\n```\n\nLike the code above, you can set which items to add to the `toolbarItems` array. Each option object has 3 properties: `groupIndex`, `itemIndex`, and item`.\n\n* `groupIndex`: Toolbar group index to add toolbar item.\n* `itemIndex`: Toolbar item index in group.\n* `item`: Toolbar Item. It is the same form as the object used in [toolbar customization](https://github.com/nhn/tui.editor/blob/master/docs/en/toolbar.md).\n\nIf the `toolbarItems` option is set, as in the example code, the toolbar item will be added as the fourth index of the first toolbar group.\n\n#### markdownPlugins, wysiwygPlugins\n\nThe editor use Prosemirror internally. Prosemirror provides its own plugin system. These Prosemirror plugins can also be defined in order to control out editor's internal state. In most cases, these options are not necessary, but are often necessary. For example, code syntax highlighting plugin are used to highlight code displayed in the WYSIWYGs editor `codeBlock`.\n\n```js\nreturn {\n  wysiwygPlugins: [() => codeSyntaxHighlighting(context, prism)],\n};\n```\n\nThe method of using this option object is the same as the plugin definition method in Prosemirror, so see [here] (https://prosemirror.net/docs/ref/#state.Plugin).\n\n#### wysiwygNodeViews\n\nMarkdown Editor's contents is plain text, but WYSIWYG Editor's content consists of specific nodes. These nodes can be customized to add attribute or class using the `customHTMLRenderer` option. However, there is a limit to `customHTMLRenderer` option if you want to control something by adding event handker or having more complex interactions. In this case, the plugin's `wysiwygNodeViews` option allows customization of nodes that are rendered in the WYSIWYG Editor.\nThis option will also be unnecessary in most cases. Like the `wysiwygPlugins` property, the `wysiwygNodeViews` property is also used in code syntax highlighting plugin.\n\n```js\nreturn {\n  wysiwygNodeViews: {\n    codeBlock: createCodeSyntaxHighlightView(registerdlanguages),\n  },\n};\n```\n\nThe method of using this option object is the same as the `nodeView` definition method in Prosemirror, so see [here] (https://prosemirror.net/docs/ref/#view.NodeView).\n\n### 'context' parameter of plugin function\nPlugin functions can use some information with `context` parameters to define the various properties described above. The `context` parameter contains the following properties.\n\n* `eventEmitter`: It is the same as `eventEmitter` in an editor. It is used to communicate with the editor.\n* `usageStatistics`: It decides whether to collect the plugin as GA for `@toast-ui/editor`.\n* `i18n`: It is an instance for adding i18n.\n* `pmState`: Some modules of [prosmirror-state] (https://prosemirror.net/docs/ref/#state).\n* `pmView`: Some modules of [prosemirror-view](https://prosemirror.net/docs/ref/#view).\n* `pmModel`: Some modules of [prosemirror-model](https://prosemirror.net/docs/ref/#model).\n\n## Example\n\nExamples can be found [here](https://nhn.github.io/tui.editor/latest/tutorial-example13-creating-plugin) and in the [plugin package](https://github.com/nhn/tui.editor/tree/master/plugins).\n"
  },
  {
    "path": "docs/en/toolbar.md",
    "content": "# 🛠 Toolbar\n\nTypically, the editor can use shortcuts or toolbar to enter specific text or nodes. In particular, in WYSIWYG editors, where no specific textual syntax exists, the role of the toolbar is important because most of the operation are done through the toolbar. The TOAST UI Editor (henceforth referred to as 'Editor') also provides a toolbar as the default UI, as well as options and APIs for customization.\n\n## Toolbar Option\nThe editor provides a total of 16 toolbar items, including bold, italic, and strike. Unless otherwise specified, the default toolbar option is shown below.\n\n```js\nconst options = {\n  // ...\n  toolbarItems: [\n    ['heading', 'bold', 'italic', 'strike'],\n    ['hr', 'quote'],\n    ['ul', 'ol', 'task', 'indent', 'outdent'],\n    ['table', 'image', 'link'],\n    ['code', 'codeblock'],\n    ['scrollSync'],\n  ],\n}\n```\n\nAs you can see in the example code, the toolbar option in the editor is defined in 2D array format. First, each toolbar group is defined as an array, and the toolbar items within the group are designated as items of the array. Each item is rendered in a group in the order in which it is defined, and the toolbar group is rendered separated by the `|` symbol.\n\n![image](https://user-images.githubusercontent.com/37766175/120914229-a137f780-c6d7-11eb-8112-b14a48f8374f.png)\n\nIf you want to change the configuration of the default toolbar, you can change it by setting the `toolbarItems` option.\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  toolbarItems: [\n    ['heading', 'bold'],\n    ['ul', 'ol', 'task'],\n    ['code', 'codeblock'],\n  ],\n});\n```\n\nThe above example code is executed as shown below.\n\n![image](https://user-images.githubusercontent.com/37766175/120914344-a47fb300-c6d8-11eb-85cd-857047e8e220.png)\n\n## Toolbar Button Customizing\nThe example seen above is actually just a combination of the basic toolbar items. Then what should user do if they  want to create and add a toolbar button? In this case, two main types of options can be customized.\n\n### Button Element Customizing\nFirst, there is a way to customize the toolbar button UI provided by the editor. This method is that overriding only the icon, tooltip, or popup operation of the button embedded in the editor. This option consists of the following interfaces:\n\n| Name | Type | Description |\n| --- | --- | --- |\n| `name` | string | A unique name for the toolbar item and must be specified as required. | \n| `tooltip` | string | Optional value, which defines the tooltip text to show when mouse is over on the toolbar item. | \n| `text` | string | Optional value, define if the toolbar button element has text to show. | \n| `className` | string | Optional value, defines the class to be applied to the toolbar item. | \n| `style` | Object | Optional value, defines the style to be applied to the toolbar item. | \n| `command` | string | Optional value, defines the command you want to execute when you click the toolbar button. It has an exclusive relationship with `popup` option. | \n| `popup` | PopupOptions | Optional value, defines if you want to show the popup when you click the toolbar button. This is an exclusive relationship with the `command` option.. | \n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  toolbarItems: [\n    [{\n      name: 'myItem',\n      tooltip: 'myItem',\n      command: 'bold',\n      text: '@',\n      className: 'toastui-editor-toolbar-icons',\n      style: { backgroundImage: 'none', color: 'red' }\n    }]\n  ],\n  // ...\n});\n```\n\nThe toolbar item is rendered with `className` and `style` options. The item has `@` text node and executes `bold` commands when clicked.\n\n![image](https://user-images.githubusercontent.com/37766175/120915118-ea3e7a80-c6dc-11eb-86cc-5229ed36c4e8.gif)\n\n### popup Option\nWhen you click the button, you might want to show the popup. In this case, you can use the `popup` option that you saw above. The interface of the `popup` option is shown below.\n\n| Name | Type | Description |\n| --- | --- | --- |\n| `body` | HTMLElement | Defines the popup DOM node to be rendered. | \n| `className` | string | Optional value, defines the class to be applied to the popup. | \n| `style` | Object | Optional value, defines the style to be applied to the popup. | \n\nThe popup node is automatically diplayed on the screen when clicked on the toolbar, and disappears automatically when clicked on another area.\n\nLet's take a look at the color picker plugin code of the editor.\n\n```js\nconst container = document.createElement('div');\n// ...\nconst button = createApplyButton(i18n.get('OK'));\n\nbutton.addEventListener('click', () => {\n  // ...\n  eventEmitter.emit('command', 'color', { selectedColor });\n  eventEmitter.emit('closePopup');\n});\n\ncontainer.appendChild(button);\n\nconst colorPickerToolber = {\n  name: 'color',\n  tooltip: 'Text color',\n  className: 'some class',\n  popup: {\n    className: 'some class',\n    body: container,\n    style: { width: 'auto' },\n  },\n};\n```\n\nIn the example code, the popup element is in a variable `container`. This element has a button element, and when clicked, it executes the `color` command and closes the itself. The popup that user defined can be communicated with editor using `eventEmitter`. In order to execute the command, you can trigger `command` event, and if you want to close the popup, you can trigger `closePopup` event.\n\nThe defined color picker toolbar item works well with popup as shown below.\n\n![image](https://user-images.githubusercontent.com/37766175/120915630-b6b11f80-c6df-11eb-8094-b264ca9312a1.gif)\n\n\n## Toolbar Item Customizing\nIf you want to create a toolbar item without using the default button UI as described above, you need to configure the `el` option as shown below.\n\n```js\nconst myCustomEl = document.createElement('span');\n\nmyCustomEl.textContent = '😎';\nmyCustomEl.style = 'cursor: pointer; background: red;'\nmyCustomEl.addEventListener('click', () => {\n  editor.exec('bold');\n});\n\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  toolbarItems: [\n    [{\n      name: 'myItem',\n      tooltip: 'myItem',\n      el: myCustomEl,\n    }]\n  ],\n  // ...\n});\n```\n\nThe element to be rendered must be specified as an `el` option. In this case, style, event handler, and class must be set, as the option is to create a complete DOM element.\n\nIf you run the example code above, it will work as follows.\n\n![iamge](https://user-images.githubusercontent.com/37766175/120915883-3e4b5e00-c6e1-11eb-8f44-95e6d31f41e7.gif)\n\n## Change Toolbar Item State\nIn the editor, you can activate which node is based on the current cursor's position by changing the style of the toolbar element. For example, if the cursor is located on a `strong` node that displays bold text, an element of the `bold` toolbar item is activated as follows.\n\n![image](https://user-images.githubusercontent.com/37766175/124843166-49d5c180-dfcc-11eb-9633-ae1e61d612ea.gif)\n\n\nIf you want to change the state of a customized toolbar element like the example above, you need to configure the `state` option.\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  toolbarItems: [\n    [{\n      name: 'myItem',\n      tooltip: 'myItem',\n      command: 'bold',\n      text: '@',\n      className: 'toastui-editor-toolbar-icons',\n      style: { backgroundImage: 'none', color: 'red' },\n      // If it is located on the `strong` node, the `active` CSS class is added to this toolbar element.\n      state: 'strong',\n    }]\n  ],\n  // ...\n});\n```\n\nIf the toolbar button is activated according to `state`, the `active` CSS class will be added and you can specify the style that you want using this class.\n\n### `state` list\nThe state of the toolbar element can only be changed by using the state value below.\n* `heading`: Heading\n* `strong`: Bold\n* `emph`: Italic\n* `strike`: Strike\n* `thematicBreak`: Horizontal Line\n* `blockQuote`: Quotes\n* `bulletList`: Bullet List\n* `orderedList`: Ordered List\n* `taskList`: Task List\n* `table`: Table\n* `code`: Inline Code\n* `codeBlock`: Code Block\n\n### `onUpdated()` option\nIf a toolbar element is created with the `el` option without using the default button UI, the state can be changed by configuring the `onUpdated` option. Because there is a limit to directly manipulating toolbar elements that are customized, it is going to provide the `onUpdated` callback options.\n\n```js\nconst myCustomEl = document.createElement('span');\n\nmyCustomEl.textContent = '😎';\nmyCustomEl.style = 'cursor: pointer; background: red;'\nmyCustomEl.addEventListener('click', () => {\n  editor.exec('bold');\n});\n\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  toolbarItems: [\n    [{\n      name: 'myItem',\n      tooltip: 'myItem',\n      el: myCustomEl,\n      state: 'strong',\n      onUpdated({ active, disabled }) {\n        if (active) {\n          myCustomEl.style.background = 'green';\n        } else {\n          myCustomEl.style.background = '';\n        }\n      }\n    }]\n  ],\n  // ...\n});\n```\n\nThe `onUpdated()` function passes the object that represent `active` and `disabled` state as parameter. This parameter allows you to add styling to an element or define the desired operation.\n\n## Example\n\nYou can see the example [here](https://nhn.github.io/tui.editor/latest/tutorial-example15-customizing-toolbar-buttons)"
  },
  {
    "path": "docs/en/viewer.md",
    "content": "# 👀 Viewer\n\n## What Is Viewer?\n\nTOAST UI Editor (henceforth referred to as 'Editor') provides the **viewer** in case you want to show _Markdown_ content without loading the Editor. The Viewer is much **lighter** than the Editor.\n\n## Creating Viewer\n\nThe method of creating the Viewer is similar to that of the Editor.\n\n> Ref. [Getting Started](https://github.com/nhn/tui.editor/blob/master/docs/en/getting-started.md)\n\n### Adding Wrapper Element\n\nYou need to add the container element where the Viewer will be created.\n\n```html\n...\n<body>\n  <div id=\"viewer\"></div>\n</body>\n...\n```\n\n### Importing Viewer's Constructor Function\n\nThe Viewer can be used by creating an instance with the constructor function. To get the constructor function, you should import the module using one of the following ways depending on your environment.\n\n#### Using Module Format in Node Environment\n\n- ES6 Modules\n\n```javascript\nimport Viewer from '@toast-ui/editor/dist/toastui-editor-viewer';\n```\n\n- CommonJS\n\n```javascript\nconst Viewer = require('@toast-ui/dist/toastui-editor-viewer');\n```\n\n#### Using Namespace in Browser Environment\n\n```javascript\nconst Viewer = toastui.Editor;\n```\n\nNote that the CDN file of the Viewer should use the following:\n\n```html\n...\n<body>\n  ...\n  <script src=\"https://uicdn.toast.com/editor/latest/toastui-editor-viewer.js\"></script>\n</body>\n...\n```\n\n### Adding CSS Files\n\nYou need to add the CSS files needed for the Viewer. Import CSS files in node environment, and add it to html file when using CDN.\n\n#### Using in Node Environment\n\n- ES6 Modules\n\n```javascript\nimport '@toast-ui/editor/dist/toastui-editor-viewer.css';\n```\n\n- CommonJS\n\n```javascript\nrequire('@toast-ui/editor/dist/toastui-editor-viewer.css');\n```\n\n#### Using in Browser Environment by CDN\n\n```html\n...\n<head>\n  ...\n  <link rel=\"stylesheet\" href=\"https://uicdn.toast.com/editor/latest/toastui-editor-viewer.min.css\" />\n</head>\n...\n```\n\n### Creating Instance\n\nYou can create an instance with options and call various API after creating an instance.\n\n```js\nconst viewer = new Viewer({\n  el: document.querySelector('#viewer'),\n  height: '600px',\n  initialValue: '# hello'\n});\n```\n\n![viewer-01](https://user-images.githubusercontent.com/37766175/121862304-a3ccc980-cd35-11eb-92c8-02b0e6fcf3cf.png)\n\nThe basic options available are:\n\n- `height`: Height in string or auto ex) `300px` | `auto`\n- `initialValue`: Initial value. Set Markdown string\n\nFind out more options [here](https://nhn.github.io/tui.editor/latest/ToastUIEditorViewer).\n\n## Another Way to Create Viewer\n\nBe careful not to load both an editor and a viewer at the same time because an editor already contains a viewer function, you can initialize with `Editor.factory()` of an editor and set the `viewer` option to value `true` in order to make the a viewer.\n\n```js\nimport Editor from '@toast-ui/editor';\n\nconst viewer = Editor.factory({\n  el: document.querySelector('#viewer'),\n  viewer: true,\n  height: '500px',\n  initialValue: '# hello'\n});\n```\n\n## Example\n\nYou can see the example [here](https://nhn.github.io/tui.editor/latest/tutorial-example04-viewer).\n"
  },
  {
    "path": "docs/en/widget.md",
    "content": "# 📱 Widget Node\n\nWhen you type a specific key in the editor, you can display a suggestion popup, or a link node as a specific widget node. The TOAST UI Editor (henceforth referred to as 'Editor') provides options and APIs for this feature.\n\n## Popup Widget\n\nWhen editing content in the editor, you may want to show the suggestion popup at the current cursor position. At this time, the `addWidget` API can be used to float the DOM node that is desired on the editor. This node does not affect other contents being edited, and is **temporarily added**. In other words, if you enter text or change focus, it disappears. The API signature is as follows.\n\n```ts\naddWidget(node: Node, style: WidgetStyle, pos?: EditorPos)\n```\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `node` | Node | DOM node to be added as widget | \n| `style` | 'top' \\| 'bottom' | Determines whether to add widget above or below the specified position. | \n| `pos` | EditorPos | Set where the widget will be added. This is optional value, if not set, adds widget to the current cursor position. | \n\n```js\nconst popup = document.createElement('ul');\n// ...\n\neditor.addWidget(popup, 'top');\n```\n\nWhen the above code is executed, a `popup` node is added as shown below.\n\n![image](https://user-images.githubusercontent.com/37766175/120617182-d6a0d300-c494-11eb-8fb9-58926c60e8b7.png)\n\nIf you want to show the widget when typing the specific key, `keyup` event is useful.\n\n```js\neditor.on('keyup', (editorType, ev) => {\n  if (ev.key === '@') {\n    const popup = document.createElement('ul');\n    // ...\n  \n    editor.addWidget(popup, 'top');\n  }\n})\n```\n\n### Inline Widget Node\n\nWe looked at how to temporarily add popup widget node depending on specific case. Then what can you do if you want to add a mention node by clicking on a specific item in the popup widget?\nBecause Markdown Editor is a text-based editor, such mention node cannot be added. In addition, WYSIWYG editor does not support the mention node internally as well, so it cannot be added.\nThe editor provides a `widgetRules` option for users who want to add *inline widget node* such as mention node. If the text conforms to the rules set for the `widgetRules` option, the node is rendered as an inline widget node in the editor. **Inline widget node is inserted into the editor as content unlike popup widget, affecting the position of other nodes**.\n\n```js\nconst reWidgetRule = /\\[(@\\S+)\\]\\((\\S+)\\)/;\n\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  widgetRules: [\n    {\n      rule: reWidgetRule,\n      toDOM(text) {\n        const rule = reWidgetRule;\n        const matched = text.match(rule);\n        const span = document.createElement('span');\n  \n        span.innerHTML = `<a class=\"widget-anchor\" href=\"${matched[2]}\">${matched[1]}</a>`;\n        return span;\n      },\n    },\n  ],\n});\n```\n\nAs shown in the example code, `widgetRules` has each rule in an array format, and each rule consists of `rule` and `toDOM` properties.\n\n* `rule`: The value should be the regular expression, and the text that matches this regular expression is replaced with a widget node and rendered.\n* `toDOM`: Defines the DOM node of the widget node to be rendered.\n\nWhen text matches to the rules of `widgetRules` is entered, it is replaced by an inline widget node as shown in the image below.\n\n![image](https://user-images.githubusercontent.com/37766175/120621226-a6f3ca00-c498-11eb-9355-0275fd3bdbdb.gif)\n\n### `insertText()`, `replaceSelection()` API\n\nYou can replace it with an inline widget node by typing text that matches the widget rules directly, but most of the time you want to insert an inline widget node, such as a mention node, by clicking on a specific item in a popup widget.\n\nIn such cases, `inserText()` and `replaceSelection()` APIs can be used to insert an inline widget node when an item in a popup widget is clicked.\n\n```js\nul.addEventListener('mousedown', (ev) => {\n  const text = ev.target.textContent.replace(/\\s/g, '').replace(/😎/g, '');\n  const [start, end] = editor.getSelection();\n\n  editor.replaceSelection(`[@${text}](${text})`, [start[0], start[1] - 1], end);\n});\n```\n\nIn the example code, the position calculated based on the current cursor position through `getSelection()` API was passed to `replaceSelection()` API because `@` character should be replaced with widget node. As a result, you can see the `@` character replaced by an inline widget node when you click an item in the popup widget as shown in the image below.\n\n![image](https://user-images.githubusercontent.com/37766175/120624280-81b48b00-c49b-11eb-9896-432120c27389.gif)\n"
  },
  {
    "path": "docs/ko/README.md",
    "content": "# 📄 Documents\n\n## 튜토리얼\n\n- [🚀 시작하기](https://github.com/nhn/tui.editor/blob/master/docs/ko/getting-started.md)\n- [👀 뷰어](https://github.com/nhn/tui.editor/blob/master/docs/ko/viewer.md)\n- [🧩 Plugins](https://github.com/nhn/tui.editor/blob/master/docs/ko/plugin.md)\n- [🌏 Internationalization (i18n)](https://github.com/nhn/tui.editor/blob/master/docs/ko/i18n.md)\n- [🎨 Custom HTML Renderer](https://github.com/nhn/tui.editor/blob/master/docs/ko/custom-html-renderer.md)\n- [🔩 커스텀 블록](https://github.com/nhn/tui.editor/blob/master/docs/ko/custom-block.md)\n- [🔗 자동 링크 확장](https://github.com/nhn/tui.editor/blob/master/docs/ko/extended-autolinks.md)\n- [🛠 툴바](https://github.com/nhn/tui.editor/blob/master/docs/ko/toolbar.md)\n- [📱 위젯](https://github.com/nhn/tui.editor/blob/master/docs/ko/widget.md)\n\n### 마이그레이션 가이드\n\n- [✈️ v3.0 마이그레이션 가이드](https://github.com/nhn/tui.editor/blob/master/docs/v3.0-migration-guide-ko.md)\n\n## Etc\n\n- [📌 Commit Message Convention](https://github.com/nhn/tui.editor/blob/master/docs/COMMIT_MESSAGE_CONVENTION.md)\n- [📌 Contributing](https://github.com/nhn/tui.editor/blob/master/CONTRIBUTING.md)\n- [📌 Code of conduct](https://github.com/nhn/tui.editor/blob/master/CODE_OF_CONDUCT.md)"
  },
  {
    "path": "docs/ko/custom-block.md",
    "content": "# 🔩 커스텀 블록 노드와 HTML 노드\n\nTOAST UI Editor(이하 '에디터'라고 명시)는 [CommonMark](https://spec.commonmark.org/0.29/) 스펙을 준수하며, 추가로 [GFM](https://github.github.com/gfm/) 스펙도 지원한다. 하지만 만약 CommonMark나 GFM에서 지원하지 않는 특정 문법을 사용하고 싶다면 어떨까? 예를 들어 마크다운에서 [LaTeX](https://www.latex-project.org/) 문법을 사용하거나 차트 같은 요소를 렌더링하고 싶을 수 있다. 에디터에서는 이러한 사용성을 위해 사용자만의 **커스텀 블록 노드**를 정의할 수 있는 옵션을 제공한다.\n\n## 커스텀 블록 노드\n\n에디터는 마크다운 AST(Abstract Syntax Tree)를 HTML 문자열로 변환할 때 커스터마이징할 수 있는 `customHTMLRenderer` 옵션을 제공한다. `customHTMLRenderer` 옵션을 사용하면 `table`, `heading`처럼 CommonMark나 GFM에서 지원하는 노드의 렌더링 결과를 커스터마이징할 수 있다. 커스텀 블록 노드 역시 이 `customHTMLRenderer` 옵션을 사용하여 정의할 수 있다.\n\n다음 코드는 LaTex 문법을 지원하는 라이브러리인 KaTeX를 사용하여 수식을 렌더링하는 커스텀 블록 노드를 정의한 것이다.\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  customHTMLRenderer: {\n    latex(node) {\n      const generator = new latexjs.HtmlGenerator({ hyphenate: false });\n      const { body } = latexjs.parse(node.literal, { generator }).htmlDocument();\n\n      return [\n        { type: 'openTag', tagName: 'div', outerNewLine: true },\n        { type: 'html', content: body.innerHTML },\n        { type: 'closeTag', tagName: 'div', outerNewLine: true }\n      ];\n    },\n  }\n});\n```\n\n`customHTMLRenderer` 옵션에 `latex` 함수 프로퍼티를 작성하였고 이 함수에서는 렌더링 될 HTML을 토큰 형태로 반환한다. 마크다운 노드를 커스터마이징을 할 때와 거의 동일한 형태로 옵션을 지정하기 때문에 쉽게 사용할 수 있다. 위의 코드는 마크다운 에디터에서 다음처럼 렌더링된다.\n\n![image](https://user-images.githubusercontent.com/37766175/120983159-65bf2b00-c7b4-11eb-84af-30c38e832585.png)\n\n위의 이미지에서 볼 수 있듯이 마크다운 에디터에서 커스텀 블록 노드를 사용하기 위해서는 `$$` 기호로 감싸진 블록 내에 텍스트를 입력해야 한다. `$$` 기호로 감싸진 블록은 에디터에서 커스텀 블록 노드로 파싱된다. 또한 어떠한 커스텀 블록 노드인지 나타내기 위해 `$$` 기호 다음에 반드시 `customHTMLRenderer` 옵션에서 정의한 노드 이름을 작성해야 한다.\n\n```js\n// $$ 기호 뒤에 옵션에서 정의한 노드 이름을 반드시 명시해야 한다.\n$$latex\n\\documentclass{article}\n\\begin{document}\n\n$\nf(x) = \\int_{-\\infty}^\\infty \\hat f(\\xi)\\,e^{2 \\pi i \\xi x} \\, d\\xi\n$\n\\end{document}\n$$\n```\n\n### 위지윅\n정의된 커스텀 블록 노드는 위지윅 에디터에서 아래 이미지처럼 동작한다.\n\n![image](https://user-images.githubusercontent.com/37766175/120984395-96539480-c7b5-11eb-8e57-2f43082f345f.gif)\n\n위지윅 에디터에서 커스텀 블록 노드는 마크다운 프리뷰와 동일한 모습으로 렌더링되며, 노드를 클릭하여 선택했을 때 나오는 편집 버튼을 통해 내용을 변경할 수 있다. 커스텀 블록 노드도 결국 특정 텍스트를 기준으로 파싱되는 것이기 때문에 위지윅 에디터에서의 편집도 텍스트를 기준으로 한다. 이는 일반적인 위지윅 에디터와는 다른 동작이지만 **TOAST UI Editor는 마크다운을 기반으로 위지윅 에디터를 지원**하기 때문에 이러한 동작이 더 이상적이다.\n\n## HTML 노드\n\nCommonMark에서는 `<`과 `>` 문자를 사용하여 기본적으로 지원하지 않는 노드를 HTML 문자열 형태로 작성할 수 있다.\n([CommonMark Raw HTML Spec 참조](https://spec.commonmark.org/0.29/#raw-html))\n\n에디터의 마크다운 에디터에서도 이러한 스펙을 준수하기 때문에 HTML 문자열은 마크다운 프리뷰에서 올바르게 렌더링 된다.\n\n![image](https://user-images.githubusercontent.com/37766175/120987131-44f8d480-c7b8-11eb-971f-0b4ecb59e112.png)\n\n### 위지윅\n하지만 안타깝게도 위지윅 에디터에서는 HTML 노드를 제대로 렌더링할 수 없다. 에디터는 내부적으로 위지윅 에디터에서 기본으로 지원하는 노드를 추상화된 모델 객체로 관리하고 있다. 위지윅 에디터에서 지원하는 노드란 CommonMark와 GFM 에서 지원하는 노드(`heading`, `list`, `strike` 등)와 커스텀 블록 노드를 의미한다.\n\n![image](https://user-images.githubusercontent.com/37766175/120989247-4c20e200-c7ba-11eb-8420-7ff5726592cf.gif)\n\n위 예시 이미지의 `iframe` 노드는 위지윅 에디터에서 기본적으로 지원하는 노드가 아니다. 그렇기 때문에 `iframe` 노드를 위지윅 에디터에서도 사용하고 싶다면 `customHTMLRenderer` 옵션을 사용하여 추가 설정을 해야 한다.\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  customHTMLRenderer: {\n    htmlBlock: {\n      iframe(node) {\n        return [\n          { type: 'openTag', tagName: 'iframe', outerNewLine: true, attributes: node.attrs },\n          { type: 'html', content: node.childrenHTML },\n          { type: 'closeTag', tagName: 'iframe', outerNewLine: true },\n        ];\n      },\n    }\n  },\n});\n```\n\nHTML 노드는 `customHTMLRenderer.htmlBlock` 프로퍼티에 정의한다. 위에서 설명한 커스텀 블록 노드와 구분하기 위해 `htmlBlock` 프로퍼티 내에서 추가할 HTML 노드의 컨버팅 함수를 정의한다. 예제 코드를 실행하면 아래 이미지처럼 위지윅에서도 `iframe` 노드가 올바르게 렌더링된다.\n\n![image](https://user-images.githubusercontent.com/37766175/120989209-40352000-c7ba-11eb-9112-047a0af4f9d6.gif)\n\n만약 인라인 HTML 노드를 사용하고 싶다면, `customHTMLRenderer.htmlInline` 프로퍼티에 정의한다.\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  customHTMLRenderer: {\n    htmlBlock: {\n      iframe(node) {\n        return [\n          { type: 'openTag', tagName: 'iframe', outerNewLine: true, attributes: node.attrs },\n          { type: 'html', content: node.childrenHTML },\n          { type: 'closeTag', tagName: 'iframe', outerNewLine: true },\n        ];\n      },\n    },\n    htmlInline: {\n      big(node, { entering }) {\n        return entering\n          ? { type: 'openTag', tagName: 'big', attributes: node.attrs }\n          : { type: 'closeTag', tagName: 'big' };\n      },\n    },\n  },\n});\n```"
  },
  {
    "path": "docs/ko/custom-html-renderer.md",
    "content": "# 🎨 Custom HTML Renderer\n\nTOAST UI Editor(이하 '에디터'라고 명시)는 마크다운 텍스트를 HTML 문자열로 변환하기 위해 `ToastMark`라는 자체 마크 다운 파서를 사용한다. `ToastMark`는 두 단계로 마크다운 텍스트를 변환한다.\n\n1. 마크다운 텍스트를 AST(Abstract Syntax Tree)로 변환한다.\n2. 변환된 AST를 순회하며 HTML 문자열을 생성한다.\n\n첫 번째 단계에서 AST를 생성할 때 커스터마이징 옵션이나 API를 제공하는 것은 파싱 과정 자체를 사용자가 이해해야 하므로 어려운 일이 될 것이다. 하지만 완성된 AST를 사용하여 HTML 문자열로 변환할 때에는 HTML 토큰화에 대해서만 이해하면 되기 때문에 사용자가 커스터마이징하기 어렵지 않다. \n\n그렇기 때문에 에디터에서는 두 번째 단계(AST를 사용하여 HTML 문자열로 변환)에서 커스터마이징할 수 있는 옵션을 사용자에게 제공한다. 이 옵션은 마크다운 프리뷰뿐만 아니라 마크다운에서 위지윅 에디터로 컨버팅할 때에도 적용이 된다. 다만 아래처럼 내부적인 컨버팅 로직은 다르게 동작한다.\n\n* 마크다운 프리뷰: 커스터마이징 옵션에 정의한 **HTML 토큰은 마크다운 HTML 문자열을 생성할 때 사용된다**.\n* 마크다운 → 위지윅 컨버팅: 커스터마이징 옵션에 정의한 **HTML 토큰은 위지윅의 노드로 변환될 때 사용된다**. 이 때 위지윅 노드는 DOM 노드가 아닌 에디터 내부적으로 관리하는 추상화된 모델 객체이다.\n\n## 기본 사용 방법\n\n에디터에서는 `customHTMLRenderer` 옵션으로 HTML 문자열 변환 과정을 커스터마이징할 수 있다. 이 옵션은 key-value 형태의 객체이며, 객체의 키는 AST의 노드 타입, 값은 AST 노드를 HTML 토큰으로 변환하여 반환하는 함수이다.\n\n다음 코드는 `customHTMLRenderer` 옵션을 사용하는 기본 예시이다.\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  customHTMLRenderer: {\n    heading(node, context) {\n      return {\n        type: context.entering ? 'openTag' : 'closeTag',\n        tagName: 'div',\n        classNames: [`heading-'${node.level}`]\n      }\n    },\n    text(node, context) {\n      const strongContent = node.parent.type === 'strong';\n      return {\n        type: 'text',\n        content: strongContent ? node.literal.toUpperCase() : node.literal\n      }\n    },\n    linebreak(node, context) {\n      return {\n        type: 'html',\n        content: '\\n<br />\\n'\n      }\n    }\n  }\n});\n```\n\n만약 마크다운 텍스트가 아래와 같다면,\n\n```markdown\n## Heading\nHello\nWorld\n```\n\n다음과 같이 변환된다.\n\n```html\n<div class=\"heading2\">HEADING</div>\n<p>Hello<br><br>World</p>\n```\n\n## HTML 토큰\n \n위의 기본 예시에서 볼 수 있듯이 각 함수는 HTML 문자열을 직접 반환하는 것이 아니라 **토큰 객체**를 반환한다. 토큰 객체는 `ToastMark` 내부 모듈에 의해 HTML 문자열로 자동 변환된다. HTML 텍스트 대신 토큰을 사용하는 이유는 구조적인 정보를 담아 기본 동작을 재정의하고 재사용할 수 있기 때문이다.\n\n토큰 객체에 사용할 수 있는 타입은 `openTag`, `closeTag`, `text`, `html` 4가지가 있다.\n\n### openTag\n\n`openTag` 토큰은 열린 태그 문자열을 나타낸다. `openTag` 토큰은 HTML 문자열을 생성하기 위해 `tagName`, `attributes`, `classNames` 프로퍼티를 가지고 있다. \n\n다음 코드처럼 `openTag` 객체 옵션을 지정한다면,\n\n```js\n{\n  type: 'openTag',\n  tagName: 'a',\n  classNames: ['my-class1', 'my-class2']\n  attributes: {\n    target: '_blank',\n    href: 'http://ui.toast.com'\n  }\n}\n```\n\n아래와 같은 HTML 문자열로 변환된다.\n\n```html\n<a class=\"my-class1 my-class2\" href=\"http://ui.toast.com\" target=\"_blank\">\n```\n\n만약 `<br />`과 `<hr />`처럼 자체적으로 닫기 태그를 지정하고 싶다면, `selfClose` 옵션을 사용하면 된다.\n\n```js\n{\n  type: 'openTag',\n  tagName: 'br',\n  classNames: ['my-class'],\n  selfClose: true\n}\n```\n\n```html\n<br class=\"my-class\" />\n```\n\n### closeTag\n\n`closeTag` 토큰은 닫는 태그 문자열을 나타낸다. `closeTag` 토큰에서는 `tagName` 프로퍼티만 지정하면 된다.\n\n```js\n{\n  type: 'closeTag',\n  tagName: 'a'\n}\n```\n\n```html\n</a>\n```\n\n### text\n\n`text` 토큰은 일반 텍스트 문자열을 나타낸다. 이 토큰에는 `content` 프로퍼티만 존재하며 이 값은 이스케이프 처리되어 HTML 텍스트로 사용된다.\n\n```js\n{\n  type: 'text',\n  content: '<br />'\n}\n```\n\n```html\n&lt;br /&gt;\n```\n\n### html\n\n`html` 토큰은 HTML 문자열을 의미한다. `text` 토큰과 마찬가지로 `content` 프로퍼티만 가지지만, 이스케이프 처리 없이 그대로 사용된다. DOM의 `innerHTML` API와 거의 동일한 역할을 한다고 이해하면 된다.\n\n```js\n{\n  type: 'html',\n  content: '<br />'\n}\n```\n\n```html\n<br />\n```\n\n## Node\n\n옵션으로 지정한 컨버팅 함수의 첫 번째 매개변수는 `Node` 객체이다. 이 객체는 `ToastMark`에 의해 생성된 AST(Abstract Syntax Tree)의 주요 구성 요소이다. 모든 노드는 `parent`, `firstChild`, `lastChild`, `prev`, `next` 등 트리를 구성하기 위한 공통의 속성을 가지고 있다.\n\n또한 각 노드는 타입에 따른 고유한 프로퍼티가 있다. 예를 들어 `heading` 노드는 헤딩 요소의 레벨을 나타내는 `level` 프로퍼티가 있고, `link` 노드에는 링크 URL을 나타내는 `destination` 프로퍼티가 있다.\n\n아래 예시를 보면 마크다운 텍스트가 AST로 변환되었을 때 어떠한 구조인지 파악할 수 있다.\n\n```md\n## TOAST UI\n**Hello** World!\n```\n\n```js\n{\n  type: 'document',\n  firstChild: {\n    type: 'heading',\n    level: 2,\n    parent: //[document node],\n    firstChild:\n      type: 'text',\n      parent: //[heading node],\n      literal: 'TOAST UI'\n    },\n    next: {\n      type: 'paragraph',\n      parent: //[document node],\n      firstChild: {\n        type: 'strong',\n        parent: //[paragraph node],\n        firstChild: {\n          type: 'text',\n          parent: //[strong node],\n          literal: 'Hello'\n        },\n        next: {\n          type: 'text',\n          parent: //[paragraph node],\n          literal: 'World !'\n        }\n      }\n    }\n  }\n}\n```\n\nAST를 구성하는 각 노드의 타입은 [이 코드](https://github.com/nhn/tui.editor/blob/master/libs/toastmark/src/commonmark/node.ts)에서 확인할 수 있다.\n\n## Context\n\n에디터가 AST를 사용하여 HTML 문자열을 생성할 때에는 전위순회 방식으로 모든 노드를 탐색한다. 노드를 방문할 때마다 노드의 타입과 동일한 키 값을 가진 컨버팅 함수가 호출되며, `context` 객체는 컨버팅 함수의 두 번째 매개변수로 주어진다.\n\n### entering\n\n에디터에서 [이 함수](https://github.com/nhn/tui.editor/blob/master/libs/toastmark/src/commonmark/node.ts#L38)에 정의된 노드 타입들은 AST의 순회 중 두 번씩 방문한다. 첫 번째는 해당 노드로 순회를 시작할 때 방문하며, 두 번째는 모든 자식 노드들을 순회한 후 방문한다. `context` 객체의 `entering` 프로퍼티를 사용하여 컨버팅 함수가 호출되는 시점을 알 수 있다.\n\n다음 코드는 `entering` 프로퍼티를 사용하는 예시이다.\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  customHTMLRenderer: {\n    heading({ level }, { entering }) {\n      return {\n        type: entering ? 'openTag' : 'closeTag',\n        tagName: `h${level}`,\n      }\n    },\n    text({ literal }) {\n      return {\n        type: 'text',\n        content: node.literal\n      }\n    }\n  }\n});\n```\n\n`heading` 노드의 컨버팅 함수는 `context.entering` 프로퍼티를 사용하여 반환할 토큰 객체의 타입을 결정한다. 값이 `true`일 때 `openTag`을 반환하여, 그렇지 않으면 `closeTag`를 반환한다. `text` 컨버팅 함수는 리프 노드이기 때문에 한 번만 호출되므로 `entering` 속성을 사용할 필요가 없다.\n\n만약 다음 마크다운 텍스트를 에디터에 입력했을 때,\n\n```markdown\n# TOAST UI\n```\n\n`ToastMark`가 생성한 AST는 아래와 같다. (편의상 필수 프로퍼티만 간략하게 나타내었다.)\n\n```js\n{\n  type: 'document',\n  firstChild: {\n    type: 'heading',\n    level: 1,\n    firstChild: {\n      type: 'text',\n      literal: 'TOAST UI'\n    }\n  }\n}\n```\n\nAST 순회를 모두 마치면 지정한 컨버팅 함수의 결과로 반환된 토큰들이 아래와 같은 배열 형태로 저장된다.\n\n```js\n[\n  { type: 'openTag', tagName: 'h1' },\n  { type: 'text', content: 'TOAST UI' },\n  { type: 'closeTag', tagName: 'h1' }\n]\n```\n\n최종적으로 에디터 내부에서 토큰 배열을 사용하여 HTML 문자열로 생성한다.\n\n```html\n<h1>TOAST UI</h1>\n```\n\n### origin()\n\n만약 `customHTMLRenderer`로 지정한 함수 안에서 원래 기존의 컨버팅 함수를 사용하고 싶다면, `origin()` 함수를 호출하여 사용할 수 있다.\n\n예를 들어 `link` 노드에 대해 아래와 같은 HTML 토큰을 반환하는 기존의 컨버팅 함수가 있다고 가정해보자.\n\n#### `entering: true`인 경우\n```js\n{\n  type: 'openTag',\n  tagName: 'a',\n  attributes: {\n    href: 'http://ui.toast.com',\n    title: 'TOAST UI'\n  }\n}\n```\n#### `entering: false`인 경우\n```js\n{\n  type: 'closeTag',\n  tagName: 'a'\n}\n```\n\n이 경우 직접 정의한 컨버팅 함수에서 `origin()` 함수를 호출하여 기존에 정의된 컨버팅 함수를 실행할 수 있다. 아래 코드는 `origin()`(기존 컨버팅 함수)을 호출하여 반환된 HTML 토큰에 `target=\"_blank\"` 속성을 추가적으로 설정한 것이다.\n\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  customHTMLRenderer: {\n    link(node, context) {\n      const { origin, entering } = context;\n      const result = origin();\n      if (entering) {\n        result.attributes.target = '_blank';\n      }\n      return result;\n    }\n  },\n}\n```\n\n#### `entering: true`인 경우\n```js\n{\n  type: 'openTag',\n  tagName: 'a',\n  attributes: {\n    href: 'http://ui.toast.com',\n    target: '_blank',\n    title: 'TOAST UI'  \n  }\n}\n```\n\n## 심화 사용 방법\n\n### getChildrenText()\n\n대부분의 경우 노드의 컨버팅 함수에서 자식 노드의 텍스트가 필요하진 않을 것이다. 하지만 종종 자식 노드의 텍스트를 가져와 속성을 설정해야 하는 경우가 있다. 이러한 경우 `context` 객체의 `getChildrenText()` 함수를 사용하면 유용하다.\n\n예를 들어 헤딩 요소에 자식 콘텐츠를 기준으로 `id`를 설정하고 싶다면 아래 코드처럼 `getChildrenText()` 함수를 사용할 수 있다.\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  customHTMLRenderer: {\n    heading(node, { entering, getChildrenText }) {\n      const tagName = `h${node.level}`;\n      \n      if (entering) {\n        return {\n          type: 'openTag',\n          tagName,\n          attributes: { \n            id: getChildrenText(node).trim().replace(/\\s+/g, '-')\n          }        \n        }\n      }\n      return { type: 'closeTag', tagName };\n    }\n  }\n});\n```\n\n다음과 같은 마크다운 텍스트가 있다면,\n\n```markdown\n# Hello *World*\n```\n\n`heading` 컨버팅 함수에서 `getChildrenText()` 함수의 반환 값은 `Hello World` 문자열이 된다. 컨버팅 함수에서는 공백 문자를 `-` 문자로 치환했기 때문에 최종 HTML 문자열은 아래와 같다.\n\n```html\n<h1 id=\"Hello-World\">Hello <em>World</em></h1>\n```\n\n### skipChildren()\n\n`skipChildren()` 함수를 호출하면 자식 노드의 순회를 건너뛴다. 자식 노드의 콘텐츠를 변환하지 않고 현재 노드의 속성만 사용하여 콘텐츠로 사용하고 싶을 때 유용하다.\n\n예를 들어 `image` 노드에는 이미지의 설명을 나타내는 자식 노드가 존재한다. 그러나 `image` 노드를 HTML로 표현하는 `img` 요소는 자식 요소를 가질 수 없다. 그렇기 때문에 `image` 노드의 자식 노드가 불필요한 HTML 문자열로 변환되지 않도록 `skipChildren()` 함수를 호출해야 한다. 만약 자식 노드의 콘텐츠가 필요하다면 앞서 보았던 `getChildrenText()`를 호출하여 사용할 수 있다. 이러한 자식 노드의 콘텐츠는 `img` 요소의 `alt` 속성으로 설정할 수 있다.\n\n다음 코드는 에디터에 내장된 `image` 노드 컨버터 함수의 예시이다.\n\n```js\nfunction image(node, context) {\n  const { destination } = node;\n  const { getChildrenText, skipChildren } = context;\n\n  skipChildren();\n\n  return {\n    type: 'openTag',\n    tagName: 'img',\n    selfClose: true,\n    attributes: {\n      src: destination,\n      alt: getChildrenText(),\n    }\n  }\n}\n```\n\n### 다중 태그 사용\n\n컨버팅 함수에서는 배열 형태의 토큰을 반환할 수 있다. 이것은 노드를 중첩된 HTML 구조로 변환하려는 경우에 유용하다. 다음 코드는 `codeBlock` 노드를 `<pre><code>...</code></pre>` 태그 문자열로 변환하는 예시이다.\n\n```js\nfunction codeBlock(node) {\n  return [\n    { type: 'openTag', tagName: 'pre', classNames: ['code-block'] },\n    { type: 'openTag', tagName: 'code' },\n    { type: 'text', content: node.literal },\n    { type: 'closeTag', tagName: 'code' },\n    { type: 'closeTag', tagName: 'pre' }\n  ];\n}\n```\n\n### 개행 추가\n\n일반적인 경우 최종적으로 변환된 HTML 문자열의 포맷에 신경 쓸 필요가 없다. 그러나 `ToastMark`는 [CommonMark Spec](https://spec.commonmark.org/0.29/)을 준수하기 때문에 개행을 제어하는 옵션을 지원해야만 한다.([공식 테스트 데이터](https://spec.commonmark.org/0.29/spec.json))\n\n컨버팅 함수의 토큰 객체에 `outerNewline`과 `innerNewline` 프로퍼티를 추가하여 개행을 제어할 수 있다.\n\n#### 토큰 배열\n```js\n[\n  {\n    type: 'text',\n    content: 'Hello'\n  },\n  { \n    type: 'openTag',\n    tagName: 'p',\n    outerNewLine: true,\n    innerNewLine: true\n  },\n  {\n    type: 'html',\n    content: '<strong>My</strong>',\n    outerNewLine: true,\n  },\n  {\n    type: 'closeTag',\n    tagName: 'p',\n    innerNewLine: true\n  },\n  {\n    type: 'text',\n    content: 'World'\n  }\n]\n```\n\n#### 변환된 HTML 문자열\n```html\nHello\n<p>\n<strong>My</strong>\n</p>World\n```\n\n위의 예시에서 볼 수 있듯이 `openTag`의 `outerNewLine` 프로퍼티는 여는 태그 문자열 시작 전에 `\\n` 문자를 추가한다. 만약 `closeTag`에 `outerNewLine` 프로퍼티가 있다면 닫는 태그 문자열 이후에 `\\n` 문자를 추가한다. 이와 반대로, `openTag`의 `innerNewLine` 프로퍼티는 여는 태그 문자열 이후에 `\\n` 문자를 추가한다. 만약 `closeTag`에 `innerNewLine` 프로퍼티가 있다면 닫는 태그 문자열 시작 전에 `\\n` 문자를 추가한다.\n\n연속된 개행이 있는 경우 중복을 막기 위해 하나의 개행으로 병합된다.\n"
  },
  {
    "path": "docs/ko/extended-autolinks.md",
    "content": "# 🔗 자동 링크 확장\n\n## 자동 링크란 무엇일까?\n\n[자동 링크](https://spec.commonmark.org/0.29/#autolinks)는 CommonMark에 정의된 스펙이다. (자동 링크의 세부 사양을 알고 싶으면 위 링크의 예를 참조바란다.)\n\n자동 링크는 `<`, `>` 사이에 위치한 절대 경로 URI 또는 이메일 주소이다. URL 또는 이메일 주소를 링크 레이블로 하여 구문 분석된다.\n\n이 기능은 TOAST UI Editor(이하 '에디터'라고 명시)가 CommonMark 스펙을 따르기 때문에 에디터에서도 별도의 설정 없이 사용할 수 있다.\n\n![image](https://user-images.githubusercontent.com/37766175/120604939-7ad04d00-c488-11eb-82c1-f9f05891039e.png)\n\n### 자동 링크 확장\n\n자동 링크 확장은 [GFM](https://github.github.com/gfm) 스펙에서 지원하는 기능이다. 이 기능을 사용하면 텍스트를 자동 링크로 구문 분석하는 경우의 수가 더 많아진다. 예를 들어 텍스트에 `www.`가 있는 경우, 유효한 도메인으로 인식되어 아래와 같이 자동 링크로 인식된다.\n\n![image](https://user-images.githubusercontent.com/37766175/120605112-a5baa100-c488-11eb-9b72-75eaa9324080.png)\n\n자동 링크 확장과 관련된 자세한 예시는 [여기](https://github.github.com/gfm/#autolinks-extension-)에서 찾을 수 있다.\n\n\n## 자동 링크 확장 설정\n\n자동 링크 확장은 `extendedAutolinks` 옵션을 설정하여 사용할 수 있다. `extendedAutolinks` 옵션 값을 설정하지 않는다면, 에디터는 `false` 값을 기본값으로 사용하여, 이 경우 자동 링크 확장 기능은 동작하지 않는다.\n\n만약 `extendedAutolinks` 값을 `true` 값으로 설정한다면, 자동 링크 확장 기능을 사용할 수 있다.\n\n```js\nconst editor = new toastui.Editor({\n  // ...\n  extendedAutolinks: true\n});\n```\n\n## 자동 링크 확장 커스터마이징\n\n에디터에서는 콜백 함수 형태로 옵션을 설정하여 사용자가 자동 링크를 확장할 수 있다. 이 옵션은 특정 링크 형식을 지원하려는 경우에 유용하다.\n\n자동 링크 확장을 커스터마이징하려면 `extendedAutolinks` 옵션을 `function`으로 설정해야 한다. 아래 간단한 예제 코드가 있다.\n\n```js\nconst reToastuiEditorRepo = /tui\\.editor/g;\n\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  extendedAutolinks: (content) => {\n    const matched = content.match(reToastuiEditorRepo);\n    if (matched) {\n      return matched.map(m =>\n        ({\n          text: 'toastui-editor',\n          url: 'https://github.com/nhn/tui.editor',\n          range: [0, 9]\n        })\n      );\n    }\n    return null;\n  }\n});\n```\n\n편집 중인 콘텐츠는 위의 코드에서 볼 수 있듯이 `content` 매개 변수로 `extendedAutolinks`에 정의된 콜백 함수에 전달된다. 만약 콘텐츠에서 원하는 형태의 텍스트를 찾는다면, 배열 형태로 확장 링크 정보를 반환해야 한다. 배열 내의 각 링크 정보는 `text`, `url`, `range` 속성으로 구성된다.\n\n* `text`: 링크 라벨\n* `url`: 링크 url\n* `range`: 내부적인 소스 위치 계산을 위한 링크 범위.\n\n아래 이미지는 예제 코드를 실행한 결과이다.\n\n![image](https://user-images.githubusercontent.com/37766175/120606618-55444300-c48a-11eb-8376-859fc6ffcf07.gif)"
  },
  {
    "path": "docs/ko/getting-started.md",
    "content": "# 🚀 시작하기\n\n## 설치하기\n\nTOAST UI Editor는 패키지 매니저를 이용하거나, 직접 소스 코드를 다운받아 사용할 수 있다. 하지만 패키지 매니저 사용을 권장한다.\n\n### 패키지 매니서 사용하기 (npm)\n\n각 패키지 매니저가 제공하는 CLI 도구를 사용하면 쉽게 패키지를 설치할 수 있다. npm 사용을 위해선 [Node.js](https://nodejs.org/ko/)를 미리 설치해야 한다.\n\n```sh\n$ npm install --save @toast-ui/editor # 최신 버전\n$ npm install --save @toast-ui/editor@<version> # 특정 버전\n```\n\nnpm을 통해 설치했다면, 아래와 같은 구조로 TOAST UI Editor가 설치된 것을 볼 수 있다.\n\n```\n- node_modules/\n   ├─ @toast-ui/editor/\n   │     ├─ dist/\n   │     │    ├─ toastui-editor.js\n   │     │    ├─ toastui-editor-viewer.js\n   │     │    ├─ toastui-editor.css\n   │     │    ├─ toastui-editor-viewer.css\n   │     │    └─ toastui-editor-only.css\n```\n\n### Contents Delivery Network (CDN) 사용하기\n\nTOAST UI Editor는 CDN을 통해 사용할 수 있다.\n\n```html\n...\n<body>\n  ...\n  <script src=\"https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js\"></script>\n</body>\n...\n```\n\n특정 버전을 사용하려면 url 경로에서 `latest` 대신 버전 태그를 사용해야 한다.\n\nCDN은 아래와 같은 디렉토리 구조로 구성된다.\n\n```\n- uicdn.toast.com/\n   ├─ editor/\n   │     ├─ latest/\n   │     │    ├─ toastui-editor-all.js\n   │     │    ├─ toastui-editor-all.min.js\n   │     │    ├─ toastui-editor-viewer.js\n   │     │    ├─ toastui-editor-viewer.min.js\n   │     │    ├─ toastui-editor-editor.js\n   │     │    ├─ toastui-editor-editor.min.js\n   │     │    ├─ toastui-editor-editor.css\n   │     │    ├─ toastui-editor-editor.min.css\n   │     │    ├─ toastui-editor-viewer.css\n   │     │    └─ toastui-editor-viewer.min.css\n   │     ├─ 3.0.0/\n   │     │    └─ ...\n```\n\n## 사용하기\n\n### 컨테이너 요소 추가\n\nTOAST UI Editor(이하 '에디터'로 명시)가 생성될 컨테이너 요소를 추가한다.\n\n```html\n...\n<body>\n  <div id=\"editor\"></div>\n</body>\n...\n```\n\n### 에디터 생성자 함수 불러오기\n\n에디터는 생성자 함수를 통해 인스턴스를 생성할 수 있다. 생성자 함수에 접근하기 위해서는 환경에 따라 접근할 수 있는 세 가지 방법이 존재한다.\n\n#### Node.js 환경에서의 모듈 사용\n\n- ES6 모듈\n\n```javascript\nimport Editor from '@toast-ui/editor';\n```\n\n- CommonJS\n\n```javascript\nconst Editor = require('@toast-ui/editor');\n```\n\n#### 브라우저 환경에서의 namespace 사용\n\n```javascript\nconst Editor = toastui.Editor;\n```\n\n### CSS 파일 추가\n\n에디터 사용을 위해 CSS파일을 추가해야 한다. Node.js 환경에서는 CSS 파일을 가져와 사용하며, CDN을 사용할 때는 html 파일에 CSS 파일 의존성을 추가하여 사용한다.\n\n#### Node.js 환경\n\n- ES6 모듈\n\n```javascript\nimport '@toast-ui/editor/dist/toastui-editor.css'; // Editor 스타일\n```\n\n- CommonJS\n\n```javascript\nrequire('@toast-ui/editor/dist/toastui-editor.css');\n```\n\n#### CDN 환경\n\n```html\n...\n<head>\n  ...\n  <!-- Editor's Style -->\n  <link rel=\"stylesheet\" href=\"https://uicdn.toast.com/editor/latest/toastui-editor.min.css\" />\n</head>\n...\n```\n\n### 인스턴스 생성하기\n\n옵션과 함께 인스턴스를 생성하여 다양한 API를 호출할 수 있다.\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor')\n});\n```\n\n![getting-started-01](https://user-images.githubusercontent.com/37766175/121855586-7d576000-cd2e-11eb-9196-0c20270d1221.png)\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  height: '600px',\n  initialEditType: 'markdown',\n  previewStyle: 'vertical'\n});\n```\n\n![getting-started-02](https://user-images.githubusercontent.com/37766175/121464762-71e2fc80-c9ef-11eb-9a0a-7b06e08d3ccb.png)\n\n대표적인 기본 옵션은 아래와 같다.\n\n- `height`: 에디터 영역의 높기 값. 문자열 값을 가진다. `300px` | `auto`\n- `initialEditType`: 최초로 보여줄 에디터 타입. `markdown` | `wysiwyg`\n- `initialValue`: 콘텐츠 초기값. 반드시 마크다운 문자열 형태여야 한다.\n- `previewStyle`: 마크다운 프리뷰 스타일. `tab` | `vertical`\n- `usageStatistics`: 에디터를 사용하는 웹 사이트의 _호스트명_을 전송한다. 어떠한 사용자가 에디터를 사용하고 있는지 수집하기 위합니다. 이 옵션은 불리언 값을 지정하여 비활성화할 수 있다. `true` | `false`\n\n더 많은 옵션은 [여기](https://nhn.github.io/tui.editor/latest/ToastUIEditor)서 볼 수 있다.\n\n## 예제\n\n예제는 [여기](https://nhn.github.io/tui.editor/latest/tutorial-example01-editor-basic)서 확인할 수 있다.\n"
  },
  {
    "path": "docs/ko/i18n.md",
    "content": "# 🌏 국제화 (i18n)\n\nTOASE UI Editor는 다양한 언어로 UI의 텍스트를 설정할 수 있는 기능을 제공한다. 기본적으로 제공되는 언어 파일이 있으며, 인스턴스를 만들 때 이 파일들을 가져와 사용할 언어를 설정할 수 있다.\n\n## 파일 구조\n\n### 소스 파일 (기여 용)\n\n[기본으로 지원하는 언어](#supported-languages) 외에 언어 파일을 추가하려면 아래 경로에 추가해야 한다. 기여 프로세스에 대한 자세한 내용은 [기여](#contributing) 섹션을 참조 바란다.\n\n```\n- tui.editor/apps/editor/src/\n  - i18n/\n    - en-us.ts\n    - ko-kr.ts\n    - ...\n```\n\n### 빌드 (메인테이너 용)\n\n```\n- tui.editor/apps/editor/dist/\n  - i18n/\n    - ko-kr.js\n    - ...\n```\n\n### npm에 배포된 파일 구조\n\n```\n- node_modules/@toast-ui/editor/dist/\n  - i18n/\n    - ko-kr.js\n    - ...\n```\n\n### CDN에 배포된 파일 구조\n\n```\n- uicdn.toast.com/editor/latest/\n  - i18n/\n    - ko-kr.js\n    - ko-kr.min.js\n    - ...\n```\n\n## 지원 언어\n\n아래는 TOAST UI Editor에서 제공하는 i18n 파일의 언어 코드 표다. 이 언어 코드는 [IETF 언어 태그](https://en.wikipedia.org/wiki/IETF_language_tag)를 기반으로 한다. 언어 파일을 가져오면 자동으로 언어 코드가 등록되고 옵션으로 사용할 언어를 설정할 수 있다.\n\n> 참고 : 기본 설정 언어는 영어이므로 `en-us.js` 언어 파일을 가져올 필요가 없다.\n\n| 언어명              | i18n 파일 | 등록 코드 |\n| -------------------------- | --------- | --------------- |\n| Arabic                     | ar.js     | `ar`            |\n| Chinese (S)                | zh-cn.js  | `zh-CN`         |\n| Chinese (T)                | zh-tw.js  | `zh-TW`         |\n| Croatian (Croatia)         | hr-hr.js  | `hr` \\| `hr-HR` |\n| Czech (Czech Republic)     | cs-cz.js  | `cs` \\| `cs-CZ` |\n| Dutch (Netherlands)        | nl-nl.js  | `nl` \\| `nl-NL` |\n| English (United States)    | en-us.js  | `en` \\| `en-US` |\n| Finnish (Finland)          | fi-fi.js  | `fi` \\| `fi-FI` |\n| French (France)            | fr-fr.js  | `fr` \\| `fr-FR` |\n| Galician (Spain)           | gl-es.js  | `gl` \\| `gl-ES` |\n| German (Germany)           | de-de.js  | `de` \\| `de-DE` |\n| Italian (Italy)            | it-it.js  | `it` \\| `it-IT` |\n| Japanese (Japan)           | ja-jp.js  | `ja` \\| `ja-JP` |\n| Korean (Korea)             | ko-kr.js  | `ko` \\| `ko-KR` |\n| Norwegian Bokmål (Norway)  | nb-no.js  | `nb` \\| `nb-NO` |\n| Polish (Poland)            | pl-pl.js  | `pl` \\| `pl-PL` |\n| Portuguese (Brazil)        | pt-br.js  | `pt` \\| `pt-BR` |\n| Russian (Russia)           | ru-ru.js  | `ru` \\| `ru-RU` |\n| Spanish (Castilian, Spain) | es-es.js  | `es` \\| `es-ES` |\n| Swedish (Sweden)           | sv-se.js  | `sv` \\| `sv-SE` |\n| Turkish (Turkey)           | tr-tr.js  | `tr` \\| `tr-TR` |\n| Ukrainian (Ukraine)        | uk-ua.js  | `uk` \\| `uk-UA` |\n\n## 언어 파일 가져오기\n\n사용할 언어 파일을 가져와 언어를 등록해야 한다. `${fileName}`은 [지원 언어](#supported-languages)의 'i18n 파일' 컬럼에 해당한다 (확장자 없이 사용할 수 있음).\n\n### ES 모듈\n\n```js\nimport '@toast-ui/editor/dist/i18n/${fileName}';\n```\n\n### CommonJS\n\n```js\nrequire('@toast-ui/editor/dist/i18n/${fileName}');\n```\n\n### CDN\n\nCDN에서는 각 언어 파일 별로 최소화 처리한 파일도 제공한다.\n\n```html\n<script src=\"https://uicdn.toast.com/editor/latest/i18n/${fileName}\"></script>\n```\n\n## 사용하기\n\n> 참고 : npm 사용을 기반으로 설명한다.\n\n### 사용 사례 1 : 기본 사용\n\n특정 언어를 설정하려면 `language` 옵션을 사용하여 에디터 인스턴스를 만들어야 한다. 이 옵션의 값은 [지원 언어](#supported-languages)의 '등록 코드' 컬럼에 해당한다. 기본값은 `en`과 `en-US`다.\n\n```js\nimport Editor from '@toast-ui/editor';\n\n// 1단계 : 언어 파일을 가져온다.\nimport '@toast-ui/editor/dist/i18n/ko-kr';\n\n// 2딘계 : 각 에디터에 언어를 설정한다.\nconst foo = new Editor({\n  // 기본 언어 사용(영어)\n  // ...\n});\n\nconst bar = new Editor({\n  // 다른 언어 사용(한국어)\n  // ...\n  language: 'ko-KR',\n});\n```\n\n### 사용 사례 2 : 언어 값 재정의\n\n`setLanguage` 정적 메서드를 사용하여 특정 언어 코드에 대한 값을 재정의할 수 있다. 기본값은 [여기](https://github.com/nhn/tui.editor/tree/master/apps/editor/src/i18n/en-us.ts)를 참조 바란다.\n\n```js\nimport Editor from '@toast-ui/editor';\n\n// 1단계 : 언어 파일을 가져온다.\nimport '@toast-ui/editor/dist/i18n/ko-kr';\n\n// 2단계 : 언어 값을 재정의한다.\nEditor.setLanguage('en-US', {\n  'Add row': '[Add Row]', // 기본값은 'Add row'이다.\n});\n\nEditor.setLanguage('ko-KR', {\n  'Add row': '[로우 추가]', // 기본값은 '행 추가'이다.\n});\n\n// 3단계 : 각 에디터에 언어를 설정한다.\nconst foo = new Editor({\n  // 기본 언어 사용(영어)\n  // ...\n});\n\nconst bar = new Editor({\n  // 다른 언어 사용(한국어)\n  // ...\n  language: 'ko-KR',\n});\n```\n\n### 사용 사례 3 : 새로운 언어 등록\n\n사용할 언어가 기본적으로 제공되지 않는 경우 `setLanguage` 정적 메서드를 사용하여 등록할 수 있다.\n\n```js\nimport Editor from '@toast-ui/editor';\n\n// 1단계 : 새로운 언어를 등록한다.\nEditor.setLanguage('en-GB', {\n  Markdown: '...',\n  WYSIWYG: '...',\n  // ...\n});\n\n// 2단계 : 새로 등록한 언어를 설정한다.\nconst bar = new Editor({\n  // ...\n  language: 'en-GB',\n});\n```\n\n## 기여\n\n다른 언어 파일을 제공하는 데 기여하고 싶다면 다음 절차를 따라야 한다.\n\n### 1단계\n\n저장소를 포크한 후 아래 경로에 언어 파일을 추가한다. 언어 파일의 이름은 `${languageCode}-${countryCode}.js` 규칙을 따라야 한다. `languageCode`와 `countryCode`는 반드시 소문자로 표기해야 한다. (예. `en-gb.ts`)\n\n> 참조 : [Nominatim/Country Codes](https://wiki.openstreetmap.org/wiki/Nominatim/Country_Codes)\n\n```\n- tui.editor/apps/editor/src/\n  - i18n/\n    - en-us.ts\n    - ko-kr.ts\n    - ...\n```\n\n### 2단계\n\n[이 파일](https://github.com/nhn/tui.editor/tree/master/apps/editor/src/i18n/en-us.ts)을 참조하여 `setLanguage` 메서드를 호출할 때 사용되는 각 매개 변수 값을 작성한다.\n\n첫 번째 매개 변수는 등록할 언어 파일에 매핑되는 코드 값이다. 코드 값은 [`${languageCode}-${countryCode}` 컨벤션](https://en.wikipedia.org/wiki/IETF_language_tag)을 따른다. `languageCode`는 소문자, `countryCode`는 대문자여야 한다.\n\n```js\n// th-th.js\n// ...\n\nEditor.setLanguage('th-TH', {\n  Markdown: '...',\n  WYSIWYG: '...',\n  // ...\n});\n```\n\n다음 조건이 충족되면 국가 코드를 제외한 언어 코드를 추가할 수 있다.\n\n> IETF 언어 태그 참조 : 언어 태그에 구별되는 정보를 추가하지 않을 경우 지역 하위 태그를 생략하는 것이 좋다. 예를 들어, 스페인어는 라틴어일 것이기 때문에 es-latn보다 es가 더 선호되고, 일본에서 사용되는 일본어는 다른 곳에서 사용되는 일본어와 크게 다르지 않기 때문에 ja-JP보다 ja가 더 선호된다.\n>\n> 모든 언어 영역이 유효한 지역의 하위 태그로 표현될 수 있는 것은 아니다: 주 언어의 하위 지역 방언은 하위 태그로 등록된다. 예를 들어, 카탈루냐어의 발렌시아 방언에 대한 발렌시아 하위 태그는 접두사 ca로 언어 하위 태그 에 등록된다. 이 방언은 스페인에서 거의 독점적으로 사용되기 때문에, 일반적으로 지역 하위 태그 ES는 생략할 수 있다.\n\n```js\n// th-th.js\n// ...\n\nEditor.setLanguage(['th', 'th-TH'], {\n  Markdown: '...',\n  WYSIWYG: '...',\n  // ...\n});\n```\n\n## 예제\n\n예제는 [여기](https://nhn.github.io/tui.editor/latest/tutorial-example16-i18n)서 확인할 수 있다.\n"
  },
  {
    "path": "docs/ko/plugin.md",
    "content": "# 🧩 플러그인\n\nTOAST UI Editor(이하 '에디터'라고 명시)는 기본으로 지원하지 않는 기능들을 플러그인으로 제공한다. 에디터에서 제공하는 플러그인은 현재 5개이며, 추후 자주 사용되는 기능은 더 추가될 수 있다.\n\n| 플러그인 명 | 패키지 명 | 설명 |\n| --- | --- | --- |\n| [`chart`](https://github.com/nhn/tui.editor/tree/master/plugins/chart) | [`@toast-ui/editor-plugin-chart`](https://www.npmjs.com/package/@toast-ui/editor-plugin-chart) | 차트를 렌더링하기 위한 플러그인 |\n| [`code-syntax-highlight`](https://github.com/nhn/tui.editor/tree/master/plugins/code-syntax-highlight) | [`@toast-ui/editor-plugin-code-syntax-highlight`](https://www.npmjs.com/package/@toast-ui/editor-plugin-code-syntax-highlight) | 코드 하이라이팅을 위한 플러그인 |\n| [`color-syntax`](https://github.com/nhn/tui.editor/tree/master/plugins/color-syntax) | [`@toast-ui/editor-plugin-color-syntax`](https://www.npmjs.com/package/@toast-ui/editor-plugin-color-syntax) | 컬러피커 사용을 위한 플러그인 |\n| [`table-merged-cell`](https://github.com/nhn/tui.editor/tree/master/plugins/table-merged-cell) | [`@toast-ui/editor-plugin-table-merged-cell`](https://www.npmjs.com/package/@toast-ui/editor-plugin-table-merged-cell) | 병합 테이블 셀을 사용하기 위한 플러그인 |\n| [`uml`](https://github.com/nhn/tui.editor/tree/master/plugins/uml) | [`@toast-ui/editor-plugin-uml`](https://www.npmjs.com/package/@toast-ui/editor-plugin-uml) | UML 사용을 위한 플러그인 |\n\n## 플러그인 설치 및 사용\n\n각 플러그인은 npm을 통해 설치하거나 CDN 형태로 사용할 수 있다.\n\n### 패키지 매니저(npm)를 통한 설치\n\nCLI를 사용하여 각 플러그인을 설치할 수 있다. 설치할 플러그인의 이름을 아래의 `${pluginName}`에 작성하여 설치한다. 예를 들어 `chart` 플러그인을 설치할 경우 `npm install @toast-ui/editor-plugin-chart`로 설치한다.\n\n```sh\n$ npm install --save @toast-ui/editor-plugin-${pluginName} \n$ npm install --save @toast-ui/editor-plugin-${pluginName}@<version>\n```\n\nnpm을 통해 설치할 경우 아래처럼 `node_modules`에 설치된다.\n\n```\n- node_modules/\n   ├─ @toast-ui/editor-plugin-${pluginName}\n   │     ├─ dist/\n   │     │    ├─ toastui-editor-plugin-${pluginName}.js\n   │     │    ├─ ...\n```\n\n설치한 플러그인은 모듈 포맷에 따라 아래처럼 가져올 수 있다.\n\n- ES 모듈\n\n```js\nimport pluginFn from '@toast-ui/editor-plugin-${pluginName}';\n```\n\n- CommonJS\n\n```js\nconst pluginFn = require('@toast-ui/editor-plugin-${pluginName}');\n```\n\n예를 들어 `chart` 플러그인은 다음과 가져올 수 있다.\n\n```js\nimport chart from '@toast-ui/editor-plugin-chart';\n```\n\n### CDN을 통한 설치\n\n각 플러그인은 [NHN Cloud](https://www.toast.com)에서 제공하는 CDN을 통해서도 사용할 수 있다.\n\n```html\n...\n<body>\n  ...\n  <script src=\"https://uicdn.toast.com/editor-plugin-${pluginName}/latest/toastui-editor-plugin-${pluginName}.min.js\"></script>\n</body>\n...\n```\n\n특정 버전을 사용하려면 url 경로에서 `latest` 대신 버전을 명시하면 된다.\n\nCDN 디렉터리의 구조는 다음과 같다.\n\n```\n- uicdn.toast.com/\n   ├─ editor-plugin-${pluginName}/\n   │     ├─ latest/\n   │     │    ├─ toastui-editor-plugin-${pluginName}.js\n   │     │    └─ ...\n   │     ├─ 3.0.0/\n   │     │    └─ ...\n```\n\n> 참조: 각 플러그인의 CDN 파일은 상황에 따라 모든 의존성을 포함하거나 다른 유형의 번들 파일을 제공한다. 자세한 내용은 각 플러그인 저장소를 확인바란다.\n\nCDN을 사용해 플러그인을 가져올 때는 `toastui.Editor.plugin`에 등록된 네임스페이스를 사용한다.\n\n```js\nconst pluginFn = toastui.Editor.plugin[${pluginName}];\n```\n\n예를 들어 `chart` 플러그인은 다음과 같이 가져온다.\n\n```js\nconst { chart } = toastui.Editor.plugin;\n```\n\n### 플러그인 사용\n\nES 모듈과 CDN에 따라 플러그인을 설치하고 가져오는 방법은 차이가 있지만, 사용하는 방법은 동일하다.\n\n가져온 플러그인을 사용하려면 에디터의 `plugins` 옵션에 플러그인 함수를 추가해야 한다. `plugins` 옵션의 타입은 `Array.<function>`이다.\n\n```js\nconst editor = new Editor({\n  // ...\n  plugins: [plugin]\n});\n```\n만약 `chart`와 `uml` 플러그인을 사용한다면, ES 모듈과 CDN 환경에서 각각 아래처럼 사용할 수 있다.\n\n- ES 모듈\n\n```js\nimport Editor from '@toast-ui/editor';\nimport chart from '@toast-ui/editor-plugin-chart';\nimport uml from '@toast-ui/editor-plugin-uml';\n\nconst editor = new Editor({\n  // ...\n  plugins: [chart, uml]\n});\n```\n\n- CDN\n\n```js\nconst { Editor } = toastui;\nconst { chart, uml } = Editor.plugin;\n\nconst editor = new Editor({\n  // ...\n  plugins: [chart, uml]\n});\n```\n\n플러그인 함수에서 사용할 옵션이 필요한 경우 `plugins` 옵션에 튜플 형태의 데이터를 추가하면 된다.\n\n```js\nconst pluginOptions = {\n  // ...\n};\n\nconst editor = new Editor({\n  // ...\n  plugins: [[plugin, pluginOptions]]\n});\n```\n\n## 플러그인 만들기\n\n기본적으로 제공되는 플러그인 외에도 사용자가 직접 플러그인 함수를 정의하여 사용할 수 있다.\n\n아래처럼 플러그인 함수를 정의하여 정해진 포맷에 맞는 객체를 반환한다.\n\n```ts\ninterface PluginInfo {\n  toHTMLRenderers?: HTMLConvertorMap;\n  toMarkdownRenderers?: ToMdConvertorMap;\n  markdownPlugins?: PluginProp[];\n  wysiwygPlugins?: PluginProp[];\n  wysiwygNodeViews?: NodeViewPropMap;\n  markdownCommands?: PluginCommandMap;\n  wysiwygCommands?: PluginCommandMap;\n  toolbarItems?: PluginToolbarItem[];\n}\n\nconst pluginResult: PluginInfo = {\n  // ...\n}\n\nfunction customPlugin() {\n  // ...\n  return pluginResult;\n}\n```\n\n다른 플러그인과 마찬가지로 에디터의 `plugins` 옵션을 통해 직접 정의한 플러그인 함수를 추가하여 사용할 수 있다.\n\n```js\nconst editor = new Editor({\n  // ...\n  plugins: [customPlugin]\n});\n```\n\n### 플러그인 반환 객체\n\n플러그인에서 반환하는 객체의 프로퍼티에 대해 알아보겠다. 아래처럼 총 8개의 프로퍼티가 존재하며, 커스터마이징을 원하는 프로퍼티만 정의하여 반환한다.\n\n```ts\ninterface PluginInfo {\n  toHTMLRenderers?: HTMLConvertorMap;\n  toMarkdownRenderers?: ToMdConvertorMap;\n  markdownCommands?: PluginCommandMap;\n  wysiwygCommands?: PluginCommandMap;\n  toolbarItems?: PluginToolbarItem[];\n  markdownPlugins?: PluginProp[];\n  wysiwygPlugins?: PluginProp[];\n  wysiwygNodeViews?: NodeViewPropMap;\n}\n```\n\n#### toHTMLRenderers\n\n`toHTMLRenderers` 객체는 에디터의 마크다운 프리뷰에서 렌더링될 때 또는 마크다운 에디터에서 위지윅 에디터로 컨버팅될 때 요소의 렌더링 결과를 변경할 수 있다. 에디터의 [customHTMLRenderer](https://github.com/nhn/tui.editor/blob/master/docs/ko/custom-html-renderer.md) 옵션과 동일하다.\n\n**toMarkdownRenderers**\n\n`toMarkdownRenderers` 객체는 위지윅 에디터에서 마크다운 에디터로 컨버팅될 때 변환되는 마크다운 텍스트를 재정의할 수 있다. `toMarkdownRenderers` 객체에 정의하는 함수는 `nodeInfo`와 `context` 두 가지 매개변수를 가진다.\n\n* `nodeInfo`: 컨버팅 대상이 되는 위지윅 노드의 정보이다.\n  * `node`: 대상 노드에 대한 정보가 담겨 있다.\n  * `parent`: 대상 노드의 부모 노드 정보가 담겨 있다.\n  * `index`: 대상 노드가 몇 번째 자식인지 알 수 있다.\n* `context`: 노드 정보 외에 컨버팅에 필요한 정보들이 담겨있다.\n  * `entering`: 해당 노드에 최초 방문인지, 자식 노드의 순회를 모두 끝내고 방문하는 것인지 알 수 있다.\n  * `origin`: 기존 컨버팅 함수의 동작을 실행하는 함수이다.\n\n`toMarkdownRenderers` 에 정의된 함수는 결과값으로 마크다운 텍스트로 변환할 때 필요한 토큰 정보들을 반환한다.\n\n```ts\ninterface ToMdConvertorReturnValues {\n  delim?: string | string[];\n  rawHTML?: string | string[] | null;\n  text?: string;\n  attrs?: Attrs;\n}\n```\n\n* `delim`: 마크다운 텍스트에서 사용할 기호를 정의한다. 마크다운 불릿 리스트의 `*`, `-`처럼 여러 기호로 변환될 수 있는 경우 사용한다.\n* `rawHTML`: 노드를 마크다운의 HTML 노드(HTML 문자열)로 변환할 경우 필요한 문자열이다.\n* `text`: 마크다운에서 보여줄 텍스트 정보이다.\n* `attrs`: 노드를 마크다운 텍스트로 변환할 때 사용할 속성 정보이다. 예를 들어 태스크 리스트의 체크 여부나 이미지 노드의 url 정보가 있다.\n\n**예시**\n```ts\nreturn {\n  toHTMLRenderers: {\n    // ...\n    tableCell(node: MergedTableCellMdNode, { entering, origin }) {\n      const result = origin!();\n\n      // ...\n      \n      return result;\n    },\n  },\n  toMarkdownRenderers: {\n    // ...\n    tableHead(nodeInfo) {\n      const row = (nodeInfo.node as ProsemirrorNode).firstChild;\n\n      let delim = '';\n\n      if (row) {\n        row.forEach(({ textContent, attrs }) => {\n          const headDelim = createTableHeadDelim(textContent, attrs.align);\n\n          delim += `| ${headDelim} `;\n\n          // ...\n        });\n      }\n      return { delim };\n    },\n  },\n};\n```\n\n위의 코드는 병합 테이블 플러그인의 예시이다. `toHTMLRenderers`에 정의된 `tableCell` 노드의 반환 결과는 마크다운 프리뷰와 위지윅 에디터로 컨버팅 시 사용되며, `toMarkdownRenderers`에 정의된 `tableHead` 노드의 텍스트 결과는 마크다운 에디터로 컨버팅 시 사용된다.\n\n![image](https://user-images.githubusercontent.com/37766175/121026660-4c80a380-c7e1-11eb-9d36-65425b6944da.gif)\n\n#### markdownCommands, wysiwygCommands\n\n플러그인에서는 `markdownCommands`, `wysiwygCommands` 옵션을 사용하여 마크다운, 위지윅 커맨드를 등록할 수 있다.\n\n각각의 커맨드 함수는 `payload`, `state`, `dispatch` 세 개의 매개변수를 가지며, 이를 사용하여 [Prosemirror](https://prosemirror.net/) 기반의 에디터 내부 동작을 제어할 수 있다.\n\n* `payload`: 커맨드 실행할 때 필요한 `payload`이다.\n* `state`: 에디터의 내부 상태를 나타내는 인스턴스로 [prosemirror-state](https://prosemirror.net/docs/ref/#state)와 동일하다.\n* `dispatch`: 커맨드 실행을 통해 에디터의 콘텐츠를 변경하고 싶은 경우 `dispatch` 함수를 실행해야 한다. Prosemirror의 [dispatch](https://prosemirror.net/docs/ref/#view.EditorView.dispatch) 함수와 동일하다.\n\n만약 커맨드 함수를 실행하여 에디터의 콘텐츠에 변경 사항이 발생한다면 반드시 `true`를 반환해야 한다. 반대의 경우에는 `false`를 반환해야 한다.\n\n```js\nreturn {\n  markdownCommands: {\n    myCommand: (payload, state, dispatch) => {\n      // ...\n      return true;\n    },\n  },\n  wysiwygCommands: {\n    myCommand: (payload, state, dispatch) => {\n      // ...\n      return true;\n    },\n  },\n};\n```\n\n위의 예시 코드처럼 플러그인에서 커맨드 함수를 정의하여 반환하면, 해당 커맨드를 에디터에서 사용할 수 있다.\n\n#### toolbarItems\n\n플러그인에서 에디터의 툴바 아이템을 등록할 수도 있다.\n\n```js\nreturn {\n  // ...\n  toolbarItems: [\n    {\n      groupIndex: 0,\n      itemIndex: 3,\n      item: toolbarItem,\n    },\n  ],\n};\n```\n\n위의 코드처럼 `toolbarItems` 배열에 어떤 아이템을 추가할지 설정할 수 있다. 각 옵션 객체는 `groupIndex`, `itemIndex`, `item` 세 가지 프로퍼티가 있으며 다음과 같다.\n\n* `groupIndex`: 툴바 아이템을 추가할 그룹의 인덱스를 지정한다.\n* `itemIndex`: 지정한 그룹 내에서 몇 번째로 추가할지 인덱스를 지정한다.\n* `item`: 추가할 툴바 아이템 요소를 지정한다. [툴바](https://github.com/nhn/tui.editor/blob/master/docs/ko/toolbar.md)의 툴바 커스터마이징에서 사용되는 객체와 동일한 형태이다.\n\n만약 예제 코드처럼 `toolbarItems` 옵션을 설정한다면, 1번째 툴바 그룹의 4번째 인덱스로 툴바 아이템을 등록할 것이다.\n\n#### markdownPlugins, wysiwygPlugins\n\n에디터는 내부적으로 Prosemirror를 사용한다. Prosemirror는 내부적으로 자체적인 플러그인 시스템을 제공한다. 에디터의 플러그인에서도 에디터의 내부 동작을 제어하기 위해 이러한 Prosemirror 플러그인을 직접 정의할 수 있다. 대부분의 경우 이러한 옵션은 필요없지만, 종종 필요한 경우가 있다. 예를 들어 코드 하이라이팅 플러그인에서는 위지윅 에디터의 `codeBlock`에 표시되는 코드를 하이라이팅할 때 사용한다.\n\n```js\nreturn {\n  wysiwygPlugins: [() => codeSyntaxHighlighting(context, prism)],\n};\n```\n\n이 옵션 객체를 사용하는 방법은 Prosemirror의 플러그인 정의 방법과 동일하니, [여기](https://prosemirror.net/docs/ref/#state.Plugin)를 참조 바란다.\n\n#### wysiwygNodeViews\n\n마크다운 에디터는 일반 텍스트이지만, 위지윅 에디터의 콘텐츠는 특정한 노드로 구성된다. 이러한 노드들은 `customHTMLRenderer` 옵션을 사용하여 속성이나 클래스를 추가하는 커스터마이징이 가능하다. 하지만 그 외에 이벤트를 등록하여 무언가를 제어하거나, 더 복잡한 상호 작용을 원하는 경우 `customHTMLRenderer` 옵션만으로는 한계가 있다. 이런 경우 플러그인의 `wysiwygNodeViews` 옵션을 사용하여 위지윅 에디터에서 렌더링되는 노드를 원하는 대로 커스터마이징할 수 있다. \n이 옵션 역시 대부분의 경우에는 필요가 없을 것이다. `wysiwygPlugins` 프로퍼티와 마찬가지로 `wysiwygNodeViews` 프로퍼티도 코드 하이라이팅 플러그인에서 사용된다.\n\n```js\nreturn {\n  wysiwygNodeViews: {\n    codeBlock: createCodeSyntaxHighlightView(registerdlanguages),\n  },\n};\n```\n\n이 옵션 객체를 사용하는 방법은 Prosemirror의 `nodeView` 정의 방법과 동일하니, [여기](https://prosemirror.net/docs/ref/#view.NodeView)를 참조 바란다.\n\n### 플러그인 함수의 `context` 매개변수\n플러그인 함수는 위에서 살펴본 다양한 프로퍼티를 정의하기 위해 `context` 매개변수로 필수적인 정보들을 사용할 수 있다. `context` 매개변수는 아래와 같은 정보들을 가지고 있다.\n\n* `eventEmitter`: 에디터의 `eventEmitter`와 동일하다. 에디터와의 통신을 위해 사용한다.\n* `usageStatistics`: 해당 플러그인을 `@toast-ui/editor`의 GA로 수집할지 결정한다.\n* `i18n`: 다국어 추가를 위한 인스턴스이다.\n* `pmState`: [prosemirror-state](https://prosemirror.net/docs/ref/#state)의 일부 모듈을 가진 프로퍼티이다.\n* `pmView`: [prosemirror-view](https://prosemirror.net/docs/ref/#view)의 일부 모듈을 가진 프로퍼티이다.\n* `pmModel`: [prosemirror-model](https://prosemirror.net/docs/ref/#model)의 일부 모듈을 가진 프로퍼티이다.\n"
  },
  {
    "path": "docs/ko/toolbar.md",
    "content": "# 🛠 툴바\n일반적으로 에디터에서는 단축키나 툴바를 사용하여 특정 텍스트나 노드를 입력할 수 있다. 특히 마크다운처럼 특정한 텍스트 문법이 존재하지 않는 위지윅 에디터에서는 대부분의 동작이 툴바를 통해 이뤄지기 때문에 툴바의 역할이 중요하다. TOAST UI Editor(이하 '에디터'라고 명시) 역시 기본 UI로 툴바를 제공하며 커스터마이징을 위한 옵션과 API도 제공한다.\n\n## 툴바 옵션\n에디터는 bold, italic, strike 등 총 16가지의 툴바를 기본으로 제공한다. 별도의 옵션을 지정하지 않았을 경우 기본 툴바 옵션은 아래와 같다.\n\n```js\nconst options = {\n  // ...\n  toolbarItems: [\n    ['heading', 'bold', 'italic', 'strike'],\n    ['hr', 'quote'],\n    ['ul', 'ol', 'task', 'indent', 'outdent'],\n    ['table', 'image', 'link'],\n    ['code', 'codeblock'],\n    ['scrollSync'],\n  ],\n}\n```\n\n예제 코드에서 볼 수 있듯이 에디터의 툴바 옵션은 2차원 배열 형태로 정의된다. 먼저 각각의 툴바 그룹을 배열 형태로 정의하며 그룹 내의 툴바 요소들을 배열의 원소로 지정한다. 각 요소들은 정의된 순서대로 그룹 내에서 렌더링되며, 툴바 그룹은 `|` 기호로 구분되어 렌더링 된다. \n\n![image](https://user-images.githubusercontent.com/37766175/120914229-a137f780-c6d7-11eb-8112-b14a48f8374f.png)\n\n만약 기본 툴바의 구성을 변경하고 싶다면 에디터의 `toolbarItems` 옵션을 지정하여 변경할 수 있다.\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  toolbarItems: [\n    ['heading', 'bold'],\n    ['ul', 'ol', 'task'],\n    ['code', 'codeblock'],\n  ],\n});\n```\n\n위의 예제 코드를 실행하면 아래처럼 렌더링된다.\n\n![image](https://user-images.githubusercontent.com/37766175/120914344-a47fb300-c6d8-11eb-85cd-857047e8e220.png)\n\n## 툴바 버튼 커스터마이징\n위에서 살펴본 예시는 사실 에디터의 기본 툴바 요소를 조합하는 것에 불과하다. 그렇다면 사용자가 직접 툴바 버튼을 만들어 추가하고 싶다면 어떻게 해야 할까? 이런 경우 크게 두 가지 형태의 옵션을 지정하여 커스터마이징할 수 있다.\n\n### 내장 버튼 요소 커스터마이징\n먼저 에디터에서 제공하는 툴바 버튼 UI를 그대로 사용하여 커스터마이징하는 방법이 있다. 이 방법은 에디터에 내장된 버튼을 툴바 요소를 렌더링하며, 여기서 버튼의 아이콘이나 툴팁, 팝업 동작만 재정의한다. 해당 옵션은 아래와 같은 인터페이스로 구성된다.\n\n| 이름 | 타입 | 설명 |\n| --- | --- | --- |\n| `name` | string | 툴바 요소의 고유한 이름이며, 필수로 지정해야 한다. | \n| `tooltip` | string | 옵셔널 값이며, 툴바 요소에 마우스를 올렸을 때 보여줄 툴팁 문자열을 정의한다. | \n| `text` | string | 옵셔널 값이며, 툴바 버튼 요소에 보여줄 텍스트가 있는 경우 정의한다. | \n| `className` | string | 옵셔널 값이며, 툴바 요소에 적용할 class를 정의한다. | \n| `style` | Object | 옵셔널 값이며, 툴바 요소에 적용할 style을 정의한다. | \n| `command` | string | 옵셔널 값이며, 툴바 버튼을 클릭했을 때 실행하고 싶은 명령을 지정한다. `popup` 옵션과는 서로 배타적인 관계이다. | \n| `popup` | PopupOptions | 옵셔널 값이며, 툴바 버튼을 클릭했을 때 팝업을 띄우고 싶은 경우 지정한다. `command` 옵션과는 서로 배타적인 관계이다. |\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  toolbarItems: [\n    [{\n      name: 'myItem',\n      tooltip: 'myItem',\n      command: 'bold',\n      text: '@',\n      className: 'toastui-editor-toolbar-icons',\n      style: { backgroundImage: 'none', color: 'red' }\n    }]\n  ],\n  // ...\n});\n```\n\n위의 예제 코드를 실행하면 옵션으로 설정한 `className`과 `style`이 적용된 툴바 요소가 생성된다. 생성된 요소는 `@` 텍스트 노드를 가지며,  클릭했을 때 `bold` 커맨드를 실행한다.\n\n![image](https://user-images.githubusercontent.com/37766175/120915118-ea3e7a80-c6dc-11eb-86cc-5229ed36c4e8.gif)\n\n### popup 옵션\n만약 버튼을 클릭했을 때 커맨드를 실행하는 것이 아니라 직접 정의한 팝업을 띄우고 싶을 수도 있을 것이다. 이런 경우 위에서 살펴본 `popup` 옵션을 사용하면 된다. `popup` 옵션의 인터페이스는 아래와 같다.\n\n| 이름 | 타입 | 설명 |\n| --- | --- | --- |\n| `body` | HTMLElement | 렌더링 될 팝업 DOM 노드를 정의한다. | \n| `className` | string | 옵셔널 값이며, 팝업 요소에 적용할 class를 정의한다. | \n| `style` | Object | 옵셔널 값이며, 팝업 요소에 적용할 style을 정의한다. | \n\n옵션으로 설정한 팝업 노드는 툴바를 클릭하였을 때 자동으로 화면에 나타나며, 다른 영역을 클릭했을 경우 자동으로 사라진다.\n\n에디터의 컬러피커 플러그인 코드를 조금 변형하여 살펴보겠다.\n\n```js\nconst container = document.createElement('div');\n// ...\nconst button = createApplyButton(i18n.get('OK'));\n\nbutton.addEventListener('click', () => {\n  // ...\n  eventEmitter.emit('command', 'color', { selectedColor });\n  eventEmitter.emit('closePopup');\n});\n\ncontainer.appendChild(button);\n\nconst colorPickerToolber = {\n  name: 'color',\n  tooltip: 'Text color',\n  className: 'some class',\n  popup: {\n    className: 'some class',\n    body: container,\n    style: { width: 'auto' },\n  },\n};\n```\n\n예제 코드에서는 팝업으로 띄울 요소를 `container`란 변수에 담아 지정하였다. 해당 요소는 버튼 요소를 가지며, 이 버튼을 클릭하였을 때 `color` 커맨드를 실행하고 팝업을 닫는다. 직접 정의한 팝업은 `eventEmitter`를 사용하여 에디터와 통신할 수 있다. 커맨드를 실행하기 위해서는 `command` 이벤트를 발생시키면 되고, 팝업을 닫고 싶을 경우 `closePopup` 이벤트를 발생시키면 된다.\n\n정의된 컬러피커 툴바 요소는 아래처럼 팝업과 잘 연동하여 동작하는 것을 볼 수 있다.\n\n![image](https://user-images.githubusercontent.com/37766175/120915630-b6b11f80-c6df-11eb-8094-b264ca9312a1.gif)\n\n\n## 툴바 요소 커스터마이징\n만약 위에서 설명한 것처럼 기본 버튼 UI를 사용하지 않고 툴바 요소를 만들고 싶다면 아래처럼 `el` 옵션을 지정해야 한다.\n\n```js\nconst myCustomEl = document.createElement('span');\n\nmyCustomEl.textContent = '😎';\nmyCustomEl.style = 'cursor: pointer; background: red;'\nmyCustomEl.addEventListener('click', () => {\n  editor.exec('bold');\n});\n\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  toolbarItems: [\n    [{\n      name: 'myItem',\n      tooltip: 'myItem',\n      el: myCustomEl,\n    }]\n  ],\n  // ...\n});\n```\n\n렌더링할 요소를 `el` 옵션으로 지정해야 한다. 이 경우 완전한 DOM 요소를 만들어 옵션으로 지정하는 것이기 때문에 클릭했을 때의 동작이나 style, class를 모두 직접 설정해야 한다.\n\n위의 예제 코드를 실행하면 아래와 같이 동작한다.\n\n![iamge](https://user-images.githubusercontent.com/37766175/120915883-3e4b5e00-c6e1-11eb-8f44-95e6d31f41e7.gif)\n\n## 툴바 상태 변경\n에디터에서는 현재 커서의 위치에 따라 어떤 노드인지 툴바 요소의 스타일로 활성화할 수 있다. 예를 들어, 커서가 굵은 텍스트를 표시하는 `strong` 노드에 위치한다면, 아래와 같이 `bold` 툴바 요소가 활성화된다.\n\n![image](https://user-images.githubusercontent.com/37766175/124843166-49d5c180-dfcc-11eb-9633-ae1e61d612ea.gif)\n\n\n위의 예시처럼 커스터마이징한 툴바 요소의 상태를 변경하고 싶다면 `state` 옵션을 지정해야 한다.\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  toolbarItems: [\n    [{\n      name: 'myItem',\n      tooltip: 'myItem',\n      command: 'bold',\n      text: '@',\n      className: 'toastui-editor-toolbar-icons',\n      style: { backgroundImage: 'none', color: 'red' },\n      // `strong` 노드에 위치할 경우 툴바 요소에 'active' 클래스가 추가된다.\n      state: 'strong',\n    }]\n  ],\n  // ...\n});\n```\n\n`state`에 따라 툴바 버튼이 활성화된다면 `active` 클래스가 추가되며, 이 클래스를 기준으로 원하는 스타일을 지정하면 된다.\n\n### state 목록\n아래의 state 값을 사용해야만 툴바 요소의 활성화 상태를 변경할 수 있다.\n* `heading`: 헤딩\n* `strong`: 볼드\n* `emph`: 이탤릭\n* `strike`: 스트라이크\n* `thematicBreak`: 수평 가로줄 \n* `blockQuote`: 인용문\n* `bulletList`: 순서가 없는 리스트\n* `orderedList`: 순서가 있는 리스트\n* `taskList`: task 리스트\n* `table`: 테이블\n* `code`: 인라인 코드\n* `codeBlock`: 코드 블럭\n\n### `onUpdated()` 옵션\n기본 버튼 UI를 사용하지 않고 `el` 옵션을 사용하여 툴바 요소를 만든 경우, `onUpdated` 옵션을 지정해야 상태를 변경할 수 있다. 에디터 내부에서 커스터마이징한 툴바 요소를 직접 조작하는 것은 한계가 있기 때문에 `onUpdated` 콜백 옵션을 제공한다.\n\n```js\nconst myCustomEl = document.createElement('span');\n\nmyCustomEl.textContent = '😎';\nmyCustomEl.style = 'cursor: pointer; background: red;'\nmyCustomEl.addEventListener('click', () => {\n  editor.exec('bold');\n});\n\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  toolbarItems: [\n    [{\n      name: 'myItem',\n      tooltip: 'myItem',\n      el: myCustomEl,\n      state: 'strong',\n      onUpdated({ active, disabled }) {\n        if (active) {\n          myCustomEl.style.background = 'green';\n        } else {\n          myCustomEl.style.background = '';\n        }\n      }\n    }]\n  ],\n  // ...\n});\n```\n\n`onUpdated()` 함수는 `active`, `disabled` 상태를 나타내는 객체를 매개변수로 전달한다. 이 매개변수를 사용하여 요소에 스타일링을 추가하거나 원하는 동작을 정의할 수 있다.\n\n## 예제\n\n예제는 [여기](https://nhn.github.io/tui.editor/latest/tutorial-example15-customizing-toolbar-buttons)서 확인할 수 있다."
  },
  {
    "path": "docs/ko/viewer.md",
    "content": "# 👀 뷰어\n\n## 뷰어는 무엇일까?\n\nTOASE UI Editor(이하 'Editor'라고 명시)는 에디터를 로딩하지 않고 _마크다운_ 콘텐츠를 보여줄 수 있도록 **뷰어**를 제공한다. 뷰어가 에디터보다 훨씬 **더 가볍다**.\n\n## 뷰어 사용하기\n\n뷰어를 사용하는 방법은 에디터와 유사하다.\n\n> 참고. [Getting Started](https://github.com/nhn/tui.editor/blob/master/docs/ko/getting-started.md)\n\n### 컨테이너 요소 추가\n\n뷰어가 생성될 컨테이너 요소를 추가한다.\n\n```html\n...\n<body>\n  <div id=\"viewer\"></div>\n</body>\n...\n```\n\n### 뷰어 생성자 함수 불러오기\n\n뷰어는 생성자 함수를 통해 인스턴스를 생성할 수 있다. 생성자 함수에 접근하기 위해서는 환경에 따라 접근할 수 있는 세 가지 방법이 존재한다.\n\n#### Node.js 환경에서의 모듈 사용\n\n- ES6 모듈\n\n```javascript\nimport Viewer from '@toast-ui/editor/dist/toastui-editor-viewer';\n```\n\n- CommonJS\n\n```javascript\nconst Viewer = require('@toast-ui/dist/toastui-editor-viewer');\n```\n\n#### 브라우저 환경에서의 namespace 사용\n\n```javascript\nconst Viewer = toastui.Editor;\n```\n\nCDN에서 뷰어는 다음처럼 사용한다.\n\n```html\n...\n<body>\n  ...\n  <script src=\"https://uicdn.toast.com/editor/latest/toastui-editor-viewer.js\"></script>\n</body>\n...\n```\n\n### CSS 파일 추가\n\n뷰어 사용을 위해 CSS파일을 추가해야 한다. Node.js 환경에서는 CSS 파일을 가져와 사용하며, CDN을 사용할 때는 html 파일에 CSS 파일 의존성을 추가하여 사용한다.\n\n#### Using in Node Environment\n\n- ES6 모듈\n\n```javascript\nimport '@toast-ui/editor/dist/toastui-editor-viewer.css';\n```\n\n- CommonJS\n\n```javascript\nrequire('@toast-ui/editor/dist/toastui-editor-viewer.css');\n```\n\n#### CDN 환경\n\n```html\n...\n<head>\n  ...\n  <link rel=\"stylesheet\" href=\"https://uicdn.toast.com/editor/latest/toastui-editor-viewer.min.css\" />\n</head>\n...\n```\n\n### 인스턴스 생성하기\n\n옵션과 함께 인스턴스를 생성하여 다양한 API를 호출할 수 있다.\n\n```js\nconst viewer = new Viewer({\n  el: document.querySelector('#viewer'),\n  height: '600px',\n  initialValue: '# hello'\n});\n```\n\n![viewer-01](https://user-images.githubusercontent.com/37766175/121862304-a3ccc980-cd35-11eb-92c8-02b0e6fcf3cf.png)\n\n대표적인 기본 옵션은 아래와 같다.\n\n- `height`: 에디터 영역의 높기 값. 문자열 값을 가진다. `300px` | `auto`\n- `initialValue`: 콘텐츠 초기값. 반드시 마크다운 문자열 형태여야 한다.\n\n더 많은 옵션은 [여기](https://nhn.github.io/tui.editor/latest/ToastUIEditorViewer)서 볼 수 있다.\n\n## 뷰어를 사용하는 다른 방법\n\n에디터에 이미 뷰어 기능이 포함되어 있으므로 에디터와 뷰어가 동시에 로드되지 않도록 주의해야 한다. 또한 `Editor.factory()` 정적 메서드를 사용하여 뷰어를 사용할 수 있다. 아래 코드처럼 `viewer` 옵션을 `true`로 설정하면 뷰어가 생성된다.\n\n```js\nimport Editor from '@toast-ui/editor';\n\nconst viewer = Editor.factory({\n  el: document.querySelector('#viewer'),\n  viewer: true,\n  height: '500px',\n  initialValue: '# hello'\n});\n```\n\n## 예제\n\n예제는 [여기](https://nhn.github.io/tui.editor/latest/tutorial-example04-viewer)서 확인할 수 있다.\n"
  },
  {
    "path": "docs/ko/widget.md",
    "content": "# 📱 위젯 노드\n\n에디터 내에서 특정 키를 입력할 때 인명 검색과 같은 팝업 창을 띄우거나, 멘션 형태의 일반 링크 노드를 특정한 위젯 노드로 보여주고 싶을 때가 있을 것이다. TOAST UI Editor(이하 '에디터'라고 명시)에서는 이러한 기능을 위해 옵션과 API를 제공한다.\n\n## 팝업 위젯\n\n에디터에서 콘텐츠를 편집하다 보면, 현재 커서의 위치에 검색 또는 추천 팝업을 띄우고 싶을 때가 있다. 이 때 `addWidget` API를 사용하여 원하는 DOM 노드를 에디터 상에 띄울 수 있다. 이 노드는 편집 중인 콘텐츠에는 영향을 미치지 않으며, **일시적으로 추가**된다. 즉, 텍스트를 입력하거나 포커스를 옮기면 사라진다. API의 시그니처는 아래와 같다.\n\n```ts\naddWidget(node: Node, style: WidgetStyle, pos?: EditorPos)\n```\n\n| 파라미터 | 타입 | 설명 |\n| --- | --- | --- |\n| `node` | Node | 위젯으로 추가할 DOM 노드 | \n| `style` | 'top' \\| 'bottom' | 위젯을 지정된 위치의 위에 추가할 지 아래에 추가할 지 결정한다. | \n| `pos` | EditorPos | 위젯이 추가될 위치를 지정한다. 옵셔널 값이며, 지정하지 않을 경우 현재 커서 위치에 위젯이 추가된다. | \n\n```js\nconst popup = document.createElement('ul');\n// ...\n\neditor.addWidget(popup, 'top');\n```\n\n위의 코드가 실행되면 아래처럼 `popup` 노드가 추가된다. \n\n![image](https://user-images.githubusercontent.com/37766175/120617182-d6a0d300-c494-11eb-8fb9-58926c60e8b7.png)\n\n만약 특정 키를 입력했을 때 위젯 노드를 띄우고 싶다면, 에디터의 `keyup` 이벤트와 연동해서 사용할 수 있다.\n\n```js\neditor.on('keyup', (editorType, ev) => {\n  if (ev.key === '@') {\n    const popup = document.createElement('ul');\n    // ...\n  \n    editor.addWidget(popup, 'top');\n  }\n})\n```\n\n### 인라인 위젯 노드\n\n일시적으로 특정 상황에 따라 팝업 위젯 노드를 추가하는 방법을 살펴보았다. 그렇다면 만약 팝업 위젯에서 특정 항목을 클릭하여 멘션 형태의 노드를 추가하고 싶다면 어떻게 할 수 있을까? \n마크다운 에디터는 텍스트 기반 에디터이기 때문에 이러한 멘션 노드를 추가할 수 없다. 위지윅 에디터에서도 내부적으로 별도의 멘션 노드를 기본으로 지원하지 않기 때문에 추가할 수 없다. \n에디터에서 멘션 노드와 같은 *인라인 위젯 노드*를 추가하고 싶은 사용자를 위해 `widgetRules` 옵션을 제공한다. 만약 텍스트가 `widgetRules` 옵션에 설정한 규칙에 맞는다면 해당 노드는 에디터에서 인라인 위젯 노드로 렌더링 된다. **인라인 위젯 노드는 팝업 위젯과는 다르게 콘텐츠로서 에디터에 삽입되며, 다른 노드의 위치에 영향을 준다**.\n\n```js\nconst reWidgetRule = /\\[(@\\S+)\\]\\((\\S+)\\)/;\n\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  widgetRules: [\n    {\n      rule: reWidgetRule,\n      toDOM(text) {\n        const rule = reWidgetRule;\n        const matched = text.match(rule);\n        const span = document.createElement('span');\n  \n        span.innerHTML = `<a class=\"widget-anchor\" href=\"${matched[2]}\">${matched[1]}</a>`;\n        return span;\n      },\n    },\n  ],\n});\n```\n\n예제 코드에서 볼 수 있듯이 `widgetRules`는 배열 형태로 각각의 규칙을 정의하며, 각 규칙은 `rule`, `toDOM`이라는 프로퍼티로 구성된다.\n\n* `rule`: 반드시 정규식 값이 와야하며, 이 정규식에 맞는 텍스트는 위젯 노드로 치환되어 렌더링된다.\n* `toDOM`: 렌더링될 위젯 노드의 DOM 노드를 정의한다.\n\n`widgetRules`의 규칙에 맞는 텍스트가 입력되면, 아래 이미지처럼 인라인 위젯 노드로 치환되어 렌더링된다.\n\n![image](https://user-images.githubusercontent.com/37766175/120621226-a6f3ca00-c498-11eb-9355-0275fd3bdbdb.gif)\n\n### `insertText()`, `replaceSelection()` API\n\n위젯 규칙에 맞는 텍스트를 직접 입력하여 인라인 위젯 노드 형태로 치환할 수도 있지만, 대부분의 경우는 팝업 위젯에서 특정 항목을 클릭하여 멘션 노드와 같은 인라인 위젯 노드를 삽입하고 싶을 것이다.\n\n이러한 경우 `insertText()`, `replaceSelection()` API를 사용하여 팝업 위젯의 항목을 클릭하였을 때 인라인 위젯 노드를 삽입할 수 있다.\n\n```js\nul.addEventListener('mousedown', (ev) => {\n  const text = ev.target.textContent.replace(/\\s/g, '').replace(/😎/g, '');\n  const [start, end] = editor.getSelection();\n\n  editor.replaceSelection(`[@${text}](${text})`, [start[0], start[1] - 1], end);\n});\n```\n\n예제 코드에서는 `@` 문자까지 포함하여 치환해야 하기 때문에 `getSelection()` API로 현재 커서 위치를 기준으로 계산한 후 `replaceSelection()` API를 호출하였다. 결과적으로 아래 이미지처럼 팝업 위젯의 항목을 클릭하였을 때 `@` 문자가 인라인 위젯 노드로 치환되는 것을 볼 수 있다.\n\n![image](https://user-images.githubusercontent.com/37766175/120624280-81b48b00-c49b-11eb-9896-432120c27389.gif)\n"
  },
  {
    "path": "docs/v3.0-migration-guide-ko.md",
    "content": "## 개요\n\n해당 문서는 TOAST UI Editor 3.0 버전 업데이트에 대한 마이그레이션 가이드로, 2.x 버전을 사용하는 사용자가 3.0 버전으로 업데이트할 때 필요한 모든 변경 사항을 기술한다.\n\nTOAST UI Editor(이하 '에디터'로 표기)는 3.0 버전에서 기존 [CodeMirror](https://codemirror.net/)와 squire, to-mark 등에 대한 의존성을 제거하고 Prosemirror를 이용하여 추상화 모델을 사용하는 에디터로 변경하는 작업을 진행하였다. 코어 모듈과 API, 플러그인 사용 방법 등이 모두 변경되었기 때문에 업데이트 시 마이그레이션 가이드의 내용을 잘 숙지하길 바란다. 목차는 다음과 같으며, 실제 적용 시에는 '변경 사항' 항목의 내용을 순서대로 진행하길 권장한다.\n\n## 목차\n\n- [변경 사항](#변경-사항)\n  1. [설치 및 사용 방법](#1-설치-및-사용-방법)\n  2. [툴바 커스터마이징](#2-툴바-커스터마이징)\n  3. [플러그인 정의](#3-플러그인-정의)\n  4. [API와 이벤트](#4-API와-이벤트)\n  5. [지원 브라우저 범위](#5-지원-브라우저-범위)\n- [제거된 기능](#제거된-기능)\n  1. [jQuery Wrapper 제거](#1-jQuery-Wrapper-제거)\n  2. [의존성 제거](#2-의존성-제거)\n  3. [제거된 API](#3-제거된-API)\n\n## 변경 사항\n\n### 1. 설치 및 사용 방법\n\n에디터 사용 방식은 기존 v2.x와 동일하게 [스코프드 패키지(Scoped package)](https://docs.npmjs.com/using-npm/scope.html)를 적용하여 다음과 같이 `@toast-ui/editor`로 패키지를 설치하여 사용한다. 아래는 npm 커맨드를 사용한 에디터 설치 예제이다.\n\n```sh\n$ npm install @toast-ui/editor\n$ npm install @toast-ui/editor@<version>\n```\n\n#### 사용 방법\n\n```js\nconst Editor = require('@toast-ui/editor'); /* CommonJS 방식 */\nimport Editor from '@toast-ui/editor'; /* ES6 모듈 방식 */\n```\n\n또한, v3.0에서는 에디터의 기본 UI를 사용하지 않고 별도로 UI를 구상하고 싶은 사용자를 위해 `EditorCore`란 모듈을 named export 형태로 제공한다. 이 모듈을 사용하면 마크다운 에디터와 프리뷰, 위지윅 에디터만 생성하며, 이를 `getEditorElements()` 메서드를 사용하여 원하는 UI에 에디터를 추가하여 사용할 수 있다. 툴바나 툴바 팝업, 스위치 탭과 같은 에디터 외부의 UI는 생성하지 않는다.\n\n```js\nimport { EditorCore } from '@toast-ui/editor'; /* ES6 모듈 방식 */\n\nconst editorCore = new EditorCore({\n  el\n  // ...\n});\n\nconst { mdEditor, mdPreview, wwEditor } = editorCore.getEditorElements();\n\n// ...\n```\n\n#### 번들 구조\n\nv3.0에서는 기존 v2.x의 번들 구조에 두 가지가 더 추가되었다.\n기존의 legacy 지원을 위한 번들과 cdn 번들 외에 ESM 번들이 추가로 제공된다. ESM 번들은 복잡한 모듈 호환 구문이 없기 때문에 더 가벼우며, 정적 분석으로 인한 트리 쉐이킹(Tree shaking)의 이점도 누릴 수 있다.\n두 번째로 다크 테마 지원을 위한 `theme/toastui-editor-dark.css` 파일이 추가되었다. 이에 대한 설명은 [다크 테마 추가](#-다크-테마-추가)에서 볼 수 있다.\n\nv3.0의 번들 구조는 다음과 같다.\n\n```\n- dist/\n   ├─ cdn/...\n   ├─ i18n/...\n   ├─ esm/\n   │    ├─ index.js\n   │    └─ index.js.map\n   ├─ theme/\n   │    └─ toastui-editor-dark.css\n   │\n   ├─ toastui-editor-only.css\n   ├─ toastui-editor-viewer.css\n   ├─ toastui-editor.css\n   ├─ toastui-editor.js\n   └─ toastui-editor-viewer.js\n```\n\n또한 v3.0에서 ESM 번들이 추가되며, package.json 파일도 이에 맞게 변경되었다. \n기존의 UMD 용 번들 파일은 main 필드에 정의되며, ESM 번들 파일은 exports 필드에 정의되었다.\n\n```json\n{\n  \"main\": \"dist/toastui-editor.js\",\n  \"module\": \"dist/esm/\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/esm/index.js\",\n      \"require\": \"./dist/toastui-editor.js\"\n    },\n    \"./viewer\": {\n      \"import\": \"./dist/esm/indexViewer.js\",\n      \"require\": \"./dist/toastui-editor-viewer.js\"\n    }\n  }\n}\n```\n\n#### 다크 테마 추가\nv3.0에서는 다크 테마가 추가되었다. 다크 테마를 적용하고 싶은 경우 `theme/toastui-editor-dark.css`를 추가한 후, 에디터의 `theme` 옵션을 `dark`로 설정하여 사용한다. 현재 v3.0에서는 다크 테마만 지원하지만, 사용자가 여러 테마를 혼합하여 사용하거나 추후 다른 테마를 지원하기 위해 `theme` 옵션을 추가하였다.\n\n```js\nimport Editor from '@toast-ui/editor';\nimport '@toast-ui/editor/dist/toastui-editor.css';\nimport '@toast-ui/editor/dist/theme/toastui-editor-dark.css';\n\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  previewStyle: 'vertical',\n  height: '500px',\n  initialValue: content,\n  theme: 'dark',\n});\n```\n\n![image](https://user-images.githubusercontent.com/37766175/120954138-73ab8680-c789-11eb-8445-87bf15842482.png)\n\n#### 의존성 정보 변경\n\n에디터 3.0에서는 v2.x에서 사용하던 의존성 모듈들이 제거되었다. 만약 CDN 환경에서 개발하고 있다면, v2.x에서 사용하던 [CodeMirror](https://codemirror.net/) 의존성 코드는 더 이상 필요없으니 제거해야 한다.\n3.0에서는 Prosemirror와 관련된 의존성 모듈들이 추가되었지만, 이는 CDN 번들에 모두 포함되기 때문에 별도로 추가할 필요가 없다. \n\n**v2.0**\n\n```html\n<head>\n  ...\n  <link\n    rel=\"stylesheet\"\n    href=\"https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/codemirror.min.css\"\n  />\n  <link rel=\"stylesheet\" href=\"https://uicdn.toast.com/editor/latest/toastui-editor.css\" />\n  ...\n</head>\n<body>\n  ...\n  <script src=\"https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js\"></script>\n  ...\n</body>\n```\n\n**v3.0**\n\n```html\n<head>\n  ...\n  <link rel=\"stylesheet\" href=\"https://uicdn.toast.com/editor/latest/toastui-editor.css\" />\n  ...\n</head>\n<body>\n  ...\n  <script src=\"https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js\"></script>\n  ...\n</body>\n```\n\n### 2. 툴바 커스터마이징\n\n`toolbarItems` 옵션이 기존 v2.x에 비해 더 간결하고 선언적으로 변경되었다. v3.0에서는 각 툴바 아이템과 툴바 그룹을 **2차원 배열** 형태의 옵션으로 정의한다. 이 방식은 그룹을 구분하기 위해 `divider`라는 불필요한 요소를 옵션으로 넘겨 정의하던 기존 방식보다 훨씬 간결하고 명확하다.\n\n**v2.0**\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  toolbarItems: [\n    'heading',\n    'bold',\n    'italic',\n    'strike',\n    // 그룹을 구분짓기 위해 옵션에 divider 요소를 추가해야 했다.\n    'divider',\n    'hr',\n    'quote',\n    'divider',\n    // ...\n  ],\n  // ...\n});\n```\n\n**v3.0**\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  toolbarItems: [\n    ['heading', 'bold', 'italic', 'strike'],\n    ['hr', 'quote'],\n    // ...\n  ],\n  // ...\n});\n```\n\n위의 예제 코드를 보면, v3.0의 코드가 더 간결하고 그룹이 어떻게 나뉘는지 훨씬 쉽게 구분할 수 있음을 알 수 있다.\n\n#### 커스터마이징\n\n툴바 아이템을 커스터마이징하는 방법도 변경되었다. v2.x에서는 툴바 아이템을 클릭하여 팝업을 띄우거나 닫을 때 에디터의 `eventManager`나 다른 UI 인스턴스에 대한 결합도가 상당히 높았다. 이는 사용자 또는 플러그인에서 커스터마이징할 때 에디터 내부 동작 방식에 대한 지식을 강요하기 때문에 사용하기 어렵고, 불필요한 제어 코드들을 생산하였다. v3.0에서는 이러한 결합도를 낮추기 위해 UI 제어를 위한 코드를 모두 내부로 캡슐화하였고, 사용자는 옵션만 설정하여 툴바 아이템을 커스터마이징할 수 있게 변경하였다.\n\n**v2.0**\n\n```js\nconst popup = editor.getUI().createPopup({\n  header: false,\n  title: null,\n  content: colorPickerContainer,\n  className: 'tui-popup-color',\n  target: editor.getUI().getToolbar().el,\n  css: {\n    width: 'auto',\n    position: 'absolute'\n  }\n});\n\neditor.eventManager.listen('focus', () => {\n  popup.hide();\n\n  // ...\n});\n\neditor.eventManager.listen('colorButtonClicked', () => {\n  // ...\n});\n\neditor.eventManager.listen('closeAllPopup', () => {\n  // ...\n});\n```\n\n위의 코드는 v2.x에서 컬러피커 플러그인의 툴바 커스터마이징 예시이다. 툴바의 팝업 동작을 제어하기 위해 `editor.getUI().createPopup()`, `editor.getUI().getToolbar()`와 같은 에디터 내부 구조에 종속적인 API를 사용해야 한다. 내부 구현에 대한 이러한 의존성은 유연한 커스터마이징을 더 어렵게 만든다. API 뿐만이 아니다. 팝업을 제어하기 위해 `eventManager`에 여러 이벤트를 등록하여 코드를 수정해야 한다.\n\n\n**v3.0**\n\n```js\nconst popup = {\n  name: 'color',\n  tooltip: 'Text color',\n  className: 'toastui-editor-toolbar-icons color',\n  popup: {\n    className: 'toastui-editor-popup-color',\n    body: colorPickerContainer,\n    style: { width: 'auto' },\n  },\n};\n```\n\n몇 가지 코드가 생략되었지만, 3.0에서는 간단한 옵션 설정으로 팝업 UI를 생성하고 제어할 수 있다. 기존처럼 내부 UI에 모듈에 대해 알 필요없이 `popup` 옵션 객체에 `className`, `style`, `body` 프로퍼티만 정의하면, 툴바 버튼을 눌렀을 때 팝업을 띄울 수 있다. 툴바 커스터마이징에 대한 더 자세한 설명은 [여기](https://github.com/nhn/tui.editor/tree/master/docs/ko/toolbar.md)를 참조 바란다.\n\n![image](https://user-images.githubusercontent.com/37766175/120915630-b6b11f80-c6df-11eb-8094-b264ca9312a1.gif)\n\n### 3. 플러그인 정의\n\nv3.0의 가장 큰 변경점은 플러그인을 정의하는 방식이다. 기존 2.x에서는 플러그인 역시 앞서 살펴본 툴바 커스터마이징처럼 에디터 내부 모듈에 대한 의존성이 굉장히 높았다. 특히 플러그인은 마크다운 에디터, 위지윅 에디터, 컨버터 등 에디터 내부 인스턴스의 동작을 더욱 깊숙이 알아야 한다. 3.0버전에서는 이 문제를 개선하기 위해 명확한 옵션을 주입하여 각각의 기능을 커스터마이징하는 형태로 변경되었다. 이 가이드에서는 옵션의 형태만 간략하게 설명할 것이며, 플러그인을 정의하는 자세한 방법은 [여기](https://github.com/nhn/tui.editor/tree/master/docs/ko/plugin.md)서 볼 수 있다.\n\n#### 커맨드 등록\n\n플러그인에서는 `markdownCommands`, `wysiwygCommands` 옵션을 사용하여 마크다운, 위지윅 커맨드를 등록할 수 있다.\n\n```js\nreturn {\n  markdownCommands: {\n    myCommand: (payload, state, dispatch) => {\n      // ...\n    },\n  },\n  wysiwygCommands: {\n    myCommand: (payload, state, dispatch) => {\n      // ...\n    },\n  },\n};\n```\n\n각각의 커맨드는 `payload`, `state`, `dispatch` 세가지 인자를 받으며, 이를 사용하여 Prosemirror 기반의 에디터 내부 동작을 제어할 수 있다. 이 방식 역시 Prosemirror의 동작을 알아야 한다는 단점이 있다. 하지만, 앞으로 에디터 자체적으로 여러 가지 기본 커맨드를 제공할 것이기 때문에 직접적으로 이러한 내부 동작을 재정의할 일은 많지 않을 것이다.\n\n#### 컨버팅\n\n특정 요소가 마크다운 프리뷰에서 렌더링될 때 또는 마크다운 에디터에서 위지윅 에디터로 컨버팅할 때 요소의 렌더링 결과를 변경할 수 있다. 반대로 위지윅 에디터에서 마크다운 에디터로 컨버팅할 때 변환되는 마크다운 텍스트를 재정의할 수 있다.\n`toHTMLRenderers`, `toMarkdownRenderers` 옵션을 사용하여 마크다운 => 위지윅, 위지윅 => 마크다운 컨버팅 시 수행할 동작을 추가할 수 있다.\n\n```ts\nreturn {\n  toHTMLRenderers: {\n    // ...\n    tableCell(node: MergedTableCellMdNode, { entering, origin }) {\n      const result = origin!();\n\n      // ...\n      \n      return result;\n    },\n  },\n  toMarkdownRenderers: {\n    // ...\n    tableHead(nodeInfo) {\n      const row = (nodeInfo.node as ProsemirrorNode).firstChild;\n\n      let delim = '';\n\n      if (row) {\n        row.forEach(({ textContent, attrs }) => {\n          const headDelim = createTableHeadDelim(textContent, attrs.align);\n\n          delim += `| ${headDelim} `;\n\n          // ...\n        });\n      }\n      return { delim };\n    },\n  },\n};\n```\n\n위의 코드는 병합 테이블 플러그인의 예시이다. `toHTMLRenderers`에 정의된 `tableCell` 노드의 반환 결과는 마크다운 프리뷰와 위지윅 에디터로 컨버팅 시 사용되며, `toMarkdownRenderers`에 정의된 `tableHead` 노드의 텍스트 결과는 마크다운 에디터로 컨버팅 시 사용된다. 각 에디터로 컨버팅 시 수행될 동작을 노드별 옵션으로 명확하게 설정할 수 있다.\n\n#### 툴바 아이템 등록\n\n플러그인에서 툴바 아이템을 등록하는 방식 역시 변경되었다. 앞서 설명한 툴바 커스터마이징 옵션과 유사하며, 어느 그룹에 추가될지 인덱스 정보만 추가로 설정하면 된다.\n\n\n```ts\nreturn {\n  // ...\n  toolbarItems: [\n    {\n      groupIndex: 0,\n      itemIndex: 3,\n      item: toolbarItem,\n    },\n  ],\n};\n```\n\n위의 코드처럼 `toolbarItems` 배열에 어떤 아이템을 추가할지 설정할 수 있다. 각 옵션 객체는 `groupIndex`, `itemIndex`, `item` 세가지 프로퍼티가 있으며 다음과 같은 역할을 한다.\n\n* `groupIndex`: 툴바 아이템을 추가할 그룹의 인덱스를 지정한다.\n* `itemIndex`: 지정한 그룹 내에서 몇 번째로 추가할지 인덱스를 지정한다.\n* `item`: 추가할 툴바 아이템 요소를 지정한다.\n\n만약 예제 코드처럼 `toolbarItems` 옵션을 설정한다면, 1번째 툴바 그룹의 4번째 인덱스로 툴바 아이템을 등록할 것이다.\n\n이외에도 마크다운, 위지윅 에디터의 Prosemirror 플러그인을 등록하는 방법, `eventEmitter`로 에디터와 플러그인 간의 통신 등 몇 가지 여기서 소개하지 않는 옵션들이 있다. 해당 내용은 [플러그인 활용 가이드](https://github.com/nhn/tui.editor/tree/master/docs/ko/plugin.md)를 자세히 읽어보길 권장한다.\n\n\n### 4. API와 이벤트\n\n3.0에서 변경된 API의 시그니처와 이벤트 명은 다음과 같다.\n\n#### 커맨드\n등록하려는 커맨드의 옵션을 이름과 핸들러로 이루어진 객체 형태가 아닌 각각의 인자로 넘겨주는 형태로 변경되었다. 또한 커맨드를 실행하는 메서드의 인자 형태도 변경되었다.\n\n**v2.x**\n\n| 메서드 시그니처              | 반환 타입         |\n| ----------------- | ------------ |\n| `addCommand(type: string, props: { name: string; exec: Command }` | `void` |\n| `exec(name: string, ...args: any[]`) | `void` |\n\n**v3.0**\n\n| 메서드 시그니처              | 반환 타입         |\n| ----------------- | ------------ |\n| `addCommand(type: string, name: string, command: CommandFn)` | `void` |\n| `exec(name: string, payload?: Object)` | `void` |\n\n#### 텍스트 조작 API\n기존에는 `getTextObject()` API를 사용하여 에디터 내에 텍스트를 삽입하거나 교체하였다. 하지만 `getTextObject()` API가 반환하는 인스턴스의 구조를 알아야 한다는 단점이 있었다. 3.0에서는 해당 API를 삭제하고 텍스트 삽입, 교체, 삭제 동작을 수행하는 API를 별도로 추가하였다.\n\n**v2.x**\n\n`TextObject`의 인터페이스\n\n```ts\ninterface TextObject {\n  setRange(range): void;\n  setEndBeforeRange(range): void;\n  expandStartOffset(): void;\n  expandEndOffset(): void;\n  getTextContent(): string;\n  replaceContent(content) : void;\n  deleteContent(): void;\n  peekStartBeforeOffset(offset): Range;\n}\n```\n\n**v3.0**\n\n| 메서드 시그니처              | 반환 타입         | 비고        |\n| ----------------- | ------------ | ------------ |\n| `replaceSelection(text: string, start?: EditorPos, end?: EditorPos) ` | `void` | 특정 범위의 텍스트를 교체한다. 범위를 지정하지 않을 경우 현재 에디터의 셀렉션 범위 내의 텍스트를 수정한다. |\n| `deleteSelection(start?: EditorPos, end?: EditorPos)` | `void` | 특정 범위의 텍스트를 삭제한다. 범위를 지정하지 않을 경우 현재 에디터의 셀렉션 범위 내의 텍스트를 삭제한다. |\n| `getSelectedText(start?: EditorPos, end?: EditorPos)` | `string` | 특정 범위의 텍스트를 가져온다. 범위를 지정하지 않을 경우 현재 에디터의 셀렉션 범위 내의 텍스트를 가져온다. |\n\n위의 API들의 위치 정보(`EditorPos`)는 마크다운 에디터, 위지윅 에디터에 따라 다르며, 아래와 같은 형태이다. 이는 마크다운과 위지윅의 위치 계산 정보 방법이 다르기 때문이다. 마크다운은 라인을 기준으로 계산하여, 위지윅은 문서 시작을 기준으로 오프셋을 계산한다.\n\n```ts\n// 마크다운 위치 정보\ntype EditorPos = [line: number, charactorOffset: number];\n// 위지윅 위치 정보\ntype EditorPos = number; // 오프셋\n```\n\n#### 인스턴스 생성 옵션 및 메서드 변경\n\n표기가 잘못되거나 기능이 명확하지 않은 옵션과 메서드가 변경되었다.\n\n* 인스턴스 생성 옵션 \n\n| v2 | v3 |\n| --- | --- |\n| `linkAttribute` | `linkAttributes` |\n\n* 인스턴스 메서드\n\n| v2 | v3 |\n| --- | --- |\n| `setHtml` | `setHTML` |\n| `getHtml` | `getHTML` |\n| `minHeight` | `setMinHeight`, `getMinHeight` |\n| `height` | `setHeight`, `getHeight` |\n| `getRange` | `getSelection` |\n| `remove` | `destroy` |\n\n#### 이벤트 명 변경\n\n몇몇 이벤트 명도 더 명확한 의미를 전달을 위해 변경되었다.\n\n| v2 | v3 |\n| --- | --- |\n| `stateChange` | `caretChange` |\n| `convertorAfterMarkdownToHtmlConverted` | `beforePreviewRender` |\n| `convertorAfterHtmlToMarkdownConverted` | `beforeConvertWysiwygToMarkdown` |\n\n### 5. 지원 브라우저 범위\n\nv3.0부터 지원 브라우저 범위가 **인터넷 익스플로러 11 이상**으로 변경된다. 이전 버전에서는 인터넷 익스플로러 10 이상을 지원하였으나 낮은 브라우저 점유율 및 Prosemirror 코어 모듈 사용을 위해 지원 범위를 변경하게 되었다.\n\n## 제거된 기능\n\n### 1. jQuery Wrapper 제거\n\nv3.0부터는 jQuery Wrapper가 제거되었다. jQuery에서 사용을 원하는 경우 직접 `@toast-ui/editor` 패키지를 랩핑하여 사용해야 한다.\n\n### 2. 의존성 제거\n\n기존의 CodeMirror, squire, to-mark 의존성이 모두 제거되었기 때문에 공식적인 API를 통해서든 비공식적 방법이든 해당 모듈에 직접 접근하여 조작하던 코드는 더 이상 동작하지 않을 것이다. 대부분의 필요한 기능은 에디터 인스턴스 API로 추가되었으니 해당 API를 사용하길 권장한다.\n\n**v2.x**\n\n```js\nconst editor = new Editor(/* */);\n\nconsole.log(editor.getCodeMirror()); // CodeMirror 인스턴스\nconsole.log(editor.getSquire()); // squire 인스턴스\n```\n\n**v3.0**\n\n```js\nconst editor = new Editor(/* */);\n\nconsole.log(editor.getCodeMirror()); // Uncaught TypeError\nconsole.log(editor.getSquire()); // Uncaught TypeError\n```\n\n### 3. 제거된 API\n\n마지막으로, 에디터 v3.0에서 제거된 API를 정리한 목록이다.\n\n#### 정적 속성\n\n| 이름                  | 타입               |\n| --------------------- | ------------------ |\n| `isViewer`            | `{boolean}` |\n| `codeBlockManager`    | `{CodeBlockManager}` |\n| `WwCodeBlockManager`  | `{Class.<WwCodeBlockManager>}` |\n| `WwTableManager`      | `{Class.<WwTableManager>}` |\n| `WwTableSelectionManager` | `{Class.<WwTableSelectionManager>}` |\n| `CommandManager` | `{Class.<CommandManager>}` |\n\n#### 정적 메서드\n\n| 이름              | 타입         |\n| ----------------- | ------------ |\n| `getInstances` | `{function}` |\n\n#### 인스턴스 생성 옵션\n\n| 이름                 | 타입                       |\n| -------------------- | -------------------------- |\n| `useDefaultHTMLSanitizer` | `boolean`         |  \n#### 인스턴스 메서드\n\n| 이름       | 타입         |\n| ---------- | ------------ |\n| `setCodeBlockLanguages`  | `{function}` |\n| `afterAddedCommand` | `{function}` |\n| `getCodeMirror` | `{function}` |\n| `getSquire` | `{function}` |\n| `getCurrentModeEditor` | `{function}` |\n| `getUI` | `{function}` |\n"
  },
  {
    "path": "docs/v3.0-migration-guide.md",
    "content": "## Introduction\n\nThis migration guide includes all information regarding changes users must be aware of when updating from TOAST UI Editor 2.x to TOAST UI Editor 3.0. \n\nTOAST UI Editor (hereafter referred to as the 'Editor') has removed the original [CodeMirror](https://codemirror.net/), squire, and to-mark dependencies and has modified the editor to use abstract models through Prosemirror. Since the core module API and plugin usages were changed, it is advised that users consult the migration guide carefully. The table of contents is as follows and refers to the 'Changes' in the order enumerated when updating. \n\n## Table of Contents\n\n- [Changes](#changes)\n  1. [Installation and Usages](#1-installation-and-usages)\n  2. [Customizing the Toolbar](#2-customizing-the-toolbar)\n  3. [Defining Plugins](#3-defining-plugins)\n  4. [APIs and Events](#4-APIs-and-events)\n  5. [Supported Browsers](#5-supported-browsers)\n- [Removed Features](#removed-features)\n  1. [Removed jQuery Wrapper](#1-removed-jquery-wrapper)\n  2. [Removed Dependencies](#2-removed-dependencies)\n  3. [Removed APIs](#3-removed-apis)\n\n## Changes\n\n### 1. Installation and Usages\n\nTo use the Editor, use the [scoped package](https://docs.npmjs.com/using-npm/scope.html) to install the `@toast-ui/editor` package as you did for the previous v2.x. The following is an example of using the npm command to install the Editor. \n\n```sh\n$ npm install @toast-ui/editor\n$ npm install @toast-ui/editor@<version>\n```\n\n#### Usages\n\n```js\nconst Editor = require('@toast-ui/editor'); /* CommonJS */\nimport Editor from '@toast-ui/editor'; /* ES6 Module */\n```\n\nFurthermore, v3.0 provides an `EditorCore` module as named export for those wanting to implement their own unique UI instead of using the default UI. This module will create the markdown editor, markdown preview, and WYSIWYG editor, and the user can use the `getEditorElements()` method to add the editor to desired UI. This module does not create external editor UIs like toolbars, toolbar popups, and switch tabs.\n\n```js\nimport { EditorCore } from '@toast-ui/editor'; /* ES6 Module */\n\nconst editorCore = new EditorCore({\n  el\n  // ...\n});\n\nconst { mdEditor, mdPreview, wwEditor } = editorCore.getEditorElements();\n\n// ...\n```\n\n#### Bundle Structure\n\nAside from the original v2.x bundle content, two new items were added to v3.0. \n\nIn addition to the original legacy support bundle and the cdn bundle, ESM bundle is included. ESM bundle is lightweight due to the fact that there is no complex module compatibility statement, and it also provides the bundle with the added benefit of tree shaking via static analysis.\n\nSecondly, the `theme/toastui-editor-dark.css` is added for the dark theme support. The dark theme will be covered more in depth in [Added Dark Theme](#-added-dark-theme). \n\nThe bundle structure for v3.0 is as follows.\n\n```\n- dist/\n   ├─ cdn/...\n   ├─ i18n/...\n   ├─ esm/\n   │    ├─ index.js\n   │    └─ index.js.map\n   ├─ theme/\n   │    └─ toastui-editor-dark.css\n   │\n   ├─ toastui-editor-only.css\n   ├─ toastui-editor-viewer.css\n   ├─ toastui-editor.css\n   ├─ toastui-editor.js\n   └─ toastui-editor-viewer.js\n```\n\nFurthermore, the ESM bundle is included in the v3.0, and the package.json file has been updated accordingly.\nThe original UMD bundle file is defined in the main field, and the ESM bundle file is defined in the exports field.\n\n```json\n{\n  \"main\": \"dist/toastui-editor.js\",\n  \"module\": \"dist/esm/\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/esm/index.js\",\n      \"require\": \"./dist/toastui-editor.js\"\n    },\n    \"./viewer\": {\n      \"import\": \"./dist/esm/indexViewer.js\",\n      \"require\": \"./dist/toastui-editor-viewer.js\"\n    }\n  }\n}\n```\n\n#### Added Dark Theme\nv3.0 ships with dark theme included. To apply the dark theme, add the `theme/toastui-editor-dark.css` and set the editor's `theme` option to be `dark`. Currently, in v3.0, only the dark theme is supported, but the `theme` option was added to support more diverse combinations of themes in the future. \n\n```js\nimport Editor from '@toast-ui/editor';\nimport '@toast-ui/editor/dist/toastui-editor.css';\nimport '@toast-ui/editor/dist/theme/toastui-editor-dark.css';\n\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  previewStyle: 'vertical',\n  height: '500px',\n  initialValue: content,\n  theme: 'dark',\n});\n```\n\n![image](https://user-images.githubusercontent.com/37766175/120954138-73ab8680-c789-11eb-8445-87bf15842482.png)\n\n#### Changes in Dependencies\n\nEditor 3.0 no longer requires some of the dependent modules that were needed for v2.x. If you are using the CDN for development, the [CodeMirror](https://codemirror.net/) dependencies required for v2.x are no longer necessary and should be removed. The v3.0 requires Prosemirror and its related modules, but the change is reflected in the CDN, so there is nothing for the user to add. \n\n**v2.0**\n\n```html\n<head>\n  ...\n  <link\n    rel=\"stylesheet\"\n    href=\"https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/codemirror.min.css\"\n  />\n  <link rel=\"stylesheet\" href=\"https://uicdn.toast.com/editor/latest/toastui-editor.css\" />\n  ...\n</head>\n<body>\n  ...\n  <script src=\"https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js\"></script>\n  ...\n</body>\n```\n\n**v3.0**\n\n```html\n<head>\n  ...\n  <link rel=\"stylesheet\" href=\"https://uicdn.toast.com/editor/latest/toastui-editor.css\" />\n  ...\n</head>\n<body>\n  ...\n  <script src=\"https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js\"></script>\n  ...\n</body>\n```\n\n### 2. Customizing the Toolbar\n\nThe `toolbarItems` option has been reworked to be more concise and declarative compared to the v2.x. In v3.0, each toolbar item and toolbar groups are defined as options in **2D array** format. This method removes the need to define the `divider` for differentiating different groups, making the final code much more intuitive. \n\n**v2.0**\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  toolbarItems: [\n    'heading',\n    'bold',\n    'italic',\n    'strike',\n    // The divider element had to be added to differentiate different groups. \n    'divider',\n    'hr',\n    'quote',\n    'divider',\n    // ...\n  ],\n  // ...\n});\n```\n\n**v3.0**\n\n```js\nconst editor = new Editor({\n  el: document.querySelector('#editor'),\n  toolbarItems: [\n    ['heading', 'bold', 'italic', 'strike'],\n    ['hr', 'quote'],\n    // ...\n  ],\n  // ...\n});\n```\n\nBy looking at the example code above, it is clear that the v3.0 code is more concise and that the group separation is made easier.\n\n#### Customization\n\nThe method of customization has changed. In v2.x, when displaying or hiding a popup on toolbar item click, the coupling between the editor's `eventManager` and other UI instances were intricate. This forced the users to be familiar with the editor's internal implementations when customizing from the user's or from the plugin's perspective. It made customization difficult and created unnecessary control codes. In v3.0, the UI control codes have been capsulated internally in order to decrease the level of coupling, and users can now customize the toolbar items just by configuring the options. \n\n**v2.0**\n\n```js\nconst popup = editor.getUI().createPopup({\n  header: false,\n  title: null,\n  content: colorPickerContainer,\n  className: 'tui-popup-color',\n  target: editor.getUI().getToolbar().el,\n  css: {\n    width: 'auto',\n    position: 'absolute'\n  }\n});\n\neditor.eventManager.listen('focus', () => {\n  popup.hide();\n\n  // ...\n});\n\neditor.eventManager.listen('colorButtonClicked', () => {\n  // ...\n});\n\neditor.eventManager.listen('closeAllPopup', () => {\n  // ...\n});\n```\n\nThe code above is an example of customizing the toolbar's color picker in v2.x. In order to customize how the toolbar popup functions, users had to use API that were dependent on the editor's internal implementations like `editor.getUI().createPopup()` and `editor.getUI().getToolbar()`. Such internal dependencies make flexible customization difficult. It is not just the API. In order to manipulate the popup, users had to register multiple events to the `eventManager`. \n\n\n**v3.0**\n\n```js\nconst popup = {\n  name: 'color',\n  tooltip: 'Text color',\n  className: 'toastui-editor-toolbar-icons color',\n  popup: {\n    className: 'toastui-editor-popup-color',\n    body: colorPickerContainer,\n    style: { width: 'auto' },\n  },\n};\n```\n\nFew bits of codes have been intentionally left out, but in v3.0, users can create and control the popup UI through a simple option object. Users no longer need to be familiar with the internal UI modules and just have to define the `popup` option object's `className`, `style`, and `body` properties to trigger a popup on click of a toolbar button. For more information regarding customizing the toolbar, refer to [this link](https://github.com/nhn/tui.editor/tree/master/docs/en/toolbar.md).\n\n![image](https://user-images.githubusercontent.com/37766175/120915630-b6b11f80-c6df-11eb-8094-b264ca9312a1.gif)\n\n### 3. Defining Plugins\n\nThe biggest change of the v3.0 is in defining plugins. In v2.x, plugins were also incredibly dependent on the editor's internal modules as seen in the previous toolbar section. For plugins, the users had to be especially familiar with the markdown editor, WYSIWYG editor, converter, and editor's other internal instances and how they function. In v3.0, in order to address this issue, defining plugins have been reworked to include clearly defined options for customizing each feature. This guide will briefly discuss the options, and for in depth guide on defining plugins can be found [here](https://github.com/nhn/tui.editor/tree/master/docs/en/plugin.md). \n\n#### Registering Commands\n\nUsers can register markdown and WYSIWYG commands through `markdownCommands` and `wysiwygCommands` options for plugins.\n\n```js\nreturn {\n  markdownCommands: {\n    myCommand: (payload, state, dispatch) => {\n      // ...\n    },\n  },\n  wysiwygCommands: {\n    myCommand: (payload, state, dispatch) => {\n      // ...\n    },\n  },\n};\n```\n\nEach command takes `payload`, `state`, and `dispatch` as inputs, and these three parameters can be used to control the internal functionalities of Prosemirror based editor. This method also requires that users be familiar with Prosemirror. However, the Editor will continue to provide our own basic commands, which will prevent users having to work with the internal implementations directly. \n\n#### Converting\n\nUsers can now change the result of a render that happens when a certain element is converted from markdown to preview or from markdown editor to WYSIWYG editor. The same is true in reverse. Users can now redefine the text of the element that is converted from WYSIWYG editor to markdown editor. The `toHTMLRenderers` and `toMarkdownRenderers` options can be used to define what happens during the conversion from markdown to WYSIWYG and from WYSIWYG to markdown.\n\n```ts\nreturn {\n  toHTMLRenderers: {\n    // ...\n    tableCell(node: MergedTableCellMdNode, { entering, origin }) {\n      const result = origin!();\n\n      // ...\n      \n      return result;\n    },\n  },\n  toMarkdownRenderers: {\n    // ...\n    tableHead(nodeInfo) {\n      const row = (nodeInfo.node as ProsemirrorNode).firstChild;\n\n      let delim = '';\n\n      if (row) {\n        row.forEach(({ textContent, attrs }) => {\n          const headDelim = createTableHeadDelim(textContent, attrs.align);\n\n          delim += `| ${headDelim} `;\n\n          // ...\n        });\n      }\n      return { delim };\n    },\n  },\n};\n```\n\nThe above code is an example of merged table plugin. The `tableCell`, defined in `toHTMLRenderers`, node's return value is used for the markdown preview and WYSIWYG editor conversion, and the `tableHead`, defined in `toMarkdownRenderers` node's text value is used for markdown editor conversion. Any process during each editor's conversion can be defined per node through options.\n\n#### Registering Toolbar Items\n\nThe method of registering toolbar items in plugins have changed. The options are similar to the previously explained toolbar customization options. The user just needs to configure the index of the to be added group. \n\n```ts\nreturn {\n  // ...\n  toolbarItems: [\n    {\n      groupIndex: 0,\n      itemIndex: 3,\n      item: toolbarItem,\n    },\n  ],\n};\n```\n\nAs the code above shows, users can configure which item to add to the `toolbarItems` array. Each option object has `groupIndex`, `itemIndex`, and `item` properties and serves the following purpose. \n\n* `groupIndex`: Defines the index of the group that the item will be added to. \n* `itemIndex`: Defines the index of the item to be placed in the determined group. \n* `item`: Defines the toolbar item to be added. \n\nFollowing the example code, the `toolbarItems` option will make it so that the toolbar item will be added to the first toolbar group's fourth index.\n\nAdditionally, there are options that this document does not cover including registering markdown and WYSIWYG editor's Prosemirror plugin, using the `eventEmitter` for communication between the editor and the plugin. For more information, it is recommended that users consult the [Guide to Using Plugins](https://github.com/nhn/tui.editor/tree/master/docs/en/plugin.md).\n\n### 4. APIs and Events\n\nThe following are the API signatures and event names that have changed as of v3.0. \n\n#### Commands\nThe options of the commands to be registered are now passed in as individual inputs instead of an object consisting of the name and the handler. Furthermore, the input format of the method that executes the command has changed as well. \n\n**v2.x**\n\n| Method Signature             | Returned Type         |\n| ----------------- | ------------ |\n| `addCommand(type: string, props: { name: string; exec: Command }` | `void` |\n| `exec(name: string, ...args: any[]`) | `void` |\n\n**v3.0**\n\n| Method Signature              | Returned Type     |\n| ----------------- | ------------ |\n| `addCommand(type: string, name: string, command: CommandFn)` | `void` |\n| `exec(name: string, payload?: Object)` | `void` |\n\n#### Text Manipulation API\nOriginally, the `getTextObject()` API was used to insert or change text from the editor. However, in order to use this, users had to be familiar with the structure of the instance returned by the `getTextObject()` API. In v3.0, the `getTextObject()` API has been replaced with individual APIs that gets, replaces, and deletes text. \n\n**v2.x**\n\n`TextObject`'s Interface\n\n```ts\ninterface TextObject {\n  setRange(range): void;\n  setEndBeforeRange(range): void;\n  expandStartOffset(): void;\n  expandEndOffset(): void;\n  getTextContent(): string;\n  replaceContent(content) : void;\n  deleteContent(): void;\n  peekStartBeforeOffset(offset): Range;\n}\n```\n\n**v3.0**\n\n| Method Signature              | Returned Type         | Notes        |\n| ----------------- | ------------ | ------------ |\n| `replaceSelection(text: string, start?: EditorPos, end?: EditorPos) ` | `void` | Replaces the text at the given range. If the range is not provided, the text present in current editor's selected range is replaced. |\n| `deleteSelection(start?: EditorPos, end?: EditorPos)` | `void` | Deletes the text at the given range. If the range is not provided, the text present in current editor's selected range is replaced. |\n| `getSelectedText(start?: EditorPos, end?: EditorPos)` | `string` | Gets the text at the given range. If the range is not provided, the text present in current editor's selected range is retrieved. |\n\nThe above APIs' positional information (`EditorPos`) differs from markdown editor to WYSIWYG editor, and has the following format. This is because markdown and WYSIWYG have different ways to calculate position. Markdown calculates position based on the line, and the WYSIWYG calculates the offset from the start of the document.\n\n```ts\n// Markdown's Position Information\ntype EditorPos = [line: number, charactorOffset: number];\n// WYSIWYG's Position Information\ntype EditorPos = number; // 오프셋\n// Offset\n```\n\n#### Changed Instance Constructing Options and Methods\n\nThere were changes in options and methods that were not named properly or that did not clearly indicate its feature. \n\n* Instance Constructing Option\n\n| v2 | v3 |\n| --- | --- |\n| `linkAttribute` | `linkAttributes` |\n\n* Instance Methods\n\n| v2 | v3 |\n| --- | --- |\n| `setHtml` | `setHTML` |\n| `getHtml` | `getHTML` |\n| `minHeight` | `setMinHeight`, `getMinHeight` |\n| `height` | `setHeight`, `getHeight` |\n| `getRange` | `getSelection` |\n| `remove` | `destroy` |\n\n#### Changed Event Names\n\nSome events were renamed to represent their meaning more clearly. \n\n| v2 | v3 |\n| --- | --- |\n| `stateChange` | `caretChange` |\n| `convertorAfterMarkdownToHtmlConverted` | `beforePreviewRender` |\n| `convertorAfterHtmlToMarkdownConverted` | `beforeConvertWysiwygToMarkdown` |\n\n### 5. Supported Browsers\n\nFrom v3.0, only the browsers **above Internet Explorer (IE) 11** will be supported. The previous version supported IE 10 and above, but the support range has been changed due to the low browser share and Prosemirror core module support. \n\n## Removed Features\n\n### 1. Removed jQuery Wrapper\n\nFrom v3.0, the jQuery Wrapper has been removed. To use jQuery, the user must wrap the `@toast-ui/editor` package separately.\n\n### 2. Removed Dependencies\n\nBecause original CodeMirror, squire, and to-mark dependencies were all removed, any code that accesses these modules directly or indirectly will no longer work. Most of the required features were added to the editor instance's API, and it is recommended that users use the according APIs.\n\n**v2.x**\n\n```js\nconst editor = new Editor(/* */);\n\nconsole.log(editor.getCodeMirror()); // CodeMirror Instance\nconsole.log(editor.getSquire()); // squire Instance\n```\n\n**v3.0**\n\n```js\nconst editor = new Editor(/* */);\n\nconsole.log(editor.getCodeMirror()); // Uncaught TypeError\nconsole.log(editor.getSquire()); // Uncaught TypeError\n```\n\n### 3. Removed APIs\n\nLastly, the following is a list of APIs removed from the v3.0.\n\n#### Static Properties\n\n| Name                  | Type               |\n| --------------------- | ------------------ |\n| `isViewer`            | `{boolean}` |\n| `codeBlockManager`    | `{CodeBlockManager}` |\n| `WwCodeBlockManager`  | `{Class.<WwCodeBlockManager>}` |\n| `WwTableManager`      | `{Class.<WwTableManager>}` |\n| `WwTableSelectionManager` | `{Class.<WwTableSelectionManager>}` |\n| `CommandManager` | `{Class.<CommandManager>}` |\n\n#### Static Methods\n\n| Name              | Type         |\n| ----------------- | ------------ |\n| `getInstances` | `{function}` |\n\n\n#### Instance Constructing Option\n\n| Name              | Type                       |\n| -------------------- | -------------------------- |\n| `useDefaultHTMLSanitizer` | `boolean`         |  \n\n\n#### Instance Method\n\n| Name     | Type         |\n| ---------- | ------------ |\n| `setCodeBlockLanguages`  | `{function}` |\n| `afterAddedCommand` | `{function}` |\n| `getCodeMirror` | `{function}` |\n| `getSquire` | `{function}` |\n| `getCurrentModeEditor` | `{function}` |\n| `getUI` | `{function}` |\n"
  },
  {
    "path": "jest-setup.js",
    "content": "import '@testing-library/jest-dom';\n\nif (global.Range) {\n  global.Range.prototype.getClientRects = jest.fn().mockReturnValue({ length: 0 });\n  global.Range.prototype.getBoundingClientRect = jest.fn().mockReturnValue({});\n}\n"
  },
  {
    "path": "jest.base.config.js",
    "content": "// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst path = require('path');\nconst setupFile = path.resolve(__dirname, './jest-setup.js');\nconst cssMockFile = path.resolve(__dirname, './__mocks__/cssMock.js');\n\nmodule.exports = {\n  preset: 'ts-jest',\n  testEnvironment: 'node',\n  setupFilesAfterEnv: [setupFile],\n  transform: {\n    '^.+\\\\.ts$': 'ts-jest',\n    '^.+\\\\.js$': 'jest-esm-transformer',\n    '^.+\\\\.css$': cssMockFile,\n  },\n  transformIgnorePatterns: ['<rootDir>/node_modules/'],\n  snapshotSerializers: ['jest-serializer-html'],\n  testMatch: ['**/__test__/**/*.spec.ts'],\n  moduleFileExtensions: ['ts', 'js', 'json'],\n};\n"
  },
  {
    "path": "jest.config.js",
    "content": "module.exports = {\n  projects: [\n    '<rootDir>/libs/toastmark/jest.config.js',\n    '<rootDir>/apps/editor/jest.config.js',\n    '<rootDir>/plugins/color-syntax/jest.config.js',\n    '<rootDir>/plugins/code-syntax-highlight/jest.config.js',\n    '<rootDir>/plugins/uml/jest.config.js',\n    '<rootDir>/plugins/chart/jest.config.js',\n  ],\n};\n"
  },
  {
    "path": "lerna.json",
    "content": "{\n  \"packages\": [\"apps/*\", \"plugins/*\", \"libs/*\"],\n  \"version\": \"3.2.2\"\n}\n"
  },
  {
    "path": "libs/toastmark/.eslintrc.js",
    "content": "module.exports = {\n  rules: {\n    'prefer-destructuring': 0,\n    'padding-line-between-statements': 0,\n    'lines-between-class-members': 0,\n    'no-undefined': 0,\n    'no-useless-escape': 0,\n    'no-shadow': 0,\n    'no-plusplus': 0,\n    'max-depth': 0,\n    '@typescript-eslint/no-empty-function': 0,\n    'no-lonely-if': 0,\n    'no-control-regex': 0,\n    'no-nested-ternary': 0,\n    'no-empty': 0,\n    'dot-notation': 0,\n    'spaced-comment': 0,\n    eqeqeq: 0,\n  },\n};\n"
  },
  {
    "path": "libs/toastmark/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 NHN Corp.\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\nall copies 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\nTHE SOFTWARE.\n\n---\n\nThe files inside src/commonmark/ are derived from commonmark.js\n(except files in gfm and __test__ directory)\nCopyright (c) 2014, John MacFarlane\n\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n---\n\nsrc/commonmark/from-code-point.js is derived from a polyfill\nCopyright Mathias Bynens <http://mathiasbynens.be/>\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n---\n\nThe test cases in src/commonmark/__test__/base-examples.json are\ncopied from commonmark spec.\n\nCopyright (C) 2014-15 John MacFarlane\n\nReleased under the Creative Commons CC-BY-SA 4.0 license:\n<http://creativecommons.org/licenses/by-sa/4.0/>."
  },
  {
    "path": "libs/toastmark/README.md",
    "content": "# ToastMark\n\nToastMark is a markdown parser extended from [commonmark.js](https://github.com/commonmark/commonmark.js), with more advanced features to be used within TOAST UI Editor. \n\n> Currently, ToastMark is for interal usage only as the API's are supposed to be changed frequently. We are planning to register this as a separate npm package when API's are stabilized.\n\n## Differences from commonmark.js\n\n### GitHub Flavored Markdown(GFM) Support\ncommonmark.js is the reference implementation of [CommonMark](https://spec.commonmark.org/0.29/) and doesn't support [GFM](https://github.github.com/gfm/), which is extended markdown syntax based on CommonMark. ToastMark has its own implementation for supporting GFM.\n\n### Source Position Information\nAlthough commonmark.js provides source position information related with each node in AST(Abstract Syntax Tree), those are limited to block-level elements. ToastMark extended this feature to provide source position information for inline-level elements also.\n\n### Incremental Parsing\nAs ToastMark is developed for the purpose of improving markdown editing experience, this must be the key feature of ToastMark. Instead of parsing the entire document whenever a user makes a change to a document, ToastMark parses only changed part of the document and update the existing AST. It also returns information about removed and inserted nodes, which can be used to update syntax highlithing or preview contents incrementally.\n\n### Searching and Editing AST\nToastMark provides useful methods to search the existing AST, such as `findNodeAtPosition` and `findNodeById`. These methods can be used for synchronizing scroll position of markdown editor and preview contents, updating the style of the toolbar buttons correspond to the cursor position, and so on. We are also planning to add more methods to edit existing AST to support commands like `Bold`, `Italic`, and `OrderedList` which can be triggered by toolbar buttons and keyboard shortcuts.\n\n### TypeScript\nThe entire codebase is converted from JavaScript to TypeScript. \n"
  },
  {
    "path": "libs/toastmark/demo/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>Demo</title>\n  </head>\n  <body>\n    <script type=\"module\">\n      import '/dist/__sample__/index.js';\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "libs/toastmark/jest.config.js",
    "content": "// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst base = require('../../jest.base.config');\n\nmodule.exports = {\n  ...base,\n};\n"
  },
  {
    "path": "libs/toastmark/package.json",
    "content": "{\n  \"name\": \"@toast-ui/toastmark\",\n  \"version\": \"0.0.1-alpha.1\",\n  \"description\": \"ToastMark - Incremental markdown parser extended from CommonMark.js\",\n  \"scripts\": {\n    \"lint\": \"eslint .\",\n    \"test:types\": \"tsc\",\n    \"test\": \"jest --watch\",\n    \"test:ci\": \"jest\",\n    \"serve\": \"snowpack dev\",\n    \"serve:ie\": \"webpack serve\",\n    \"build\": \"rollup -c && webpack build\"\n  },\n  \"types\": \"types/index.d.ts\",\n  \"files\": [\n    \"dist\",\n    \"types\"\n  ],\n  \"keywords\": [\n    \"markdown\",\n    \"parser\"\n  ],\n  \"author\": \"NHN\",\n  \"main\": \"dist/toastmark.js\",\n  \"module\": \"dist/esm/index.js\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"@types/codemirror\": \"5.60.5\",\n    \"@types/mdurl\": \"^1.0.2\",\n    \"codemirror\": \"^5.51.0\",\n    \"entities\": \"^2.0.0\",\n    \"mdurl\": \"^1.0.1\"\n  }\n}\n"
  },
  {
    "path": "libs/toastmark/rollup.config.js",
    "content": "import typescript from '@rollup/plugin-typescript';\nimport commonjs from '@rollup/plugin-commonjs';\nimport { nodeResolve } from '@rollup/plugin-node-resolve';\nimport json from '@rollup/plugin-json';\n\nexport default [\n  {\n    input: 'src/index.ts',\n    output: {\n      dir: 'dist/esm',\n      format: 'es',\n      sourcemap: false,\n    },\n    plugins: [typescript(), commonjs(), nodeResolve(), json()],\n  },\n];\n"
  },
  {
    "path": "libs/toastmark/snowpack.config.js",
    "content": "/** @type {import(\"snowpack\").SnowpackUserConfig } */\nmodule.exports = {\n  mount: {\n    demo: '/',\n    src: '/dist',\n  },\n  devOptions: {\n    port: 8000,\n  },\n  alias: {\n    '@t': './types',\n  },\n};\n"
  },
  {
    "path": "libs/toastmark/src/__sample__/index.css",
    "content": ".container {\n  display: flex;\n  height: 500px;\n}\n\n.container > div {\n  flex: 1;\n  border: 1px solid #ccc;\n  overflow-x: hidden;\n  overflow-y: auto;\n}\n\n.CodeMirror {\n  height: 500px;\n}\n"
  },
  {
    "path": "libs/toastmark/src/__sample__/index.ts",
    "content": "import codemirror from 'codemirror';\nimport { ToastMark } from '../toastmark';\nimport { Renderer } from '../html/renderer';\nimport { last } from '../helper';\nimport 'codemirror/lib/codemirror.css';\nimport './index.css';\n\ndocument.body.innerHTML = `\n  <section class=\"container\">\n    <div class=\"editor\"></div>\n    <div class=\"preview\"></div>\n    <div class=\"html\"></div>\n  </section>\n`;\n\nconst editorEl = document.querySelector('.editor') as HTMLElement;\nconst htmlEl = document.querySelector('.html') as HTMLElement;\nconst previewEl = document.querySelector('.preview') as HTMLElement;\n\nconst cm = codemirror(editorEl, { lineNumbers: true });\nconst doc = new ToastMark();\nconst renderer = new Renderer({ gfm: true, nodeId: true });\n\nconst tokenTypes = {\n  heading: 'header',\n  emph: 'em',\n  strong: 'strong',\n  strike: 'strikethrough',\n  item: 'variable-2',\n  image: 'variable-3',\n  blockQuote: 'quote',\n};\n\ntype TokenTypes = typeof tokenTypes;\n\ncm.on('change', (editor, changeObj) => {\n  const { from, to, text } = changeObj;\n  const changed = doc.editMarkdown(\n    [from.line + 1, from.ch + 1],\n    [to.line + 1, to.ch + 1],\n    text.join('\\n')\n  );\n\n  changed.forEach((result) => {\n    const { nodes, removedNodeRange } = result;\n    const html = renderer.render(doc.getRootNode());\n    htmlEl.innerText = html;\n\n    if (!removedNodeRange) {\n      previewEl.innerHTML = html;\n    } else {\n      const [startNodeId, endNodeId] = removedNodeRange.id;\n      const startEl = previewEl.querySelector(`[data-nodeid=\"${startNodeId}\"]`);\n      const endEl = previewEl.querySelector(`[data-nodeid=\"${endNodeId}\"]`);\n      const newHtml = nodes.map((node) => renderer.render(node)).join('');\n\n      if (startEl) {\n        startEl.insertAdjacentHTML('beforebegin', newHtml);\n        let el: Element = startEl;\n        while (el !== endEl) {\n          const nextEl: Element | null = el.nextElementSibling;\n          el.remove();\n          el = nextEl!;\n        }\n        el.remove();\n      }\n    }\n\n    if (!nodes.length) {\n      return;\n    }\n\n    const editFromPos = nodes[0].sourcepos![0];\n    const editToPos = last(nodes).sourcepos![1];\n    const editFrom = { line: editFromPos[0] - 1, ch: editFromPos[1] - 1 };\n    const editTo = { line: editToPos[0] - 1, ch: editToPos[1] };\n    const marks = cm.findMarks(editFrom, editTo);\n\n    for (const mark of marks) {\n      mark.clear();\n    }\n\n    for (const parent of nodes) {\n      const walker = parent.walker();\n      let event;\n      while ((event = walker.next())) {\n        const { node, entering } = event;\n        if (entering) {\n          const [startLine, startCh] = node.sourcepos![0];\n          const [endLine, endCh] = node.sourcepos![1];\n          const start = { line: startLine - 1, ch: startCh - 1 };\n          const end = { line: endLine - 1, ch: endCh };\n          const token = tokenTypes[node.type as keyof TokenTypes];\n\n          if (token) {\n            cm.markText(start, end, { className: `cm-${token}` });\n          }\n        }\n      }\n    }\n  });\n});\n"
  },
  {
    "path": "libs/toastmark/src/__test__/toastmark.spec.ts",
    "content": "import { ParserOptions } from '@t/parser';\nimport { ToastMark } from '../toastmark';\nimport { Parser } from '../commonmark/blocks';\nimport { getChildNodes } from '../nodeHelper';\nimport { Node } from '../commonmark/node';\n\nfunction removeIdAttrFromAllNode(root: Node) {\n  const walker = root.walker();\n  let event;\n  while ((event = walker.next())) {\n    const { entering, node } = event;\n    if (entering) {\n      // @ts-ignore\n      delete node.id;\n    }\n  }\n}\n\nfunction assertParseResult(doc: ToastMark, lineTexts: string[], options?: Partial<ParserOptions>) {\n  expect(doc.getLineTexts()).toEqual(lineTexts);\n\n  const reader = new Parser(options);\n  const root = doc.getRootNode();\n  const expectedRoot = reader.parse(lineTexts.join('\\n'));\n\n  removeIdAttrFromAllNode(root);\n  removeIdAttrFromAllNode(expectedRoot);\n  expect(root).toEqual(expectedRoot);\n}\n\nfunction assertResultNodes(doc: ToastMark, nodes: Node[], startIdx = 0) {\n  const root = doc.getRootNode();\n  const newNodes = getChildNodes(root);\n\n  for (let i = 0; i < nodes.length; i += 1) {\n    expect(nodes[i]).toBe(newNodes[i + startIdx]);\n  }\n}\n\ndescribe('findNodeAtPosition()', () => {\n  it('should return a node at the given position', () => {\n    const doc = new ToastMark('# Hello *World*\\n\\n- Item 1\\n- Item **2**');\n\n    expect(doc.findNodeAtPosition([1, 1])).toMatchObject({\n      type: 'heading',\n    });\n\n    expect(doc.findNodeAtPosition([1, 3])).toMatchObject({\n      type: 'text',\n      literal: 'Hello ',\n    });\n\n    expect(doc.findNodeAtPosition([1, 9])).toMatchObject({\n      type: 'emph',\n      firstChild: {\n        type: 'text',\n        literal: 'World',\n      },\n    });\n\n    expect(doc.findNodeAtPosition([1, 10])).toMatchObject({\n      type: 'text',\n      literal: 'World',\n    });\n\n    expect(doc.findNodeAtPosition([3, 1])).toMatchObject({\n      type: 'item',\n    });\n\n    expect(doc.findNodeAtPosition([3, 3])).toMatchObject({\n      type: 'text',\n      literal: 'Item 1',\n    });\n\n    expect(doc.findNodeAtPosition([4, 8])).toMatchObject({\n      type: 'strong',\n      firstChild: {\n        type: 'text',\n        literal: '2',\n      },\n    });\n\n    expect(doc.findNodeAtPosition([4, 10])).toMatchObject({\n      type: 'text',\n      literal: '2',\n    });\n\n    expect(doc.findNodeAtPosition([5, 1])).toBeNull();\n  });\n\n  it('should return null if matched node does not exist', () => {\n    const doc = new ToastMark('# Hello\\n\\nWorld');\n\n    // position in between two node (blank line)\n    expect(doc.findNodeAtPosition([2, 1])).toBeNull();\n\n    // position out of document range\n    expect(doc.findNodeAtPosition([4, 1])).toBeNull();\n  });\n});\n\ndescribe('findFirstNodeAtLine()', () => {\n  const markdown = [\n    '# Hello *World*',\n    '',\n    '- Item D1',\n    '  - Item D2',\n    '![Image](URL)',\n    '',\n    'Paragraph',\n  ].join('\\n');\n\n  it('should return the first node at the given line', () => {\n    const doc = new ToastMark(markdown);\n\n    expect(doc.findFirstNodeAtLine(1)).toMatchObject({ type: 'heading' });\n    expect(doc.findFirstNodeAtLine(3)).toMatchObject({\n      type: 'list',\n      prev: { type: 'heading' },\n    });\n    expect(doc.findFirstNodeAtLine(4)).toMatchObject({\n      type: 'list',\n      parent: { type: 'item' },\n    });\n    expect(doc.findFirstNodeAtLine(5)).toMatchObject({ type: 'image' });\n    expect(doc.findFirstNodeAtLine(7)).toMatchObject({ type: 'paragraph' });\n  });\n\n  it('if the given line is blank, returns the first node at the previous line', () => {\n    const doc = new ToastMark(markdown);\n\n    expect(doc.findFirstNodeAtLine(2)).toMatchObject({ type: 'heading' });\n    expect(doc.findFirstNodeAtLine(6)).toMatchObject({ type: 'image' });\n    expect(doc.findFirstNodeAtLine(8)).toMatchObject({ type: 'paragraph' });\n  });\n\n  it('should return null if nothing mathces', () => {\n    const doc = new ToastMark('\\n\\n');\n\n    expect(doc.findFirstNodeAtLine(0)).toBeNull();\n    expect(doc.findFirstNodeAtLine(1)).toBeNull();\n    expect(doc.findFirstNodeAtLine(2)).toBeNull();\n    expect(doc.findFirstNodeAtLine(3)).toBeNull();\n  });\n});\n\ndescribe('editText()', () => {\n  describe('single paragraph', () => {\n    it('should insert character within a line', () => {\n      const doc = new ToastMark('Hello World');\n      const result = doc.editMarkdown([1, 6], [1, 6], ',')[0];\n\n      assertParseResult(doc, ['Hello, World']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('should remove entire text', () => {\n      const doc = new ToastMark('Hello World');\n      const result = doc.editMarkdown([1, 1], [1, 12], '')[0];\n\n      assertParseResult(doc, ['']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('should remove preceding newline', () => {\n      const doc = new ToastMark('\\nHello World');\n      const result = doc.editMarkdown([1, 1], [2, 1], '')[0];\n\n      assertParseResult(doc, ['Hello World']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('should remove last newline', () => {\n      const doc = new ToastMark('Hello World\\n');\n      const result = doc.editMarkdown([1, 12], [2, 1], '')[0];\n\n      assertParseResult(doc, ['Hello World']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('should insert characters and newlines', () => {\n      const doc = new ToastMark('Hello World');\n      const result = doc.editMarkdown([1, 6], [1, 7], '!\\n\\nMy ')[0];\n\n      assertParseResult(doc, ['Hello!', '', 'My World']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('should replace multiline text with characters', () => {\n      const doc = new ToastMark('Hello\\nMy\\nWorld');\n      const result = doc.editMarkdown([1, 5], [3, 3], 'ooo Wooo')[0];\n\n      assertParseResult(doc, ['Hellooo Wooorld']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('should prepend characters', () => {\n      const doc = new ToastMark('Hello World');\n      const result = doc.editMarkdown([1, 1], [1, 1], 'Hi, ')[0];\n\n      assertParseResult(doc, ['Hi, Hello World']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('should append character', () => {\n      const doc = new ToastMark('Hello World');\n      const result = doc.editMarkdown([1, 12], [1, 12], '!!')[0];\n\n      assertParseResult(doc, ['Hello World!!']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('should prepend newlines', () => {\n      const doc = new ToastMark('Hello World');\n      const result = doc.editMarkdown([1, 1], [1, 1], '\\n\\n\\n')[0];\n\n      assertParseResult(doc, ['', '', '', 'Hello World']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('should prepend characters (unmatched position)', () => {\n      const doc = new ToastMark('  Hello World');\n      const result = doc.editMarkdown([1, 1], [1, 1], 'Hi,')[0];\n\n      assertParseResult(doc, ['Hi,  Hello World']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('should insert newlines into preceding empty line of first paragraph', () => {\n      const doc = new ToastMark('\\nHello World');\n      const result = doc.editMarkdown([1, 1], [1, 1], '\\n')[0];\n\n      assertParseResult(doc, ['', '', 'Hello World']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('should append characters with newline', () => {\n      const doc = new ToastMark('Hello World\\n');\n      const result = doc.editMarkdown([2, 1], [2, 1], '\\nHi')[0];\n\n      assertParseResult(doc, ['Hello World', '', 'Hi']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('should remove lines with top blank line', () => {\n      const doc = new ToastMark('\\n\\nabove linebreak\\n\\nHello World');\n      const result = doc.editMarkdown([1, 1], [5, 12], '')[0];\n\n      assertParseResult(doc, ['']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('should parse the table with including prev line', () => {\n      const doc = new ToastMark('| a | b\\n--| ---\\n c');\n      const result = doc.editMarkdown([3, 1], [3, 3], '|c|')[0];\n\n      assertParseResult(doc, ['| a | b', '--| ---', '|c|']);\n      assertResultNodes(doc, result.nodes);\n    });\n  });\n\n  describe('multiple paragraph', () => {\n    it('should insert paragraphs within multiple paragraphs', () => {\n      const doc = new ToastMark('Hello\\n\\nMy\\n\\nWorld');\n      const result = doc.editMarkdown([1, 6], [5, 1], ',\\n\\nMy ')[0];\n\n      assertParseResult(doc, ['Hello,', '', 'My World']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('should replace multiple paragraphs with a heading', () => {\n      const doc = new ToastMark('Hello\\n\\nMy\\n\\nWorld');\n      const result = doc.editMarkdown([1, 1], [5, 1], '# Hello ')[0];\n\n      assertParseResult(doc, ['# Hello World']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('should remove last block with newlines', () => {\n      const doc = new ToastMark('Hello\\n\\nWorld\\n');\n      const result = doc.editMarkdown([3, 1], [4, 1], '')[0];\n\n      assertParseResult(doc, ['Hello', '', '']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('should insert a characters in between paragraphs', () => {\n      const doc = new ToastMark('Hello\\n\\nWorld');\n      const result = doc.editMarkdown([2, 1], [2, 1], 'My')[0];\n\n      assertParseResult(doc, ['Hello', 'My', 'World']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('update sourcepos for every next nodes', () => {\n      const doc = new ToastMark('Hello\\n\\nMy\\n\\nWorld *!!*');\n      const result = doc.editMarkdown([1, 1], [1, 1], 'Hey,\\n')[0];\n\n      assertParseResult(doc, ['Hey,', 'Hello', '', 'My', '', 'World *!!*']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('should update to fenced code blocks to the end of the document', () => {\n      const doc = new ToastMark('``\\nHello\\n\\nMy World');\n      const result = doc.editMarkdown([1, 3], [1, 3], '`')[0];\n\n      assertParseResult(doc, ['```', 'Hello', '', 'My World']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('should update to custom block to the end of the document', () => {\n      const doc = new ToastMark('$\\nHello\\n\\nMy World');\n      const result = doc.editMarkdown([1, 2], [1, 2], '$custom')[0];\n\n      assertParseResult(doc, ['$$custom', 'Hello', '', 'My World']);\n      assertResultNodes(doc, result.nodes);\n    });\n  });\n\n  describe('list item', () => {\n    it('single empty item - append characters', () => {\n      const doc = new ToastMark('-');\n      const result = doc.editMarkdown([1, 2], [1, 2], ' Hello')[0];\n\n      assertParseResult(doc, ['- Hello']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('single item paragraph - append characters', () => {\n      const doc = new ToastMark('- Hello');\n      const result = doc.editMarkdown([1, 8], [1, 8], ' World')[0];\n\n      assertParseResult(doc, ['- Hello World']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('single item - append new item', () => {\n      const doc = new ToastMark('- Hello');\n      const result = doc.editMarkdown([1, 8], [1, 8], '\\n- World')[0];\n\n      assertParseResult(doc, ['- Hello', '- World']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('prepend a new list before an existing list', () => {\n      const doc = new ToastMark('Hello\\n\\n- World');\n      const result = doc.editMarkdown([1, 1], [1, 1], '- ')[0];\n\n      assertParseResult(doc, ['- Hello', '', '- World']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('prepend a new list before a padded paragraph', () => {\n      const doc = new ToastMark('\\n\\n  My\\n\\n  World');\n      const result = doc.editMarkdown([1, 1], [1, 1], '- Hello')[0];\n\n      assertParseResult(doc, ['- Hello', '', '  My', '', '  World']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('prepend a new list before a padded codeblock containing list-like text', () => {\n      const doc = new ToastMark('\\n\\n    - World');\n      const result = doc.editMarkdown([1, 1], [1, 1], '- Hello')[0];\n\n      assertParseResult(doc, ['- Hello', '', '    - World']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('convert a paragraph preceded by a list to a list ', () => {\n      const doc = new ToastMark('- Hello\\n\\nWorld');\n      const result = doc.editMarkdown([3, 1], [3, 1], '- ')[0];\n\n      assertParseResult(doc, ['- Hello', '', '- World']);\n      assertResultNodes(doc, result.nodes);\n    });\n\n    it('add paddings to a paragraph preceded by a list', () => {\n      const doc = new ToastMark('- Hello\\n\\nWorld');\n      const result = doc.editMarkdown([3, 1], [3, 1], '  ')[0];\n\n      assertParseResult(doc, ['- Hello', '', '  World']);\n      assertResultNodes(doc, result.nodes);\n    });\n  });\n\n  describe('Reference Def', () => {\n    it('should parse reference link nodes when modifying url of Reference Def node', () => {\n      const doc = new ToastMark('[foo]: /test\\n\\n[foo]\\n\\n[foo]', { referenceDefinition: true });\n      doc.editMarkdown([1, 1], [1, 13], '[foo]: /test2');\n\n      assertParseResult(doc, ['[foo]: /test2', '', '[foo]', '', '[foo]'], {\n        referenceDefinition: true,\n      });\n    });\n\n    it('should change reference link nodes to paragraph nodes when modifying label of Reference Def node', () => {\n      const doc = new ToastMark('[foo]: /test\\n\\n[foo]\\n\\n[foo]', { referenceDefinition: true });\n      doc.editMarkdown([1, 1], [1, 13], '[food]: /test2');\n\n      assertParseResult(doc, ['[food]: /test2', '', '[foo]', '', '[foo]'], {\n        referenceDefinition: true,\n      });\n    });\n\n    it('should merge the Reference Def node as paragraph', () => {\n      const doc = new ToastMark('test\\n\\n[foo]: /test', { referenceDefinition: true });\n      doc.editMarkdown([2, 1], [2, 1], 'test');\n\n      assertParseResult(doc, ['test', 'test', '[foo]: /test'], {\n        referenceDefinition: true,\n      });\n    });\n  });\n});\n\nit('return the node - findNodeById()', () => {\n  const doc = new ToastMark('# Hello *World*\\n\\n- Item 1\\n- Item **2**');\n  const firstNodeId = doc.findFirstNodeAtLine(1)!.id;\n\n  expect(doc.findNodeById(firstNodeId)).toMatchObject({\n    type: 'heading',\n  });\n});\n\nit('remove all node in the map - removeAllNode()', () => {\n  const doc = new ToastMark('# Hello *World*\\n\\n- Item 1\\n- Item **2**');\n  const firstNodeId = doc.findFirstNodeAtLine(1)!.id;\n\n  doc.removeAllNode();\n  expect(doc.findNodeById(firstNodeId)).toEqual(null);\n});\n\ndescribe('front matter', () => {\n  it('should change normal paragraph to front matter ', () => {\n    const doc = new ToastMark('---\\ntitle: front matter\\n--', { frontMatter: true });\n    doc.editMarkdown([3, 3], [3, 3], '-');\n\n    assertParseResult(doc, ['---', 'title: front matter', '---'], { frontMatter: true });\n  });\n\n  it('should change front matter to normal paragraph', () => {\n    const doc = new ToastMark('---\\ntitle: front matter\\n---', { frontMatter: true });\n    doc.editMarkdown([3, 2], [3, 3], '');\n\n    assertParseResult(doc, ['---', 'title: front matter', '--'], { frontMatter: true });\n  });\n\n  it('should change front matter to setext heading', () => {\n    const doc = new ToastMark('---\\ntitle: front matter\\n---', { frontMatter: true });\n    doc.editMarkdown([1, 1], [1, 1], 'heading\\n');\n\n    assertParseResult(doc, ['heading', '---', 'title: front matter', '---'], { frontMatter: true });\n  });\n\n  it('following paragraph should be changed properly', () => {\n    const doc = new ToastMark('---\\ntitle: front matter\\n---\\npara', { frontMatter: true });\n    doc.editMarkdown([4, 1], [4, 5], 'changed');\n\n    assertParseResult(doc, ['---', 'title: front matter', '---', 'changed'], { frontMatter: true });\n  });\n});\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/__test__/base-examples.json",
    "content": "[\n  {\n    \"markdown\": \"\\tfoo\\tbaz\\t\\tbim\\n\",\n    \"html\": \"<pre><code>foo\\tbaz\\t\\tbim\\n</code></pre>\\n\",\n    \"example\": 1,\n    \"start_line\": 352,\n    \"end_line\": 357,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \"  \\tfoo\\tbaz\\t\\tbim\\n\",\n    \"html\": \"<pre><code>foo\\tbaz\\t\\tbim\\n</code></pre>\\n\",\n    \"example\": 2,\n    \"start_line\": 359,\n    \"end_line\": 364,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \"    a\\ta\\n    ὐ\\ta\\n\",\n    \"html\": \"<pre><code>a\\ta\\nὐ\\ta\\n</code></pre>\\n\",\n    \"example\": 3,\n    \"start_line\": 366,\n    \"end_line\": 373,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \"  - foo\\n\\n\\tbar\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>foo</p>\\n<p>bar</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 4,\n    \"start_line\": 379,\n    \"end_line\": 390,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \"- foo\\n\\n\\t\\tbar\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>foo</p>\\n<pre><code>  bar\\n</code></pre>\\n</li>\\n</ul>\\n\",\n    \"example\": 5,\n    \"start_line\": 392,\n    \"end_line\": 404,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \">\\t\\tfoo\\n\",\n    \"html\": \"<blockquote>\\n<pre><code>  foo\\n</code></pre>\\n</blockquote>\\n\",\n    \"example\": 6,\n    \"start_line\": 415,\n    \"end_line\": 422,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \"-\\t\\tfoo\\n\",\n    \"html\": \"<ul>\\n<li>\\n<pre><code>  foo\\n</code></pre>\\n</li>\\n</ul>\\n\",\n    \"example\": 7,\n    \"start_line\": 424,\n    \"end_line\": 433,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \"    foo\\n\\tbar\\n\",\n    \"html\": \"<pre><code>foo\\nbar\\n</code></pre>\\n\",\n    \"example\": 8,\n    \"start_line\": 436,\n    \"end_line\": 443,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \" - foo\\n   - bar\\n\\t - baz\\n\",\n    \"html\": \"<ul>\\n<li>foo\\n<ul>\\n<li>bar\\n<ul>\\n<li>baz</li>\\n</ul>\\n</li>\\n</ul>\\n</li>\\n</ul>\\n\",\n    \"example\": 9,\n    \"start_line\": 445,\n    \"end_line\": 461,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \"#\\tFoo\\n\",\n    \"html\": \"<h1>Foo</h1>\\n\",\n    \"example\": 10,\n    \"start_line\": 463,\n    \"end_line\": 467,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \"*\\t*\\t*\\t\\n\",\n    \"html\": \"<hr />\\n\",\n    \"example\": 11,\n    \"start_line\": 469,\n    \"end_line\": 473,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \"- `one\\n- two`\\n\",\n    \"html\": \"<ul>\\n<li>`one</li>\\n<li>two`</li>\\n</ul>\\n\",\n    \"example\": 12,\n    \"start_line\": 496,\n    \"end_line\": 504,\n    \"section\": \"Precedence\"\n  },\n  {\n    \"markdown\": \"***\\n---\\n___\\n\",\n    \"html\": \"<hr />\\n<hr />\\n<hr />\\n\",\n    \"example\": 13,\n    \"start_line\": 535,\n    \"end_line\": 543,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"+++\\n\",\n    \"html\": \"<p>+++</p>\\n\",\n    \"example\": 14,\n    \"start_line\": 548,\n    \"end_line\": 552,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"===\\n\",\n    \"html\": \"<p>===</p>\\n\",\n    \"example\": 15,\n    \"start_line\": 555,\n    \"end_line\": 559,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"--\\n**\\n__\\n\",\n    \"html\": \"<p>--\\n**\\n__</p>\\n\",\n    \"example\": 16,\n    \"start_line\": 564,\n    \"end_line\": 572,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \" ***\\n  ***\\n   ***\\n\",\n    \"html\": \"<hr />\\n<hr />\\n<hr />\\n\",\n    \"example\": 17,\n    \"start_line\": 577,\n    \"end_line\": 585,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"    ***\\n\",\n    \"html\": \"<pre><code>***\\n</code></pre>\\n\",\n    \"example\": 18,\n    \"start_line\": 590,\n    \"end_line\": 595,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"Foo\\n    ***\\n\",\n    \"html\": \"<p>Foo\\n***</p>\\n\",\n    \"example\": 19,\n    \"start_line\": 598,\n    \"end_line\": 604,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"_____________________________________\\n\",\n    \"html\": \"<hr />\\n\",\n    \"example\": 20,\n    \"start_line\": 609,\n    \"end_line\": 613,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \" - - -\\n\",\n    \"html\": \"<hr />\\n\",\n    \"example\": 21,\n    \"start_line\": 618,\n    \"end_line\": 622,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \" **  * ** * ** * **\\n\",\n    \"html\": \"<hr />\\n\",\n    \"example\": 22,\n    \"start_line\": 625,\n    \"end_line\": 629,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"-     -      -      -\\n\",\n    \"html\": \"<hr />\\n\",\n    \"example\": 23,\n    \"start_line\": 632,\n    \"end_line\": 636,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"- - - -    \\n\",\n    \"html\": \"<hr />\\n\",\n    \"example\": 24,\n    \"start_line\": 641,\n    \"end_line\": 645,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"_ _ _ _ a\\n\\na------\\n\\n---a---\\n\",\n    \"html\": \"<p>_ _ _ _ a</p>\\n<p>a------</p>\\n<p>---a---</p>\\n\",\n    \"example\": 25,\n    \"start_line\": 650,\n    \"end_line\": 660,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \" *-*\\n\",\n    \"html\": \"<p><em>-</em></p>\\n\",\n    \"example\": 26,\n    \"start_line\": 666,\n    \"end_line\": 670,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"- foo\\n***\\n- bar\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n</ul>\\n<hr />\\n<ul>\\n<li>bar</li>\\n</ul>\\n\",\n    \"example\": 27,\n    \"start_line\": 675,\n    \"end_line\": 687,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"Foo\\n***\\nbar\\n\",\n    \"html\": \"<p>Foo</p>\\n<hr />\\n<p>bar</p>\\n\",\n    \"example\": 28,\n    \"start_line\": 692,\n    \"end_line\": 700,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"Foo\\n---\\nbar\\n\",\n    \"html\": \"<h2>Foo</h2>\\n<p>bar</p>\\n\",\n    \"example\": 29,\n    \"start_line\": 709,\n    \"end_line\": 716,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"* Foo\\n* * *\\n* Bar\\n\",\n    \"html\": \"<ul>\\n<li>Foo</li>\\n</ul>\\n<hr />\\n<ul>\\n<li>Bar</li>\\n</ul>\\n\",\n    \"example\": 30,\n    \"start_line\": 722,\n    \"end_line\": 734,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"- Foo\\n- * * *\\n\",\n    \"html\": \"<ul>\\n<li>Foo</li>\\n<li>\\n<hr />\\n</li>\\n</ul>\\n\",\n    \"example\": 31,\n    \"start_line\": 739,\n    \"end_line\": 749,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"# foo\\n## foo\\n### foo\\n#### foo\\n##### foo\\n###### foo\\n\",\n    \"html\": \"<h1>foo</h1>\\n<h2>foo</h2>\\n<h3>foo</h3>\\n<h4>foo</h4>\\n<h5>foo</h5>\\n<h6>foo</h6>\\n\",\n    \"example\": 32,\n    \"start_line\": 768,\n    \"end_line\": 782,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"####### foo\\n\",\n    \"html\": \"<p>####### foo</p>\\n\",\n    \"example\": 33,\n    \"start_line\": 787,\n    \"end_line\": 791,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"#5 bolt\\n\\n#hashtag\\n\",\n    \"html\": \"<p>#5 bolt</p>\\n<p>#hashtag</p>\\n\",\n    \"example\": 34,\n    \"start_line\": 802,\n    \"end_line\": 809,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"\\\\## foo\\n\",\n    \"html\": \"<p>## foo</p>\\n\",\n    \"example\": 35,\n    \"start_line\": 814,\n    \"end_line\": 818,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"# foo *bar* \\\\*baz\\\\*\\n\",\n    \"html\": \"<h1>foo <em>bar</em> *baz*</h1>\\n\",\n    \"example\": 36,\n    \"start_line\": 823,\n    \"end_line\": 827,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"#                  foo                     \\n\",\n    \"html\": \"<h1>foo</h1>\\n\",\n    \"example\": 37,\n    \"start_line\": 832,\n    \"end_line\": 836,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \" ### foo\\n  ## foo\\n   # foo\\n\",\n    \"html\": \"<h3>foo</h3>\\n<h2>foo</h2>\\n<h1>foo</h1>\\n\",\n    \"example\": 38,\n    \"start_line\": 841,\n    \"end_line\": 849,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"    # foo\\n\",\n    \"html\": \"<pre><code># foo\\n</code></pre>\\n\",\n    \"example\": 39,\n    \"start_line\": 854,\n    \"end_line\": 859,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"foo\\n    # bar\\n\",\n    \"html\": \"<p>foo\\n# bar</p>\\n\",\n    \"example\": 40,\n    \"start_line\": 862,\n    \"end_line\": 868,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"## foo ##\\n  ###   bar    ###\\n\",\n    \"html\": \"<h2>foo</h2>\\n<h3>bar</h3>\\n\",\n    \"example\": 41,\n    \"start_line\": 873,\n    \"end_line\": 879,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"# foo ##################################\\n##### foo ##\\n\",\n    \"html\": \"<h1>foo</h1>\\n<h5>foo</h5>\\n\",\n    \"example\": 42,\n    \"start_line\": 884,\n    \"end_line\": 890,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"### foo ###     \\n\",\n    \"html\": \"<h3>foo</h3>\\n\",\n    \"example\": 43,\n    \"start_line\": 895,\n    \"end_line\": 899,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"### foo ### b\\n\",\n    \"html\": \"<h3>foo ### b</h3>\\n\",\n    \"example\": 44,\n    \"start_line\": 906,\n    \"end_line\": 910,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"# foo#\\n\",\n    \"html\": \"<h1>foo#</h1>\\n\",\n    \"example\": 45,\n    \"start_line\": 915,\n    \"end_line\": 919,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"### foo \\\\###\\n## foo #\\\\##\\n# foo \\\\#\\n\",\n    \"html\": \"<h3>foo ###</h3>\\n<h2>foo ###</h2>\\n<h1>foo #</h1>\\n\",\n    \"example\": 46,\n    \"start_line\": 925,\n    \"end_line\": 933,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"****\\n## foo\\n****\\n\",\n    \"html\": \"<hr />\\n<h2>foo</h2>\\n<hr />\\n\",\n    \"example\": 47,\n    \"start_line\": 939,\n    \"end_line\": 947,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"Foo bar\\n# baz\\nBar foo\\n\",\n    \"html\": \"<p>Foo bar</p>\\n<h1>baz</h1>\\n<p>Bar foo</p>\\n\",\n    \"example\": 48,\n    \"start_line\": 950,\n    \"end_line\": 958,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"## \\n#\\n### ###\\n\",\n    \"html\": \"<h2></h2>\\n<h1></h1>\\n<h3></h3>\\n\",\n    \"example\": 49,\n    \"start_line\": 963,\n    \"end_line\": 971,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"Foo *bar*\\n=========\\n\\nFoo *bar*\\n---------\\n\",\n    \"html\": \"<h1>Foo <em>bar</em></h1>\\n<h2>Foo <em>bar</em></h2>\\n\",\n    \"example\": 50,\n    \"start_line\": 1006,\n    \"end_line\": 1015,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo *bar\\nbaz*\\n====\\n\",\n    \"html\": \"<h1>Foo <em>bar\\nbaz</em></h1>\\n\",\n    \"example\": 51,\n    \"start_line\": 1020,\n    \"end_line\": 1027,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"  Foo *bar\\nbaz*\\t\\n====\\n\",\n    \"html\": \"<h1>Foo <em>bar\\nbaz</em></h1>\\n\",\n    \"example\": 52,\n    \"start_line\": 1034,\n    \"end_line\": 1041,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\n-------------------------\\n\\nFoo\\n=\\n\",\n    \"html\": \"<h2>Foo</h2>\\n<h1>Foo</h1>\\n\",\n    \"example\": 53,\n    \"start_line\": 1046,\n    \"end_line\": 1055,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"   Foo\\n---\\n\\n  Foo\\n-----\\n\\n  Foo\\n  ===\\n\",\n    \"html\": \"<h2>Foo</h2>\\n<h2>Foo</h2>\\n<h1>Foo</h1>\\n\",\n    \"example\": 54,\n    \"start_line\": 1061,\n    \"end_line\": 1074,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"    Foo\\n    ---\\n\\n    Foo\\n---\\n\",\n    \"html\": \"<pre><code>Foo\\n---\\n\\nFoo\\n</code></pre>\\n<hr />\\n\",\n    \"example\": 55,\n    \"start_line\": 1079,\n    \"end_line\": 1092,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\n   ----      \\n\",\n    \"html\": \"<h2>Foo</h2>\\n\",\n    \"example\": 56,\n    \"start_line\": 1098,\n    \"end_line\": 1103,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\n    ---\\n\",\n    \"html\": \"<p>Foo\\n---</p>\\n\",\n    \"example\": 57,\n    \"start_line\": 1108,\n    \"end_line\": 1114,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\n= =\\n\\nFoo\\n--- -\\n\",\n    \"html\": \"<p>Foo\\n= =</p>\\n<p>Foo</p>\\n<hr />\\n\",\n    \"example\": 58,\n    \"start_line\": 1119,\n    \"end_line\": 1130,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo  \\n-----\\n\",\n    \"html\": \"<h2>Foo</h2>\\n\",\n    \"example\": 59,\n    \"start_line\": 1135,\n    \"end_line\": 1140,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\\\\\n----\\n\",\n    \"html\": \"<h2>Foo\\\\</h2>\\n\",\n    \"example\": 60,\n    \"start_line\": 1145,\n    \"end_line\": 1150,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"`Foo\\n----\\n`\\n\\n<a title=\\\"a lot\\n---\\nof dashes\\\"/>\\n\",\n    \"html\": \"<h2>`Foo</h2>\\n<p>`</p>\\n<h2>&lt;a title=&quot;a lot</h2>\\n<p>of dashes&quot;/&gt;</p>\\n\",\n    \"example\": 61,\n    \"start_line\": 1156,\n    \"end_line\": 1169,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"> Foo\\n---\\n\",\n    \"html\": \"<blockquote>\\n<p>Foo</p>\\n</blockquote>\\n<hr />\\n\",\n    \"example\": 62,\n    \"start_line\": 1175,\n    \"end_line\": 1183,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"> foo\\nbar\\n===\\n\",\n    \"html\": \"<blockquote>\\n<p>foo\\nbar\\n===</p>\\n</blockquote>\\n\",\n    \"example\": 63,\n    \"start_line\": 1186,\n    \"end_line\": 1196,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"- Foo\\n---\\n\",\n    \"html\": \"<ul>\\n<li>Foo</li>\\n</ul>\\n<hr />\\n\",\n    \"example\": 64,\n    \"start_line\": 1199,\n    \"end_line\": 1207,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\nBar\\n---\\n\",\n    \"html\": \"<h2>Foo\\nBar</h2>\\n\",\n    \"example\": 65,\n    \"start_line\": 1214,\n    \"end_line\": 1221,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"---\\nFoo\\n---\\nBar\\n---\\nBaz\\n\",\n    \"html\": \"<hr />\\n<h2>Foo</h2>\\n<h2>Bar</h2>\\n<p>Baz</p>\\n\",\n    \"example\": 66,\n    \"start_line\": 1227,\n    \"end_line\": 1239,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"\\n====\\n\",\n    \"html\": \"<p>====</p>\\n\",\n    \"example\": 67,\n    \"start_line\": 1244,\n    \"end_line\": 1249,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"---\\n---\\n\",\n    \"html\": \"<hr />\\n<hr />\\n\",\n    \"example\": 68,\n    \"start_line\": 1256,\n    \"end_line\": 1262,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"- foo\\n-----\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n</ul>\\n<hr />\\n\",\n    \"example\": 69,\n    \"start_line\": 1265,\n    \"end_line\": 1273,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"    foo\\n---\\n\",\n    \"html\": \"<pre><code>foo\\n</code></pre>\\n<hr />\\n\",\n    \"example\": 70,\n    \"start_line\": 1276,\n    \"end_line\": 1283,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"> foo\\n-----\\n\",\n    \"html\": \"<blockquote>\\n<p>foo</p>\\n</blockquote>\\n<hr />\\n\",\n    \"example\": 71,\n    \"start_line\": 1286,\n    \"end_line\": 1294,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"\\\\> foo\\n------\\n\",\n    \"html\": \"<h2>&gt; foo</h2>\\n\",\n    \"example\": 72,\n    \"start_line\": 1300,\n    \"end_line\": 1305,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\n\\nbar\\n---\\nbaz\\n\",\n    \"html\": \"<p>Foo</p>\\n<h2>bar</h2>\\n<p>baz</p>\\n\",\n    \"example\": 73,\n    \"start_line\": 1331,\n    \"end_line\": 1341,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\nbar\\n\\n---\\n\\nbaz\\n\",\n    \"html\": \"<p>Foo\\nbar</p>\\n<hr />\\n<p>baz</p>\\n\",\n    \"example\": 74,\n    \"start_line\": 1347,\n    \"end_line\": 1359,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\nbar\\n* * *\\nbaz\\n\",\n    \"html\": \"<p>Foo\\nbar</p>\\n<hr />\\n<p>baz</p>\\n\",\n    \"example\": 75,\n    \"start_line\": 1365,\n    \"end_line\": 1375,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\nbar\\n\\\\---\\nbaz\\n\",\n    \"html\": \"<p>Foo\\nbar\\n---\\nbaz</p>\\n\",\n    \"example\": 76,\n    \"start_line\": 1380,\n    \"end_line\": 1390,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"    a simple\\n      indented code block\\n\",\n    \"html\": \"<pre><code>a simple\\n  indented code block\\n</code></pre>\\n\",\n    \"example\": 77,\n    \"start_line\": 1408,\n    \"end_line\": 1415,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"  - foo\\n\\n    bar\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>foo</p>\\n<p>bar</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 78,\n    \"start_line\": 1422,\n    \"end_line\": 1433,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"1.  foo\\n\\n    - bar\\n\",\n    \"html\": \"<ol>\\n<li>\\n<p>foo</p>\\n<ul>\\n<li>bar</li>\\n</ul>\\n</li>\\n</ol>\\n\",\n    \"example\": 79,\n    \"start_line\": 1436,\n    \"end_line\": 1449,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"    <a/>\\n    *hi*\\n\\n    - one\\n\",\n    \"html\": \"<pre><code>&lt;a/&gt;\\n*hi*\\n\\n- one\\n</code></pre>\\n\",\n    \"example\": 80,\n    \"start_line\": 1456,\n    \"end_line\": 1467,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"    chunk1\\n\\n    chunk2\\n  \\n \\n \\n    chunk3\\n\",\n    \"html\": \"<pre><code>chunk1\\n\\nchunk2\\n\\n\\n\\nchunk3\\n</code></pre>\\n\",\n    \"example\": 81,\n    \"start_line\": 1472,\n    \"end_line\": 1489,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"    chunk1\\n      \\n      chunk2\\n\",\n    \"html\": \"<pre><code>chunk1\\n  \\n  chunk2\\n</code></pre>\\n\",\n    \"example\": 82,\n    \"start_line\": 1495,\n    \"end_line\": 1504,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"Foo\\n    bar\\n\\n\",\n    \"html\": \"<p>Foo\\nbar</p>\\n\",\n    \"example\": 83,\n    \"start_line\": 1510,\n    \"end_line\": 1517,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"    foo\\nbar\\n\",\n    \"html\": \"<pre><code>foo\\n</code></pre>\\n<p>bar</p>\\n\",\n    \"example\": 84,\n    \"start_line\": 1524,\n    \"end_line\": 1531,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"# Heading\\n    foo\\nHeading\\n------\\n    foo\\n----\\n\",\n    \"html\": \"<h1>Heading</h1>\\n<pre><code>foo\\n</code></pre>\\n<h2>Heading</h2>\\n<pre><code>foo\\n</code></pre>\\n<hr />\\n\",\n    \"example\": 85,\n    \"start_line\": 1537,\n    \"end_line\": 1552,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"        foo\\n    bar\\n\",\n    \"html\": \"<pre><code>    foo\\nbar\\n</code></pre>\\n\",\n    \"example\": 86,\n    \"start_line\": 1557,\n    \"end_line\": 1564,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"\\n    \\n    foo\\n    \\n\\n\",\n    \"html\": \"<pre><code>foo\\n</code></pre>\\n\",\n    \"example\": 87,\n    \"start_line\": 1570,\n    \"end_line\": 1579,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"    foo  \\n\",\n    \"html\": \"<pre><code>foo  \\n</code></pre>\\n\",\n    \"example\": 88,\n    \"start_line\": 1584,\n    \"end_line\": 1589,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"```\\n<\\n >\\n```\\n\",\n    \"html\": \"<pre><code>&lt;\\n &gt;\\n</code></pre>\\n\",\n    \"example\": 89,\n    \"start_line\": 1639,\n    \"end_line\": 1648,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"~~~\\n<\\n >\\n~~~\\n\",\n    \"html\": \"<pre><code>&lt;\\n &gt;\\n</code></pre>\\n\",\n    \"example\": 90,\n    \"start_line\": 1653,\n    \"end_line\": 1662,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"``\\nfoo\\n``\\n\",\n    \"html\": \"<p><code>foo</code></p>\\n\",\n    \"example\": 91,\n    \"start_line\": 1666,\n    \"end_line\": 1672,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"```\\naaa\\n~~~\\n```\\n\",\n    \"html\": \"<pre><code>aaa\\n~~~\\n</code></pre>\\n\",\n    \"example\": 92,\n    \"start_line\": 1677,\n    \"end_line\": 1686,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"~~~\\naaa\\n```\\n~~~\\n\",\n    \"html\": \"<pre><code>aaa\\n```\\n</code></pre>\\n\",\n    \"example\": 93,\n    \"start_line\": 1689,\n    \"end_line\": 1698,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"````\\naaa\\n```\\n``````\\n\",\n    \"html\": \"<pre><code>aaa\\n```\\n</code></pre>\\n\",\n    \"example\": 94,\n    \"start_line\": 1703,\n    \"end_line\": 1712,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"~~~~\\naaa\\n~~~\\n~~~~\\n\",\n    \"html\": \"<pre><code>aaa\\n~~~\\n</code></pre>\\n\",\n    \"example\": 95,\n    \"start_line\": 1715,\n    \"end_line\": 1724,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"```\\n\",\n    \"html\": \"<pre><code></code></pre>\\n\",\n    \"example\": 96,\n    \"start_line\": 1730,\n    \"end_line\": 1734,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"`````\\n\\n```\\naaa\\n\",\n    \"html\": \"<pre><code>\\n```\\naaa\\n</code></pre>\\n\",\n    \"example\": 97,\n    \"start_line\": 1737,\n    \"end_line\": 1747,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"> ```\\n> aaa\\n\\nbbb\\n\",\n    \"html\": \"<blockquote>\\n<pre><code>aaa\\n</code></pre>\\n</blockquote>\\n<p>bbb</p>\\n\",\n    \"example\": 98,\n    \"start_line\": 1750,\n    \"end_line\": 1761,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"```\\n\\n  \\n```\\n\",\n    \"html\": \"<pre><code>\\n  \\n</code></pre>\\n\",\n    \"example\": 99,\n    \"start_line\": 1766,\n    \"end_line\": 1775,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"```\\n```\\n\",\n    \"html\": \"<pre><code></code></pre>\\n\",\n    \"example\": 100,\n    \"start_line\": 1780,\n    \"end_line\": 1785,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \" ```\\n aaa\\naaa\\n```\\n\",\n    \"html\": \"<pre><code>aaa\\naaa\\n</code></pre>\\n\",\n    \"example\": 101,\n    \"start_line\": 1792,\n    \"end_line\": 1801,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"  ```\\naaa\\n  aaa\\naaa\\n  ```\\n\",\n    \"html\": \"<pre><code>aaa\\naaa\\naaa\\n</code></pre>\\n\",\n    \"example\": 102,\n    \"start_line\": 1804,\n    \"end_line\": 1815,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"   ```\\n   aaa\\n    aaa\\n  aaa\\n   ```\\n\",\n    \"html\": \"<pre><code>aaa\\n aaa\\naaa\\n</code></pre>\\n\",\n    \"example\": 103,\n    \"start_line\": 1818,\n    \"end_line\": 1829,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"    ```\\n    aaa\\n    ```\\n\",\n    \"html\": \"<pre><code>```\\naaa\\n```\\n</code></pre>\\n\",\n    \"example\": 104,\n    \"start_line\": 1834,\n    \"end_line\": 1843,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"```\\naaa\\n  ```\\n\",\n    \"html\": \"<pre><code>aaa\\n</code></pre>\\n\",\n    \"example\": 105,\n    \"start_line\": 1849,\n    \"end_line\": 1856,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"   ```\\naaa\\n  ```\\n\",\n    \"html\": \"<pre><code>aaa\\n</code></pre>\\n\",\n    \"example\": 106,\n    \"start_line\": 1859,\n    \"end_line\": 1866,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"```\\naaa\\n    ```\\n\",\n    \"html\": \"<pre><code>aaa\\n    ```\\n</code></pre>\\n\",\n    \"example\": 107,\n    \"start_line\": 1871,\n    \"end_line\": 1879,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"``` ```\\naaa\\n\",\n    \"html\": \"<p><code> </code>\\naaa</p>\\n\",\n    \"example\": 108,\n    \"start_line\": 1885,\n    \"end_line\": 1891,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"~~~~~~\\naaa\\n~~~ ~~\\n\",\n    \"html\": \"<pre><code>aaa\\n~~~ ~~\\n</code></pre>\\n\",\n    \"example\": 109,\n    \"start_line\": 1894,\n    \"end_line\": 1902,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"foo\\n```\\nbar\\n```\\nbaz\\n\",\n    \"html\": \"<p>foo</p>\\n<pre><code>bar\\n</code></pre>\\n<p>baz</p>\\n\",\n    \"example\": 110,\n    \"start_line\": 1908,\n    \"end_line\": 1919,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"foo\\n---\\n~~~\\nbar\\n~~~\\n# baz\\n\",\n    \"html\": \"<h2>foo</h2>\\n<pre><code>bar\\n</code></pre>\\n<h1>baz</h1>\\n\",\n    \"example\": 111,\n    \"start_line\": 1925,\n    \"end_line\": 1937,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"```ruby\\ndef foo(x)\\n  return 3\\nend\\n```\\n\",\n    \"html\": \"<pre><code class=\\\"language-ruby\\\">def foo(x)\\n  return 3\\nend\\n</code></pre>\\n\",\n    \"example\": 112,\n    \"start_line\": 1947,\n    \"end_line\": 1958,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"~~~~    ruby startline=3 $%@#$\\ndef foo(x)\\n  return 3\\nend\\n~~~~~~~\\n\",\n    \"html\": \"<pre><code class=\\\"language-ruby\\\">def foo(x)\\n  return 3\\nend\\n</code></pre>\\n\",\n    \"example\": 113,\n    \"start_line\": 1961,\n    \"end_line\": 1972,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"````;\\n````\\n\",\n    \"html\": \"<pre><code class=\\\"language-;\\\"></code></pre>\\n\",\n    \"example\": 114,\n    \"start_line\": 1975,\n    \"end_line\": 1980,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"``` aa ```\\nfoo\\n\",\n    \"html\": \"<p><code>aa</code>\\nfoo</p>\\n\",\n    \"example\": 115,\n    \"start_line\": 1985,\n    \"end_line\": 1991,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"~~~ aa ``` ~~~\\nfoo\\n~~~\\n\",\n    \"html\": \"<pre><code class=\\\"language-aa\\\">foo\\n</code></pre>\\n\",\n    \"example\": 116,\n    \"start_line\": 1996,\n    \"end_line\": 2003,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"```\\n``` aaa\\n```\\n\",\n    \"html\": \"<pre><code>``` aaa\\n</code></pre>\\n\",\n    \"example\": 117,\n    \"start_line\": 2008,\n    \"end_line\": 2015,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"<table><tr><td>\\n<pre>\\n**Hello**,\\n\\n_world_.\\n</pre>\\n</td></tr></table>\\n\",\n    \"html\": \"<table><tr><td>\\n<pre>\\n**Hello**,\\n<p><em>world</em>.\\n</pre></p>\\n</td></tr></table>\\n\",\n    \"example\": 118,\n    \"start_line\": 2087,\n    \"end_line\": 2102,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<table>\\n  <tr>\\n    <td>\\n           hi\\n    </td>\\n  </tr>\\n</table>\\n\\nokay.\\n\",\n    \"html\": \"<table>\\n  <tr>\\n    <td>\\n           hi\\n    </td>\\n  </tr>\\n</table>\\n<p>okay.</p>\\n\",\n    \"example\": 119,\n    \"start_line\": 2116,\n    \"end_line\": 2135,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \" <div>\\n  *hello*\\n         <foo><a>\\n\",\n    \"html\": \" <div>\\n  *hello*\\n         <foo><a>\\n\",\n    \"example\": 120,\n    \"start_line\": 2138,\n    \"end_line\": 2146,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"</div>\\n*foo*\\n\",\n    \"html\": \"</div>\\n*foo*\\n\",\n    \"example\": 121,\n    \"start_line\": 2151,\n    \"end_line\": 2157,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<DIV CLASS=\\\"foo\\\">\\n\\n*Markdown*\\n\\n</DIV>\\n\",\n    \"html\": \"<DIV CLASS=\\\"foo\\\">\\n<p><em>Markdown</em></p>\\n</DIV>\\n\",\n    \"example\": 122,\n    \"start_line\": 2162,\n    \"end_line\": 2172,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div id=\\\"foo\\\"\\n  class=\\\"bar\\\">\\n</div>\\n\",\n    \"html\": \"<div id=\\\"foo\\\"\\n  class=\\\"bar\\\">\\n</div>\\n\",\n    \"example\": 123,\n    \"start_line\": 2178,\n    \"end_line\": 2186,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div id=\\\"foo\\\" class=\\\"bar\\n  baz\\\">\\n</div>\\n\",\n    \"html\": \"<div id=\\\"foo\\\" class=\\\"bar\\n  baz\\\">\\n</div>\\n\",\n    \"example\": 124,\n    \"start_line\": 2189,\n    \"end_line\": 2197,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div>\\n*foo*\\n\\n*bar*\\n\",\n    \"html\": \"<div>\\n*foo*\\n<p><em>bar</em></p>\\n\",\n    \"example\": 125,\n    \"start_line\": 2201,\n    \"end_line\": 2210,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div id=\\\"foo\\\"\\n*hi*\\n\",\n    \"html\": \"<div id=\\\"foo\\\"\\n*hi*\\n\",\n    \"example\": 126,\n    \"start_line\": 2217,\n    \"end_line\": 2223,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div class\\nfoo\\n\",\n    \"html\": \"<div class\\nfoo\\n\",\n    \"example\": 127,\n    \"start_line\": 2226,\n    \"end_line\": 2232,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div *???-&&&-<---\\n*foo*\\n\",\n    \"html\": \"<div *???-&&&-<---\\n*foo*\\n\",\n    \"example\": 128,\n    \"start_line\": 2238,\n    \"end_line\": 2244,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div><a href=\\\"bar\\\">*foo*</a></div>\\n\",\n    \"html\": \"<div><a href=\\\"bar\\\">*foo*</a></div>\\n\",\n    \"example\": 129,\n    \"start_line\": 2250,\n    \"end_line\": 2254,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<table><tr><td>\\nfoo\\n</td></tr></table>\\n\",\n    \"html\": \"<table><tr><td>\\nfoo\\n</td></tr></table>\\n\",\n    \"example\": 130,\n    \"start_line\": 2257,\n    \"end_line\": 2265,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div></div>\\n``` c\\nint x = 33;\\n```\\n\",\n    \"html\": \"<div></div>\\n``` c\\nint x = 33;\\n```\\n\",\n    \"example\": 131,\n    \"start_line\": 2274,\n    \"end_line\": 2284,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<a href=\\\"foo\\\">\\n*bar*\\n</a>\\n\",\n    \"html\": \"<a href=\\\"foo\\\">\\n*bar*\\n</a>\\n\",\n    \"example\": 132,\n    \"start_line\": 2291,\n    \"end_line\": 2299,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<Warning>\\n*bar*\\n</Warning>\\n\",\n    \"html\": \"<Warning>\\n*bar*\\n</Warning>\\n\",\n    \"example\": 133,\n    \"start_line\": 2304,\n    \"end_line\": 2312,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<i class=\\\"foo\\\">\\n*bar*\\n</i>\\n\",\n    \"html\": \"<i class=\\\"foo\\\">\\n*bar*\\n</i>\\n\",\n    \"example\": 134,\n    \"start_line\": 2315,\n    \"end_line\": 2323,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"</ins>\\n*bar*\\n\",\n    \"html\": \"</ins>\\n*bar*\\n\",\n    \"example\": 135,\n    \"start_line\": 2326,\n    \"end_line\": 2332,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<del>\\n*foo*\\n</del>\\n\",\n    \"html\": \"<del>\\n*foo*\\n</del>\\n\",\n    \"example\": 136,\n    \"start_line\": 2341,\n    \"end_line\": 2349,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<del>\\n\\n*foo*\\n\\n</del>\\n\",\n    \"html\": \"<del>\\n<p><em>foo</em></p>\\n</del>\\n\",\n    \"example\": 137,\n    \"start_line\": 2356,\n    \"end_line\": 2366,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<del>*foo*</del>\\n\",\n    \"html\": \"<p><del><em>foo</em></del></p>\\n\",\n    \"example\": 138,\n    \"start_line\": 2374,\n    \"end_line\": 2378,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<pre language=\\\"haskell\\\"><code>\\nimport Text.HTML.TagSoup\\n\\nmain :: IO ()\\nmain = print $ parseTags tags\\n</code></pre>\\nokay\\n\",\n    \"html\": \"<pre language=\\\"haskell\\\"><code>\\nimport Text.HTML.TagSoup\\n\\nmain :: IO ()\\nmain = print $ parseTags tags\\n</code></pre>\\n<p>okay</p>\\n\",\n    \"example\": 139,\n    \"start_line\": 2390,\n    \"end_line\": 2406,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<script type=\\\"text/javascript\\\">\\n// JavaScript example\\n\\ndocument.getElementById(\\\"demo\\\").innerHTML = \\\"Hello JavaScript!\\\";\\n</script>\\nokay\\n\",\n    \"html\": \"<script type=\\\"text/javascript\\\">\\n// JavaScript example\\n\\ndocument.getElementById(\\\"demo\\\").innerHTML = \\\"Hello JavaScript!\\\";\\n</script>\\n<p>okay</p>\\n\",\n    \"example\": 140,\n    \"start_line\": 2411,\n    \"end_line\": 2425,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<style\\n  type=\\\"text/css\\\">\\nh1 {color:red;}\\n\\np {color:blue;}\\n</style>\\nokay\\n\",\n    \"html\": \"<style\\n  type=\\\"text/css\\\">\\nh1 {color:red;}\\n\\np {color:blue;}\\n</style>\\n<p>okay</p>\\n\",\n    \"example\": 141,\n    \"start_line\": 2430,\n    \"end_line\": 2446,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<style\\n  type=\\\"text/css\\\">\\n\\nfoo\\n\",\n    \"html\": \"<style\\n  type=\\\"text/css\\\">\\n\\nfoo\\n\",\n    \"example\": 142,\n    \"start_line\": 2453,\n    \"end_line\": 2463,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"> <div>\\n> foo\\n\\nbar\\n\",\n    \"html\": \"<blockquote>\\n<div>\\nfoo\\n</blockquote>\\n<p>bar</p>\\n\",\n    \"example\": 143,\n    \"start_line\": 2466,\n    \"end_line\": 2477,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"- <div>\\n- foo\\n\",\n    \"html\": \"<ul>\\n<li>\\n<div>\\n</li>\\n<li>foo</li>\\n</ul>\\n\",\n    \"example\": 144,\n    \"start_line\": 2480,\n    \"end_line\": 2490,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<style>p{color:red;}</style>\\n*foo*\\n\",\n    \"html\": \"<style>p{color:red;}</style>\\n<p><em>foo</em></p>\\n\",\n    \"example\": 145,\n    \"start_line\": 2495,\n    \"end_line\": 2501,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<!-- foo -->*bar*\\n*baz*\\n\",\n    \"html\": \"<!-- foo -->*bar*\\n<p><em>baz</em></p>\\n\",\n    \"example\": 146,\n    \"start_line\": 2504,\n    \"end_line\": 2510,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<script>\\nfoo\\n</script>1. *bar*\\n\",\n    \"html\": \"<script>\\nfoo\\n</script>1. *bar*\\n\",\n    \"example\": 147,\n    \"start_line\": 2516,\n    \"end_line\": 2524,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<!-- Foo\\n\\nbar\\n   baz -->\\nokay\\n\",\n    \"html\": \"<!-- Foo\\n\\nbar\\n   baz -->\\n<p>okay</p>\\n\",\n    \"example\": 148,\n    \"start_line\": 2529,\n    \"end_line\": 2541,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<?php\\n\\n  echo '>';\\n\\n?>\\nokay\\n\",\n    \"html\": \"<?php\\n\\n  echo '>';\\n\\n?>\\n<p>okay</p>\\n\",\n    \"example\": 149,\n    \"start_line\": 2547,\n    \"end_line\": 2561,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<!DOCTYPE html>\\n\",\n    \"html\": \"<!DOCTYPE html>\\n\",\n    \"example\": 150,\n    \"start_line\": 2566,\n    \"end_line\": 2570,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<![CDATA[\\nfunction matchwo(a,b)\\n{\\n  if (a < b && a < 0) then {\\n    return 1;\\n\\n  } else {\\n\\n    return 0;\\n  }\\n}\\n]]>\\nokay\\n\",\n    \"html\": \"<![CDATA[\\nfunction matchwo(a,b)\\n{\\n  if (a < b && a < 0) then {\\n    return 1;\\n\\n  } else {\\n\\n    return 0;\\n  }\\n}\\n]]>\\n<p>okay</p>\\n\",\n    \"example\": 151,\n    \"start_line\": 2575,\n    \"end_line\": 2603,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"  <!-- foo -->\\n\\n    <!-- foo -->\\n\",\n    \"html\": \"  <!-- foo -->\\n<pre><code>&lt;!-- foo --&gt;\\n</code></pre>\\n\",\n    \"example\": 152,\n    \"start_line\": 2608,\n    \"end_line\": 2616,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"  <div>\\n\\n    <div>\\n\",\n    \"html\": \"  <div>\\n<pre><code>&lt;div&gt;\\n</code></pre>\\n\",\n    \"example\": 153,\n    \"start_line\": 2619,\n    \"end_line\": 2627,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"Foo\\n<div>\\nbar\\n</div>\\n\",\n    \"html\": \"<p>Foo</p>\\n<div>\\nbar\\n</div>\\n\",\n    \"example\": 154,\n    \"start_line\": 2633,\n    \"end_line\": 2643,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div>\\nbar\\n</div>\\n*foo*\\n\",\n    \"html\": \"<div>\\nbar\\n</div>\\n*foo*\\n\",\n    \"example\": 155,\n    \"start_line\": 2650,\n    \"end_line\": 2660,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"Foo\\n<a href=\\\"bar\\\">\\nbaz\\n\",\n    \"html\": \"<p>Foo\\n<a href=\\\"bar\\\">\\nbaz</p>\\n\",\n    \"example\": 156,\n    \"start_line\": 2665,\n    \"end_line\": 2673,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div>\\n\\n*Emphasized* text.\\n\\n</div>\\n\",\n    \"html\": \"<div>\\n<p><em>Emphasized</em> text.</p>\\n</div>\\n\",\n    \"example\": 157,\n    \"start_line\": 2706,\n    \"end_line\": 2716,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div>\\n*Emphasized* text.\\n</div>\\n\",\n    \"html\": \"<div>\\n*Emphasized* text.\\n</div>\\n\",\n    \"example\": 158,\n    \"start_line\": 2719,\n    \"end_line\": 2727,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<table>\\n\\n<tr>\\n\\n<td>\\nHi\\n</td>\\n\\n</tr>\\n\\n</table>\\n\",\n    \"html\": \"<table>\\n<tr>\\n<td>\\nHi\\n</td>\\n</tr>\\n</table>\\n\",\n    \"example\": 159,\n    \"start_line\": 2741,\n    \"end_line\": 2761,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<table>\\n\\n  <tr>\\n\\n    <td>\\n      Hi\\n    </td>\\n\\n  </tr>\\n\\n</table>\\n\",\n    \"html\": \"<table>\\n  <tr>\\n<pre><code>&lt;td&gt;\\n  Hi\\n&lt;/td&gt;\\n</code></pre>\\n  </tr>\\n</table>\\n\",\n    \"example\": 160,\n    \"start_line\": 2768,\n    \"end_line\": 2789,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"[foo]: /url \\\"title\\\"\\n\\n[foo]\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\">foo</a></p>\\n\",\n    \"example\": 161,\n    \"start_line\": 2816,\n    \"end_line\": 2822,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"   [foo]: \\n      /url  \\n           'the title'  \\n\\n[foo]\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"the title\\\">foo</a></p>\\n\",\n    \"example\": 162,\n    \"start_line\": 2825,\n    \"end_line\": 2833,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[Foo*bar\\\\]]:my_(url) 'title (with parens)'\\n\\n[Foo*bar\\\\]]\\n\",\n    \"html\": \"<p><a href=\\\"my_(url)\\\" title=\\\"title (with parens)\\\">Foo*bar]</a></p>\\n\",\n    \"example\": 163,\n    \"start_line\": 2836,\n    \"end_line\": 2842,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[Foo bar]:\\n<my url>\\n'title'\\n\\n[Foo bar]\\n\",\n    \"html\": \"<p><a href=\\\"my%20url\\\" title=\\\"title\\\">Foo bar</a></p>\\n\",\n    \"example\": 164,\n    \"start_line\": 2845,\n    \"end_line\": 2853,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: /url '\\ntitle\\nline1\\nline2\\n'\\n\\n[foo]\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"\\ntitle\\nline1\\nline2\\n\\\">foo</a></p>\\n\",\n    \"example\": 165,\n    \"start_line\": 2858,\n    \"end_line\": 2872,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: /url 'title\\n\\nwith blank line'\\n\\n[foo]\\n\",\n    \"html\": \"<p>[foo]: /url 'title</p>\\n<p>with blank line'</p>\\n<p>[foo]</p>\\n\",\n    \"example\": 166,\n    \"start_line\": 2877,\n    \"end_line\": 2887,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]:\\n/url\\n\\n[foo]\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\">foo</a></p>\\n\",\n    \"example\": 167,\n    \"start_line\": 2892,\n    \"end_line\": 2899,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]:\\n\\n[foo]\\n\",\n    \"html\": \"<p>[foo]:</p>\\n<p>[foo]</p>\\n\",\n    \"example\": 168,\n    \"start_line\": 2904,\n    \"end_line\": 2911,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: <>\\n\\n[foo]\\n\",\n    \"html\": \"<p><a href=\\\"\\\">foo</a></p>\\n\",\n    \"example\": 169,\n    \"start_line\": 2916,\n    \"end_line\": 2922,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: <bar>(baz)\\n\\n[foo]\\n\",\n    \"html\": \"<p>[foo]: <bar>(baz)</p>\\n<p>[foo]</p>\\n\",\n    \"example\": 170,\n    \"start_line\": 2927,\n    \"end_line\": 2934,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: /url\\\\bar\\\\*baz \\\"foo\\\\\\\"bar\\\\baz\\\"\\n\\n[foo]\\n\",\n    \"html\": \"<p><a href=\\\"/url%5Cbar*baz\\\" title=\\\"foo&quot;bar\\\\baz\\\">foo</a></p>\\n\",\n    \"example\": 171,\n    \"start_line\": 2940,\n    \"end_line\": 2946,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]\\n\\n[foo]: url\\n\",\n    \"html\": \"<p><a href=\\\"url\\\">foo</a></p>\\n\",\n    \"example\": 172,\n    \"start_line\": 2951,\n    \"end_line\": 2957,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]\\n\\n[foo]: first\\n[foo]: second\\n\",\n    \"html\": \"<p><a href=\\\"first\\\">foo</a></p>\\n\",\n    \"example\": 173,\n    \"start_line\": 2963,\n    \"end_line\": 2970,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[FOO]: /url\\n\\n[Foo]\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\">Foo</a></p>\\n\",\n    \"example\": 174,\n    \"start_line\": 2976,\n    \"end_line\": 2982,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[ΑΓΩ]: /φου\\n\\n[αγω]\\n\",\n    \"html\": \"<p><a href=\\\"/%CF%86%CE%BF%CF%85\\\">αγω</a></p>\\n\",\n    \"example\": 175,\n    \"start_line\": 2985,\n    \"end_line\": 2991,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: /url\\n\",\n    \"html\": \"\",\n    \"example\": 176,\n    \"start_line\": 2997,\n    \"end_line\": 3000,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[\\nfoo\\n]: /url\\nbar\\n\",\n    \"html\": \"<p>bar</p>\\n\",\n    \"example\": 177,\n    \"start_line\": 3005,\n    \"end_line\": 3012,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: /url \\\"title\\\" ok\\n\",\n    \"html\": \"<p>[foo]: /url &quot;title&quot; ok</p>\\n\",\n    \"example\": 178,\n    \"start_line\": 3018,\n    \"end_line\": 3022,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: /url\\n\\\"title\\\" ok\\n\",\n    \"html\": \"<p>&quot;title&quot; ok</p>\\n\",\n    \"example\": 179,\n    \"start_line\": 3027,\n    \"end_line\": 3032,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"    [foo]: /url \\\"title\\\"\\n\\n[foo]\\n\",\n    \"html\": \"<pre><code>[foo]: /url &quot;title&quot;\\n</code></pre>\\n<p>[foo]</p>\\n\",\n    \"example\": 180,\n    \"start_line\": 3038,\n    \"end_line\": 3046,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"```\\n[foo]: /url\\n```\\n\\n[foo]\\n\",\n    \"html\": \"<pre><code>[foo]: /url\\n</code></pre>\\n<p>[foo]</p>\\n\",\n    \"example\": 181,\n    \"start_line\": 3052,\n    \"end_line\": 3062,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"Foo\\n[bar]: /baz\\n\\n[bar]\\n\",\n    \"html\": \"<p>Foo\\n[bar]: /baz</p>\\n<p>[bar]</p>\\n\",\n    \"example\": 182,\n    \"start_line\": 3067,\n    \"end_line\": 3076,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"# [Foo]\\n[foo]: /url\\n> bar\\n\",\n    \"html\": \"<h1><a href=\\\"/url\\\">Foo</a></h1>\\n<blockquote>\\n<p>bar</p>\\n</blockquote>\\n\",\n    \"example\": 183,\n    \"start_line\": 3082,\n    \"end_line\": 3091,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: /url\\nbar\\n===\\n[foo]\\n\",\n    \"html\": \"<h1>bar</h1>\\n<p><a href=\\\"/url\\\">foo</a></p>\\n\",\n    \"example\": 184,\n    \"start_line\": 3093,\n    \"end_line\": 3101,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: /url\\n===\\n[foo]\\n\",\n    \"html\": \"<p>===\\n<a href=\\\"/url\\\">foo</a></p>\\n\",\n    \"example\": 185,\n    \"start_line\": 3103,\n    \"end_line\": 3110,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: /foo-url \\\"foo\\\"\\n[bar]: /bar-url\\n  \\\"bar\\\"\\n[baz]: /baz-url\\n\\n[foo],\\n[bar],\\n[baz]\\n\",\n    \"html\": \"<p><a href=\\\"/foo-url\\\" title=\\\"foo\\\">foo</a>,\\n<a href=\\\"/bar-url\\\" title=\\\"bar\\\">bar</a>,\\n<a href=\\\"/baz-url\\\">baz</a></p>\\n\",\n    \"example\": 186,\n    \"start_line\": 3116,\n    \"end_line\": 3129,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]\\n\\n> [foo]: /url\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\">foo</a></p>\\n<blockquote>\\n</blockquote>\\n\",\n    \"example\": 187,\n    \"start_line\": 3137,\n    \"end_line\": 3145,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: /url\\n\",\n    \"html\": \"\",\n    \"example\": 188,\n    \"start_line\": 3154,\n    \"end_line\": 3157,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"aaa\\n\\nbbb\\n\",\n    \"html\": \"<p>aaa</p>\\n<p>bbb</p>\\n\",\n    \"example\": 189,\n    \"start_line\": 3171,\n    \"end_line\": 3178,\n    \"section\": \"Paragraphs\"\n  },\n  {\n    \"markdown\": \"aaa\\nbbb\\n\\nccc\\nddd\\n\",\n    \"html\": \"<p>aaa\\nbbb</p>\\n<p>ccc\\nddd</p>\\n\",\n    \"example\": 190,\n    \"start_line\": 3183,\n    \"end_line\": 3194,\n    \"section\": \"Paragraphs\"\n  },\n  {\n    \"markdown\": \"aaa\\n\\n\\nbbb\\n\",\n    \"html\": \"<p>aaa</p>\\n<p>bbb</p>\\n\",\n    \"example\": 191,\n    \"start_line\": 3199,\n    \"end_line\": 3207,\n    \"section\": \"Paragraphs\"\n  },\n  {\n    \"markdown\": \"  aaa\\n bbb\\n\",\n    \"html\": \"<p>aaa\\nbbb</p>\\n\",\n    \"example\": 192,\n    \"start_line\": 3212,\n    \"end_line\": 3218,\n    \"section\": \"Paragraphs\"\n  },\n  {\n    \"markdown\": \"aaa\\n             bbb\\n                                       ccc\\n\",\n    \"html\": \"<p>aaa\\nbbb\\nccc</p>\\n\",\n    \"example\": 193,\n    \"start_line\": 3224,\n    \"end_line\": 3232,\n    \"section\": \"Paragraphs\"\n  },\n  {\n    \"markdown\": \"   aaa\\nbbb\\n\",\n    \"html\": \"<p>aaa\\nbbb</p>\\n\",\n    \"example\": 194,\n    \"start_line\": 3238,\n    \"end_line\": 3244,\n    \"section\": \"Paragraphs\"\n  },\n  {\n    \"markdown\": \"    aaa\\nbbb\\n\",\n    \"html\": \"<pre><code>aaa\\n</code></pre>\\n<p>bbb</p>\\n\",\n    \"example\": 195,\n    \"start_line\": 3247,\n    \"end_line\": 3254,\n    \"section\": \"Paragraphs\"\n  },\n  {\n    \"markdown\": \"aaa     \\nbbb     \\n\",\n    \"html\": \"<p>aaa<br />\\nbbb</p>\\n\",\n    \"example\": 196,\n    \"start_line\": 3261,\n    \"end_line\": 3267,\n    \"section\": \"Paragraphs\"\n  },\n  {\n    \"markdown\": \"  \\n\\naaa\\n  \\n\\n# aaa\\n\\n  \\n\",\n    \"html\": \"<p>aaa</p>\\n<h1>aaa</h1>\\n\",\n    \"example\": 197,\n    \"start_line\": 3278,\n    \"end_line\": 3290,\n    \"section\": \"Blank lines\"\n  },\n  {\n    \"markdown\": \"> # Foo\\n> bar\\n> baz\\n\",\n    \"html\": \"<blockquote>\\n<h1>Foo</h1>\\n<p>bar\\nbaz</p>\\n</blockquote>\\n\",\n    \"example\": 198,\n    \"start_line\": 3344,\n    \"end_line\": 3354,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"># Foo\\n>bar\\n> baz\\n\",\n    \"html\": \"<blockquote>\\n<h1>Foo</h1>\\n<p>bar\\nbaz</p>\\n</blockquote>\\n\",\n    \"example\": 199,\n    \"start_line\": 3359,\n    \"end_line\": 3369,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"   > # Foo\\n   > bar\\n > baz\\n\",\n    \"html\": \"<blockquote>\\n<h1>Foo</h1>\\n<p>bar\\nbaz</p>\\n</blockquote>\\n\",\n    \"example\": 200,\n    \"start_line\": 3374,\n    \"end_line\": 3384,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"    > # Foo\\n    > bar\\n    > baz\\n\",\n    \"html\": \"<pre><code>&gt; # Foo\\n&gt; bar\\n&gt; baz\\n</code></pre>\\n\",\n    \"example\": 201,\n    \"start_line\": 3389,\n    \"end_line\": 3398,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> # Foo\\n> bar\\nbaz\\n\",\n    \"html\": \"<blockquote>\\n<h1>Foo</h1>\\n<p>bar\\nbaz</p>\\n</blockquote>\\n\",\n    \"example\": 202,\n    \"start_line\": 3404,\n    \"end_line\": 3414,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> bar\\nbaz\\n> foo\\n\",\n    \"html\": \"<blockquote>\\n<p>bar\\nbaz\\nfoo</p>\\n</blockquote>\\n\",\n    \"example\": 203,\n    \"start_line\": 3420,\n    \"end_line\": 3430,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> foo\\n---\\n\",\n    \"html\": \"<blockquote>\\n<p>foo</p>\\n</blockquote>\\n<hr />\\n\",\n    \"example\": 204,\n    \"start_line\": 3444,\n    \"end_line\": 3452,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> - foo\\n- bar\\n\",\n    \"html\": \"<blockquote>\\n<ul>\\n<li>foo</li>\\n</ul>\\n</blockquote>\\n<ul>\\n<li>bar</li>\\n</ul>\\n\",\n    \"example\": 205,\n    \"start_line\": 3464,\n    \"end_line\": 3476,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \">     foo\\n    bar\\n\",\n    \"html\": \"<blockquote>\\n<pre><code>foo\\n</code></pre>\\n</blockquote>\\n<pre><code>bar\\n</code></pre>\\n\",\n    \"example\": 206,\n    \"start_line\": 3482,\n    \"end_line\": 3492,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> ```\\nfoo\\n```\\n\",\n    \"html\": \"<blockquote>\\n<pre><code></code></pre>\\n</blockquote>\\n<p>foo</p>\\n<pre><code></code></pre>\\n\",\n    \"example\": 207,\n    \"start_line\": 3495,\n    \"end_line\": 3505,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> foo\\n    - bar\\n\",\n    \"html\": \"<blockquote>\\n<p>foo\\n- bar</p>\\n</blockquote>\\n\",\n    \"example\": 208,\n    \"start_line\": 3511,\n    \"end_line\": 3519,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \">\\n\",\n    \"html\": \"<blockquote>\\n</blockquote>\\n\",\n    \"example\": 209,\n    \"start_line\": 3535,\n    \"end_line\": 3540,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \">\\n>  \\n> \\n\",\n    \"html\": \"<blockquote>\\n</blockquote>\\n\",\n    \"example\": 210,\n    \"start_line\": 3543,\n    \"end_line\": 3550,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \">\\n> foo\\n>  \\n\",\n    \"html\": \"<blockquote>\\n<p>foo</p>\\n</blockquote>\\n\",\n    \"example\": 211,\n    \"start_line\": 3555,\n    \"end_line\": 3563,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> foo\\n\\n> bar\\n\",\n    \"html\": \"<blockquote>\\n<p>foo</p>\\n</blockquote>\\n<blockquote>\\n<p>bar</p>\\n</blockquote>\\n\",\n    \"example\": 212,\n    \"start_line\": 3568,\n    \"end_line\": 3579,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> foo\\n> bar\\n\",\n    \"html\": \"<blockquote>\\n<p>foo\\nbar</p>\\n</blockquote>\\n\",\n    \"example\": 213,\n    \"start_line\": 3590,\n    \"end_line\": 3598,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> foo\\n>\\n> bar\\n\",\n    \"html\": \"<blockquote>\\n<p>foo</p>\\n<p>bar</p>\\n</blockquote>\\n\",\n    \"example\": 214,\n    \"start_line\": 3603,\n    \"end_line\": 3612,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"foo\\n> bar\\n\",\n    \"html\": \"<p>foo</p>\\n<blockquote>\\n<p>bar</p>\\n</blockquote>\\n\",\n    \"example\": 215,\n    \"start_line\": 3617,\n    \"end_line\": 3625,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> aaa\\n***\\n> bbb\\n\",\n    \"html\": \"<blockquote>\\n<p>aaa</p>\\n</blockquote>\\n<hr />\\n<blockquote>\\n<p>bbb</p>\\n</blockquote>\\n\",\n    \"example\": 216,\n    \"start_line\": 3631,\n    \"end_line\": 3643,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> bar\\nbaz\\n\",\n    \"html\": \"<blockquote>\\n<p>bar\\nbaz</p>\\n</blockquote>\\n\",\n    \"example\": 217,\n    \"start_line\": 3649,\n    \"end_line\": 3657,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> bar\\n\\nbaz\\n\",\n    \"html\": \"<blockquote>\\n<p>bar</p>\\n</blockquote>\\n<p>baz</p>\\n\",\n    \"example\": 218,\n    \"start_line\": 3660,\n    \"end_line\": 3669,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> bar\\n>\\nbaz\\n\",\n    \"html\": \"<blockquote>\\n<p>bar</p>\\n</blockquote>\\n<p>baz</p>\\n\",\n    \"example\": 219,\n    \"start_line\": 3672,\n    \"end_line\": 3681,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> > > foo\\nbar\\n\",\n    \"html\": \"<blockquote>\\n<blockquote>\\n<blockquote>\\n<p>foo\\nbar</p>\\n</blockquote>\\n</blockquote>\\n</blockquote>\\n\",\n    \"example\": 220,\n    \"start_line\": 3688,\n    \"end_line\": 3700,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \">>> foo\\n> bar\\n>>baz\\n\",\n    \"html\": \"<blockquote>\\n<blockquote>\\n<blockquote>\\n<p>foo\\nbar\\nbaz</p>\\n</blockquote>\\n</blockquote>\\n</blockquote>\\n\",\n    \"example\": 221,\n    \"start_line\": 3703,\n    \"end_line\": 3717,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \">     code\\n\\n>    not code\\n\",\n    \"html\": \"<blockquote>\\n<pre><code>code\\n</code></pre>\\n</blockquote>\\n<blockquote>\\n<p>not code</p>\\n</blockquote>\\n\",\n    \"example\": 222,\n    \"start_line\": 3725,\n    \"end_line\": 3737,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"A paragraph\\nwith two lines.\\n\\n    indented code\\n\\n> A block quote.\\n\",\n    \"html\": \"<p>A paragraph\\nwith two lines.</p>\\n<pre><code>indented code\\n</code></pre>\\n<blockquote>\\n<p>A block quote.</p>\\n</blockquote>\\n\",\n    \"example\": 223,\n    \"start_line\": 3779,\n    \"end_line\": 3794,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"1.  A paragraph\\n    with two lines.\\n\\n        indented code\\n\\n    > A block quote.\\n\",\n    \"html\": \"<ol>\\n<li>\\n<p>A paragraph\\nwith two lines.</p>\\n<pre><code>indented code\\n</code></pre>\\n<blockquote>\\n<p>A block quote.</p>\\n</blockquote>\\n</li>\\n</ol>\\n\",\n    \"example\": 224,\n    \"start_line\": 3801,\n    \"end_line\": 3820,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- one\\n\\n two\\n\",\n    \"html\": \"<ul>\\n<li>one</li>\\n</ul>\\n<p>two</p>\\n\",\n    \"example\": 225,\n    \"start_line\": 3834,\n    \"end_line\": 3843,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- one\\n\\n  two\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>one</p>\\n<p>two</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 226,\n    \"start_line\": 3846,\n    \"end_line\": 3857,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \" -    one\\n\\n     two\\n\",\n    \"html\": \"<ul>\\n<li>one</li>\\n</ul>\\n<pre><code> two\\n</code></pre>\\n\",\n    \"example\": 227,\n    \"start_line\": 3860,\n    \"end_line\": 3870,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \" -    one\\n\\n      two\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>one</p>\\n<p>two</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 228,\n    \"start_line\": 3873,\n    \"end_line\": 3884,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"   > > 1.  one\\n>>\\n>>     two\\n\",\n    \"html\": \"<blockquote>\\n<blockquote>\\n<ol>\\n<li>\\n<p>one</p>\\n<p>two</p>\\n</li>\\n</ol>\\n</blockquote>\\n</blockquote>\\n\",\n    \"example\": 229,\n    \"start_line\": 3895,\n    \"end_line\": 3910,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \">>- one\\n>>\\n  >  > two\\n\",\n    \"html\": \"<blockquote>\\n<blockquote>\\n<ul>\\n<li>one</li>\\n</ul>\\n<p>two</p>\\n</blockquote>\\n</blockquote>\\n\",\n    \"example\": 230,\n    \"start_line\": 3922,\n    \"end_line\": 3935,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"-one\\n\\n2.two\\n\",\n    \"html\": \"<p>-one</p>\\n<p>2.two</p>\\n\",\n    \"example\": 231,\n    \"start_line\": 3941,\n    \"end_line\": 3948,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- foo\\n\\n\\n  bar\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>foo</p>\\n<p>bar</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 232,\n    \"start_line\": 3954,\n    \"end_line\": 3966,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"1.  foo\\n\\n    ```\\n    bar\\n    ```\\n\\n    baz\\n\\n    > bam\\n\",\n    \"html\": \"<ol>\\n<li>\\n<p>foo</p>\\n<pre><code>bar\\n</code></pre>\\n<p>baz</p>\\n<blockquote>\\n<p>bam</p>\\n</blockquote>\\n</li>\\n</ol>\\n\",\n    \"example\": 233,\n    \"start_line\": 3971,\n    \"end_line\": 3993,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- Foo\\n\\n      bar\\n\\n\\n      baz\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>Foo</p>\\n<pre><code>bar\\n\\n\\nbaz\\n</code></pre>\\n</li>\\n</ul>\\n\",\n    \"example\": 234,\n    \"start_line\": 3999,\n    \"end_line\": 4017,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"123456789. ok\\n\",\n    \"html\": \"<ol start=\\\"123456789\\\">\\n<li>ok</li>\\n</ol>\\n\",\n    \"example\": 235,\n    \"start_line\": 4021,\n    \"end_line\": 4027,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"1234567890. not ok\\n\",\n    \"html\": \"<p>1234567890. not ok</p>\\n\",\n    \"example\": 236,\n    \"start_line\": 4030,\n    \"end_line\": 4034,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"0. ok\\n\",\n    \"html\": \"<ol start=\\\"0\\\">\\n<li>ok</li>\\n</ol>\\n\",\n    \"example\": 237,\n    \"start_line\": 4039,\n    \"end_line\": 4045,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"003. ok\\n\",\n    \"html\": \"<ol start=\\\"3\\\">\\n<li>ok</li>\\n</ol>\\n\",\n    \"example\": 238,\n    \"start_line\": 4048,\n    \"end_line\": 4054,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"-1. not ok\\n\",\n    \"html\": \"<p>-1. not ok</p>\\n\",\n    \"example\": 239,\n    \"start_line\": 4059,\n    \"end_line\": 4063,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- foo\\n\\n      bar\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>foo</p>\\n<pre><code>bar\\n</code></pre>\\n</li>\\n</ul>\\n\",\n    \"example\": 240,\n    \"start_line\": 4082,\n    \"end_line\": 4094,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"  10.  foo\\n\\n           bar\\n\",\n    \"html\": \"<ol start=\\\"10\\\">\\n<li>\\n<p>foo</p>\\n<pre><code>bar\\n</code></pre>\\n</li>\\n</ol>\\n\",\n    \"example\": 241,\n    \"start_line\": 4099,\n    \"end_line\": 4111,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"    indented code\\n\\nparagraph\\n\\n    more code\\n\",\n    \"html\": \"<pre><code>indented code\\n</code></pre>\\n<p>paragraph</p>\\n<pre><code>more code\\n</code></pre>\\n\",\n    \"example\": 242,\n    \"start_line\": 4118,\n    \"end_line\": 4130,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"1.     indented code\\n\\n   paragraph\\n\\n       more code\\n\",\n    \"html\": \"<ol>\\n<li>\\n<pre><code>indented code\\n</code></pre>\\n<p>paragraph</p>\\n<pre><code>more code\\n</code></pre>\\n</li>\\n</ol>\\n\",\n    \"example\": 243,\n    \"start_line\": 4133,\n    \"end_line\": 4149,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"1.      indented code\\n\\n   paragraph\\n\\n       more code\\n\",\n    \"html\": \"<ol>\\n<li>\\n<pre><code> indented code\\n</code></pre>\\n<p>paragraph</p>\\n<pre><code>more code\\n</code></pre>\\n</li>\\n</ol>\\n\",\n    \"example\": 244,\n    \"start_line\": 4155,\n    \"end_line\": 4171,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"   foo\\n\\nbar\\n\",\n    \"html\": \"<p>foo</p>\\n<p>bar</p>\\n\",\n    \"example\": 245,\n    \"start_line\": 4182,\n    \"end_line\": 4189,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"-    foo\\n\\n  bar\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n</ul>\\n<p>bar</p>\\n\",\n    \"example\": 246,\n    \"start_line\": 4192,\n    \"end_line\": 4201,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"-  foo\\n\\n   bar\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>foo</p>\\n<p>bar</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 247,\n    \"start_line\": 4209,\n    \"end_line\": 4220,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"-\\n  foo\\n-\\n  ```\\n  bar\\n  ```\\n-\\n      baz\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n<li>\\n<pre><code>bar\\n</code></pre>\\n</li>\\n<li>\\n<pre><code>baz\\n</code></pre>\\n</li>\\n</ul>\\n\",\n    \"example\": 248,\n    \"start_line\": 4237,\n    \"end_line\": 4258,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"-   \\n  foo\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n</ul>\\n\",\n    \"example\": 249,\n    \"start_line\": 4263,\n    \"end_line\": 4270,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"-\\n\\n  foo\\n\",\n    \"html\": \"<ul>\\n<li></li>\\n</ul>\\n<p>foo</p>\\n\",\n    \"example\": 250,\n    \"start_line\": 4277,\n    \"end_line\": 4286,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- foo\\n-\\n- bar\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n<li></li>\\n<li>bar</li>\\n</ul>\\n\",\n    \"example\": 251,\n    \"start_line\": 4291,\n    \"end_line\": 4301,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- foo\\n-   \\n- bar\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n<li></li>\\n<li>bar</li>\\n</ul>\\n\",\n    \"example\": 252,\n    \"start_line\": 4306,\n    \"end_line\": 4316,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"1. foo\\n2.\\n3. bar\\n\",\n    \"html\": \"<ol>\\n<li>foo</li>\\n<li></li>\\n<li>bar</li>\\n</ol>\\n\",\n    \"example\": 253,\n    \"start_line\": 4321,\n    \"end_line\": 4331,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"*\\n\",\n    \"html\": \"<ul>\\n<li></li>\\n</ul>\\n\",\n    \"example\": 254,\n    \"start_line\": 4336,\n    \"end_line\": 4342,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"foo\\n*\\n\\nfoo\\n1.\\n\",\n    \"html\": \"<p>foo\\n*</p>\\n<p>foo\\n1.</p>\\n\",\n    \"example\": 255,\n    \"start_line\": 4346,\n    \"end_line\": 4357,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \" 1.  A paragraph\\n     with two lines.\\n\\n         indented code\\n\\n     > A block quote.\\n\",\n    \"html\": \"<ol>\\n<li>\\n<p>A paragraph\\nwith two lines.</p>\\n<pre><code>indented code\\n</code></pre>\\n<blockquote>\\n<p>A block quote.</p>\\n</blockquote>\\n</li>\\n</ol>\\n\",\n    \"example\": 256,\n    \"start_line\": 4368,\n    \"end_line\": 4387,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"  1.  A paragraph\\n      with two lines.\\n\\n          indented code\\n\\n      > A block quote.\\n\",\n    \"html\": \"<ol>\\n<li>\\n<p>A paragraph\\nwith two lines.</p>\\n<pre><code>indented code\\n</code></pre>\\n<blockquote>\\n<p>A block quote.</p>\\n</blockquote>\\n</li>\\n</ol>\\n\",\n    \"example\": 257,\n    \"start_line\": 4392,\n    \"end_line\": 4411,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"   1.  A paragraph\\n       with two lines.\\n\\n           indented code\\n\\n       > A block quote.\\n\",\n    \"html\": \"<ol>\\n<li>\\n<p>A paragraph\\nwith two lines.</p>\\n<pre><code>indented code\\n</code></pre>\\n<blockquote>\\n<p>A block quote.</p>\\n</blockquote>\\n</li>\\n</ol>\\n\",\n    \"example\": 258,\n    \"start_line\": 4416,\n    \"end_line\": 4435,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"    1.  A paragraph\\n        with two lines.\\n\\n            indented code\\n\\n        > A block quote.\\n\",\n    \"html\": \"<pre><code>1.  A paragraph\\n    with two lines.\\n\\n        indented code\\n\\n    &gt; A block quote.\\n</code></pre>\\n\",\n    \"example\": 259,\n    \"start_line\": 4440,\n    \"end_line\": 4455,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"  1.  A paragraph\\nwith two lines.\\n\\n          indented code\\n\\n      > A block quote.\\n\",\n    \"html\": \"<ol>\\n<li>\\n<p>A paragraph\\nwith two lines.</p>\\n<pre><code>indented code\\n</code></pre>\\n<blockquote>\\n<p>A block quote.</p>\\n</blockquote>\\n</li>\\n</ol>\\n\",\n    \"example\": 260,\n    \"start_line\": 4470,\n    \"end_line\": 4489,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"  1.  A paragraph\\n    with two lines.\\n\",\n    \"html\": \"<ol>\\n<li>A paragraph\\nwith two lines.</li>\\n</ol>\\n\",\n    \"example\": 261,\n    \"start_line\": 4494,\n    \"end_line\": 4502,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"> 1. > Blockquote\\ncontinued here.\\n\",\n    \"html\": \"<blockquote>\\n<ol>\\n<li>\\n<blockquote>\\n<p>Blockquote\\ncontinued here.</p>\\n</blockquote>\\n</li>\\n</ol>\\n</blockquote>\\n\",\n    \"example\": 262,\n    \"start_line\": 4507,\n    \"end_line\": 4521,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"> 1. > Blockquote\\n> continued here.\\n\",\n    \"html\": \"<blockquote>\\n<ol>\\n<li>\\n<blockquote>\\n<p>Blockquote\\ncontinued here.</p>\\n</blockquote>\\n</li>\\n</ol>\\n</blockquote>\\n\",\n    \"example\": 263,\n    \"start_line\": 4524,\n    \"end_line\": 4538,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- foo\\n  - bar\\n    - baz\\n      - boo\\n\",\n    \"html\": \"<ul>\\n<li>foo\\n<ul>\\n<li>bar\\n<ul>\\n<li>baz\\n<ul>\\n<li>boo</li>\\n</ul>\\n</li>\\n</ul>\\n</li>\\n</ul>\\n</li>\\n</ul>\\n\",\n    \"example\": 264,\n    \"start_line\": 4552,\n    \"end_line\": 4573,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- foo\\n - bar\\n  - baz\\n   - boo\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n<li>bar</li>\\n<li>baz</li>\\n<li>boo</li>\\n</ul>\\n\",\n    \"example\": 265,\n    \"start_line\": 4578,\n    \"end_line\": 4590,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"10) foo\\n    - bar\\n\",\n    \"html\": \"<ol start=\\\"10\\\">\\n<li>foo\\n<ul>\\n<li>bar</li>\\n</ul>\\n</li>\\n</ol>\\n\",\n    \"example\": 266,\n    \"start_line\": 4595,\n    \"end_line\": 4606,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"10) foo\\n   - bar\\n\",\n    \"html\": \"<ol start=\\\"10\\\">\\n<li>foo</li>\\n</ol>\\n<ul>\\n<li>bar</li>\\n</ul>\\n\",\n    \"example\": 267,\n    \"start_line\": 4611,\n    \"end_line\": 4621,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- - foo\\n\",\n    \"html\": \"<ul>\\n<li>\\n<ul>\\n<li>foo</li>\\n</ul>\\n</li>\\n</ul>\\n\",\n    \"example\": 268,\n    \"start_line\": 4626,\n    \"end_line\": 4636,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"1. - 2. foo\\n\",\n    \"html\": \"<ol>\\n<li>\\n<ul>\\n<li>\\n<ol start=\\\"2\\\">\\n<li>foo</li>\\n</ol>\\n</li>\\n</ul>\\n</li>\\n</ol>\\n\",\n    \"example\": 269,\n    \"start_line\": 4639,\n    \"end_line\": 4653,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- # Foo\\n- Bar\\n  ---\\n  baz\\n\",\n    \"html\": \"<ul>\\n<li>\\n<h1>Foo</h1>\\n</li>\\n<li>\\n<h2>Bar</h2>\\nbaz</li>\\n</ul>\\n\",\n    \"example\": 270,\n    \"start_line\": 4658,\n    \"end_line\": 4672,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- foo\\n- bar\\n+ baz\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n<li>bar</li>\\n</ul>\\n<ul>\\n<li>baz</li>\\n</ul>\\n\",\n    \"example\": 271,\n    \"start_line\": 4894,\n    \"end_line\": 4906,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"1. foo\\n2. bar\\n3) baz\\n\",\n    \"html\": \"<ol>\\n<li>foo</li>\\n<li>bar</li>\\n</ol>\\n<ol start=\\\"3\\\">\\n<li>baz</li>\\n</ol>\\n\",\n    \"example\": 272,\n    \"start_line\": 4909,\n    \"end_line\": 4921,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"Foo\\n- bar\\n- baz\\n\",\n    \"html\": \"<p>Foo</p>\\n<ul>\\n<li>bar</li>\\n<li>baz</li>\\n</ul>\\n\",\n    \"example\": 273,\n    \"start_line\": 4928,\n    \"end_line\": 4938,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"The number of windows in my house is\\n14.  The number of doors is 6.\\n\",\n    \"html\": \"<p>The number of windows in my house is\\n14.  The number of doors is 6.</p>\\n\",\n    \"example\": 274,\n    \"start_line\": 5005,\n    \"end_line\": 5011,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"The number of windows in my house is\\n1.  The number of doors is 6.\\n\",\n    \"html\": \"<p>The number of windows in my house is</p>\\n<ol>\\n<li>The number of doors is 6.</li>\\n</ol>\\n\",\n    \"example\": 275,\n    \"start_line\": 5015,\n    \"end_line\": 5023,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- foo\\n\\n- bar\\n\\n\\n- baz\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>foo</p>\\n</li>\\n<li>\\n<p>bar</p>\\n</li>\\n<li>\\n<p>baz</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 276,\n    \"start_line\": 5029,\n    \"end_line\": 5048,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- foo\\n  - bar\\n    - baz\\n\\n\\n      bim\\n\",\n    \"html\": \"<ul>\\n<li>foo\\n<ul>\\n<li>bar\\n<ul>\\n<li>\\n<p>baz</p>\\n<p>bim</p>\\n</li>\\n</ul>\\n</li>\\n</ul>\\n</li>\\n</ul>\\n\",\n    \"example\": 277,\n    \"start_line\": 5050,\n    \"end_line\": 5072,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- foo\\n- bar\\n\\n<!-- -->\\n\\n- baz\\n- bim\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n<li>bar</li>\\n</ul>\\n<!-- -->\\n<ul>\\n<li>baz</li>\\n<li>bim</li>\\n</ul>\\n\",\n    \"example\": 278,\n    \"start_line\": 5080,\n    \"end_line\": 5098,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"-   foo\\n\\n    notcode\\n\\n-   foo\\n\\n<!-- -->\\n\\n    code\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>foo</p>\\n<p>notcode</p>\\n</li>\\n<li>\\n<p>foo</p>\\n</li>\\n</ul>\\n<!-- -->\\n<pre><code>code\\n</code></pre>\\n\",\n    \"example\": 279,\n    \"start_line\": 5101,\n    \"end_line\": 5124,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n - b\\n  - c\\n   - d\\n  - e\\n - f\\n- g\\n\",\n    \"html\": \"<ul>\\n<li>a</li>\\n<li>b</li>\\n<li>c</li>\\n<li>d</li>\\n<li>e</li>\\n<li>f</li>\\n<li>g</li>\\n</ul>\\n\",\n    \"example\": 280,\n    \"start_line\": 5132,\n    \"end_line\": 5150,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"1. a\\n\\n  2. b\\n\\n   3. c\\n\",\n    \"html\": \"<ol>\\n<li>\\n<p>a</p>\\n</li>\\n<li>\\n<p>b</p>\\n</li>\\n<li>\\n<p>c</p>\\n</li>\\n</ol>\\n\",\n    \"example\": 281,\n    \"start_line\": 5153,\n    \"end_line\": 5171,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n - b\\n  - c\\n   - d\\n    - e\\n\",\n    \"html\": \"<ul>\\n<li>a</li>\\n<li>b</li>\\n<li>c</li>\\n<li>d\\n- e</li>\\n</ul>\\n\",\n    \"example\": 282,\n    \"start_line\": 5177,\n    \"end_line\": 5191,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"1. a\\n\\n  2. b\\n\\n    3. c\\n\",\n    \"html\": \"<ol>\\n<li>\\n<p>a</p>\\n</li>\\n<li>\\n<p>b</p>\\n</li>\\n</ol>\\n<pre><code>3. c\\n</code></pre>\\n\",\n    \"example\": 283,\n    \"start_line\": 5197,\n    \"end_line\": 5214,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n- b\\n\\n- c\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>a</p>\\n</li>\\n<li>\\n<p>b</p>\\n</li>\\n<li>\\n<p>c</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 284,\n    \"start_line\": 5220,\n    \"end_line\": 5237,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"* a\\n*\\n\\n* c\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>a</p>\\n</li>\\n<li></li>\\n<li>\\n<p>c</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 285,\n    \"start_line\": 5242,\n    \"end_line\": 5257,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n- b\\n\\n  c\\n- d\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>a</p>\\n</li>\\n<li>\\n<p>b</p>\\n<p>c</p>\\n</li>\\n<li>\\n<p>d</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 286,\n    \"start_line\": 5264,\n    \"end_line\": 5283,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n- b\\n\\n  [ref]: /url\\n- d\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>a</p>\\n</li>\\n<li>\\n<p>b</p>\\n</li>\\n<li>\\n<p>d</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 287,\n    \"start_line\": 5286,\n    \"end_line\": 5304,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n- ```\\n  b\\n\\n\\n  ```\\n- c\\n\",\n    \"html\": \"<ul>\\n<li>a</li>\\n<li>\\n<pre><code>b\\n\\n\\n</code></pre>\\n</li>\\n<li>c</li>\\n</ul>\\n\",\n    \"example\": 288,\n    \"start_line\": 5309,\n    \"end_line\": 5328,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n  - b\\n\\n    c\\n- d\\n\",\n    \"html\": \"<ul>\\n<li>a\\n<ul>\\n<li>\\n<p>b</p>\\n<p>c</p>\\n</li>\\n</ul>\\n</li>\\n<li>d</li>\\n</ul>\\n\",\n    \"example\": 289,\n    \"start_line\": 5335,\n    \"end_line\": 5353,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"* a\\n  > b\\n  >\\n* c\\n\",\n    \"html\": \"<ul>\\n<li>a\\n<blockquote>\\n<p>b</p>\\n</blockquote>\\n</li>\\n<li>c</li>\\n</ul>\\n\",\n    \"example\": 290,\n    \"start_line\": 5359,\n    \"end_line\": 5373,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n  > b\\n  ```\\n  c\\n  ```\\n- d\\n\",\n    \"html\": \"<ul>\\n<li>a\\n<blockquote>\\n<p>b</p>\\n</blockquote>\\n<pre><code>c\\n</code></pre>\\n</li>\\n<li>d</li>\\n</ul>\\n\",\n    \"example\": 291,\n    \"start_line\": 5379,\n    \"end_line\": 5397,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n\",\n    \"html\": \"<ul>\\n<li>a</li>\\n</ul>\\n\",\n    \"example\": 292,\n    \"start_line\": 5402,\n    \"end_line\": 5408,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n  - b\\n\",\n    \"html\": \"<ul>\\n<li>a\\n<ul>\\n<li>b</li>\\n</ul>\\n</li>\\n</ul>\\n\",\n    \"example\": 293,\n    \"start_line\": 5411,\n    \"end_line\": 5422,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"1. ```\\n   foo\\n   ```\\n\\n   bar\\n\",\n    \"html\": \"<ol>\\n<li>\\n<pre><code>foo\\n</code></pre>\\n<p>bar</p>\\n</li>\\n</ol>\\n\",\n    \"example\": 294,\n    \"start_line\": 5428,\n    \"end_line\": 5442,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"* foo\\n  * bar\\n\\n  baz\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>foo</p>\\n<ul>\\n<li>bar</li>\\n</ul>\\n<p>baz</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 295,\n    \"start_line\": 5447,\n    \"end_line\": 5462,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n  - b\\n  - c\\n\\n- d\\n  - e\\n  - f\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>a</p>\\n<ul>\\n<li>b</li>\\n<li>c</li>\\n</ul>\\n</li>\\n<li>\\n<p>d</p>\\n<ul>\\n<li>e</li>\\n<li>f</li>\\n</ul>\\n</li>\\n</ul>\\n\",\n    \"example\": 296,\n    \"start_line\": 5465,\n    \"end_line\": 5490,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"`hi`lo`\\n\",\n    \"html\": \"<p><code>hi</code>lo`</p>\\n\",\n    \"example\": 297,\n    \"start_line\": 5499,\n    \"end_line\": 5503,\n    \"section\": \"Inlines\"\n  },\n  {\n    \"markdown\": \"\\\\!\\\\\\\"\\\\#\\\\$\\\\%\\\\&\\\\'\\\\(\\\\)\\\\*\\\\+\\\\,\\\\-\\\\.\\\\/\\\\:\\\\;\\\\<\\\\=\\\\>\\\\?\\\\@\\\\[\\\\\\\\\\\\]\\\\^\\\\_\\\\`\\\\{\\\\|\\\\}\\\\~\\n\",\n    \"html\": \"<p>!&quot;#$%&amp;'()*+,-./:;&lt;=&gt;?@[\\\\]^_`{|}~</p>\\n\",\n    \"example\": 298,\n    \"start_line\": 5513,\n    \"end_line\": 5517,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"\\\\\\t\\\\A\\\\a\\\\ \\\\3\\\\φ\\\\«\\n\",\n    \"html\": \"<p>\\\\\\t\\\\A\\\\a\\\\ \\\\3\\\\φ\\\\«</p>\\n\",\n    \"example\": 299,\n    \"start_line\": 5523,\n    \"end_line\": 5527,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"\\\\*not emphasized*\\n\\\\<br/> not a tag\\n\\\\[not a link](/foo)\\n\\\\`not code`\\n1\\\\. not a list\\n\\\\* not a list\\n\\\\# not a heading\\n\\\\[foo]: /url \\\"not a reference\\\"\\n\\\\&ouml; not a character entity\\n\",\n    \"html\": \"<p>*not emphasized*\\n&lt;br/&gt; not a tag\\n[not a link](/foo)\\n`not code`\\n1. not a list\\n* not a list\\n# not a heading\\n[foo]: /url &quot;not a reference&quot;\\n&amp;ouml; not a character entity</p>\\n\",\n    \"example\": 300,\n    \"start_line\": 5533,\n    \"end_line\": 5553,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"\\\\\\\\*emphasis*\\n\",\n    \"html\": \"<p>\\\\<em>emphasis</em></p>\\n\",\n    \"example\": 301,\n    \"start_line\": 5558,\n    \"end_line\": 5562,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"foo\\\\\\nbar\\n\",\n    \"html\": \"<p>foo<br />\\nbar</p>\\n\",\n    \"example\": 302,\n    \"start_line\": 5567,\n    \"end_line\": 5573,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"`` \\\\[\\\\` ``\\n\",\n    \"html\": \"<p><code>\\\\[\\\\`</code></p>\\n\",\n    \"example\": 303,\n    \"start_line\": 5579,\n    \"end_line\": 5583,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"    \\\\[\\\\]\\n\",\n    \"html\": \"<pre><code>\\\\[\\\\]\\n</code></pre>\\n\",\n    \"example\": 304,\n    \"start_line\": 5586,\n    \"end_line\": 5591,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"~~~\\n\\\\[\\\\]\\n~~~\\n\",\n    \"html\": \"<pre><code>\\\\[\\\\]\\n</code></pre>\\n\",\n    \"example\": 305,\n    \"start_line\": 5594,\n    \"end_line\": 5601,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"<http://example.com?find=\\\\*>\\n\",\n    \"html\": \"<p><a href=\\\"http://example.com?find=%5C*\\\">http://example.com?find=\\\\*</a></p>\\n\",\n    \"example\": 306,\n    \"start_line\": 5604,\n    \"end_line\": 5608,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"<a href=\\\"/bar\\\\/)\\\">\\n\",\n    \"html\": \"<a href=\\\"/bar\\\\/)\\\">\\n\",\n    \"example\": 307,\n    \"start_line\": 5611,\n    \"end_line\": 5615,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"[foo](/bar\\\\* \\\"ti\\\\*tle\\\")\\n\",\n    \"html\": \"<p><a href=\\\"/bar*\\\" title=\\\"ti*tle\\\">foo</a></p>\\n\",\n    \"example\": 308,\n    \"start_line\": 5621,\n    \"end_line\": 5625,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"[foo]\\n\\n[foo]: /bar\\\\* \\\"ti\\\\*tle\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/bar*\\\" title=\\\"ti*tle\\\">foo</a></p>\\n\",\n    \"example\": 309,\n    \"start_line\": 5628,\n    \"end_line\": 5634,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"``` foo\\\\+bar\\nfoo\\n```\\n\",\n    \"html\": \"<pre><code class=\\\"language-foo+bar\\\">foo\\n</code></pre>\\n\",\n    \"example\": 310,\n    \"start_line\": 5637,\n    \"end_line\": 5644,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"&nbsp; &amp; &copy; &AElig; &Dcaron;\\n&frac34; &HilbertSpace; &DifferentialD;\\n&ClockwiseContourIntegral; &ngE;\\n\",\n    \"html\": \"<p>  &amp; © Æ Ď\\n¾ ℋ ⅆ\\n∲ ≧̸</p>\\n\",\n    \"example\": 311,\n    \"start_line\": 5674,\n    \"end_line\": 5682,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"&#35; &#1234; &#992; &#0;\\n\",\n    \"html\": \"<p># Ӓ Ϡ �</p>\\n\",\n    \"example\": 312,\n    \"start_line\": 5693,\n    \"end_line\": 5697,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"&#X22; &#XD06; &#xcab;\\n\",\n    \"html\": \"<p>&quot; ആ ಫ</p>\\n\",\n    \"example\": 313,\n    \"start_line\": 5706,\n    \"end_line\": 5710,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"&nbsp &x; &#; &#x;\\n&#987654321;\\n&#abcdef0;\\n&ThisIsNotDefined; &hi?;\\n\",\n    \"html\": \"<p>&amp;nbsp &amp;x; &amp;#; &amp;#x;\\n&amp;#987654321;\\n&amp;#abcdef0;\\n&amp;ThisIsNotDefined; &amp;hi?;</p>\\n\",\n    \"example\": 314,\n    \"start_line\": 5715,\n    \"end_line\": 5725,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"&copy\\n\",\n    \"html\": \"<p>&amp;copy</p>\\n\",\n    \"example\": 315,\n    \"start_line\": 5732,\n    \"end_line\": 5736,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"&MadeUpEntity;\\n\",\n    \"html\": \"<p>&amp;MadeUpEntity;</p>\\n\",\n    \"example\": 316,\n    \"start_line\": 5742,\n    \"end_line\": 5746,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"<a href=\\\"&ouml;&ouml;.html\\\">\\n\",\n    \"html\": \"<a href=\\\"&ouml;&ouml;.html\\\">\\n\",\n    \"example\": 317,\n    \"start_line\": 5753,\n    \"end_line\": 5757,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"[foo](/f&ouml;&ouml; \\\"f&ouml;&ouml;\\\")\\n\",\n    \"html\": \"<p><a href=\\\"/f%C3%B6%C3%B6\\\" title=\\\"föö\\\">foo</a></p>\\n\",\n    \"example\": 318,\n    \"start_line\": 5760,\n    \"end_line\": 5764,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"[foo]\\n\\n[foo]: /f&ouml;&ouml; \\\"f&ouml;&ouml;\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/f%C3%B6%C3%B6\\\" title=\\\"föö\\\">foo</a></p>\\n\",\n    \"example\": 319,\n    \"start_line\": 5767,\n    \"end_line\": 5773,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"``` f&ouml;&ouml;\\nfoo\\n```\\n\",\n    \"html\": \"<pre><code class=\\\"language-föö\\\">foo\\n</code></pre>\\n\",\n    \"example\": 320,\n    \"start_line\": 5776,\n    \"end_line\": 5783,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"`f&ouml;&ouml;`\\n\",\n    \"html\": \"<p><code>f&amp;ouml;&amp;ouml;</code></p>\\n\",\n    \"example\": 321,\n    \"start_line\": 5789,\n    \"end_line\": 5793,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"    f&ouml;f&ouml;\\n\",\n    \"html\": \"<pre><code>f&amp;ouml;f&amp;ouml;\\n</code></pre>\\n\",\n    \"example\": 322,\n    \"start_line\": 5796,\n    \"end_line\": 5801,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"&#42;foo&#42;\\n*foo*\\n\",\n    \"html\": \"<p>*foo*\\n<em>foo</em></p>\\n\",\n    \"example\": 323,\n    \"start_line\": 5808,\n    \"end_line\": 5814,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"&#42; foo\\n\\n* foo\\n\",\n    \"html\": \"<p>* foo</p>\\n<ul>\\n<li>foo</li>\\n</ul>\\n\",\n    \"example\": 324,\n    \"start_line\": 5816,\n    \"end_line\": 5825,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"foo&#10;&#10;bar\\n\",\n    \"html\": \"<p>foo\\n\\nbar</p>\\n\",\n    \"example\": 325,\n    \"start_line\": 5827,\n    \"end_line\": 5833,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"&#9;foo\\n\",\n    \"html\": \"<p>\\tfoo</p>\\n\",\n    \"example\": 326,\n    \"start_line\": 5835,\n    \"end_line\": 5839,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"[a](url &quot;tit&quot;)\\n\",\n    \"html\": \"<p>[a](url &quot;tit&quot;)</p>\\n\",\n    \"example\": 327,\n    \"start_line\": 5842,\n    \"end_line\": 5846,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"`foo`\\n\",\n    \"html\": \"<p><code>foo</code></p>\\n\",\n    \"example\": 328,\n    \"start_line\": 5870,\n    \"end_line\": 5874,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"`` foo ` bar ``\\n\",\n    \"html\": \"<p><code>foo ` bar</code></p>\\n\",\n    \"example\": 329,\n    \"start_line\": 5881,\n    \"end_line\": 5885,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"` `` `\\n\",\n    \"html\": \"<p><code>``</code></p>\\n\",\n    \"example\": 330,\n    \"start_line\": 5891,\n    \"end_line\": 5895,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"`  ``  `\\n\",\n    \"html\": \"<p><code> `` </code></p>\\n\",\n    \"example\": 331,\n    \"start_line\": 5899,\n    \"end_line\": 5903,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"` a`\\n\",\n    \"html\": \"<p><code> a</code></p>\\n\",\n    \"example\": 332,\n    \"start_line\": 5908,\n    \"end_line\": 5912,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"` b `\\n\",\n    \"html\": \"<p><code> b </code></p>\\n\",\n    \"example\": 333,\n    \"start_line\": 5917,\n    \"end_line\": 5921,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"` `\\n`  `\\n\",\n    \"html\": \"<p><code> </code>\\n<code>  </code></p>\\n\",\n    \"example\": 334,\n    \"start_line\": 5925,\n    \"end_line\": 5931,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"``\\nfoo\\nbar  \\nbaz\\n``\\n\",\n    \"html\": \"<p><code>foo bar   baz</code></p>\\n\",\n    \"example\": 335,\n    \"start_line\": 5936,\n    \"end_line\": 5944,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"``\\nfoo \\n``\\n\",\n    \"html\": \"<p><code>foo </code></p>\\n\",\n    \"example\": 336,\n    \"start_line\": 5946,\n    \"end_line\": 5952,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"`foo   bar \\nbaz`\\n\",\n    \"html\": \"<p><code>foo   bar  baz</code></p>\\n\",\n    \"example\": 337,\n    \"start_line\": 5957,\n    \"end_line\": 5962,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"`foo\\\\`bar`\\n\",\n    \"html\": \"<p><code>foo\\\\</code>bar`</p>\\n\",\n    \"example\": 338,\n    \"start_line\": 5974,\n    \"end_line\": 5978,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"``foo`bar``\\n\",\n    \"html\": \"<p><code>foo`bar</code></p>\\n\",\n    \"example\": 339,\n    \"start_line\": 5985,\n    \"end_line\": 5989,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"` foo `` bar `\\n\",\n    \"html\": \"<p><code>foo `` bar</code></p>\\n\",\n    \"example\": 340,\n    \"start_line\": 5991,\n    \"end_line\": 5995,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"*foo`*`\\n\",\n    \"html\": \"<p>*foo<code>*</code></p>\\n\",\n    \"example\": 341,\n    \"start_line\": 6003,\n    \"end_line\": 6007,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"[not a `link](/foo`)\\n\",\n    \"html\": \"<p>[not a <code>link](/foo</code>)</p>\\n\",\n    \"example\": 342,\n    \"start_line\": 6012,\n    \"end_line\": 6016,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"`<a href=\\\"`\\\">`\\n\",\n    \"html\": \"<p><code>&lt;a href=&quot;</code>&quot;&gt;`</p>\\n\",\n    \"example\": 343,\n    \"start_line\": 6022,\n    \"end_line\": 6026,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"<a href=\\\"`\\\">`\\n\",\n    \"html\": \"<p><a href=\\\"`\\\">`</p>\\n\",\n    \"example\": 344,\n    \"start_line\": 6031,\n    \"end_line\": 6035,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"`<http://foo.bar.`baz>`\\n\",\n    \"html\": \"<p><code>&lt;http://foo.bar.</code>baz&gt;`</p>\\n\",\n    \"example\": 345,\n    \"start_line\": 6040,\n    \"end_line\": 6044,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"<http://foo.bar.`baz>`\\n\",\n    \"html\": \"<p><a href=\\\"http://foo.bar.%60baz\\\">http://foo.bar.`baz</a>`</p>\\n\",\n    \"example\": 346,\n    \"start_line\": 6049,\n    \"end_line\": 6053,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"```foo``\\n\",\n    \"html\": \"<p>```foo``</p>\\n\",\n    \"example\": 347,\n    \"start_line\": 6059,\n    \"end_line\": 6063,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"`foo\\n\",\n    \"html\": \"<p>`foo</p>\\n\",\n    \"example\": 348,\n    \"start_line\": 6066,\n    \"end_line\": 6070,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"`foo``bar``\\n\",\n    \"html\": \"<p>`foo<code>bar</code></p>\\n\",\n    \"example\": 349,\n    \"start_line\": 6075,\n    \"end_line\": 6079,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"*foo bar*\\n\",\n    \"html\": \"<p><em>foo bar</em></p>\\n\",\n    \"example\": 350,\n    \"start_line\": 6292,\n    \"end_line\": 6296,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"a * foo bar*\\n\",\n    \"html\": \"<p>a * foo bar*</p>\\n\",\n    \"example\": 351,\n    \"start_line\": 6302,\n    \"end_line\": 6306,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"a*\\\"foo\\\"*\\n\",\n    \"html\": \"<p>a*&quot;foo&quot;*</p>\\n\",\n    \"example\": 352,\n    \"start_line\": 6313,\n    \"end_line\": 6317,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"* a *\\n\",\n    \"html\": \"<p>* a *</p>\\n\",\n    \"example\": 353,\n    \"start_line\": 6322,\n    \"end_line\": 6326,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo*bar*\\n\",\n    \"html\": \"<p>foo<em>bar</em></p>\\n\",\n    \"example\": 354,\n    \"start_line\": 6331,\n    \"end_line\": 6335,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"5*6*78\\n\",\n    \"html\": \"<p>5<em>6</em>78</p>\\n\",\n    \"example\": 355,\n    \"start_line\": 6338,\n    \"end_line\": 6342,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo bar_\\n\",\n    \"html\": \"<p><em>foo bar</em></p>\\n\",\n    \"example\": 356,\n    \"start_line\": 6347,\n    \"end_line\": 6351,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_ foo bar_\\n\",\n    \"html\": \"<p>_ foo bar_</p>\\n\",\n    \"example\": 357,\n    \"start_line\": 6357,\n    \"end_line\": 6361,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"a_\\\"foo\\\"_\\n\",\n    \"html\": \"<p>a_&quot;foo&quot;_</p>\\n\",\n    \"example\": 358,\n    \"start_line\": 6367,\n    \"end_line\": 6371,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo_bar_\\n\",\n    \"html\": \"<p>foo_bar_</p>\\n\",\n    \"example\": 359,\n    \"start_line\": 6376,\n    \"end_line\": 6380,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"5_6_78\\n\",\n    \"html\": \"<p>5_6_78</p>\\n\",\n    \"example\": 360,\n    \"start_line\": 6383,\n    \"end_line\": 6387,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"пристаням_стремятся_\\n\",\n    \"html\": \"<p>пристаням_стремятся_</p>\\n\",\n    \"example\": 361,\n    \"start_line\": 6390,\n    \"end_line\": 6394,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"aa_\\\"bb\\\"_cc\\n\",\n    \"html\": \"<p>aa_&quot;bb&quot;_cc</p>\\n\",\n    \"example\": 362,\n    \"start_line\": 6400,\n    \"end_line\": 6404,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo-_(bar)_\\n\",\n    \"html\": \"<p>foo-<em>(bar)</em></p>\\n\",\n    \"example\": 363,\n    \"start_line\": 6411,\n    \"end_line\": 6415,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo*\\n\",\n    \"html\": \"<p>_foo*</p>\\n\",\n    \"example\": 364,\n    \"start_line\": 6423,\n    \"end_line\": 6427,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo bar *\\n\",\n    \"html\": \"<p>*foo bar *</p>\\n\",\n    \"example\": 365,\n    \"start_line\": 6433,\n    \"end_line\": 6437,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo bar\\n*\\n\",\n    \"html\": \"<p>*foo bar\\n*</p>\\n\",\n    \"example\": 366,\n    \"start_line\": 6442,\n    \"end_line\": 6448,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*(*foo)\\n\",\n    \"html\": \"<p>*(*foo)</p>\\n\",\n    \"example\": 367,\n    \"start_line\": 6455,\n    \"end_line\": 6459,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*(*foo*)*\\n\",\n    \"html\": \"<p><em>(<em>foo</em>)</em></p>\\n\",\n    \"example\": 368,\n    \"start_line\": 6465,\n    \"end_line\": 6469,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo*bar\\n\",\n    \"html\": \"<p><em>foo</em>bar</p>\\n\",\n    \"example\": 369,\n    \"start_line\": 6474,\n    \"end_line\": 6478,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo bar _\\n\",\n    \"html\": \"<p>_foo bar _</p>\\n\",\n    \"example\": 370,\n    \"start_line\": 6487,\n    \"end_line\": 6491,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_(_foo)\\n\",\n    \"html\": \"<p>_(_foo)</p>\\n\",\n    \"example\": 371,\n    \"start_line\": 6497,\n    \"end_line\": 6501,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_(_foo_)_\\n\",\n    \"html\": \"<p><em>(<em>foo</em>)</em></p>\\n\",\n    \"example\": 372,\n    \"start_line\": 6506,\n    \"end_line\": 6510,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo_bar\\n\",\n    \"html\": \"<p>_foo_bar</p>\\n\",\n    \"example\": 373,\n    \"start_line\": 6515,\n    \"end_line\": 6519,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_пристаням_стремятся\\n\",\n    \"html\": \"<p>_пристаням_стремятся</p>\\n\",\n    \"example\": 374,\n    \"start_line\": 6522,\n    \"end_line\": 6526,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo_bar_baz_\\n\",\n    \"html\": \"<p><em>foo_bar_baz</em></p>\\n\",\n    \"example\": 375,\n    \"start_line\": 6529,\n    \"end_line\": 6533,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_(bar)_.\\n\",\n    \"html\": \"<p><em>(bar)</em>.</p>\\n\",\n    \"example\": 376,\n    \"start_line\": 6540,\n    \"end_line\": 6544,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo bar**\\n\",\n    \"html\": \"<p><strong>foo bar</strong></p>\\n\",\n    \"example\": 377,\n    \"start_line\": 6549,\n    \"end_line\": 6553,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"** foo bar**\\n\",\n    \"html\": \"<p>** foo bar**</p>\\n\",\n    \"example\": 378,\n    \"start_line\": 6559,\n    \"end_line\": 6563,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"a**\\\"foo\\\"**\\n\",\n    \"html\": \"<p>a**&quot;foo&quot;**</p>\\n\",\n    \"example\": 379,\n    \"start_line\": 6570,\n    \"end_line\": 6574,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo**bar**\\n\",\n    \"html\": \"<p>foo<strong>bar</strong></p>\\n\",\n    \"example\": 380,\n    \"start_line\": 6579,\n    \"end_line\": 6583,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo bar__\\n\",\n    \"html\": \"<p><strong>foo bar</strong></p>\\n\",\n    \"example\": 381,\n    \"start_line\": 6588,\n    \"end_line\": 6592,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__ foo bar__\\n\",\n    \"html\": \"<p>__ foo bar__</p>\\n\",\n    \"example\": 382,\n    \"start_line\": 6598,\n    \"end_line\": 6602,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__\\nfoo bar__\\n\",\n    \"html\": \"<p>__\\nfoo bar__</p>\\n\",\n    \"example\": 383,\n    \"start_line\": 6606,\n    \"end_line\": 6612,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"a__\\\"foo\\\"__\\n\",\n    \"html\": \"<p>a__&quot;foo&quot;__</p>\\n\",\n    \"example\": 384,\n    \"start_line\": 6618,\n    \"end_line\": 6622,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo__bar__\\n\",\n    \"html\": \"<p>foo__bar__</p>\\n\",\n    \"example\": 385,\n    \"start_line\": 6627,\n    \"end_line\": 6631,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"5__6__78\\n\",\n    \"html\": \"<p>5__6__78</p>\\n\",\n    \"example\": 386,\n    \"start_line\": 6634,\n    \"end_line\": 6638,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"пристаням__стремятся__\\n\",\n    \"html\": \"<p>пристаням__стремятся__</p>\\n\",\n    \"example\": 387,\n    \"start_line\": 6641,\n    \"end_line\": 6645,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo, __bar__, baz__\\n\",\n    \"html\": \"<p><strong>foo, <strong>bar</strong>, baz</strong></p>\\n\",\n    \"example\": 388,\n    \"start_line\": 6648,\n    \"end_line\": 6652,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo-__(bar)__\\n\",\n    \"html\": \"<p>foo-<strong>(bar)</strong></p>\\n\",\n    \"example\": 389,\n    \"start_line\": 6659,\n    \"end_line\": 6663,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo bar **\\n\",\n    \"html\": \"<p>**foo bar **</p>\\n\",\n    \"example\": 390,\n    \"start_line\": 6672,\n    \"end_line\": 6676,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**(**foo)\\n\",\n    \"html\": \"<p>**(**foo)</p>\\n\",\n    \"example\": 391,\n    \"start_line\": 6685,\n    \"end_line\": 6689,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*(**foo**)*\\n\",\n    \"html\": \"<p><em>(<strong>foo</strong>)</em></p>\\n\",\n    \"example\": 392,\n    \"start_line\": 6695,\n    \"end_line\": 6699,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**Gomphocarpus (*Gomphocarpus physocarpus*, syn.\\n*Asclepias physocarpa*)**\\n\",\n    \"html\": \"<p><strong>Gomphocarpus (<em>Gomphocarpus physocarpus</em>, syn.\\n<em>Asclepias physocarpa</em>)</strong></p>\\n\",\n    \"example\": 393,\n    \"start_line\": 6702,\n    \"end_line\": 6708,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo \\\"*bar*\\\" foo**\\n\",\n    \"html\": \"<p><strong>foo &quot;<em>bar</em>&quot; foo</strong></p>\\n\",\n    \"example\": 394,\n    \"start_line\": 6711,\n    \"end_line\": 6715,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo**bar\\n\",\n    \"html\": \"<p><strong>foo</strong>bar</p>\\n\",\n    \"example\": 395,\n    \"start_line\": 6720,\n    \"end_line\": 6724,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo bar __\\n\",\n    \"html\": \"<p>__foo bar __</p>\\n\",\n    \"example\": 396,\n    \"start_line\": 6732,\n    \"end_line\": 6736,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__(__foo)\\n\",\n    \"html\": \"<p>__(__foo)</p>\\n\",\n    \"example\": 397,\n    \"start_line\": 6742,\n    \"end_line\": 6746,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_(__foo__)_\\n\",\n    \"html\": \"<p><em>(<strong>foo</strong>)</em></p>\\n\",\n    \"example\": 398,\n    \"start_line\": 6752,\n    \"end_line\": 6756,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo__bar\\n\",\n    \"html\": \"<p>__foo__bar</p>\\n\",\n    \"example\": 399,\n    \"start_line\": 6761,\n    \"end_line\": 6765,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__пристаням__стремятся\\n\",\n    \"html\": \"<p>__пристаням__стремятся</p>\\n\",\n    \"example\": 400,\n    \"start_line\": 6768,\n    \"end_line\": 6772,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo__bar__baz__\\n\",\n    \"html\": \"<p><strong>foo__bar__baz</strong></p>\\n\",\n    \"example\": 401,\n    \"start_line\": 6775,\n    \"end_line\": 6779,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__(bar)__.\\n\",\n    \"html\": \"<p><strong>(bar)</strong>.</p>\\n\",\n    \"example\": 402,\n    \"start_line\": 6786,\n    \"end_line\": 6790,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo [bar](/url)*\\n\",\n    \"html\": \"<p><em>foo <a href=\\\"/url\\\">bar</a></em></p>\\n\",\n    \"example\": 403,\n    \"start_line\": 6798,\n    \"end_line\": 6802,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo\\nbar*\\n\",\n    \"html\": \"<p><em>foo\\nbar</em></p>\\n\",\n    \"example\": 404,\n    \"start_line\": 6805,\n    \"end_line\": 6811,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo __bar__ baz_\\n\",\n    \"html\": \"<p><em>foo <strong>bar</strong> baz</em></p>\\n\",\n    \"example\": 405,\n    \"start_line\": 6817,\n    \"end_line\": 6821,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo _bar_ baz_\\n\",\n    \"html\": \"<p><em>foo <em>bar</em> baz</em></p>\\n\",\n    \"example\": 406,\n    \"start_line\": 6824,\n    \"end_line\": 6828,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo_ bar_\\n\",\n    \"html\": \"<p><em><em>foo</em> bar</em></p>\\n\",\n    \"example\": 407,\n    \"start_line\": 6831,\n    \"end_line\": 6835,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo *bar**\\n\",\n    \"html\": \"<p><em>foo <em>bar</em></em></p>\\n\",\n    \"example\": 408,\n    \"start_line\": 6838,\n    \"end_line\": 6842,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo **bar** baz*\\n\",\n    \"html\": \"<p><em>foo <strong>bar</strong> baz</em></p>\\n\",\n    \"example\": 409,\n    \"start_line\": 6845,\n    \"end_line\": 6849,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo**bar**baz*\\n\",\n    \"html\": \"<p><em>foo<strong>bar</strong>baz</em></p>\\n\",\n    \"example\": 410,\n    \"start_line\": 6851,\n    \"end_line\": 6855,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo**bar*\\n\",\n    \"html\": \"<p><em>foo**bar</em></p>\\n\",\n    \"example\": 411,\n    \"start_line\": 6875,\n    \"end_line\": 6879,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"***foo** bar*\\n\",\n    \"html\": \"<p><em><strong>foo</strong> bar</em></p>\\n\",\n    \"example\": 412,\n    \"start_line\": 6888,\n    \"end_line\": 6892,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo **bar***\\n\",\n    \"html\": \"<p><em>foo <strong>bar</strong></em></p>\\n\",\n    \"example\": 413,\n    \"start_line\": 6895,\n    \"end_line\": 6899,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo**bar***\\n\",\n    \"html\": \"<p><em>foo<strong>bar</strong></em></p>\\n\",\n    \"example\": 414,\n    \"start_line\": 6902,\n    \"end_line\": 6906,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo***bar***baz\\n\",\n    \"html\": \"<p>foo<em><strong>bar</strong></em>baz</p>\\n\",\n    \"example\": 415,\n    \"start_line\": 6913,\n    \"end_line\": 6917,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo******bar*********baz\\n\",\n    \"html\": \"<p>foo<strong><strong><strong>bar</strong></strong></strong>***baz</p>\\n\",\n    \"example\": 416,\n    \"start_line\": 6919,\n    \"end_line\": 6923,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo **bar *baz* bim** bop*\\n\",\n    \"html\": \"<p><em>foo <strong>bar <em>baz</em> bim</strong> bop</em></p>\\n\",\n    \"example\": 417,\n    \"start_line\": 6928,\n    \"end_line\": 6932,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo [*bar*](/url)*\\n\",\n    \"html\": \"<p><em>foo <a href=\\\"/url\\\"><em>bar</em></a></em></p>\\n\",\n    \"example\": 418,\n    \"start_line\": 6935,\n    \"end_line\": 6939,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"** is not an empty emphasis\\n\",\n    \"html\": \"<p>** is not an empty emphasis</p>\\n\",\n    \"example\": 419,\n    \"start_line\": 6944,\n    \"end_line\": 6948,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**** is not an empty strong emphasis\\n\",\n    \"html\": \"<p>**** is not an empty strong emphasis</p>\\n\",\n    \"example\": 420,\n    \"start_line\": 6951,\n    \"end_line\": 6955,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo [bar](/url)**\\n\",\n    \"html\": \"<p><strong>foo <a href=\\\"/url\\\">bar</a></strong></p>\\n\",\n    \"example\": 421,\n    \"start_line\": 6964,\n    \"end_line\": 6968,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo\\nbar**\\n\",\n    \"html\": \"<p><strong>foo\\nbar</strong></p>\\n\",\n    \"example\": 422,\n    \"start_line\": 6971,\n    \"end_line\": 6977,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo _bar_ baz__\\n\",\n    \"html\": \"<p><strong>foo <em>bar</em> baz</strong></p>\\n\",\n    \"example\": 423,\n    \"start_line\": 6983,\n    \"end_line\": 6987,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo __bar__ baz__\\n\",\n    \"html\": \"<p><strong>foo <strong>bar</strong> baz</strong></p>\\n\",\n    \"example\": 424,\n    \"start_line\": 6990,\n    \"end_line\": 6994,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"____foo__ bar__\\n\",\n    \"html\": \"<p><strong><strong>foo</strong> bar</strong></p>\\n\",\n    \"example\": 425,\n    \"start_line\": 6997,\n    \"end_line\": 7001,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo **bar****\\n\",\n    \"html\": \"<p><strong>foo <strong>bar</strong></strong></p>\\n\",\n    \"example\": 426,\n    \"start_line\": 7004,\n    \"end_line\": 7008,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo *bar* baz**\\n\",\n    \"html\": \"<p><strong>foo <em>bar</em> baz</strong></p>\\n\",\n    \"example\": 427,\n    \"start_line\": 7011,\n    \"end_line\": 7015,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo*bar*baz**\\n\",\n    \"html\": \"<p><strong>foo<em>bar</em>baz</strong></p>\\n\",\n    \"example\": 428,\n    \"start_line\": 7018,\n    \"end_line\": 7022,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"***foo* bar**\\n\",\n    \"html\": \"<p><strong><em>foo</em> bar</strong></p>\\n\",\n    \"example\": 429,\n    \"start_line\": 7025,\n    \"end_line\": 7029,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo *bar***\\n\",\n    \"html\": \"<p><strong>foo <em>bar</em></strong></p>\\n\",\n    \"example\": 430,\n    \"start_line\": 7032,\n    \"end_line\": 7036,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo *bar **baz**\\nbim* bop**\\n\",\n    \"html\": \"<p><strong>foo <em>bar <strong>baz</strong>\\nbim</em> bop</strong></p>\\n\",\n    \"example\": 431,\n    \"start_line\": 7041,\n    \"end_line\": 7047,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo [*bar*](/url)**\\n\",\n    \"html\": \"<p><strong>foo <a href=\\\"/url\\\"><em>bar</em></a></strong></p>\\n\",\n    \"example\": 432,\n    \"start_line\": 7050,\n    \"end_line\": 7054,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__ is not an empty emphasis\\n\",\n    \"html\": \"<p>__ is not an empty emphasis</p>\\n\",\n    \"example\": 433,\n    \"start_line\": 7059,\n    \"end_line\": 7063,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"____ is not an empty strong emphasis\\n\",\n    \"html\": \"<p>____ is not an empty strong emphasis</p>\\n\",\n    \"example\": 434,\n    \"start_line\": 7066,\n    \"end_line\": 7070,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo ***\\n\",\n    \"html\": \"<p>foo ***</p>\\n\",\n    \"example\": 435,\n    \"start_line\": 7076,\n    \"end_line\": 7080,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo *\\\\**\\n\",\n    \"html\": \"<p>foo <em>*</em></p>\\n\",\n    \"example\": 436,\n    \"start_line\": 7083,\n    \"end_line\": 7087,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo *_*\\n\",\n    \"html\": \"<p>foo <em>_</em></p>\\n\",\n    \"example\": 437,\n    \"start_line\": 7090,\n    \"end_line\": 7094,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo *****\\n\",\n    \"html\": \"<p>foo *****</p>\\n\",\n    \"example\": 438,\n    \"start_line\": 7097,\n    \"end_line\": 7101,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo **\\\\***\\n\",\n    \"html\": \"<p>foo <strong>*</strong></p>\\n\",\n    \"example\": 439,\n    \"start_line\": 7104,\n    \"end_line\": 7108,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo **_**\\n\",\n    \"html\": \"<p>foo <strong>_</strong></p>\\n\",\n    \"example\": 440,\n    \"start_line\": 7111,\n    \"end_line\": 7115,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo*\\n\",\n    \"html\": \"<p>*<em>foo</em></p>\\n\",\n    \"example\": 441,\n    \"start_line\": 7122,\n    \"end_line\": 7126,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo**\\n\",\n    \"html\": \"<p><em>foo</em>*</p>\\n\",\n    \"example\": 442,\n    \"start_line\": 7129,\n    \"end_line\": 7133,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"***foo**\\n\",\n    \"html\": \"<p>*<strong>foo</strong></p>\\n\",\n    \"example\": 443,\n    \"start_line\": 7136,\n    \"end_line\": 7140,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"****foo*\\n\",\n    \"html\": \"<p>***<em>foo</em></p>\\n\",\n    \"example\": 444,\n    \"start_line\": 7143,\n    \"end_line\": 7147,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo***\\n\",\n    \"html\": \"<p><strong>foo</strong>*</p>\\n\",\n    \"example\": 445,\n    \"start_line\": 7150,\n    \"end_line\": 7154,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo****\\n\",\n    \"html\": \"<p><em>foo</em>***</p>\\n\",\n    \"example\": 446,\n    \"start_line\": 7157,\n    \"end_line\": 7161,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo ___\\n\",\n    \"html\": \"<p>foo ___</p>\\n\",\n    \"example\": 447,\n    \"start_line\": 7167,\n    \"end_line\": 7171,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo _\\\\__\\n\",\n    \"html\": \"<p>foo <em>_</em></p>\\n\",\n    \"example\": 448,\n    \"start_line\": 7174,\n    \"end_line\": 7178,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo _*_\\n\",\n    \"html\": \"<p>foo <em>*</em></p>\\n\",\n    \"example\": 449,\n    \"start_line\": 7181,\n    \"end_line\": 7185,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo _____\\n\",\n    \"html\": \"<p>foo _____</p>\\n\",\n    \"example\": 450,\n    \"start_line\": 7188,\n    \"end_line\": 7192,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo __\\\\___\\n\",\n    \"html\": \"<p>foo <strong>_</strong></p>\\n\",\n    \"example\": 451,\n    \"start_line\": 7195,\n    \"end_line\": 7199,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo __*__\\n\",\n    \"html\": \"<p>foo <strong>*</strong></p>\\n\",\n    \"example\": 452,\n    \"start_line\": 7202,\n    \"end_line\": 7206,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo_\\n\",\n    \"html\": \"<p>_<em>foo</em></p>\\n\",\n    \"example\": 453,\n    \"start_line\": 7209,\n    \"end_line\": 7213,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo__\\n\",\n    \"html\": \"<p><em>foo</em>_</p>\\n\",\n    \"example\": 454,\n    \"start_line\": 7220,\n    \"end_line\": 7224,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"___foo__\\n\",\n    \"html\": \"<p>_<strong>foo</strong></p>\\n\",\n    \"example\": 455,\n    \"start_line\": 7227,\n    \"end_line\": 7231,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"____foo_\\n\",\n    \"html\": \"<p>___<em>foo</em></p>\\n\",\n    \"example\": 456,\n    \"start_line\": 7234,\n    \"end_line\": 7238,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo___\\n\",\n    \"html\": \"<p><strong>foo</strong>_</p>\\n\",\n    \"example\": 457,\n    \"start_line\": 7241,\n    \"end_line\": 7245,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo____\\n\",\n    \"html\": \"<p><em>foo</em>___</p>\\n\",\n    \"example\": 458,\n    \"start_line\": 7248,\n    \"end_line\": 7252,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo**\\n\",\n    \"html\": \"<p><strong>foo</strong></p>\\n\",\n    \"example\": 459,\n    \"start_line\": 7258,\n    \"end_line\": 7262,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*_foo_*\\n\",\n    \"html\": \"<p><em><em>foo</em></em></p>\\n\",\n    \"example\": 460,\n    \"start_line\": 7265,\n    \"end_line\": 7269,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo__\\n\",\n    \"html\": \"<p><strong>foo</strong></p>\\n\",\n    \"example\": 461,\n    \"start_line\": 7272,\n    \"end_line\": 7276,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_*foo*_\\n\",\n    \"html\": \"<p><em><em>foo</em></em></p>\\n\",\n    \"example\": 462,\n    \"start_line\": 7279,\n    \"end_line\": 7283,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"****foo****\\n\",\n    \"html\": \"<p><strong><strong>foo</strong></strong></p>\\n\",\n    \"example\": 463,\n    \"start_line\": 7289,\n    \"end_line\": 7293,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"____foo____\\n\",\n    \"html\": \"<p><strong><strong>foo</strong></strong></p>\\n\",\n    \"example\": 464,\n    \"start_line\": 7296,\n    \"end_line\": 7300,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"******foo******\\n\",\n    \"html\": \"<p><strong><strong><strong>foo</strong></strong></strong></p>\\n\",\n    \"example\": 465,\n    \"start_line\": 7307,\n    \"end_line\": 7311,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"***foo***\\n\",\n    \"html\": \"<p><em><strong>foo</strong></em></p>\\n\",\n    \"example\": 466,\n    \"start_line\": 7316,\n    \"end_line\": 7320,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_____foo_____\\n\",\n    \"html\": \"<p><em><strong><strong>foo</strong></strong></em></p>\\n\",\n    \"example\": 467,\n    \"start_line\": 7323,\n    \"end_line\": 7327,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo _bar* baz_\\n\",\n    \"html\": \"<p><em>foo _bar</em> baz_</p>\\n\",\n    \"example\": 468,\n    \"start_line\": 7332,\n    \"end_line\": 7336,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo __bar *baz bim__ bam*\\n\",\n    \"html\": \"<p><em>foo <strong>bar *baz bim</strong> bam</em></p>\\n\",\n    \"example\": 469,\n    \"start_line\": 7339,\n    \"end_line\": 7343,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo **bar baz**\\n\",\n    \"html\": \"<p>**foo <strong>bar baz</strong></p>\\n\",\n    \"example\": 470,\n    \"start_line\": 7348,\n    \"end_line\": 7352,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo *bar baz*\\n\",\n    \"html\": \"<p>*foo <em>bar baz</em></p>\\n\",\n    \"example\": 471,\n    \"start_line\": 7355,\n    \"end_line\": 7359,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*[bar*](/url)\\n\",\n    \"html\": \"<p>*<a href=\\\"/url\\\">bar*</a></p>\\n\",\n    \"example\": 472,\n    \"start_line\": 7364,\n    \"end_line\": 7368,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo [bar_](/url)\\n\",\n    \"html\": \"<p>_foo <a href=\\\"/url\\\">bar_</a></p>\\n\",\n    \"example\": 473,\n    \"start_line\": 7371,\n    \"end_line\": 7375,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*<img src=\\\"foo\\\" title=\\\"*\\\"/>\\n\",\n    \"html\": \"<p>*<img src=\\\"foo\\\" title=\\\"*\\\"/></p>\\n\",\n    \"example\": 474,\n    \"start_line\": 7378,\n    \"end_line\": 7382,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**<a href=\\\"**\\\">\\n\",\n    \"html\": \"<p>**<a href=\\\"**\\\"></p>\\n\",\n    \"example\": 475,\n    \"start_line\": 7385,\n    \"end_line\": 7389,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__<a href=\\\"__\\\">\\n\",\n    \"html\": \"<p>__<a href=\\\"__\\\"></p>\\n\",\n    \"example\": 476,\n    \"start_line\": 7392,\n    \"end_line\": 7396,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*a `*`*\\n\",\n    \"html\": \"<p><em>a <code>*</code></em></p>\\n\",\n    \"example\": 477,\n    \"start_line\": 7399,\n    \"end_line\": 7403,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_a `_`_\\n\",\n    \"html\": \"<p><em>a <code>_</code></em></p>\\n\",\n    \"example\": 478,\n    \"start_line\": 7406,\n    \"end_line\": 7410,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**a<http://foo.bar/?q=**>\\n\",\n    \"html\": \"<p>**a<a href=\\\"http://foo.bar/?q=**\\\">http://foo.bar/?q=**</a></p>\\n\",\n    \"example\": 479,\n    \"start_line\": 7413,\n    \"end_line\": 7417,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__a<http://foo.bar/?q=__>\\n\",\n    \"html\": \"<p>__a<a href=\\\"http://foo.bar/?q=__\\\">http://foo.bar/?q=__</a></p>\\n\",\n    \"example\": 480,\n    \"start_line\": 7420,\n    \"end_line\": 7424,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"[link](/uri \\\"title\\\")\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\" title=\\\"title\\\">link</a></p>\\n\",\n    \"example\": 481,\n    \"start_line\": 7503,\n    \"end_line\": 7507,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](/uri)\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">link</a></p>\\n\",\n    \"example\": 482,\n    \"start_line\": 7512,\n    \"end_line\": 7516,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link]()\\n\",\n    \"html\": \"<p><a href=\\\"\\\">link</a></p>\\n\",\n    \"example\": 483,\n    \"start_line\": 7521,\n    \"end_line\": 7525,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](<>)\\n\",\n    \"html\": \"<p><a href=\\\"\\\">link</a></p>\\n\",\n    \"example\": 484,\n    \"start_line\": 7528,\n    \"end_line\": 7532,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](/my uri)\\n\",\n    \"html\": \"<p>[link](/my uri)</p>\\n\",\n    \"example\": 485,\n    \"start_line\": 7537,\n    \"end_line\": 7541,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](</my uri>)\\n\",\n    \"html\": \"<p><a href=\\\"/my%20uri\\\">link</a></p>\\n\",\n    \"example\": 486,\n    \"start_line\": 7543,\n    \"end_line\": 7547,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](foo\\nbar)\\n\",\n    \"html\": \"<p>[link](foo\\nbar)</p>\\n\",\n    \"example\": 487,\n    \"start_line\": 7552,\n    \"end_line\": 7558,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](<foo\\nbar>)\\n\",\n    \"html\": \"<p>[link](<foo\\nbar>)</p>\\n\",\n    \"example\": 488,\n    \"start_line\": 7560,\n    \"end_line\": 7566,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[a](<b)c>)\\n\",\n    \"html\": \"<p><a href=\\\"b)c\\\">a</a></p>\\n\",\n    \"example\": 489,\n    \"start_line\": 7571,\n    \"end_line\": 7575,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](<foo\\\\>)\\n\",\n    \"html\": \"<p>[link](&lt;foo&gt;)</p>\\n\",\n    \"example\": 490,\n    \"start_line\": 7579,\n    \"end_line\": 7583,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[a](<b)c\\n[a](<b)c>\\n[a](<b>c)\\n\",\n    \"html\": \"<p>[a](&lt;b)c\\n[a](&lt;b)c&gt;\\n[a](<b>c)</p>\\n\",\n    \"example\": 491,\n    \"start_line\": 7588,\n    \"end_line\": 7596,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](\\\\(foo\\\\))\\n\",\n    \"html\": \"<p><a href=\\\"(foo)\\\">link</a></p>\\n\",\n    \"example\": 492,\n    \"start_line\": 7600,\n    \"end_line\": 7604,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](foo(and(bar)))\\n\",\n    \"html\": \"<p><a href=\\\"foo(and(bar))\\\">link</a></p>\\n\",\n    \"example\": 493,\n    \"start_line\": 7609,\n    \"end_line\": 7613,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](foo\\\\(and\\\\(bar\\\\))\\n\",\n    \"html\": \"<p><a href=\\\"foo(and(bar)\\\">link</a></p>\\n\",\n    \"example\": 494,\n    \"start_line\": 7618,\n    \"end_line\": 7622,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](<foo(and(bar)>)\\n\",\n    \"html\": \"<p><a href=\\\"foo(and(bar)\\\">link</a></p>\\n\",\n    \"example\": 495,\n    \"start_line\": 7625,\n    \"end_line\": 7629,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](foo\\\\)\\\\:)\\n\",\n    \"html\": \"<p><a href=\\\"foo):\\\">link</a></p>\\n\",\n    \"example\": 496,\n    \"start_line\": 7635,\n    \"end_line\": 7639,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](#fragment)\\n\\n[link](http://example.com#fragment)\\n\\n[link](http://example.com?foo=3#frag)\\n\",\n    \"html\": \"<p><a href=\\\"#fragment\\\">link</a></p>\\n<p><a href=\\\"http://example.com#fragment\\\">link</a></p>\\n<p><a href=\\\"http://example.com?foo=3#frag\\\">link</a></p>\\n\",\n    \"example\": 497,\n    \"start_line\": 7644,\n    \"end_line\": 7654,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](foo\\\\bar)\\n\",\n    \"html\": \"<p><a href=\\\"foo%5Cbar\\\">link</a></p>\\n\",\n    \"example\": 498,\n    \"start_line\": 7660,\n    \"end_line\": 7664,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](foo%20b&auml;)\\n\",\n    \"html\": \"<p><a href=\\\"foo%20b%C3%A4\\\">link</a></p>\\n\",\n    \"example\": 499,\n    \"start_line\": 7676,\n    \"end_line\": 7680,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](\\\"title\\\")\\n\",\n    \"html\": \"<p><a href=\\\"%22title%22\\\">link</a></p>\\n\",\n    \"example\": 500,\n    \"start_line\": 7687,\n    \"end_line\": 7691,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](/url \\\"title\\\")\\n[link](/url 'title')\\n[link](/url (title))\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\">link</a>\\n<a href=\\\"/url\\\" title=\\\"title\\\">link</a>\\n<a href=\\\"/url\\\" title=\\\"title\\\">link</a></p>\\n\",\n    \"example\": 501,\n    \"start_line\": 7696,\n    \"end_line\": 7704,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](/url \\\"title \\\\\\\"&quot;\\\")\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title &quot;&quot;\\\">link</a></p>\\n\",\n    \"example\": 502,\n    \"start_line\": 7710,\n    \"end_line\": 7714,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](/url \\\"title\\\")\\n\",\n    \"html\": \"<p><a href=\\\"/url%C2%A0%22title%22\\\">link</a></p>\\n\",\n    \"example\": 503,\n    \"start_line\": 7720,\n    \"end_line\": 7724,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](/url \\\"title \\\"and\\\" title\\\")\\n\",\n    \"html\": \"<p>[link](/url &quot;title &quot;and&quot; title&quot;)</p>\\n\",\n    \"example\": 504,\n    \"start_line\": 7729,\n    \"end_line\": 7733,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](/url 'title \\\"and\\\" title')\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title &quot;and&quot; title\\\">link</a></p>\\n\",\n    \"example\": 505,\n    \"start_line\": 7738,\n    \"end_line\": 7742,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](   /uri\\n  \\\"title\\\"  )\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\" title=\\\"title\\\">link</a></p>\\n\",\n    \"example\": 506,\n    \"start_line\": 7762,\n    \"end_line\": 7767,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link] (/uri)\\n\",\n    \"html\": \"<p>[link] (/uri)</p>\\n\",\n    \"example\": 507,\n    \"start_line\": 7773,\n    \"end_line\": 7777,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link [foo [bar]]](/uri)\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">link [foo [bar]]</a></p>\\n\",\n    \"example\": 508,\n    \"start_line\": 7783,\n    \"end_line\": 7787,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link] bar](/uri)\\n\",\n    \"html\": \"<p>[link] bar](/uri)</p>\\n\",\n    \"example\": 509,\n    \"start_line\": 7790,\n    \"end_line\": 7794,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link [bar](/uri)\\n\",\n    \"html\": \"<p>[link <a href=\\\"/uri\\\">bar</a></p>\\n\",\n    \"example\": 510,\n    \"start_line\": 7797,\n    \"end_line\": 7801,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link \\\\[bar](/uri)\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">link [bar</a></p>\\n\",\n    \"example\": 511,\n    \"start_line\": 7804,\n    \"end_line\": 7808,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link *foo **bar** `#`*](/uri)\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">link <em>foo <strong>bar</strong> <code>#</code></em></a></p>\\n\",\n    \"example\": 512,\n    \"start_line\": 7813,\n    \"end_line\": 7817,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[![moon](moon.jpg)](/uri)\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\"><img src=\\\"moon.jpg\\\" alt=\\\"moon\\\" /></a></p>\\n\",\n    \"example\": 513,\n    \"start_line\": 7820,\n    \"end_line\": 7824,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo [bar](/uri)](/uri)\\n\",\n    \"html\": \"<p>[foo <a href=\\\"/uri\\\">bar</a>](/uri)</p>\\n\",\n    \"example\": 514,\n    \"start_line\": 7829,\n    \"end_line\": 7833,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo *[bar [baz](/uri)](/uri)*](/uri)\\n\",\n    \"html\": \"<p>[foo <em>[bar <a href=\\\"/uri\\\">baz</a>](/uri)</em>](/uri)</p>\\n\",\n    \"example\": 515,\n    \"start_line\": 7836,\n    \"end_line\": 7840,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"![[[foo](uri1)](uri2)](uri3)\\n\",\n    \"html\": \"<p><img src=\\\"uri3\\\" alt=\\\"[foo](uri2)\\\" /></p>\\n\",\n    \"example\": 516,\n    \"start_line\": 7843,\n    \"end_line\": 7847,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"*[foo*](/uri)\\n\",\n    \"html\": \"<p>*<a href=\\\"/uri\\\">foo*</a></p>\\n\",\n    \"example\": 517,\n    \"start_line\": 7853,\n    \"end_line\": 7857,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo *bar](baz*)\\n\",\n    \"html\": \"<p><a href=\\\"baz*\\\">foo *bar</a></p>\\n\",\n    \"example\": 518,\n    \"start_line\": 7860,\n    \"end_line\": 7864,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"*foo [bar* baz]\\n\",\n    \"html\": \"<p><em>foo [bar</em> baz]</p>\\n\",\n    \"example\": 519,\n    \"start_line\": 7870,\n    \"end_line\": 7874,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo <bar attr=\\\"](baz)\\\">\\n\",\n    \"html\": \"<p>[foo <bar attr=\\\"](baz)\\\"></p>\\n\",\n    \"example\": 520,\n    \"start_line\": 7880,\n    \"end_line\": 7884,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo`](/uri)`\\n\",\n    \"html\": \"<p>[foo<code>](/uri)</code></p>\\n\",\n    \"example\": 521,\n    \"start_line\": 7887,\n    \"end_line\": 7891,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo<http://example.com/?search=](uri)>\\n\",\n    \"html\": \"<p>[foo<a href=\\\"http://example.com/?search=%5D(uri)\\\">http://example.com/?search=](uri)</a></p>\\n\",\n    \"example\": 522,\n    \"start_line\": 7894,\n    \"end_line\": 7898,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][bar]\\n\\n[bar]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\">foo</a></p>\\n\",\n    \"example\": 523,\n    \"start_line\": 7932,\n    \"end_line\": 7938,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link [foo [bar]]][ref]\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">link [foo [bar]]</a></p>\\n\",\n    \"example\": 524,\n    \"start_line\": 7947,\n    \"end_line\": 7953,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link \\\\[bar][ref]\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">link [bar</a></p>\\n\",\n    \"example\": 525,\n    \"start_line\": 7956,\n    \"end_line\": 7962,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link *foo **bar** `#`*][ref]\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">link <em>foo <strong>bar</strong> <code>#</code></em></a></p>\\n\",\n    \"example\": 526,\n    \"start_line\": 7967,\n    \"end_line\": 7973,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[![moon](moon.jpg)][ref]\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\"><img src=\\\"moon.jpg\\\" alt=\\\"moon\\\" /></a></p>\\n\",\n    \"example\": 527,\n    \"start_line\": 7976,\n    \"end_line\": 7982,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo [bar](/uri)][ref]\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p>[foo <a href=\\\"/uri\\\">bar</a>]<a href=\\\"/uri\\\">ref</a></p>\\n\",\n    \"example\": 528,\n    \"start_line\": 7987,\n    \"end_line\": 7993,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo *bar [baz][ref]*][ref]\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p>[foo <em>bar <a href=\\\"/uri\\\">baz</a></em>]<a href=\\\"/uri\\\">ref</a></p>\\n\",\n    \"example\": 529,\n    \"start_line\": 7996,\n    \"end_line\": 8002,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"*[foo*][ref]\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p>*<a href=\\\"/uri\\\">foo*</a></p>\\n\",\n    \"example\": 530,\n    \"start_line\": 8011,\n    \"end_line\": 8017,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo *bar][ref]\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">foo *bar</a></p>\\n\",\n    \"example\": 531,\n    \"start_line\": 8020,\n    \"end_line\": 8026,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo <bar attr=\\\"][ref]\\\">\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p>[foo <bar attr=\\\"][ref]\\\"></p>\\n\",\n    \"example\": 532,\n    \"start_line\": 8032,\n    \"end_line\": 8038,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo`][ref]`\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p>[foo<code>][ref]</code></p>\\n\",\n    \"example\": 533,\n    \"start_line\": 8041,\n    \"end_line\": 8047,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo<http://example.com/?search=][ref]>\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p>[foo<a href=\\\"http://example.com/?search=%5D%5Bref%5D\\\">http://example.com/?search=][ref]</a></p>\\n\",\n    \"example\": 534,\n    \"start_line\": 8050,\n    \"end_line\": 8056,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][BaR]\\n\\n[bar]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\">foo</a></p>\\n\",\n    \"example\": 535,\n    \"start_line\": 8061,\n    \"end_line\": 8067,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[Толпой][Толпой] is a Russian word.\\n\\n[ТОЛПОЙ]: /url\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\">Толпой</a> is a Russian word.</p>\\n\",\n    \"example\": 536,\n    \"start_line\": 8072,\n    \"end_line\": 8078,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[Foo\\n  bar]: /url\\n\\n[Baz][Foo bar]\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\">Baz</a></p>\\n\",\n    \"example\": 537,\n    \"start_line\": 8084,\n    \"end_line\": 8091,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo] [bar]\\n\\n[bar]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p>[foo] <a href=\\\"/url\\\" title=\\\"title\\\">bar</a></p>\\n\",\n    \"example\": 538,\n    \"start_line\": 8097,\n    \"end_line\": 8103,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo]\\n[bar]\\n\\n[bar]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p>[foo]\\n<a href=\\\"/url\\\" title=\\\"title\\\">bar</a></p>\\n\",\n    \"example\": 539,\n    \"start_line\": 8106,\n    \"end_line\": 8114,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo]: /url1\\n\\n[foo]: /url2\\n\\n[bar][foo]\\n\",\n    \"html\": \"<p><a href=\\\"/url1\\\">bar</a></p>\\n\",\n    \"example\": 540,\n    \"start_line\": 8147,\n    \"end_line\": 8155,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[bar][foo\\\\!]\\n\\n[foo!]: /url\\n\",\n    \"html\": \"<p>[bar][foo!]</p>\\n\",\n    \"example\": 541,\n    \"start_line\": 8162,\n    \"end_line\": 8168,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][ref[]\\n\\n[ref[]: /uri\\n\",\n    \"html\": \"<p>[foo][ref[]</p>\\n<p>[ref[]: /uri</p>\\n\",\n    \"example\": 542,\n    \"start_line\": 8174,\n    \"end_line\": 8181,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][ref[bar]]\\n\\n[ref[bar]]: /uri\\n\",\n    \"html\": \"<p>[foo][ref[bar]]</p>\\n<p>[ref[bar]]: /uri</p>\\n\",\n    \"example\": 543,\n    \"start_line\": 8184,\n    \"end_line\": 8191,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[[[foo]]]\\n\\n[[[foo]]]: /url\\n\",\n    \"html\": \"<p>[[[foo]]]</p>\\n<p>[[[foo]]]: /url</p>\\n\",\n    \"example\": 544,\n    \"start_line\": 8194,\n    \"end_line\": 8201,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][ref\\\\[]\\n\\n[ref\\\\[]: /uri\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">foo</a></p>\\n\",\n    \"example\": 545,\n    \"start_line\": 8204,\n    \"end_line\": 8210,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[bar\\\\\\\\]: /uri\\n\\n[bar\\\\\\\\]\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">bar\\\\</a></p>\\n\",\n    \"example\": 546,\n    \"start_line\": 8215,\n    \"end_line\": 8221,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[]\\n\\n[]: /uri\\n\",\n    \"html\": \"<p>[]</p>\\n<p>[]: /uri</p>\\n\",\n    \"example\": 547,\n    \"start_line\": 8226,\n    \"end_line\": 8233,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[\\n ]\\n\\n[\\n ]: /uri\\n\",\n    \"html\": \"<p>[\\n]</p>\\n<p>[\\n]: /uri</p>\\n\",\n    \"example\": 548,\n    \"start_line\": 8236,\n    \"end_line\": 8247,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\">foo</a></p>\\n\",\n    \"example\": 549,\n    \"start_line\": 8259,\n    \"end_line\": 8265,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[*foo* bar][]\\n\\n[*foo* bar]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\"><em>foo</em> bar</a></p>\\n\",\n    \"example\": 550,\n    \"start_line\": 8268,\n    \"end_line\": 8274,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[Foo][]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\">Foo</a></p>\\n\",\n    \"example\": 551,\n    \"start_line\": 8279,\n    \"end_line\": 8285,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo] \\n[]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\">foo</a>\\n[]</p>\\n\",\n    \"example\": 552,\n    \"start_line\": 8292,\n    \"end_line\": 8300,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\">foo</a></p>\\n\",\n    \"example\": 553,\n    \"start_line\": 8312,\n    \"end_line\": 8318,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[*foo* bar]\\n\\n[*foo* bar]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\"><em>foo</em> bar</a></p>\\n\",\n    \"example\": 554,\n    \"start_line\": 8321,\n    \"end_line\": 8327,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[[*foo* bar]]\\n\\n[*foo* bar]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p>[<a href=\\\"/url\\\" title=\\\"title\\\"><em>foo</em> bar</a>]</p>\\n\",\n    \"example\": 555,\n    \"start_line\": 8330,\n    \"end_line\": 8336,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[[bar [foo]\\n\\n[foo]: /url\\n\",\n    \"html\": \"<p>[[bar <a href=\\\"/url\\\">foo</a></p>\\n\",\n    \"example\": 556,\n    \"start_line\": 8339,\n    \"end_line\": 8345,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[Foo]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\">Foo</a></p>\\n\",\n    \"example\": 557,\n    \"start_line\": 8350,\n    \"end_line\": 8356,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo] bar\\n\\n[foo]: /url\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\">foo</a> bar</p>\\n\",\n    \"example\": 558,\n    \"start_line\": 8361,\n    \"end_line\": 8367,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"\\\\[foo]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p>[foo]</p>\\n\",\n    \"example\": 559,\n    \"start_line\": 8373,\n    \"end_line\": 8379,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo*]: /url\\n\\n*[foo*]\\n\",\n    \"html\": \"<p>*<a href=\\\"/url\\\">foo*</a></p>\\n\",\n    \"example\": 560,\n    \"start_line\": 8385,\n    \"end_line\": 8391,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][bar]\\n\\n[foo]: /url1\\n[bar]: /url2\\n\",\n    \"html\": \"<p><a href=\\\"/url2\\\">foo</a></p>\\n\",\n    \"example\": 561,\n    \"start_line\": 8397,\n    \"end_line\": 8404,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][]\\n\\n[foo]: /url1\\n\",\n    \"html\": \"<p><a href=\\\"/url1\\\">foo</a></p>\\n\",\n    \"example\": 562,\n    \"start_line\": 8406,\n    \"end_line\": 8412,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo]()\\n\\n[foo]: /url1\\n\",\n    \"html\": \"<p><a href=\\\"\\\">foo</a></p>\\n\",\n    \"example\": 563,\n    \"start_line\": 8416,\n    \"end_line\": 8422,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo](not a link)\\n\\n[foo]: /url1\\n\",\n    \"html\": \"<p><a href=\\\"/url1\\\">foo</a>(not a link)</p>\\n\",\n    \"example\": 564,\n    \"start_line\": 8424,\n    \"end_line\": 8430,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][bar][baz]\\n\\n[baz]: /url\\n\",\n    \"html\": \"<p>[foo]<a href=\\\"/url\\\">bar</a></p>\\n\",\n    \"example\": 565,\n    \"start_line\": 8435,\n    \"end_line\": 8441,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][bar][baz]\\n\\n[baz]: /url1\\n[bar]: /url2\\n\",\n    \"html\": \"<p><a href=\\\"/url2\\\">foo</a><a href=\\\"/url1\\\">baz</a></p>\\n\",\n    \"example\": 566,\n    \"start_line\": 8447,\n    \"end_line\": 8454,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][bar][baz]\\n\\n[baz]: /url1\\n[foo]: /url2\\n\",\n    \"html\": \"<p>[foo]<a href=\\\"/url1\\\">bar</a></p>\\n\",\n    \"example\": 567,\n    \"start_line\": 8460,\n    \"end_line\": 8467,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"![foo](/url \\\"title\\\")\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"foo\\\" title=\\\"title\\\" /></p>\\n\",\n    \"example\": 568,\n    \"start_line\": 8483,\n    \"end_line\": 8487,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo *bar*]\\n\\n[foo *bar*]: train.jpg \\\"train & tracks\\\"\\n\",\n    \"html\": \"<p><img src=\\\"train.jpg\\\" alt=\\\"foo bar\\\" title=\\\"train &amp; tracks\\\" /></p>\\n\",\n    \"example\": 569,\n    \"start_line\": 8490,\n    \"end_line\": 8496,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo ![bar](/url)](/url2)\\n\",\n    \"html\": \"<p><img src=\\\"/url2\\\" alt=\\\"foo bar\\\" /></p>\\n\",\n    \"example\": 570,\n    \"start_line\": 8499,\n    \"end_line\": 8503,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo [bar](/url)](/url2)\\n\",\n    \"html\": \"<p><img src=\\\"/url2\\\" alt=\\\"foo bar\\\" /></p>\\n\",\n    \"example\": 571,\n    \"start_line\": 8506,\n    \"end_line\": 8510,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo *bar*][]\\n\\n[foo *bar*]: train.jpg \\\"train & tracks\\\"\\n\",\n    \"html\": \"<p><img src=\\\"train.jpg\\\" alt=\\\"foo bar\\\" title=\\\"train &amp; tracks\\\" /></p>\\n\",\n    \"example\": 572,\n    \"start_line\": 8520,\n    \"end_line\": 8526,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo *bar*][foobar]\\n\\n[FOOBAR]: train.jpg \\\"train & tracks\\\"\\n\",\n    \"html\": \"<p><img src=\\\"train.jpg\\\" alt=\\\"foo bar\\\" title=\\\"train &amp; tracks\\\" /></p>\\n\",\n    \"example\": 573,\n    \"start_line\": 8529,\n    \"end_line\": 8535,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo](train.jpg)\\n\",\n    \"html\": \"<p><img src=\\\"train.jpg\\\" alt=\\\"foo\\\" /></p>\\n\",\n    \"example\": 574,\n    \"start_line\": 8538,\n    \"end_line\": 8542,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"My ![foo bar](/path/to/train.jpg  \\\"title\\\"   )\\n\",\n    \"html\": \"<p>My <img src=\\\"/path/to/train.jpg\\\" alt=\\\"foo bar\\\" title=\\\"title\\\" /></p>\\n\",\n    \"example\": 575,\n    \"start_line\": 8545,\n    \"end_line\": 8549,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo](<url>)\\n\",\n    \"html\": \"<p><img src=\\\"url\\\" alt=\\\"foo\\\" /></p>\\n\",\n    \"example\": 576,\n    \"start_line\": 8552,\n    \"end_line\": 8556,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![](/url)\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"\\\" /></p>\\n\",\n    \"example\": 577,\n    \"start_line\": 8559,\n    \"end_line\": 8563,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo][bar]\\n\\n[bar]: /url\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"foo\\\" /></p>\\n\",\n    \"example\": 578,\n    \"start_line\": 8568,\n    \"end_line\": 8574,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo][bar]\\n\\n[BAR]: /url\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"foo\\\" /></p>\\n\",\n    \"example\": 579,\n    \"start_line\": 8577,\n    \"end_line\": 8583,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo][]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"foo\\\" title=\\\"title\\\" /></p>\\n\",\n    \"example\": 580,\n    \"start_line\": 8588,\n    \"end_line\": 8594,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![*foo* bar][]\\n\\n[*foo* bar]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"foo bar\\\" title=\\\"title\\\" /></p>\\n\",\n    \"example\": 581,\n    \"start_line\": 8597,\n    \"end_line\": 8603,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![Foo][]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"Foo\\\" title=\\\"title\\\" /></p>\\n\",\n    \"example\": 582,\n    \"start_line\": 8608,\n    \"end_line\": 8614,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo] \\n[]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"foo\\\" title=\\\"title\\\" />\\n[]</p>\\n\",\n    \"example\": 583,\n    \"start_line\": 8620,\n    \"end_line\": 8628,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"foo\\\" title=\\\"title\\\" /></p>\\n\",\n    \"example\": 584,\n    \"start_line\": 8633,\n    \"end_line\": 8639,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![*foo* bar]\\n\\n[*foo* bar]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"foo bar\\\" title=\\\"title\\\" /></p>\\n\",\n    \"example\": 585,\n    \"start_line\": 8642,\n    \"end_line\": 8648,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![[foo]]\\n\\n[[foo]]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p>![[foo]]</p>\\n<p>[[foo]]: /url &quot;title&quot;</p>\\n\",\n    \"example\": 586,\n    \"start_line\": 8653,\n    \"end_line\": 8660,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![Foo]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"Foo\\\" title=\\\"title\\\" /></p>\\n\",\n    \"example\": 587,\n    \"start_line\": 8665,\n    \"end_line\": 8671,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"!\\\\[foo]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p>![foo]</p>\\n\",\n    \"example\": 588,\n    \"start_line\": 8677,\n    \"end_line\": 8683,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"\\\\![foo]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p>!<a href=\\\"/url\\\" title=\\\"title\\\">foo</a></p>\\n\",\n    \"example\": 589,\n    \"start_line\": 8689,\n    \"end_line\": 8695,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"<http://foo.bar.baz>\\n\",\n    \"html\": \"<p><a href=\\\"http://foo.bar.baz\\\">http://foo.bar.baz</a></p>\\n\",\n    \"example\": 590,\n    \"start_line\": 8722,\n    \"end_line\": 8726,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<http://foo.bar.baz/test?q=hello&id=22&boolean>\\n\",\n    \"html\": \"<p><a href=\\\"http://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean\\\">http://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean</a></p>\\n\",\n    \"example\": 591,\n    \"start_line\": 8729,\n    \"end_line\": 8733,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<irc://foo.bar:2233/baz>\\n\",\n    \"html\": \"<p><a href=\\\"irc://foo.bar:2233/baz\\\">irc://foo.bar:2233/baz</a></p>\\n\",\n    \"example\": 592,\n    \"start_line\": 8736,\n    \"end_line\": 8740,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<MAILTO:FOO@BAR.BAZ>\\n\",\n    \"html\": \"<p><a href=\\\"MAILTO:FOO@BAR.BAZ\\\">MAILTO:FOO@BAR.BAZ</a></p>\\n\",\n    \"example\": 593,\n    \"start_line\": 8745,\n    \"end_line\": 8749,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<a+b+c:d>\\n\",\n    \"html\": \"<p><a href=\\\"a+b+c:d\\\">a+b+c:d</a></p>\\n\",\n    \"example\": 594,\n    \"start_line\": 8757,\n    \"end_line\": 8761,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<made-up-scheme://foo,bar>\\n\",\n    \"html\": \"<p><a href=\\\"made-up-scheme://foo,bar\\\">made-up-scheme://foo,bar</a></p>\\n\",\n    \"example\": 595,\n    \"start_line\": 8764,\n    \"end_line\": 8768,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<http://../>\\n\",\n    \"html\": \"<p><a href=\\\"http://../\\\">http://../</a></p>\\n\",\n    \"example\": 596,\n    \"start_line\": 8771,\n    \"end_line\": 8775,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<localhost:5001/foo>\\n\",\n    \"html\": \"<p><a href=\\\"localhost:5001/foo\\\">localhost:5001/foo</a></p>\\n\",\n    \"example\": 597,\n    \"start_line\": 8778,\n    \"end_line\": 8782,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<http://foo.bar/baz bim>\\n\",\n    \"html\": \"<p>&lt;http://foo.bar/baz bim&gt;</p>\\n\",\n    \"example\": 598,\n    \"start_line\": 8787,\n    \"end_line\": 8791,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<http://example.com/\\\\[\\\\>\\n\",\n    \"html\": \"<p><a href=\\\"http://example.com/%5C%5B%5C\\\">http://example.com/\\\\[\\\\</a></p>\\n\",\n    \"example\": 599,\n    \"start_line\": 8796,\n    \"end_line\": 8800,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<foo@bar.example.com>\\n\",\n    \"html\": \"<p><a href=\\\"mailto:foo@bar.example.com\\\">foo@bar.example.com</a></p>\\n\",\n    \"example\": 600,\n    \"start_line\": 8818,\n    \"end_line\": 8822,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<foo+special@Bar.baz-bar0.com>\\n\",\n    \"html\": \"<p><a href=\\\"mailto:foo+special@Bar.baz-bar0.com\\\">foo+special@Bar.baz-bar0.com</a></p>\\n\",\n    \"example\": 601,\n    \"start_line\": 8825,\n    \"end_line\": 8829,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<foo\\\\+@bar.example.com>\\n\",\n    \"html\": \"<p>&lt;foo+@bar.example.com&gt;</p>\\n\",\n    \"example\": 602,\n    \"start_line\": 8834,\n    \"end_line\": 8838,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<>\\n\",\n    \"html\": \"<p>&lt;&gt;</p>\\n\",\n    \"example\": 603,\n    \"start_line\": 8843,\n    \"end_line\": 8847,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"< http://foo.bar >\\n\",\n    \"html\": \"<p>&lt; http://foo.bar &gt;</p>\\n\",\n    \"example\": 604,\n    \"start_line\": 8850,\n    \"end_line\": 8854,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<m:abc>\\n\",\n    \"html\": \"<p>&lt;m:abc&gt;</p>\\n\",\n    \"example\": 605,\n    \"start_line\": 8857,\n    \"end_line\": 8861,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<foo.bar.baz>\\n\",\n    \"html\": \"<p>&lt;foo.bar.baz&gt;</p>\\n\",\n    \"example\": 606,\n    \"start_line\": 8864,\n    \"end_line\": 8868,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"http://example.com\\n\",\n    \"html\": \"<p>http://example.com</p>\\n\",\n    \"example\": 607,\n    \"start_line\": 8871,\n    \"end_line\": 8875,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"foo@bar.example.com\\n\",\n    \"html\": \"<p>foo@bar.example.com</p>\\n\",\n    \"example\": 608,\n    \"start_line\": 8878,\n    \"end_line\": 8882,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<a><bab><c2c>\\n\",\n    \"html\": \"<p><a><bab><c2c></p>\\n\",\n    \"example\": 609,\n    \"start_line\": 8960,\n    \"end_line\": 8964,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"<a/><b2/>\\n\",\n    \"html\": \"<p><a/><b2/></p>\\n\",\n    \"example\": 610,\n    \"start_line\": 8969,\n    \"end_line\": 8973,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"<a  /><b2\\ndata=\\\"foo\\\" >\\n\",\n    \"html\": \"<p><a  /><b2\\ndata=\\\"foo\\\" ></p>\\n\",\n    \"example\": 611,\n    \"start_line\": 8978,\n    \"end_line\": 8984,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"<a foo=\\\"bar\\\" bam = 'baz <em>\\\"</em>'\\n_boolean zoop:33=zoop:33 />\\n\",\n    \"html\": \"<p><a foo=\\\"bar\\\" bam = 'baz <em>\\\"</em>'\\n_boolean zoop:33=zoop:33 /></p>\\n\",\n    \"example\": 612,\n    \"start_line\": 8989,\n    \"end_line\": 8995,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"Foo <responsive-image src=\\\"foo.jpg\\\" />\\n\",\n    \"html\": \"<p>Foo <responsive-image src=\\\"foo.jpg\\\" /></p>\\n\",\n    \"example\": 613,\n    \"start_line\": 9000,\n    \"end_line\": 9004,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"<33> <__>\\n\",\n    \"html\": \"<p>&lt;33&gt; &lt;__&gt;</p>\\n\",\n    \"example\": 614,\n    \"start_line\": 9009,\n    \"end_line\": 9013,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"<a h*#ref=\\\"hi\\\">\\n\",\n    \"html\": \"<p>&lt;a h*#ref=&quot;hi&quot;&gt;</p>\\n\",\n    \"example\": 615,\n    \"start_line\": 9018,\n    \"end_line\": 9022,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"<a href=\\\"hi'> <a href=hi'>\\n\",\n    \"html\": \"<p>&lt;a href=&quot;hi'&gt; &lt;a href=hi'&gt;</p>\\n\",\n    \"example\": 616,\n    \"start_line\": 9027,\n    \"end_line\": 9031,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"< a><\\nfoo><bar/ >\\n<foo bar=baz\\nbim!bop />\\n\",\n    \"html\": \"<p>&lt; a&gt;&lt;\\nfoo&gt;&lt;bar/ &gt;\\n&lt;foo bar=baz\\nbim!bop /&gt;</p>\\n\",\n    \"example\": 617,\n    \"start_line\": 9036,\n    \"end_line\": 9046,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"<a href='bar'title=title>\\n\",\n    \"html\": \"<p>&lt;a href='bar'title=title&gt;</p>\\n\",\n    \"example\": 618,\n    \"start_line\": 9051,\n    \"end_line\": 9055,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"</a></foo >\\n\",\n    \"html\": \"<p></a></foo ></p>\\n\",\n    \"example\": 619,\n    \"start_line\": 9060,\n    \"end_line\": 9064,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"</a href=\\\"foo\\\">\\n\",\n    \"html\": \"<p>&lt;/a href=&quot;foo&quot;&gt;</p>\\n\",\n    \"example\": 620,\n    \"start_line\": 9069,\n    \"end_line\": 9073,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"foo <!-- this is a\\ncomment - with hyphen -->\\n\",\n    \"html\": \"<p>foo <!-- this is a\\ncomment - with hyphen --></p>\\n\",\n    \"example\": 621,\n    \"start_line\": 9078,\n    \"end_line\": 9084,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"foo <!-- not a comment -- two hyphens -->\\n\",\n    \"html\": \"<p>foo &lt;!-- not a comment -- two hyphens --&gt;</p>\\n\",\n    \"example\": 622,\n    \"start_line\": 9087,\n    \"end_line\": 9091,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"foo <!--> foo -->\\n\\nfoo <!-- foo--->\\n\",\n    \"html\": \"<p>foo &lt;!--&gt; foo --&gt;</p>\\n<p>foo &lt;!-- foo---&gt;</p>\\n\",\n    \"example\": 623,\n    \"start_line\": 9096,\n    \"end_line\": 9103,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"foo <?php echo $a; ?>\\n\",\n    \"html\": \"<p>foo <?php echo $a; ?></p>\\n\",\n    \"example\": 624,\n    \"start_line\": 9108,\n    \"end_line\": 9112,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"foo <!ELEMENT br EMPTY>\\n\",\n    \"html\": \"<p>foo <!ELEMENT br EMPTY></p>\\n\",\n    \"example\": 625,\n    \"start_line\": 9117,\n    \"end_line\": 9121,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"foo <![CDATA[>&<]]>\\n\",\n    \"html\": \"<p>foo <![CDATA[>&<]]></p>\\n\",\n    \"example\": 626,\n    \"start_line\": 9126,\n    \"end_line\": 9130,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"foo <a href=\\\"&ouml;\\\">\\n\",\n    \"html\": \"<p>foo <a href=\\\"&ouml;\\\"></p>\\n\",\n    \"example\": 627,\n    \"start_line\": 9136,\n    \"end_line\": 9140,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"foo <a href=\\\"\\\\*\\\">\\n\",\n    \"html\": \"<p>foo <a href=\\\"\\\\*\\\"></p>\\n\",\n    \"example\": 628,\n    \"start_line\": 9145,\n    \"end_line\": 9149,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"<a href=\\\"\\\\\\\"\\\">\\n\",\n    \"html\": \"<p>&lt;a href=&quot;&quot;&quot;&gt;</p>\\n\",\n    \"example\": 629,\n    \"start_line\": 9152,\n    \"end_line\": 9156,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"foo  \\nbaz\\n\",\n    \"html\": \"<p>foo<br />\\nbaz</p>\\n\",\n    \"example\": 630,\n    \"start_line\": 9166,\n    \"end_line\": 9172,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"foo\\\\\\nbaz\\n\",\n    \"html\": \"<p>foo<br />\\nbaz</p>\\n\",\n    \"example\": 631,\n    \"start_line\": 9178,\n    \"end_line\": 9184,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"foo       \\nbaz\\n\",\n    \"html\": \"<p>foo<br />\\nbaz</p>\\n\",\n    \"example\": 632,\n    \"start_line\": 9189,\n    \"end_line\": 9195,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"foo  \\n     bar\\n\",\n    \"html\": \"<p>foo<br />\\nbar</p>\\n\",\n    \"example\": 633,\n    \"start_line\": 9200,\n    \"end_line\": 9206,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"foo\\\\\\n     bar\\n\",\n    \"html\": \"<p>foo<br />\\nbar</p>\\n\",\n    \"example\": 634,\n    \"start_line\": 9209,\n    \"end_line\": 9215,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"*foo  \\nbar*\\n\",\n    \"html\": \"<p><em>foo<br />\\nbar</em></p>\\n\",\n    \"example\": 635,\n    \"start_line\": 9221,\n    \"end_line\": 9227,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"*foo\\\\\\nbar*\\n\",\n    \"html\": \"<p><em>foo<br />\\nbar</em></p>\\n\",\n    \"example\": 636,\n    \"start_line\": 9230,\n    \"end_line\": 9236,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"`code \\nspan`\\n\",\n    \"html\": \"<p><code>code  span</code></p>\\n\",\n    \"example\": 637,\n    \"start_line\": 9241,\n    \"end_line\": 9246,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"`code\\\\\\nspan`\\n\",\n    \"html\": \"<p><code>code\\\\ span</code></p>\\n\",\n    \"example\": 638,\n    \"start_line\": 9249,\n    \"end_line\": 9254,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"<a href=\\\"foo  \\nbar\\\">\\n\",\n    \"html\": \"<p><a href=\\\"foo  \\nbar\\\"></p>\\n\",\n    \"example\": 639,\n    \"start_line\": 9259,\n    \"end_line\": 9265,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"<a href=\\\"foo\\\\\\nbar\\\">\\n\",\n    \"html\": \"<p><a href=\\\"foo\\\\\\nbar\\\"></p>\\n\",\n    \"example\": 640,\n    \"start_line\": 9268,\n    \"end_line\": 9274,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"foo\\\\\\n\",\n    \"html\": \"<p>foo\\\\</p>\\n\",\n    \"example\": 641,\n    \"start_line\": 9281,\n    \"end_line\": 9285,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"foo  \\n\",\n    \"html\": \"<p>foo</p>\\n\",\n    \"example\": 642,\n    \"start_line\": 9288,\n    \"end_line\": 9292,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"### foo\\\\\\n\",\n    \"html\": \"<h3>foo\\\\</h3>\\n\",\n    \"example\": 643,\n    \"start_line\": 9295,\n    \"end_line\": 9299,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"### foo  \\n\",\n    \"html\": \"<h3>foo</h3>\\n\",\n    \"example\": 644,\n    \"start_line\": 9302,\n    \"end_line\": 9306,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"foo\\nbaz\\n\",\n    \"html\": \"<p>foo\\nbaz</p>\\n\",\n    \"example\": 645,\n    \"start_line\": 9317,\n    \"end_line\": 9323,\n    \"section\": \"Soft line breaks\"\n  },\n  {\n    \"markdown\": \"foo \\n baz\\n\",\n    \"html\": \"<p>foo\\nbaz</p>\\n\",\n    \"example\": 646,\n    \"start_line\": 9329,\n    \"end_line\": 9335,\n    \"section\": \"Soft line breaks\"\n  },\n  {\n    \"markdown\": \"hello $.;'there\\n\",\n    \"html\": \"<p>hello $.;'there</p>\\n\",\n    \"example\": 647,\n    \"start_line\": 9349,\n    \"end_line\": 9353,\n    \"section\": \"Textual content\"\n  },\n  {\n    \"markdown\": \"Foo χρῆν\\n\",\n    \"html\": \"<p>Foo χρῆν</p>\\n\",\n    \"example\": 648,\n    \"start_line\": 9356,\n    \"end_line\": 9360,\n    \"section\": \"Textual content\"\n  },\n  {\n    \"markdown\": \"Multiple     spaces\\n\",\n    \"html\": \"<p>Multiple     spaces</p>\\n\",\n    \"example\": 649,\n    \"start_line\": 9365,\n    \"end_line\": 9369,\n    \"section\": \"Textual content\"\n  }\n]"
  },
  {
    "path": "libs/toastmark/src/commonmark/__test__/base-examples.spec.ts",
    "content": "import { Parser } from '../blocks';\nimport { Renderer } from '../../html/renderer';\nimport specs from './base-examples.json';\n\nconst reader = new Parser({ referenceDefinition: true });\nconst renderer = new Renderer();\n\nspecs.forEach((spec) => {\n  const { example, section, markdown, html } = spec;\n\n  it(`Example ${example} (${section})`, () => {\n    const parsed = reader.parse(markdown);\n    const result = renderer.render(parsed);\n\n    expect(result).toBe(html);\n  });\n});\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/__test__/helper.spec.ts",
    "content": "import { Node, BlockNode, createNode } from '../node';\n\nexport function pos(line1: number, col1: number, line2: number, col2: number) {\n  return [\n    [line1, col1],\n    [line2, col2],\n  ];\n}\n\nexport function convertToArrayTree(root: BlockNode, attrs: (keyof BlockNode)[]) {\n  function recur(node: Node) {\n    const newNode: any = {};\n    attrs.forEach((attr) => {\n      const attrVal = node[attr as keyof Node];\n      if (attrVal !== undefined && attrVal !== null) {\n        newNode[attr] = attrVal;\n      }\n    });\n\n    let child = node.firstChild;\n    if (child) {\n      newNode.children = [];\n    }\n    while (child) {\n      newNode.children.push(recur(child));\n      child = child.next;\n    }\n    return newNode;\n  }\n\n  return recur(root);\n}\n\nit('convert', () => {\n  const list = createNode('list', [\n    [1, 1],\n    [2, 10],\n  ]);\n  const listItem1 = createNode('item', [\n    [1, 1],\n    [1, 10],\n  ]);\n  const listItem2 = createNode('item', [\n    [2, 1],\n    [2, 10],\n  ]);\n  const para = createNode('paragraph', [\n    [2, 1],\n    [2, 10],\n  ]);\n  const emph = createNode('emph', [\n    [2, 1],\n    [2, 5],\n  ]);\n  const text = createNode('text', [\n    [2, 6],\n    [2, 10],\n  ]);\n\n  list.appendChild(listItem1);\n  list.appendChild(listItem2);\n  listItem2.appendChild(para);\n  para.appendChild(emph);\n  para.appendChild(text);\n\n  const attrs: (keyof Node)[] = ['type', 'sourcepos'];\n  expect(convertToArrayTree(list, attrs)).toEqual({\n    type: 'list',\n    sourcepos: [\n      [1, 1],\n      [2, 10],\n    ],\n    children: [\n      {\n        type: 'item',\n        sourcepos: [\n          [1, 1],\n          [1, 10],\n        ],\n      },\n      {\n        type: 'item',\n        sourcepos: [\n          [2, 1],\n          [2, 10],\n        ],\n        children: [\n          {\n            type: 'paragraph',\n            sourcepos: [\n              [2, 1],\n              [2, 10],\n            ],\n            children: [\n              {\n                type: 'emph',\n                sourcepos: [\n                  [2, 1],\n                  [2, 5],\n                ],\n              },\n              {\n                type: 'text',\n                sourcepos: [\n                  [2, 6],\n                  [2, 10],\n                ],\n              },\n            ],\n          },\n        ],\n      },\n    ],\n  });\n});\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/__test__/options.spec.ts",
    "content": "import { CustomParserMap } from '@t/parser';\nimport { Parser } from '../blocks';\nimport { pos } from './helper.spec';\nimport { Node } from '../node';\n\nit('tags in disallowedHtmlBlockTags should not be parsed as a HTML block', () => {\n  const reader = new Parser({ disallowedHtmlBlockTags: ['br', 'span'] });\n  const root = reader.parse('<BR />\\nHello\\n\\n<span class=\"toast\">\\nWorld');\n\n  expect(root).toMatchObject({\n    type: 'document',\n    firstChild: {\n      type: 'paragraph',\n      firstChild: {\n        type: 'htmlInline',\n        literal: '<BR />',\n        next: {\n          type: 'softbreak',\n          next: {\n            type: 'text',\n            literal: 'Hello',\n          },\n        },\n      },\n      next: {\n        type: 'paragraph',\n        firstChild: {\n          type: 'htmlInline',\n          literal: '<span class=\"toast\">',\n          next: {\n            type: 'softbreak',\n            next: {\n              type: 'text',\n              literal: 'World',\n            },\n          },\n        },\n      },\n    },\n  });\n});\n\ndescribe('disallowDeepHeading: true', () => {\n  it('the nested seTextHeading is disallowed in list', () => {\n    const reader = new Parser({ disallowDeepHeading: true });\n    const root = reader.parse('- item1\\n\\t-');\n\n    expect(root).toMatchObject({\n      type: 'document',\n      firstChild: {\n        type: 'list',\n        firstChild: {\n          type: 'item',\n          listData: {\n            bulletChar: '-',\n            markerOffset: 0,\n            padding: 2,\n            start: 0,\n            type: 'bullet',\n          },\n          firstChild: {\n            type: 'paragraph',\n            sourcepos: pos(1, 3, 2, 2),\n            firstChild: {\n              type: 'text',\n              literal: 'item1',\n              next: {\n                type: 'softbreak',\n                next: {\n                  type: 'text',\n                  literal: '-',\n                  sourcepos: pos(2, 2, 2, 2),\n                },\n              },\n            },\n          },\n        },\n      },\n    });\n  });\n\n  it('the nested atxHeading is disallowed in list', () => {\n    const reader = new Parser({ disallowDeepHeading: true });\n    const root = reader.parse('- # item1');\n\n    expect(root).toMatchObject({\n      type: 'document',\n      firstChild: {\n        type: 'list',\n        firstChild: {\n          type: 'item',\n          listData: {\n            bulletChar: '-',\n            markerOffset: 0,\n            padding: 2,\n            start: 0,\n            type: 'bullet',\n          },\n          firstChild: {\n            type: 'paragraph',\n            sourcepos: pos(1, 3, 1, 9),\n            firstChild: {\n              type: 'text',\n              literal: '# item1',\n              sourcepos: pos(1, 3, 1, 9),\n            },\n          },\n        },\n      },\n    });\n  });\n\n  it('the nested seTextHeading is disallowed in blockquote', () => {\n    const reader = new Parser({ disallowDeepHeading: true });\n    const root = reader.parse('> item1\\n> -');\n\n    expect(root).toMatchObject({\n      type: 'document',\n      firstChild: {\n        type: 'blockQuote',\n        sourcepos: pos(1, 1, 2, 3),\n        firstChild: {\n          type: 'paragraph',\n          sourcepos: pos(1, 3, 2, 3),\n          firstChild: {\n            type: 'text',\n            literal: 'item1',\n            sourcepos: pos(1, 3, 1, 7),\n            next: {\n              type: 'softbreak',\n              next: {\n                type: 'text',\n                literal: '-',\n                sourcepos: pos(2, 3, 2, 3),\n              },\n            },\n          },\n        },\n      },\n    });\n  });\n\n  it('the nested atxHeading is disallowed in blockquote', () => {\n    const reader = new Parser({ disallowDeepHeading: true });\n    const root = reader.parse('> # item1');\n\n    expect(root).toMatchObject({\n      type: 'document',\n      firstChild: {\n        type: 'blockQuote',\n        sourcepos: pos(1, 1, 1, 9),\n        firstChild: {\n          type: 'paragraph',\n          sourcepos: pos(1, 3, 1, 9),\n          firstChild: {\n            type: 'text',\n            literal: '# item1',\n            sourcepos: pos(1, 3, 1, 9),\n          },\n        },\n      },\n    });\n  });\n});\n\nit('should apply the custom parser', () => {\n  let inEmph = false;\n  const customParser: CustomParserMap = {\n    emph(node: Node, { entering }) {\n      inEmph = entering;\n\n      while (node.firstChild) {\n        node.insertBefore(node.firstChild);\n      }\n      node.unlink();\n    },\n    text(node: Node) {\n      if (inEmph) {\n        node.literal = node.literal!.toUpperCase();\n      }\n    },\n  };\n  const reader = new Parser({ customParser });\n  const root = reader.parse('*test*');\n\n  expect(root).toMatchObject({\n    type: 'document',\n    firstChild: {\n      type: 'paragraph',\n      sourcepos: pos(1, 1, 1, 6),\n      firstChild: {\n        type: 'text',\n        sourcepos: pos(1, 2, 1, 5),\n        literal: 'TEST',\n      },\n    },\n  });\n});\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/__test__/sourcepos.spec.ts",
    "content": "import { Parser } from '../blocks';\nimport { Node, CodeNode } from '../node';\n\nlet reader = new Parser();\n\ndescribe('paragraph', () => {\n  it('simple text', () => {\n    const root = reader.parse('Hello World');\n    const text = root.firstChild!.firstChild!;\n\n    expect(text.sourcepos).toEqual([\n      [1, 1],\n      [1, 11],\n    ]);\n  });\n\n  it('simple delimiter text', () => {\n    const root = reader.parse('<hi');\n    const text = root.firstChild!.firstChild!;\n\n    expect(text.sourcepos).toEqual([\n      [1, 1],\n      [1, 3],\n    ]);\n  });\n\n  it('multiple offset', () => {\n    const root = reader.parse('  Hello  \\n  World');\n    const text1 = root.firstChild!.firstChild!;\n    const linebreak = text1.next!;\n    const text2 = linebreak.next!;\n\n    expect(text1.sourcepos).toEqual([\n      [1, 3],\n      [1, 7],\n    ]);\n    expect(linebreak.sourcepos).toEqual([\n      [1, 8],\n      [1, 10],\n    ]);\n    // preceeding whitespaces are not included in text node\n    expect(text2.sourcepos).toEqual([\n      [2, 3],\n      [2, 7],\n    ]);\n  });\n\n  it('text and emphasis', () => {\n    const root = reader.parse('Hello *World*');\n    const text = root.firstChild!.firstChild!;\n    const emph = text.next as Node;\n    const emphText = emph.firstChild!;\n\n    expect(text.sourcepos).toEqual([\n      [1, 1],\n      [1, 6],\n    ]);\n    expect(emph.sourcepos).toEqual([\n      [1, 7],\n      [1, 13],\n    ]);\n    expect(emphText.sourcepos).toEqual([\n      [1, 8],\n      [1, 12],\n    ]);\n  });\n\n  it('text and strong emphasis', () => {\n    const root = reader.parse('Hello **World**');\n    const text = root.firstChild!.firstChild!;\n    const strong = text.next!;\n    const strongText = strong.firstChild!;\n\n    expect(text.sourcepos).toEqual([\n      [1, 1],\n      [1, 6],\n    ]);\n    expect(strong.sourcepos).toEqual([\n      [1, 7],\n      [1, 15],\n    ]);\n    expect(strongText.sourcepos).toEqual([\n      [1, 9],\n      [1, 13],\n    ]);\n  });\n\n  it('text and image', () => {\n    const root = reader.parse('Hello ![World](http://nhn.com)');\n    const text = root.firstChild!.firstChild!;\n    const image = text.next!;\n    const imageText = image.firstChild!;\n\n    expect(text.sourcepos).toEqual([\n      [1, 1],\n      [1, 6],\n    ]);\n    expect(image.sourcepos).toEqual([\n      [1, 7],\n      [1, 30],\n    ]);\n    expect(imageText.sourcepos).toEqual([\n      [1, 9],\n      [1, 13],\n    ]);\n  });\n\n  it('text with ampersand code ', () => {\n    const root = reader.parse('&#91; Hello &#93;');\n    const text = root.firstChild!.firstChild!;\n\n    expect(text.sourcepos).toEqual([\n      [1, 1],\n      [1, 17],\n    ]);\n  });\n\n  it('text and link', () => {\n    const root = reader.parse('Hello [World](http://nhn.com)');\n    const text = root.firstChild!.firstChild!;\n    const link = text.next!;\n    const linkText = link.firstChild!;\n\n    expect(text.sourcepos).toEqual([\n      [1, 1],\n      [1, 6],\n    ]);\n    expect(link.sourcepos).toEqual([\n      [1, 7],\n      [1, 29],\n    ]);\n    expect(linkText.sourcepos).toEqual([\n      [1, 8],\n      [1, 12],\n    ]);\n  });\n\n  it('text and codespan', () => {\n    const root = reader.parse('Hello ``World``');\n    const text = root.firstChild!.firstChild!;\n    const code = text.next as CodeNode;\n\n    expect(text.sourcepos).toEqual([\n      [1, 1],\n      [1, 6],\n    ]);\n    expect(code.tickCount).toBe(2);\n    expect(code.sourcepos).toEqual([\n      [1, 7],\n      [1, 15],\n    ]);\n  });\n\n  it('text and raw html', () => {\n    const root = reader.parse('Hello <strong>World</strong>');\n    const text1 = root.firstChild!.firstChild!;\n    const html1 = text1.next!;\n    const text2 = html1.next!;\n    const html2 = text2.next!;\n\n    expect(text1.sourcepos).toEqual([\n      [1, 1],\n      [1, 6],\n    ]);\n    expect(html1.sourcepos).toEqual([\n      [1, 7],\n      [1, 14],\n    ]);\n    expect(text2.sourcepos).toEqual([\n      [1, 15],\n      [1, 19],\n    ]);\n    expect(html2.sourcepos).toEqual([\n      [1, 20],\n      [1, 28],\n    ]);\n  });\n\n  it('autolink', () => {\n    const root = reader.parse('Hello <http://nhn.com>');\n    const link = root.firstChild!.firstChild!.next!;\n    const linkText = link.firstChild!;\n\n    expect(link.sourcepos).toEqual([\n      [1, 7],\n      [1, 22],\n    ]);\n    expect(linkText.sourcepos).toEqual([\n      [1, 8],\n      [1, 21],\n    ]);\n  });\n\n  it('autolink (mailto)', () => {\n    const root = reader.parse('Hello <world@nhn.com>');\n    const link = root.firstChild!.firstChild!.next!;\n    const linkText = link.firstChild!;\n\n    expect(link.sourcepos).toEqual([\n      [1, 7],\n      [1, 21],\n    ]);\n    expect(linkText.sourcepos).toEqual([\n      [1, 8],\n      [1, 20],\n    ]);\n  });\n});\n\ndescribe('softbreak and linebreak', () => {\n  it('text with softbreak', () => {\n    const root = reader.parse('Hello\\nWorld');\n    const text1 = root.firstChild!.firstChild!;\n    const softbreak = text1.next!;\n    const text2 = softbreak.next!;\n\n    expect(text1.sourcepos).toEqual([\n      [1, 1],\n      [1, 5],\n    ]);\n    expect(softbreak.type).toBe('softbreak');\n    expect(softbreak.sourcepos).toEqual([\n      [1, 6],\n      [1, 6],\n    ]);\n    expect(text2.sourcepos).toEqual([\n      [2, 1],\n      [2, 5],\n    ]);\n  });\n\n  it('text with linebreak(space)', () => {\n    const root = reader.parse('Hello   \\nWorld');\n    const text1 = root.firstChild!.firstChild!;\n    const linebreak = text1.next!;\n    const text2 = linebreak.next!;\n\n    // trailing spaces are not included in text node\n    expect(text1.sourcepos).toEqual([\n      [1, 1],\n      [1, 5],\n    ]);\n    // preceeding spaces are included in linebreak node\n    expect(linebreak.sourcepos).toEqual([\n      [1, 6],\n      [1, 9],\n    ]);\n    expect(text2.sourcepos).toEqual([\n      [2, 1],\n      [2, 5],\n    ]);\n  });\n\n  it('text with linebreak(backslash)', () => {\n    const root = reader.parse('Hello\\\\\\nWorld');\n    const text1 = root.firstChild!.firstChild!;\n    const linebreak = text1.next!;\n    const text2 = linebreak.next!;\n\n    expect(text1.sourcepos).toEqual([\n      [1, 1],\n      [1, 5],\n    ]);\n    // preceeding backslash is included in linebreak node\n    expect(linebreak.sourcepos).toEqual([\n      [1, 6],\n      [1, 7],\n    ]);\n    expect(text2.sourcepos).toEqual([\n      [2, 1],\n      [2, 5],\n    ]);\n  });\n});\n\ndescribe('atx header', () => {\n  it('text and emphasis', () => {\n    const root = reader.parse('# Hello *World*');\n    const text = root.firstChild!.firstChild!;\n    const emph = text.next!;\n    const emphText = emph.firstChild!;\n\n    expect(text.sourcepos).toEqual([\n      [1, 3],\n      [1, 8],\n    ]);\n    expect(emph.sourcepos).toEqual([\n      [1, 9],\n      [1, 15],\n    ]);\n    expect(emphText.sourcepos).toEqual([\n      [1, 10],\n      [1, 14],\n    ]);\n  });\n\n  it('text and emphasis (header level 3)', () => {\n    const root = reader.parse('### Hello *World*');\n    const text = root.firstChild!.firstChild!;\n    const emph = text.next!;\n    const emphText = emph.firstChild!;\n\n    expect(text.sourcepos).toEqual([\n      [1, 5],\n      [1, 10],\n    ]);\n    expect(emph.sourcepos).toEqual([\n      [1, 11],\n      [1, 17],\n    ]);\n    expect(emphText.sourcepos).toEqual([\n      [1, 12],\n      [1, 16],\n    ]);\n  });\n});\n\ndescribe('list items', () => {\n  it('with columns', () => {\n    const root = reader.parse('   - Hello\\n\\n     World');\n    const listItem = root.firstChild!.firstChild!;\n    const para1 = listItem.firstChild!;\n    const para1Text = para1.firstChild!;\n    const para2 = para1.next!;\n    const para2Text = para2.firstChild!;\n\n    expect(para1Text.sourcepos).toEqual([\n      [1, 6],\n      [1, 10],\n    ]);\n    expect(para2Text.sourcepos).toEqual([\n      [3, 6],\n      [3, 10],\n    ]);\n  });\n});\n\ndescribe('block quote', () => {\n  it('nested paragraph', () => {\n    const root = reader.parse('> Hello\\n> > World');\n    const quote1 = root.firstChild!;\n    const para1 = quote1.firstChild!;\n    const quote2 = para1.next!;\n    const para2 = quote2.firstChild!;\n\n    expect(para1.firstChild!.sourcepos).toEqual([\n      [1, 3],\n      [1, 7],\n    ]);\n    expect(para2.firstChild!.sourcepos).toEqual([\n      [2, 5],\n      [2, 9],\n    ]);\n  });\n});\n\ndescribe('code block', () => {\n  it('empty line', () => {\n    const root = reader.parse('```\\n\\n```\\nHello');\n    const codeblock = root.firstChild!;\n    const para = codeblock.next!;\n\n    expect(codeblock.sourcepos).toEqual([\n      [1, 1],\n      [3, 3],\n    ]);\n    expect(para.sourcepos).toEqual([\n      [4, 1],\n      [4, 5],\n    ]);\n  });\n});\n\ndescribe('inlline code', () => {\n  it('multi line', () => {\n    const root = reader.parse('`a\\n  b\\n   c`\\n d');\n    const para = root.firstChild!;\n    const code = para.firstChild!;\n    const linebreak = code.next!;\n    const text = linebreak.next!;\n\n    expect(code.sourcepos).toEqual([\n      [1, 1],\n      [3, 5],\n    ]);\n    expect(linebreak.sourcepos).toEqual([\n      [3, 6],\n      [3, 6],\n    ]);\n    expect(text.sourcepos).toEqual([\n      [4, 2],\n      [4, 2],\n    ]);\n  });\n});\n\ndescribe('merge text nodes', () => {\n  it('tokens', () => {\n    const root = reader.parse(['\\\\ Text *', '[ Text !', '![ Text ]'].join('\\n'));\n    const text1 = root.firstChild!.firstChild!;\n    const text2 = text1.next!.next!;\n    const text3 = text2.next!.next!;\n\n    expect(text1.literal).toBe('\\\\ Text *');\n    expect(text1.sourcepos).toEqual([\n      [1, 1],\n      [1, 8],\n    ]);\n    expect(text2.literal).toBe('[ Text !');\n    expect(text2.sourcepos).toEqual([\n      [2, 1],\n      [2, 8],\n    ]);\n\n    expect(text3.literal).toBe('![ Text ]');\n    expect(text3.sourcepos).toEqual([\n      [3, 1],\n      [3, 9],\n    ]);\n  });\n});\n\ndescribe('reference link definition', () => {\n  reader = new Parser({ referenceDefinition: true });\n\n  afterAll(() => {\n    reader = new Parser();\n  });\n\n  it('single line without title', () => {\n    const root = reader.parse('[foo]: test');\n    const refDef = root.firstChild!;\n\n    expect(refDef.sourcepos).toEqual([\n      [1, 1],\n      [1, 11],\n    ]);\n  });\n\n  it('single line with title', () => {\n    const root = reader.parse('[foo]: test \"title\"');\n    const refDef = root.firstChild!;\n\n    expect(refDef.sourcepos).toEqual([\n      [1, 1],\n      [1, 19],\n    ]);\n  });\n\n  it('multi line without title', () => {\n    const root = reader.parse('[foo]:\\n  test');\n    const refDef = root.firstChild!;\n\n    expect(refDef.sourcepos).toEqual([\n      [1, 1],\n      [2, 4],\n    ]);\n  });\n\n  it('multi line with title', () => {\n    const root = reader.parse('[foo]:\\n  test \"title\"');\n    const refDef = root.firstChild!;\n\n    expect(refDef.sourcepos).toEqual([\n      [1, 1],\n      [2, 12],\n    ]);\n  });\n\n  it('multi line title which has multi line', () => {\n    const root = reader.parse('[foo]:\\n  test \"\\n  tit  \\n  l  \\n  e\"');\n    const refDef = root.firstChild!;\n\n    expect(refDef.sourcepos).toEqual([\n      [1, 1],\n      [5, 2],\n    ]);\n  });\n});\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/__test__/syntax-info.spec.ts",
    "content": "import { Parser } from '../blocks';\nimport { HeadingNode, CodeBlockNode } from '../node';\n\nconst parser = new Parser();\n\ndescribe('headingType ', () => {\n  it('atx heading', () => {\n    const root = parser.parse('# Heading');\n    const heading = root.firstChild as HeadingNode;\n\n    expect(heading.headingType).toBe('atx');\n  });\n\n  it('setext heading', () => {\n    const root = parser.parse('Heading\\n----');\n    const heading = root.firstChild as HeadingNode;\n\n    expect(heading.headingType).toBe('setext');\n  });\n});\n\ndescribe('CodeBlockNode', () => {\n  it('infoPadding is none', () => {\n    const root = parser.parse('```js');\n    const codeBlock = root.firstChild as CodeBlockNode;\n\n    expect(codeBlock.infoPadding).toBe(0);\n  });\n\n  it('infoPadding is more than zero', () => {\n    const root = parser.parse('```   js');\n    const codeBlock = root.firstChild as CodeBlockNode;\n\n    expect(codeBlock.infoPadding).toBe(3);\n  });\n\n  it('info string', () => {\n    const root = parser.parse('```   javascript  ');\n    const codeBlock = root.firstChild as CodeBlockNode;\n\n    expect(codeBlock.info).toBe('javascript');\n  });\n});\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/blockHandlers.ts",
    "content": "import { Parser } from './blocks';\nimport { taskListItemFinalize } from './gfm/taskListItem';\nimport {\n  table,\n  tableHead,\n  tableBody,\n  tableRow,\n  tableCell,\n  tableDelimRow,\n  tableDelimCell,\n} from './gfm/tableBlockHandler';\nimport { customBlock } from './custom/customBlockHandler';\nimport { ListNode, BlockNode, CodeBlockNode, HtmlBlockNode } from './node';\nimport {\n  peek,\n  isBlank,\n  isSpaceOrTab,\n  endsWithBlankLine,\n  reClosingCodeFence,\n  CODE_INDENT,\n  C_OPEN_BRACKET,\n  C_GREATERTHAN,\n} from './blockHelper';\nimport { unescapeString } from './common';\n\nexport const enum Process {\n  Go = 0,\n  Stop = 1,\n  Finished = 2,\n}\n\n// 'finalize' is run when the block is closed.\n// 'continue' is run to check whether the block is continuing\n// at a certain line and offset (e.g. whether a block quote\n// contains a `>`.  It returns 0 for matched, 1 for not matched,\n// and 2 for \"we've dealt with this line completely, go to next.\"\nexport interface BlockHandler {\n  continue(parser: Parser, container: BlockNode): Process;\n  finalize(parser: Parser, block: BlockNode): void;\n  canContain(type: string): boolean;\n  acceptsLines: boolean;\n}\n\nconst noop: BlockHandler = {\n  continue() {\n    return Process.Stop;\n  },\n  finalize() {},\n  canContain() {\n    return false;\n  },\n  acceptsLines: true,\n};\n\nconst document: BlockHandler = {\n  continue() {\n    return Process.Go;\n  },\n  finalize() {},\n  canContain(t) {\n    return t !== 'item';\n  },\n  acceptsLines: false,\n};\n\nconst list: BlockHandler = {\n  continue() {\n    return Process.Go;\n  },\n  finalize(_, block: ListNode) {\n    let item = block.firstChild as BlockNode;\n    while (item) {\n      // check for non-final list item ending with blank line:\n      if (endsWithBlankLine(item) && item.next) {\n        block.listData!.tight = false;\n        break;\n      }\n      // recurse into children of list item, to see if there are\n      // spaces between any of them:\n      let subitem = item.firstChild as BlockNode;\n      while (subitem) {\n        if (endsWithBlankLine(subitem) && (item.next || subitem.next)) {\n          block.listData!.tight = false;\n          break;\n        }\n        subitem = subitem.next as BlockNode;\n      }\n      item = item.next as BlockNode;\n    }\n  },\n  canContain(t) {\n    return t === 'item';\n  },\n  acceptsLines: false,\n};\n\nconst blockQuote: BlockHandler = {\n  continue(parser) {\n    const ln = parser.currentLine;\n    if (!parser.indented && peek(ln, parser.nextNonspace) === C_GREATERTHAN) {\n      parser.advanceNextNonspace();\n      parser.advanceOffset(1, false);\n      if (isSpaceOrTab(peek(ln, parser.offset))) {\n        parser.advanceOffset(1, true);\n      }\n    } else {\n      return Process.Stop;\n    }\n    return Process.Go;\n  },\n  finalize() {},\n  canContain(t) {\n    return t !== 'item';\n  },\n  acceptsLines: false,\n};\n\nconst item: BlockHandler = {\n  continue(parser, container: ListNode) {\n    if (parser.blank) {\n      if (container.firstChild === null) {\n        // Blank line after empty list item\n        return Process.Stop;\n      }\n      parser.advanceNextNonspace();\n    } else if (parser.indent >= container.listData!.markerOffset + container.listData!.padding) {\n      parser.advanceOffset(container.listData!.markerOffset + container.listData!.padding, true);\n    } else {\n      return Process.Stop;\n    }\n    return Process.Go;\n  },\n  finalize: taskListItemFinalize,\n  canContain(t) {\n    return t !== 'item';\n  },\n  acceptsLines: false,\n};\n\nconst heading: BlockHandler = {\n  continue() {\n    // a heading can never container > 1 line, so fail to match:\n    return Process.Stop;\n  },\n  finalize() {},\n  canContain() {\n    return false;\n  },\n  acceptsLines: false,\n};\n\nconst thematicBreak: BlockHandler = {\n  continue() {\n    // a thematic break can never container > 1 line, so fail to match:\n    return Process.Stop;\n  },\n  finalize() {},\n  canContain() {\n    return false;\n  },\n  acceptsLines: false,\n};\n\nconst codeBlock: BlockHandler = {\n  continue(parser, container: CodeBlockNode) {\n    const ln = parser.currentLine;\n    const indent = parser.indent;\n    if (container.isFenced) {\n      // fenced\n      const match =\n        indent <= 3 &&\n        ln.charAt(parser.nextNonspace) === container.fenceChar &&\n        ln.slice(parser.nextNonspace).match(reClosingCodeFence);\n      if (match && match[0].length >= container.fenceLength) {\n        // closing fence - we're at end of line, so we can return\n        parser.lastLineLength = parser.offset + indent + match[0].length;\n        parser.finalize(container as BlockNode, parser.lineNumber);\n        return Process.Finished;\n      }\n      // skip optional spaces of fence offset\n      let i = container.fenceOffset;\n      while (i > 0 && isSpaceOrTab(peek(ln, parser.offset))) {\n        parser.advanceOffset(1, true);\n        i--;\n      }\n    } else {\n      // indented\n      if (indent >= CODE_INDENT) {\n        parser.advanceOffset(CODE_INDENT, true);\n      } else if (parser.blank) {\n        parser.advanceNextNonspace();\n      } else {\n        return Process.Stop;\n      }\n    }\n    return Process.Go;\n  },\n  finalize(_, block: CodeBlockNode) {\n    if (block.stringContent === null) {\n      return;\n    }\n    if (block.isFenced) {\n      // fenced\n      // first line becomes info string\n      const content = block.stringContent;\n      const newlinePos = content.indexOf('\\n');\n      const firstLine = content.slice(0, newlinePos);\n      const rest = content.slice(newlinePos + 1);\n      const infoString = firstLine.match(/^(\\s*)(.*)/);\n      block.infoPadding = infoString![1].length;\n      block.info = unescapeString(infoString![2].trim());\n      block.literal = rest;\n    } else {\n      // indented\n      block.literal = block.stringContent?.replace(/(\\n *)+$/, '\\n');\n    }\n    block.stringContent = null; // allow GC\n  },\n  canContain() {\n    return false;\n  },\n  acceptsLines: true,\n};\n\nconst htmlBlock: BlockHandler = {\n  continue(parser, container: HtmlBlockNode) {\n    return parser.blank && (container.htmlBlockType === 6 || container.htmlBlockType === 7)\n      ? Process.Stop\n      : Process.Go;\n  },\n  finalize(_, block) {\n    block.literal = block.stringContent?.replace(/(\\n *)+$/, '') || null;\n    block.stringContent = null; // allow GC\n  },\n  canContain() {\n    return false;\n  },\n  acceptsLines: true,\n};\n\nconst paragraph: BlockHandler = {\n  continue(parser) {\n    return parser.blank ? Process.Stop : Process.Go;\n  },\n  finalize(parser, block) {\n    if (block.stringContent === null) {\n      return;\n    }\n\n    let pos: number;\n    let hasReferenceDefs = false;\n\n    // try parsing the beginning as link reference definitions:\n    while (\n      peek(block.stringContent, 0) === C_OPEN_BRACKET &&\n      (pos = parser.inlineParser.parseReference(block, parser.refMap))\n    ) {\n      block.stringContent = block.stringContent.slice(pos);\n      hasReferenceDefs = true;\n    }\n    if (hasReferenceDefs && isBlank(block.stringContent)) {\n      block.unlink();\n    }\n  },\n  canContain() {\n    return false;\n  },\n  acceptsLines: true,\n};\n\nconst refDef = noop;\nconst frontMatter = noop;\n\nexport const blockHandlers = {\n  document,\n  list,\n  blockQuote,\n  item,\n  heading,\n  thematicBreak,\n  codeBlock,\n  htmlBlock,\n  paragraph,\n  table,\n  tableBody,\n  tableHead,\n  tableRow,\n  tableCell,\n  tableDelimRow,\n  tableDelimCell,\n  refDef,\n  customBlock,\n  frontMatter,\n};\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/blockHelper.ts",
    "content": "import { BlockNode } from './node';\n\nexport const CODE_INDENT = 4;\nexport const C_TAB = 9;\nexport const C_NEWLINE = 10;\nexport const C_GREATERTHAN = 62;\nexport const C_LESSTHAN = 60;\nexport const C_SPACE = 32;\nexport const C_OPEN_BRACKET = 91;\n\nexport const reNonSpace = /[^ \\t\\f\\v\\r\\n]/;\n\nexport const reClosingCodeFence = /^(?:`{3,}|~{3,})(?= *$)/;\n\n// Returns true if block ends with a blank line, descending if needed\n// into lists and sublists.\nexport function endsWithBlankLine(block: BlockNode) {\n  let curBlock: BlockNode | null = block;\n\n  while (curBlock) {\n    if (curBlock.lastLineBlank) {\n      return true;\n    }\n    const t = curBlock.type;\n    if (!curBlock.lastLineChecked && (t === 'list' || t === 'item')) {\n      curBlock.lastLineChecked = true;\n      curBlock = curBlock.lastChild as BlockNode;\n    } else {\n      curBlock.lastLineChecked = true;\n      break;\n    }\n  }\n  return false;\n}\n\nexport function peek(ln: string, pos: number) {\n  if (pos < ln.length) {\n    return ln.charCodeAt(pos);\n  }\n  return -1;\n}\n\n// Returns true if string contains only space characters.\nexport function isBlank(s: string) {\n  return !reNonSpace.test(s);\n}\n\nexport function isSpaceOrTab(c: number) {\n  return c === C_SPACE || c === C_TAB;\n}\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/blockStarts.ts",
    "content": "import { ListData } from '@t/node';\nimport { ListNode, HtmlBlockNode, HeadingNode, CodeBlockNode, createNode, BlockNode } from './node';\nimport { OPENTAG, CLOSETAG } from './rawHtml';\nimport {\n  peek,\n  isSpaceOrTab,\n  reNonSpace,\n  CODE_INDENT,\n  C_OPEN_BRACKET,\n  C_GREATERTHAN,\n  C_LESSTHAN,\n  C_TAB,\n  C_SPACE,\n} from './blockHelper';\nimport { Parser } from './blocks';\nimport { tableHead, tableBody } from './gfm/tableBlockStart';\nimport { customBlock } from './custom/customBlockStart';\n\nexport const enum Matched {\n  None = 0, // No Match\n  Container, // Keep Going\n  Leaf, // No more block starts\n}\nexport interface BlockStart {\n  (parser: Parser, container: BlockNode): Matched;\n}\n\nconst reCodeFence = /^`{3,}(?!.*`)|^~{3,}/;\nconst reHtmlBlockOpen = [\n  /./, // dummy for 0\n  /^<(?:script|pre|style)(?:\\s|>|$)/i,\n  /^<!--/,\n  /^<[?]/,\n  /^<![A-Z]/,\n  /^<!\\[CDATA\\[/,\n  /^<[/]?(?:address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[123456]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?:\\s|[/]?[>]|$)/i,\n  new RegExp(`^(?:${OPENTAG}|${CLOSETAG})\\\\s*$`, 'i'),\n];\nconst reSetextHeadingLine = /^(?:=+|-+)[ \\t]*$/;\nconst reATXHeadingMarker = /^#{1,6}(?:[ \\t]+|$)/;\nconst reThematicBreak = /^(?:(?:\\*[ \\t]*){3,}|(?:_[ \\t]*){3,}|(?:-[ \\t]*){3,})[ \\t]*$/;\nexport const reBulletListMarker = /^[*+-]/;\nexport const reOrderedListMarker = /^(\\d{1,9})([.)])/;\n\n// Parse a list marker and return data on the marker (type,\n// start, delimiter, bullet character, padding) or null.\nfunction parseListMarker(parser: Parser, container: ListNode): ListData | null {\n  const rest = parser.currentLine.slice(parser.nextNonspace);\n  let match;\n  let nextc;\n  const data: ListData = {\n    type: 'bullet',\n    tight: true, // lists are tight by default\n    bulletChar: '',\n    start: 0,\n    delimiter: '',\n    padding: 0,\n    markerOffset: parser.indent,\n    // GFM: Task List Item\n    task: false,\n    checked: false,\n  };\n\n  if (parser.indent >= 4) {\n    return null;\n  }\n  if ((match = rest.match(reBulletListMarker))) {\n    data.type = 'bullet';\n    data.bulletChar = match[0][0];\n  } else if (\n    (match = rest.match(reOrderedListMarker)) &&\n    (container.type !== 'paragraph' || match[1] === '1')\n  ) {\n    data.type = 'ordered';\n    data.start = parseInt(match[1], 10);\n    data.delimiter = match[2];\n  } else {\n    return null;\n  }\n  // make sure we have spaces after\n  nextc = peek(parser.currentLine, parser.nextNonspace + match[0].length);\n  if (!(nextc === -1 || nextc === C_TAB || nextc === C_SPACE)) {\n    return null;\n  }\n\n  // if it interrupts paragraph, make sure first line isn't blank\n  if (\n    container.type === 'paragraph' &&\n    !parser.currentLine.slice(parser.nextNonspace + match[0].length).match(reNonSpace)\n  ) {\n    return null;\n  }\n\n  // we've got a match! advance offset and calculate padding\n  parser.advanceNextNonspace(); // to start of marker\n  parser.advanceOffset(match[0].length, true); // to end of marker\n  const spacesStartCol = parser.column;\n  const spacesStartOffset = parser.offset;\n  do {\n    parser.advanceOffset(1, true);\n    nextc = peek(parser.currentLine, parser.offset);\n  } while (parser.column - spacesStartCol < 5 && isSpaceOrTab(nextc));\n  const blankItem = peek(parser.currentLine, parser.offset) === -1;\n  const spacesAfterMarker = parser.column - spacesStartCol;\n  if (spacesAfterMarker >= 5 || spacesAfterMarker < 1 || blankItem) {\n    data.padding = match[0].length + 1;\n    parser.column = spacesStartCol;\n    parser.offset = spacesStartOffset;\n    if (isSpaceOrTab(peek(parser.currentLine, parser.offset))) {\n      parser.advanceOffset(1, true);\n    }\n  } else {\n    data.padding = match[0].length + spacesAfterMarker;\n  }\n\n  return data;\n}\n\n// Returns true if the two list items are of the same type,\n// with the same delimiter and bullet character.  This is used\n// in agglomerating list items into lists.\nfunction listsMatch(listData: ListData, itemData: ListData) {\n  return (\n    listData.type === itemData.type &&\n    listData.delimiter === itemData.delimiter &&\n    listData.bulletChar === itemData.bulletChar\n  );\n}\n\nfunction isDisallowedDeepHeading(parser: Parser, node: BlockNode) {\n  return parser.options.disallowDeepHeading && (node.type === 'blockQuote' || node.type === 'item');\n}\n\nconst blockQuote: BlockStart = (parser) => {\n  if (!parser.indented && peek(parser.currentLine, parser.nextNonspace) === C_GREATERTHAN) {\n    parser.advanceNextNonspace();\n    parser.advanceOffset(1, false);\n    // optional following space\n    if (isSpaceOrTab(peek(parser.currentLine, parser.offset))) {\n      parser.advanceOffset(1, true);\n    }\n    parser.closeUnmatchedBlocks();\n    parser.addChild('blockQuote', parser.nextNonspace);\n    return Matched.Container;\n  }\n  return Matched.None;\n};\n\nconst atxHeading: BlockStart = (parser, container) => {\n  let match;\n  if (\n    !parser.indented &&\n    // The nested Heading is disallowed in list and blockquote with 'disallowDeepHeading' option\n    !isDisallowedDeepHeading(parser, container) &&\n    (match = parser.currentLine.slice(parser.nextNonspace).match(reATXHeadingMarker))\n  ) {\n    parser.advanceNextNonspace();\n    parser.advanceOffset(match[0].length, false);\n    parser.closeUnmatchedBlocks();\n\n    const heading = parser.addChild('heading', parser.nextNonspace) as HeadingNode;\n    heading.level = match[0].trim().length; // number of #s\n    heading.headingType = 'atx';\n    // remove trailing ###s:\n    heading.stringContent = parser.currentLine\n      .slice(parser.offset)\n      .replace(/^[ \\t]*#+[ \\t]*$/, '')\n      .replace(/[ \\t]+#+[ \\t]*$/, '');\n    parser.advanceOffset(parser.currentLine.length - parser.offset);\n    return Matched.Leaf;\n  }\n  return Matched.None;\n};\n\nconst fencedCodeBlock: BlockStart = (parser) => {\n  let match;\n  if (\n    !parser.indented &&\n    (match = parser.currentLine.slice(parser.nextNonspace).match(reCodeFence))\n  ) {\n    const fenceLength = match[0].length;\n    parser.closeUnmatchedBlocks();\n    const container = parser.addChild('codeBlock', parser.nextNonspace) as CodeBlockNode;\n    container.isFenced = true;\n    container.fenceLength = fenceLength;\n    container.fenceChar = match[0][0];\n    container.fenceOffset = parser.indent;\n    parser.advanceNextNonspace();\n    parser.advanceOffset(fenceLength, false);\n    return Matched.Leaf;\n  }\n  return Matched.None;\n};\n\nconst htmlBlock: BlockStart = (parser, container) => {\n  if (!parser.indented && peek(parser.currentLine, parser.nextNonspace) === C_LESSTHAN) {\n    const s = parser.currentLine.slice(parser.nextNonspace);\n    const disallowedTags = parser.options.disallowedHtmlBlockTags;\n    let blockType;\n\n    for (blockType = 1; blockType <= 7; blockType++) {\n      const matched = s.match(reHtmlBlockOpen[blockType]);\n      if (matched) {\n        if (blockType === 7) {\n          if (container.type === 'paragraph') {\n            return Matched.None;\n          }\n          if (disallowedTags.length > 0) {\n            const reDisallowedTags = new RegExp(`<\\/?(?:${disallowedTags.join('|')})`, 'i');\n            if (reDisallowedTags.test(matched[0])) {\n              return Matched.None;\n            }\n          }\n        }\n\n        parser.closeUnmatchedBlocks();\n        // We don't adjust parser.offset;\n        // spaces are part of the HTML block:\n        const b = parser.addChild('htmlBlock', parser.offset) as HtmlBlockNode;\n        b.htmlBlockType = blockType;\n        return Matched.Leaf;\n      }\n    }\n  }\n  return Matched.None;\n};\n\nconst seTextHeading: BlockStart = (parser, container) => {\n  let match;\n  if (\n    container.stringContent !== null &&\n    !parser.indented &&\n    container.type === 'paragraph' &&\n    // The nested Heading is disallowed in list and blockquote with 'disallowDeepHeading' option\n    !isDisallowedDeepHeading(parser, container.parent as BlockNode) &&\n    (match = parser.currentLine.slice(parser.nextNonspace).match(reSetextHeadingLine))\n  ) {\n    parser.closeUnmatchedBlocks();\n    // resolve reference link definitions\n    let pos;\n    while (\n      peek(container.stringContent, 0) === C_OPEN_BRACKET &&\n      (pos = parser.inlineParser.parseReference(container, parser.refMap))\n    ) {\n      container.stringContent = container.stringContent.slice(pos);\n    }\n    if (container.stringContent.length > 0) {\n      const heading = createNode('heading', container.sourcepos);\n      heading.level = match[0][0] === '=' ? 1 : 2;\n      heading.headingType = 'setext';\n      heading.stringContent = container.stringContent;\n      container.insertAfter(heading);\n      container.unlink();\n      parser.tip = heading;\n      parser.advanceOffset(parser.currentLine.length - parser.offset, false);\n      return Matched.Leaf;\n    }\n    return Matched.None;\n  }\n  return Matched.None;\n};\n\nconst thematicBreak: BlockStart = (parser) => {\n  if (!parser.indented && reThematicBreak.test(parser.currentLine.slice(parser.nextNonspace))) {\n    parser.closeUnmatchedBlocks();\n    parser.addChild('thematicBreak', parser.nextNonspace);\n    parser.advanceOffset(parser.currentLine.length - parser.offset, false);\n    return Matched.Leaf;\n  }\n  return Matched.None;\n};\n\nconst listItem: BlockStart = (parser, container) => {\n  let data;\n  let currNode = container as ListNode;\n\n  if (\n    (!parser.indented || container.type === 'list') &&\n    (data = parseListMarker(parser, currNode))\n  ) {\n    parser.closeUnmatchedBlocks();\n\n    // add the list if needed\n    if (parser.tip.type !== 'list' || !listsMatch(currNode.listData!, data)) {\n      currNode = parser.addChild('list', parser.nextNonspace) as ListNode;\n      currNode.listData = data;\n    }\n\n    // add the list item\n    currNode = parser.addChild('item', parser.nextNonspace) as ListNode;\n    currNode.listData = data;\n\n    return Matched.Container;\n  }\n  return Matched.None;\n};\n\n// indented code block\nconst indentedCodeBlock: BlockStart = (parser) => {\n  if (parser.indented && parser.tip.type !== 'paragraph' && !parser.blank) {\n    // indented code\n    parser.advanceOffset(CODE_INDENT, true);\n    parser.closeUnmatchedBlocks();\n    parser.addChild('codeBlock', parser.offset);\n    return Matched.Leaf;\n  }\n  return Matched.None;\n};\n\nexport const blockStarts = [\n  blockQuote,\n  atxHeading,\n  fencedCodeBlock,\n  htmlBlock,\n  seTextHeading,\n  thematicBreak,\n  listItem,\n  indentedCodeBlock,\n  tableHead,\n  tableBody,\n  customBlock,\n];\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/blocks.ts",
    "content": "import {\n  BlockParser,\n  ParserOptions,\n  RefDefCandidateMap,\n  RefLinkCandidateMap,\n  RefMap,\n} from '@t/parser';\nimport { BlockNodeType } from '@t/node';\nimport { repeat } from './common';\nimport { Node, BlockNode, isCodeBlock, isHtmlBlock, createNode, TableCellNode } from './node';\nimport { InlineParser, C_NEWLINE } from './inlines';\nimport { blockHandlers, Process } from './blockHandlers';\nimport { CODE_INDENT } from './blockHelper';\nimport { blockStarts, Matched } from './blockStarts';\nimport { clearObj } from '../helper';\nimport { frontMatter as frontMatterHandler } from './frontMatter/frontMatterHandler';\nimport { frontMatter as frontMatterStart } from './frontMatter/frontMatterStart';\n\nconst reHtmlBlockClose = [\n  /./, // dummy for 0\n  /<\\/(?:script|pre|style)>/i,\n  /-->/,\n  /\\?>/,\n  />/,\n  /\\]\\]>/,\n];\n\nconst reMaybeSpecial = /^[#`~*+_=<>0-9-;$]/;\nconst reLineEnding = /\\r\\n|\\n|\\r/;\n\nfunction document() {\n  return createNode('document', [\n    [1, 1],\n    [0, 0],\n  ]);\n}\n\nconst defaultOptions = {\n  smart: false,\n  tagFilter: false,\n  extendedAutolinks: false,\n  disallowedHtmlBlockTags: [],\n  referenceDefinition: false,\n  disallowDeepHeading: false,\n  customParser: null,\n  frontMatter: false,\n};\n\nexport class Parser implements BlockParser {\n  public doc: BlockNode;\n  public tip: BlockNode;\n  public oldtip: BlockNode;\n  public currentLine: string;\n  public lineNumber: number;\n  public offset: number;\n  public column: number;\n  public nextNonspace: number;\n  public nextNonspaceColumn: number;\n  public indent: number;\n  public indented: boolean;\n  public blank: boolean;\n  private partiallyConsumedTab: boolean;\n  private allClosed: boolean;\n  private lastMatchedContainer: Node;\n  public refMap: RefMap;\n  public refLinkCandidateMap: RefLinkCandidateMap;\n  public refDefCandidateMap: RefDefCandidateMap;\n  public lastLineLength: number;\n  public inlineParser: InlineParser;\n  public options: ParserOptions;\n  public lines: string[];\n\n  constructor(options?: Partial<ParserOptions>) {\n    this.options = { ...defaultOptions, ...options };\n    this.doc = document();\n    this.tip = this.doc;\n    this.oldtip = this.doc;\n    this.lineNumber = 0;\n    this.offset = 0;\n    this.column = 0;\n    this.nextNonspace = 0;\n    this.nextNonspaceColumn = 0;\n    this.indent = 0;\n    this.currentLine = '';\n    this.indented = false;\n    this.blank = false;\n    this.partiallyConsumedTab = false;\n    this.allClosed = true;\n    this.lastMatchedContainer = this.doc;\n    this.refMap = {};\n    this.refLinkCandidateMap = {};\n    this.refDefCandidateMap = {};\n    this.lastLineLength = 0;\n    this.lines = [];\n\n    if (this.options.frontMatter) {\n      blockHandlers.frontMatter = frontMatterHandler;\n      blockStarts.unshift(frontMatterStart);\n    }\n\n    this.inlineParser = new InlineParser(this.options);\n  }\n\n  advanceOffset(count: number, columns = false) {\n    const currentLine = this.currentLine;\n    let charsToTab: number, charsToAdvance: number;\n    let c: string;\n    while (count > 0 && (c = currentLine[this.offset])) {\n      if (c === '\\t') {\n        charsToTab = 4 - (this.column % 4);\n        if (columns) {\n          this.partiallyConsumedTab = charsToTab > count;\n          charsToAdvance = charsToTab > count ? count : charsToTab;\n          this.column += charsToAdvance;\n          this.offset += this.partiallyConsumedTab ? 0 : 1;\n          count -= charsToAdvance;\n        } else {\n          this.partiallyConsumedTab = false;\n          this.column += charsToTab;\n          this.offset += 1;\n          count -= 1;\n        }\n      } else {\n        this.partiallyConsumedTab = false;\n        this.offset += 1;\n        this.column += 1; // assume ascii; block starts are ascii\n        count -= 1;\n      }\n    }\n  }\n\n  advanceNextNonspace() {\n    this.offset = this.nextNonspace;\n    this.column = this.nextNonspaceColumn;\n    this.partiallyConsumedTab = false;\n  }\n\n  findNextNonspace() {\n    const currentLine = this.currentLine;\n    let i = this.offset;\n    let cols = this.column;\n    let c: string;\n\n    while ((c = currentLine.charAt(i)) !== '') {\n      if (c === ' ') {\n        i++;\n        cols++;\n      } else if (c === '\\t') {\n        i++;\n        cols += 4 - (cols % 4);\n      } else {\n        break;\n      }\n    }\n    this.blank = c === '\\n' || c === '\\r' || c === '';\n    this.nextNonspace = i;\n    this.nextNonspaceColumn = cols;\n    this.indent = this.nextNonspaceColumn - this.column;\n    this.indented = this.indent >= CODE_INDENT;\n  }\n\n  // Add a line to the block at the tip.  We assume the tip\n  // can accept lines -- that check should be done before calling this.\n  addLine() {\n    if (this.partiallyConsumedTab) {\n      this.offset += 1; // skip over tab\n      // add space characters:\n      const charsToTab = 4 - (this.column % 4);\n      this.tip.stringContent += repeat(' ', charsToTab);\n    }\n    if (this.tip.lineOffsets) {\n      this.tip.lineOffsets.push(this.offset);\n    } else {\n      this.tip.lineOffsets = [this.offset];\n    }\n    this.tip.stringContent += `${this.currentLine.slice(this.offset)}\\n`;\n  }\n\n  // Add block of type tag as a child of the tip.  If the tip can't\n  // accept children, close and finalize it and try its parent,\n  // and so on til we find a block that can accept children.\n  addChild(tag: BlockNodeType, offset: number) {\n    while (!blockHandlers[this.tip.type].canContain(tag)) {\n      this.finalize(this.tip, this.lineNumber - 1);\n    }\n\n    const columnNumber = offset + 1; // offset 0 = column 1\n    const newBlock = createNode(tag, [\n      [this.lineNumber, columnNumber],\n      [0, 0],\n    ]);\n    newBlock.stringContent = '';\n    this.tip.appendChild(newBlock);\n    this.tip = newBlock;\n    return newBlock;\n  }\n\n  // Finalize and close any unmatched blocks.\n  closeUnmatchedBlocks() {\n    if (!this.allClosed) {\n      // finalize any blocks not matched\n      while (this.oldtip !== this.lastMatchedContainer) {\n        const parent = this.oldtip.parent as BlockNode;\n        this.finalize(this.oldtip, this.lineNumber - 1);\n        this.oldtip = parent!;\n      }\n      this.allClosed = true;\n    }\n  }\n\n  // Finalize a block.  Close it and do any necessary postprocessing,\n  // e.g. creating stringContent from strings, setting the 'tight'\n  // or 'loose' status of a list, and parsing the beginnings\n  // of paragraphs for reference definitions.  Reset the tip to the\n  // parent of the closed block.\n  finalize(block: BlockNode, lineNumber: number) {\n    const above = block.parent as BlockNode;\n    block.open = false;\n    block.sourcepos![1] = [lineNumber, this.lastLineLength];\n    blockHandlers[block.type].finalize(this, block);\n\n    this.tip = above;\n  }\n\n  // Walk through a block & children recursively, parsing string content\n  // into inline content where appropriate.\n  processInlines(block: BlockNode) {\n    let event;\n    const { customParser } = this.options;\n    const walker = block.walker();\n    this.inlineParser.refMap = this.refMap;\n    this.inlineParser.refLinkCandidateMap = this.refLinkCandidateMap;\n    this.inlineParser.refDefCandidateMap = this.refDefCandidateMap;\n    this.inlineParser.options = this.options;\n\n    while ((event = walker.next())) {\n      const { node, entering } = event;\n      const t = node.type;\n\n      if (customParser && customParser[t]) {\n        customParser[t]!(node, { entering, options: this.options });\n      }\n\n      if (\n        !entering &&\n        (t === 'paragraph' ||\n          t === 'heading' ||\n          (t === 'tableCell' && !(node as TableCellNode).ignored))\n      ) {\n        this.inlineParser.parse(node as BlockNode);\n      }\n    }\n  }\n\n  // Analyze a line of text and update the document appropriately.\n  // We parse markdown text by calling this on each line of input,\n  // then finalizing the document.\n  incorporateLine(ln: string) {\n    let container = this.doc;\n    this.oldtip = this.tip;\n    this.offset = 0;\n    this.column = 0;\n    this.blank = false;\n    this.partiallyConsumedTab = false;\n    this.lineNumber += 1;\n\n    // replace NUL characters for security\n    if (ln.indexOf('\\u0000') !== -1) {\n      ln = ln.replace(/\\0/g, '\\uFFFD');\n    }\n\n    this.currentLine = ln;\n\n    // For each containing block, try to parse the associated line start.\n    // Bail out on failure: container will point to the last matching block.\n    // Set allMatched to false if not all containers match.\n    let allMatched = true;\n    let lastChild: BlockNode;\n    while ((lastChild = container.lastChild as BlockNode) && lastChild.open) {\n      container = lastChild;\n\n      this.findNextNonspace();\n\n      switch (blockHandlers[container.type]['continue'](this, container)) {\n        case Process.Go: // we've matched, keep going\n          break;\n        case Process.Stop: // we've failed to match a block\n          allMatched = false;\n          break;\n        case Process.Finished: // we've hit end of line for fenced code close and can return\n          this.lastLineLength = ln.length;\n          return;\n        default:\n          throw new Error('continue returned illegal value, must be 0, 1, or 2');\n      }\n      if (!allMatched) {\n        container = container.parent as BlockNode; // back up to last matching block\n        break;\n      }\n    }\n\n    this.allClosed = container === this.oldtip;\n    this.lastMatchedContainer = container;\n\n    let matchedLeaf = container.type !== 'paragraph' && blockHandlers[container.type].acceptsLines;\n    const blockStartsLen = blockStarts.length;\n    // Unless last matched container is a code block, try new container starts,\n    // adding children to the last matched container:\n    while (!matchedLeaf) {\n      this.findNextNonspace();\n\n      // this is a little performance optimization:\n      if (\n        container.type !== 'table' &&\n        container.type !== 'tableBody' &&\n        container.type !== 'paragraph' &&\n        !this.indented &&\n        !reMaybeSpecial.test(ln.slice(this.nextNonspace))\n      ) {\n        this.advanceNextNonspace();\n        break;\n      }\n\n      let i = 0;\n      while (i < blockStartsLen) {\n        const res = blockStarts[i](this, container);\n        if (res === Matched.Container) {\n          container = this.tip;\n          break;\n        } else if (res === Matched.Leaf) {\n          container = this.tip;\n          matchedLeaf = true;\n          break;\n        } else {\n          i++;\n        }\n      }\n\n      if (i === blockStartsLen) {\n        // nothing matched\n        this.advanceNextNonspace();\n        break;\n      }\n    }\n\n    // What remains at the offset is a text line.  Add the text to the\n    // appropriate container.\n\n    // First check for a lazy paragraph continuation:\n    if (!this.allClosed && !this.blank && this.tip.type === 'paragraph') {\n      // lazy paragraph continuation\n      this.addLine();\n    } else {\n      // not a lazy continuation\n\n      // finalize any blocks not matched\n      this.closeUnmatchedBlocks();\n      if (this.blank && container.lastChild) {\n        (container.lastChild as BlockNode).lastLineBlank = true;\n      }\n\n      const t = container.type;\n      // Block quote lines are never blank as they start with >\n      // and we don't count blanks in fenced code for purposes of tight/loose\n      // lists or breaking out of lists. We also don't set _lastLineBlank\n      // on an empty list item, or if we just closed a fenced block.\n      const lastLineBlank =\n        this.blank &&\n        !(\n          t === 'blockQuote' ||\n          (isCodeBlock(container) && container.isFenced) ||\n          (t === 'item' && !container.firstChild && container.sourcepos![0][0] === this.lineNumber)\n        );\n\n      // propagate lastLineBlank up through parents:\n      let cont: BlockNode | null = container;\n      while (cont) {\n        cont.lastLineBlank = lastLineBlank;\n        cont = cont.parent as BlockNode;\n      }\n\n      if (blockHandlers[t].acceptsLines) {\n        this.addLine();\n        // if HtmlBlock, check for end condition\n        if (\n          isHtmlBlock(container) &&\n          container.htmlBlockType >= 1 &&\n          container.htmlBlockType <= 5 &&\n          reHtmlBlockClose[container.htmlBlockType].test(this.currentLine.slice(this.offset))\n        ) {\n          this.lastLineLength = ln.length;\n          this.finalize(container, this.lineNumber);\n        }\n      } else if (this.offset < ln.length && !this.blank) {\n        // create paragraph container for line\n        container = this.addChild('paragraph', this.offset);\n        this.advanceNextNonspace();\n        this.addLine();\n      }\n    }\n    this.lastLineLength = ln.length;\n  }\n\n  // The main parsing function.  Returns a parsed document AST.\n  parse(input: string, lineTexts?: string[]) {\n    this.doc = document();\n    this.tip = this.doc;\n    this.lineNumber = 0;\n    this.lastLineLength = 0;\n    this.offset = 0;\n    this.column = 0;\n    this.lastMatchedContainer = this.doc;\n    this.currentLine = '';\n    const lines = input.split(reLineEnding);\n    let len = lines.length;\n    this.lines = lineTexts ? lineTexts : lines;\n    if (this.options.referenceDefinition) {\n      this.clearRefMaps();\n    }\n    if (input.charCodeAt(input.length - 1) === C_NEWLINE) {\n      // ignore last blank line created by final newline\n      len -= 1;\n    }\n    for (let i = 0; i < len; i++) {\n      this.incorporateLine(lines[i]);\n    }\n    while (this.tip) {\n      this.finalize(this.tip, len);\n    }\n    this.processInlines(this.doc);\n\n    return this.doc;\n  }\n\n  partialParseStart(lineNumber: number, lines: string[]) {\n    this.doc = document();\n    this.tip = this.doc;\n    this.lineNumber = lineNumber - 1;\n    this.lastLineLength = 0;\n    this.offset = 0;\n    this.column = 0;\n    this.lastMatchedContainer = this.doc;\n    this.currentLine = '';\n    const len = lines.length;\n\n    for (let i = 0; i < len; i++) {\n      this.incorporateLine(lines[i]);\n    }\n\n    return this.doc;\n  }\n\n  partialParseExtends(lines: string[]) {\n    for (let i = 0; i < lines.length; i++) {\n      this.incorporateLine(lines[i]);\n    }\n  }\n\n  partialParseFinish() {\n    while (this.tip) {\n      this.finalize(this.tip, this.lineNumber);\n    }\n    this.processInlines(this.doc);\n  }\n\n  setRefMaps(\n    refMap: RefMap,\n    refLinkCandidateMap: RefLinkCandidateMap,\n    refDefCandidateMap: RefDefCandidateMap\n  ) {\n    this.refMap = refMap;\n    this.refLinkCandidateMap = refLinkCandidateMap;\n    this.refDefCandidateMap = refDefCandidateMap;\n  }\n\n  clearRefMaps() {\n    [this.refMap, this.refLinkCandidateMap, this.refDefCandidateMap].forEach((map) => {\n      clearObj(map);\n    });\n  }\n}\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/common.ts",
    "content": "import encode from 'mdurl/encode';\nimport { decodeHTML } from 'entities';\n\nexport const ENTITY = '&(?:#x[a-f0-9]{1,6}|#[0-9]{1,7}|[a-z][a-z0-9]{1,31});';\nconst C_BACKSLASH = 92;\nconst reBackslashOrAmp = /[\\\\&]/;\nexport const ESCAPABLE = '[!\"#$%&\\'()*+,./:;<=>?@[\\\\\\\\\\\\]^_`{|}~-]';\nconst reEntityOrEscapedChar = new RegExp(`\\\\\\\\${ESCAPABLE}|${ENTITY}`, 'gi');\nconst XMLSPECIAL = '[&<>\"]';\nconst reXmlSpecial = new RegExp(XMLSPECIAL, 'g');\n\nconst unescapeChar = function (s: string) {\n  if (s.charCodeAt(0) === C_BACKSLASH) {\n    return s.charAt(1);\n  }\n  return decodeHTML(s);\n};\n\n// Replace entities and backslash escapes with literal characters.\nexport function unescapeString(s: string) {\n  if (reBackslashOrAmp.test(s)) {\n    return s.replace(reEntityOrEscapedChar, unescapeChar);\n  }\n  return s;\n}\n\nexport function normalizeURI(uri: string) {\n  try {\n    return encode(uri);\n  } catch (err) {\n    return uri;\n  }\n}\n\nfunction replaceUnsafeChar(s: string) {\n  switch (s) {\n    case '&':\n      return '&amp;';\n    case '<':\n      return '&lt;';\n    case '>':\n      return '&gt;';\n    case '\"':\n      return '&quot;';\n    default:\n      return s;\n  }\n}\n\nexport function escapeXml(s: string) {\n  if (reXmlSpecial.test(s)) {\n    return s.replace(reXmlSpecial, replaceUnsafeChar);\n  }\n  return s;\n}\n\nexport function repeat(str: string, count: number): string {\n  const arr = [];\n  for (let i = 0; i < count; i++) {\n    arr.push(str);\n  }\n  return arr.join('');\n}\n\nexport function last<T>(arr: T[]) {\n  if (!arr.length) {\n    return null;\n  }\n  return arr[arr.length - 1];\n}\n\nexport function isEmpty(str: string) {\n  if (!str) {\n    return true;\n  }\n  return !/[^ \\t]+/.test(str);\n}\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/custom/__test__/customBlock.spec.ts",
    "content": "import { HTMLConvertorMap } from '@t/renderer';\nimport { Parser } from '../../blocks';\nimport { Renderer } from '../../../html/renderer';\nimport { source } from 'common-tags';\n\nconst convertors: HTMLConvertorMap = {\n  myCustom(node) {\n    return [\n      { type: 'openTag', tagName: 'div', outerNewLine: true, classNames: ['myCustom-block'] },\n      { type: 'html', content: node.literal! },\n      { type: 'closeTag', tagName: 'div', outerNewLine: true },\n    ];\n  },\n};\nconst reader = new Parser();\nconst renderer = new Renderer({ gfm: true, convertors });\n\ndescribe('customBlock', () => {\n  it('basic', () => {\n    const input = source`\n      $$myCustom\n      my custom block\n\n      should be parsed\n      $$\n    `;\n    const output = source`\n      <div class=\"myCustom-block\">my custom block\n\n      should be parsed\n      </div>\n    `;\n\n    const root = reader.parse(input);\n    const html = renderer.render(root);\n    expect(html).toBe(`${output}\\n`);\n  });\n\n  it('if cannot find the proper custom type renderer, the content would be rendered as text', () => {\n    const input = source`\n      $$custom\n      custom block\n      $$\n    `;\n    const output = source`\n      <div>custom block\n      </div>\n    `;\n\n    const root = reader.parse(input);\n    const html = renderer.render(root);\n    expect(html).toBe(`${output}\\n`);\n  });\n\n  it('should be rendered regardless of the case insensitive', () => {\n    const input = source`\n      $$MYCuSTOM\n      my custom block\n\n      should be parsed\n      $$\n    `;\n    const output = source`\n      <div class=\"myCustom-block\">my custom block\n\n      should be parsed\n      </div>\n    `;\n\n    const root = reader.parse(input);\n    const html = renderer.render(root);\n    expect(html).toBe(`${output}\\n`);\n  });\n\n  it('should be parsed as paragraph without meta information', () => {\n    const input = source`\n      $$\n        custom block\n      $$\n    `;\n    const output = source`\n      <p>$$\n      custom block\n      $$</p>\n    `;\n\n    const root = reader.parse(input);\n    const html = renderer.render(root);\n    expect(html).toBe(`${output}\\n`);\n  });\n\n  it('should be rendered regardless of the white space', () => {\n    const input = source`\n      $$  myCustom\n      my custom block\n\n      should be parsed\n      $$\n    `;\n    const output = source`\n      <div class=\"myCustom-block\">my custom block\n\n      should be parsed\n      </div>\n    `;\n\n    const root = reader.parse(input);\n    const html = renderer.render(root);\n    expect(html).toBe(`${output}\\n`);\n  });\n});\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/custom/__test__/customInline.spec.ts",
    "content": "import { Parser } from '../../blocks';\nimport { Renderer } from '../../../html/renderer';\nimport { CustomInlineNode } from '../../node';\n\nconst reader = new Parser();\nconst renderer = new Renderer();\n\ndescribe('customInline', () => {\n  it('basic example', () => {\n    const root = reader.parse('Hello $$myInline World$$');\n    const para = root.firstChild!;\n    const text = para.firstChild!;\n    const customInline = text.next as CustomInlineNode;\n    const inlineText = customInline.firstChild!;\n\n    expect(text.literal).toBe('Hello ');\n    expect(inlineText.literal).toBe('World');\n    expect(customInline.info).toBe('myInline');\n    expect(customInline.sourcepos).toEqual([\n      [1, 7],\n      [1, 24],\n    ]);\n    expect(inlineText.sourcepos).toEqual([\n      [1, 17],\n      [1, 22],\n    ]);\n\n    const html = renderer.render(root);\n\n    expect(html).toBe('<p>Hello <span>$$myInline World$$</span></p>\\n');\n  });\n\n  it('nested markdown text example', () => {\n    const root = reader.parse('Hello $$myInline *World*$$');\n    const para = root.firstChild!;\n    const text = para.firstChild!;\n    const customInline = text.next as CustomInlineNode;\n    const emph = customInline.lastChild!;\n\n    expect(text.literal).toBe('Hello ');\n    expect(customInline.info).toBe('myInline');\n    expect(customInline.sourcepos).toEqual([\n      [1, 7],\n      [1, 26],\n    ]);\n    expect(emph.sourcepos).toEqual([\n      [1, 18],\n      [1, 24],\n    ]);\n\n    const html = renderer.render(root);\n\n    expect(html).toBe('<p>Hello <span>$$myInline <em>World</em>$$</span></p>\\n');\n  });\n\n  it('should be parsed as text without meta information', () => {\n    const root = reader.parse('Hello $$ world$$');\n    const para = root.firstChild!;\n    const text = para.firstChild!;\n\n    expect(text.literal).toBe('Hello $$ world$$');\n    expect(text.sourcepos).toEqual([\n      [1, 1],\n      [1, 16],\n    ]);\n\n    const html = renderer.render(root);\n\n    expect(html).toBe('<p>Hello $$ world$$</p>\\n');\n  });\n\n  it('should be render properly with meta information only', () => {\n    const root = reader.parse('Hello $$myInline$$');\n    const para = root.firstChild!;\n    const text = para.firstChild!;\n    const customInline = text.next as CustomInlineNode;\n\n    expect(text.literal).toBe('Hello ');\n    expect(customInline.info).toBe('myInline');\n    expect(customInline.sourcepos).toEqual([\n      [1, 7],\n      [1, 18],\n    ]);\n\n    const html = renderer.render(root);\n\n    expect(html).toBe('<p>Hello <span>$$myInline$$</span></p>\\n');\n  });\n});\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/custom/customBlockHandler.ts",
    "content": "import { Process, BlockHandler } from '../blockHandlers';\nimport { isSpaceOrTab, peek } from '../blockHelper';\nimport { unescapeString } from '../common';\nimport { CustomBlockNode, BlockNode } from '../node';\n\nconst reClosingCustomBlock = /^\\$\\$$/;\n\nexport const customBlock: BlockHandler = {\n  continue(parser, container: CustomBlockNode) {\n    const line = parser.currentLine;\n    const match = line.match(reClosingCustomBlock);\n    if (match) {\n      // closing custom block\n      parser.lastLineLength = match[0].length;\n      parser.finalize(container as BlockNode, parser.lineNumber);\n      return Process.Finished;\n    }\n    // skip optional spaces of custom block offset\n    let i = container.offset;\n    while (i > 0 && isSpaceOrTab(peek(line, parser.offset))) {\n      parser.advanceOffset(1, true);\n      i--;\n    }\n    return Process.Go;\n  },\n  finalize(_, block: CustomBlockNode) {\n    if (block.stringContent === null) {\n      return;\n    }\n    // first line becomes info string\n    const content = block.stringContent;\n    const newlinePos = content.indexOf('\\n');\n    const firstLine = content.slice(0, newlinePos);\n    const rest = content.slice(newlinePos + 1);\n    const infoString = firstLine.match(/^(\\s*)(.*)/);\n\n    block.info = unescapeString(infoString![2].trim());\n    block.literal = rest;\n    block.stringContent = null;\n  },\n  canContain() {\n    return false;\n  },\n  acceptsLines: true,\n};\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/custom/customBlockStart.ts",
    "content": "import { BlockStart, Matched } from '../blockStarts';\nimport { CustomBlockNode } from '../node';\n\nconst reCustomBlock = /^(\\$\\$)(\\s*[a-zA-Z])+/;\nconst reCanBeCustomInline = /^(\\$\\$)(\\s*[a-zA-Z])+.*(\\$\\$)/;\n\nexport const customBlock: BlockStart = (parser) => {\n  let match;\n  if (\n    !parser.indented &&\n    !reCanBeCustomInline.test(parser.currentLine) &&\n    (match = parser.currentLine.match(reCustomBlock))\n  ) {\n    const syntaxLength = match[1].length;\n    parser.closeUnmatchedBlocks();\n\n    const container = parser.addChild('customBlock', parser.nextNonspace) as CustomBlockNode;\n    container.syntaxLength = syntaxLength;\n    container.offset = parser.indent;\n\n    parser.advanceNextNonspace();\n    parser.advanceOffset(syntaxLength, false);\n    return Matched.Leaf;\n  }\n  return Matched.None;\n};\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/from-code-point.ts",
    "content": "// derived from https://github.com/mathiasbynens/String.fromCodePoint\n/*! http://mths.be/fromcodepoint v0.2.1 by @mathias */\nlet fromCodePoint: (c: number) => string;\n\nif (String.fromCodePoint) {\n  fromCodePoint = function (_) {\n    try {\n      return String.fromCodePoint(_);\n    } catch (e) {\n      if (e instanceof RangeError) {\n        return String.fromCharCode(0xfffd);\n      }\n      throw e;\n    }\n  };\n} else {\n  const stringFromCharCode = String.fromCharCode;\n  const floor = Math.floor;\n  fromCodePoint = function (...args) {\n    const MAX_SIZE = 0x4000;\n    const codeUnits = [];\n    let highSurrogate: number;\n    let lowSurrogate: number;\n    let index = -1;\n    const length = args.length;\n    if (!length) {\n      return '';\n    }\n    let result = '';\n    while (++index < length) {\n      let codePoint = Number(args[index]);\n      if (\n        !isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity`\n        codePoint < 0 || // not a valid Unicode code point\n        codePoint > 0x10ffff || // not a valid Unicode code point\n        floor(codePoint) !== codePoint // not an integer\n      ) {\n        return String.fromCharCode(0xfffd);\n      }\n      if (codePoint <= 0xffff) {\n        // BMP code point\n        codeUnits.push(codePoint);\n      } else {\n        // Astral code point; split in surrogate halves\n        // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae\n        codePoint -= 0x10000;\n        highSurrogate = (codePoint >> 10) + 0xd800;\n        lowSurrogate = (codePoint % 0x400) + 0xdc00;\n        codeUnits.push(highSurrogate, lowSurrogate);\n      }\n      if (index + 1 === length || codeUnits.length > MAX_SIZE) {\n        result += stringFromCharCode(...codeUnits);\n        codeUnits.length = 0;\n      }\n    }\n    return result;\n  };\n}\n\nexport default fromCodePoint;\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/frontMatter/__test__/frontMatter.spec.ts",
    "content": "import { source, stripIndent } from 'common-tags';\nimport { Parser } from '../../blocks';\nimport { Renderer } from '../../../html/renderer';\n\nconst reader = new Parser({ frontMatter: true });\nconst renderer = new Renderer();\n\ndescribe('front matter', () => {\n  it('should be parsed with YAML(`---`)', () => {\n    const frontMatterText = stripIndent`\n      ---\n      title: front matter\n      ---\n    `;\n    const root = reader.parse(frontMatterText);\n\n    expect(root).toMatchObject({\n      type: 'document',\n      firstChild: {\n        type: 'frontMatter',\n        literal: frontMatterText,\n        sourcepos: [\n          [1, 1],\n          [3, 3],\n        ],\n      },\n    });\n  });\n\n  it('should be parsed with TOML(`+++`)', () => {\n    const frontMatterText = stripIndent`\n      +++\n      title: front matter\n      +++\n    `;\n    const root = reader.parse(frontMatterText);\n\n    expect(root).toMatchObject({\n      type: 'document',\n      firstChild: {\n        type: 'frontMatter',\n        literal: frontMatterText,\n        sourcepos: [\n          [1, 1],\n          [3, 3],\n        ],\n      },\n    });\n  });\n\n  it('should be parsed with JSON(`;;;`)', () => {\n    const frontMatterText = stripIndent`\n      ;;;\n      title: front matter\n      ;;;\n    `;\n    const root = reader.parse(frontMatterText);\n\n    expect(root).toMatchObject({\n      type: 'document',\n      firstChild: {\n        type: 'frontMatter',\n        literal: frontMatterText,\n        sourcepos: [\n          [1, 1],\n          [3, 3],\n        ],\n      },\n    });\n  });\n\n  it('should be parsed with the empty line', () => {\n    const markdownText = stripIndent`\n      ---\n\n      title: front matter\n\n      description: with empty line\n\n      ---\n    `;\n    const root = reader.parse(markdownText);\n\n    expect(root).toMatchObject({\n      type: 'document',\n      firstChild: {\n        type: 'frontMatter',\n        literal: markdownText,\n        sourcepos: [\n          [1, 1],\n          [7, 3],\n        ],\n      },\n    });\n  });\n\n  it('should be parsed with following paragraph', () => {\n    const root = reader.parse('---\\ntitle: front matter\\n---\\npara');\n\n    expect(root).toMatchObject({\n      type: 'document',\n      firstChild: {\n        type: 'frontMatter',\n        literal: '---\\ntitle: front matter\\n---',\n        sourcepos: [\n          [1, 1],\n          [3, 3],\n        ],\n      },\n      lastChild: {\n        type: 'paragraph',\n        literal: null,\n        sourcepos: [\n          [4, 1],\n          [4, 4],\n        ],\n        firstChild: {\n          literal: 'para',\n          sourcepos: [\n            [4, 1],\n            [4, 4],\n          ],\n        },\n      },\n    });\n  });\n\n  it('should be parsed only once from the top.', () => {\n    const frontMatterText = stripIndent`\n      ---\n\n      title: front matter\n\n      description: with empty line\n\n      ---\n    `;\n    const markdownText = `${frontMatterText}\\n---`;\n    const root = reader.parse(markdownText);\n\n    expect(root).toMatchObject({\n      type: 'document',\n      firstChild: {\n        type: 'frontMatter',\n        literal: frontMatterText,\n        sourcepos: [\n          [1, 1],\n          [7, 3],\n        ],\n        next: {\n          type: 'thematicBreak',\n          literal: null,\n          sourcepos: [\n            [8, 1],\n            [8, 3],\n          ],\n        },\n      },\n    });\n  });\n});\n\ndescribe('Exmaple', () => {\n  const examples = [\n    {\n      no: 1,\n      input: source`\n      ---\n      title: front matter\n      ---\n      `,\n      output: source`\n        <div style=\"white-space: pre; display: none;\">---\n        title: front matter\n        ---</div>\n      `,\n    },\n    {\n      no: 2,\n      input: source`\n      ---\n\n      title: front matter\n\n      description: with empty line\n\n      ---\n      `,\n      output: source`\n        <div style=\"white-space: pre; display: none;\">---\n\n        title: front matter\n\n        description: with empty line\n\n        ---</div>\n      `,\n    },\n    {\n      no: 3,\n      input: source`\n\n      ---\n      title: front matter\n      ---\n      `,\n      output: source`\n        <div style=\"white-space: pre; display: none;\">---\n        title: front matter\n        ---</div>\n      `,\n    },\n    {\n      no: 4,\n      input: source`\n      ---\n      title: front matter\n      ---\n      para\n      `,\n      output: source`\n        <div style=\"white-space: pre; display: none;\">---\n        title: front matter\n        ---</div>\n        <p>para</p>\n      `,\n    },\n    {\n      no: 5,\n      input: source`\n      para\n      ---\n      title: front matter\n      ---\n      `,\n      output: source`\n        <h2>para</h2>\n        <h2>title: front matter</h2>\n      `,\n    },\n  ];\n\n  examples.forEach(({ no, input, output }) => {\n    it(String(no), () => {\n      const root = reader.parse(input);\n      const html = renderer.render(root);\n      expect(html).toBe(`${output}\\n`);\n    });\n  });\n});\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/frontMatter/frontMatterHandler.ts",
    "content": "import { Process, BlockHandler } from '../blockHandlers';\nimport { BlockNode } from '../node';\nimport { reFrontMatter } from './frontMatterStart';\n\nexport const frontMatter: BlockHandler = {\n  continue(parser, container: BlockNode) {\n    const line = parser.currentLine;\n    const match = line.match(reFrontMatter);\n\n    if (container.type === 'frontMatter' && match) {\n      container.stringContent += line;\n      parser.lastLineLength = match[0].length;\n      parser.finalize(container as BlockNode, parser.lineNumber);\n      return Process.Finished;\n    }\n    return Process.Go;\n  },\n  finalize(_, block: BlockNode) {\n    if (block.stringContent === null) {\n      return;\n    }\n    block.literal = block.stringContent;\n    block.stringContent = null;\n  },\n  canContain() {\n    return false;\n  },\n  acceptsLines: true,\n};\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/frontMatter/frontMatterStart.ts",
    "content": "import { BlockStart, Matched } from '../blockStarts';\nimport { BlockNode } from '../node';\n\n// `---` for YAML, `+++` for TOML, `;;;` for JSON\nexport const reFrontMatter = /^(-{3}|\\+{3}|;{3})$/;\n\nexport const frontMatter: BlockStart = (parser, container) => {\n  const { currentLine, lineNumber, indented } = parser;\n\n  if (\n    lineNumber === 1 &&\n    !indented &&\n    container.type === 'document' &&\n    reFrontMatter.test(currentLine)\n  ) {\n    parser.closeUnmatchedBlocks();\n    const frontMatter = parser.addChild('frontMatter', parser.nextNonspace) as BlockNode;\n    frontMatter.stringContent = currentLine;\n\n    parser.advanceNextNonspace();\n    parser.advanceOffset(currentLine.length, false);\n\n    return Matched.Leaf;\n  }\n  return Matched.None;\n};\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/gfm/__test__/autolinks.spec.ts",
    "content": "import { Parser } from '../../blocks';\nimport { Renderer } from '../../../html/renderer';\nimport { LinkNode } from '../../node';\nimport { pos } from '../../__test__/helper.spec';\nimport { parseUrlLink, parseEmailLink } from '../autoLinks';\n\ndescribe('parseUrlLink()', () => {\n  // https://github.github.com/gfm/#extended-www-autolink\n  // https://github.github.com/gfm/#extended-url-autolink\n  it('domain not preceeded by www is invalid', () => {\n    expect(parseUrlLink('nhn.com')).toEqual([]);\n    expect(parseUrlLink('ui.toast.com')).toEqual([]);\n  });\n\n  it('domain preceeded by www with less than 2 periods(.) is invalid', () => {\n    expect(parseUrlLink('www.nhn')).toEqual([]);\n  });\n\n  it('domain preceeded by www is valid', () => {\n    expect(parseUrlLink('www.nhn.com')).toEqual([\n      {\n        text: 'www.nhn.com',\n        url: `http://www.nhn.com`,\n        range: [0, 10],\n      },\n    ]);\n\n    expect(parseUrlLink('Visit www.nhn.com Now!')).toEqual([\n      {\n        text: 'www.nhn.com',\n        url: `http://www.nhn.com`,\n        range: [6, 16],\n      },\n    ]);\n  });\n\n  it('domain preceeded by http(s):// is valid', () => {\n    expect(parseUrlLink('http://nhn.com')).toEqual([\n      {\n        text: 'http://nhn.com',\n        url: `http://nhn.com`,\n        range: [0, 13],\n      },\n    ]);\n\n    expect(parseUrlLink('https://nhn.com')).toEqual([\n      {\n        text: 'https://nhn.com',\n        url: `https://nhn.com`,\n        range: [0, 14],\n      },\n    ]);\n  });\n\n  it('zero or more non-space non-< characters may follow', () => {\n    expect(parseUrlLink('www.nhn.com/help<me')).toEqual([\n      {\n        text: 'www.nhn.com/help',\n        url: `http://www.nhn.com/help`,\n        range: [0, 15],\n      },\n    ]);\n  });\n\n  it('tailing punctuation (?!,.:*_~) is not considered part of the link', () => {\n    const pairs = [\n      ['www.nhn.com/?help?', 'www.nhn.com/?help'],\n      ['www.nhn.com/!help!', 'www.nhn.com/!help'],\n      ['www.nhn.com/,help,', 'www.nhn.com/,help'],\n      ['www.nhn.com/.help.', 'www.nhn.com/.help'],\n      ['www.nhn.com/:help:', 'www.nhn.com/:help'],\n      ['www.nhn.com/*help*', 'www.nhn.com/*help'],\n      ['www.nhn.com/~help~', 'www.nhn.com/~help'],\n      ['http://nhn.com/~help~', 'http://nhn.com/~help'],\n      ['https://nhn.com/~help~', 'https://nhn.com/~help'],\n    ];\n\n    pairs.forEach(([input, text]) => {\n      expect(parseUrlLink(input)![0].text).toBe(text);\n    });\n  });\n\n  it('trailing closing parens without matching opening parens are excluded', () => {\n    const pairs = [\n      ['www.nhn.com/(ui)', 'www.nhn.com/(ui)'],\n      ['www.nhn.com/(ui))', 'www.nhn.com/(ui)'],\n      ['(www.nhn.com/(ui))', 'www.nhn.com/(ui)'],\n      ['(www.nhn.com/((ui))', 'www.nhn.com/((ui))'],\n      ['(www.nhn.com/(ui)', 'www.nhn.com/(ui)'],\n      ['(www.nhn.com/)))(ui))', 'www.nhn.com/)))(ui)'],\n      ['(http://nhn.com/)))(ui))', 'http://nhn.com/)))(ui)'],\n      ['(https://nhn.com/)))(ui))', 'https://nhn.com/)))(ui)'],\n    ];\n\n    pairs.forEach(([input, text]) => {\n      expect(parseUrlLink(input)![0].text).toBe(text);\n    });\n  });\n\n  it('trailing entity-like pattern (&xxx;) are excluded', () => {\n    const pairs = [\n      ['www.nhn.com/ui&editor;grid', 'www.nhn.com/ui&editor;grid'],\n      ['www.nhn.com/ui&grid;', 'www.nhn.com/ui'],\n      ['www.nhn.com/ui&?grid;', 'www.nhn.com/ui&?grid;'],\n      ['http://nhn.com/ui&?grid;', 'http://nhn.com/ui&?grid;'],\n      ['https://nhn.com/ui&?grid;', 'https://nhn.com/ui&?grid;'],\n    ];\n\n    pairs.forEach(([input, text]) => {\n      expect(parseUrlLink(input)![0].text).toBe(text);\n    });\n  });\n\n  it('should handle multiple occurrences', () => {\n    expect(parseUrlLink('Hello www.nhn.com and http://toast.com')).toEqual([\n      {\n        text: 'www.nhn.com',\n        url: 'http://www.nhn.com',\n        range: [6, 16],\n      },\n      {\n        text: 'http://toast.com',\n        url: 'http://toast.com',\n        range: [22, 37],\n      },\n    ]);\n  });\n});\n\ndescribe('parseEmailLink', () => {\n  it('simple example', () => {\n    expect(parseEmailLink('ui@toast.com')).toEqual([\n      {\n        text: 'ui@toast.com',\n        url: 'mailto:ui@toast.com',\n        range: [0, 11],\n      },\n    ]);\n\n    expect(parseEmailLink('Hello ui@toast.com guys')).toEqual([\n      {\n        text: 'ui@toast.com',\n        url: 'mailto:ui@toast.com',\n        range: [6, 17],\n      },\n    ]);\n  });\n\n  it('+ can occur before the @, but not after.', () => {\n    expect(parseEmailLink('ui@to+ast.com')).toEqual([]);\n    expect(parseEmailLink('u+i@toast.com')).toEqual([\n      {\n        text: 'u+i@toast.com',\n        url: 'mailto:u+i@toast.com',\n        range: [0, 12],\n      },\n    ]);\n  });\n\n  it('trailing dash(-) and underscore(_) are invalid, trailing dot(.) is excluded ', () => {\n    const pairs = [\n      ['a.b-c_d@a.b', 'a.b-c_d@a.b'],\n      ['a.b-c_d@a.b.', 'a.b-c_d@a.b'],\n    ];\n    const invalids = ['a.b-c_d@a.b-', 'a.b-c_d@a.b_'];\n\n    pairs.forEach(([input, text]) => {\n      expect(parseEmailLink(input)![0].text).toBe(text);\n    });\n    invalids.forEach((input) => {\n      expect(parseEmailLink(input)).toEqual([]);\n    });\n  });\n\n  it('should handle multiple occurrences', () => {\n    expect(parseEmailLink('Hello ui@toast.com and file@toast.com')).toEqual([\n      {\n        text: 'ui@toast.com',\n        url: 'mailto:ui@toast.com',\n        range: [6, 17],\n      },\n      {\n        text: 'file@toast.com',\n        url: 'mailto:file@toast.com',\n        range: [23, 36],\n      },\n    ]);\n  });\n});\n\ndescribe('custom autolink parser', () => {\n  const renderer = new Renderer();\n  const reader = new Parser({\n    extendedAutolinks: (content) => {\n      const regex = /\\d{3}/g;\n      const result = [];\n      let matched;\n\n      while ((matched = regex.exec(content))) {\n        const { index } = matched;\n        const text = matched[0];\n        const range: [number, number] = [index, index + text.length - 1];\n        const url = `num:${text}`;\n\n        result.push({ text, url, range });\n      }\n      return result;\n    },\n  });\n\n  it('should parse custom pattern', () => {\n    const root = reader.parse('A 111 B 222');\n    const para = root.firstChild!;\n    const link1 = para.firstChild!.next!;\n    const link2 = link1.next!.next!;\n\n    expect(link1).toMatchObject({\n      destination: 'num:111',\n      extendedAutolink: true,\n      sourcepos: pos(1, 3, 1, 5),\n      firstChild: {\n        literal: '111',\n      },\n    });\n\n    expect(link2).toMatchObject({\n      destination: 'num:222',\n      extendedAutolink: true,\n      sourcepos: pos(1, 9, 1, 11),\n      firstChild: {\n        literal: '222',\n      },\n    });\n\n    expect(renderer.render(root)).toBe(\n      '<p>A <a href=\"num:111\">111</a> B <a href=\"num:222\">222</a></p>\\n'\n    );\n  });\n});\n\n// https://github.github.com/gfm/#example-621\ndescribe('GFM Examples', () => {\n  const reader = new Parser({ extendedAutolinks: true });\n  const renderer = new Renderer();\n\n  it('621', () => {\n    const root = reader.parse('www.commonmark.org');\n    const link = root.firstChild!.firstChild as LinkNode;\n    const linkText = link.firstChild!;\n\n    expect(link).toMatchObject({\n      type: 'link',\n      destination: 'http://www.commonmark.org',\n      extendedAutolink: true,\n      sourcepos: pos(1, 1, 1, 18),\n    });\n\n    expect(linkText).toMatchObject({\n      literal: 'www.commonmark.org',\n      sourcepos: pos(1, 1, 1, 18),\n    });\n\n    const html = renderer.render(root);\n    expect(html).toBe('<p><a href=\"http://www.commonmark.org\">www.commonmark.org</a></p>\\n');\n  });\n\n  it('622', () => {\n    const root = reader.parse('Visit www.commonmark.org/help for more information.');\n    const text1 = root.firstChild!.firstChild!;\n    const link = text1.next as LinkNode;\n    const linkText = link.firstChild!;\n    const text2 = link.next!;\n\n    expect(text1.literal).toBe('Visit ');\n    expect(link).toMatchObject({\n      type: 'link',\n      extendedAutolink: true,\n      destination: 'http://www.commonmark.org/help',\n      sourcepos: pos(1, 7, 1, 29),\n    });\n\n    expect(linkText.literal).toBe('www.commonmark.org/help');\n    expect(linkText.sourcepos).toEqual(pos(1, 7, 1, 29));\n\n    expect(text2.literal).toBe(' for more information.');\n    expect(text2.sourcepos).toEqual(pos(1, 30, 1, 51));\n\n    const html = renderer.render(root);\n    expect(html).toBe(\n      '<p>Visit <a href=\"http://www.commonmark.org/help\">www.commonmark.org/help</a> for more information.</p>\\n'\n    );\n  });\n\n  const examples = [\n    {\n      no: 623,\n      input: ['Visit www.commonmark.org.\\n\\n', 'Visit www.commonmark.org/a.b.'].join(''),\n      output: [\n        '<p>Visit <a href=\"http://www.commonmark.org\">www.commonmark.org</a>.</p>\\n',\n        '<p>Visit <a href=\"http://www.commonmark.org/a.b\">www.commonmark.org/a.b</a>.</p>\\n',\n      ].join(''),\n    },\n    {\n      no: 624,\n      input: [\n        'www.google.com/search?q=Markup+(business)\\n\\n',\n        'www.google.com/search?q=Markup+(business)))\\n\\n',\n        '(www.google.com/search?q=Markup+(business))\\n\\n',\n        '(www.google.com/search?q=Markup+(business)',\n      ].join(''),\n      output: [\n        '<p><a href=\"http://www.google.com/search?q=Markup+(business)\">',\n        'www.google.com/search?q=Markup+(business)</a></p>\\n',\n        '<p><a href=\"http://www.google.com/search?q=Markup+(business)\">',\n        'www.google.com/search?q=Markup+(business)</a>))</p>\\n',\n        '<p>(<a href=\"http://www.google.com/search?q=Markup+(business)\">',\n        'www.google.com/search?q=Markup+(business)</a>)</p>\\n',\n        '<p>(<a href=\"http://www.google.com/search?q=Markup+(business)\">',\n        'www.google.com/search?q=Markup+(business)</a></p>\\n',\n      ].join(''),\n    },\n    {\n      no: 625,\n      input: 'www.google.com/search?q=(business))+ok',\n      output: [\n        '<p><a href=\"http://www.google.com/search?q=(business))+ok\">',\n        'www.google.com/search?q=(business))+ok</a></p>\\n',\n      ].join(''),\n    },\n    {\n      no: 626,\n      input: [\n        'www.google.com/search?q=commonmark&hl=en\\n\\n',\n        'www.google.com/search?q=commonmark&hl;',\n      ].join(''),\n      output: [\n        '<p><a href=\"http://www.google.com/search?q=commonmark&amp;hl=en\">',\n        'www.google.com/search?q=commonmark&amp;hl=en</a></p>\\n',\n        '<p><a href=\"http://www.google.com/search?q=commonmark\">',\n        'www.google.com/search?q=commonmark</a>&amp;hl;</p>\\n',\n      ].join(''),\n    },\n    {\n      no: 627,\n      input: 'www.commonmark.org/he<lp',\n      output: '<p><a href=\"http://www.commonmark.org/he\">www.commonmark.org/he</a>&lt;lp</p>\\n',\n    },\n    {\n      no: 628,\n      input: [\n        'http://commonmark.org\\n\\n',\n        '(Visit https://encrypted.google.com/search?q=Markup+(business))',\n      ].join(''),\n      output: [\n        '<p><a href=\"http://commonmark.org\">http://commonmark.org</a></p>\\n',\n        '<p>(Visit <a href=\"https://encrypted.google.com/search?q=Markup+(business)\">',\n        'https://encrypted.google.com/search?q=Markup+(business)</a>)</p>\\n',\n      ].join(''),\n    },\n    {\n      no: 629,\n      input: 'foo@bar.baz',\n      output: '<p><a href=\"mailto:foo@bar.baz\">foo@bar.baz</a></p>\\n',\n    },\n    {\n      no: 630,\n      input: `hello@mail+xyz.example isn't valid, but hello+xyz@mail.example is.`,\n      output: [\n        `<p>hello@mail+xyz.example isn't valid, but `,\n        `<a href=\"mailto:hello+xyz@mail.example\">hello+xyz@mail.example</a> is.</p>\\n`,\n      ].join(''),\n    },\n    {\n      no: 631,\n      input: ['a.b-c_d@a.b\\n\\n', 'a.b-c_d@a.b.\\n\\n', 'a.b-c_d@a.b-\\n\\n', 'a.b-c_d@a.b_'].join(''),\n      output: [\n        '<p><a href=\"mailto:a.b-c_d@a.b\">a.b-c_d@a.b</a></p>\\n',\n        '<p><a href=\"mailto:a.b-c_d@a.b\">a.b-c_d@a.b</a>.</p>\\n',\n        '<p>a.b-c_d@a.b-</p>\\n',\n        '<p>a.b-c_d@a.b_</p>\\n',\n      ].join(''),\n    },\n  ];\n\n  examples.forEach(({ no, input, output }) => {\n    it(String(no), () => {\n      const root = reader.parse(input);\n      const html = renderer.render(root);\n      expect(html).toBe(output);\n    });\n  });\n});\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/gfm/__test__/strikethrough.spec.ts",
    "content": "import { Parser } from '../../blocks';\nimport { Renderer } from '../../../html/renderer';\nimport { source } from 'common-tags';\n\nconst reader = new Parser({ smart: true });\nconst renderer = new Renderer({ gfm: true });\n\ndescribe('smart punctuation', () => {\n  it('single quote', () => {\n    const root = reader.parse(`Hello *'World'*`);\n    const html = renderer.render(root);\n\n    expect(html).toBe('<p>Hello <em>\\u2018World\\u2019</em></p>\\n');\n  });\n\n  it('double quote', () => {\n    const root = reader.parse(`Hello \"*World*\"`);\n    const html = renderer.render(root);\n\n    expect(html).toBe('<p>Hello \\u201C<em>World</em>\\u201D</p>\\n');\n  });\n});\n\ndescribe('strikethrough', () => {\n  // https://github.github.com/gfm/#example-491\n  it('GFM Example 491', () => {\n    const root = reader.parse('~~Hi~~ Hello, world!');\n    const html = renderer.render(root);\n\n    expect(html).toBe('<p><del>Hi</del> Hello, world!</p>\\n');\n  });\n\n  it('GFM Example 492', () => {\n    const input = source`\n      This ~~has a\n\n      new paragraph~~.\n    `;\n    const output = source`\n      <p>This ~~has a</p>\n      <p>new paragraph~~.</p>\n    `;\n\n    const root = reader.parse(input);\n    const html = renderer.render(root);\n\n    expect(html).toEqual(`${output}\\n`);\n  });\n\n  it('basic example', () => {\n    const root = reader.parse('Hello ~~World~~');\n    const para = root.firstChild!;\n    const text = para.firstChild!;\n    const strike = text.next!;\n    const strikeText = strike.firstChild!;\n\n    expect(text.literal).toBe('Hello ');\n    expect(strikeText.literal).toBe('World');\n    expect(strike.sourcepos).toEqual([\n      [1, 7],\n      [1, 15],\n    ]);\n    expect(strikeText.sourcepos).toEqual([\n      [1, 9],\n      [1, 13],\n    ]);\n\n    const html = renderer.render(root);\n\n    expect(html).toBe('<p>Hello <del>World</del></p>\\n');\n  });\n\n  it('complex delimiters', () => {\n    // 6 long delimiter-run after 'Hello' can be both open and close delimiter\n    const root = reader.parse('~~Hello~~~~~~World~~~');\n    const para = root.firstChild!;\n    const strike1 = para.firstChild!;\n    const text1 = strike1.next!;\n    const strike2 = text1.next!;\n    const text2 = strike2.next!;\n\n    expect(strike1.firstChild!.literal).toBe('Hello');\n    expect(text1.literal).toBe('~~');\n    expect(strike2.firstChild!.literal).toBe('World');\n    expect(text2.literal).toBe('~');\n\n    expect(strike1.sourcepos).toEqual([\n      [1, 1],\n      [1, 9],\n    ]);\n    expect(text1.sourcepos).toEqual([\n      [1, 10],\n      [1, 11],\n    ]);\n    expect(strike2.sourcepos).toEqual([\n      [1, 12],\n      [1, 20],\n    ]);\n    expect(text2.sourcepos).toEqual([\n      [1, 21],\n      [1, 21],\n    ]);\n\n    const html = renderer.render(root);\n\n    expect(html).toBe('<p><del>Hello</del>~~<del>World</del>~</p>\\n');\n  });\n\n  it('nested delimiters (only strikethrough)', () => {\n    const root = reader.parse('Hello~~~~~~World~~~~~');\n    const para = root.firstChild!;\n    const text1 = para.firstChild!;\n    const strike1 = text1.next!;\n    const strike2 = strike1.firstChild!; // nested\n    const text2 = strike1.next!;\n\n    expect(text1.literal).toBe('Hello~~');\n    expect(strike2.firstChild!.literal).toBe('World');\n    expect(text2.literal).toBe('~');\n\n    expect(text1.sourcepos).toEqual([\n      [1, 1],\n      [1, 7],\n    ]);\n    expect(strike1.sourcepos).toEqual([\n      [1, 8],\n      [1, 20],\n    ]);\n    expect(strike2.sourcepos).toEqual([\n      [1, 10],\n      [1, 18],\n    ]);\n    expect(text2.sourcepos).toEqual([\n      [1, 21],\n      [1, 21],\n    ]);\n\n    const html = renderer.render(root);\n\n    expect(html).toBe('<p>Hello~~<del><del>World</del></del>~</p>\\n');\n  });\n\n  it('nested delimiters (with emphasis)', () => {\n    const root = reader.parse('~~*Hello*~~**~~World~~**');\n    const para = root.firstChild!;\n    const strike1 = para.firstChild!;\n    const emph = strike1.firstChild!;\n    const strong = strike1.next!;\n    const strike2 = strong.firstChild!;\n\n    expect(strike1.sourcepos).toEqual([\n      [1, 1],\n      [1, 11],\n    ]);\n    expect(emph.sourcepos).toEqual([\n      [1, 3],\n      [1, 9],\n    ]);\n    expect(strong.sourcepos).toEqual([\n      [1, 12],\n      [1, 24],\n    ]);\n    expect(strike2.sourcepos).toEqual([\n      [1, 14],\n      [1, 22],\n    ]);\n\n    const html = renderer.render(root);\n\n    expect(html).toBe('<p><del><em>Hello</em></del><strong><del>World</del></strong></p>\\n');\n  });\n});\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/gfm/__test__/table.spec.ts",
    "content": "import { Parser } from '../../blocks';\nimport { Renderer } from '../../../html/renderer';\nimport { convertToArrayTree } from '../../__test__/helper.spec';\nimport { BlockNode, TableNode } from 'src/commonmark/node';\nimport { source } from 'common-tags';\n\nconst reader = new Parser();\nconst renderer = new Renderer({ gfm: true });\n\n// Shortcut function to prevent prettier from adding linebreak beetween nested arrays\nconst pos = (a: number, b: number, c: number, d: number) => [\n  [a, b],\n  [c, d],\n];\n\ndescribe('table', () => {\n  it('basic', () => {\n    const root = reader.parse('  a |  b\\n --| ---\\n|  c |  |\\n e');\n    const result = convertToArrayTree(root, [\n      'type',\n      'sourcepos',\n      'stringContent',\n      'paddingLeft',\n      'paddingRight',\n      'literal',\n    ] as (keyof BlockNode)[]);\n\n    expect(result).toEqual({\n      type: 'document',\n      sourcepos: pos(1, 1, 4, 2),\n      children: [\n        {\n          type: 'table',\n          sourcepos: pos(1, 3, 3, 9),\n          children: [\n            {\n              type: 'tableHead',\n              sourcepos: pos(1, 3, 2, 8),\n              children: [\n                {\n                  type: 'tableRow',\n                  sourcepos: pos(1, 3, 1, 8),\n                  children: [\n                    {\n                      type: 'tableCell',\n                      paddingLeft: 0,\n                      paddingRight: 1,\n                      sourcepos: pos(1, 3, 1, 4),\n                      children: [\n                        {\n                          type: 'text',\n                          literal: 'a',\n                          sourcepos: pos(1, 3, 1, 3),\n                        },\n                      ],\n                    },\n                    {\n                      type: 'tableCell',\n                      paddingLeft: 2,\n                      paddingRight: 0,\n                      sourcepos: pos(1, 6, 1, 8),\n                      children: [\n                        {\n                          type: 'text',\n                          literal: 'b',\n                          sourcepos: pos(1, 8, 1, 8),\n                        },\n                      ],\n                    },\n                  ],\n                },\n                {\n                  type: 'tableDelimRow',\n                  sourcepos: pos(2, 2, 2, 8),\n                  children: [\n                    {\n                      type: 'tableDelimCell',\n                      paddingLeft: 0,\n                      paddingRight: 0,\n                      stringContent: '--',\n                      sourcepos: pos(2, 2, 2, 3),\n                    },\n                    {\n                      type: 'tableDelimCell',\n                      paddingLeft: 1,\n                      paddingRight: 0,\n                      stringContent: '---',\n                      sourcepos: pos(2, 5, 2, 8),\n                    },\n                  ],\n                },\n              ],\n            },\n            {\n              type: 'tableBody',\n              sourcepos: pos(3, 1, 3, 9),\n              children: [\n                {\n                  type: 'tableRow',\n                  sourcepos: pos(3, 1, 3, 9),\n                  children: [\n                    {\n                      type: 'tableCell',\n                      paddingLeft: 2,\n                      paddingRight: 1,\n                      sourcepos: pos(3, 2, 3, 5),\n                      children: [\n                        {\n                          type: 'text',\n                          literal: 'c',\n                          sourcepos: pos(3, 4, 3, 4),\n                        },\n                      ],\n                    },\n                    {\n                      type: 'tableCell',\n                      paddingLeft: 0,\n                      paddingRight: 0,\n                      sourcepos: pos(3, 7, 3, 8),\n                    },\n                  ],\n                },\n              ],\n            },\n          ],\n        },\n        {\n          type: 'paragraph',\n          sourcepos: pos(4, 2, 4, 2),\n          children: [\n            {\n              type: 'text',\n              sourcepos: pos(4, 2, 4, 2),\n              literal: 'e',\n            },\n          ],\n        },\n      ],\n    });\n\n    const html = renderer.render(root);\n    const output = source`\n      <table>\n      <thead>\n      <tr>\n      <th>a</th>\n      <th>b</th>\n      </tr>\n      </thead>\n      <tbody>\n      <tr>\n      <td>c</td>\n      <td></td>\n      </tr>\n      </tbody>\n      </table>\n      <p>e</p>\n    `;\n    expect(html).toBe(`${output}\\n`);\n  });\n\n  it('preceded by non-empty line', () => {\n    const input = source`\n      Hello\n      World\n      | a | b |\n      | - | - |\n      | c | d |\n    `;\n    const root = reader.parse(input);\n    const result = convertToArrayTree(root, ['type', 'sourcepos'] as (keyof BlockNode)[]);\n\n    expect(result).toMatchObject({\n      type: 'document',\n      children: [\n        {\n          type: 'paragraph',\n          sourcepos: pos(1, 1, 2, 5),\n        },\n        {\n          type: 'table',\n          sourcepos: pos(3, 1, 5, 9),\n        },\n      ],\n    });\n  });\n\n  it('with aligns', () => {\n    const root = reader.parse('left | center | right\\n:--- | :---: | ---:\\na | b | c');\n    const tableNode = root.firstChild as TableNode;\n\n    expect(tableNode.columns).toEqual([{ align: 'left' }, { align: 'center' }, { align: 'right' }]);\n  });\n\n  it('with empty cells', () => {\n    const input = source`\n      | a |  |  |\n      | - | - | - |\n      |  | b |  |\n      |  |  | c |\n    `;\n    const output = source`\n      <table>\n      <thead>\n      <tr>\n      <th>a</th>\n      <th></th>\n      <th></th>\n      </tr>\n      </thead>\n      <tbody>\n      <tr>\n      <td></td>\n      <td>b</td>\n      <td></td>\n      </tr>\n      <tr>\n      <td></td>\n      <td></td>\n      <td>c</td>\n      </tr>\n      </tbody>\n      </table>\n    `;\n\n    const root = reader.parse(input);\n    const html = renderer.render(root);\n    expect(html).toBe(`${output}\\n`);\n  });\n});\n\ndescribe('GFM Exmaple', () => {\n  const examples = [\n    {\n      no: 198,\n      input: source`\n        | foo | bar |\n        | --- | --- |\n        | baz | bim |\n      `,\n      output: source`\n        <table>\n        <thead>\n        <tr>\n        <th>foo</th>\n        <th>bar</th>\n        </tr>\n        </thead>\n        <tbody>\n        <tr>\n        <td>baz</td>\n        <td>bim</td>\n        </tr>\n        </tbody>\n        </table>\n      `,\n    },\n    {\n      no: 199,\n      input: source`\n        | abc | defghi |\n        :-: | -----------:\n        bar | baz\n      `,\n      output: source`\n        <table>\n        <thead>\n        <tr>\n        <th align=\"center\">abc</th>\n        <th align=\"right\">defghi</th>\n        </tr>\n        </thead>\n        <tbody>\n        <tr>\n        <td align=\"center\">bar</td>\n        <td align=\"right\">baz</td>\n        </tr>\n        </tbody>\n        </table>\n      `,\n    },\n    {\n      no: 200,\n      input: source`\n        | f\\\\|oo  |\n        | ------ |\n        | b \\`\\\\|\\` az |\n        | b **\\\\|** im |\n      `,\n      output: source`\n        <table>\n        <thead>\n        <tr>\n        <th>f|oo</th>\n        </tr>\n        </thead>\n        <tbody>\n        <tr>\n        <td>b <code>|</code> az</td>\n        </tr>\n        <tr>\n        <td>b <strong>|</strong> im</td>\n        </tr>\n        </tbody>\n        </table>\n      `,\n    },\n    {\n      no: 201,\n      input: source`\n        | abc | def |\n        | --- | --- |\n        | bar | baz |\n        > bar\n      `,\n      output: source`\n        <table>\n        <thead>\n        <tr>\n        <th>abc</th>\n        <th>def</th>\n        </tr>\n        </thead>\n        <tbody>\n        <tr>\n        <td>bar</td>\n        <td>baz</td>\n        </tr>\n        </tbody>\n        </table>\n        <blockquote>\n        <p>bar</p>\n        </blockquote>\n      `,\n    },\n    {\n      no: 202,\n      input: source`\n        | abc | def |\n        | --- | --- |\n        | bar | baz |\n        bar\n\n        bar\n      `,\n      output: source`\n        <table>\n        <thead>\n        <tr>\n        <th>abc</th>\n        <th>def</th>\n        </tr>\n        </thead>\n        <tbody>\n        <tr>\n        <td>bar</td>\n        <td>baz</td>\n        </tr>\n        </tbody>\n        </table>\n        <p>bar</p>\n        <p>bar</p>\n      `,\n    },\n    // TODO: need to find a way to parse merged-column and re-activate this test case\n    // {\n    //   no: 203,\n    //   input: source`\n    //     | abc | def |\n    //     | --- |\n    //     | bar |\n    //   `,\n    //   output: source`\n    //     <p>| abc | def |\n    //     | --- |\n    //     | bar |</p>\n    //   `\n    // },\n    {\n      no: 204,\n      input: source`\n        | abc | def |\n        | --- | --- |\n        | bar |\n        | bar | baz | boo |\n      `,\n      output: source`\n        <table>\n        <thead>\n        <tr>\n        <th>abc</th>\n        <th>def</th>\n        </tr>\n        </thead>\n        <tbody>\n        <tr>\n        <td>bar</td>\n        <td></td>\n        </tr>\n        <tr>\n        <td>bar</td>\n        <td>baz</td>\n        </tr>\n        </tbody>\n        </table>\n      `,\n    },\n    {\n      no: 205,\n      input: source`\n        | abc | def |\n        | --- | --- |\n      `,\n      output: source`\n        <table>\n        <thead>\n        <tr>\n        <th>abc</th>\n        <th>def</th>\n        </tr>\n        </thead>\n        </table>\n      `,\n    },\n  ];\n\n  examples.forEach(({ no, input, output }) => {\n    it(String(no), () => {\n      const root = reader.parse(input);\n      const html = renderer.render(root);\n      expect(html).toBe(`${output}\\n`);\n    });\n  });\n});\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/gfm/__test__/tagfilter.spec.ts",
    "content": "import { Parser } from '../../blocks';\nimport { Renderer } from '../../../html/renderer';\nimport { source } from 'common-tags';\n\nconst reader = new Parser();\nconst renderer = new Renderer({ gfm: true, tagFilter: true });\n\n// https://github.github.com/gfm/#example-653\nit('GFM Example 653', () => {\n  const input = source`\n    <strong> <title> <style> <em>\n\n    <blockquote>\n    <xmp> is disallowed.  <XMP> is also disallowed.\n    </blockquote>\n  `;\n  const output = source`\n    <p><strong> &lt;title> &lt;style> <em></p>\n    <blockquote>\n    &lt;xmp> is disallowed.  &lt;XMP> is also disallowed.\n    </blockquote>\n  `;\n\n  const root = reader.parse(input);\n  const html = renderer.render(root);\n\n  expect(html).toEqual(`${output}\\n`);\n});\n\nit('Disallowed tags with attributes and closing tags', () => {\n  const input = source`\n    <strong> <TITLE> <style type=\"text/css\"> <em>\n\n    <blockquote>\n    </xmp> is disallowed.  </XMP> is also disallowed.\n    </blockquote>\n  `;\n  const output = source`\n    <p><strong> &lt;TITLE> &lt;style type=\"text/css\"> <em></p>\n    <blockquote>\n    &lt;/xmp> is disallowed.  &lt;/XMP> is also disallowed.\n    </blockquote>\n  `;\n\n  const root = reader.parse(input);\n  const html = renderer.render(root);\n\n  expect(html).toEqual(`${output}\\n`);\n});\n\nit('Keep BlockHTML as is, and only escape during rendering phase', () => {\n  const input = source`\n    <iframe>\n    Hello **World**\n    </iframe>\n  `;\n  // Does not convert emphasis inside <iframe>, as it's inside BlockHTML\n  const output = source`\n    &lt;iframe>\n    Hello **World**\n    &lt;/iframe>\n  `;\n\n  const root = reader.parse(input);\n  const html = renderer.render(root);\n\n  expect(html).toEqual(`${output}\\n`);\n});\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/gfm/__test__/taskListItem.spec.ts",
    "content": "import { source } from 'common-tags';\nimport { Parser } from '../../blocks';\nimport { Renderer } from '../../../html/renderer';\nimport { pos } from '../../__test__/helper.spec';\n\nconst reader = new Parser();\nconst renderer = new Renderer({ gfm: true });\n\ndescribe('Task list item', () => {\n  it('Parse', () => {\n    const root = reader.parse(source`\n      - [ ] Item1\n      -  [x] Item2\n      -   [X]  Item3\n    `);\n    expect(root).toMatchObject({\n      firstChild: {\n        type: 'list',\n        firstChild: {\n          type: 'item',\n          listData: {\n            task: true,\n            checked: false,\n          },\n          firstChild: {\n            type: 'paragraph',\n            sourcepos: pos(1, 7, 1, 11),\n          },\n          next: {\n            type: 'item',\n            listData: {\n              task: true,\n              checked: true,\n            },\n            firstChild: {\n              type: 'paragraph',\n              sourcepos: pos(2, 8, 2, 12),\n            },\n            next: {\n              type: 'item',\n              listData: {\n                task: true,\n                checked: true,\n              },\n              firstChild: {\n                type: 'paragraph',\n                sourcepos: pos(3, 10, 3, 14),\n              },\n            },\n          },\n        },\n      },\n    });\n  });\n\n  // https://github.github.com/gfm/#example-279\n  it('GFM Example 279', () => {\n    const input = source`\n      - [ ] foo\n      - [x] bar\n    `;\n    const output = source`\n      <ul>\n      <li><input disabled=\"\" type=\"checkbox\" /> foo</li>\n      <li><input checked=\"\" disabled=\"\" type=\"checkbox\" /> bar</li>\n      </ul>\n    `;\n\n    const root = reader.parse(input);\n    const html = renderer.render(root);\n\n    expect(html).toEqual(`${output}\\n`);\n  });\n\n  // https://github.github.com/gfm/#example-280\n  it('GFM Example 280', () => {\n    const input = source`\n      - [x] foo\n        - [ ] bar\n        - [x] baz\n      - [ ] bim\n    `;\n    const output = source`\n      <ul>\n      <li><input checked=\"\" disabled=\"\" type=\"checkbox\" /> foo\n      <ul>\n      <li><input disabled=\"\" type=\"checkbox\" /> bar</li>\n      <li><input checked=\"\" disabled=\"\" type=\"checkbox\" /> baz</li>\n      </ul>\n      </li>\n      <li><input disabled=\"\" type=\"checkbox\" /> bim</li>\n      </ul>\n    `;\n\n    const root = reader.parse(input);\n    const html = renderer.render(root);\n\n    expect(html).toEqual(`${output}\\n`);\n  });\n});\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/gfm/autoLinks.ts",
    "content": "import { Sourcepos } from '@t/node';\nimport { AutolinkParser } from '@t/parser';\nimport { createNode, text } from '../node';\nimport NodeWalker from '../nodeWalker';\n\nconst DOMAIN = '(?:[w-]+.)*[A-Za-z0-9-]+.[A-Za-z0-9-]+';\nconst PATH = '[^<\\\\s]*[^<?!.,:*_?~\\\\s]';\nconst EMAIL = '[\\\\w.+-]+@(?:[\\\\w-]+\\\\.)+[\\\\w-]+';\n\nfunction trimUnmatchedTrailingParens(source: string) {\n  const trailingParen = /\\)+$/.exec(source);\n  if (trailingParen) {\n    let count = 0;\n    for (const ch of source) {\n      if (ch === '(') {\n        if (count < 0) {\n          count = 1;\n        } else {\n          count += 1;\n        }\n      } else if (ch === ')') {\n        count -= 1;\n      }\n    }\n\n    if (count < 0) {\n      const trimCount = Math.min(-count, trailingParen[0].length);\n      return source.substring(0, source.length - trimCount);\n    }\n  }\n  return source;\n}\n\nfunction trimTrailingEntity(source: string) {\n  return source.replace(/&[A-Za-z0-9]+;$/, '');\n}\n\ninterface LinkInfo {\n  text: string;\n  url: string;\n  range: [number, number];\n}\n\nexport function parseEmailLink(source: string) {\n  const reEmailLink = new RegExp(EMAIL, 'g');\n  const result: LinkInfo[] = [];\n  let m;\n  while ((m = reEmailLink.exec(source))) {\n    const text = m[0];\n    if (!/[_-]+$/.test(text)) {\n      result.push({\n        text,\n        range: [m.index, m.index + text.length - 1],\n        url: `mailto:${text}`,\n      });\n    }\n  }\n\n  return result;\n}\n\nexport function parseUrlLink(source: string) {\n  const reWwwAutolink = new RegExp(`(www|https?://)\\.${DOMAIN}${PATH}`, 'g');\n  const result: LinkInfo[] = [];\n  let m;\n\n  while ((m = reWwwAutolink.exec(source))) {\n    const text = trimTrailingEntity(trimUnmatchedTrailingParens(m[0]));\n    const scheme = m[1] === 'www' ? 'http://' : '';\n    result.push({\n      text,\n      range: [m.index, m.index + text.length - 1],\n      url: `${scheme}${text}`,\n    });\n  }\n\n  return result;\n}\n\nfunction baseAutolinkParser(source: string) {\n  return [...parseUrlLink(source), ...parseEmailLink(source)].sort(\n    (a, b) => a.range[0] - b.range[0]\n  );\n}\n\nexport function convertExtAutoLinks(walker: NodeWalker, autolinkParser: boolean | AutolinkParser) {\n  if (typeof autolinkParser === 'boolean') {\n    autolinkParser = baseAutolinkParser;\n  }\n\n  let event;\n  while ((event = walker.next())) {\n    const { entering, node } = event;\n    if (entering && node.type === 'text' && node.parent!.type !== 'link') {\n      const literal = node.literal!;\n      const linkInfos = autolinkParser(literal);\n\n      if (!linkInfos || !linkInfos.length) {\n        continue;\n      }\n\n      let lastIdx = 0;\n      const [lineNum, chPos] = node.sourcepos![0];\n      const sourcepos = (startIdx: number, endIdx: number): Sourcepos => [\n        [lineNum, chPos + startIdx],\n        [lineNum, chPos + endIdx],\n      ];\n      const newNodes = [];\n      for (const { range, url, text: linkText } of linkInfos) {\n        if (range[0] > lastIdx) {\n          newNodes.push(\n            text(literal.substring(lastIdx, range[0]), sourcepos(lastIdx, range[0] - 1))\n          );\n        }\n        const linkNode = createNode('link', sourcepos(...range));\n        linkNode.appendChild(text(linkText, sourcepos(...range)));\n        linkNode.destination = url;\n        linkNode.extendedAutolink = true;\n        newNodes.push(linkNode);\n        lastIdx = range[1] + 1;\n      }\n      if (lastIdx < literal.length) {\n        newNodes.push(text(literal.substring(lastIdx), sourcepos(lastIdx, literal.length - 1)));\n      }\n\n      for (const newNode of newNodes) {\n        node.insertBefore(newNode);\n      }\n      node.unlink();\n    }\n  }\n}\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/gfm/tableBlockHandler.ts",
    "content": "import { MdNodeType } from '@t/node';\nimport { Process, BlockHandler } from '../blockHandlers';\n\nexport const table: BlockHandler = {\n  continue() {\n    return Process.Go;\n  },\n  finalize() {},\n  canContain(t: MdNodeType) {\n    return t === 'tableHead' || t === 'tableBody';\n  },\n  acceptsLines: false,\n};\n\nexport const tableBody: BlockHandler = {\n  continue() {\n    return Process.Go;\n  },\n  finalize() {},\n  canContain(t: MdNodeType) {\n    return t === 'tableRow';\n  },\n  acceptsLines: false,\n};\n\nexport const tableHead: BlockHandler = {\n  continue() {\n    return Process.Stop;\n  },\n  finalize() {},\n  canContain(t: MdNodeType) {\n    return t === 'tableRow' || t === 'tableDelimRow';\n  },\n  acceptsLines: false,\n};\n\nexport const tableDelimRow: BlockHandler = {\n  continue() {\n    return Process.Stop;\n  },\n  finalize() {},\n  canContain(t: MdNodeType) {\n    return t === 'tableDelimCell';\n  },\n  acceptsLines: false,\n};\n\nexport const tableDelimCell: BlockHandler = {\n  continue() {\n    return Process.Stop;\n  },\n  finalize() {},\n  canContain() {\n    return false;\n  },\n  acceptsLines: false,\n};\n\nexport const tableRow: BlockHandler = {\n  continue() {\n    return Process.Stop;\n  },\n  finalize() {},\n  canContain(t: MdNodeType) {\n    return t === 'tableCell';\n  },\n  acceptsLines: false,\n};\n\nexport const tableCell: BlockHandler = {\n  continue() {\n    return Process.Stop;\n  },\n  finalize() {},\n  canContain() {\n    return false;\n  },\n  acceptsLines: false,\n};\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/gfm/tableBlockStart.ts",
    "content": "import { Sourcepos, TableColumn } from '@t/node';\nimport { isEmpty } from '../common';\nimport { BlockStart, Matched } from '../blockStarts';\nimport { createNode, TableNode, TableCellNode } from '../node';\nimport { last } from '../../helper';\n\nfunction parseRowContent(content: string): [number, string[]] {\n  let startIdx = 0;\n  let offset = 0;\n  const cells = [];\n  for (let i = 0; i < content.length; i += 1) {\n    if (content[i] === '|' && content[i - 1] !== '\\\\') {\n      const cell = content.substring(startIdx, i);\n      if (startIdx === 0 && isEmpty(cell)) {\n        offset = i + 1;\n      } else {\n        cells.push(cell);\n      }\n      startIdx = i + 1;\n    }\n  }\n\n  if (startIdx < content.length) {\n    const cell = content.substring(startIdx, content.length);\n    if (!isEmpty(cell)) {\n      cells.push(cell);\n    }\n  }\n\n  return [offset, cells];\n}\n\nfunction generateTableCells(\n  cellType: 'tableCell' | 'tableDelimCell',\n  contents: string[],\n  lineNum: number,\n  chPos: number\n) {\n  const cells = [];\n  for (const content of contents) {\n    const preSpaces = content.match(/^[ \\t]+/);\n    let paddingLeft = preSpaces ? preSpaces[0].length : 0;\n    let paddingRight, trimmed;\n\n    if (paddingLeft === content.length) {\n      paddingLeft = 0;\n      paddingRight = 0;\n      trimmed = '';\n    } else {\n      const postSpaces = content.match(/[ \\t]+$/);\n      paddingRight = postSpaces ? postSpaces[0].length : 0;\n      trimmed = content.slice(paddingLeft, content.length - paddingRight);\n    }\n\n    const chPosStart = chPos + paddingLeft;\n    const tableCell = createNode(cellType, [\n      [lineNum, chPos],\n      [lineNum, chPos + content.length - 1],\n    ]) as TableCellNode;\n\n    tableCell.stringContent = trimmed.replace(/\\\\\\|/g, '|'); // replace esacped pipe(\\|)\n    tableCell.startIdx = cells.length;\n    tableCell.endIdx = cells.length;\n    tableCell.lineOffsets = [chPosStart - 1];\n    tableCell.paddingLeft = paddingLeft;\n    tableCell.paddingRight = paddingRight;\n    cells.push(tableCell);\n\n    chPos += content.length + 1;\n  }\n\n  return cells;\n}\n\nfunction getColumnFromDelimCell(cellNode: TableCellNode) {\n  let align = null;\n  const content = cellNode.stringContent!;\n  const firstCh = content[0];\n  const lastCh = content[content.length - 1];\n\n  if (lastCh === ':') {\n    align = firstCh === ':' ? 'center' : 'right';\n  } else if (firstCh === ':') {\n    align = 'left';\n  }\n\n  return { align } as TableColumn;\n}\n\nexport const tableHead: BlockStart = (parser, container) => {\n  const stringContent = container.stringContent!;\n  if (container.type === 'paragraph' && !parser.indented && !parser.blank) {\n    const lastNewLineIdx = stringContent.length - 1;\n    const lastLineStartIdx = stringContent.lastIndexOf('\\n', lastNewLineIdx - 1) + 1;\n    const headerContent = stringContent.slice(lastLineStartIdx, lastNewLineIdx);\n    const delimContent = parser.currentLine.slice(parser.nextNonspace);\n    const [headerOffset, headerCells] = parseRowContent(headerContent);\n    const [delimOffset, delimCells] = parseRowContent(delimContent);\n    const reValidDelimCell = /^[ \\t]*:?-+:?[ \\t]*$/;\n\n    if (\n      // not checking if the number of header cells and delimiter cells are the same\n      // to consider the case of merged-column (via plugin)\n      !headerCells.length ||\n      !delimCells.length ||\n      delimCells.some((cell) => !reValidDelimCell.test(cell)) ||\n      // to prevent to regard setTextHeading as tabel delim cell with 'disallowDeepHeading' option\n      (delimCells.length === 1 && delimContent.indexOf('|') !== 0)\n    ) {\n      return Matched.None;\n    }\n\n    const lineOffsets = container.lineOffsets!;\n    const firstLineNum = parser.lineNumber - 1;\n    const firstLineStart = last(lineOffsets) + 1;\n    const table = createNode('table', [\n      [firstLineNum, firstLineStart],\n      [parser.lineNumber, parser.offset],\n    ]);\n    // eslint-disable-next-line arrow-body-style\n    table.columns = delimCells.map(() => ({ align: null }));\n\n    container.insertAfter(table);\n    if (lineOffsets.length === 1) {\n      container.unlink();\n    } else {\n      container.stringContent = stringContent.slice(0, lastLineStartIdx);\n      const paraLastLineStartIdx = stringContent.lastIndexOf('\\n', lastLineStartIdx - 2) + 1;\n      const paraLastLineLen = lastLineStartIdx - paraLastLineStartIdx - 1;\n      parser.lastLineLength = lineOffsets[lineOffsets.length - 2] + paraLastLineLen;\n      parser.finalize(container, firstLineNum - 1);\n    }\n    parser.advanceOffset(parser.currentLine.length - parser.offset, false);\n\n    const tableHead = createNode('tableHead', [\n      [firstLineNum, firstLineStart],\n      [parser.lineNumber, parser.offset],\n    ] as Sourcepos);\n    table.appendChild(tableHead);\n\n    const tableHeadRow = createNode('tableRow', [\n      [firstLineNum, firstLineStart],\n      [firstLineNum, firstLineStart + headerContent.length - 1],\n    ]);\n    const tableDelimRow = createNode('tableDelimRow', [\n      [parser.lineNumber, parser.nextNonspace + 1],\n      [parser.lineNumber, parser.offset],\n    ]);\n    tableHead.appendChild(tableHeadRow);\n    tableHead.appendChild(tableDelimRow);\n\n    generateTableCells(\n      'tableCell',\n      headerCells,\n      firstLineNum,\n      firstLineStart + headerOffset\n    ).forEach((cellNode) => {\n      tableHeadRow.appendChild(cellNode);\n    });\n\n    const delimCellNodes = generateTableCells(\n      'tableDelimCell',\n      delimCells,\n      parser.lineNumber,\n      parser.nextNonspace + 1 + delimOffset\n    );\n\n    delimCellNodes.forEach((cellNode) => {\n      tableDelimRow.appendChild(cellNode);\n    });\n\n    table.columns = delimCellNodes.map(getColumnFromDelimCell);\n    parser.tip = table;\n\n    return Matched.Leaf;\n  }\n\n  return Matched.None;\n};\n\nexport const tableBody: BlockStart = (parser, container) => {\n  if (\n    (container.type !== 'table' && container.type !== 'tableBody') ||\n    (!parser.blank && parser.currentLine.indexOf('|') === -1)\n  ) {\n    return Matched.None;\n  }\n\n  parser.advanceOffset(parser.currentLine.length - parser.offset, false);\n\n  if (parser.blank) {\n    let table = container;\n    if (container.type === 'tableBody') {\n      table = container.parent as TableNode;\n      parser.finalize(container, parser.lineNumber - 1);\n    }\n    parser.finalize(table, parser.lineNumber - 1);\n    return Matched.None;\n  }\n\n  let tableBody = container;\n  if (container.type === 'table') {\n    tableBody = parser.addChild('tableBody', parser.nextNonspace);\n    tableBody.stringContent = null;\n  }\n  const tableRow = createNode('tableRow', [\n    [parser.lineNumber, parser.nextNonspace + 1],\n    [parser.lineNumber, parser.currentLine.length],\n  ]);\n  tableBody.appendChild(tableRow);\n\n  const table = tableBody.parent as TableNode;\n  const content = parser.currentLine.slice(parser.nextNonspace);\n  const [offset, cellContents] = parseRowContent(content);\n\n  generateTableCells(\n    'tableCell',\n    cellContents,\n    parser.lineNumber,\n    parser.nextNonspace + 1 + offset\n  ).forEach((cellNode, idx) => {\n    if (idx >= table.columns.length) {\n      cellNode.ignored = true;\n    }\n    tableRow.appendChild(cellNode);\n  });\n\n  return Matched.Leaf;\n};\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/gfm/taskListItem.ts",
    "content": "import { Parser } from '../blocks';\nimport { ListNode, BlockNode } from '../node';\n\nconst reTaskListItemMarker = /^\\[([ \\txX])\\][ \\t]+/;\n\n// finalize for block handler\nexport function taskListItemFinalize(_: Parser, block: ListNode) {\n  if (block.firstChild && block.firstChild.type === 'paragraph') {\n    const p = block.firstChild as BlockNode;\n    const m = p.stringContent!.match(reTaskListItemMarker);\n    if (m) {\n      const mLen = m[0].length;\n      p.stringContent = p.stringContent!.substring(mLen - 1);\n      p.sourcepos![0][1] += mLen;\n      p.lineOffsets![0] += mLen;\n      block.listData!.task = true;\n      block.listData!.checked = /[xX]/.test(m[1]);\n    }\n  }\n}\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/inlines.ts",
    "content": "import { InlineNodeType, Sourcepos, CustomBlockMdNode } from '@t/node';\nimport { RefMap, RefLinkCandidateMap, RefDefCandidateMap, ParserOptions } from '@t/parser';\nimport { Node, BlockNode, isHeading, LinkNode, createNode, text, CustomInlineNode } from './node';\nimport { repeat, normalizeURI, unescapeString, ESCAPABLE, ENTITY } from './common';\nimport { reHtmlTag } from './rawHtml';\nimport fromCodePoint from './from-code-point';\nimport { decodeHTML } from 'entities';\nimport NodeWalker from './nodeWalker';\nimport { convertExtAutoLinks } from './gfm/autoLinks';\nimport { last, normalizeReference } from '../helper';\nimport { createRefDefState } from '../toastmark';\n\nexport const C_NEWLINE = 10;\nconst C_ASTERISK = 42;\nconst C_UNDERSCORE = 95;\nconst C_BACKTICK = 96;\nconst C_OPEN_BRACKET = 91;\nconst C_CLOSE_BRACKET = 93;\nconst C_TILDE = 126;\nconst C_LESSTHAN = 60;\nconst C_BANG = 33;\nconst C_BACKSLASH = 92;\nconst C_AMPERSAND = 38;\nconst C_OPEN_PAREN = 40;\nconst C_CLOSE_PAREN = 41;\nconst C_COLON = 58;\nconst C_SINGLEQUOTE = 39;\nconst C_DOUBLEQUOTE = 34;\nconst C_DOLLAR = 36;\n\n// Some regexps used in inline parser:\nconst ESCAPED_CHAR = `\\\\\\\\${ESCAPABLE}`;\n\nconst rePunctuation = new RegExp(\n  /[!\"#$%&'()*+,\\-./:;<=>?@\\[\\]\\\\^_`{|}~\\xA1\\xA7\\xAB\\xB6\\xB7\\xBB\\xBF\\u037E\\u0387\\u055A-\\u055F\\u0589\\u058A\\u05BE\\u05C0\\u05C3\\u05C6\\u05F3\\u05F4\\u0609\\u060A\\u060C\\u060D\\u061B\\u061E\\u061F\\u066A-\\u066D\\u06D4\\u0700-\\u070D\\u07F7-\\u07F9\\u0830-\\u083E\\u085E\\u0964\\u0965\\u0970\\u0AF0\\u0DF4\\u0E4F\\u0E5A\\u0E5B\\u0F04-\\u0F12\\u0F14\\u0F3A-\\u0F3D\\u0F85\\u0FD0-\\u0FD4\\u0FD9\\u0FDA\\u104A-\\u104F\\u10FB\\u1360-\\u1368\\u1400\\u166D\\u166E\\u169B\\u169C\\u16EB-\\u16ED\\u1735\\u1736\\u17D4-\\u17D6\\u17D8-\\u17DA\\u1800-\\u180A\\u1944\\u1945\\u1A1E\\u1A1F\\u1AA0-\\u1AA6\\u1AA8-\\u1AAD\\u1B5A-\\u1B60\\u1BFC-\\u1BFF\\u1C3B-\\u1C3F\\u1C7E\\u1C7F\\u1CC0-\\u1CC7\\u1CD3\\u2010-\\u2027\\u2030-\\u2043\\u2045-\\u2051\\u2053-\\u205E\\u207D\\u207E\\u208D\\u208E\\u2308-\\u230B\\u2329\\u232A\\u2768-\\u2775\\u27C5\\u27C6\\u27E6-\\u27EF\\u2983-\\u2998\\u29D8-\\u29DB\\u29FC\\u29FD\\u2CF9-\\u2CFC\\u2CFE\\u2CFF\\u2D70\\u2E00-\\u2E2E\\u2E30-\\u2E42\\u3001-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u3030\\u303D\\u30A0\\u30FB\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA673\\uA67E\\uA6F2-\\uA6F7\\uA874-\\uA877\\uA8CE\\uA8CF\\uA8F8-\\uA8FA\\uA8FC\\uA92E\\uA92F\\uA95F\\uA9C1-\\uA9CD\\uA9DE\\uA9DF\\uAA5C-\\uAA5F\\uAADE\\uAADF\\uAAF0\\uAAF1\\uABEB\\uFD3E\\uFD3F\\uFE10-\\uFE19\\uFE30-\\uFE52\\uFE54-\\uFE61\\uFE63\\uFE68\\uFE6A\\uFE6B\\uFF01-\\uFF03\\uFF05-\\uFF0A\\uFF0C-\\uFF0F\\uFF1A\\uFF1B\\uFF1F\\uFF20\\uFF3B-\\uFF3D\\uFF3F\\uFF5B\\uFF5D\\uFF5F-\\uFF65]|\\uD800[\\uDD00-\\uDD02\\uDF9F\\uDFD0]|\\uD801\\uDD6F|\\uD802[\\uDC57\\uDD1F\\uDD3F\\uDE50-\\uDE58\\uDE7F\\uDEF0-\\uDEF6\\uDF39-\\uDF3F\\uDF99-\\uDF9C]|\\uD804[\\uDC47-\\uDC4D\\uDCBB\\uDCBC\\uDCBE-\\uDCC1\\uDD40-\\uDD43\\uDD74\\uDD75\\uDDC5-\\uDDC9\\uDDCD\\uDDDB\\uDDDD-\\uDDDF\\uDE38-\\uDE3D\\uDEA9]|\\uD805[\\uDCC6\\uDDC1-\\uDDD7\\uDE41-\\uDE43\\uDF3C-\\uDF3E]|\\uD809[\\uDC70-\\uDC74]|\\uD81A[\\uDE6E\\uDE6F\\uDEF5\\uDF37-\\uDF3B\\uDF44]|\\uD82F\\uDC9F|\\uD836[\\uDE87-\\uDE8B]/\n);\n\nconst reLinkTitle = new RegExp(\n  `^(?:\"(${ESCAPED_CHAR}|[^\"\\\\x00])*\"` +\n    `|` +\n    `'(${ESCAPED_CHAR}|[^'\\\\x00])*'` +\n    `|` +\n    `\\\\((${ESCAPED_CHAR}|[^()\\\\x00])*\\\\))`\n);\n\nconst reLinkDestinationBraces = /^(?:<(?:[^<>\\n\\\\\\x00]|\\\\.)*>)/;\nconst reEscapable = new RegExp(`^${ESCAPABLE}`);\nconst reEntityHere = new RegExp(`^${ENTITY}`, 'i');\nconst reTicks = /`+/;\nconst reTicksHere = /^`+/;\nconst reEllipses = /\\.\\.\\./g;\nconst reDash = /--+/g;\nconst reEmailAutolink = /^<([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/;\nconst reAutolink = /^<[A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\\x00-\\x20]*>/i;\nconst reSpnl = /^ *(?:\\n *)?/;\nconst reWhitespaceChar = /^[ \\t\\n\\x0b\\x0c\\x0d]/;\nconst reUnicodeWhitespaceChar = /^\\s/;\nconst reFinalSpace = / *$/;\nconst reInitialSpace = /^ */;\nconst reSpaceAtEndOfLine = /^ *(?:\\n|$)/;\nconst reLinkLabel = /^\\[(?:[^\\\\\\[\\]]|\\\\.){0,1000}\\]/;\n\n// Matches a string of non-special characters.\nconst reMain = /^[^\\n`\\[\\]\\\\!<&*_'\"~$]+/m;\n\ntype DelimiterCC =\n  | typeof C_ASTERISK\n  | typeof C_UNDERSCORE\n  | typeof C_SINGLEQUOTE\n  | typeof C_DOUBLEQUOTE\n  | typeof C_TILDE\n  | typeof C_DOLLAR;\n\ntype Delimiter = {\n  cc: DelimiterCC;\n  numdelims: number;\n  origdelims: number;\n  node: Node;\n  previous: Delimiter | null;\n  next: Delimiter | null;\n  canOpen: boolean;\n  canClose: boolean;\n};\n\ntype Bracket = {\n  node: Node;\n  previous: Bracket | null;\n  previousDelimiter: Delimiter | null;\n  index: number;\n  image: boolean;\n  active: boolean;\n  bracketAfter?: boolean;\n  startpos: [number, number];\n};\n\nexport class InlineParser {\n  // An InlineParser keeps track of a subject (a string to be parsed)\n  // and a position in that subject.\n  private subject = '';\n  private delimiters: Delimiter | null = null; // used by handleDelim method\n  private brackets: Bracket | null = null;\n  private pos = 0;\n  private lineStartNum = 0;\n  private lineIdx = 0;\n  private lineOffsets: number[] = [0];\n  private linePosOffset = 0;\n  public refMap: RefMap = {};\n  public refLinkCandidateMap: RefLinkCandidateMap = {};\n  public refDefCandidateMap: RefDefCandidateMap = {};\n  public options: ParserOptions;\n\n  constructor(options: ParserOptions) {\n    this.options = options;\n  }\n\n  sourcepos(start: number): [number, number];\n  sourcepos(start: number, end: number): Sourcepos;\n  sourcepos(start: number, end?: number): [number, number] | Sourcepos {\n    const linePosOffset = this.linePosOffset + this.lineOffsets[this.lineIdx];\n    const lineNum = this.lineStartNum + this.lineIdx;\n    const startpos = [lineNum, start + linePosOffset];\n\n    if (typeof end === 'number') {\n      return [startpos, [lineNum, end + linePosOffset]] as Sourcepos;\n    }\n    return startpos as [number, number];\n  }\n\n  nextLine() {\n    this.lineIdx += 1;\n    this.linePosOffset = -this.pos;\n  }\n\n  // If re matches at current position in the subject, advance\n  // position in subject and return the match; otherwise return null.\n  match(re: RegExp) {\n    const m = re.exec(this.subject.slice(this.pos));\n    if (m === null) {\n      return null;\n    }\n    this.pos += m.index + m[0].length;\n    return m[0];\n  }\n\n  // Returns the code for the character at the current subject position, or -1\n  // there are no more characters.\n  peek() {\n    if (this.pos < this.subject.length) {\n      return this.subject.charCodeAt(this.pos);\n    }\n    return -1;\n  }\n\n  // Parse zero or more space characters, including at most one newline\n  spnl() {\n    this.match(reSpnl);\n    return true;\n  }\n\n  // All of the parsers below try to match something at the current position\n  // in the subject.  If they succeed in matching anything, they\n  // return the inline matched, advancing the subject.\n\n  // Attempt to parse backticks, adding either a backtick code span or a\n  // literal sequence of backticks.\n  parseBackticks(block: BlockNode) {\n    const startpos = this.pos + 1;\n    const ticks = this.match(reTicksHere);\n    if (ticks === null) {\n      return false;\n    }\n    const afterOpenTicks = this.pos;\n    let matched: string | null;\n    while ((matched = this.match(reTicks)) !== null) {\n      if (matched === ticks) {\n        let contents = this.subject.slice(afterOpenTicks, this.pos - ticks.length);\n        const sourcepos = this.sourcepos(startpos, this.pos);\n        const lines = contents.split('\\n');\n        if (lines.length > 1) {\n          const lastLine = last(lines);\n          this.lineIdx += lines.length - 1;\n          this.linePosOffset = -(this.pos - lastLine.length - ticks.length);\n          sourcepos[1] = this.sourcepos(this.pos);\n          contents = lines.join(' ');\n        }\n        const node = createNode('code', sourcepos);\n\n        if (\n          contents.length > 0 &&\n          contents.match(/[^ ]/) !== null &&\n          contents[0] == ' ' &&\n          contents[contents.length - 1] == ' '\n        ) {\n          node.literal = contents.slice(1, contents.length - 1);\n        } else {\n          node.literal = contents;\n        }\n        node.tickCount = ticks.length;\n        block.appendChild(node);\n        return true;\n      }\n    }\n    // If we got here, we didn't match a closing backtick sequence.\n    this.pos = afterOpenTicks;\n    block.appendChild(text(ticks, this.sourcepos(startpos, this.pos - 1)));\n    return true;\n  }\n\n  // Parse a backslash-escaped special character, adding either the escaped\n  // character, a hard line break (if the backslash is followed by a newline),\n  // or a literal backslash to the block's children.  Assumes current character\n  // is a backslash.\n  parseBackslash(block: BlockNode) {\n    const subj = this.subject;\n    let node: Node;\n    this.pos += 1;\n    const startpos = this.pos;\n\n    if (this.peek() === C_NEWLINE) {\n      this.pos += 1;\n      node = createNode('linebreak', this.sourcepos(this.pos - 1, this.pos));\n      block.appendChild(node);\n      this.nextLine();\n    } else if (reEscapable.test(subj.charAt(this.pos))) {\n      block.appendChild(text(subj.charAt(this.pos), this.sourcepos(startpos, this.pos)));\n      this.pos += 1;\n    } else {\n      block.appendChild(text('\\\\', this.sourcepos(startpos, startpos)));\n    }\n    return true;\n  }\n\n  // Attempt to parse an autolink (URL or email in pointy brackets).\n  parseAutolink(block: BlockNode) {\n    let m: string | null;\n    let dest: string;\n    let node: LinkNode;\n    const startpos = this.pos + 1;\n    if ((m = this.match(reEmailAutolink))) {\n      dest = m.slice(1, m.length - 1);\n      node = createNode('link', this.sourcepos(startpos, this.pos));\n      node.destination = normalizeURI(`mailto:${dest}`);\n      node.title = '';\n      node.appendChild(text(dest, this.sourcepos(startpos + 1, this.pos - 1)));\n      block.appendChild(node);\n      return true;\n    }\n    if ((m = this.match(reAutolink))) {\n      dest = m.slice(1, m.length - 1);\n      node = createNode('link', this.sourcepos(startpos, this.pos));\n      node.destination = normalizeURI(dest);\n      node.title = '';\n      node.appendChild(text(dest, this.sourcepos(startpos + 1, this.pos - 1)));\n      block.appendChild(node);\n      return true;\n    }\n    return false;\n  }\n\n  // Attempt to parse a raw HTML tag.\n  parseHtmlTag(block: BlockNode) {\n    const startpos = this.pos + 1;\n    const m = this.match(reHtmlTag);\n\n    if (m === null) {\n      return false;\n    }\n    const node = createNode('htmlInline', this.sourcepos(startpos, this.pos));\n    node.literal = m;\n    block.appendChild(node);\n    return true;\n  }\n\n  // Scan a sequence of characters with code cc, and return information about\n  // the number of delimiters and whether they are positioned such that\n  // they can open and/or close emphasis or strong emphasis.  A utility\n  // function for strong/emph parsing.\n  scanDelims(cc: number) {\n    let numdelims = 0;\n    const startpos = this.pos;\n\n    if (cc === C_SINGLEQUOTE || cc === C_DOUBLEQUOTE) {\n      numdelims++;\n      this.pos++;\n    } else {\n      while (this.peek() === cc) {\n        numdelims++;\n        this.pos++;\n      }\n    }\n\n    if (numdelims === 0 || (numdelims < 2 && (cc === C_TILDE || cc === C_DOLLAR))) {\n      this.pos = startpos;\n      return null;\n    }\n    const charBefore = startpos === 0 ? '\\n' : this.subject.charAt(startpos - 1);\n    const ccAfter = this.peek();\n    let charAfter: string;\n    if (ccAfter === -1) {\n      charAfter = '\\n';\n    } else {\n      charAfter = fromCodePoint(ccAfter);\n    }\n\n    const afterIsWhitespace = reUnicodeWhitespaceChar.test(charAfter);\n    const afterIsPunctuation = rePunctuation.test(charAfter);\n    const beforeIsWhitespace = reUnicodeWhitespaceChar.test(charBefore);\n    const beforeIsPunctuation = rePunctuation.test(charBefore);\n    const leftFlanking =\n      !afterIsWhitespace && (!afterIsPunctuation || beforeIsWhitespace || beforeIsPunctuation);\n    const rightFlanking =\n      !beforeIsWhitespace && (!beforeIsPunctuation || afterIsWhitespace || afterIsPunctuation);\n    let canOpen: boolean;\n    let canClose: boolean;\n\n    if (cc === C_UNDERSCORE) {\n      canOpen = leftFlanking && (!rightFlanking || beforeIsPunctuation);\n      canClose = rightFlanking && (!leftFlanking || afterIsPunctuation);\n    } else if (cc === C_SINGLEQUOTE || cc === C_DOUBLEQUOTE) {\n      canOpen = leftFlanking && !rightFlanking;\n      canClose = rightFlanking;\n    } else if (cc === C_DOLLAR) {\n      canOpen = !afterIsWhitespace;\n      canClose = !beforeIsWhitespace;\n    } else {\n      canOpen = leftFlanking;\n      canClose = rightFlanking;\n    }\n    this.pos = startpos;\n    return { numdelims, canOpen, canClose };\n  }\n\n  // Handle a delimiter marker for emphasis or a quote.\n  handleDelim(cc: DelimiterCC, block: BlockNode) {\n    const res = this.scanDelims(cc);\n    if (!res) {\n      return false;\n    }\n    const numdelims = res.numdelims;\n    const startpos = this.pos + 1;\n    let contents: string;\n\n    this.pos += numdelims;\n    if (cc === C_SINGLEQUOTE) {\n      contents = '\\u2019';\n    } else if (cc === C_DOUBLEQUOTE) {\n      contents = '\\u201C';\n    } else {\n      contents = this.subject.slice(startpos - 1, this.pos);\n    }\n\n    const node = text(contents, this.sourcepos(startpos, this.pos));\n    block.appendChild(node);\n\n    // Add entry to stack for this opener\n    if (\n      (res.canOpen || res.canClose) &&\n      (this.options.smart || (cc !== C_SINGLEQUOTE && cc !== C_DOUBLEQUOTE))\n    ) {\n      this.delimiters = {\n        cc,\n        numdelims,\n        origdelims: numdelims,\n        node,\n        previous: this.delimiters,\n        next: null,\n        canOpen: res.canOpen,\n        canClose: res.canClose,\n      };\n      if (this.delimiters.previous) {\n        this.delimiters.previous.next = this.delimiters;\n      }\n    }\n    return true;\n  }\n\n  removeDelimiter(delim: Delimiter) {\n    if (delim.previous !== null) {\n      delim.previous.next = delim.next;\n    }\n    if (delim.next === null) {\n      // top of stack\n      this.delimiters = delim.previous;\n    } else {\n      delim.next.previous = delim.previous;\n    }\n  }\n\n  removeDelimitersBetween(bottom: Delimiter, top: Delimiter) {\n    if (bottom.next !== top) {\n      bottom.next = top;\n      top.previous = bottom;\n    }\n  }\n\n  /**\n   * Process all delimiters - emphasis, strong emphasis, strikethrough(gfm)\n   * If the smart punctuation options is true,\n   * convert single/double quotes to corresponding unicode characters.\n   **/\n  processEmphasis(stackBottom: Delimiter | null) {\n    let opener: Delimiter | null;\n    let closer: Delimiter | null;\n    let oldCloser: Delimiter | null;\n    let openerInl: Node, closerInl: Node;\n    let openerFound: boolean;\n    let oddMatch = false;\n    const openersBottom = {\n      [C_UNDERSCORE]: [stackBottom, stackBottom, stackBottom],\n      [C_ASTERISK]: [stackBottom, stackBottom, stackBottom],\n      [C_SINGLEQUOTE]: [stackBottom],\n      [C_DOUBLEQUOTE]: [stackBottom],\n      [C_TILDE]: [stackBottom],\n      [C_DOLLAR]: [stackBottom],\n    };\n\n    // find first closer above stackBottom:\n    closer = this.delimiters;\n    while (closer !== null && closer.previous !== stackBottom) {\n      closer = closer.previous;\n    }\n    // move forward, looking for closers, and handling each\n    while (closer !== null) {\n      const closercc = closer.cc;\n      const closerEmph = closercc === C_UNDERSCORE || closercc === C_ASTERISK;\n      if (!closer.canClose) {\n        closer = closer.next;\n      } else {\n        // found emphasis closer. now look back for first matching opener:\n        opener = closer.previous;\n        openerFound = false;\n\n        while (\n          opener !== null &&\n          opener !== stackBottom &&\n          opener !== openersBottom[closercc][closerEmph ? closer.origdelims % 3 : 0]\n        ) {\n          oddMatch =\n            closerEmph &&\n            (closer.canOpen || opener.canClose) &&\n            closer.origdelims % 3 !== 0 &&\n            (opener.origdelims + closer.origdelims) % 3 === 0;\n\n          if (opener.cc === closer.cc && opener.canOpen && !oddMatch) {\n            openerFound = true;\n            break;\n          }\n          opener = opener.previous;\n        }\n        oldCloser = closer;\n\n        if (closerEmph || closercc === C_TILDE || closercc === C_DOLLAR) {\n          if (!openerFound) {\n            closer = closer.next;\n          } else if (opener) {\n            // (null opener check for type narrowing)\n            // calculate actual number of delimiters used from closer\n            const useDelims = closer.numdelims >= 2 && opener.numdelims >= 2 ? 2 : 1;\n            const emptyDelims = closerEmph ? 0 : 1;\n\n            openerInl = opener.node;\n            closerInl = closer.node;\n\n            // build contents for new emph element\n            let nodeType: InlineNodeType = closerEmph\n              ? useDelims === 1\n                ? 'emph'\n                : 'strong'\n              : 'strike';\n            if (closercc === C_DOLLAR) {\n              nodeType = 'customInline';\n            }\n            const newNode = createNode(nodeType);\n            const openerEndPos = openerInl.sourcepos![1];\n            const closerStartPos = closerInl.sourcepos![0];\n            newNode.sourcepos = [\n              [openerEndPos[0], openerEndPos[1] - useDelims + 1],\n              [closerStartPos[0], closerStartPos[1] + useDelims - 1],\n            ];\n            openerInl.sourcepos![1][1] -= useDelims;\n            closerInl.sourcepos![0][1] += useDelims;\n\n            openerInl.literal = openerInl.literal!.slice(useDelims);\n            closerInl.literal = closerInl.literal!.slice(useDelims);\n            opener.numdelims -= useDelims;\n            closer.numdelims -= useDelims;\n\n            // remove used delimiters from stack elts and inlines\n            let tmp = openerInl.next;\n            let next;\n            while (tmp && tmp !== closerInl) {\n              next = tmp.next;\n              tmp.unlink();\n              newNode.appendChild(tmp);\n              tmp = next;\n            }\n\n            // build custom inline node\n            if (closercc === C_DOLLAR) {\n              const textNode = newNode.firstChild!;\n              const literal = textNode.literal || '';\n              const [info] = literal.split(/\\s/)!;\n\n              (newNode as CustomInlineNode).info = info;\n              if (literal.length <= info.length) {\n                textNode.unlink();\n              } else {\n                textNode.sourcepos![0][1] += info.length;\n                textNode.literal = literal.replace(`${info} `, '');\n              }\n            }\n\n            openerInl.insertAfter(newNode);\n\n            // remove elts between opener and closer in delimiters stack\n            this.removeDelimitersBetween(opener, closer);\n\n            // if opener has 0 delims, remove it and the inline\n            // if opener has 1 delims and character is tilde, remove delimiter only\n            if (opener.numdelims <= emptyDelims) {\n              if (opener.numdelims === 0) {\n                openerInl.unlink();\n              }\n              this.removeDelimiter(opener);\n            }\n\n            // if closer has 0 delims, remove it and the inline\n            // if closer has 1 delims and character is tilde, remove delimiter only\n            if (closer.numdelims <= emptyDelims) {\n              if (closer.numdelims === 0) {\n                closerInl.unlink();\n              }\n              const tempstack = closer.next;\n              this.removeDelimiter(closer);\n              closer = tempstack;\n            }\n          }\n        } else if (closercc === C_SINGLEQUOTE) {\n          closer.node.literal = '\\u2019';\n          if (openerFound) {\n            opener!.node.literal = '\\u2018';\n          }\n          closer = closer.next;\n        } else if (closercc === C_DOUBLEQUOTE) {\n          closer.node.literal = '\\u201D';\n          if (openerFound) {\n            opener!.node.literal = '\\u201C';\n          }\n          closer = closer.next;\n        }\n\n        if (!openerFound) {\n          // Set lower bound for future searches for openers:\n          openersBottom[closercc][closerEmph ? oldCloser.origdelims % 3 : 0] = oldCloser.previous;\n          if (!oldCloser.canOpen) {\n            // We can remove a closer that can't be an opener,\n            // once we've seen there's no matching opener:\n            this.removeDelimiter(oldCloser);\n          }\n        }\n      }\n    }\n\n    // remove all delimiters\n    while (this.delimiters !== null && this.delimiters !== stackBottom) {\n      this.removeDelimiter(this.delimiters);\n    }\n  }\n\n  // Attempt to parse link title (sans quotes), returning the string\n  // or null if no match.\n  parseLinkTitle() {\n    const title = this.match(reLinkTitle);\n    if (title === null) {\n      return null;\n    }\n    // chop off quotes from title and unescape:\n    return unescapeString(title.substr(1, title.length - 2));\n  }\n\n  // Attempt to parse link destination, returning the string or null if no match.\n  parseLinkDestination() {\n    let res = this.match(reLinkDestinationBraces);\n    if (res === null) {\n      if (this.peek() === C_LESSTHAN) {\n        return null;\n      }\n      // @TODO handrolled parser; res should be null or the string\n      const savepos = this.pos;\n      let openparens = 0;\n      let c: number;\n      while ((c = this.peek()) !== -1) {\n        if (c === C_BACKSLASH && reEscapable.test(this.subject.charAt(this.pos + 1))) {\n          this.pos += 1;\n          if (this.peek() !== -1) {\n            this.pos += 1;\n          }\n        } else if (c === C_OPEN_PAREN) {\n          this.pos += 1;\n          openparens += 1;\n        } else if (c === C_CLOSE_PAREN) {\n          if (openparens < 1) {\n            break;\n          } else {\n            this.pos += 1;\n            openparens -= 1;\n          }\n        } else if (reWhitespaceChar.exec(fromCodePoint(c)) !== null) {\n          break;\n        } else {\n          this.pos += 1;\n        }\n      }\n      if (this.pos === savepos && c !== C_CLOSE_PAREN) {\n        return null;\n      }\n      if (openparens !== 0) {\n        return null;\n      }\n      res = this.subject.substr(savepos, this.pos - savepos);\n      return normalizeURI(unescapeString(res));\n    } // chop off surrounding <..>:\n    return normalizeURI(unescapeString(res.substr(1, res.length - 2)));\n  }\n\n  // Attempt to parse a link label, returning number of characters parsed.\n  parseLinkLabel() {\n    const m = this.match(reLinkLabel);\n    if (m === null || m.length > 1001) {\n      return 0;\n    }\n    return m.length;\n  }\n\n  // Add open bracket to delimiter stack and add a text node to block's children.\n  parseOpenBracket(block: BlockNode) {\n    const startpos = this.pos;\n    this.pos += 1;\n\n    const node = text('[', this.sourcepos(this.pos, this.pos));\n    block.appendChild(node);\n\n    // Add entry to stack for this opener\n    this.addBracket(node, startpos, false);\n    return true;\n  }\n\n  // IF next character is [, and ! delimiter to delimiter stack and\n  // add a text node to block's children.  Otherwise just add a text node.\n  parseBang(block: BlockNode) {\n    const startpos = this.pos;\n    this.pos += 1;\n    if (this.peek() === C_OPEN_BRACKET) {\n      this.pos += 1;\n\n      const node = text('![', this.sourcepos(this.pos - 1, this.pos));\n      block.appendChild(node);\n\n      // Add entry to stack for this opener\n      this.addBracket(node, startpos + 1, true);\n    } else {\n      const node = text('!', this.sourcepos(this.pos, this.pos));\n      block.appendChild(node);\n    }\n    return true;\n  }\n\n  // Try to match close bracket against an opening in the delimiter\n  // stack.  Add either a link or image, or a plain [ character,\n  // to block's children.  If there is a matching delimiter,\n  // remove it from the delimiter stack.\n  parseCloseBracket(block: BlockNode) {\n    let dest: string | null = null;\n    let title: string | null = null;\n    let matched = false;\n\n    this.pos += 1;\n    const startpos = this.pos;\n    // get last [ or ![\n    let opener = this.brackets;\n\n    if (opener === null) {\n      // no matched opener, just return a literal\n      block.appendChild(text(']', this.sourcepos(startpos, startpos)));\n      return true;\n    }\n\n    if (!opener.active) {\n      // no matched opener, just return a literal\n      block.appendChild(text(']', this.sourcepos(startpos, startpos)));\n      // take opener off brackets stack\n      this.removeBracket();\n      return true;\n    }\n\n    // If we got here, open is a potential opener\n    const isImage = opener.image;\n\n    // Check to see if we have a link/image\n    const savepos = this.pos;\n\n    // Inline link?\n    if (this.peek() === C_OPEN_PAREN) {\n      this.pos++;\n      if (\n        this.spnl() &&\n        (dest = this.parseLinkDestination()) !== null &&\n        this.spnl() &&\n        // make sure there's a space before the title:\n        ((reWhitespaceChar.test(this.subject.charAt(this.pos - 1)) &&\n          (title = this.parseLinkTitle())) ||\n          true) &&\n        this.spnl() &&\n        this.peek() === C_CLOSE_PAREN\n      ) {\n        this.pos += 1;\n        matched = true;\n      } else {\n        this.pos = savepos;\n      }\n    }\n\n    let refLabel = '';\n\n    if (!matched) {\n      // Next, see if there's a link label\n      const beforelabel = this.pos;\n      const n = this.parseLinkLabel();\n      if (n > 2) {\n        refLabel = this.subject.slice(beforelabel, beforelabel + n);\n      } else if (!opener.bracketAfter) {\n        // Empty or missing second label means to use the first label as the reference.\n        // The reference must not contain a bracket. If we know there's a bracket, we don't even bother checking it.\n        refLabel = this.subject.slice(opener.index, startpos);\n      }\n      if (n === 0) {\n        // If shortcut reference link, rewind before spaces we skipped.\n        this.pos = savepos;\n      }\n\n      if (refLabel) {\n        refLabel = normalizeReference(refLabel);\n        // lookup rawlabel in refMap\n        const link = this.refMap[refLabel];\n        if (link) {\n          dest = link.destination;\n          title = link.title;\n          matched = true;\n        }\n      }\n    }\n\n    if (matched) {\n      const node = createNode(isImage ? 'image' : 'link');\n      node.destination = dest;\n      node.title = title || '';\n      node.sourcepos = [opener.startpos, this.sourcepos(this.pos)];\n\n      let tmp = opener.node.next;\n      let next: Node | null;\n      while (tmp) {\n        next = tmp.next;\n        tmp.unlink();\n        node.appendChild(tmp);\n        tmp = next;\n      }\n      block.appendChild(node);\n      this.processEmphasis(opener.previousDelimiter);\n      this.removeBracket();\n      opener.node.unlink();\n\n      // We remove this bracket and processEmphasis will remove later delimiters.\n      // Now, for a link, we also deactivate earlier link openers.\n      // (no links in links)\n      if (!isImage) {\n        opener = this.brackets;\n        while (opener !== null) {\n          if (!opener.image) {\n            opener.active = false; // deactivate this opener\n          }\n          opener = opener.previous;\n        }\n      }\n\n      if (this.options.referenceDefinition) {\n        this.refLinkCandidateMap[block.id] = { node: block, refLabel };\n      }\n      return true;\n    } // no match\n\n    this.removeBracket(); // remove this opener from stack\n    this.pos = startpos;\n    block.appendChild(text(']', this.sourcepos(startpos, startpos)));\n\n    if (this.options.referenceDefinition) {\n      this.refLinkCandidateMap[block.id] = { node: block, refLabel };\n    }\n    return true;\n  }\n\n  addBracket(node: Node, index: number, image: boolean) {\n    if (this.brackets !== null) {\n      this.brackets.bracketAfter = true;\n    }\n    this.brackets = {\n      node,\n      startpos: this.sourcepos(index + (image ? 0 : 1)),\n      previous: this.brackets,\n      previousDelimiter: this.delimiters,\n      index,\n      image,\n      active: true,\n    };\n  }\n\n  removeBracket() {\n    if (this.brackets) {\n      this.brackets = this.brackets.previous;\n    }\n  }\n\n  // Attempt to parse an entity.\n  parseEntity(block: BlockNode) {\n    let m;\n    const startpos = this.pos + 1;\n    if ((m = this.match(reEntityHere))) {\n      block.appendChild(text(decodeHTML(m), this.sourcepos(startpos, this.pos)));\n      return true;\n    }\n    return false;\n  }\n\n  // Parse a run of ordinary characters, or a single character with\n  // a special meaning in markdown, as a plain string.\n  parseString(block: BlockNode) {\n    let m;\n    const startpos = this.pos + 1;\n\n    if ((m = this.match(reMain))) {\n      if (this.options.smart) {\n        const lit = m.replace(reEllipses, '\\u2026').replace(reDash, function (chars) {\n          let enCount = 0;\n          let emCount = 0;\n          if (chars.length % 3 === 0) {\n            // If divisible by 3, use all em dashes\n            emCount = chars.length / 3;\n          } else if (chars.length % 2 === 0) {\n            // If divisible by 2, use all en dashes\n            enCount = chars.length / 2;\n          } else if (chars.length % 3 === 2) {\n            // If 2 extra dashes, use en dash for last 2; em dashes for rest\n            enCount = 1;\n            emCount = (chars.length - 2) / 3;\n          } else {\n            // Use en dashes for last 4 hyphens; em dashes for rest\n            enCount = 2;\n            emCount = (chars.length - 4) / 3;\n          }\n          return repeat('\\u2014', emCount) + repeat('\\u2013', enCount);\n        });\n        block.appendChild(text(lit, this.sourcepos(startpos, this.pos)));\n      } else {\n        const node = text(m, this.sourcepos(startpos, this.pos));\n        block.appendChild(node);\n      }\n      return true;\n    }\n    return false;\n  }\n\n  // Parse a newline.  If it was preceded by two spaces, return a hard\n  // line break; otherwise a soft line break.\n  parseNewline(block: BlockNode) {\n    this.pos += 1; // assume we're at a \\n\n\n    // check previous node for trailing spaces\n    const lastc = block.lastChild;\n    if (lastc && lastc.type === 'text' && lastc.literal![lastc.literal!.length - 1] === ' ') {\n      const hardbreak = lastc.literal![lastc.literal!.length - 2] === ' ';\n      const litLen = lastc.literal!.length;\n      lastc.literal = lastc.literal!.replace(reFinalSpace, '');\n      const finalSpaceLen = litLen - lastc.literal.length;\n      lastc.sourcepos![1][1] -= finalSpaceLen;\n\n      block.appendChild(\n        createNode(\n          hardbreak ? 'linebreak' : 'softbreak',\n          this.sourcepos(this.pos - finalSpaceLen, this.pos)\n        )\n      );\n    } else {\n      block.appendChild(createNode('softbreak', this.sourcepos(this.pos, this.pos)));\n    }\n    this.nextLine();\n    this.match(reInitialSpace); // gobble leading spaces in next line\n    return true;\n  }\n\n  // Attempt to parse a link reference, modifying refmap.\n  parseReference(block: BlockNode, refMap: RefMap) {\n    if (!this.options.referenceDefinition) {\n      return 0;\n    }\n\n    this.subject = block.stringContent!;\n    this.pos = 0;\n    let title = null;\n    const startpos = this.pos;\n\n    // label:\n    const matchChars = this.parseLinkLabel();\n    if (matchChars === 0) {\n      return 0;\n    }\n    const rawlabel = this.subject.substr(0, matchChars);\n\n    // colon:\n    if (this.peek() === C_COLON) {\n      this.pos++;\n    } else {\n      this.pos = startpos;\n      return 0;\n    }\n\n    //  link url\n    this.spnl();\n\n    const dest = this.parseLinkDestination();\n    if (dest === null) {\n      this.pos = startpos;\n      return 0;\n    }\n\n    const beforetitle = this.pos;\n    this.spnl();\n    if (this.pos !== beforetitle) {\n      title = this.parseLinkTitle();\n    }\n    if (title === null) {\n      title = '';\n      // rewind before spaces\n      this.pos = beforetitle;\n    }\n\n    // make sure we're at line end:\n    let atLineEnd = true;\n    if (this.match(reSpaceAtEndOfLine) === null) {\n      if (title === '') {\n        atLineEnd = false;\n      } else {\n        // the potential title we found is not at the line end,\n        // but it could still be a legal link reference if we\n        // discard the title\n        title = '';\n        // rewind before spaces\n        this.pos = beforetitle;\n        // and instead check if the link URL is at the line end\n        atLineEnd = this.match(reSpaceAtEndOfLine) !== null;\n      }\n    }\n\n    if (!atLineEnd) {\n      this.pos = startpos;\n      return 0;\n    }\n\n    const normalLabel = normalizeReference(rawlabel);\n    if (normalLabel === '') {\n      // label must contain non-whitespace characters\n      this.pos = startpos;\n      return 0;\n    }\n\n    const sourcepos = this.getReferenceDefSourcepos(block);\n    block.sourcepos![0][0] = sourcepos[1][0] + 1;\n\n    const node = createNode('refDef', sourcepos);\n    node.title = title;\n    node.dest = dest;\n    node.label = normalLabel;\n\n    block.insertBefore(node);\n\n    if (!refMap[normalLabel]) {\n      refMap[normalLabel] = createRefDefState(node);\n    } else {\n      this.refDefCandidateMap[node.id] = node;\n    }\n\n    return this.pos - startpos;\n  }\n\n  mergeTextNodes(walker: NodeWalker) {\n    let event;\n    let textNodes: Node[] = [];\n\n    while ((event = walker.next())) {\n      const { entering, node } = event;\n      if (entering && node.type === 'text') {\n        textNodes.push(node);\n      } else if (textNodes.length === 1) {\n        textNodes = [];\n      } else if (textNodes.length > 1) {\n        const firstNode = textNodes[0];\n        const lastNode = textNodes[textNodes.length - 1];\n        if (firstNode.sourcepos && lastNode.sourcepos) {\n          firstNode.sourcepos![1] = lastNode.sourcepos![1];\n        }\n        firstNode.next = lastNode.next;\n        if (firstNode.next) {\n          firstNode.next.prev = firstNode;\n        }\n\n        for (let i = 1; i < textNodes.length; i += 1) {\n          firstNode.literal! += textNodes[i].literal;\n          textNodes[i].unlink();\n        }\n        textNodes = [];\n      }\n    }\n  }\n\n  getReferenceDefSourcepos(block: BlockNode): Sourcepos {\n    const lines = block.stringContent!.split(/\\n|\\r\\n/);\n    let passedUrlLine = false;\n    let quotationCount = 0;\n    let lastLineOffset = { line: 0, ch: 0 };\n\n    for (let i = 0; i < lines.length; i += 1) {\n      const line = lines[i];\n\n      if (reWhitespaceChar.test(line)) {\n        break;\n      }\n      if (/\\:/.test(line) && quotationCount === 0) {\n        if (passedUrlLine) {\n          break;\n        }\n        const lineOffset = line.indexOf(':') === line.length - 1 ? i + 1 : i;\n        lastLineOffset = { line: lineOffset, ch: lines[lineOffset].length };\n        passedUrlLine = true;\n      }\n      // should consider extendable title\n      const matched = line.match(/'|\"/g);\n      if (matched) {\n        quotationCount += matched.length;\n      }\n      if (quotationCount === 2) {\n        lastLineOffset = { line: i, ch: line.length };\n        break;\n      }\n    }\n    return [\n      [block.sourcepos![0][0], block.sourcepos![0][1]],\n      [block.sourcepos![0][0] + lastLineOffset.line, lastLineOffset.ch],\n    ];\n  }\n\n  // Parse the next inline element in subject, advancing subject position.\n  // On success, add the result to block's children and return true.\n  // On failure, return false.\n  parseInline(block: BlockNode) {\n    let res = false;\n    const c = this.peek();\n    if (c === -1) {\n      return false;\n    }\n\n    switch (c) {\n      case C_NEWLINE:\n        res = this.parseNewline(block);\n        break;\n      case C_BACKSLASH:\n        res = this.parseBackslash(block);\n        break;\n      case C_BACKTICK:\n        res = this.parseBackticks(block);\n        break;\n      case C_ASTERISK:\n      case C_UNDERSCORE:\n      case C_TILDE:\n      case C_DOLLAR:\n        res = this.handleDelim(c, block);\n        break;\n      case C_SINGLEQUOTE:\n      case C_DOUBLEQUOTE:\n        res = !!this.options?.smart && this.handleDelim(c, block);\n        break;\n      case C_OPEN_BRACKET:\n        res = this.parseOpenBracket(block);\n        break;\n      case C_BANG:\n        res = this.parseBang(block);\n        break;\n      case C_CLOSE_BRACKET:\n        res = this.parseCloseBracket(block);\n        break;\n      case C_LESSTHAN:\n        res = this.parseAutolink(block) || this.parseHtmlTag(block);\n        break;\n      case C_AMPERSAND:\n        if (!(block as CustomBlockMdNode).disabledEntityParse) {\n          res = this.parseEntity(block);\n        }\n        break;\n      default:\n        res = this.parseString(block);\n        break;\n    }\n\n    if (!res) {\n      this.pos += 1;\n      block.appendChild(text(fromCodePoint(c), this.sourcepos(this.pos, this.pos + 1)));\n    }\n\n    return true;\n  }\n\n  // Parse string content in block into inline children,\n  // using refmap to resolve references.\n  parse(block: BlockNode) {\n    this.subject = block.stringContent!.trim();\n    this.pos = 0;\n    this.delimiters = null;\n    this.brackets = null;\n    this.lineOffsets = block.lineOffsets || [0];\n    this.lineIdx = 0;\n    this.linePosOffset = 0;\n    this.lineStartNum = block.sourcepos![0][0];\n    if (isHeading(block)) {\n      this.lineOffsets[0] += block.level + 1;\n    }\n\n    while (this.parseInline(block)) {}\n    block.stringContent = null; // allow raw string to be garbage collected\n    this.processEmphasis(null);\n    this.mergeTextNodes(block.walker());\n\n    const { extendedAutolinks, customParser } = this.options;\n    if (extendedAutolinks) {\n      convertExtAutoLinks(block.walker(), extendedAutolinks);\n    }\n    if (customParser && block.firstChild) {\n      let event;\n      const walker = block.firstChild.walker();\n\n      while ((event = walker.next())) {\n        const { node, entering } = event;\n        if (customParser[node.type]) {\n          customParser[node.type]!(node, { entering, options: this.options });\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/node.ts",
    "content": "import {\n  BlockMdNode,\n  BlockNodeType,\n  CodeBlockMdNode,\n  CodeMdNode,\n  CustomBlockMdNode,\n  CustomInlineMdNode,\n  HeadingMdNode,\n  HtmlBlockMdNode,\n  LinkMdNode,\n  ListData,\n  ListMdNode,\n  MdNode,\n  MdNodeType,\n  RefDefMdNode,\n  Sourcepos,\n  TableCellMdNode,\n  TableColumn,\n  TableMdNode,\n} from '@t/node';\nimport NodeWalker from './nodeWalker';\n\nexport function isContainer(node: Node) {\n  switch (node.type) {\n    case 'document':\n    case 'blockQuote':\n    case 'list':\n    case 'item':\n    case 'paragraph':\n    case 'heading':\n    case 'emph':\n    case 'strong':\n    case 'strike':\n    case 'link':\n    case 'image':\n    case 'table':\n    case 'tableHead':\n    case 'tableBody':\n    case 'tableRow':\n    case 'tableCell':\n    case 'tableDelimRow':\n    case 'customInline':\n      return true;\n    default:\n      return false;\n  }\n}\n\nlet lastNodeId = 1;\nlet nodeMap: { [key: number]: Node } = {};\n\nexport function getNodeById(id: number) {\n  return nodeMap[id];\n}\n\nexport function removeNodeById(id: number) {\n  delete nodeMap[id];\n}\n\nexport function removeAllNode() {\n  nodeMap = {};\n}\n\nexport class Node implements MdNode {\n  type: MdNodeType;\n  id: number;\n  parent: Node | null = null;\n  prev: Node | null = null;\n  next: Node | null = null;\n  sourcepos?: Sourcepos;\n\n  // only for container node\n  firstChild: Node | null = null;\n  lastChild: Node | null = null;\n\n  // only for leaf node\n  literal: string | null = null;\n\n  constructor(nodeType: MdNodeType, sourcepos?: Sourcepos) {\n    if (nodeType === 'document') {\n      this.id = -1;\n    } else {\n      this.id = lastNodeId++;\n    }\n\n    this.type = nodeType;\n    this.sourcepos = sourcepos;\n    nodeMap![this.id] = this;\n  }\n\n  isContainer() {\n    return isContainer(this);\n  }\n\n  unlink() {\n    if (this.prev) {\n      this.prev.next = this.next;\n    } else if (this.parent) {\n      this.parent.firstChild = this.next;\n    }\n    if (this.next) {\n      this.next.prev = this.prev;\n    } else if (this.parent) {\n      this.parent.lastChild = this.prev;\n    }\n    this.parent = null;\n    this.next = null;\n    this.prev = null;\n  }\n\n  replaceWith(node: Node) {\n    this.insertBefore(node);\n    this.unlink();\n  }\n\n  insertAfter(sibling: Node) {\n    sibling.unlink();\n    sibling.next = this.next;\n    if (sibling.next) {\n      sibling.next.prev = sibling;\n    }\n    sibling.prev = this;\n    this.next = sibling;\n    if (this.parent) {\n      sibling.parent = this.parent;\n      if (!sibling.next) {\n        sibling.parent.lastChild = sibling;\n      }\n    }\n  }\n\n  insertBefore(sibling: Node) {\n    sibling.unlink();\n    sibling.prev = this.prev;\n    if (sibling.prev) {\n      sibling.prev.next = sibling;\n    }\n    sibling.next = this;\n    this.prev = sibling;\n    sibling.parent = this.parent;\n    if (!sibling.prev) {\n      sibling.parent!.firstChild = sibling;\n    }\n  }\n\n  appendChild(child: Node) {\n    child.unlink();\n    child.parent = this;\n    if (this.lastChild) {\n      this.lastChild.next = child;\n      child.prev = this.lastChild;\n      this.lastChild = child;\n    } else {\n      this.firstChild = child;\n      this.lastChild = child;\n    }\n  }\n\n  prependChild(child: Node) {\n    child.unlink();\n    child.parent = this;\n    if (this.firstChild) {\n      this.firstChild.prev = child;\n      child.next = this.firstChild;\n      this.firstChild = child;\n    } else {\n      this.firstChild = child;\n      this.lastChild = child;\n    }\n  }\n\n  walker() {\n    return new NodeWalker(this);\n  }\n}\n\nexport class BlockNode extends Node implements BlockMdNode {\n  type: BlockNodeType;\n\n  // temporal data (for parsing)\n  open = true;\n  lineOffsets: number[] | null = null;\n  stringContent: string | null = null;\n  lastLineBlank = false;\n  lastLineChecked = false;\n\n  constructor(nodeType: BlockNodeType, sourcepos?: Sourcepos) {\n    super(nodeType, sourcepos);\n    this.type = nodeType;\n  }\n}\n\nexport class ListNode extends BlockNode implements ListMdNode {\n  listData: ListData | null = null;\n}\n\nexport class HeadingNode extends BlockNode implements HeadingMdNode {\n  level = 0;\n  headingType: 'atx' | 'setext' = 'atx';\n}\n\nexport class CodeBlockNode extends BlockNode implements CodeBlockMdNode {\n  isFenced = false;\n  fenceChar: string | null = null;\n  fenceLength = 0;\n  fenceOffset = -1;\n  info: string | null = null;\n  infoPadding = 0;\n}\n\nexport class TableNode extends BlockNode implements TableMdNode {\n  columns: TableColumn[] = [];\n}\n\nexport class TableCellNode extends BlockNode implements TableCellMdNode {\n  startIdx = 0;\n  endIdx = 0;\n  paddingLeft = 0;\n  paddingRight = 0;\n  ignored = false;\n}\n\nexport class RefDefNode extends BlockNode implements RefDefMdNode {\n  title = '';\n  dest = '';\n  label = '';\n}\n\nexport class CustomBlockNode extends BlockNode implements CustomBlockMdNode {\n  syntaxLength = 0;\n  offset = -1;\n  info = '';\n}\n\nexport class HtmlBlockNode extends BlockNode implements HtmlBlockMdNode {\n  htmlBlockType = -1;\n}\n\nexport class LinkNode extends Node implements LinkMdNode {\n  destination: string | null = null;\n  title: string | null = null;\n  extendedAutolink = false;\n  lastChild!: Node;\n}\n\nexport class CodeNode extends Node implements CodeMdNode {\n  tickCount = 0;\n}\n\nexport class CustomInlineNode extends Node implements CustomInlineMdNode {\n  info = '';\n}\n\nexport function createNode(type: 'heading', sourcepos?: Sourcepos): HeadingNode;\nexport function createNode(type: 'list' | 'item', sourcepos?: Sourcepos): ListNode;\nexport function createNode(type: 'codeBlock', sourcepos?: Sourcepos): CodeBlockNode;\nexport function createNode(type: 'htmlBlock', sourcepos?: Sourcepos): HtmlBlockNode;\nexport function createNode(type: 'link' | 'image', sourcepos?: Sourcepos): LinkNode;\nexport function createNode(type: 'code', sourcepos?: Sourcepos): CodeNode;\nexport function createNode(type: 'table', sourcepos?: Sourcepos): TableNode;\nexport function createNode(type: 'tableCell', sourcepos?: Sourcepos): TableNode;\nexport function createNode(type: 'refDef', sourcepos?: Sourcepos): RefDefNode;\nexport function createNode(type: 'customBlock', sourcepos?: Sourcepos): CustomBlockNode;\nexport function createNode(type: BlockNodeType, sourcepos?: Sourcepos): BlockNode;\nexport function createNode(type: MdNodeType, sourcepos?: Sourcepos): Node;\nexport function createNode(type: MdNodeType, sourcepos?: Sourcepos) {\n  switch (type) {\n    case 'heading':\n      return new HeadingNode(type, sourcepos);\n    case 'list':\n    case 'item':\n      return new ListNode(type, sourcepos);\n    case 'link':\n    case 'image':\n      return new LinkNode(type, sourcepos);\n    case 'codeBlock':\n      return new CodeBlockNode(type, sourcepos);\n    case 'htmlBlock':\n      return new HtmlBlockNode(type, sourcepos);\n    case 'table':\n      return new TableNode(type, sourcepos);\n    case 'tableCell':\n      return new TableCellNode(type, sourcepos);\n    case 'document':\n    case 'paragraph':\n    case 'blockQuote':\n    case 'thematicBreak':\n    case 'tableRow':\n    case 'tableBody':\n    case 'tableHead':\n    case 'frontMatter':\n      return new BlockNode(type, sourcepos);\n    case 'code':\n      return new CodeNode(type, sourcepos);\n    case 'refDef':\n      return new RefDefNode(type, sourcepos);\n    case 'customBlock':\n      return new CustomBlockNode(type, sourcepos);\n    case 'customInline':\n      return new CustomInlineNode(type, sourcepos);\n    default:\n      return new Node(type, sourcepos) as Node;\n  }\n}\n\nexport function isCodeBlock(node: Node): node is CodeBlockNode {\n  return node.type === 'codeBlock';\n}\n\nexport function isHtmlBlock(node: Node): node is HtmlBlockNode {\n  return node.type === 'htmlBlock';\n}\n\nexport function isHeading(node: Node): node is HeadingNode {\n  return node.type === 'heading';\n}\n\nexport function isList(node: Node): node is ListNode {\n  return node.type === 'list';\n}\n\nexport function isTable(node: Node): node is TableNode {\n  return node.type === 'table';\n}\n\nexport function isRefDef(node: Node): node is RefDefNode {\n  return node.type === 'refDef';\n}\n\nexport function isCustomBlock(node: Node): node is CustomBlockNode {\n  return node.type === 'customBlock';\n}\n\nexport function isCustomInline(node: Node) {\n  return node.type === 'customInline';\n}\n\nexport function text(s: string, sourcepos?: Sourcepos) {\n  const node = createNode('text', sourcepos);\n  node.literal = s;\n  return node;\n}\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/nodeWalker.ts",
    "content": "import { NodeWalker as BaseNodeWalker } from '@t/node';\nimport { Node, isContainer } from './node';\n\nexport default class NodeWalker implements BaseNodeWalker {\n  current: Node | null;\n  root: Node;\n  entering: boolean;\n\n  constructor(root: Node) {\n    this.current = root;\n    this.root = root;\n    this.entering = true;\n  }\n\n  next() {\n    const cur = this.current;\n    const entering = this.entering;\n\n    if (cur === null) {\n      return null;\n    }\n\n    const container = isContainer(cur);\n\n    if (entering && container) {\n      if (cur.firstChild) {\n        this.current = cur.firstChild;\n        this.entering = true;\n      } else {\n        // stay on node but exit\n        this.entering = false;\n      }\n    } else if (cur === this.root) {\n      this.current = null;\n    } else if (cur.next === null) {\n      this.current = cur.parent;\n      this.entering = false;\n    } else {\n      this.current = cur.next;\n      this.entering = true;\n    }\n\n    return { entering, node: cur };\n  }\n\n  resumeAt(node: Node, entering: boolean) {\n    this.current = node;\n    this.entering = entering === true;\n  }\n}\n"
  },
  {
    "path": "libs/toastmark/src/commonmark/rawHtml.ts",
    "content": "const TAGNAME = '[A-Za-z][A-Za-z0-9-]*';\nconst ATTRIBUTENAME = '[a-zA-Z_:][a-zA-Z0-9:._-]*';\nconst UNQUOTEDVALUE = '[^\"\\'=<>`\\\\x00-\\\\x20]+';\n\nconst SINGLEQUOTEDVALUE = \"'[^']*'\";\nconst DOUBLEQUOTEDVALUE = '\"[^\"]*\"';\n\nconst ATTRIBUTEVALUE = `(?:${UNQUOTEDVALUE}|${SINGLEQUOTEDVALUE}|${DOUBLEQUOTEDVALUE})`;\nconst ATTRIBUTEVALUESPEC = `${'(?:\\\\s*=\\\\s*'}${ATTRIBUTEVALUE})`;\nconst ATTRIBUTE = `${'(?:\\\\s+'}${ATTRIBUTENAME}${ATTRIBUTEVALUESPEC}?)`;\n\nexport const OPENTAG = `<${TAGNAME}${ATTRIBUTE}*\\\\s*/?>`;\nexport const CLOSETAG = `</${TAGNAME}\\\\s*[>]`;\n\nconst HTMLCOMMENT = '<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->';\nconst PROCESSINGINSTRUCTION = '[<][?].*?[?][>]';\nconst DECLARATION = '<![A-Z]+\\\\s+[^>]*>';\nconst CDATA = '<!\\\\[CDATA\\\\[[\\\\s\\\\S]*?\\\\]\\\\]>';\n\nconst HTMLTAG = `(?:${OPENTAG}|${CLOSETAG}|${HTMLCOMMENT}|${PROCESSINGINSTRUCTION}|${DECLARATION}|${CDATA})`;\n\nexport const reHtmlTag = new RegExp(`^${HTMLTAG}`, 'i');\n"
  },
  {
    "path": "libs/toastmark/src/helper.ts",
    "content": "export function last<T>(arr: T[]): T;\nexport function last(arr: string): string;\n\nexport function last<T>(arr: T[] | string) {\n  return arr[arr.length - 1];\n}\n\n// normalize a reference in reference link (remove []s, trim,\n// collapse internal space, unicode case fold.\n// See commonmark/commonmark.js#168.\nexport function normalizeReference(str: string) {\n  return str\n    .slice(1, str.length - 1)\n    .trim()\n    .replace(/[ \\t\\r\\n]+/, ' ')\n    .toLowerCase()\n    .toUpperCase();\n}\n\nexport function iterateObject<T>(obj: T, iteratee: (key: keyof T, value: T[keyof T]) => void) {\n  Object.keys(obj).forEach((key) => {\n    iteratee(key as keyof T, obj[key as keyof T]);\n  });\n}\n\nexport function omit<T extends object>(obj: T, ...propNames: (keyof T)[]) {\n  const resultMap = { ...obj };\n  propNames.forEach((key) => {\n    delete resultMap[key];\n  });\n  return resultMap;\n}\n\nexport function isEmptyObj<T extends object>(obj: T) {\n  return !Object.keys(obj).length;\n}\n\nexport function clearObj<T extends object>(obj: T) {\n  Object.keys(obj).forEach((key) => {\n    delete obj[key as keyof T];\n  });\n}\n"
  },
  {
    "path": "libs/toastmark/src/html/__test__/render.spec.ts",
    "content": "import { source } from 'common-tags';\nimport { OpenTagToken } from '@t/renderer';\nimport { Parser } from '../../commonmark/blocks';\nimport { Renderer } from '../renderer';\n\nconst parser = new Parser();\n\ndescribe('softbreak options', () => {\n  it('softbreak option value should be used as a raw HTML string', () => {\n    const renderer = new Renderer({\n      softbreak: '\\n<br />\\n',\n    });\n    const html = renderer.render(parser.parse('Hello\\nWorld'));\n\n    expect(html).toBe('<p>Hello\\n<br />\\nWorld</p>\\n');\n  });\n});\n\ndescribe('nodeId options', () => {\n  const renderer = new Renderer({ nodeId: true });\n\n  it('every html tag corresponds to container node should contain data-nodeid', () => {\n    const root = parser.parse('*Hello* **World**');\n    const para = root.firstChild!;\n    const emph = para.firstChild!;\n    const strong = emph.next!.next!;\n\n    expect(renderer.render(root)).toBe(\n      [\n        `<p data-nodeid=\"${para.id}\">`,\n        `<em data-nodeid=\"${emph.id}\">Hello</em> `,\n        `<strong data-nodeid=\"${strong.id}\">World</strong>`,\n        '</p>\\n',\n      ].join('')\n    );\n  });\n\n  it('htmlBlock should be wrapped by div to contain data-nodeid', () => {\n    const root = parser.parse('<li>Hi</li>');\n    const htmlBlock = root.firstChild!;\n\n    expect(renderer.render(root)).toBe(`<div data-nodeid=\"${htmlBlock.id}\"><li>Hi</li></div>\\n`);\n  });\n\n  it('only top-level tag for each node should contain data-nodeid', () => {\n    const root = parser.parse('```\\nHello\\n```');\n    const codeBlock = root.firstChild!;\n\n    expect(renderer.render(root)).toBe(\n      `<pre data-nodeid=\"${codeBlock.id}\"><code>Hello\\n</code></pre>\\n`\n    );\n  });\n});\n\ndescribe('convertors options', () => {\n  it('should pass the context object to convertor', () => {\n    const spy = jest.fn(() => null);\n    const options = {\n      gfm: true,\n      softbreak: '<br />\\n',\n      nodeId: true,\n    };\n    const renderer = new Renderer({\n      ...options,\n      convertors: {\n        paragraph: spy,\n      },\n    });\n    const root = parser.parse('Hello World');\n    renderer.render(root);\n\n    expect(spy).toHaveBeenCalledTimes(2);\n\n    const firstCall = spy.mock.calls[0] as any[];\n    expect(firstCall[0]).toBe(root.firstChild);\n    expect(firstCall[1]).toMatchObject({\n      entering: true,\n      leaf: false,\n      options,\n    });\n\n    const secondCall = spy.mock.calls[1] as any[];\n    expect(secondCall[0]).toBe(root.firstChild);\n    expect(secondCall[1]).toMatchObject({\n      entering: false,\n      leaf: false,\n      options,\n    });\n  });\n\n  it('context object has origin convertor', () => {\n    const renderer = new Renderer({\n      convertors: {\n        paragraph(_, { entering, origin }) {\n          const result = origin!();\n          if (entering) {\n            (result as OpenTagToken).classNames = ['my-class'];\n            return result;\n          }\n          return result;\n        },\n      },\n    });\n    const html = renderer.render(parser.parse('Hello World'));\n\n    expect(html).toBe('<p class=\"my-class\">Hello World</p>\\n');\n  });\n});\n\ndescribe('gfm convertors', () => {\n  it('should apply custom renderer without changing node type to lower case', () => {\n    const spy = jest.fn();\n\n    const renderer = new Renderer({\n      gfm: true,\n      convertors: {\n        tableCell(_, { origin }) {\n          spy();\n          return origin!();\n        },\n      },\n    });\n    const input = source`\n      | a |  |  |\n      | - | - | - |\n      |  | b |  |\n      |  |  | c |\n    `;\n\n    renderer.render(parser.parse(input));\n\n    expect(spy).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "libs/toastmark/src/html/baseConvertors.ts",
    "content": "import { HTMLConvertorMap } from '@t/renderer';\nimport {\n  Node,\n  HeadingNode,\n  CodeBlockNode,\n  ListNode,\n  LinkNode,\n  CustomBlockNode,\n} from '../commonmark/node';\nimport { escapeXml } from '../commonmark/common';\nimport { filterDisallowedTags } from './tagFilter';\n\nconst CUSTOM_SYNTAX_LENGTH = 4;\n\nexport const baseConvertors: HTMLConvertorMap = {\n  heading(node, { entering }) {\n    return {\n      type: entering ? 'openTag' : 'closeTag',\n      tagName: `h${(node as HeadingNode).level}`,\n      outerNewLine: true,\n    };\n  },\n\n  text(node) {\n    return {\n      type: 'text',\n      content: node.literal!,\n    };\n  },\n\n  softbreak(_, { options }) {\n    return {\n      type: 'html',\n      content: options.softbreak,\n    };\n  },\n\n  linebreak() {\n    return {\n      type: 'html',\n      content: '<br />\\n',\n    };\n  },\n\n  emph(_, { entering }) {\n    return {\n      type: entering ? 'openTag' : 'closeTag',\n      tagName: 'em',\n    };\n  },\n\n  strong(_, { entering }) {\n    return {\n      type: entering ? 'openTag' : 'closeTag',\n      tagName: 'strong',\n    };\n  },\n\n  paragraph(node, { entering }) {\n    const grandparent = node.parent?.parent;\n    if (grandparent && grandparent.type === 'list') {\n      if ((grandparent as ListNode).listData!.tight) {\n        return null;\n      }\n    }\n\n    return {\n      type: entering ? 'openTag' : 'closeTag',\n      tagName: 'p',\n      outerNewLine: true,\n    };\n  },\n\n  thematicBreak() {\n    return {\n      type: 'openTag',\n      tagName: 'hr',\n      outerNewLine: true,\n      selfClose: true,\n    };\n  },\n\n  blockQuote(_, { entering }) {\n    return {\n      type: entering ? 'openTag' : 'closeTag',\n      tagName: 'blockquote',\n      outerNewLine: true,\n      innerNewLine: true,\n    };\n  },\n\n  list(node, { entering }) {\n    const { type, start } = (node as ListNode).listData!;\n    const tagName = type === 'bullet' ? 'ul' : 'ol';\n    const attributes: Record<string, string> = {};\n    if (tagName === 'ol' && start !== null && start !== 1) {\n      attributes.start = start.toString();\n    }\n\n    return {\n      type: entering ? 'openTag' : 'closeTag',\n      tagName,\n      attributes,\n      outerNewLine: true,\n    };\n  },\n\n  item(_, { entering }) {\n    return {\n      type: entering ? 'openTag' : 'closeTag',\n      tagName: 'li',\n      outerNewLine: true,\n    };\n  },\n\n  htmlInline(node, { options }) {\n    const content = options.tagFilter ? filterDisallowedTags(node.literal!) : node.literal!;\n\n    return { type: 'html', content };\n  },\n\n  htmlBlock(node, { options }) {\n    const content = options.tagFilter ? filterDisallowedTags(node.literal!) : node.literal!;\n\n    if (options.nodeId) {\n      return [\n        { type: 'openTag', tagName: 'div', outerNewLine: true },\n        { type: 'html', content },\n        { type: 'closeTag', tagName: 'div', outerNewLine: true },\n      ];\n    }\n\n    return { type: 'html', content, outerNewLine: true };\n  },\n\n  code(node) {\n    return [\n      { type: 'openTag', tagName: 'code' },\n      { type: 'text', content: node.literal! },\n      { type: 'closeTag', tagName: 'code' },\n    ];\n  },\n\n  codeBlock(node) {\n    const infoStr = (node as CodeBlockNode).info;\n    const infoWords = infoStr ? infoStr.split(/\\s+/) : [];\n    const codeClassNames = [];\n    if (infoWords.length > 0 && infoWords[0].length > 0) {\n      codeClassNames.push(`language-${escapeXml(infoWords[0])}`);\n    }\n\n    return [\n      { type: 'openTag', tagName: 'pre', outerNewLine: true },\n      { type: 'openTag', tagName: 'code', classNames: codeClassNames },\n      { type: 'text', content: node.literal! },\n      { type: 'closeTag', tagName: 'code' },\n      { type: 'closeTag', tagName: 'pre', outerNewLine: true },\n    ];\n  },\n\n  link(node: Node, { entering }) {\n    if (entering) {\n      const { title, destination } = node as LinkNode;\n\n      return {\n        type: 'openTag',\n        tagName: 'a',\n        attributes: {\n          href: escapeXml(destination!),\n          ...(title && { title: escapeXml(title) }),\n        },\n      };\n    }\n    return { type: 'closeTag', tagName: 'a' };\n  },\n\n  image(node: Node, { getChildrenText, skipChildren }) {\n    const { title, destination } = node as LinkNode;\n\n    skipChildren();\n\n    return {\n      type: 'openTag',\n      tagName: 'img',\n      selfClose: true,\n      attributes: {\n        src: escapeXml(destination!),\n        alt: getChildrenText(node),\n        ...(title && { title: escapeXml(title) }),\n      },\n    };\n  },\n\n  customBlock(node, context, convertors) {\n    const info = (node as CustomBlockNode).info!.trim().toLowerCase();\n    const customConvertor = convertors![info];\n\n    if (customConvertor) {\n      try {\n        return customConvertor!(node, context);\n      } catch (e) {\n        console.warn(\n          `[@toast-ui/editor] - The error occurred when ${info} block node was parsed in markdown renderer: ${e}`\n        );\n      }\n    }\n\n    return [\n      { type: 'openTag', tagName: 'div', outerNewLine: true },\n      { type: 'text', content: node.literal! },\n      { type: 'closeTag', tagName: 'div', outerNewLine: true },\n    ];\n  },\n\n  frontMatter(node) {\n    return [\n      {\n        type: 'openTag',\n        tagName: 'div',\n        outerNewLine: true,\n        // Because front matter is metadata, it should not be render.\n        attributes: { style: 'white-space: pre; display: none;' },\n      },\n      { type: 'text', content: node.literal! },\n      { type: 'closeTag', tagName: 'div', outerNewLine: true },\n    ];\n  },\n\n  customInline(node, context, convertors) {\n    const { info, firstChild } = node as CustomBlockNode;\n    const nomalizedInfo = info.trim().toLowerCase();\n    const customConvertor = convertors![nomalizedInfo];\n    const { entering } = context;\n\n    if (customConvertor) {\n      try {\n        return customConvertor!(node, context);\n      } catch (e) {\n        console.warn(\n          `[@toast-ui/editor] - The error occurred when ${nomalizedInfo} inline node was parsed in markdown renderer: ${e}`\n        );\n      }\n    }\n\n    return entering\n      ? [\n          { type: 'openTag', tagName: 'span' },\n          { type: 'text', content: `$$${info}${firstChild ? ' ' : ''}` },\n        ]\n      : [\n          { type: 'text', content: '$$' },\n          { type: 'closeTag', tagName: 'span' },\n        ];\n  },\n};\n"
  },
  {
    "path": "libs/toastmark/src/html/gfmConvertors.ts",
    "content": "import { HTMLConvertorMap, HTMLToken, OpenTagToken } from '@t/renderer';\nimport { Node, ListNode, TableNode, TableCellNode } from '../commonmark/node';\n\nexport const gfmConvertors: HTMLConvertorMap = {\n  strike(_, { entering }) {\n    return {\n      type: entering ? 'openTag' : 'closeTag',\n      tagName: 'del',\n    };\n  },\n\n  item(node: Node, { entering }) {\n    const { checked, task } = (node as ListNode).listData!;\n\n    if (entering) {\n      const itemTag: OpenTagToken = {\n        type: 'openTag',\n        tagName: 'li',\n        outerNewLine: true,\n      };\n\n      if (task) {\n        return [\n          itemTag,\n          {\n            type: 'openTag',\n            tagName: 'input',\n            selfClose: true,\n            attributes: {\n              ...(checked && { checked: '' }),\n              disabled: '',\n              type: 'checkbox',\n            },\n          },\n          {\n            type: 'text',\n            content: ' ',\n          },\n        ];\n      }\n      return itemTag;\n    }\n\n    return {\n      type: 'closeTag',\n      tagName: 'li',\n      outerNewLine: true,\n    };\n  },\n\n  table(_, { entering }) {\n    return {\n      type: entering ? 'openTag' : 'closeTag',\n      tagName: 'table',\n      outerNewLine: true,\n    };\n  },\n\n  tableHead(_, { entering }) {\n    return {\n      type: entering ? 'openTag' : 'closeTag',\n      tagName: 'thead',\n      outerNewLine: true,\n    };\n  },\n\n  tableBody(_, { entering }) {\n    return {\n      type: entering ? 'openTag' : 'closeTag',\n      tagName: 'tbody',\n      outerNewLine: true,\n    };\n  },\n\n  tableRow(node: Node, { entering }) {\n    if (entering) {\n      return {\n        type: 'openTag',\n        tagName: 'tr',\n        outerNewLine: true,\n      };\n    }\n\n    const result: HTMLToken[] = [];\n    if (node.lastChild) {\n      const columnLen = (node.parent!.parent as TableNode).columns.length;\n      const lastColIdx = (node.lastChild as TableCellNode).endIdx;\n      for (let i = lastColIdx + 1; i < columnLen; i += 1) {\n        result.push(\n          {\n            type: 'openTag',\n            tagName: 'td',\n            outerNewLine: true,\n          },\n          {\n            type: 'closeTag',\n            tagName: 'td',\n            outerNewLine: true,\n          }\n        );\n      }\n    }\n\n    result.push({\n      type: 'closeTag',\n      tagName: 'tr',\n      outerNewLine: true,\n    });\n\n    return result;\n  },\n\n  tableCell(node: Node, { entering }) {\n    if ((node as TableCellNode).ignored) {\n      return {\n        type: 'text',\n        content: '',\n      };\n    }\n\n    const tablePart = node.parent!.parent!;\n    const tagName = tablePart.type === 'tableHead' ? 'th' : 'td';\n    const table = tablePart.parent as TableNode;\n    const columnInfo = table.columns[(node as TableCellNode).startIdx];\n    const attributes = columnInfo?.align ? { align: columnInfo.align } : null;\n\n    if (entering) {\n      return {\n        type: 'openTag',\n        tagName,\n        outerNewLine: true,\n        ...(attributes && { attributes }),\n      };\n    }\n\n    return {\n      type: 'closeTag',\n      tagName,\n      outerNewLine: true,\n    };\n  },\n};\n"
  },
  {
    "path": "libs/toastmark/src/html/renderer.ts",
    "content": "import {\n  CloseTagToken,\n  Context,\n  HTMLConvertorMap,\n  HTMLRenderer,\n  HTMLToken,\n  OpenTagToken,\n  RawHTMLToken,\n  RendererOptions,\n  TagToken,\n  TextToken,\n} from '@t/renderer';\nimport { MdNodeType } from '@t/node';\nimport { Node, isContainer, isCustomBlock, isCustomInline } from '../commonmark/node';\nimport { escapeXml } from '../commonmark/common';\nimport { last } from '../helper';\nimport { baseConvertors } from './baseConvertors';\nimport { gfmConvertors } from './gfmConvertors';\n\nconst defaultOptions: RendererOptions = {\n  softbreak: '\\n',\n  gfm: false,\n  tagFilter: false,\n  nodeId: false,\n};\n\nfunction getChildrenText(node: Node) {\n  const buffer: string[] = [];\n  const walker = node.walker();\n  let event: ReturnType<typeof walker.next> = null;\n\n  while ((event = walker.next())) {\n    const { node } = event;\n    if (node.type === 'text') {\n      buffer.push(node.literal!);\n    }\n  }\n  return buffer.join('');\n}\n\nexport class Renderer implements HTMLRenderer {\n  private convertors: HTMLConvertorMap;\n\n  private options: RendererOptions;\n\n  private buffer: string[] = [];\n\n  constructor(customOptions?: Partial<RendererOptions>) {\n    this.options = { ...defaultOptions, ...customOptions };\n    this.convertors = this.createConvertors();\n\n    delete this.options.convertors;\n  }\n\n  private createConvertors() {\n    let convertors: HTMLConvertorMap = { ...baseConvertors };\n\n    if (this.options.gfm) {\n      convertors = { ...convertors, ...gfmConvertors };\n    }\n\n    if (this.options.convertors) {\n      const customConvertors = this.options.convertors;\n      const nodeTypes = Object.keys(customConvertors) as MdNodeType[];\n      const defaultConvertors = { ...baseConvertors, ...gfmConvertors };\n      nodeTypes.forEach((nodeType) => {\n        const orgConvertor = convertors[nodeType];\n        const convertor = customConvertors[nodeType]!;\n        const convertorType =\n          Object.keys(defaultConvertors).indexOf(nodeType) === -1\n            ? nodeType.toLowerCase()\n            : nodeType;\n\n        if (orgConvertor) {\n          convertors[convertorType] = (node, context, convertors) => {\n            context.origin = () => orgConvertor(node, context, convertors);\n            return convertor(node, context);\n          };\n        } else {\n          convertors[convertorType] = convertor;\n        }\n      });\n    }\n    return convertors;\n  }\n\n  getConvertors() {\n    return this.convertors;\n  }\n\n  getOptions() {\n    return this.options;\n  }\n\n  render(rootNode: Node): string {\n    this.buffer = [];\n\n    const walker = rootNode.walker();\n    let event: ReturnType<typeof walker.next> = null;\n\n    while ((event = walker.next())) {\n      const { node, entering } = event;\n      const convertor = this.convertors[node.type];\n      if (!convertor) {\n        continue;\n      }\n\n      let skipped = false;\n      const context: Context = {\n        entering,\n        leaf: !isContainer(node),\n        options: this.options,\n        getChildrenText,\n        skipChildren: () => {\n          skipped = true;\n        },\n      };\n\n      const converted =\n        isCustomBlock(node) || isCustomInline(node)\n          ? convertor(node, context, this.convertors)\n          : convertor(node, context);\n      if (converted) {\n        const htmlNodes = Array.isArray(converted) ? converted : [converted];\n        htmlNodes.forEach((htmlNode, index) => {\n          if (htmlNode.type === 'openTag' && this.options.nodeId && index === 0) {\n            if (!htmlNode.attributes) {\n              htmlNode.attributes = {};\n            }\n            htmlNode.attributes['data-nodeid'] = String(node.id);\n          }\n          this.renderHTMLNode(htmlNode);\n        });\n\n        if (skipped) {\n          walker.resumeAt(node, false);\n          walker.next();\n        }\n      }\n    }\n    this.addNewLine();\n\n    return this.buffer.join('');\n  }\n\n  renderHTMLNode(node: HTMLToken) {\n    switch (node.type) {\n      case 'openTag':\n      case 'closeTag':\n        this.renderElementNode(node);\n        break;\n      case 'text':\n        this.renderTextNode(node);\n        break;\n      case 'html':\n        this.renderRawHtmlNode(node);\n        break;\n      default:\n      // no-default-case\n    }\n  }\n\n  private generateOpenTagString(node: OpenTagToken) {\n    const { tagName, classNames, attributes } = node;\n\n    this.buffer.push(`<${tagName}`);\n\n    if (classNames && classNames.length > 0) {\n      this.buffer.push(` class=\"${classNames.join(' ')}\"`);\n    }\n\n    if (attributes) {\n      Object.keys(attributes).forEach((attrName) => {\n        const attrValue = attributes[attrName];\n        this.buffer.push(` ${attrName}=\"${attrValue}\"`);\n      });\n    }\n\n    if (node.selfClose) {\n      this.buffer.push(' /');\n    }\n    this.buffer.push('>');\n  }\n\n  private generateCloseTagString({ tagName }: CloseTagToken) {\n    this.buffer.push(`</${tagName}>`);\n  }\n\n  private addNewLine() {\n    if (this.buffer.length && last(last(this.buffer)) !== '\\n') {\n      this.buffer.push('\\n');\n    }\n  }\n\n  private addOuterNewLine(node: TagToken | RawHTMLToken) {\n    if (node.outerNewLine) {\n      this.addNewLine();\n    }\n  }\n\n  private addInnerNewLine(node: TagToken) {\n    if (node.innerNewLine) {\n      this.addNewLine();\n    }\n  }\n\n  private renderTextNode(node: TextToken) {\n    this.buffer.push(escapeXml(node.content));\n  }\n\n  private renderRawHtmlNode(node: RawHTMLToken) {\n    this.addOuterNewLine(node);\n    this.buffer.push(node.content);\n    this.addOuterNewLine(node);\n  }\n\n  private renderElementNode(node: OpenTagToken | CloseTagToken) {\n    if (node.type === 'openTag') {\n      this.addOuterNewLine(node);\n      this.generateOpenTagString(node);\n      if (node.selfClose) {\n        this.addOuterNewLine(node);\n      } else {\n        this.addInnerNewLine(node);\n      }\n    } else {\n      this.addInnerNewLine(node);\n      this.generateCloseTagString(node);\n      this.addOuterNewLine(node);\n    }\n  }\n}\n"
  },
  {
    "path": "libs/toastmark/src/html/tagFilter.ts",
    "content": "const disallowedTags = [\n  'title',\n  'textarea',\n  'style',\n  'xmp',\n  'iframe',\n  'noembed',\n  'noframes',\n  'script',\n  'plaintext',\n];\n\nconst reDisallowedTag = new RegExp(`<(\\/?(?:${disallowedTags.join('|')})[^>]*>)`, 'ig');\n\nexport function filterDisallowedTags(str: string) {\n  if (reDisallowedTag.test(str)) {\n    return str.replace(reDisallowedTag, (_, group) => `&lt;${group}`);\n  }\n  return str;\n}\n"
  },
  {
    "path": "libs/toastmark/src/index.ts",
    "content": "export { ToastMark } from './toastmark';\nexport { Renderer } from './html/renderer';\nexport { Parser } from './commonmark/blocks';\n"
  },
  {
    "path": "libs/toastmark/src/nodeHelper.ts",
    "content": "import { Pos, Sourcepos } from '@t/node';\nimport { Node, getNodeById, removeNodeById } from './commonmark/node';\n\nexport const enum Compare {\n  LT = 1,\n  EQ = 0,\n  GT = -1,\n}\n\nfunction comparePos(p1: Pos, p2: Pos) {\n  if (p1[0] < p2[0]) {\n    return Compare.LT;\n  }\n  if (p1[0] > p2[0]) {\n    return Compare.GT;\n  }\n  if (p1[1] < p2[1]) {\n    return Compare.LT;\n  }\n  if (p1[1] > p2[1]) {\n    return Compare.GT;\n  }\n  return Compare.EQ;\n}\n\nfunction compareRangeAndPos([startPos, endPos]: Sourcepos, pos: Pos) {\n  if (comparePos(endPos, pos) === Compare.LT) {\n    return Compare.LT;\n  }\n  if (comparePos(startPos, pos) === Compare.GT) {\n    return Compare.GT;\n  }\n  return Compare.EQ;\n}\n\nexport function getAllParents(node: Node) {\n  const parents = [];\n  while (node.parent) {\n    parents.push(node.parent);\n    node = node.parent;\n  }\n  return parents.reverse();\n}\n\nexport function removeNextUntil(node: Node, last: Node) {\n  if (node.parent !== last.parent || node === last) {\n    return;\n  }\n\n  let next = node.next;\n  while (next && next !== last) {\n    const temp = next.next;\n    for (const type of ['parent', 'prev', 'next'] as const) {\n      if (next[type]) {\n        removeNodeById(next[type]!.id);\n        next[type] = null;\n      }\n    }\n    next = temp;\n  }\n  node.next = last.next;\n  if (last.next) {\n    last.next.prev = node;\n  } else {\n    node.parent!.lastChild = node;\n  }\n}\n\nexport function getChildNodes(parent: Node) {\n  const nodes = [];\n  let curr: Node | null = parent.firstChild!;\n  while (curr) {\n    nodes.push(curr);\n    curr = curr.next;\n  }\n  return nodes;\n}\n\nexport function insertNodesBefore(target: Node, nodes: Node[]) {\n  for (const node of nodes) {\n    target.insertBefore(node);\n  }\n}\n\nexport function prependChildNodes(parent: Node, nodes: Node[]) {\n  for (let i = nodes.length - 1; i >= 0; i -= 1) {\n    parent.prependChild(nodes[i]);\n  }\n}\n\nexport function updateNextLineNumbers(base: Node | null, diff: number) {\n  if (!base || !base.parent || diff === 0) {\n    return;\n  }\n\n  const walker = base.parent.walker();\n  walker.resumeAt(base, true);\n\n  let event;\n  while ((event = walker.next())) {\n    const { node, entering } = event;\n    if (entering) {\n      node.sourcepos![0][0] += diff;\n      node.sourcepos![1][0] += diff;\n    }\n  }\n}\n\nfunction compareRangeAndLine([startPos, endPos]: Sourcepos, line: number) {\n  if (endPos[0] < line) {\n    return Compare.LT;\n  }\n  if (startPos[0] > line) {\n    return Compare.GT;\n  }\n  return Compare.EQ;\n}\n\nexport function findChildNodeAtLine(parent: Node, line: number) {\n  let node = parent.firstChild;\n  while (node) {\n    const comp = compareRangeAndLine(node.sourcepos!, line);\n    if (comp === Compare.EQ) {\n      return node;\n    }\n    if (comp === Compare.GT) {\n      // To consider that top line is blank line\n      return node.prev || node;\n    }\n    node = node.next;\n  }\n  return parent.lastChild;\n}\n\nfunction lastLeafNode(node: Node) {\n  while (node.lastChild) {\n    node = node.lastChild;\n  }\n  return node;\n}\n\nfunction sameLineTopAncestor(node: Node) {\n  while (\n    node.parent &&\n    node.parent.type !== 'document' &&\n    node.parent.sourcepos![0][0] === node.sourcepos![0][0]\n  ) {\n    node = node.parent;\n  }\n  return node;\n}\n\nexport function findFirstNodeAtLine(parent: Node, line: number) {\n  let node = parent.firstChild;\n  let prev: Node | null = null;\n  while (node) {\n    const comp = compareRangeAndLine(node.sourcepos!, line);\n    if (comp === Compare.EQ) {\n      if (node.sourcepos![0][0] === line || !node.firstChild) {\n        return node;\n      }\n      prev = node;\n      node = node.firstChild;\n    } else if (comp === Compare.GT) {\n      break;\n    } else {\n      prev = node;\n      node = node.next;\n    }\n  }\n\n  if (prev) {\n    return sameLineTopAncestor(lastLeafNode(prev));\n  }\n  return null;\n}\n\nexport function findNodeAtPosition(parent: Node, pos: Pos) {\n  let node: Node | null = parent;\n  let prev: Node | null = null;\n\n  while (node) {\n    const comp = compareRangeAndPos(node.sourcepos!, pos);\n    if (comp === Compare.EQ) {\n      if (node.firstChild) {\n        prev = node;\n        node = node.firstChild;\n      } else {\n        return node;\n      }\n    } else if (comp === Compare.GT) {\n      return prev;\n    } else if (node.next) {\n      node = node.next;\n    } else {\n      return prev;\n    }\n  }\n  return node;\n}\n\nexport function toString(node: Node | null) {\n  if (!node) {\n    return 'null';\n  }\n  return `type: ${node.type}, sourcepos: ${node.sourcepos}, firstChild: ${\n    node.firstChild && node.firstChild.type\n  }, lastChild: ${node.lastChild && node.lastChild.type}, prev: ${\n    node.prev && node.prev.type\n  }, next: ${node.next && node.next.type}`;\n}\n\nexport function findNodeById(id: number) {\n  return getNodeById(id) || null;\n}\n\nexport function invokeNextUntil(callback: Function, start: Node | null, end: Node | null = null) {\n  if (start) {\n    const walker = start.walker();\n    while (start && start !== end) {\n      callback(start);\n      const next = walker.next();\n      if (next) {\n        start = next.node;\n      } else {\n        break;\n      }\n    }\n  }\n}\n\nexport function isUnlinked(id: number) {\n  let node = findNodeById(id);\n\n  if (!node) {\n    return true;\n  }\n\n  while (node && node.type !== 'document') {\n    // eslint-disable-next-line no-loop-func\n    if (!node.parent && !node.prev && !node.next) {\n      return true;\n    }\n    node = node.parent!;\n  }\n  return false;\n}\n"
  },
  {
    "path": "libs/toastmark/src/toastmark.ts",
    "content": "import {\n  EditResult,\n  EventHandlerMap,\n  EventName,\n  RemovedNodeRange,\n  ToastMark as ToastMarkParser,\n} from '@t/toastMark';\nimport { ParserOptions, RefDefCandidateMap, RefLinkCandidateMap, RefMap } from '@t/parser';\nimport { Pos } from '@t/node';\nimport { Parser } from './commonmark/blocks';\nimport {\n  BlockNode,\n  isList,\n  removeAllNode,\n  removeNodeById,\n  Node,\n  isRefDef,\n  RefDefNode,\n  isTable,\n  isCodeBlock,\n  isCustomBlock,\n} from './commonmark/node';\nimport {\n  removeNextUntil,\n  getChildNodes,\n  insertNodesBefore,\n  prependChildNodes,\n  updateNextLineNumbers,\n  findChildNodeAtLine,\n  findFirstNodeAtLine,\n  findNodeAtPosition,\n  findNodeById,\n  invokeNextUntil,\n  isUnlinked,\n} from './nodeHelper';\nimport { reBulletListMarker, reOrderedListMarker } from './commonmark/blockStarts';\nimport { iterateObject, omit, isEmptyObj } from './helper';\nimport { isBlank } from './commonmark/blockHelper';\n\nexport const reLineEnding = /\\r\\n|\\n|\\r/;\n\ntype ParseResult = EditResult & { nextNode: Node | null };\n\nfunction canBeContinuedListItem(lineText: string) {\n  const spaceMatch = lineText.match(/^[ \\t]+/);\n  if (spaceMatch && (spaceMatch[0].length >= 2 || /\\t/.test(spaceMatch[0]))) {\n    return true;\n  }\n\n  const leftTrimmed = spaceMatch ? lineText.slice(spaceMatch.length) : lineText;\n  return reBulletListMarker.test(leftTrimmed) || reOrderedListMarker.test(leftTrimmed);\n}\n\nfunction canBeContinuedTableBody(lineText: string) {\n  return !isBlank(lineText) && lineText.indexOf('|') !== -1;\n}\n\nexport function createRefDefState(node: RefDefNode) {\n  const { id, title, sourcepos, dest } = node;\n  return {\n    id,\n    title,\n    sourcepos: sourcepos!,\n    unlinked: false,\n    destination: dest,\n  };\n}\n\nexport class ToastMark implements ToastMarkParser {\n  lineTexts: string[];\n  private parser: Parser;\n  private root: BlockNode;\n  private eventHandlerMap: EventHandlerMap;\n  private refMap: RefMap;\n  private refLinkCandidateMap: RefLinkCandidateMap;\n  private refDefCandidateMap: RefDefCandidateMap;\n  private referenceDefinition: boolean;\n\n  constructor(contents?: string, options?: Partial<ParserOptions>) {\n    this.refMap = {};\n    this.refLinkCandidateMap = {};\n    this.refDefCandidateMap = {};\n    this.referenceDefinition = !!options?.referenceDefinition;\n    this.parser = new Parser(options);\n    this.parser.setRefMaps(this.refMap, this.refLinkCandidateMap, this.refDefCandidateMap);\n    this.eventHandlerMap = { change: [] };\n\n    contents = contents || '';\n    this.lineTexts = contents.split(reLineEnding);\n    this.root = this.parser.parse(contents, this.lineTexts);\n  }\n\n  private updateLineTexts(startPos: Pos, endPos: Pos, newText: string) {\n    const [startLine, startCol] = startPos;\n    const [endLine, endCol] = endPos;\n    const newLines = newText.split(reLineEnding);\n    const newLineLen = newLines.length;\n    const startLineText = this.lineTexts[startLine - 1];\n    const endLineText = this.lineTexts[endLine - 1];\n    newLines[0] = startLineText.slice(0, startCol - 1) + newLines[0];\n    newLines[newLineLen - 1] = newLines[newLineLen - 1] + endLineText.slice(endCol - 1);\n\n    const removedLineLen = endLine - startLine + 1;\n    this.lineTexts.splice(startLine - 1, removedLineLen, ...newLines);\n\n    return newLineLen - removedLineLen;\n  }\n\n  private updateRootNodeState() {\n    if (this.lineTexts.length === 1 && this.lineTexts[0] === '') {\n      this.root.lastLineBlank = true;\n      this.root.sourcepos = [\n        [1, 1],\n        [1, 0],\n      ];\n      return;\n    }\n\n    if (this.root.lastChild) {\n      this.root.lastLineBlank = (this.root.lastChild as BlockNode).lastLineBlank;\n    }\n\n    const { lineTexts } = this;\n    let idx = lineTexts.length - 1;\n    while (lineTexts[idx] === '') {\n      idx -= 1;\n    }\n    if (lineTexts.length - 2 > idx) {\n      idx += 1;\n    }\n\n    this.root.sourcepos![1] = [idx + 1, lineTexts[idx].length];\n  }\n\n  private replaceRangeNodes(\n    startNode: BlockNode | null,\n    endNode: BlockNode | null,\n    newNodes: BlockNode[]\n  ) {\n    if (!startNode) {\n      if (endNode) {\n        insertNodesBefore(endNode, newNodes);\n        removeNodeById(endNode.id);\n        endNode.unlink();\n      } else {\n        prependChildNodes(this.root, newNodes);\n      }\n    } else {\n      insertNodesBefore(startNode, newNodes);\n      removeNextUntil(startNode, endNode!);\n      [startNode.id, endNode!.id].forEach((id) => removeNodeById(id));\n      startNode.unlink();\n    }\n  }\n\n  private getNodeRange(startPos: Pos, endPos: Pos) {\n    const startNode = findChildNodeAtLine(this.root, startPos[0]);\n    let endNode = findChildNodeAtLine(this.root, endPos[0]);\n\n    // extend node range to include a following block which doesn't have preceding blank line\n    if (endNode && endNode.next && endPos[0] + 1 === endNode.next.sourcepos![0][0]) {\n      endNode = endNode.next;\n    }\n\n    return [startNode, endNode] as [BlockNode, BlockNode];\n  }\n\n  private trigger(eventName: EventName, param: any) {\n    this.eventHandlerMap[eventName].forEach((handler) => {\n      handler(param);\n    });\n  }\n\n  private extendEndLine(line: number) {\n    while (this.lineTexts[line] === '') {\n      line += 1;\n    }\n    return line;\n  }\n\n  private parseRange(\n    startNode: BlockNode | null,\n    endNode: BlockNode | null,\n    startLine: number,\n    endLine: number\n  ) {\n    // extends starting range if the first node can be a continued list item\n    if (\n      startNode &&\n      startNode.prev &&\n      ((isList(startNode.prev) && canBeContinuedListItem(this.lineTexts[startLine - 1])) ||\n        (isTable(startNode.prev) && canBeContinuedTableBody(this.lineTexts[startLine - 1])))\n    ) {\n      startNode = startNode.prev;\n      startLine = startNode.sourcepos![0][0];\n    }\n\n    const editedLines = this.lineTexts.slice(startLine - 1, endLine);\n    const root = this.parser.partialParseStart(startLine, editedLines);\n\n    // extends ending range if the following node can be a fenced code block or a continued list item\n    let nextNode = endNode ? endNode.next : this.root.firstChild;\n    const { lastChild } = root;\n    const isOpenedLastChildCodeBlock = lastChild && isCodeBlock(lastChild) && lastChild.open;\n    const isOpenedLastChildCustomBlock = lastChild && isCustomBlock(lastChild) && lastChild.open;\n    const isLastChildList = lastChild && isList(lastChild);\n\n    while (\n      ((isOpenedLastChildCodeBlock || isOpenedLastChildCustomBlock) && nextNode) ||\n      (isLastChildList && nextNode && (nextNode.type === 'list' || nextNode.sourcepos![0][1] >= 2))\n    ) {\n      const newEndLine = this.extendEndLine(nextNode.sourcepos![1][0]);\n      this.parser.partialParseExtends(this.lineTexts.slice(endLine, newEndLine));\n\n      if (!startNode) {\n        startNode = endNode;\n      }\n      endNode = nextNode as BlockNode;\n      endLine = newEndLine;\n      nextNode = nextNode.next;\n    }\n\n    this.parser.partialParseFinish();\n\n    const newNodes = getChildNodes(root)! as BlockNode[];\n    return { newNodes, extStartNode: startNode, extEndNode: endNode };\n  }\n\n  private getRemovedNodeRange(\n    extStartNode: BlockNode | null,\n    extEndNode: BlockNode | null\n  ): RemovedNodeRange | null {\n    if (\n      !extStartNode ||\n      (extStartNode && isRefDef(extStartNode)) ||\n      (extEndNode && isRefDef(extEndNode))\n    ) {\n      return null;\n    }\n    return {\n      id: [extStartNode.id, extEndNode!.id],\n      line: [extStartNode.sourcepos![0][0] - 1, extEndNode!.sourcepos![1][0] - 1],\n    };\n  }\n\n  private markDeletedRefMap(extStartNode: BlockNode | null, extEndNode: BlockNode | null) {\n    if (!isEmptyObj(this.refMap)) {\n      const markDeleted = (node: BlockNode) => {\n        if (isRefDef(node)) {\n          const refDefState = this.refMap[node.label];\n          if (refDefState && node.id === refDefState.id) {\n            refDefState.unlinked = true;\n          }\n        }\n      };\n      if (extStartNode) {\n        invokeNextUntil(markDeleted, extStartNode.parent!, extEndNode);\n      }\n      if (extEndNode) {\n        invokeNextUntil(markDeleted, extEndNode);\n      }\n    }\n  }\n\n  private replaceWithNewRefDefState(nodes: BlockNode[]) {\n    if (!isEmptyObj(this.refMap)) {\n      const replaceWith = (node: BlockNode) => {\n        if (isRefDef(node)) {\n          const { label } = node;\n          const refDefState = this.refMap[label];\n          if (!refDefState || refDefState.unlinked) {\n            this.refMap[label] = createRefDefState(node);\n          }\n        }\n      };\n      nodes.forEach((node) => {\n        invokeNextUntil(replaceWith, node);\n      });\n    }\n  }\n\n  private replaceWithRefDefCandidate() {\n    if (!isEmptyObj(this.refDefCandidateMap)) {\n      iterateObject(this.refDefCandidateMap, (_, candidate) => {\n        const { label, sourcepos } = candidate;\n        const refDefState = this.refMap[label];\n\n        if (\n          !refDefState ||\n          refDefState.unlinked ||\n          refDefState.sourcepos[0][0] > sourcepos![0][0]\n        ) {\n          this.refMap[label] = createRefDefState(candidate);\n        }\n      });\n    }\n  }\n\n  private getRangeWithRefDef(\n    startLine: number,\n    endLine: number,\n    startNode: BlockNode,\n    endNode: BlockNode,\n    lineDiff: number\n  ) {\n    if (this.referenceDefinition && !isEmptyObj(this.refMap)) {\n      const prevNode = findChildNodeAtLine(this.root, startLine - 1);\n      const nextNode = findChildNodeAtLine(this.root, endLine + 1);\n\n      if (prevNode && isRefDef(prevNode) && prevNode !== startNode && prevNode !== endNode) {\n        startNode = prevNode;\n        startLine = startNode.sourcepos![0][0];\n      }\n\n      if (nextNode && isRefDef(nextNode) && nextNode !== startNode && nextNode !== endNode) {\n        endNode = nextNode;\n        endLine = this.extendEndLine(endNode.sourcepos![1][0] + lineDiff);\n      }\n    }\n\n    return [startNode, endNode, startLine, endLine] as const;\n  }\n\n  private parse(startPos: Pos, endPos: Pos, lineDiff = 0): ParseResult {\n    const range = this.getNodeRange(startPos, endPos);\n    const [startNode, endNode] = range;\n    const startLine = startNode ? Math.min(startNode.sourcepos![0][0], startPos[0]) : startPos[0];\n    const endLine = this.extendEndLine(\n      (endNode ? Math.max(endNode.sourcepos![1][0], endPos[0]) : endPos[0]) + lineDiff\n    );\n\n    const parseResult = this.parseRange(\n      ...this.getRangeWithRefDef(startLine, endLine, startNode, endNode, lineDiff)\n    );\n    const { newNodes, extStartNode, extEndNode } = parseResult;\n    const removedNodeRange = this.getRemovedNodeRange(extStartNode, extEndNode);\n\n    const nextNode = extEndNode ? extEndNode.next : this.root.firstChild;\n\n    if (this.referenceDefinition) {\n      this.markDeletedRefMap(extStartNode, extEndNode);\n      this.replaceRangeNodes(extStartNode, extEndNode, newNodes);\n      this.replaceWithNewRefDefState(newNodes);\n    } else {\n      this.replaceRangeNodes(extStartNode, extEndNode, newNodes);\n    }\n\n    return { nodes: newNodes, removedNodeRange, nextNode };\n  }\n\n  private parseRefLink() {\n    const result: EditResult[] = [];\n\n    if (!isEmptyObj(this.refMap)) {\n      iterateObject(this.refMap, (label, value) => {\n        if (value.unlinked) {\n          delete this.refMap[label];\n        }\n        iterateObject(this.refLinkCandidateMap, (_, candidate) => {\n          const { node, refLabel } = candidate;\n          if (refLabel === label) {\n            result.push(this.parse(node.sourcepos![0], node.sourcepos![1]));\n          }\n        });\n      });\n    }\n\n    return result;\n  }\n\n  private removeUnlinkedCandidate() {\n    if (!isEmptyObj(this.refDefCandidateMap)) {\n      [this.refLinkCandidateMap, this.refDefCandidateMap].forEach((candidateMap) => {\n        iterateObject(candidateMap, (id) => {\n          if (isUnlinked(id)) {\n            delete candidateMap[id];\n          }\n        });\n      });\n    }\n  }\n\n  editMarkdown(startPos: Pos, endPos: Pos, newText: string) {\n    const lineDiff = this.updateLineTexts(startPos, endPos, newText);\n    const parseResult = this.parse(startPos, endPos, lineDiff);\n    const editResult: EditResult = omit(parseResult, 'nextNode');\n\n    updateNextLineNumbers(parseResult.nextNode, lineDiff);\n    this.updateRootNodeState();\n\n    let result = [editResult];\n\n    if (this.referenceDefinition) {\n      this.removeUnlinkedCandidate();\n      this.replaceWithRefDefCandidate();\n      result = result.concat(this.parseRefLink());\n    }\n\n    this.trigger('change', result);\n\n    return result;\n  }\n\n  getLineTexts() {\n    return this.lineTexts;\n  }\n\n  getRootNode() {\n    return this.root;\n  }\n\n  findNodeAtPosition(pos: Pos) {\n    const node = findNodeAtPosition(this.root, pos);\n    if (!node || node === this.root) {\n      return null;\n    }\n    return node;\n  }\n\n  findFirstNodeAtLine(line: number) {\n    return findFirstNodeAtLine(this.root, line);\n  }\n\n  on(eventName: EventName, callback: () => void) {\n    this.eventHandlerMap[eventName].push(callback);\n  }\n\n  off(eventName: EventName, callback: Function) {\n    const handlers = this.eventHandlerMap[eventName];\n    const idx = handlers.indexOf(callback);\n    handlers.splice(idx, 1);\n  }\n\n  findNodeById(id: number) {\n    return findNodeById(id);\n  }\n\n  removeAllNode() {\n    removeAllNode();\n  }\n}\n"
  },
  {
    "path": "libs/toastmark/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"src/**/*.ts\", \"src/**/*.js\", \"types/**/*\", \"../../types/**/*\"],\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"resolveJsonModule\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@t/*\": [\"types/*\"]\n    },\n    \"lib\": [\"esnext\", \"dom\"]\n  }\n}"
  },
  {
    "path": "libs/toastmark/types/index.d.ts",
    "content": "export {\n  BlockNodeType,\n  InlineNodeType,\n  MdNodeType,\n  NodeWalker,\n  MdNode,\n  BlockMdNode,\n  ListData,\n  ListMdNode,\n  ListItemMdNode,\n  HeadingMdNode,\n  CodeBlockMdNode,\n  TableColumn,\n  TableMdNode,\n  TableCellMdNode,\n  CustomBlockMdNode,\n  HtmlBlockMdNode,\n  LinkMdNode,\n  CodeMdNode,\n  CustomInlineMdNode,\n  Pos as MdPos,\n  Sourcepos,\n} from './node';\nexport { ToastMark, EditResult } from './toastMark';\nexport {\n  HTMLConvertor,\n  HTMLConvertorMap,\n  RendererOptions,\n  Context,\n  OpenTagToken,\n  CloseTagToken,\n  TextToken,\n  RawHTMLToken,\n  HTMLToken,\n  HTMLRenderer as Renderer,\n} from './renderer';\nexport { ParserOptions, BlockParser as Parser, CustomParserMap } from './parser';\n"
  },
  {
    "path": "libs/toastmark/types/node.d.ts",
    "content": "export type BlockNodeType =\n  | 'document'\n  | 'list'\n  | 'blockQuote'\n  | 'item'\n  | 'heading'\n  | 'thematicBreak'\n  | 'paragraph'\n  | 'codeBlock'\n  | 'htmlBlock'\n  | 'table'\n  | 'tableHead'\n  | 'tableBody'\n  | 'tableRow'\n  | 'tableCell'\n  | 'tableDelimRow'\n  | 'tableDelimCell'\n  | 'refDef'\n  | 'customBlock'\n  | 'frontMatter';\n\nexport type InlineNodeType =\n  | 'code'\n  | 'text'\n  | 'emph'\n  | 'strong'\n  | 'strike'\n  | 'link'\n  | 'image'\n  | 'htmlInline'\n  | 'linebreak'\n  | 'softbreak'\n  | 'customInline';\n\nexport type MdNodeType = BlockNodeType | InlineNodeType;\n\nexport type Pos = [number, number];\nexport type Sourcepos = [Pos, Pos];\n\nexport interface NodeWalker {\n  current: MdNode | null;\n  root: MdNode;\n  entering: boolean;\n\n  next(): { entering: boolean; node: MdNode } | null;\n  resumeAt(node: MdNode, entering: boolean): void;\n}\n\nexport interface MdNode {\n  type: MdNodeType;\n  id: number;\n  parent: MdNode | null;\n  prev: MdNode | null;\n  next: MdNode | null;\n  sourcepos?: Sourcepos;\n  firstChild: MdNode | null;\n  lastChild: MdNode | null;\n  literal: string | null;\n\n  isContainer(): boolean;\n  unlink(): void;\n  replaceWith(node: MdNode): void;\n  insertAfter(node: MdNode): void;\n  insertBefore(node: MdNode): void;\n  appendChild(child: MdNode): void;\n  prependChild(child: MdNode): void;\n  walker(): NodeWalker;\n}\n\nexport interface BlockMdNode extends MdNode {\n  type: BlockNodeType;\n\n  // temporal data (for parsing)\n  open: boolean;\n  lineOffsets: number[] | null;\n  stringContent: string | null;\n  lastLineBlank: boolean;\n  lastLineChecked: boolean;\n}\n\nexport interface ListData {\n  type: 'ordered' | 'bullet';\n  tight: boolean;\n  start: number;\n  bulletChar: string;\n  delimiter: string;\n  markerOffset: number;\n  padding: number;\n  task: boolean;\n  checked: boolean;\n}\n\nexport interface ListMdNode extends BlockMdNode {\n  listData: ListData | null;\n}\n\nexport interface ListItemMdNode extends BlockMdNode {\n  parent: MdNode;\n  listData: ListData;\n}\n\nexport interface HeadingMdNode extends BlockMdNode {\n  level: number;\n  headingType: 'atx' | 'setext';\n}\n\nexport interface CodeBlockMdNode extends BlockMdNode {\n  fenceOffset: number;\n  fenceLength: number;\n  fenceChar: string | null;\n  info: string | null;\n  infoPadding: number;\n}\n\nexport interface TableColumn {\n  align: 'left' | 'center' | 'right' | null;\n}\n\nexport interface TableMdNode extends BlockMdNode {\n  columns: TableColumn[];\n}\n\nexport interface TableCellMdNode extends BlockMdNode {\n  startIdx: number;\n  endIdx: number;\n  paddingLeft: number;\n  paddingRight: number;\n  ignored: boolean;\n  attrs?: Record<string, any>;\n}\n\nexport interface CustomBlockMdNode extends BlockMdNode {\n  disabledEntityParse?: boolean;\n}\n\nexport interface RefDefMdNode extends BlockMdNode {\n  title: string;\n  dest: string;\n  label: string;\n}\n\nexport interface CustomBlockMdNode extends BlockMdNode {\n  syntaxLength: number;\n  offset: number;\n  info: string;\n}\n\nexport interface HtmlBlockMdNode extends BlockMdNode {\n  htmlBlockType: number;\n}\n\nexport interface LinkMdNode extends MdNode {\n  destination: string | null;\n  title: string | null;\n  extendedAutolink: boolean;\n  lastChild: MdNode;\n}\n\nexport interface CodeMdNode extends MdNode {\n  tickCount: number;\n}\n\nexport interface CustomInlineMdNode extends MdNode {\n  info: string;\n}\n"
  },
  {
    "path": "libs/toastmark/types/parser.d.ts",
    "content": "import { BlockMdNode, BlockNodeType, MdNode, MdNodeType, RefDefMdNode, Sourcepos } from './node';\n\nexport type AutolinkParser = (\n  content: string\n) => {\n  url: string;\n  text: string;\n  range: [number, number];\n}[];\n\nexport type CustomParser = (\n  node: MdNode,\n  context: { entering: boolean; options: ParserOptions }\n) => void;\nexport type CustomParserMap = Partial<Record<MdNodeType, CustomParser>>;\n\ntype RefDefState = {\n  id: number;\n  destination: string;\n  title: string;\n  unlinked: boolean;\n  sourcepos: Sourcepos;\n};\n\nexport type RefMap = {\n  [k: string]: RefDefState;\n};\n\nexport type RefLinkCandidateMap = {\n  [k: number]: {\n    node: BlockMdNode;\n    refLabel: string;\n  };\n};\n\nexport type RefDefCandidateMap = {\n  [k: number]: RefDefMdNode;\n};\n\nexport interface ParserOptions {\n  smart: boolean;\n  tagFilter: boolean;\n  extendedAutolinks: boolean | AutolinkParser;\n  disallowedHtmlBlockTags: string[];\n  referenceDefinition: boolean;\n  disallowDeepHeading: boolean;\n  frontMatter: boolean;\n  customParser: CustomParserMap | null;\n}\n\nexport class BlockParser {\n  constructor(options?: Partial<ParserOptions>);\n\n  advanceOffset(count: number, columns?: boolean): void;\n\n  advanceNextNonspace(): void;\n\n  findNextNonspace(): void;\n\n  addLine(): void;\n\n  addChild(tag: BlockNodeType, offset: number): BlockMdNode;\n\n  closeUnmatchedBlocks(): void;\n\n  finalize(block: BlockMdNode, lineNumber: number): void;\n\n  processInlines(block: BlockMdNode): void;\n\n  incorporateLine(ln: string): void;\n\n  // The main parsing function.  Returns a parsed document AST.\n  parse(input: string, lineTexts?: string[]): MdNode;\n\n  partialParseStart(lineNumber: number, lines: string[]): MdNode;\n\n  partialParseExtends(lines: string[]): void;\n\n  partialParseFinish(): void;\n\n  setRefMaps(\n    refMap: RefMap,\n    refLinkCandidateMap: RefLinkCandidateMap,\n    refDefCandidateMap: RefDefCandidateMap\n  ): void;\n\n  clearRefMaps(): void;\n}\n"
  },
  {
    "path": "libs/toastmark/types/renderer.d.ts",
    "content": "import { MdNode, MdNodeType } from './node';\n\nexport type HTMLConvertor = (\n  node: MdNode,\n  context: Context,\n  convertors?: HTMLConvertorMap\n) => HTMLToken | HTMLToken[] | null;\n\nexport type HTMLConvertorMap = Partial<Record<MdNodeType | string, HTMLConvertor>>;\n\ninterface RendererOptions {\n  gfm: boolean;\n  softbreak: string;\n  nodeId: boolean;\n  tagFilter: boolean;\n  convertors?: HTMLConvertorMap;\n}\n\ninterface Context {\n  entering: boolean;\n  leaf: boolean;\n  options: Omit<RendererOptions, 'convertors'>;\n  getChildrenText: (node: MdNode) => string;\n  skipChildren: () => void;\n  origin?: () => ReturnType<HTMLConvertor>;\n}\n\ninterface TagToken {\n  tagName: string;\n  outerNewLine?: boolean;\n  innerNewLine?: boolean;\n}\n\nexport interface OpenTagToken extends TagToken {\n  type: 'openTag';\n  classNames?: string[];\n  attributes?: Record<string, any>;\n  selfClose?: boolean;\n}\n\nexport interface CloseTagToken extends TagToken {\n  type: 'closeTag';\n}\n\nexport interface TextToken {\n  type: 'text';\n  content: string;\n}\n\nexport interface RawHTMLToken {\n  type: 'html';\n  content: string;\n  outerNewLine?: boolean;\n}\n\nexport type HTMLToken = OpenTagToken | CloseTagToken | TextToken | RawHTMLToken;\n\nexport class HTMLRenderer {\n  constructor(customOptions?: Partial<RendererOptions>);\n\n  getConvertors(): HTMLConvertorMap;\n\n  getOptions(): RendererOptions;\n\n  render(rootNode: MdNode): string;\n\n  renderHTMLNode(node: HTMLToken): void;\n}\n"
  },
  {
    "path": "libs/toastmark/types/toastMark.d.ts",
    "content": "import { MdNode, Pos } from './node';\nimport { ParserOptions } from './parser';\n\nexport interface RemovedNodeRange {\n  id: [number, number];\n  line: [number, number];\n}\n\nexport interface EditResult {\n  nodes: MdNode[];\n  removedNodeRange: RemovedNodeRange | null;\n}\n\ntype EventName = 'change';\n\ntype EventHandlerMap = {\n  [key in EventName]: Function[];\n};\n\nexport class ToastMark {\n  constructor(contents?: string, options?: Partial<ParserOptions>);\n\n  lineTexts: string[];\n\n  editMarkdown(startPos: Pos, endPos: Pos, newText: string): EditResult[];\n\n  getLineTexts(): string[];\n\n  getRootNode(): MdNode;\n\n  findNodeAtPosition(pos: Pos): MdNode | null;\n\n  findFirstNodeAtLine(line: number): MdNode | null;\n\n  on(eventName: EventName, callback: () => void): void;\n\n  off(eventName: EventName, callback: () => void): void;\n\n  findNodeById(id: number): MdNode | null;\n\n  removeAllNode(): void;\n}\n"
  },
  {
    "path": "libs/toastmark/webpack.config.js",
    "content": "/* eslint-disable @typescript-eslint/no-var-requires */\nconst path = require('path');\nconst { merge } = require('webpack-merge');\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\nconst TerserPlugin = require('terser-webpack-plugin');\n\nconst commonConfig = {\n  entry: path.resolve(__dirname, './src/index.ts'),\n  mode: 'production',\n  module: {\n    rules: [\n      {\n        test: /\\.ts$/,\n        use: [\n          {\n            loader: 'ts-loader',\n            options: {\n              transpileOnly: true,\n            },\n          },\n        ],\n        exclude: /node_modules/,\n      },\n    ],\n  },\n  resolve: {\n    extensions: ['.ts', '.js'],\n  },\n  output: {\n    environment: {\n      arrowFunction: false,\n      const: false,\n    },\n    filename: 'toastmark.js',\n    library: {\n      type: 'commonjs',\n    },\n    publicPath: '/dist',\n    path: path.resolve(__dirname, 'dist'),\n  },\n  optimization: {\n    minimize: true,\n    minimizer: [\n      new TerserPlugin({\n        parallel: true,\n        extractComments: false,\n      }),\n    ],\n  },\n};\n\nmodule.exports = (env) => {\n  const isProduction = env.WEBPACK_BUILD;\n\n  if (isProduction) {\n    return commonConfig;\n  }\n\n  return merge(commonConfig, {\n    entry: path.resolve(__dirname, './src/__sample__/index.ts'),\n    mode: 'development',\n    devtool: 'inline-source-map',\n    output: {\n      library: {\n        type: 'umd',\n      },\n      publicPath: '/',\n      path: path.resolve(__dirname, '/'),\n    },\n    module: {\n      rules: [\n        {\n          test: /\\.css$/,\n          use: [{ loader: 'style-loader' }, { loader: 'css-loader' }],\n        },\n      ],\n    },\n    plugins: [\n      new HtmlWebpackPlugin({\n        filename: 'index.html',\n      }),\n    ],\n    devServer: {\n      open: true,\n      inline: true,\n      host: '0.0.0.0',\n      port: 8000,\n      disableHostCheck: true,\n    },\n  });\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"root\",\n  \"private\": true,\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.8.3\",\n    \"@babel/plugin-proposal-class-properties\": \"^7.8.3\",\n    \"@babel/preset-env\": \"^7.8.3\",\n    \"@rollup/plugin-commonjs\": \"^19.0.0\",\n    \"@rollup/plugin-json\": \"^4.1.0\",\n    \"@rollup/plugin-node-resolve\": \"^13.0.0\",\n    \"@rollup/plugin-typescript\": \"^8.2.1\",\n    \"@testing-library/dom\": \"^8.11.3\",\n    \"@testing-library/jest-dom\": \"^5.11.9\",\n    \"@types/common-tags\": \"^1.8.0\",\n    \"@types/jest\": \"^27.4.0\",\n    \"@types/node\": \"^17.0.14\",\n    \"@types/prosemirror-commands\": \"^1.0.3\",\n    \"@types/prosemirror-history\": \"^1.0.1\",\n    \"@types/prosemirror-inputrules\": \"^1.0.4\",\n    \"@types/prosemirror-keymap\": \"^1.0.3\",\n    \"@types/prosemirror-model\": \"^1.7.2\",\n    \"@types/prosemirror-state\": \"^1.2.5\",\n    \"@types/prosemirror-view\": \"^1.15.1\",\n    \"@typescript-eslint/eslint-plugin\": \"^4.17.0\",\n    \"@typescript-eslint/parser\": \"^4.17.0\",\n    \"command-line-args\": \"^5.1.1\",\n    \"common-tags\": \"^1.8.0\",\n    \"copy-webpack-plugin\": \"^10.2.4\",\n    \"css-loader\": \"^6.6.0\",\n    \"css-minimizer-webpack-plugin\": \"^3.4.1\",\n    \"eslint\": \"^7.22.0\",\n    \"eslint-config-prettier\": \"^8.1.0\",\n    \"eslint-config-tui\": \"^4.0.0\",\n    \"eslint-plugin-prettier\": \"^3.3.1\",\n    \"eslint-plugin-react\": \"^7.22.0\",\n    \"eslint-plugin-vue\": \"^8.4.1\",\n    \"eslint-webpack-plugin\": \"^2.5.2\",\n    \"filemanager-webpack-plugin\": \"^4.0.0\",\n    \"html-webpack-plugin\": \"^5.3.1\",\n    \"http-proxy\": \"^1.18.1\",\n    \"jest\": \"^26.6.3\",\n    \"jest-esm-transformer\": \"^1.0.0\",\n    \"jest-serializer-html\": \"^7.0.0\",\n    \"lerna\": \"^3.20.2\",\n    \"mini-css-extract-plugin\": \"^2.5.3\",\n    \"node-fetch\": \"^2.6.1\",\n    \"prettier\": \"^2.1.2\",\n    \"resize-observer-polyfill\": \"^1.5.1\",\n    \"rollup\": \"^2.52.0\",\n    \"rollup-plugin-banner\": \"^0.2.1\",\n    \"rollup-plugin-vue\": \"^5.1.9\",\n    \"snowpack\": \"^3.0.13\",\n    \"style-loader\": \"^2.0.0\",\n    \"terser-webpack-plugin\": \"^5.1.1\",\n    \"ts-jest\": \"^26.5.3\",\n    \"ts-loader\": \"^8.0.18\",\n    \"tslib\": \"^2.1.0\",\n    \"tui-code-snippet\": \"^2.3.2\",\n    \"typescript\": \"^4.2.3\",\n    \"url-loader\": \"^4.1.1\",\n    \"vm\": \"^0.1.0\",\n    \"vue-eslint-parser\": \"^8.2.0\",\n    \"webpack\": \"^5.26.0\",\n    \"webpack-bundle-analyzer\": \"^4.4.0\",\n    \"webpack-cli\": \"^4.5.0\",\n    \"webpack-dev-server\": \"^3.11.2\",\n    \"webpack-glob-entry\": \"^2.1.1\",\n    \"webpack-merge\": \"^5.7.3\"\n  },\n  \"scripts\": {\n    \"lint:all\": \"lerna run --stream lint\",\n    \"test:all\": \"jest\",\n    \"test:types:all\": \"lerna run --stream test:types\",\n    \"build:all\": \"lerna run --stream build\",\n    \"lint\": \"node ./scripts/pkg-script.js --script lint --type $type\",\n    \"test\": \"node ./scripts/pkg-script.js --script test --type $type\",\n    \"test:ci\": \"node ./scripts/pkg-script.js --script test:ci --type $type\",\n    \"test:types\": \"node ./scripts/pkg-script.js --script test:types --type $type\",\n    \"serve\": \"node ./scripts/pkg-script.js --script serve --type $type\",\n    \"serve:ie\": \"node ./scripts/pkg-script.js --script serve:ie --type $type\",\n    \"build\": \"node ./scripts/pkg-script.js --script build --type $type\",\n    \"doc:dev\": \"node ./scripts/pkg-script.js --script doc:dev --type editor\",\n    \"doc\": \"node ./scripts/pkg-script.js --script doc --type editor\",\n    \"publish:cdn\": \"node ./scripts/publish-cdn.js\"\n  },\n  \"workspaces\": [\n    \"apps/*\",\n    \"libs/*\",\n    \"plugins/*\"\n  ]\n}\n"
  },
  {
    "path": "plugins/chart/README.md",
    "content": "# TOAST UI Editor : Chart Plugin\n\n> This is a plugin of [TOAST UI Editor](https://github.com/nhn/tui.editor/tree/master/apps/editor) to render chart.\n\n[![npm version](https://img.shields.io/npm/v/@toast-ui/editor-plugin-chart.svg)](https://www.npmjs.com/package/@toast-ui/editor-plugin-chart)\n\n![chart](https://user-images.githubusercontent.com/37766175/121808323-d8d41000-cc92-11eb-9117-b92a435c9b43.png)\n\n## 🚩 Table of Contents\n\n- [Bundle File Structure](#-bundle-file-structure)\n- [Usage npm](#-usage-npm)\n- [Usage CDN](#-usage-cdn)\n\n## 📁 Bundle File Structure\n\n### Files Distributed on npm\n\n```\n- node_modules/\n  - @toast-ui/\n    - editor-plugin-chart/\n      - dist/\n        - toastui-editor-plugin-chart.js\n```\n\n### Files Distributed on CDN\n\nThe bundle files include all dependencies of this plugin.\n\n```\n- uicdn.toast.com/\n  - editor-plugin-chart/\n    - latest/\n      - toastui-editor-plugin-chart.js\n      - toastui-editor-plugin-chart.min.js\n```\n\n## 📦 Usage npm\n\nTo use the plugin, [`@toast-ui/editor`](https://github.com/nhn/tui.editor/tree/master/apps/editor) must be installed.\n\n> Ref. [Getting Started](https://github.com/nhn/tui.editor/blob/master/docs/en/getting-started.md)\n\n### Install\n\n```sh\n$ npm install @toast-ui/editor-plugin-chart\n```\n\n### Import Plugin\n\nAlong with the plugin, the plugin's dependency style must be imported. The `chart` plugin has [TOAST UI Chart](https://github.com/nhn/tui.chart) as a dependency, and you need to add a CSS file of TOAST UI Chart.\n\n#### ES Modules\n\n```js\nimport '@toast-ui/chart/dist/toastui-chart.css';\n\nimport chart from '@toast-ui/editor-plugin-chart';\n```\n\n#### CommonJS\n\n```js\nrequire('@toast-ui/chart/dist/toastui-chart.css');\n\nconst chart = require('@toast-ui/editor-plugin-chart');\n```\n\n### Create Instance\n\n#### Basic\n\n```js\n// ...\nimport '@toast-ui/chart/dist/toastui-chart.css';\n\nimport Editor from '@toast-ui/editor';\nimport chart from '@toast-ui/editor-plugin-chart';\n\nconst editor = new Editor({\n  // ...\n  plugins: [chart]\n});\n```\n\n#### With Viewer\n\n```js\n// ...\nimport '@toast-ui/chart/dist/toastui-chart.css';\n\nimport Viewer from '@toast-ui/editor/dist/toastui-editor-viewer';\nimport chart from '@toast-ui/editor-plugin-chart';\n\nconst viewer = new Viewer({\n  // ...\n  plugins: [chart]\n});\n```\n\nor\n\n```js\n// ...\nimport '@toast-ui/chart/dist/toastui-chart.css';\n\nimport Editor from '@toast-ui/editor';\nimport chart from '@toast-ui/editor-plugin-chart';\n\nconst viewer = Editor.factory({\n  // ...\n  plugins: [chart],\n  viewer: true\n});\n```\n\n## 🗂 Usage CDN\n\nTo use the plugin, the CDN files(CSS, Script) of `@toast-ui/editor` must be included.\n\n### Include Files\n\n```html\n...\n<head>\n  ...\n  <link rel=\"stylesheet\" href=\"https://uicdn.toast.com/chart/latest/toastui-chart.min.css\" />\n  ...\n</head>\n<body>\n  ...\n  <!-- Chart -->\n  <script src=\"https://uicdn.toast.com/chart/latest/toastui-chart.min.js\"></script>\n  <!-- Editor -->\n  <script src=\"https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js\"></script>\n  <!-- Editor's Plugin -->\n  <script src=\"https://uicdn.toast.com/editor-plugin-chart/latest/toastui-editor-plugin-chart.min.js\"></script>\n  ...\n</body>\n```\n\n### Create Instance\n\n#### Basic\n\n```js\nconst { Editor } = toastui;\nconst { chart } = Editor.plugin;\n\nconst editor = new Editor({\n  // ...\n  plugins: [chart]\n});\n```\n\n#### With Viewer\n\n```js\nconst Viewer = toastui.Editor;\nconst { chart } = Viewer.plugin;\n\nconst viewer = new Viewer({\n  // ...\n  plugins: [chart]\n});\n```\n\nor\n\n```js\nconst { Editor } = toastui;\nconst { chart } = Editor.plugin;\n\nconst viewer = Editor.factory({\n  // ...\n  plugins: [chart],\n  viewer: true\n});\n```\n\n### [Optional] Use Plugin with Options\n\nThe `chart` plugin can set options when used. Just add the plugin function and options related to the plugin to the array(`[pluginFn, pluginOptions]`) and push them to the `plugins` option of the editor.\n\nThe following options are available in the `chart` plugin.\nThese options are used to set the dimensions of the chart drawn in the editor.\n\n| Name        | Type             | Default Value | Description          |\n| ----------- | ---------------- | ------------- | -------------------- |\n| `width`     | `number\\|string` | `'auto'`      | Default width value  |\n| `height`    | `number\\|string` | `'auto'`      | Default height value |\n| `minWidth`  | `number`         | `0`           | Minimum width value  |\n| `minHeight` | `number`         | `0`           | Minimum height value |\n| `maxWidth`  | `number`         | `Infinity`    | Maximum width value  |\n| `maxHeight` | `number`         | `Infinity`    | Maximum height value |\n\n```js\n// ...\nimport '@toast-ui/chart/dist/toastui-chart.css';\n\nimport Editor from '@toast-ui/editor';\nimport chart from '@toast-ui/editor-plugin-chart';\n\nconst chartOptions = {\n  minWidth: 100,\n  maxWidth: 600,\n  minHeight: 100,\n  maxHeight: 300\n};\n\nconst editor = new Editor({\n  // ...\n  plugins: [[chart, chartOptions]]\n});\n```\n"
  },
  {
    "path": "plugins/chart/demo/editor.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>Editor</title>\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"http://localhost:8080/dist/cdn/toastui-editor.css\" />\n    <link rel=\"stylesheet\" href=\"https://uicdn.toast.com/chart/v4.1.4/toastui-chart.min.css\" />\n  </head>\n  <body>\n    <div class=\"code-html\">\n      <!-- Editor -->\n      <h2>Editor</h2>\n      <div id=\"editor\"></div>\n      <!-- Editor's Viewer -->\n      <h2>Viewer</h2>\n      <div id=\"viewer\"></div>\n    </div>\n    <!-- Chart -->\n    <script src=\"https://uicdn.toast.com/chart/v4.1.4/toastui-chart.js\"></script>\n    <!-- Editor -->\n    <script src=\"http://localhost:8080/dist/cdn/toastui-editor-all.js\"></script>\n    <!-- Plugin -->\n    <script src=\"../dist/cdn/toastui-editor-plugin-chart.js\"></script>\n    <script class=\"code-js\">\n      const content = [\n        '$$chart',\n        ',category1,category2',\n        'Jan,21,23',\n        'Feb,31,17',\n        '',\n        'type: column',\n        'title: Monthly Revenue',\n        'x.title: Amount',\n        'y.title: Month',\n        'y.min: 1',\n        'y.max: 40',\n        'y.suffix: $',\n        '$$'\n      ].join('\\n');\n      \n      const chartOptions = {\n        minWidth: 100,\n        maxWidth: 600,\n        minHeight: 100,\n        maxHeight: 300\n      };\n      const { Editor } = toastui;\n      const { chart } = Editor.plugin;\n\n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: content,\n        plugins: [[chart, chartOptions]]\n      });\n\n      const viewer = Editor.factory({\n        el: document.querySelector('#viewer'),\n        viewer: true,\n        height: '500px',\n        initialValue: content,\n        plugins: [[chart, chartOptions]]\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "plugins/chart/demo/esm/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>Editor</title>\n    <link rel=\"stylesheet\" href=\"https://uicdn.toast.com/chart/v4.1.4/toastui-chart.min.css\" />\n  </head>\n  <body>\n    <div class=\"code-html\">\n      <!-- Editor -->\n      <h2>Editor</h2>\n      <div id=\"editor\"></div>\n      <!-- Editor's Viewer -->\n      <h2>Viewer</h2>\n      <div id=\"viewer\"></div>\n    </div>\n    <!-- Editor -->\n    <script type=\"module\">\n      import { Editor } from 'http://localhost:8080/dist/index.js';\n      import chartPlugin from '/dist/index.js';\n\n      const content = [\n        '$$chart',\n        ',category1,category2',\n        'Jan,21,23',\n        'Feb,31,17',\n        '',\n        'type: column',\n        'title: Monthly Revenue',\n        'x.title: Amount',\n        'y.title: Month',\n        'y.min: 1',\n        'y.max: 40',\n        'y.suffix: $',\n        '$$'\n      ].join('\\n');\n\n      const chartOptions = {\n        minWidth: 100,\n        maxWidth: 600,\n        minHeight: 100,\n        maxHeight: 300\n      };\n\n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: content,\n        plugins: [[chartPlugin, chartOptions]]\n      });\n\n      const viewer = Editor.factory({\n        el: document.querySelector('#viewer'),\n        viewer: true,\n        height: '500px',\n        initialValue: content,\n        plugins: [[chartPlugin, chartOptions]]\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "plugins/chart/demo/viewer.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>Editor</title>\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"http://localhost:8080/dist/cdn/toastui-editor.css\" />\n    <link rel=\"stylesheet\" href=\"https://uicdn.toast.com/chart/v4.1.4/toastui-chart.min.css\" />\n  </head>\n  <body>\n    <div class=\"code-html\">\n      <div id=\"viewer\"></div>\n    </div>\n    <!-- Chart -->\n    <script src=\"https://uicdn.toast.com/chart/v4.1.4/toastui-chart.js\"></script>\n    <!-- Editor's Viewer -->\n    <script src=\"http://localhost:8080/dist/cdn/toastui-editor-viewer.js\"></script>\n    <!-- Plugin -->\n    <script src=\"../dist/cdn/toastui-editor-plugin-chart.js\"></script>\n    <script class=\"code-js\">\n      const content = [\n        '$$chart',\n        ',category1,category2',\n        'Jan,21,23',\n        'Feb,31,17',\n        '',\n        'type: column',\n        'title: Monthly Revenue',\n        'x.title: Amount',\n        'y.title: Month',\n        'y.min: 1',\n        'y.max: 40',\n        'y.suffix: $',\n        '$$'\n      ].join('\\n');\n      \n      const chartOptions = {\n        minWidth: 100,\n        maxWidth: 600,\n        minHeight: 100,\n        maxHeight: 300\n      };\n      const Viewer = toastui.Editor;\n      const { chart } = Viewer.plugin;\n\n      const viewer = new Viewer({\n        el: document.querySelector('#viewer'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: content,\n        plugins: [[chart, chartOptions]]\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "plugins/chart/jest.config.js",
    "content": "// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst base = require('../../jest.base.config');\n\nmodule.exports = {\n  ...base,\n  testEnvironment: 'jsdom',\n  moduleNameMapper: {\n    '^@/(.*)$': '<rootDir>/src/$1',\n  },\n};\n"
  },
  {
    "path": "plugins/chart/package.json",
    "content": "{\n  \"name\": \"@toast-ui/editor-plugin-chart\",\n  \"version\": \"3.0.1\",\n  \"description\": \"TOAST UI Editor : Chart Plugin\",\n  \"keywords\": [\n    \"nhn\",\n    \"nhn cloud\",\n    \"toast\",\n    \"toastui\",\n    \"toast-ui\",\n    \"editor\",\n    \"plugin\",\n    \"chart\"\n  ],\n  \"main\": \"dist/toastui-editor-plugin-chart.js\",\n  \"types\": \"types/index.d.ts\",\n  \"files\": [\n    \"dist/*.js\",\n    \"types/index.d.ts\"\n  ],\n  \"browserslist\": \"last 2 versions, not ie <= 10\",\n  \"author\": \"NHN Cloud FE Development Lab <dl_javascript@nhn.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/nhn/tui.editor.git\",\n    \"directory\": \"plugins/chart\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/nhn/tui.editor/issues\"\n  },\n  \"homepage\": \"https://ui.toast.com\",\n  \"scripts\": {\n    \"lint\": \"eslint .\",\n    \"test:types\": \"tsc\",\n    \"test\": \"jest --watch\",\n    \"test:ci\": \"jest\",\n    \"serve\": \"snowpack dev\",\n    \"serve:ie\": \"webpack serve\",\n    \"build:cdn\": \"webpack build --env cdn & webpack build --env cdn minify\",\n    \"build\": \"webpack build && npm run build:cdn\"\n  },\n  \"devDependencies\": {\n    \"buffer\": \"^6.0.3\",\n    \"jest-canvas-mock\": \"^2.3.1\",\n    \"os-browserify\": \"^0.3.0\",\n    \"stream-browserify\": \"^3.0.0\"\n  },\n  \"dependencies\": {\n    \"@toast-ui/chart\": \"^4.1.4\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  }\n}\n"
  },
  {
    "path": "plugins/chart/snowpack.config.js",
    "content": "// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst httpProxy = require('http-proxy');\nconst proxy = httpProxy.createServer({ target: 'http://localhost:8080' });\n\n/** @type {import(\"snowpack\").SnowpackUserConfig } */\nmodule.exports = {\n  mount: {\n    'demo/esm': '/',\n    src: '/dist',\n  },\n  devOptions: {\n    port: 8081,\n  },\n  routes: [\n    {\n      src: '/img/.*',\n      dest: (req, res) => {\n        proxy.web(req, res);\n      },\n    },\n  ],\n  alias: {\n    '@': './src',\n    '@t': './types',\n  },\n};\n"
  },
  {
    "path": "plugins/chart/src/__test__/unit/chartPlugin.spec.ts",
    "content": "import 'jest-canvas-mock';\nimport { PluginOptions } from '@t/index';\nimport {\n  parseToChartOption,\n  parseToChartData,\n  detectDelimiter,\n  setDefaultOptions,\n  ChartOptions,\n} from '@/index';\n\ndescribe('parseToChartOption()', () => {\n  it('should parse option code into object', () => {\n    expect(\n      parseToChartOption(`\n          key1.keyA: value1\n          key1.keyB: value2\n        `)\n    ).toEqual({\n      key1: {\n        keyA: 'value1',\n        keyB: 'value2',\n      },\n    });\n  });\n\n  it('should parse option code into object with reserved keys(type, url)', () => {\n    // type & url -> editor.Chart & editorChart.url\n    expect(\n      parseToChartOption(`\n          type: line\n          url: http://some.url/to/data/file\n        `)\n    ).toEqual({\n      editorChart: {\n        type: 'line',\n        url: 'http://some.url/to/data/file',\n      },\n    });\n  });\n\n  it('should parse option code into object with 1 depth keys(without dot)', () => {\n    // keyA & keyB ... -> chart.keyA, chart.keyB ...\n    expect(\n      parseToChartOption(`\n          keyA: value1\n          keyB: value2\n        `)\n    ).toEqual({\n      chart: {\n        keyA: 'value1',\n        keyB: 'value2',\n      },\n    });\n  });\n\n  it('should parse option code into object with x & y keys', () => {\n    // x & y keys should be translated to xAxis & yAxis\n    expect(\n      parseToChartOption(`\n          x.keyA: value1\n          y.keyB: value2\n        `)\n    ).toEqual({\n      xAxis: {\n        keyA: 'value1',\n      },\n      yAxis: {\n        keyB: 'value2',\n      },\n    });\n  });\n\n  it('should parse option code into object with string numeric value', () => {\n    expect(\n      parseToChartOption(`\n          key1.keyA: 1.234\n          key1.keyB: 12\n        `)\n    ).toEqual({\n      key1: {\n        keyA: 1.234,\n        keyB: 12,\n      },\n    });\n  });\n\n  it('should parse option code into object with string array value', () => {\n    expect(\n      parseToChartOption(`\n          key1.keyA: [1,2]\n          key1.keyB: [\"a\", \"b\"]\n        `)\n    ).toEqual({\n      key1: {\n        keyA: [1, 2],\n        keyB: ['a', 'b'],\n      },\n    });\n  });\n\n  it('should parse option code into object with string object value', () => {\n    expect(\n      parseToChartOption(`\n          key1.keyA: {\"k1\": \"v1\"}\n          key1.keyB: {\"k2\": \"v2\"}\n        `)\n    ).toEqual({\n      key1: {\n        keyA: {\n          k1: 'v1',\n        },\n        keyB: {\n          k2: 'v2',\n        },\n      },\n    });\n  });\n});\n\ndescribe('parseToChartData()', () => {\n  it('should parse csv to @toast-ui/chart data format', () => {\n    expect(\n      parseToChartData(\n        `\n            ,series a,series b\n            category 1, 1.234, 2.345\n            category 2, 3.456, 4.567\n          `,\n        ','\n      )\n    ).toEqual({\n      categories: ['category 1', 'category 2'],\n      series: [\n        {\n          name: 'series a',\n          data: [1.234, 3.456],\n        },\n        {\n          name: 'series b',\n          data: [2.345, 4.567],\n        },\n      ],\n    });\n  });\n\n  it('should parse tsv to @toast-ui/chart data format', () => {\n    expect(\n      parseToChartData(\n        `\n            \\tseries a\\tseries b\n            category 1\\t1.234\\t2.345\n            category 2\\t3.456\\t4.567\n          `,\n        '\\t'\n      )\n    ).toEqual({\n      categories: ['category 1', 'category 2'],\n      series: [\n        {\n          name: 'series a',\n          data: [1.234, 3.456],\n        },\n        {\n          name: 'series b',\n          data: [2.345, 4.567],\n        },\n      ],\n    });\n  });\n\n  it('should parse whitespace separated values to @toast-ui/chart data format', () => {\n    expect(\n      parseToChartData(\n        ['\\t\"series a\" \"series b\"', '\"category 1\" 1.234 2.345', '\"category 2\" 3.456 4.567'].join(\n          '\\n'\n        ),\n        /\\s+/\n      )\n    ).toEqual({\n      categories: ['category 1', 'category 2'],\n      series: [\n        {\n          name: 'series a',\n          data: [1.234, 3.456],\n        },\n        {\n          name: 'series b',\n          data: [2.345, 4.567],\n        },\n      ],\n    });\n  });\n\n  it('should parse data with legends to @toast-ui/chart data format', () => {\n    expect(\n      parseToChartData(\n        `\n            series a,series b\n            1.234, 2.345\n            3.456, 4.567\n          `,\n        ','\n      )\n    ).toEqual({\n      categories: [],\n      series: [\n        {\n          name: 'series a',\n          data: [1.234, 3.456],\n        },\n        {\n          name: 'series b',\n          data: [2.345, 4.567],\n        },\n      ],\n    });\n  });\n\n  it('should parse data with categories to @toast-ui/chart data format', () => {\n    expect(\n      parseToChartData(\n        `\n            category 1, 1.234, 2.345\n            category 2, 3.456, 4.567\n          `,\n        ','\n      )\n    ).toEqual({\n      categories: ['category 1', 'category 2'],\n      series: [\n        {\n          data: [1.234, 3.456],\n        },\n        {\n          data: [2.345, 4.567],\n        },\n      ],\n    });\n  });\n});\n\ndescribe('detectDelimiter()', () => {\n  it('should detect csv', () => {\n    expect(\n      detectDelimiter(`\n          ,series a,series b\n          category 1, 1.234, 2.345\n          category 2, 3.456, 4.567\n        `)\n    ).toEqual(',');\n  });\n\n  it('should detect tsv', () => {\n    expect(\n      detectDelimiter(`\n          \\tseries a\\tseries b\n          category 1\\t1.234\\t2.345\n          category 2\\t3.456\\t4.567\n        `)\n    ).toEqual('\\t');\n  });\n\n  it('should detect regex', () => {\n    expect(\n      detectDelimiter(\n        ['\\t\"series a\" \"series b\"', '\"category 1\"\\t1.234 2.345', '\"category 2\" 3.456 4.567'].join(\n          '\\n'\n        )\n      )\n    ).toEqual(/\\s+/);\n  });\n});\n\ndescribe('setDefaultOptions', () => {\n  let container: HTMLElement;\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('should respect default min/max width/height', () => {\n    const chartOptions = setDefaultOptions(\n      {\n        chart: {\n          width: -10,\n          height: -10,\n        },\n      } as ChartOptions,\n      {} as PluginOptions,\n      container\n    );\n\n    expect(chartOptions.chart!.width).toBe(0);\n    expect(chartOptions.chart!.height).toBe(0);\n  });\n\n  it('should respect default width/height', () => {\n    const chartOptions = setDefaultOptions(\n      {} as ChartOptions,\n      {\n        width: 300,\n        height: 400,\n      } as PluginOptions,\n      container\n    );\n\n    expect(chartOptions.chart!.width).toBe(300);\n    expect(chartOptions.chart!.height).toBe(400);\n  });\n\n  it('should use width/height from codeblock', () => {\n    const pluginOptions = {\n      minWidth: 300,\n      minHeight: 400,\n      maxWidth: 700,\n      maxHeight: 800,\n      width: 400,\n      height: 500,\n    };\n    const chartOptions = setDefaultOptions(\n      {\n        chart: {\n          width: 500,\n          height: 600,\n        },\n      } as ChartOptions,\n      pluginOptions,\n      container\n    );\n\n    expect(chartOptions.chart!.width).toBe(500);\n    expect(chartOptions.chart!.height).toBe(600);\n  });\n\n  it('should respect min/max width/height', () => {\n    const pluginOptions = {\n      minWidth: 300,\n      minHeight: 400,\n      maxWidth: 700,\n      maxHeight: 800,\n    } as PluginOptions;\n    let chartOptions = setDefaultOptions(\n      {\n        chart: {\n          width: 200,\n          height: 200,\n        },\n      } as ChartOptions,\n      pluginOptions,\n      container\n    );\n\n    expect(chartOptions.chart!.width).toBe(300);\n    expect(chartOptions.chart!.height).toBe(400);\n\n    chartOptions = setDefaultOptions(\n      {\n        chart: {\n          width: 1000,\n          height: 1000,\n        },\n      } as ChartOptions,\n      pluginOptions,\n      container\n    );\n    expect(chartOptions.chart!.width).toBe(700);\n    expect(chartOptions.chart!.height).toBe(800);\n  });\n});\n"
  },
  {
    "path": "plugins/chart/src/csv.js",
    "content": "/* eslint-disable */\n/*\n CSV-JS - A Comma-Separated Values parser for JS\n\n Built to rfc4180 standard, with options for adjusting strictness:\n    - optional carriage returns for non-microsoft sources\n    - automatically type-cast numeric an boolean values\n    - relaxed mode which: ignores blank lines, ignores gargabe following quoted tokens, does not enforce a consistent record length\n\n Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php\n\n Permission is hereby granted, free of charge, to any person obtaining a copy\n of this software and associated documentation files (the \"Software\"), to deal\n in the Software without restriction, including without limitation the rights\n to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the Software is\n furnished to do so, subject to the following conditions:\n\n The above copyright notice and this permission notice shall be included in\n all copies or substantial portions of the Software.\n\n Author Greg Kindel (twitter @gkindel), 2014\n */\n/**\n * @modifier NHN Cloud FE Development Lab <dl_javascript@nhn.com>\n */\n\n'use strict';\n/**\n * @name CSV\n * @namespace\n * @ignore\n */\n// implemented as a singleton because JS is single threaded\nvar CSV = {};\nCSV.RELAXED = false;\nCSV.IGNORE_RECORD_LENGTH = false;\nCSV.IGNORE_QUOTES = false;\nCSV.LINE_FEED_OK = true;\nCSV.CARRIAGE_RETURN_OK = true;\nCSV.DETECT_TYPES = true;\nCSV.IGNORE_QUOTE_WHITESPACE = true;\nCSV.DEBUG = false;\n\nCSV.COLUMN_SEPARATOR = ',';\n\nCSV.ERROR_EOF = 'UNEXPECTED_END_OF_FILE';\nCSV.ERROR_CHAR = 'UNEXPECTED_CHARACTER';\nCSV.ERROR_EOL = 'UNEXPECTED_END_OF_RECORD';\nCSV.WARN_SPACE = 'UNEXPECTED_WHITESPACE'; // not per spec, but helps debugging\n\nvar QUOTE = '\"',\n  CR = '\\r',\n  LF = '\\n',\n  SPACE = ' ',\n  TAB = '\\t';\n\n// states\nvar PRE_TOKEN = 0,\n  MID_TOKEN = 1,\n  POST_TOKEN = 2,\n  POST_RECORD = 4;\n/**\n * @name CSV.parse\n * @function\n * @description rfc4180 standard csv parse\n * with options for strictness and data type conversion\n * By default, will automatically type-cast numeric an boolean values.\n * @param {String} str A CSV string\n * @return {Array} An array records, each of which is an array of scalar values.\n * @example\n * // simple\n * var rows = CSV.parse(\"one,two,three\\nfour,five,six\")\n * // rows equals [[\"one\",\"two\",\"three\"],[\"four\",\"five\",\"six\"]]\n * @example\n * // Though not a jQuery plugin, it is recommended to use with the $.ajax pipe() method:\n * $.get(\"csv.txt\")\n *    .pipe( CSV.parse )\n *    .done( function(rows) {\n *        for( var i =0; i < rows.length; i++){\n *            console.log(rows[i])\n *        }\n *  });\n * @see http://www.ietf.org/rfc/rfc4180.txt\n */\nCSV.parse = function (str) {\n  var result = (CSV.result = []);\n  CSV.COLUMN_SEPARATOR =\n    CSV.COLUMN_SEPARATOR instanceof RegExp\n      ? new RegExp('^' + CSV.COLUMN_SEPARATOR.source)\n      : CSV.COLUMN_SEPARATOR;\n\n  CSV.offset = 0;\n  CSV.str = str;\n  CSV.record_begin();\n\n  CSV.debug('parse()', str);\n\n  var c;\n  while (1) {\n    // pull char\n    c = str[CSV.offset++];\n    CSV.debug('c', c);\n\n    // detect eof\n    if (c == null) {\n      if (CSV.escaped) {\n        CSV.error(CSV.ERROR_EOF);\n      }\n\n      if (CSV.record) {\n        CSV.token_end();\n        CSV.record_end();\n      }\n\n      CSV.debug('...bail', c, CSV.state, CSV.record);\n      CSV.reset();\n      break;\n    }\n\n    if (CSV.record == null) {\n      // if relaxed mode, ignore blank lines\n      if (CSV.RELAXED && (c == LF || (c == CR && str[CSV.offset + 1] == LF))) {\n        continue;\n      }\n      CSV.record_begin();\n    }\n\n    // pre-token: look for start of escape sequence\n    if (CSV.state == PRE_TOKEN) {\n      if ((c === SPACE || c === TAB) && CSV.next_nonspace() == QUOTE) {\n        if (CSV.RELAXED || CSV.IGNORE_QUOTE_WHITESPACE) {\n          continue;\n        } else {\n          // not technically an error, but ambiguous and hard to debug otherwise\n          CSV.warn(CSV.WARN_SPACE);\n        }\n      }\n\n      if (c == QUOTE && !CSV.IGNORE_QUOTES) {\n        CSV.debug('...escaped start', c);\n        CSV.escaped = true;\n        CSV.state = MID_TOKEN;\n        continue;\n      }\n      CSV.state = MID_TOKEN;\n    }\n\n    // mid-token and escaped, look for sequences and end quote\n    if (CSV.state == MID_TOKEN && CSV.escaped) {\n      if (c == QUOTE) {\n        if (str[CSV.offset] == QUOTE) {\n          CSV.debug('...escaped quote', c);\n          CSV.token += QUOTE;\n          CSV.offset++;\n        } else {\n          CSV.debug('...escaped end', c);\n          CSV.escaped = false;\n          CSV.state = POST_TOKEN;\n        }\n      } else {\n        CSV.token += c;\n        CSV.debug('...escaped add', c, CSV.token);\n      }\n      continue;\n    }\n\n    // fall-through: mid-token or post-token, not escaped\n    if (c == CR) {\n      if (str[CSV.offset] == LF) CSV.offset++;\n      else if (!CSV.CARRIAGE_RETURN_OK) CSV.error(CSV.ERROR_CHAR);\n      CSV.token_end();\n      CSV.record_end();\n    } else if (c == LF) {\n      if (!(CSV.LINE_FEED_OK || CSV.RELAXED)) CSV.error(CSV.ERROR_CHAR);\n      CSV.token_end();\n      CSV.record_end();\n    } else if (CSV.test_regex_separator(str) || CSV.COLUMN_SEPARATOR == c) {\n      CSV.token_end();\n    } else if (CSV.state == MID_TOKEN) {\n      CSV.token += c;\n      CSV.debug('...add', c, CSV.token);\n    } else if (c === SPACE || c === TAB) {\n      if (!CSV.IGNORE_QUOTE_WHITESPACE) CSV.error(CSV.WARN_SPACE);\n    } else if (!CSV.RELAXED) {\n      CSV.error(CSV.ERROR_CHAR);\n    }\n  }\n  return result;\n};\n\n/**\n * @name CSV.stream\n * @function\n * @description stream a CSV file\n * @example\n * node -e \"c=require('CSV-JS');require('fs').createReadStream('csv.txt').pipe(c.stream()).pipe(c.stream.json()).pipe(process.stdout)\"\n * @ignore\n */\nCSV.stream = function () {\n  var stream = require('stream');\n  var s = new stream.Transform({ objectMode: true });\n  s.EOL = '\\n';\n  s.prior = '';\n  s.emitter = (function (s) {\n    return function (e) {\n      s.push(CSV.parse(e + s.EOL));\n    };\n  })(s);\n\n  s._transform = function (chunk, encoding, done) {\n    var lines =\n      this.prior == ''\n        ? chunk.toString().split(this.EOL)\n        : (this.prior + chunk.toString()).split(this.EOL);\n    this.prior = lines.pop();\n    lines.forEach(this.emitter);\n    done();\n  };\n\n  s._flush = function (done) {\n    if (this.prior != '') {\n      this.emitter(this.prior);\n      this.prior = '';\n    }\n    done();\n  };\n  return s;\n};\n\nCSV.test_regex_separator = function (str) {\n  if (!(CSV.COLUMN_SEPARATOR instanceof RegExp)) {\n    return false;\n  }\n\n  var match;\n  str = str.slice(CSV.offset - 1);\n  match = CSV.COLUMN_SEPARATOR.exec(str);\n  if (match) {\n    CSV.offset += match[0].length - 1;\n  }\n\n  return match !== null;\n};\n\nCSV.stream.json = function () {\n  var os = require('os');\n  var stream = require('stream');\n  var s = new streamTransform({ objectMode: true });\n  s._transform = function (chunk, encoding, done) {\n    s.push(JSON.stringify(chunk.toString()) + os.EOL);\n    done();\n  };\n  return s;\n};\n\nCSV.reset = function () {\n  CSV.state = null;\n  CSV.token = null;\n  CSV.escaped = null;\n  CSV.record = null;\n  CSV.offset = null;\n  CSV.result = null;\n  CSV.str = null;\n};\n\nCSV.next_nonspace = function () {\n  var i = CSV.offset;\n  var c;\n  while (i < CSV.str.length) {\n    c = CSV.str[i++];\n    if (!(c == SPACE || c === TAB)) {\n      return c;\n    }\n  }\n  return null;\n};\n\nCSV.record_begin = function () {\n  CSV.escaped = false;\n  CSV.record = [];\n  CSV.token_begin();\n  CSV.debug('record_begin');\n};\n\nCSV.record_end = function () {\n  CSV.state = POST_RECORD;\n  if (\n    !(CSV.IGNORE_RECORD_LENGTH || CSV.RELAXED) &&\n    CSV.result.length > 0 &&\n    CSV.record.length != CSV.result[0].length\n  ) {\n    CSV.error(CSV.ERROR_EOL);\n  }\n  CSV.result.push(CSV.record);\n  CSV.debug('record end', CSV.record);\n  CSV.record = null;\n};\n\nCSV.resolve_type = function (token) {\n  if (token.match(/^[-+]?[0-9]+(\\.[0-9]+)?([eE][-+]?[0-9]+)?$/)) {\n    token = parseFloat(token);\n  } else if (token.match(/^(true|false)$/i)) {\n    token = Boolean(token.match(/true/i));\n  } else if (token === 'undefined') {\n    token = undefined;\n  } else if (token === 'null') {\n    token = null;\n  }\n  return token;\n};\n\nCSV.token_begin = function () {\n  CSV.state = PRE_TOKEN;\n  // considered using array, but http://www.sitepen.com/blog/2008/05/09/string-performance-an-analysis/\n  CSV.token = '';\n};\n\nCSV.token_end = function () {\n  if (CSV.DETECT_TYPES) {\n    CSV.token = CSV.resolve_type(CSV.token);\n  }\n  CSV.record.push(CSV.token);\n  CSV.debug('token end', CSV.token);\n  CSV.token_begin();\n};\n\nCSV.debug = function () {\n  if (CSV.DEBUG) console.log(arguments);\n};\n\nCSV.dump = function (msg) {\n  return [\n    msg,\n    'at char',\n    CSV.offset,\n    ':',\n    CSV.str\n      .substr(CSV.offset - 50, 50)\n      .replace(/\\r/gm, '\\\\r')\n      .replace(/\\n/gm, '\\\\n')\n      .replace(/\\t/gm, '\\\\t'),\n  ].join(' ');\n};\n\nCSV.error = function (err) {\n  var msg = CSV.dump(err);\n  CSV.reset();\n  throw msg;\n};\n\nCSV.warn = function (err) {\n  if (!CSV.DEBUG) {\n    return;\n  }\n\n  var msg = CSV.dump(err);\n  try {\n    console.warn(msg);\n    return;\n  } catch (e) {}\n\n  try {\n    console.log(msg);\n  } catch (e) {}\n};\n\nexport default CSV;\n"
  },
  {
    "path": "plugins/chart/src/index.ts",
    "content": "/**\n * @example\n * $$chart\n * \\tcat1\\tcat2                => tsv, csv format chart data\n * jan\\t21\\t23\n * feb\\t351\\t45\n * // url: http://url.to/csv   => fetch data from the url when not using plain data\n *                             => space required as a separator\n * type: area                  => tui.chart.areaChart()\n * width: 700                  => chart.width\n * height: 300                 => chart.height\n * title: Monthly Revenue      => chart.title\n * format: 1000                => chart.format\n * x.title: Amount             => xAxis.title\n * x.min: 0                    => xAxis.min\n * x.max 9000                  => xAxis.max\n * x.suffix: $                 => xAxis.suffix\n * y.title: Month              => yAxis.title\n * $$\n */\nimport type { PluginInfo, MdNode, PluginContext } from '@toast-ui/editor';\nimport Chart, {\n  BaseOptions,\n  LineChart,\n  AreaChart,\n  BarChart,\n  PieChart,\n  ColumnChart,\n} from '@toast-ui/chart';\nimport isString from 'tui-code-snippet/type/isString';\nimport isUndefined from 'tui-code-snippet/type/isUndefined';\nimport inArray from 'tui-code-snippet/array/inArray';\nimport extend from 'tui-code-snippet/object/extend';\n// @ts-ignore\nimport ajax from 'tui-code-snippet/ajax/index.js';\n\nimport { PluginOptions } from '@t/index';\nimport csv from './csv';\nimport { trimKeepingTabs, isNumeric, clamp } from './util';\n\n// csv configuration\ncsv.IGNORE_QUOTE_WHITESPACE = false;\ncsv.IGNORE_RECORD_LENGTH = true;\ncsv.DETECT_TYPES = false;\n\nconst reEOL = /[\\n\\r]/;\nconst reGroupByDelimiter = /([^:]+)?:?(.*)/;\nconst DEFAULT_DELIMITER = /\\s+/;\nconst DELIMITERS = [',', '\\t'];\nconst MINIMUM_DELIM_CNT = 2;\nconst SUPPORTED_CHART_TYPES = ['bar', 'column', 'line', 'area', 'pie'];\nconst CATEGORY_CHART_TYPES = ['line', 'area'];\nconst DEFAULT_DIMENSION_OPTIONS = {\n  minWidth: 0,\n  maxWidth: Infinity,\n  minHeight: 0,\n  maxHeight: Infinity,\n  height: 'auto',\n  width: 'auto',\n};\nconst RESERVED_KEYS = ['type', 'url'];\nconst chart = {\n  bar: Chart.barChart,\n  column: Chart.columnChart,\n  area: Chart.areaChart,\n  line: Chart.lineChart,\n  pie: Chart.pieChart,\n};\nconst chartMap: Record<string, ChartInstance> = {};\n\ntype ChartType = keyof typeof chart;\nexport type ChartOptions = BaseOptions & { editorChart: { type?: ChartType; url?: string } };\ntype ChartInstance = BarChart | ColumnChart | AreaChart | LineChart | PieChart;\ntype ChartData = {\n  categories: string[];\n  series: { data: number[]; name?: string }[];\n};\ntype ParserCallback = (parsedInfo?: { data: ChartData; options?: ChartOptions }) => void;\ntype OnSuccess = (res: { data: any }) => void;\n\nexport function parse(text: string, callback: ParserCallback) {\n  text = trimKeepingTabs(text);\n  const [firstTexts, secondTexts] = text.split(/\\n{2,}/);\n  const urlOptions = parseToChartOption(firstTexts);\n  const url = urlOptions?.editorChart?.url;\n\n  // if first text is `options` and has `url` option, fetch data from url\n  if (isString(url)) {\n    // url option provided\n    // fetch data from url\n    const success: OnSuccess = ({ data }) => {\n      callback({ data: parseToChartData(data), options: parseToChartOption(firstTexts) });\n    };\n    const error = () => callback();\n\n    ajax.get(url, { success, error });\n  } else {\n    const data = parseToChartData(firstTexts);\n    const options = parseToChartOption(secondTexts);\n\n    callback({ data, options });\n  }\n}\n\nexport function detectDelimiter(text: string) {\n  let delimiter: string | RegExp = DEFAULT_DELIMITER;\n  let delimCnt = 0;\n\n  text = trimKeepingTabs(text);\n\n  DELIMITERS.forEach((delim) => {\n    const matched = text.match(new RegExp(delim, 'g'))!;\n\n    if (matched?.length > Math.max(MINIMUM_DELIM_CNT, delimCnt)) {\n      delimiter = delim;\n      delimCnt = matched.length;\n    }\n  });\n\n  return delimiter;\n}\n\nexport function parseToChartData(text: string, delimiter?: string | RegExp) {\n  // trim all heading/trailing blank lines\n  text = trimKeepingTabs(text);\n\n  // @ts-ignore\n  csv.COLUMN_SEPARATOR = delimiter || detectDelimiter(text);\n  let dsv: string[][] = csv.parse(text);\n\n  // trim all values in 2D array\n  dsv = dsv.map((arr) => arr.map((val) => val.trim()));\n\n  // test a first row for legends. ['anything', '1', '2', '3'] === false, ['anything', 't1', '2', 't3'] === true\n  const hasLegends = dsv[0]\n    .filter((_, i) => i > 0)\n    .reduce((hasNaN, item) => hasNaN || !isNumeric(item), false);\n  const legends = hasLegends ? dsv.shift()! : [];\n\n  // test a first column for categories\n  const hasCategories = dsv.slice(1).reduce((hasNaN, row) => hasNaN || !isNumeric(row[0]), false);\n  const categories = hasCategories ? dsv.map((arr) => arr.shift()!) : [];\n\n  if (hasCategories) {\n    legends.shift();\n  }\n\n  // transpose dsv, parse number\n  // [['1','2','3']    [[1,4,7]\n  //  ['4','5','6'] =>  [2,5,8]\n  //  ['7','8','9']]    [3,6,9]]\n  const tdsv = dsv[0].map((_, i) => dsv.map((x) => parseFloat(x[i])));\n\n  // make series\n  const series = tdsv.map((data, i) =>\n    hasLegends\n      ? {\n          name: legends[i],\n          data,\n        }\n      : {\n          data,\n        }\n  );\n\n  return { categories, series };\n}\n\nfunction createOptionKeys(keyString: string) {\n  const keys = keyString.trim().split('.');\n  const [topKey] = keys;\n\n  if (inArray(topKey, RESERVED_KEYS) >= 0) {\n    // reserved keys for chart plugin option\n    keys.unshift('editorChart');\n  } else if (keys.length === 1) {\n    // short names for `chart`\n    keys.unshift('chart');\n  } else if (topKey === 'x' || topKey === 'y') {\n    // short-handed keys\n    keys[0] = `${topKey}Axis`;\n  }\n\n  return keys;\n}\n\nexport function parseToChartOption(text: string) {\n  const options: Record<string, any> = {};\n\n  if (!isUndefined(text)) {\n    const lineTexts = text.split(reEOL);\n\n    lineTexts.forEach((lineText) => {\n      const matched = lineText.match(reGroupByDelimiter);\n\n      if (matched) {\n        // keyString can be nested object keys\n        // ex) key1.key2.key3: value\n        // eslint-disable-next-line prefer-const\n        let [, keyString, value] = matched;\n\n        if (value) {\n          try {\n            value = JSON.parse(value.trim());\n          } catch (e) {\n            value = value.trim();\n          }\n\n          const keys = createOptionKeys(keyString);\n          let refOptions = options;\n\n          keys.forEach((key, index) => {\n            refOptions[key] = refOptions[key] || (keys.length - 1 === index ? value : {});\n            // should change the ref option object to assign nested property\n            refOptions = refOptions[key];\n          });\n        }\n      }\n    });\n  }\n\n  return options as ChartOptions;\n}\n\nfunction getAdjustedDimension(size: 'auto' | number, containerWidth: number) {\n  return size === 'auto' ? containerWidth : size;\n}\n\nfunction getChartDimension(\n  chartOptions: ChartOptions,\n  pluginOptions: PluginOptions,\n  chartContainer: HTMLElement\n) {\n  const dimensionOptions = extend({ ...DEFAULT_DIMENSION_OPTIONS }, pluginOptions);\n  const { maxWidth, minWidth, maxHeight, minHeight } = dimensionOptions;\n  // if no width or height specified, set width and height to container width\n  const { width: containerWidth } = chartContainer.getBoundingClientRect();\n  let { width = dimensionOptions.width, height = dimensionOptions.height } = chartOptions.chart!;\n\n  width = getAdjustedDimension(width, containerWidth);\n  height = getAdjustedDimension(height, containerWidth);\n\n  return {\n    width: clamp(width, minWidth, maxWidth),\n    height: clamp(height, minHeight, maxHeight),\n  };\n}\n\nexport function setDefaultOptions(\n  chartOptions: ChartOptions,\n  pluginOptions: PluginOptions,\n  chartContainer: HTMLElement\n) {\n  chartOptions = extend(\n    {\n      editorChart: {},\n      chart: {},\n      exportMenu: {},\n    },\n    chartOptions\n  );\n\n  const { width, height } = getChartDimension(chartOptions, pluginOptions, chartContainer);\n\n  chartOptions.chart!.width = width;\n  chartOptions.chart!.height = height;\n\n  // default chart type\n  chartOptions.editorChart.type = chartOptions.editorChart.type || 'column';\n  // default visibility of export menu\n  chartOptions.exportMenu!.visible = !!chartOptions.exportMenu!.visible;\n\n  return chartOptions;\n}\n\nfunction destroyChart() {\n  Object.keys(chartMap).forEach((id) => {\n    const container = document.querySelector<HTMLElement>(`[data-chart-id=${id}]`);\n\n    if (!container) {\n      chartMap[id].destroy();\n\n      delete chartMap[id];\n    }\n  });\n}\n\nfunction renderChart(\n  id: string,\n  text: string,\n  usageStatistics: boolean,\n  pluginOptions: PluginOptions\n) {\n  // should draw the chart after rendering container element\n  const chartContainer = document.querySelector<HTMLElement>(`[data-chart-id=${id}]`)!;\n\n  destroyChart();\n\n  if (chartContainer) {\n    try {\n      parse(text, (parsedInfo) => {\n        const { data, options } = parsedInfo || {};\n        const chartOptions = setDefaultOptions(options!, pluginOptions, chartContainer);\n        const chartType = chartOptions.editorChart.type!;\n\n        if (\n          !data ||\n          (CATEGORY_CHART_TYPES.indexOf(chartType) > -1 &&\n            data.categories.length !== data.series[0].data.length)\n        ) {\n          chartContainer.innerHTML = 'invalid chart data';\n        } else if (SUPPORTED_CHART_TYPES.indexOf(chartType) < 0) {\n          chartContainer.innerHTML = `invalid chart type. type: bar, column, line, area, pie`;\n        } else {\n          const toastuiChart = chart[chartType];\n\n          chartOptions.usageStatistics = usageStatistics;\n          // @ts-ignore\n          chartMap[id] = toastuiChart({ el: chartContainer, data, options: chartOptions });\n        }\n      });\n    } catch (e) {\n      chartContainer.innerHTML = 'invalid chart data';\n    }\n  }\n}\n\nfunction generateId() {\n  return `chart-${Math.random().toString(36).substr(2, 10)}`;\n}\n\nlet timer: NodeJS.Timeout | null = null;\n\nfunction clearTimer() {\n  if (timer) {\n    clearTimeout(timer);\n    timer = null;\n  }\n}\n\n/**\n * Chart plugin\n * @param {Object} context - plugin context for communicating with editor\n * @param {Object} options - chart options\n * @param {number} [options.minWidth=0] - minimum width\n * @param {number} [options.minHeight=0] - minimum height\n * @param {number} [options.maxWidth=Infinity] - maximum width\n * @param {number} [options.maxHeight=Infinity] - maximum height\n * @param {number|string} [options.width='auto'] - default width\n * @param {number|string} [options.height='auto'] - default height\n */\nexport default function chartPlugin(\n  { usageStatistics = true }: PluginContext,\n  options: PluginOptions\n): PluginInfo {\n  return {\n    toHTMLRenderers: {\n      chart(node: MdNode) {\n        const id = generateId();\n\n        clearTimer();\n\n        timer = setTimeout(() => {\n          renderChart(id, node.literal!, usageStatistics, options);\n        });\n        return [\n          {\n            type: 'openTag',\n            tagName: 'div',\n            outerNewLine: true,\n            attributes: { 'data-chart-id': id },\n          },\n          { type: 'closeTag', tagName: 'div', outerNewLine: true },\n        ];\n      },\n    },\n  };\n}\n"
  },
  {
    "path": "plugins/chart/src/util.ts",
    "content": "export function trimKeepingTabs(text: string) {\n  return text.replace(/(^(\\s*[\\n\\r])+)|([\\n\\r]+\\s*$)/g, '');\n}\n\nexport function isNumeric(text: string) {\n  const mayBeNum = Number(text);\n\n  return !isNaN(mayBeNum) && isFinite(mayBeNum);\n}\n\nexport function clamp(value: number, min: number, max: number) {\n  if (min > max) {\n    [max, min] = [min, max];\n  }\n\n  return Math.max(min, Math.min(value, max));\n}\n"
  },
  {
    "path": "plugins/chart/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"src/**/*.ts\", \"src/**/*.js\", \"types/**/*\", \"../../types/**/*\"],\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"importHelpers\": false,\n    \"paths\": {\n      \"@/*\": [\"src/*\"],\n      \"@t/*\": [\"types/*\"]\n    },\n    \"lib\": [\"esnext\", \"dom\", \"dom.iterable\"]\n  }\n}"
  },
  {
    "path": "plugins/chart/types/index.d.ts",
    "content": "import type { PluginContext, PluginInfo } from '@toast-ui/editor';\n\nexport interface PluginOptions {\n  minWidth: number;\n  maxWidth: number;\n  minHeight: number;\n  maxHeight: number;\n  width: number | 'auto';\n  height: number | 'auto';\n}\n\nexport default function chartPlugin(context: PluginContext, options: PluginOptions): PluginInfo;\n"
  },
  {
    "path": "plugins/chart/webpack.config.js",
    "content": "/* eslint-disable @typescript-eslint/no-var-requires */\nconst path = require('path');\nconst webpack = require('webpack');\nconst { name, version, author, license } = require('./package.json');\n\nconst TerserPlugin = require('terser-webpack-plugin');\nconst ESLintPlugin = require('eslint-webpack-plugin');\n\nfunction getOutputConfig(isProduction, isCDN, minify) {\n  const filename = `toastui-${name.replace(/@toast-ui\\//, '')}`;\n  const defaultConfig = {\n    library: {\n      name: ['toastui', 'Editor', 'plugin', 'chart'],\n      export: 'default',\n      type: 'umd',\n    },\n    environment: {\n      arrowFunction: false,\n      const: false,\n    },\n  };\n\n  if (!isProduction || isCDN) {\n    const config = {\n      ...defaultConfig,\n      path: path.resolve(__dirname, 'dist/cdn'),\n      filename: `${filename}${minify ? '.min' : ''}.js`,\n    };\n\n    if (!isProduction) {\n      config.publicPath = '/dist/cdn';\n    }\n\n    return config;\n  }\n\n  return {\n    ...defaultConfig,\n    path: path.resolve(__dirname, 'dist'),\n    filename: `${filename}.js`,\n  };\n}\n\nfunction getExternalsConfig() {\n  return [\n    {\n      '@toast-ui/chart': {\n        commonjs: '@toast-ui/chart',\n        commonjs2: '@toast-ui/chart',\n        amd: '@toast-ui/chart',\n        root: ['toastui', 'Chart'],\n      },\n    },\n  ];\n}\n\nfunction getOptimizationConfig(isProduction, minify) {\n  const minimizer = [];\n\n  if (isProduction && minify) {\n    minimizer.push(\n      new TerserPlugin({\n        parallel: true,\n        extractComments: false,\n      })\n    );\n  }\n\n  return { minimizer };\n}\n\nmodule.exports = (env) => {\n  const isProduction = env.WEBPACK_BUILD;\n  const { minify = false, cdn = false } = env;\n  const config = {\n    mode: isProduction ? 'production' : 'development',\n    entry: './src/index.ts',\n    output: getOutputConfig(isProduction, cdn, minify),\n    externals: getExternalsConfig(isProduction, cdn),\n    resolve: {\n      fallback: {\n        stream: require.resolve('stream-browserify'),\n        buffer: require.resolve('buffer'),\n        os: require.resolve('os-browserify'),\n      },\n      extensions: ['.ts', '.js'],\n    },\n    module: {\n      rules: [\n        {\n          test: /\\.ts$|\\.js$/,\n          use: [\n            {\n              loader: 'ts-loader',\n              options: {\n                transpileOnly: true,\n              },\n            },\n          ],\n        },\n      ],\n    },\n    plugins: [\n      new ESLintPlugin({\n        extensions: ['js', 'ts'],\n        exclude: ['node_modules', 'dist'],\n        failOnError: isProduction,\n      }),\n    ],\n    optimization: getOptimizationConfig(isProduction, minify),\n  };\n\n  if (isProduction) {\n    config.plugins.push(\n      new webpack.BannerPlugin(\n        [\n          'TOAST UI Editor : Chart Plugin',\n          `@version ${version} | ${new Date().toDateString()}`,\n          `@author ${author}`,\n          `@license ${license}`,\n        ].join('\\n')\n      )\n    );\n  } else {\n    config.devServer = {\n      // https://github.com/webpack/webpack-dev-server/issues/2484\n      injectClient: false,\n      inline: true,\n      host: '0.0.0.0',\n      port: 8081,\n    };\n    config.devtool = 'inline-source-map';\n  }\n\n  return config;\n};\n"
  },
  {
    "path": "plugins/code-syntax-highlight/README.md",
    "content": "# TOAST UI Editor : Code Syntax Highlight Plugin\n\n> This is a plugin of [TOAST UI Editor](https://github.com/nhn/tui.editor/tree/master/apps/editor) to highlight code syntax.\n\n[![npm version](https://img.shields.io/npm/v/@toast-ui/editor-plugin-code-syntax-highlight.svg)](https://www.npmjs.com/package/@toast-ui/editor-plugin-code-syntax-highlight)\n\n![code-syntax-highlight](https://user-images.githubusercontent.com/37766175/121834103-de6c3d00-cd08-11eb-870f-6ff943f65f8b.png)\n\n## 🚩 Table of Contents\n\n- [Bundle File Structure](#-bundle-file-structure)\n- [Usage npm](#-usage-npm)\n- [Usage CDN](#-usage-cdn)\n\n## 📁 Bundle File Structure\n\n### Serve with npm\n\n### Files Distributed on npm\n\n```\n- node_modules/\n  - @toast-ui/\n    - editor-plugin-code-syntax-highlight/\n      - dist/\n        - toastui-editor-plugin-code-syntax-highlight-all.js\n        - toastui-editor-plugin-code-syntax-highlight.js\n        - toastui-editor-plugin-code-syntax-highlight.css\n```\n\n### Files Distributed on CDN\n\n```\n- uicdn.toast.com/\n  - editor-plugin-code-syntax-highlight/\n    - latest/\n      - toastui-editor-plugin-code-syntax-highlight.js\n      - toastui-editor-plugin-code-syntax-highlight.min.js\n      - toastui-editor-plugin-code-syntax-highlight-all.js\n      - toastui-editor-plugin-code-syntax-highlight-all.min.js\n      - toastui-editor-plugin-code-syntax-highlight.css\n      - toastui-editor-plugin-code-syntax-highlight.min.css\n```\n\n## 📦 Usage npm\n\nTo use the plugin, [`@toast-ui/editor`](https://github.com/nhn/tui.editor/tree/master/apps/editor) must be installed.\n\n> Ref. [Getting Started](https://github.com/nhn/tui.editor/blob/master/docs/en/getting-started.md)\n\n### Install\n\n```sh\n$ npm install @toast-ui/editor-plugin-code-syntax-highlight\n```\n\n### Import Plugin\n\nAlong with the plugin, the plugin's dependency style must be imported. \nThe `code-syntax-highlight` plugin has [`prismjs`](https://prismjs.com/) as a dependency, and you need to add a CSS file of `prismjs`.\n\n#### ES Modules\n\n```js\nimport 'prismjs/themes/prism.css';\nimport '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css';\n\nimport codeSyntaxHighlight from '@toast-ui/editor-plugin-code-syntax-highlight';\n```\n\n#### CommonJS\n\n```js\nrequire('prismjs/themes/prism.css');\nrequire('@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css');\n\nconst codeSyntaxHighlight = require('@toast-ui/editor-plugin-code-syntax-highlight');\n```\n\n### Create Instance\n\nWhen you set up a plugin function, you must set it with an option. The option has `highlighter`, and you need to import [`prismjs`](https://www.npmjs.com/package/prismjs) before creating an instance and set it to the value of that option.\n\nThe main bundle file of `prismjs` contains just several language pack it supports. So we provides the bundle file(`toastui-editor-plugin-code-syntax-highlight-all.js`) to import all languages you need in `prismjs`.\n\n#### Basic\n\n##### Import All Languages\n\n```js\n// ...\nimport 'prismjs/themes/prism.css';\nimport '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css';\n\nimport Editor from '@toast-ui/editor';\nimport codeSyntaxHighlight from '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight-all.js';\n\n\nconst editor = new Editor({\n  // ...\n  plugins: [codeSyntaxHighlight]\n});\n```\n\n##### Import Only Languages ​​You Need\n\nYou need to import the language files you want to use in the code block and register them in the `prismjs` object. A list of available language files can be found [here](https://github.com/PrismJS/prism/tree/master/components).\n\n```js\n// ...\nimport 'prismjs/themes/prism.css';\nimport '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css';\n\n// Step 1. Import prismjs\nimport Prism from 'prismjs';\n\n// Step 2. Import language files of prismjs that you need\nimport 'prismjs/components/prism-clojure.js';\n\nimport Editor from '@toast-ui/editor';\nimport codeSyntaxHighlight from '@toast-ui/editor-plugin-code-syntax-highlight';\n\nconst editor = new Editor({\n  // ...\n  plugins: [[codeSyntaxHighlight, { highlighter: Prism }]]\n});\n```\n\n#### With Viewer\n\nAs with creating an editor instance, you need to import `prismjs` and pass it to the `highlighter` option.\n\n```js\n// ...\nimport 'prismjs/themes/prism.css';\nimport '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css';\n\n// Import prismjs\nimport Prism from 'prismjs';\n\nimport Viewer from '@toast-ui/editor/dist/toastui-editor-viewer';\nimport codeSyntaxHighlight from '@toast-ui/editor-plugin-code-syntax-highlight';\n\n\nconst viewer = new Viewer({\n  // ...\n  plugins: [[codeSyntaxHighlight, { highlighter: Prism }]]\n});\n```\n\nor\n\n```js\n// ...\nimport 'prismjs/themes/prism.css';\nimport '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css';\n\n// Import prismjs\nimport Prism from 'prismjs';\n\nimport Editor from '@toast-ui/editor';\nimport codeSyntaxHighlight from '@toast-ui/editor-plugin-code-syntax-highlight';\n\nconst viewer = Editor.factory({\n  // ...\n  viewer: true,\n  plugins: [[codeSyntaxHighlight, { highlighter: Prism }]]\n});\n```\n\n## 🗂 Usage CDN\n\n### Include Files\n\nTo use the plugin, the CDN files(CSS, Script) of `@toast-ui/editor` must be included.\n\n### Create Instance\n\n#### Basic\n\nFirst, include the editor file. And include the plugin file as needed. If you want to include all language files provided by `prismjs`, see the first title(_Include All Languages_). If you want to register and use only the languages ​​you need, see the second title(_Include Only Languages ​​You Need_).\n\n##### Include All Languages\n\nBy including the **all** version of the plugin, all languages ​​of `prismjs` are available in the code block.\n\n```html\n...\n<head>\n  ...\n  <link\n      rel=\"stylesheet\"\n      href=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/themes/prism.min.css\"\n  />\n  <link\n    rel=\"stylesheet\"\n    href=\"https://uicdn.toast.com/editor-plugin-code-syntax-highlight/latest/toastui-editor-plugin-code-syntax-highlight.min.css\"\n  />\n  ...\n</head>\n<body>\n  ...\n  <!-- Editor -->\n  <script src=\"https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js\"></script>\n  <!-- Editor's Plugin -->\n  <script src=\"https://uicdn.toast.com/editor-plugin-code-syntax-highlight/latest/toastui-editor-plugin-code-syntax-highlight-all.min.js\"></script>\n  ...\n</body>\n...\n```\n\n```js\nconst { Editor } = toastui;\nconst { codeSyntaxHighlight } = Editor.plugin;\n\nconst instance = new Editor({\n  // ...\n  plugins: [codeSyntaxHighlight]\n});\n```\n\n##### Include Only Languages ​​You Need\n\nIf you include the **normal** version of the plugin, only the languages ​​you need are available. At this time, you should also include the language files of `prismjs`, and if you only include it, the languages ​​available to the plugin are registered.\n\n> Note : The CDN provided by `prismjs` contains several language files. If you want to add other language files, you can use [cdnjs](https://cdnjs.com/libraries/prism) to add each language file or upload a file containing only the language you need on [this page](https://prismjs.com/download.html).\n\n```html\n...\n<head>\n  ...\n  <link\n      rel=\"stylesheet\"\n      href=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/themes/prism.min.css\"\n  />\n  <link\n    rel=\"stylesheet\"\n    href=\"https://uicdn.toast.com/editor-plugin-code-syntax-highlight/latest/toastui-editor-plugin-code-syntax-highlight.min.css\"\n  />\n  ...\n</head>\n<body>\n  ...\n  <!-- Editor -->\n  <script src=\"https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js\"></script>\n  <!-- prismjs Languages -->\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/prism.min.js\"></script>\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/components/prism-clojure.min.js\"></script>\n  <!-- Editor's Plugin -->\n  <script src=\"https://uicdn.toast.com/editor-plugin-code-syntax-highlight/latest/toastui-editor-plugin-code-syntax-highlight.min.js\"></script>\n  ...\n</body>\n...\n```\n\n#### With Viewer\n\nThe way to include the plugin and the language files of `prismjs` is the same as above.\n\n##### Use Option of Editor\n\n```js\nconst { Editor } = tosatui;\nconst { codeSyntaxHighlight } = Editor.plugin;\n\nconst editor = Editor.factory({\n  // ...\n  plugins: [codeSyntaxHighlight],\n  viewer: true\n});\n```\n\n##### Use Viewer\n\nInclude the Viewer file instead of the Editor.\n\n```html\n...\n<head>\n  ...\n  <link\n      rel=\"stylesheet\"\n      href=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/themes/prism.min.css\"\n  />\n  <link\n    rel=\"stylesheet\"\n    href=\"https://uicdn.toast.com/editor-plugin-code-syntax-highlight/latest/toastui-editor-plugin-code-syntax-highlight.min.css\"\n  />\n  ...\n</head>\n<body>\n  ...\n  <!-- Viewer -->\n  <script src=\"https://uicdn.toast.com/editor/latest/toastui-editor-viewer.min.js\"></script>\n  <!-- Viewer's Plugin -->\n  <script src=\"https://uicdn.toast.com/editor-plugin-code-syntax-highlight/latest/toastui-editor-plugin-code-syntax-highlight-all.min.js\"></script>\n  ...\n</body>\n...\n```\n\n```js\nconst Viewer = toastui.Editor;\nconst { codeSyntaxHighlight } = Viewer.plugin;\n\nconst viewer = new Viewer({\n  // ...\n  plugins: [codeSyntaxHighlight]\n});\n```\n"
  },
  {
    "path": "plugins/code-syntax-highlight/demo/editor-all-langs.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>Editor</title>\n    <link\n      rel=\"stylesheet\"\n      href=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/themes/prism.min.css\"\n    />\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"http://localhost:8080/dist/cdn/toastui-editor.css\" />\n    <!-- Plugin -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor-plugin-code-syntax-highlight.css\" />\n  </head>\n  <body>\n    <div class=\"code-html\">\n      <!-- Editor -->\n      <h2>Editor</h2>\n      <div id=\"editor\"></div>\n      <!-- Editor's Viewer -->\n      <h2>Viewer</h2>\n      <div id=\"viewer\"></div>\n    </div>\n    <!-- Editor -->\n    <script src=\"http://localhost:8080/dist/cdn/toastui-editor-all.js\"></script>\n    <!-- Plugin -->\n    <script src=\"../dist/cdn/toastui-editor-plugin-code-syntax-highlight-all.js\"></script>\n    <script class=\"code-js\">\n      const content = [\n        '```js',\n        `console.log('foo')`,\n        '```',\n        '```javascript',\n        `console.log('bar')`,\n        '```',\n        '```html',\n        '<div id=\"editor\"><span>baz</span></div>',\n        '```',\n        '```wrong',\n        '[1 2 3]',\n        '```',\n        '```clojure',\n        '[1 2 3]',\n        '```',\n      ].join('\\n');\n\n      const { Editor } = toastui;\n      const { codeSyntaxHighlight } = Editor.plugin;\n\n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: content,\n        plugins: [codeSyntaxHighlight],\n      });\n\n      const viewer = Editor.factory({\n        el: document.querySelector('#viewer'),\n        viewer: true,\n        height: '500px',\n        initialValue: content,\n        plugins: [codeSyntaxHighlight],\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "plugins/code-syntax-highlight/demo/editor.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>Editor</title>\n    <link\n      rel=\"stylesheet\"\n      href=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/themes/prism.min.css\"\n    />\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"http://localhost:8080/dist/cdn/toastui-editor.css\" />\n    <!-- Plugin -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor-plugin-code-syntax-highlight.css\" />\n  </head>\n  <body>\n    <div class=\"code-html\">\n      <!-- Editor -->\n      <h2>Editor</h2>\n      <div id=\"editor\"></div>\n      <!-- Editor's Viewer -->\n      <h2>Viewer</h2>\n      <div id=\"viewer\"></div>\n    </div>\n    <!-- Editor -->\n    <script src=\"http://localhost:8080/dist/cdn/toastui-editor-all.js\"></script>\n    <!-- Plugin -->\n    <script src=\"../dist/cdn/toastui-editor-plugin-code-syntax-highlight.js\"></script>\n    <!-- Prismjs Languages -->\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/prism.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/components/prism-clojure.min.js\"></script>\n    <script class=\"code-js\">\n      const content = [\n        '```js',\n        `console.log('foo')`,\n        '```',\n        '```javascript',\n        `console.log('bar')`,\n        '```',\n        '```html',\n        '<div id=\"editor\"><span>baz</span></div>',\n        '```',\n        '```wrong',\n        '[1 2 3]',\n        '```',\n        '```clojure',\n        '[1 2 3]',\n        '```',\n      ].join('\\n');\n\n      const { Editor } = toastui;\n      const { codeSyntaxHighlight } = Editor.plugin;\n      const { Prism } = window;\n\n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: content,\n        plugins: [[codeSyntaxHighlight, { highlighter: Prism }]],\n      });\n\n      const viewer = Editor.factory({\n        el: document.querySelector('#viewer'),\n        viewer: true,\n        height: '500px',\n        initialValue: content,\n        plugins: [[codeSyntaxHighlight, { highlighter: Prism }]],\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "plugins/code-syntax-highlight/demo/esm/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>Test to use plugin in node environment</title>\n    <!-- Plugin -->\n    <link\n      rel=\"stylesheet\"\n      href=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/themes/prism.min.css\"\n    />\n  </head>\n  <body>\n    <div id=\"editor\"></div>\n    <!-- Editor -->\n    <script type=\"module\">\n      import { Editor } from 'http://localhost:8080/dist/index.js';\n      \n      import Prism from 'prismjs';\n      \n      // method 1: import selected languages\n      import codeSyntaxHighlightPlugin from '/dist/index.js';\n      import 'prismjs/components/prism-clojure.js';\n\n      // method 2: import all languages\n      // import codeSyntaxHighlightPlugin from '/dist/indexAll.js';\n\n      const content = [\n        '```js',\n        `console.log('foo')`,\n        '```',\n        '',\n        '```javascript',\n        `console.log('bar')`,\n        '```',\n        '',\n        '```html',\n        '<div id=\"editor\"><span>baz</span></div>',\n        '```',\n        '',\n        '```wrong',\n        '[1 2 3]',\n        '```',\n        '',\n        '```clojure',\n        '[1 2 3]',\n        '```',\n      ].join('\\n');\n\n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: content,\n        plugins: [[codeSyntaxHighlightPlugin, { highlighter: Prism }]],\n      });\n\n      window.editor = editor;\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "plugins/code-syntax-highlight/demo/viewer.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>Viewer</title>\n    <link\n      rel=\"stylesheet\"\n      href=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/themes/prism.min.css\"\n    />\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"http://localhost:8080/dist/toastui-editor-viewer.css\" />\n    <!-- Plugin -->\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor-plugin-code-syntax-highlight.css\" />\n  </head>\n  <body>\n    <div class=\"code-html\">\n      <div id=\"viewer\"></div>\n    </div>\n    <!-- Editor's Viewer -->\n    <script src=\"http://localhost:8080/dist/toastui-editor-viewer.js\"></script>\n    <!-- Plugin -->\n    <script src=\"../dist/cdn/toastui-editor-plugin-code-syntax-highlight.js\"></script>\n    <!-- Prismjs Languages -->\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/prism.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/components/prism-clojure.min.js\"></script>\n    <script class=\"code-js\">\n      const content = [\n        '```js',\n        `console.log('foo')`,\n        '```',\n        '```javascript',\n        `console.log('bar')`,\n        '```',\n        '```html',\n        '<div id=\"editor\"><span>baz</span></div>',\n        '```',\n        '```wrong',\n        '[1 2 3]',\n        '```',\n        '```clojure',\n        '[1 2 3]',\n        '```',\n      ].join('\\n');\n\n      const Viewer = toastui.Editor;\n      const { codeSyntaxHighlight } = Viewer.plugin;\n      const { Prism } = window;\n\n      const instance = new Viewer({\n        el: document.querySelector('#viewer'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: content,\n        plugins: [[codeSyntaxHighlight, { highlighter: Prism }]],\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "plugins/code-syntax-highlight/jest.config.js",
    "content": "// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst base = require('../../jest.base.config');\n\nmodule.exports = {\n  ...base,\n  testEnvironment: 'jsdom',\n  moduleNameMapper: {\n    '^@/(.*)$': '<rootDir>/src/$1',\n  },\n};\n"
  },
  {
    "path": "plugins/code-syntax-highlight/package.json",
    "content": "{\n  \"name\": \"@toast-ui/editor-plugin-code-syntax-highlight\",\n  \"version\": \"3.1.0\",\n  \"description\": \"TOAST UI Editor : Code Syntax Highlight Plugin\",\n  \"keywords\": [\n    \"nhn\",\n    \"nhn cloud\",\n    \"toast\",\n    \"toastui\",\n    \"toast-ui\",\n    \"editor\",\n    \"plugin\",\n    \"codeblock\",\n    \"highlight\"\n  ],\n  \"main\": \"dist/toastui-editor-plugin-code-syntax-highlight.js\",\n  \"types\": \"types/index.d.ts\",\n  \"files\": [\n    \"dist/*.js\",\n    \"dist/*.css\",\n    \"types/index.d.ts\"\n  ],\n  \"author\": \"NHN Cloud FE Development Lab <dl_javascript@nhn.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/nhn/tui.editor.git\",\n    \"directory\": \"plugins/code-syntax-highlight\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/nhn/tui.editor/issues\"\n  },\n  \"homepage\": \"https://ui.toast.com\",\n  \"browserslist\": \"last 2 versions, not ie <= 10\",\n  \"scripts\": {\n    \"lint\": \"eslint .\",\n    \"test:types\": \"tsc\",\n    \"test\": \"jest --watch\",\n    \"test:ci\": \"jest\",\n    \"serve\": \"snowpack dev\",\n    \"serve:ie\": \"webpack serve\",\n    \"serve:ie:all\": \"webpack serve --env all\",\n    \"build:all\": \"webpack build --env all\",\n    \"build:cdn\": \"webpack build --env cdn & webpack build --env cdn minify\",\n    \"build:cdn-all\": \"webpack build --env cdn all & webpack build --env cdn all minify\",\n    \"build\": \"webpack build && npm run build:cdn && npm run build:cdn-all && npm run build:all\"\n  },\n  \"devDependencies\": {\n    \"@types/prismjs\": \"^1.16.3\",\n    \"cross-env\": \"^6.0.3\"\n  },\n  \"dependencies\": {\n    \"prismjs\": \"^1.23.0\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  }\n}\n"
  },
  {
    "path": "plugins/code-syntax-highlight/snowpack.config.js",
    "content": "// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst httpProxy = require('http-proxy');\nconst proxy = httpProxy.createServer({ target: 'http://localhost:8080' });\n\n/** @type {import(\"snowpack\").SnowpackUserConfig } */\nmodule.exports = {\n  mount: {\n    'demo/esm': '/',\n    src: '/dist',\n  },\n  devOptions: {\n    port: 8081,\n  },\n  routes: [\n    {\n      src: '/img/.*',\n      dest: (req, res) => {\n        proxy.web(req, res);\n      },\n    },\n  ],\n  alias: {\n    '@': './src',\n    '@t': './types',\n  },\n};\n"
  },
  {
    "path": "plugins/code-syntax-highlight/src/__test__/integration/__snapshots__/codeHighlightPlugin.spec.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`codeSyntaxHighlightPlugin should render codeblock element with no language info in markdown preview 1`] = `\n<pre class=\"toastui-editor-md-preview-highlight\">\n  <code>\n    console.log(123);\n  </code>\n</pre>\n`;\n\nexports[`codeSyntaxHighlightPlugin should render highlighted codeblock element in markdown preview 1`] = `\n<pre class=\"lang-yaml toastui-editor-md-preview-highlight\">\n  <code data-language=\"yaml\">\n    <span class=\"token key atrule\">\n      martin\n    </span>\n    <span class=\"token punctuation\">\n      :\n    </span>\n    <span class=\"token key atrule\">\n      name\n    </span>\n    <span class=\"token punctuation\">\n      :\n    </span>\n    Martin D'vloper\n    <span class=\"token key atrule\">\n      job\n    </span>\n    <span class=\"token punctuation\">\n      :\n    </span>\n    Developer\n    <span class=\"token key atrule\">\n      skill\n    </span>\n    <span class=\"token punctuation\">\n      :\n    </span>\n    Elite\n  </code>\n</pre>\n`;\n\nexports[`codeSyntaxHighlightPlugin should render highlighted codeblock element in wysiwyg 1`] = `\n<div data-language=\"yaml\"\n     class=\"toastui-editor-ww-code-block-highlighting\"\n>\n  <pre class=\"language-yaml\">\n    <code data-language=\"yaml\"\n          class=\"language-yaml\"\n    >\n      <span class=\"token key atrule\">\n        martin\n      </span>\n      <span class=\"token punctuation\">\n        :\n      </span>\n      <span class=\"token key atrule\">\n        name\n      </span>\n      <span class=\"token punctuation\">\n        :\n      </span>\n      Martin D'vloper\n      <span class=\"token key atrule\">\n        job\n      </span>\n      <span class=\"token punctuation\">\n        :\n      </span>\n      Developer\n      <span class=\"token key atrule\">\n        skill\n      </span>\n      <span class=\"token punctuation\">\n        :\n      </span>\n      Elite\n    </code>\n  </pre>\n</div>\n`;\n"
  },
  {
    "path": "plugins/code-syntax-highlight/src/__test__/integration/__snapshots__/codeHighlightPluginWithAllLangs.spec.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`codeSyntaxHighlightPlugin should render highlighted codeblock element in markdown preview 1`] = `\n<pre class=\"lang-yaml toastui-editor-md-preview-highlight\">\n  <code data-language=\"yaml\">\n    <span class=\"token key atrule\">\n      martin\n    </span>\n    <span class=\"token punctuation\">\n      :\n    </span>\n    <span class=\"token key atrule\">\n      name\n    </span>\n    <span class=\"token punctuation\">\n      :\n    </span>\n    Martin D'vloper\n    <span class=\"token key atrule\">\n      job\n    </span>\n    <span class=\"token punctuation\">\n      :\n    </span>\n    Developer\n    <span class=\"token key atrule\">\n      skill\n    </span>\n    <span class=\"token punctuation\">\n      :\n    </span>\n    Elite\n  </code>\n</pre>\n`;\n\nexports[`codeSyntaxHighlightPlugin should render highlighted codeblock element in wysiwyg 1`] = `\n<div data-language=\"yaml\"\n     class=\"toastui-editor-ww-code-block-highlighting\"\n>\n  <pre class=\"language-yaml\">\n    <code data-language=\"yaml\"\n          class=\"language-yaml\"\n    >\n      <span class=\"token key atrule\">\n        martin\n      </span>\n      <span class=\"token punctuation\">\n        :\n      </span>\n      <span class=\"token key atrule\">\n        name\n      </span>\n      <span class=\"token punctuation\">\n        :\n      </span>\n      Martin D'vloper\n      <span class=\"token key atrule\">\n        job\n      </span>\n      <span class=\"token punctuation\">\n        :\n      </span>\n      Developer\n      <span class=\"token key atrule\">\n        skill\n      </span>\n      <span class=\"token punctuation\">\n        :\n      </span>\n      Elite\n    </code>\n  </pre>\n</div>\n`;\n"
  },
  {
    "path": "plugins/code-syntax-highlight/src/__test__/integration/codeHighlightPlugin.spec.ts",
    "content": "import { source } from 'common-tags';\n\nimport Editor from '@toast-ui/editor';\nimport codeSyntaxHighlightPlugin from '@/index';\n\nimport Prism from 'prismjs';\nimport 'prismjs/components/prism-yaml.js';\n\ndescribe('codeSyntaxHighlightPlugin', () => {\n  let container: HTMLElement, mdPreview: HTMLElement, wwEditor: HTMLElement, editor: Editor;\n\n  const initialValue = source`\n    \\`\\`\\`yaml\n    martin:\n      name: Martin D'vloper\n      job: Developer\n      skill: Elite\n    \\`\\`\\`\n  `;\n\n  function getPreviewHTML() {\n    return mdPreview\n      .querySelector('.toastui-editor-contents')!\n      .innerHTML.replace(/\\sdata-nodeid=\"\\d+\"|\\n/g, '')\n      .trim();\n  }\n\n  function getWwEditorHTML() {\n    return wwEditor.firstElementChild!.innerHTML;\n  }\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    editor = new Editor({\n      el: container,\n      previewStyle: 'vertical',\n      initialValue,\n      plugins: [[codeSyntaxHighlightPlugin, { highlighter: Prism }]],\n    });\n\n    const elements = editor.getEditorElements();\n\n    mdPreview = elements.mdPreview!;\n    wwEditor = elements.wwEditor!;\n\n    document.body.appendChild(container);\n  });\n\n  afterEach(() => {\n    editor.destroy();\n    document.body.removeChild(container);\n  });\n\n  it('should render highlighted codeblock element in markdown preview', () => {\n    const previewHTML = getPreviewHTML();\n\n    expect(previewHTML).toMatchSnapshot();\n  });\n\n  it('should render highlighted codeblock element in wysiwyg', () => {\n    editor.changeMode('wysiwyg');\n\n    const wwEditorHTML = getWwEditorHTML();\n\n    expect(wwEditorHTML).toMatchSnapshot();\n  });\n\n  it('should render codeblock element with no language info in markdown preview', () => {\n    const markdown = source`\n      \\`\\`\\`\n        console.log(123);\n      \\`\\`\\`\n    `;\n\n    editor.setMarkdown(markdown);\n\n    const previewHTML = getPreviewHTML();\n\n    expect(previewHTML).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "plugins/code-syntax-highlight/src/__test__/integration/codeHighlightPluginWithAllLangs.spec.ts",
    "content": "import { source } from 'common-tags';\n\nimport Editor from '@toast-ui/editor';\nimport codeSyntaxHighlightPlugin from '@/indexAll';\n\ndescribe('codeSyntaxHighlightPlugin', () => {\n  let container: HTMLElement, mdPreview: HTMLElement, wwEditor: HTMLElement, editor: Editor;\n\n  const initialValue = source`\n    \\`\\`\\`yaml\n    martin:\n      name: Martin D'vloper\n      job: Developer\n      skill: Elite\n    \\`\\`\\`\n  `;\n\n  function getPreviewHTML() {\n    return mdPreview\n      .querySelector('.toastui-editor-contents')!\n      .innerHTML.replace(/\\sdata-nodeid=\"\\d+\"|\\n/g, '')\n      .trim();\n  }\n\n  function getWwEditorHTML() {\n    return wwEditor.firstElementChild!.innerHTML;\n  }\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    editor = new Editor({\n      el: container,\n      previewStyle: 'vertical',\n      initialValue,\n      plugins: [codeSyntaxHighlightPlugin],\n    });\n\n    const elements = editor.getEditorElements();\n\n    mdPreview = elements.mdPreview!;\n    wwEditor = elements.wwEditor!;\n\n    document.body.appendChild(container);\n  });\n\n  afterEach(() => {\n    editor.destroy();\n    document.body.removeChild(container);\n  });\n\n  it('should render highlighted codeblock element in markdown preview', () => {\n    const previewHTML = getPreviewHTML();\n\n    expect(previewHTML).toMatchSnapshot();\n  });\n\n  it('should render highlighted codeblock element in wysiwyg', () => {\n    editor.changeMode('wysiwyg');\n\n    const wwEditorHTML = getWwEditorHTML();\n\n    expect(wwEditorHTML).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "plugins/code-syntax-highlight/src/__test__/unit/languageSelectBox.spec.ts",
    "content": "import {\n  LanguageSelectBox,\n  WRAPPER_CLASS_NAME,\n  INPUT_CLASS_NANE,\n  LIST_CLASS_NAME,\n} from '@/nodeViews/languageSelectBox';\nimport { cls } from '@/utils/dom';\nimport type { Emitter } from '@toast-ui/editor';\n\nElement.prototype.scrollIntoView = jest.fn();\n\ndescribe('languageSelectBox', () => {\n  let selectBox: LanguageSelectBox,\n    eventEmitter: Emitter,\n    wrapper: HTMLElement,\n    input: HTMLInputElement,\n    list: HTMLElement,\n    wwContainer: HTMLElement;\n\n  beforeEach(() => {\n    eventEmitter = {\n      emit: jest.fn(),\n      emitReduce: jest.fn(),\n      listen: jest.fn(),\n      removeEventHandler: jest.fn(),\n      addEventType: jest.fn(),\n      getEvents: jest.fn(),\n      holdEventInvoke: jest.fn(),\n    };\n\n    wwContainer = document.createElement('div');\n    wwContainer.className = 'toastui-editor ww-mode';\n    document.body.appendChild(wwContainer);\n\n    selectBox = new LanguageSelectBox(document.body, eventEmitter, ['js', 'css', 'ts']);\n\n    wrapper = document.body.querySelector(`.${cls(WRAPPER_CLASS_NAME)}`)!;\n    input = document.body.querySelector(`.${cls(INPUT_CLASS_NANE)} > input`)!;\n    list = document.body.querySelector(`.${cls(LIST_CLASS_NAME)}`)!;\n  });\n\n  afterEach(() => {\n    selectBox.destroy();\n    document.body.removeChild(wwContainer);\n  });\n\n  it('should create language select box element', () => {\n    expect(wrapper).toHaveClass(`${cls(WRAPPER_CLASS_NAME)}`);\n  });\n\n  it('show() should show language select box element', () => {\n    selectBox.show();\n\n    expect(wrapper).not.toHaveStyle('display: none');\n  });\n\n  it('hide() should hide language select box element', () => {\n    selectBox.show();\n    selectBox.hide();\n\n    expect(wrapper).toHaveStyle('display: none');\n  });\n\n  it('destory() should remove element on body', () => {\n    selectBox.destroy();\n\n    expect(wwContainer).toBeEmptyDOMElement();\n    expect(eventEmitter.removeEventHandler).toHaveBeenCalled();\n  });\n\n  it('setLanguage() should change input value to selected language', () => {\n    selectBox.setLanguage('foo');\n\n    expect(input).toHaveValue('foo');\n  });\n\n  describe('wrapper element', () => {\n    it('should change to active state when input is focused', () => {\n      input.focus();\n\n      expect(wrapper).toHaveClass('active');\n    });\n\n    it('should change to inactive state when input is focused out', () => {\n      input.focus();\n      input.blur();\n\n      expect(wrapper).not.toHaveClass('active');\n    });\n  });\n\n  describe('language list element', () => {\n    it('should show when input is focused', () => {\n      input.focus();\n\n      expect(list).toHaveStyle('display: block');\n    });\n\n    it('should hide when input is focused out', () => {\n      input.focus();\n      input.blur();\n\n      expect(list).toHaveStyle('display: none');\n    });\n  });\n});\n"
  },
  {
    "path": "plugins/code-syntax-highlight/src/css/plugin.css",
    "content": "/* prevent to create draggable box in IE with prism */\npre[class*=\"language-\"] {\n  overflow: visible;\n}\n\n.toastui-editor-ww-code-block-highlighting {\n  position: relative;\n}\n\n.toastui-editor-ww-code-block-highlighting:after {\n  content: attr(data-language);\n  position: absolute;\n  display: inline-block;\n  top: 10px;\n  right: 10px;\n  height: 24px;\n  padding: 3px 30px 0 10px;\n  font-weight: bold;\n  font-size: 13px;\n  color: #333;\n  background-color: #e5e9ea;\n  background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI1LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IuugiOydtOyWtF8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiCgkgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMzAgMzAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMwIDMwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGwtcnVsZTpldmVub2RkO2NsaXAtcnVsZTpldmVub2RkO2ZpbGw6IzU1NTU1NTt9Cjwvc3R5bGU+CjxnPgoJPGc+CgkJPGc+CgkJCTxnPgoJCQkJPGc+CgkJCQkJPHBhdGggY2xhc3M9InN0MCIgZD0iTTE1LjUsMTIuNWwyLDJMMTIsMjBoLTJ2LTJMMTUuNSwxMi41eiBNMTgsMTBsMiwybC0xLjUsMS41bC0yLTJMMTgsMTB6Ii8+CgkJCQk8L2c+CgkJCTwvZz4KCQk8L2c+Cgk8L2c+CjwvZz4KPC9zdmc+Cg==');\n  background-repeat: no-repeat;\n  background-position: right;\n  background-size: 30px 30px;\n  border-radius: 2px;\n  cursor: pointer;\n}\n\n.toastui-editor-code-block-language {\n  position: fixed;\n  display: inline-block;\n  right: 35px;\n  z-index: 30;\n}\n\n.toastui-editor-code-block-language-input {\n  position: relative;\n  display: inline-block;\n  padding: 0 22px 0 10px;\n  width: 112px;\n  height: 26px;\n  border: 1px solid #ccc;\n  border-radius: 2px;\n  background-color: #fff;\n  cursor: pointer;\n}\n\n.toastui-editor-code-block-language-input input {\n  margin: 0;\n  padding: 0;\n  height: 100%;\n  width: 100%;\n  background-color: #fff;\n  color: #222;\n  border: none;\n  outline: none;\n}\n\n.toastui-editor-code-block-language-input input::placeholder {\n  color: #ccc;\n}\n\n.toastui-editor-code-block-language-input input::-ms-clear {\n  display: none;\n}\n\n.toastui-editor-code-block-language .toastui-editor-code-block-language-input::after {\n  content: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI1LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IuugiOydtOyWtF8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiCgkgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTIgMTQiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDEyIDE0OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGwtcnVsZTpldmVub2RkO2NsaXAtcnVsZTpldmVub2RkO2ZpbGw6IzIyMjIyMjt9Cjwvc3R5bGU+CjxkZXNjPkNyZWF0ZWQgd2l0aCBza2V0Y2h0b29sLjwvZGVzYz4KPGcgaWQ9IlN5bWJvbHMiPgoJPGcgaWQ9ImNvbS10cmFuZ2xlLWQtc2lkZSI+CgkJPHBvbHlnb24gaWQ9IlJlY3RhbmdsZS03IiBjbGFzcz0ic3QwIiBwb2ludHM9IjIsNSAxMCw1IDYsMTAgCQkiLz4KCTwvZz4KPC9nPgo8L3N2Zz4K');\n  position: absolute;\n  display: inline-block;\n  top: 7px;\n  right: 5px;\n  width: 12px;\n  height: 14px;\n}\n\n.toastui-editor-code-block-language.active .toastui-editor-code-block-language-input::after {\n  content: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI1LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IuugiOydtOyWtF8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiCgkgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTIgMTQiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDEyIDE0OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGwtcnVsZTpldmVub2RkO2NsaXAtcnVsZTpldmVub2RkO2ZpbGw6IzIyMjIyMjt9Cjwvc3R5bGU+CjxkZXNjPkNyZWF0ZWQgd2l0aCBza2V0Y2h0b29sLjwvZGVzYz4KPGcgaWQ9IlN5bWJvbHMiPgoJPGcgaWQ9ImNvbS10cmFuZ2xlLXVwLXNpZGUiPgoJCTxwb2x5Z29uIGlkPSJSZWN0YW5nbGUtNyIgY2xhc3M9InN0MCIgcG9pbnRzPSIyLDkgMTAsOSA2LDQgCQkiLz4KCTwvZz4KPC9nPgo8L3N2Zz4K');\n}\n\n.toastui-editor-code-block-language-list {\n  position: fixed;\n  margin-top: -1px;\n  width: 144px;\n  border: solid 1px #ccc;\n  border-bottom-left-radius: 2px;\n  border-bottom-right-radius: 2px;\n}\n\n.toastui-editor-code-block-language-list .buttons {\n  max-height: 169px;\n  overflow: auto;\n  padding: 0;\n}\n\n.toastui-editor-code-block-language-list button {\n  width: 100%;\n  background-color: #fff;\n  border: none;\n  outline: 0;\n  padding: 0 10px;\n  font-size: 13px;\n  line-height: 24px;\n  text-align: left;\n  color: #222;\n  cursor: pointer;\n}\n\n.toastui-editor-code-block-language-list button.active {\n  color: #4b96e6;\n  font-weight: bold;\n}\n\n.toastui-editor-code-block-language-list button:hover {\n  background-color: #f4f7f8;\n}\n\n.toastui-editor-dark .toastui-editor-code-block-language-input input::placeholder {\n  color: #eee;\n}\n\n.toastui-editor-dark .toastui-editor-ww-code-block-highlighting:after {\n  background-color: #232428;\n  border: 1px solid #393b42;\n  color: #eee;\n  background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI1LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IuugiOydtOyWtF8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiCgkgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMzAgMzAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMwIDMwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGwtcnVsZTpldmVub2RkO2NsaXAtcnVsZTpldmVub2RkO2ZpbGw6I2ZmZjt9Cjwvc3R5bGU+CjxnPgoJPGc+CgkJPGc+CgkJCTxnPgoJCQkJPGc+CgkJCQkJPHBhdGggY2xhc3M9InN0MCIgZD0iTTE1LjUsMTIuNWwyLDJMMTIsMjBoLTJ2LTJMMTUuNSwxMi41eiBNMTgsMTBsMiwybC0xLjUsMS41bC0yLTJMMTgsMTB6Ii8+CgkJCQk8L2c+CgkJCTwvZz4KCQk8L2c+Cgk8L2c+CjwvZz4KPC9zdmc+Cg==');\n}\n\n.toastui-editor-dark .toastui-editor-code-block-language span {\n  border: 1px solid #494c56;\n  background-color: #121212;\n}\n\n.toastui-editor-dark .toastui-editor-code-block-language input {\n  background-color: #121212;\n  color: #eee;\n}\n\n.toastui-editor-dark .toastui-editor-code-block-language-list {\n  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.08);\n  border: 1px solid #494c56;\n  border-radius: 2px;\n}\n\n.toastui-editor-dark .toastui-editor-code-block-language-list button {\n  color: #eee;\n  background-color: #121212;\n}\n\n.toastui-editor-dark .toastui-editor-code-block-language-list button.active {\n  color: #4b96e6;\n}\n\n.toastui-editor-dark .toastui-editor-code-block-language-list button:hover {\n  background-color: #36383f;\n}\n\n.toastui-editor-dark .toastui-editor-code-block-language .toastui-editor-code-block-language-input::after {\n  content: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI1LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IuugiOydtOyWtF8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiCgkgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTIgMTQiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDEyIDE0OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGwtcnVsZTpldmVub2RkO2NsaXAtcnVsZTpldmVub2RkO2ZpbGw6I2ZmZjt9Cjwvc3R5bGU+CjxkZXNjPkNyZWF0ZWQgd2l0aCBza2V0Y2h0b29sLjwvZGVzYz4KPGcgaWQ9IlN5bWJvbHMiPgoJPGcgaWQ9ImNvbS10cmFuZ2xlLWQtc2lkZSI+CgkJPHBvbHlnb24gaWQ9IlJlY3RhbmdsZS03IiBjbGFzcz0ic3QwIiBwb2ludHM9IjIsNSAxMCw1IDYsMTAgCQkiLz4KCTwvZz4KPC9nPgo8L3N2Zz4K');\n}\n\n.toastui-editor-dark .toastui-editor-code-block-language.active .toastui-editor-code-block-language-input::after {\n  content: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI1LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IuugiOydtOyWtF8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiCgkgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTIgMTQiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDEyIDE0OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGwtcnVsZTpldmVub2RkO2NsaXAtcnVsZTpldmVub2RkO2ZpbGw6I2ZmZjt9Cjwvc3R5bGU+CjxkZXNjPkNyZWF0ZWQgd2l0aCBza2V0Y2h0b29sLjwvZGVzYz4KPGcgaWQ9IlN5bWJvbHMiPgoJPGcgaWQ9ImNvbS10cmFuZ2xlLXVwLXNpZGUiPgoJCTxwb2x5Z29uIGlkPSJSZWN0YW5nbGUtNyIgY2xhc3M9InN0MCIgcG9pbnRzPSIyLDkgMTAsOSA2LDQgCQkiLz4KCTwvZz4KPC9nPgo8L3N2Zz4K');\n}"
  },
  {
    "path": "plugins/code-syntax-highlight/src/index.ts",
    "content": "import { codeSyntaxHighlightPlugin } from '@/plugin';\n\nimport '@/css/plugin.css';\n\n// Prevent to highlight all code elements automatically.\n// @link https://prismjs.com/docs/Prism.html#.manual\n// eslint-disable-next-line no-undefined\nif (typeof window !== undefined) {\n  window.Prism = window.Prism || {};\n  window.Prism.manual = true;\n}\n\nexport default codeSyntaxHighlightPlugin;\n"
  },
  {
    "path": "plugins/code-syntax-highlight/src/indexAll.ts",
    "content": "import Prism from 'prismjs';\nimport type { PluginContext, PluginInfo } from '@toast-ui/editor';\nimport { codeSyntaxHighlightPlugin } from '@/plugin';\nimport { PrismJs } from '@t/index';\n\nimport '@/prismjs-langs';\nimport '@/css/plugin.css';\n\n// Prevent to highlight all code elements automatically.\n// @link https://prismjs.com/docs/Prism.html#.manual\n// eslint-disable-next-line no-undefined\nif (typeof window !== undefined) {\n  window.Prism = window.Prism || {};\n  window.Prism.manual = true;\n}\n\nexport default function plugin(context: PluginContext): PluginInfo {\n  return codeSyntaxHighlightPlugin(context, { highlighter: Prism as PrismJs });\n}\n"
  },
  {
    "path": "plugins/code-syntax-highlight/src/nodeViews/codeSyntaxHighlightView.ts",
    "content": "import type { EditorView, NodeView } from 'prosemirror-view';\nimport type { Node as ProsemirrorNode } from 'prosemirror-model';\n\nimport isFunction from 'tui-code-snippet/type/isFunction';\nimport addClass from 'tui-code-snippet/domUtil/addClass';\n\nimport { cls } from '@/utils/dom';\nimport { LanguageSelectBox } from '@/nodeViews/languageSelectBox';\nimport type { Emitter } from '@toast-ui/editor';\n\ntype GetPos = (() => number) | boolean;\n\ntype CodeBlockPos = { top: number; right: number };\n\nconst WRAPPER_CLASS_NAME = 'ww-code-block-highlighting';\n\nfunction getCustomAttrs(attrs: Record<string, any>) {\n  const { htmlAttrs, classNames } = attrs;\n\n  return { ...htmlAttrs, class: classNames ? classNames.join(' ') : null };\n}\n\nclass CodeSyntaxHighlightView implements NodeView {\n  dom!: HTMLElement;\n\n  contentDOM: HTMLElement | null = null;\n\n  private languageSelectBox: LanguageSelectBox | null = null;\n\n  private languageEditing: boolean;\n\n  // eslint-disable-next-line max-params\n  constructor(\n    private node: ProsemirrorNode,\n    private view: EditorView,\n    private getPos: GetPos,\n    private eventEmitter: Emitter,\n    private languages: string[]\n  ) {\n    this.node = node;\n    this.view = view;\n    this.getPos = getPos;\n    this.eventEmitter = eventEmitter;\n    this.languageEditing = false;\n    this.languages = languages;\n\n    this.createElement();\n    this.bindDOMEvent();\n    this.bindEvent();\n  }\n\n  private createElement() {\n    const { language } = this.node.attrs;\n    const wrapper = document.createElement('div');\n\n    wrapper.setAttribute('data-language', language || 'text');\n    addClass(wrapper, cls(WRAPPER_CLASS_NAME));\n\n    const pre = this.createCodeBlockElement();\n    const code = pre.firstChild as HTMLElement;\n\n    if (language) {\n      addClass(pre, `language-${language}`);\n      addClass(code, `language-${language}`);\n    }\n\n    wrapper.appendChild(pre);\n\n    this.dom = wrapper;\n    this.contentDOM = code;\n  }\n\n  private createCodeBlockElement() {\n    const pre = document.createElement('pre');\n    const code = document.createElement('code');\n    const { language } = this.node.attrs;\n    const attrs = getCustomAttrs(this.node.attrs);\n\n    if (language) {\n      code.setAttribute('data-language', language);\n    }\n\n    Object.keys(attrs).forEach((attrName) => {\n      if (attrs[attrName]) {\n        pre.setAttribute(attrName, attrs[attrName]);\n      }\n    });\n\n    pre.appendChild(code);\n\n    return pre;\n  }\n\n  private bindDOMEvent() {\n    if (this.dom) {\n      this.dom.addEventListener('click', this.onClickEditingButton);\n      this.view.dom.addEventListener('mousedown', this.finishLanguageEditing);\n      window.addEventListener('resize', this.finishLanguageEditing);\n    }\n  }\n\n  private bindEvent() {\n    this.eventEmitter.listen('selectLanguage', this.onSelectLanguage);\n    this.eventEmitter.listen('scroll', this.finishLanguageEditing);\n    this.eventEmitter.listen('finishLanguageEditing', this.finishLanguageEditing);\n  }\n\n  private onSelectLanguage = (language: string) => {\n    if (this.languageEditing) {\n      this.changeLanguage(language);\n    }\n  };\n\n  private onClickEditingButton = (ev: MouseEvent) => {\n    const target = ev.target as HTMLElement;\n    const style = getComputedStyle(target, ':after');\n\n    // judge to click pseudo element with background image for IE11\n    if (style.backgroundImage !== 'none' && isFunction(this.getPos)) {\n      const pos = this.view.coordsAtPos(this.getPos());\n\n      this.openLanguageSelectBox(pos);\n    }\n  };\n\n  private openLanguageSelectBox(pos: CodeBlockPos) {\n    this.languageSelectBox = new LanguageSelectBox(\n      this.view.dom.parentElement!,\n      this.eventEmitter,\n      this.languages\n    );\n    this.eventEmitter.emit('showCodeBlockLanguages', pos, this.node.attrs.language);\n    this.languageEditing = true;\n  }\n\n  private changeLanguage(language: string) {\n    if (isFunction(this.getPos)) {\n      this.reset();\n\n      const pos = this.getPos();\n      const { tr } = this.view.state;\n\n      tr.setNodeMarkup(pos, null, { language });\n      this.view.dispatch(tr);\n    }\n  }\n\n  private finishLanguageEditing = () => {\n    if (this.languageEditing) {\n      this.reset();\n    }\n  };\n\n  private reset() {\n    if (this.languageSelectBox) {\n      this.languageSelectBox.destroy();\n      this.languageSelectBox = null;\n    }\n\n    this.languageEditing = false;\n  }\n\n  stopEvent() {\n    return true;\n  }\n\n  update(node: ProsemirrorNode) {\n    if (!node.sameMarkup(this.node)) {\n      return false;\n    }\n\n    this.node = node;\n\n    return true;\n  }\n\n  destroy() {\n    this.reset();\n\n    if (this.dom) {\n      this.dom.removeEventListener('click', this.onClickEditingButton);\n      this.view.dom.removeEventListener('mousedown', this.finishLanguageEditing);\n      window.removeEventListener('resize', this.finishLanguageEditing);\n    }\n\n    this.eventEmitter.removeEventHandler('selectLanguage', this.onSelectLanguage);\n    this.eventEmitter.removeEventHandler('scroll', this.finishLanguageEditing);\n    this.eventEmitter.removeEventHandler('finishLanguageEditing', this.finishLanguageEditing);\n  }\n}\n\nexport function createCodeSyntaxHighlightView(languages: string[]) {\n  return (node: ProsemirrorNode, view: EditorView, getPos: GetPos, emitter: Emitter) =>\n    new CodeSyntaxHighlightView(node, view, getPos, emitter, languages);\n}\n"
  },
  {
    "path": "plugins/code-syntax-highlight/src/nodeViews/languageSelectBox.ts",
    "content": "import css from 'tui-code-snippet/domUtil/css';\nimport addClass from 'tui-code-snippet/domUtil/addClass';\nimport removeClass from 'tui-code-snippet/domUtil/removeClass';\nimport hasClass from 'tui-code-snippet/domUtil/hasClass';\nimport toArray from 'tui-code-snippet/collection/toArray';\nimport inArray from 'tui-code-snippet/array/inArray';\n\nimport { isPositionInBox, removeNode, cls } from '@/utils/dom';\nimport type { Emitter } from '@toast-ui/editor';\n\nexport const WRAPPER_CLASS_NAME = 'code-block-language';\nexport const INPUT_CLASS_NANE = 'code-block-language-input';\nexport const LIST_CLASS_NAME = 'code-block-language-list';\nexport const LANG_ATTR = 'data-language';\n\nconst CODE_BLOCK_PADDING = 10;\n\nfunction getButtonsHTML(languages: string[]) {\n  return languages\n    .map((language) => `<button type=\"button\" data-language=\"${language}\">${language}</button>`)\n    .join('');\n}\n\nexport class LanguageSelectBox {\n  private rootEl: HTMLElement;\n\n  private eventEmitter: Emitter;\n\n  private languages: string[];\n\n  private wrapper!: HTMLElement;\n\n  private input!: HTMLInputElement;\n\n  private list!: HTMLElement;\n\n  private buttons: Element[] = [];\n\n  private currentButton!: Element;\n\n  private prevStoredLanguage = '';\n\n  constructor(rootEl: HTMLElement, eventEmitter: Emitter, languages: string[]) {\n    this.rootEl = rootEl;\n    this.eventEmitter = eventEmitter;\n    this.languages = languages;\n\n    this.createElement();\n    this.bindDOMEvent();\n    this.bindEvent();\n  }\n\n  private createElement() {\n    this.wrapper = document.createElement('div');\n\n    addClass(this.wrapper, cls(WRAPPER_CLASS_NAME));\n\n    this.createInputElement();\n    this.createLanguageListElement();\n\n    this.rootEl.appendChild(this.wrapper);\n\n    this.hide();\n  }\n\n  private createInputElement() {\n    const wrapper = document.createElement('span');\n\n    addClass(wrapper, cls(INPUT_CLASS_NANE));\n\n    const input = document.createElement('input');\n\n    input.type = 'text';\n    input.setAttribute('maxlength', '20');\n\n    this.input = input;\n\n    wrapper.appendChild(this.input);\n    this.wrapper.appendChild(wrapper);\n  }\n\n  private createLanguageListElement() {\n    this.list = document.createElement('div');\n    addClass(this.list, cls(LIST_CLASS_NAME));\n\n    const buttonsContainer = document.createElement('div');\n\n    addClass(buttonsContainer, 'buttons');\n    buttonsContainer.innerHTML = getButtonsHTML(this.languages);\n\n    this.buttons = toArray(buttonsContainer.children);\n    this.list.appendChild(buttonsContainer);\n    this.wrapper.appendChild(this.list);\n\n    this.activateButtonByIndex(0);\n    this.hideList();\n  }\n\n  private bindDOMEvent() {\n    this.wrapper.addEventListener('mousedown', this.onSelectToggleButton);\n    this.input.addEventListener('keydown', this.handleKeydown);\n    this.input.addEventListener('focus', () => this.activateSelectBox());\n    this.input.addEventListener('blur', () => this.inactivateSelectBox());\n    this.list.addEventListener('mousedown', this.onSelectLanguageButtons);\n  }\n\n  private bindEvent() {\n    this.eventEmitter.listen('showCodeBlockLanguages', this.showLangugaeSelectBox);\n  }\n\n  private onSelectToggleButton = (ev: MouseEvent) => {\n    const target = ev.target as HTMLElement;\n    const style = getComputedStyle(target, ':after');\n    const { offsetX, offsetY } = ev;\n\n    if (isPositionInBox(style, offsetX, offsetY)) {\n      ev.preventDefault();\n      this.toggleFocus();\n    }\n  };\n\n  private onSelectLanguageButtons = (ev: MouseEvent) => {\n    const target = ev.target as HTMLElement;\n    const language = target.getAttribute(LANG_ATTR);\n\n    if (language) {\n      this.selectLanguage(language);\n    }\n  };\n\n  private handleKeydown = (ev: KeyboardEvent) => {\n    const { key } = ev;\n\n    if (key === 'ArrowUp') {\n      this.selectPrevLanguage();\n      ev.preventDefault();\n    } else if (key === 'ArrowDown') {\n      this.selectNextLanguage();\n      ev.preventDefault();\n    } else if (key === 'Enter' || key === 'Tab') {\n      this.storeInputLanguage();\n      ev.preventDefault();\n    } else {\n      this.hideList();\n    }\n  };\n\n  private showLangugaeSelectBox = (\n    { top, right }: { top: number; right: number },\n    language: string\n  ) => {\n    if (language) {\n      this.setLanguage(language);\n    }\n\n    this.show();\n\n    const { width } = this.input.parentElement!.getBoundingClientRect();\n\n    css(this.wrapper!, {\n      top: `${top + CODE_BLOCK_PADDING}px`,\n      left: `${right - width - CODE_BLOCK_PADDING}px`,\n    });\n\n    this.toggleFocus();\n  };\n\n  private activateSelectBox() {\n    addClass(this.wrapper, 'active');\n    css(this.list, { display: 'block' });\n  }\n\n  private inactivateSelectBox() {\n    this.input!.value = this.prevStoredLanguage;\n    removeClass(this.wrapper, 'active');\n    this.hideList();\n  }\n\n  private toggleFocus() {\n    if (hasClass(this.wrapper, 'active')) {\n      this.input.blur();\n    } else {\n      this.input.focus();\n    }\n  }\n\n  private storeInputLanguage() {\n    const selectedLanguage = this.input!.value;\n\n    this.setLanguage(selectedLanguage);\n    this.hideList();\n\n    this.eventEmitter.emit('selectLanguage', selectedLanguage);\n  }\n\n  private activateButtonByIndex(index: number) {\n    if (this.currentButton) {\n      removeClass(this.currentButton, 'active');\n    }\n\n    if (this.buttons.length) {\n      this.currentButton = this.buttons[index];\n      this.input!.value = this.currentButton.getAttribute(LANG_ATTR)!;\n      addClass(this.currentButton, 'active');\n      this.currentButton.scrollIntoView();\n    }\n  }\n\n  private selectLanguage(selectedLanguage: string) {\n    this.input!.value = selectedLanguage;\n    this.storeInputLanguage();\n  }\n\n  private selectPrevLanguage() {\n    let index = inArray(this.currentButton, this.buttons) - 1;\n\n    if (index < 0) {\n      index = this.buttons.length - 1;\n    }\n\n    this.activateButtonByIndex(index);\n  }\n\n  private selectNextLanguage() {\n    let index = inArray(this.currentButton, this.buttons) + 1;\n\n    if (index >= this.buttons.length) {\n      index = 0;\n    }\n\n    this.activateButtonByIndex(index);\n  }\n\n  private hideList() {\n    css(this.list, { display: 'none' });\n  }\n\n  show() {\n    css(this.wrapper!, { display: 'inline-block' });\n  }\n\n  hide() {\n    css(this.wrapper!, { display: 'none' });\n  }\n\n  setLanguage(language: string) {\n    this.prevStoredLanguage = language;\n    this.input!.value = language;\n\n    const item = this.buttons.filter((button) => button.getAttribute(LANG_ATTR) === language);\n\n    if (item.length) {\n      const index = inArray(item[0], this.buttons);\n\n      this.activateButtonByIndex(index);\n    }\n  }\n\n  destroy() {\n    removeNode(this.wrapper);\n    this.eventEmitter.removeEventHandler('showCodeBlockLanguages', this.showLangugaeSelectBox);\n  }\n}\n"
  },
  {
    "path": "plugins/code-syntax-highlight/src/plugin.ts",
    "content": "import isFunction from 'tui-code-snippet/type/isFunction';\n\nimport { getHTMLRenderers } from '@/renderers/toHTMLRenderers';\nimport { codeSyntaxHighlighting } from '@/plugins/codeSyntaxHighlighting';\nimport { createCodeSyntaxHighlightView } from '@/nodeViews/codeSyntaxHighlightView';\n\nimport type { PluginContext, PluginInfo } from '@toast-ui/editor';\nimport { PluginOptions } from '@t/index';\n\nexport function codeSyntaxHighlightPlugin(\n  context: PluginContext,\n  options?: PluginOptions\n): PluginInfo {\n  if (options) {\n    const { eventEmitter } = context;\n    const { highlighter: prism } = options;\n\n    eventEmitter.addEventType('showCodeBlockLanguages');\n    eventEmitter.addEventType('selectLanguage');\n    eventEmitter.addEventType('finishLanguageEditing');\n\n    const { languages } = prism!;\n    const registerdlanguages = Object.keys(languages).filter(\n      (language) => !isFunction(languages[language])\n    );\n\n    return {\n      toHTMLRenderers: getHTMLRenderers(prism!),\n      wysiwygPlugins: [() => codeSyntaxHighlighting(context, prism!)],\n      wysiwygNodeViews: {\n        codeBlock: createCodeSyntaxHighlightView(registerdlanguages),\n      },\n    };\n  }\n  return {};\n}\n"
  },
  {
    "path": "plugins/code-syntax-highlight/src/plugins/codeSyntaxHighlighting.ts",
    "content": "import type { Node as ProsemirrorNode } from 'prosemirror-model';\nimport type { Decoration } from 'prosemirror-view';\n\nimport isString from 'tui-code-snippet/type/isString';\n\nimport { flatten } from '@/utils/common';\n\nimport type { PluginContext } from '@toast-ui/editor';\nimport { PrismJs } from '@t/index';\n\ninterface ChildNodeInfo {\n  node: ProsemirrorNode;\n  pos: number;\n}\n\ninterface HighlightedNodeInfo {\n  text: string;\n  classes: string[];\n}\n\nconst NODE_TYPE = 'codeBlock';\n\nfunction findCodeBlocks(doc: ProsemirrorNode) {\n  const descendants: ChildNodeInfo[] = [];\n\n  doc.descendants((node, pos) => {\n    if (node.isBlock && node.type.name === NODE_TYPE) {\n      descendants.push({ node, pos });\n    }\n  });\n\n  return descendants;\n}\n\nfunction parseTokens(\n  tokens: (string | Prism.Token)[],\n  classNames: string[] = []\n): HighlightedNodeInfo[] {\n  if (isString(tokens)) {\n    return [{ text: tokens, classes: classNames }];\n  }\n\n  return tokens.map((token) => {\n    const { type, alias } = token as Prism.Token;\n\n    let typeClassNames: string[] = [];\n    let aliasClassNames: string[] = [];\n\n    if (type) {\n      typeClassNames = ['token', type];\n    }\n\n    if (alias) {\n      aliasClassNames = isString(alias) ? [alias] : alias;\n    }\n\n    const classes: string[] = [...classNames, ...typeClassNames, ...aliasClassNames];\n\n    return isString(token)\n      ? {\n          text: token,\n          classes,\n        }\n      : parseTokens(token.content as Prism.Token[], classes);\n  }) as HighlightedNodeInfo[];\n}\n\nfunction getDecorations(doc: ProsemirrorNode, context: PluginContext, prism: PrismJs) {\n  const { pmView } = context;\n  const decorations: Decoration[] = [];\n  const codeBlocks = findCodeBlocks(doc);\n\n  codeBlocks.forEach(({ pos, node }) => {\n    const { language } = node.attrs;\n    const registeredLang = prism.languages[language];\n    const prismTokens = registeredLang ? prism.tokenize(node.textContent, registeredLang) : [];\n    const nodeInfos = flatten(parseTokens(prismTokens));\n\n    let startPos = pos + 1;\n\n    nodeInfos.forEach(({ text, classes }) => {\n      const from = startPos;\n      const to = from + text.length;\n\n      startPos = to;\n\n      const classNames = classes.join(' ');\n      const decoration = pmView.Decoration.inline(from, to, {\n        class: classNames,\n      });\n\n      if (classNames.length) {\n        decorations.push(decoration);\n      }\n    });\n  });\n\n  return pmView.DecorationSet.create(doc, decorations);\n}\n\nexport function codeSyntaxHighlighting(context: PluginContext, prism: PrismJs) {\n  return new context.pmState.Plugin({\n    state: {\n      init(_, { doc }) {\n        return getDecorations(doc, context, prism);\n      },\n      apply(tr, set) {\n        if (!tr.docChanged) {\n          return set.map(tr.mapping, tr.doc);\n        }\n\n        return getDecorations(tr.doc, context, prism);\n      },\n    },\n    props: {\n      decorations(state) {\n        return this.getState(state);\n      },\n    },\n  });\n}\n"
  },
  {
    "path": "plugins/code-syntax-highlight/src/prismjs-langs.ts",
    "content": "import 'prismjs/components/prism-abap.js';\nimport 'prismjs/components/prism-abnf.js';\nimport 'prismjs/components/prism-actionscript.js';\nimport 'prismjs/components/prism-ada.js';\nimport 'prismjs/components/prism-agda.js';\nimport 'prismjs/components/prism-al.js';\nimport 'prismjs/components/prism-antlr4.js';\nimport 'prismjs/components/prism-apacheconf.js';\nimport 'prismjs/components/prism-apex.js';\nimport 'prismjs/components/prism-apl.js';\nimport 'prismjs/components/prism-applescript.js';\nimport 'prismjs/components/prism-aql.js';\nimport 'prismjs/components/prism-arff.js';\nimport 'prismjs/components/prism-asciidoc.js';\nimport 'prismjs/components/prism-asm6502.js';\nimport 'prismjs/components/prism-aspnet.js';\nimport 'prismjs/components/prism-autohotkey.js';\nimport 'prismjs/components/prism-autoit.js';\nimport 'prismjs/components/prism-bash.js';\nimport 'prismjs/components/prism-basic.js';\nimport 'prismjs/components/prism-batch.js';\nimport 'prismjs/components/prism-bbcode.js';\nimport 'prismjs/components/prism-birb.js';\nimport 'prismjs/components/prism-bnf.js';\nimport 'prismjs/components/prism-brainfuck.js';\nimport 'prismjs/components/prism-brightscript.js';\nimport 'prismjs/components/prism-bro.js';\nimport 'prismjs/components/prism-bsl.js';\nimport 'prismjs/components/prism-c.js';\nimport 'prismjs/components/prism-bison.js';\nimport 'prismjs/components/prism-cil.js';\nimport 'prismjs/components/prism-clojure.js';\nimport 'prismjs/components/prism-cmake.js';\nimport 'prismjs/components/prism-coffeescript.js';\nimport 'prismjs/components/prism-concurnas.js';\nimport 'prismjs/components/prism-cpp.js';\nimport 'prismjs/components/prism-arduino.js';\nimport 'prismjs/components/prism-csharp.js';\nimport 'prismjs/components/prism-csp.js';\nimport 'prismjs/components/prism-css-extras.js';\nimport 'prismjs/components/prism-cypher.js';\nimport 'prismjs/components/prism-d.js';\nimport 'prismjs/components/prism-dart.js';\nimport 'prismjs/components/prism-dataweave.js';\nimport 'prismjs/components/prism-dax.js';\nimport 'prismjs/components/prism-dhall.js';\nimport 'prismjs/components/prism-diff.js';\nimport 'prismjs/components/prism-markup-templating.js';\nimport 'prismjs/components/prism-django.js';\nimport 'prismjs/components/prism-dns-zone-file.js';\nimport 'prismjs/components/prism-docker.js';\nimport 'prismjs/components/prism-ebnf.js';\nimport 'prismjs/components/prism-editorconfig.js';\nimport 'prismjs/components/prism-eiffel.js';\nimport 'prismjs/components/prism-ejs.js';\nimport 'prismjs/components/prism-elixir.js';\nimport 'prismjs/components/prism-elm.js';\nimport 'prismjs/components/prism-erb.js';\nimport 'prismjs/components/prism-erlang.js';\nimport 'prismjs/components/prism-etlua.js';\nimport 'prismjs/components/prism-excel-formula.js';\nimport 'prismjs/components/prism-factor.js';\nimport 'prismjs/components/prism-firestore-security-rules.js';\nimport 'prismjs/components/prism-flow.js';\nimport 'prismjs/components/prism-fortran.js';\nimport 'prismjs/components/prism-fsharp.js';\nimport 'prismjs/components/prism-ftl.js';\nimport 'prismjs/components/prism-gcode.js';\nimport 'prismjs/components/prism-gdscript.js';\nimport 'prismjs/components/prism-gedcom.js';\nimport 'prismjs/components/prism-gherkin.js';\nimport 'prismjs/components/prism-git.js';\nimport 'prismjs/components/prism-glsl.js';\nimport 'prismjs/components/prism-gml.js';\nimport 'prismjs/components/prism-go.js';\nimport 'prismjs/components/prism-graphql.js';\nimport 'prismjs/components/prism-groovy.js';\nimport 'prismjs/components/prism-haml.js';\nimport 'prismjs/components/prism-handlebars.js';\nimport 'prismjs/components/prism-haskell.js';\nimport 'prismjs/components/prism-haxe.js';\nimport 'prismjs/components/prism-hcl.js';\nimport 'prismjs/components/prism-hlsl.js';\nimport 'prismjs/components/prism-hpkp.js';\nimport 'prismjs/components/prism-hsts.js';\nimport 'prismjs/components/prism-http.js';\nimport 'prismjs/components/prism-ichigojam.js';\nimport 'prismjs/components/prism-icon.js';\nimport 'prismjs/components/prism-iecst.js';\nimport 'prismjs/components/prism-ignore.js';\nimport 'prismjs/components/prism-inform7.js';\nimport 'prismjs/components/prism-ini.js';\nimport 'prismjs/components/prism-io.js';\nimport 'prismjs/components/prism-j.js';\nimport 'prismjs/components/prism-java.js';\nimport 'prismjs/components/prism-javadoclike.js';\nimport 'prismjs/components/prism-javadoc.js';\nimport 'prismjs/components/prism-typescript.js';\nimport 'prismjs/components/prism-javastacktrace.js';\nimport 'prismjs/components/prism-jolie.js';\nimport 'prismjs/components/prism-jq.js';\nimport 'prismjs/components/prism-js-extras.js';\nimport 'prismjs/components/prism-js-templates.js';\nimport 'prismjs/components/prism-jsdoc.js';\nimport 'prismjs/components/prism-json.js';\nimport 'prismjs/components/prism-json5.js';\nimport 'prismjs/components/prism-jsonp.js';\nimport 'prismjs/components/prism-jsstacktrace.js';\nimport 'prismjs/components/prism-jsx.js';\nimport 'prismjs/components/prism-julia.js';\nimport 'prismjs/components/prism-keyman.js';\nimport 'prismjs/components/prism-kotlin.js';\nimport 'prismjs/components/prism-latex.js';\nimport 'prismjs/components/prism-latte.js';\nimport 'prismjs/components/prism-less.js';\nimport 'prismjs/components/prism-lilypond.js';\nimport 'prismjs/components/prism-liquid.js';\nimport 'prismjs/components/prism-lisp.js';\nimport 'prismjs/components/prism-livescript.js';\nimport 'prismjs/components/prism-llvm.js';\nimport 'prismjs/components/prism-lolcode.js';\nimport 'prismjs/components/prism-lua.js';\nimport 'prismjs/components/prism-makefile.js';\nimport 'prismjs/components/prism-markdown.js';\nimport 'prismjs/components/prism-matlab.js';\nimport 'prismjs/components/prism-mel.js';\nimport 'prismjs/components/prism-mizar.js';\nimport 'prismjs/components/prism-mongodb.js';\nimport 'prismjs/components/prism-monkey.js';\nimport 'prismjs/components/prism-moonscript.js';\nimport 'prismjs/components/prism-n1ql.js';\nimport 'prismjs/components/prism-n4js.js';\nimport 'prismjs/components/prism-nand2tetris-hdl.js';\nimport 'prismjs/components/prism-naniscript.js';\nimport 'prismjs/components/prism-nasm.js';\nimport 'prismjs/components/prism-neon.js';\nimport 'prismjs/components/prism-nginx.js';\nimport 'prismjs/components/prism-nim.js';\nimport 'prismjs/components/prism-nix.js';\nimport 'prismjs/components/prism-nsis.js';\nimport 'prismjs/components/prism-objectivec.js';\nimport 'prismjs/components/prism-ocaml.js';\nimport 'prismjs/components/prism-opencl.js';\nimport 'prismjs/components/prism-oz.js';\nimport 'prismjs/components/prism-parigp.js';\nimport 'prismjs/components/prism-parser.js';\nimport 'prismjs/components/prism-pascal.js';\nimport 'prismjs/components/prism-pascaligo.js';\nimport 'prismjs/components/prism-pcaxis.js';\nimport 'prismjs/components/prism-peoplecode.js';\nimport 'prismjs/components/prism-perl.js';\nimport 'prismjs/components/prism-php-extras.js';\nimport 'prismjs/components/prism-php.js';\nimport 'prismjs/components/prism-phpdoc.js';\nimport 'prismjs/components/prism-sql.js';\nimport 'prismjs/components/prism-plsql.js';\nimport 'prismjs/components/prism-powerquery.js';\nimport 'prismjs/components/prism-powershell.js';\nimport 'prismjs/components/prism-processing.js';\nimport 'prismjs/components/prism-prolog.js';\nimport 'prismjs/components/prism-promql.js';\nimport 'prismjs/components/prism-properties.js';\nimport 'prismjs/components/prism-protobuf.js';\nimport 'prismjs/components/prism-pug.js';\nimport 'prismjs/components/prism-puppet.js';\nimport 'prismjs/components/prism-pure.js';\nimport 'prismjs/components/prism-purebasic.js';\nimport 'prismjs/components/prism-purescript.js';\nimport 'prismjs/components/prism-python.js';\nimport 'prismjs/components/prism-q.js';\nimport 'prismjs/components/prism-qml.js';\nimport 'prismjs/components/prism-qore.js';\nimport 'prismjs/components/prism-r.js';\nimport 'prismjs/components/prism-scheme.js';\nimport 'prismjs/components/prism-racket.js';\nimport 'prismjs/components/prism-reason.js';\nimport 'prismjs/components/prism-regex.js';\nimport 'prismjs/components/prism-renpy.js';\nimport 'prismjs/components/prism-rest.js';\nimport 'prismjs/components/prism-rip.js';\nimport 'prismjs/components/prism-roboconf.js';\nimport 'prismjs/components/prism-robotframework.js';\nimport 'prismjs/components/prism-ruby.js';\nimport 'prismjs/components/prism-crystal.js';\nimport 'prismjs/components/prism-rust.js';\nimport 'prismjs/components/prism-sas.js';\nimport 'prismjs/components/prism-sass.js';\nimport 'prismjs/components/prism-scala.js';\nimport 'prismjs/components/prism-scss.js';\nimport 'prismjs/components/prism-shell-session.js';\nimport 'prismjs/components/prism-smali.js';\nimport 'prismjs/components/prism-smalltalk.js';\nimport 'prismjs/components/prism-smarty.js';\nimport 'prismjs/components/prism-sml.js';\nimport 'prismjs/components/prism-solidity.js';\nimport 'prismjs/components/prism-solution-file.js';\nimport 'prismjs/components/prism-soy.js';\nimport 'prismjs/components/prism-turtle.js';\nimport 'prismjs/components/prism-sparql.js';\nimport 'prismjs/components/prism-splunk-spl.js';\nimport 'prismjs/components/prism-sqf.js';\nimport 'prismjs/components/prism-stan.js';\nimport 'prismjs/components/prism-stylus.js';\nimport 'prismjs/components/prism-swift.js';\nimport 'prismjs/components/prism-t4-templating.js';\nimport 'prismjs/components/prism-t4-cs.js';\nimport 'prismjs/components/prism-t4-vb.js';\nimport 'prismjs/components/prism-tap.js';\nimport 'prismjs/components/prism-tcl.js';\nimport 'prismjs/components/prism-textile.js';\nimport 'prismjs/components/prism-toml.js';\nimport 'prismjs/components/prism-tsx.js';\nimport 'prismjs/components/prism-tt2.js';\nimport 'prismjs/components/prism-twig.js';\nimport 'prismjs/components/prism-typoscript.js';\nimport 'prismjs/components/prism-unrealscript.js';\nimport 'prismjs/components/prism-vala.js';\nimport 'prismjs/components/prism-vbnet.js';\nimport 'prismjs/components/prism-velocity.js';\nimport 'prismjs/components/prism-verilog.js';\nimport 'prismjs/components/prism-vhdl.js';\nimport 'prismjs/components/prism-vim.js';\nimport 'prismjs/components/prism-visual-basic.js';\nimport 'prismjs/components/prism-warpscript.js';\nimport 'prismjs/components/prism-wasm.js';\nimport 'prismjs/components/prism-wiki.js';\nimport 'prismjs/components/prism-xeora.js';\nimport 'prismjs/components/prism-xml-doc.js';\nimport 'prismjs/components/prism-xojo.js';\nimport 'prismjs/components/prism-xquery.js';\nimport 'prismjs/components/prism-yaml.js';\nimport 'prismjs/components/prism-yang.js';\nimport 'prismjs/components/prism-zig.js';\n"
  },
  {
    "path": "plugins/code-syntax-highlight/src/renderers/toHTMLRenderers.ts",
    "content": "import type { MdNode, CodeBlockMdNode } from '@toast-ui/editor';\nimport type { HTMLToken } from '@toast-ui/toastmark';\nimport { PrismJs } from '@t/index';\n\nconst BACKTICK_COUNT = 3;\n\nexport function getHTMLRenderers(prism: PrismJs) {\n  return {\n    codeBlock(node: MdNode): HTMLToken[] {\n      const { fenceLength, info } = node as CodeBlockMdNode;\n      const infoWords = info ? info.split(/\\s+/) : [];\n      const preClasses = [];\n      const codeAttrs: Record<string, any> = {};\n\n      if (fenceLength > BACKTICK_COUNT) {\n        codeAttrs['data-backticks'] = fenceLength;\n      }\n\n      let content = node.literal!;\n\n      if (infoWords.length && infoWords[0].length) {\n        const [lang] = infoWords;\n\n        preClasses.push(`lang-${lang}`);\n        codeAttrs['data-language'] = lang;\n\n        const registeredLang = prism.languages[lang];\n\n        if (registeredLang) {\n          content = prism.highlight(node.literal!, registeredLang, lang);\n        }\n      }\n\n      return [\n        { type: 'openTag', tagName: 'pre', classNames: preClasses },\n        { type: 'openTag', tagName: 'code', attributes: codeAttrs },\n        { type: 'html', content },\n        { type: 'closeTag', tagName: 'code' },\n        { type: 'closeTag', tagName: 'pre' },\n      ];\n    },\n  };\n}\n"
  },
  {
    "path": "plugins/code-syntax-highlight/src/utils/common.ts",
    "content": "export function flatten<T>(arr: T[]): T[] {\n  return arr.reduce<T[]>((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []);\n}\n"
  },
  {
    "path": "plugins/code-syntax-highlight/src/utils/dom.ts",
    "content": "function stringToNumber(value: string) {\n  return parseInt(value, 10);\n}\n\nexport function isPositionInBox(style: CSSStyleDeclaration, offsetX: number, offsetY: number) {\n  const left = stringToNumber(style.left);\n  const top = stringToNumber(style.top);\n  const width =\n    stringToNumber(style.width) +\n    stringToNumber(style.paddingLeft) +\n    stringToNumber(style.paddingRight);\n  const height =\n    stringToNumber(style.height) +\n    stringToNumber(style.paddingTop) +\n    stringToNumber(style.paddingBottom);\n\n  return offsetX >= left && offsetX <= left + width && offsetY >= top && offsetY <= top + height;\n}\n\nexport function removeNode(node: Node) {\n  if (node.parentNode) {\n    node.parentNode.removeChild(node);\n  }\n}\n\nconst CLS_PREFIX = 'toastui-editor-';\n\nexport function cls(...names: string[]) {\n  return names.map((className) => `${CLS_PREFIX}${className}`).join(' ');\n}\n"
  },
  {
    "path": "plugins/code-syntax-highlight/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"src/**/*.ts\", \"src/**/*.js\", \"types/**/*\", \"../../types/**/*\"],\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"importHelpers\": false,\n    \"paths\": {\n      \"@/*\": [\"src/*\"],\n      \"@t/*\": [\"types/*\"]\n    },\n    \"lib\": [\"esnext\", \"dom\", \"dom.iterable\"]\n  }\n}"
  },
  {
    "path": "plugins/code-syntax-highlight/types/index.d.ts",
    "content": "import type { PluginContext, PluginInfo } from '@toast-ui/editor';\nimport Prism from 'prismjs';\n\ntype PrismJs = typeof Prism & {\n  manual: boolean;\n};\n\ndeclare global {\n  interface Window {\n    Prism: PrismJs;\n  }\n}\n\nexport type PluginOptions = {\n  highlighter?: PrismJs;\n};\n\nexport default function codeSyntaxHighlightPlugin(\n  context: PluginContext,\n  options: PluginOptions\n): PluginInfo;\n"
  },
  {
    "path": "plugins/code-syntax-highlight/types/prosemirror-transform.d.ts",
    "content": "import { Node, Mark } from 'prosemirror-model';\nimport 'prosemirror-transform';\n\ndeclare module 'prosemirror-transform' {\n  export interface Transform {\n    setNodeMarkup(\n      pos: number,\n      type: Node | null,\n      attrs?: { [key: string]: any },\n      marks?: Mark[]\n    ): Transform;\n  }\n}\n"
  },
  {
    "path": "plugins/code-syntax-highlight/webpack.config.js",
    "content": "/* eslint-disable @typescript-eslint/no-var-requires */\nconst path = require('path');\nconst webpack = require('webpack');\nconst { name, version, author, license } = require('./package.json');\n\nconst TerserPlugin = require('terser-webpack-plugin');\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin');\nconst CssMinimizerPlugin = require('css-minimizer-webpack-plugin');\nconst ESLintPlugin = require('eslint-webpack-plugin');\n\nconst filename = `toastui-${name.replace(/@toast-ui\\//, '')}`;\n\nconst ENTRY = './src/index.ts';\nconst ENTRY_ALL_LANG = './src/indexAll.ts';\n\nfunction getOutputConfig(isProduction, isCDN, isAll, minify) {\n  const defaultConfig = {\n    environment: {\n      arrowFunction: false,\n      const: false,\n    },\n  };\n\n  if (!isProduction || isCDN) {\n    const config = {\n      ...defaultConfig,\n      library: {\n        name: ['toastui', 'Editor', 'plugin', 'codeSyntaxHighlight'],\n        export: 'default',\n        type: 'umd',\n      },\n      path: path.resolve(__dirname, 'dist/cdn'),\n      filename: `${filename}${isAll ? '-all' : ''}${minify ? '.min' : ''}.js`,\n    };\n\n    if (!isProduction) {\n      config.publicPath = '/dist/cdn';\n    }\n\n    return config;\n  }\n\n  return {\n    ...defaultConfig,\n    library: {\n      export: 'default',\n      type: 'commonjs2',\n    },\n    path: path.resolve(__dirname, 'dist'),\n    filename: `${filename}${isAll ? '-all' : ''}.js`,\n  };\n}\n\nfunction getOptimizationConfig(isProduction, minify) {\n  const minimizer = [];\n\n  if (isProduction && minify) {\n    minimizer.push(\n      new TerserPlugin({\n        parallel: true,\n        extractComments: false,\n      })\n    );\n    minimizer.push(new CssMinimizerPlugin());\n  }\n\n  return { minimizer };\n}\n\nmodule.exports = (env) => {\n  const isProduction = env.WEBPACK_BUILD;\n  const { minify = false, cdn = false, all = false } = env;\n  const config = {\n    mode: isProduction ? 'production' : 'development',\n    entry: all ? ENTRY_ALL_LANG : ENTRY,\n    output: getOutputConfig(isProduction, cdn, all, minify),\n    externals: ['prosemirror-state', 'prosemirror-view'],\n    module: {\n      rules: [\n        {\n          test: /\\.ts$|\\.js$/,\n          use: [\n            {\n              loader: 'ts-loader',\n              options: {\n                transpileOnly: true,\n              },\n            },\n          ],\n          exclude: /node_modules/,\n        },\n        {\n          test: /\\.css$/,\n          use: [MiniCssExtractPlugin.loader, 'css-loader'],\n        },\n      ],\n    },\n    resolve: {\n      extensions: ['.ts', '.js'],\n      alias: {\n        '@': path.resolve('src'),\n        '@t': path.resolve('types'),\n      },\n    },\n    plugins: [\n      new MiniCssExtractPlugin({\n        filename: () => `${filename}${minify ? '.min' : ''}.css`,\n      }),\n      new ESLintPlugin({\n        extensions: ['js', 'ts'],\n        exclude: ['node_modules', 'dist'],\n        failOnError: isProduction,\n      }),\n    ],\n    optimization: getOptimizationConfig(isProduction, minify),\n  };\n\n  if (isProduction) {\n    config.plugins.push(\n      new webpack.BannerPlugin(\n        [\n          'TOAST UI Editor : Code Syntax Highlight Plugin',\n          `@version ${version} | ${new Date().toDateString()}`,\n          `@author ${author}`,\n          `@license ${license}`,\n        ].join('\\n')\n      )\n    );\n  } else {\n    config.devServer = {\n      // https://github.com/webpack/webpack-dev-server/issues/2484\n      injectClient: false,\n      inline: true,\n      host: '0.0.0.0',\n      port: 8081,\n    };\n    config.devtool = 'inline-source-map';\n  }\n\n  return config;\n};\n"
  },
  {
    "path": "plugins/color-syntax/README.md",
    "content": "# TOAST UI Editor : Color Syntax Plugin\n\n> This is a plugin of [TOAST UI Editor](https://github.com/nhn/tui.editor/tree/master/apps/editor) to color editing text.\n\n[![npm version](https://img.shields.io/npm/v/@toast-ui/editor-plugin-color-syntax.svg)](https://www.npmjs.com/package/@toast-ui/editor-plugin-color-syntax)\n\n![color-syntax](https://user-images.githubusercontent.com/37766175/121813686-28710680-cca8-11eb-87c6-1dc9625369b0.png)\n\n## 🚩 Table of Contents\n\n- [Bundle File Structure](#-bundle-file-structure)\n- [Usage npm](#-usage-npm)\n- [Usage CDN](#-usage-cdn)\n\n## 📁 Bundle File Structure\n\n### Files Distributed on npm\n\n```\n- node_modules/\n  - @toast-ui/\n    - editor-plugin-color-syntax/\n      - dist/\n        - toastui-editor-plugin-color-syntax.js\n        - toastui-editor-plugin-color-syntax.css\n```\n\n### Files Distributed on CDN\n\nThe bundle files include all dependencies of this plugin.\n\n```\n- uicdn.toast.com/\n  - editor-plugin-color-syntax/\n    - latest/\n      - toastui-editor-plugin-color-syntax.js\n      - toastui-editor-plugin-color-syntax.min.js\n      - toastui-editor-plugin-color-syntax.css\n      - toastui-editor-plugin-color-syntax.min.css\n```\n\n## 📦 Usage npm\n\nTo use the plugin, [`@toast-ui/editor`](https://github.com/nhn/tui.editor/tree/master/apps/editor) must be installed.\n\n> Ref. [Getting Started](https://github.com/nhn/tui.editor/blob/master/docs/en/getting-started.md)\n\n### Install\n\n```sh\n$ npm install @toast-ui/editor-plugin-color-syntax\n```\n\n### Import Plugin\n\nAlong with the plugin, the plugin's dependency style must be imported. The `color-syntax` plugin has [TOAST UI Color Picker](https://github.com/nhn/tui.color-picker) as a dependency, and you need to add a CSS file of TOAST UI Color Picker.\n\n#### ES Modules\n\n```js\nimport 'tui-color-picker/dist/tui-color-picker.css';\nimport '@toast-ui/editor-plugin-color-syntax/dist/toastui-editor-plugin-color-syntax.css';\n\nimport colorSyntax from '@toast-ui/editor-plugin-color-syntax';\n```\n\n#### CommonJS\n\n```js\nrequire('tui-color-picker/dist/tui-color-picker.css');\nrequire('@toast-ui/editor-plugin-color-syntax/dist/toastui-editor-plugin-color-syntax.css');\n\nconst colorSyntax = require('@toast-ui/editor-plugin-color-syntax');\n```\n\n### Create Instance\n\n#### Basic\n\n```js\n// ...\nimport 'tui-color-picker/dist/tui-color-picker.css';\nimport '@toast-ui/editor-plugin-color-syntax/dist/toastui-editor-plugin-color-syntax.css';\n\nimport Editor from '@toast-ui/editor';\nimport colorSyntax from '@toast-ui/editor-plugin-color-syntax';\n\nconst editor = new Editor({\n  // ...\n  plugins: [colorSyntax]\n});\n```\n\n## 🗂 Usage CDN\n\nTo use the plugin, the CDN files(CSS, Script) of `@toast-ui/editor` must be included.\n\n### Include Files\n\n```html\n...\n<head>\n  ...\n  <link\n    rel=\"stylesheet\"\n    href=\"https://uicdn.toast.com/tui-color-picker/latest/tui-color-picker.min.css\"\n  />\n  <link\n    rel=\"stylesheet\"\n    href=\"https://uicdn.toast.com/editor-plugin-color-syntax/latest/toastui-editor-plugin-color-syntax.min.css\"\n  />\n  ...\n</head>\n<body>\n  ...\n  <!-- Color Picker -->\n  <script src=\"https://uicdn.toast.com/tui-color-picker/latest/tui-color-picker.min.js\"></script>\n  <!-- Editor -->\n  <script src=\"https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js\"></script>\n  <!-- Editor's Plugin -->\n  <script src=\"https://uicdn.toast.com/editor-plugin-color-syntax/latest/toastui-editor-plugin-color-syntax.min.js\"></script>\n  ...\n</body>\n...\n```\n\n### Create Instance\n\n#### Basic\n\n```js\nconst { Editor } = toastui;\nconst { colorSyntax } = Editor.plugin;\n\nconst editor = new Editor({\n  // ...\n  plugins: [colorSyntax]\n});\n```\n\n### [Optional] Use Plugin with Options\n\nThe `color-syntax` plugin can set options when used. Just add the plugin function and options related to the plugin to the array(`[pluginFn, pluginOptions]`) and push them to the `plugins` option of the editor.\n\nThe following options are available in the `color-syntax` plugin.\n\n| Name              | Type             | Default Value | Description                      |\n| ----------------- | ---------------- | ------------- | -------------------------------- |\n| `preset`          | `Array.<string>` |               | Preset for color palette         |\n\n```js\n// ...\nimport 'tui-color-picker/dist/tui-color-picker.css';\nimport '@toast-ui/editor-plugin-color-syntax/dist/toastui-editor-plugin-color-syntax.css';\n\nimport Editor from '@toast-ui/editor';\nimport colorSyntax from '@toast-ui/editor-plugin-color-syntax';\n\nconst colorSyntaxOptions = {\n  preset: ['#181818', '#292929', '#393939']\n};\n\nconst editor = new Editor({\n  // ...\n  plugins: [[colorSyntax, colorSyntaxOptions]]\n});\n```\n"
  },
  {
    "path": "plugins/color-syntax/demo/editor.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>Editor</title>\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"http://localhost:8080/dist/cdn/toastui-editor.css\" />\n    <!-- Plugin -->\n    <link rel=\"stylesheet\" href=\"https://uicdn.toast.com/tui-color-picker/v2.2.6/tui-color-picker.css\" />\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor-plugin-color-syntax.css\" />\n  </head>\n  <body>\n    <div class=\"code-html\">\n      <div id=\"editor\"></div>\n    </div>\n    <!-- Editor -->\n    <script src=\"http://localhost:8080/dist/cdn/toastui-editor-all.js\"></script>\n    <script src=\"https://uicdn.toast.com/tui-color-picker/v2.2.6/tui-color-picker.min.js\"></script>\n    <!-- Plugin -->\n    <script src=\"../dist/cdn/toastui-editor-plugin-color-syntax.js\"></script>\n    <script class=\"code-js\">\n      const { Editor } = toastui;\n      const { colorSyntax } = Editor.plugin;\n\n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialEditType: 'wysiwyg',\n        initialValue: 'Select some text and choose a color from the toolbar.',\n        plugins: [colorSyntax]\n      });\n    </script>\n  </body>\n</html>"
  },
  {
    "path": "plugins/color-syntax/demo/esm/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>Editor</title>\n    <!-- Plugin -->\n    <link\n      rel=\"stylesheet\"\n      href=\"https://uicdn.toast.com/tui-color-picker/v2.2.6/tui-color-picker.css\"\n    />\n  </head>\n  <body>\n    <div class=\"code-html\">\n      <div id=\"editor\"></div>\n    </div>\n    <!-- Editor -->\n    <script type=\"module\">\n      import { Editor } from 'http://localhost:8080/dist/index.js';\n      import colorPickerPlugin from '/dist/index.js';\n\n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: 'Select some text and choose a color from the toolbar.',\n        plugins: [colorPickerPlugin]\n      });\n      window.editor = editor;\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "plugins/color-syntax/jest.config.js",
    "content": "// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst base = require('../../jest.base.config');\n\nmodule.exports = {\n  ...base,\n  testEnvironment: 'jsdom',\n  moduleNameMapper: {\n    '^@/(.*)$': '<rootDir>/src/$1',\n  },\n};\n"
  },
  {
    "path": "plugins/color-syntax/package.json",
    "content": "{\n  \"name\": \"@toast-ui/editor-plugin-color-syntax\",\n  \"version\": \"3.1.0\",\n  \"description\": \"TOAST UI Editor : Color Syntax Plugin\",\n  \"keywords\": [\n    \"nhn\",\n    \"nhn cloud\",\n    \"toast\",\n    \"toastui\",\n    \"toast-ui\",\n    \"editor\",\n    \"plugin\",\n    \"color-syntax\",\n    \"color-picker\"\n  ],\n  \"main\": \"dist/toastui-editor-plugin-color-syntax.js\",\n  \"files\": [\n    \"dist/*.js\",\n    \"dist/*.css\",\n    \"types/index.d.ts\"\n  ],\n  \"types\": \"types/index.d.ts\",\n  \"author\": \"NHN Cloud FE Development Lab <dl_javascript@nhn.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/nhn/tui.editor.git\",\n    \"directory\": \"plugins/color-syntax\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/nhn/tui.editor/issues\"\n  },\n  \"homepage\": \"https://ui.toast.com\",\n  \"browserslist\": \"last 2 versions, not ie <= 10\",\n  \"scripts\": {\n    \"lint\": \"eslint .\",\n    \"test:types\": \"tsc\",\n    \"test\": \"jest --watch\",\n    \"test:ci\": \"jest\",\n    \"serve\": \"snowpack dev\",\n    \"serve:ie\": \"webpack serve\",\n    \"build:cdn\": \"webpack build --env cdn & webpack build --env cdn minify\",\n    \"build\": \"webpack build && npm run build:cdn\"\n  },\n  \"devDependencies\": {\n    \"cross-env\": \"^6.0.3\"\n  },\n  \"dependencies\": {\n    \"tui-color-picker\": \"^2.2.6\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  }\n}\n"
  },
  {
    "path": "plugins/color-syntax/snowpack.config.js",
    "content": "// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst httpProxy = require('http-proxy');\nconst proxy = httpProxy.createServer({ target: 'http://localhost:8080' });\n\n/** @type {import(\"snowpack\").SnowpackUserConfig } */\nmodule.exports = {\n  mount: {\n    'demo/esm': '/',\n    src: '/dist',\n  },\n  alias: {\n    '@t': './types',\n  },\n  devOptions: {\n    port: 8081,\n  },\n  routes: [\n    {\n      src: '/img/.*',\n      dest: (req, res) => {\n        proxy.web(req, res);\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "plugins/color-syntax/src/__test__/integration/colorSyntaxPlugin.spec.ts",
    "content": "import Editor from '@toast-ui/editor';\nimport colorPicker from 'tui-color-picker';\nimport { oneLineTrim } from 'common-tags';\nimport colorSyntaxPlugin from '@/index';\nimport { removeProseMirrorHackNodes } from '@/utils/dom';\n\nfunction removeDataAttr(html: string) {\n  return html\n    .replace(/\\sdata-nodeid=\"\\d{1,}\"/g, '')\n    .replace(/\\n/g, '')\n    .trim();\n}\n\ndescribe('colorSyntax', () => {\n  let container: HTMLElement, editor: Editor;\n\n  function assertWwEditorHTML(html: string) {\n    const wwEditorEl = editor.getEditorElements().wwEditor;\n\n    const wwEditorHTML = removeProseMirrorHackNodes(wwEditorEl.outerHTML);\n\n    expect(wwEditorHTML).toContain(html);\n  }\n\n  function assertMdPreviewHTML(html: string) {\n    const mdPreviewEl = editor.getEditorElements().mdPreview;\n\n    expect(removeDataAttr(mdPreviewEl.innerHTML)).toContain(html);\n  }\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n  });\n\n  afterEach(() => {\n    editor.destroy();\n    document.body.removeChild(container);\n  });\n\n  describe('usageStatistics option', () => {\n    it('when setting false, GA of color picker is disabled', () => {\n      jest.spyOn(colorPicker, 'create');\n\n      editor = new Editor({\n        el: container,\n        previewStyle: 'vertical',\n        plugins: [colorSyntaxPlugin],\n        usageStatistics: false,\n      });\n\n      expect(colorPicker.create).toHaveBeenCalledWith(\n        expect.objectContaining({\n          usageStatistics: false,\n        })\n      );\n    });\n\n    it('when setting true, GA of color picker is enabled', () => {\n      jest.spyOn(colorPicker, 'create');\n\n      editor = new Editor({\n        el: container,\n        previewStyle: 'vertical',\n        plugins: [colorSyntaxPlugin],\n      });\n\n      expect(colorPicker.create).toHaveBeenCalledWith(\n        expect.objectContaining({\n          usageStatistics: true,\n        })\n      );\n    });\n  });\n\n  describe('convertor', () => {\n    beforeEach(() => {\n      editor = new Editor({\n        el: container,\n        previewStyle: 'vertical',\n        height: '100px',\n        initialEditType: 'markdown',\n        plugins: [colorSyntaxPlugin],\n      });\n    });\n\n    it('should convert markdown to wysiwyg properly', () => {\n      editor.setMarkdown('text');\n      editor.exec('selectAll');\n      editor.exec('color', { selectedColor: '#f0f' });\n\n      editor.changeMode('wysiwyg');\n\n      assertWwEditorHTML('<p><span style=\"color: #f0f\">text</span></p>');\n    });\n\n    it('should convert wysiwyg to markdown properly', () => {\n      editor.setMarkdown('text');\n      editor.exec('selectAll');\n      editor.exec('color', { selectedColor: '#f0f' });\n\n      editor.changeMode('wysiwyg');\n      editor.changeMode('markdown');\n\n      assertMdPreviewHTML('<span style=\"color: #f0f\">text</span>');\n    });\n\n    it('should convert markdown to wysiwyg in table cell properly', () => {\n      editor.exec('addTable', {\n        columnCount: 2,\n        rowCount: 2,\n      });\n      editor.setSelection([1, 5], [1, 5]);\n      editor.insertText('foo');\n      editor.setSelection([1, 5], [1, 8]);\n\n      editor.exec('color', { selectedColor: '#f0f' });\n\n      editor.changeMode('wysiwyg');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p><span style=\"color: #f0f\">foo</span></p></th>\n              <th><p><br></p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p><br></p></td>\n              <td><p><br></p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      assertWwEditorHTML(expected);\n    });\n\n    it('should convert wysiwyg to markdown in table cell properly', () => {\n      editor.changeMode('wysiwyg');\n\n      editor.exec('addTable', {\n        rowCount: 2,\n        columnCount: 2,\n        data: ['foo', 'bar', 'baz', 'qux'],\n      });\n      editor.setSelection(4, 8);\n      editor.exec('color', { selectedColor: '#f0f' });\n\n      editor.changeMode('markdown');\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><span style=\"color: #f0f\">foo</span></th>\n              <th>bar</th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td>baz</td>\n              <td>qux</td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      assertMdPreviewHTML(expected);\n    });\n  });\n\n  describe('commands', () => {\n    beforeEach(() => {\n      editor = new Editor({\n        el: container,\n        previewStyle: 'vertical',\n        height: '100px',\n        initialEditType: 'markdown',\n        plugins: [colorSyntaxPlugin],\n      });\n    });\n\n    it('add color in markdown', () => {\n      editor.setMarkdown('text');\n      editor.exec('selectAll');\n      editor.exec('color', { selectedColor: '#f0f' });\n\n      assertMdPreviewHTML('<span style=\"color: #f0f\">text</span>');\n    });\n\n    it(`don't add color if value isn't truthy in markdown`, () => {\n      editor.setMarkdown('text');\n      editor.exec('selectAll');\n      editor.exec('color');\n\n      assertMdPreviewHTML('<p class=\"toastui-editor-md-preview-highlight\">text</p>');\n    });\n\n    it('add color in wysiwyg', () => {\n      editor.setMarkdown('text');\n      editor.changeMode('wysiwyg');\n\n      editor.exec('selectAll');\n      editor.exec('color', { selectedColor: '#f0f' });\n\n      assertWwEditorHTML('<p><span style=\"color: #f0f\">text</span></p>');\n    });\n\n    it(`don't add color if value isn't truthy in wysiwyg`, () => {\n      editor.setMarkdown('text');\n      editor.changeMode('wysiwyg');\n\n      editor.exec('selectAll');\n      editor.exec('color');\n\n      assertWwEditorHTML('<p>text</p>');\n    });\n\n    it('add color in selected table cell in wysiwyg', () => {\n      editor.changeMode('wysiwyg');\n\n      editor.exec('addTable', {\n        rowCount: 2,\n        columnCount: 2,\n        data: ['foo', 'bar', 'baz', 'qux'],\n      });\n      editor.setSelection(4, 8);\n\n      editor.exec('color', { selectedColor: '#f0f' });\n\n      const expected = oneLineTrim`\n        <table>\n          <thead>\n            <tr>\n              <th><p><span style=\"color: #f0f\">foo</span></p></th>\n              <th><p>bar</p></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td><p>baz</p></td>\n              <td><p>qux</p></td>\n            </tr>\n          </tbody>\n        </table>\n      `;\n\n      assertWwEditorHTML(expected);\n    });\n  });\n\n  describe('multi instances', () => {\n    let container2: HTMLElement, editor2: Editor;\n\n    beforeEach(() => {\n      container2 = document.createElement('div');\n      document.body.appendChild(container2);\n    });\n\n    afterEach(() => {\n      editor2.destroy();\n      document.body.removeChild(container2);\n    });\n\n    it('should focus to correct editor when using color syntax plugin', () => {\n      editor = new Editor({\n        el: container,\n        previewStyle: 'vertical',\n        height: '100px',\n        initialEditType: 'markdown',\n        plugins: [colorSyntaxPlugin],\n      });\n\n      editor2 = new Editor({\n        el: container2,\n        previewStyle: 'vertical',\n        height: '100px',\n        initialEditType: 'markdown',\n        plugins: [colorSyntaxPlugin],\n      });\n\n      editor2.exec('selectAll');\n      editor2.exec('color', { selectedColor: '#f0f' });\n\n      expect(container2).toContainElement(document.activeElement as HTMLElement);\n    });\n  });\n});\n"
  },
  {
    "path": "plugins/color-syntax/src/css/plugin.css",
    "content": "\n.toastui-editor-popup-color {\n  padding: 0;\n}\n\n.toastui-editor-popup-color .tui-colorpicker-container,\n.toastui-editor-popup-color .tui-colorpicker-palette-container {\n  width: 147px;\n}\n\n.toastui-editor-popup-color .tui-colorpicker-container ul {\n  width: 152px;\n  margin-bottom: 10px;\n}\n\n.toastui-editor-popup-color .tui-colorpicker-container li {\n  padding: 0 3px 3px 0;\n}\n\n.toastui-editor-popup-color .tui-colorpicker-container li .tui-colorpicker-palette-button {\n  border: solid 1px rgba(0, 0, 0, 0.1);\n  border-radius: 50%;\n  box-sizing: border-box;\n  width: 16px;\n  height: 16px;\n}\n\n.toastui-editor-popup-color .tui-popup-body {\n  padding: 10px;\n}\n\n.toastui-editor-popup-color .tui-colorpicker-container .tui-colorpicker-palette-toggle-slider {\n  display: none;\n}\n\n.toastui-editor-popup-color .tui-colorpicker-container .tui-colorpicker-svg-slider {\n  border-radius: 3px;\n  border: solid 1px rgba(0, 0, 0, 0.05);\n}\n\n\n.toastui-editor-popup-color .tui-colorpicker-palette-hex {\n  float: right;\n}\n\n.toastui-editor-popup-body input[type='text'].tui-colorpicker-palette-hex {\n  font-family: inherit;\n  font-size: 13px;\n  height: 24px;\n  width: 65px;\n  padding: 3px 25px 3px 10px;\n  border: 1px solid #e1e3e9;\n  border-radius: 2px;\n  float: left;\n}\n\n.toastui-editor-popup-color button {\n  height: 32px;\n  width: 40px;\n  color: #555;\n  background: #f7f9fc;\n  border: 1px solid #e1e3e9;\n  top: 68px;\n  position: absolute;\n  right: 15px;\n}\n\n.toastui-editor-popup-color button:hover {\n  border-color: #cbcfdb;\n}\n\n.toastui-editor-popup-color .tui-colorpicker-container div.tui-colorpicker-clearfix {\n  display: inline-block;\n  margin: 5px 0;\n  width: 102px;\n}\n\n.toastui-editor-popup-color .tui-colorpicker-container .tui-colorpicker-palette-preview {\n  margin-top: 8px;\n  margin-left: -22px;\n  width: 16px;\n  height: 16px;\n  border-radius: 50%;\n  border: solid 1px rgba(0, 0, 0, 0.1);\n  box-sizing: border-box;\n}\n\n.toastui-editor-popup-color .tui-colorpicker-slider-container .tui-colorpicker-slider-right {\n  width: 19px;\n}\n\n.toastui-editor-popup-color .tui-colorpicker-slider-container .tui-colorpicker-svg-huebar {\n  border: solid 1px rgba(0, 0, 0, 0.05);\n  border-radius: 3px;\n  overflow: auto;\n}\n\n.toastui-editor-popup-color .tui-colorpicker-slider-container .tui-colorpicker-huebar-handle {\n  display: none;\n}\n\n.toastui-editor-toolbar-icons.color {\n  background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIxMTYiIHZpZXdCb3g9IjAgMCAyNCAxMTYiPgogICAgPGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgICAgICA8Zz4KICAgICAgICAgICAgPGc+CiAgICAgICAgICAgICAgICA8Zz4KICAgICAgICAgICAgICAgICAgICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNjAwIC0xOTIpIHRyYW5zbGF0ZSg2MDAgMTkyKSI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0wIDBIMjRWMjRIMHoiLz4KICAgICAgICAgICAgICAgICAgICAgICAgPGc+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cGF0aCBmaWxsPSIjNTU1IiBkPSJNMiA4LjI1TDEwIDguMjUgMTAgOS43NSAyIDkuNzV6IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2IDQuNzUpIi8+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cGF0aCBzdHJva2U9IiM1NTUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIxLjUiIGQ9Ik0wIDE0LjVMNiAwIDEyIDE0LjUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDYgNC43NSkiLz4KICAgICAgICAgICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICAgICAgICAgICAgICA8cmVjdCB3aWR0aD0iNSIgaGVpZ2h0PSI1IiB4PSIxOCIgeT0iNCIgZmlsbD0iI0ZBMjgyOCIgcng9IjIuNSIvPgogICAgICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgICAgIDxnPgogICAgICAgICAgICAgICAgICAgIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKC02MDAgLTE5MikgdHJhbnNsYXRlKDYwMCAxOTIpIHRyYW5zbGF0ZSgwIDUyKSI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0wIDBIMjRWMjRIMHoiLz4KICAgICAgICAgICAgICAgICAgICAgICAgPGc+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cGF0aCBmaWxsPSIjRUVFIiBkPSJNMiA4LjI1TDEwIDguMjUgMTAgOS43NSAyIDkuNzV6IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2IDQuNzUpIi8+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cGF0aCBzdHJva2U9IiNFRUUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIxLjUiIGQ9Ik0wIDE0LjVMNiAwIDEyIDE0LjUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDYgNC43NSkiLz4KICAgICAgICAgICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICAgICAgICAgICAgICA8cmVjdCB3aWR0aD0iNSIgaGVpZ2h0PSI1IiB4PSIxOCIgeT0iNCIgZmlsbD0iI0ZGNDg0OCIgcng9IjIuNSIvPgogICAgICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgICAgIDxnPgogICAgICAgICAgICAgICAgICAgIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKC02MDAgLTE5MikgdHJhbnNsYXRlKDYwMCAxOTIpIHRyYW5zbGF0ZSgwIDI2KSI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0wIDBIMjRWMjRIMHoiLz4KICAgICAgICAgICAgICAgICAgICAgICAgPGc+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cGF0aCBmaWxsPSIjMDBBOUZGIiBkPSJNMiA4LjI1TDEwIDguMjUgMTAgOS43NSAyIDkuNzV6IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2IDQuNzUpIi8+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cGF0aCBzdHJva2U9IiMwMEE5RkYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIxLjUiIGQ9Ik0wIDE0LjVMNiAwIDEyIDE0LjUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDYgNC43NSkiLz4KICAgICAgICAgICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICAgICAgICAgICAgICA8cmVjdCB3aWR0aD0iNSIgaGVpZ2h0PSI1IiB4PSIxOCIgeT0iNCIgZmlsbD0iI0ZBMjgyOCIgcng9IjIuNSIvPgogICAgICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgICAgIDxnPgogICAgICAgICAgICAgICAgICAgIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKC02MDAgLTE5MikgdHJhbnNsYXRlKDYwMCAxOTIpIHRyYW5zbGF0ZSgwIDc4KSI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0wIDBIMjRWMjRIMHoiLz4KICAgICAgICAgICAgICAgICAgICAgICAgPGc+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cGF0aCBmaWxsPSIjNjdDQ0ZGIiBkPSJNMiA4LjI1TDEwIDguMjUgMTAgOS43NSAyIDkuNzV6IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2IDQuNzUpIi8+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cGF0aCBzdHJva2U9IiM2N0NDRkYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIxLjUiIGQ9Ik0wIDE0LjVMNiAwIDEyIDE0LjUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDYgNC43NSkiLz4KICAgICAgICAgICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICAgICAgICAgICAgICA8cmVjdCB3aWR0aD0iNSIgaGVpZ2h0PSI1IiB4PSIxOCIgeT0iNCIgZmlsbD0iI0ZGNDg0OCIgcng9IjIuNSIvPgogICAgICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgICAgIDxnIGZpbGw9IiNGRkYiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9Ii4yIj4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNNiAuNWMxLjUxOSAwIDIuODk0LjYxNiAzLjg5IDEuNjEuOTk0Ljk5NiAxLjYxIDIuMzcxIDEuNjEgMy44OSAwIDEuNTE5LS42MTYgMi44OTQtMS42MSAzLjg5LS45OTYuOTk0LTIuMzcxIDEuNjEtMy44OSAxLjYxLTEuNTE5IDAtMi44OTQtLjYxNi0zLjg5LTEuNjFDMS4xMTcgOC44OTMuNSA3LjUxOC41IDZjMC0xLjUxOS42MTYtMi44OTQgMS42MS0zLjg5QzMuMTA3IDEuMTE3IDQuNDgyLjUgNiAuNXpNNiAzYy0uODI4IDAtMS41NzguMzM2LTIuMTIxLjg3OUMzLjMzNiA0LjQyMiAzIDUuMTcyIDMgNmMwIC44MjguMzM2IDEuNTc4Ljg3OSAyLjEyMUM0LjQyMiA4LjY2NCA1LjE3MiA5IDYgOWMuODI4IDAgMS41NzgtLjMzNiAyLjEyMS0uODc5QzguNjY0IDcuNTc4IDkgNi44MjggOSA2YzAtLjgyOC0uMzM2LTEuNTc4LS44NzktMi4xMjFDNy41NzggMy4zMzYgNi44MjggMyA2IDN6IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNjAwIC0xOTIpIHRyYW5zbGF0ZSg2MDAgMTkyKSB0cmFuc2xhdGUoMCAxMDQpIi8+CiAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPgo=');\n  background-size: 23px 112px;\n  background-position: 3px 3px;\n}\n\n.toastui-editor-dark .toastui-editor-toolbar-icons.color {\n  background-position-y: -47px;\n}\n\n.toastui-editor-dark .toastui-editor-popup-body input[type='text'].tui-colorpicker-palette-hex {\n  border-color: #303238;\n}\n\n.toastui-editor-dark .toastui-editor-popup-color button {\n  color: #eee;\n  border-color: #303238;\n  background-color: #232428;\n}\n\n.toastui-editor-dark .toastui-editor-popup-color button:hover {\n  border-color: #494c56;\n}\n\n.toastui-editor-dark .toastui-editor-popup-color .tui-colorpicker-container li .tui-colorpicker-palette-button {\n  border-color: rgba(255, 255, 255, 0.1);\n}\n\n.toastui-editor-dark .toastui-editor-popup-color .tui-colorpicker-container .tui-colorpicker-svg-slider,\n.toastui-editor-dark .toastui-editor-popup-color .tui-colorpicker-slider-container .tui-colorpicker-svg-huebar {\n  border-color: rgba(255, 255, 255, 0.05);\n}\n"
  },
  {
    "path": "plugins/color-syntax/src/i18n/langs.ts",
    "content": "import type { I18n } from '@toast-ui/editor';\n\nexport function addLangs(i18n: I18n) {\n  i18n.setLanguage('ar', {\n    'Text color': 'لون النص',\n  });\n\n  i18n.setLanguage(['cs', 'cs-CZ'], {\n    'Text color': 'Barva textu',\n  });\n\n  i18n.setLanguage(['de', 'de-DE'], {\n    'Text color': 'Textfarbe',\n  });\n\n  i18n.setLanguage(['en', 'en-US'], {\n    'Text color': 'Text color',\n  });\n\n  i18n.setLanguage(['es', 'es-ES'], {\n    'Text color': 'Color del texto',\n  });\n\n  i18n.setLanguage(['fi', 'fi-FI'], {\n    'Text color': 'Tekstin väri',\n  });\n\n  i18n.setLanguage(['fr', 'fr-FR'], {\n    'Text color': 'Couleur du texte',\n  });\n\n  i18n.setLanguage(['gl', 'gl-ES'], {\n    'Text color': 'Cor do texto',\n  });\n\n  i18n.setLanguage(['hr', 'hr-HR'], {\n    'Text color': 'Boja teksta',\n  });\n\n  i18n.setLanguage(['it', 'it-IT'], {\n    'Text color': 'Colore del testo',\n  });\n\n  i18n.setLanguage(['ja', 'ja-JP'], {\n    'Text color': '文字色相',\n  });\n\n  i18n.setLanguage(['ko', 'ko-KR'], {\n    'Text color': '글자 색상',\n  });\n\n  i18n.setLanguage(['nb', 'nb-NO'], {\n    'Text color': 'Tekstfarge',\n  });\n\n  i18n.setLanguage(['nl', 'nl-NL'], {\n    'Text color': 'Tekstkleur',\n  });\n\n  i18n.setLanguage(['pl', 'pl-PL'], {\n    'Text color': 'Kolor tekstu',\n  });\n\n  i18n.setLanguage(['pt', 'pt-BR'], {\n    'Text color': 'Cor do texto',\n  });\n\n  i18n.setLanguage(['ru', 'ru-RU'], {\n    'Text color': 'Цвет текста',\n  });\n\n  i18n.setLanguage(['sv', 'sv-SE'], {\n    'Text color': 'Textfärg',\n  });\n\n  i18n.setLanguage(['tr', 'tr-TR'], {\n    'Text color': 'Metin rengi',\n  });\n\n  i18n.setLanguage(['uk', 'uk-UA'], {\n    'Text color': 'Колір тексту',\n  });\n\n  i18n.setLanguage('zh-CN', {\n    'Text color': '文字颜色',\n  });\n\n  i18n.setLanguage('zh-TW', {\n    'Text color': '文字顏色',\n  });\n}\n"
  },
  {
    "path": "plugins/color-syntax/src/index.ts",
    "content": "import ColorPicker from 'tui-color-picker';\nimport type { Context } from '@toast-ui/toastmark';\nimport type { PluginContext, PluginInfo, HTMLMdNode, I18n } from '@toast-ui/editor';\nimport type { Transaction, Selection, TextSelection } from 'prosemirror-state';\nimport { PluginOptions } from '@t/index';\nimport { addLangs } from './i18n/langs';\n\nimport './css/plugin.css';\nimport { findParentByClassName } from './utils/dom';\n\nconst PREFIX = 'toastui-editor-';\n\nfunction createApplyButton(text: string) {\n  const button = document.createElement('button');\n\n  button.setAttribute('type', 'button');\n  button.textContent = text;\n\n  return button;\n}\n\nfunction createToolbarItemOption(colorPickerContainer: HTMLDivElement, i18n: I18n) {\n  return {\n    name: 'color',\n    tooltip: i18n.get('Text color'),\n    className: `${PREFIX}toolbar-icons color`,\n    popup: {\n      className: `${PREFIX}popup-color`,\n      body: colorPickerContainer,\n      style: { width: 'auto' },\n    },\n  };\n}\n\nfunction createSelection(\n  tr: Transaction,\n  selection: Selection,\n  SelectionClass: typeof TextSelection,\n  openTag: string,\n  closeTag: string\n) {\n  const { mapping, doc } = tr;\n  const { from, to, empty } = selection;\n  const mappedFrom = mapping.map(from) + openTag.length;\n  const mappedTo = mapping.map(to) - closeTag.length;\n\n  return empty\n    ? SelectionClass.create(doc, mappedTo, mappedTo)\n    : SelectionClass.create(doc, mappedFrom, mappedTo);\n}\n\nfunction getCurrentEditorEl(colorPickerEl: HTMLElement, containerClassName: string) {\n  const editorDefaultEl = findParentByClassName(colorPickerEl, `${PREFIX}defaultUI`)!;\n\n  return editorDefaultEl.querySelector<HTMLElement>(`.${containerClassName} .ProseMirror`)!;\n}\n\ninterface ColorPickerOption {\n  container: HTMLDivElement;\n  preset?: Array<string>;\n  usageStatistics: boolean;\n}\n\nlet containerClassName: string;\nlet currentEditorEl: HTMLElement;\n\n// @TODO: add custom syntax for plugin\n/**\n * Color syntax plugin\n * @param {Object} context - plugin context for communicating with editor\n * @param {Object} options - options for plugin\n * @param {Array.<string>} [options.preset] - preset for color palette (ex: ['#181818', '#292929'])\n * @param {boolean} [options.useCustomSyntax=false] - whether use custom syntax or not\n */\nexport default function colorSyntaxPlugin(\n  context: PluginContext,\n  options: PluginOptions = {}\n): PluginInfo {\n  const { eventEmitter, i18n, usageStatistics = true, pmState } = context;\n  const { preset } = options;\n  const container = document.createElement('div');\n  const colorPickerOption: ColorPickerOption = { container, usageStatistics };\n\n  addLangs(i18n);\n\n  if (preset) {\n    colorPickerOption.preset = preset;\n  }\n\n  const colorPicker = ColorPicker.create(colorPickerOption);\n  const button = createApplyButton(i18n.get('OK'));\n\n  eventEmitter.listen('focus', (editType) => {\n    containerClassName = `${PREFIX}${editType === 'markdown' ? 'md' : 'ww'}-container`;\n  });\n\n  container.addEventListener('click', (ev) => {\n    if ((ev.target as HTMLElement).getAttribute('type') === 'button') {\n      const selectedColor = colorPicker.getColor();\n\n      currentEditorEl = getCurrentEditorEl(container, containerClassName);\n\n      eventEmitter.emit('command', 'color', { selectedColor });\n      eventEmitter.emit('closePopup');\n      // force the current editor to focus for preventing to lose focus\n      currentEditorEl.focus();\n    }\n  });\n\n  colorPicker.slider.toggle(true);\n  container.appendChild(button);\n\n  const toolbarItem = createToolbarItemOption(container, i18n);\n\n  return {\n    markdownCommands: {\n      color: ({ selectedColor }, { tr, selection, schema }, dispatch) => {\n        if (selectedColor) {\n          const slice = selection.content();\n          const textContent = slice.content.textBetween(0, slice.content.size, '\\n');\n          const openTag = `<span style=\"color: ${selectedColor}\">`;\n          const closeTag = `</span>`;\n          const colored = `${openTag}${textContent}${closeTag}`;\n\n          tr.replaceSelectionWith(schema.text(colored)).setSelection(\n            createSelection(tr, selection, pmState.TextSelection, openTag, closeTag)\n          );\n\n          dispatch!(tr);\n\n          return true;\n        }\n        return false;\n      },\n    },\n    wysiwygCommands: {\n      color: ({ selectedColor }, { tr, selection, schema }, dispatch) => {\n        if (selectedColor) {\n          const { from, to } = selection;\n          const attrs = { htmlAttrs: { style: `color: ${selectedColor}` } };\n          const mark = schema.marks.span.create(attrs);\n\n          tr.addMark(from, to, mark);\n          dispatch!(tr);\n\n          return true;\n        }\n        return false;\n      },\n    },\n    toolbarItems: [\n      {\n        groupIndex: 0,\n        itemIndex: 3,\n        item: toolbarItem,\n      },\n    ],\n    toHTMLRenderers: {\n      htmlInline: {\n        span(node: HTMLMdNode, { entering }: Context) {\n          return entering\n            ? { type: 'openTag', tagName: 'span', attributes: node.attrs! }\n            : { type: 'closeTag', tagName: 'span' };\n        },\n      },\n    },\n  };\n}\n"
  },
  {
    "path": "plugins/color-syntax/src/utils/dom.ts",
    "content": "function hasClass(element: HTMLElement, className: string) {\n  return element.classList.contains(className);\n}\n\nexport function findParentByClassName(el: HTMLElement, className: string) {\n  let currentEl: HTMLElement | null = el;\n\n  while (currentEl && !hasClass(currentEl, className)) {\n    currentEl = currentEl.parentElement;\n  }\n\n  return currentEl;\n}\n\nexport function removeProseMirrorHackNodes(html: string) {\n  const reProseMirrorImage = /<img class=\"ProseMirror-separator\" alt=\"\">/g;\n  const reProseMirrorTrailingBreak = / class=\"ProseMirror-trailingBreak\"/g;\n\n  let resultHTML = html;\n\n  resultHTML = resultHTML.replace(reProseMirrorImage, '');\n  resultHTML = resultHTML.replace(reProseMirrorTrailingBreak, '');\n\n  return resultHTML;\n}\n"
  },
  {
    "path": "plugins/color-syntax/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"src/**/*.ts\", \"src/**/*.js\", \"types/**/*\"],\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"],\n      \"@t/*\": [\"types/*\"]\n    },\n    \"typeRoots\": [\"./types\", \"node_modules/@types\", \"../../node_modules/@types\"],\n    \"lib\": [\"esnext\", \"dom\", \"dom.iterable\"]\n  }\n}"
  },
  {
    "path": "plugins/color-syntax/types/index.d.ts",
    "content": "import type { PluginContext, PluginInfo } from '@toast-ui/editor';\n\nexport interface PluginOptions {\n  preset?: string[];\n}\n\nexport default function colorPlugin(context: PluginContext, options: PluginOptions): PluginInfo;\n"
  },
  {
    "path": "plugins/color-syntax/types/prosemirror-model.d.ts",
    "content": "declare module 'prosemirror-model' {\n  export interface Fragment {\n    textBetween(from: number, to: number, blockSeparator?: string, leafText?: string): string;\n  }\n}\n"
  },
  {
    "path": "plugins/color-syntax/types/tui-color-picker.d.ts",
    "content": "interface ColorPickerOption {\n  container: HTMLElement;\n  preset?: string[];\n}\n\ndeclare module 'tui-color-picker' {\n  interface ColorPicker {\n    getColor(): string;\n    slider: {\n      toggle(type: boolean): void;\n    };\n  }\n\n  function create(options: ColorPickerOption): ColorPicker;\n}\n"
  },
  {
    "path": "plugins/color-syntax/webpack.config.js",
    "content": "/* eslint-disable @typescript-eslint/no-var-requires */\nconst path = require('path');\nconst webpack = require('webpack');\nconst { name, version, author, license } = require('./package.json');\n\nconst TerserPlugin = require('terser-webpack-plugin');\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin');\nconst CssMinimizerPlugin = require('css-minimizer-webpack-plugin');\nconst ESLintPlugin = require('eslint-webpack-plugin');\n\nconst filename = `toastui-${name.replace(/@toast-ui\\//, '')}`;\n\nfunction getOutputConfig(isProduction, isCDN, minify) {\n  const defaultConfig = {\n    library: {\n      name: ['toastui', 'Editor', 'plugin', 'uml'],\n      export: 'default',\n      type: 'umd',\n    },\n    environment: {\n      arrowFunction: false,\n      const: false,\n    },\n  };\n\n  if (!isProduction || isCDN) {\n    const config = {\n      ...defaultConfig,\n      library: {\n        name: ['toastui', 'Editor', 'plugin', 'colorSyntax'],\n        export: 'default',\n        type: 'umd',\n      },\n      path: path.resolve(__dirname, 'dist/cdn'),\n      filename: `${filename}${minify ? '.min' : ''}.js`,\n    };\n\n    if (!isProduction) {\n      config.publicPath = '/dist/cdn';\n    }\n\n    return config;\n  }\n\n  return {\n    ...defaultConfig,\n    path: path.resolve(__dirname, 'dist'),\n    filename: `${filename}.js`,\n  };\n}\n\nfunction getExternalsConfig() {\n  return [\n    {\n      'tui-color-picker': {\n        commonjs: 'tui-color-picker',\n        commonjs2: 'tui-color-picker',\n        amd: 'tui-color-picker',\n        root: ['tui', 'colorPicker'],\n      },\n    },\n  ];\n}\n\nfunction getOptimizationConfig(isProduction, minify) {\n  const minimizer = [];\n\n  if (isProduction && minify) {\n    minimizer.push(\n      new TerserPlugin({\n        parallel: true,\n        extractComments: false,\n      })\n    );\n    minimizer.push(new CssMinimizerPlugin());\n  }\n\n  return { minimizer };\n}\n\nmodule.exports = (env) => {\n  const isProduction = env.WEBPACK_BUILD;\n  const { minify = false, cdn = false } = env;\n  const config = {\n    mode: isProduction ? 'production' : 'development',\n    entry: './src/index.ts',\n    output: getOutputConfig(isProduction, cdn, minify),\n    externals: getExternalsConfig(),\n    module: {\n      rules: [\n        {\n          test: /\\.ts$|\\.js$/,\n          use: [\n            {\n              loader: 'ts-loader',\n              options: {\n                transpileOnly: true,\n              },\n            },\n          ],\n          exclude: /node_modules/,\n        },\n        {\n          test: /\\.css$/,\n          use: [MiniCssExtractPlugin.loader, 'css-loader'],\n        },\n      ],\n    },\n    resolve: {\n      extensions: ['.ts', '.js'],\n    },\n    plugins: [\n      new MiniCssExtractPlugin({\n        filename: () => `${filename}${minify ? '.min' : ''}.css`,\n      }),\n      new ESLintPlugin({\n        extensions: ['js', 'ts'],\n        exclude: ['node_modules', 'dist'],\n        failOnError: isProduction,\n      }),\n    ],\n    optimization: getOptimizationConfig(isProduction, minify),\n  };\n\n  if (isProduction) {\n    config.plugins.push(\n      new webpack.BannerPlugin(\n        [\n          'TOAST UI Editor : Color Syntax Plugin',\n          `@version ${version} | ${new Date().toDateString()}`,\n          `@author ${author}`,\n          `@license ${license}`,\n        ].join('\\n')\n      )\n    );\n  } else {\n    config.devServer = {\n      // https://github.com/webpack/webpack-dev-server/issues/2484\n      injectClient: false,\n      inline: true,\n      host: '0.0.0.0',\n      port: 8081,\n    };\n    config.devtool = 'inline-source-map';\n  }\n\n  return config;\n};\n"
  },
  {
    "path": "plugins/table-merged-cell/README.md",
    "content": "# TOAST UI Editor : Table Merged Cell Plugin\n\n> This is a plugin of [TOAST UI Editor](https://github.com/nhn/tui.editor/tree/master/apps/editor) to merge table columns.\n\n[![npm version](https://img.shields.io/npm/v/@toast-ui/editor-plugin-table-merged-cell.svg)](https://www.npmjs.com/package/@toast-ui/editor-plugin-table-merged-cell)\n\n![table-merged-cell](https://user-images.githubusercontent.com/37766175/121814008-c0232480-cca9-11eb-8611-7ccc0fe8707f.png)\n\n## 🚩 Table of Contents\n\n- [Bundle File Structure](#-bundle-file-structure)\n- [Usage npm](#-usage-npm)\n- [Usage CDN](#-usage-cdn)\n\n## 📁 Bundle File Structure\n\n### Files Distributed on npm\n\n```\n- node_modules/\n  - @toast-ui/\n    - editor-plugin-table-merged-cell/\n      - dist/\n        - toastui-editor-plugin-table-merged-cell.js\n        - toastui-editor-plugin-table-merged-cell.css\n```\n\n### Files Distributed on CDN\n\nThe bundle files include all dependencies of this plugin.\n\n```\n- uicdn.toast.com/\n  - editor-plugin-table-merged-cell/\n    - latest/\n      - toastui-editor-plugin-table-merged-cell.js\n      - toastui-editor-plugin-table-merged-cell.min.js\n      - toastui-editor-plugin-table-merged-cell.css\n      - toastui-editor-plugin-table-merged-cell.min.css\n```\n\n## 📦 Usage npm\n\nTo use the plugin, [`@toast-ui/editor`](https://github.com/nhn/tui.editor/tree/master/apps/editor) must be installed.\n\n> Ref. [Getting Started](https://github.com/nhn/tui.editor/blob/master/docs/en/getting-started.md)\n\n### Install\n\n```sh\n$ npm install @toast-ui/editor-plugin-table-merged-cell\n```\n\n### Import Plugin\n\n#### ES Modules\n\n```js\nimport '@toast-ui/editor-plugin-table-merged-cell/dist/toastui-editor-plugin-table-merged-cell.css';\n\nimport tableMergedCell from '@toast-ui/editor-plugin-table-merged-cell';\n```\n\n#### CommonJS\n\n```js\nrequire('@toast-ui/editor-plugin-table-merged-cell/dist/toastui-editor-plugin-table-merged-cell.css');\n\nconst tableMergedCell = require('@toast-ui/editor-plugin-table-merged-cell');\n```\n\n### Create Instance\n\n#### Basic\n\n```js\nimport '@toast-ui/editor-plugin-table-merged-cell/dist/toastui-editor-plugin-table-merged-cell.css';\n\nimport Editor from '@toast-ui/editor';\nimport tableMergedCell from '@toast-ui/editor-plugin-table-merged-cell';\n\nconst editor = new Editor({\n  // ...\n  plugins: [tableMergedCell]\n});\n```\n\n#### With Viewer\n\n```js\nimport '@toast-ui/editor-plugin-table-merged-cell/dist/toastui-editor-plugin-table-merged-cell.css';\n\nimport Viewer from '@toast-ui/editor/dist/toastui-editor-viewer';\nimport tableMergedCell from '@toast-ui/editor-plugin-table-merged-cell';\n\nconst viewer = new Viewer({\n  // ...\n  plugins: [tableMergedCell]\n});\n```\n\nor\n\n```js\nimport '@toast-ui/editor-plugin-table-merged-cell/dist/toastui-editor-plugin-table-merged-cell.css';\n\nimport Editor from '@toast-ui/editor';\nimport tableMergedCell from '@toast-ui/editor-plugin-table-merged-cell';\n\nconst viewer = Editor.factory({\n  // ...\n  plugins: [tableMergedCell],\n  viewer: true\n});\n```\n\n## 🗂 Usage CDN\n\nTo use the plugin, the CDN files(CSS, Script) of `@toast-ui/editor` must be included.\n\n### Include Files\n\n```html\n...\n<head>\n  ...\n  <link\n    rel=\"stylesheet\"\n    href=\"https://uicdn.toast.com/editor-plugin-table-merged-cell/latest/toastui-editor-plugin-table-merged-cell.min.css\"\n  />\n  ...\n</head>\n<body>\n  ...\n  <!-- Editor -->\n  <script src=\"https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js\"></script>\n  <!-- Editor's Plugin -->\n  <script src=\"https://uicdn.toast.com/editor-plugin-table-merged-cell/latest/toastui-editor-plugin-table-merged-cell.min.js\"></script>\n  ...\n</body>\n...\n```\n\n### Create Instance\n\n#### Basic\n\n```js\nconst { Editor } = toastui;\nconst { tableMergedCell } = Editor.plugin;\n\nconst editor = new Editor({\n  // ...\n  plugins: [tableMergedCell]\n});\n```\n\n#### With Viewer\n\n```js\nconst Viewer = toastui.Editor;\nconst { tableMergedCell } = Viewer.plugin;\n\nconst viewer = new Viewer({\n  // ...\n  plugins: [tableMergedCell]\n});\n```\n\nor\n\n```js\nconst { Editor } = toastui;\nconst { tableMergedCell } = Editor.plugin;\n\nconst viewer = Editor.factory({\n  // ...\n  plugins: [tableMergedCell],\n  viewer: true\n});\n```\n"
  },
  {
    "path": "plugins/table-merged-cell/demo/data.js",
    "content": "// merge cell example1\nconst content1 = [\n  '| @cols=2:mergedHead1 | @cols=3:mergedHead2 |',\n  '| --- | --- | --- | --- | --- |',\n  '| @cols=2:mergedCell1-1 | cell1-2 | @cols=2:@rows=5:mergedCell1-3 |',\n  '| @rows=2:mergedCell2-1 | @rows=2:mergedCell2-2 | cell2-3 | cell2-4 | cell2-5 | cell2-6 |',\n  '| cell3-1 |',\n  '| cell4-1 | cell4 | cell4-3 |',\n  '| cell5-1 | cell5-2 | cell5-3 | cell5-4 |',\n  '',\n].join('\\n');\n\n// merge cell example2\nconst content2 = [\n  '| @cols=2:merged | @cols=5:merged |',\n  '| --- | --- | --- | --- | --- | --- | --- |',\n  '| @cols=2:merged | table |  |  |  | table2 |',\n  '| @rows=2:merged | @rows=2:table | table2 |  |  |  | asdf |',\n  '| table |  |  |  | table2 |',\n  '| @cols=3:@rows=2:merged |  |  |  | table2 |',\n  '|  |  |  | table |',\n].join('\\n');\n\n// normal cell example\nconst content3 = [\n  '| a | b| c | d |',\n  '| --- | --- | --- | --- |',\n  '| table | table2 | table3 | table4 |',\n  '| table5 | table6 | table7 | table8 |',\n  '| table9 | table10 | table11 | table22 |',\n].join('\\n');\n"
  },
  {
    "path": "plugins/table-merged-cell/demo/editor.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>Editor</title>\n    <!-- Editor's Dependencies -->\n    <link\n      rel=\"stylesheet\"\n      href=\"https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.33.0/codemirror.css\"\n    />\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"http://localhost:8080/dist/cdn/toastui-editor.css\" />\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor-plugin-table-merged-cell.css\" />\n  </head>\n  <body>\n    <div class=\"code-html\">\n      <!-- Editor -->\n      <h2>Editor</h2>\n      <div id=\"editor\"></div>\n      <!-- Editor's Viewer -->\n      <h2>Viewer</h2>\n      <div id=\"viewer\"></div>\n    </div>\n    <!-- Editor -->\n    <script src=\"http://localhost:8080/dist/cdn/toastui-editor-all.js\"></script>\n    <!-- Plugin -->\n    <script src=\"./data.js\"></script>\n    <script src=\"../dist/cdn/toastui-editor-plugin-table-merged-cell.js\"></script>\n    <script class=\"code-js\">\n      const { Editor } = toastui;\n      const { tableMergedCell } = Editor.plugin;\n\n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialEditType: 'wysiwyg',\n        initialValue: content1,\n        plugins: [tableMergedCell]\n      });\n\n      const viewer = Editor.factory({\n        el: document.querySelector('#viewer'),\n        viewer: true,\n        height: '500px',\n        initialValue: content1,\n        plugins: [tableMergedCell]\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "plugins/table-merged-cell/demo/esm/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>Test to use plugin in node environment</title>\n  </head>\n  <body>\n    <div id=\"editor\"></div>\n    <!-- Editor -->\n    <script type=\"module\">\n      import { Editor } from 'http://localhost:8080/dist/index.js';\n      import mergedTableCellPlugin from '/dist/index.js';\n\n      const content = [\n        '| @cols=2:mergedHead1 | @cols=3:mergedHead2 |',\n        '| --- | --- | --- | --- | --- |',\n        '| @cols=2:mergedCell1-1 | cell1-2 | @cols=2:@rows=5:mergedCell1-3 |',\n        '| @rows=2:mergedCell2-1 | @rows=2:mergedCell2-2 | cell2-3 | cell2-4 | cell2-5 | cell2-6 |',\n        '| cell3-1 |',\n        '| cell4-1 | cell4 | cell4-3 |',\n        '| cell5-1 | cell5-2 | cell5-3 | cell5-4 |',\n        '',\n      ].join('\\n');\n      \n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialEditType: 'wysiwyg',\n        initialValue: content,\n        plugins: [mergedTableCellPlugin],\n      });\n\n      window.editor = editor;\n    </script>\n  </body>\n</html>\n\n"
  },
  {
    "path": "plugins/table-merged-cell/demo/viewer.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>Viewer</title>\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"http://localhost:8080/dist/cdn/toastui-editor-viewer.css\" />\n    <link rel=\"stylesheet\" href=\"../dist/cdn/toastui-editor-plugin-table-merged-cell.css\" />\n  </head>\n  <body>\n    <div class=\"code-html\">\n      <div id=\"viewer\"></div>\n    </div>\n    <!-- Editor's Viewer -->\n    <script src=\"http://localhost:8080/dist/cdn/toastui-editor-viewer.js\"></script>\n    <!-- Plugin -->\n    <script src=\"./data.js\"></script>\n    <script src=\"../dist/cdn/toastui-editor-plugin-table-merged-cell.js\"></script>\n    <script class=\"code-js\">\n      const Viewer = toastui.Editor;\n      const { tableMergedCell } = Viewer.plugin;\n\n      const instance = new Viewer({\n        el: document.querySelector('#viewer'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: content1,\n        plugins: [tableMergedCell]\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "plugins/table-merged-cell/jest.config.js",
    "content": "// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst base = require('../../jest.base.config');\n\nmodule.exports = {\n  ...base,\n  testEnvironment: 'jsdom',\n  moduleNameMapper: {\n    '^@/(.*)$': '<rootDir>/src/$1',\n  },\n};\n"
  },
  {
    "path": "plugins/table-merged-cell/package.json",
    "content": "{\n  \"name\": \"@toast-ui/editor-plugin-table-merged-cell\",\n  \"version\": \"3.1.0\",\n  \"description\": \"TOAST UI Editor : Table Merged Cell Plugin\",\n  \"keywords\": [\n    \"nhn\",\n    \"nhn cloud\",\n    \"toast\",\n    \"toastui\",\n    \"toast-ui\",\n    \"editor\",\n    \"plugin\",\n    \"table\",\n    \"merged-cell\"\n  ],\n  \"main\": \"dist/toastui-editor-plugin-table-merged-cell.js\",\n  \"files\": [\n    \"dist/*.js\",\n    \"dist/*.css\",\n    \"types/index.d.ts\"\n  ],\n  \"types\": \"types/index.d.ts\",\n  \"author\": \"NHN Cloud FE Development Lab <dl_javascript@nhn.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/nhn/tui.editor.git\",\n    \"directory\": \"plugins/table-merged-cell\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/nhn/tui.editor/issues\"\n  },\n  \"homepage\": \"https://ui.toast.com\",\n  \"browserslist\": \"last 2 versions, not ie <= 10\",\n  \"scripts\": {\n    \"lint\": \"eslint .\",\n    \"test:types\": \"tsc\",\n    \"test\": \"jest --watch\",\n    \"test:ci\": \"jest\",\n    \"serve\": \"snowpack dev\",\n    \"serve:ie\": \"webpack serve\",\n    \"build:cdn\": \"webpack build --env cdn & webpack build --env cdn minify\",\n    \"build\": \"webpack build && npm run build:cdn\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  }\n}\n"
  },
  {
    "path": "plugins/table-merged-cell/snowpack.config.js",
    "content": "// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst httpProxy = require('http-proxy');\nconst proxy = httpProxy.createServer({ target: 'http://localhost:8080' });\n\n/** @type {import(\"snowpack\").SnowpackUserConfig } */\nmodule.exports = {\n  mount: {\n    'demo/esm': '/',\n    src: '/dist',\n  },\n  devOptions: {\n    port: 8081,\n  },\n  routes: [\n    {\n      src: '/img/.*',\n      dest: (req, res) => {\n        proxy.web(req, res);\n      },\n    },\n  ],\n  alias: {\n    '@': './src',\n    '@t': './types',\n  },\n};\n"
  },
  {
    "path": "plugins/table-merged-cell/src/__test__/integration/convertor.spec.ts",
    "content": "import { oneLineTrim } from 'common-tags';\nimport Editor from '@toast-ui/editor';\nimport mergedTableCellPlugin from '@/index';\n\ndescribe('convertor with merged table plugin', () => {\n  let container: HTMLElement, editor: Editor;\n\n  function assertMdEditorText(markdownText: string) {\n    expect(editor.getMarkdown()).toBe(markdownText);\n  }\n\n  function assertWYSIWYGHTML(html: string) {\n    const wwEditorEl = editor.getEditorElements().wwEditor;\n\n    expect(wwEditorEl.innerHTML).toContain(html);\n  }\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n\n    editor = new Editor({\n      el: container,\n      previewStyle: 'vertical',\n      plugins: [mergedTableCellPlugin],\n    });\n  });\n\n  afterEach(() => {\n    editor.destroy();\n    document.body.removeChild(container);\n  });\n\n  it('should convert to wysiwyg properly', () => {\n    const content = [\n      '| @cols=2:mergedHead1 | @cols=3:mergedHead2 |',\n      '| --- | --- | --- | --- | --- |',\n      '| @cols=2:mergedCell1-1 | cell1-2 | @cols=2:@rows=5:mergedCell1-3 |',\n      '| @rows=2:mergedCell2-1 | @rows=2:mergedCell2-2 | cell2-3 |',\n      '| cell3-1 |',\n      '| cell4-1 | cell4 | cell4-3 |',\n      '| cell5-1 | cell5-2 | cell5-3 |',\n      '',\n    ].join('\\n');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"2\"><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"5\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td rowspan=\"2\"><p>mergedCell2-1</p></td>\n            <td rowspan=\"2\"><p>mergedCell2-2</p></td>\n            <td><p>cell2-3</p></td></tr><tr><td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    editor.setMarkdown(content);\n    editor.changeMode('wysiwyg');\n\n    assertWYSIWYGHTML(expected);\n  });\n\n  it('should convert to markdown properly', () => {\n    const content = [\n      '| @cols=2:mergedHead1 | @cols=3:mergedHead2 |',\n      '| ----------- | ----------- | ----------- | ----------- | ----------- |',\n      '| @cols=2:mergedCell1-1 | cell1-2 | @cols=2:@rows=5:mergedCell1-3 |',\n      '| @rows=2:mergedCell2-1 | @rows=2:mergedCell2-2 | cell2-3 |',\n      '| cell3-1 |',\n      '| cell4-1 | cell4 | cell4-3 |',\n      '| cell5-1 | cell5-2 | cell5-3 |',\n      '',\n    ].join('\\n');\n\n    editor.setMarkdown(content);\n    editor.changeMode('wysiwyg');\n    editor.changeMode('markdown');\n\n    assertMdEditorText(content);\n  });\n});\n"
  },
  {
    "path": "plugins/table-merged-cell/src/__test__/integration/markdown/mergedTablePreview.spec.ts",
    "content": "import { source } from 'common-tags';\nimport Editor from '@toast-ui/editor';\nimport mergedTableCellPlugin from '@/index';\n\ndescribe('markdown merged table plugin', () => {\n  let container: HTMLElement, editor: Editor;\n\n  function removeDataAttr(html: string) {\n    return html\n      .replace(/\\sdata-nodeid=\"\\d{1,}\"/g, '')\n      .replace(/\\sclass=\"toastui-editor-md-preview-highlight\"/g, '')\n      .trim();\n  }\n\n  function assertMdPreviewHTML(html: string) {\n    const mdPreviewContentEl = editor.getEditorElements().mdPreview.firstChild as HTMLElement;\n\n    expect(removeDataAttr(mdPreviewContentEl.innerHTML)).toContain(html);\n  }\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n\n    editor = new Editor({\n      el: container,\n      previewStyle: 'vertical',\n      plugins: [mergedTableCellPlugin],\n    });\n  });\n\n  afterEach(() => {\n    editor.destroy();\n    document.body.removeChild(container);\n  });\n\n  it('should render basic table properly', () => {\n    const content = source`\n      | head1 | head2 |\n      | --- | --- |\n      | cell1 | cell2 |\n    `;\n    const result = source`\n      <table>\n      <thead>\n      <tr>\n      <th>head1</th>\n      <th>head2</th>\n      </tr>\n      </thead>\n      <tbody>\n      <tr>\n      <td>cell1</td>\n      <td>cell2</td>\n      </tr>\n      </tbody>\n      </table>\n    `;\n\n    editor.setMarkdown(content);\n\n    assertMdPreviewHTML(result);\n  });\n\n  it('should render merged cell with colspan header properly', () => {\n    const content = source`\n      | @cols=2:mergedHead1 | @cols=2:mergedHead2 |\n      | --- | --- | --- | --- |\n      | cell1 | cell2 | cell3 | cell4 |\n    `;\n    const result = source`\n      <table>\n      <thead>\n      <tr>\n      <th colspan=\"2\">mergedHead1</th>\n      <th colspan=\"2\">mergedHead2</th>\n      </tr>\n      </thead>\n      <tbody>\n      <tr>\n      <td>cell1</td>\n      <td>cell2</td>\n      <td>cell3</td>\n      <td>cell4</td>\n      </tr>\n      </tbody>\n      </table>\n    `;\n\n    editor.setMarkdown(content);\n\n    assertMdPreviewHTML(result);\n  });\n\n  it('should render merged cell with colspan header, body properly', () => {\n    const content = source`\n      | @cols=2:mergedHead1 | @cols=2:mergedHead2 |\n      | --- | --- | --- | --- |\n      | @cols=2:mergedCell1 | cell2 | cell3 |\n    `;\n    const result = source`\n      <table>\n      <thead>\n      <tr>\n      <th colspan=\"2\">mergedHead1</th>\n      <th colspan=\"2\">mergedHead2</th>\n      </tr>\n      </thead>\n      <tbody>\n      <tr>\n      <td colspan=\"2\">mergedCell1</td>\n      <td>cell2</td>\n      <td>cell3</td>\n      </tr>\n      </tbody>\n      </table>\n    `;\n\n    editor.setMarkdown(content);\n\n    assertMdPreviewHTML(result);\n  });\n\n  it('should render merged cell with rowspan body properly', () => {\n    const content = source`\n      | head1 | head2 |\n      | --- | --- |\n      | cell1-1 | @rows=2:cell1-2  |\n      | cell2-1 | cell2-2  |\n    `;\n    const result = source`\n      <table>\n      <thead>\n      <tr>\n      <th>head1</th>\n      <th>head2</th>\n      </tr>\n      </thead>\n      <tbody>\n      <tr>\n      <td>cell1-1</td>\n      <td rowspan=\"2\">cell1-2</td>\n      </tr>\n      <tr>\n      <td>cell2-1</td>\n      </tr>\n      </tbody>\n      </table>\n    `;\n\n    editor.setMarkdown(content);\n\n    assertMdPreviewHTML(result);\n  });\n\n  describe('should render merged cell with rowspan, colspan properly', () => {\n    const examples = [\n      {\n        no: 1,\n        content: source`\n        | @cols=2:mergedHead1 | @cols=2:mergedHead2 |\n        | --- | --- | --- | --- |\n        | @cols=2:mergedCell1-1 | cell1-2 | cell1-3 |\n        | @rows=3:mergedCell2-1 | cell2-2 | cell2-3 | cell2-4 |\n        | cell3-1 | cell3-2 | cell3-3 | cell3-4 |\n        | cell4-1 | cell4-2 | cell4-3 | cell4-4 |\n        | cell5-1 | cell5-2 | cell5-3 | cell5-4 |\n      `,\n        result: source`\n        <table>\n        <thead>\n        <tr>\n        <th colspan=\"2\">mergedHead1</th>\n        <th colspan=\"2\">mergedHead2</th>\n        </tr>\n        </thead>\n        <tbody>\n        <tr>\n        <td colspan=\"2\">mergedCell1-1</td>\n        <td>cell1-2</td>\n        <td>cell1-3</td>\n        </tr>\n        <tr>\n        <td rowspan=\"3\">mergedCell2-1</td>\n        <td>cell2-2</td>\n        <td>cell2-3</td>\n        <td>cell2-4</td>\n        </tr>\n        <tr>\n        <td>cell3-1</td>\n        <td>cell3-2</td>\n        <td>cell3-3</td>\n        </tr>\n        <tr>\n        <td>cell4-1</td>\n        <td>cell4-2</td>\n        <td>cell4-3</td>\n        </tr>\n        <tr>\n        <td>cell5-1</td>\n        <td>cell5-2</td>\n        <td>cell5-3</td>\n        <td>cell5-4</td>\n        </tr>\n        </tbody>\n        </table>\n      `,\n      },\n      {\n        no: 2,\n        content: source`\n        | @cols=2:mergedHead1 | @cols=2:mergedHead2 |\n        | --- | --- | --- | --- |\n        | @cols=2:mergedCell1-1 | cell1-2 | cell1-3 |\n        | @rows=2:mergedCell2-1 | cell2-2 | cell2-3 | cell2-4 |\n        | cell3-1 | cell3-2 | cell3-3 | cell3-4 |\n        | @cols=3:@rows=2:cell4-1 | cell4-2 | cell4-3 | cell4-4 |\n        | cell5-1 | cell5-2 | cell5-3 | cell5-4 |\n      `,\n        result: source`\n        <table>\n        <thead>\n        <tr>\n        <th colspan=\"2\">mergedHead1</th>\n        <th colspan=\"2\">mergedHead2</th>\n        </tr>\n        </thead>\n        <tbody>\n        <tr>\n        <td colspan=\"2\">mergedCell1-1</td>\n        <td>cell1-2</td>\n        <td>cell1-3</td>\n        </tr>\n        <tr>\n        <td rowspan=\"2\">mergedCell2-1</td>\n        <td>cell2-2</td>\n        <td>cell2-3</td>\n        <td>cell2-4</td>\n        </tr>\n        <tr>\n        <td>cell3-1</td>\n        <td>cell3-2</td>\n        <td>cell3-3</td>\n        </tr>\n        <tr>\n        <td rowspan=\"2\" colspan=\"3\">cell4-1</td>\n        <td>cell4-2</td>\n        </tr>\n        <tr>\n        <td>cell5-1</td>\n        </tr>\n        </tbody>\n        </table>\n      `,\n      },\n      {\n        no: 3,\n        content: source`\n        | @cols=2:mergedHead1 | @cols=2:mergedHead2 |\n        | --- | --- | --- | --- |\n        | @cols=2:mergedCell1-1 | cell1-2 | cell1-3 |\n        | @rows=2:mergedCell2-1 | cell2-2 | cell2-3 | cell2-4 |\n        | cell3-1 | cell3-2 | cell3-3 | cell3-4 |\n        | @cols=3:@rows=2:cell4-1 | cell4-2 | cell4-3 | cell4-4 |\n        | @rows=2:cell5-1 | cell5-2 | cell5-3 | cell5-4 |\n        | cell6-1 | cell6-2 | cell6-3 | cell6-4 |\n      `,\n        result: source`\n        <table>\n        <thead>\n        <tr>\n        <th colspan=\"2\">mergedHead1</th>\n        <th colspan=\"2\">mergedHead2</th>\n        </tr>\n        </thead>\n        <tbody>\n        <tr>\n        <td colspan=\"2\">mergedCell1-1</td>\n        <td>cell1-2</td>\n        <td>cell1-3</td>\n        </tr>\n        <tr>\n        <td rowspan=\"2\">mergedCell2-1</td>\n        <td>cell2-2</td>\n        <td>cell2-3</td>\n        <td>cell2-4</td>\n        </tr>\n        <tr>\n        <td>cell3-1</td>\n        <td>cell3-2</td>\n        <td>cell3-3</td>\n        </tr>\n        <tr>\n        <td rowspan=\"2\" colspan=\"3\">cell4-1</td>\n        <td>cell4-2</td>\n        </tr>\n        <tr>\n        <td rowspan=\"2\">cell5-1</td>\n        </tr>\n        <tr>\n        <td>cell6-1</td>\n        <td>cell6-2</td>\n        <td>cell6-3</td>\n        </tr>\n        </tbody>\n        </table>\n      `,\n      },\n      {\n        no: 4,\n        content: source`\n        | @cols=2:mergedHead1 | @cols=5:mergedHead2 |\n        | --- | --- | --- | --- | --- | --- | --- |\n        | @cols=2:mergedCell1-1 | cell1-2 | cell1-3 | cell1-4 | cell1-5 | cell1-6 |\n        | @rows=2:mergedCell2-1 | @rows=2:mergedCell2-2 | cell2-3 | cell2-4 | cell2-5 | cell2-6 |\n        | cell3-1 | cell3-2 | cell3-3 | cell3-4 | cell3-5 | cell3-6 |\n        | @cols=3:@rows=2:mergedCell4-1 | cell4-2 | cell4-3 | cell4-4 |\n        | @rows=2:mergedCell5-1 | cell5-2 | cell5-3 | cell5-4 | cell5-5 |\n        | cell6-1 | cell6-2 | cell6-3 | cell6-4 | cell6-5 |\n      `,\n        result: source`\n        <table>\n        <thead>\n        <tr>\n        <th colspan=\"2\">mergedHead1</th>\n        <th colspan=\"5\">mergedHead2</th>\n        </tr>\n        </thead>\n        <tbody>\n        <tr>\n        <td colspan=\"2\">mergedCell1-1</td>\n        <td>cell1-2</td>\n        <td>cell1-3</td>\n        <td>cell1-4</td>\n        <td>cell1-5</td>\n        <td>cell1-6</td>\n        </tr>\n        <tr>\n        <td rowspan=\"2\">mergedCell2-1</td>\n        <td rowspan=\"2\">mergedCell2-2</td>\n        <td>cell2-3</td>\n        <td>cell2-4</td>\n        <td>cell2-5</td>\n        <td>cell2-6</td>\n        <td></td>\n        </tr>\n        <tr>\n        <td>cell3-1</td>\n        <td>cell3-2</td>\n        <td>cell3-3</td>\n        <td>cell3-4</td>\n        <td>cell3-5</td>\n        </tr>\n        <tr>\n        <td rowspan=\"2\" colspan=\"3\">mergedCell4-1</td>\n        <td>cell4-2</td>\n        <td>cell4-3</td>\n        <td>cell4-4</td>\n        <td></td>\n        </tr>\n        <tr>\n        <td rowspan=\"2\">mergedCell5-1</td>\n        <td>cell5-2</td>\n        <td>cell5-3</td>\n        <td>cell5-4</td>\n        </tr>\n        <tr>\n        <td>cell6-1</td>\n        <td>cell6-2</td>\n        <td>cell6-3</td>\n        <td>cell6-4</td>\n        <td>cell6-5</td>\n        <td></td>\n        </tr>\n        </tbody>\n        </table>\n      `,\n      },\n      {\n        no: 5,\n        content: source`\n        | @cols=2:mergedHead1 | @cols=5:mergedHead2 |\n        | --- | --- | --- | --- | --- | --- | --- |\n        | @cols=2:mergedCell1-1 | cell1-2 | @cols=2:@rows=5:mergedCell1-3 | cell1-4 | cell1-5 | cell1-6 |\n        | @rows=2:mergedCell2-1 | @rows=2:mergedCell2-2 | cell2-3 | cell2-4 | cell2-5 | cell2-6 |\n        | cell3-1 | cell3-2 |\n        | @cols=3:@rows=2:mergedCell4-1 | cell4-2 |\n        | cell5-1 | cell5-2 |\n      `,\n        result: source`\n        <table>\n        <thead>\n        <tr>\n        <th colspan=\"2\">mergedHead1</th>\n        <th colspan=\"5\">mergedHead2</th>\n        </tr>\n        </thead>\n        <tbody>\n        <tr>\n        <td colspan=\"2\">mergedCell1-1</td>\n        <td>cell1-2</td>\n        <td rowspan=\"5\" colspan=\"2\">mergedCell1-3</td>\n        <td>cell1-4</td>\n        <td>cell1-5</td>\n        </tr>\n        <tr>\n        <td rowspan=\"2\">mergedCell2-1</td>\n        <td rowspan=\"2\">mergedCell2-2</td>\n        <td>cell2-3</td>\n        <td>cell2-4</td>\n        <td>cell2-5</td>\n        </tr>\n        <tr>\n        <td>cell3-1</td>\n        <td>cell3-2</td>\n        <td></td>\n        </tr>\n        <tr>\n        <td rowspan=\"2\" colspan=\"3\">mergedCell4-1</td>\n        <td>cell4-2</td>\n        <td></td>\n        </tr>\n        <tr>\n        <td>cell5-1</td>\n        <td>cell5-2</td>\n        </tr>\n        </tbody>\n        </table>\n      `,\n      },\n      {\n        no: 6,\n        content: source`\n        | @cols=2:mergedHead1 | @cols=3:mergedHead2 |\n        | --- | --- | --- | --- | --- |\n        | @cols=2:mergedCell1-1 | cell1-2 | @cols=2:@rows=5:mergedCell1-3 |\n        | @rows=2:mergedCell2-1 | @rows=2:mergedCell2-2 | cell2-3 | cell2-4 | cell2-5 | cell2-6 |\n        | cell3-1 |\n        | cell4-1 | cell4-2 |\n        | cell5-1 | cell5-2 | cell5-3 | cell5-4 |\n      `,\n        result: source`\n        <table>\n        <thead>\n        <tr>\n        <th colspan=\"2\">mergedHead1</th>\n        <th colspan=\"3\">mergedHead2</th>\n        </tr>\n        </thead>\n        <tbody>\n        <tr>\n        <td colspan=\"2\">mergedCell1-1</td>\n        <td>cell1-2</td>\n        <td rowspan=\"5\" colspan=\"2\">mergedCell1-3</td>\n        </tr>\n        <tr>\n        <td rowspan=\"2\">mergedCell2-1</td>\n        <td rowspan=\"2\">mergedCell2-2</td>\n        <td>cell2-3</td>\n        </tr>\n        <tr>\n        <td>cell3-1</td>\n        </tr>\n        <tr>\n        <td>cell4-1</td>\n        <td>cell4-2</td>\n        <td></td>\n        </tr>\n        <tr>\n        <td>cell5-1</td>\n        <td>cell5-2</td>\n        <td>cell5-3</td>\n        </tr>\n        </tbody>\n        </table>\n      `,\n      },\n      {\n        no: 7,\n        content: source`\n        | @cols=2:mergedHead1 | @cols=3:mergedHead2 |\n        | --- | --- | --- | --- | --- |\n        | @cols=2:mergedCell1-1 | @rows=3:mergedCell1-2 | @cols=2:@rows=5:mergedCell1-3 |\n        | @rows=2:mergedCell2-1 | @rows=2:mergedCell2-2 |\n        |\n        | cell4-1 | cell4-2 |  |\n        | cell5-1 | cell5-2 | cell5-3 |\n      `,\n        result: source`\n        <table>\n        <thead>\n        <tr>\n        <th colspan=\"2\">mergedHead1</th>\n        <th colspan=\"3\">mergedHead2</th>\n        </tr>\n        </thead>\n        <tbody>\n        <tr>\n        <td colspan=\"2\">mergedCell1-1</td>\n        <td rowspan=\"3\">mergedCell1-2</td>\n        <td rowspan=\"5\" colspan=\"2\">mergedCell1-3</td>\n        </tr>\n        <tr>\n        <td rowspan=\"2\">mergedCell2-1</td>\n        <td rowspan=\"2\">mergedCell2-2</td>\n        </tr>\n        <tr></tr>\n        <tr>\n        <td>cell4-1</td>\n        <td>cell4-2</td>\n        <td></td>\n        </tr>\n        <tr>\n        <td>cell5-1</td>\n        <td>cell5-2</td>\n        <td>cell5-3</td>\n        </tr>\n        </tbody>\n        </table>\n      `,\n      },\n      {\n        no: 8,\n        content: source`\n        | @cols=2:foo\"bar\" | @cols=2:<span style=\"color: red;\">baz</span> |\n        | --- | --- | --- | --- |\n        | @cols=2:foo\"bar\" | cell1-2 | cell1-3 |\n        | @rows=2:<span style=\"color: red;\">baz</span> | cell2-2 | cell2-3 | cell2-4 |\n        | cell3-1 | cell3-2 | cell3-3 | cell3-4 |\n        | @cols=3:@rows=2:foo\"bar\"<span style=\"color: red;\">baz</span> | cell4-2 | cell4-3 | cell4-4 |\n        | cell5-1 | cell5-2 | cell5-3 | cell5-4 |\n      `,\n        result: source`\n        <table>\n        <thead>\n        <tr>\n        <th colspan=\"2\">foo\"bar\"</th>\n        <th colspan=\"2\"><span style=\"color: red;\">baz</span></th>\n        </tr>\n        </thead>\n        <tbody>\n        <tr>\n        <td colspan=\"2\">foo\"bar\"</td>\n        <td>cell1-2</td>\n        <td>cell1-3</td>\n        </tr>\n        <tr>\n        <td rowspan=\"2\"><span style=\"color: red;\">baz</span></td>\n        <td>cell2-2</td>\n        <td>cell2-3</td>\n        <td>cell2-4</td>\n        </tr>\n        <tr>\n        <td>cell3-1</td>\n        <td>cell3-2</td>\n        <td>cell3-3</td>\n        </tr>\n        <tr>\n        <td rowspan=\"2\" colspan=\"3\">foo\"bar\"<span style=\"color: red;\">baz</span></td>\n        <td>cell4-2</td>\n        </tr>\n        <tr>\n        <td>cell5-1</td>\n        </tr>\n        </tbody>\n        </table>\n      `,\n      },\n    ];\n\n    examples.forEach(({ no, content, result }) => {\n      it(` - example${no}`, () => {\n        editor.setMarkdown(content);\n\n        assertMdPreviewHTML(result);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "plugins/table-merged-cell/src/__test__/integration/wysiwyg/addColumn.spec.ts",
    "content": "import { oneLineTrim } from 'common-tags';\nimport Editor from '@toast-ui/editor';\nimport { assertWYSIWYGHTML, createEditor } from './helper/utils';\n\nlet container: HTMLElement, editor: Editor;\n\nbeforeEach(() => {\n  const editorInfo = createEditor();\n\n  container = editorInfo.container;\n  editor = editorInfo.editor;\n});\n\nafterEach(() => {\n  editor.destroy();\n  document.body.removeChild(container);\n});\n\ndescribe('addColumnToLeft command', () => {\n  it('should add column to left and extend col-spanning cell', () => {\n    editor.setSelection(102, 102); // select [2, 1] cell(mergedCell2-2 text)\n    editor.exec('addColumnToLeft');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"3\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"3\"><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"5\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td rowspan=\"2\"><p>mergedCell2-1</p></td>\n            <td><p><br></p></td>\n            <td rowspan=\"2\"><p>mergedCell2-2</p></td>\n            <td><p>cell2-3</p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n            <td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p><br></p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p><br></p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n\n  it('should add column to left', () => {\n    editor.setSelection(85, 85); // select [2, 0] cell(mergedCell2-1 text)\n    editor.exec('addColumnToLeft');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th><p><br></p></th>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td><p><br></p></td>\n            <td colspan=\"2\"><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"5\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n            <td rowspan=\"2\"><p>mergedCell2-1</p></td>\n            <td rowspan=\"2\"><p>mergedCell2-2</p></td>\n            <td><p>cell2-3</p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n            <td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n\n  it('should add column to left as many as the col-spanning count', () => {\n    editor.setSelection(38, 38); // select [1, 0] cell(mergedCell1-1 text)\n    editor.exec('addColumnToLeft');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th><p><br></p></th>\n            <th><p><br></p></th>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n            <td colspan=\"2\"><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"5\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n            <td rowspan=\"2\"><p>mergedCell2-1</p></td>\n            <td rowspan=\"2\"><p>mergedCell2-2</p></td>\n            <td><p>cell2-3</p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n            <td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n});\n\ndescribe('addColumnToRight command', () => {\n  it('should add column to right and extend col-spanning cell', () => {\n    editor.setSelection(85, 85); // select [2, 0] cell(mergedCell2-1 text)\n    editor.exec('addColumnToRight');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"3\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"3\"><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"5\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td rowspan=\"2\"><p>mergedCell2-1</p></td>\n            <td><p><br></p></td>\n            <td rowspan=\"2\"><p>mergedCell2-2</p></td>\n            <td><p>cell2-3</p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n            <td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p><br></p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p><br></p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n\n  it('should add column to right', () => {\n    editor.setSelection(102, 102); // select [2, 1] cell(mergedCell2-2 text)\n    editor.exec('addColumnToRight');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th><p><br></p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n        <tr>\n            <td colspan=\"2\"><p>mergedCell1-1</p></td>\n            <td><p><br></p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"5\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td rowspan=\"2\"><p>mergedCell2-1</p></td>\n            <td rowspan=\"2\"><p>mergedCell2-2</p></td>\n            <td><p><br></p></td>\n            <td><p>cell2-3</p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n            <td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p><br></p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p><br></p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n\n  it('should add column to right as many as the col-spanning count', () => {\n    editor.setSelection(38, 38); // select [1, 0] cell(mergedCell1-1 text)\n    editor.exec('addColumnToRight');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th><p><br></p></th>\n            <th><p><br></p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"2\"><p>mergedCell1-1</p></td>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"5\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td rowspan=\"2\"><p>mergedCell2-1</p></td>\n            <td rowspan=\"2\"><p>mergedCell2-2</p></td>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n            <td><p>cell2-3</p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n            <td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n});\n"
  },
  {
    "path": "plugins/table-merged-cell/src/__test__/integration/wysiwyg/addRow.spec.ts",
    "content": "import { oneLineTrim } from 'common-tags';\nimport Editor from '@toast-ui/editor';\nimport { assertWYSIWYGHTML, createEditor } from './helper/utils';\n\nlet container: HTMLElement, editor: Editor;\n\nbeforeEach(() => {\n  const editorInfo = createEditor();\n\n  container = editorInfo.container;\n  editor = editorInfo.editor;\n});\n\nafterEach(() => {\n  editor.destroy();\n  document.body.removeChild(container);\n});\n\ndescribe('addRowToUp command', () => {\n  it('should add row to up and not extend row-spanning cell', () => {\n    editor.setSelection(119, 119); // select [2, 2] cell(cell2-3 text)\n    editor.exec('addRowToUp');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"2\"><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"6\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n          </tr>\n          <tr>\n            <td rowspan=\"2\"><p>mergedCell2-1</p></td>\n            <td rowspan=\"2\"><p>mergedCell2-2</p></td>\n            <td><p>cell2-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n\n  it('should add row to up and extend row-spanning cell', () => {\n    editor.setSelection(132, 132); // select [3, 2] cell(cell3-1 text)\n    editor.exec('addRowToUp');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"2\"><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"6\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td rowspan=\"3\"><p>mergedCell2-1</p></td>\n            <td rowspan=\"3\"><p>mergedCell2-2</p></td>\n            <td><p>cell2-3</p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n          </tr>\n          <tr>\n            <td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n\n  it('should add row to up as many as the row-spanning count', () => {\n    editor.setSelection(100, 100); // select [2, 1] cell(mergedCell2-2 text)\n    editor.exec('addRowToUp');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"2\"><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"7\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n          </tr>\n          <tr>\n            <td rowspan=\"2\"><p>mergedCell2-1</p></td>\n            <td rowspan=\"2\"><p>mergedCell2-2</p></td>\n            <td><p>cell2-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n});\n\ndescribe('addRowToDown command', () => {\n  it('should add row to down and extend row-spanning cell', () => {\n    editor.setSelection(132, 132); // select [3, 2] cell(cell3-1 text)\n    editor.exec('addRowToDown');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"2\"><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"6\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td rowspan=\"2\"><p>mergedCell2-1</p></td>\n            <td rowspan=\"2\"><p>mergedCell2-2</p></td>\n            <td><p>cell2-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n\n  it('should add row to down and not extend row-spanning cell', () => {\n    editor.setSelection(119, 119); // select [2, 2] cell(cell2-3 text)\n    editor.exec('addRowToDown');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"2\"><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"6\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td rowspan=\"3\"><p>mergedCell2-1</p></td>\n            <td rowspan=\"3\"><p>mergedCell2-2</p></td>\n            <td><p>cell2-3</p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n          </tr>\n          <tr>\n            <td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n\n  it('should add row to down as many as the row-spanning count', () => {\n    editor.setSelection(100, 100); // select [2, 1] cell(mergedCell2-2 text)\n    editor.exec('addRowToDown');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"2\"><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"7\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td rowspan=\"2\"><p>mergedCell2-1</p></td>\n            <td rowspan=\"2\"><p>mergedCell2-2</p></td>\n            <td><p>cell2-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n});\n"
  },
  {
    "path": "plugins/table-merged-cell/src/__test__/integration/wysiwyg/helper/cellSelection.ts",
    "content": "import { Node, ResolvedPos, Slice, Fragment } from 'prosemirror-model';\nimport { Selection, SelectionRange, TextSelection } from 'prosemirror-state';\nimport { Mappable } from 'prosemirror-transform';\nimport { SelectionInfo } from '@t/index';\nimport { TableOffsetMap } from './tableOffsetMap';\n\nfunction getSelectionRanges(\n  doc: Node,\n  map: TableOffsetMap,\n  { startRowIdx, startColIdx, endRowIdx, endColIdx }: SelectionInfo\n) {\n  const ranges = [];\n\n  for (let rowIdx = startRowIdx; rowIdx <= endRowIdx; rowIdx += 1) {\n    for (let colIdx = startColIdx; colIdx <= endColIdx; colIdx += 1) {\n      const { offset, nodeSize } = map.getCellInfo(rowIdx, colIdx);\n\n      ranges.push(new SelectionRange(doc.resolve(offset + 1), doc.resolve(offset + nodeSize - 1)));\n    }\n  }\n  return ranges;\n}\n\nfunction createTableFragment(tableHead: Node, tableBody: Node) {\n  const fragment: Node[] = [];\n\n  if (tableHead.childCount) {\n    fragment.push(tableHead);\n  }\n  if (tableBody.childCount) {\n    fragment.push(tableBody);\n  }\n  return Fragment.from(fragment);\n}\n\nexport default class CellSelection extends Selection {\n  private offsetMap: TableOffsetMap;\n\n  startCell: ResolvedPos;\n\n  endCell: ResolvedPos;\n\n  isCellSelection: boolean;\n\n  constructor(startCellPos: ResolvedPos, endCellPos = startCellPos) {\n    const doc = startCellPos.node(0);\n\n    const map = TableOffsetMap.create(startCellPos)!;\n    const selectionInfo = map.getRectOffsets(startCellPos, endCellPos);\n    const ranges = getSelectionRanges(doc, map, selectionInfo);\n\n    super(ranges[0].$from, ranges[0].$to, ranges);\n\n    this.startCell = startCellPos;\n    this.endCell = endCellPos;\n    this.offsetMap = map;\n    this.isCellSelection = true;\n\n    // This property is the api of the 'Selection' in prosemirror,\n    // and is used to disable the text selection.\n    this.visible = false;\n  }\n\n  map(doc: Node, mapping: Mappable) {\n    const startPos = this.startCell.pos;\n    const endPos = this.endCell.pos;\n    const startCell = doc.resolve(mapping.map(startPos));\n    const endCell = doc.resolve(mapping.map(endPos));\n    const map = TableOffsetMap.create(startCell)!;\n\n    // text selection when rows or columns are deleted\n    if (\n      this.offsetMap.totalColumnCount > map.totalColumnCount ||\n      this.offsetMap.totalRowCount > map.totalRowCount\n    ) {\n      const depthMap = { tableBody: 1, tableRow: 2, tableCell: 3, paragraph: 4 };\n      const depthFromTable = depthMap[endCell.parent.type.name as keyof typeof depthMap];\n      const tableEndPos = endCell.end(endCell.depth - depthFromTable);\n      // subtract 4(</td></tr></tbody></table> tag length)\n      const from = Math.min(tableEndPos - 4, endCell.pos);\n\n      return TextSelection.create(doc, from);\n    }\n    return new CellSelection(startCell, endCell);\n  }\n\n  eq(cell: CellSelection) {\n    return (\n      cell instanceof CellSelection &&\n      cell.startCell.pos === this.startCell.pos &&\n      cell.endCell.pos === this.endCell.pos\n    );\n  }\n\n  content() {\n    const table = this.startCell.node(-2);\n    const tableOffset = this.startCell.start(-2);\n    const row = table.child(1).firstChild!;\n    const tableHead = table.child(0).type.create()!;\n    const tableBody = table.child(1).type.create()!;\n\n    const map = TableOffsetMap.create(this.startCell)!;\n    const selectionInfo = map.getRectOffsets(this.startCell, this.endCell);\n    const { startRowIdx, startColIdx, endRowIdx, endColIdx } = selectionInfo;\n\n    let isTableHeadCell = false;\n\n    for (let rowIdx = startRowIdx; rowIdx <= endRowIdx; rowIdx += 1) {\n      const cells = [];\n\n      for (let colIdx = startColIdx; colIdx <= endColIdx; colIdx += 1) {\n        const { offset } = map.getCellInfo(rowIdx, colIdx);\n        const cell = table.nodeAt(offset - tableOffset);\n\n        if (cell) {\n          isTableHeadCell = cell.type.name === 'tableHeadCell';\n          // mark the extended cell for pasting\n          if (map.extendedRowspan(rowIdx, colIdx) || map.extendedColspan(rowIdx, colIdx)) {\n            cells.push(cell.type.create({ extended: true }));\n          } else {\n            cells.push(cell.copy(cell.content));\n          }\n        }\n      }\n\n      const copiedRow = row.copy(Fragment.from(cells));\n      const targetNode = isTableHeadCell ? tableHead : tableBody;\n\n      // @ts-ignore\n      targetNode.content = targetNode.content.append(Fragment.from(copiedRow));\n    }\n    return new Slice(createTableFragment(tableHead, tableBody), 1, 1);\n  }\n\n  toJSON() {\n    return JSON.stringify(this);\n  }\n}\n"
  },
  {
    "path": "plugins/table-merged-cell/src/__test__/integration/wysiwyg/helper/tableOffsetMap.ts",
    "content": "import type { Node, ResolvedPos } from 'prosemirror-model';\nimport { findNodeBy } from '@/wysiwyg/util';\n\nexport interface CellInfo {\n  offset: number;\n  nodeSize: number;\n  extended?: boolean;\n}\nexport interface SelectionInfo {\n  startRowIdx: number;\n  startColIdx: number;\n  endRowIdx: number;\n  endColIdx: number;\n}\n\ninterface SpanMap {\n  [key: number]: { count: number; startSpanIdx: number };\n}\n\nexport interface RowInfo {\n  [key: number]: CellInfo;\n  length: number;\n  rowspanMap: SpanMap;\n  colspanMap: SpanMap;\n}\n\nfunction getSortedNumPair(valueA: number, valueB: number) {\n  return valueA > valueB ? [valueB, valueA] : [valueA, valueB];\n}\n\nexport class TableOffsetMap {\n  private table: Node;\n\n  private tableRows: Node[];\n\n  private tableStartPos: number;\n\n  private rowInfo: RowInfo[];\n\n  constructor(table: Node, tableRows: Node[], tableStartPos: number, rowInfo: RowInfo[]) {\n    this.table = table;\n    this.tableRows = tableRows;\n    this.tableStartPos = tableStartPos;\n    this.rowInfo = rowInfo;\n  }\n\n  static create(cellPos: ResolvedPos): TableOffsetMap | null {\n    const table = findNodeBy(cellPos, ({ type }: Node) => type.name === 'table');\n\n    if (table) {\n      const { node, depth } = table;\n\n      const rows: Node[] = [];\n      const tablePos = cellPos.start(depth);\n\n      const thead = node.child(0);\n      const tbody = node.child(1);\n\n      const theadCellInfo = createOffsetMap(thead, tablePos);\n      const tbodyCellInfo = createOffsetMap(tbody, tablePos + thead.nodeSize);\n\n      thead.forEach((row) => rows.push(row));\n      tbody.forEach((row) => rows.push(row));\n\n      const map = new TableOffsetMap(node, rows, tablePos, theadCellInfo.concat(tbodyCellInfo));\n\n      return map;\n    }\n\n    return null;\n  }\n\n  get totalRowCount() {\n    return this.rowInfo.length;\n  }\n\n  get totalColumnCount() {\n    return this.rowInfo[0].length;\n  }\n\n  get tableStartOffset() {\n    return this.tableStartPos;\n  }\n\n  get tableEndOffset() {\n    return this.tableStartPos + this.table.nodeSize - 1;\n  }\n\n  getCellInfo(rowIdx: number, colIdx: number) {\n    return this.rowInfo[rowIdx][colIdx];\n  }\n\n  posAt(rowIdx: number, colIdx: number): number {\n    for (let i = 0, rowStart = this.tableStartPos; ; i += 1) {\n      const rowEnd = rowStart + this.tableRows[i].nodeSize;\n\n      if (i === rowIdx) {\n        let index = colIdx;\n\n        // Skip the cells from previous row(via rowspan)\n        while (index < this.totalColumnCount && this.rowInfo[i][index].offset < rowStart) {\n          index += 1;\n        }\n        return index === this.totalColumnCount ? rowEnd : this.rowInfo[i][index].offset;\n      }\n      rowStart = rowEnd;\n    }\n  }\n\n  getNodeAndPos(rowIdx: number, colIdx: number) {\n    const cellInfo = this.rowInfo[rowIdx][colIdx];\n\n    return { node: this.table.nodeAt(cellInfo.offset - 1)!, pos: cellInfo.offset };\n  }\n\n  extendedRowspan(rowIdx: number, colIdx: number) {\n    const rowspanInfo = this.rowInfo[rowIdx].rowspanMap[colIdx];\n\n    return !!rowspanInfo && rowspanInfo.startSpanIdx !== rowIdx;\n  }\n\n  extendedColspan(rowIdx: number, colIdx: number) {\n    const colspanInfo = this.rowInfo[rowIdx].colspanMap[colIdx];\n\n    return !!colspanInfo && colspanInfo.startSpanIdx !== colIdx;\n  }\n\n  getRowspanCount(rowIdx: number, colIdx: number) {\n    const rowspanInfo = this.rowInfo[rowIdx].rowspanMap[colIdx];\n\n    return rowspanInfo ? rowspanInfo.count : 0;\n  }\n\n  getColspanCount(rowIdx: number, colIdx: number) {\n    const colspanInfo = this.rowInfo[rowIdx].colspanMap[colIdx];\n\n    return colspanInfo ? colspanInfo.count : 0;\n  }\n\n  decreaseColspanCount(rowIdx: number, colIdx: number) {\n    const colspanInfo = this.rowInfo[rowIdx].colspanMap[colIdx];\n    const startColspanInfo = this.rowInfo[rowIdx].colspanMap[colspanInfo.startSpanIdx];\n\n    startColspanInfo.count -= 1;\n\n    return startColspanInfo.count;\n  }\n\n  decreaseRowspanCount(rowIdx: number, colIdx: number) {\n    const rowspanInfo = this.rowInfo[rowIdx].rowspanMap[colIdx];\n    const startRowspanInfo = this.rowInfo[rowspanInfo.startSpanIdx].rowspanMap[colIdx];\n\n    startRowspanInfo.count -= 1;\n\n    return startRowspanInfo.count;\n  }\n\n  getColspanStartInfo(rowIdx: number, colIdx: number) {\n    const { colspanMap } = this.rowInfo[rowIdx];\n    const colspanInfo = colspanMap[colIdx];\n\n    if (colspanInfo) {\n      const { startSpanIdx } = colspanInfo;\n      const cellInfo = this.rowInfo[rowIdx][startSpanIdx];\n\n      return {\n        node: this.table.nodeAt(cellInfo.offset - 1)!,\n        pos: cellInfo.offset,\n        startSpanIdx,\n        count: colspanMap[startSpanIdx].count,\n      };\n    }\n    return null;\n  }\n\n  getRowspanStartInfo(rowIdx: number, colIdx: number) {\n    const { rowspanMap } = this.rowInfo[rowIdx];\n    const rowspanInfo = rowspanMap[colIdx];\n\n    if (rowspanInfo) {\n      const { startSpanIdx } = rowspanInfo;\n      const cellInfo = this.rowInfo[startSpanIdx][colIdx];\n\n      return {\n        node: this.table.nodeAt(cellInfo.offset - 1)!,\n        pos: cellInfo.offset,\n        startSpanIdx,\n        count: this.rowInfo[startSpanIdx].rowspanMap[colIdx].count,\n      };\n    }\n    return null;\n  }\n\n  getSpannedOffsets(selectionInfo: SelectionInfo): SelectionInfo {\n    let { startRowIdx, startColIdx, endRowIdx, endColIdx } = selectionInfo;\n\n    for (let rowIdx = endRowIdx; rowIdx >= startRowIdx; rowIdx -= 1) {\n      if (this.rowInfo[rowIdx]) {\n        const { rowspanMap, colspanMap } = this.rowInfo[rowIdx];\n\n        for (let colIdx = endColIdx; colIdx >= startColIdx; colIdx -= 1) {\n          const rowspanInfo = rowspanMap[colIdx];\n          const colspanInfo = colspanMap[colIdx];\n\n          if (rowspanInfo) {\n            startRowIdx = Math.min(startRowIdx, rowspanInfo.startSpanIdx);\n          }\n          if (colspanInfo) {\n            startColIdx = Math.min(startColIdx, colspanInfo.startSpanIdx);\n          }\n        }\n      }\n    }\n\n    for (let rowIdx = startRowIdx; rowIdx <= endRowIdx; rowIdx += 1) {\n      if (this.rowInfo[rowIdx]) {\n        const { rowspanMap, colspanMap } = this.rowInfo[rowIdx];\n\n        for (let colIdx = startColIdx; colIdx <= endColIdx; colIdx += 1) {\n          const rowspanInfo = rowspanMap[colIdx];\n          const colspanInfo = colspanMap[colIdx];\n\n          if (rowspanInfo) {\n            endRowIdx = Math.max(endRowIdx, rowIdx + rowspanInfo.count - 1);\n          }\n          if (colspanInfo) {\n            endColIdx = Math.max(endColIdx, colIdx + colspanInfo.count - 1);\n          }\n        }\n      }\n    }\n\n    return { startRowIdx, startColIdx, endRowIdx, endColIdx };\n  }\n\n  getCellStartOffset(rowIdx: number, colIdx: number) {\n    const { offset } = this.rowInfo[rowIdx][colIdx];\n\n    return this.extendedRowspan(rowIdx, colIdx) ? this.posAt(rowIdx, colIdx) : offset;\n  }\n\n  getCellEndOffset(rowIdx: number, colIdx: number) {\n    const { offset, nodeSize } = this.rowInfo[rowIdx][colIdx];\n\n    return this.extendedRowspan(rowIdx, colIdx) ? this.posAt(rowIdx, colIdx) : offset + nodeSize;\n  }\n\n  getCellIndex(cellPos: ResolvedPos): [rowIdx: number, colIdx: number] {\n    for (let rowIdx = 0; rowIdx < this.totalRowCount; rowIdx += 1) {\n      const rowInfo = this.rowInfo[rowIdx];\n\n      for (let colIdx = 0; colIdx < this.totalColumnCount; colIdx += 1) {\n        if (rowInfo[colIdx].offset + 1 > cellPos.pos) {\n          return [rowIdx, colIdx];\n        }\n      }\n    }\n    return [0, 0];\n  }\n\n  getRectOffsets(startCellPos: ResolvedPos, endCellPos = startCellPos) {\n    if (startCellPos.pos > endCellPos.pos) {\n      [startCellPos, endCellPos] = [endCellPos, startCellPos];\n    }\n    let [startRowIdx, startColIdx] = this.getCellIndex(startCellPos);\n    let [endRowIdx, endColIdx] = this.getCellIndex(endCellPos);\n\n    [startRowIdx, endRowIdx] = getSortedNumPair(startRowIdx, endRowIdx);\n    [startColIdx, endColIdx] = getSortedNumPair(startColIdx, endColIdx);\n\n    return this.getSpannedOffsets({ startRowIdx, startColIdx, endRowIdx, endColIdx });\n  }\n}\n\nfunction extendPrevRowspan(prevRowInfo: RowInfo, rowInfo: RowInfo) {\n  const { rowspanMap, colspanMap } = rowInfo;\n  const { rowspanMap: prevRowspanMap, colspanMap: prevColspanMap } = prevRowInfo;\n\n  Object.keys(prevRowspanMap).forEach((key) => {\n    const colIdx = Number(key);\n    const prevRowspanInfo = prevRowspanMap[colIdx];\n\n    if (prevRowspanInfo?.count > 1) {\n      const prevColspanInfo = prevColspanMap[colIdx];\n      const { count, startSpanIdx } = prevRowspanInfo;\n\n      rowspanMap[colIdx] = { count: count - 1, startSpanIdx };\n      colspanMap[colIdx] = prevColspanInfo;\n\n      rowInfo[colIdx] = { ...prevRowInfo[colIdx], extended: true };\n      rowInfo.length += 1;\n    }\n  });\n}\n\nfunction extendPrevColspan(\n  rowspan: number,\n  colspan: number,\n  rowIdx: number,\n  colIdx: number,\n  rowInfo: RowInfo\n) {\n  const { rowspanMap, colspanMap } = rowInfo;\n\n  for (let i = 1; i < colspan; i += 1) {\n    colspanMap[colIdx + i] = { count: colspan - i, startSpanIdx: colIdx };\n\n    if (rowspan > 1) {\n      rowspanMap[colIdx + i] = { count: rowspan, startSpanIdx: rowIdx };\n    }\n\n    rowInfo[colIdx + i] = { ...rowInfo[colIdx] };\n    rowInfo.length += 1;\n  }\n}\n\nfunction createOffsetMap(headOrBody: Node, startOffset: number, startFromBody = false) {\n  const cellInfoMatrix: RowInfo[] = [];\n  const beInBody = headOrBody.type.name === 'tableBody';\n\n  headOrBody.forEach((row: Node, rowOffset: number, rowIdx: number) => {\n    // get row index based on table(not table head or table body)\n    const rowIdxInWholeTable = beInBody && !startFromBody ? rowIdx + 1 : rowIdx;\n    const prevRowInfo = cellInfoMatrix[rowIdx - 1];\n    const rowInfo: RowInfo = { rowspanMap: {}, colspanMap: {}, length: 0 };\n\n    if (prevRowInfo) {\n      extendPrevRowspan(prevRowInfo, rowInfo);\n    }\n\n    row.forEach(({ nodeSize, attrs }: Node, cellOffset: number) => {\n      const colspan: number = attrs.colspan ?? 1;\n      const rowspan: number = attrs.rowspan ?? 1;\n      let colIdx = 0;\n\n      while (rowInfo[colIdx]) {\n        colIdx += 1;\n      }\n\n      rowInfo[colIdx] = {\n        // 2 is the sum of the front and back positions of the tag\n        offset: startOffset + rowOffset + cellOffset + 2,\n        nodeSize,\n      };\n\n      rowInfo.length += 1;\n\n      if (rowspan > 1) {\n        rowInfo.rowspanMap[colIdx] = { count: rowspan, startSpanIdx: rowIdxInWholeTable };\n      }\n\n      if (colspan > 1) {\n        rowInfo.colspanMap[colIdx] = { count: colspan, startSpanIdx: colIdx };\n        extendPrevColspan(rowspan, colspan, rowIdxInWholeTable, colIdx, rowInfo);\n      }\n    });\n    cellInfoMatrix.push(rowInfo);\n  });\n\n  return cellInfoMatrix;\n}\n"
  },
  {
    "path": "plugins/table-merged-cell/src/__test__/integration/wysiwyg/helper/utils.ts",
    "content": "import Editor from '@toast-ui/editor';\nimport mergedTableCellPlugin from '@/index';\n\nexport function assertWYSIWYGHTML(editor: Editor, html: string) {\n  const wwEditorEl = editor.getEditorElements().wwEditor;\n  const wwEditorHTML = removeProseMirrorHackNodes(wwEditorEl.outerHTML);\n\n  expect(wwEditorHTML).toContain(html);\n}\n\nexport function createEditor() {\n  const content = [\n    '| @cols=2:mergedHead1 | @cols=3:mergedHead2 |',\n    '| --- | --- | --- | --- | --- |',\n    '| @cols=2:mergedCell1-1 | cell1-2 | @cols=2:@rows=5:mergedCell1-3 |',\n    '| @rows=2:mergedCell2-1 | @rows=2:mergedCell2-2 | cell2-3 |',\n    '| cell3-1 |',\n    '| cell4-1 | cell4 | cell4-3 |',\n    '| cell5-1 | cell5-2 | cell5-3 |',\n    '',\n  ].join('\\n');\n\n  const container = document.createElement('div');\n\n  document.body.appendChild(container);\n\n  const editor = new Editor({\n    el: container,\n    initialEditType: 'wysiwyg',\n    initialValue: content,\n    previewStyle: 'vertical',\n    plugins: [mergedTableCellPlugin],\n  });\n\n  return { container, editor };\n}\n\nexport function removeProseMirrorHackNodes(html: string) {\n  const reProseMirrorImage = /<img class=\"ProseMirror-separator\" alt=\"\">/g;\n  const reProseMirrorTrailingBreak = / class=\"ProseMirror-trailingBreak\"/g;\n\n  let resultHTML = html;\n\n  resultHTML = resultHTML.replace(reProseMirrorImage, '');\n  resultHTML = resultHTML.replace(reProseMirrorTrailingBreak, '');\n\n  return resultHTML;\n}\n"
  },
  {
    "path": "plugins/table-merged-cell/src/__test__/integration/wysiwyg/mergeCells.spec.ts",
    "content": "import { oneLineTrim } from 'common-tags';\nimport Editor from '@toast-ui/editor';\nimport { assertWYSIWYGHTML, createEditor } from './helper/utils';\nimport type { EditorView } from 'prosemirror-view';\nimport CellSelection from './helper/cellSelection';\n\nlet container: HTMLElement, editor: Editor;\n\nbeforeEach(() => {\n  const editorInfo = createEditor();\n\n  container = editorInfo.container;\n  editor = editorInfo.editor;\n});\n\nafterEach(() => {\n  editor.destroy();\n  document.body.removeChild(container);\n});\n\nfunction setCellSelection(startPos: number, endPos: number) {\n  // @ts-ignore\n  const wwView: EditorView = editor.wwEditor.view;\n  const { tr } = wwView.state;\n\n  wwView.dispatch!(\n    tr.setSelection(new CellSelection(tr.doc.resolve(startPos), tr.doc.resolve(endPos)))\n  );\n}\n\ndescribe('mergeCells command', () => {\n  it('should merge cells included spanning cell', () => {\n    setCellSelection(37, 131); // select [1, 0] cell(mergedCell1-1 text) to [3, 2](cell3-1 text)\n    editor.exec('mergeCells');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"3\" rowspan=\"3\">\n              <p>mergedCell1-1</p>\n              <p>cell1-2</p>\n              <p>mergedCell2-1</p>\n              <p>mergedCell2-2</p>\n              <p>cell2-3</p>\n              <p>cell3-1</p>\n            </td>\n            <td colspan=\"2\" rowspan=\"5\">\n              <p>mergedCell1-3</p>\n            </td>\n          </tr>\n          <tr></tr>\n          <tr></tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n\n  it('should merge cells(normal cells)', () => {\n    setCellSelection(54, 131); // select [1, 2] cell(cell1-2 text) to [3, 2](cell3-1 text)\n    editor.exec('mergeCells');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"2\"><p>mergedCell1-1</p></td>\n            <td colspan=\"1\" rowspan=\"3\">\n              <p>cell1-2</p>\n              <p>cell2-3</p>\n              <p>cell3-1</p>\n            </td>\n            <td colspan=\"2\" rowspan=\"5\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td rowspan=\"2\"><p>mergedCell2-1</p></td>\n            <td rowspan=\"2\"><p>mergedCell2-2</p></td>\n          </tr>\n          <tr></tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n\n  it('should not merge cells in case of selecting all cells', () => {\n    setCellSelection(37, 65); // select all body cells\n    editor.exec('mergeCells');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"2\"><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"5\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td rowspan=\"2\"><p>mergedCell2-1</p></td>\n            <td rowspan=\"2\"><p>mergedCell2-2</p></td>\n            <td><p>cell2-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n\n  it('should not merge cells in case of selecting head cells', () => {\n    setCellSelection(1, 37); // select [0, 0](mergedHead1 text)  cellto [1, 0](mergedCell1-1 text)\n    editor.exec('mergeCells');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"2\"><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"5\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td rowspan=\"2\"><p>mergedCell2-1</p></td>\n            <td rowspan=\"2\"><p>mergedCell2-2</p></td>\n            <td><p>cell2-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n});\n"
  },
  {
    "path": "plugins/table-merged-cell/src/__test__/integration/wysiwyg/removeColumn.spec.ts",
    "content": "import { oneLineTrim } from 'common-tags';\nimport Editor from '@toast-ui/editor';\nimport { assertWYSIWYGHTML, createEditor } from './helper/utils';\n\nlet container: HTMLElement, editor: Editor;\n\nbeforeEach(() => {\n  const editorInfo = createEditor();\n\n  container = editorInfo.container;\n  editor = editorInfo.editor;\n});\n\nafterEach(() => {\n  editor.destroy();\n  document.body.removeChild(container);\n});\n\ndescribe('removeColumn command', () => {\n  it('should remove column included col-spanning cell(normal single cell)', () => {\n    editor.setSelection(85, 85); // select [2, 0] cell(mergedCell2-1 text)\n    editor.exec('removeColumn');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"5\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td rowspan=\"2\"><p>mergedCell2-2</p></td>\n            <td><p>cell2-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n\n  it('should remove column(normal single cell)', () => {\n    editor.setSelection(102, 102); // select [2, 1] cell(mergedCell2-2 text)\n    editor.exec('removeColumn');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n        <tr>\n            <td><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"5\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td rowspan=\"2\"><p>mergedCell2-1</p></td>\n            <td><p>cell2-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n\n  it('should remove column(selected col-spanning cell)', () => {\n    editor.setSelection(38, 38); // select [1, 0] cell(mergedCell1-1 text)\n    editor.exec('removeColumn');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"5\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell2-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n});\n"
  },
  {
    "path": "plugins/table-merged-cell/src/__test__/integration/wysiwyg/removeRow.spec.ts",
    "content": "import { oneLineTrim } from 'common-tags';\nimport Editor from '@toast-ui/editor';\nimport { assertWYSIWYGHTML, createEditor } from './helper/utils';\n\nlet container: HTMLElement, editor: Editor;\n\nbeforeEach(() => {\n  const editorInfo = createEditor();\n\n  container = editorInfo.container;\n  editor = editorInfo.editor;\n});\n\nafterEach(() => {\n  editor.destroy();\n  document.body.removeChild(container);\n});\n\ndescribe('removeRow command', () => {\n  it('should remove row included row-spanning cell(normal single cell)', () => {\n    editor.setSelection(119, 119); // select [2, 2] cell(cell2-3 text)\n    editor.exec('removeRow');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"2\"><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"4\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td><p>mergedCell2-1</p></td>\n            <td><p>mergedCell2-2</p></td>\n            <td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n\n  it('should remove row(normal single cell)', () => {\n    editor.setSelection(132, 132); // select [3, 2] cell(cell3-1 text)\n    editor.exec('removeRow');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"2\"><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"4\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td><p>mergedCell2-1</p></td>\n            <td><p>mergedCell2-2</p></td>\n            <td><p>cell2-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n\n  it('should remove row(selected row-spanning cell)', () => {\n    editor.setSelection(100, 100); // select [2, 1] cell(mergedCell2-2 text)\n    editor.exec('removeRow');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"2\"><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"3\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n});\n"
  },
  {
    "path": "plugins/table-merged-cell/src/__test__/integration/wysiwyg/splitCells.spec.ts",
    "content": "import { oneLineTrim } from 'common-tags';\nimport Editor from '@toast-ui/editor';\nimport { assertWYSIWYGHTML, createEditor } from './helper/utils';\nimport type { EditorView } from 'prosemirror-view';\nimport CellSelection from './helper/cellSelection';\n\nlet container: HTMLElement, editor: Editor;\n\nbeforeEach(() => {\n  const editorInfo = createEditor();\n\n  container = editorInfo.container;\n  editor = editorInfo.editor;\n});\n\nafterEach(() => {\n  editor.destroy();\n  document.body.removeChild(container);\n});\n\nfunction setCellSelection(startPos: number, endPos: number) {\n  // @ts-ignore\n  const wwView: EditorView = editor.wwEditor.view;\n  const { tr } = wwView.state;\n\n  wwView.dispatch!(\n    tr.setSelection(new CellSelection(tr.doc.resolve(startPos), tr.doc.resolve(endPos)))\n  );\n}\n\ndescribe('splitCells command', () => {\n  it('should split cells included spanning cell', () => {\n    setCellSelection(37, 131); // select [1, 0] cell(mergedCell1-1 text) to [3, 2](cell3-1 text)\n    editor.exec('splitCells');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td><p>mergedCell1-1</p></td>\n            <td><p><br></p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"5\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td><p>mergedCell2-1</p></td>\n            <td><p>mergedCell2-2</p></td>\n            <td><p>cell2-3</p></td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n            <td><p>cell3-1</p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n\n  it('should split cell(single spanning cell)', () => {\n    editor.setSelection(66, 66); // select [1, 3] cell(mergedCell1-3 text)\n    editor.exec('splitCells');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"2\"><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td><p>mergedCell1-3</p></td>\n            <td><p><br></p></td>\n          </tr>\n          <tr>\n            <td rowspan=\"2\"><p>mergedCell2-1</p></td>\n            <td rowspan=\"2\"><p>mergedCell2-2</p></td>\n            <td><p>cell2-3</p></td>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n          </tr>\n          <tr>\n            <td><p>cell3-1</p></td>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n            <td><p><br></p></td>\n            <td><p><br></p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n\n  it('should split cells in case that all cells are spanning on the row', () => {\n    setCellSelection(118, 131); // select [2, 2] cell(cell2-3 text) to [3, 2](cell3-1 text)\n    editor.exec('mergeCells');\n\n    editor.exec('splitCells');\n\n    const expected = oneLineTrim`\n      <table>\n        <thead>\n          <tr>\n            <th colspan=\"2\"><p>mergedHead1</p></th>\n            <th colspan=\"3\"><p>mergedHead2</p></th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"2\"><p>mergedCell1-1</p></td>\n            <td><p>cell1-2</p></td>\n            <td colspan=\"2\" rowspan=\"5\"><p>mergedCell1-3</p></td>\n          </tr>\n          <tr>\n            <td rowspan=\"2\"><p>mergedCell2-1</p></td>\n            <td rowspan=\"2\"><p>mergedCell2-2</p></td>\n            <td>\n              <p>cell2-3</p>\n              <p>cell3-1</p>\n            </td>\n          </tr>\n          <tr>\n            <td><p><br></p></td>\n          </tr>\n          <tr>\n            <td><p>cell4-1</p></td>\n            <td><p>cell4</p></td>\n            <td><p>cell4-3</p></td>\n          </tr>\n          <tr>\n            <td><p>cell5-1</p></td>\n            <td><p>cell5-2</p></td>\n            <td><p>cell5-3</p></td>\n          </tr>\n        </tbody>\n      </table>\n    `;\n\n    assertWYSIWYGHTML(editor, expected);\n  });\n});\n"
  },
  {
    "path": "plugins/table-merged-cell/src/css/plugin.css",
    "content": ".toastui-editor-context-menu .menu-item .merge-cells::before {\n  background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmlld0JveD0iMCAwIDUxMiA1MTIiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGcgZmlsbD0iIzQzNDM0MyIgc3Ryb2tlPSIjNDM0MzQzIj4NCgk8cGF0aCBkPSJNMjM2LjcsMjUzLjdsLTk3LjYtNzcuMmMtMS45LTEuNi00LjctMC4xLTQuNywyLjNWMjM0SDQ0VjQ0aDEzMS45djgyLjdjMCwxLjQsMS4yLDIuNiwyLjYsMi42aDM4LjhjMS40LDAsMi42LTEuMiwyLjYtMi42DQoJCVYxOC4xYzAtMTAtOC4xLTE4LjEtMTguMS0xOC4xSDE4LjFDOC4xLDAsMCw4LjEsMCwxOC4xdjQ3NS44YzAsMTAsOC4xLDE4LjEsMTguMSwxOC4xaDE4My42YzEwLDAsMTguMS04LjEsMTguMS0xOC4xVjM4NS4zDQoJCWMwLTEuNC0xLjItMi42LTIuNi0yLjZoLTM4LjhjLTEuNCwwLTIuNiwxLjItMi42LDIuNlY0NjhINDRWMjc4aDkwLjV2NTUuMmMwLDIuNSwyLjgsMy45LDQuNywyLjNsOTcuNi03Ny4yDQoJCUMyMzguMywyNTcuMiwyMzguMywyNTQuOCwyMzYuNywyNTMuN3ogTTQ5My45LDBIMzEwLjNjLTEwLDAtMTguMSw4LjEtMTguMSwxOC4xdjEwOC42YzAsMS40LDEuMiwyLjYsMi42LDIuNmgzOC44DQoJCWMxLjQsMCwyLjYtMS4yLDIuNi0yLjZWNDRINDY4VjIzNGgtOTAuNXYtNTUuMmMwLTIuNS0yLjgtMy45LTQuNy0yLjNsLTk3LjYsNzcuMmMtMS41LDEuMi0xLjUsMy40LDAsNC42bDk3LjYsNzcuMw0KCQljMS45LDEuNSw0LjcsMC4xLDQuNy0yLjNWMjc4SDQ2OFY0NjhIMzM2LjJ2LTgyLjdjMC0xLjQtMS4yLTIuNi0yLjYtMi42aC0zOC44Yy0xLjQsMC0yLjYsMS4yLTIuNiwyLjZ2MTA4LjYNCgkJYzAsMTAsOC4xLDE4LjEsMTguMSwxOC4xaDE4My42YzEwLDAsMTguMS04LjEsMTguMS0xOC4xVjE4LjFDNTEyLDguMSw1MDMuOSwwLDQ5My45LDB6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==');\n  background-position: 5px 2px;\n  background-size: 14px 14px;\n}\n\n.toastui-editor-context-menu .menu-item .split-cells::before {\n  background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmlld0JveD0iMCAwIDUxMiA1MTIiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGcgZmlsbD0iIzQzNDM0MyIgc3Ryb2tlPSIjNDM0MzQzIj4NCgk8cGF0aCBkPSJNNTEwLjksMjUzLjhsLTkwLjMtNzEuNGMtMS44LTEuNC00LjQtMC4xLTQuNCwyLjJ2NTEuMWgtODYuMVY1OS44aDEyMnY3Ni42YzAsMS4zLDEuMSwyLjQsMi40LDIuNGgzNS45DQoJCWMxLjMsMCwyLjQtMS4xLDIuNC0yLjRWMzUuOWMwLTkuMy03LjUtMTYuNy0xNi43LTE2LjdIMzA2LjJjLTkuMywwLTE2LjcsNy41LTE2LjcsMTYuN3Y0NDAuMmMwLDkuMyw3LjUsMTYuNywxNi43LDE2LjdoMTY5LjkNCgkJYzkuMywwLDE2LjctNy41LDE2LjctMTYuN1YzNzUuNmMwLTEuMy0xLjEtMi40LTIuNC0yLjRoLTM1LjljLTEuMywwLTIuNCwxLjEtMi40LDIuNHY3Ni42aC0xMjJWMjc2LjNoODYuMXY1MS4xDQoJCWMwLDIuMywyLjYsMy42LDQuNCwyLjJsOTAuMy03MS40QzUxMi40LDI1Ny4xLDUxMi40LDI1NC45LDUxMC45LDI1My44eiBNMjA1LjgsMTkuMUgzNS45Yy05LjMsMC0xNi43LDcuNS0xNi43LDE2Ljd2MTAwLjUNCgkJYzAsMS4zLDEuMSwyLjQsMi40LDIuNGgzNS45YzEuMywwLDIuNC0xLjEsMi40LTIuNFY1OS44aDEyMnYxNzUuOEg5NS43di01MS4xYzAtMi4zLTIuNi0zLjYtNC40LTIuMkwxLDI1My44DQoJCWMtMS40LDEuMS0xLjQsMy4yLDAsNC4ybDkwLjMsNzEuNWMxLjcsMS40LDQuNCwwLjEsNC40LTIuMnYtNTEuMWg4Ni4xdjE3NS44aC0xMjJ2LTc2LjZjMC0xLjMtMS4xLTIuNC0yLjQtMi40SDIxLjUNCgkJYy0xLjMsMC0yLjQsMS4xLTIuNCwyLjR2MTAwLjVjMCw5LjMsNy41LDE2LjcsMTYuNywxNi43aDE2OS45YzkuMywwLDE2LjctNy41LDE2LjctMTYuN1YzNS45QzIyMi41LDI2LjYsMjE1LDE5LjEsMjA1LjgsMTkuMXoiLz4NCjwvZz4NCjwvc3ZnPg0K');\n  background-position: 5px 2px;\n  background-size: 14px 14px;\n}\n\n.toastui-editor-dark .toastui-editor-context-menu .menu-item .merge-cells::before {\n  background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmlld0JveD0iMCAwIDUxMiA1MTIiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGcgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjNDM0MzQzIj4NCgk8cGF0aCBkPSJNMjM2LjcsMjUzLjdsLTk3LjYtNzcuMmMtMS45LTEuNi00LjctMC4xLTQuNywyLjNWMjM0SDQ0VjQ0aDEzMS45djgyLjdjMCwxLjQsMS4yLDIuNiwyLjYsMi42aDM4LjhjMS40LDAsMi42LTEuMiwyLjYtMi42DQoJCVYxOC4xYzAtMTAtOC4xLTE4LjEtMTguMS0xOC4xSDE4LjFDOC4xLDAsMCw4LjEsMCwxOC4xdjQ3NS44YzAsMTAsOC4xLDE4LjEsMTguMSwxOC4xaDE4My42YzEwLDAsMTguMS04LjEsMTguMS0xOC4xVjM4NS4zDQoJCWMwLTEuNC0xLjItMi42LTIuNi0yLjZoLTM4LjhjLTEuNCwwLTIuNiwxLjItMi42LDIuNlY0NjhINDRWMjc4aDkwLjV2NTUuMmMwLDIuNSwyLjgsMy45LDQuNywyLjNsOTcuNi03Ny4yDQoJCUMyMzguMywyNTcuMiwyMzguMywyNTQuOCwyMzYuNywyNTMuN3ogTTQ5My45LDBIMzEwLjNjLTEwLDAtMTguMSw4LjEtMTguMSwxOC4xdjEwOC42YzAsMS40LDEuMiwyLjYsMi42LDIuNmgzOC44DQoJCWMxLjQsMCwyLjYtMS4yLDIuNi0yLjZWNDRINDY4VjIzNGgtOTAuNXYtNTUuMmMwLTIuNS0yLjgtMy45LTQuNy0yLjNsLTk3LjYsNzcuMmMtMS41LDEuMi0xLjUsMy40LDAsNC42bDk3LjYsNzcuMw0KCQljMS45LDEuNSw0LjcsMC4xLDQuNy0yLjNWMjc4SDQ2OFY0NjhIMzM2LjJ2LTgyLjdjMC0xLjQtMS4yLTIuNi0yLjYtMi42aC0zOC44Yy0xLjQsMC0yLjYsMS4yLTIuNiwyLjZ2MTA4LjYNCgkJYzAsMTAsOC4xLDE4LjEsMTguMSwxOC4xaDE4My42YzEwLDAsMTguMS04LjEsMTguMS0xOC4xVjE4LjFDNTEyLDguMSw1MDMuOSwwLDQ5My45LDB6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==');\n  background-position: 5px 2px;\n}\n\n.toastui-editor-dark .toastui-editor-context-menu .menu-item .split-cells::before {\n  background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmlld0JveD0iMCAwIDUxMiA1MTIiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGcgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjNDM0MzQzIj4NCgk8cGF0aCBkPSJNNTEwLjksMjUzLjhsLTkwLjMtNzEuNGMtMS44LTEuNC00LjQtMC4xLTQuNCwyLjJ2NTEuMWgtODYuMVY1OS44aDEyMnY3Ni42YzAsMS4zLDEuMSwyLjQsMi40LDIuNGgzNS45DQoJCWMxLjMsMCwyLjQtMS4xLDIuNC0yLjRWMzUuOWMwLTkuMy03LjUtMTYuNy0xNi43LTE2LjdIMzA2LjJjLTkuMywwLTE2LjcsNy41LTE2LjcsMTYuN3Y0NDAuMmMwLDkuMyw3LjUsMTYuNywxNi43LDE2LjdoMTY5LjkNCgkJYzkuMywwLDE2LjctNy41LDE2LjctMTYuN1YzNzUuNmMwLTEuMy0xLjEtMi40LTIuNC0yLjRoLTM1LjljLTEuMywwLTIuNCwxLjEtMi40LDIuNHY3Ni42aC0xMjJWMjc2LjNoODYuMXY1MS4xDQoJCWMwLDIuMywyLjYsMy42LDQuNCwyLjJsOTAuMy03MS40QzUxMi40LDI1Ny4xLDUxMi40LDI1NC45LDUxMC45LDI1My44eiBNMjA1LjgsMTkuMUgzNS45Yy05LjMsMC0xNi43LDcuNS0xNi43LDE2Ljd2MTAwLjUNCgkJYzAsMS4zLDEuMSwyLjQsMi40LDIuNGgzNS45YzEuMywwLDIuNC0xLjEsMi40LTIuNFY1OS44aDEyMnYxNzUuOEg5NS43di01MS4xYzAtMi4zLTIuNi0zLjYtNC40LTIuMkwxLDI1My44DQoJCWMtMS40LDEuMS0xLjQsMy4yLDAsNC4ybDkwLjMsNzEuNWMxLjcsMS40LDQuNCwwLjEsNC40LTIuMnYtNTEuMWg4Ni4xdjE3NS44aC0xMjJ2LTc2LjZjMC0xLjMtMS4xLTIuNC0yLjQtMi40SDIxLjUNCgkJYy0xLjMsMC0yLjQsMS4xLTIuNCwyLjR2MTAwLjVjMCw5LjMsNy41LDE2LjcsMTYuNywxNi43aDE2OS45YzkuMywwLDE2LjctNy41LDE2LjctMTYuN1YzNS45QzIyMi41LDI2LjYsMjE1LDE5LjEsMjA1LjgsMTkuMXoiLz4NCjwvZz4NCjwvc3ZnPg0K');\n  background-position: 5px 2px;\n}"
  },
  {
    "path": "plugins/table-merged-cell/src/i18n/langs.ts",
    "content": "import { I18n } from '@toast-ui/editor';\n\nexport function addLangs(i18n: I18n) {\n  i18n.setLanguage(['ko', 'ko-KR'], {\n    'Merge cells': '셀 병합',\n    'Split cells': '셀 병합해제',\n    'Cannot change part of merged cell': '병합된 셀의 일부를 변경할 수 없습니다.',\n    'Cannot paste row merged cells into the table header':\n      '테이블 헤더에는 행 병합된 셀을 붙여넣을 수 없습니다.',\n  });\n\n  i18n.setLanguage(['en', 'en-US'], {\n    'Merge cells': 'Merge cells',\n    'Split cells': 'Split cells',\n    'Cannot change part of merged cell': 'Cannot change part of merged cell.',\n    'Cannot paste row merged cells into the table header':\n      'Cannot paste row merged cells into the table header.',\n  });\n\n  i18n.setLanguage(['es', 'es-ES'], {\n    'Merge cells': 'Combinar celdas',\n    'Split cells': 'Separar celdas',\n    'Cannot change part of merged cell': 'No se puede cambiar parte de una celda combinada.',\n    'Cannot paste row merged cells into the table header':\n      'No se pueden pegar celdas combinadas en el encabezado de tabla.',\n  });\n\n  i18n.setLanguage(['ja', 'ja-JP'], {\n    'Merge cells': 'セルの結合',\n    'Split cells': 'セルの結合を解除',\n    'Cannot change part of merged cell': '結合されたセルの一部を変更することはできません。',\n    'Cannot paste row merged cells into the table header':\n      '行にマージされたセルをヘッダーに貼り付けることはできません。',\n  });\n\n  i18n.setLanguage(['nl', 'nl-NL'], {\n    'Merge cells': 'Cellen samenvoegen',\n    'Split cells': 'Samengevoegde cellen ongedaan maken',\n    'Cannot change part of merged cell': 'Kan geen deel uit van een samengevoegde cel veranderen.',\n    'Cannot paste row merged cells into the table header':\n      'Kan geen rij met samengevoegde cellen in de koptekst plakken.',\n  });\n\n  i18n.setLanguage('zh-CN', {\n    'Merge cells': '合并单元格',\n    'Split cells': '取消合并单元格',\n    'Cannot change part of merged cell': '无法更改合并单元格的一部分。',\n    'Cannot paste row merged cells into the table header': '无法将行合并单元格粘贴到标题中。',\n  });\n\n  i18n.setLanguage(['de', 'de-DE'], {\n    'Merge cells': 'Zellen zusammenführen',\n    'Split cells': 'Zusammenführen rückgängig machen',\n    'Cannot change part of merged cell':\n      'Der Teil der verbundenen Zelle kann nicht geändert werden.',\n    'Cannot paste row merged cells into the table header':\n      'Die Zeile der verbundenen Zellen kann nicht in die Kopfzeile eingefügt werden.',\n  });\n\n  i18n.setLanguage(['ru', 'ru-RU'], {\n    'Merge cells': 'Объединить ячейки',\n    'Split cells': 'Разъединить ячейки',\n    'Cannot change part of merged cell': 'Вы не можете изменять часть комбинированной ячейки.',\n    'Cannot paste row merged cells into the table header':\n      'Вы не можете вставлять объединенные ячейки в заголовок таблицы.',\n  });\n\n  i18n.setLanguage(['fr', 'fr-FR'], {\n    'Merge cells': 'Fusionner les cellules',\n    'Split cells': 'Séparer les cellules',\n    'Cannot change part of merged cell':\n      'Impossible de modifier une partie de la cellule fusionnée.',\n    'Cannot paste row merged cells into the table header':\n      \"Impossible de coller les cellules fusionnées dans l'en-tête du tableau.\",\n  });\n\n  i18n.setLanguage(['uk', 'uk-UA'], {\n    'Merge cells': \"Об'єднати комірки\",\n    'Split cells': \"Роз'єднати комірки\",\n    'Cannot change part of merged cell': 'Ви не можете змінювати частину комбінованої комірки.',\n    'Cannot paste row merged cells into the table header':\n      \"Ви не можете вставляти об'єднані комірки в заголовок таблиці.\",\n  });\n\n  i18n.setLanguage(['tr', 'tr-TR'], {\n    'Merge cells': 'Hücreleri birleştir',\n    'Split cells': 'Hücreleri ayır',\n    'Cannot change part of merged cell': 'Birleştirilmiş hücrelerin bir kısmı değiştirelemez.',\n    'Cannot paste row merged cells into the table header':\n      'Satırda birleştirilmiş hücreler sütun başlığına yapıştırılamaz',\n  });\n\n  i18n.setLanguage(['fi', 'fi-FI'], {\n    'Merge cells': 'Yhdistä solut',\n    'Split cells': 'Jaa solut',\n    'Cannot change part of merged cell': 'Yhdistettyjen solujen osaa ei voi muuttaa',\n    'Cannot paste row merged cells into the table header':\n      'Soluja ei voi yhdistää taulukon otsikkoriviin',\n  });\n\n  i18n.setLanguage(['cs', 'cs-CZ'], {\n    'Merge cells': 'Spojit buňky',\n    'Split cells': 'Rozpojit buňky',\n    'Cannot change part of merged cell': 'Nelze měnit část spojené buňky',\n    'Cannot paste row merged cells into the table header':\n      'Nelze vkládat spojené buňky do záhlaví tabulky',\n  });\n\n  i18n.setLanguage('ar', {\n    'Merge cells': 'دمج الوحدات',\n    'Split cells': 'إلغاء دمج الوحدات',\n    'Cannot change part of merged cell': 'لا يمكن تغيير جزء من الخلية المدموجة',\n    'Cannot paste row merged cells into the table header':\n      'لا يمكن لصق الخلايا المدموجة من صف واحد في رأس الجدول',\n  });\n\n  i18n.setLanguage(['pl', 'pl-PL'], {\n    'Merge cells': 'Scal komórki',\n    'Split cells': 'Rozłącz komórki',\n    'Cannot change part of merged cell': 'Nie można zmienić części scalonej komórki.',\n    'Cannot paste row merged cells into the table header':\n      'Nie można wkleić komórek o scalonym rzędzie w nagłówek tabeli.',\n  });\n\n  i18n.setLanguage('zh-TW', {\n    'Merge cells': '合併儲存格',\n    'Split cells': '取消合併儲存格',\n    'Cannot change part of merged cell': '無法變更儲存格的一部分。',\n    'Cannot paste row merged cells into the table header': '無法將合併的儲存格貼上至表格標題中。',\n  });\n\n  i18n.setLanguage(['gl', 'gl-ES'], {\n    'Merge cells': 'Combinar celas',\n    'Split cells': 'Separar celas',\n    'Cannot change part of merged cell': 'Non se pode cambiar parte dunha cela combinada',\n    'Cannot paste row merged cells into the table header':\n      'Non se poden pegar celas no encabezado da táboa',\n  });\n\n  i18n.setLanguage(['sv', 'sv-SE'], {\n    'Merge cells': 'Sammanfoga celler',\n    'Split cells': 'Dela celler',\n    'Cannot change part of merged cell': 'Ej möjligt att ändra en del av en sammanfogad cell',\n    'Cannot paste row merged cells into the table header':\n      'Ej möjligt att klistra in rad-sammanfogade celler i tabellens huvud',\n  });\n\n  i18n.setLanguage(['it', 'it-IT'], {\n    'Merge cells': 'Unisci celle',\n    'Split cells': 'Separa celle',\n    'Cannot change part of merged cell': 'Non è possibile modificare parte di una cella unita',\n    'Cannot paste row merged cells into the table header':\n      \"Non è possibile incollare celle unite per riga nell'intestazione della tabella\",\n  });\n\n  i18n.setLanguage(['nb', 'nb-NO'], {\n    'Merge cells': 'Slå sammen celler',\n    'Split cells': 'Separer celler',\n    'Cannot change part of merged cell': 'Kan ikke endre deler av sammenslåtte celler',\n    'Cannot paste row merged cells into the table header':\n      'Kan ikke lime inn rad med sammenslåtte celler',\n  });\n\n  i18n.setLanguage(['hr', 'hr-HR'], {\n    'Merge cells': 'Spoji ćelije',\n    'Split cells': 'Odspoji ćelije',\n    'Cannot change part of merged cell': 'Ne mogu mijenjati dio spojene ćelije.',\n    'Cannot paste row merged cells into the table header':\n      'Ne mogu zaljepiti redak spojenih ćelija u zaglavlje tablice',\n  });\n}\n"
  },
  {
    "path": "plugins/table-merged-cell/src/index.ts",
    "content": "import type { PluginContext, PluginInfo } from '@toast-ui/editor';\nimport { markdownParsers } from '@/markdown/parser';\nimport { toHTMLRenderers } from '@/markdown/renderer';\nimport { toMarkdownRenderers } from '@/wysiwyg/renderer';\nimport { addLangs } from '@/i18n/langs';\nimport { offsetMapMixin, createOffsetMapMixin } from '@/wysiwyg/tableOffsetMapMixin';\nimport { addMergedTableContextMenu } from '@/wysiwyg/contextMenu';\nimport { createCommands } from '@/wysiwyg/commandFactory';\n\nimport './css/plugin.css';\n\nexport default function tableMergedCellPlugin(context: PluginContext): PluginInfo {\n  const { i18n, eventEmitter } = context;\n  const TableOffsetMap = eventEmitter.emitReduce(\n    'mixinTableOffsetMapPrototype',\n    offsetMapMixin,\n    createOffsetMapMixin\n  );\n\n  addLangs(i18n);\n  addMergedTableContextMenu(context);\n\n  return {\n    toHTMLRenderers,\n    markdownParsers,\n    toMarkdownRenderers,\n    wysiwygCommands: createCommands(context, TableOffsetMap),\n  };\n}\n"
  },
  {
    "path": "plugins/table-merged-cell/src/markdown/parser.ts",
    "content": "import type { CustomParserMap } from '@toast-ui/toastmark';\nimport { MergedTableRowMdNode, MergedTableCellMdNode, SpanType } from '@t/index';\n\ninterface Attrs {\n  colspan?: number;\n  rowspan?: number;\n}\ntype CellSpanInfo = [spanCount: number, content: string];\n\nfunction getSpanInfo(content: string, type: SpanType, oppositeType: SpanType): CellSpanInfo {\n  const reSpan = new RegExp(`^((?:${oppositeType}=[0-9]+:)?)${type}=([0-9]+):(.*)`);\n  const parsed = reSpan.exec(content);\n  let spanCount = 1;\n\n  if (parsed) {\n    spanCount = parseInt(parsed[2], 10);\n    content = parsed[1] + parsed[3];\n  }\n\n  return [spanCount, content];\n}\n\nfunction extendTableCellIndexWithRowspanMap(\n  node: MergedTableCellMdNode,\n  parent: MergedTableRowMdNode,\n  rowspan: number\n) {\n  const prevRow = parent.prev;\n\n  if (prevRow) {\n    const columnLen = parent.parent.parent.columns.length;\n\n    // increment the index when prev row has the rowspan count.\n    for (let i = node.startIdx; i < columnLen; i += 1) {\n      const prevRowspanCount = prevRow.rowspanMap[i];\n\n      if (prevRowspanCount && prevRowspanCount > 1) {\n        parent.rowspanMap[i] = prevRowspanCount - 1;\n\n        if (i <= node.endIdx) {\n          node.startIdx += 1;\n          node.endIdx += 1;\n        }\n      }\n    }\n  }\n\n  if (rowspan > 1) {\n    const { startIdx, endIdx } = node;\n\n    for (let i = startIdx; i <= endIdx; i += 1) {\n      parent.rowspanMap[i] = rowspan;\n    }\n  }\n}\n\nexport const markdownParsers: CustomParserMap = {\n  // @ts-expect-error\n  tableRow(node: MergedTableRowMdNode, { entering }) {\n    if (entering) {\n      node.rowspanMap = {};\n\n      if (node.prev && !node.firstChild) {\n        const prevRowspanMap = node.prev.rowspanMap;\n\n        Object.keys(prevRowspanMap).forEach((key) => {\n          if (prevRowspanMap[key] > 1) {\n            node.rowspanMap[key] = prevRowspanMap[key] - 1;\n          }\n        });\n      }\n    }\n  },\n  // @ts-expect-error\n  tableCell(node: MergedTableCellMdNode, { entering }) {\n    const { parent, prev, stringContent } = node;\n\n    if (entering) {\n      const attrs: Attrs = {};\n      let content = stringContent!;\n      let [colspan, rowspan] = [1, 1];\n\n      [colspan, content] = getSpanInfo(content, '@cols', '@rows');\n      [rowspan, content] = getSpanInfo(content, '@rows', '@cols');\n\n      node.stringContent = content;\n\n      if (prev) {\n        node.startIdx = prev.endIdx + 1;\n        node.endIdx = node.startIdx;\n      }\n      if (colspan > 1) {\n        attrs.colspan = colspan;\n        node.endIdx += colspan - 1;\n      }\n      if (rowspan > 1) {\n        attrs.rowspan = rowspan;\n      }\n      node.attrs = attrs;\n\n      extendTableCellIndexWithRowspanMap(node, parent, rowspan);\n\n      const tablePart = parent.parent;\n\n      if (tablePart.type === 'tableBody' && node.endIdx >= tablePart.parent.columns.length) {\n        node.ignored = true;\n      }\n    }\n  },\n};\n"
  },
  {
    "path": "plugins/table-merged-cell/src/markdown/renderer.ts",
    "content": "import type { CustomHTMLRenderer } from '@toast-ui/editor';\nimport type { OpenTagToken } from '@toast-ui/toastmark';\nimport { MergedTableCellMdNode, MergedTableRowMdNode } from '@t/index';\n\nexport const toHTMLRenderers: CustomHTMLRenderer = {\n  // @ts-ignore\n  tableRow(node: MergedTableRowMdNode, { entering, origin }) {\n    if (entering) {\n      return origin!();\n    }\n\n    const result = [];\n\n    if (node.lastChild) {\n      const columnLen = node.parent.parent.columns.length;\n      const lastColIdx = node.lastChild.endIdx;\n\n      for (let i = lastColIdx + 1; i < columnLen; i += 1) {\n        if (!node.prev || !node.prev.rowspanMap[i] || node.prev.rowspanMap[i] <= 1) {\n          result.push(\n            {\n              type: 'openTag',\n              tagName: 'td',\n              outerNewLine: true,\n            },\n            {\n              type: 'closeTag',\n              tagName: 'td',\n              outerNewLine: true,\n            }\n          );\n        }\n      }\n    }\n\n    result.push({\n      type: 'closeTag',\n      tagName: 'tr',\n      outerNewLine: true,\n    });\n\n    return result;\n  },\n  // @ts-ignore\n  tableCell(node: MergedTableCellMdNode, { entering, origin }) {\n    const result = origin!();\n\n    if (node.ignored) {\n      return result;\n    }\n\n    if (entering) {\n      const attributes: Record<string, string> = { ...node.attrs };\n\n      (result as OpenTagToken).attributes = {\n        ...(result as OpenTagToken).attributes,\n        ...attributes,\n      };\n    }\n    return result;\n  },\n};\n"
  },
  {
    "path": "plugins/table-merged-cell/src/wysiwyg/command/addColumn.ts",
    "content": "import type { PluginContext } from '@toast-ui/editor';\nimport type { TableOffsetMapFactory, TableOffsetMap, CommandFn, SelectionInfo } from '@t/index';\nimport { createDummyCells, getResolvedSelection, getRowAndColumnCount, setAttrs } from '../util';\nimport { Direction } from './direction';\n\ntype ColDirection = Direction.LEFT | Direction.RIGHT;\n\nfunction getTargetColInfo(\n  direction: ColDirection,\n  map: TableOffsetMap,\n  selectionInfo: SelectionInfo\n) {\n  let targetColIdx: number;\n  let judgeToExtendColspan: (rowIdx: number) => boolean;\n  let insertColIdx: number;\n\n  if (direction === Direction.LEFT) {\n    targetColIdx = selectionInfo.startColIdx;\n    judgeToExtendColspan = (rowIdx: number) => map.extendedColspan(rowIdx, targetColIdx);\n    insertColIdx = targetColIdx;\n  } else {\n    targetColIdx = selectionInfo.endColIdx;\n    judgeToExtendColspan = (rowIdx: number) => map.getColspanCount(rowIdx, targetColIdx) > 1;\n    insertColIdx = targetColIdx + 1;\n  }\n\n  return { targetColIdx, judgeToExtendColspan, insertColIdx };\n}\n\nexport function createAddColumnCommand(\n  context: PluginContext,\n  OffsetMap: TableOffsetMapFactory,\n  direction: ColDirection\n) {\n  const addColumn: CommandFn = (_, state, dispatch) => {\n    const { selection, tr, schema } = state;\n    const { anchor, head } = getResolvedSelection(selection, context);\n\n    if (!anchor || !head) {\n      return false;\n    }\n\n    const map = OffsetMap.create(anchor)!;\n    const selectionInfo = map.getRectOffsets(anchor, head);\n\n    const { targetColIdx, judgeToExtendColspan, insertColIdx } = getTargetColInfo(\n      direction,\n      map,\n      selectionInfo\n    );\n\n    const { columnCount } = getRowAndColumnCount(selectionInfo);\n    const { totalRowCount } = map;\n\n    for (let rowIdx = 0; rowIdx < totalRowCount; rowIdx += 1) {\n      // increase colspan count inside the col-spanning cell\n      if (judgeToExtendColspan(rowIdx)) {\n        const { node, pos } = map.getColspanStartInfo(rowIdx, targetColIdx)!;\n        const attrs = setAttrs(node, { colspan: node.attrs.colspan + columnCount });\n\n        tr.setNodeMarkup(tr.mapping.map(pos), null, attrs);\n      } else {\n        const cells = createDummyCells(columnCount, rowIdx, schema);\n\n        tr.insert(tr.mapping.map(map.posAt(rowIdx, insertColIdx)), cells);\n      }\n    }\n    dispatch!(tr);\n    return true;\n  };\n\n  return addColumn;\n}\n"
  },
  {
    "path": "plugins/table-merged-cell/src/wysiwyg/command/addRow.ts",
    "content": "import type { PluginContext } from '@toast-ui/editor';\nimport type { TableOffsetMapFactory, TableOffsetMap, CommandFn, SelectionInfo } from '@t/index';\nimport type { Node } from 'prosemirror-model';\nimport { createDummyCells, getResolvedSelection, getRowAndColumnCount, setAttrs } from '../util';\nimport { Direction } from './direction';\n\ntype RowDirection = Direction.UP | Direction.DOWN;\n\nfunction getTargetRowInfo(\n  direction: RowDirection,\n  map: TableOffsetMap,\n  selectionInfo: SelectionInfo\n) {\n  let targetRowIdx: number;\n  let judgeToExtendRowspan: (rowIdx: number) => boolean;\n  let insertColIdx: number;\n  let nodeSize: number;\n\n  if (direction === Direction.UP) {\n    targetRowIdx = selectionInfo.startRowIdx;\n    judgeToExtendRowspan = (colIdx: number) => map.extendedRowspan(targetRowIdx, colIdx);\n    insertColIdx = 0;\n    nodeSize = -1;\n  } else {\n    targetRowIdx = selectionInfo.endRowIdx;\n    judgeToExtendRowspan = (colIdx: number) => map.getRowspanCount(targetRowIdx, colIdx) > 1;\n    insertColIdx = map.totalColumnCount - 1;\n    nodeSize = !map.extendedRowspan(targetRowIdx, insertColIdx)\n      ? map.getCellInfo(targetRowIdx, insertColIdx).nodeSize + 1\n      : 2;\n  }\n  return { targetRowIdx, judgeToExtendRowspan, insertColIdx, nodeSize };\n}\n\nexport function createAddRowCommand(\n  context: PluginContext,\n  OffsetMap: TableOffsetMapFactory,\n  direction: RowDirection\n) {\n  const addRow: CommandFn = (_, state, dispatch) => {\n    const { selection, schema, tr } = state;\n    const { anchor, head } = getResolvedSelection(selection, context);\n\n    if (!anchor || !head) {\n      return false;\n    }\n\n    const map = OffsetMap.create(anchor)!;\n    const { totalColumnCount } = map;\n    const selectionInfo = map.getRectOffsets(anchor, head);\n    const { rowCount } = getRowAndColumnCount(selectionInfo);\n    const { targetRowIdx, judgeToExtendRowspan, insertColIdx, nodeSize } = getTargetRowInfo(\n      direction,\n      map,\n      selectionInfo\n    );\n    const selectedThead = targetRowIdx === 0;\n\n    if (selectedThead) {\n      return false;\n    }\n\n    const rows: Node[] = [];\n\n    const from = tr.mapping.map(map.posAt(targetRowIdx, insertColIdx)) + nodeSize;\n    let cells: Node[] = [];\n\n    for (let colIdx = 0; colIdx < totalColumnCount; colIdx += 1) {\n      // increase rowspan count inside the row-spanning cell\n      if (judgeToExtendRowspan(colIdx)) {\n        const { node, pos } = map.getRowspanStartInfo(targetRowIdx, colIdx)!;\n        const attrs = setAttrs(node, { rowspan: node.attrs.rowspan + rowCount });\n\n        tr.setNodeMarkup(tr.mapping.map(pos), null, attrs);\n      } else {\n        cells = cells.concat(createDummyCells(1, targetRowIdx, schema));\n      }\n    }\n\n    for (let i = 0; i < rowCount; i += 1) {\n      rows.push(schema.nodes.tableRow.create(null, cells));\n    }\n    dispatch!(tr.insert(from, rows));\n    return true;\n  };\n\n  return addRow;\n}\n"
  },
  {
    "path": "plugins/table-merged-cell/src/wysiwyg/command/direction.ts",
    "content": "// eslint-disable-next-line no-shadow\nexport const enum Direction {\n  LEFT = 'left',\n  RIGHT = 'right',\n  UP = 'up',\n  DOWN = 'down',\n}\n"
  },
  {
    "path": "plugins/table-merged-cell/src/wysiwyg/command/mergeCells.ts",
    "content": "import type { Transaction } from 'prosemirror-state';\nimport type { PluginContext } from '@toast-ui/editor';\nimport type { TableOffsetMapFactory, TableOffsetMap, CommandFn } from '@t/index';\nimport type { Fragment, Node } from 'prosemirror-model';\nimport {\n  getCellSelectionClass,\n  getResolvedSelection,\n  getRowAndColumnCount,\n  setAttrs,\n} from '../util';\n\ninterface RangeInfo {\n  startNode: Node;\n  startPos: number;\n  rowCount: number;\n  columnCount: number;\n}\n\nexport function createMergeCellsCommand(context: PluginContext, OffsetMap: TableOffsetMapFactory) {\n  const { Fragment: FragmentClass } = context.pmModel;\n\n  const mergeCells: CommandFn = (_, state, dispatch) => {\n    const { selection, tr } = state;\n    const { anchor, head } = getResolvedSelection(selection, context);\n\n    // @ts-ignore\n    // judge cell selection\n    if (!anchor || !head || !selection.isCellSelection) {\n      return false;\n    }\n\n    const map = OffsetMap.create(anchor)!;\n    const CellSelection = getCellSelectionClass(selection);\n\n    const { totalRowCount, totalColumnCount } = map;\n    const selectionInfo = map.getRectOffsets(anchor, head);\n    const { rowCount, columnCount } = getRowAndColumnCount(selectionInfo);\n\n    const { startRowIdx, startColIdx, endRowIdx, endColIdx } = selectionInfo;\n\n    const allSelected = rowCount >= totalRowCount - 1 && columnCount === totalColumnCount;\n    const hasTableHead = startRowIdx === 0 && endRowIdx > startRowIdx;\n\n    if (allSelected || hasTableHead) {\n      return false;\n    }\n\n    let fragment = FragmentClass.empty;\n\n    for (let rowIdx = startRowIdx; rowIdx <= endRowIdx; rowIdx += 1) {\n      for (let colIdx = startColIdx; colIdx <= endColIdx; colIdx += 1) {\n        // set first cell content\n        if (rowIdx === startRowIdx && colIdx === startColIdx) {\n          fragment = appendFragment(rowIdx, colIdx, fragment, map);\n          // set each cell content and delete the cell for spanning\n        } else if (!map.extendedRowspan(rowIdx, colIdx) && !map.extendedColspan(rowIdx, colIdx)) {\n          const { offset, nodeSize } = map.getCellInfo(rowIdx, colIdx);\n          const from = tr.mapping.map(offset);\n          const to = from + nodeSize;\n\n          fragment = appendFragment(rowIdx, colIdx, fragment, map);\n\n          tr.delete(from, to);\n        }\n      }\n    }\n\n    const { node, pos } = map.getNodeAndPos(startRowIdx, startColIdx);\n\n    // set rowspan, colspan to first root cell\n    setSpanToRootCell(tr, fragment, {\n      startNode: node,\n      startPos: pos,\n      rowCount,\n      columnCount,\n    });\n\n    tr.setSelection(new CellSelection(tr.doc.resolve(pos)));\n\n    dispatch!(tr);\n    return true;\n  };\n\n  return mergeCells;\n}\n\nfunction setSpanToRootCell(tr: Transaction, fragment: Fragment, rangeInfo: RangeInfo) {\n  const { startNode, startPos, rowCount, columnCount } = rangeInfo;\n\n  tr.setNodeMarkup(\n    startPos,\n    null,\n    setAttrs(startNode, { colspan: columnCount, rowspan: rowCount })\n  );\n\n  if (fragment.size) {\n    // add 1 for text start offset(not node start offset)\n    tr.replaceWith(startPos + 1, startPos + startNode.content.size, fragment);\n  }\n}\n\nfunction appendFragment(rowIdx: number, colIdx: number, fragment: Fragment, map: TableOffsetMap) {\n  const targetFragment = map.getNodeAndPos(rowIdx, colIdx).node.content;\n\n  // prevent to add empty string\n  return targetFragment.size > 2 ? fragment.append(targetFragment) : fragment;\n}\n"
  },
  {
    "path": "plugins/table-merged-cell/src/wysiwyg/command/removeColumn.ts",
    "content": "import type { PluginContext } from '@toast-ui/editor';\nimport type { TableOffsetMapFactory, CommandFn } from '@t/index';\nimport { getResolvedSelection, getRowAndColumnCount, setAttrs } from '../util';\n\nexport function createRemoveColumnCommand(\n  context: PluginContext,\n  OffsetMap: TableOffsetMapFactory\n) {\n  const removeColumn: CommandFn = (_, state, dispatch) => {\n    const { selection, tr } = state;\n    const { anchor, head } = getResolvedSelection(selection, context);\n\n    if (!anchor || !head) {\n      return false;\n    }\n\n    const map = OffsetMap.create(anchor)!;\n    const selectionInfo = map.getRectOffsets(anchor, head);\n\n    const { totalColumnCount, totalRowCount } = map;\n    const { columnCount } = getRowAndColumnCount(selectionInfo);\n    const selectedAllColumn = columnCount === totalColumnCount;\n\n    if (selectedAllColumn) {\n      return false;\n    }\n\n    const { startColIdx, endColIdx } = selectionInfo;\n    const mapStart = tr.mapping.maps.length;\n\n    for (let rowIdx = 0; rowIdx < totalRowCount; rowIdx += 1) {\n      for (let colIdx = endColIdx; colIdx >= startColIdx; colIdx -= 1) {\n        const { offset, nodeSize } = map.getCellInfo(rowIdx, colIdx);\n        const colspanInfo = map.getColspanStartInfo(rowIdx, colIdx)!;\n\n        if (!map.extendedRowspan(rowIdx, colIdx)) {\n          // decrease colspan count inside the col-spanning cell\n          if (colspanInfo?.count > 1) {\n            const { node, pos } = map.getColspanStartInfo(rowIdx, colIdx)!;\n            const colspan = map.decreaseColspanCount(rowIdx, colIdx);\n            const attrs = setAttrs(node, { colspan: colspan > 1 ? colspan : null });\n\n            tr.setNodeMarkup(tr.mapping.slice(mapStart).map(pos), null, attrs);\n          } else {\n            const from = tr.mapping.slice(mapStart).map(offset);\n            const to = from + nodeSize;\n\n            tr.delete(from, to);\n          }\n        }\n      }\n    }\n    dispatch!(tr);\n    return true;\n  };\n\n  return removeColumn;\n}\n"
  },
  {
    "path": "plugins/table-merged-cell/src/wysiwyg/command/removeRow.ts",
    "content": "import type { PluginContext } from '@toast-ui/editor';\nimport type { TableOffsetMapFactory, TableOffsetMap, CommandFn } from '@t/index';\nimport { getResolvedSelection, getRowAndColumnCount, setAttrs } from '../util';\n\nfunction getRowRanges(map: TableOffsetMap, rowIdx: number) {\n  const { totalColumnCount } = map;\n  let from = Number.MAX_VALUE;\n  let to = 0;\n\n  for (let colIdx = 0; colIdx < totalColumnCount; colIdx += 1) {\n    if (!map.extendedRowspan(rowIdx, colIdx)) {\n      const { offset, nodeSize } = map.getCellInfo(rowIdx, colIdx);\n\n      from = Math.min(from, offset);\n      to = Math.max(to, offset + nodeSize);\n    }\n  }\n  return { from, to };\n}\n\nexport function createRemoveRowCommand(context: PluginContext, OffsetMap: TableOffsetMapFactory) {\n  const removeRow: CommandFn = (_, state, dispatch) => {\n    const { selection, tr } = state;\n    const { anchor, head } = getResolvedSelection(selection, context);\n\n    if (anchor && head) {\n      let map = OffsetMap.create(anchor)!;\n      const { totalRowCount, totalColumnCount } = map;\n      const selectionInfo = map.getRectOffsets(anchor, head);\n      const { rowCount } = getRowAndColumnCount(selectionInfo);\n      const { startRowIdx, endRowIdx } = selectionInfo;\n\n      const selectedThead = startRowIdx === 0;\n      const selectedAllTbodyRow = rowCount === totalRowCount - 1;\n\n      if (selectedAllTbodyRow || selectedThead) {\n        return false;\n      }\n\n      for (let rowIdx = endRowIdx; rowIdx >= startRowIdx; rowIdx -= 1) {\n        const mapStart = tr.mapping.maps.length;\n        const { from, to } = getRowRanges(map, rowIdx);\n\n        // delete table row\n        tr.delete(from - 1, to + 1);\n\n        for (let colIdx = 0; colIdx < totalColumnCount; colIdx += 1) {\n          const rowspanInfo = map.getRowspanStartInfo(rowIdx, colIdx)!;\n\n          if (rowspanInfo?.count > 1 && !map.extendedColspan(rowIdx, colIdx)) {\n            // decrease rowspan count inside the row-spanning cell\n            // eslint-disable-next-line max-depth\n            if (map.extendedRowspan(rowIdx, colIdx)) {\n              const { node, pos } = map.getRowspanStartInfo(rowIdx, colIdx)!;\n              const rowspan = map.decreaseRowspanCount(rowIdx, colIdx);\n              const attrs = setAttrs(node, { rowspan: rowspan > 1 ? rowspan : null });\n\n              tr.setNodeMarkup(tr.mapping.slice(mapStart).map(pos), null, attrs);\n              // the row-spanning cell should be moved down\n            } else if (!map.extendedRowspan(rowIdx, colIdx)) {\n              const { node, count } = map.getRowspanStartInfo(rowIdx, colIdx)!;\n              const attrs = setAttrs(node, { rowspan: count > 2 ? count - 1 : null });\n              const copiedCell = node.type.create(attrs, node.content);\n\n              tr.insert(tr.mapping.slice(mapStart).map(map.posAt(rowIdx + 1, colIdx)), copiedCell);\n            }\n          }\n        }\n        map = OffsetMap.create(tr.doc.resolve(map.tableStartOffset))!;\n      }\n      dispatch!(tr);\n      return true;\n    }\n\n    return false;\n  };\n\n  return removeRow;\n}\n"
  },
  {
    "path": "plugins/table-merged-cell/src/wysiwyg/command/splitCells.ts",
    "content": "import type { PluginContext } from '@toast-ui/editor';\nimport type { TableOffsetMapFactory, TableOffsetMap, CommandFn, SelectionInfo } from '@t/index';\nimport type { EditorView } from 'prosemirror-view';\nimport type { Selection } from 'prosemirror-state';\nimport { getCellSelectionClass, getResolvedSelection, setAttrs } from '../util';\n\nfunction getColspanEndIdx(rowIdx: number, colIdx: number, map: TableOffsetMap) {\n  let endColIdx = colIdx;\n\n  if (!map.extendedRowspan(rowIdx, colIdx) && map.extendedColspan(rowIdx, colIdx)) {\n    const { startSpanIdx, count } = map.getColspanStartInfo(rowIdx, colIdx)!;\n\n    endColIdx = startSpanIdx + count;\n  }\n  return endColIdx;\n}\n\nfunction judgeInsertToNextRow(\n  map: TableOffsetMap,\n  mappedPos: number,\n  rowIdx: number,\n  colIdx: number\n) {\n  const { totalColumnCount } = map;\n\n  return (\n    map.extendedRowspan(rowIdx, colIdx) &&\n    map.extendedRowspan(rowIdx, totalColumnCount - 1) &&\n    mappedPos === map.posAt(rowIdx, totalColumnCount - 1)\n  );\n}\n\nexport function createSplitCellsCommand(context: PluginContext, OffsetMap: TableOffsetMapFactory) {\n  const splitCells: CommandFn = (_, state, dispatch, view) => {\n    const { selection, tr } = state;\n    const { anchor, head } = getResolvedSelection(selection, context);\n\n    if (!anchor || !head) {\n      return false;\n    }\n\n    const map = OffsetMap.create(anchor)!;\n    const selectionInfo = map.getRectOffsets(anchor, head);\n    const { startRowIdx, startColIdx, endRowIdx, endColIdx } = selectionInfo;\n\n    let lastCellPos = -1;\n\n    for (let rowIdx = startRowIdx; rowIdx <= endRowIdx; rowIdx += 1) {\n      for (let colIdx = startColIdx; colIdx <= endColIdx; colIdx += 1) {\n        if (map.extendedRowspan(rowIdx, colIdx) || map.extendedColspan(rowIdx, colIdx)) {\n          // insert empty cell in spanning cell position\n          const { node } = map.getNodeAndPos(rowIdx, colIdx);\n          const colspanEndIdx = getColspanEndIdx(rowIdx, colIdx, map);\n          const mappedPos = map.posAt(rowIdx, colspanEndIdx);\n\n          let pos = tr.mapping.map(mappedPos);\n\n          // add 2(tr end, open tag length) to insert the cell on the next row\n          // in case that all next cells are spanning on the current row\n          if (judgeInsertToNextRow(map, mappedPos, rowIdx, colspanEndIdx)) {\n            pos += 2;\n          }\n\n          // get the last cell position for cell selection after splitting cells\n          lastCellPos = Math.max(pos, lastCellPos);\n\n          tr.insert(\n            pos,\n            node.type.createAndFill(setAttrs(node, { colspan: null, rowspan: null }))!\n          );\n        } else {\n          // remove colspan, rowspan of the root spanning cell\n          const { node, pos } = map.getNodeAndPos(rowIdx, colIdx);\n\n          // get the last cell position for cell selection after splitting cells\n          lastCellPos = Math.max(tr.mapping.map(pos), lastCellPos);\n\n          tr.setNodeMarkup(\n            tr.mapping.map(pos),\n            null,\n            setAttrs(node, { colspan: null, rowspan: null })\n          );\n        }\n      }\n    }\n    dispatch!(tr);\n    setCellSelection(view, selection, OffsetMap, map.tableStartOffset, selectionInfo);\n\n    return true;\n  };\n\n  return splitCells;\n}\n\nfunction setCellSelection(\n  view: EditorView,\n  selection: Selection,\n  OffsetMap: TableOffsetMapFactory,\n  tableStartPos: number,\n  selectionInfo: SelectionInfo\n) {\n  // @ts-ignore\n  // judge cell selection\n  if (selection.isCellSelection) {\n    const { tr } = view.state;\n    const CellSelection = getCellSelectionClass(selection);\n    const { startRowIdx, startColIdx, endRowIdx, endColIdx } = selectionInfo;\n\n    // get changed cell offsets\n    const map = OffsetMap.create(tr.doc.resolve(tableStartPos))!;\n    const { offset: startOffset } = map.getCellInfo(startRowIdx, startColIdx);\n    const { offset: endOffset } = map.getCellInfo(endRowIdx, endColIdx);\n\n    tr.setSelection(new CellSelection(tr.doc.resolve(startOffset), tr.doc.resolve(endOffset)));\n    view.dispatch(tr);\n  }\n}\n"
  },
  {
    "path": "plugins/table-merged-cell/src/wysiwyg/commandFactory.ts",
    "content": "import type { PluginContext } from '@toast-ui/editor';\nimport type { TableOffsetMapFactory } from '@t/index';\nimport { createMergeCellsCommand } from './command/mergeCells';\nimport { createSplitCellsCommand } from './command/splitCells';\nimport { createRemoveColumnCommand } from './command/removeColumn';\nimport { createRemoveRowCommand } from './command/removeRow';\nimport { createAddRowCommand } from './command/addRow';\nimport { createAddColumnCommand } from './command/addColumn';\nimport { Direction } from './command/direction';\n\nexport function createCommands(context: PluginContext, OffsetMap: TableOffsetMapFactory) {\n  return {\n    mergeCells: createMergeCellsCommand(context, OffsetMap),\n    splitCells: createSplitCellsCommand(context, OffsetMap),\n    addRowToUp: createAddRowCommand(context, OffsetMap, Direction.UP),\n    addRowToDown: createAddRowCommand(context, OffsetMap, Direction.DOWN),\n    removeRow: createRemoveRowCommand(context, OffsetMap),\n    addColumnToLeft: createAddColumnCommand(context, OffsetMap, Direction.LEFT),\n    addColumnToRight: createAddColumnCommand(context, OffsetMap, Direction.RIGHT),\n    removeColumn: createRemoveColumnCommand(context, OffsetMap),\n  };\n}\n"
  },
  {
    "path": "plugins/table-merged-cell/src/wysiwyg/contextMenu.ts",
    "content": "import type { PluginContext } from '@toast-ui/editor';\nimport toArray from 'tui-code-snippet/collection/toArray';\n\nconst TABLE_CELL_SELECT_CLASS = '.toastui-editor-cell-selected';\n\nfunction hasSpanAttr(tableCell: Element) {\n  return (\n    Number(tableCell.getAttribute('colspan')) > 1 || Number(tableCell.getAttribute('rowspan')) > 1\n  );\n}\n\nfunction hasSpanningCell(headOrBody: Element) {\n  return toArray(headOrBody.querySelectorAll(TABLE_CELL_SELECT_CLASS)).some(hasSpanAttr);\n}\n\nfunction isCellSelected(headOrBody: Element) {\n  return !!headOrBody.querySelectorAll(TABLE_CELL_SELECT_CLASS).length;\n}\n\nfunction createMergedTableContextMenu(context: PluginContext, tableCell: Element) {\n  const { i18n, eventEmitter } = context;\n  const headOrBody = tableCell.parentElement!.parentElement!;\n  const mergedTableContextMenu = [];\n\n  if (isCellSelected(headOrBody)) {\n    mergedTableContextMenu.push({\n      label: i18n.get('Merge cells'),\n      onClick: () => eventEmitter.emit('command', 'mergeCells'),\n      className: 'merge-cells',\n    });\n  }\n\n  if (hasSpanAttr(tableCell) || hasSpanningCell(headOrBody)) {\n    mergedTableContextMenu.push({\n      label: i18n.get('Split cells'),\n      onClick: () => eventEmitter.emit('command', 'splitCells'),\n      className: 'split-cells',\n    });\n  }\n\n  return mergedTableContextMenu;\n}\n\nexport function addMergedTableContextMenu(context: PluginContext) {\n  context.eventEmitter.listen('contextmenu', (...args) => {\n    const [{ menuGroups, tableCell }] = args;\n    const mergedTableContextMenu = createMergedTableContextMenu(context, tableCell);\n\n    if (mergedTableContextMenu.length) {\n      // add merged table context menu on third group\n      menuGroups.splice(2, 0, mergedTableContextMenu);\n    }\n  });\n}\n"
  },
  {
    "path": "plugins/table-merged-cell/src/wysiwyg/renderer.ts",
    "content": "import type { Node as ProsemirrorNode } from 'prosemirror-model';\nimport type { ToMdConvertorMap } from '@toast-ui/editor';\n\ntype ColumnAlign = 'left' | 'right' | 'center';\nconst DELIM_LENGH = 3;\n\nfunction repeat(text: string, count: number) {\n  let result = '';\n\n  for (let i = 0; i < count; i += 1) {\n    result += text;\n  }\n\n  return result;\n}\n\nfunction createTableHeadDelim(textContent: string, columnAlign: ColumnAlign) {\n  let textLen = textContent.length;\n  let leftDelim = '';\n  let rightDelim = '';\n\n  if (columnAlign === 'left') {\n    leftDelim = ':';\n    textLen -= 1;\n  } else if (columnAlign === 'right') {\n    rightDelim = ':';\n    textLen -= 1;\n  } else if (columnAlign === 'center') {\n    leftDelim = ':';\n    rightDelim = ':';\n    textLen -= 2;\n  }\n\n  return `${leftDelim}${repeat('-', Math.max(textLen, DELIM_LENGH))}${rightDelim}`;\n}\n\nfunction createDelim(node: ProsemirrorNode) {\n  const { rowspan, colspan } = node.attrs;\n  let spanInfo = '';\n\n  if (rowspan) {\n    spanInfo = `@rows=${rowspan}:`;\n  }\n  if (colspan) {\n    spanInfo = `@cols=${colspan}:${spanInfo}`;\n  }\n\n  return { delim: `| ${spanInfo}` };\n}\n\nexport const toMarkdownRenderers: ToMdConvertorMap = {\n  tableHead(nodeInfo) {\n    const row = (nodeInfo.node as ProsemirrorNode).firstChild;\n\n    let delim = '';\n\n    if (row) {\n      row.forEach(({ textContent, attrs }) => {\n        const headDelim = createTableHeadDelim(textContent, attrs.align);\n\n        delim += `| ${headDelim} `;\n\n        if (attrs.colspan) {\n          for (let i = 0; i < attrs.colspan - 1; i += 1) {\n            delim += `| ${headDelim} `;\n          }\n        }\n      });\n    }\n    return { delim };\n  },\n  tableHeadCell(nodeInfo) {\n    return createDelim(nodeInfo.node as ProsemirrorNode);\n  },\n  tableBodyCell(nodeInfo) {\n    return createDelim(nodeInfo.node as ProsemirrorNode);\n  },\n};\n"
  },
  {
    "path": "plugins/table-merged-cell/src/wysiwyg/tableOffsetMapMixin.ts",
    "content": "import type { RowInfo, SelectionInfo, TableOffsetMap } from '@t/index';\nimport type { Node } from 'prosemirror-model';\n\nexport const offsetMapMixin = {\n  extendedRowspan(rowIdx: number, colIdx: number) {\n    const rowspanInfo = this.rowInfo[rowIdx].rowspanMap[colIdx];\n\n    return !!rowspanInfo && rowspanInfo.startSpanIdx !== rowIdx;\n  },\n  extendedColspan(rowIdx: number, colIdx: number) {\n    const colspanInfo = this.rowInfo[rowIdx].colspanMap[colIdx];\n\n    return !!colspanInfo && colspanInfo.startSpanIdx !== colIdx;\n  },\n  getRowspanCount(rowIdx: number, colIdx: number) {\n    const rowspanInfo = this.rowInfo[rowIdx].rowspanMap[colIdx];\n\n    return rowspanInfo ? rowspanInfo.count : 0;\n  },\n  getColspanCount(rowIdx: number, colIdx: number) {\n    const colspanInfo = this.rowInfo[rowIdx].colspanMap[colIdx];\n\n    return colspanInfo ? colspanInfo.count : 0;\n  },\n  decreaseColspanCount(rowIdx: number, colIdx: number) {\n    const colspanInfo = this.rowInfo[rowIdx].colspanMap[colIdx];\n    const startColspanInfo = this.rowInfo[rowIdx].colspanMap[colspanInfo.startSpanIdx];\n\n    startColspanInfo.count -= 1;\n\n    return startColspanInfo.count;\n  },\n  decreaseRowspanCount(rowIdx: number, colIdx: number) {\n    const rowspanInfo = this.rowInfo[rowIdx].rowspanMap[colIdx];\n    const startRowspanInfo = this.rowInfo[rowspanInfo.startSpanIdx].rowspanMap[colIdx];\n\n    startRowspanInfo.count -= 1;\n\n    return startRowspanInfo.count;\n  },\n  getColspanStartInfo(rowIdx: number, colIdx: number) {\n    const { colspanMap } = this.rowInfo[rowIdx];\n    const colspanInfo = colspanMap[colIdx];\n\n    if (colspanInfo) {\n      const { startSpanIdx } = colspanInfo;\n      const cellInfo = this.rowInfo[rowIdx][startSpanIdx];\n\n      return {\n        node: this.table.nodeAt(cellInfo.offset - this.tableStartOffset)!,\n        pos: cellInfo.offset,\n        startSpanIdx,\n        count: colspanMap[startSpanIdx].count,\n      };\n    }\n    return null;\n  },\n  getRowspanStartInfo(rowIdx: number, colIdx: number) {\n    const { rowspanMap } = this.rowInfo[rowIdx];\n    const rowspanInfo = rowspanMap[colIdx];\n\n    if (rowspanInfo) {\n      const { startSpanIdx } = rowspanInfo;\n      const cellInfo = this.rowInfo[startSpanIdx][colIdx];\n\n      return {\n        node: this.table.nodeAt(cellInfo.offset - this.tableStartOffset)!,\n        pos: cellInfo.offset,\n        startSpanIdx,\n        count: this.rowInfo[startSpanIdx].rowspanMap[colIdx].count,\n      };\n    }\n    return null;\n  },\n  getSpannedOffsets(selectionInfo: SelectionInfo): SelectionInfo {\n    let { startRowIdx, startColIdx, endRowIdx, endColIdx } = selectionInfo;\n\n    for (let rowIdx = endRowIdx; rowIdx >= startRowIdx; rowIdx -= 1) {\n      if (this.rowInfo[rowIdx]) {\n        const { rowspanMap, colspanMap } = this.rowInfo[rowIdx];\n\n        for (let colIdx = endColIdx; colIdx >= startColIdx; colIdx -= 1) {\n          const rowspanInfo = rowspanMap[colIdx];\n          const colspanInfo = colspanMap[colIdx];\n\n          if (rowspanInfo) {\n            startRowIdx = Math.min(startRowIdx, rowspanInfo.startSpanIdx);\n          }\n          if (colspanInfo) {\n            startColIdx = Math.min(startColIdx, colspanInfo.startSpanIdx);\n          }\n        }\n      }\n    }\n\n    for (let rowIdx = startRowIdx; rowIdx <= endRowIdx; rowIdx += 1) {\n      if (this.rowInfo[rowIdx]) {\n        const { rowspanMap, colspanMap } = this.rowInfo[rowIdx];\n\n        for (let colIdx = startColIdx; colIdx <= endColIdx; colIdx += 1) {\n          const rowspanInfo = rowspanMap[colIdx];\n          const colspanInfo = colspanMap[colIdx];\n\n          if (rowspanInfo) {\n            endRowIdx = Math.max(endRowIdx, rowIdx + rowspanInfo.count - 1);\n          }\n          if (colspanInfo) {\n            endColIdx = Math.max(endColIdx, colIdx + colspanInfo.count - 1);\n          }\n        }\n      }\n    }\n\n    return { startRowIdx, startColIdx, endRowIdx, endColIdx };\n  },\n} as TableOffsetMap;\n\nfunction extendPrevRowspan(prevRowInfo: RowInfo, rowInfo: RowInfo) {\n  const { rowspanMap, colspanMap } = rowInfo;\n  const { rowspanMap: prevRowspanMap, colspanMap: prevColspanMap } = prevRowInfo;\n\n  Object.keys(prevRowspanMap).forEach((key) => {\n    const colIdx = Number(key);\n    const prevRowspanInfo = prevRowspanMap[colIdx];\n\n    if (prevRowspanInfo?.count > 1) {\n      const prevColspanInfo = prevColspanMap[colIdx];\n      const { count, startSpanIdx } = prevRowspanInfo;\n\n      rowspanMap[colIdx] = { count: count - 1, startSpanIdx };\n      colspanMap[colIdx] = prevColspanInfo;\n\n      rowInfo[colIdx] = { ...prevRowInfo[colIdx], extended: true };\n      rowInfo.length += 1;\n    }\n  });\n}\n\nfunction extendPrevColspan(\n  rowspan: number,\n  colspan: number,\n  rowIdx: number,\n  colIdx: number,\n  rowInfo: RowInfo\n) {\n  const { rowspanMap, colspanMap } = rowInfo;\n\n  for (let i = 1; i < colspan; i += 1) {\n    colspanMap[colIdx + i] = { count: colspan - i, startSpanIdx: colIdx };\n\n    if (rowspan > 1) {\n      rowspanMap[colIdx + i] = { count: rowspan, startSpanIdx: rowIdx };\n    }\n\n    rowInfo[colIdx + i] = { ...rowInfo[colIdx] };\n    rowInfo.length += 1;\n  }\n}\n\nexport const createOffsetMapMixin = (\n  headOrBody: Node,\n  startOffset: number,\n  startFromBody = false\n) => {\n  const cellInfoMatrix: RowInfo[] = [];\n  const beInBody = headOrBody.type.name === 'tableBody';\n\n  headOrBody.forEach((row: Node, rowOffset: number, rowIdx: number) => {\n    // get row index based on table(not table head or table body)\n    const rowIdxInWholeTable = beInBody && !startFromBody ? rowIdx + 1 : rowIdx;\n    const prevRowInfo = cellInfoMatrix[rowIdx - 1];\n    const rowInfo: RowInfo = { rowspanMap: {}, colspanMap: {}, length: 0 };\n\n    if (prevRowInfo) {\n      extendPrevRowspan(prevRowInfo, rowInfo);\n    }\n\n    row.forEach(({ nodeSize, attrs }: Node, cellOffset: number) => {\n      const colspan: number = attrs.colspan ?? 1;\n      const rowspan: number = attrs.rowspan ?? 1;\n      let colIdx = 0;\n\n      while (rowInfo[colIdx]) {\n        colIdx += 1;\n      }\n\n      rowInfo[colIdx] = {\n        // 2 is the sum of the front and back positions of the tag\n        offset: startOffset + rowOffset + cellOffset + 2,\n        nodeSize,\n      };\n\n      rowInfo.length += 1;\n\n      if (rowspan > 1) {\n        rowInfo.rowspanMap[colIdx] = { count: rowspan, startSpanIdx: rowIdxInWholeTable };\n      }\n\n      if (colspan > 1) {\n        rowInfo.colspanMap[colIdx] = { count: colspan, startSpanIdx: colIdx };\n        extendPrevColspan(rowspan, colspan, rowIdxInWholeTable, colIdx, rowInfo);\n      }\n    });\n    cellInfoMatrix.push(rowInfo);\n  });\n\n  return cellInfoMatrix;\n};\n"
  },
  {
    "path": "plugins/table-merged-cell/src/wysiwyg/util.ts",
    "content": "import type { ResolvedPos, Node, Schema } from 'prosemirror-model';\nimport type { Selection } from 'prosemirror-state';\nimport type { PluginContext } from '@toast-ui/editor';\nimport type { CellSelection, SelectionInfo } from '@t/index';\n\nexport function findNodeBy(pos: ResolvedPos, condition: (node: Node, depth: number) => boolean) {\n  let { depth } = pos;\n\n  while (depth >= 0) {\n    const node = pos.node(depth);\n\n    if (condition(node, depth)) {\n      return {\n        node,\n        depth,\n        offset: depth > 0 ? pos.before(depth) : 0,\n      };\n    }\n\n    depth -= 1;\n  }\n\n  return null;\n}\n\nexport function findCell(pos: ResolvedPos) {\n  return findNodeBy(\n    pos,\n    ({ type }: Node) => type.name === 'tableHeadCell' || type.name === 'tableBodyCell'\n  );\n}\n\nexport function getResolvedSelection(selection: Selection, context: PluginContext) {\n  if (selection instanceof context.pmState.TextSelection) {\n    const { $anchor } = selection;\n    const foundCell = findCell($anchor);\n\n    if (foundCell) {\n      const anchor = $anchor.node(0).resolve($anchor.before(foundCell.depth));\n\n      return { anchor, head: anchor };\n    }\n  }\n\n  const { startCell, endCell } = selection as CellSelection;\n\n  return { anchor: startCell, head: endCell };\n}\n\nexport function getRowAndColumnCount({\n  startRowIdx,\n  startColIdx,\n  endRowIdx,\n  endColIdx,\n}: SelectionInfo) {\n  return { rowCount: endRowIdx - startRowIdx + 1, columnCount: endColIdx - startColIdx + 1 };\n}\n\nexport function setAttrs(cell: Node, attrs: Record<string, any>) {\n  return { ...cell.attrs, ...attrs };\n}\n\nexport function getCellSelectionClass(selection: Selection) {\n  const proto = Object.getPrototypeOf(selection);\n\n  return proto.constructor;\n}\n\nexport function createDummyCells(\n  columnCount: number,\n  rowIdx: number,\n  schema: Schema,\n  attrs: Record<string, any> | null = null\n) {\n  const { tableHeadCell, tableBodyCell, paragraph } = schema.nodes;\n  const cell = rowIdx === 0 ? tableHeadCell : tableBodyCell;\n  const cells = [];\n\n  for (let index = 0; index < columnCount; index += 1) {\n    cells.push(cell.create(attrs, paragraph.create()));\n  }\n\n  return cells;\n}\n"
  },
  {
    "path": "plugins/table-merged-cell/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"src/**/*.ts\", \"src/**/*.js\", \"types/**/*\", \"../../types/**/*\"],\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"importHelpers\": false,\n    \"paths\": {\n      \"@/*\": [\"src/*\"],\n      \"@t/*\": [\"types/*\"]\n    },\n    \"lib\": [\"esnext\", \"dom\", \"dom.iterable\"]\n  }\n}\n"
  },
  {
    "path": "plugins/table-merged-cell/types/index.d.ts",
    "content": "import type {\n  PluginCommandMap,\n  TableMdNode,\n  TableCellMdNode,\n  MdNode,\n  PluginContext,\n  PluginInfo,\n} from '@toast-ui/editor';\nimport type { Selection } from 'prosemirror-state';\nimport type { Node, ResolvedPos } from 'prosemirror-model';\n\ninterface TableBodyMdNode extends MdNode {\n  parent: TableMdNode;\n}\n\ninterface TableHeadMdNode extends MdNode {\n  parent: TableMdNode;\n  firstChild: MergedTableRowMdNode;\n  lastChild: MergedTableRowMdNode;\n  next: TableBodyMdNode;\n}\n\ntype SpanType = '@cols' | '@rows';\n\nexport interface MergedTableRowMdNode extends MdNode {\n  firstChild: MergedTableCellMdNode | null;\n  lastChild: MergedTableCellMdNode | null;\n  parent: TableBodyMdNode | TableHeadMdNode;\n  prev: MergedTableRowMdNode | null;\n  next: MergedTableRowMdNode | null;\n  rowspanMap: { [key: string]: number };\n}\n\nexport interface MergedTableCellMdNode extends TableCellMdNode {\n  prev: MergedTableCellMdNode | null;\n  next: MergedTableCellMdNode | null;\n  parent: MergedTableRowMdNode;\n}\n\nexport interface CellSelection extends Selection {\n  startCell: ResolvedPos;\n  endCell: ResolvedPos;\n  isCellSelection: boolean;\n}\n\ninterface CellInfo {\n  offset: number;\n  nodeSize: number;\n  extended?: boolean;\n}\n\ninterface SelectionInfo {\n  startRowIdx: number;\n  startColIdx: number;\n  endRowIdx: number;\n  endColIdx: number;\n}\n\ninterface SpanMap {\n  [key: number]: { count: number; startSpanIdx: number };\n}\n\nexport interface RowInfo {\n  [key: number]: CellInfo;\n  length: number;\n  rowspanMap: SpanMap;\n  colspanMap: SpanMap;\n}\n\ninterface SpanInfo {\n  node: Node;\n  pos: number;\n  count: number;\n  startSpanIdx: number;\n}\n\nexport interface TableOffsetMapFactory {\n  create(pos: ResolvedPos): TableOffsetMap;\n}\n\nexport interface TableOffsetMap {\n  rowInfo: RowInfo[];\n  table: Node;\n  totalRowCount: number;\n  totalColumnCount: number;\n  tableStartOffset: number;\n  tableEndOffset: number;\n  getCellInfo(rowIdx: number, colIdx: number): CellInfo;\n  posAt(rowIdx: number, colIdx: number): number;\n  getNodeAndPos(rowIdx: number, colIdx: number): { node: Node; pos: number };\n  extendedRowspan(rowIdx: number, colIdx: number): boolean;\n  extendedColspan(rowIdx: number, colIdx: number): boolean;\n  getRowspanCount(rowIdx: number, colIdx: number): number;\n  getColspanCount(rowIdx: number, colIdx: number): number;\n  decreaseColspanCount(rowIdx: number, colIdx: number): number;\n  decreaseRowspanCount(rowIdx: number, colIdx: number): number;\n  getColspanStartInfo(rowIdx: number, colIdx: number): SpanInfo | null;\n  getRowspanStartInfo(rowIdx: number, colIdx: number): SpanInfo | null;\n  getRectOffsets(startCellPos: ResolvedPos, endCellPos?: ResolvedPos): SelectionInfo;\n  getSpannedOffsets(selectionInfo: SelectionInfo): SelectionInfo;\n}\n\nexport type CommandFn = PluginCommandMap[keyof PluginCommandMap];\n\nexport default function tableMergedCellPlugin(context: PluginContext): PluginInfo;\n"
  },
  {
    "path": "plugins/table-merged-cell/types/prosemirror-transform.d.ts",
    "content": "import { Node, Mark } from 'prosemirror-model';\nimport 'prosemirror-transform';\n\ndeclare module 'prosemirror-transform' {\n  export interface Transform {\n    setNodeMarkup(\n      pos: number,\n      type: Node | null,\n      attrs?: { [key: string]: any },\n      marks?: Mark[]\n    ): Transform;\n  }\n}\n"
  },
  {
    "path": "plugins/table-merged-cell/webpack.config.js",
    "content": "/* eslint-disable @typescript-eslint/no-var-requires */\nconst path = require('path');\nconst webpack = require('webpack');\nconst { name, version, author, license } = require('./package.json');\n\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin');\nconst CssMinimizerPlugin = require('css-minimizer-webpack-plugin');\nconst TerserPlugin = require('terser-webpack-plugin');\nconst ESLintPlugin = require('eslint-webpack-plugin');\n\nconst filename = `toastui-${name.replace(/@toast-ui\\//, '')}`;\n\nfunction getOutputConfig(isProduction, isCDN, minify) {\n  const defaultConfig = {\n    environment: {\n      arrowFunction: false,\n      const: false,\n    },\n  };\n\n  if (!isProduction || isCDN) {\n    const config = {\n      ...defaultConfig,\n      library: {\n        name: ['toastui', 'Editor', 'plugin', 'tableMergedCell'],\n        export: 'default',\n        type: 'umd',\n      },\n      path: path.resolve(__dirname, 'dist/cdn'),\n      filename: `${filename}${minify ? '.min' : ''}.js`,\n    };\n\n    if (!isProduction) {\n      config.publicPath = '/dist/cdn';\n    }\n\n    return config;\n  }\n\n  return {\n    ...defaultConfig,\n    library: {\n      export: 'default',\n      type: 'commonjs2',\n    },\n    path: path.resolve(__dirname, 'dist'),\n    filename: `${filename}.js`,\n  };\n}\n\nfunction getOptimizationConfig(isProduction, minify) {\n  const minimizer = [];\n\n  if (isProduction && minify) {\n    minimizer.push(\n      new TerserPlugin({\n        parallel: true,\n        extractComments: false,\n      })\n    );\n    minimizer.push(new CssMinimizerPlugin());\n  }\n\n  return { minimizer };\n}\n\nmodule.exports = (env) => {\n  const isProduction = env.WEBPACK_BUILD;\n  const { minify = false, cdn = false } = env;\n  const config = {\n    mode: isProduction ? 'production' : 'development',\n    entry: './src/index.ts',\n    output: getOutputConfig(isProduction, cdn, minify),\n    module: {\n      rules: [\n        {\n          test: /\\.ts$|\\.js$/,\n          use: [\n            {\n              loader: 'ts-loader',\n              options: {\n                transpileOnly: true,\n              },\n            },\n          ],\n          exclude: /node_modules/,\n        },\n        {\n          test: /\\.css$/,\n          use: [MiniCssExtractPlugin.loader, 'css-loader'],\n        },\n      ],\n    },\n    resolve: {\n      extensions: ['.ts', '.js'],\n      alias: {\n        '@': path.resolve('src'),\n        '@t': path.resolve('types'),\n      },\n    },\n    plugins: [\n      new MiniCssExtractPlugin({\n        filename: () => `${filename}${minify ? '.min' : ''}.css`,\n      }),\n      new ESLintPlugin({\n        extensions: ['js', 'ts'],\n        exclude: ['node_modules', 'dist'],\n        failOnError: isProduction,\n      }),\n    ],\n    optimization: getOptimizationConfig(isProduction, minify),\n  };\n\n  if (isProduction) {\n    config.plugins.push(\n      new webpack.BannerPlugin(\n        [\n          'TOAST UI Editor : Table Merged Cell Plugin',\n          `@version ${version} | ${new Date().toDateString()}`,\n          `@author ${author}`,\n          `@license ${license}`,\n        ].join('\\n')\n      )\n    );\n  } else {\n    config.devServer = {\n      // https://github.com/webpack/webpack-dev-server/issues/2484\n      injectClient: false,\n      inline: true,\n      host: '0.0.0.0',\n      port: 8081,\n    };\n    config.devtool = 'inline-source-map';\n  }\n\n  return config;\n};\n"
  },
  {
    "path": "plugins/uml/README.md",
    "content": "# TOAST UI Editor : UML Plugin\n\n> This is a plugin of [TOAST UI Editor](https://github.com/nhn/tui.editor/tree/master/apps/editor) to render UML.\n\n[![npm version](https://img.shields.io/npm/v/@toast-ui/editor-plugin-uml.svg)](https://www.npmjs.com/package/@toast-ui/editor-plugin-uml)\n\n![uml](https://user-images.githubusercontent.com/37766175/121813437-01fe9b80-cca7-11eb-966b-598333c8ec14.png)\n\n## 🚩 Table of Contents\n\n- [Bundle File Structure](#-bundle-file-structure)\n- [Usage npm](#-usage-npm)\n- [Usage CDN](#-usage-cdn)\n\n## 📁 Bundle File Structure\n\n### Files Distributed on npm\n\n```\n- node_modules/\n  - @toast-ui/\n    - editor-plugin-uml/\n      - dist/\n        - toastui-editor-plugin-uml.js\n```\n\n### Files Distributed on CDN\n\nThe bundle files include all dependencies of this plugin.\n\n```\n- uicdn.toast.com/\n  - editor-plugin-uml/\n    - latest/\n      - toastui-editor-plugin-uml.js\n      - toastui-editor-plugin-uml.min.js\n```\n\n## 📦 Usage npm\n\nTo use the plugin, [`@toast-ui/editor`](https://github.com/nhn/tui.editor/tree/master/apps/editor) must be installed.\n\n> Ref. [Getting Started](https://github.com/nhn/tui.editor/blob/master/docs/en/getting-started.md)\n\n### Install\n\n```sh\n$ npm install @toast-ui/editor-plugin-uml\n```\n\n### Import Plugin\n\n#### ES Modules\n\n```js\nimport uml from '@toast-ui/editor-plugin-uml';\n```\n\n#### CommonJS\n\n```js\nconst uml = require('@toast-ui/editor-plugin-uml');\n```\n\n### Create Instance\n\n#### Basic\n\n```js\nimport Editor from '@toast-ui/editor';\nimport uml from '@toast-ui/editor-plugin-uml';\n\nconst editor = new Editor({\n  // ...\n  plugins: [uml]\n});\n```\n\n#### With Viewer\n\n```js\nimport Viewer from '@toast-ui/editor/dist/toustui-editor-viewer';\nimport uml from '@toast-ui/editor-plugin-uml';\n\nconst viewer = new Viewer({\n  // ...\n  plugins: [uml]\n});\n```\n\nor\n\n```js\nimport Editor from '@toast-ui/editor';\nimport uml from '@toast-ui/editor-plugin-uml';\n\nconst viewer = Editor.factory({\n  // ...\n  plugins: [uml],\n  viewer: true\n});\n```\n\n## 🗂 Usage CDN\n\nTo use the plugin, the CDN files(CSS, Script) of `@toast-ui/editor` must be included.\n\n### Include Files\n\n```html\n...\n<body>\n  ...\n  <!-- Editor -->\n  <script src=\"https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js\"></script>\n  <!-- Editor's Plugin -->\n  <script src=\"https://uicdn.toast.com/editor-plugin-uml/latest/toastui-editor-plugin-uml.min.js\"></script>\n  ...\n</body>\n...\n```\n\n### Create Instance\n\n#### Basic\n\n```js\nconst { Editor } = toastui;\nconst { uml } = Editor.plugin;\n\nconst editor = new Editor({\n  // ...\n  plugins: [uml]\n});\n```\n\n#### With Viewer\n\n```js\nconst Viewer = toastui.Editor;\nconst { uml } = Viewer.plugin;\n\nconst viewer = new Viewer({\n  // ...\n  plugins: [uml]\n});\n```\n\nor\n\n```js\nconst { Editor } = toastui;\nconst { uml } = Editor.plugin;\n\nconst viewer = Editor.factory({\n  // ...\n  plugins: [uml],\n  viewer: true\n});\n```\n\n### [Optional] Use Plugin with Options\n\nThe `uml` plugin can set options when used. Just add the plugin function and options related to the plugin to the array(`[pluginFn, pluginOptions]`) and push them to the `plugins` option of the editor.\n\nThe following option is available in the `uml` plugin.\n\n| Name          | Type     | Default Value                             | Description               |\n| ------------- | -------- | ----------------------------------------- | ------------------------- |\n| `rendererURL` | `string` | `'http://www.plantuml.com/plantuml/png/'` | URL of plant uml renderer |\n\n```js\n// ...\n\nimport Editor from '@toast-ui/editor';\nimport uml from '@toast-ui/editor-plugin-uml';\n\nconst umlOptions = {\n  rendererURL: // ...\n};\n\nconst editor = new Editor({\n  // ...\n  plugins: [[uml, umlOptions]]\n});\n```\n"
  },
  {
    "path": "plugins/uml/demo/editor.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>Editor</title>\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"http://localhost:8080/dist/cdn/toastui-editor.css\" />\n  </head>\n  <body>\n    <div class=\"code-html\">\n      <!-- Editor -->\n      <h2>Editor</h2>\n      <div id=\"editor\"></div>\n      <!-- Editor's Viewer -->\n      <h2>Viewer</h2>\n      <div id=\"viewer\"></div>\n    </div>\n    <!-- Editor -->\n    <script src=\"http://localhost:8080/dist/cdn/toastui-editor-all.js\"></script>\n    <!-- Plugin -->\n    <script src=\"../dist/cdn/toastui-editor-plugin-uml.js\"></script>\n    <script class=\"code-js\">\n      const content = [\n        '$$uml',\n        'partition Conductor {',\n        '  (*) --> \"Climbs on Platform\"',\n        '  --> === S1 ===',\n        '  --> Bows',\n        '}',\n        '',\n        'partition Audience #LightSkyBlue {',\n        '  === S1 === --> Applauds',\n        '}',\n        '',\n        'partition Conductor {',\n        '  Bows --> === S2 ===',\n        '  --> WavesArmes',\n        '  Applauds --> === S2 ===',\n        '}',\n        '',\n        'partition Orchestra #CCCCEE {',\n        '  WavesArmes --> Introduction',\n        '  --> \"Play music\"',\n        '}',\n        '$$'\n      ].join('\\n');\n\n      const { Editor } = toastui;\n      const { uml } = Editor.plugin;\n\n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: content,\n        plugins: [uml]\n      });\n\n      const viewer = Editor.factory({\n        el: document.querySelector('#viewer'),\n        viewer: true,\n        height: '500px',\n        initialValue: content,\n        plugins: [uml]\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "plugins/uml/demo/esm/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>Editor</title>\n  </head>\n  <body>\n    <div class=\"code-html\">\n      <!-- Editor -->\n      <h2>Editor</h2>\n      <div id=\"editor\"></div>\n      <!-- Editor's Viewer -->\n      <h2>Viewer</h2>\n      <div id=\"viewer\"></div>\n    </div>\n    <!-- Editor -->\n    <script type=\"module\">\n      import { Editor } from 'http://localhost:8080/dist/index.js';\n      import umlPlugin from '/dist/index.js';\n\n      const content = [\n        '$$uml',\n        'partition Conductor {',\n        '  (*) --> \"Climbs on Platform\"',\n        '  --> === S1 ===',\n        '  --> Bows',\n        '}',\n        '',\n        'partition Audience #LightSkyBlue {',\n        '  === S1 === --> Applauds',\n        '}',\n        '',\n        'partition Conductor {',\n        '  Bows --> === S2 ===',\n        '  --> WavesArmes',\n        '  Applauds --> === S2 ===',\n        '}',\n        '',\n        'partition Orchestra #CCCCEE {',\n        '  WavesArmes --> Introduction',\n        '  --> \"Play music\"',\n        '}',\n        '$$'\n      ].join('\\n');\n\n      const editor = new Editor({\n        el: document.querySelector('#editor'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: content,\n        plugins: [umlPlugin]\n      });\n\n      const viewer = Editor.factory({\n        el: document.querySelector('#viewer'),\n        viewer: true,\n        height: '500px',\n        initialValue: content,\n        plugins: [umlPlugin]\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "plugins/uml/demo/viewer.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <title>Viewer</title>\n    <!-- Editor -->\n    <link rel=\"stylesheet\" href=\"http://localhost:8080/dist/cdn/toastui-editor-viewer.css\" />\n  </head>\n  <body>\n    <div class=\"code-html\">\n      <div id=\"viewer\"></div>\n    </div>\n    <!-- Editor's Viewer -->\n    <script src=\"http://localhost:8080/dist/cdn/toastui-editor-viewer.js\"></script>\n    <!-- Plugin -->\n    <script src=\"../dist/cdn/toastui-editor-plugin-uml.js\"></script>\n    <script class=\"code-js\">\n      const content = [\n        '$$uml',\n        'partition Conductor {',\n        '  (*) --> \"Climbs on Platform\"',\n        '  --> === S1 ===',\n        '  --> Bows',\n        '}',\n        '',\n        'partition Audience #LightSkyBlue {',\n        '  === S1 === --> Applauds',\n        '}',\n        '',\n        'partition Conductor {',\n        '  Bows --> === S2 ===',\n        '  --> WavesArmes',\n        '  Applauds --> === S2 ===',\n        '}',\n        '',\n        'partition Orchestra #CCCCEE {',\n        '  WavesArmes --> Introduction',\n        '  --> \"Play music\"',\n        '}',\n        '$$'\n      ].join('\\n');\n\n      const Viewer = toastui.Editor;\n      const { uml } = Viewer.plugin;\n\n      const instance = new Viewer({\n        el: document.querySelector('#viewer'),\n        previewStyle: 'vertical',\n        height: '500px',\n        initialValue: content,\n        plugins: [uml]\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "plugins/uml/index.d.ts",
    "content": "import type { PluginContext, PluginInfo } from '@toast-ui/editor';\n\nexport interface PluginOptions {\n  rendererURL?: string;\n}\n\nexport default function umlPlugin(context: PluginContext, options: PluginOptions): PluginInfo;\n"
  },
  {
    "path": "plugins/uml/jest.config.js",
    "content": "// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst base = require('../../jest.base.config');\n\nmodule.exports = {\n  ...base,\n  testEnvironment: 'jsdom',\n  moduleNameMapper: {\n    '^@/(.*)$': '<rootDir>/src/$1',\n  },\n};\n"
  },
  {
    "path": "plugins/uml/package.json",
    "content": "{\n  \"name\": \"@toast-ui/editor-plugin-uml\",\n  \"version\": \"3.0.1\",\n  \"description\": \"TOAST UI Editor : UML Plugin\",\n  \"keywords\": [\n    \"nhn\",\n    \"nhn cloud\",\n    \"toast\",\n    \"toastui\",\n    \"toast-ui\",\n    \"editor\",\n    \"plugin\",\n    \"uml\"\n  ],\n  \"main\": \"dist/toastui-editor-plugin-uml.js\",\n  \"types\": \"index.d.ts\",\n  \"files\": [\n    \"dist/*.js\",\n    \"index.d.ts\"\n  ],\n  \"author\": \"NHN Cloud FE Development Lab <dl_javascript@nhn.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/nhn/tui.editor.git\",\n    \"directory\": \"plugins/uml\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/nhn/tui.editor/issues\"\n  },\n  \"homepage\": \"https://ui.toast.com\",\n  \"browserslist\": \"last 2 versions, not ie <= 10\",\n  \"scripts\": {\n    \"lint\": \"eslint .\",\n    \"test:types\": \"tsc\",\n    \"test\": \"jest --watch\",\n    \"test:ci\": \"jest\",\n    \"serve\": \"snowpack dev\",\n    \"serve:ie\": \"webpack serve\",\n    \"build:cdn\": \"webpack build --env cdn & webpack build --env cdn minify\",\n    \"build\": \"webpack build && npm run build:cdn\"\n  },\n  \"devDependencies\": {\n    \"@types/plantuml-encoder\": \"^1.4.0\",\n    \"cross-env\": \"^6.0.3\"\n  },\n  \"dependencies\": {\n    \"plantuml-encoder\": \"^1.4.0\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  }\n}\n"
  },
  {
    "path": "plugins/uml/snowpack.config.js",
    "content": "// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst httpProxy = require('http-proxy');\nconst proxy = httpProxy.createServer({ target: 'http://localhost:8080' });\n\n/** @type {import(\"snowpack\").SnowpackUserConfig } */\nmodule.exports = {\n  mount: {\n    'demo/esm': '/',\n    src: '/dist',\n  },\n  devOptions: {\n    port: 8081,\n  },\n  routes: [\n    {\n      src: '/img/.*',\n      dest: (req, res) => {\n        proxy.web(req, res);\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "plugins/uml/src/__test__/integration/umlPlugin.spec.ts",
    "content": "/**\n * @fileoverview Test uml plugin\n * @author NHN FE Development Lab <dl_javascript@nhn.com>\n */\nimport Editor from '@toast-ui/editor';\nimport umlPlugin from '@/index';\n\nfunction removeDataAttr(html: string) {\n  return html\n    .replace(/\\sdata-nodeid=\"\\d{1,}\"/g, '')\n    .replace(/\\n/g, '')\n    .trim();\n}\n\ndescribe('uml plugin', () => {\n  let container: HTMLElement, editor: Editor;\n\n  function assertWwEditorHTML(html: string) {\n    const wwEditorEl = editor.getEditorElements().wwEditor;\n\n    expect(wwEditorEl).toContainHTML(html);\n  }\n\n  function assertMdPreviewHTML(html: string) {\n    const mdPreviewEl = editor.getEditorElements().mdPreview;\n\n    expect(removeDataAttr(mdPreviewEl.innerHTML)).toContain(html);\n  }\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    editor = new Editor({\n      el: container,\n      previewStyle: 'vertical',\n      plugins: [umlPlugin],\n    });\n  });\n\n  afterEach(() => {\n    editor.destroy();\n  });\n\n  it('should render plant uml image in markdown preview', () => {\n    const lang = 'uml';\n\n    editor.setMarkdown(`$$${lang}\\nAlice -> Bob: Hello\\n$$`);\n\n    assertMdPreviewHTML('src=\"//www.plantuml.com/plantuml/png');\n  });\n\n  it('should render plant uml image in markdown preview', () => {\n    const lang = 'plantuml';\n\n    editor.setMarkdown(`$$${lang}\\nAlice -> Bob: Hello\\n$$`);\n\n    assertMdPreviewHTML('src=\"//www.plantuml.com/plantuml/png');\n  });\n\n  it('should render uml image in wysiwyg', () => {\n    editor.setMarkdown('$$uml\\nAlice -> Bob: Hello\\n$$');\n    editor.changeMode('wysiwyg');\n\n    assertWwEditorHTML('src=\"//www.plantuml.com/plantuml/png');\n  });\n});\n"
  },
  {
    "path": "plugins/uml/src/index.ts",
    "content": "/**\n * @fileoverview Implements uml plugin\n * @author NHN FE Development Lab <dl_javascript@nhn.com>\n */\nimport plantumlEncoder from 'plantuml-encoder';\nimport { PluginOptions } from '../index';\n\nimport type { MdNode, PluginContext, PluginInfo } from '@toast-ui/editor';\nimport type { HTMLToken } from '@toast-ui/toastmark';\n\nconst DEFAULT_RENDERER_URL = '//www.plantuml.com/plantuml/png/';\n\nfunction createUMLTokens(text: string, rendererURL: string): HTMLToken[] {\n  let renderedHTML;\n\n  try {\n    if (!plantumlEncoder) {\n      throw new Error('plantuml-encoder dependency required');\n    }\n    renderedHTML = `<img src=\"${rendererURL}${plantumlEncoder.encode(text)}\" />`;\n  } catch (err) {\n    renderedHTML = `Error occurred on encoding uml: ${err.message}`;\n  }\n\n  return [\n    { type: 'openTag', tagName: 'div', outerNewLine: true },\n    { type: 'html', content: renderedHTML },\n    { type: 'closeTag', tagName: 'div', outerNewLine: true },\n  ];\n}\n\n/**\n * UML plugin\n * @param {Object} context - plugin context for communicating with editor\n * @param {Object} options - options for plugin\n * @param {string} [options.rendererURL] - url of plant uml renderer\n */\nexport default function umlPlugin(_: PluginContext, options: PluginOptions = {}): PluginInfo {\n  const { rendererURL = DEFAULT_RENDERER_URL } = options;\n\n  return {\n    toHTMLRenderers: {\n      uml(node: MdNode) {\n        return createUMLTokens(node.literal!, rendererURL);\n      },\n      plantUml(node: MdNode) {\n        return createUMLTokens(node.literal!, rendererURL);\n      },\n    },\n  };\n}\n"
  },
  {
    "path": "plugins/uml/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"src/**/*.ts\", \"src/**/*.js\", \"index.d.ts\"],\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"],\n    },\n    \"importHelpers\": false,\n    \"typeRoots\": [\"./types\", \"node_modules/@types\", \"../../node_modules/@types\"],\n    \"lib\": [\"esnext\", \"dom\", \"dom.iterable\"]\n  }\n}"
  },
  {
    "path": "plugins/uml/webpack.config.js",
    "content": "/* eslint-disable @typescript-eslint/no-var-requires */\nconst path = require('path');\nconst webpack = require('webpack');\nconst { name, version, author, license } = require('./package.json');\n\nconst TerserPlugin = require('terser-webpack-plugin');\nconst ESLintPlugin = require('eslint-webpack-plugin');\n\nfunction getOutputConfig(isProduction, isCDN, minify) {\n  const filename = `toastui-${name.replace(/@toast-ui\\//, '')}`;\n  const defaultConfig = {\n    library: {\n      name: ['toastui', 'Editor', 'plugin', 'uml'],\n      export: 'default',\n      type: 'umd',\n    },\n    environment: {\n      arrowFunction: false,\n      const: false,\n    },\n  };\n\n  if (!isProduction || isCDN) {\n    const config = {\n      ...defaultConfig,\n      path: path.resolve(__dirname, 'dist/cdn'),\n      filename: `${filename}${minify ? '.min' : ''}.js`,\n    };\n\n    if (!isProduction) {\n      config.publicPath = '/dist/cdn';\n    }\n\n    return config;\n  }\n\n  return {\n    ...defaultConfig,\n    path: path.resolve(__dirname, 'dist'),\n    filename: `${filename}.js`,\n  };\n}\n\nfunction getExternalsConfig(isProduction, isCDN) {\n  if (isProduction && !isCDN) {\n    return ['plantuml-encoder'];\n  }\n\n  return [];\n}\n\nfunction getOptimizationConfig(isProduction, minify) {\n  const minimizer = [];\n\n  if (isProduction && minify) {\n    minimizer.push(\n      new TerserPlugin({\n        parallel: true,\n        extractComments: false,\n      })\n    );\n  }\n\n  return { minimizer };\n}\n\nmodule.exports = (env) => {\n  const isProduction = env.WEBPACK_BUILD;\n  const { minify = false, cdn = false } = env;\n  const config = {\n    mode: isProduction ? 'production' : 'development',\n    entry: './src/index.ts',\n    output: getOutputConfig(isProduction, cdn, minify),\n    externals: getExternalsConfig(isProduction, cdn),\n    module: {\n      rules: [\n        {\n          test: /\\.ts$|\\.js$/,\n          use: [\n            {\n              loader: 'ts-loader',\n              options: {\n                transpileOnly: true,\n              },\n            },\n          ],\n          exclude: /node_modules/,\n        },\n      ],\n    },\n    resolve: {\n      extensions: ['.ts', '.js'],\n    },\n    optimization: getOptimizationConfig(isProduction, minify),\n  };\n\n  if (isProduction) {\n    config.plugins = [\n      new webpack.BannerPlugin(\n        [\n          'TOAST UI Editor : UML Plugin',\n          `@version ${version} | ${new Date().toDateString()}`,\n          `@author ${author}`,\n          `@license ${license}`,\n        ].join('\\n')\n      ),\n      new ESLintPlugin({\n        extensions: ['js', 'ts'],\n        exclude: ['node_modules', 'dist'],\n        failOnError: isProduction,\n      }),\n    ];\n  } else {\n    config.devServer = {\n      // https://github.com/webpack/webpack-dev-server/issues/2484\n      injectClient: false,\n      inline: true,\n      host: '0.0.0.0',\n      port: 8081,\n    };\n    config.devtool = 'inline-source-map';\n  }\n\n  return config;\n};\n"
  },
  {
    "path": "scripts/pkg-script.js",
    "content": "/* eslint-disable @typescript-eslint/no-var-requires */\nconst { spawn } = require('child_process');\nconst { exit } = require('process');\nconst commandLineArgs = require('command-line-args');\nconst optionDefinitions = [\n  { name: 'type', alias: 't', type: String },\n  { name: 'script', alias: 's', type: String, defaultOption: true },\n];\nconst options = commandLineArgs(optionDefinitions);\n\nconst pkgMap = {\n  editor: '@toast-ui/editor',\n  react: '@toast-ui/react-editor',\n  vue: '@toast-ui/vue-editor',\n  toastmark: '@toast-ui/toastmark',\n  chart: '@toast-ui/editor-plugin-chart',\n  color: '@toast-ui/editor-plugin-color-syntax',\n  code: '@toast-ui/editor-plugin-code-syntax-highlight',\n  table: '@toast-ui/editor-plugin-table-merged-cell',\n  uml: '@toast-ui/editor-plugin-uml',\n};\n\nconst pathMap = {\n  editor: 'apps/editor',\n  react: 'apps/react-editor',\n  vue: 'apps/vue-editor',\n  toastmark: 'libs/toastmark',\n  chart: 'plugins/chart',\n  color: 'plugins/color-syntax',\n  code: 'plugins/code-syntax-highlight',\n  table: 'plugins/table-merged-cell',\n  uml: 'plugins/uml',\n};\n\nlet script;\nlet pkg;\nlet path;\n\nObject.keys(options).forEach((key) => {\n  const value = options[key];\n\n  if (key === 'script') {\n    script = value;\n  }\n\n  if (key === 'type') {\n    pkg = pkgMap[value];\n    path = pathMap[value];\n  }\n});\n\nif (!script) {\n  throw new Error(\n    `You should choose \"lint\", \"test\", \"test:types\", \"serve\", \"serve:ie\", \"build\" as the type of script`\n  );\n}\n\nif (!pkg) {\n  throw new Error(\n    `You should choose \"editor\", \"react\", \"vue\", \"toastmark\", \"chart\", \"color\", \"code\", \"uml\", \"table\"\n    as the configuration of type\n    `\n  );\n}\n\nif (script === 'test') {\n  spawn('jest', ['--watch', '--projects', path], {\n    stdio: 'inherit',\n  }).on('exit', (code) => {\n    exit(code);\n  });\n} else {\n  spawn('lerna', ['run', '--stream', '--scope', pkg, script], {\n    stdio: 'inherit',\n  }).on('exit', (code) => {\n    exit(code);\n  });\n}\n"
  },
  {
    "path": "scripts/publish-cdn.js",
    "content": "/* eslint-disable @typescript-eslint/no-var-requires, no-process-env */\nconst path = require('path');\nconst fs = require('fs');\nconst fetch = require('node-fetch');\nconst pkg = require('../apps/editor/package.json');\n\nconst LOCAL_DIST_PATH = path.join(__dirname, '../apps/editor/dist/cdn');\nconst STORAGE_API_URL = 'https://api-storage.cloud.toast.com/v1';\nconst IDENTITY_API_URL = 'https://api-identity.infrastructure.cloud.toast.com/v2.0';\n\nconst tenantId = process.env.TOAST_CLOUD_TENENTID;\nconst storageId = process.env.TOAST_CLOUD_STORAGEID;\nconst username = process.env.TOAST_CLOUD_USERNAME;\nconst password = process.env.TOAST_CLOUD_PASSWORD;\n\nasync function getTOASTCloudContainer(token) {\n  const response = await fetch(`${STORAGE_API_URL}/${storageId}`, {\n    method: 'GET',\n    headers: {\n      'Content-Type': 'application/json',\n      'X-Auth-Token': token,\n    },\n  });\n  const container = await response.text();\n\n  return `${container.trim()}/editor`;\n}\n\nasync function getTOASTCloudToken() {\n  const data = {\n    auth: {\n      tenantId,\n      passwordCredentials: {\n        username,\n        password,\n      },\n    },\n  };\n\n  const response = await fetch(`${IDENTITY_API_URL}/tokens`, {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify(data),\n  });\n  const result = await response.json();\n\n  return result.access.token.id;\n}\n\nfunction publishToCdn(token, localPath, cdnPath) {\n  const files = fs.readdirSync(localPath);\n\n  files.forEach((fileName) => {\n    const objectPath = `${cdnPath}/${fileName}`;\n\n    if (fileName.match(/(js|css)$/)) {\n      const readStream = fs.createReadStream(`${localPath}/${fileName}`);\n      const contentType = /css$/.test(fileName) ? 'text/css' : 'text/javascript';\n\n      fetch(`${STORAGE_API_URL}/${objectPath}`, {\n        method: 'PUT',\n        headers: {\n          'Content-Type': contentType,\n          'X-Auth-Token': token,\n        },\n        body: readStream,\n      });\n    } else {\n      publishToCdn(token, `${localPath}/${fileName}`, objectPath);\n    }\n  });\n}\n\nasync function publish() {\n  const token = await getTOASTCloudToken();\n  const container = await getTOASTCloudContainer(token);\n  const cdnPath = `${storageId}/${container}`;\n\n  [pkg.version, 'latest'].forEach((dir) => {\n    publishToCdn(token, LOCAL_DIST_PATH, `${cdnPath}/${dir}`);\n  });\n}\n\npublish();\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"skipLibCheck\": true,\n    \"target\": \"es5\",\n    \"module\": \"es6\",\n    \"allowJs\": true,\n    \"strict\": true,\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"importHelpers\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"sourceMap\": true,\n    \"baseUrl\": \".\",\n    \"noEmit\": true,\n  }\n}"
  },
  {
    "path": "types/tui-code-snippet.d.ts",
    "content": "declare module 'tui-code-snippet/type/isFunction' {\n  export default function isFunction(value: unknown): value is Function;\n}\n\ndeclare module 'tui-code-snippet/type/isUndefined' {\n  export default function isUndefined(value: unknown): value is undefined;\n}\n\ndeclare module 'tui-code-snippet/type/isFalsy' {\n  export default function isFalsy(value: unknown): value is false;\n}\n\ndeclare module 'tui-code-snippet/type/isString' {\n  export default function isString(value: unknown): value is string;\n}\n\ndeclare module 'tui-code-snippet/type/isArray' {\n  export default function isArray(value: unknown): value is any[];\n}\n\ndeclare module 'tui-code-snippet/type/isExisty' {\n  export default function isExisty(value: unknown): value is NonNullable<any>;\n}\n\ndeclare module 'tui-code-snippet/type/isNumber' {\n  export default function isNumber(value: unknown): value is number;\n}\n\ndeclare module 'tui-code-snippet/type/isNull' {\n  export default function isNull(value: unknown): value is null;\n}\n\ndeclare module 'tui-code-snippet/type/isObject' {\n  export default function isObject(value: unknown): value is object;\n}\n\ndeclare module 'tui-code-snippet/type/isBoolean' {\n  export default function isBoolean(value: unknown): value is boolean;\n}\n\ndeclare module 'tui-code-snippet/collection/forEachOwnProperties' {\n  export default function forEachOwnProperties<T extends object>(\n    obj: T,\n    iteratee: (value: NonNullable<T[keyof T]>, key: keyof T, targetObj: T) => boolean | void,\n    context?: object\n  ): void;\n}\n\ndeclare module 'tui-code-snippet/collection/forEachArray' {\n  export default function forEachArray<T>(\n    arr: Array<T> | ArrayLike<T>,\n    iteratee: (value: T, index: number, targetArr: Array<T> | ArrayLike<T>) => boolean | void,\n    context?: object\n  ): void;\n}\n\ndeclare module 'tui-code-snippet/collection/toArray' {\n  export default function toArray<T>(value: ArrayLike<T>): T[];\n}\n\ndeclare module 'tui-code-snippet/array/inArray' {\n  export default function inArray<T>(value: T, array: T[], startIndex?: number): number;\n}\n\ndeclare module 'tui-code-snippet/object/extend' {\n  export default function extend<T extends object, K extends object>(target: T, source: K): T & K;\n}\n\ndeclare module 'tui-code-snippet/domUtil/css' {\n  export default function css(\n    element: Element,\n    key: string | Record<string, any>,\n    value?: string\n  ): void;\n}\n\ndeclare module 'tui-code-snippet/domUtil/addClass' {\n  export default function addClass(element: Element, ...classNames: string[]): void;\n}\n\ndeclare module 'tui-code-snippet/domUtil/removeClass' {\n  export default function removeClass(element: Element, ...classNames: string[]): void;\n}\n\ndeclare module 'tui-code-snippet/domUtil/hasClass' {\n  export default function hasClass(element: Element, ...classNames: string[]): boolean;\n}\n\ndeclare module 'tui-code-snippet/domEvent/on' {\n  export default function on(\n    element: Element,\n    types: string,\n    handler: (...args: any[]) => any\n  ): void;\n}\n\ndeclare module 'tui-code-snippet/domEvent/off' {\n  export default function off(\n    element: Element,\n    types: string,\n    handler?: (...args: any[]) => any\n  ): void;\n}\n\ndeclare module 'tui-code-snippet/request/sendHostname' {\n  export default function sendHostname(appName: string, trackingId: string): void;\n}\n\ndeclare module 'tui-code-snippet/domUtil/matches' {\n  export default function matches(element: Element, selector: string): boolean;\n}\n\ndeclare module 'tui-code-snippet/tricks/throttle' {\n  export default function throttle(fn: () => void, interval: number): () => void;\n}\n\ndeclare module 'tui-code-snippet/domUtil/closest' {\n  export default function closest(el: HTMLElement, found: string): HTMLElement | null;\n}\n"
  }
]