├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── public └── index.html ├── scripts ├── rollup.config.js ├── webpack.common.js ├── webpack.config.js └── webpack.dev.js ├── src ├── index.less ├── index.ts ├── packages │ ├── assets │ │ └── icons │ │ │ ├── check │ │ │ └── 2.png │ │ │ ├── copy │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ └── 3.png │ │ │ ├── download │ │ │ └── 2.png │ │ │ ├── image │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ └── 3.png │ │ │ ├── imageEdit │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ └── 3.png │ │ │ ├── image_fail │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ └── 3.png │ │ │ ├── image_light.png │ │ │ ├── image_light_fail.png │ │ │ ├── select │ │ │ └── 2.png │ │ │ └── view │ │ │ └── 2.png │ ├── index.ts │ ├── libs │ │ └── sequence │ │ │ ├── danielbd.woff │ │ │ ├── danielbd.woff2 │ │ │ ├── sequence-diagram-snap.js │ │ │ └── sequence-diagram.less │ ├── modules │ │ ├── clipboard │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── command.ts │ │ ├── content │ │ │ ├── block.ts │ │ │ ├── blockRenderers │ │ │ │ ├── commonMark │ │ │ │ │ ├── atxHeading.ts │ │ │ │ │ ├── blockQuote.ts │ │ │ │ │ ├── bulletList.ts │ │ │ │ │ ├── codeBlock │ │ │ │ │ │ ├── code.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── language.ts │ │ │ │ │ │ └── utlis │ │ │ │ │ │ │ ├── highlight.ts │ │ │ │ │ │ │ └── languages.ts │ │ │ │ │ ├── htmlBlock.ts │ │ │ │ │ ├── listItem.ts │ │ │ │ │ ├── orderList.ts │ │ │ │ │ ├── paragraph │ │ │ │ │ │ ├── backspace.ts │ │ │ │ │ │ ├── enter.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── tab.ts │ │ │ │ │ ├── setextHeading.ts │ │ │ │ │ └── thematicBreak.ts │ │ │ │ ├── document.ts │ │ │ │ ├── eventHandler.ts │ │ │ │ ├── extra │ │ │ │ │ ├── diagramBlock.ts │ │ │ │ │ ├── frontmatter.ts │ │ │ │ │ └── mathBlock.ts │ │ │ │ ├── gfm │ │ │ │ │ ├── table │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── td.ts │ │ │ │ │ │ └── th.ts │ │ │ │ │ └── taskList │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── item.ts │ │ │ │ ├── index.ts │ │ │ │ ├── renderer.ts │ │ │ │ └── utils.ts │ │ │ ├── commands │ │ │ │ ├── format.ts │ │ │ │ ├── index.ts │ │ │ │ └── inline.ts │ │ │ ├── index.ts │ │ │ ├── inlineRenderers │ │ │ │ ├── index.ts │ │ │ │ ├── nodes │ │ │ │ │ ├── autoLink.ts │ │ │ │ │ ├── autoLinkExtension.ts │ │ │ │ │ ├── backlash.ts │ │ │ │ │ ├── codeFense.ts │ │ │ │ │ ├── del.ts │ │ │ │ │ ├── em.ts │ │ │ │ │ ├── emoji.ts │ │ │ │ │ ├── emojiValid.ts │ │ │ │ │ ├── footnoteIdentifier.ts │ │ │ │ │ ├── hardLineBreak.ts │ │ │ │ │ ├── header.ts │ │ │ │ │ ├── hr.ts │ │ │ │ │ ├── htmlBr.ts │ │ │ │ │ ├── htmlComment.ts │ │ │ │ │ ├── htmlEscape.ts │ │ │ │ │ ├── htmlImg.ts │ │ │ │ │ ├── htmlRuby.ts │ │ │ │ │ ├── htmlTag.ts │ │ │ │ │ ├── htmlValidTag.ts │ │ │ │ │ ├── image.ts │ │ │ │ │ ├── inlineCode.ts │ │ │ │ │ ├── inlineMath.ts │ │ │ │ │ ├── link.ts │ │ │ │ │ ├── linkNoText.ts │ │ │ │ │ ├── multipleMath.ts │ │ │ │ │ ├── node.ts │ │ │ │ │ ├── referenceDefinition.ts │ │ │ │ │ ├── referenceImage.ts │ │ │ │ │ ├── referenceLink.ts │ │ │ │ │ ├── softLineBreak.ts │ │ │ │ │ ├── strong.ts │ │ │ │ │ ├── subScript.ts │ │ │ │ │ ├── superScript.ts │ │ │ │ │ └── tailHeader.ts │ │ │ │ └── tokenizer │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rules.ts │ │ │ │ │ └── utils.ts │ │ │ └── utils │ │ │ │ ├── convert.ts │ │ │ │ ├── dom.ts │ │ │ │ ├── find.ts │ │ │ │ ├── image.ts │ │ │ │ └── patch.ts │ │ ├── dragdrop.ts │ │ ├── editable │ │ │ ├── dom.d.ts │ │ │ ├── index.ts │ │ │ ├── merge.ts │ │ │ ├── parser.ts │ │ │ ├── range.ts │ │ │ └── selection.ts │ │ ├── event.ts │ │ ├── i18n │ │ │ ├── index.ts │ │ │ └── locales │ │ │ │ ├── en.js │ │ │ │ ├── index.ts │ │ │ │ └── zh.js │ │ ├── index.ts │ │ ├── layout.ts │ │ ├── module.ts │ │ ├── plugin.ts │ │ ├── search.ts │ │ ├── stack.ts │ │ └── state │ │ │ ├── emptyStates.ts │ │ │ ├── htmlToMarkdown.ts │ │ │ ├── index.ts │ │ │ ├── markdownToState.ts │ │ │ ├── stateToHtml.ts │ │ │ ├── stateToMarkdown.ts │ │ │ ├── stateToPlainText.ts │ │ │ └── utils.ts │ ├── plugins │ │ ├── base.ts │ │ ├── contextMenu │ │ │ ├── index.less │ │ │ └── index.ts │ │ └── index.ts │ ├── styles │ │ ├── editor.less │ │ ├── highlight-code.less │ │ ├── index.less │ │ ├── theme.less │ │ ├── typeset-code.less │ │ └── typeset.less │ ├── types │ │ ├── asset.d.ts │ │ └── index.d.ts │ ├── umd.ts │ └── utils │ │ ├── classNames.ts │ │ ├── convert.ts │ │ ├── diagram │ │ ├── index.ts │ │ ├── plantuml │ │ │ └── index.ts │ │ └── sequence │ │ │ └── index.ts │ │ ├── domUtils.ts │ │ ├── dompurify.ts │ │ ├── dtd.ts │ │ ├── emoji.ts │ │ ├── env.ts │ │ ├── escapeCharacter.ts │ │ ├── keymap.ts │ │ ├── listeners.ts │ │ ├── marked │ │ ├── LICENSE │ │ ├── README.md │ │ ├── blockRules.ts │ │ ├── index.ts │ │ ├── inlineLexer.ts │ │ ├── inlineRules.ts │ │ ├── lexer.ts │ │ ├── options.ts │ │ ├── parser.ts │ │ ├── renderer.ts │ │ ├── slugger.ts │ │ ├── textRenderer.ts │ │ ├── urlify.ts │ │ └── utils.ts │ │ ├── math.ts │ │ ├── nodeTypes.ts │ │ ├── outline.ts │ │ ├── paste.ts │ │ ├── tags.ts │ │ ├── turndownService.ts │ │ ├── url.ts │ │ └── utils.ts └── test │ └── state.test.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 GeekEditor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | **MEditable** is a WYSIWYG markdown editor for web browser applications, derived from the open-source library [Muya](https://github.com/marktext/muya), with most features re-implemented from it, such as visual rendering of markdown text, text search and replacement. What's more, MEditable supports a lot highly efficient features, such as format replacement, dragging images to editor and so on. 3 | 4 | MEditable is still under development as a stand-alone library, providing markdown editing for [GeekEditor](https://www.geekeditor.com). 5 | 6 | ## Installing 7 | 8 | ```sh 9 | npm install @geekeditor/meditable 10 | ``` 11 | 12 | ## Usage 13 | 14 | ```javascript 15 | import MEditable from '@geekeditor/meditable' 16 | 17 | const container = document.querySelector('#editor') 18 | const meditable = new MEditable({container}) 19 | meditable.prepare().then(() => { 20 | meditable.setContent(`**MEditable** a WYSIWYG markdown editor for web browser applications.`) 21 | }); 22 | ``` 23 | 24 | ## Documents 25 | 26 | Coming soon!!! 27 | 28 | ## Development 29 | 30 | ```sh 31 | # step1: install dependencies 32 | npm install 33 | # step2: run the development codes 34 | npm run dev 35 | ``` 36 | 37 | ## Build 38 | 39 | ```sh 40 | npm run build 41 | ``` 42 | 43 | ## Publish 44 | 45 | ```sh 46 | # update version numbers 47 | npm publish 48 | ``` 49 | 50 | ## FAQ 51 | 52 | 53 | None 54 | 55 | ## Built with MEditable 56 | 57 | - [GeekEditor](https://www.geekeditor.com) - A markdown-based note-taker using your Github/Gitee/GitLab's repositories as storage. 58 | 59 | ## License 60 | 61 | MIT © [montisan](https://github.com/montisan) -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | targets: { 7 | node: true, 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | transform: { 6 | "^.+\\.(js|jsx)$": "babel-jest", 7 | }, 8 | moduleNameMapper:{ 9 | '@/(.*)$': '/src/$1' 10 | } 11 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@geekeditor/meditable", 3 | "version": "0.0.4", 4 | "description": "A WYSIWYG markdown editor for web browser applications", 5 | "main": "./dist/cjs/index.js", 6 | "module": "./dist/esm/index.js", 7 | "types": "./dist/types/index.d.ts", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/geekeditor/meditable" 11 | }, 12 | "bugs": "https://github.com/geekeditor/meditable/issues", 13 | "homepage": "https://github.com/geekeditor/meditable#readme", 14 | "scripts": { 15 | "lint": "eslint --ext .js,.ts ./", 16 | "test": "jest", 17 | "dev": "webpack-dev-server --config scripts/webpack.dev.js", 18 | "build": "npm run build:umd && npm run build:rollup", 19 | "build:rollup": "rollup -c scripts/rollup.config.js", 20 | "build:umd": "cross-env NODE_ENV=production webpack --config scripts/webpack.config.js" 21 | }, 22 | "keywords": [], 23 | "sideEffects": false, 24 | "exports": { 25 | ".": { 26 | "types": "./dist/index.d.ts", 27 | "import": "./dist/esm/index.js", 28 | "require": "./dist/cjs/index.js" 29 | }, 30 | "./*": [ 31 | "./*" 32 | ] 33 | }, 34 | "author": "montisan ", 35 | "license": "MIT", 36 | "files": [ 37 | "dist" 38 | ], 39 | "devDependencies": { 40 | "@babel/core": "^7.23.6", 41 | "@babel/preset-env": "^7.23.6", 42 | "@rollup/plugin-alias": "^5.0.1", 43 | "@rollup/plugin-babel": "^6.0.3", 44 | "@rollup/plugin-commonjs": "^25.0.3", 45 | "@rollup/plugin-json": "^6.0.1", 46 | "@rollup/plugin-node-resolve": "^15.1.0", 47 | "@rollup/plugin-replace": "^5.0.2", 48 | "@rollup/plugin-terser": "^0.4.3", 49 | "@types/jest": "^29.5.11", 50 | "@types/node": "^20.4.9", 51 | "ajv": "^8.17.1", 52 | "babel": "^6.23.0", 53 | "babel-jest": "^29.7.0", 54 | "clean-css": "^5.3.2", 55 | "cross-env": "^7.0.3", 56 | "css-loader": "^6.8.1", 57 | "eslint": "^8.46.0", 58 | "html-webpack-plugin": "^5.5.3", 59 | "jest": "^29.7.0", 60 | "less": "^4.2.0", 61 | "less-loader": "^11.1.3", 62 | "mini-css-extract-plugin": "^2.7.6", 63 | "raw-loader": "^4.0.2", 64 | "rollup": "^3.28.0", 65 | "rollup-plugin-dts": "^5.3.1", 66 | "rollup-plugin-flow-no-whitespace": "^1.0.0", 67 | "rollup-plugin-less": "^1.1.3", 68 | "rollup-plugin-node-builtins": "^2.1.2", 69 | "rollup-plugin-node-globals": "^1.4.0", 70 | "rollup-plugin-postcss": "^4.0.2", 71 | "rollup-plugin-typescript2": "^0.35.0", 72 | "style-loader": "^3.3.3", 73 | "ts-jest": "^29.1.1", 74 | "ts-loader": "^9.4.4", 75 | "typescript": "^5.7.2", 76 | "url-loader": "^4.1.1", 77 | "vconsole": "^3.15.1", 78 | "vega": "^5.30.0", 79 | "vega-lite": "^5.23.0", 80 | "webpack": "^5.88.2", 81 | "webpack-cli": "^5.1.4", 82 | "webpack-dev-server": "^4.15.1" 83 | }, 84 | "dependencies": { 85 | "dompurify": "^3.0.6", 86 | "flowchart.js": "^1.17.1", 87 | "highlight.js": "^11.9.0", 88 | "html-tags": "^3.3.1", 89 | "joplin-turndown-plugin-gfm": "^1.0.12", 90 | "js-base64": "^3.7.5", 91 | "js-md5": "^0.8.3", 92 | "katex": "^0.16.9", 93 | "mathjax": "^3.2.2", 94 | "mermaid": "^10.6.0", 95 | "plantuml-encoder": "^1.4.0", 96 | "snapsvg-cjs": "^0.0.6", 97 | "turndown": "^7.1.2", 98 | "underscore": "^1.13.6", 99 | "vega-embed": "^6.23.0", 100 | "viewerjs": "^1.11.7", 101 | "webfontloader": "^1.6.28" 102 | }, 103 | "publishConfig": { 104 | "access": "public", 105 | "registry": "https://registry.npmjs.org" 106 | }, 107 | "env": { 108 | "node": true, 109 | "jest": true 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /scripts/rollup.config.js: -------------------------------------------------------------------------------- 1 | // import { defineConfig } from 'rollup' 2 | // import fs from 'fs' 3 | // import ts from 'rollup-plugin-typescript2' 4 | // import commonjs from '@rollup/plugin-commonjs' 5 | // import babel from '@rollup/plugin-babel' 6 | // import resolve from '@rollup/plugin-node-resolve' 7 | // import globals from 'rollup-plugin-node-globals' 8 | // import builtins from 'rollup-plugin-node-builtins' 9 | // import terser from '@rollup/plugin-terser' 10 | // import dts from 'rollup-plugin-dts' 11 | // import flow from 'rollup-plugin-flow-no-whitespace' 12 | // import less from 'rollup-plugin-less' 13 | // import CleanCss from 'clean-css' 14 | const fs = require('fs') 15 | const path = require('path') 16 | const ts = require('rollup-plugin-typescript2') 17 | const commonjs = require('@rollup/plugin-commonjs') 18 | const babel = require('@rollup/plugin-babel') 19 | const resolve = require('@rollup/plugin-node-resolve') 20 | const globals = require('rollup-plugin-node-globals') 21 | const builtins = require('rollup-plugin-node-builtins') 22 | const terser = require('@rollup/plugin-terser') 23 | const dts = require('rollup-plugin-dts') 24 | const flow = require('rollup-plugin-flow-no-whitespace') 25 | const postcss = require('rollup-plugin-postcss') 26 | const less = require('rollup-plugin-less') 27 | const CleanCss = require('clean-css') 28 | const rollup = require('rollup') 29 | const alias = require('@rollup/plugin-alias') 30 | const json = require('@rollup/plugin-json') 31 | const version = require('../package.json').version 32 | // const version = '' 33 | const getBanner = (name) => { 34 | return '/*!\n' + 35 | ` * ${name} v${version}\n` + 36 | ` * Github: https://github.com/geekeditor/meditable\n` + 37 | ` * (c) 2023-${new Date().getFullYear()} montisan \n` + 38 | ' * Released under the MIT License.\n' + 39 | ' */'; 40 | } 41 | 42 | const banner = getBanner('meditable') 43 | 44 | function pathResolve(dir) { 45 | return path.join(__dirname, dir) 46 | } 47 | 48 | const config = rollup.defineConfig([ 49 | { 50 | input: ['src/packages/index.ts'], 51 | output: [ 52 | { 53 | dir: 'dist/esm', 54 | format: 'esm', 55 | preserveModules: true, 56 | banner 57 | }, 58 | { 59 | dir: 'dist/cjs', 60 | format: 'cjs', 61 | preserveModules: true, 62 | banner 63 | }, 64 | ], 65 | plugins: [ 66 | alias({ 67 | entries: [ 68 | {find: '@', replacement: pathResolve('../src')} 69 | ] 70 | }), 71 | // less({ 72 | // output: function (styles, styleNodes) { 73 | // console.log('sss', styles); 74 | // fs.writeFileSync('dist/meditable.css', banner + '\n' + styles) 75 | // const compressed = new CleanCss().minify(styles).styles; 76 | // fs.writeFileSync('dist/meditable.min.css', banner + '\n' + compressed) 77 | // return false; 78 | // } 79 | // }), 80 | ts(), 81 | babel({ exclude: '**/node_modules/**' }), 82 | commonjs() 83 | ], 84 | }, 85 | // { 86 | // input: 'src/umd.ts', 87 | // output: [ 88 | // { 89 | // file: 'dist/umd/index.js', 90 | // format: 'umd', 91 | // name: 'utils', 92 | // banner 93 | // }, 94 | // ], 95 | // plugins: [ 96 | // alias({ 97 | // entries: [ 98 | // {find: '@', replacement: pathResolve('../src')} 99 | // ] 100 | // }), 101 | // // postcss(), 102 | // less({ 103 | // output: function (styles, styleNodes) { 104 | // console.log('sss', styles); 105 | // fs.writeFileSync('dist/meditable.css', banner + '\n' + styles) 106 | // const compressed = new CleanCss().minify(styles).styles; 107 | // fs.writeFileSync('dist/meditable.min.css', banner + '\n' + compressed) 108 | // return false; 109 | // } 110 | // }), 111 | 112 | // ts(), 113 | // babel({ exclude: '**/node_modules/**' }), 114 | // json(), 115 | // commonjs(), 116 | // resolve({ preferBuiltins: true, mainFields: ['browser'] }), 117 | // globals(), 118 | // builtins(), 119 | // terser(), 120 | // ], 121 | // }, 122 | { 123 | input: 'src/packages/index.ts', 124 | output: { 125 | dir: 'dist/types', 126 | format: 'esm', 127 | preserveModules: true, 128 | banner 129 | }, 130 | plugins: [ 131 | alias({ 132 | entries: [ 133 | {find: '@', replacement: pathResolve('../src')} 134 | ] 135 | }), 136 | dts.default(), 137 | ], 138 | }, 139 | ]) 140 | 141 | module.exports = config -------------------------------------------------------------------------------- /scripts/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 3 | // const ESLintPlugin = require("eslint-webpack-plugin"); 4 | // const NodePolyfillPlugin = require("node-polyfill-webpack-plugin"); 5 | 6 | const proMode = process.env.NODE_ENV === "production"; 7 | 8 | function resolve(dir) { 9 | return path.join(__dirname, dir) 10 | } 11 | exports.default = { 12 | resolve: { 13 | extensions: [".ts", ".tsx", ".js", ".md"], 14 | alias: { 15 | "@": resolve("../src"), 16 | }, 17 | // fallback: { 18 | // stream: false, 19 | // assert: false, 20 | // util: false, 21 | // buffer: false, 22 | // }, 23 | }, 24 | 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.tsx?$/, 29 | use: "ts-loader", 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: [proMode 34 | ? { 35 | loader: MiniCssExtractPlugin.loader, 36 | options: { 37 | } 38 | } 39 | : 'style-loader', "css-loader"], 40 | }, 41 | { 42 | test: /\.less$/, 43 | use: [ proMode 44 | ? { 45 | loader: MiniCssExtractPlugin.loader, 46 | options: { 47 | } 48 | } 49 | : 'style-loader', "css-loader", "less-loader"], 50 | }, 51 | { 52 | // Match woff2 in addition to patterns like .woff?v=1.1.1. 53 | test: /\.(woff|ttf|svg|eot)(\?.*)?$/, 54 | loader: "url-loader", 55 | options: { 56 | // Limit at 50k. Above that it emits separate files 57 | limit: 500000, 58 | 59 | // url-loader sets mimetype if it's passed. 60 | // Without this it derives it from the file extension 61 | mimetype: "application/font-woff", 62 | 63 | // Output below fonts directory 64 | name: "./fonts/[name].[ext]", 65 | }, 66 | }, 67 | { 68 | test: /\.(?:ico|gif|png|jpg|jpeg|)$/i, 69 | type: "asset/resource", 70 | }, 71 | { 72 | test: /\.md$/, 73 | loader: "raw-loader", 74 | }, 75 | ], 76 | }, 77 | plugins: [ 78 | // new NodePolyfillPlugin(), 79 | // new ESLintPlugin({ 80 | // formatter: require("eslint-friendly-formatter"), 81 | // }), 82 | ], 83 | }; 84 | -------------------------------------------------------------------------------- /scripts/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 4 | const pkg = require('../package.json') 5 | const commonConfig = require('./webpack.common') 6 | 7 | const bannerPack = new webpack.BannerPlugin({ 8 | banner: `MEditable v${pkg.version}\n` + 9 | `Github: https://github.com/geekeditor/meditable\n` + 10 | `(c) 2023-${new Date().getFullYear()} montisan \n` + 11 | 'Released under the MIT License.' , 12 | entryOnly: true 13 | }) 14 | 15 | const constantPack = new webpack.DefinePlugin({ 16 | MEDITABLE_VERSION: JSON.stringify(pkg.version) 17 | }) 18 | 19 | const proMode = process.env.NODE_ENV === 'production' 20 | 21 | module.exports = { 22 | ...commonConfig.default, 23 | 24 | mode: proMode ? 'production' : 'development', 25 | 26 | entry: { 27 | meditable: './src/packages/umd.ts' 28 | }, 29 | 30 | output: { 31 | filename: 'umd/meditable.js', 32 | path: path.resolve(__dirname, '../dist'), 33 | assetModuleFilename: 'assets/[hash][ext][query]', 34 | library: { 35 | name: 'MEditable', 36 | type: 'umd' 37 | }, 38 | clean: true 39 | }, 40 | 41 | plugins: [ 42 | bannerPack, 43 | constantPack, 44 | new MiniCssExtractPlugin({ 45 | filename: 'assets/[name].css' 46 | }) 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /scripts/webpack.dev.js: -------------------------------------------------------------------------------- 1 | 2 | const common = require('./webpack.common') 3 | 4 | module.exports = { 5 | ...common.default, 6 | entry: "./src/index.ts", 7 | output: { 8 | filename: 'bundle.js' 9 | }, 10 | mode: "development", 11 | devServer: { 12 | devMiddleware: { 13 | publicPath: "/dist", 14 | }, 15 | }, 16 | plugins: [ 17 | ], 18 | }; -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | font-family: sans-serif; 4 | font-size: 16px; 5 | margin: 0; 6 | box-sizing: border-box; 7 | width: 100%; 8 | height: 100%; 9 | } 10 | .tools { 11 | padding: 0px 5px; 12 | } 13 | #editor { 14 | height: calc(100% - 25px); 15 | } 16 | #search { 17 | width: 200px; 18 | height: 18px; 19 | 20 | border: 1px slid #eee; 21 | border-radius: 4px; 22 | outline: none; 23 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import MEditable from '@/packages/umd' 2 | import {MEPluginContextMenu} from './packages/plugins/' 3 | require("@/index.less") 4 | const README = require('../README.md') 5 | 6 | const editor = document.getElementById('editor') 7 | const searchInput = document.querySelector('#search') as HTMLInputElement 8 | const previousBtn = document.querySelector('#previous') 9 | const nextBtn = document.querySelector('#next') 10 | const replaceInput = document.querySelector('#replace') as HTMLInputElement 11 | const singleBtn = document.querySelector('#single') 12 | const allBtn = document.querySelector('#all') 13 | const clearBtn = document.querySelector('#clear') 14 | 15 | MEditable.use(MEPluginContextMenu) 16 | 17 | const markdown = README.default 18 | const meditable = new MEditable({ container: editor, locale: {lang: navigator.language} }) 19 | meditable.prepare().then(() => { 20 | meditable.setContent(markdown) 21 | }); 22 | (window as any).meditable = meditable; 23 | (window as any).MEditable = MEditable; 24 | 25 | searchInput.addEventListener('input', (event) => { 26 | const searchKey = (event.target as HTMLInputElement).value 27 | if(searchKey) { 28 | meditable.search({ searchKey, all: true, dir: 1 }) 29 | } else { 30 | meditable.searchClear() 31 | } 32 | }) 33 | 34 | previousBtn.addEventListener('click', () => { 35 | meditable.searchJumpPrev() 36 | }) 37 | 38 | nextBtn.addEventListener('click', () => { 39 | meditable.searchJumpNext() 40 | }) 41 | 42 | singleBtn.addEventListener('click', () => { 43 | if(searchInput.value) { 44 | meditable.replace({searchKey: searchInput.value, all: false, dir: 1, replaceKey: replaceInput.value, replace: true, replaceType: 'text' }) 45 | } 46 | }) 47 | 48 | allBtn.addEventListener('click', () => { 49 | if(searchInput.value) { 50 | meditable.replace({searchKey: searchInput.value, all: true, dir: 1, replaceKey: replaceInput.value, replace: true, replaceType: 'text' }) 51 | } 52 | }) 53 | 54 | clearBtn.addEventListener('click', () => { 55 | meditable.searchClear() 56 | }) 57 | 58 | if(/(iPhone|iPad|iPod|Android)/i.test(navigator.userAgent)) { 59 | const VConsole = require('vconsole') 60 | const vConsole = new VConsole() 61 | vConsole.init && vConsole.init() 62 | } -------------------------------------------------------------------------------- /src/packages/assets/icons/check/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/assets/icons/check/2.png -------------------------------------------------------------------------------- /src/packages/assets/icons/copy/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/assets/icons/copy/1.png -------------------------------------------------------------------------------- /src/packages/assets/icons/copy/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/assets/icons/copy/2.png -------------------------------------------------------------------------------- /src/packages/assets/icons/copy/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/assets/icons/copy/3.png -------------------------------------------------------------------------------- /src/packages/assets/icons/download/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/assets/icons/download/2.png -------------------------------------------------------------------------------- /src/packages/assets/icons/image/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/assets/icons/image/1.png -------------------------------------------------------------------------------- /src/packages/assets/icons/image/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/assets/icons/image/2.png -------------------------------------------------------------------------------- /src/packages/assets/icons/image/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/assets/icons/image/3.png -------------------------------------------------------------------------------- /src/packages/assets/icons/imageEdit/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/assets/icons/imageEdit/1.png -------------------------------------------------------------------------------- /src/packages/assets/icons/imageEdit/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/assets/icons/imageEdit/2.png -------------------------------------------------------------------------------- /src/packages/assets/icons/imageEdit/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/assets/icons/imageEdit/3.png -------------------------------------------------------------------------------- /src/packages/assets/icons/image_fail/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/assets/icons/image_fail/1.png -------------------------------------------------------------------------------- /src/packages/assets/icons/image_fail/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/assets/icons/image_fail/2.png -------------------------------------------------------------------------------- /src/packages/assets/icons/image_fail/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/assets/icons/image_fail/3.png -------------------------------------------------------------------------------- /src/packages/assets/icons/image_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/assets/icons/image_light.png -------------------------------------------------------------------------------- /src/packages/assets/icons/image_light_fail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/assets/icons/image_light_fail.png -------------------------------------------------------------------------------- /src/packages/assets/icons/select/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/assets/icons/select/2.png -------------------------------------------------------------------------------- /src/packages/assets/icons/view/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/assets/icons/view/2.png -------------------------------------------------------------------------------- /src/packages/libs/sequence/danielbd.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/libs/sequence/danielbd.woff -------------------------------------------------------------------------------- /src/packages/libs/sequence/danielbd.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/libs/sequence/danielbd.woff2 -------------------------------------------------------------------------------- /src/packages/libs/sequence/sequence-diagram.less: -------------------------------------------------------------------------------- 1 | /** js sequence diagrams 2 | * https://bramp.github.io/js-sequence-diagrams/ 3 | * (c) 2012-2017 Andrew Brampton (bramp.net) 4 | * Simplified BSD license. 5 | */ 6 | @font-face { 7 | font-weight: normal; 8 | font-family: danielbd; 9 | font-style: normal; 10 | src: url('danielbd.woff2') format('woff2'), 11 | url('danielbd.woff') format('woff'); 12 | } 13 | -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/commonMark/atxHeading.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockType } from "@/packages/types"; 2 | import MEParagraphRenderer from "./paragraph"; 3 | import { handleBackspaceInParagraph } from "./paragraph/backspace"; 4 | 5 | export class MEAtxHeading1Renderer extends MEParagraphRenderer { 6 | static type: MEBlockType = "atx-heading1"; 7 | static tagName: string = 'h1'; 8 | 9 | backspaceHandler(event: KeyboardEvent) { 10 | const cursor = this.getCursor(); 11 | if (!cursor) { 12 | return null; 13 | } 14 | 15 | const { start, end } = cursor 16 | if (start.offset === 0 && end.offset === 0) { 17 | event.preventDefault() 18 | handleBackspaceInParagraph.call(this) 19 | } else { 20 | super.backspaceHandler(event) 21 | } 22 | } 23 | } 24 | 25 | export class MEAtxHeading2Renderer extends MEAtxHeading1Renderer { 26 | static type: MEBlockType = "atx-heading2"; 27 | static tagName: string = 'h2'; 28 | } 29 | 30 | export class MEAtxHeading3Renderer extends MEAtxHeading1Renderer { 31 | static type: MEBlockType = "atx-heading3"; 32 | static tagName: string = 'h3'; 33 | } 34 | 35 | export class MEAtxHeading4Renderer extends MEAtxHeading1Renderer { 36 | static type: MEBlockType = "atx-heading4"; 37 | static tagName: string = 'h4'; 38 | } 39 | 40 | export class MEAtxHeading5Renderer extends MEAtxHeading1Renderer { 41 | static type: MEBlockType = "atx-heading5"; 42 | static tagName: string = 'h5'; 43 | } 44 | 45 | export class MEAtxHeading6Renderer extends MEAtxHeading1Renderer { 46 | static type: MEBlockType = "atx-heading6"; 47 | static tagName: string = 'h6'; 48 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/commonMark/blockQuote.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockType } from "@/packages/types"; 2 | import MEBlockRenderer from "../renderer"; 3 | 4 | export default class MEBlockQuoteRenderer extends MEBlockRenderer { 5 | static type: MEBlockType = "block-quote"; 6 | static tagName: string = 'blockquote'; 7 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/commonMark/bulletList.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockRendererRenderOptions, MEBlockType, MECursorState } from "@/packages/types"; 2 | import MEBlockRenderer from "../renderer"; 3 | 4 | export default class MEBulletListRenderer extends MEBlockRenderer { 5 | static type: MEBlockType = "bullet-list"; 6 | static tagName: string = 'ul'; 7 | 8 | render({text, meta, cursor}: MEBlockRendererRenderOptions): boolean { 9 | 10 | const rendered = super.render({text, meta, cursor}); 11 | if(rendered && meta) { 12 | this.nodes.el.dataset.marker = meta.marker; 13 | } 14 | 15 | return rendered; 16 | } 17 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/commonMark/codeBlock/index.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockType } from "@/packages/types"; 2 | import MEBlockRenderer from "../../renderer"; 3 | 4 | export default class MECodeBlockRenderer extends MEBlockRenderer { 5 | static type: MEBlockType = "code-block"; 6 | static tagName: string = 'pre'; 7 | 8 | updateContent() { 9 | super.updateContent() 10 | this.nodes.el.dataset.codeType = this.meta.type; 11 | return true; 12 | } 13 | 14 | forceUpdate() { 15 | const languageRenderer = this.block.firstContentInDescendant().renderer; 16 | const codeRendererer = this.block.lastContentInDescendant().renderer; 17 | codeRendererer.render({meta: {lang: languageRenderer.text}, text: codeRendererer.text}); 18 | if(languageRenderer.text.length) { 19 | this.meta.type = 'fenced'; 20 | this.updateContent(); 21 | } 22 | 23 | } 24 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/commonMark/codeBlock/language.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockType } from "@/packages/types"; 2 | import MEBlockRenderer from "../../renderer"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | 5 | export default class MELanguageRenderer extends MEBlockRenderer { 6 | static type: MEBlockType = "language"; 7 | static tagName: string = 'span'; 8 | 9 | static async staticRender():Promise { 10 | return `` 11 | } 12 | 13 | get anchor() { 14 | return this.block.parent 15 | } 16 | 17 | updateContent() { 18 | if (!this.nodes.el) { 19 | this.nodes.el = this.make(this.tagName, [CLASS_NAMES.ME_EDITABLE, CLASS_NAMES.ME_LANGUAGE], { contenteditable: 'true', hint: this.t("Input code language") }); 20 | this.nodes.holder = this.nodes.el; 21 | } 22 | 23 | this.nodes.holder.textContent = this.text; 24 | 25 | 26 | return true; 27 | } 28 | 29 | forceUpdate() { 30 | const cursor = this.getCursor(); 31 | if (!cursor) { 32 | return null; 33 | } 34 | 35 | const { start, end } = cursor 36 | const textContent = this.nodes.holder.textContent 37 | const lang = textContent.split(/\s+/)[0] 38 | const startOffset = Math.min(lang.length, start.offset) 39 | const endOffset = Math.min(lang.length, end.offset) 40 | this.render({text: lang}) 41 | this.setCursor({anchor: {offset: startOffset}, focus: {offset: endOffset}}) 42 | this.block.parent.renderer.forceUpdate(); 43 | } 44 | 45 | enterHandler(event: KeyboardEvent) { 46 | event.preventDefault() 47 | 48 | this.block.parent.lastContentInDescendant().renderer.setCursor() 49 | 50 | } 51 | 52 | backspaceHandler(event: KeyboardEvent) { 53 | const cursor = this.getCursor(); 54 | if (!cursor) { 55 | return null; 56 | } 57 | 58 | const { start } = cursor 59 | 60 | if (start.offset === 0) { 61 | event.preventDefault() 62 | const cursorBlock = this.block.parent.previousContentInContext() 63 | if(cursorBlock) { 64 | const offset = cursorBlock.renderer.text.length 65 | cursorBlock.renderer.setCursor({focus: {offset}}) 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/commonMark/codeBlock/utlis/highlight.ts: -------------------------------------------------------------------------------- 1 | import {Languages} from "./languages"; 2 | import hljs from 'highlight.js/lib/core'; 3 | 4 | 5 | export const Constant = { 6 | block: 'be-code', 7 | line: 'be-code__line', 8 | text: 'be-code__line-text', 9 | langAttr: 'data-lang', 10 | codeBlockTagName: "pre", 11 | lineTagName: "code", 12 | lineTextTagName: "span", 13 | tabSize: " ", 14 | classPrefix: "be-code__" 15 | }; 16 | 17 | for (const key in Languages) { 18 | if (Object.prototype.hasOwnProperty.call(Languages, key)) { 19 | const lang = Languages[key]; 20 | hljs.registerLanguage(key, lang); 21 | hljs.configure({ 22 | // classPrefix: Constant.classPrefix, 23 | }) 24 | } 25 | } 26 | 27 | export function highlight(lang: string, code: string) { 28 | return lang && Languages[lang] ? hljs.highlight(code, {language: lang}) : hljs.highlightAuto(code); 29 | } 30 | -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/commonMark/codeBlock/utlis/languages.ts: -------------------------------------------------------------------------------- 1 | 2 | import { LanguageFn } from 'highlight.js' 3 | import javascript from 'highlight.js/lib/languages/javascript' 4 | import apache from 'highlight.js/lib/languages/apache' 5 | import bash from 'highlight.js/lib/languages/bash' 6 | import shell from 'highlight.js/lib/languages/shell' 7 | import csharp from 'highlight.js/lib/languages/csharp' 8 | import cpp from 'highlight.js/lib/languages/cpp' 9 | import css from 'highlight.js/lib/languages/css' 10 | import typescript from 'highlight.js/lib/languages/taggerscript' 11 | import diff from 'highlight.js/lib/languages/diff' 12 | import xml from 'highlight.js/lib/languages/xml' 13 | import http from 'highlight.js/lib/languages/http' 14 | import ini from 'highlight.js/lib/languages/ini' 15 | import json from 'highlight.js/lib/languages/json' 16 | import java from 'highlight.js/lib/languages/java' 17 | import makefile from 'highlight.js/lib/languages/makefile' 18 | import markdown from 'highlight.js/lib/languages/markdown' 19 | import nginx from 'highlight.js/lib/languages/nginx' 20 | import objectivec from 'highlight.js/lib/languages/objectivec' 21 | import php from 'highlight.js/lib/languages/php' 22 | import perl from 'highlight.js/lib/languages/perl' 23 | import properties from 'highlight.js/lib/languages/properties' 24 | import python from 'highlight.js/lib/languages/python' 25 | import ruby from 'highlight.js/lib/languages/ruby' 26 | import sql from 'highlight.js/lib/languages/sql' 27 | import powershell from 'highlight.js/lib/languages/powershell' 28 | import swift from 'highlight.js/lib/languages/swift' 29 | import kotlin from 'highlight.js/lib/languages/kotlin' 30 | import go from 'highlight.js/lib/languages/go' 31 | import latex from 'highlight.js/lib/languages/latex' 32 | import yaml from 'highlight.js/lib/languages/yaml' 33 | 34 | export const Languages:{[key:string]:LanguageFn} = { 35 | javascript, 36 | apache, 37 | bash, 38 | shell, 39 | csharp, 40 | cpp, 41 | css, 42 | typescript, 43 | diff, 44 | xml, 45 | http, 46 | ini, 47 | json, 48 | java, 49 | makefile, 50 | markdown, 51 | nginx, 52 | objectivec, 53 | php, 54 | perl, 55 | properties, 56 | python, 57 | ruby, 58 | sql, 59 | powershell, 60 | swift, 61 | kotlin, 62 | go, 63 | latex, 64 | yaml 65 | } 66 | -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/commonMark/htmlBlock.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockRendererStaticRenderOptions, MEBlockType } from "@/packages/types"; 2 | import MEBlockRenderer from "../renderer"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | import { canCopyBlob } from "@/packages/utils/utils"; 5 | 6 | export default class MEHtmlBlockRenderer extends MEBlockRenderer { 7 | static type: MEBlockType = "html-block"; 8 | static tagName: string = 'figure'; 9 | static async staticRender({ data }: MEBlockRendererStaticRenderOptions): Promise { 10 | const innerHTML = data.text 11 | const lang = data.children[0]?.meta?.lang 12 | return `<${this.tagName} class="${this.type} ${lang}">${innerHTML || ''}` 13 | } 14 | 15 | get preview() { 16 | return this.nodes.el.firstElementChild as HTMLElement; 17 | } 18 | 19 | get previewContent() { 20 | return this.preview?.firstElementChild as HTMLElement 21 | } 22 | 23 | get canExportImage() { 24 | return false; 25 | } 26 | 27 | clickPreviewHandler(event) { 28 | event.preventDefault() 29 | event.stopPropagation() 30 | 31 | const { target } = event 32 | if (target.closest(`.${CLASS_NAMES.ME_TOOL}, .${CLASS_NAMES.ME_TOOLBAR}`)) { 33 | return 34 | } 35 | 36 | const cursorBlock = this.block.firstContentInDescendant() 37 | cursorBlock.renderer.setCursor() 38 | } 39 | 40 | // mouseDownPreviewHandler(event) { 41 | // event.preventDefault() 42 | // } 43 | 44 | 45 | updatePreview(html: string) { 46 | if (!this.nodes.el) { 47 | this.nodes.el = this.make(this.tagName); 48 | this.nodes.el.appendChild(this.make('div', [CLASS_NAMES.ME_PREVIEW], { 49 | spellcheck: 'false', 50 | contenteditable: 'false' 51 | })) 52 | this.nodes.holder = this.make('pre', [CLASS_NAMES.ME_CONTAINER]); 53 | this.nodes.el.appendChild(this.nodes.holder); 54 | // this.mutableListeners.on(this.preview, 'mousedown', this.mouseDownPreviewHandler.bind(this)) 55 | this.mutableListeners.on(this.preview, 'click', this.clickPreviewHandler.bind(this)) 56 | 57 | this.preview.appendChild(this.make('div', [CLASS_NAMES.ME_PREVIEW_CONTENT], { 58 | spellcheck: 'false', 59 | contenteditable: 'false' 60 | })) 61 | 62 | if (this.canExportImage) { 63 | const toolbar = this.make('span', [CLASS_NAMES.ME_TOOLBAR]); 64 | this.preview.appendChild(toolbar); 65 | if (canCopyBlob()) { 66 | const copyIcon = this.make('span', [CLASS_NAMES.ME_TOOL, CLASS_NAMES.ME_TOOL__COPY]); 67 | toolbar.appendChild(copyIcon); 68 | this.mutableListeners.on(copyIcon, 'click', this.copyImage.bind(this)) 69 | } 70 | const downloadIcon = this.make('span', [CLASS_NAMES.ME_TOOL, CLASS_NAMES.ME_TOOL__DOWNLOAD]); 71 | toolbar.appendChild(downloadIcon); 72 | this.mutableListeners.on(downloadIcon, 'click', this.downloadImage.bind(this)) 73 | } 74 | } 75 | this.previewContent.innerHTML = html; 76 | } 77 | 78 | updateContent() { 79 | this.updatePreview(this.text); 80 | return true; 81 | } 82 | 83 | forceUpdate() { 84 | const codeRendererer = this.block.lastContentInDescendant().renderer; 85 | const text = codeRendererer.text; 86 | this.render({ text }) 87 | } 88 | 89 | copyImage(event) { 90 | 91 | } 92 | 93 | downloadImage(event) { 94 | 95 | } 96 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/commonMark/listItem.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockType } from "@/packages/types"; 2 | import MEBlockRenderer from "../renderer"; 3 | 4 | export default class MEListItemRenderer extends MEBlockRenderer { 5 | static type: MEBlockType = "list-item"; 6 | static tagName: string = 'li'; 7 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/commonMark/orderList.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockType } from "@/packages/types"; 2 | import MEBlockRenderer from "../renderer"; 3 | 4 | export default class MEOrderListRenderer extends MEBlockRenderer { 5 | static type: MEBlockType = "order-list"; 6 | static tagName: string = 'ol'; 7 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/commonMark/paragraph/backspace.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockRendererInstance } from "@/packages/types" 2 | import { convertIfNeeded } from "../../../utils/convert"; 3 | 4 | export function handleBackspaceInParagraph() { 5 | const renderer = this as MEBlockRendererInstance; 6 | const {block} = renderer; 7 | const previousContentBlock = block.previousContentInContext(); 8 | if (!previousContentBlock) { 9 | 10 | const nextContentBlock = block.nextContentInContext(); 11 | if(nextContentBlock) { 12 | block.remove(); 13 | nextContentBlock.renderer.setCursor(); 14 | return true; 15 | } 16 | 17 | return false; 18 | } 19 | const { text: oldText } = previousContentBlock.renderer; 20 | const offset = oldText.length; 21 | const text = oldText + renderer.text; 22 | const focus = {offset} 23 | previousContentBlock.renderer.render({text, cursor: {anchor: focus, focus, focusBlock: previousContentBlock, anchorBlock: previousContentBlock}}) 24 | renderer.block.remove() 25 | previousContentBlock.renderer.setCursor({focus}) 26 | if(previousContentBlock.type === 'paragraph') { 27 | convertIfNeeded.call(previousContentBlock.renderer, text) 28 | } 29 | return true; 30 | } 31 | 32 | export function handleBackspaceInBlockQuote() { 33 | const renderer = this as MEBlockRendererInstance; 34 | const {block} = renderer; 35 | const blockQuote = block.parent; 36 | 37 | if (!block.isOnlyChild && !block.isFirstChild) { 38 | return handleBackspaceInParagraph.call(renderer) 39 | } 40 | 41 | if (block.isOnlyChild) { 42 | blockQuote?.replaceWith({data: block.data, needToFocus: true}) 43 | } else if (block.isFirstChild) { 44 | const cloneParagraph = block.data 45 | blockQuote?.insertAdjacent("beforebegin", {data: cloneParagraph, needToFocus: true}) 46 | block.remove() 47 | } 48 | 49 | return true; 50 | } 51 | 52 | export function handleBackspaceInList() { 53 | const renderer = this as MEBlockRendererInstance; 54 | const {block} = renderer; 55 | const listItem = block.parent 56 | const list = listItem?.parent 57 | 58 | if (!block.isFirstChild) { 59 | return handleBackspaceInParagraph.call(renderer); 60 | } 61 | 62 | if(!listItem || !list) { 63 | return false; 64 | } 65 | 66 | if (listItem.isOnlyChild) { 67 | listItem.children.forEach((node, i) => { 68 | const paragraph = node.data 69 | list.insertAdjacent("beforebegin", {data: paragraph, needToFocus: i === 0}) 70 | }) 71 | 72 | list.remove() 73 | } else if (listItem.isFirstChild) { 74 | listItem.children.forEach((node, i) => { 75 | const paragraph = node.data 76 | list.insertAdjacent("beforebegin", {data: paragraph, needToFocus: i === 0}) 77 | }) 78 | 79 | listItem.remove() 80 | } else { 81 | const previousListItem = listItem.previous; 82 | listItem.children.forEach((node, i) => { 83 | const paragraph = node.data 84 | previousListItem?.append({data: paragraph, needToFocus: i === 0}) 85 | }) 86 | 87 | listItem.remove() 88 | } 89 | 90 | return true; 91 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/commonMark/setextHeading.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockType } from "@/packages/types"; 2 | import MEParagraphRenderer from "./paragraph"; 3 | 4 | export class MESetextHeading1Renderer extends MEParagraphRenderer { 5 | static type: MEBlockType = "setext-heading1"; 6 | static tagName: string = 'h1'; 7 | } 8 | 9 | export class MESetextHeading2Renderer extends MEParagraphRenderer { 10 | static type: MEBlockType = "setext-heading2"; 11 | static tagName: string = 'h2'; 12 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/commonMark/thematicBreak.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockType } from "@/packages/types"; 2 | import MEParagraphRenderer from "./paragraph"; 3 | 4 | export default class METhematicBreakRenderer extends MEParagraphRenderer { 5 | static type: MEBlockType = "thematic-break"; 6 | static async staticRender():Promise { 7 | return `
` 8 | } 9 | 10 | updateContent(checkUpdate) { 11 | const isFirstRender = !this.nodes.el; 12 | const isRendered = super.updateContent(checkUpdate); 13 | if(isFirstRender) { 14 | this.nodes.el.appendChild(this.make('hr')) 15 | } 16 | 17 | return isRendered; 18 | } 19 | 20 | mouseDownHandler(event: Event) { 21 | if(event.target === this.nodes.el || (event.target as Element).tagName === 'HR') { 22 | event.preventDefault(); 23 | this.setCursor({focus: {offset: this.text.length}}) 24 | } else { 25 | super.clickHandler(event); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/document.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockData, MEBlockType } from "@/packages/types"; 2 | import MEBlockRenderer from "./renderer"; 3 | import { generateId } from "@/packages/utils/utils"; 4 | 5 | export default class MEDocumentRenderer extends MEBlockRenderer { 6 | static type: MEBlockType = "document"; 7 | render(): boolean { 8 | if (!this.nodes.el) { 9 | const { layout } = this.instance.context; 10 | this.nodes.el = layout.nodes.content; 11 | this.nodes.holder = this.nodes.el; 12 | } 13 | return true; 14 | } 15 | 16 | clickHandler(event) { 17 | 18 | const lastChild = this.block.lastChild 19 | const lastContentBlock = lastChild.lastContentInDescendant() 20 | if (lastChild.type === 'paragraph' && lastContentBlock.renderer.text === '') { 21 | lastContentBlock.renderer.setCursor() 22 | } else { 23 | const data: MEBlockData = { 24 | id: generateId(), 25 | type: 'paragraph', 26 | text: '' 27 | } 28 | 29 | this.block.append({ 30 | data, 31 | needToFocus: true, 32 | focus: { offset: 0 } 33 | }) 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/eventHandler.ts: -------------------------------------------------------------------------------- 1 | import domUtils from "@/packages/utils/domUtils"; 2 | import MEModule from "../../module"; 3 | 4 | export default class MEEventHandler extends MEModule<{ el: HTMLElement; holder: HTMLElement }> { 5 | 6 | 7 | isComposing: boolean = false; 8 | 9 | backspaceHandler(event: KeyboardEvent) {} 10 | deleteHandler(event: KeyboardEvent) {} 11 | enterHandler(event: KeyboardEvent) {} 12 | arrowHandler(event: KeyboardEvent) {} 13 | tabHandler(event: KeyboardEvent) {} 14 | 15 | mouseDownHandler(event: Event) {} 16 | clickHandler(event: Event) {} 17 | keydownHandler(event: KeyboardEvent) { 18 | switch (event.key) { 19 | case domUtils.keys.Backspace: 20 | this.backspaceHandler(event); 21 | break; 22 | 23 | case domUtils.keys.Delete: 24 | this.deleteHandler(event); 25 | break; 26 | 27 | case domUtils.keys.Enter: 28 | if (!this.isComposing) { 29 | this.enterHandler(event); 30 | } 31 | break; 32 | 33 | case domUtils.keys.ArrowUp: 34 | case domUtils.keys.ArrowDown: 35 | case domUtils.keys.ArrowLeft: 36 | case domUtils.keys.ArrowRight: 37 | if (!this.isComposing) { 38 | this.arrowHandler(event); 39 | } 40 | break; 41 | 42 | case domUtils.keys.Tab: 43 | this.tabHandler(event); 44 | break; 45 | default: 46 | break; 47 | } 48 | } 49 | keyupHandler(event: KeyboardEvent) {} 50 | composeHandler(event: CompositionEvent) { 51 | this.isComposing = event.type === 'compositionstart'; 52 | if (event.type === 'compositionend') { 53 | this.forceUpdate(event); 54 | } 55 | } 56 | inputHandler(event: InputEvent) { 57 | if (this.isComposing || event.isComposing || /historyUndo|historyRedo/.test(event.inputType)) { 58 | return; 59 | } 60 | this.forceUpdate(event); 61 | } 62 | forceUpdate(event?: InputEvent|CompositionEvent) { 63 | throw (new Error('Inherit in subclasses')) 64 | } 65 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/extra/diagramBlock.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockRendererStaticRenderOptions, MEBlockType } from "@/packages/types"; 2 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 3 | import MEHtmlBlockRenderer from "../commonMark/htmlBlock"; 4 | import renderDiagram from "@/packages/utils/diagram"; 5 | import { md5 } from "js-md5"; 6 | import { svgToBlob, svgToDataURI } from "@/packages/utils/convert"; 7 | import { copyBlob, saveToDisk } from "@/packages/utils/utils"; 8 | 9 | export default class MEDiagramBlockRenderer extends MEHtmlBlockRenderer { 10 | static type: MEBlockType = "diagram-block"; 11 | static htmlMap: Map = new Map(); 12 | static async staticRender({ data, diagramHtmlType }: MEBlockRendererStaticRenderOptions): Promise { 13 | const code = data.text; 14 | const type = data.meta?.type; 15 | const key = md5(`${type}_${data.text}`); 16 | let innerHTML = ""; 17 | if (this.htmlMap.has(key)) { 18 | innerHTML = this.htmlMap.get(key); 19 | } else { 20 | try { 21 | innerHTML = await renderDiagram({ type, code, theme: 'hand' }) 22 | } catch (error) { 23 | 24 | } 25 | } 26 | 27 | innerHTML = innerHTML && diagramHtmlType === 'img' ? `` : innerHTML || '' 28 | return `<${this.tagName} class="${this.type} ${type}">${innerHTML}` 29 | } 30 | 31 | get type() { 32 | return this.meta.type; 33 | } 34 | 35 | get canExportImage() { 36 | return true; 37 | } 38 | 39 | updateContent() { 40 | const htmlMap = MEDiagramBlockRenderer.htmlMap; 41 | const code = this.text; 42 | const key = md5(`${this.type}_${this.text}`); 43 | let diagramHtml; 44 | 45 | if (code) { 46 | let renderErr = false; 47 | if (htmlMap.has(key)) { 48 | diagramHtml = htmlMap.get(key); 49 | } else { 50 | 51 | (async () => { 52 | try { 53 | diagramHtml = this.t("Loading..."); 54 | this.updatePreview(diagramHtml); 55 | const html = await renderDiagram({ type: this.type, code, target: this.previewContent, theme: 'hand' }); 56 | htmlMap.set(key, html) 57 | 58 | // htmlMap.set(key, diagramHtml); 59 | } catch (err) { 60 | renderErr = true; 61 | diagramHtml = this.t("Invalid Diagram Code"); 62 | } 63 | })() 64 | 65 | } 66 | } else { 67 | diagramHtml = this.t("Empty Diagram") 68 | } 69 | 70 | 71 | this.updatePreview(diagramHtml) 72 | this.nodes.holder.dataset.role = this.type; 73 | return true; 74 | } 75 | 76 | async getSVG() { 77 | const htmlMap = MEDiagramBlockRenderer.htmlMap; 78 | const key = md5(`${this.type}_${this.text}`); 79 | const type = this.type; 80 | const code = this.text; 81 | let svg = ""; 82 | if (htmlMap.has(key)) { 83 | svg = htmlMap.get(key); 84 | } else { 85 | try { 86 | svg = await renderDiagram({ type, code, theme: 'hand' }) 87 | } catch (error) { 88 | 89 | } 90 | } 91 | return svg; 92 | } 93 | 94 | async copyImage(event) { 95 | const svg = await this.getSVG(); 96 | if (svg) { 97 | svgToBlob(svg, 4).then((blob) => { 98 | if (blob) { 99 | copyBlob(blob).then(() => { 100 | event.target.classList.toggle(CLASS_NAMES.ME_TOOL__SUCCESS, true) 101 | setTimeout(() => { 102 | event.target.classList.toggle(CLASS_NAMES.ME_TOOL__SUCCESS, false) 103 | }, 1000) 104 | }); 105 | } 106 | }); 107 | } 108 | } 109 | 110 | async downloadImage() { 111 | const svg = await this.getSVG(); 112 | if (svg) { 113 | svgToBlob(svg, 4).then((blob) => { 114 | if (blob) { 115 | saveToDisk("diagram-" + Date.now() + ".png", blob); 116 | } 117 | }); 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/extra/frontmatter.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockType } from "@/packages/types"; 2 | import MEBlockRenderer from "../renderer"; 3 | 4 | export default class MEFrontmatterRenderer extends MEBlockRenderer { 5 | static type: MEBlockType = "frontmatter"; 6 | static tagName: string = 'pre'; 7 | 8 | forceUpdate() { 9 | const codeRendererer = this.block.lastContentInDescendant().renderer; 10 | const text = codeRendererer.text; 11 | this.render({text}) 12 | } 13 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/extra/mathBlock.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockRendererStaticRenderOptions, MEBlockType } from "@/packages/types"; 2 | import katex from 'katex'; 3 | import MEHtmlBlockRenderer from "../commonMark/htmlBlock"; 4 | import { md5 } from "js-md5"; 5 | import { tex2svgPromise } from "@/packages/utils/math"; 6 | import { svgToBlob } from "@/packages/utils/convert"; 7 | import { copyBlob, saveToDisk } from "@/packages/utils/utils"; 8 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 9 | export default class MEMathBlockRenderer extends MEHtmlBlockRenderer { 10 | static type: MEBlockType = "math-block"; 11 | static mathMap: Map = new Map(); 12 | static mathMap2: Map = new Map(); 13 | static async staticRender({data}:MEBlockRendererStaticRenderOptions):Promise { 14 | const math = data.text; 15 | const key = md5(math); 16 | let mathHtml = ""; 17 | if(this.mathMap2.has(key)) { 18 | mathHtml = this.mathMap2.get(key); 19 | } else { 20 | try { 21 | mathHtml = await tex2svgPromise(math); 22 | this.mathMap2.set(math, mathHtml) 23 | } catch (error) { 24 | 25 | } 26 | } 27 | return `<${this.tagName} class="${this.type}">${mathHtml||''}` 28 | } 29 | 30 | get canExportImage() { 31 | return true; 32 | } 33 | 34 | updateContent() { 35 | const mathMap = MEMathBlockRenderer.mathMap; 36 | const math = this.text; 37 | const key = md5(math); 38 | let mathHtml; 39 | let renderErr = false; 40 | if (mathMap.has(key)) { 41 | mathHtml = mathMap.get(key); 42 | } else { 43 | try { 44 | mathHtml = katex.renderToString(math, { 45 | displayMode: false, 46 | }); 47 | mathMap.set(key, mathHtml); 48 | } catch (err) { 49 | renderErr = true; 50 | mathHtml = "Invalid Mathematical Formula"; 51 | } 52 | } 53 | 54 | this.updatePreview(mathHtml) 55 | return true; 56 | } 57 | 58 | async getSVG() { 59 | const math = this.text; 60 | let svg = ""; 61 | try { 62 | svg = await tex2svgPromise(math); 63 | } catch (error) { 64 | 65 | } 66 | 67 | return svg; 68 | } 69 | 70 | async copyImage(event) { 71 | const svg = await this.getSVG(); 72 | if (svg) { 73 | svgToBlob(svg).then((blob) => { 74 | if (blob) { 75 | copyBlob(blob).then(() => { 76 | event.target.classList.toggle(CLASS_NAMES.ME_TOOL__SUCCESS, true) 77 | setTimeout(() => { 78 | event.target.classList.toggle(CLASS_NAMES.ME_TOOL__SUCCESS, false) 79 | }, 1000) 80 | }); 81 | ; 82 | } 83 | }); 84 | } 85 | } 86 | 87 | async downloadImage() { 88 | const svg = await this.getSVG(); 89 | if (svg) { 90 | svgToBlob(svg).then((blob) => { 91 | if (blob) { 92 | saveToDisk("math-" + Date.now() + ".png", blob); 93 | } 94 | }); 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/gfm/table/th.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockData, MEBlockType } from "@/packages/types"; 2 | import METableTdRenderer from "./td"; 3 | import { generateId } from "@/packages/utils/utils"; 4 | 5 | export default class METableThRenderer extends METableTdRenderer { 6 | static type: MEBlockType = "table-th"; 7 | static tagName: string = 'th'; 8 | 9 | get rowOffset() { 10 | return this.row.index 11 | } 12 | 13 | findNextRow() { 14 | const { row } = this 15 | return row.next || row.parent.next?.firstChild 16 | } 17 | 18 | findPreviousRow() { 19 | const { row } = this 20 | return row.previous || null 21 | } 22 | 23 | commandEnter(event) { 24 | const { table } = this 25 | const tableRenderer = table.renderer as any; 26 | 27 | const cell = tableRenderer.insertRow(0) 28 | cell.renderer.setCursor() 29 | } 30 | 31 | commandDelete(event) { 32 | const {table, cell, row} = this 33 | const focusRow = this.findNextRow() 34 | if(focusRow) { 35 | const index = cell.index; 36 | const data: MEBlockData = { 37 | id: generateId(), 38 | type: 'table-tr', 39 | children: focusRow.data.children.map((c)=>{ 40 | return { 41 | ...c, 42 | type: 'table-th' 43 | } 44 | }) 45 | } 46 | 47 | const newRow = row.replaceWith({data}) 48 | const focusBlock = newRow.children[index].firstContentInDescendant() 49 | focusRow.remove() 50 | focusBlock.renderer.setCursor() 51 | } else { 52 | event.preventDefault() 53 | const data: MEBlockData = { 54 | id: generateId(), 55 | type: 'paragraph', 56 | text: '' 57 | } 58 | table.replaceWith({data, needToFocus: true}) 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/gfm/taskList/index.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockType } from "@/packages/types"; 2 | import MEBlockRenderer from "../../renderer"; 3 | 4 | export default class METaskListRenderer extends MEBlockRenderer { 5 | static type: MEBlockType = "task-list"; 6 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/gfm/taskList/item.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockRendererStaticRenderOptions, MEBlockType } from "@/packages/types"; 2 | import MEBlockRenderer from "../../renderer"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | 5 | export default class METaskListItemRenderer extends MEBlockRenderer { 6 | static type: MEBlockType = "task-list-item"; 7 | static async staticRender({data, innerHTML}:MEBlockRendererStaticRenderOptions):Promise { 8 | const checked = !!data.meta.checked 9 | const checkBoxClassName = `${this.type}-checkbox` 10 | const contentClassName = `${this.type}-content` 11 | const checkBoxCheckedClassName = `${this.type}-checkbox-checked` 12 | const classNames = [checkBoxClassName] 13 | if(checked) { 14 | classNames.push(checkBoxCheckedClassName) 15 | } 16 | return `<${this.tagName} class="${this.type}"><${this.tagName} class="${contentClassName}">${innerHTML||''}` 17 | } 18 | 19 | 20 | get checkbox() { 21 | return this.nodes.el.firstElementChild; 22 | } 23 | 24 | updateContent(checkUpdate?: boolean): boolean { 25 | if (!this.nodes.el) { 26 | this.nodes.el = this.make(this.tagName); 27 | this.nodes.el.appendChild(this.make('span', [CLASS_NAMES.ME_TASK_LIST_ITEM_CHECKBOX], { 28 | spellcheck: 'false', 29 | contenteditable: 'false' 30 | })) 31 | this.nodes.holder = this.make(this.tagName, [CLASS_NAMES.ME_TASK_LIST_ITEM_CONTENT]); 32 | this.nodes.el.appendChild(this.nodes.holder); 33 | 34 | this.mutableListeners.on(this.checkbox, 'click', this.clickCheckboxHandler.bind(this)) 35 | } 36 | 37 | this.checkbox.classList.toggle(CLASS_NAMES.ME_TASK_LIST_ITEM_CHECKBOX__CHECKED, !!this.meta.checked); 38 | return true; 39 | } 40 | 41 | clickCheckboxHandler(event) { 42 | event.preventDefault(); 43 | this.meta.checked = !this.meta.checked; 44 | this.checkbox.classList.toggle(CLASS_NAMES.ME_TASK_LIST_ITEM_CHECKBOX__CHECKED, !!this.meta.checked); 45 | } 46 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/index.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockInstance, MEBlockRendererStaticRenderOptions, MEBlockType } from "@/packages/types"; 2 | import MEBlockRenderer from "./renderer"; 3 | import MEDocumentRenderer from "./document"; 4 | import MEParagraphRenderer from "./commonMark/paragraph"; 5 | import { MEAtxHeading1Renderer, MEAtxHeading2Renderer, MEAtxHeading3Renderer, MEAtxHeading4Renderer, MEAtxHeading5Renderer, MEAtxHeading6Renderer } from "./commonMark/atxHeading"; 6 | import { MESetextHeading1Renderer, MESetextHeading2Renderer } from "./commonMark/setextHeading"; 7 | import MEBlockQuoteRenderer from './commonMark/blockQuote'; 8 | import MEBulletListRenderer from "./commonMark/bulletList"; 9 | import MEOrderListRenderer from "./commonMark/orderList"; 10 | import MEListItemRenderer from "./commonMark/listItem"; 11 | import METhematicBreakRenderer from "./commonMark/thematicBreak"; 12 | import MELanguageRenderer from "./commonMark/codeBlock/language"; 13 | import MECodeRenderer from "./commonMark/codeBlock/code"; 14 | import MECodeBlockRenderer from "./commonMark/codeBlock"; 15 | import MEHtmlBlockRenderer from "./commonMark/htmlBlock"; 16 | import MEMathBlockRenderer from "./extra/mathBlock"; 17 | import MEDiagramBlockRenderer from "./extra/diagramBlock"; 18 | import METaskListRenderer from "./gfm/taskList"; 19 | import METaskListItemRenderer from "./gfm/taskList/item"; 20 | import METableRenderer, { METableTbodyRenderer, METableTheadRenderer, METableTrRenderer } from "./gfm/table"; 21 | import METableTdRenderer from "./gfm/table/td"; 22 | import METableThRenderer from "./gfm/table/th"; 23 | import MEFrontmatterRenderer from "./extra/frontmatter"; 24 | 25 | MEBlockRenderer.register(MEDocumentRenderer) 26 | MEBlockRenderer.register(MEParagraphRenderer) 27 | MEBlockRenderer.register(MEAtxHeading1Renderer) 28 | MEBlockRenderer.register(MEAtxHeading2Renderer) 29 | MEBlockRenderer.register(MEAtxHeading3Renderer) 30 | MEBlockRenderer.register(MEAtxHeading4Renderer) 31 | MEBlockRenderer.register(MEAtxHeading5Renderer) 32 | MEBlockRenderer.register(MEAtxHeading6Renderer) 33 | MEBlockRenderer.register(MESetextHeading1Renderer) 34 | MEBlockRenderer.register(MESetextHeading2Renderer) 35 | MEBlockRenderer.register(MEBlockQuoteRenderer) 36 | MEBlockRenderer.register(MEBulletListRenderer) 37 | MEBlockRenderer.register(MEOrderListRenderer) 38 | MEBlockRenderer.register(MEListItemRenderer) 39 | MEBlockRenderer.register(METhematicBreakRenderer) 40 | MEBlockRenderer.register(MELanguageRenderer) 41 | MEBlockRenderer.register(MECodeRenderer) 42 | MEBlockRenderer.register(MECodeBlockRenderer) 43 | MEBlockRenderer.register(MEHtmlBlockRenderer) 44 | MEBlockRenderer.register(MEMathBlockRenderer) 45 | MEBlockRenderer.register(MEDiagramBlockRenderer) 46 | MEBlockRenderer.register(METaskListRenderer) 47 | MEBlockRenderer.register(METaskListItemRenderer) 48 | MEBlockRenderer.register(METableRenderer) 49 | MEBlockRenderer.register(METableTheadRenderer) 50 | MEBlockRenderer.register(METableTbodyRenderer) 51 | MEBlockRenderer.register(METableTrRenderer) 52 | MEBlockRenderer.register(METableTdRenderer) 53 | MEBlockRenderer.register(METableThRenderer) 54 | MEBlockRenderer.register(MEFrontmatterRenderer) 55 | 56 | export function createRenderer(instance: MEBlockInstance, type: MEBlockType) { 57 | const Constructable = MEBlockRenderer.renders[type] || MEBlockRenderer; 58 | return new Constructable(instance); 59 | } 60 | 61 | export async function blockStaticRender(type: MEBlockType, options:MEBlockRendererStaticRenderOptions):Promise { 62 | const Constructable = MEBlockRenderer.renders[type] || MEBlockRenderer 63 | return Constructable.staticRender(options) 64 | } -------------------------------------------------------------------------------- /src/packages/modules/content/blockRenderers/utils.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockInstance } from "@/packages/types"; 2 | import domUtils from "@/packages/utils/domUtils"; 3 | 4 | export function getCursorYOffset(el: HTMLElement, rangeRect: DOMRect) { 5 | 6 | const { y } = rangeRect; 7 | const { height, top } = el.getBoundingClientRect(); 8 | const lineHeight = parseFloat(getComputedStyle(el).lineHeight); 9 | const topOffset = Math.floor((y - top) / lineHeight); 10 | const bottomOffset = Math.round( 11 | (top + height - lineHeight - y) / lineHeight 12 | ); 13 | 14 | return { 15 | topOffset, 16 | bottomOffset, 17 | }; 18 | 19 | } 20 | 21 | export function adjustOffset(offset: number, block: MEBlockInstance, event: KeyboardEvent) { 22 | if ( 23 | block.type.includes("atx-heading") && 24 | event.key === domUtils.keys.ArrowDown 25 | ) { 26 | const match = /^\s{0,3}(?:#{1,6})(?:\s{1,}|$)/.exec(block.renderer.text); 27 | if (match) { 28 | return match[0].length; 29 | } 30 | } else if (block.type === 'thematic-break' && event.key === domUtils.keys.ArrowDown) { 31 | return block.renderer.text.length; 32 | } 33 | 34 | return offset; 35 | }; -------------------------------------------------------------------------------- /src/packages/modules/content/commands/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | bold, 3 | italic, 4 | subscript, 5 | supscript, 6 | underline, 7 | strikethrough, 8 | mark, 9 | code, 10 | math 11 | } from "./inline" 12 | import {format, removeformat} from './format' 13 | 14 | export default { 15 | format, 16 | removeformat, 17 | bold, 18 | italic, 19 | subscript, 20 | supscript, 21 | underline, 22 | strikethrough, 23 | mark, 24 | code, 25 | math 26 | } -------------------------------------------------------------------------------- /src/packages/modules/content/commands/inline.ts: -------------------------------------------------------------------------------- 1 | 2 | // const inlineTags = ['strong', 'b', 'del', 's', 'strike', 'em', 'i','mark','u','code','sup', 'sub', 'math', 'kbd'] 3 | 4 | 5 | export const COMMAND_TYPE_MAP = { 6 | bold: "strong", 7 | italic: "em", 8 | inline_code: "inline_code", 9 | subscript: "sub", 10 | superscript: "sup", 11 | underline: "u", 12 | strikethrough: "del", 13 | mark: "mark", 14 | inline_math: "inline_math", 15 | }; 16 | 17 | const getCommand = (cmdName: string, shortcutKeys?: { [key: string]: any }) => { 18 | return { 19 | cmdName, 20 | execCommand(cmdName: string) { 21 | this.execCommand('format', {type: COMMAND_TYPE_MAP[cmdName]}) 22 | }, 23 | queryCommandState() { 24 | return 0; 25 | }, 26 | shortcutKeys, 27 | } 28 | } 29 | 30 | const bold = getCommand('bold', { 31 | "Ctrl+B": {} 32 | }); 33 | const italic = getCommand('italic', { 34 | "Ctrl+I": {} 35 | }); 36 | const subscript = getCommand('subscript',{ 37 | "Ctrl+Shift+,": {} 38 | }); 39 | const supscript = getCommand('superscript', { 40 | "Ctrl+Shift+.": {} 41 | }); 42 | const underline = getCommand('underline', { 43 | "Ctrl+U": {} 44 | }); 45 | const strikethrough = getCommand('strikethrough', { 46 | "Ctrl+D": {} 47 | }); 48 | const mark = getCommand('mark', { 49 | "Ctrl+M": {} 50 | }); 51 | const code = getCommand('inline_code', { 52 | "Ctrl+E": {} 53 | }); 54 | const math = getCommand('inline_math', { 55 | "Ctrl+Shift+M": {} 56 | }); 57 | 58 | export { 59 | bold, 60 | italic, 61 | subscript, 62 | supscript, 63 | underline, 64 | strikethrough, 65 | mark, 66 | code, 67 | math 68 | }; -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/index.ts: -------------------------------------------------------------------------------- 1 | import MENode from "./nodes/node"; 2 | import MEStrong from "./nodes/strong"; 3 | import MEEm from "./nodes/em"; 4 | import MEHeader from "./nodes/header"; 5 | import { MEBlockRendererInstance, MENodeRendererStaticRenderOptions, MENodeType } from "@/packages/types"; 6 | import MEHr from "./nodes/hr"; 7 | import MECodeFense from "./nodes/codeFense"; 8 | import MEDel from "./nodes/del"; 9 | import MESoftLineBreak from "./nodes/softLineBreak"; 10 | import MEHardLineBreak from "./nodes/hardLineBreak"; 11 | import MESubScript from "./nodes/subScript"; 12 | import MESuperScript from "./nodes/superScript"; 13 | import MEHtmlTag from "./nodes/htmlTag"; 14 | import MEHtmlBr from "./nodes/htmlBr"; 15 | import MEHtmlValidTag from "./nodes/htmlValidTag"; 16 | import MEHtmlRuby from "./nodes/htmlRuby"; 17 | import MEHtmlEscape from "./nodes/htmlEscape"; 18 | import MEEmoji from "./nodes/emoji"; 19 | import MEEmojiValid from "./nodes/emojiValid"; 20 | import MEInlineCode from "./nodes/inlineCode"; 21 | import METailHeader from "./nodes/tailHeader"; 22 | import MEAutoLink from "./nodes/autoLink"; 23 | import MEAutoLinkExtension from "./nodes/autoLinkExtension"; 24 | import MEBacklash from "./nodes/backlash"; 25 | import MELink from "./nodes/link"; 26 | import MELinkNoText from "./nodes/linkNoText"; 27 | import MEHtmlComment from "./nodes/htmlComment"; 28 | import MEFootnoteIdentifier from "./nodes/footnoteIdentifier"; 29 | import MEReferenceDefinition from "./nodes/referenceDefinition"; 30 | import MEReferenceLink from "./nodes/referenceLink"; 31 | import MEReferenceImage from "./nodes/referenceImage"; 32 | import MEHtmlImg from "./nodes/htmlImg"; 33 | import MEImage from "./nodes/image"; 34 | import MEInlineMath from "./nodes/inlineMath"; 35 | import MEMultipleMath from "./nodes/multipleMath"; 36 | 37 | MENode.register(MEStrong) 38 | MENode.register(MEEm) 39 | MENode.register(MEHeader) 40 | MENode.register(METailHeader) 41 | MENode.register(MEHr) 42 | MENode.register(MECodeFense) 43 | MENode.register(MEDel) 44 | MENode.register(MESoftLineBreak) 45 | MENode.register(MEHardLineBreak) 46 | MENode.register(MESubScript) 47 | MENode.register(MESuperScript) 48 | MENode.register(MEHtmlTag) 49 | MENode.register(MEHtmlBr) 50 | MENode.register(MEHtmlValidTag) 51 | MENode.register(MEHtmlComment) 52 | MENode.register(MEHtmlRuby) 53 | MENode.register(MEHtmlEscape) 54 | MENode.register(MEEmoji) 55 | MENode.register(MEEmojiValid) 56 | MENode.register(MEInlineCode) 57 | MENode.register(MEAutoLink) 58 | MENode.register(MEAutoLinkExtension) 59 | MENode.register(MELink) 60 | MENode.register(MELinkNoText) 61 | MENode.register(MEBacklash) 62 | MENode.register(MEFootnoteIdentifier) 63 | MENode.register(MEReferenceDefinition) 64 | MENode.register(MEReferenceLink) 65 | MENode.register(MEReferenceImage) 66 | MENode.register(MEHtmlImg) 67 | MENode.register(MEImage) 68 | MENode.register(MEInlineMath) 69 | MENode.register(MEMultipleMath) 70 | 71 | export function createNode(instance: MEBlockRendererInstance, type: MENodeType) { 72 | const Constructable = MENode.nodes[type] || MENode; 73 | return new Constructable(instance); 74 | } 75 | 76 | export async function nodeStaticRender(type: MENodeType, { innerHTML, data, labels }: MENodeRendererStaticRenderOptions) { 77 | const Constructable = MENode.nodes[type] || MENode 78 | return Constructable.staticRender({ innerHTML, data, labels }) 79 | } 80 | 81 | /* 82 | export function render(el: HTMLElement, hasBeginRules?: boolean) { 83 | const text = el['BLOCK_RENDERER_INSTANCE'].text || getTextContent(el, [CLASS_NAMES.ME_INLINE_RENDER, CLASS_NAMES.ME_INLINE_RENDER]); 84 | const tokens = tokenizer(text, { hasBeginRules }) 85 | 86 | } 87 | */ -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/autoLink.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeRendererStaticRenderOptions, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | import { sanitizeHyperlink } from "@/packages/utils/url"; 5 | 6 | export default class MEAutoLink extends MENode { 7 | static type: MENodeType = "auto_link"; 8 | static tagName: string = "a"; 9 | static async staticRender({data}:MENodeRendererStaticRenderOptions) { 10 | const { isLink, href, email, content } = data; 11 | const hyperlink = sanitizeHyperlink(isLink ? encodeURI(href) : `mailto:${email}`); 12 | return `<${this.tagName} class="${this.type}" target="_blank" href="${hyperlink}">${content}` 13 | } 14 | get dirty() { 15 | const content = this.data?.url || this.data?.email; 16 | if(("<" !== this.nodes.el.firstChild?.textContent || ">" !== this.nodes.el.lastChild?.textContent) || (content !== this.nodes.holder.textContent)){ 17 | return true; 18 | } 19 | return false; 20 | } 21 | renderSelf(data: MENodeData) { 22 | 23 | const { isLink, href, email } = data; 24 | const hyperlink = sanitizeHyperlink(isLink ? encodeURI(href) : `mailto:${email}`); 25 | if(!this.nodes.el) { 26 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE]); 27 | this.nodes.el.innerHTML = `<>` 28 | this.nodes.el.dataset.nodeType = this.type; 29 | 30 | this.nodes.holder = this.make(this.tagName, [CLASS_NAMES.ME_AUTO_LINK], {spellcheck: "false", target: "_blank", href: hyperlink}); 31 | this.nodes.el.insertBefore(this.nodes.holder, this.nodes.el.lastChild); 32 | } else { 33 | const el = this.nodes.holder as HTMLAnchorElement; 34 | el.href = hyperlink; 35 | } 36 | 37 | if("<" !== this.nodes.el.firstChild.textContent) { 38 | this.nodes.el.firstChild.textContent = "<" 39 | } 40 | 41 | if(">" !== this.nodes.el.lastChild.textContent) { 42 | this.nodes.el.lastChild.textContent = ">" 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/autoLinkExtension.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeRendererStaticRenderOptions, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | 5 | export default class MEAutoLinkExtension extends MENode { 6 | static type: MENodeType = "auto_link_extension"; 7 | static tagName: string = "a"; 8 | static async staticRender({data}:MENodeRendererStaticRenderOptions) { 9 | const { linkType, www, url, email, raw } = data; 10 | const hyperlink = 11 | linkType === "www" 12 | ? encodeURI(`http://${www}`) 13 | : linkType === "url" 14 | ? encodeURI(url) 15 | : `mailto:${email}`; 16 | return `<${this.tagName} class="${this.type}" target="_blank" href="${hyperlink}">${raw}` 17 | } 18 | get dirty() { 19 | const content = this.data?.www || this.data.url || this.data?.email 20 | if (content && (content !== this.nodes.el.textContent)) { 21 | return true; 22 | } 23 | return false; 24 | } 25 | renderSelf(data: MENodeData) { 26 | const { linkType, www, url, email, raw } = data; 27 | const hyperlink = 28 | linkType === "www" 29 | ? encodeURI(`http://${www}`) 30 | : linkType === "url" 31 | ? encodeURI(url) 32 | : `mailto:${email}`; 33 | 34 | if (!this.nodes.el) { 35 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE]); 36 | this.nodes.el.dataset.nodeType = this.type; 37 | this.nodes.holder = this.make(this.tagName, [CLASS_NAMES.ME_AUTO_LINK_EXTENSION], { spellcheck: "false", target: "_blank", href: hyperlink }); 38 | this.nodes.el.appendChild(this.nodes.holder); 39 | } 40 | 41 | const el = this.nodes.holder as HTMLAnchorElement; 42 | if(hyperlink !== el.href) { 43 | el.href = hyperlink; 44 | } 45 | if(raw !== el.textContent) { 46 | el.textContent = raw; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/backlash.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | 5 | export default class MEBacklash extends MENode { 6 | static type: MENodeType = "backlash"; 7 | static async staticRender() { 8 | return `` 9 | } 10 | get dirty() { 11 | const marker = this.data?.marker 12 | if(marker && (marker !== this.nodes.el.firstChild?.textContent || marker !== this.nodes.el.lastChild?.textContent)){ 13 | return true; 14 | } 15 | return false; 16 | } 17 | renderSelf(data: MENodeData) { 18 | if(!this.nodes.el) { 19 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE]); 20 | this.nodes.el.innerHTML = `${data.marker}` 21 | this.nodes.el.dataset.nodeType = this.type; 22 | } 23 | 24 | const marker = data.marker; 25 | if(this.nodes.el.firstChild && marker !== this.nodes.el.firstChild.textContent) { 26 | this.nodes.el.firstChild.textContent = marker 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/codeFense.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | 5 | export default class MECodeFense extends MENode { 6 | static type: MENodeType = "code_fense"; 7 | get dirty() { 8 | const marker = this.data?.marker 9 | if(marker && (marker !== this.nodes.el.firstChild?.textContent)){ 10 | return true; 11 | } 12 | return super.dirty; 13 | } 14 | renderSelf(data: MENodeData) { 15 | if(!this.nodes.el) { 16 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE]); 17 | this.nodes.el.innerHTML = `` 18 | this.nodes.el.dataset.type = this.type; 19 | 20 | this.nodes.holder = this.nodes.el.lastChild as HTMLElement; 21 | } else if(!this.nodes.holder.parentNode) { 22 | // fix: holer node removed by Backspace key 23 | this.nodes.el.appendChild(this.nodes.holder) 24 | } 25 | 26 | const marker = data.marker||"" 27 | if(this.nodes.el.firstChild && marker !== this.nodes.el.firstChild.textContent) { 28 | this.nodes.el.firstChild.textContent = marker || '' 29 | } 30 | 31 | const content = data.content || ''; 32 | if(content !== this.nodes.holder.textContent) { 33 | this.nodes.holder.textContent = content 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/del.ts: -------------------------------------------------------------------------------- 1 | import { MENodeType } from "@/packages/types"; 2 | import MEEm from "./em"; 3 | 4 | export default class MEDel extends MEEm { 5 | static type: MENodeType = "del"; 6 | static tagName: string = "del"; 7 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/em.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeRendererStaticRenderOptions, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | 5 | export default class MEEm extends MENode { 6 | static type: MENodeType = "em"; 7 | static tagName: string = "em"; 8 | static async staticRender({innerHTML}:MENodeRendererStaticRenderOptions) { 9 | return `<${this.tagName} class="${this.type}">${innerHTML}` 10 | } 11 | 12 | get dirty() { 13 | const marker = this.data?.marker 14 | if(marker && (marker !== this.nodes.el.firstChild?.textContent || marker !== this.nodes.el.lastChild?.textContent)){ 15 | return true; 16 | } 17 | return super.dirty; 18 | } 19 | 20 | renderSelf(data: MENodeData) { 21 | if(!this.nodes.el) { 22 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE]); 23 | this.nodes.el.innerHTML = `${data.marker}${data.marker}` 24 | this.nodes.el.dataset.nodeType = this.type; 25 | 26 | this.nodes.holder = this.make(this.tagName); 27 | this.nodes.el.insertBefore(this.nodes.holder, this.nodes.el.lastChild); 28 | } 29 | 30 | const marker = data.marker; 31 | if(this.nodes.el.firstChild && marker !== this.nodes.el.firstChild.textContent) { 32 | this.nodes.el.firstChild.textContent = marker || '*' 33 | } 34 | 35 | if(this.nodes.el.lastChild && marker !== this.nodes.el.lastChild.textContent) { 36 | this.nodes.el.lastChild.textContent = marker || '*' 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/emoji.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeRendererStaticRenderOptions, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | import { validEmoji } from "@/packages/utils/emoji"; 5 | 6 | export default class MEEmoji extends MENode { 7 | static type: MENodeType = "emoji"; 8 | static async staticRender({data}:MENodeRendererStaticRenderOptions) { 9 | const { content, marker, raw } = data; 10 | const validation = validEmoji(content) 11 | return validation.emoji 12 | } 13 | get dirty() { 14 | const raw = this.data?.raw 15 | if (raw !== this.nodes.el.firstChild?.textContent) { 16 | return true; 17 | } 18 | 19 | return false; 20 | } 21 | renderSelf(data: MENodeData) { 22 | 23 | const { content, marker, raw } = data; 24 | const validation = validEmoji(content) 25 | const { start, end } = data.range; 26 | const dataset = { 27 | start: start + marker.length, 28 | end: end - marker.length, 29 | } 30 | 31 | if (!this.nodes.el) { 32 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE, CLASS_NAMES.ME_EMOJI]); 33 | this.nodes.el.innerHTML = `` 34 | this.nodes.el.dataset.nodeType = this.type; 35 | 36 | const preview = this.make('span', [CLASS_NAMES.ME_INLINE_RENDER], { 37 | contenteditable: "false", 38 | spellcheck: "false", 39 | }, dataset); 40 | this.nodes.el.appendChild(preview); 41 | } else { 42 | const el = this.nodes.el.lastElementChild as HTMLElement; 43 | for (const key in dataset) { 44 | if (Object.prototype.hasOwnProperty.call(dataset, key)) { 45 | el.dataset[key] = dataset[key] 46 | } 47 | } 48 | } 49 | 50 | if (validation && validation.emoji !== this.nodes.el.lastChild.textContent) { 51 | this.nodes.el.lastChild.textContent = validation.emoji 52 | } 53 | 54 | if (raw !== this.nodes.el.firstChild.textContent) { 55 | this.nodes.el.firstChild.textContent = raw; 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/emojiValid.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | 5 | export default class MEEmojiValid extends MENode { 6 | static type: MENodeType = "emoji_valid"; 7 | renderSelf(data: MENodeData) { 8 | 9 | const { raw } = data; 10 | 11 | if (!this.nodes.el) { 12 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE]); 13 | this.nodes.el.innerHTML = `` 14 | this.nodes.el.dataset.nodeType = this.type; 15 | } 16 | 17 | if (raw !== this.nodes.el.firstChild.textContent) { 18 | this.nodes.el.firstChild.textContent = raw || '' 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/footnoteIdentifier.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeRendererStaticRenderOptions, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | 5 | export default class MEFootnoteIdentifier extends MENode { 6 | static type: MENodeType = "footnote_identifier"; 7 | static tagName: string = "a"; 8 | static async staticRender({data}:MENodeRendererStaticRenderOptions) { 9 | const { content } = data; 10 | return `<${this.tagName}>${content}` 11 | } 12 | get dirty() { 13 | const startMarker = this.data?.marker 14 | const endMarker = `]` 15 | const content = this.data?.content 16 | if((startMarker !== this.nodes.el.firstChild?.textContent || endMarker !== this.nodes.el.lastChild?.textContent) || (content !== this.nodes.holder.textContent)){ 17 | return true; 18 | } 19 | return super.dirty; 20 | } 21 | renderSelf(data: MENodeData) { 22 | 23 | const { marker, content } = data; 24 | const id = `noteref-${content}` 25 | if(!this.nodes.el) { 26 | this.nodes.el = this.make("sup", [CLASS_NAMES.ME_NODE], {id}); 27 | this.nodes.el.innerHTML = `` 28 | this.nodes.el.dataset.nodeType = this.type; 29 | 30 | this.nodes.holder = this.make(this.tagName, [CLASS_NAMES.ME_LINK], {spellcheck: "false"}); 31 | this.nodes.el.insertBefore(this.nodes.holder, this.nodes.el.lastChild); 32 | } else { 33 | const el = this.nodes.holder as HTMLAnchorElement; 34 | el.id = id; 35 | } 36 | 37 | 38 | const startMarker = marker 39 | const endMarker = `]` 40 | 41 | if(startMarker !== this.nodes.el.firstChild.textContent) { 42 | this.nodes.el.firstChild.textContent = startMarker 43 | } 44 | 45 | if(endMarker !== this.nodes.el.lastChild.textContent) { 46 | this.nodes.el.lastChild.textContent = endMarker 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/hardLineBreak.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeRendererStaticRenderOptions, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | 5 | export default class MEHardLineBreak extends MENode { 6 | static type: MENodeType = "hard_line_break"; 7 | static async staticRender({data}:MENodeRendererStaticRenderOptions) { 8 | const { spaces } = data; 9 | return `${spaces}
` 10 | } 11 | private _isAtEnd: boolean; 12 | renderSelf(data: MENodeData) { 13 | if(!this.nodes.el) { 14 | const classNames = [CLASS_NAMES.ME_NODE] 15 | classNames.push(CLASS_NAMES.ME_HARD_LINE_BREAK) 16 | if(data.isAtEnd) { 17 | this.nodes.el = this.make("span", classNames) 18 | this.nodes.el.innerHTML = `${data.spaces}${data.lineBreak}` 19 | } else { 20 | this.nodes.el = this.make("span", classNames) 21 | this.nodes.el.innerHTML = `${data.spaces}${data.lineBreak}` 22 | } 23 | 24 | this.nodes.el.dataset.nodeType = this.type 25 | this.nodes.holder = this.nodes.el 26 | this._isAtEnd = !!data.isAtEnd; 27 | } else if(this._isAtEnd !== (!!data.isAtEnd)) { 28 | if(data.isAtEnd) { 29 | this.nodes.el.innerHTML = `${data.spaces}${data.lineBreak}` 30 | } else { 31 | this.nodes.el.innerHTML = `${data.spaces}${data.lineBreak}` 32 | } 33 | this._isAtEnd = !!data.isAtEnd; 34 | } else { 35 | const spaceNode = this._isAtEnd ? this.nodes.el.firstChild.firstChild : this.nodes.el.firstChild; 36 | if(spaceNode.textContent !== data.spaces) { 37 | spaceNode.textContent = data.spaces; 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/header.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeRendererStaticRenderOptions, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | 5 | export default class MEHeader extends MENode { 6 | static type: MENodeType = "header"; 7 | static async staticRender({data}:MENodeRendererStaticRenderOptions) { 8 | const content = (data.content || ' ').substring(1) 9 | return content ? `<${this.tagName} class="${this.type}">${content}` : '' 10 | } 11 | get dirty() { 12 | const marker = this.data?.marker; 13 | if(marker && (marker !== this.nodes.el.firstChild?.textContent)){ 14 | return true; 15 | } 16 | return false; 17 | } 18 | renderSelf(data: MENodeData) { 19 | if(!this.nodes.el) { 20 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE]); 21 | this.nodes.el.innerHTML = `` 22 | this.nodes.el.dataset.type = this.type; 23 | 24 | this.nodes.holder = this.nodes.el.lastChild as HTMLElement; 25 | } 26 | 27 | const marker = (data.marker||"").substring(0, data.range.end - data.range.start - (data.content||'').length + 1); 28 | if(this.nodes.el.firstChild && marker !== this.nodes.el.firstChild.textContent) { 29 | this.nodes.el.firstChild.textContent = marker || '' 30 | } 31 | 32 | const content = (data.content || ' ').substring(1); 33 | // if(content !== this.nodes.holder.textContent) { 34 | // this.nodes.holder.textContent = content 35 | // } 36 | 37 | data.marker = marker; 38 | data.content = content; 39 | } 40 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/hr.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | 5 | export default class MEHr extends MENode { 6 | static type: MENodeType = "hr"; 7 | get dirty() { 8 | const marker = this.data?.marker 9 | if(marker && (marker !== this.nodes.el.firstChild?.textContent || marker !== this.nodes.el.lastChild?.textContent)){ 10 | return true; 11 | } 12 | return false; 13 | } 14 | renderSelf(data: MENodeData) { 15 | if(!this.nodes.el) { 16 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE]); 17 | this.nodes.el.innerHTML = `${data.marker}` 18 | this.nodes.el.dataset.nodeType = this.type; 19 | } 20 | 21 | const marker = data.marker; 22 | if(this.nodes.el.firstChild && marker !== this.nodes.el.firstChild.textContent) { 23 | this.nodes.el.firstChild.textContent = marker || '***' 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/htmlBr.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | 5 | export default class MEHtmlBr extends MENode { 6 | static type: MENodeType = "html_br"; 7 | static async staticRender() { 8 | return `
` 9 | } 10 | renderSelf(data: MENodeData) { 11 | 12 | const { tag, openTag } = data; 13 | const classNames = [CLASS_NAMES.ME_HTML_TAG] 14 | 15 | if (!this.nodes.el) { 16 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE]); 17 | this.nodes.el.innerHTML = `` 18 | this.nodes.el.dataset.nodeType = this.type; 19 | 20 | this.nodes.holder = this.make(tag, classNames); 21 | this.nodes.el.appendChild(this.nodes.holder); 22 | } 23 | 24 | if (openTag !== this.nodes.el.firstChild.textContent) { 25 | this.nodes.el.firstChild.textContent = openTag || '' 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/htmlComment.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | 5 | export default class MEHtmlComment extends MENode { 6 | static type: MENodeType = "html_comment"; 7 | renderSelf(data: MENodeData) { 8 | 9 | const { openTag } = data; 10 | 11 | if (!this.nodes.el) { 12 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE]); 13 | this.nodes.el.innerHTML = `` 14 | this.nodes.el.dataset.nodeType = this.type; 15 | } 16 | 17 | if (openTag !== this.nodes.el.firstChild.textContent) { 18 | this.nodes.el.firstChild.textContent = openTag || '' 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/htmlEscape.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeRendererStaticRenderOptions, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | import escapeCharactersMap from "@/packages/utils/escapeCharacter"; 5 | 6 | export default class MEHtmlEscape extends MENode { 7 | static type: MENodeType = "html_escape"; 8 | static async staticRender({data}:MENodeRendererStaticRenderOptions) { 9 | const { escapeCharacter } = data; 10 | const character = escapeCharactersMap[escapeCharacter] 11 | return character 12 | } 13 | get dirty() { 14 | const escapeCharacter = this.data?.escapeCharacter 15 | if (escapeCharacter !== this.nodes.el.firstChild?.textContent) { 16 | return true; 17 | } 18 | 19 | return false; 20 | } 21 | renderSelf(data: MENodeData) { 22 | 23 | const { escapeCharacter } = data; 24 | const character = escapeCharactersMap[escapeCharacter] 25 | const { start, end } = data.range; 26 | const dataset = { 27 | begin: 0, 28 | length: end - start 29 | } 30 | 31 | if (!this.nodes.el) { 32 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE, CLASS_NAMES.ME_HTML_ESCAPE]); 33 | this.nodes.el.innerHTML = `` 34 | this.nodes.el.dataset.nodeType = this.type; 35 | 36 | this.nodes.holder = this.make('span', [CLASS_NAMES.ME_INLINE_RENDER], { 37 | contenteditable: "false", 38 | spellcheck: "false", 39 | }, dataset); 40 | this.nodes.el.appendChild(this.nodes.holder); 41 | } else { 42 | const el = this.nodes.holder; 43 | for (const key in dataset) { 44 | if (Object.prototype.hasOwnProperty.call(dataset, key)) { 45 | el.dataset[key] = dataset[key] 46 | } 47 | } 48 | } 49 | 50 | if (character !== this.nodes.holder.innerHTML) { 51 | this.nodes.holder.innerHTML = character 52 | } 53 | 54 | if (escapeCharacter !== this.nodes.el.firstChild.textContent) { 55 | this.nodes.el.firstChild.textContent = escapeCharacter; 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/htmlRuby.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | 5 | export default class MEHtmlRuby extends MENode { 6 | static type: MENodeType = "html_ruby"; 7 | 8 | get dirty() { 9 | const content = this.data?.raw 10 | if (content !== this.textContent) { 11 | return true; 12 | } 13 | 14 | return false; 15 | } 16 | renderSelf(data: MENodeData) { 17 | 18 | const { raw, openTag, closeTag} = data; 19 | const { start, end } = data.range; 20 | const dataset = { 21 | start: start + openTag.length, // ''.length 22 | end: end - closeTag.length, //''.length 23 | } 24 | 25 | if (!this.nodes.el) { 26 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE, CLASS_NAMES.ME_RUBY]); 27 | this.nodes.el.innerHTML = `` 28 | this.nodes.el.dataset.nodeType = this.type; 29 | 30 | const preview = this.make('span', [CLASS_NAMES.ME_INLINE_RENDER], { 31 | contenteditable: "false", 32 | spellcheck: "false", 33 | }, dataset); 34 | this.nodes.el.appendChild(preview); 35 | } else { 36 | const el = this.nodes.el.lastElementChild as HTMLElement; 37 | for (const key in dataset) { 38 | if (Object.prototype.hasOwnProperty.call(dataset, key)) { 39 | el.dataset[key] = dataset[key] 40 | } 41 | } 42 | } 43 | 44 | if (raw !== this.nodes.el.firstChild.textContent) { 45 | this.nodes.el.firstChild.textContent = raw 46 | } 47 | 48 | if (raw !== this.nodes.el.lastElementChild.innerHTML) { 49 | this.nodes.el.lastElementChild.innerHTML = raw 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/htmlValidTag.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | 5 | export default class MEHtmlValidTag extends MENode { 6 | static type: MENodeType = "html_valid_tag"; 7 | renderSelf(data: MENodeData) { 8 | 9 | const { openTag } = data; 10 | 11 | if (!this.nodes.el) { 12 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE, CLASS_NAMES.ME_HTML_VALID_TAG]); 13 | this.nodes.el.innerHTML = `` 14 | this.nodes.el.dataset.nodeType = this.type; 15 | } 16 | 17 | if (openTag !== this.nodes.el.firstChild.textContent) { 18 | this.nodes.el.firstChild.textContent = openTag || '' 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/image.ts: -------------------------------------------------------------------------------- 1 | import { MENodeType } from "@/packages/types"; 2 | import MEHtmlImg from "./htmlImg"; 3 | 4 | export default class MEImage extends MEHtmlImg { 5 | static type: MENodeType = "image"; 6 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/inlineCode.ts: -------------------------------------------------------------------------------- 1 | import { MENodeType } from "@/packages/types"; 2 | import MEEm from "./em"; 3 | 4 | export default class MEInlineCode extends MEEm { 5 | static type: MENodeType = "inline_code"; 6 | static tagName: string = "code"; 7 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/inlineMath.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeRendererStaticRenderOptions, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | import katex from 'katex'; 5 | import "katex/dist/contrib/mhchem.min.js"; 6 | import "katex/dist/katex.min.css"; 7 | import { tex2svgPromise } from "@/packages/utils/math"; 8 | 9 | export default class MEInlineMath extends MENode { 10 | static type: MENodeType = "inline_math"; 11 | static mathMap: Map = new Map(); 12 | static mathMap2: Map = new Map(); 13 | static async staticRender({data}:MENodeRendererStaticRenderOptions) { 14 | const math = data.content; 15 | let mathHtml; 16 | if(this.mathMap2.has(math)) { 17 | mathHtml = this.mathMap2.get(math) 18 | } else { 19 | mathHtml = await tex2svgPromise(math); 20 | this.mathMap2.set(math, mathHtml) 21 | } 22 | return `${mathHtml}` 23 | } 24 | get dirty() { 25 | const content = this.data?.raw 26 | if (content !== this.textContent) { 27 | return true; 28 | } 29 | 30 | return false; 31 | } 32 | 33 | get startMarkerNode() { 34 | return this.nodes.el.firstChild; 35 | } 36 | 37 | get mathNode() { 38 | return this.nodes.el.childNodes[1]; 39 | } 40 | 41 | get endMarkerNode() { 42 | return this.nodes.el.childNodes[2]; 43 | } 44 | 45 | get previewNode() { 46 | return this.nodes.el.lastElementChild as HTMLElement; 47 | } 48 | renderSelf(data: MENodeData) { 49 | 50 | const { marker, content: math, } = data; 51 | const { start, end } = data.range; 52 | const dataset = { 53 | begin: marker.length, 54 | length: end - marker.length - start - marker.length 55 | } 56 | 57 | const mathMap = MEInlineMath.mathMap; 58 | const displayMode = false; 59 | const key = `${math}`; 60 | let mathHtml = ""; 61 | let renderErr = false; 62 | if (mathMap.has(key)) { 63 | mathHtml = mathMap.get(key); 64 | } else { 65 | try { 66 | mathHtml = katex.renderToString(math, { 67 | displayMode, 68 | }); 69 | mathMap.set(key, mathHtml); 70 | } catch (err) { 71 | renderErr = true; 72 | mathHtml = "Invalid Mathematical Formula"; 73 | } 74 | } 75 | 76 | 77 | if (!this.nodes.el || this.nodes.el.childNodes.length !== 4) { 78 | this.nodes.el = this.nodes.el || this.make("span", [CLASS_NAMES.ME_NODE, CLASS_NAMES.ME_MATH]); 79 | this.nodes.el.innerHTML = `` 80 | this.nodes.el.dataset.nodeType = this.type; 81 | 82 | const preview = this.make('span', [CLASS_NAMES.ME_INLINE_RENDER], { 83 | contenteditable: "false", 84 | spellcheck: "false", 85 | }, dataset); 86 | this.nodes.el.appendChild(preview); 87 | 88 | preview.classList.toggle(CLASS_NAMES.ME_MATH_ERROR, renderErr) 89 | } else { 90 | 91 | const el = this.previewNode; 92 | el.classList.toggle(CLASS_NAMES.ME_MATH_ERROR, renderErr) 93 | for (const key in dataset) { 94 | if (Object.prototype.hasOwnProperty.call(dataset, key)) { 95 | el.dataset[key] = dataset[key] 96 | } 97 | } 98 | } 99 | 100 | if (marker !== this.startMarkerNode.textContent) { 101 | this.startMarkerNode.textContent = marker 102 | } 103 | if (marker !== this.endMarkerNode.textContent) { 104 | this.endMarkerNode.textContent = marker 105 | } 106 | if (math !== this.mathNode.textContent) { 107 | this.mathNode.textContent = math 108 | } 109 | 110 | if (mathHtml !== this.previewNode.innerHTML) { 111 | this.previewNode.innerHTML = mathHtml 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/link.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeRendererStaticRenderOptions, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | import { sanitizeHyperlink } from "@/packages/utils/url"; 5 | 6 | export default class MELink extends MENode { 7 | static type: MENodeType = "link"; 8 | static tagName: string = "a"; 9 | static async staticRender({data, innerHTML}:MENodeRendererStaticRenderOptions) { 10 | const { href, title } = data; 11 | const hyperlink = sanitizeHyperlink(encodeURI(href)); 12 | return `<${this.tagName} class="${this.type}" target="_blank" href="${hyperlink}" title="${title}">${innerHTML}` 13 | } 14 | get dirty() { 15 | const startMarker = "[" 16 | const endMarker = `](${this.data?.hrefAndTitle||""})` 17 | const anchor = this.data?.anchor 18 | if((startMarker !== this.nodes.el.firstChild?.textContent || endMarker !== this.nodes.el.lastChild?.textContent) || (anchor !== this.nodes.holder.textContent)){ 19 | return true; 20 | } 21 | return super.dirty; 22 | } 23 | renderSelf(data: MENodeData) { 24 | 25 | const { raw, href, title, hrefAndTitle } = data; 26 | const {start, end} = data.range; 27 | const dataset = { 28 | start, 29 | end, 30 | raw 31 | } 32 | const hyperlink = sanitizeHyperlink(encodeURI(href)); 33 | if(!this.nodes.el) { 34 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE]); 35 | this.nodes.el.innerHTML = `` 36 | this.nodes.el.dataset.nodeType = this.type; 37 | 38 | this.nodes.holder = this.make(this.tagName, [CLASS_NAMES.ME_LINK], {spellcheck: "false", target: "_blank", href: hyperlink, title}, dataset); 39 | this.nodes.el.insertBefore(this.nodes.holder, this.nodes.el.lastChild); 40 | } else { 41 | const el = this.nodes.holder as HTMLAnchorElement; 42 | el.href = hyperlink; 43 | 44 | for (const key in dataset) { 45 | if (Object.prototype.hasOwnProperty.call(dataset, key)) { 46 | el.dataset[key] = dataset[key] 47 | } 48 | } 49 | 50 | } 51 | 52 | 53 | const startMarker = "[" 54 | const endMarker = `](${hrefAndTitle})` 55 | 56 | if(startMarker !== this.nodes.el.firstChild.textContent) { 57 | this.nodes.el.firstChild.textContent = startMarker 58 | } 59 | 60 | if(endMarker !== this.nodes.el.lastChild.textContent) { 61 | this.nodes.el.lastChild.textContent = endMarker 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/linkNoText.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeRendererStaticRenderOptions, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | import { sanitizeHyperlink } from "@/packages/utils/url"; 5 | 6 | export default class MELinkNoText extends MENode { 7 | static type: MENodeType = "link_no_text"; 8 | static tagName: string = "a"; 9 | static async staticRender({data, innerHTML}:MENodeRendererStaticRenderOptions) { 10 | const { href, title } = data; 11 | const hyperlink = sanitizeHyperlink(encodeURI(href)); 12 | return `<${this.tagName} class="${this.type}" target="_blank" href="${hyperlink}" title="${title}">${innerHTML}` 13 | } 14 | get dirty() { 15 | const startMarker = "[](" 16 | const endMarker = ")" 17 | const hrefAndTitle = this.data?.hrefAndTitle 18 | if((startMarker !== this.nodes.el.firstChild?.textContent || endMarker !== this.nodes.el.lastChild?.textContent) || (hrefAndTitle !== this.nodes.holder.textContent)){ 19 | return true; 20 | } 21 | return super.dirty; 22 | } 23 | renderSelf(data: MENodeData) { 24 | 25 | const { raw, href, title, hrefAndTitle } = data; 26 | const {start, end} = data.range; 27 | const dataset = { 28 | start, 29 | end, 30 | raw 31 | } 32 | const hyperlink = sanitizeHyperlink(encodeURI(href)); 33 | if(!this.nodes.el) { 34 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE]); 35 | this.nodes.el.innerHTML = `<>` 36 | this.nodes.el.dataset.nodeType = this.type; 37 | 38 | this.nodes.holder = this.make(this.tagName, [CLASS_NAMES.ME_LINK], {spellcheck: "false", target: "_blank", href: hyperlink, title}, dataset); 39 | this.nodes.el.insertBefore(this.nodes.holder, this.nodes.el.lastChild); 40 | } else { 41 | const el = this.nodes.holder as HTMLAnchorElement; 42 | el.href = hyperlink; 43 | 44 | for (const key in dataset) { 45 | if (Object.prototype.hasOwnProperty.call(dataset, key)) { 46 | el.dataset[key] = dataset[key] 47 | } 48 | } 49 | 50 | } 51 | 52 | 53 | const startMarker = "[](" 54 | const endMarker = ")" 55 | 56 | if(startMarker !== this.nodes.el.firstChild.textContent) { 57 | this.nodes.el.firstChild.textContent = startMarker 58 | } 59 | 60 | if(endMarker !== this.nodes.el.lastChild.textContent) { 61 | this.nodes.el.lastChild.textContent = endMarker 62 | } 63 | 64 | if(hrefAndTitle !== this.nodes.holder.textContent) { 65 | this.nodes.holder.textContent = hrefAndTitle 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/multipleMath.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | 5 | export default class MEMultipleMath extends MENode { 6 | static type: MENodeType = "multiple_math"; 7 | renderSelf(data: MENodeData) { 8 | 9 | const { marker } = data; 10 | 11 | if (!this.nodes.el) { 12 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE]); 13 | this.nodes.el.innerHTML = `${marker}` 14 | this.nodes.el.dataset.nodeType = this.type; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/node.ts: -------------------------------------------------------------------------------- 1 | import domUtils from "@/packages/utils/domUtils"; 2 | import MEModule from "@/packages/modules/module"; 3 | import { MEBlockRendererInstance, MENodeConstructable, MENodeData, MENodeRendererStaticRenderOptions, MENodeType } from "@/packages/types"; 4 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 5 | import { inlinePatch } from "../../utils/patch"; 6 | import { getTextContent } from "../../utils/dom"; 7 | 8 | 9 | export default class MENode extends MEModule<{ el: HTMLElement; holder: HTMLElement }> { 10 | 11 | static nodes: { [type: string]: MENodeConstructable } = {} 12 | static type: MENodeType = "text"; 13 | static tagName: string = "span"; 14 | private _data: MENodeData; 15 | blockRenderer!: MEBlockRendererInstance; 16 | children: MENode[] = []; 17 | 18 | static register(node: MENodeConstructable) { 19 | this.nodes[node.type] = node; 20 | } 21 | 22 | static async staticRender({data}:MENodeRendererStaticRenderOptions) { 23 | const {raw} = data 24 | return `<${this.tagName} class="${this.type}">${raw}` 25 | } 26 | 27 | constructor(instance: MEBlockRendererInstance) { 28 | super(instance.instance); 29 | this.blockRenderer = instance; 30 | } 31 | 32 | get type() { 33 | return this.constructor['type']; 34 | } 35 | 36 | get dirty() { 37 | 38 | const childNodes = Array.from(this.nodes.el.childNodes) 39 | return (childNodes.length > 1 && childNodes.some((node)=>node.nodeType===3)) || (this.children && this.children.some((child)=>child.dirty)); 40 | } 41 | 42 | get data() { 43 | return this._data; 44 | } 45 | 46 | get tagName() { 47 | return this.constructor['tagName'] 48 | } 49 | 50 | get textContent() { 51 | return getTextContent(this.nodes.el, [CLASS_NAMES.ME_INLINE_RENDER]) 52 | } 53 | 54 | renderSelf(data: MENodeData) { 55 | if (!this.nodes.el) { 56 | this.nodes.el = this.make(this.tagName, [CLASS_NAMES.ME_NODE]); 57 | this.nodes.el.dataset.nodeType = this.type; 58 | this.nodes.holder = this.nodes.el; 59 | } 60 | } 61 | 62 | render(data: MENodeData): HTMLElement { 63 | this.renderSelf(data); 64 | // remove wild text nodes 65 | if(this.nodes.el.childNodes.length > 1) { 66 | Array.from(this.nodes.el.childNodes).forEach((node)=>{ 67 | if(node.nodeType === 3) { 68 | this.nodes.el.removeChild(node) 69 | } 70 | }) 71 | } 72 | 73 | if(this.nodes.holder) { 74 | if (data.children) { 75 | inlinePatch.call(this, data.children); 76 | } else if(typeof data.content !== "undefined"){ 77 | const content = data.content; 78 | if (content !== this.nodes.holder.textContent) { 79 | this.nodes.holder.textContent = content 80 | } 81 | } 82 | } 83 | 84 | this._data = data; 85 | this.nodes.el.classList.toggle(CLASS_NAMES.ME_NODE__ACTIVED, !!data.actived); 86 | return this.nodes.el; 87 | } 88 | 89 | make(tagName: string, classNames: string | string[] | null = null, attributes: any = {}, dataset: any = {}) { 90 | return domUtils.make(tagName, classNames, attributes, dataset) 91 | } 92 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/referenceDefinition.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeRendererStaticRenderOptions, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | 5 | export default class MEReferenceDefinition extends MENode { 6 | static type: MENodeType = "reference_definition"; 7 | static async staticRender({data, innerHTML}:MENodeRendererStaticRenderOptions) { 8 | const { leftBracket, rightBracket, titleMarker, leftTitleSpace, rightTitleSpace, title, href } = data; 9 | return `${leftBracket}${innerHTML||''}${rightBracket}${href}${leftTitleSpace + titleMarker}${title}${titleMarker + rightTitleSpace}` 10 | } 11 | get dirty() { 12 | const { leftBracket, rightBracket, titleMarker, raw, leftTitleSpace, rightTitleSpace, title, href } = this.data; 13 | const startMarker = leftBracket 14 | const middleMarker = rightBracket + href + leftTitleSpace + titleMarker 15 | const endMarker = titleMarker + rightTitleSpace 16 | if((startMarker !== this.nodes.el.firstChild?.textContent || endMarker !== this.nodes.el.lastChild?.textContent) || (middleMarker !== this.nodes.el.childNodes[2].textContent) || (title !== this.nodes.el.childNodes[3].textContent)){ 17 | return true; 18 | } 19 | return super.dirty; 20 | } 21 | renderSelf(data: MENodeData) { 22 | 23 | const { leftBracket, rightBracket, titleMarker, raw, leftTitleSpace, rightTitleSpace, title, href } = data; 24 | if(!this.nodes.el || this.nodes.el.childNodes.length !== 5) { 25 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE]); 26 | this.nodes.el.innerHTML = `` 27 | this.nodes.el.dataset.nodeType = this.type; 28 | 29 | this.nodes.holder = this.nodes.el.childNodes[1] as HTMLElement; 30 | } 31 | 32 | 33 | const startMarker = leftBracket 34 | const middleMarker = rightBracket + href + leftTitleSpace + titleMarker 35 | const endMarker = titleMarker + rightTitleSpace 36 | 37 | if(startMarker !== this.nodes.el.firstChild.textContent) { 38 | this.nodes.el.firstChild.textContent = startMarker 39 | } 40 | 41 | if(middleMarker !== this.nodes.el.childNodes[2].textContent) { 42 | this.nodes.el.childNodes[2].textContent = middleMarker 43 | } 44 | 45 | if(title !== this.nodes.el.childNodes[3].textContent) { 46 | this.nodes.el.childNodes[3].textContent = title 47 | } 48 | 49 | if(endMarker !== this.nodes.el.lastChild.textContent) { 50 | this.nodes.el.lastChild.textContent = endMarker 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/referenceImage.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeRendererStaticRenderOptions, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | import { getImageSrc } from "../../utils/image"; 5 | 6 | export default class MEReferenceImage extends MENode { 7 | static type: MENodeType = "reference_image"; 8 | img: HTMLElement; 9 | static async staticRender({data, labels}:MENodeRendererStaticRenderOptions) { 10 | const { label, alt } = data; 11 | let href = ""; 12 | let title = ""; 13 | if (labels.has(label.toLowerCase())) { 14 | ({ href, title } = labels.get(label.toLowerCase())); 15 | } 16 | 17 | const {src} = getImageSrc(href); 18 | const attributes = {alt, src, title} 19 | const attrString = Object.entries(attributes).map(([key, value])=>(`${key}="${value}"`)).join(" ") 20 | return `` 21 | } 22 | get dirty() { 23 | const content = this.data?.raw 24 | if (content !== this.nodes.el.firstChild.textContent) { 25 | return true; 26 | } 27 | 28 | return false; 29 | } 30 | renderSelf(data: MENodeData) { 31 | 32 | const { raw, label, alt } = data; 33 | const { start, end } = data.range; 34 | const dataset = { 35 | start: start + 2 + alt.length, // label 36 | end: end - 1, // 37 | } 38 | 39 | const labels = this.instance.context.state.labels; 40 | 41 | let href = ""; 42 | let title = ""; 43 | if (labels.has(label.toLowerCase())) { 44 | ({ href, title } = labels.get(label.toLowerCase())); 45 | } 46 | 47 | const {src} = getImageSrc(href); 48 | const attributes = {alt, src, title} 49 | 50 | if (!this.nodes.el) { 51 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE, CLASS_NAMES.ME_REFERENCE_IMAGE]); 52 | this.nodes.el.innerHTML = `` 53 | this.nodes.el.dataset.nodeType = this.type; 54 | 55 | const preview = this.make('span', [CLASS_NAMES.ME_INLINE_RENDER], { 56 | contenteditable: "false", 57 | spellcheck: "false", 58 | }, dataset); 59 | this.nodes.el.appendChild(preview); 60 | this.img = this.make('img', [], attributes); 61 | this.nodes.el.appendChild(this.img); 62 | 63 | // this.nodes.holder = this.nodes.el.firstChild as HTMLElement; 64 | } else { 65 | const el = this.nodes.el.lastElementChild as HTMLElement; 66 | for (const key in dataset) { 67 | if (Object.prototype.hasOwnProperty.call(dataset, key)) { 68 | el.dataset[key] = dataset[key] 69 | } 70 | } 71 | 72 | for(let key in attributes) { 73 | this.img.setAttribute(key, attributes[key]) 74 | } 75 | } 76 | 77 | if (raw !== this.nodes.el.firstChild.textContent) { 78 | this.nodes.el.firstChild.textContent = raw 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/referenceLink.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeRendererStaticRenderOptions, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | import { sanitizeHyperlink } from "@/packages/utils/url"; 5 | 6 | export default class MEReferenceLink extends MENode { 7 | static type: MENodeType = "reference_link"; 8 | static async staticRender({data, innerHTML}:MENodeRendererStaticRenderOptions) { 9 | const { href, title } = data; 10 | const hyperlink = sanitizeHyperlink(encodeURI(href)); 11 | return `<${this.tagName} class="${this.type}" target="_blank" href="${hyperlink}" title="${title}">${innerHTML||''}` 12 | } 13 | get dirty() { 14 | const { raw, anchor } = this.data; 15 | const startMarker = "[" 16 | const endMarker = raw.substring(1+anchor.length) 17 | if((startMarker !== this.nodes.el.firstChild?.textContent || endMarker !== this.nodes.el.lastChild?.textContent) || (anchor !== this.nodes.holder.textContent)){ 18 | return true; 19 | } 20 | return super.dirty; 21 | } 22 | get tagName() { 23 | return 'a' 24 | } 25 | renderSelf(data: MENodeData) { 26 | 27 | const { raw, href, title, anchor } = data; 28 | const {start, end} = data.range; 29 | const dataset = { 30 | start, 31 | end, 32 | raw 33 | } 34 | const hyperlink = sanitizeHyperlink(encodeURI(href)); 35 | if(!this.nodes.el) { 36 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE]); 37 | this.nodes.el.innerHTML = `` 38 | this.nodes.el.dataset.nodeType = this.type; 39 | 40 | this.nodes.holder = this.make(this.tagName, [CLASS_NAMES.ME_REFERENCE_LINK], {spellcheck: "false", target: "_blank", href: hyperlink, title}, dataset); 41 | this.nodes.el.insertBefore(this.nodes.holder, this.nodes.el.lastChild); 42 | } else { 43 | const el = this.nodes.holder as HTMLAnchorElement; 44 | el.href = hyperlink; 45 | 46 | for (const key in dataset) { 47 | if (Object.prototype.hasOwnProperty.call(dataset, key)) { 48 | el.dataset[key] = dataset[key] 49 | } 50 | } 51 | 52 | } 53 | 54 | 55 | const startMarker = "[" 56 | const endMarker = raw.substring(1+anchor.length); 57 | 58 | if(startMarker !== this.nodes.el.firstChild.textContent) { 59 | this.nodes.el.firstChild.textContent = startMarker 60 | } 61 | 62 | if(endMarker !== this.nodes.el.lastChild.textContent) { 63 | this.nodes.el.lastChild.textContent = endMarker 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/softLineBreak.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | 5 | export default class MESoftLineBreak extends MENode { 6 | static type: MENodeType = "soft_line_break"; 7 | static async staticRender() { 8 | return `
` 9 | } 10 | renderSelf(data: MENodeData) { 11 | if(!this.nodes.el) { 12 | const classNames = [CLASS_NAMES.ME_NODE, CLASS_NAMES.ME_SOFT_LINE_BREAK] 13 | if(data.isAtEnd) { 14 | classNames.push(CLASS_NAMES.ME_LINE_END) 15 | } 16 | this.nodes.el = this.make("span", classNames) 17 | this.nodes.el.dataset.nodeType = this.type 18 | this.nodes.holder = this.nodes.el 19 | } 20 | this.nodes.el.textContent = data.lineBreak 21 | this.nodes.el.classList.toggle(CLASS_NAMES.ME_LINE_END, data.isAtEnd) 22 | } 23 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/strong.ts: -------------------------------------------------------------------------------- 1 | import { MENodeType } from "@/packages/types"; 2 | import MEEm from "./em"; 3 | 4 | export default class MEStrong extends MEEm { 5 | static type: MENodeType = "strong"; 6 | static tagName: string = "strong"; 7 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/subScript.ts: -------------------------------------------------------------------------------- 1 | import { MENodeType } from "@/packages/types"; 2 | import MEEm from "./em"; 3 | 4 | export default class MESubScript extends MEEm { 5 | static type: MENodeType = "sub"; 6 | static tagName: string = "sub"; 7 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/superScript.ts: -------------------------------------------------------------------------------- 1 | import { MENodeType } from "@/packages/types"; 2 | import MEEm from "./em"; 3 | 4 | export default class MESuperScript extends MEEm { 5 | static type: MENodeType = "sup"; 6 | static tagName: string = "sup"; 7 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/nodes/tailHeader.ts: -------------------------------------------------------------------------------- 1 | import { MENodeData, MENodeType } from "@/packages/types"; 2 | import MENode from "./node"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | 5 | export default class METailHeader extends MENode { 6 | static type: MENodeType = "tail_header"; 7 | renderSelf(data: MENodeData) { 8 | 9 | const { raw } = data; 10 | 11 | if (!this.nodes.el) { 12 | this.nodes.el = this.make("span", [CLASS_NAMES.ME_NODE]); 13 | this.nodes.el.innerHTML = `` 14 | this.nodes.el.dataset.nodeType = this.type; 15 | } 16 | 17 | if (raw !== this.nodes.el.firstChild.textContent) { 18 | this.nodes.el.firstChild.textContent = raw || '' 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/packages/modules/content/inlineRenderers/tokenizer/rules.ts: -------------------------------------------------------------------------------- 1 | import { escapeCharacters } from "@/packages/utils/escapeCharacter"; 2 | 3 | /* eslint-disable no-useless-escape */ 4 | export const beginRules = { 5 | hr: /^(\*{3,}$|^\-{3,}$|^\_{3,}$)/, 6 | code_fense: /^(`{3,})([^`]*)$/, 7 | header: /(^ {0,3}#{1,6}(\s{1,}|$))/, 8 | reference_definition: 9 | /^( {0,3}\[)([^\]]+?)(\\*)(\]: *)(]+)(>?)(?:( +)(["'(]?)([^\n"'\(\)]+)\9)?( *)$/, 10 | 11 | // extra syntax (not belogs to GFM) 12 | multiple_math: /^(\$\$)$/, 13 | }; 14 | 15 | export const endRules = { 16 | tail_header: /^(\s{1,}#{1,})(\s*)$/, 17 | }; 18 | 19 | export const commonMarkRules = { 20 | strong: /^(\*\*|__)(?=\S)([\s\S]*?[^\s\\])(\\*)\1(?!(\*|_))/, // can nest 21 | em: /^(\*|_)(?=\S)([\s\S]*?[^\s\*\\])(\\*)\1(?!\1)/, // can nest 22 | inline_code: /^(`{1,3})([^`]+?|.{2,})\1/, 23 | image: /^(\!\[)(.*?)(\\*)\]\((.*)(\\*)\)/, 24 | link: /^(\[)((?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*?)(\\*)\]\((|.*[^\\]+)(\\*)\)/, // can nest 25 | reference_link: /^\[([^\]]+?)(\\*)\](?:\[([^\]]*?)(\\*)\])?/, 26 | reference_image: /^\!\[([^\]]+?)(\\*)\](?:\[([^\]]*?)(\\*)\])?/, 27 | html_tag: 28 | /^(|(<([a-zA-Z]{1}[a-zA-Z\d-]*) *[^\n<>]* *(?:\/)?>)(?:([\s\S]*?)(<\/\3 *>))?)/, // raw html 29 | html_escape: new RegExp(`^(${escapeCharacters.join("|")})`, "i"), 30 | soft_line_break: /^(\n)(?!\n)/, 31 | hard_line_break: /^( {2,})(\n)(?!\n)/, 32 | 33 | // patched math marker `$` 34 | backlash: /^(\\)([\\`*{}\[\]()#+\-.!_>~:\|\<\>$]{1})/, 35 | }; 36 | 37 | export const gfmRules = { 38 | emoji: /^(:)([a-z_\d+-]+?)\1/, 39 | del: /^(~{2})(?=\S)([\s\S]*?\S)(\\*)\1/, // can nest 40 | auto_link: 41 | /^<(?:([a-zA-Z]{1}[a-zA-Z\d\+\.\-]{1,31}:[^ <>]*)|([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])?)*))>/, 42 | // (extended www autolink|extended url autolink|extended email autolink) the email regexp is the same as auto_link. 43 | auto_link_extension: 44 | /^(?:(www\.[a-z_-]+\.[a-z]{2,}(?::[0-9]{1,5})?(?:\/[\S]+)?)|(http(?:s)?:\/\/(?:[a-z0-9\-._~]+\.[a-z]{2,}|[0-9.]+|localhost|\[[a-f0-9.:]+\])(?::[0-9]{1,5})?(?:\/[\S]+)?)|([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])?)*))(?=\s|$)/, 45 | }; 46 | 47 | // Markdown extensions (not belongs to GFM and Commonmark) 48 | export const inlineExtensionRules = { 49 | inline_math: /^(\$)([^\$]*?[^\$\\])(\\*)\1(?!\1)/, 50 | // This is not the best regexp, because it not support `2^2\\^`. 51 | superscript: /^(\^)((?:[^\^\s]|(?<=\\)\1|(?<=\\) )+?)(?findBlock(child, el)); 11 | } 12 | 13 | export function findBlockByElement(el: HTMLElement|Node): MEBlockInstance|null { 14 | if (!domUtils.isElement(el)) { 15 | el = el.parentNode as HTMLElement; 16 | } 17 | 18 | if (!el) { 19 | return null; 20 | } 21 | 22 | const blockEl = (el as HTMLElement).closest(`.${CLASS_NAMES.ME_BLOCK}`) 23 | const find = blockEl && blockEl['BLOCK_INSTANCE']; 24 | return find; 25 | } 26 | 27 | export function findDropBlock(block: MEBlockInstance): MEBlockInstance|undefined { 28 | 29 | const target = block.nodes.el.querySelector(`.${CLASS_NAMES.ME_BLOCK__DROP_TARGET}`); 30 | if(target) { 31 | return target['BLOCK_INSTANCE'] 32 | } 33 | 34 | return; 35 | } 36 | 37 | export function clearDropBlock(block: MEBlockInstance): MEBlockInstance|undefined { 38 | block.dropTarget = false; 39 | if(!block.children) { 40 | return; 41 | } 42 | block.children.forEach((child)=>clearDropBlock(child)); 43 | } 44 | 45 | 46 | export const findInlineNode = (node) => { 47 | if (node.nodeType === 3) { 48 | node = node.parentNode; 49 | } 50 | 51 | if (!node) return null; 52 | 53 | let target:Element|null = null; 54 | while((node = node.closest(`.${CLASS_NAMES.ME_NODE}`))) { 55 | target = node; 56 | node = node.parentNode; 57 | } 58 | 59 | return target; 60 | }; 61 | 62 | export const findActivedNodes = (node, offset) => { 63 | const activedNodes: Element[] = []; 64 | const target = findInlineNode(node); 65 | if (target) { 66 | const tOffset = getOffsetOfParent(node, target) + offset; 67 | const text = getTextContent(target, [CLASS_NAMES.ME_INLINE_RENDER]) 68 | // console.log(node, focusNode, offset, text, text.length); 69 | activedNodes.push(target) 70 | if (tOffset === 0 && target.previousElementSibling) { 71 | activedNodes.push(target.previousElementSibling); 72 | } 73 | if (tOffset === text.length && target.nextElementSibling) { 74 | activedNodes.push(target.nextElementSibling); 75 | } 76 | } 77 | return activedNodes; 78 | } -------------------------------------------------------------------------------- /src/packages/modules/dragdrop.ts: -------------------------------------------------------------------------------- 1 | import MEModule from "./module"; 2 | 3 | export default class MEDragDrop extends MEModule { 4 | 5 | async prepare(): Promise { 6 | this.dispatchEvents() 7 | return true; 8 | } 9 | 10 | private dispatchEvents() { 11 | const { event, editable } = this.instance.context; 12 | const { selection } = editable; 13 | 14 | event.on("drop", async (type, event) => { 15 | await this.processDrop(event as DragEvent); 16 | }) 17 | 18 | event.on("dragstart", (type, event) => { 19 | this.processDragStart() 20 | }) 21 | 22 | event.on("dragover", (type, event) => { 23 | this.processDragOver(event as DragEvent); 24 | }); 25 | } 26 | private async processDrop(dropEvent: DragEvent): Promise { 27 | const { 28 | content, 29 | clipboard, 30 | } = this.instance.context; 31 | 32 | dropEvent.preventDefault(); 33 | 34 | const targetBlock = content.dropTargetBlock || content.lastChild; 35 | content.clearDropTargetBlock(); 36 | 37 | if (targetBlock) { 38 | targetBlock.renderer.setCursor({focus: {offset: targetBlock.renderer.text.length}}) 39 | await clipboard.processDataTransfer(targetBlock, dropEvent.dataTransfer, true); 40 | } 41 | } 42 | 43 | private processDragStart(): void { 44 | } 45 | 46 | private processDragOver(dragEvent: DragEvent): void { 47 | dragEvent.preventDefault(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/packages/modules/editable/dom.d.ts: -------------------------------------------------------------------------------- 1 | export type MENativeNode = Node | Node & ParentNode | ChildNode | Text 2 | export type MEIndexedNode = MENativeNode & { 3 | [index: string]: any 4 | } 5 | 6 | export interface MEEnv { 7 | ie?: boolean; 8 | ie11Compat?: boolean; 9 | ie9Compat?: boolean; 10 | ie8?: boolean; 11 | ie8Compat?: boolean; 12 | ie7Compat?: boolean; 13 | ie6Compat?: boolean; 14 | ie9above?: boolean; 15 | ie9below?: boolean; 16 | ie11above?: boolean; 17 | ie11below?: boolean; 18 | opera?: boolean; 19 | webkit?: boolean; 20 | mac?: boolean; 21 | quirks?: boolean; 22 | gecko?: boolean; 23 | safari?: number; 24 | chrome?: number; 25 | version: number; 26 | isCompatible?: boolean; 27 | } 28 | 29 | export interface MEBookmark { 30 | start: string | HTMLSpanElement; 31 | end: string | HTMLSpanElement | null; 32 | id: string | undefined; 33 | } 34 | 35 | export interface MEAddress { 36 | startAddress: number[]; 37 | endAddress?: number[]; 38 | collapsed: boolean; 39 | } -------------------------------------------------------------------------------- /src/packages/modules/editable/index.ts: -------------------------------------------------------------------------------- 1 | import MEModule from "@/packages/modules/module"; 2 | import MESelection from "./selection"; 3 | import { CLASS_NAMES } from "@/packages/utils/classNames"; 4 | import env from "@/packages/utils/env"; 5 | 6 | 7 | export default class MEEditable extends MEModule { 8 | 9 | private _holder!: HTMLElement; 10 | private _rootDocNode!: Node; 11 | private _actived!: boolean; 12 | private _selection!: MESelection; 13 | private _document!: HTMLDocument; 14 | private _window!: Window; 15 | private _enabled!: boolean; 16 | 17 | get holder() { 18 | return this._holder; 19 | } 20 | 21 | get rootNode() { 22 | return this._rootDocNode; 23 | } 24 | 25 | get actived() { 26 | return this._actived; 27 | } 28 | 29 | set actived(actived: boolean) { 30 | this._actived = actived; 31 | const event = this.instance.context.event; 32 | event.trigger("actived") 33 | } 34 | 35 | get enabled() { 36 | return this._enabled; 37 | } 38 | 39 | set enabled(enabled: boolean) { 40 | this._enabled = enabled; 41 | } 42 | 43 | get selection() { 44 | return this._selection; 45 | } 46 | 47 | get document() { 48 | return this._document; 49 | } 50 | 51 | get window() { 52 | return this._window; 53 | } 54 | 55 | public mount(el: HTMLElement, selection?: Selection | null) { 56 | this._rootDocNode = el.getRootNode(); 57 | if (el.nodeType !== 1 || !el.ownerDocument || !this._rootDocNode || (this._rootDocNode.nodeType !== 9 && this._rootDocNode.nodeType !== 11)) throw "el is invalid!"; 58 | 59 | const { spellcheckEnabled = false } = this.instance.options 60 | this._holder = el; 61 | this._holder.setAttribute("contenteditable", "true"); 62 | 63 | this._holder.setAttribute("spellcheck", spellcheckEnabled ? "true" : "false"); 64 | this._holder.style.cursor = "text"; 65 | this._holder.dataset.root = 'root' 66 | 67 | this._document = el.ownerDocument; 68 | this._window = this._document.defaultView as Window; 69 | 70 | this._selection = new MESelection( 71 | el.ownerDocument, 72 | this._holder, 73 | this.instance 74 | ); 75 | 76 | this._actived = true; 77 | 78 | this.mutableListeners.on(this._holder, "mousedown", (event)=>{ 79 | const mouseEvent = event as MouseEvent; 80 | if(mouseEvent.ctrlKey || mouseEvent.metaKey) { 81 | const target = mouseEvent.target as Element; 82 | const anchor = target.nodeType === 3 ? target.parentElement?.closest("a") : target.closest("a"); 83 | if(anchor && anchor.href) { 84 | const {linkClick} = this.instance.options; 85 | if(linkClick) { 86 | linkClick(anchor.href) 87 | } else { 88 | this.window.open(anchor.href, "_blank") 89 | } 90 | event.preventDefault(); 91 | event.stopPropagation(); 92 | } 93 | } 94 | }, true); 95 | this.mutableListeners.on(this._holder, "mousemove", (event)=>{ 96 | const mouseEvent = event as MouseEvent; 97 | this._holder.classList.toggle(CLASS_NAMES.ME_CONTENT__CONTROLLING, mouseEvent.ctrlKey||mouseEvent.metaKey) 98 | }, true); 99 | this.mutableListeners.on(this._document, "keydown", (event)=>{ 100 | const keyEvent = event as KeyboardEvent; 101 | if(keyEvent.key === 'Control' || keyEvent.key === 'Meta') { 102 | this._holder.classList.toggle(CLASS_NAMES.ME_CONTENT__CONTROLLING, true) 103 | } 104 | }, true); 105 | this.mutableListeners.on(this._document, "keyup", (event)=>{ 106 | const keyEvent = event as KeyboardEvent; 107 | if(keyEvent.key === 'Control' || keyEvent.key === 'Meta') { 108 | this._holder.classList.toggle(CLASS_NAMES.ME_CONTENT__CONTROLLING, false) 109 | } 110 | }, true); 111 | 112 | return this; 113 | } 114 | 115 | destroy() { 116 | super.destroy(); 117 | this._actived = false; 118 | } 119 | 120 | } -------------------------------------------------------------------------------- /src/packages/modules/event.ts: -------------------------------------------------------------------------------- 1 | import { CLASS_NAMES } from "../utils/classNames"; 2 | import MEModule from "./module"; 3 | import { debounce } from "@/packages/utils/utils" 4 | 5 | export interface EventEmitterInstance { 6 | on: (types: string | string[], handler: Function) => void; 7 | off: (types: string | string[], handler: Function) => void; 8 | trigger: (types: string | string[], ...args: any[]) => void; 9 | } 10 | 11 | export default class MEEvent extends MEModule { 12 | listeners!: { [key: string]: Function[] }; 13 | async prepare(): Promise { 14 | this.bindEvents() 15 | return true 16 | } 17 | 18 | private bindEvents() { 19 | this.proxyDomEvent = this.proxyDomEvent.bind(this); 20 | this.selectionChange = debounce(this.selectionChange, 20); 21 | 22 | const { editable, layout } = this.instance.context; 23 | [ 24 | "keydown", 25 | "keyup", 26 | "keypress", 27 | "input", 28 | "selectstart", 29 | "focus", 30 | "blur", 31 | "compositionstart", 32 | "compositionend" 33 | ].forEach((type) => { 34 | this.mutableListeners.on( editable.holder, type, this.proxyDomEvent); 35 | }); 36 | 37 | [ 38 | "copy", 39 | "cut", 40 | "paste" 41 | ].forEach((type) => { 42 | this.mutableListeners.on(editable.holder, type, this.proxyDomEvent); 43 | }); 44 | 45 | [ 46 | "click", 47 | "contextmenu", 48 | "mousedown", 49 | "mouseup", 50 | "mouseover", 51 | "mouseout", 52 | "drop", 53 | "dragstart", 54 | "dragover" 55 | ].forEach((type) => { 56 | this.mutableListeners.on(layout.nodes.scroller, type, this.proxyDomEvent); 57 | }) 58 | 59 | const selectionChangeEvent = (evt: any) => { 60 | 61 | if (evt.button === 2) return; 62 | 63 | this.selectionChange(evt); 64 | } 65 | 66 | ["mouseup", "keydown"].forEach((type) => { 67 | this.mutableListeners.on(editable.holder, type, selectionChangeEvent); 68 | }) 69 | // Maybe move mouse out of holder 70 | this.mutableListeners.on(editable.document, 'mouseup', selectionChangeEvent); 71 | this.mutableListeners.on(editable.document, 'selectionchange', selectionChangeEvent); 72 | 73 | const preventSelection = (event) => { 74 | const { target } = event 75 | if (target.closest(`.${CLASS_NAMES.ME_TOOL}, .${CLASS_NAMES.ME_TOOLBAR}, .${CLASS_NAMES.ME_PREVIEW}`)) { 76 | event.preventDefault() 77 | return 78 | } 79 | } 80 | this.mutableListeners.on(editable.document, 'mousedown', preventSelection) 81 | } 82 | 83 | private proxyDomEvent(evt: Event) { 84 | const type = evt.type.replace(/^on/, "").toLowerCase(); 85 | this.trigger(type, evt); 86 | } 87 | 88 | selectionChange(evt?: Event) { 89 | if (!this.instance) { 90 | return; 91 | } 92 | const causeByUi = !!evt; 93 | const { selection } = this.instance.context.editable; 94 | selection.cache(); 95 | if (selection.cachedRange && selection.cachedCursor) { 96 | this.trigger("beforeselectionchange"); 97 | this.trigger("selectionchange", causeByUi); 98 | this.trigger("afterselectionchange"); 99 | // selection.clearCache(); 100 | } 101 | } 102 | private getListener(type: string) { 103 | this.listeners = this.listeners || {} 104 | return this.listeners[type] || (this.listeners[type] = [] as Function[]) 105 | } 106 | on(types: string | string[], listener: Function) { 107 | if (typeof types === 'string') { 108 | types = types.trim().split(/\s+/); 109 | } 110 | 111 | for (const type of types) { 112 | const listeners = this.getListener(type), 113 | index = listeners.findIndex((l) => l === listener); 114 | if (index === -1) { 115 | listeners.push(listener); 116 | } 117 | } 118 | } 119 | off(types: string | string[], listener: Function) { 120 | if (typeof types === 'string') { 121 | types = types.trim().split(/\s+/); 122 | } 123 | 124 | for (const type of types) { 125 | const listeners = this.getListener(type), 126 | index = listeners.findIndex((l) => l === listener); 127 | if (index !== -1) { 128 | listeners.splice(index, 1); 129 | } 130 | } 131 | } 132 | trigger(types: string | string[], ...args: any[]) { 133 | if (typeof types === 'string') { 134 | types = types.trim().split(/\s+/); 135 | } 136 | 137 | for (const type of types) { 138 | const listeners = this.getListener(type); 139 | const tagrs = [type, ...args]; 140 | listeners.forEach((listener) => { 141 | listener.apply(this, tagrs); 142 | }) 143 | } 144 | } 145 | 146 | } -------------------------------------------------------------------------------- /src/packages/modules/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import MEModule from "../module"; 2 | import resources from "./locales"; 3 | 4 | class MEI18n extends MEModule { 5 | 6 | private lang: string = 'en' 7 | private resources: { [lang: string]: { [key: string]: string } } 8 | constructor(instance) { 9 | super(instance) 10 | } 11 | 12 | async prepare(): Promise { 13 | this.lang = this.instance.options.locale?.lang || 'en' 14 | this.resources = { 15 | ...resources, 16 | ...(this.instance.options.locale?.resources || {}) 17 | } 18 | return true 19 | } 20 | 21 | t(key: string) { 22 | const { lang, resources } = this; 23 | return resources[lang]?.[key] || resources.en[key] || key; 24 | } 25 | 26 | addLocales(resources: { [lang: string]: { [key: string]: string } }, lang?: string) { 27 | this.resources = { 28 | ...this.resources, 29 | ...resources 30 | } 31 | this.lang = lang || this.lang 32 | } 33 | 34 | changeLanguage(lang: string) { 35 | this.lang = lang; 36 | } 37 | } 38 | 39 | export default MEI18n; -------------------------------------------------------------------------------- /src/packages/modules/i18n/locales/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Input code language': 'Code language', 3 | 'Double-click to select image': 'Double-click to select image', 4 | 'Loading...': 'Loading...', 5 | 'Invalid Diagram Code': 'Invalid Diagram Code', 6 | 'Empty Diagram': 'Empty Diagram' 7 | } -------------------------------------------------------------------------------- /src/packages/modules/i18n/locales/index.ts: -------------------------------------------------------------------------------- 1 | import zh from './zh' 2 | import en from './en' 3 | 4 | export default { 5 | zh, 6 | 'zh-CN': zh, 7 | 'zh-TW': zh, 8 | 'en-US': en, 9 | en 10 | } -------------------------------------------------------------------------------- /src/packages/modules/i18n/locales/zh.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Input code language': '代码语言', 3 | 'Double-click to select image': '双击选择图片', 4 | 'Loading...': '加载中...', 5 | 'Invalid Diagram Code': '无效图表', 6 | 'Empty Diagram': '空图表' 7 | } -------------------------------------------------------------------------------- /src/packages/modules/index.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import layout from './layout' 4 | import editable from './editable' 5 | import event from './event' 6 | import command from './command' 7 | import i18n from './i18n' 8 | import content from './content' 9 | import clipboard from './clipboard' 10 | import dragdrop from './dragdrop' 11 | import state from './state' 12 | import stack from './stack' 13 | import search from './search' 14 | import plugin from './plugin' 15 | 16 | export default { 17 | layout, 18 | editable, 19 | event, 20 | command, 21 | i18n, 22 | content, 23 | clipboard, 24 | dragdrop, 25 | state, 26 | stack, 27 | search, 28 | plugin 29 | } -------------------------------------------------------------------------------- /src/packages/modules/module.ts: -------------------------------------------------------------------------------- 1 | import { MEInstance, MEModuleInstance } from "../types"; 2 | 3 | export type ModuleNodes = object; 4 | export default abstract class MEModule implements MEModuleInstance { 5 | 6 | public readonly nodes: T = {} as any; 7 | public readonly mutableListeners = { 8 | on: ( 9 | element: EventTarget, 10 | eventType: string, 11 | handler: (event: Event) => void, 12 | options: boolean | AddEventListenerOptions = false 13 | ) => { 14 | const id = this.instance.eventListeners.on(element, eventType, (event: Event)=>{ 15 | const {editable} = this.instance.context; 16 | if(!editable.actived) return; 17 | handler(event); 18 | }, options) 19 | 20 | if(id) { 21 | this.mutableListenerIds.push(id); 22 | } 23 | 24 | return id; 25 | }, 26 | clearAll: (): void => { 27 | for (const id of this.mutableListenerIds) { 28 | this.instance.eventListeners.off(id); 29 | } 30 | 31 | this.mutableListenerIds = []; 32 | }, 33 | }; 34 | 35 | private mutableListenerIds: string[] = []; 36 | 37 | instance: MEInstance; 38 | constructor(instance: MEInstance) { 39 | this.instance = instance; 40 | } 41 | 42 | t(key: string) { 43 | return this.instance.t(key); 44 | } 45 | 46 | async prepare() { 47 | return true; 48 | }; 49 | 50 | destroy() { 51 | this.mutableListeners.clearAll(); 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/packages/modules/plugin.ts: -------------------------------------------------------------------------------- 1 | import MEModule from "@/packages/modules/module"; 2 | import { MEPluginConstructable, MEPluginInstance, MEPluginOptions } from "@/packages/types"; 3 | 4 | 5 | 6 | export default class MEPlugin extends MEModule { 7 | 8 | static plugins: { Plugin: MEPluginConstructable; options?: MEPluginOptions }[] = []; 9 | static use(Plugin: MEPluginConstructable, options?: MEPluginOptions) { 10 | this.plugins.push({ 11 | Plugin, 12 | options 13 | }) 14 | } 15 | 16 | plugins: { [key: string]: MEPluginInstance } = {} 17 | async prepare(): Promise { 18 | if (MEPlugin.plugins.length) { 19 | for (const { Plugin, options } of MEPlugin.plugins) { 20 | this.plugins[Plugin.pluginName] = new Plugin(this.instance, options); 21 | } 22 | } 23 | 24 | await Object.keys(this.plugins).reduce( 25 | (promise, pluginName) => promise.then(async () => { 26 | try { 27 | await this.plugins[pluginName].prepare(); 28 | } catch (e) { 29 | } 30 | }), 31 | Promise.resolve() 32 | ) 33 | 34 | return true 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/packages/modules/state/emptyStates.ts: -------------------------------------------------------------------------------- 1 | const emptyStates = { 2 | paragraph: { 3 | type: "paragraph", 4 | text: "", 5 | }, 6 | "thematic-break": { 7 | type: "thematic-break", 8 | text: "---", // --- or ___ or *** 9 | }, 10 | frontmatter: { 11 | type: "frontmatter", 12 | text: "", 13 | meta: { 14 | lang: "yaml", // yaml | toml | json 15 | style: "-", // `-` for yaml | `+` for toml | `;;;` and `{}` for json 16 | }, 17 | }, 18 | "atx-heading1": { 19 | type: "atx-heading1", 20 | meta: { 21 | }, 22 | text: "# ", // can not contain `\n`! 23 | }, 24 | table: { 25 | type: "table", 26 | children: [ 27 | { 28 | type: "table-thead", 29 | children: [ 30 | { 31 | type: "table-tr", 32 | children: [ 33 | { 34 | type: "table-th", 35 | meta: { 36 | align: "none", // none left center right, cells in the same column has the same alignment. 37 | }, 38 | text: "", 39 | }, 40 | { 41 | type: "table-th", 42 | meta: { 43 | align: "none", // none left center right, cells in the same column has the same alignment. 44 | }, 45 | text: "", 46 | }, 47 | ], 48 | }, 49 | ] 50 | }, 51 | { 52 | type: "table-tbody", 53 | children: [ 54 | { 55 | type: "table-tr", 56 | children: [ 57 | { 58 | type: "table-td", 59 | meta: { 60 | align: "none", // none left center right, cells in the same column has the same alignment. 61 | }, 62 | text: "", 63 | }, 64 | { 65 | type: "table-td", 66 | meta: { 67 | align: "none", // none left center right, cells in the same column has the same alignment. 68 | }, 69 | text: "", 70 | }, 71 | ], 72 | }, 73 | ] 74 | }, 75 | ], 76 | }, 77 | "math-block": { 78 | type: "math-block", 79 | text: "", 80 | meta: { 81 | mathStyle: "", // '' for `$$` and 'gitlab' for ```math 82 | }, 83 | }, 84 | "html-block": { 85 | type: "html-block", 86 | text: "
\n\n
", 87 | }, 88 | "code-block": { 89 | type: "code-block", 90 | meta: { 91 | type: "fenced", // indented or fenced 92 | lang: "", // lang will be enpty string if block is indented block. set language will auto change into fenced code block. 93 | }, 94 | text: "", 95 | children: [ 96 | { 97 | type: "language", 98 | text: "" 99 | }, 100 | { 101 | type: "code", 102 | meta: { 103 | lang: "" 104 | }, 105 | text: "" 106 | } 107 | ] 108 | }, 109 | "block-quote": { 110 | type: "block-quote", 111 | children: [ 112 | { 113 | // Can contains any type and number of leaf blocks. 114 | type: "paragraph", 115 | text: "", 116 | }, 117 | ], 118 | }, 119 | "order-list": { 120 | type: "order-list", 121 | meta: { 122 | start: 1, // 0 ~ 999999999 123 | loose: true, // true or false, true is loose list and false is tight. 124 | delimiter: ".", // . or ) 125 | }, 126 | children: [ 127 | // List Item 128 | { 129 | type: "list-item", // Can contains any type and number of leaf blocks. 130 | children: [ 131 | { 132 | type: "paragraph", 133 | text: "", 134 | }, 135 | ], 136 | }, 137 | ], 138 | }, 139 | "bullet-list": { 140 | type: "bullet-list", 141 | meta: { 142 | marker: "-", // - + * 143 | loose: false, // true or false 144 | }, 145 | children: [ 146 | // List Item 147 | { 148 | type: "list-item", // Can contains any type and number of leaf blocks. 149 | children: [ 150 | { 151 | type: "paragraph", 152 | text: "", 153 | }, 154 | ], 155 | }, 156 | ], 157 | }, 158 | "task-list": { 159 | type: "task-list", 160 | meta: { 161 | marker: "-", // - + * 162 | loose: false, 163 | }, 164 | children: [ 165 | { 166 | type: "task-list-item", 167 | meta: { 168 | checked: true, // true or false 169 | }, 170 | children: [ 171 | { 172 | type: "paragraph", 173 | text: "", 174 | }, 175 | ], 176 | }, 177 | ], 178 | }, 179 | "diagram-block": { 180 | type: "diagram-block", 181 | text: "", 182 | meta: { 183 | lang: "yaml", 184 | type: "mermaid", 185 | }, 186 | }, 187 | }; 188 | 189 | export default emptyStates; 190 | -------------------------------------------------------------------------------- /src/packages/modules/state/htmlToMarkdown.ts: -------------------------------------------------------------------------------- 1 | import TurndownService, { DEFAULT_TURNDOWN_CONFIG, addPluginAddRules } from "@/packages/utils/turndownService"; 2 | 3 | 4 | // Just because turndown change `\n`(soft line break) to space, So we add `span.ag-soft-line-break` to workaround. 5 | const turnSoftBreakToSpan = (html) => { 6 | const parser = new DOMParser(); 7 | const doc = parser.parseFromString( 8 | `${html}`, 9 | "text/html" 10 | ); 11 | const root = doc.querySelector("#turn-root"); 12 | const travel = (childNodes) => { 13 | for (const node of childNodes) { 14 | if (node.nodeType === 3 && node.parentNode.tagName !== "CODE") { 15 | let startLen = 0; 16 | let endLen = 0; 17 | const text = node.nodeValue 18 | .replace(/^(\n+)/, (_, p) => { 19 | startLen = p.length; 20 | 21 | return ""; 22 | }) 23 | .replace(/(\n+)$/, (_, p) => { 24 | endLen = p.length; 25 | 26 | return ""; 27 | }); 28 | if (/\n/.test(text)) { 29 | const tokens = text.split("\n"); 30 | const params = []; 31 | let i = 0; 32 | const len = tokens.length; 33 | 34 | for (; i < len; i++) { 35 | let text = tokens[i]; 36 | if (i === 0 && startLen !== 0) { 37 | text = "\n".repeat(startLen) + text; 38 | } else if (i === len - 1 && endLen !== 0) { 39 | text = text + "\n".repeat(endLen); 40 | } 41 | params.push(document.createTextNode(text)); 42 | if (i !== len - 1) { 43 | const softBreak = document.createElement("span"); 44 | softBreak.classList.add("mu-soft-line-break"); 45 | params.push(softBreak); 46 | } 47 | } 48 | node.replaceWith(...params); 49 | } 50 | } else if (node.nodeType === 1) { 51 | travel(node.childNodes); 52 | } 53 | } 54 | }; 55 | travel(root.childNodes); 56 | 57 | return root.innerHTML.trim(); 58 | }; 59 | 60 | export default class HtmlToMarkdown { 61 | private options = {}; 62 | 63 | constructor(options = {}) { 64 | this.options = Object.assign( 65 | {}, 66 | DEFAULT_TURNDOWN_CONFIG, 67 | options 68 | ); 69 | } 70 | 71 | generate(html, keeps = []) { 72 | // turn html to markdown 73 | const { options } = this; 74 | const turndownService = new TurndownService(options); 75 | addPluginAddRules(turndownService, keeps); 76 | 77 | // fix #752, but I don't know why the   vanlished. 78 | html = html.replace(/ <\/span>/g, String.fromCharCode(160)); 79 | 80 | html = turnSoftBreakToSpan(html); 81 | const markdown = turndownService.turndown(html); 82 | 83 | return markdown; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/packages/modules/state/index.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockData, MEContentType, MECursorState, MEStateScene } from "@/packages/types"; 2 | import MEModule from "../module"; 3 | import MarkdownToState from './markdownToState'; 4 | import StateToMarkdown from './stateToMarkdown'; 5 | import HtmlToMarkdown from "./htmlToMarkdown"; 6 | import StateToHtml from "./stateToHtml"; 7 | import StateToPlainText from "./stateToPlainText"; 8 | import { collectReferenceDefinitions } from "./utils"; 9 | 10 | 11 | 12 | 13 | class MEState extends MEModule { 14 | 15 | 16 | markdownToState: MarkdownToState; 17 | stateToMarkdown: StateToMarkdown; 18 | htmlToMarkdown: HtmlToMarkdown; 19 | stateToHtml: StateToHtml; 20 | stateToPlainText: StateToPlainText; 21 | state: MEBlockData; 22 | labels: Map; 23 | 24 | constructor(instance) { 25 | super(instance) 26 | 27 | this.markdownToState = new MarkdownToState(); 28 | this.stateToMarkdown = new StateToMarkdown(); 29 | this.htmlToMarkdown = new HtmlToMarkdown(); 30 | this.stateToHtml = new StateToHtml({diagramHtmlType: instance.options.diagramHtmlType}); 31 | this.stateToPlainText = new StateToPlainText(); 32 | } 33 | 34 | 35 | async prepare(): Promise { 36 | return true 37 | } 38 | 39 | 40 | setContent(text: string, type: MEContentType = "md") { 41 | if (type === 'html') { 42 | text = this.htmlToMarkdown.generate(text) 43 | } 44 | const { content, stack, event } = this.instance.context; 45 | const data = this.markdownToState.generate(text); 46 | this.labels = collectReferenceDefinitions(data.children); 47 | content.render({ data }); 48 | content.setDefaultCursor(); 49 | stack.reset(); 50 | requestAnimationFrame(()=>{ 51 | const currentScene = this.getScene(); 52 | event.trigger("aftersetcontent", currentScene); 53 | }) 54 | 55 | } 56 | 57 | async getContent(type: MEContentType = "md") { 58 | 59 | const { content } = this.instance.context; 60 | const data = content.data; 61 | 62 | if (type === 'text') { 63 | const text = this.stateToPlainText.generate(data.children || []) 64 | return text; 65 | } else if (type === 'html') { 66 | const html = await this.stateToHtml.generate(data.children || []) 67 | return html; 68 | } else { 69 | const markdown = this.stateToMarkdown.generate(data.children || []) 70 | return markdown; 71 | } 72 | } 73 | 74 | getWordCount() { 75 | const { content } = this.instance.context; 76 | const data = content.data; 77 | const text = this.stateToPlainText.generate(data.children || []); 78 | const pattern = /[a-zA-Z0-9_\u0392-\u03c9]+|[\u4E00-\u9FFF\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af]+/g; 79 | const match = text.match(pattern); 80 | let count = 0; 81 | if (match == null) { return count; } 82 | for (var i = 0; i < match.length; i++) { 83 | if (match[i].charCodeAt(0) >= 0x4E00) { 84 | count += match[i].length; 85 | } else { 86 | count += 1; 87 | } 88 | } 89 | return count; 90 | } 91 | 92 | 93 | 94 | getScene() { 95 | const { content, editable } = this.instance.context; 96 | const { anchorBlock, focusBlock, ...cursor } = editable.selection.cursor; 97 | const state = content.data; 98 | 99 | 100 | return { 101 | state, 102 | cursor 103 | } 104 | 105 | } 106 | 107 | setScene({ state, cursor }: MEStateScene) { 108 | try { 109 | const data = state 110 | this.labels = collectReferenceDefinitions(data.children) 111 | const { content, editable } = this.instance.context 112 | content.render({ data, cursor }) 113 | editable.selection.setCursor(cursor) 114 | } catch (error) { 115 | 116 | } 117 | } 118 | } 119 | 120 | export default MEState; -------------------------------------------------------------------------------- /src/packages/modules/state/stateToHtml.ts: -------------------------------------------------------------------------------- 1 | 2 | import { MEBlockData, MEDiagramHtmlType, MENodeData } from "@/packages/types"; 3 | import { tokenizer } from "../content/inlineRenderers/tokenizer"; 4 | import { collectReferenceDefinitions } from "./utils"; 5 | import { blockStaticRender } from "../content/blockRenderers"; 6 | import { nodeStaticRender } from "../content/inlineRenderers"; 7 | 8 | export default class StateToHtml { 9 | private diagramHtmlType: MEDiagramHtmlType | { mermaid?: MEDiagramHtmlType; flowchart?: MEDiagramHtmlType; sequence?: MEDiagramHtmlType; "vega-lite"?: MEDiagramHtmlType; plantuml?: MEDiagramHtmlType } = 'svg'; 10 | private labels: Map; 11 | 12 | constructor(options?: { diagramHtmlType?: MEDiagramHtmlType | { mermaid?: MEDiagramHtmlType; flowchart?: MEDiagramHtmlType; sequence?: MEDiagramHtmlType; "vega-lite"?: MEDiagramHtmlType; plantuml?: MEDiagramHtmlType } }) { 13 | if (options) { 14 | this.diagramHtmlType = options.diagramHtmlType || 'svg' 15 | } 16 | } 17 | 18 | async generate(states: MEBlockData[], asFile?: boolean) { 19 | this.labels = collectReferenceDefinitions(states); 20 | const html = await this.convertStatesToHtml(states); 21 | return html; 22 | } 23 | 24 | async convertStatesToHtml(states: MEBlockData[]) { 25 | const result: string[] = []; 26 | for (const state of states) { 27 | const innerHTML = /(code|html-block|math-block|diagram-block|language)$/.test(state.type) ? '' : (state.children && state.children.length) ? await this.convertStatesToHtml(state.children) : await this.stateToHtml(state); 28 | 29 | let diagramHtmlType: MEDiagramHtmlType = 'svg' 30 | if (/diagram-block/.test(state.type)) { 31 | if (typeof this.diagramHtmlType === 'string') { 32 | diagramHtmlType = this.diagramHtmlType 33 | } else if (this.diagramHtmlType && this.diagramHtmlType[state.meta.type]) { 34 | diagramHtmlType = this.diagramHtmlType[state.meta.type] 35 | } 36 | } 37 | 38 | const html = await blockStaticRender(state.type, { innerHTML, diagramHtmlType, data: state }) 39 | result.push(html) 40 | } 41 | 42 | return result.join(""); 43 | } 44 | 45 | async stateToHtml(state: MEBlockData) { 46 | if (!state.text) { 47 | return '' 48 | } 49 | const { labels } = this 50 | const hasBeginRules = /thematic-break|paragraph|atx-heading/.test(state.type) 51 | const tokens = tokenizer(state.text, { hasBeginRules, labels }); 52 | return this.tokensToHtml(tokens) 53 | } 54 | 55 | async tokensToHtml(tokens: MENodeData[]) { 56 | const { labels } = this 57 | const result: string[] = []; 58 | for (const token of tokens) { 59 | const innerHTML = (token.children && token.children.length) ? await this.tokensToHtml(token.children) : token.content || ""; 60 | const html = await nodeStaticRender(token.type, { innerHTML, data: token, labels }) 61 | result.push(html) 62 | } 63 | 64 | return result.join("") 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/packages/modules/state/stateToPlainText.ts: -------------------------------------------------------------------------------- 1 | 2 | import { MEBlockData, MENodeData } from "@/packages/types"; 3 | import { tokenizer } from "../content/inlineRenderers/tokenizer"; 4 | import { collectReferenceDefinitions } from "./utils"; 5 | 6 | export default class StateToPlainText { 7 | private labels: Map; 8 | 9 | generate(states: MEBlockData[], asFile?: boolean) { 10 | this.labels = collectReferenceDefinitions(states); 11 | const plainText = this.convertStatesToPlainText(states); 12 | return plainText; 13 | } 14 | 15 | convertStatesToPlainText(states: MEBlockData[]) { 16 | const result: string[] = []; 17 | for (const state of states) { 18 | const plainText = /(code|html-block|math-block|diagram-block|language)$/.test(state.type) ? state.text || '' : (state.children && state.children.length) ? this.convertStatesToPlainText(state.children) : this.stateToPlainText(state); 19 | result.push(plainText) 20 | } 21 | 22 | return result.join("\n"); 23 | } 24 | 25 | stateToPlainText(state: MEBlockData) { 26 | if (!state.text) { 27 | return '' 28 | } 29 | const { labels } = this 30 | const hasBeginRules = /thematicbreak|paragraph|atxheading/.test(state.type) 31 | const tokens = tokenizer(state.text, { hasBeginRules, labels }); 32 | return this.tokensToPlainText(tokens) 33 | } 34 | 35 | tokensToPlainText(tokens: MENodeData[]) { 36 | const { labels } = this 37 | const result: string[] = []; 38 | for (const token of tokens) { 39 | const plainText = (token.children && token.children.length) ? this.tokensToPlainText(token.children) : token.content || ""; 40 | result.push(plainText) 41 | } 42 | 43 | return result.join("") 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/packages/modules/state/utils.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockData } from "@/packages/types"; 2 | import { beginRules } from "../content/inlineRenderers/tokenizer/rules"; 3 | 4 | export function collectReferenceDefinitions(state: MEBlockData[]) { 5 | 6 | const labels = new Map() 7 | 8 | const travel = sts => { 9 | if (Array.isArray(sts) && sts.length) { 10 | for (const st of sts) { 11 | if (st.name === 'paragraph') { 12 | const { label, info } = getLabelInfo(st) 13 | if (label && info) { 14 | labels.set(label, info) 15 | } 16 | } else if (st.children) { 17 | travel(st.children) 18 | } 19 | } 20 | } 21 | } 22 | 23 | travel(state) 24 | 25 | return labels 26 | } 27 | 28 | export function getLabelInfo(block: MEBlockData) { 29 | const { text } = block 30 | const tokens = beginRules.reference_definition.exec(text) 31 | let label = null 32 | let info = null 33 | if (tokens) { 34 | label = (tokens[2] + tokens[3]).toLowerCase() 35 | info = { 36 | href: tokens[6], 37 | title: tokens[10] || '' 38 | } 39 | } 40 | 41 | return { label, info } 42 | } -------------------------------------------------------------------------------- /src/packages/plugins/base.ts: -------------------------------------------------------------------------------- 1 | import MEModule from "@/packages/modules/module"; 2 | import { MEInstance, MEPluginInstance, MEPluginOptions } from "@/packages/types"; 3 | 4 | export default class MEPluginBase extends MEModule implements MEPluginInstance { 5 | protected options: MEPluginOptions; 6 | constructor(instance: MEInstance, options?: MEPluginOptions) { 7 | super(instance) 8 | this.options = options || {} 9 | } 10 | } -------------------------------------------------------------------------------- /src/packages/plugins/contextMenu/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekeditor/meditable/7219827c6eb17823911d03dab2bbf500db74a31e/src/packages/plugins/contextMenu/index.less -------------------------------------------------------------------------------- /src/packages/plugins/contextMenu/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import MEPluginBase from "../base"; 3 | 4 | export default class MEPluginContextMenu extends MEPluginBase { 5 | static pluginName = "contextMenu" 6 | async prepare(): Promise { 7 | const { layout } = this.instance.context; 8 | this.mutableListeners.on(layout.nodes.wrapper, 'contextmenu', (event: Event) => { 9 | event.preventDefault() 10 | event.stopPropagation() 11 | }) 12 | return true; 13 | } 14 | } -------------------------------------------------------------------------------- /src/packages/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import MEPluginContextMenu from "./contextMenu" 2 | export {MEPluginContextMenu} -------------------------------------------------------------------------------- /src/packages/styles/highlight-code.less: -------------------------------------------------------------------------------- 1 | 2 | .hljs-comment, 3 | .hljs-quote { 4 | color: #afafaf; 5 | font-style: italic 6 | } 7 | 8 | .hljs-keyword, 9 | .hljs-selector-tag, 10 | .hljs-subst { 11 | color: #ca7d37 12 | } 13 | 14 | .hljs-number, 15 | .hljs-literal, 16 | .hljs-variable, 17 | .hljs-template-variable, 18 | .hljs-tag .hljs-attr { 19 | color: #0e9ce5 20 | } 21 | 22 | .hljs-string, 23 | .hljs-doctag { 24 | color: rgba(33, 181, 111, 1) 25 | } 26 | 27 | .hljs-title, 28 | .hljs-section, 29 | .hljs-selector-id { 30 | color: #d14 31 | } 32 | 33 | .hljs-subst { 34 | font-weight: normal 35 | } 36 | 37 | .hljs-type, 38 | .hljs-class .hljs-title { 39 | color: #0e9ce5 40 | } 41 | 42 | .hljs-tag, 43 | .hljs-name, 44 | .hljs-attribute { 45 | color: #0e9ce5; 46 | font-weight: normal 47 | } 48 | 49 | .hljs-regexp, 50 | .hljs-link { 51 | color: #ca7d37 52 | } 53 | 54 | .hljs-symbol, 55 | .hljs-bullet { 56 | color: #d14 57 | } 58 | 59 | .hljs-built_in, 60 | .hljs-builtin-name { 61 | color: #ca7d37 62 | } 63 | 64 | .hljs-meta { 65 | color: #afafaf 66 | } 67 | 68 | .hljs-deletion { 69 | background: #fdd 70 | } 71 | 72 | .hljs-addition { 73 | background: #dfd 74 | } 75 | 76 | .hljs-emphasis { 77 | font-style: italic 78 | } 79 | 80 | .hljs-strong { 81 | font-weight: bold 82 | } -------------------------------------------------------------------------------- /src/packages/styles/index.less: -------------------------------------------------------------------------------- 1 | @import '../libs/sequence/sequence-diagram.less'; 2 | @import './theme.less'; 3 | @import './editor.less'; 4 | @import './typeset.less'; 5 | @import './highlight-code.less'; -------------------------------------------------------------------------------- /src/packages/styles/theme.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | .meui-wrapper { 4 | --primary-color: rgba(33, 181, 111, 1); 5 | --highlight-color: rgba(33, 181, 111, 0.4); 6 | --selection-color: rgba(45, 170, 219, 0.3); 7 | --editor-color: rgba(0, 0, 0, 0.7); 8 | --editor-color-80: rgba(0, 0, 0, 0.8); 9 | --editor-color-50: rgba(0, 0, 0, 0.5); 10 | --editor-color-30: rgba(0, 0, 0, 0.3); 11 | --editor-color-10: rgba(0, 0, 0, 0.1); 12 | --editor-color-04: rgba(0, 0, 0, 0.03); 13 | --editor-bg-color: rgba(255, 255, 255, 1); 14 | --delete-color: #ff6969; 15 | --icon-color: #6b737b; 16 | --code-block-bg-color: rgba(0, 0, 0, 0.03); 17 | --table-border-color: #e5e5e5; 18 | --input-bg-color: rgba(0, 0, 0, 0.06); 19 | --button-font-color: var(--editor-color); 20 | --button-bg-color: #fff; 21 | --button-border: 1px solid #dcdfe6; 22 | --button-bg-color-hover: linear-gradient(#f9f9f9, #f2f2f2); 23 | --button-border-hover: var(--button-border); 24 | --float-bg-color: #fff; 25 | --float-hover-color: rgba(0, 0, 0, 0.04); 26 | --float-border-color: rgba(0, 0, 0, 0.1); 27 | --float-shadow: rgba(15, 15, 15, 0.03) 0 0 0 1px, 28 | rgba(15, 15, 15, 0.04) 0 3px 6px, rgba(15, 15, 15, 0.05) 0 9px 24px; 29 | 30 | 31 | --editor-bgcolor: #f9f9f9; 32 | --editor-content-bgcolor: #fff; 33 | --editor-border-color: #e8e8e8; 34 | --editor-box-shadow: 0 2px 8px rgb(115 115 115 / 8%); 35 | --editor-toolbar-bgcolor: #fdfdfd; 36 | --editor-toolbar-bdcolor: #eaeaea; 37 | --editor-toolbar-btncolor: #000; 38 | --editor-toolbar-hover-color: #eff2f5; 39 | --editor-toolbar-setting-bgcolor: #eff2f5; 40 | --dark-color: black; 41 | --placeholder-color: rgba(112, 118, 132, 0.2); 42 | --block-selected-bgcolor: #d4ecff; 43 | 44 | --editor-scrollbar-shadow: #dddddd; 45 | } 46 | 47 | 48 | .meui-wrapper.dark-mode { 49 | --editor-bgcolor: #030303; 50 | --editor-content-bgcolor: #101010; 51 | --editor-border-color: #1f1f1f; 52 | --editor-box-shadow: 0 2px 8px rgb(0 0 0 / 8%); 53 | --editor-toolbar-bgcolor: #303030; 54 | --editor-toolbar-bdcolor: #1f1f1f; 55 | --editor-toolbar-btncolor: #fff; 56 | --editor-toolbar-hover-color: #000; 57 | --editor-toolbar-setting-bgcolor: #303030; 58 | --dark-color: rgba(255, 255, 255, 0.8); 59 | --placeholder-color: rgba(255, 255, 255, 0.8); 60 | --block-selected-bgcolor: #56779c; 61 | --editor-scrollbar-shadow: #dddddd; 62 | } -------------------------------------------------------------------------------- /src/packages/styles/typeset-code.less: -------------------------------------------------------------------------------- 1 | /* code html math ...code block style */ 2 | pre.me-block--actived[data-block-type="frontmatter"]::before, 3 | pre.me-block--actived[data-block-type="frontmatter"]::after { 4 | content: '---'; 5 | } 6 | 7 | .me-block--actived[data-block-type="math-block"] pre::before, 8 | .me-block--actived[data-block-type="math-block"] pre::after { 9 | content: '$$'; 10 | } 11 | 12 | .me-block--actived[data-block-type="diagram-block"] pre[data-role="mermaid"]::before { 13 | content: '``` mermaid'; 14 | } 15 | 16 | .me-block--actived[data-block-type="diagram-block"] pre[data-role="flowchart"]::before { 17 | content: '``` flowchart'; 18 | } 19 | 20 | .me-block--actived[data-block-type="diagram-block"] pre[data-role="sequence"]::before { 21 | content: '``` sequence'; 22 | } 23 | 24 | .me-block--actived[data-block-type="diagram-block"] pre[data-role="plantuml"]::before { 25 | content: '``` plantuml'; 26 | } 27 | 28 | .me-block--actived[data-block-type="diagram-block"] pre[data-role="vega-lite"]::before { 29 | content: '``` vega-lite'; 30 | } 31 | 32 | pre.me-block--actived[data-code-type="fenced"]::before, 33 | pre.me-block--actived[data-code-type="fenced"]::after, 34 | .me-block--actived[data-block-type="diagram-block"] pre::after { 35 | content: '```'; 36 | } 37 | 38 | pre.me-block--actived[data-block-type="frontmatter"]::before, 39 | pre.me-block--actived[data-block-type="frontmatter"]::after, 40 | .me-block--actived[data-block-type="diagram-block"] pre::before, 41 | .me-block--actived[data-block-type="diagram-block"] pre::after, 42 | pre.me-block--actived[data-code-type="fenced"]::before, 43 | pre.me-block--actived[data-code-type="fenced"]::after, 44 | .me-block--actived[data-block-type="math-block"] pre::before, 45 | .me-block--actived[data-block-type="math-block"] pre::after { 46 | position: absolute; 47 | left: 0; 48 | color: var(--editor-color-30); 49 | font-weight: 100; 50 | } 51 | 52 | pre.me-block--actived[data-block-type="frontmatter"]::before, 53 | .me-block--actived[data-block-type="diagram-block"] pre::before, 54 | .me-block--actived[data-block-type="math-block"] pre::before, 55 | pre.me-block--actived[data-code-type="fenced"]::before { 56 | top: -20px; 57 | } 58 | 59 | pre.me-block--actived[data-block-type="frontmatter"]::after, 60 | .me-block--actived[data-block-type="diagram-block"] pre::after, 61 | .me-block--actived[data-block-type="math-block"] pre::after, 62 | pre.me-block--actived[data-code-type="fenced"]::after { 63 | bottom: -23px; 64 | } 65 | 66 | .me-block--actived[data-code-type="fenced"] [data-block-type="code"]::after { 67 | content: 'fenced'; 68 | } 69 | 70 | .me-block--actived[data-code-type="infenced"] [data-block-type="code"]::after { 71 | content: 'indented'; 72 | } 73 | 74 | .me-block--actived[data-code-type="fenced"] [data-block-type="code"]::after, 75 | .me-block--actived[data-code-type="infenced"] [data-block-type="code"]::after { 76 | position: absolute; 77 | right: .5em; 78 | bottom: 0; 79 | z-index: 1; 80 | 81 | color: var(--editor-color-30); 82 | } 83 | 84 | .me-language { 85 | position: absolute; 86 | top: 0; 87 | left: 0; 88 | z-index: -1; 89 | 90 | min-width: 20em; 91 | 92 | font-weight: 600; 93 | font-size: .8em; 94 | 95 | opacity: 0; 96 | } 97 | 98 | .me-block--actived[data-block-type="code-block"] .me-language { 99 | top: -1.8em; 100 | z-index: initial; 101 | opacity: 1; 102 | } 103 | 104 | .me-block--actived[data-block-type="code-block"] .me-language:empty::after { 105 | color: var(--editor-color-10); 106 | content: attr(hint); 107 | } 108 | 109 | .me-block--actived[data-code-type="fenced"] .me-language { 110 | left: 30px; 111 | } -------------------------------------------------------------------------------- /src/packages/types/asset.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' 2 | declare module '*.png' 3 | declare module '*.jpg' 4 | declare module '*.jpeg' 5 | declare module '*.gif' 6 | declare module '*.bmp' 7 | declare module '*.tiff' -------------------------------------------------------------------------------- /src/packages/umd.ts: -------------------------------------------------------------------------------- 1 | import MEditable from "."; 2 | require('./styles/index.less') 3 | 4 | export default MEditable; -------------------------------------------------------------------------------- /src/packages/utils/classNames.ts: -------------------------------------------------------------------------------- 1 | 2 | export const genUpper2LowerKeyHash = (keys) => { 3 | return keys.reduce((acc, key) => { 4 | const value = key.toLowerCase().replace(/_/g, "-"); 5 | 6 | return Object.assign(acc, { [key]: value }); 7 | }, {}); 8 | }; 9 | 10 | export const CLASS_NAMES = genUpper2LowerKeyHash([ 11 | // Editor 12 | "MEUI_FRAME", 13 | "MEUI_WRAPPER", 14 | "MEUI_SCROLLER", 15 | "MEUI_EDITOR", 16 | "MEUI_PRESENTATION", 17 | "MEUI_SCROLLER_DECORATION", 18 | "MEUI_EDITOR__WEB", 19 | "MEUI_EDITOR__PRINT", 20 | "MEUI_EDITOR__MOBILE", 21 | "MEUI_EDITOR__EMPTY", 22 | "MEUI_EDITOR__FOCUS_MODE", 23 | "MEUI_EDITOR__ACTIVE", 24 | "MEUI_EDITOR_ZONE", 25 | "MEUI_EDITOR_CONTENT", 26 | 27 | // Block 28 | "ME_CONTENT", 29 | "ME_CONTENT__SEARCHING", 30 | "ME_CONTENT__CONTROLLING", 31 | "ME_BLOCK", 32 | "ME_BLOCK__FOCUSED", 33 | "ME_BLOCK__ACTIVED", 34 | "ME_BLOCK__SELECTED", 35 | "ME_BLOCK__DROP_TARGET", 36 | "ME_EDITABLE", 37 | "ME_LANGUAGE", 38 | "ME_CODE", 39 | "ME_CODE_COPY", 40 | "ME_CODE_COPY__SUCCESS", 41 | "ME_PREVIEW", 42 | "ME_PREVIEW_CONTENT", 43 | "ME_TOOLBAR", 44 | "ME_TOOL", 45 | "ME_TOOL__SELECT", 46 | "ME_TOOL__COPY", 47 | "ME_TOOL__SUCCESS", 48 | "ME_TOOL__VIEW", 49 | "ME_TOOL__DOWNLOAD", 50 | "ME_CONTAINER", 51 | "ME_TASK_LIST", 52 | "ME_TASK_LIST_ITEM_CHECKBOX", 53 | "ME_TASK_LIST_ITEM_CHECKBOX__CHECKED", 54 | "ME_TASK_LIST_ITEM_CONTENT", 55 | 56 | // Node 57 | "ME_NODE", 58 | "ME_TEXT", 59 | "ME_NODE__ACTIVED", 60 | "ME_MARKER", 61 | "ME_FIXED_MARKER", 62 | "MU_HEADER_SPACE", 63 | "ME_MATH", 64 | "ME_MATH_ERROR", 65 | "ME_MATH_MARKER", 66 | "ME_INLINE_RENDER", 67 | "ME_RUBY", 68 | "ME_RUBY_TEXT", 69 | "ME_SOFT_LINE_BREAK", 70 | "ME_LINE_END", 71 | "ME_HTML_VALID_TAG", 72 | "ME_HTML_ESCAPE", 73 | "ME_HTML_IMG", 74 | "ME_EMOJI", 75 | "ME_EMOJI_VALID", 76 | "ME_TAIL_HEADER", 77 | "ME_REFERENCE_IMAGE", 78 | "ME_IMAGE", 79 | "ME_IMAGE_ICON", 80 | "ME_IMAGE_LOADING_ICON", 81 | "ME_IMAGE_EMPTY", 82 | "ME_IMAGE_LOADING", 83 | "ME_IMAGE_ERROR", 84 | "ME_IMAGE_TOOLBAR", 85 | 86 | 87 | 88 | 89 | "ME_AUTO_LINK", 90 | "ME_AUTO_LINK_EXTENSION", 91 | "ME_BACKLASH", 92 | "ME_BUG", 93 | "ME_BULLET_LIST", 94 | "ME_BULLET_LIST_ITEM", 95 | "ME_CHECKBOX_CHECKED", 96 | "ME_CONTAINER_BLOCK", 97 | "ME_CONTAINER_PREVIEW", 98 | "ME_CONTAINER_ICON", 99 | "ME_COPY_REMOVE", 100 | "ME_DISABLE_HTML_RENDER", 101 | "ME_EMOJI_MARKED_TEXT", 102 | "ME_EMOJI_MARKER", 103 | "ME_EMPTY", 104 | "ME_FENCE_CODE", 105 | "ME_FLOWCHART", 106 | "ME_FOCUS_MODE", 107 | "ME_FRONT_MATTER", 108 | "ME_FRONT_ICON", 109 | "ME_GRAY", 110 | "ME_HARD_LINE_BREAK", 111 | "ME_HARD_LINE_BREAK_SPACE", 112 | 113 | "ME_HEADER_TIGHT_SPACE", 114 | "ME_HIDE", 115 | "ME_HIGHLIGHT", 116 | "ME_HTML_BLOCK", 117 | 118 | "ME_HTML_PREVIEW", 119 | "ME_HTML_TAG", 120 | "ME_IMAGE_FAIL", 121 | "ME_IMAGE_BUTTONS", 122 | "ME_IMAGE_LOADING", 123 | "ME_EMPTY_IMAGE", 124 | "ME_IMAGE_MARKED_TEXT", 125 | "ME_IMAGE_SRC", 126 | "ME_IMAGE_CONTAINER", 127 | "ME_INLINE_IMAGE", 128 | "ME_IMAGE_SUCCESS", 129 | "ME_IMAGE_UPLOADING", 130 | "ME_INLINE_IMAGE_SELECTED", 131 | "ME_INLINE_IMAGE_IS_EDIT", 132 | "ME_INDENT_CODE", 133 | "ME_INLINE_FOOTNOTE_IDENTIFIER", 134 | "ME_INLINE_RULE", 135 | "ME_LANGUAGE", 136 | "ME_LANGUAGE_INPUT", 137 | "ME_LINK", 138 | "ME_LINK_IN_BRACKET", 139 | "ME_LIST_ITEM", 140 | "ME_LOOSE_LIST_ITEM", 141 | 142 | "ME_SELECTED", 143 | 144 | "ME_MERMAID", 145 | "ME_MULTIPLE_MATH", 146 | "ME_NOTEXT_LINK", 147 | "ME_ORDER_LIST", 148 | "ME_ORDER_LIST_ITEM", 149 | "ME_OUTPUT_REMOVE", 150 | "ME_PARAGRAPH", 151 | "ME_RAW_HTML", 152 | "ME_REFERENCE_LABEL", 153 | "ME_REFERENCE_LINK", 154 | "ME_REFERENCE_MARKER", 155 | "ME_REFERENCE_TITLE", 156 | "ME_REMOVE", 157 | "ME_SELECTION", 158 | "ME_SEQUENCE", 159 | "ME_SHOW_PREVIEW", 160 | "ME_TASK_LIST", 161 | "ME_TASK_LIST_ITEM", 162 | "ME_TASK_LIST_ITEM_CHECKBOX", 163 | "ME_TIGHT_LIST_ITEM", 164 | "ME_TOOL_BAR", 165 | "ME_VEGA_LITE", 166 | "ME_WARN", 167 | "ME_SHOW_QUICK_INSERT_HINT", 168 | ]); -------------------------------------------------------------------------------- /src/packages/utils/diagram/index.ts: -------------------------------------------------------------------------------- 1 | import sanitize, { PREVIEW_DOMPURIFY_CONFIG } from "../dompurify"; 2 | import { vega } from "vega-embed"; 3 | 4 | const rendererCache = new Map(); 5 | /** 6 | * 7 | * @param {string} name the renderer name: sequence, plantuml, flowchart, mermaid, vega-lite 8 | */ 9 | const loadRenderer = async (name) => { 10 | if (!rendererCache.has(name)) { 11 | let m; 12 | switch (name) { 13 | case "sequence": 14 | m = await import("./sequence"); 15 | rendererCache.set(name, m.default); 16 | break; 17 | 18 | case "plantuml": 19 | m = await import("./plantuml"); 20 | rendererCache.set(name, m.default); 21 | break; 22 | 23 | case "flowchart": 24 | m = await import("flowchart.js"); 25 | rendererCache.set(name, m.default); 26 | break; 27 | 28 | case "mermaid": 29 | m = await import("mermaid"); 30 | rendererCache.set(name, m.default); 31 | break; 32 | 33 | case "vega-lite": 34 | m = await import("vega-embed"); 35 | rendererCache.set(name, m.default); 36 | break; 37 | default: 38 | throw new Error(`Unknown diagram name ${name}`); 39 | } 40 | } 41 | 42 | return rendererCache.get(name); 43 | }; 44 | 45 | const observeRenderComplete = (target, renderCallback): Promise=> { 46 | 47 | return new Promise((resolve)=>{ 48 | const observer = new MutationObserver((mutations)=>{ 49 | mutations.forEach((mutation)=>{ 50 | if(mutation.addedNodes.length) { 51 | observer.disconnect() 52 | resolve(true) 53 | } 54 | }) 55 | }) 56 | 57 | observer.observe(target, {childList: true}) 58 | renderCallback() 59 | }) 60 | 61 | } 62 | 63 | const renderDiagram = async ({ type, code, target, theme }: {type: string; code: string; target?: HTMLElement; theme?: string}):Promise => { 64 | const render = await loadRenderer(type) 65 | target = target || document.createElement('div') 66 | const options = {} 67 | if (type === 'sequence') { 68 | Object.assign(options, { theme }) 69 | } else if (type === 'vega-lite') { 70 | Object.assign(options, { 71 | actions: false, 72 | tooltip: false, 73 | renderer: 'svg', 74 | theme 75 | }) 76 | } 77 | 78 | if (type === 'flowchart' || type === 'sequence') { 79 | const diagram = render.parse(code) 80 | target.innerHTML = '' 81 | await observeRenderComplete(target, ()=>{ 82 | diagram.drawSVG(target, options) 83 | }) 84 | } else if (type === 'plantuml') { 85 | const diagram = render.parse(code) 86 | target.innerHTML = '' 87 | await diagram.insertElement(target) 88 | } else if (type === 'vega-lite') { 89 | await render(target, JSON.parse(code), options) 90 | } else if (type === 'mermaid') { 91 | render.initialize({ 92 | startOnLoad: false, 93 | securityLevel: 'strict', 94 | theme 95 | }) 96 | await render.parse(code) 97 | target.innerHTML = sanitize(code, PREVIEW_DOMPURIFY_CONFIG) 98 | target.removeAttribute('data-processed') 99 | await render.run({ 100 | nodes: [target] 101 | }) 102 | } 103 | 104 | return target.innerHTML; 105 | } 106 | 107 | export default renderDiagram; 108 | -------------------------------------------------------------------------------- /src/packages/utils/diagram/plantuml/index.ts: -------------------------------------------------------------------------------- 1 | import plantumlEncoder from "plantuml-encoder"; 2 | 3 | export default class Diagram { 4 | public encodedInput = ""; 5 | 6 | /** 7 | * Builds a Diagram object storing the encoded input value 8 | */ 9 | static parse(input: string) { 10 | const diagram = new Diagram(); 11 | diagram.encode(input); 12 | 13 | return diagram; 14 | } 15 | 16 | /** 17 | * Encodes a diagram following PlantUML specs, I used `plantuml-encoder` at last. 18 | * 19 | * From https://plantuml.com/text-encoding 20 | * 1. Encoded in UTF-8 21 | * 2. Compressed using Deflate or Brotli algorithm 22 | * 3. Re-encoded in ASCII using a transformation close to base64 23 | */ 24 | encode(value: string) { 25 | this.encodedInput = plantumlEncoder.encode(value); 26 | } 27 | 28 | async insertElement(container: string | HTMLElement) { 29 | const PLANTUML_URL = "https://www.plantuml.com/plantuml"; 30 | const div = 31 | typeof container === "string" 32 | ? document.getElementById(container) 33 | : container; 34 | if (div === null || !div.tagName) { 35 | throw new Error("Invalid container: " + container); 36 | } 37 | const src = `${PLANTUML_URL}/svg/${this.encodedInput}`; 38 | return fetch(src) 39 | .then((response) => response.text()) 40 | .then((svg) => div.innerHTML = svg) 41 | 42 | // div.innerHTML = ``; 43 | } 44 | } -------------------------------------------------------------------------------- /src/packages/utils/diagram/sequence/index.ts: -------------------------------------------------------------------------------- 1 | import Diagram from "@/packages/libs/sequence/sequence-diagram-snap"; 2 | 3 | export default Diagram; 4 | -------------------------------------------------------------------------------- /src/packages/utils/dompurify.ts: -------------------------------------------------------------------------------- 1 | import purify from "dompurify"; 2 | 3 | const { sanitize, isValidAttribute } = purify; 4 | 5 | export { isValidAttribute }; 6 | 7 | export const PREVIEW_DOMPURIFY_CONFIG = { 8 | // do not forbid `class` because `code` element use class to present language 9 | FORBID_ATTR: ["style", "contenteditable"], 10 | ALLOW_DATA_ATTR: false, 11 | USE_PROFILES: { 12 | html: true, 13 | svg: true, 14 | svgFilters: true, 15 | mathMl: false, 16 | }, 17 | RETURN_TRUSTED_TYPE: false, 18 | }; 19 | 20 | export const EXPORT_DOMPURIFY_CONFIG = { 21 | FORBID_ATTR: ["contenteditable"], 22 | ALLOW_DATA_ATTR: false, 23 | ADD_ATTR: ["data-align"], 24 | USE_PROFILES: { 25 | html: true, 26 | svg: true, 27 | svgFilters: true, 28 | mathMl: false, 29 | }, 30 | RETURN_TRUSTED_TYPE: false, 31 | // Allow "file" protocol to export images on Windows (#1997). 32 | ALLOWED_URI_REGEXP: 33 | /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp|file):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i, 34 | }; 35 | 36 | export default sanitize; 37 | -------------------------------------------------------------------------------- /src/packages/utils/env.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | const win = window as any; 4 | const env = (function () { 5 | const agent = navigator.userAgent.toLowerCase(), 6 | opera = win.opera, 7 | env: any = { 8 | isOsx: /Mac/.test(navigator.platform), 9 | isWin: /win32|wow32|win64|wow64/i.test(agent), 10 | isMobile: /(iPhone|iPad|iPod|Android|BlackBerry|BB10|Silk|Mobi|Mini|Fennec|IEMobile|Opera Mini|Windows Phone|Kindle|Mobile|Opera Mobi|Tablet)/i.test(agent), 11 | ie: /(msie\s|trident.*rv:)([\w.]+)/i.test(agent), 12 | opera: !!opera && !!opera.version, 13 | webkit: agent.indexOf(" applewebkit/") > -1, 14 | mac: agent.indexOf("macintosh") > -1, 15 | quirks: document.compatMode === "BackCompat", 16 | version: 0 17 | }; 18 | 19 | 20 | env.gecko = 21 | navigator.product === "Gecko" && !env.webkit && !env.opera && !env.ie; 22 | 23 | let version = 0; 24 | 25 | // Internet Explorer 6.0+ 26 | if (env.ie) { 27 | const v1 = agent.match(/(?:msie\s([\w.]+))/); 28 | const v2 = agent.match(/(?:trident.*rv:([\w.]+))/); 29 | if (v1 && v2 && v1[1] && v2[1]) { 30 | version = Math.max(parseFloat(v1[1]) * 1, parseFloat(v2[1]) * 1); 31 | } else if (v1 && v1[1]) { 32 | version = parseFloat(v1[1]) * 1; 33 | } else if (v2 && v2[1]) { 34 | version = parseFloat(v2[1]) * 1; 35 | } else { 36 | version = 0; 37 | } 38 | 39 | const documentMode = (document as any).documentMode 40 | 41 | env.ie11Compat = documentMode === 11; 42 | env.ie9Compat = documentMode === 9; 43 | env.ie8 = !!documentMode; 44 | env.ie8Compat = documentMode === 8; 45 | env.ie7Compat = 46 | (version === 7 && !documentMode) || documentMode === 7; 47 | env.ie6Compat = version < 7 || env.quirks; 48 | env.ie9above = version > 8; 49 | env.ie9below = version < 9; 50 | env.ie11above = version > 10; 51 | env.ie11below = version < 11; 52 | } 53 | 54 | // Gecko. 55 | if (env.gecko) { 56 | let geckoRelease = agent.match(/rv:([\d\.]+)/); 57 | if (geckoRelease) { 58 | let release = geckoRelease[1].split("."); 59 | version = 60 | parseFloat(release[0]) * 10000 + 61 | (parseFloat(release[1]) || 0) * 100 + 62 | (parseFloat(release[2]) || 0) * 1; 63 | } 64 | } 65 | 66 | if (/chrome\/(\d+\.\d)/i.test(agent)) { 67 | env.chrome = +RegExp["\x241"]; 68 | } 69 | 70 | if ( 71 | /(\d+\.\d)?(?:\.\d)?\s+safari\/?(\d+\.\d+)?/i.test(agent) && 72 | !/chrome/i.test(agent) 73 | ) { 74 | env.safari = +(RegExp["\x241"] || RegExp["\x242"]); 75 | } 76 | 77 | // Opera 9.50+ 78 | if (env.opera) version = parseFloat(opera.version()); 79 | 80 | // WebKit 522+ (Safari 3+) 81 | if (env.webkit) { 82 | const match = agent.match(/ applewebkit\/(\d+)/) 83 | if (match) { 84 | version = parseFloat(match[1]); 85 | } 86 | } 87 | 88 | env.version = version; 89 | env.isCompatible = 90 | ((env.ie && version >= 6) || 91 | (env.gecko && version >= 10801) || 92 | (env.opera && version >= 9.5) || 93 | (env.webkit && version >= 522) || 94 | false); 95 | return env; 96 | })(); 97 | 98 | export default env; -------------------------------------------------------------------------------- /src/packages/utils/keymap.ts: -------------------------------------------------------------------------------- 1 | const keymap: { [key: string]: number } = { 2 | Backspace: 8, 3 | Tab: 9, 4 | Enter: 13, 5 | 6 | Shift: 16, 7 | Control: 17, 8 | Alt: 18, 9 | CapsLock: 20, 10 | 11 | Esc: 27, 12 | 13 | Spacebar: 32, 14 | 15 | PageUp: 33, 16 | PageDown: 34, 17 | End: 35, 18 | Home: 36, 19 | 20 | Left: 37, 21 | Up: 38, 22 | Right: 39, 23 | Down: 40, 24 | 25 | Insert: 45, 26 | 27 | Del: 46, 28 | 29 | NumLock: 144, 30 | 31 | Cmd: 91, 32 | 33 | "=": 187, 34 | "-": 189, 35 | '.': 190, 36 | ',': 188, 37 | "'": 222, 38 | a: 65, 39 | b: 66, 40 | c: 67, 41 | d: 68, 42 | e: 69, 43 | f: 70, 44 | g: 71, 45 | h: 72, 46 | i: 73, 47 | j: 74, 48 | k: 75, 49 | l: 76, 50 | m: 77, 51 | n: 78, 52 | o: 79, 53 | p: 80, 54 | q: 81, 55 | r: 82, 56 | s: 83, 57 | t: 84, 58 | 59 | u: 85, 60 | v: 86, 61 | w: 87, 62 | 63 | x: 88, 64 | y: 89, 65 | z: 90, //回退 66 | }; 67 | 68 | export function isPrintableKey(keyCode: number): boolean { 69 | return (keyCode > 47 && keyCode < 58) || // number keys 70 | keyCode === 32 || keyCode === 13 || // Spacebar & return key(s) 71 | keyCode === 229 || // processing key input for certain languages — Chinese, Japanese, etc. 72 | (keyCode > 64 && keyCode < 91) || // letter keys 73 | (keyCode > 95 && keyCode < 112) || // Numpad keys 74 | (keyCode > 185 && keyCode < 193) || // ;=,-./` (in order) 75 | (keyCode > 218 && keyCode < 223); // [\]' (in order) 76 | } 77 | 78 | export default keymap; -------------------------------------------------------------------------------- /src/packages/utils/listeners.ts: -------------------------------------------------------------------------------- 1 | 2 | import { generateId } from './utils' 3 | 4 | export interface EventListener { 5 | /** 6 | * Listener unique identifier 7 | */ 8 | id: string; 9 | 10 | /** 11 | * Element where to listen to dispatched events 12 | */ 13 | element: EventTarget; 14 | 15 | /** 16 | * Event to listen 17 | */ 18 | eventType: string; 19 | 20 | /** 21 | * Event handler 22 | * 23 | * @param {Event} event - event object 24 | */ 25 | handler: (event: Event) => void; 26 | 27 | /** 28 | * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener 29 | */ 30 | options: boolean | AddEventListenerOptions; 31 | } 32 | 33 | export default class EventListeners { 34 | 35 | private allEventListeners: EventListener[] = []; 36 | 37 | /** 38 | * Assigns event listener on element and returns unique identifier 39 | * 40 | * @param {EventTarget} element - DOM element that needs to be listened 41 | * @param {string} eventType - event type 42 | * @param {Function} handler - method that will be fired on event 43 | * @param {boolean|AddEventListenerOptions} options - useCapture or {capture, passive, once} 44 | * 45 | * @returns {string} 46 | */ 47 | public on( 48 | element: EventTarget, 49 | eventType: string, 50 | handler: (event: Event) => void, 51 | options: boolean | AddEventListenerOptions = false 52 | ): string | undefined { 53 | const id = generateId(); 54 | const assignedEventData = { 55 | id, 56 | element, 57 | eventType, 58 | handler, 59 | options, 60 | }; 61 | 62 | const alreadyExist = this.allEventListeners.some((listener) => { 63 | if (listener.element === element && listener.eventType === eventType && listener.handler === handler) { 64 | return true; 65 | } 66 | return false 67 | }); 68 | 69 | if (alreadyExist) { 70 | return; 71 | } 72 | 73 | this.allEventListeners.push(assignedEventData); 74 | element.addEventListener(eventType, handler, options); 75 | 76 | return id; 77 | } 78 | 79 | /** 80 | * Removes listener by id 81 | * 82 | * @param {string} id - listener identifier 83 | */ 84 | public off(id: string) { 85 | const listener = this.allEventListeners.find((listener) => listener.id === id); 86 | 87 | if (!listener) { 88 | return; 89 | } 90 | 91 | listener.element.removeEventListener(listener.eventType, listener.handler, listener.options); 92 | } 93 | 94 | /** 95 | * Removes all listeners 96 | */ 97 | public removeAllEventListeners(): void { 98 | this.allEventListeners.map((listener) => { 99 | listener.element.removeEventListener(listener.eventType, listener.handler, listener.options); 100 | }); 101 | 102 | this.allEventListeners = []; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/packages/utils/marked/LICENSE: -------------------------------------------------------------------------------- 1 | Marked 2 | 3 | Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/packages/utils/marked/README.md: -------------------------------------------------------------------------------- 1 | # Marked 2 | 3 | This folder contains a patched [Marked.js](https://github.com/markedjs/marked/) version based on `v0.8.2` and bug fixes from `v1.2.5`, no breaking changes from `v1.0.0` are included. 4 | 5 | ## Changes 6 | 7 | ### Features 8 | 9 | - Markdown Extra: frontmatter and inline and block math 10 | - GFM like: emojis 11 | 12 | ### (Inline) Lexer 13 | 14 | - `disableInline` mode 15 | - Custom list and list item implementation based on an older marked.js version 16 | - Slightly modified definition due `disableInline` 17 | - More token information like list item bullet type 18 | 19 | ### Renderer 20 | 21 | - Emoji renderer 22 | - Frontmatter renderer 23 | - Inline and block (`multiplemath`) math renderer 24 | 25 | ## License 26 | 27 | [MIT](LICENSE) 28 | -------------------------------------------------------------------------------- /src/packages/utils/marked/index.ts: -------------------------------------------------------------------------------- 1 | import Renderer from "./renderer"; 2 | import Lexer from "./lexer"; 3 | import Parser from "./parser"; 4 | import options from "./options"; 5 | import { escape } from "./utils"; 6 | 7 | /** 8 | * Marked 9 | */ 10 | 11 | function marked(src, opt = {}) { 12 | // throw error in case of non string input 13 | if (typeof src === "undefined" || src === null) { 14 | throw new Error("marked(): input parameter is undefined or null"); 15 | } 16 | 17 | if (typeof src !== "string") { 18 | throw new Error( 19 | "marked(): input parameter is of type " + 20 | Object.prototype.toString.call(src) + 21 | ", string expected" 22 | ); 23 | } 24 | 25 | try { 26 | opt = Object.assign({}, options, opt); 27 | 28 | return new Parser(opt).parse(new Lexer(opt).lex(src)); 29 | } catch (e: any) { 30 | e.message += 31 | "\nPlease report this to https://github.com/marktext/muya/issues."; 32 | if ((opt as any).silent) { 33 | return ( 34 | "

An error occurred:

" +
35 |         escape(e.message + "", true) +
36 |         "
" 37 | ); 38 | } 39 | throw e; 40 | } 41 | } 42 | 43 | export { Renderer, Lexer, Parser }; 44 | 45 | export default marked; 46 | -------------------------------------------------------------------------------- /src/packages/utils/marked/options.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | baseUrl: null, 3 | breaks: false, 4 | gfm: true, 5 | headerIds: true, 6 | headerPrefix: "", 7 | highlight: null, 8 | mathRenderer: null, 9 | emojiRenderer: null, 10 | tocRenderer: null, 11 | langPrefix: "language-", 12 | mangle: true, 13 | pedantic: false, 14 | renderer: null, // new Renderer(), 15 | silent: false, 16 | smartLists: false, 17 | smartypants: false, 18 | xhtml: false, 19 | disableInline: false, 20 | 21 | // NOTE: sanitize and sanitizer are deprecated since version 0.7.0, should not be used and will be removed in the future. 22 | sanitize: false, 23 | sanitizer: null, 24 | 25 | // Markdown extensions: 26 | // TODO: We set whether to support `emoji`, `math`, `frontMatter` default value to `true` 27 | // After we add user setting, we maybe set math and frontMatter default value to false. 28 | // User need to enable them in the user setting. 29 | emoji: true, 30 | math: true, 31 | frontMatter: true, 32 | superSubScript: false, 33 | footnote: false, 34 | isGitlabCompatibilityEnabled: false, 35 | 36 | isHtmlEnabled: true, 37 | }; 38 | -------------------------------------------------------------------------------- /src/packages/utils/marked/slugger.ts: -------------------------------------------------------------------------------- 1 | import { downcode } from "./urlify"; 2 | 3 | /** 4 | * Slugger generates header id 5 | */ 6 | 7 | function Slugger() { 8 | this.seen = {}; 9 | this.downcodeUnicode = true; 10 | } 11 | 12 | /** 13 | * Convert string to unique id 14 | */ 15 | 16 | Slugger.prototype.slug = function (value) { 17 | let slug = this.downcodeUnicode ? downcode(value) : value; 18 | slug = slug 19 | .toLowerCase() 20 | .trim() 21 | // remove html tags 22 | .replace(/<[!\/a-z].*?>/gi, "") // eslint-disable-line no-useless-escape 23 | // remove unwanted chars 24 | .replace( 25 | /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g, 26 | "" 27 | ) 28 | .replace(/\s/g, "-"); 29 | 30 | if (this.seen[slug] !== undefined) { 31 | const originalSlug = slug; 32 | do { 33 | this.seen[originalSlug]++; 34 | slug = originalSlug + "-" + this.seen[originalSlug]; 35 | } while (this.seen[slug] !== undefined); 36 | } 37 | this.seen[slug] = 0; 38 | 39 | return slug; 40 | }; 41 | 42 | export default Slugger; 43 | -------------------------------------------------------------------------------- /src/packages/utils/marked/textRenderer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * TextRenderer 3 | * returns only the textual part of the token 4 | */ 5 | 6 | function TextRenderer() {} 7 | 8 | // no need for block level renderers 9 | 10 | TextRenderer.prototype.strong = 11 | TextRenderer.prototype.em = 12 | TextRenderer.prototype.codespan = 13 | TextRenderer.prototype.del = 14 | TextRenderer.prototype.text = 15 | function (text) { 16 | return text; 17 | }; 18 | 19 | TextRenderer.prototype.html = function (html) { 20 | return html; 21 | }; 22 | 23 | TextRenderer.prototype.inlineMath = function (math, displayMode) { 24 | return math; 25 | }; 26 | 27 | TextRenderer.prototype.emoji = function (text, emoji) { 28 | return emoji; 29 | }; 30 | 31 | TextRenderer.prototype.script = function (content, marker) { 32 | const tagName = marker === "^" ? "sup" : "sub"; 33 | 34 | return `<${tagName}>${content}`; 35 | }; 36 | 37 | TextRenderer.prototype.footnoteIdentifier = function ( 38 | identifier, 39 | { footnoteId, footnoteIdentifierId, order } 40 | ) { 41 | return `${ 44 | order || identifier 45 | }`; 46 | }; 47 | 48 | TextRenderer.prototype.link = TextRenderer.prototype.image = function ( 49 | href, 50 | title, 51 | text 52 | ) { 53 | return "" + text; 54 | }; 55 | 56 | TextRenderer.prototype.br = function () { 57 | return ""; 58 | }; 59 | 60 | export default TextRenderer; 61 | -------------------------------------------------------------------------------- /src/packages/utils/math.ts: -------------------------------------------------------------------------------- 1 | import "mathjax/es5/tex-mml-svg"; 2 | import { removeSVGUse } from "./convert"; 3 | const MathJax: any = (window as any).MathJax; 4 | 5 | export function tex2svgPromise(tex: string): Promise { 6 | return MathJax.tex2svgPromise(tex).then((node: HTMLElement) => { 7 | const svg = node.querySelector('svg'); 8 | if(svg) { 9 | removeSVGUse(svg) 10 | const html = svg.outerHTML; 11 | return html; 12 | } 13 | 14 | return '' 15 | }) 16 | } 17 | 18 | export async function renderMath(tex: string, target: HTMLElement): Promise { 19 | const html = await tex2svgPromise(tex); 20 | target.innerHTML = html; 21 | return html; 22 | } -------------------------------------------------------------------------------- /src/packages/utils/nodeTypes.ts: -------------------------------------------------------------------------------- 1 | export const PARAGRAPH_TYPES = [ 2 | "p", 3 | "h1", 4 | "h2", 5 | "h3", 6 | "h4", 7 | "h5", 8 | "h6", 9 | "blockquote", 10 | "pre", 11 | "ul", 12 | "ol", 13 | "li", 14 | "figure", 15 | ]; 16 | 17 | export const BLOCK_TYPE6 = [ 18 | "address", 19 | "article", 20 | "aside", 21 | "base", 22 | "basefont", 23 | "blockquote", 24 | "body", 25 | "caption", 26 | "center", 27 | "col", 28 | "colgroup", 29 | "dd", 30 | "details", 31 | "dialog", 32 | "dir", 33 | "div", 34 | "dl", 35 | "dt", 36 | "fieldset", 37 | "figcaption", 38 | "figure", 39 | "footer", 40 | "form", 41 | "frame", 42 | "frameset", 43 | "h1", 44 | "h2", 45 | "h3", 46 | "h4", 47 | "h5", 48 | "h6", 49 | "head", 50 | "header", 51 | "hr", 52 | "html", 53 | "iframe", 54 | "legend", 55 | "li", 56 | "link", 57 | "main", 58 | "menu", 59 | "menuitem", 60 | "meta", 61 | "nav", 62 | "noframes", 63 | "ol", 64 | "optgroup", 65 | "option", 66 | "p", 67 | "param", 68 | "section", 69 | "source", 70 | "summary", 71 | "table", 72 | "tbody", 73 | "td", 74 | "tfoot", 75 | "th", 76 | "thead", 77 | "title", 78 | "tr", 79 | "track", 80 | "ul", 81 | ]; -------------------------------------------------------------------------------- /src/packages/utils/outline.ts: -------------------------------------------------------------------------------- 1 | import { MEBlockData, MEOutlineItem } from "../types"; 2 | 3 | export function regexWithKey(key: string) { 4 | const words = []; 5 | for (let i = 0; i < key.length; i++) { 6 | words.push(`(${key.charAt(i)})`) 7 | } 8 | return new RegExp(`(.*)${words.join('(.*)')}(.*)`) 9 | } 10 | 11 | function flattenIteration(result: MEOutlineItem[], data: MEBlockData) { 12 | let match; 13 | if(/(heading\d|paragraph|code|td|tr)$/.test(data.type)) { 14 | result.push({ 15 | id: data.id, 16 | text: data.text||'', 17 | type: data.type, 18 | match 19 | }) 20 | } 21 | 22 | const children = data.children || [] 23 | children.forEach((child)=>{ 24 | flattenIteration(result, child) 25 | }) 26 | } 27 | 28 | export function flattenToOutline(data: MEBlockData) { 29 | const result: MEOutlineItem[] = []; 30 | flattenIteration(result, data); 31 | return result; 32 | } 33 | 34 | export function filterOutline(outline: MEOutlineItem[], {filterKey, filterTypeRegex}: {filterKey?: string; filterTypeRegex?: RegExp}) { 35 | const filterRegex = filterKey ? regexWithKey(filterKey) : null; 36 | if(!filterRegex && !filterTypeRegex) { 37 | return outline; 38 | } 39 | return outline.filter((it)=>{ 40 | const match = filterRegex ? it.text.match(filterRegex) : null 41 | it.match = match 42 | return (!filterRegex || match) && (!filterTypeRegex || filterTypeRegex.test(it.type)); 43 | }) 44 | } -------------------------------------------------------------------------------- /src/packages/utils/tags.ts: -------------------------------------------------------------------------------- 1 | import htmlTags from "html-tags"; 2 | import voidHtmlTags from "html-tags/void"; 3 | 4 | 5 | export const VOID_HTML_TAGS = voidHtmlTags; 6 | export const HTML_TAGS = htmlTags; -------------------------------------------------------------------------------- /src/packages/utils/turndownService.ts: -------------------------------------------------------------------------------- 1 | 2 | import { identity } from "./utils"; 3 | const turndownPluginGfm = require("joplin-turndown-plugin-gfm"); 4 | import TurndownService from "turndown"; 5 | 6 | export const addPluginAddRules = (turndownService, keeps) => { 7 | // Use the gfm plugin 8 | const { gfm } = turndownPluginGfm; 9 | turndownService.use(gfm); 10 | 11 | // We need a extra strikethrough rule because the strikethrough rule in gfm is single `~`. 12 | turndownService.addRule("strikethrough", { 13 | filter: ["del", "s", "strike"], 14 | replacement(content) { 15 | return "~~" + content + "~~"; 16 | }, 17 | }); 18 | 19 | turndownService.addRule("paragraph", { 20 | filter: "p", 21 | 22 | replacement: function (content, node) { 23 | const isTaskListItemParagraph = 24 | node.previousElementSibling && 25 | node.previousElementSibling.tagName === "INPUT"; 26 | 27 | return isTaskListItemParagraph 28 | ? content + "\n\n" 29 | : "\n\n" + content + "\n\n"; 30 | }, 31 | }); 32 | 33 | turndownService.addRule("listItem", { 34 | filter: "li", 35 | 36 | replacement: function (content, node, options) { 37 | content = content 38 | .replace(/^\n+/, "") // remove leading newlines 39 | .replace(/\n+$/, "\n") // replace trailing newlines with just a single one 40 | .replace(/\n/gm, "\n "); // indent 41 | 42 | let prefix = options.bulletListMarker + " "; 43 | const parent = node.parentNode; 44 | if (parent.nodeName === "OL") { 45 | const start = parent.getAttribute("start"); 46 | const index = Array.prototype.indexOf.call(parent.children, node); 47 | prefix = (start ? Number(start) + index : index + 1) + ". "; 48 | } 49 | 50 | return ( 51 | prefix + 52 | content + 53 | (node.nextSibling && !/\n$/.test(content) ? "\n" : "") 54 | ); 55 | }, 56 | }); 57 | 58 | // Handle multiple math lines 59 | turndownService.addRule("multiplemath", { 60 | filter(node, options) { 61 | return ( 62 | node.nodeName === "PRE" && node.classList.contains("multiple-math") 63 | ); 64 | }, 65 | replacement(content, node, options) { 66 | return `$$\n${content}\n$$`; 67 | }, 68 | }); 69 | 70 | turndownService.escape = identity; 71 | turndownService.keep(keeps); 72 | }; 73 | 74 | export const LINE_BREAK = "\n"; 75 | export const DEFAULT_TURNDOWN_CONFIG = { 76 | headingStyle: "atx", // setext or atx 77 | hr: "---", 78 | bulletListMarker: "-", // -, +, or * 79 | codeBlockStyle: "fenced", // fenced or indented 80 | fence: "```", // ``` or ~~~ 81 | emDelimiter: "*", // _ or * 82 | strongDelimiter: "**", // ** or __ 83 | linkStyle: "inlined", 84 | linkReferenceStyle: "full", 85 | blankReplacement(content, node, options) { 86 | if (node && node.classList.contains("mu-soft-line-break")) { 87 | return LINE_BREAK; 88 | } else if (node && node.classList.contains("mu-hard-line-break")) { 89 | return " " + LINE_BREAK; 90 | } else if (node && node.classList.contains("mu-hard-line-break-sapce")) { 91 | return ""; 92 | } else { 93 | return node.isBlock ? "\n\n" : ""; 94 | } 95 | }, 96 | }; 97 | 98 | export default TurndownService; 99 | -------------------------------------------------------------------------------- /src/packages/utils/url.ts: -------------------------------------------------------------------------------- 1 | import { isValidAttribute } from "./dompurify"; 2 | 3 | export const URL_REG = 4 | /^http(s)?:\/\/([a-z0-9\-._~]+\.[a-z]{2,}|[0-9.]+|localhost|\[[a-f0-9.:]+\])(:[0-9]{1,5})?\/[\S]+/i; 5 | 6 | export const sanitizeHyperlink = (rawLink) => { 7 | if ( 8 | rawLink && 9 | typeof rawLink === "string" && 10 | isValidAttribute("a", "href", rawLink) 11 | ) { 12 | return rawLink; 13 | } 14 | 15 | return ""; 16 | }; -------------------------------------------------------------------------------- /src/test/state.test.js: -------------------------------------------------------------------------------- 1 | import StateToMarkdown from "@/packages/modules/state/stateToMarkdown"; 2 | 3 | describe("State module", () => { 4 | test("State to markdown test", () => { 5 | const stateToMarkdown = new StateToMarkdown(); 6 | const markdown = stateToMarkdown.generate([ 7 | { 8 | id: "", 9 | type: "paragraph", 10 | text: "xxxx", 11 | }, 12 | ]); 13 | expect(markdown).toBe(markdown); 14 | }); 15 | }); 16 | --------------------------------------------------------------------------------