├── .nvmrc
├── src
├── styles
│ ├── index.scss
│ ├── mixins.scss
│ ├── transition.scss
│ └── var.scss
├── assets
│ ├── css
│ │ └── theme
│ │ │ └── base.css
│ └── font
│ │ ├── iconfont.eot
│ │ ├── iconfont.ttf
│ │ ├── iconfont.woff
│ │ └── iconfont.woff2
├── utils
│ ├── constants
│ │ ├── menu-mode.js
│ │ ├── editor-mode.js
│ │ ├── markup.js
│ │ └── command.js
│ ├── xss
│ │ ├── common.js
│ │ ├── KaTex.js
│ │ ├── svg.js
│ │ └── index.js
│ ├── scroll-top.js
│ ├── lang.js
│ ├── deep-assign.js
│ ├── markdown-it-mermaid.js
│ ├── toolbar.js
│ ├── markdown-it-pre-wrapper.js
│ ├── markdown-it-heading-tag.js
│ ├── command.js
│ ├── scrollbar-width.js
│ ├── markdown-it.js
│ ├── key-codes.js
│ ├── v-md-parser.js
│ ├── markdown-it-copy-code.js
│ ├── smooth-scroll.js
│ ├── resize-event.js
│ ├── file.js
│ ├── markdown-it-line-number.js
│ ├── markdown-it-container.js
│ ├── util.js
│ ├── markdown-it-link.js
│ ├── clickoutside.js
│ └── markdown-it-highlight-lines.js
├── plugins
│ ├── emoji
│ │ ├── emoji.css
│ │ ├── parser-full.js
│ │ ├── parser.js
│ │ ├── index.js
│ │ ├── full.js
│ │ ├── command.js
│ │ ├── parser-creator.js
│ │ ├── toolbar.js
│ │ └── creator.js
│ ├── katex
│ │ ├── cdn.js
│ │ ├── npm.js
│ │ ├── parser-npm.js
│ │ ├── creator.js
│ │ ├── parser-cdn.js
│ │ └── parser-creator.js
│ ├── mermaid
│ │ ├── npm.js
│ │ ├── parser.js
│ │ ├── cdn.js
│ │ ├── mermaid.css
│ │ └── creator.js
│ ├── align
│ │ ├── index.js
│ │ └── parser.js
│ ├── line-number
│ │ ├── index.js
│ │ └── parser.js
│ ├── highlight-lines
│ │ ├── index.js
│ │ ├── parser.js
│ │ └── highlight-lines.css
│ ├── copy-code
│ │ ├── parser.js
│ │ ├── index.js
│ │ ├── copy-code.css
│ │ └── preview.js
│ ├── todo-list
│ │ ├── toolbar.js
│ │ ├── command.js
│ │ ├── parser.js
│ │ ├── todo-list.css
│ │ └── index.js
│ └── tip
│ │ ├── tip.css
│ │ └── parser.js
├── hotkeys
│ ├── save.js
│ ├── h1.js
│ ├── h2.js
│ ├── h3.js
│ ├── h4.js
│ ├── h5.js
│ ├── h6.js
│ ├── ol.js
│ ├── ul.js
│ ├── bold.js
│ ├── link.js
│ ├── italic.js
│ └── quote.js
├── command
│ ├── clear.js
│ ├── redo.js
│ ├── undo.js
│ ├── sync-scroll.js
│ ├── fullscreen.js
│ ├── hr.js
│ ├── table.js
│ ├── quote.js
│ ├── code.js
│ ├── h1.js
│ ├── ul.js
│ ├── h2.js
│ ├── h3.js
│ ├── h4.js
│ ├── h5.js
│ ├── h6.js
│ ├── ol.js
│ ├── link.js
│ ├── bold.js
│ ├── italic.js
│ ├── strikethrough.js
│ └── image.js
├── components
│ ├── render.js
│ ├── upload-file.vue
│ ├── scrollbar
│ │ └── util.js
│ ├── toc-nav.vue
│ └── toolbar-item
│ │ └── tooltip.vue
├── mixins
│ ├── lang.js
│ ├── v-model.js
│ ├── command.js
│ ├── hotkeys.js
│ ├── fullscreen.js
│ ├── scroll.js
│ ├── upload-image.js
│ ├── list.js
│ ├── toolbar.js
│ ├── toc.js
│ └── preview.js
├── toolbar
│ ├── save.js
│ ├── code.js
│ ├── hr.js
│ ├── ol.js
│ ├── ul.js
│ ├── clear.js
│ ├── table.js
│ ├── bold.js
│ ├── link.js
│ ├── redo.js
│ ├── undo.js
│ ├── quote.js
│ ├── italic.js
│ ├── strikethrough.js
│ ├── toc.js
│ ├── fullscreen.js
│ ├── sync-scroll.js
│ ├── preview.js
│ ├── image.js
│ └── h.js
├── theme
│ ├── github
│ │ ├── index.js
│ │ ├── parser.js
│ │ └── theme.js
│ ├── vuepress
│ │ ├── parser.js
│ │ ├── index.js
│ │ └── theme.js
│ └── base
│ │ ├── highlight.js
│ │ ├── prism.js
│ │ └── base.js
├── preview-html.js
├── preview-html.vue
├── preview.js
├── base-editor.js
├── codemirror-editor.js
├── create-editor.js
├── preview.vue
└── lang
│ ├── zh-CN.js
│ ├── zh-TW.js
│ ├── ko-KR.js
│ ├── ja-JP.js
│ ├── en-US.js
│ ├── pt-BR.js
│ ├── pl-PL.js
│ ├── es-ES.js
│ ├── fr-FR.js
│ └── de-DE.js
├── docs
├── zh
│ ├── communication.md
│ ├── senior
│ │ ├── image-size.md
│ │ ├── xss-extend.md
│ │ ├── toc.md
│ │ └── upload-image.md
│ ├── plugins
│ │ ├── line-number.md
│ │ ├── highlight-lines.md
│ │ ├── emoji.md
│ │ ├── tip.md
│ │ ├── katex.md
│ │ ├── todo-list.md
│ │ └── copy-code.md
│ ├── README.md
│ ├── examples
│ │ ├── preview-demo.md
│ │ ├── base-editor.md
│ │ └── codemirror-editor.md
│ ├── quick-start.md
│ ├── Internationalization.md
│ └── theme
│ │ ├── github.md
│ │ └── vuepress.md
├── senior
│ ├── image-size.md
│ ├── xss-extend.md
│ ├── toc.md
│ └── upload-image.md
├── .vuepress
│ ├── styles
│ │ └── index.styl
│ ├── enhanceApp.js
│ └── components
│ │ ├── preview-demo.vue
│ │ ├── base-editor.vue
│ │ ├── codemirror-editor.vue
│ │ ├── vuepress-theme.vue
│ │ ├── plugin-katex.vue
│ │ ├── extend-vuepress-theme.vue
│ │ ├── image-size.vue
│ │ ├── toc-nav.vue
│ │ ├── line-number-vuepress.vue
│ │ ├── plugin-emoji.vue
│ │ ├── plugin-todo-list.vue
│ │ ├── plugin-tip.vue
│ │ ├── extend-github-theme.vue
│ │ ├── line-number-github.vue
│ │ ├── plugin-highlight-lines-vuepress.vue
│ │ ├── plugin-highlight-lines-github.vue
│ │ ├── upload-image.vue
│ │ ├── plugin-copy-code.vue
│ │ ├── vuepress-theme-tip.vue
│ │ └── extend-toolbar.vue
├── plugins
│ ├── line-number.md
│ ├── highlight-lines.md
│ ├── emoji.md
│ ├── tip.md
│ ├── katex.md
│ ├── todo-list.md
│ └── copy-code.md
├── README.md
├── examples
│ ├── base-editor.md
│ ├── preview-demo.md
│ └── codemirror-editor.md
├── quick-start.md
├── Internationalization.md
└── theme
│ ├── github.md
│ └── vuepress.md
├── .eslintignore
├── .DS_Store
├── SECURITY.md
├── commitlint.config.js
├── postcss.config.js
├── .prettierrc
├── .gitignore
├── jsconfig.json
├── .stylelintrc.json
├── .eslintrc.js
├── dev
├── index.html
├── test-cdn.html
├── App.vue
└── text.js
├── babel.config.js
└── LICENSE
/.nvmrc:
--------------------------------------------------------------------------------
1 | v16
--------------------------------------------------------------------------------
/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import './transition';
2 |
--------------------------------------------------------------------------------
/docs/zh/communication.md:
--------------------------------------------------------------------------------
1 | # 交流
2 |
3 | QQ 群:798884474
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # npm
2 | node_modules
3 |
4 | # dist
5 | lib
6 | dist
7 | es
--------------------------------------------------------------------------------
/src/assets/css/theme/base.css:
--------------------------------------------------------------------------------
1 | [data-v-md-anchor] {
2 | cursor: pointer;
3 | }
4 |
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code-farmer-i/vue-markdown-editor/HEAD/.DS_Store
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Reporting a Vulnerability
4 |
5 | 672247608@qq.com
6 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | };
4 |
--------------------------------------------------------------------------------
/docs/senior/image-size.md:
--------------------------------------------------------------------------------
1 | # Image Size
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/docs/zh/senior/image-size.md:
--------------------------------------------------------------------------------
1 | # 设置图片大小
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/src/utils/constants/menu-mode.js:
--------------------------------------------------------------------------------
1 | export default {
2 | LIST: 'list',
3 | PANEL: 'panel',
4 | };
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "singleQuote": true,
4 | "trailingComma": "es5"
5 | }
6 |
--------------------------------------------------------------------------------
/src/plugins/emoji/emoji.css:
--------------------------------------------------------------------------------
1 | .v-md-emoji-panel-item {
2 | font-family: 'Microsoft YaHei', 'Segoe UI Emoji';
3 | }
4 |
--------------------------------------------------------------------------------
/src/assets/font/iconfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code-farmer-i/vue-markdown-editor/HEAD/src/assets/font/iconfont.eot
--------------------------------------------------------------------------------
/src/assets/font/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code-farmer-i/vue-markdown-editor/HEAD/src/assets/font/iconfont.ttf
--------------------------------------------------------------------------------
/src/assets/font/iconfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code-farmer-i/vue-markdown-editor/HEAD/src/assets/font/iconfont.woff
--------------------------------------------------------------------------------
/src/assets/font/iconfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code-farmer-i/vue-markdown-editor/HEAD/src/assets/font/iconfont.woff2
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # npm
2 | node_modules
3 | package-lock.json
4 |
5 | # dist file
6 | dist
7 | lib
8 | es
9 |
10 | .DS_Store
11 |
--------------------------------------------------------------------------------
/src/utils/constants/editor-mode.js:
--------------------------------------------------------------------------------
1 | export default {
2 | PREVIEW: 'preview',
3 | EDITABLE: 'editable',
4 | EDIT: 'edit',
5 | };
6 |
--------------------------------------------------------------------------------
/src/hotkeys/save.js:
--------------------------------------------------------------------------------
1 | export default {
2 | modifier: 'ctrl',
3 | key: 's',
4 | action(editor) {
5 | editor.save();
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/src/plugins/katex/cdn.js:
--------------------------------------------------------------------------------
1 | import creator from './creator';
2 | import parserCdn from './parser-cdn';
3 |
4 | export default creator(parserCdn);
5 |
--------------------------------------------------------------------------------
/src/plugins/katex/npm.js:
--------------------------------------------------------------------------------
1 | import creator from './creator';
2 | import parserNpm from './parser-npm';
3 |
4 | export default creator(parserNpm);
5 |
--------------------------------------------------------------------------------
/src/command/clear.js:
--------------------------------------------------------------------------------
1 | export { clear as name } from '@/utils/constants/command.js';
2 |
3 | export default function (editor) {
4 | editor.clear();
5 | }
6 |
--------------------------------------------------------------------------------
/src/command/redo.js:
--------------------------------------------------------------------------------
1 | export { redo as name } from '@/utils/constants/command.js';
2 |
3 | export default function (editor) {
4 | editor.redo();
5 | }
6 |
--------------------------------------------------------------------------------
/src/command/undo.js:
--------------------------------------------------------------------------------
1 | export { undo as name } from '@/utils/constants/command.js';
2 |
3 | export default function (editor) {
4 | editor.undo();
5 | }
6 |
--------------------------------------------------------------------------------
/src/plugins/katex/parser-npm.js:
--------------------------------------------------------------------------------
1 | import parserCreator from './parser-creator';
2 | import katex from 'katex';
3 |
4 | export default parserCreator(katex);
5 |
--------------------------------------------------------------------------------
/src/plugins/mermaid/npm.js:
--------------------------------------------------------------------------------
1 | import creator from './creator';
2 | import mermaid from 'mermaid/dist/mermaid.min.js';
3 |
4 | export default creator(mermaid);
5 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "paths": {
5 | "@/*": ["./src/*"]
6 | }
7 | },
8 | "exclude": ["node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/src/plugins/emoji/parser-full.js:
--------------------------------------------------------------------------------
1 | import mdEmojiPlugin from 'markdown-it-emoji';
2 | import parserCreator from './parser-creator';
3 |
4 | export default parserCreator(mdEmojiPlugin);
5 |
--------------------------------------------------------------------------------
/src/plugins/emoji/parser.js:
--------------------------------------------------------------------------------
1 | import mdEmojiPlugin from 'markdown-it-emoji/light';
2 | import parserCreator from './parser-creator';
3 |
4 | export default parserCreator(mdEmojiPlugin);
5 |
--------------------------------------------------------------------------------
/src/utils/constants/markup.js:
--------------------------------------------------------------------------------
1 | export const LINE_MARKUP = 'data-v-md-line';
2 | export const HEADING_MARKUP = 'data-v-md-heading';
3 | export const ANCHOR_MARKUP = 'data-v-md-anchor';
4 |
--------------------------------------------------------------------------------
/src/components/render.js:
--------------------------------------------------------------------------------
1 | const renderFn = function (props, { attrs }) {
2 | return props.render(...attrs);
3 | };
4 |
5 | renderFn.props = ['render'];
6 |
7 | export default renderFn;
8 |
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@vant/stylelint-config"],
3 | "rules": {
4 | "at-rule-no-unknown": null,
5 | "font-family-no-missing-generic-family-keyword": null
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/command/sync-scroll.js:
--------------------------------------------------------------------------------
1 | export { syncScroll as name } from '@/utils/constants/command.js';
2 |
3 | export default function (editor, isEnable) {
4 | editor.toggleSyncScroll(isEnable);
5 | }
6 |
--------------------------------------------------------------------------------
/src/command/fullscreen.js:
--------------------------------------------------------------------------------
1 | export { fullscreen as name } from '@/utils/constants/command.js';
2 |
3 | export default function (editor, fullScreen) {
4 | editor.toggleFullScreen(fullScreen);
5 | }
6 |
--------------------------------------------------------------------------------
/src/hotkeys/h1.js:
--------------------------------------------------------------------------------
1 | import { h1 } from '@/utils/constants/command';
2 |
3 | export default {
4 | modifier: 'ctrl',
5 | key: '1',
6 | action(editor) {
7 | editor.execCommand(h1);
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/src/hotkeys/h2.js:
--------------------------------------------------------------------------------
1 | import { h2 } from '@/utils/constants/command';
2 |
3 | export default {
4 | modifier: 'ctrl',
5 | key: '2',
6 | action(editor) {
7 | editor.execCommand(h2);
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/src/hotkeys/h3.js:
--------------------------------------------------------------------------------
1 | import { h3 } from '@/utils/constants/command';
2 |
3 | export default {
4 | modifier: 'ctrl',
5 | key: '3',
6 | action(editor) {
7 | editor.execCommand(h3);
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/src/hotkeys/h4.js:
--------------------------------------------------------------------------------
1 | import { h4 } from '@/utils/constants/command';
2 |
3 | export default {
4 | modifier: 'ctrl',
5 | key: '4',
6 | action(editor) {
7 | editor.execCommand(h4);
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/src/hotkeys/h5.js:
--------------------------------------------------------------------------------
1 | import { h5 } from '@/utils/constants/command';
2 |
3 | export default {
4 | modifier: 'ctrl',
5 | key: '5',
6 | action(editor) {
7 | editor.execCommand(h5);
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/src/hotkeys/h6.js:
--------------------------------------------------------------------------------
1 | import { h6 } from '@/utils/constants/command';
2 |
3 | export default {
4 | modifier: 'ctrl',
5 | key: '6',
6 | action(editor) {
7 | editor.execCommand(h6);
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/src/hotkeys/ol.js:
--------------------------------------------------------------------------------
1 | import { ol } from '@/utils/constants/command';
2 |
3 | export default {
4 | modifier: 'ctrl',
5 | key: 'o',
6 | action(editor) {
7 | editor.execCommand(ol);
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/src/hotkeys/ul.js:
--------------------------------------------------------------------------------
1 | import { ul } from '@/utils/constants/command';
2 |
3 | export default {
4 | modifier: 'ctrl',
5 | key: 'u',
6 | action(editor) {
7 | editor.execCommand(ul);
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/src/mixins/lang.js:
--------------------------------------------------------------------------------
1 | export default {
2 | computed: {
3 | langConfig() {
4 | const lang = this.$options.lang.config;
5 |
6 | return lang.langConfig[lang.lang];
7 | },
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/src/hotkeys/bold.js:
--------------------------------------------------------------------------------
1 | import { bold } from '@/utils/constants/command';
2 |
3 | export default {
4 | modifier: 'ctrl',
5 | key: 'b',
6 | action(editor) {
7 | editor.execCommand(bold);
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/src/hotkeys/link.js:
--------------------------------------------------------------------------------
1 | import { link } from '@/utils/constants/command';
2 |
3 | export default {
4 | modifier: 'ctrl',
5 | key: 'l',
6 | action(editor) {
7 | editor.execCommand(link);
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/src/plugins/align/index.js:
--------------------------------------------------------------------------------
1 | import parser from './parser';
2 |
3 | export default function () {
4 | return {
5 | install(VMdEditor) {
6 | VMdEditor.vMdParser.use(parser);
7 | },
8 | };
9 | }
10 |
--------------------------------------------------------------------------------
/src/plugins/emoji/index.js:
--------------------------------------------------------------------------------
1 | import emojiJson from 'markdown-it-emoji/lib/data/light.json';
2 | import creator from './creator';
3 | import parser from './parser';
4 |
5 | export default creator({ emojiJson, parser });
6 |
--------------------------------------------------------------------------------
/src/toolbar/save.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'save',
3 | icon: 'v-md-icon-save',
4 | title: (editor) => `${editor.langConfig.save.toolbar}(Ctrl+S)`,
5 | action(editor) {
6 | editor.save();
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/src/command/hr.js:
--------------------------------------------------------------------------------
1 | export { hr as name } from '@/utils/constants/command.js';
2 |
3 | export default function (editor) {
4 | editor.insert(() => ({
5 | text: '------------------------------------',
6 | }));
7 | }
8 |
--------------------------------------------------------------------------------
/src/hotkeys/italic.js:
--------------------------------------------------------------------------------
1 | import { italic } from '@/utils/constants/command';
2 |
3 | export default {
4 | modifier: 'ctrl',
5 | key: 'i',
6 | action(editor) {
7 | editor.execCommand(italic);
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/src/hotkeys/quote.js:
--------------------------------------------------------------------------------
1 | import { quote } from '@/utils/constants/command';
2 |
3 | export default {
4 | modifier: 'ctrl',
5 | key: 'q',
6 | action(editor) {
7 | editor.execCommand(quote);
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/src/plugins/emoji/full.js:
--------------------------------------------------------------------------------
1 | import emojiJson from 'markdown-it-emoji/lib/data/full.json';
2 | import creator from './creator';
3 | import parser from './parser-full';
4 |
5 | export default creator({ emojiJson, parser });
6 |
--------------------------------------------------------------------------------
/src/plugins/line-number/index.js:
--------------------------------------------------------------------------------
1 | import parser from './parser';
2 |
3 | export default function () {
4 | return {
5 | install(VMdEditor) {
6 | VMdEditor.vMdParser.use(parser);
7 | },
8 | };
9 | }
10 |
--------------------------------------------------------------------------------
/src/plugins/highlight-lines/index.js:
--------------------------------------------------------------------------------
1 | import parser from './parser';
2 |
3 | export default function () {
4 | return {
5 | install(VMdEditor) {
6 | VMdEditor.vMdParser.use(parser);
7 | },
8 | };
9 | }
10 |
--------------------------------------------------------------------------------
/src/plugins/copy-code/parser.js:
--------------------------------------------------------------------------------
1 | import markdownItCopyCode from '@/utils/markdown-it-copy-code';
2 |
3 | export default function (vMdParser) {
4 | vMdParser.extendMarkdown((mdParser) => {
5 | mdParser.use(markdownItCopyCode);
6 | });
7 | }
8 |
--------------------------------------------------------------------------------
/src/plugins/line-number/parser.js:
--------------------------------------------------------------------------------
1 | import markdownItCodeLineNumber from '@vuepress/markdown/lib/lineNumbers';
2 |
3 | export default function (vMdParser) {
4 | vMdParser.extendMarkdown((mdParser) => {
5 | mdParser.use(markdownItCodeLineNumber);
6 | });
7 | }
8 |
--------------------------------------------------------------------------------
/src/plugins/highlight-lines/parser.js:
--------------------------------------------------------------------------------
1 | import markdownItHighlightLines from '@/utils/markdown-it-highlight-lines';
2 |
3 | export default function (vMdParser) {
4 | vMdParser.extendMarkdown((mdParser) => {
5 | mdParser.use(markdownItHighlightLines);
6 | });
7 | }
8 |
--------------------------------------------------------------------------------
/src/plugins/katex/creator.js:
--------------------------------------------------------------------------------
1 | export default function (parser) {
2 | return function createKatexPlugin(katexOptions) {
3 | return {
4 | install(VMdEditor) {
5 | VMdEditor.vMdParser.use(parser, katexOptions);
6 | },
7 | };
8 | };
9 | }
10 |
--------------------------------------------------------------------------------
/src/plugins/emoji/command.js:
--------------------------------------------------------------------------------
1 | export default function commandHandler(editor, emojiType) {
2 | editor.insert(() => {
3 | const prefix = ':';
4 | const suffix = ':';
5 |
6 | return {
7 | text: `${prefix}${emojiType}${suffix}`,
8 | };
9 | });
10 | }
11 |
--------------------------------------------------------------------------------
/src/toolbar/code.js:
--------------------------------------------------------------------------------
1 | import { code } from '@/utils/constants/command';
2 |
3 | export default {
4 | name: code,
5 | icon: 'v-md-icon-code',
6 | title: (editor) => editor.langConfig.code.toolbar,
7 | action(editor) {
8 | editor.execCommand(code);
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/src/toolbar/hr.js:
--------------------------------------------------------------------------------
1 | import { hr } from '@/utils/constants/command';
2 |
3 | export default {
4 | name: hr,
5 | icon: 'v-md-icon-horizontal',
6 | title: (editor) => editor.langConfig.hr.toolbar,
7 | action(editor) {
8 | editor.execCommand(hr);
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/docs/zh/senior/xss-extend.md:
--------------------------------------------------------------------------------
1 | # Xss Extension
2 |
3 | ```js
4 | import VMdEditor from '@kangc/v-md-editor';
5 |
6 | VMdEditor.xss.extend({
7 | // 扩展白名单
8 | whiteList: {
9 | source: [],
10 | },
11 | });
12 | ```
13 |
14 | 配置项参考 [xss](https://www.npmjs.com/package/xss)
15 |
--------------------------------------------------------------------------------
/src/plugins/todo-list/toolbar.js:
--------------------------------------------------------------------------------
1 | export default function createToolbar({ commandName, text, title, icon }) {
2 | return {
3 | title,
4 | icon,
5 | text,
6 | action(editor) {
7 | editor.execCommand(commandName, { type: 'todo' });
8 | },
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/src/toolbar/ol.js:
--------------------------------------------------------------------------------
1 | import { ol } from '@/utils/constants/command';
2 |
3 | export default {
4 | name: ol,
5 | icon: 'v-md-icon-ol',
6 | title: (editor) => `${editor.langConfig.ol.toolbar}(Ctrl+O)`,
7 | action(editor) {
8 | editor.execCommand(ol);
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/src/toolbar/ul.js:
--------------------------------------------------------------------------------
1 | import { ul } from '@/utils/constants/command';
2 |
3 | export default {
4 | name: ul,
5 | icon: 'v-md-icon-ul',
6 | title: (editor) => `${editor.langConfig.ul.toolbar}(Ctrl+U)`,
7 | action(editor) {
8 | editor.execCommand(ul);
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/src/plugins/mermaid/parser.js:
--------------------------------------------------------------------------------
1 | import markdownItMermaid from '@/utils/markdown-it-mermaid';
2 |
3 | export default function parser(vMdParser) {
4 | vMdParser.extendMarkdown((mdParser) => {
5 | if (mdParser) {
6 | mdParser.use(markdownItMermaid);
7 | }
8 | });
9 | }
10 |
--------------------------------------------------------------------------------
/src/toolbar/clear.js:
--------------------------------------------------------------------------------
1 | import { clear } from '@/utils/constants/command';
2 |
3 | export default {
4 | name: clear,
5 | icon: 'v-md-icon-clear',
6 | title: (editor) => editor.langConfig.clear.toolbar,
7 | action(editor) {
8 | editor.execCommand(clear);
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/src/toolbar/table.js:
--------------------------------------------------------------------------------
1 | import { table } from '@/utils/constants/command';
2 |
3 | export default {
4 | name: table,
5 | icon: 'v-md-icon-table',
6 | title: (editor) => editor.langConfig.table.toolbar,
7 | action(editor) {
8 | editor.execCommand(table);
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/src/plugins/mermaid/cdn.js:
--------------------------------------------------------------------------------
1 | import creator from './creator';
2 |
3 | const isServer = typeof window === 'undefined';
4 | if (!isServer && !window.mermaid) {
5 | console.error('Please import resources mermaid from cdn');
6 | }
7 |
8 | export default creator(!isServer ? window.mermaid : null);
9 |
--------------------------------------------------------------------------------
/src/plugins/mermaid/mermaid.css:
--------------------------------------------------------------------------------
1 | .github-markdown-body pre.v-md-mermaid {
2 | background-color: unset;
3 | }
4 |
5 | .vuepress-markdown-body pre.v-md-mermaid {
6 | background-color: unset;
7 | }
8 |
9 | .vuepress-markdown-body pre.v-md-mermaid code {
10 | color: #2c3e50;
11 | }
12 |
--------------------------------------------------------------------------------
/src/toolbar/bold.js:
--------------------------------------------------------------------------------
1 | import { bold } from '@/utils/constants/command';
2 |
3 | export default {
4 | name: bold,
5 | icon: 'v-md-icon-bold',
6 | title: (editor) => `${editor.langConfig.bold.toolbar}(Ctrl+B)`,
7 | action(editor) {
8 | editor.execCommand(bold);
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/src/toolbar/link.js:
--------------------------------------------------------------------------------
1 | import { link } from '@/utils/constants/command';
2 |
3 | export default {
4 | name: link,
5 | icon: 'v-md-icon-link',
6 | title: (editor) => `${editor.langConfig.link.toolbar}(Ctrl+L)`,
7 | action(editor) {
8 | editor.execCommand(link);
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/src/toolbar/redo.js:
--------------------------------------------------------------------------------
1 | import { redo } from '@/utils/constants/command';
2 |
3 | export default {
4 | name: redo,
5 | icon: 'v-md-icon-redo',
6 | title: (editor) => `${editor.langConfig.redo.toolbar}(Ctrl+Y)`,
7 | action(editor) {
8 | editor.execCommand(redo);
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/src/toolbar/undo.js:
--------------------------------------------------------------------------------
1 | import { undo } from '@/utils/constants/command';
2 |
3 | export default {
4 | name: undo,
5 | icon: 'v-md-icon-undo',
6 | title: (editor) => `${editor.langConfig.undo.toolbar}(Ctrl+Z)`,
7 | action(editor) {
8 | editor.execCommand(undo);
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/src/toolbar/quote.js:
--------------------------------------------------------------------------------
1 | import { quote } from '@/utils/constants/command';
2 |
3 | export default {
4 | name: quote,
5 | icon: 'v-md-icon-quote',
6 | title: (editor) => `${editor.langConfig.quote.toolbar}(Ctrl+Q)`,
7 | action(editor) {
8 | editor.execCommand(quote);
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/src/toolbar/italic.js:
--------------------------------------------------------------------------------
1 | import { italic } from '@/utils/constants/command';
2 |
3 | export default {
4 | name: italic,
5 | icon: 'v-md-icon-italic',
6 | title: (editor) => `${editor.langConfig.italic.toolbar}(Ctrl+I)`,
7 | action(editor) {
8 | editor.execCommand(italic);
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/src/utils/xss/common.js:
--------------------------------------------------------------------------------
1 | export const attrWhiteList = ['style', 'align', 'class', 'id'];
2 |
3 | export const prefixAttrWhiteList = ['data-'];
4 |
5 | export const commonWhiteList = {
6 | input: ['type'],
7 | ol: ['reversed', 'start', 'type'],
8 | button: ['type'],
9 | summary: [],
10 | };
11 |
--------------------------------------------------------------------------------
/src/plugins/katex/parser-cdn.js:
--------------------------------------------------------------------------------
1 | import parserCreator from './parser-creator';
2 |
3 | const isServer = typeof window === 'undefined';
4 | if (!isServer && !window.katex) {
5 | console.error('Please import resources katex from cdn');
6 | }
7 |
8 | export default parserCreator(!isServer ? window.katex : null);
9 |
--------------------------------------------------------------------------------
/docs/senior/xss-extend.md:
--------------------------------------------------------------------------------
1 | # Xss Extension
2 |
3 | ```js
4 | import VMdEditor from '@kangc/v-md-editor';
5 |
6 | VMdEditor.xss.extend({
7 | // extend white list
8 | whiteList: {
9 | source: [],
10 | },
11 | });
12 | ```
13 |
14 | Configuration reference [xss](https://www.npmjs.com/package/xss)
15 |
--------------------------------------------------------------------------------
/src/theme/github/index.js:
--------------------------------------------------------------------------------
1 | import parser from './parser';
2 |
3 | // style
4 | import '@/assets/css/theme/base';
5 | import '@/assets/css/theme/github-markdown';
6 |
7 | const install = function (VMdEditor, options) {
8 | VMdEditor.vMdParser.use(parser, options);
9 | };
10 |
11 | export default {
12 | install,
13 | };
14 |
--------------------------------------------------------------------------------
/src/toolbar/strikethrough.js:
--------------------------------------------------------------------------------
1 | import { strikethrough } from '@/utils/constants/command';
2 |
3 | export default {
4 | name: strikethrough,
5 | icon: 'v-md-icon-strikethrough',
6 | title: (editor) => editor.langConfig.strikethrough.toolbar,
7 | action(editor) {
8 | editor.execCommand(strikethrough);
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/src/plugins/copy-code/index.js:
--------------------------------------------------------------------------------
1 | import parser from './parser';
2 | import createCopyCodePreview from './preview';
3 |
4 | export default function createCopyCodePlugin() {
5 | return {
6 | install(VMdEditor) {
7 | VMdEditor.vMdParser.use(parser);
8 |
9 | VMdEditor.use(createCopyCodePreview());
10 | },
11 | };
12 | }
13 |
--------------------------------------------------------------------------------
/src/command/table.js:
--------------------------------------------------------------------------------
1 | export { table as name } from '@/utils/constants/command.js';
2 |
3 | export default function (editor) {
4 | editor.insert(() => {
5 | const content = '|column1|column2|column3|\n|-|-|-|\n|content1|content2|content3|';
6 |
7 | return {
8 | text: content,
9 | selected: 'column1',
10 | };
11 | });
12 | }
13 |
--------------------------------------------------------------------------------
/src/toolbar/toc.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'toc',
3 | icon: 'v-md-icon-toc',
4 | title: (editor) => {
5 | const tocLang = editor.langConfig.toc;
6 |
7 | return editor.tocVisible ? tocLang.disabled : tocLang.enabled;
8 | },
9 | active: (editor) => editor.tocVisible,
10 | action(editor) {
11 | editor.toggleToc();
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/src/command/quote.js:
--------------------------------------------------------------------------------
1 | export { quote as name } from '@/utils/constants/command.js';
2 |
3 | export default function (editor) {
4 | editor.insert((selected) => {
5 | const prefix = '>';
6 | const content = selected || editor.langConfig.quote.placeholder;
7 |
8 | return {
9 | text: `${prefix} ${content}`,
10 | selected: content,
11 | };
12 | });
13 | }
14 |
--------------------------------------------------------------------------------
/src/styles/mixins.scss:
--------------------------------------------------------------------------------
1 | @import './var';
2 |
3 | @mixin scrollbar-style {
4 | &::-webkit-scrollbar {
5 | width: $scrollbar-width;
6 | }
7 |
8 | &::-webkit-scrollbar-thumb {
9 | background-color: $scrollbar-background-color;
10 | border-radius: $scrollbar-border-radius;
11 |
12 | &:hover {
13 | background-color: $scrollbar-active-background-color;
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/theme/github/parser.js:
--------------------------------------------------------------------------------
1 | import createGithubTheme from './theme';
2 |
3 | export default function (vMdParser, options = {}) {
4 | const { extend, config, codeHighlightExtensionMap, Hljs } = options;
5 | const theme = createGithubTheme({
6 | Hljs,
7 | baseConfig: config,
8 | codeHighlightExtensionMap,
9 | });
10 |
11 | if (extend) theme.extend(extend);
12 | vMdParser.theme(theme);
13 | }
14 |
--------------------------------------------------------------------------------
/src/mixins/v-model.js:
--------------------------------------------------------------------------------
1 | export default {
2 | props: {
3 | modelValue: {
4 | type: String,
5 | default: '',
6 | },
7 | },
8 | emits: ['update:modelValue'],
9 | data() {
10 | return {
11 | text: this.modelValue,
12 | };
13 | },
14 | methods: {
15 | handleInput(val) {
16 | this.text = val;
17 | this.$emit('update:modelValue', val);
18 | },
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/src/theme/vuepress/parser.js:
--------------------------------------------------------------------------------
1 | import createVuepressTheme from './theme';
2 |
3 | export default function (vMdParser, options = {}) {
4 | const { extend, config, codeHighlightExtensionMap, Prism } = options;
5 | const theme = createVuepressTheme({
6 | Prism,
7 | baseConfig: config,
8 | codeHighlightExtensionMap,
9 | });
10 |
11 | if (extend) theme.extend(extend);
12 | vMdParser.theme(theme);
13 | }
14 |
--------------------------------------------------------------------------------
/docs/.vuepress/styles/index.styl:
--------------------------------------------------------------------------------
1 | .theme-default-content:not(.custom) {
2 | max-width: 900px;
3 | }
4 |
5 | .page-nav {
6 | max-width: 900px !important;
7 | }
8 |
9 | .github-markdown-body div[class*='v-md-pre-wrapper-'].line-numbers-mode .line-numbers-wrapper .line-number {
10 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif,
11 | Apple Color Emoji, Segoe UI Emoji !important;
12 | }
--------------------------------------------------------------------------------
/src/plugins/katex/parser-creator.js:
--------------------------------------------------------------------------------
1 | import markdownItKatex from '@/utils/markdown-it-katex';
2 |
3 | export default function parserCreator(katex) {
4 | return function parser(vMdParser, katexOptions) {
5 | vMdParser.extendMarkdown((mdParser) => {
6 | if (katex) {
7 | mdParser.use(markdownItKatex, {
8 | ...katexOptions,
9 | katex,
10 | });
11 | }
12 | });
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/src/command/code.js:
--------------------------------------------------------------------------------
1 | export { code as name } from '@/utils/constants/command.js';
2 |
3 | export default function (editor) {
4 | editor.insert((selected) => {
5 | const prefix = '``` language';
6 | const suffix = '```';
7 | let text = `${prefix}\n${suffix}`;
8 |
9 | if (selected) {
10 | text = `${prefix}\n ${selected}\n${suffix}`;
11 | }
12 |
13 | return {
14 | text,
15 | selected: 'language',
16 | };
17 | });
18 | }
19 |
--------------------------------------------------------------------------------
/docs/plugins/line-number.md:
--------------------------------------------------------------------------------
1 | # LineNumber
2 |
3 | github:
4 |
5 |
6 |
7 |
8 |
9 | vuepress:
10 |
11 |
12 |
13 |
14 |
15 | ### Import:
16 |
17 | ```js
18 | import VueMarkdownEditor from '@kangc/v-md-editor';
19 | import createLineNumbertPlugin from '@kangc/v-md-editor/lib/plugins/line-number/index';
20 |
21 | VueMarkdownEditor.use(createLineNumbertPlugin());
22 | ```
23 |
--------------------------------------------------------------------------------
/docs/zh/senior/toc.md:
--------------------------------------------------------------------------------
1 | # 目录导航
2 |
3 | 开启目录导航来提升编辑效率。
4 |
5 |
6 |
7 |
8 |
9 | ## 相关配置
10 |
11 | 配置生成目录导航时包含的标题,默认值为[2, 3]。
12 |
13 | ```vue
14 |
15 |
16 |
17 | ```
18 |
19 | 配置 toc 生成导航时包含的标题,默认值为[2, 3]。
20 |
21 | ```js
22 | VMdEditor.use(githubTheme, {
23 | config: {
24 | toc: {
25 | includeLevel: [3, 4],
26 | },
27 | },
28 | });
29 | ```
30 |
--------------------------------------------------------------------------------
/src/utils/scroll-top.js:
--------------------------------------------------------------------------------
1 | export function getScrollTop(target) {
2 | let result = 0;
3 |
4 | if (target === window) {
5 | result = target.pageYOffset;
6 | } else if (target) {
7 | result = target.scrollTop;
8 | }
9 |
10 | return result;
11 | }
12 |
13 | export function scrollTo(target, scrollTop) {
14 | if (target === window) {
15 | window.scrollTo(window.pageYOffset, scrollTop);
16 | } else if (target) {
17 | target.scrollTop = scrollTop;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/toolbar/fullscreen.js:
--------------------------------------------------------------------------------
1 | import { fullscreen } from '@/utils/constants/command';
2 |
3 | export default {
4 | name: fullscreen,
5 | icon: 'v-md-icon-fullscreen',
6 | title: (editor) => {
7 | const fullscreenLang = editor.langConfig.fullscreen;
8 |
9 | return editor.fullscreen ? fullscreenLang.disabled : fullscreenLang.enabled;
10 | },
11 | active: (editor) => editor.fullscreen,
12 | action(editor) {
13 | editor.execCommand(fullscreen, !editor.fullscreen);
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/src/plugins/emoji/parser-creator.js:
--------------------------------------------------------------------------------
1 | export default function parserCreator(mdEmojiPlugin) {
2 | return function (vMdParser, options = {}) {
3 | vMdParser.extendMarkdown((mdParser) => {
4 | // extend markdown-it
5 | mdParser.use(mdEmojiPlugin);
6 |
7 | if (options.customEmoji) {
8 | mdParser.renderer.rules.emoji = function (token, idx) {
9 | return ' ';
10 | };
11 | }
12 | });
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/src/toolbar/sync-scroll.js:
--------------------------------------------------------------------------------
1 | import { syncScroll } from '@/utils/constants/command';
2 |
3 | export default {
4 | name: syncScroll,
5 | icon: 'v-md-icon-sync',
6 | title: (editor) => {
7 | const syncScrollLang = editor.langConfig.syncScroll;
8 |
9 | return editor.enableSyncScroll ? syncScrollLang.disabled : syncScrollLang.enabled;
10 | },
11 | active: (editor) => editor.enableSyncScroll,
12 | action(editor) {
13 | editor.execCommand(syncScroll, !editor.enableSyncScroll);
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/docs/plugins/highlight-lines.md:
--------------------------------------------------------------------------------
1 | # Highlight Lines
2 |
3 | github:
4 |
5 |
6 |
7 |
8 |
9 | vuepress:
10 |
11 |
12 |
13 |
14 |
15 | ### Import:
16 |
17 | ```js
18 | import VueMarkdownEditor from '@kangc/v-md-editor';
19 | import createHighlightLinesPlugin from '@kangc/v-md-editor/lib/plugins/highlight-lines/index';
20 |
21 | VueMarkdownEditor.use(createHighlightLinesPlugin());
22 | ```
23 |
--------------------------------------------------------------------------------
/docs/zh/plugins/line-number.md:
--------------------------------------------------------------------------------
1 | # LineNumber 代码行号
2 |
3 | 可以引入 `line-number` 插件来支持显示代码行号:
4 |
5 | github:
6 |
7 |
8 |
9 |
10 |
11 | vuepress:
12 |
13 |
14 |
15 |
16 |
17 | ### 引入:
18 |
19 | ```js
20 | import VueMarkdownEditor from '@kangc/v-md-editor';
21 | import createLineNumbertPlugin from '@kangc/v-md-editor/lib/plugins/line-number/index';
22 |
23 | VueMarkdownEditor.use(createLineNumbertPlugin());
24 | ```
25 |
--------------------------------------------------------------------------------
/docs/zh/plugins/highlight-lines.md:
--------------------------------------------------------------------------------
1 | # Highlight Lines 高亮代码行
2 |
3 | github:
4 |
5 |
6 |
7 |
8 |
9 | vuepress:
10 |
11 |
12 |
13 |
14 |
15 | ### Import:
16 |
17 | ```js
18 | import VueMarkdownEditor from '@kangc/v-md-editor';
19 | import createHighlightLinesPlugin from '@kangc/v-md-editor/lib/plugins/highlight-lines/index';
20 |
21 | VueMarkdownEditor.use(createHighlightLinesPlugin());
22 | ```
23 |
--------------------------------------------------------------------------------
/src/theme/vuepress/index.js:
--------------------------------------------------------------------------------
1 | import parser from './parser';
2 | import createTipPlugin from '@/plugins/tip/index';
3 |
4 | // style
5 | import '@/assets/css/theme/base';
6 | import '@/assets/css/theme/vuepress-markdown';
7 |
8 | // tip plugin style
9 | import '@/plugins/tip/tip.css';
10 |
11 | const install = function (VMdEditor, options) {
12 | const tipPlugin = createTipPlugin();
13 |
14 | VMdEditor.vMdParser.use(parser, options);
15 | VMdEditor.use(tipPlugin);
16 | };
17 |
18 | export default {
19 | install,
20 | };
21 |
--------------------------------------------------------------------------------
/docs/.vuepress/enhanceApp.js:
--------------------------------------------------------------------------------
1 | export default async ({ Vue }) => {
2 | const { default: Lang, vMdEditorlangConfig } = await import('../../lib/lang/index');
3 | const { default: enUS } = await import('../../lib/lang/en-US');
4 |
5 | Lang.add({
6 | 'en-US': enUS,
7 | });
8 |
9 | Vue.mixin({
10 | watch: {
11 | $lang: {
12 | immediate: true,
13 | handler() {
14 | if (vMdEditorlangConfig.lang !== this.$lang) vMdEditorlangConfig.lang = this.$lang;
15 | },
16 | },
17 | },
18 | });
19 | };
20 |
--------------------------------------------------------------------------------
/docs/senior/toc.md:
--------------------------------------------------------------------------------
1 | # Toc
2 |
3 |
4 |
5 |
6 |
7 | ## Related configuration
8 |
9 | Configure the titles included when generating directory navigation. The default is [2, 3].
10 |
11 | ```vue
12 |
13 |
14 |
15 | ```
16 |
17 | Configure toc to generate the title included in the navigation, the default value is [2, 3].
18 |
19 | ```js
20 | VMdEditor.use(githubTheme, {
21 | config: {
22 | toc: {
23 | includeLevel: [3, 4],
24 | },
25 | },
26 | });
27 | ```
28 |
--------------------------------------------------------------------------------
/src/utils/lang.js:
--------------------------------------------------------------------------------
1 | import { deepAssign } from '@/utils/deep-assign';
2 |
3 | export default class Lang {
4 | constructor(options = {}) {
5 | this.config = {
6 | lang: 'zh-CN',
7 | langConfig: {
8 | 'zh-CN': {},
9 | },
10 | };
11 |
12 | this.options = options;
13 | }
14 |
15 | use(lang, config) {
16 | this.config.lang = lang;
17 | this.add({ [lang]: config });
18 |
19 | if (this.options.afterUse) this.options.afterUse(lang, config);
20 | }
21 |
22 | add(config = {}) {
23 | deepAssign(this.config.langConfig, config);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/preview-demo.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/zh/README.md:
--------------------------------------------------------------------------------
1 | # 介绍
2 |
3 | v-md-editor 是基于 Vue 开发的 markdown 编辑器组件
4 |
5 | ## 轻量版编辑器
6 |
7 | 轻量版编辑器左侧编辑区域使用 textarea 实现。
8 |
9 | 优点:
10 |
11 | - 足够轻量
12 |
13 | 缺点:
14 |
15 | - 不支持更为丰富的编辑区功能扩展。例如:无法根据不同的语法在输入框内显示不同的样式。
16 |
17 | ## 进阶版编辑器
18 |
19 | 进阶版编辑器左侧编辑区域使用 [CodeMirror](https://codemirror.net/) 实现。
20 |
21 | 优点:
22 |
23 | - 可以根据 CodeMirror 提供的 Api 来自定义扩展编辑区域功能,提高编辑体验。
24 |
25 | 缺点:
26 |
27 | - 文件体积远大于轻量版
28 |
29 | ## 预览组件
30 |
31 | 当你只需要对 markdown 语法进行解析并预览的时候,可以使用该组件。
32 |
33 | ## 特性
34 |
35 | 1. 高度可定制化
36 | 2. 高度可扩展性
37 | 3. 支持自定义主题包
38 | 4. 提供开箱即用的主题包
39 | 5. 提供多个组件。可按需使用。
40 |
--------------------------------------------------------------------------------
/src/toolbar/preview.js:
--------------------------------------------------------------------------------
1 | import EDITOR_MODE from '@/utils/constants/editor-mode';
2 |
3 | export default {
4 | name: 'preview',
5 | icon: 'v-md-icon-preview',
6 | title: (editor) => {
7 | const previewLang = editor.langConfig.preview;
8 |
9 | return editor.currentMode === EDITOR_MODE.EDITABLE ? previewLang.disabled : previewLang.enabled;
10 | },
11 | active: (editor) => editor.currentMode === EDITOR_MODE.EDITABLE,
12 | action(editor) {
13 | editor.currentMode =
14 | editor.currentMode === EDITOR_MODE.EDITABLE ? EDITOR_MODE.EDIT : EDITOR_MODE.EDITABLE;
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/base-editor.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/codemirror-editor.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/theme/github/theme.js:
--------------------------------------------------------------------------------
1 | import createHljsTheme from '@/theme/base/highlight';
2 |
3 | export default function createGithubTheme(config) {
4 | const hljsTheme = createHljsTheme({
5 | Hljs: config.Hljs,
6 | baseConfig: config.baseConfig,
7 | codeBlockClass: config.codeBlockClass || ((lang) => `v-md-hljs-${lang}`),
8 | codeHighlightExtensionMap: config.codeHighlightExtensionMap || {},
9 | });
10 |
11 | return {
12 | previewClass: 'github-markdown-body',
13 | extend(callback) {
14 | hljsTheme.extend(callback);
15 | },
16 | markdownParser: hljsTheme.markdownParser,
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/docs/plugins/emoji.md:
--------------------------------------------------------------------------------
1 | # Emoji
2 |
3 |
4 |
5 |
6 |
7 | ### Import:
8 |
9 | ```js
10 | import VueMarkdownEditor from '@kangc/v-md-editor';
11 | import createEmojiPlugin from '@kangc/v-md-editor/lib/plugins/emoji/index';
12 |
13 | VueMarkdownEditor.use(createEmojiPlugin());
14 | ```
15 |
16 | ### Usage:
17 |
18 | ```vue
19 |
20 |
21 |
22 |
23 |
32 | ```
33 |
--------------------------------------------------------------------------------
/src/plugins/emoji/toolbar.js:
--------------------------------------------------------------------------------
1 | export function generatorMenuItems(emojiJson, commandName) {
2 | return Object.keys(emojiJson).map((emojiType) => ({
3 | name: emojiType,
4 | text: emojiJson[emojiType],
5 | class: 'v-md-emoji-panel-item',
6 | action(editor) {
7 | editor.execCommand(commandName, emojiType);
8 | },
9 | }));
10 | }
11 |
12 | export default function createToolbar({ commandName, emojiJson, text, title, icon }) {
13 | return {
14 | title,
15 | icon,
16 | text,
17 | menus: {
18 | mode: 'panel',
19 | items: generatorMenuItems(emojiJson, commandName),
20 | },
21 | };
22 | }
23 |
--------------------------------------------------------------------------------
/src/styles/transition.scss:
--------------------------------------------------------------------------------
1 | // zoom-in
2 | .v-md-zoom-in-top-enter-active,
3 | .v-md-zoom-in-top-leave-active {
4 | transform: scaleY(1);
5 | opacity: 1;
6 | transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1),
7 | opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
8 | }
9 | .v-md-zoom-in-top-enter-from,
10 | .v-md-zoom-in-top-leave-active {
11 | transform: scaleY(0);
12 | opacity: 0;
13 | }
14 |
15 | // fade-in
16 | .v-md-fade-in-enter-active,
17 | .v-md-fade-in-leave-active {
18 | transition: opacity 0.3s cubic-bezier(0.55, 0, 0.1, 1);
19 | }
20 | .v-md-fade-in-enter-from,
21 | .v-md-fade-in-leave-active {
22 | opacity: 0;
23 | }
24 |
--------------------------------------------------------------------------------
/src/utils/deep-assign.js:
--------------------------------------------------------------------------------
1 | import { isObject } from './util';
2 |
3 | const { hasOwnProperty } = Object.prototype;
4 |
5 | function assignKey(to, from, key) {
6 | const val = from[key];
7 |
8 | if (val === undefined || val === null) {
9 | return;
10 | }
11 |
12 | if (!hasOwnProperty.call(to, key) || !isObject(val)) {
13 | to[key] = val;
14 | } else {
15 | // eslint-disable-next-line
16 | to[key] = deepAssign(Object(to[key]), from[key]);
17 | }
18 | }
19 |
20 | export function deepAssign(to, from) {
21 | Object.keys(from).forEach((key) => {
22 | assignKey(to, from, key);
23 | });
24 |
25 | return to;
26 | }
27 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/vuepress-theme.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/preview-html.js:
--------------------------------------------------------------------------------
1 | // This file is auto generated by build/build-entry.js
2 | import Component from './preview-html.vue';
3 | // font css
4 | import '@/assets/css/font';
5 |
6 | const version = '2.3.18';
7 |
8 | const install = (app) => {
9 | app.component(Component.name, Component);
10 | };
11 |
12 | Component.version = version;
13 | Component.install = install;
14 | Component.use = function (optionsOrInstall, opt) {
15 | if (typeof optionsOrInstall === 'function') {
16 | optionsOrInstall(Component, opt);
17 | } else {
18 | optionsOrInstall.install(Component, opt);
19 | }
20 |
21 | return Component;
22 | };
23 |
24 | export default Component;
25 |
--------------------------------------------------------------------------------
/src/preview-html.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
33 |
--------------------------------------------------------------------------------
/src/utils/markdown-it-mermaid.js:
--------------------------------------------------------------------------------
1 | export default function (md, { className = 'v-md-mermaid' } = {}) {
2 | const wrap = (wrapped) => (...args) => {
3 | const [tokens, idx] = args;
4 | const token = tokens[idx];
5 | const rawCode = wrapped(...args);
6 |
7 | if (token.info === 'mermaid') {
8 | return `
${token.content
9 | .replace(//g, '>')} `;
11 | }
12 |
13 | return rawCode;
14 | };
15 |
16 | const { fence, code_block: codeBlock } = md.renderer.rules;
17 | md.renderer.rules.fence = wrap(fence);
18 | md.renderer.rules.code_block = wrap(codeBlock);
19 | }
20 |
--------------------------------------------------------------------------------
/docs/zh/plugins/emoji.md:
--------------------------------------------------------------------------------
1 | # Emoji 表情插件
2 |
3 | 可以引入 `emoji` 插件来支持插入 emoji 表情:
4 |
5 |
6 |
7 |
8 |
9 | ### 引入:
10 |
11 | ```js
12 | import VueMarkdownEditor from '@kangc/v-md-editor';
13 | import createEmojiPlugin from '@kangc/v-md-editor/lib/plugins/emoji/index';
14 |
15 | VueMarkdownEditor.use(createEmojiPlugin());
16 | ```
17 |
18 | ### 使用:
19 |
20 | ```vue
21 |
22 |
23 |
24 |
25 |
34 | ```
35 |
--------------------------------------------------------------------------------
/src/command/h1.js:
--------------------------------------------------------------------------------
1 | import { generatorText } from '@/utils/util';
2 |
3 | export { h1 as name } from '@/utils/constants/command.js';
4 |
5 | export default function (editor) {
6 | editor.insert((selected) => {
7 | const prefix = '#';
8 | const { placeholder } = editor.langConfig.h1;
9 |
10 | const selectedGetter = (selected) => selected || placeholder;
11 | const { insertContent, newSelected } = generatorText({
12 | selected,
13 | InsertGetter: (selected) => `${prefix} ${selectedGetter(selected)}`,
14 | selectedGetter,
15 | });
16 |
17 | return {
18 | text: insertContent,
19 | selected: newSelected,
20 | };
21 | });
22 | }
23 |
--------------------------------------------------------------------------------
/src/command/ul.js:
--------------------------------------------------------------------------------
1 | import { generatorText } from '@/utils/util';
2 |
3 | export { ul as name } from '@/utils/constants/command.js';
4 |
5 | export default function (editor) {
6 | editor.insert((selected) => {
7 | const { placeholder } = editor.langConfig.ul;
8 |
9 | const selectedGetter = (selected) => selected || placeholder;
10 | const { insertContent, newSelected } = generatorText({
11 | selected,
12 | InsertGetter: (selected) => `- ${selectedGetter(selected)}`,
13 | selectedGetter,
14 | ignoreEmptyLine: false,
15 | });
16 |
17 | return {
18 | text: insertContent,
19 | selected: newSelected,
20 | };
21 | });
22 | }
23 |
--------------------------------------------------------------------------------
/src/command/h2.js:
--------------------------------------------------------------------------------
1 | import { generatorText } from '@/utils/util';
2 |
3 | export { h2 as name } from '@/utils/constants/command.js';
4 |
5 | export default function (editor) {
6 | editor.insert((selected) => {
7 | const prefix = '##';
8 | const { placeholder } = editor.langConfig.h2;
9 |
10 | const selectedGetter = (selected) => selected || placeholder;
11 | const { insertContent, newSelected } = generatorText({
12 | selected,
13 | InsertGetter: (selected) => `${prefix} ${selectedGetter(selected)}`,
14 | selectedGetter,
15 | });
16 |
17 | return {
18 | text: insertContent,
19 | selected: newSelected,
20 | };
21 | });
22 | }
23 |
--------------------------------------------------------------------------------
/src/command/h3.js:
--------------------------------------------------------------------------------
1 | import { generatorText } from '@/utils/util';
2 |
3 | export { h3 as name } from '@/utils/constants/command.js';
4 |
5 | export default function (editor) {
6 | editor.insert((selected) => {
7 | const prefix = '###';
8 | const { placeholder } = editor.langConfig.h3;
9 |
10 | const selectedGetter = (selected) => selected || placeholder;
11 | const { insertContent, newSelected } = generatorText({
12 | selected,
13 | InsertGetter: (selected) => `${prefix} ${selectedGetter(selected)}`,
14 | selectedGetter,
15 | });
16 |
17 | return {
18 | text: insertContent,
19 | selected: newSelected,
20 | };
21 | });
22 | }
23 |
--------------------------------------------------------------------------------
/src/command/h4.js:
--------------------------------------------------------------------------------
1 | import { generatorText } from '@/utils/util';
2 |
3 | export { h4 as name } from '@/utils/constants/command.js';
4 |
5 | export default function (editor) {
6 | editor.insert((selected) => {
7 | const prefix = '####';
8 | const { placeholder } = editor.langConfig.h4;
9 |
10 | const selectedGetter = (selected) => selected || placeholder;
11 | const { insertContent, newSelected } = generatorText({
12 | selected,
13 | InsertGetter: (selected) => `${prefix} ${selectedGetter(selected)}`,
14 | selectedGetter,
15 | });
16 |
17 | return {
18 | text: insertContent,
19 | selected: newSelected,
20 | };
21 | });
22 | }
23 |
--------------------------------------------------------------------------------
/src/command/h5.js:
--------------------------------------------------------------------------------
1 | import { generatorText } from '@/utils/util';
2 |
3 | export { h5 as name } from '@/utils/constants/command.js';
4 |
5 | export default function (editor) {
6 | editor.insert((selected) => {
7 | const prefix = '#####';
8 | const { placeholder } = editor.langConfig.h5;
9 |
10 | const selectedGetter = (selected) => selected || placeholder;
11 | const { insertContent, newSelected } = generatorText({
12 | selected,
13 | InsertGetter: (selected) => `${prefix} ${selectedGetter(selected)}`,
14 | selectedGetter,
15 | });
16 |
17 | return {
18 | text: insertContent,
19 | selected: newSelected,
20 | };
21 | });
22 | }
23 |
--------------------------------------------------------------------------------
/src/command/h6.js:
--------------------------------------------------------------------------------
1 | import { generatorText } from '@/utils/util';
2 |
3 | export { h6 as name } from '@/utils/constants/command.js';
4 |
5 | export default function (editor) {
6 | editor.insert((selected) => {
7 | const prefix = '######';
8 | const { placeholder } = editor.langConfig.h6;
9 |
10 | const selectedGetter = (selected) => selected || placeholder;
11 | const { insertContent, newSelected } = generatorText({
12 | selected,
13 | InsertGetter: (selected) => `${prefix} ${selectedGetter(selected)}`,
14 | selectedGetter,
15 | });
16 |
17 | return {
18 | text: insertContent,
19 | selected: newSelected,
20 | };
21 | });
22 | }
23 |
--------------------------------------------------------------------------------
/src/plugins/todo-list/command.js:
--------------------------------------------------------------------------------
1 | import { generatorText } from '@/utils/util';
2 |
3 | export default function commandHandler(editor, { type = 'todo' } = {}) {
4 | editor.insert((selected) => {
5 | const { placeholder } = editor.langConfig.task;
6 | const markup = type === 'todo' ? ' ' : 'x';
7 |
8 | const selectedGetter = (selected) => selected || placeholder;
9 | const { insertContent, newSelected } = generatorText({
10 | selected,
11 | InsertGetter: (selected) => `- [${markup}] ${selectedGetter(selected)}`,
12 | selectedGetter,
13 | });
14 |
15 | return {
16 | text: insertContent,
17 | selected: newSelected,
18 | };
19 | });
20 | }
21 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/plugin-katex.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | v-md-editor is a markdown editor component developed based on Vue
4 |
5 | ## Base Editor
6 |
7 | The editing area on the left of the base editor is implemented using textarea.
8 |
9 | Advantage:
10 |
11 | - lightweight
12 |
13 | Disadvantages:
14 |
15 | - The editing experience is not good enough。
16 |
17 | ## CodeMirror
18 |
19 | The editing area on the left of the base editor is implemented using [CodeMirror](https://codemirror.net/).
20 |
21 | Advantage:
22 |
23 | - Good editing experience。
24 |
25 | Disadvantages:
26 |
27 | - Larger resources
28 |
29 | ## Preview Component
30 |
31 | If you only need to preview markdown, you can use this component.
32 |
--------------------------------------------------------------------------------
/docs/examples/base-editor.md:
--------------------------------------------------------------------------------
1 | # Base Editor
2 |
3 |
4 |
5 |
6 |
7 | ## Import
8 |
9 | ```js
10 | import Vue from 'vue';
11 | import VMdEditor from '@kangc/v-md-editor';
12 | import '@kangc/v-md-editor/lib/style/base-editor.css';
13 | import githubTheme from '@kangc/v-md-editor/lib/theme/github.js';
14 |
15 | VMdEditor.use(githubTheme);
16 |
17 | Vue.use(VMdEditor);
18 | ```
19 |
20 | ## Usage
21 |
22 | ```vue
23 |
24 |
25 |
26 |
27 |
36 | ```
37 |
--------------------------------------------------------------------------------
/src/command/ol.js:
--------------------------------------------------------------------------------
1 | import { generatorText } from '@/utils/util';
2 |
3 | export { ol as name } from '@/utils/constants/command.js';
4 |
5 | export default function (editor) {
6 | editor.insert((selected) => {
7 | const { placeholder } = editor.langConfig.ol;
8 |
9 | const selectedGetter = (selected) => selected || placeholder;
10 | const { insertContent, newSelected } = generatorText({
11 | selected,
12 | InsertGetter: (selected, rowIndex) => `${rowIndex}. ${selectedGetter(selected)}`,
13 | selectedGetter,
14 | ignoreEmptyLine: false,
15 | });
16 |
17 | return {
18 | text: insertContent,
19 | selected: newSelected,
20 | };
21 | });
22 | }
23 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/extend-vuepress-theme.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/examples/preview-demo.md:
--------------------------------------------------------------------------------
1 | # Preview Component
2 |
3 |
4 |
5 |
6 |
7 | ## Import
8 |
9 | ```js
10 | import Vue from 'vue';
11 | import VMdPreview from '@kangc/v-md-editor/lib/preview';
12 | import '@kangc/v-md-editor/lib/style/preview.css';
13 | import githubTheme from '@kangc/v-md-editor/lib/theme/github.js';
14 |
15 | VMdPreview.use(githubTheme);
16 |
17 | Vue.use(VMdPreview);
18 | ```
19 |
20 | ## Usage
21 |
22 | ```vue
23 |
24 |
25 |
26 |
27 |
36 | ```
37 |
--------------------------------------------------------------------------------
/docs/plugins/tip.md:
--------------------------------------------------------------------------------
1 | # Tip
2 |
3 |
4 |
5 |
6 |
7 | ### Import:
8 |
9 | ```js
10 | import VueMarkdownEditor from '@kangc/v-md-editor';
11 | import createTipPlugin from '@kangc/v-md-editor/lib/plugins/tip/index';
12 |
13 | VueMarkdownEditor.use(createTipPlugin());
14 | ```
15 |
16 | ### Usage:
17 |
18 | ```vue
19 |
20 |
21 |
22 |
23 |
32 | ```
33 |
34 | ::: warning
35 | This plugin is already built into the vuePress theme
36 | :::
37 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/image-size.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/toc-nav.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/docs/zh/plugins/tip.md:
--------------------------------------------------------------------------------
1 | # Tip 提示插件
2 |
3 | 可以引入 `tip` 插件来支持插入提示信息:
4 |
5 |
6 |
7 |
8 |
9 | ### 引入:
10 |
11 | ```js
12 | import VueMarkdownEditor from '@kangc/v-md-editor';
13 | import createTipPlugin from '@kangc/v-md-editor/lib/plugins/tip/index';
14 |
15 | VueMarkdownEditor.use(createTipPlugin());
16 | ```
17 |
18 | ### 使用:
19 |
20 | ```vue
21 |
22 |
23 |
24 |
25 |
34 | ```
35 |
36 | ::: warning 注意
37 | vuepress 主题已内置此插件
38 | :::
39 |
--------------------------------------------------------------------------------
/src/command/link.js:
--------------------------------------------------------------------------------
1 | import { generatorText } from '@/utils/util';
2 |
3 | export { link as name } from '@/utils/constants/command.js';
4 |
5 | export default function (editor) {
6 | editor.insert((selected) => {
7 | const { descPlaceholder } = editor.langConfig.link;
8 | const linkPlaceholder = 'http://';
9 |
10 | const { insertContent, newSelected } = generatorText({
11 | selected,
12 | InsertGetter: (selected) => `[${selected || descPlaceholder}](${linkPlaceholder})`,
13 | selectedGetter: (selected) => (selected ? linkPlaceholder : descPlaceholder),
14 | });
15 |
16 | return {
17 | text: insertContent,
18 | selected: newSelected,
19 | };
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/src/command/bold.js:
--------------------------------------------------------------------------------
1 | import { generatorText } from '@/utils/util';
2 |
3 | export { bold as name } from '@/utils/constants/command.js';
4 |
5 | export default function (editor) {
6 | editor.insert((selected) => {
7 | const prefix = '**';
8 | const suffix = '**';
9 | const { placeholder } = editor.langConfig.bold;
10 |
11 | const selectedGetter = (selected) => selected || placeholder;
12 | const { insertContent, newSelected } = generatorText({
13 | selected,
14 | InsertGetter: (selected) => `${prefix}${selectedGetter(selected)}${suffix}`,
15 | selectedGetter,
16 | });
17 |
18 | return {
19 | text: insertContent,
20 | selected: newSelected,
21 | };
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/src/preview.js:
--------------------------------------------------------------------------------
1 | // This file is auto generated by build/build-entry.js
2 | import Component from './preview.vue';
3 | import xss from '@/utils/xss/index';
4 | // font css
5 | import '@/assets/css/font';
6 |
7 | const version = '2.3.18';
8 |
9 | const install = (app) => {
10 | app.component(Component.name, Component);
11 | };
12 |
13 | Component.version = version;
14 | Component.install = install;
15 | Component.xss = xss;
16 |
17 | Component.use = function (optionsOrInstall, opt) {
18 | if (typeof optionsOrInstall === 'function') {
19 | optionsOrInstall(Component, opt);
20 | } else {
21 | optionsOrInstall.install(Component, opt);
22 | }
23 |
24 | return Component;
25 | };
26 |
27 | export default Component;
28 |
--------------------------------------------------------------------------------
/src/theme/vuepress/theme.js:
--------------------------------------------------------------------------------
1 | import createPrismTheme from '@/theme/base/prism';
2 |
3 | export default function createVuepressTheme(config) {
4 | const prismTheme = createPrismTheme({
5 | Prism: config.Prism,
6 | codeHighlightExtensionMap: config.codeHighlightExtensionMap || {},
7 | codeBlockClass: config.codeBlockClass || ((lang) => `v-md-prism-${lang}`),
8 | baseConfig: {
9 | link: {
10 | openLinkIcon: true,
11 | },
12 | ...config.baseConfig,
13 | },
14 | });
15 |
16 | return {
17 | previewClass: 'vuepress-markdown-body',
18 | extend(callback) {
19 | prismTheme.extend(callback);
20 | },
21 | markdownParser: prismTheme.markdownParser,
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/line-number-vuepress.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/plugin-emoji.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/command/italic.js:
--------------------------------------------------------------------------------
1 | import { generatorText } from '@/utils/util';
2 |
3 | export { italic as name } from '@/utils/constants/command.js';
4 |
5 | export default function (editor) {
6 | editor.insert((selected) => {
7 | const prefix = '*';
8 | const suffix = '*';
9 | const { placeholder } = editor.langConfig.italic;
10 |
11 | const selectedGetter = (selected) => selected || placeholder;
12 | const { insertContent, newSelected } = generatorText({
13 | selected,
14 | InsertGetter: (selected) => `${prefix}${selectedGetter(selected)}${suffix}`,
15 | selectedGetter,
16 | });
17 |
18 | return {
19 | text: insertContent,
20 | selected: newSelected,
21 | };
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/docs/zh/examples/preview-demo.md:
--------------------------------------------------------------------------------
1 | # 预览组件
2 |
3 | 当你的项目中只需要解析预览 markdown 的时候,可以直接使用此组件。
4 |
5 |
6 |
7 |
8 |
9 | ## 引入
10 |
11 | ```js
12 | import Vue from 'vue';
13 | import VMdPreview from '@kangc/v-md-editor/lib/preview';
14 | import '@kangc/v-md-editor/lib/style/preview.css';
15 | import githubTheme from '@kangc/v-md-editor/lib/theme/github.js';
16 |
17 | VMdPreview.use(githubTheme);
18 |
19 | Vue.use(VMdPreview);
20 | ```
21 |
22 | ## 使用
23 |
24 | ```vue
25 |
26 |
27 |
28 |
29 |
38 | ```
39 |
--------------------------------------------------------------------------------
/src/base-editor.js:
--------------------------------------------------------------------------------
1 | // This file is auto generated by build/build-entry.js
2 | import Component from './base-editor.vue';
3 | import xss from '@/utils/xss/index';
4 | // font css
5 | import '@/assets/css/font';
6 |
7 | const version = '2.3.18';
8 |
9 | const install = (app) => {
10 | app.component(Component.name, Component);
11 | };
12 |
13 | Component.version = version;
14 | Component.install = install;
15 | Component.xss = xss;
16 |
17 | Component.use = function (optionsOrInstall, opt) {
18 | if (typeof optionsOrInstall === 'function') {
19 | optionsOrInstall(Component, opt);
20 | } else {
21 | optionsOrInstall.install(Component, opt);
22 | }
23 |
24 | return Component;
25 | };
26 |
27 | export default Component;
28 |
--------------------------------------------------------------------------------
/docs/zh/examples/base-editor.md:
--------------------------------------------------------------------------------
1 | # 轻量版
2 |
3 | 编辑器由 textarea 实现,十分轻量。如果对编辑器的没有较高的要求,推荐使用此组件。
4 |
5 |
6 |
7 |
8 |
9 | ## 引入
10 |
11 | ```js
12 | import Vue from 'vue';
13 | import VMdEditor from '@kangc/v-md-editor';
14 | import '@kangc/v-md-editor/lib/style/base-editor.css';
15 | import githubTheme from '@kangc/v-md-editor/lib/theme/github.js';
16 |
17 | VMdEditor.use(githubTheme);
18 |
19 | Vue.use(VMdEditor);
20 | ```
21 |
22 | ## 使用
23 |
24 | ```vue
25 |
26 |
27 |
28 |
29 |
38 | ```
39 |
--------------------------------------------------------------------------------
/src/codemirror-editor.js:
--------------------------------------------------------------------------------
1 | // This file is auto generated by build/build-entry.js
2 | import Component from './codemirror-editor.vue';
3 | import xss from '@/utils/xss/index';
4 | // font css
5 | import '@/assets/css/font';
6 |
7 | const version = '2.3.18';
8 |
9 | const install = (app) => {
10 | app.component(Component.name, Component);
11 | };
12 |
13 | Component.version = version;
14 | Component.install = install;
15 | Component.xss = xss;
16 |
17 | Component.use = function (optionsOrInstall, opt) {
18 | if (typeof optionsOrInstall === 'function') {
19 | optionsOrInstall(Component, opt);
20 | } else {
21 | optionsOrInstall.install(Component, opt);
22 | }
23 |
24 | return Component;
25 | };
26 |
27 | export default Component;
28 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/plugin-todo-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/utils/constants/command.js:
--------------------------------------------------------------------------------
1 | export const bold = 'bold';
2 | export const clear = 'clear';
3 | export const code = 'code';
4 | export const fullscreen = 'fullscreen';
5 | export const h1 = 'h1';
6 | export const h2 = 'h2';
7 | export const h3 = 'h3';
8 | export const h4 = 'h4';
9 | export const h5 = 'h5';
10 | export const h6 = 'h6';
11 | export const hr = 'hr';
12 | export const image = 'image';
13 | export const italic = 'italic';
14 | export const link = 'link';
15 | export const ol = 'ol';
16 | export const quote = 'quote';
17 | export const redo = 'redo';
18 | export const strikethrough = 'strikethrough';
19 | export const syncScroll = 'sync-scroll';
20 | export const table = 'table';
21 | export const ul = 'ul';
22 | export const undo = 'undo';
23 |
--------------------------------------------------------------------------------
/src/command/strikethrough.js:
--------------------------------------------------------------------------------
1 | import { generatorText } from '@/utils/util';
2 |
3 | export { strikethrough as name } from '@/utils/constants/command.js';
4 |
5 | export default function (editor) {
6 | editor.insert((selected) => {
7 | const prefix = '~~';
8 | const suffix = '~~';
9 | const { placeholder } = editor.langConfig.strikethrough;
10 |
11 | const selectedGetter = (selected) => selected || placeholder;
12 | const { insertContent, newSelected } = generatorText({
13 | selected,
14 | InsertGetter: (selected) => `${prefix}${selectedGetter(selected)}${suffix}`,
15 | selectedGetter,
16 | });
17 |
18 | return {
19 | text: insertContent,
20 | selected: newSelected,
21 | };
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/plugin-tip.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/docs/zh/quick-start.md:
--------------------------------------------------------------------------------
1 | ## 安装
2 |
3 | ```bash
4 | # 使用 npm
5 | npm i @kangc/v-md-editor -S
6 |
7 | # 使用yarn
8 | yarn add @kangc/v-md-editor
9 | ```
10 |
11 | ## 快速开始
12 |
13 | ```js
14 | import Vue from 'vue';
15 | import VueMarkdownEditor from '@kangc/v-md-editor';
16 | import '@kangc/v-md-editor/lib/style/base-editor.css';
17 | import vuepressTheme from '@kangc/v-md-editor/lib/theme/vuepress.js';
18 |
19 | VueMarkdownEditor.use(vuepressTheme);
20 |
21 | Vue.use(VueMarkdownEditor);
22 | ```
23 |
24 | ## 使用
25 |
26 | ```vue
27 |
28 |
29 |
30 |
31 |
40 | ```
41 |
--------------------------------------------------------------------------------
/src/utils/xss/KaTex.js:
--------------------------------------------------------------------------------
1 | export const katexTagWhiteList = {
2 | math: [],
3 | annotation: [],
4 | semantics: [],
5 | mtext: [],
6 | mn: [],
7 | mo: [],
8 | mi: [],
9 | mspace: [],
10 | mover: [],
11 | munder: [],
12 | munderover: [],
13 | msup: [],
14 | msub: [],
15 | msubsup: [],
16 | mfrac: [],
17 | mroot: [],
18 | msqrt: [],
19 | mtable: [],
20 | mtr: [],
21 | mtd: [],
22 | mlabeledtr: [],
23 | mrow: [],
24 | menclose: [],
25 | mstyle: [],
26 | mpadded: [],
27 | mphantom: [],
28 | mglyph: [],
29 | };
30 |
31 | export const katexAttrWhiteList = [
32 | 'mathcolor',
33 | 'mathbackground',
34 | 'mathsize',
35 | 'mathvariant',
36 | 'mathfamily',
37 | 'mathweight',
38 | 'mathstyle',
39 | 'mathdisplay',
40 | ];
41 |
--------------------------------------------------------------------------------
/docs/quick-start.md:
--------------------------------------------------------------------------------
1 | ## Install
2 |
3 | ```bash
4 | # use npm
5 | npm i @kangc/v-md-editor -S
6 |
7 | # use yarn
8 | yarn add @kangc/v-md-editor
9 | ```
10 |
11 | ## Quick Start
12 |
13 | ```js
14 | import Vue from 'vue';
15 | import VueMarkdownEditor from '@kangc/v-md-editor';
16 | import '@kangc/v-md-editor/lib/style/base-editor.css';
17 | import vuepressTheme from '@kangc/v-md-editor/lib/theme/vuepress.js';
18 |
19 | VueMarkdownEditor.use(vuepressTheme);
20 |
21 | Vue.use(VueMarkdownEditor);
22 | ```
23 |
24 | ## Usage
25 |
26 | ```vue
27 |
28 |
29 |
30 |
31 |
40 | ```
41 |
--------------------------------------------------------------------------------
/docs/zh/Internationalization.md:
--------------------------------------------------------------------------------
1 | # 国际化
2 |
3 | ::: warning 注意
4 | 1.4.0 版本开始支持国际化
5 | :::
6 |
7 | ## 介绍
8 |
9 | v-md-editor 默认采用中文作为语言,如果需要使用其他语言,可以参考下面的方案。
10 |
11 | ## 语言切换
12 |
13 | 通过 use 方法可以切换当前使用的语言,示例如下:
14 |
15 | ```js
16 | import enUS from '@kangc/v-md-editor/lib/lang/en-US';
17 |
18 | VueMarkdownEditor.lang.use('en-US', enUS);
19 | ```
20 |
21 | ## 修改默认文案
22 |
23 | 通过 add 方法可以实现文案的修改和扩展,示例如下:
24 |
25 | ```js
26 | VueMarkdownEditor.lang.add({
27 | 'zh-CN': {
28 | h1: {
29 | toolbar: '标题1',
30 | },
31 | },
32 | });
33 | ```
34 |
35 | ## 配置文件
36 |
37 | | 语言 | 文件名 |
38 | | -------- | ------ |
39 | | 简体中文 | zh-CN |
40 | | 英语 | en-US |
41 | | 韩文 | ko-KR |
42 |
43 | 在 [这里](https://github.com/code-farmer-i/vue-markdown-editor/tree/dev/src/lang) 查看所有的 i18n 配置文件
44 |
--------------------------------------------------------------------------------
/docs/examples/codemirror-editor.md:
--------------------------------------------------------------------------------
1 | # CodeMirror Editor
2 |
3 |
4 |
5 |
6 |
7 | ## Import
8 |
9 | ```js
10 | import Vue from 'vue';
11 | import VMdEditor from '@kangc/v-md-editor/lib/codemirror-editor';
12 | import '@kangc/v-md-editor/lib/style/codemirror-editor.css';
13 | import githubTheme from '@kangc/v-md-editor/lib/theme/github.js';
14 |
15 | VMdEditor.use(githubTheme);
16 |
17 | Vue.use(VMdEditor);
18 | ```
19 |
20 | ## Usage
21 |
22 | ```vue
23 |
24 |
25 |
26 |
27 |
36 | ```
37 |
38 | Reference:
39 |
40 | - [CodeMirror](https://codemirror.net/)
41 |
--------------------------------------------------------------------------------
/src/mixins/command.js:
--------------------------------------------------------------------------------
1 | import registerCommand from '@/utils/command';
2 |
3 | export default function (Component) {
4 | return {
5 | created() {
6 | const { commands } = Component;
7 |
8 | this.commands = {};
9 |
10 | Object.keys(commands).forEach((name) => {
11 | this.registerCommand(name, commands[name]);
12 | });
13 | },
14 | methods: {
15 | registerCommand(name, callback) {
16 | registerCommand(this.commands, name, callback);
17 | },
18 | execCommand(name, ...arg) {
19 | const commandCallBack = this.commands[name];
20 |
21 | if (commandCallBack) {
22 | commandCallBack(this, ...arg);
23 | } else {
24 | console.error(`Command not found: ${name}`);
25 | }
26 | },
27 | },
28 | };
29 | }
30 |
--------------------------------------------------------------------------------
/src/utils/toolbar.js:
--------------------------------------------------------------------------------
1 | import { importAll } from '@/utils/util';
2 |
3 | const defaultToolbars = {};
4 | importAll(defaultToolbars, require.context('@/toolbar', false, /\.(js)$/));
5 |
6 | export default function registerToolbar(target, name, config) {
7 | if (name) {
8 | target[name] = { ...config };
9 | } else {
10 | console.error('Toolbar name is required');
11 | }
12 | }
13 |
14 | export function toolbarWrapper(component) {
15 | component.toolbars = {};
16 |
17 | component.toolbar = function (name, config) {
18 | registerToolbar(component.toolbars, name, config);
19 | };
20 |
21 | Object.keys(defaultToolbars).forEach((key) => {
22 | const module = defaultToolbars[key];
23 | const { default: config } = module;
24 |
25 | component.toolbar(config.name, config);
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/extend-github-theme.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/zh/examples/codemirror-editor.md:
--------------------------------------------------------------------------------
1 | # 进阶版
2 |
3 | 编辑器由 CodeMirror 实现,拥有较为良好的编辑体验,但是包的体积会变大。
4 |
5 |
6 |
7 |
8 |
9 | ## 引入
10 |
11 | ```js
12 | import Vue from 'vue';
13 | import VMdEditor from '@kangc/v-md-editor/lib/codemirror-editor';
14 | import '@kangc/v-md-editor/lib/style/codemirror-editor.css';
15 | import githubTheme from '@kangc/v-md-editor/lib/theme/github.js';
16 |
17 | VMdEditor.use(githubTheme);
18 |
19 | Vue.use(VMdEditor);
20 | ```
21 |
22 | ## 使用
23 |
24 | ```vue
25 |
26 |
27 |
28 |
29 |
38 | ```
39 |
40 | 参考:
41 |
42 | - [CodeMirror](https://codemirror.net/)
43 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/line-number-github.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/plugins/align/parser.js:
--------------------------------------------------------------------------------
1 | import markdownItContainer from '@/utils/markdown-it-container';
2 |
3 | export default function (vMdParser) {
4 | vMdParser.extendMarkdown((mdParser) => {
5 | markdownItContainer(mdParser, {
6 | type: 'align-left',
7 | defaultTitle: '',
8 | before: (info) => `${info || ''}\n`,
9 | after: () => '
\n',
10 | });
11 |
12 | markdownItContainer(mdParser, {
13 | type: 'align-center',
14 | defaultTitle: '',
15 | before: (info) => `${info || ''}\n`,
16 | after: () => '
\n',
17 | });
18 |
19 | markdownItContainer(mdParser, {
20 | type: 'align-right',
21 | defaultTitle: '',
22 | before: (info) => `${info || ''}\n`,
23 | after: () => '
\n',
24 | });
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | },
6 | extends: ['plugin:vue/vue3-recommended', 'eslint:recommended'],
7 | parserOptions: {
8 | parser: 'babel-eslint',
9 | },
10 | rules: {
11 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
13 | 'vue/attributes-order': 'off',
14 | 'vue/no-v-html': 'off',
15 | 'vue/require-v-for-key': 'off',
16 | 'vue/require-default-prop': 'off',
17 | 'vue/no-unused-components': 'off',
18 | 'vue/no-template-shadow': 'off',
19 | 'vue/name-property-casing': ['error', 'kebab-case'],
20 | 'vue/component-definition-name-casing': ['error', 'kebab-case'],
21 | 'vue/component-name-in-template-casing': ['error', 'kebab-case'],
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/plugin-highlight-lines-vuepress.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/utils/markdown-it-pre-wrapper.js:
--------------------------------------------------------------------------------
1 | // Modified from https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/markdown/lib/preWrapper.js
2 |
3 | export default function (md, options = {}) {
4 | const { getWrapperClass = (lang) => `language-${lang}` } = options;
5 |
6 | const wrap = (wrapped) => (...args) => {
7 | const [tokens, idx] = args;
8 | const token = tokens[idx];
9 | const rawCode = wrapped(...args);
10 |
11 | return (
12 | ``
16 | );
17 | };
18 |
19 | const { fence, code_block: codeBlock } = md.renderer.rules;
20 | md.renderer.rules.fence = wrap(fence);
21 | md.renderer.rules.code_block = wrap(codeBlock);
22 | }
23 |
--------------------------------------------------------------------------------
/docs/plugins/katex.md:
--------------------------------------------------------------------------------
1 | # Katex
2 |
3 |
4 |
5 |
6 |
7 | ### Introduce katex resources from cdn
8 |
9 | ```html
10 |
11 |
12 | ```
13 |
14 | ### Import Plugin:
15 |
16 | ```js
17 | import VueMarkdownEditor from '@kangc/v-md-editor';
18 | import createKatexPlugin from '@kangc/v-md-editor/lib/plugins/katex/cdn';
19 |
20 | VueMarkdownEditor.use(createKatexPlugin());
21 | ```
22 |
23 | ### Usage:
24 |
25 | ```vue
26 |
27 |
28 |
29 |
30 |
39 | ```
40 |
--------------------------------------------------------------------------------
/docs/zh/plugins/katex.md:
--------------------------------------------------------------------------------
1 | # Katex 插件
2 |
3 | 可以引入 `katex` 插件来支持插入 katex 公式:
4 |
5 |
6 |
7 |
8 |
9 | ### cdn 引入 katex 资源
10 |
11 | ```html
12 |
13 |
14 | ```
15 |
16 | ### 引入插件:
17 |
18 | ```js
19 | import VueMarkdownEditor from '@kangc/v-md-editor';
20 | import createKatexPlugin from '@kangc/v-md-editor/lib/plugins/katex/cdn';
21 |
22 | VueMarkdownEditor.use(createKatexPlugin());
23 | ```
24 |
25 | ### 使用:
26 |
27 | ```vue
28 |
29 |
30 |
31 |
32 |
41 | ```
42 |
--------------------------------------------------------------------------------
/docs/zh/plugins/todo-list.md:
--------------------------------------------------------------------------------
1 | # TodoList 任务列表
2 |
3 | 可以引入 `todo-list` 插件来支持插入 任务列表:
4 |
5 |
6 |
7 |
8 |
9 | ### 引入:
10 |
11 | ```js
12 | import VueMarkdownEditor from '@kangc/v-md-editor';
13 | import createTodoListPlugin from '@kangc/v-md-editor/lib/plugins/todo-list/index';
14 |
15 | VueMarkdownEditor.use(createTodoListPlugin());
16 | ```
17 |
18 | ### 使用:
19 |
20 | ```vue
21 |
22 |
23 |
24 |
25 |
34 | ```
35 |
36 | ### Options
37 |
38 | createTodoListPlugin 时可以传入的参数。例如 createTodoListPlugin({ color: '' })
39 |
40 | ### color
41 |
42 | - 类型: `String`。
43 | - 默认值: `#3eaf7c`。
44 |
45 | checkbox 的颜色。
46 |
--------------------------------------------------------------------------------
/src/plugins/todo-list/parser.js:
--------------------------------------------------------------------------------
1 | import markdownItTodoList from '@/utils/markdown-it-todo-list';
2 |
3 | export default function (vMdParser, options) {
4 | vMdParser.extendMarkdown((mdParser) => {
5 | const color = options?.color || '#3eaf7c';
6 | const defaultBorderColor = '#d9d9d9';
7 | const border = (type) => `border-color: ${type === 'todo' ? defaultBorderColor : color}`;
8 | const background = `background-color: ${color}`;
9 |
10 | mdParser.use(markdownItTodoList, {
11 | renderCheckbox(type) {
12 | const checkboxClass = 'v-md-editor__todo-list-checkbox';
13 | const style = type === 'todo' ? `${border(type)}` : `${border(type)};${background}`;
14 |
15 | return ` `;
18 | },
19 | });
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/src/plugins/todo-list/todo-list.css:
--------------------------------------------------------------------------------
1 | .v-md-editor-preview li.v-md-editor__todo-list-item {
2 | margin-left: -20px;
3 | list-style: none;
4 | }
5 |
6 | .v-md-editor-preview .v-md-editor__todo-list-checkbox {
7 | position: relative;
8 | display: inline-block;
9 | width: 15px;
10 | height: 15px;
11 | margin-top: -3px;
12 | margin-right: 4px;
13 | vertical-align: middle;
14 | border-style: solid;
15 | border-width: 1px;
16 | border-radius: 2px;
17 | }
18 |
19 | .v-md-editor-preview .v-md-editor__todo-list-checkbox--checked::after {
20 | position: absolute;
21 | top: 2px;
22 | left: 5px;
23 | display: table;
24 | width: 3px;
25 | height: 7px;
26 | border: 2px solid #fff;
27 | border-top: 0;
28 | border-left: 0;
29 | -webkit-transform: rotate(45deg) scale(1);
30 | transform: rotate(45deg) scale(1);
31 | transition: all 0.2s ease-in-out;
32 | content: '';
33 | }
34 |
--------------------------------------------------------------------------------
/docs/plugins/todo-list.md:
--------------------------------------------------------------------------------
1 | # TodoList
2 |
3 |
4 |
5 |
6 |
7 | ### Import:
8 |
9 | ```js
10 | import VueMarkdownEditor from '@kangc/v-md-editor';
11 | import createTodoListPlugin from '@kangc/v-md-editor/lib/plugins/todo-list/index';
12 |
13 | VueMarkdownEditor.use(createTodoListPlugin());
14 | ```
15 |
16 | ### Usage:
17 |
18 | ```vue
19 |
20 |
21 |
22 |
23 |
32 | ```
33 |
34 | ### Options
35 |
36 | Parameters that can be passed in when creatingTodoListPlugin. For example: createTodoListPlugin({ color:'' })
37 |
38 | ### color
39 |
40 | - type: `String`。
41 | - default: `#3eaf7c`。
42 |
43 | checkbox color。
44 |
--------------------------------------------------------------------------------
/src/mixins/hotkeys.js:
--------------------------------------------------------------------------------
1 | import { importAll } from '@/utils/util';
2 |
3 | const defaultHotkeys = {};
4 | importAll(defaultHotkeys, require.context('@/hotkeys', false, /\.(js)$/));
5 |
6 | export default function (Component) {
7 | return {
8 | mounted() {
9 | if (this.isPreviewMode) return;
10 |
11 | const { hotkeys } = Component;
12 |
13 | Object.keys(defaultHotkeys).forEach((key) => {
14 | this.registerHotkeys(defaultHotkeys[key].default);
15 | });
16 |
17 | hotkeys.forEach((config) => {
18 | this.registerHotkeys(config);
19 | });
20 | },
21 | methods: {
22 | registerHotkeys({ modifier, key, action, preventDefault = true }) {
23 | this.editorRegisterHotkeys({
24 | modifier,
25 | key,
26 | preventDefault,
27 | action: (...arg) => action(this, ...arg),
28 | });
29 | },
30 | },
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/src/utils/markdown-it-heading-tag.js:
--------------------------------------------------------------------------------
1 | export default function (md, options = {}) {
2 | const { getMarks } = options;
3 |
4 | if (!getMarks) return;
5 |
6 | md.core.ruler.push('anchor', (state) => {
7 | const slugs = {};
8 | const { tokens } = state;
9 |
10 | tokens
11 | .filter((token) => token.type === 'heading_open')
12 | .forEach((token) => {
13 | // Aggregate the next token children text.
14 | const heading = tokens[tokens.indexOf(token) + 1];
15 | const title = heading.content;
16 | const level = Number(token.tag.substr(1));
17 |
18 | slugs[title] = title in slugs ? Number(slugs[title]) + 1 : '';
19 | const marks = getMarks(title, level, slugs[title]);
20 |
21 | if (marks) {
22 | marks.forEach(({ attr, value }) => {
23 | token.attrPush([attr, value]);
24 | });
25 | }
26 | });
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/src/theme/base/highlight.js:
--------------------------------------------------------------------------------
1 | import createBaseTheme from './base';
2 | import { createHighlightRender } from '@/utils/markdown-it';
3 |
4 | export default function createHljsTheme({
5 | Hljs,
6 | baseConfig,
7 | codeBlockClass,
8 | codeHighlightExtensionMap = {},
9 | } = {}) {
10 | const baseTheme = createBaseTheme(baseConfig);
11 |
12 | baseTheme.extend((md) => {
13 | md.set({
14 | highlight: createHighlightRender({
15 | codeHighlightExtensionMap,
16 | hasLang: (lang) => Hljs.getLanguage(lang),
17 | codeBlockClass,
18 | highlight: (str, lang) => Hljs.highlight(str, { language: lang }).value,
19 | }),
20 | });
21 | });
22 |
23 | return {
24 | previewClass: 'markdown-body',
25 | extend(callback) {
26 | baseTheme.extend((...arg) => {
27 | callback(...arg);
28 | });
29 | },
30 | markdownParser: baseTheme.markdownParser,
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/src/theme/base/prism.js:
--------------------------------------------------------------------------------
1 | import createBaseTheme from './base';
2 | import { createHighlightRender } from '@/utils/markdown-it';
3 |
4 | export default function createPrismTheme({
5 | Prism,
6 | baseConfig,
7 | codeBlockClass,
8 | codeHighlightExtensionMap = {},
9 | } = {}) {
10 | const baseTheme = createBaseTheme(baseConfig);
11 |
12 | baseTheme.extend((md) => {
13 | md.set({
14 | highlight: createHighlightRender({
15 | codeHighlightExtensionMap,
16 | hasLang: (lang) => Prism.languages[lang],
17 | codeBlockClass,
18 | highlight: (str, lang) => Prism.highlight(str, Prism.languages[lang], lang),
19 | }),
20 | });
21 | });
22 |
23 | return {
24 | previewClass: 'markdown-body',
25 | extend(callback) {
26 | baseTheme.extend((...arg) => {
27 | callback(...arg, Prism);
28 | });
29 | },
30 | markdownParser: baseTheme.markdownParser,
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/src/command/image.js:
--------------------------------------------------------------------------------
1 | export { image as name } from '@/utils/constants/command.js';
2 |
3 | export default function (editor, { url, desc, width, height } = {}) {
4 | editor.insert(() => {
5 | const urlPlaceholder = 'http://';
6 | const descPlaceholder = 'Description';
7 | let selected = urlPlaceholder;
8 | let text = ``;
9 | const style = [];
10 |
11 | if (width) {
12 | style.push(`width="${width}"`);
13 | }
14 |
15 | if (height) {
16 | style.push(`height="${height}"`);
17 | }
18 |
19 | if (style.length) {
20 | text += `{{{${style.join(' ')}}}}`;
21 | }
22 |
23 | if (url && desc) {
24 | selected = null;
25 | } else if (url) {
26 | selected = descPlaceholder;
27 | } else if (desc) {
28 | selected = urlPlaceholder;
29 | }
30 |
31 | return {
32 | text,
33 | selected,
34 | };
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/upload-file.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
46 |
--------------------------------------------------------------------------------
/src/components/scrollbar/util.js:
--------------------------------------------------------------------------------
1 | // Modified from https://github.com/ElemeFE/element/tree/dev/packages/scrollbar
2 |
3 | export const BAR_MAP = {
4 | vertical: {
5 | offset: 'offsetHeight',
6 | scroll: 'scrollTop',
7 | scrollSize: 'scrollHeight',
8 | size: 'height',
9 | key: 'vertical',
10 | axis: 'Y',
11 | client: 'clientY',
12 | direction: 'top',
13 | },
14 | horizontal: {
15 | offset: 'offsetWidth',
16 | scroll: 'scrollLeft',
17 | scrollSize: 'scrollWidth',
18 | size: 'width',
19 | key: 'horizontal',
20 | axis: 'X',
21 | client: 'clientX',
22 | direction: 'left',
23 | },
24 | };
25 |
26 | export function renderThumbStyle({ move, size, bar }) {
27 | const style = {};
28 | const translate = `translate${bar.axis}(${move}%)`;
29 |
30 | style[bar.size] = size;
31 | style.transform = translate;
32 | style.msTransform = translate;
33 | style.webkitTransform = translate;
34 |
35 | return style;
36 | }
37 |
--------------------------------------------------------------------------------
/dev/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 | vue-markdown-editor
11 |
12 |
17 |
18 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/plugin-highlight-lines-github.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/utils/command.js:
--------------------------------------------------------------------------------
1 | import { importAll } from '@/utils/util';
2 |
3 | const defaultCommands = {};
4 | importAll(defaultCommands, require.context('@/command', false, /\.(js)$/));
5 |
6 | export default function registerCommand(target, commandName, callback) {
7 | if (commandName) {
8 | if (!target[commandName]) {
9 | target[commandName] = callback;
10 | } else {
11 | console.error(`The command name is already in use: ${commandName}`);
12 | }
13 | } else {
14 | console.error('Command name is required');
15 | }
16 | }
17 |
18 | export function commandWrapper(component) {
19 | component.commands = {};
20 |
21 | component.command = function (commandName, callback) {
22 | registerCommand(component.commands, commandName, callback);
23 | };
24 |
25 | Object.keys(defaultCommands).forEach((key) => {
26 | const module = defaultCommands[key];
27 | const { name, default: callback } = module;
28 |
29 | component.command(name, callback);
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | if (api) {
3 | api.cache.never();
4 | }
5 |
6 | const { BABEL_MODULE, USE_BABEL_RESOLVE } = process.env;
7 | const useESModules = BABEL_MODULE !== 'commonjs';
8 |
9 | const config = {
10 | presets: [
11 | [
12 | '@babel/preset-env',
13 | {
14 | loose: true,
15 | modules: useESModules ? false : 'commonjs',
16 | },
17 | ],
18 | ],
19 | plugins: [
20 | '@vue/babel-plugin-jsx',
21 | [
22 | '@babel/plugin-transform-runtime',
23 | {
24 | corejs: false,
25 | useESModules,
26 | },
27 | ],
28 | '@babel/plugin-transform-object-assign',
29 | ],
30 | };
31 |
32 | if (USE_BABEL_RESOLVE) {
33 | config.plugins.push([
34 | 'module-resolver',
35 | {
36 | alias: {
37 | '@': useESModules ? './es' : './lib',
38 | },
39 | },
40 | ]);
41 | }
42 |
43 | return config;
44 | };
45 |
--------------------------------------------------------------------------------
/docs/zh/senior/upload-image.md:
--------------------------------------------------------------------------------
1 | # 上传本地图片
2 |
3 | 原理:上传图片至文件服务器,上传成功后将返回的图片相关信息插入编辑区域。
4 |
5 |
6 |
7 |
8 |
9 | ::: warning 注意
10 | 上传图片菜单默认为禁用状态 设置 disabled-menus 为空数组可以开启。
11 | :::
12 |
13 | 示例代码:
14 |
15 | ```vue
16 |
17 |
24 |
25 |
26 |
48 | ```
49 |
--------------------------------------------------------------------------------
/src/utils/scrollbar-width.js:
--------------------------------------------------------------------------------
1 | // Modified from https://github.com/ElemeFE/element/blob/dev/src/utils/scrollbar-width.js
2 |
3 | let scrollBarWidth;
4 | const isServer = typeof window === 'undefined';
5 |
6 | export default function () {
7 | if (isServer) return 0;
8 | if (scrollBarWidth !== undefined) return scrollBarWidth;
9 |
10 | const outer = document.createElement('div');
11 | outer.className = 'scrollbar__wrap';
12 | outer.style.visibility = 'hidden';
13 | outer.style.width = '100px';
14 | outer.style.position = 'absolute';
15 | outer.style.top = '-9999px';
16 | document.body.appendChild(outer);
17 |
18 | const widthNoScroll = outer.offsetWidth;
19 | outer.style.overflow = 'scroll';
20 |
21 | const inner = document.createElement('div');
22 | inner.style.width = '100%';
23 | outer.appendChild(inner);
24 |
25 | const widthWithScroll = inner.offsetWidth;
26 | outer.parentNode.removeChild(outer);
27 | scrollBarWidth = widthNoScroll - widthWithScroll;
28 |
29 | return scrollBarWidth;
30 | }
31 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/upload-image.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/utils/markdown-it.js:
--------------------------------------------------------------------------------
1 | import MarkdownIt from 'markdown-it';
2 | import { escapeHtml } from 'markdown-it/lib/common/utils';
3 |
4 | export function createHighlightRender({
5 | codeHighlightExtensionMap = {},
6 | hasLang = () => true,
7 | highlight = (str) => str,
8 | codeBlockClass,
9 | }) {
10 | const getCodeBlockClass = (lang) => (codeBlockClass ? codeBlockClass(lang) : `language-${lang}`);
11 |
12 | return function (str, lang) {
13 | let res = escapeHtml(str);
14 |
15 | lang = codeHighlightExtensionMap[lang] || lang;
16 |
17 | if (lang) {
18 | if (hasLang(lang)) {
19 | res = highlight(str, lang);
20 | }
21 | }
22 |
23 | return `${res} `;
24 | };
25 | }
26 |
27 | export default function () {
28 | const markdownItInstance = new MarkdownIt();
29 |
30 | markdownItInstance.set({
31 | html: true,
32 | breaks: true,
33 | linkify: false,
34 | typographer: true,
35 | });
36 |
37 | return markdownItInstance;
38 | }
39 |
--------------------------------------------------------------------------------
/src/plugins/emoji/creator.js:
--------------------------------------------------------------------------------
1 | import createToolbar from './toolbar';
2 | import commandHandler from './command';
3 |
4 | export default function creator({ emojiJson, parser }) {
5 | return function createEmojiPlugin({
6 | name = 'emoji',
7 | icon = 'v-md-icon-emoji',
8 | text,
9 | title = (editor) => editor.langConfig.emoji,
10 | customEmoji,
11 | } = {}) {
12 | const toolbar = createToolbar({ commandName: name, title, text, icon, emojiJson });
13 |
14 | return {
15 | install(VMdEditor) {
16 | if (VMdEditor.name === 'v-md-editor') {
17 | VMdEditor.command(name, commandHandler);
18 | VMdEditor.toolbar(name, toolbar);
19 | VMdEditor.lang.add({
20 | 'zh-CN': {
21 | emoji: '插入emoji表情',
22 | },
23 | 'en-US': {
24 | emoji: 'Insert emoji',
25 | },
26 | });
27 | }
28 |
29 | VMdEditor.vMdParser.use(parser, {
30 | customEmoji,
31 | });
32 | },
33 | };
34 | };
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 code-farmer-i
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/dev/test-cdn.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 | vue-markdown-editor
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/docs/zh/theme/github.md:
--------------------------------------------------------------------------------
1 | # github 主题
2 |
3 | ## 介绍
4 |
5 | markdown 解析使用 [markdown-it](https://github.com/markdown-it/markdown-it) 来实现,代码块解析使用 [highlight.js](https://github.com/highlightjs/highlight.js) 来实现。
6 |
7 | 效果如下:
8 |
9 |
10 |
11 |
12 |
13 | ## 扩展
14 |
15 | 主题包默认只支持了 js(javascript), xml(html), css。以免引入太多冗余代码导致包的体积过大。如果需要支持更多的语言代码高亮,请按需引入对应的语言包。
16 |
17 | ```js
18 | import VueMarkdownEditor from '@kangc/v-md-editor';
19 | // 按需引入 highlightjs 的语言包,此处以 json 为例
20 | import json from 'highlight.js/lib/languages/json';
21 | import githubTheme from '@kangc/v-md-editor/lib/theme/github.js';
22 |
23 | VueMarkdownEditor.use(githubTheme, {
24 | extend(md, hljs) {
25 | // md为 markdown-it 实例,可以在此处进行修改配置,并使用 plugin 进行语法扩展
26 | // md.set(option).use(plugin);
27 | // 注册语言包
28 | hljs.registerLanguage('json', json);
29 | },
30 | });
31 | ```
32 |
33 | [查看 highlight.js 支持的语言包](https://github.com/highlightjs/highlight.js/tree/master/src/languages)
34 |
35 | 扩展后就能支持对应的代码块高亮了,如下
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/docs/Internationalization.md:
--------------------------------------------------------------------------------
1 | # internationalization
2 |
3 | ::: warning
4 | Version 1.4.0 began to support internationalization
5 | :::
6 |
7 | ## Introduction
8 |
9 | v-md-editor uses Chinese as the language by default. If you need to use other languages, you can refer to the following scheme.
10 |
11 | ## Switch languages
12 |
13 | You can switch the currently used language through the use method.
14 |
15 | ```js
16 | import enUS from '@kangc/v-md-editor/lib/lang/en-US';
17 |
18 | VueMarkdownEditor.lang.use('en-US', enUS);
19 | ```
20 |
21 | ## Modify default configs
22 |
23 | The copy method can be used to modify and expand the copy.
24 |
25 | ```js
26 | VueMarkdownEditor.lang.add({
27 | 'zh-CN': {
28 | h1: {
29 | toolbar: 'title 1',
30 | },
31 | },
32 | });
33 | ```
34 |
35 | ## Config files
36 |
37 | | Language | Filename |
38 | | -------- | -------- |
39 | | Chinese | zh-CN |
40 | | English | en-US |
41 | | Korean | ko-KR |
42 | | Japanese | ja-JP |
43 |
44 |
45 | View all language configs [Here](https://github.com/code-farmer-i/vue-markdown-editor/tree/dev/src/lang).
46 |
--------------------------------------------------------------------------------
/docs/zh/plugins/copy-code.md:
--------------------------------------------------------------------------------
1 | # Copy Code 快捷复制代码
2 |
3 | 通过按钮快速复制代码块
4 |
5 |
6 |
7 |
8 |
9 | ### Import:
10 |
11 | ```js
12 | import VueMarkdownEditor from '@kangc/v-md-editor';
13 | import createCopyCodePlugin from '@kangc/v-md-editor/lib/plugins/copy-code/index';
14 |
15 | VueMarkdownEditor.use(createCopyCodePlugin());
16 | ```
17 |
18 | ### Usage:
19 |
20 | ```vue
21 |
22 |
23 |
24 |
25 |
49 | ```
50 |
--------------------------------------------------------------------------------
/src/toolbar/image.js:
--------------------------------------------------------------------------------
1 | import { image } from '@/utils/constants/command';
2 | import { filesFilter } from '@/utils/file';
3 |
4 | export default {
5 | name: image,
6 | icon: 'v-md-icon-img',
7 | title: (editor) => editor.langConfig.image.toolbar,
8 | menus: [
9 | {
10 | name: 'image-link',
11 | text: (editor) => editor.langConfig.imageLink.toolbar,
12 | action(editor, config) {
13 | if (config?.insertWithSize) {
14 | editor.execCommand(image, { width: 'auto', height: 'auto' });
15 | } else {
16 | editor.execCommand(image);
17 | }
18 | },
19 | },
20 | {
21 | name: 'upload-image',
22 | text: (editor) => editor.langConfig.uploadImage.toolbar,
23 | action(editor) {
24 | editor.uploadConfig = editor.uploadImgConfig;
25 | editor.$nextTick(async () => {
26 | const event = await editor.$refs.uploadFile.upload();
27 | const files = filesFilter(event.target.files, editor.uploadImgConfig);
28 |
29 | editor.emitUploadImage(event, files);
30 | });
31 | },
32 | },
33 | ],
34 | };
35 |
--------------------------------------------------------------------------------
/docs/plugins/copy-code.md:
--------------------------------------------------------------------------------
1 | # Copy Code
2 |
3 | Quickly copy code blocks with buttons
4 |
5 |
6 |
7 |
8 |
9 | ### Import:
10 |
11 | ```js
12 | import VueMarkdownEditor from '@kangc/v-md-editor';
13 | import createCopyCodePlugin from '@kangc/v-md-editor/lib/plugins/copy-code/index';
14 |
15 | VueMarkdownEditor.use(createCopyCodePlugin());
16 | ```
17 |
18 | ### Usage:
19 |
20 | ```vue
21 |
22 |
23 |
24 |
25 |
49 | ```
50 |
--------------------------------------------------------------------------------
/src/styles/var.scss:
--------------------------------------------------------------------------------
1 | // color
2 | $text-color: #2c3e50;
3 | $text-color-placeholder: #c0c4cc;
4 |
5 | // box-shadow
6 | $box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
7 |
8 | // editor
9 | $editor-font-family: menlo, 'Ubuntu Mono', consolas, 'Courier New', 'Microsoft Yahei',
10 | 'Hiragino Sans GB', 'WenQuanYi Micro Hei', sans-serif;
11 | $editor-line-height: 1.5;
12 | $editor-font-size: 14px;
13 |
14 | // border-color
15 | $border-color: #ddd;
16 |
17 | // toolbar
18 | $toolbar-font-size: 16px;
19 | $toolbar-text-color: #595959;
20 | $toolbar-hover-background: #f5f5f5;
21 | $toolbar-active-background: #e8e8e8;
22 |
23 | // menu
24 | $menu-font-size: 14px;
25 |
26 | // tooltip
27 | $tooltip-color: #fff;
28 | $tooltip-background: #878787;
29 | $tooltip-border-radius: 2px;
30 | $tooltip-z-index: 101;
31 | $tooltip-padding: 4px 6px;
32 | $tooltip-font-size: 12px;
33 |
34 | // scrollbar
35 | $scrollbar-width: 6px;
36 | $scrollbar-border-radius: 4px;
37 | $scrollbar-background-color: rgba(144, 147, 153, 0.3);
38 | $scrollbar-active-background-color: rgba(144, 147, 153, 0.5);
39 | $scrollbar-opacity-transition: 0.34s ease-out;
40 | $scrollbar-background-transition: 0.3s;
41 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/plugin-copy-code.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/mixins/fullscreen.js:
--------------------------------------------------------------------------------
1 | export default {
2 | props: {
3 | defaultFullscreen: Boolean,
4 | },
5 | emits: ['fullscreen-change'],
6 | data() {
7 | return {
8 | fullscreen: false,
9 | };
10 | },
11 | watch: {
12 | fullscreen() {
13 | this.$emit('fullscreen-change', this.fullscreen);
14 | },
15 | },
16 | mounted() {
17 | window.addEventListener('keyup', this.handleWindowKeyup, false);
18 |
19 | if (this.defaultFullscreen) {
20 | this.toggleFullScreen();
21 | }
22 | },
23 | beforeUnmount() {
24 | window.removeEventListener('keyup', this.handleWindowKeyup, false);
25 | },
26 | methods: {
27 | handleWindowKeyup(e) {
28 | // esc
29 | if (e.keyCode === 27 && this.fullscreen) {
30 | this.toggleFullScreen(false);
31 | }
32 | },
33 | toggleFullScreen(fullscreen = !this.fullscreen) {
34 | this.fullscreen = fullscreen;
35 | const { 0: html, 1: body } = document.querySelectorAll('html, body');
36 | const overflow = this.fullscreen ? 'hidden' : null;
37 |
38 | body.style.overflow = overflow;
39 | html.style.overflow = overflow;
40 | },
41 | },
42 | };
43 |
--------------------------------------------------------------------------------
/src/utils/key-codes.js:
--------------------------------------------------------------------------------
1 | export const keyCodesToName = {
2 | // Numbers and letters
3 | 48: 0,
4 | 49: 1,
5 | 50: 2,
6 | 51: 3,
7 | 52: 4,
8 | 53: 5,
9 | 54: 6,
10 | 55: 7,
11 | 56: 8,
12 | 57: 9,
13 |
14 | 65: 'a',
15 | 66: 'b',
16 | 67: 'c',
17 | 68: 'd',
18 | 69: 'e',
19 | 70: 'f',
20 | 71: 'g',
21 | 72: 'h',
22 | 73: 'i',
23 | 74: 'j',
24 | 75: 'k',
25 | 76: 'l',
26 | 77: 'm',
27 | 78: 'n',
28 | 79: 'o',
29 | 80: 'p',
30 | 81: 'q',
31 | 82: 'r',
32 | 83: 's',
33 | 84: 't',
34 | 85: 'u',
35 | 86: 'v',
36 | 87: 'w',
37 | 88: 'x',
38 | 89: 'y',
39 | 90: 'z',
40 |
41 | // Function keys
42 | 112: 'F1',
43 | 113: 'F2',
44 | 114: 'F3',
45 | 115: 'F4',
46 | 116: 'F5',
47 | 117: 'F6',
48 | 118: 'F7',
49 | 119: 'F8',
50 | 120: 'F9',
51 | 121: 'F10',
52 | 122: 'F11',
53 | 123: 'F12',
54 | };
55 |
56 | export const keyNames = {
57 | esc: ['Esc', 'Escape'],
58 | tab: 'Tab',
59 | enter: 'Enter',
60 | space: [' ', 'Spacebar'],
61 | up: ['Up', 'ArrowUp'],
62 | left: ['Left', 'ArrowLeft'],
63 | right: ['Right', 'ArrowRight'],
64 | down: ['Down', 'ArrowDown'],
65 | delete: ['Backspace', 'Delete', 'Del'],
66 | };
67 |
--------------------------------------------------------------------------------
/dev/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
19 |
20 |
21 |
22 |
54 |
--------------------------------------------------------------------------------
/src/utils/v-md-parser.js:
--------------------------------------------------------------------------------
1 | import Lang from '@/utils/lang';
2 |
3 | export class VMdParser {
4 | constructor() {
5 | this.lang = new Lang();
6 | }
7 |
8 | defaultMarkdownLoader(text) {
9 | return text;
10 | }
11 |
12 | use(optionsOrInstall, opt) {
13 | if (typeof optionsOrInstall === 'function') {
14 | optionsOrInstall(this, opt);
15 | } else {
16 | optionsOrInstall.install(this, opt);
17 | }
18 |
19 | return this;
20 | }
21 |
22 | theme(themeConfig) {
23 | this.themeConfig = themeConfig;
24 | }
25 |
26 | extendMarkdown(extender) {
27 | if (!this.themeConfig) {
28 | return console.error('Please use theme before using plugins');
29 | }
30 |
31 | const { markdownParser } = this.themeConfig;
32 |
33 | extender(markdownParser);
34 | }
35 |
36 | parse(text) {
37 | const { markdownParser } = this.themeConfig;
38 | const markdownLoader =
39 | markdownParser?.render?.bind(markdownParser) || this.defaultMarkdownLoader;
40 |
41 | if (typeof markdownLoader !== 'function' || markdownLoader === this.defaultMarkdownLoader) {
42 | console.error('Please configure your markdown parser');
43 | }
44 |
45 | return markdownLoader(text);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/utils/markdown-it-copy-code.js:
--------------------------------------------------------------------------------
1 | // markdown-it plugin for generating copy code button.
2 | // It depends on preWrapper plugin.
3 |
4 | /* eslint-disable max-len */
5 | export default function (md) {
6 | const { fence } = md.renderer.rules;
7 | md.renderer.rules.fence = (...args) => {
8 | const rawCode = fence(...args);
9 | const button = `
10 |
11 |
12 |
13 |
14 |
15 |
16 | `;
17 |
18 | const finalCode = rawCode
19 | .replace('', `${button}`)
20 | .replace('v-md-pre-wrapper', 'v-md-pre-wrapper copy-code-mode');
21 |
22 | return finalCode;
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/src/utils/smooth-scroll.js:
--------------------------------------------------------------------------------
1 | import { getScrollTop, scrollTo } from './scroll-top';
2 |
3 | export function smooth({ currentScrollTop, scrollToTop, scrollFn, percent = 10, onScrollEnd }) {
4 | const scrollWay = scrollToTop > currentScrollTop ? 'down' : 'up';
5 | const step = (scrollToTop - currentScrollTop) * (percent / 100);
6 | let id;
7 |
8 | const scroll = () => {
9 | currentScrollTop += step;
10 |
11 | if (
12 | (scrollWay === 'down' && currentScrollTop >= scrollToTop) ||
13 | (scrollWay === 'up' && currentScrollTop <= scrollToTop)
14 | ) {
15 | scrollFn(scrollToTop);
16 |
17 | window.cancelAnimationFrame(id);
18 | if (onScrollEnd) window.requestAnimationFrame(onScrollEnd);
19 | } else {
20 | scrollFn(currentScrollTop);
21 | window.requestAnimationFrame(scroll);
22 | }
23 | };
24 |
25 | window.requestAnimationFrame(scroll);
26 | }
27 |
28 | export default function smoothScroll({ scrollTarget, scrollToTop, percent = 10, onScrollEnd }) {
29 | const currentScrollTop = getScrollTop(scrollTarget);
30 |
31 | smooth({
32 | currentScrollTop,
33 | scrollToTop,
34 | scrollFn: (scrollTop) => scrollTo(scrollTarget, scrollTop),
35 | percent,
36 | onScrollEnd,
37 | });
38 | }
39 |
--------------------------------------------------------------------------------
/docs/senior/upload-image.md:
--------------------------------------------------------------------------------
1 | # Upload Image
2 |
3 | upload the picture to the file server, and insert the relevant information of the returned picture into the editing area after the upload is successful.
4 |
5 |
6 |
7 |
8 |
9 | ::: warning
10 | The upload image menu is disabled by default. Set disabled-menus to an empty array to enable it.
11 | :::
12 |
13 | example:
14 |
15 | ```vue
16 |
17 |
24 |
25 |
26 |
48 | ```
49 |
--------------------------------------------------------------------------------
/src/plugins/copy-code/copy-code.css:
--------------------------------------------------------------------------------
1 | .v-md-pre-wrapper.copy-code-mode .v-md-copy-code-btn {
2 | position: absolute;
3 | top: 0.4em;
4 | right: 0.4em;
5 | z-index: 1;
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | width: 32px;
10 | height: 24px;
11 | padding: 0;
12 | color: #ddd;
13 | font-size: 14px;
14 | background-color: #666;
15 | border: none;
16 | border-radius: 6px;
17 | outline: none;
18 | box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.2);
19 | visibility: hidden;
20 | cursor: pointer;
21 | opacity: 0;
22 | transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
23 | user-select: none;
24 | }
25 |
26 | .v-md-pre-wrapper.copy-code-mode .v-md-copy-code-btn i {
27 | display: inline-block;
28 | color: inherit;
29 | font-style: normal;
30 | line-height: 0;
31 | text-align: center;
32 | text-transform: none;
33 | vertical-align: -0.125em;
34 | text-rendering: optimizeLegibility;
35 | pointer-events: none;
36 | }
37 |
38 | .v-md-pre-wrapper.copy-code-mode::before {
39 | transition: 0.3s;
40 | }
41 |
42 | .v-md-pre-wrapper.copy-code-mode:hover .v-md-copy-code-btn {
43 | visibility: visible;
44 | opacity: 1;
45 | }
46 |
47 | .v-md-pre-wrapper.copy-code-mode:hover::before {
48 | display: none;
49 | }
50 |
--------------------------------------------------------------------------------
/src/toolbar/h.js:
--------------------------------------------------------------------------------
1 | import { h1, h2, h3, h4, h5, h6 } from '@/utils/constants/command';
2 |
3 | export default {
4 | name: 'h',
5 | text: 'H',
6 | title: (editor) => `${editor.langConfig.h.toolbar}(Ctrl+1~6)`,
7 | menus: [
8 | {
9 | name: h1,
10 | text: (editor) => editor.langConfig.h1.toolbar,
11 | action(editor) {
12 | editor.execCommand(h1);
13 | },
14 | },
15 | {
16 | name: h2,
17 | text: (editor) => editor.langConfig.h2.toolbar,
18 | action(editor) {
19 | editor.execCommand(h2);
20 | },
21 | },
22 | {
23 | name: h3,
24 | text: (editor) => editor.langConfig.h3.toolbar,
25 | action(editor) {
26 | editor.execCommand(h3);
27 | },
28 | },
29 | {
30 | name: h4,
31 | text: (editor) => editor.langConfig.h4.toolbar,
32 | action(editor) {
33 | editor.execCommand(h4);
34 | },
35 | },
36 | {
37 | name: h5,
38 | text: (editor) => editor.langConfig.h5.toolbar,
39 | action(editor) {
40 | editor.execCommand(h5);
41 | },
42 | },
43 | {
44 | name: h6,
45 | text: (editor) => editor.langConfig.h6.toolbar,
46 | action(editor) {
47 | editor.execCommand(h6);
48 | },
49 | },
50 | ],
51 | };
52 |
--------------------------------------------------------------------------------
/src/utils/resize-event.js:
--------------------------------------------------------------------------------
1 | // Modified from https://github.com/ElemeFE/element/blob/dev/src/utils/resize-event.js
2 |
3 | /* eslint-disable no-underscore-dangle */
4 | import ResizeObserver from 'resize-observer-polyfill';
5 |
6 | const isServer = typeof window === 'undefined';
7 |
8 | /* istanbul ignore next */
9 | const resizeHandler = function (entries) {
10 | entries.forEach((entry) => {
11 | const listeners = entry.target.__resizeListeners__ || [];
12 | if (listeners.length) {
13 | listeners.forEach((fn) => {
14 | fn();
15 | });
16 | }
17 | });
18 | };
19 |
20 | /* istanbul ignore next */
21 | export const addResizeListener = function (element, fn) {
22 | if (isServer) return;
23 | if (!element.__resizeListeners__) {
24 | element.__resizeListeners__ = [];
25 | element.__ro__ = new ResizeObserver(resizeHandler);
26 | element.__ro__.observe(element);
27 | }
28 | element.__resizeListeners__.push(fn);
29 | };
30 |
31 | /* istanbul ignore next */
32 | export const removeResizeListener = function (element, fn) {
33 | if (!element || !element.__resizeListeners__) return;
34 | element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1);
35 | if (!element.__resizeListeners__.length) {
36 | element.__ro__.disconnect();
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/docs/theme/github.md:
--------------------------------------------------------------------------------
1 | # Github
2 |
3 | ## Introduction
4 |
5 | example:
6 |
7 |
8 |
9 |
10 |
11 | ## Expand
12 |
13 | The theme package only supports js(javascript), xml(html), css by default. In order to avoid introducing too much redundant code, the package size is too large. If you need to support more language code highlighting, please introduce the corresponding language pack as needed.
14 |
15 | ```js
16 | import VueMarkdownEditor from '@kangc/v-md-editor';
17 | // Introduce highlightjs language packs as needed, here is json as an example
18 | import json from 'highlight.js/lib/languages/json';
19 | import githubTheme from '@kangc/v-md-editor/lib/theme/github.js';
20 |
21 | VueMarkdownEditor.use(githubTheme, {
22 | extend(md, hljs) {
23 | // md is a markdown-it instance, you can modify the configuration here, and use plugin for syntax expansion
24 | // md.set(option).use(plugin);
25 | // Register Language Pack
26 | hljs.registerLanguage('json', json);
27 | },
28 | });
29 | ```
30 |
31 | [Check out the language packs supported by highlight.js](https://github.com/highlightjs/highlight.js/tree/master/src/languages)
32 |
33 | After expansion, the corresponding code block can be highlighted.
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/components/toc-nav.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 | {{ item.title }}
12 |
13 |
14 |
15 |
16 |
29 |
30 |
63 |
--------------------------------------------------------------------------------
/src/plugins/todo-list/index.js:
--------------------------------------------------------------------------------
1 | import createToolbar from './toolbar';
2 | import commandHandler from './command';
3 | import parser from './parser';
4 |
5 | export default function createTodoListPlugin({
6 | name = 'todo-list',
7 | icon = 'v-md-icon-checkbox',
8 | text,
9 | color,
10 | } = {}) {
11 | const toolbar = createToolbar({
12 | commandName: name,
13 | title: (editor) => `${editor.langConfig.task.toolbar}(Ctrl+Shift+U)`,
14 | text,
15 | icon,
16 | });
17 |
18 | return {
19 | install(VMdEditor) {
20 | if (VMdEditor.name === 'v-md-editor') {
21 | VMdEditor.command(name, commandHandler);
22 | VMdEditor.toolbar(name, toolbar);
23 | VMdEditor.hotkey({
24 | modifier: 'ctrlShift',
25 | key: 'u',
26 | action(editor) {
27 | editor.execCommand(name);
28 | },
29 | });
30 | VMdEditor.lang.add({
31 | 'zh-CN': {
32 | task: {
33 | toolbar: '任务列表',
34 | placeholder: '任务列表',
35 | },
36 | },
37 | 'en-US': {
38 | task: {
39 | toolbar: 'Task',
40 | placeholder: 'Task',
41 | },
42 | },
43 | });
44 | }
45 |
46 | VMdEditor.vMdParser.use(parser, {
47 | color,
48 | });
49 | },
50 | };
51 | }
52 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/vuepress-theme-tip.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/mixins/scroll.js:
--------------------------------------------------------------------------------
1 | export default {
2 | props: {
3 | previewScrollContainer: Function,
4 | },
5 | computed: {
6 | getPreviewScrollContainer() {
7 | return () => {
8 | const previewScrollContainer = this.$refs.previewScroller.$el.querySelector(
9 | '.scrollbar__wrap'
10 | );
11 | const defaultContainer = this.isPreviewMode ? window : previewScrollContainer;
12 |
13 | return this.previewScrollContainer ? this.previewScrollContainer() : defaultContainer;
14 | };
15 | },
16 | },
17 | methods: {
18 | previewScrollTo(scrollTop) {
19 | this.$refs.previewScroller.scrollTo(scrollTop);
20 | },
21 | scrollToLine(lineIndex) {
22 | if (!this.isPreviewMode) {
23 | this.editorScrollToLine(lineIndex);
24 | }
25 |
26 | if (!this.isEditMode) {
27 | this.ignoreSyncScroll = true;
28 | this.previewScrollToLine({
29 | lineIndex,
30 | onScrollEnd: () => {
31 | this.ignoreSyncScroll = false;
32 | },
33 | });
34 | }
35 | },
36 | editorScrollToLine(lineIndex) {
37 | const offsetTop = this.heightAtLine(lineIndex - 1, 'local');
38 |
39 | this.editorScrollToTop(offsetTop);
40 | },
41 | previewScrollToTarget(...arg) {
42 | this.$refs.preview.scrollToTarget(...arg);
43 | },
44 | previewScrollToLine({ lineIndex, onScrollEnd }) {
45 | this.$refs.preview.scrollToLine({ lineIndex, onScrollEnd });
46 | },
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/src/plugins/tip/tip.css:
--------------------------------------------------------------------------------
1 | .v-md-plugin-tip p {
2 | margin-top: 1em;
3 | margin-bottom: 1em;
4 | }
5 | .v-md-plugin-tip .v-md-plugin-tip-title {
6 | margin-bottom: -0.4rem;
7 | font-weight: 600;
8 | }
9 | .v-md-plugin-tip.danger,
10 | .v-md-plugin-tip.tip,
11 | .v-md-plugin-tip.warning {
12 | margin: 1rem 0;
13 | padding: 0.1rem 1.5rem;
14 | border-left-width: 0.5rem;
15 | border-left-style: solid;
16 | }
17 | .v-md-plugin-tip.tip {
18 | background-color: #f3f5f7;
19 | border-color: #42b983;
20 | }
21 | .v-md-plugin-tip.warning {
22 | color: #6b5900;
23 | background-color: rgba(255, 229, 100, 0.3);
24 | border-color: #e7c000;
25 | }
26 | .v-md-plugin-tip.warning .v-md-plugin-tip-title {
27 | color: #b29400;
28 | }
29 | .v-md-plugin-tip.warning a {
30 | color: #2c3e50;
31 | }
32 | .v-md-plugin-tip.danger {
33 | color: #4d0000;
34 | background-color: #ffe6e6;
35 | border-color: #c00;
36 | }
37 | .v-md-plugin-tip.danger .v-md-plugin-tip-title {
38 | color: #900;
39 | }
40 | .v-md-plugin-tip.danger a {
41 | color: #2c3e50;
42 | }
43 | .v-md-plugin-tip.details {
44 | position: relative;
45 | display: block;
46 | margin: 1.6em 0;
47 | padding: 1.6em;
48 | background-color: #eee;
49 | border-radius: 2px;
50 | }
51 | .v-md-plugin-tip.details h4 {
52 | margin-top: 0;
53 | }
54 | .v-md-plugin-tip.details figure:last-child,
55 | .v-md-plugin-tip.details p:last-child {
56 | margin-bottom: 0;
57 | padding-bottom: 0;
58 | }
59 | .v-md-plugin-tip.details summary {
60 | outline: none;
61 | cursor: pointer;
62 | }
63 |
--------------------------------------------------------------------------------
/src/utils/file.js:
--------------------------------------------------------------------------------
1 | // Modified from https://github.com/ElemeFE/element/blob/dev/packages/upload/src/upload-dragger.vue
2 | export function filesFilter(files, config) {
3 | const { accept } = config;
4 |
5 | const filesKeys = Object.keys(files).filter((key) => {
6 | const file = files[key];
7 | const { type, name } = file;
8 | const extension = name.indexOf('.') > -1 ? `.${name.split('.').pop()}` : '';
9 | const baseType = type.replace(/\/.*$/, '');
10 |
11 | return accept
12 | .split(',')
13 | .map((type) => type.trim())
14 | .filter((type) => type)
15 | .some((acceptedType) => {
16 | if (/\..+$/.test(acceptedType)) {
17 | return extension === acceptedType;
18 | }
19 |
20 | if (/\/\*$/.test(acceptedType)) {
21 | return baseType === acceptedType.replace(/\/\*$/, '');
22 | }
23 |
24 | // eslint-disable-next-line no-useless-escape
25 | if (/^[^\/]+\/[^\/]+$/.test(acceptedType)) {
26 | return type === acceptedType;
27 | }
28 |
29 | return false;
30 | });
31 | });
32 |
33 | return filesKeys.map((key) => files[key]);
34 | }
35 |
36 | export function getFilesFromClipboardData(clipboardData) {
37 | const files = [];
38 |
39 | Object.keys(clipboardData.items).forEach((key) => {
40 | const item = clipboardData.items[key];
41 |
42 | if (item.kind === 'file') {
43 | const file = item.getAsFile();
44 |
45 | if (file) files.push(file);
46 | }
47 | });
48 |
49 | return files;
50 | }
51 |
--------------------------------------------------------------------------------
/src/mixins/upload-image.js:
--------------------------------------------------------------------------------
1 | import UploadFile from '@/components/upload-file';
2 | import imageToolbar from '@/toolbar/image';
3 | import { filesFilter, getFilesFromClipboardData } from '@/utils/file';
4 | import { image } from '@/utils/constants/command';
5 |
6 | const defaultConfig = {
7 | accept: 'image/*',
8 | multiple: false,
9 | };
10 |
11 | export default {
12 | components: {
13 | [UploadFile.name]: UploadFile,
14 | },
15 | props: {
16 | uploadImageConfig: Object,
17 | },
18 | emits: ['upload-image'],
19 | computed: {
20 | uploadImgConfig() {
21 | return { ...defaultConfig, ...this.uploadImageConfig };
22 | },
23 | hasUploadImage() {
24 | return !this.disabledMenus.includes(`${imageToolbar.name}/upload-image`);
25 | },
26 | },
27 | methods: {
28 | handleDrop(e) {
29 | const files = filesFilter(e.dataTransfer.files, this.uploadImgConfig);
30 |
31 | this.emitUploadImage(e, files);
32 | },
33 | handlePaste(e) {
34 | const { clipboardData } = e;
35 |
36 | if (!clipboardData) return;
37 |
38 | const files = filesFilter(getFilesFromClipboardData(clipboardData), this.uploadImgConfig);
39 |
40 | this.emitUploadImage(e, files);
41 | },
42 | emitUploadImage(e, files) {
43 | if (this.hasUploadImage && files.length) {
44 | e.preventDefault();
45 |
46 | this.$emit(
47 | 'upload-image',
48 | e,
49 | (imageConfig) => {
50 | this.execCommand(image, imageConfig);
51 | },
52 | files
53 | );
54 | }
55 | },
56 | },
57 | };
58 |
--------------------------------------------------------------------------------
/src/mixins/list.js:
--------------------------------------------------------------------------------
1 | const ol = /^\s*([\d]+\.)( \[[ xX]])? /;
2 | const ul = /^\s*([-*])( \[[ xX]])? /;
3 |
4 | const ulSyntax = /([*-] |[\d]+\. )/;
5 | const olSyntax = /([\d])+\.( \[[ xX]])? /;
6 |
7 | export default {
8 | mounted() {
9 | if (this.isPreviewMode) return;
10 |
11 | this.registerHotkeys({
12 | key: 'enter',
13 | preventDefault: false,
14 | action: (editor, e) => {
15 | if (e.isComposing) return;
16 |
17 | const cursorLineLeftText = this.getCursorLineLeftText();
18 | let suffix;
19 | let syntax;
20 |
21 | if (ol.test(cursorLineLeftText)) {
22 | suffix = 'x. ';
23 | syntax = olSyntax;
24 |
25 | e.preventDefault();
26 | } else if (ul.test(cursorLineLeftText)) {
27 | suffix = '- ';
28 | syntax = ulSyntax;
29 |
30 | e.preventDefault();
31 | } else {
32 | return;
33 | }
34 |
35 | const indent = cursorLineLeftText.search(syntax);
36 | const suffixIndex = indent + suffix.length;
37 | let beforeText = cursorLineLeftText.slice(0, suffixIndex);
38 | const content = cursorLineLeftText.slice(suffixIndex, cursorLineLeftText.length);
39 |
40 | if (content) {
41 | if (suffix === 'x. ') {
42 | beforeText = beforeText.replace(/(\d+)/, window.parseInt(beforeText) + 1);
43 | }
44 |
45 | this.replaceSelectionText(`\n${beforeText}`, 'end');
46 | } else {
47 | // break
48 | this.delLineLeft();
49 | this.replaceSelectionText('\n', 'end');
50 | }
51 | },
52 | });
53 | },
54 | };
55 |
--------------------------------------------------------------------------------
/src/utils/markdown-it-line-number.js:
--------------------------------------------------------------------------------
1 | export default function (md, { lineMarkup = 'data-line' } = {}) {
2 | const defaultRender = function (tokens, idx, options, env, self) {
3 | return self.renderToken(tokens, idx, options);
4 | };
5 |
6 | function addAttrwrapper(originalRender) {
7 | return function (tokens, idx, options, env, self) {
8 | const token = tokens[idx];
9 |
10 | token.attrPush([lineMarkup, token.map[0] + 1]);
11 |
12 | return originalRender(tokens, idx, options, env, self);
13 | };
14 | }
15 |
16 | function modifyCodewrapper(originalRender) {
17 | return function (tokens, idx, options, env, self) {
18 | const rawCode = originalRender(tokens, idx, options, env, self);
19 | const token = tokens[idx];
20 | const lineNumber = token.map[0] + 1;
21 |
22 | return `${rawCode}
`;
23 | };
24 | }
25 |
26 | const wrapperMap = {
27 | table_open: addAttrwrapper,
28 | blockquote_open: addAttrwrapper,
29 | bullet_list_open: addAttrwrapper,
30 | ordered_list_open: addAttrwrapper,
31 | reference_open: addAttrwrapper,
32 | heading_open: addAttrwrapper,
33 | lheading_open: addAttrwrapper,
34 | paragraph_open: addAttrwrapper,
35 | hr: addAttrwrapper,
36 | html_block: modifyCodewrapper,
37 | code_block: modifyCodewrapper,
38 | fence: modifyCodewrapper,
39 | };
40 |
41 | Object.keys(wrapperMap).forEach((ruleName) => {
42 | const originalRender = md.renderer.rules[ruleName];
43 | const render = originalRender || defaultRender;
44 |
45 | md.renderer.rules[ruleName] = wrapperMap[ruleName](render);
46 | });
47 | }
48 |
--------------------------------------------------------------------------------
/docs/zh/theme/vuepress.md:
--------------------------------------------------------------------------------
1 | # vuepress 主题
2 |
3 | ## 介绍
4 |
5 | markdown 解析使用 [markdown-it](https://github.com/markdown-it/markdown-it) 来实现,代码块解析使用 [prism](https://github.com/PrismJS/prism) 来实现。
6 |
7 | 效果如下:
8 |
9 |
10 |
11 |
12 |
13 | ## 内置功能
14 |
15 | vuepress 主题内置扩展了 tip 功能。同时你也可以在 left-toolbar 属性中配置 tip 来快捷插入。
16 |
17 | 效果如下:
18 |
19 |
20 |
21 |
22 |
23 | 示例代码:
24 |
25 | ```html
26 |
27 |
28 |
29 |
30 |
60 | ```
61 |
62 | ## 扩展
63 |
64 | 主题包默认只支持了 markup, html, xml, svg, mathml, css, clike, jacascript(js)。以免引入太多冗余代码导致包的体积过大。如果需要支持更多的语言代码高亮,请按需引入对应的语言包。
65 |
66 | ```js
67 | import VueMarkdownEditor from '@kangc/v-md-editor';
68 | import vuepressTheme from '@kangc/v-md-editor/lib/theme/vuepress.js';
69 | // 直接按需引入 prism 的语言包即可,此处以 json 为例
70 | import 'prismjs/components/prism-json';
71 |
72 | VueMarkdownEditor.use(vuepressTheme);
73 | ```
74 |
75 | ::: warning 注意
76 | 语言包需要在引入主题之后引入,否则不会生效。
77 | :::
78 |
79 | [查看 prism 支持的语言包](https://github.com/PrismJS/prism/tree/master/components)
80 |
81 | 扩展后就能支持对应的代码块高亮了,如下
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/src/plugins/copy-code/preview.js:
--------------------------------------------------------------------------------
1 | import copyToClipboard from 'copy-to-clipboard';
2 |
3 | function isCopyButton(el) {
4 | return el.classList.contains('v-md-copy-code-btn');
5 | }
6 |
7 | function findCodeWrapperEl(el) {
8 | if (el.classList.contains('v-md-pre-wrapper')) {
9 | return el;
10 | }
11 |
12 | return findCodeWrapperEl(el.parentNode);
13 | }
14 |
15 | function getPreviewEl(el) {
16 | const previewElClass = 'v-md-editor-preview';
17 |
18 | return el.classList.contains(previewElClass) ? el : el.querySelector(`.${previewElClass}`);
19 | }
20 |
21 | export default function createCopyCodePreview() {
22 | return {
23 | install(VMdEditor) {
24 | if (!VMdEditor.mixins) VMdEditor.mixins = [];
25 |
26 | VMdEditor.mixins.push({
27 | emits: ['copy-code-success'],
28 | mounted() {
29 | this.$nextTick(() => {
30 | const previewEl = getPreviewEl(this.$el);
31 |
32 | previewEl.addEventListener('click', this.handleCopyCodeClick);
33 | });
34 | },
35 | beforeUnmount() {
36 | const previewEl = getPreviewEl(this.$el);
37 |
38 | previewEl.removeEventListener('click', this.handleCopyCodeClick);
39 | },
40 | methods: {
41 | handleCopyCodeClick({ target }) {
42 | if (isCopyButton(target)) {
43 | const codeWrapper = findCodeWrapperEl(target.parentNode);
44 |
45 | if (codeWrapper) {
46 | const code = codeWrapper.querySelector('code').innerText;
47 |
48 | copyToClipboard(code);
49 | this.$emit('copy-code-success', code);
50 | }
51 | }
52 | },
53 | },
54 | });
55 | },
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/toolbar-item/tooltip.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 | {{ text }}
12 |
13 |
14 |
15 |
16 |
57 |
58 |
76 |
--------------------------------------------------------------------------------
/src/mixins/toolbar.js:
--------------------------------------------------------------------------------
1 | import registerToolbar from '@/utils/toolbar';
2 |
3 | export default function (Component) {
4 | return {
5 | props: {
6 | leftToolbar: {
7 | type: String,
8 | default:
9 | 'undo redo clear | h bold italic strikethrough quote | ul ol table hr | link image code | save',
10 | },
11 | rightToolbar: {
12 | type: String,
13 | default: 'preview toc sync-scroll fullscreen',
14 | },
15 | toolbar: {
16 | type: Object,
17 | default: () => ({}),
18 | },
19 | disabledMenus: {
20 | type: Array,
21 | default: () => ['image/upload-image'],
22 | },
23 | toolbarConfig: {
24 | type: Object,
25 | default: () => ({}),
26 | },
27 | },
28 | created() {
29 | const { toolbars } = Component;
30 |
31 | this.toolbars = {};
32 |
33 | Object.keys(toolbars).forEach((name) => {
34 | this.registerToolbar(name, toolbars[name]);
35 | });
36 |
37 | Object.keys(this.toolbar).forEach((name) => {
38 | this.registerToolbar(name, this.toolbar[name]);
39 | });
40 | },
41 | methods: {
42 | registerToolbar(name, config) {
43 | registerToolbar(this.toolbars, name, config);
44 | },
45 | handleToolbarItemClick(toolbar) {
46 | if (toolbar.action && !toolbar.menus?.length && typeof toolbar.action === 'function') {
47 | toolbar.action.call(toolbar, this, this.toolbarConfig[toolbar.name]);
48 | }
49 | },
50 | handleToolbarMenuClick(menu) {
51 | if (menu.action && typeof menu.action === 'function') {
52 | menu.action.call(menu, this, this.toolbarConfig[menu.name]);
53 | }
54 | },
55 | },
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/src/utils/markdown-it-container.js:
--------------------------------------------------------------------------------
1 | // Modified from https://github.com/vuepress/vuepress-community/blob/master/packages/vuepress-plugin-container/src/markdown-it-container.ts
2 |
3 | import container from 'markdown-it-container';
4 |
5 | function wrapRenderPlaceFunction(func) {
6 | if (typeof func === 'string') {
7 | return () => func;
8 | }
9 | return func;
10 | }
11 |
12 | export default function (
13 | md,
14 | {
15 | validate,
16 | marker,
17 | render,
18 | type,
19 | before,
20 | after,
21 | defaultTitle = type.toUpperCase(),
22 | blockClass = 'custom-block',
23 | }
24 | ) {
25 | if (!type) {
26 | return;
27 | }
28 |
29 | if (!render) {
30 | let renderBefore;
31 | let renderAfter;
32 |
33 | if (before !== undefined && after !== undefined) {
34 | // user defined
35 | renderBefore = wrapRenderPlaceFunction(before);
36 | renderAfter = wrapRenderPlaceFunction(after);
37 | } else {
38 | // fallback default
39 | renderBefore = (info) =>
40 | `${
41 | info ? `
${info}
` : ''
42 | }\n`;
43 | renderAfter = () => '
\n';
44 | }
45 |
46 | render = (tokens, index) => {
47 | const token = tokens[index];
48 |
49 | let info = token.info.trim().slice(type.length).trim();
50 |
51 | if (!info && defaultTitle) {
52 | if (typeof defaultTitle === 'function') {
53 | info = defaultTitle();
54 | } else {
55 | info = defaultTitle;
56 | }
57 | }
58 |
59 | if (token.nesting === 1) {
60 | return renderBefore(info);
61 | }
62 |
63 | return renderAfter(info);
64 | };
65 | }
66 |
67 | md.use(container, type, { render, validate, marker });
68 | }
69 |
--------------------------------------------------------------------------------
/src/create-editor.js:
--------------------------------------------------------------------------------
1 | import Lang from '@/utils/lang';
2 | import zhCNConfig from '@/lang/zh-CN';
3 | import { reactive } from 'vue';
4 |
5 | import { commandWrapper } from '@/utils/command';
6 | import { toolbarWrapper } from '@/utils/toolbar';
7 |
8 | // mixins
9 | import commonMixin from '@/mixins/common';
10 | import vModelMixin from '@/mixins/v-model';
11 | import fullscreenMixin from '@/mixins/fullscreen';
12 | import uploadImageMixin from '@/mixins/upload-image';
13 | import syncScrollMixin from '@/mixins/sync-scroll';
14 | import toolbarMixin from '@/mixins/toolbar';
15 | import commandMixin from '@/mixins/command';
16 | import tocMixin from '@/mixins/toc';
17 | import scrollMixin from '@/mixins/scroll';
18 | import hotkeysMixin from '@/mixins/hotkeys';
19 | import listMixin from '@/mixins/list';
20 | import langMixin from '@/mixins/lang';
21 |
22 | import Preview from '@/preview';
23 |
24 | const lang = new Lang({
25 | afterUse(lang) {
26 | Preview.vMdParser.lang.config.lang = lang;
27 | },
28 | });
29 | lang.config = reactive(lang.config);
30 | lang.add({
31 | 'zh-CN': zhCNConfig,
32 | });
33 |
34 | export default function createEditor(component) {
35 | commandWrapper(component);
36 | toolbarWrapper(component);
37 |
38 | component.name = 'v-md-editor';
39 | component.lang = lang;
40 | component.vMdParser = Preview.vMdParser;
41 | component.Preview = Preview;
42 | component.hotkeys = [];
43 | component.hotkey = function (config) {
44 | component.hotkeys.push(config);
45 | };
46 | component.mixins = [
47 | commonMixin,
48 | vModelMixin,
49 | toolbarMixin(component),
50 | commandMixin(component),
51 | hotkeysMixin(component),
52 | fullscreenMixin,
53 | uploadImageMixin,
54 | syncScrollMixin,
55 | tocMixin,
56 | scrollMixin,
57 | listMixin,
58 | langMixin,
59 | ];
60 | }
61 |
--------------------------------------------------------------------------------
/src/utils/util.js:
--------------------------------------------------------------------------------
1 | const { toString } = Object.prototype;
2 |
3 | export const isObject = (target) => toString.call(target) === '[object Object]';
4 |
5 | function extend(to, _from) {
6 | Object.keys(_from).forEach((key) => {
7 | to[key] = _from[key];
8 | });
9 |
10 | return to;
11 | }
12 |
13 | export function arraytoObject(arr) {
14 | const res = {};
15 | for (let i = 0; i < arr.length; i++) {
16 | if (arr[i]) {
17 | extend(res, arr[i]);
18 | }
19 | }
20 | return res;
21 | }
22 |
23 | export function importAll(map, r) {
24 | r.keys().forEach((filePath) => {
25 | map[filePath] = r(filePath);
26 | });
27 | }
28 |
29 | export const inBrowser = typeof window !== 'undefined';
30 |
31 | export function isKorean(text) {
32 | const reg = /([(\uAC00-\uD7AF)|(\u3130-\u318F)])+/gi;
33 |
34 | return reg.test(text);
35 | }
36 |
37 | export function generatorText({
38 | selected,
39 | InsertGetter,
40 | selectedGetter = (selected) => selected,
41 | ignoreEmptyLine = true,
42 | }) {
43 | let insertContent;
44 | let newSelected;
45 |
46 | if (selected) {
47 | newSelected = selectedGetter(selected);
48 | insertContent = InsertGetter(selected, 1);
49 |
50 | // 如果当前选中的文本包含换行 则插入后选中插入的所有文本
51 | if (selected.indexOf('\n') !== -1) {
52 | insertContent = selected
53 | .split('\n')
54 | .map((rowText, index) => {
55 | const isEmptyLine = !rowText;
56 | if (ignoreEmptyLine && isEmptyLine) return '';
57 |
58 | return InsertGetter(rowText, index + 1).replace(selectedGetter(null), '');
59 | })
60 | .join('\n');
61 |
62 | newSelected = insertContent;
63 | }
64 | } else {
65 | insertContent = InsertGetter(null, 1);
66 | newSelected = selectedGetter(selected);
67 | }
68 |
69 | return {
70 | insertContent,
71 | newSelected,
72 | };
73 | }
74 |
--------------------------------------------------------------------------------
/src/mixins/toc.js:
--------------------------------------------------------------------------------
1 | import { LINE_MARKUP } from '@/utils/constants/markup';
2 |
3 | export default {
4 | data() {
5 | return {
6 | tocVisible: this.defaultShowToc,
7 | titles: [],
8 | };
9 | },
10 | props: {
11 | includeLevel: {
12 | type: Array,
13 | default: () => [2, 3],
14 | },
15 | defaultShowToc: Boolean,
16 | },
17 | watch: {
18 | text: {
19 | immediate: true,
20 | handler(newval, oldVal) {
21 | // render in the first time
22 | if (typeof oldVal === 'undefined') {
23 | this.$nextTick(this.updateTocNav);
24 | return;
25 | }
26 |
27 | if (this.updateTocNavTimmer) clearTimeout(this.updateTocNavTimmer);
28 |
29 | this.updateTocNavTimmer = setTimeout(this.updateTocNav, 800);
30 | },
31 | },
32 | },
33 | computed: {
34 | anchorsSelector() {
35 | return this.includeLevel.map((level) => `h${level}`).join(',');
36 | },
37 | },
38 | methods: {
39 | toggleToc(visible = !this.tocVisible) {
40 | this.tocVisible = visible;
41 | },
42 | updateTocNav() {
43 | const previewEl = this.$refs.preview?.$el;
44 |
45 | if (!previewEl) return;
46 |
47 | const anchors = previewEl.querySelectorAll(this.anchorsSelector);
48 | const titles = Array.from(anchors).filter((title) => !!title.innerText.trim());
49 |
50 | if (!titles.length) {
51 | this.titles = [];
52 | return;
53 | }
54 |
55 | const hTags = Array.from(new Set(titles.map((title) => title.tagName))).sort();
56 |
57 | this.titles = titles.map((el) => ({
58 | title: el.innerText,
59 | lineIndex: el.getAttribute(LINE_MARKUP),
60 | indent: hTags.indexOf(el.tagName),
61 | }));
62 | },
63 | handleNavClick({ lineIndex }) {
64 | this.scrollToLine(lineIndex);
65 | },
66 | },
67 | };
68 |
--------------------------------------------------------------------------------
/src/plugins/highlight-lines/highlight-lines.css:
--------------------------------------------------------------------------------
1 | .vuepress-markdown-body div[class*='v-md-pre-wrapper-'] .highlight-lines {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | width: 100%;
6 | padding-top: 1.3rem;
7 | line-height: 1.4;
8 | -webkit-user-select: none;
9 | user-select: none;
10 | }
11 | .vuepress-markdown-body
12 | div[class*='v-md-pre-wrapper-'].line-numbers-mode
13 | .highlight-lines
14 | .highlighted {
15 | position: relative;
16 | }
17 | .vuepress-markdown-body
18 | div[class*='v-md-pre-wrapper-'].line-numbers-mode
19 | .highlight-lines
20 | .highlighted::before {
21 | position: absolute;
22 | top: 0;
23 | left: 0;
24 | z-index: 3;
25 | display: block;
26 | width: 3.5rem;
27 | height: 100%;
28 | background-color: rgba(0, 0, 0, 0.66);
29 | content: ' ';
30 | }
31 | .vuepress-markdown-body div[class*='v-md-pre-wrapper-'] .highlight-lines .highlighted {
32 | background-color: rgba(0, 0, 0, 0.66);
33 | }
34 | .github-markdown-body
35 | div[class*='v-md-pre-wrapper-'].line-numbers-mode
36 | .highlight-lines
37 | .highlighted {
38 | position: relative;
39 | }
40 | .github-markdown-body
41 | div[class*='v-md-pre-wrapper-'].line-numbers-mode
42 | .highlight-lines
43 | .highlighted::before {
44 | position: absolute;
45 | top: 0;
46 | left: 0;
47 | z-index: 3;
48 | display: block;
49 | width: 3.5rem;
50 | height: 100%;
51 | background-color: rgba(208, 213, 221, 0.66);
52 | content: ' ';
53 | }
54 | .github-markdown-body div[class*='v-md-pre-wrapper-'] .highlight-lines {
55 | position: absolute;
56 | top: 0;
57 | left: 0;
58 | width: 100%;
59 | padding-top: 16px;
60 | font-size: 85%;
61 | line-height: 1.4;
62 | -webkit-user-select: none;
63 | -ms-user-select: none;
64 | user-select: none;
65 | }
66 | .github-markdown-body div[class*='v-md-pre-wrapper-'] .highlight-lines .highlighted {
67 | background-color: rgba(208, 213, 221, 0.66);
68 | }
69 |
--------------------------------------------------------------------------------
/dev/text.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | export default `Markdown Editor built on Vue
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | ## Links
12 |
13 | - [Demo](https://code-farmer-i.github.io/vue-markdown-editor/examples/base-editor.html)
14 | - [Documentation](https://code-farmer-i.github.io/vue-markdown-editor/)
15 | - [Changelog](https://code-farmer-i.github.io/vue-markdown-editor/changelog.html)
16 |
17 | ## Install
18 |
19 | \`\`\`bash
20 | # use npm
21 | npm i @kangc/v-md-editor -S
22 |
23 | # use yarn
24 | yarn add @kangc/v-md-editor
25 | \`\`\`
26 |
27 | ## Quick Start
28 |
29 | \`\`\`js
30 | import Vue from 'vue';
31 | import VueMarkdownEditor from '@kangc/v-md-editor';
32 | import '@kangc/v-md-editor/lib/style/base-editor.css';
33 | import vuepressTheme from '@kangc/v-md-editor/lib/theme/vuepress.js';
34 |
35 | VueMarkdownEditor.use(vuepressTheme);
36 |
37 | Vue.use(VueMarkdownEditor);
38 | \`\`\`
39 |
40 | ## Usage
41 |
42 | \`\`\`html
43 |
44 |
45 |
46 |
47 |
56 | \`\`\`
57 |
58 | ## Refrence
59 |
60 | - [ElementUi Scrollbar Component](https://github.com/ElemeFE/element/tree/dev/packages/scrollbar)
61 | - [vuepress-plugin-container](https://vuepress.github.io/zh/plugins/container/)
62 | `;
63 |
--------------------------------------------------------------------------------
/src/utils/markdown-it-link.js:
--------------------------------------------------------------------------------
1 | // Modified from https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/markdown/lib/link.js
2 |
3 | export default function (md, { externalAttrs, openLinkIcon, openLinkIconClass }) {
4 | let hasOpenExternalLink = false;
5 |
6 | md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
7 | const token = tokens[idx];
8 | const hrefIndex = token.attrIndex('href');
9 |
10 | if (hrefIndex >= 0) {
11 | const link = token.attrs[hrefIndex];
12 | const href = link[1];
13 | const isExternal = /^https?:/.test(href);
14 |
15 | if (isExternal) {
16 | Object.keys(externalAttrs).forEach((key) => {
17 | token.attrSet(key, externalAttrs[key]);
18 | });
19 | if (/_blank/i.test(externalAttrs.target)) {
20 | hasOpenExternalLink = true;
21 | }
22 | }
23 | }
24 |
25 | return self.renderToken(tokens, idx, options);
26 | };
27 |
28 | md.renderer.rules.link_close = (tokens, idx, options, env, self) => {
29 | if (hasOpenExternalLink) {
30 | hasOpenExternalLink = false;
31 |
32 | if (openLinkIcon) {
33 | if (openLinkIconClass) {
34 | return ` ` + self.renderToken(tokens, idx, options);
35 | }
36 |
37 | return (
38 | // eslint-disable-next-line max-len
39 | ' ' +
40 | self.renderToken(tokens, idx, options)
41 | );
42 | }
43 | }
44 | return self.renderToken(tokens, idx, options);
45 | };
46 | }
47 |
--------------------------------------------------------------------------------
/src/preview.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
88 |
--------------------------------------------------------------------------------
/src/utils/clickoutside.js:
--------------------------------------------------------------------------------
1 | // Modified from https://github.com/ElemeFE/element/blob/dev/src/utils/clickoutside.js
2 | import { inBrowser } from '@/utils/util';
3 |
4 | const nodeList = [];
5 | const ctx = '@@clickoutsideContext';
6 |
7 | let startClick;
8 | let seed = 0;
9 |
10 | if (inBrowser) {
11 | document.addEventListener('mousedown', (e) => {
12 | startClick = e;
13 | });
14 |
15 | document.addEventListener('mouseup', (e) => {
16 | nodeList.forEach((node) => node[ctx].documentHandler(e, startClick));
17 | });
18 | }
19 |
20 | function createDocumentHandler(el, binding, vnode) {
21 | return function (mouseup = {}, mousedown = {}) {
22 | if (
23 | !vnode ||
24 | !binding ||
25 | !binding.instance ||
26 | !mouseup.target ||
27 | !mousedown.target ||
28 | el.contains(mouseup.target) ||
29 | el.contains(mousedown.target) ||
30 | el === mouseup.target
31 | ) {
32 | return;
33 | }
34 |
35 | if (binding.arg && el[ctx].methodName && binding.instance[el[ctx].methodName]) {
36 | binding.instance[el[ctx].methodName]();
37 | } else {
38 | el[ctx].bindingFn && el[ctx].bindingFn();
39 | }
40 | };
41 | }
42 |
43 | export default {
44 | beforeMount(el, binding, vnode) {
45 | nodeList.push(el);
46 | const id = seed++;
47 | el[ctx] = {
48 | id,
49 | documentHandler: createDocumentHandler(el, binding, vnode),
50 | methodName: binding.arg,
51 | bindingFn: binding.value,
52 | };
53 | },
54 |
55 | updated(el, binding, vnode) {
56 | el[ctx].documentHandler = createDocumentHandler(el, binding, vnode);
57 | el[ctx].methodName = binding.arg;
58 | el[ctx].bindingFn = binding.value;
59 | },
60 |
61 | unmounted(el) {
62 | const len = nodeList.length;
63 |
64 | for (let i = 0; i < len; i++) {
65 | if (nodeList[i][ctx].id === el[ctx].id) {
66 | nodeList.splice(i, 1);
67 | break;
68 | }
69 | }
70 | delete el[ctx];
71 | },
72 | };
73 |
--------------------------------------------------------------------------------
/src/utils/markdown-it-highlight-lines.js:
--------------------------------------------------------------------------------
1 | // Modified from https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/markdown/lib/highlightLines.js
2 | // It depends on preWrapper plugin.
3 |
4 | export default function (md, options = {}) {
5 | const { fence } = md.renderer.rules;
6 | const { leftDelimiter = '{', rightDelimiter = '}' } = options;
7 | const RE = new RegExp(`${leftDelimiter}([\\d,-]+)${rightDelimiter}`);
8 |
9 | md.renderer.rules.fence = (...args) => {
10 | const rawCode = fence(...args);
11 | const [tokens, idx] = args;
12 | const token = tokens[idx];
13 |
14 | if (!token.lineNumbers) {
15 | const rawInfo = token.info;
16 | if (!rawInfo || !RE.test(rawInfo)) {
17 | return fence(...args);
18 | }
19 |
20 | const langName = rawInfo.replace(RE, '').trim();
21 | // ensure the next plugin get the correct lang.
22 | token.info = langName;
23 |
24 | token.lineNumbers = RE.exec(rawInfo)[1]
25 | .split(',')
26 | .map((v) => v.split('-').map((v) => parseInt(v, 10)));
27 | }
28 |
29 | const code = rawCode.slice(rawCode.indexOf(''), rawCode.indexOf(''));
30 | const highlightLinesCode = code
31 | .split('\n')
32 | .map((split, index) => {
33 | const lineNumber = index + 1;
34 | const inRange = token.lineNumbers.some(([start, end]) => {
35 | if (start && end) {
36 | return lineNumber >= start && lineNumber <= end;
37 | }
38 | return lineNumber === start;
39 | });
40 | if (inRange) {
41 | return '
';
42 | }
43 | return ' ';
44 | })
45 | .join('');
46 |
47 | const highlightLinesWrapperCode = `${highlightLinesCode}
`;
48 | const finalCode = rawCode.replace(
49 | '',
50 | `${highlightLinesWrapperCode}`
51 | );
52 |
53 | return finalCode;
54 | };
55 | }
56 |
--------------------------------------------------------------------------------
/src/plugins/tip/parser.js:
--------------------------------------------------------------------------------
1 | import markdownItContainer from '@/utils/markdown-it-container';
2 |
3 | export default function (vMdParser) {
4 | vMdParser.extendMarkdown((mdParser) => {
5 | const blockClass = 'v-md-plugin-tip';
6 | const getLangConfig = function () {
7 | const lang = vMdParser.lang.config;
8 | const langConfig = lang.langConfig[lang.lang];
9 |
10 | return langConfig;
11 | };
12 |
13 | markdownItContainer(mdParser, {
14 | type: 'tip',
15 | defaultTitle: () => getLangConfig().tip.tip.defaultTitle,
16 | blockClass,
17 | });
18 |
19 | markdownItContainer(mdParser, {
20 | type: 'warning',
21 | defaultTitle: () => getLangConfig().tip.warning.defaultTitle,
22 | blockClass,
23 | });
24 |
25 | markdownItContainer(mdParser, {
26 | type: 'danger',
27 | defaultTitle: () => getLangConfig().tip.danger.defaultTitle,
28 | blockClass,
29 | });
30 |
31 | markdownItContainer(mdParser, {
32 | type: 'details',
33 | defaultTitle: () => getLangConfig().tip.details.defaultTitle,
34 | before: (info) =>
35 | `${info ? `${info} ` : ''}\n`,
36 | after: () => ' \n',
37 | });
38 | });
39 |
40 | vMdParser.lang.add({
41 | 'zh-CN': {
42 | tip: {
43 | tip: {
44 | defaultTitle: '提示',
45 | },
46 | warning: {
47 | defaultTitle: '注意',
48 | },
49 | danger: {
50 | defaultTitle: '警告',
51 | },
52 | details: {
53 | defaultTitle: '详细信息',
54 | },
55 | },
56 | },
57 | 'en-US': {
58 | tip: {
59 | tip: {
60 | defaultTitle: 'TIP',
61 | },
62 | warning: {
63 | defaultTitle: 'WARNING',
64 | },
65 | danger: {
66 | defaultTitle: 'DANGER',
67 | },
68 | details: {
69 | defaultTitle: 'DETAILS',
70 | },
71 | },
72 | },
73 | });
74 | }
75 |
--------------------------------------------------------------------------------
/src/lang/zh-CN.js:
--------------------------------------------------------------------------------
1 | export default {
2 | undo: {
3 | toolbar: '撤销',
4 | },
5 | redo: {
6 | toolbar: '重做',
7 | },
8 | clear: {
9 | toolbar: '清空',
10 | },
11 | h: {
12 | toolbar: '标题',
13 | },
14 | h1: {
15 | toolbar: '一级标题',
16 | placeholder: '一级标题',
17 | },
18 | h2: {
19 | toolbar: '二级标题',
20 | placeholder: '二级标题',
21 | },
22 | h3: {
23 | toolbar: '三级标题',
24 | placeholder: '三级标题',
25 | },
26 | h4: {
27 | toolbar: '四级标题',
28 | placeholder: '四级标题',
29 | },
30 | h5: {
31 | toolbar: '五级标题',
32 | placeholder: '五级标题',
33 | },
34 | h6: {
35 | toolbar: '六级标题',
36 | placeholder: '六级标题',
37 | },
38 | bold: {
39 | toolbar: '粗体',
40 | placeholder: '粗体',
41 | },
42 | italic: {
43 | toolbar: '斜体',
44 | placeholder: '斜体',
45 | },
46 | strikethrough: {
47 | toolbar: '删除线',
48 | placeholder: '删除线',
49 | },
50 | quote: {
51 | toolbar: '插入引用',
52 | placeholder: '引用',
53 | },
54 | ul: {
55 | toolbar: '无序列表',
56 | placeholder: '无序列表',
57 | },
58 | ol: {
59 | toolbar: '有序列表',
60 | placeholder: '有序列表',
61 | },
62 | table: {
63 | toolbar: '表格',
64 | },
65 | hr: {
66 | toolbar: '插入分割线',
67 | },
68 | link: {
69 | toolbar: '插入链接',
70 | descPlaceholder: '链接',
71 | },
72 | image: {
73 | toolbar: '插入图片',
74 | },
75 | imageLink: {
76 | toolbar: '添加图片链接',
77 | },
78 | uploadImage: {
79 | toolbar: '上传本地图片',
80 | },
81 | code: {
82 | toolbar: '插入代码块',
83 | },
84 | save: {
85 | toolbar: '保存',
86 | },
87 | preview: {
88 | enabled: '开启预览',
89 | disabled: '关闭预览',
90 | },
91 | toc: {
92 | title: '目录导航',
93 | enabled: '开启目录导航',
94 | disabled: '关闭目录导航',
95 | },
96 | syncScroll: {
97 | enabled: '开启同步滚动',
98 | disabled: '关闭同步滚动',
99 | },
100 | fullscreen: {
101 | enabled: '全屏(按ESC还原)',
102 | disabled: '退出全屏',
103 | },
104 | };
105 |
--------------------------------------------------------------------------------
/src/lang/zh-TW.js:
--------------------------------------------------------------------------------
1 | export default {
2 | undo: {
3 | toolbar: '復原',
4 | },
5 | redo: {
6 | toolbar: '取消復原',
7 | },
8 | clear: {
9 | toolbar: '清空',
10 | },
11 | h: {
12 | toolbar: '標題',
13 | },
14 | h1: {
15 | toolbar: '一級標題',
16 | placeholder: '一級標題',
17 | },
18 | h2: {
19 | toolbar: '二級標題',
20 | placeholder: '二級標題',
21 | },
22 | h3: {
23 | toolbar: '三級標題',
24 | placeholder: '三級標題',
25 | },
26 | h4: {
27 | toolbar: '四級標題',
28 | placeholder: '四級標題',
29 | },
30 | h5: {
31 | toolbar: '五級標題',
32 | placeholder: '五級標題',
33 | },
34 | h6: {
35 | toolbar: '六級標題',
36 | placeholder: '六級標題',
37 | },
38 | bold: {
39 | toolbar: '粗體',
40 | placeholder: '粗體',
41 | },
42 | italic: {
43 | toolbar: '斜體',
44 | placeholder: '斜體',
45 | },
46 | strikethrough: {
47 | toolbar: '刪除線',
48 | placeholder: '刪除線',
49 | },
50 | quote: {
51 | toolbar: '插入引用',
52 | placeholder: '引用',
53 | },
54 | ul: {
55 | toolbar: '無序列表',
56 | placeholder: '無序列表',
57 | },
58 | ol: {
59 | toolbar: '有序列表',
60 | placeholder: '有序列表',
61 | },
62 | table: {
63 | toolbar: '表格',
64 | },
65 | hr: {
66 | toolbar: '插入分隔線',
67 | },
68 | link: {
69 | toolbar: '插入連結',
70 | descPlaceholder: '連結',
71 | },
72 | image: {
73 | toolbar: '插入圖片',
74 | },
75 | imageLink: {
76 | toolbar: '添加圖片連結',
77 | },
78 | uploadImage: {
79 | toolbar: '上傳圖片',
80 | },
81 | code: {
82 | toolbar: '插入程式碼',
83 | },
84 | save: {
85 | toolbar: '保存',
86 | },
87 | preview: {
88 | enabled: '開啟預覽',
89 | disabled: '關閉預覽',
90 | },
91 | toc: {
92 | title: '目錄導航',
93 | enabled: '開啟目錄導航',
94 | disabled: '關閉目錄導航',
95 | },
96 | syncScroll: {
97 | enabled: '開啟同步滾動',
98 | disabled: '關閉同步滾動',
99 | },
100 | fullscreen: {
101 | enabled: '全螢幕(按ESC還原)',
102 | disabled: '退出全螢幕',
103 | },
104 | };
105 |
--------------------------------------------------------------------------------
/src/lang/ko-KR.js:
--------------------------------------------------------------------------------
1 | export default {
2 | undo: {
3 | toolbar: '실행취소',
4 | },
5 | redo: {
6 | toolbar: '복원',
7 | },
8 | clear: {
9 | toolbar: '전체 삭제',
10 | },
11 | h: {
12 | toolbar: '헤딩',
13 | },
14 | h1: {
15 | toolbar: '헤딩 1',
16 | placeholder: '헤딩 1',
17 | },
18 | h2: {
19 | toolbar: '헤딩 2',
20 | placeholder: '헤딩 2',
21 | },
22 | h3: {
23 | toolbar: '헤딩 3',
24 | placeholder: '헤딩 3',
25 | },
26 | h4: {
27 | toolbar: '헤딩 4',
28 | placeholder: '헤딩 4',
29 | },
30 | h5: {
31 | toolbar: '헤딩 5',
32 | placeholder: '헤딩 5',
33 | },
34 | h6: {
35 | toolbar: '헤딩 6',
36 | placeholder: '헤딩 6',
37 | },
38 | bold: {
39 | toolbar: '굵게',
40 | placeholder: '굵게',
41 | },
42 | italic: {
43 | toolbar: '이탤릭',
44 | placeholder: '이탤릭',
45 | },
46 | strikethrough: {
47 | toolbar: '취소선',
48 | placeholder: '취소선',
49 | },
50 | quote: {
51 | toolbar: '인용',
52 | placeholder: '인용',
53 | },
54 | ul: {
55 | toolbar: '리스트',
56 | placeholder: '리스트',
57 | },
58 | ol: {
59 | toolbar: '순서 리스트',
60 | placeholder: '순서 리스트',
61 | },
62 | table: {
63 | toolbar: '표',
64 | },
65 | hr: {
66 | toolbar: '가로줄',
67 | },
68 | link: {
69 | toolbar: '링크',
70 | descPlaceholder: '링크',
71 | },
72 | image: {
73 | toolbar: '이미지 추가',
74 | },
75 | imageLink: {
76 | toolbar: '이미지 링크 추가',
77 | },
78 | uploadImage: {
79 | toolbar: '이미지 업로드',
80 | },
81 | code: {
82 | toolbar: '코드블럭',
83 | },
84 | save: {
85 | toolbar: '저장',
86 | },
87 | preview: {
88 | enabled: '미리보기 켬',
89 | disabled: '미리보기 끔',
90 | },
91 | toc: {
92 | title: '네비게이션',
93 | enabled: 'TOC 켬',
94 | disabled: 'TOC 끔',
95 | },
96 | syncScroll: {
97 | enabled: '스크롤 동기화 켬',
98 | disabled: '스크롤 동기화 끔',
99 | },
100 | fullscreen: {
101 | enabled: '전체화면 (ESC를 누르면 복원)',
102 | disabled: '전체화면 나가기',
103 | },
104 | };
105 |
--------------------------------------------------------------------------------
/src/plugins/mermaid/creator.js:
--------------------------------------------------------------------------------
1 | import parser from './parser';
2 | import { deepAssign } from '@/utils/deep-assign';
3 | import { inBrowser } from '@/utils/util';
4 |
5 | function getPreviewEl(el) {
6 | const previewElClass = 'v-md-editor-preview';
7 |
8 | return el.classList.contains(previewElClass) ? el : el.querySelector(`.${previewElClass}`);
9 | }
10 |
11 | export default function creator(mermaid) {
12 | async function handleMdChange() {
13 | if (!inBrowser) return;
14 |
15 | await this.$nextTick();
16 |
17 | const previewEl = getPreviewEl(this.$el);
18 | const eles = previewEl.querySelectorAll('.v-md-mermaid');
19 |
20 | if (!eles.length) return;
21 |
22 | mermaid.run({
23 | nodes: eles,
24 | });
25 | }
26 |
27 | return function createMermaidPlugin({ mermaidInitializeOptions = {} } = {}) {
28 | const initialize = {
29 | altFontFamily: 'sans-serif',
30 | flowchart: {
31 | htmlLabels: true,
32 | useMaxWidth: true,
33 | },
34 | fontFamily: 'sans-serif',
35 | gantt: {
36 | leftPadding: 75,
37 | rightPadding: 20,
38 | },
39 | securityLevel: 'loose',
40 | sequence: {
41 | boxMargin: 8,
42 | diagramMarginX: 8,
43 | diagramMarginY: 8,
44 | useMaxWidth: true,
45 | },
46 | startOnLoad: false,
47 | };
48 |
49 | deepAssign(initialize, mermaidInitializeOptions);
50 |
51 | return {
52 | install(VMdEditor) {
53 | VMdEditor.vMdParser.use(parser);
54 |
55 | if (!VMdEditor.mixins) VMdEditor.mixins = [];
56 |
57 | const mixin = {
58 | created() {
59 | mermaid.initialize(initialize);
60 | },
61 | watch: {
62 | html: {
63 | immediate: true,
64 | handler: handleMdChange,
65 | },
66 | },
67 | };
68 |
69 | if (VMdEditor.name === 'v-md-editor') {
70 | VMdEditor.Preview.mixins.push(mixin);
71 | } else {
72 | VMdEditor.mixins.push(mixin);
73 | }
74 | },
75 | };
76 | };
77 | }
78 |
--------------------------------------------------------------------------------
/docs/theme/vuepress.md:
--------------------------------------------------------------------------------
1 | # VuePress
2 |
3 | ## Introduction
4 |
5 | example:
6 |
7 |
8 |
9 |
10 |
11 | ## Built-in functions
12 |
13 | The vuepress theme has built-in extended tip functionality. At the same time, you can also configure tip in the left-toolbar property to insert quickly.
14 |
15 |
16 |
17 |
18 |
19 | demo:
20 |
21 | ```html
22 |
23 |
24 |
25 |
26 |
56 | ```
57 |
58 | ## Expand
59 |
60 | The theme package only supports markup, html, xml, svg, mathml, css, clike, jacascript(js) by default. In order to avoid introducing too much redundant code, the package size is too large. If you need to support more language code highlighting, please introduce the corresponding language pack as needed.
61 |
62 | ```js
63 | import VueMarkdownEditor from '@kangc/v-md-editor';
64 | import vuepressTheme from '@kangc/v-md-editor/lib/theme/vuepress.js';
65 | // Introduce prism language packs as needed, here is json as an example
66 | import 'prismjs/components/prism-json';
67 |
68 | VueMarkdownEditor.use(vuepressTheme);
69 | ```
70 |
71 | ::: warning
72 | Language packs need to be introduced after the introduction of the theme, otherwise it will not take effect.
73 | :::
74 |
75 | [View languages supported by prism](https://github.com/PrismJS/prism/tree/master/components)
76 |
77 | After expansion, the corresponding code block can be highlighted.
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/src/lang/ja-JP.js:
--------------------------------------------------------------------------------
1 | export default {
2 | undo: {
3 | toolbar: '元に戻す',
4 | },
5 | redo: {
6 | toolbar: 'やり直し',
7 | },
8 | clear: {
9 | toolbar: 'クリア',
10 | },
11 | h: {
12 | toolbar: '見出し',
13 | },
14 | h1: {
15 | toolbar: '見出し1',
16 | placeholder: '見出し1',
17 | },
18 | h2: {
19 | toolbar: '見出し2',
20 | placeholder: '見出し2',
21 | },
22 | h3: {
23 | toolbar: '見出し3',
24 | placeholder: '見出し3',
25 | },
26 | h4: {
27 | toolbar: '見出し4',
28 | placeholder: '見出し4',
29 | },
30 | h5: {
31 | toolbar: '見出し5',
32 | placeholder: '見出し5',
33 | },
34 | h6: {
35 | toolbar: '見出し6',
36 | placeholder: '見出し6',
37 | },
38 | bold: {
39 | toolbar: '太字',
40 | placeholder: '太字',
41 | },
42 | italic: {
43 | toolbar: '斜体',
44 | placeholder: '斜体',
45 | },
46 | strikethrough: {
47 | toolbar: '取り消し線',
48 | placeholder: '取り消し線',
49 | },
50 | quote: {
51 | toolbar: '引用',
52 | placeholder: '引用',
53 | },
54 | ul: {
55 | toolbar: '箇条書きリスト',
56 | placeholder: '箇条書きリスト',
57 | },
58 | ol: {
59 | toolbar: '番号付きリスト',
60 | placeholder: '番号付きリスト',
61 | },
62 | table: {
63 | toolbar: 'テーブル挿入',
64 | },
65 | hr: {
66 | toolbar: '水平線',
67 | },
68 | link: {
69 | toolbar: 'リンク挿入',
70 | descPlaceholder: 'リンク',
71 | },
72 | image: {
73 | toolbar: '画像挿入',
74 | },
75 | imageLink: {
76 | toolbar: 'リンク挿入',
77 | },
78 | uploadImage: {
79 | toolbar: '画像アップロード',
80 | },
81 | code: {
82 | toolbar: 'コードブロック挿入',
83 | },
84 | save: {
85 | toolbar: '保存',
86 | },
87 | preview: {
88 | enabled: 'プレビューを有効にする',
89 | disabled: 'プレビューを無効にする',
90 | },
91 | toc: {
92 | title: 'ディレクトリナビゲーション',
93 | enabled: '目次を有効にする',
94 | disabled: '目次を無効にする',
95 | },
96 | syncScroll: {
97 | enabled: 'スクロール同期を有効にする',
98 | disabled: 'スクロール同期を無効にする',
99 | },
100 | fullscreen: {
101 | enabled: 'フルスクリーンモード(ESCキーで終了)',
102 | disabled: 'フルスクリーンを終了',
103 | },
104 | };
105 |
--------------------------------------------------------------------------------
/src/utils/xss/svg.js:
--------------------------------------------------------------------------------
1 | export const svgTagWhiteList = {
2 | svg: [],
3 | altGlyph: [],
4 | altGlyphDef: [],
5 | altGlyphItem: [],
6 | animate: [],
7 | animateColor: [],
8 | animateMotion: [],
9 | animateTransform: [],
10 | circle: [],
11 | clipPath: [],
12 | 'color-profile': [],
13 | cursor: [],
14 | 'definition-src': [],
15 | defs: [],
16 | desc: [],
17 | ellipse: [],
18 | feBlend: [],
19 | feColorMatrix: [],
20 | feComponentTransfer: [],
21 | feComposite: [],
22 | feConvolveMatrix: [],
23 | feDiffuseLighting: [],
24 | feDisplacementMap: [],
25 | feDistantLight: [],
26 | feFlood: [],
27 | feFuncA: [],
28 | feFuncB: [],
29 | feFuncG: [],
30 | feFuncR: [],
31 | feGaussianBlur: [],
32 | feImage: [],
33 | feMerge: [],
34 | feMergeNode: [],
35 | feMorphology: [],
36 | feOffset: [],
37 | fePointLight: [],
38 | feSpecularLighting: [],
39 | feSpotLight: [],
40 | feTile: [],
41 | feTurbulence: [],
42 | filter: [],
43 | font: [],
44 | foreignObject: [],
45 | g: [],
46 | glyph: [],
47 | glyphRef: [],
48 | hkern: [],
49 | image: [],
50 | line: [],
51 | linearGradient: [],
52 | marker: [],
53 | mask: [],
54 | metadata: [],
55 | 'missing-glyph': [],
56 | mpath: [],
57 | path: [],
58 | pattern: [],
59 | polygon: [],
60 | polyline: [],
61 | radialGradient: [],
62 | rect: [],
63 | set: [],
64 | stop: [],
65 | style: [],
66 | switch: [],
67 | symbol: [],
68 | text: [],
69 | textPath: [],
70 | title: [],
71 | tref: [],
72 | tspan: [],
73 | use: [],
74 | view: [],
75 | vkern: [],
76 | };
77 |
78 | export const svgAttrWhiteList = [
79 | 'width',
80 | 'height',
81 | 'x',
82 | 'y',
83 | 'rx',
84 | 'ry',
85 | 'cx',
86 | 'cy',
87 | 'r',
88 | 'viewbox',
89 | 'points',
90 | 'fill',
91 | 'stroke',
92 | 'stroke-width',
93 | 'opacity',
94 | 'transform',
95 | 'd',
96 | 'text-anchor',
97 | 'font-family',
98 | 'font-size',
99 | 'font-weight',
100 | 'filter',
101 | 'href',
102 | 'xlink:href',
103 | ];
104 |
--------------------------------------------------------------------------------
/src/utils/xss/index.js:
--------------------------------------------------------------------------------
1 | import xss from 'xss';
2 | import { svgTagWhiteList, svgAttrWhiteList } from './svg';
3 | import { katexTagWhiteList, katexAttrWhiteList } from './KaTex';
4 | import { attrWhiteList, prefixAttrWhiteList, commonWhiteList } from './common';
5 |
6 | const options = {
7 | whiteList: {
8 | ...xss.getDefaultWhiteList(),
9 | ...commonWhiteList,
10 | ...svgTagWhiteList,
11 | ...katexTagWhiteList,
12 | },
13 | onIgnoreTagAttr(tag, name, value) {
14 | if (
15 | (svgTagWhiteList[tag] && svgAttrWhiteList.includes(name)) ||
16 | (katexTagWhiteList[tag] && katexAttrWhiteList.includes(name)) ||
17 | attrWhiteList.some((attr) => attr === name) ||
18 | prefixAttrWhiteList.some((prefix) => name.startsWith(prefix))
19 | ) {
20 | return `${name}="${xss.escapeAttrValue(value)}"`;
21 | }
22 | },
23 | };
24 |
25 | const xssFilterInstance = new xss.FilterXSS(options);
26 |
27 | xssFilterInstance.extend = function (extendOptions) {
28 | const instanceOptions = xssFilterInstance.options;
29 |
30 | Object.keys(extendOptions).forEach((optionName) => {
31 | // extend whiteList
32 | if (optionName === 'whiteList') {
33 | Object.keys(extendOptions.whiteList).forEach((tagName) => {
34 | const tagAttrWhiteList = extendOptions.whiteList[tagName];
35 | const instanceWhiteList = instanceOptions.whiteList;
36 |
37 | if (instanceWhiteList[tagName]) {
38 | instanceWhiteList[tagName] = [...instanceWhiteList[tagName], ...tagAttrWhiteList];
39 | } else {
40 | instanceWhiteList[tagName] = tagAttrWhiteList;
41 | }
42 | });
43 | } else if (optionName === 'onIgnoreTagAttr') {
44 | const oldHandler = instanceOptions[optionName];
45 | instanceOptions[optionName] = function (...arg) {
46 | const oldReturnVal = oldHandler.call(this, ...arg);
47 | const newReturnVal = extendOptions[optionName].call(this, ...arg);
48 |
49 | return oldReturnVal || newReturnVal;
50 | };
51 | } else {
52 | instanceOptions[optionName] = extendOptions[optionName];
53 | }
54 | });
55 | };
56 |
57 | export default xssFilterInstance;
58 |
--------------------------------------------------------------------------------
/src/theme/base/base.js:
--------------------------------------------------------------------------------
1 | import markdownItAttr from 'markdown-it-attrs';
2 | import markdownItLineNumber from '@/utils/markdown-it-line-number';
3 | import markdownItHeadingTag from '@/utils/markdown-it-heading-tag';
4 | import markdownItTableOfContent from '@/utils/markdown-it-table-of-content';
5 | import markdownItPreWrapper from '@/utils/markdown-it-pre-wrapper';
6 | import markdownItLink from '@/utils/markdown-it-link';
7 | import { LINE_MARKUP, HEADING_MARKUP, ANCHOR_MARKUP } from '@/utils/constants/markup';
8 | import slugify from '@vuepress/shared-utils/lib/slugify';
9 |
10 | import markdownIt from '@/utils/markdown-it';
11 |
12 | export default function createBaseTheme({ toc, link, attrs } = {}) {
13 | const mdIt = markdownIt();
14 |
15 | mdIt
16 | .use(markdownItLink, {
17 | externalAttrs: {
18 | target: '_blank',
19 | },
20 | ...link,
21 | })
22 | .use(markdownItPreWrapper, {
23 | getWrapperClass(lang) {
24 | return `v-md-pre-wrapper v-md-pre-wrapper-${lang}`;
25 | },
26 | })
27 | .use(markdownItAttr, {
28 | leftDelimiter: '{{{',
29 | rightDelimiter: '}}}',
30 | ...attrs,
31 | allowedAttributes: ['width', 'height', ...attrs?.allowedAttributes],
32 | })
33 | .use(markdownItHeadingTag, {
34 | getMarks(title, level, unique) {
35 | return [
36 | {
37 | attr: HEADING_MARKUP,
38 | value: `${slugify(title)}${unique ? `-${unique}` : ''}`,
39 | },
40 | ];
41 | },
42 | })
43 | .use(markdownItTableOfContent, {
44 | listClass: 'v-md-toc',
45 | listItemClass: 'v-md-toc-item',
46 | getAnchorAttrs(title, level, unique) {
47 | return [
48 | {
49 | attr: ANCHOR_MARKUP,
50 | value: `${slugify(title)}${unique ? `-${unique}` : ''}`,
51 | },
52 | ];
53 | },
54 | ...toc,
55 | })
56 | .use(markdownItLineNumber, {
57 | lineMarkup: LINE_MARKUP,
58 | });
59 |
60 | return {
61 | previewClass: 'markdown-body',
62 | extend(callback) {
63 | callback(mdIt);
64 | },
65 | markdownParser: mdIt,
66 | };
67 | }
68 |
--------------------------------------------------------------------------------
/src/lang/en-US.js:
--------------------------------------------------------------------------------
1 | export default {
2 | undo: {
3 | toolbar: 'Undo',
4 | },
5 | redo: {
6 | toolbar: 'Redo',
7 | },
8 | clear: {
9 | toolbar: 'Clear',
10 | },
11 | h: {
12 | toolbar: 'Headings',
13 | },
14 | h1: {
15 | toolbar: 'Heading 1',
16 | placeholder: 'Heading 1',
17 | },
18 | h2: {
19 | toolbar: 'Heading 2',
20 | placeholder: 'Heading 2',
21 | },
22 | h3: {
23 | toolbar: 'Heading 3',
24 | placeholder: 'Heading 3',
25 | },
26 | h4: {
27 | toolbar: 'Heading 4',
28 | placeholder: 'Heading 4',
29 | },
30 | h5: {
31 | toolbar: 'Heading 5',
32 | placeholder: 'Heading 5',
33 | },
34 | h6: {
35 | toolbar: 'Heading 6',
36 | placeholder: 'Heading 6',
37 | },
38 | bold: {
39 | toolbar: 'Bold',
40 | placeholder: 'Bold',
41 | },
42 | italic: {
43 | toolbar: 'Italic',
44 | placeholder: 'Italic',
45 | },
46 | strikethrough: {
47 | toolbar: 'Strike',
48 | placeholder: 'Strike',
49 | },
50 | quote: {
51 | toolbar: 'Blockquote',
52 | placeholder: 'Blockquote',
53 | },
54 | ul: {
55 | toolbar: 'Unordered list',
56 | placeholder: 'Unordered list',
57 | },
58 | ol: {
59 | toolbar: 'Ordered list',
60 | placeholder: 'Ordered list',
61 | },
62 | table: {
63 | toolbar: 'Insert table',
64 | },
65 | hr: {
66 | toolbar: 'Line',
67 | },
68 | link: {
69 | toolbar: 'Insert link',
70 | descPlaceholder: 'Link',
71 | },
72 | image: {
73 | toolbar: 'Insert image',
74 | },
75 | imageLink: {
76 | toolbar: 'Insert link',
77 | },
78 | uploadImage: {
79 | toolbar: 'Upload Image',
80 | },
81 | code: {
82 | toolbar: 'Insert Codeblock',
83 | },
84 | save: {
85 | toolbar: 'Save',
86 | },
87 | preview: {
88 | enabled: 'Enable preview',
89 | disabled: 'Disable preview',
90 | },
91 | toc: {
92 | title: 'Directory navigation',
93 | enabled: 'Enable toc',
94 | disabled: 'Disable toc',
95 | },
96 | syncScroll: {
97 | enabled: 'Enable sync scroll ',
98 | disabled: 'Disable sync scroll',
99 | },
100 | fullscreen: {
101 | enabled: 'Full screen(Press ESC to exit)',
102 | disabled: 'Exit Full Screen',
103 | },
104 | };
105 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/extend-toolbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/lang/pt-BR.js:
--------------------------------------------------------------------------------
1 | export default {
2 | undo: {
3 | toolbar: 'Desfazer',
4 | },
5 | redo: {
6 | toolbar: 'Refazer',
7 | },
8 | clear: {
9 | toolbar: 'Limpar',
10 | },
11 | h: {
12 | toolbar: 'Títulos',
13 | },
14 | h1: {
15 | toolbar: 'Título 1',
16 | placeholder: 'Título 1',
17 | },
18 | h2: {
19 | toolbar: 'Título 2',
20 | placeholder: 'Título 2',
21 | },
22 | h3: {
23 | toolbar: 'Título 3',
24 | placeholder: 'Título 3',
25 | },
26 | h4: {
27 | toolbar: 'Título 4',
28 | placeholder: 'Título 4',
29 | },
30 | h5: {
31 | toolbar: 'Título 5',
32 | placeholder: 'Título 5',
33 | },
34 | h6: {
35 | toolbar: 'Título 6',
36 | placeholder: 'Título 6',
37 | },
38 | bold: {
39 | toolbar: 'Negrito',
40 | placeholder: 'Negrito',
41 | },
42 | italic: {
43 | toolbar: 'Itálico',
44 | placeholder: 'Itálico',
45 | },
46 | strikethrough: {
47 | toolbar: 'Riscar',
48 | placeholder: 'Riscar',
49 | },
50 | quote: {
51 | toolbar: 'Citação',
52 | placeholder: 'Citação',
53 | },
54 | ul: {
55 | toolbar: 'Lista não ordenada',
56 | placeholder: 'Lista não ordenada',
57 | },
58 | ol: {
59 | toolbar: 'Lista Ordenada',
60 | placeholder: 'Lista Ordenada',
61 | },
62 | table: {
63 | toolbar: 'Inserir tabela',
64 | },
65 | hr: {
66 | toolbar: 'Inserir linha',
67 | },
68 | link: {
69 | toolbar: 'Inserir link',
70 | descPlaceholder: 'Link',
71 | },
72 | image: {
73 | toolbar: 'Inserir imagem',
74 | },
75 | imageLink: {
76 | toolbar: 'Inserir link da imagem',
77 | },
78 | uploadImage: {
79 | toolbar: 'Enviar imagem',
80 | },
81 | code: {
82 | toolbar: 'Inserir bloco de código',
83 | },
84 | save: {
85 | toolbar: 'Salvar',
86 | },
87 | preview: {
88 | enabled: 'Ativar pré-visualização',
89 | disabled: 'Desativar pré-visualização',
90 | },
91 | toc: {
92 | title: 'Diretório de navegação',
93 | enabled: 'Ativar toc',
94 | disabled: 'Desativar toc',
95 | },
96 | syncScroll: {
97 | enabled: 'Ativar rolagem sincronizada',
98 | disabled: 'Desativar rolagem sincronizada',
99 | },
100 | fullscreen: {
101 | enabled: 'Tela cheia (ESC para sair)',
102 | disabled: 'Sair de tela cheia',
103 | },
104 | };
105 |
--------------------------------------------------------------------------------
/src/lang/pl-PL.js:
--------------------------------------------------------------------------------
1 | export default {
2 | undo: {
3 | toolbar: 'Cofnij',
4 | },
5 | redo: {
6 | toolbar: 'Powtórz',
7 | },
8 | clear: {
9 | toolbar: 'Wyczyść wszystko',
10 | },
11 | h: {
12 | toolbar: 'Nagłówki',
13 | },
14 | h1: {
15 | toolbar: 'Nagłówek 1',
16 | placeholder: 'Nagłówek 1',
17 | },
18 | h2: {
19 | toolbar: 'Nagłówek 2',
20 | placeholder: 'Nagłówek 2',
21 | },
22 | h3: {
23 | toolbar: 'Nagłówek 3',
24 | placeholder: 'Nagłówek 3',
25 | },
26 | h4: {
27 | toolbar: 'Nagłówek 4',
28 | placeholder: 'Nagłówek 4',
29 | },
30 | h5: {
31 | toolbar: 'Nagłówek 5',
32 | placeholder: 'Nagłówek 5',
33 | },
34 | h6: {
35 | toolbar: 'Nagłówek 6',
36 | placeholder: 'Nagłówek 6',
37 | },
38 | bold: {
39 | toolbar: 'Pogrubienie',
40 | placeholder: 'Pogrubienie',
41 | },
42 | italic: {
43 | toolbar: 'Kursywa',
44 | placeholder: 'Kursywa',
45 | },
46 | strikethrough: {
47 | toolbar: 'Przekreślenie',
48 | placeholder: 'Przekreślenie',
49 | },
50 | quote: {
51 | toolbar: 'Blok cytatu',
52 | placeholder: 'Blok cytatu',
53 | },
54 | ul: {
55 | toolbar: 'Lista nieuporządkowana',
56 | placeholder: 'Lista nieuporządkowana',
57 | },
58 | ol: {
59 | toolbar: 'Lista uporządkowana',
60 | placeholder: 'Lista uporządkowana',
61 | },
62 | table: {
63 | toolbar: 'Wstaw tabelę',
64 | },
65 | hr: {
66 | toolbar: 'Linia',
67 | },
68 | link: {
69 | toolbar: 'Wstaw link',
70 | descPlaceholder: 'Link',
71 | },
72 | image: {
73 | toolbar: 'Wstaw grafikę',
74 | },
75 | imageLink: {
76 | toolbar: 'Z linku',
77 | },
78 | uploadImage: {
79 | toolbar: 'Wyślij zdjęcie',
80 | },
81 | code: {
82 | toolbar: 'Wstaw blok kodu',
83 | },
84 | save: {
85 | toolbar: 'Zapisz',
86 | },
87 | preview: {
88 | enabled: 'Pokaż podgląd',
89 | disabled: 'Ukryj podgląd',
90 | },
91 | toc: {
92 | title: 'Spis treści',
93 | enabled: 'Pokaż spis treści',
94 | disabled: 'Ukryj spis treści',
95 | },
96 | syncScroll: {
97 | enabled: 'Synchronizuj przesunięcie',
98 | disabled: 'Nie synchronizuj przesunięcia',
99 | },
100 | fullscreen: {
101 | enabled: 'Pełny ekran (Wciśnij ESC, aby opuścić)',
102 | disabled: 'Opuść tryb pełnoekranowy',
103 | },
104 | };
105 |
--------------------------------------------------------------------------------
/src/lang/es-ES.js:
--------------------------------------------------------------------------------
1 | export default {
2 | undo: {
3 | toolbar: 'Deshacer',
4 | },
5 | redo: {
6 | toolbar: 'Rehacer',
7 | },
8 | clear: {
9 | toolbar: 'Borrar',
10 | },
11 | h: {
12 | toolbar: 'Encabezados',
13 | },
14 | h1: {
15 | toolbar: 'Titulo 1',
16 | placeholder: 'Título 1',
17 | },
18 | h2: {
19 | toolbar: 'Título 2',
20 | placeholder: 'Título 2',
21 | },
22 | h3: {
23 | toolbar: 'Título 3',
24 | placeholder: 'Título 3',
25 | },
26 | h4: {
27 | toolbar: 'Título 4',
28 | placeholder: 'Título 4',
29 | },
30 | h5: {
31 | toolbar: 'Título 5',
32 | placeholder: 'Título 5',
33 | },
34 | h6: {
35 | toolbar: 'Título 6',
36 | placeholder: 'Título 6',
37 | },
38 | bold: {
39 | toolbar: 'Negrita',
40 | placeholder: 'Negrita',
41 | },
42 | italic: {
43 | toolbar: 'Itálica',
44 | placeholder: 'Itálica',
45 | },
46 | strikethrough: {
47 | toolbar: 'Tachado',
48 | placeholder: 'Tachado',
49 | },
50 | quote: {
51 | toolbar: 'Blockquote',
52 | placeholder: 'Blockquote',
53 | },
54 | ul: {
55 | toolbar: 'Lista desordenada',
56 | placeholder: 'Lista desordenada',
57 | },
58 | ol: {
59 | toolbar: 'Lista ordenada',
60 | placeholder: 'Lista ordenada',
61 | },
62 | table: {
63 | toolbar: 'Insertar tabla',
64 | },
65 | hr: {
66 | toolbar: 'Línea',
67 | },
68 | link: {
69 | toolbar: 'Insertar enlace',
70 | descPlaceholder: 'Enlace',
71 | },
72 | image: {
73 | toolbar: 'Insertar imagen',
74 | },
75 | imageLink: {
76 | toolbar: 'Insertar enlace',
77 | },
78 | uploadImage: {
79 | toolbar: 'Subir Imagen',
80 | },
81 | code: {
82 | toolbar: 'Insertar Bloque de Código',
83 | },
84 | save: {
85 | toolbar: 'Guardar',
86 | },
87 | preview: {
88 | enabled: 'Habilitar vista previa',
89 | disabled: 'Deshabilitar vista previa',
90 | },
91 | toc: {
92 | title: 'Navegación de directorio',
93 | enabled: 'Habilitar Toc',
94 | disabled: 'Deshabilitar toc',
95 | },
96 | syncScroll: {
97 | enabled: 'Habilitar sincronización de desplazamiento',
98 | disabled: 'Deshabilitar sincronización de desplazamiento',
99 | },
100 | fullscreen: {
101 | enabled: 'Pantalla completa(Press ESC para salir)',
102 | disabled: 'Salir de pantalla completa',
103 | },
104 | };
105 |
--------------------------------------------------------------------------------
/src/lang/fr-FR.js:
--------------------------------------------------------------------------------
1 | export default {
2 | undo: {
3 | toolbar: 'Annuler',
4 | },
5 | redo: {
6 | toolbar: 'Restaurer',
7 | },
8 | clear: {
9 | toolbar: 'Vider',
10 | },
11 | h: {
12 | toolbar: 'Entêtes',
13 | },
14 | h1: {
15 | toolbar: 'Entête 1',
16 | placeholder: 'Entête 1',
17 | },
18 | h2: {
19 | toolbar: 'Entête 2',
20 | placeholder: 'Entête 2',
21 | },
22 | h3: {
23 | toolbar: 'Entête 3',
24 | placeholder: 'Entête 3',
25 | },
26 | h4: {
27 | toolbar: 'Entête 4',
28 | placeholder: 'Entête 4',
29 | },
30 | h5: {
31 | toolbar: 'Entête 5',
32 | placeholder: 'Entête 5',
33 | },
34 | h6: {
35 | toolbar: 'Entête 6',
36 | placeholder: 'Entête 6',
37 | },
38 | bold: {
39 | toolbar: 'Gras',
40 | placeholder: 'Gras',
41 | },
42 | italic: {
43 | toolbar: 'Italique',
44 | placeholder: 'Italique',
45 | },
46 | strikethrough: {
47 | toolbar: 'Barré',
48 | placeholder: 'Barré',
49 | },
50 | quote: {
51 | toolbar: 'Citation',
52 | placeholder: 'Citation',
53 | },
54 | ul: {
55 | toolbar: 'List désordonnée',
56 | placeholder: 'List désordonnée',
57 | },
58 | ol: {
59 | toolbar: 'Liste ordonnée',
60 | placeholder: 'Liste ordonnée',
61 | },
62 | table: {
63 | toolbar: 'Insérer un tavleau',
64 | },
65 | hr: {
66 | toolbar: 'Ligne',
67 | },
68 | link: {
69 | toolbar: 'Insérer un lien',
70 | descPlaceholder: 'Lien',
71 | },
72 | image: {
73 | toolbar: 'Insérer une image',
74 | },
75 | imageLink: {
76 | toolbar: 'Insérer un lien',
77 | },
78 | uploadImage: {
79 | toolbar: 'Téléverser une image',
80 | },
81 | code: {
82 | toolbar: 'Insérer un bloc de code',
83 | },
84 | save: {
85 | toolbar: 'Enregister',
86 | },
87 | preview: {
88 | enabled: 'Activer la prévisualisation',
89 | disabled: 'Désactiver la prévisualisation',
90 | },
91 | toc: {
92 | title: 'Table des matières',
93 | enabled: 'Activer la table des matières',
94 | disabled: 'Désactiver la table des matières',
95 | },
96 | syncScroll: {
97 | enabled: 'Activer le défilement synchronisé',
98 | disabled: 'Désactiver le défilement synchronisé',
99 | },
100 | fullscreen: {
101 | enabled: 'Plein écran (Pressez ESC pour quitter)',
102 | disabled: 'Quitter le plein écran',
103 | },
104 | };
105 |
--------------------------------------------------------------------------------
/src/mixins/preview.js:
--------------------------------------------------------------------------------
1 | import { getScrollTop } from '@/utils/scroll-top';
2 | import smoothScroll from '@/utils/smooth-scroll';
3 | import { LINE_MARKUP, HEADING_MARKUP, ANCHOR_MARKUP } from '@/utils/constants/markup';
4 |
5 | export default {
6 | props: {
7 | tabSize: {
8 | type: Number,
9 | default: 2,
10 | },
11 | scrollContainer: {
12 | type: Function,
13 | default: () => window,
14 | },
15 | top: {
16 | type: Number,
17 | default: 0,
18 | },
19 | },
20 | emits: ['image-click'],
21 | methods: {
22 | handlePreviewClick(e) {
23 | const { target } = e;
24 |
25 | // image preview
26 | if (target.tagName === 'IMG') {
27 | const src = target.getAttribute('src');
28 |
29 | if (!src) return;
30 |
31 | const imageEls = Array.from(this.$el.querySelectorAll('img'));
32 | const images = imageEls.map((el) => el.getAttribute('src')).filter((src) => src);
33 | const imagePreviewInitIndex = imageEls.indexOf(target);
34 |
35 | this.$emit('image-click', images, imagePreviewInitIndex);
36 |
37 | return;
38 | }
39 |
40 | const scrollToTargetId = target.getAttribute(ANCHOR_MARKUP);
41 | const scrollToTarget = this.$el.querySelector(`[${HEADING_MARKUP}="${scrollToTargetId}"]`);
42 |
43 | if (scrollToTarget) {
44 | this.scrollToTarget({
45 | target: scrollToTarget,
46 | });
47 | }
48 | },
49 | getOffsetTop(target, container) {
50 | const rect = target.getBoundingClientRect();
51 |
52 | if (container === window || container === document.documentElement) {
53 | return rect.top;
54 | }
55 |
56 | return rect.top - container.getBoundingClientRect().top;
57 | },
58 | scrollToTarget({
59 | target,
60 | scrollContainer = this.scrollContainer(),
61 | top = this.top,
62 | onScrollEnd,
63 | }) {
64 | const offsetTop = this.getOffsetTop(target, scrollContainer);
65 | const scrollTop = getScrollTop(scrollContainer) + offsetTop - top;
66 |
67 | smoothScroll({
68 | scrollTarget: scrollContainer,
69 | scrollToTop: scrollTop,
70 | onScrollEnd,
71 | });
72 | },
73 | scrollToLine({ lineIndex, onScrollEnd }) {
74 | if (lineIndex) {
75 | const target = this.$el.querySelector(`[${LINE_MARKUP}="${lineIndex}"]`);
76 |
77 | if (target) this.scrollToTarget({ target, onScrollEnd });
78 | }
79 | },
80 | },
81 | };
82 |
--------------------------------------------------------------------------------
/src/lang/de-DE.js:
--------------------------------------------------------------------------------
1 | export default {
2 | undo: {
3 | toolbar: 'Rückgängig',
4 | },
5 | redo: {
6 | toolbar: 'Wiederholen',
7 | },
8 | clear: {
9 | toolbar: 'Löschen',
10 | },
11 | h: {
12 | toolbar: 'Überschriften',
13 | },
14 | h1: {
15 | toolbar: 'Überschrift 1',
16 | placeholder: 'Überschrift 1',
17 | },
18 | h2: {
19 | toolbar: 'Überschrift 2',
20 | placeholder: 'Überschrift 2',
21 | },
22 | h3: {
23 | toolbar: 'Überschrift 3',
24 | placeholder: 'Überschrift 3',
25 | },
26 | h4: {
27 | toolbar: 'Überschrift 4',
28 | placeholder: 'Überschrift 4',
29 | },
30 | h5: {
31 | toolbar: 'Überschrift 5',
32 | placeholder: 'Überschrift 5',
33 | },
34 | h6: {
35 | toolbar: 'Überschrift 6',
36 | placeholder: 'Überschrift 6',
37 | },
38 | bold: {
39 | toolbar: 'Fett',
40 | placeholder: 'Fett',
41 | },
42 | italic: {
43 | toolbar: 'Kursiv',
44 | placeholder: 'Kursiv',
45 | },
46 | strikethrough: {
47 | toolbar: 'Strike',
48 | placeholder: 'Strike',
49 | },
50 | quote: {
51 | toolbar: 'Zitat',
52 | placeholder: 'Zitat',
53 | },
54 | ul: {
55 | toolbar: 'Ungeordnete Liste',
56 | placeholder: 'Ungeordnete Liste',
57 | },
58 | ol: {
59 | toolbar: 'Geordnete Liste',
60 | placeholder: 'Geordnete Liste',
61 | },
62 | table: {
63 | toolbar: 'Tabelle einfügen',
64 | },
65 | hr: {
66 | toolbar: 'Horizontale Linie',
67 | },
68 | link: {
69 | toolbar: 'Link einfügen',
70 | descPlaceholder: 'Link',
71 | },
72 | image: {
73 | toolbar: 'Bild einfügen',
74 | },
75 | imageLink: {
76 | toolbar: 'Bildlink einfügen',
77 | },
78 | uploadImage: {
79 | toolbar: 'Bild hochladen',
80 | },
81 | code: {
82 | toolbar: 'Codeblock einfügen',
83 | },
84 | save: {
85 | toolbar: 'Speichern',
86 | },
87 | preview: {
88 | enabled: 'Vorschau anzeigen',
89 | disabled: 'Vorschau ausblenden',
90 | },
91 | toc: {
92 | title: 'Inhaltsverzeichnis',
93 | enabled: 'Inhaltsverzeichnis anzeigen',
94 | disabled: 'Inhaltsverzeichnis ausblenden',
95 | },
96 | syncScroll: {
97 | enabled: 'Synchrones Scrollen aktivieren',
98 | disabled: 'Synchrones Scrollen deaktivieren',
99 | },
100 | fullscreen: {
101 | enabled: 'Vollbildschirm (ESC zum Verlassen drücken)',
102 | disabled: 'Vollbildschirm verlassen',
103 | },
104 | };
105 |
--------------------------------------------------------------------------------