插件改造

This commit is contained in:
purocean
2021-02-27 11:09:17 +08:00
parent 43060874f9
commit bfab83881e
28 changed files with 589 additions and 399 deletions
+1 -1
View File
@@ -16,7 +16,7 @@
"github-markdown-css": "^4.0.0",
"highlight.js": "^10.4.1",
"katex": "^0.6.0",
"loadsh": "^0.0.4",
"lodash": "^4.17.21",
"markdown-it": "^12.0.1",
"markdown-it-attrs": "^3.0.3",
"markdown-it-footnote": "^3.0.2",
+17 -83
View File
@@ -45,7 +45,6 @@ export default defineComponent({
const getEditor = () => refEditor.value
const revealLine = (line: number) => getEditor().revealLine(line)
const revealLineInCenter = (line: number) => getEditor().revealLineInCenter(line)
const switchTodo = (line: number, checked: boolean) => getEditor().switchTodo(line, checked)
const toUri = (file: any) => File.toUri(file)
async function inputPassword (title: string, filename: string) {
@@ -253,100 +252,35 @@ export default defineComponent({
const resize = () => nextTick(() => getEditor().resize())
const insert = (text?: string) => getEditor().insert(text)
const replaceValue = (value?: { search: string; replace: string }) => {
if (value) {
getEditor().replaceValue(value.search, value.replace)
}
}
const toggleWrap = () => getEditor().toggleWrap()
const editTableCell = async (params?: { start: number; end: number; cellIndex: number }) => {
if (!params) {
return
const replaceLine = (params?: { line: number; value: string }) => {
if (params) {
getEditor().replaceLine(params.line, params.value)
}
const { start, end, cellIndex } = params
if (end - start !== 1) {
toast.show('warning', '暂只支持编辑单行文本')
return
}
const escapedSplit = (str: string) => {
const result = []
const max = str.length
let pos = 0
let ch = str.charCodeAt(pos)
let isEscaped = false
let lastPos = 0
let current = ''
while (pos < max) {
if (ch === 0x7c/* | */) {
if (!isEscaped) {
// pipe separating cells, '|'
result.push(current + str.substring(lastPos, pos))
current = ''
lastPos = pos + 1
} else {
// escaped pipe, '\|'
current += str.substring(lastPos, pos - 1)
lastPos = pos
}
}
isEscaped = (ch === 0x5c/* \ */)
pos++
ch = str.charCodeAt(pos)
}
result.push(current + str.substring(lastPos))
if (result.length && result[0] === '') result.shift()
if (result.length && result[result.length - 1] === '') result.pop()
return result
}
const text = getEditor().getLineContent(start).trim()
const columns = escapedSplit(text)
const cellText = columns[cellIndex]
if (typeof cellText !== 'string') {
toast.show('warning', '编辑错误')
return
}
let value = await modal.input({
title: '编辑单元格',
type: 'textarea',
value: cellText,
modalWidth: '600px',
hint: '单元格内容',
})
if (typeof value !== 'string') {
toast.show('warning', '取消编辑')
return
}
if (!value.startsWith(' ') && cellIndex > 0) value = ' ' + value
if (!value.endsWith(' ') && cellIndex < columns.length - 1) value += ' '
columns[cellIndex] = value.replace(/\|/g, '\\|').replace(/\n/g, ' ')
let content = columns.join('|').trim()
if (text.startsWith('|')) content = '| ' + content
if (text.endsWith('|')) content += ' |'
getEditor().replaceLine(start, content)
}
const getLine = (params?: {line: number; callback?: (value: string) => void}) => {
if (params) {
const { line, callback } = params
callback && callback(getEditor().getLineContent(line))
}
}
const toggleWrap = () => getEditor().toggleWrap()
onMounted(() => {
bus.on('resize', resize)
bus.on('editor-insert-value', insert)
bus.on('editor-replace-value', replaceValue)
bus.on('editor-get-line', getLine)
bus.on('editor-replace-line', replaceLine)
bus.on('editor-toggle-wrap', toggleWrap)
bus.on('editor-edit-table-cell', editTableCell)
bus.on('file-new', createFile as any)
restartTimer()
})
@@ -355,16 +289,16 @@ export default defineComponent({
bus.off('resize', resize)
bus.off('editor-insert-value', insert)
bus.off('editor-replace-value', replaceValue)
bus.off('editor-get-line', getLine)
bus.off('editor-replace-line', replaceLine)
bus.off('editor-toggle-wrap', toggleWrap)
bus.off('file-new', createFile as any)
bus.off('editor-edit-table-cell', editTableCell)
})
return {
refEditor,
revealLine,
revealLineInCenter,
switchTodo,
setCurrentValue,
syncScrollView,
pasteImg,
-11
View File
@@ -204,16 +204,6 @@ export default defineComponent({
return getEditor().getModel()!!.getLineContent(line)
}
function switchTodo (line: number, checked: boolean) {
if (checked) {
const value = getLineContent(line).replace('[ ]', `[x] ~~${dayjs().format('YYYY-MM-DD HH:mm')}~~`)
replaceLine(line, value)
} else {
const value = getLineContent(line).replace(/(\[x\] ~~[\d-: ]+~~|\[x\])/, '[ ]')
replaceLine(line, value)
}
}
function getValue () {
return getEditor().getModel()!!.getValue(getMonaco().editor.DefaultEndOfLine.LF)
}
@@ -323,7 +313,6 @@ export default defineComponent({
revealLine,
revealLineInCenter,
setScrollToTop,
switchTodo,
replaceValue,
replaceLine,
toggleWrap,
+17 -206
View File
@@ -38,7 +38,7 @@
</template>
<script lang="ts">
import loadsh from 'lodash'
import lodash from 'lodash'
import { useStore } from 'vuex'
import { computed, defineComponent, nextTick, onBeforeUnmount, onMounted, ref, toRefs, watch } from 'vue'
import Markdown from 'markdown-it'
@@ -49,30 +49,13 @@ import MultimdTable from 'markdown-it-multimd-table'
import Footnote from 'markdown-it-footnote'
import Highlight from 'highlight.js'
import HighlightLineNumber from '../plugins/HightLightNumberPlugin'
import MarkdownItToc from '../plugins/TocPlugin'
import MarkdownItECharts from '../plugins/EChartsPlugin'
import RunPlugin from '../plugins/RunPlugin'
import AppletPlugin from '../plugins/AppletPlugin'
import SourceLinePlugin from '../plugins/SourceLinePlugin'
import LinkTargetPlugin from '../plugins/LinkTargetPlugin'
import DrawioPlugin from '../plugins/DrawioPlugin'
import PlantumlPlugin from '../plugins/PlantumlPlugin'
import MermaidPlugin from '../plugins/MermaidPlugin'
import CodePlugin from '../plugins/CodePlugin'
import MindMapPlugin from '../plugins/MindMapPlugin'
import file from '../useful/file'
import env from '../useful/env'
import { encodeMarkdownLink } from '../useful/utils'
import { triggerHook } from '../useful/plugin'
import { getMarkdownItPlugins, triggerHook } from '../useful/plugin'
import { useBus } from '../useful/bus'
import 'github-markdown-css/github-markdown.css'
import 'highlight.js/styles/atom-one-dark.css'
import 'katex/dist/katex.min.css'
import { useBus } from '../useful/bus'
import { hasCtrlCmd } from '../useful/shortcut'
HighlightLineNumber.addStyles()
const markdown = Markdown({
linkify: true,
@@ -89,21 +72,14 @@ const markdown = Markdown({
}
})
.use(TaskLists, { enabled: true })
.use(PlantumlPlugin)
.use(RunPlugin)
.use(AppletPlugin)
.use(katex)
.use(SourceLinePlugin)
.use(MarkdownItAttrs)
.use(LinkTargetPlugin)
.use(Footnote)
.use(DrawioPlugin)
.use(MultimdTable, { multiline: true })
.use(MarkdownItToc)
.use(MarkdownItECharts)
.use(MermaidPlugin)
.use(CodePlugin)
.use(MindMapPlugin)
getMarkdownItPlugins().forEach(({ plugin, params }) => {
markdown.use(plugin, params)
})
export default defineComponent({
name: 'xview',
@@ -120,6 +96,8 @@ export default defineComponent({
const refView = ref<HTMLElement | null>(null)
const refConvertForm = ref<HTMLFormElement | null>(null)
const getViewDom = () => refView.value
const width = ref(1024)
const height = ref(1024)
const heads = ref<{
@@ -138,35 +116,6 @@ export default defineComponent({
height.value = refViewWrapper.value!!.clientHeight
}
function updatePlantuml () {
const nodes = refView.value!!.querySelectorAll<HTMLImageElement>('img[data-plantuml-src]')
nodes.forEach(el => {
el.src = el.dataset.plantumlSrc || ''
})
}
function runAppletScript () {
const nodes = refView.value!!.querySelectorAll<HTMLElement>('.applet[data-code]')
nodes.forEach(AppletPlugin.runScript)
}
function renderMindMap () {
const nodes = refView.value!!.querySelectorAll<HTMLElement>('.mindmap-list[data-mindmap-source]')
nodes.forEach(MindMapPlugin.render)
}
const updatePlantumlDebounce = loadsh.debounce(() => {
updatePlantuml()
}, 3000, { leading: true })
const runAppletScriptDebounce = loadsh.debounce(() => {
runAppletScript()
}, 1000, { leading: true })
const renderMindmapDebounce = loadsh.debounce(() => {
renderMindMap()
}, 1000, { leading: true })
function replaceRelativeLink (md: string) {
if (!fileRepo.value) {
return md
@@ -208,22 +157,6 @@ export default defineComponent({
})
}
function initDrawio () {
if (!refView.value) {
return
}
const nodes = refView.value!!.querySelectorAll<HTMLIFrameElement>('.drawio[data-url]')
nodes.forEach(el => {
const url = el.dataset.url
if (url) {
DrawioPlugin.load(el, url)
} else {
DrawioPlugin.load(el)
}
})
}
function updateOutline () {
const tags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
const nodes = refView.value!!.querySelectorAll<HTMLHeadElement>(tags.join(','))
@@ -242,14 +175,7 @@ export default defineComponent({
todoDoneCount.value = refView.value!!.querySelectorAll('input[type=checkbox][checked]').length
}
function hightCode () {
const nodes = refView.value!!.querySelectorAll<HTMLImageElement>('code[class^="language-"]')
nodes.forEach(el => {
HighlightLineNumber.lineNumbersBlock(el)
})
}
const render = loadsh.debounce(() => {
const render = lodash.debounce(() => {
// 编辑非 markdown 文件预览直接显示代码
const content = (filePath.value || '').endsWith('.md')
? currentContent.value
@@ -257,23 +183,15 @@ export default defineComponent({
const source = replaceRelativeLink(content)
refView.value!!.innerHTML = markdown.render(source, { source })
runAppletScriptDebounce()
renderMindmapDebounce()
initDrawio()
updateOutline()
updateTodoCount()
updatePlantumlDebounce()
hightCode()
MarkdownItECharts.update()
MermaidPlugin.update()
// 渲染完成后触发渲染完成事件
nextTick(() => bus.emit('preview-rendered'))
triggerHook('ON_VIEW_RENDER', { getViewDom })
nextTick(() => triggerHook('ON_VIEW_RENDERED', { getViewDom }))
}, 500, { leading: true })
async function keydownHandler (e: KeyboardEvent) {
const getRefview = () => refView.value
triggerHook('ON_VIEW_KEY_DOWN', e, getRefview())
triggerHook('ON_VIEW_KEY_DOWN', e, getViewDom())
}
function handleScroll (e: any) {
@@ -284,10 +202,6 @@ export default defineComponent({
emit('sync-scroll', line)
}
function switchTodo (line: number, checked: boolean) {
emit('switch-todo', line, checked)
}
function scrollToTop () {
refViewWrapper.value!!.scrollTo(0, 0)
syncScroll(1)
@@ -298,103 +212,6 @@ export default defineComponent({
const target = e.target as HTMLElement
const preventEvent = () => {
e.preventDefault()
e.stopPropagation()
}
const handleLink = (link: HTMLAnchorElement) => {
// 系统中打开附件
if (link.classList.contains('open')) {
fetch(link.href.replace('api/attachment', 'api/open'))
return preventEvent()
}
const href = link.getAttribute('href') || ''
if (/^(http:|https:|ftp:)\/\//i.test(href)) { // 处理外链
// Electron 中打开外链
if (env.isElectron) {
env.require && env.require('opn')(link.href)
preventEvent()
}
} else { // 处理相对链接
if (/(\.md$|\.md#)/.test(href)) { // 处理打开相对 md 文件
const tmp = decodeURI(href).split('#')
let path = tmp[0]
if (path.startsWith('.')) { // 将相对路径转换为绝对路径
path = file.dirname(filePath.value || '') + path.replace('.', '')
}
// 打开文件
store.commit('setCurrentFile', {
path,
name: file.basename(path),
repo: fileRepo.value,
type: 'file'
})
// 跳转锚点
const hash = tmp.slice(1).join('#')
if (hash) {
bus.once('preview-rendered', () => {
const el = document.getElementById(hash) ||
document.getElementById(encodeURIComponent(hash))
if (el) {
// 如果是标题的话,也顺便将编辑器滚动到可视区域
if (hash.startsWith('h-')) {
el.click()
}
el.scrollIntoView()
}
})
}
preventEvent()
} else if (href && href.startsWith('#')) { // 处理 TOC 跳转
const el = document.getElementById(href.replace(/^#/, ''))
if (el) {
el.scrollIntoView()
}
preventEvent()
}
}
}
if (target.tagName === 'A') {
handleLink(target as HTMLAnchorElement)
}
// 复制标题链接
if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].indexOf(target.tagName) > -1 && target.id && hasCtrlCmd(e)) {
bus.emit('copy-text', encodeMarkdownLink(filePath.value) + '#' + encodeMarkdownLink(decodeURIComponent(target.id)))
return preventEvent()
}
// 复制内容
if (target.classList.contains('copy-inner-text')) {
bus.emit('copy-text', target.innerText)
}
if (target.tagName === 'INPUT' && target.parentElement!!.classList.contains('source-line')) {
switchTodo(parseInt(target.parentElement!!.dataset.sourceLine || '0'), (target as HTMLInputElement).checked)
e.preventDefault()
e.stopPropagation()
return
}
if (target.tagName === 'TD' && target.classList.contains('yank-td') && hasCtrlCmd(e)) {
const start = parseInt(target.dataset.sourceLine || '0')
const end = parseInt(target.dataset.sourceLineEnd || '0')
const td = target as HTMLTableDataCellElement
const cellIndex = [...td.parentElement!.children as any]
.slice(0, td.cellIndex)
.reduce((prev, current) => prev + current.colSpan, 0)
bus.emit('editor-edit-table-cell', { start, end, cellIndex })
}
if (target.classList.contains('source-line') && window.getSelection()!!.toString().length < 1) {
syncScroll(parseInt(target.dataset.sourceLine || '0'))
e.stopPropagation()
@@ -419,7 +236,7 @@ export default defineComponent({
}
function convertFile (type: string) {
MarkdownItECharts.preparePrint()
triggerHook('ON_VIEW_BEFORE_CONVERT', { getViewDom })
let baseUrl = location.origin + location.pathname.substring(0, location.pathname.lastIndexOf('/')) + '/'
@@ -433,7 +250,6 @@ export default defineComponent({
html: refView.value!!.outerHTML.replace(/src="api/g, `src="${baseUrl}api`),
type
}
MarkdownItECharts.update()
setTimeout(() => {
refConvertForm.value!!.submit()
}, 300)
@@ -444,11 +260,8 @@ export default defineComponent({
}
onMounted(() => {
render()
setTimeout(() => {
updatePlantuml()
}, 100)
nextTick(render)
triggerHook('ON_VIEW_MOUNTED', { getViewDom })
window.addEventListener('keydown', keydownHandler, true)
bus.on('resize', resizeHandler)
bus.on('view-rerender', render)
@@ -465,9 +278,7 @@ export default defineComponent({
watch(filePath, () => {
// 切换文件后,开启自动预览
store.commit('setAutoPreview', true)
setTimeout(() => {
updatePlantuml()
}, 100)
triggerHook('ON_VIEW_FILE_CHANGE', { getViewDom })
})
return {
+4 -6
View File
@@ -38,10 +38,10 @@ import { useToast } from '../useful/toast'
import { useModal } from '../useful/modal'
import { useContextMenu } from '../useful/context-menu'
import File from '../useful/file'
import { Components } from '../types'
import Extensions from '../useful/extensions'
import { triggerHook } from '../useful/plugin'
import { Components } from '../types'
import SvgIcon from './SvgIcon.vue'
import DrawioPlugin from '../plugins/DrawioPlugin'
export default defineComponent({
name: 'tree-node',
@@ -139,14 +139,12 @@ export default defineComponent({
}
}
function select (item: any) {
async function select (item: any) {
if (item.type !== 'dir') {
if (Extensions.supported(item.name)) {
store.commit('setCurrentFile', item)
} else {
if (item.path.toLowerCase().endsWith('.drawio')) {
DrawioPlugin.open(item)
} else {
if (!(await triggerHook('ON_TREE_NODE_SELECT', item))) {
File.openInOS(props.item)
}
}
-15
View File
@@ -1,15 +0,0 @@
import { getKeyLabel } from '@/useful/shortcut'
import Markdown from 'markdown-it'
export default (md: Markdown) => {
const Fun = (fn: Function) => (tokens: any, idx: any, options: any, env: any, slf: any) => {
if (tokens[idx].attrIndex('title') < 0) {
tokens[idx].attrJoin('class', 'copy-inner-text')
tokens[idx].attrPush(['title', getKeyLabel('CtrlCmd') + ' + 单击复制'])
}
return (fn)(tokens, idx, options, env, slf)
}
md.renderer.rules.code_inline = Fun(md.renderer.rules.code_inline!!.bind(md.renderer.rules))
}
-13
View File
@@ -1,13 +0,0 @@
import Markdown from 'markdown-it'
const LinkTargetPlugin = (md: Markdown) => {
md.renderer.rules.link_open = (tokens, idx, options, _, slf) => {
if (tokens[idx].attrIndex('target') < 0) {
tokens[idx].attrPush(['target', '_blank'])
}
return slf.renderToken(tokens, idx, options)
}
}
export default LinkTargetPlugin
@@ -1,7 +1,9 @@
import CryptoJS from 'crypto-js'
import lodash from 'lodash'
import dayjs from 'dayjs'
import Markdown from 'markdown-it'
import { useBus } from '@/useful/bus'
import { Plugin } from '@/useful/plugin'
const RunPlugin = (md: Markdown) => {
const temp = md.renderer.rules.fence!!.bind(md.renderer.rules)
@@ -33,7 +35,7 @@ const RunPlugin = (md: Markdown) => {
}
}
RunPlugin.runScript = (el: HTMLElement) => {
const runScript = (el: HTMLElement) => {
const appletId = el.id
const appletCode = el.dataset.code
if (!appletCode) {
@@ -44,7 +46,7 @@ RunPlugin.runScript = (el: HTMLElement) => {
iframe.srcdoc = `
<style>
::selection {
background: #d3d3d3;
background: #515455;
}
::-webkit-scrollbar {
@@ -140,4 +142,17 @@ RunPlugin.runScript = (el: HTMLElement) => {
el.appendChild(iframe)
}
export default RunPlugin
export default {
name: 'applet',
register: ctx => {
ctx.registerMarkdownItPlugin(RunPlugin)
function runAppletScript ({ getViewDom }: any) {
const refView: HTMLElement = getViewDom()
const nodes = refView.querySelectorAll<HTMLElement>('.applet[data-code]')
nodes.forEach(runScript)
}
ctx.registerHook('ON_VIEW_RENDER', lodash.debounce(runAppletScript, 1000, { leading: true }))
}
} as Plugin
@@ -2,6 +2,8 @@
// https://github.com/wcoder/highlightjs-line-numbers.js
import { Plugin } from '@/useful/plugin'
const TABLE_NAME = 'hljs-ln'
const LINE_NAME = 'hljs-ln-line'
const CODE_BLOCK_NAME = 'hljs-ln-code'
@@ -149,4 +151,14 @@ function format (format: string, args: any) {
})
}
export default { addStyles, lineNumbersBlock }
export default {
name: 'markdown-toc',
register: ctx => {
ctx.registerHook('ON_STARTUP', addStyles)
ctx.registerHook('ON_VIEW_RENDER', ({ getViewDom }) => {
const refView: HTMLElement = getViewDom()
const nodes = refView.querySelectorAll<HTMLImageElement>('code[class^="language-"]')
nodes.forEach(lineNumbersBlock)
})
}
} as Plugin
+42
View File
@@ -0,0 +1,42 @@
import { Plugin, Ctx } from '@/useful/plugin'
import { hasCtrlCmd } from '@/useful/shortcut'
import { encodeMarkdownLink } from '@/useful/utils'
import { useBus } from '@/useful/bus'
import { copyText } from '@/useful/copy-text'
import store from '@/store'
export default {
name: 'copy-text',
register: (ctx: Ctx) => {
ctx.registerHook('ON_STARTUP', () => {
const bus = useBus()
bus.on('copy-text', copyText)
})
ctx.registerHook('ON_VIEW_ELEMENT_CLICK', async (e: MouseEvent) => {
const target = e.target as HTMLElement
const bus = useBus()
const preventEvent = () => {
e.preventDefault()
e.stopPropagation()
return true
}
// 复制标题链接
if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].indexOf(target.tagName) > -1 && target.id && hasCtrlCmd(e)) {
const { currentFile } = store.state
bus.emit('copy-text', encodeMarkdownLink(currentFile.path) + '#' + encodeMarkdownLink(decodeURIComponent(target.id)))
return preventEvent()
}
// 复制内容
if (target.classList.contains('copy-inner-text')) {
bus.emit('copy-text', target.innerText)
return preventEvent()
}
return false
})
}
} as Plugin
@@ -1,9 +1,10 @@
import Markdown from 'markdown-it'
import { Plugin } from '@/useful/plugin'
import file from '@/useful/file'
import { useBus } from '@/useful/bus'
import { openInNewWindow } from '@/useful/utils'
const Plugin = (md: Markdown) => {
const MarkdownItPlugin = (md: Markdown) => {
const renderHtml = ({ url, content }: any) => {
const iframe = document.createElement('iframe')
iframe.className = 'drawio'
@@ -119,7 +120,7 @@ const buildSrcdoc = async ({ repo, path, content, url }: F) => {
`
}
Plugin.load = async (el: HTMLIFrameElement, url?: string) => {
const load = async (el: HTMLIFrameElement, url?: string) => {
const bus = useBus()
const content = el.dataset.content || ''
@@ -154,9 +155,33 @@ Plugin.load = async (el: HTMLIFrameElement, url?: string) => {
el.parentElement!!.appendChild(action)
}
Plugin.open = async (file: F) => {
const srcdoc = await buildSrcdoc(file)
openInNewWindow(srcdoc)
}
export default {
name: 'drawio',
register: ctx => {
ctx.registerMarkdownItPlugin(MarkdownItPlugin)
export default Plugin
ctx.registerHook('ON_VIEW_RENDER', ({ getViewDom }) => {
const refView: HTMLElement = getViewDom()
const nodes = refView.querySelectorAll<HTMLIFrameElement>('.drawio[data-url]')
nodes.forEach(el => {
const url = el.dataset.url
if (url) {
load(el, url)
} else {
load(el)
}
})
})
ctx.registerHook('ON_TREE_NODE_SELECT', async (item: any) => {
if (item.path.toLowerCase().endsWith('.drawio')) {
const srcdoc = await buildSrcdoc(item)
openInNewWindow(srcdoc)
return true
}
return false
})
}
} as Plugin
@@ -1,6 +1,7 @@
import echarts from 'echarts'
import CryptoJS from 'crypto-js'
import Markdown from 'markdown-it'
import { Plugin } from '@/useful/plugin'
const renderHtml = (code: string) => {
const id = `echart-${CryptoJS.MD5(code).toString()}-${Math.random().toString(36).substr(2)}`
@@ -24,7 +25,7 @@ const EChartsPlugin = (md: Markdown) => {
let charts: {[key: string]: any} = {}
EChartsPlugin.update = (theme = 'dark', animation: boolean | undefined = undefined, renderImg = false) => {
const update = (theme = 'dark', animation: boolean | undefined = undefined, renderImg = false) => {
Object.values(charts).forEach(x => x.chart.dispose())
charts = {}
@@ -60,8 +61,8 @@ EChartsPlugin.update = (theme = 'dark', animation: boolean | undefined = undefin
}
}
EChartsPlugin.preparePrint = () => {
EChartsPlugin.update('light', false, true)
const preparePrint = () => {
update('light', false, true)
for (const ele of document.getElementsByClassName('echarts')) {
while (ele.firstChild) {
@@ -81,7 +82,19 @@ window.addEventListener('resize', () => {
})
window.addEventListener('beforeprint', () => {
EChartsPlugin.update('light', false)
update('light', false)
})
export default EChartsPlugin
export default {
name: 'markdown-toc',
register: ctx => {
ctx.registerMarkdownItPlugin(EChartsPlugin)
ctx.registerHook('ON_VIEW_RENDER', () => {
update()
})
ctx.registerHook('ON_VIEW_BEFORE_CONVERT', () => {
preparePrint() // 转换成图片形式,设置主题
setTimeout(() => update(), 0) // 恢复
})
}
} as Plugin
+20
View File
@@ -0,0 +1,20 @@
import { getKeyLabel } from '@/useful/shortcut'
import { Plugin, Ctx } from '@/useful/plugin'
export default {
name: 'markdown-code',
register: (ctx: Ctx) => {
ctx.registerMarkdownItPlugin(md => {
const Fun = (fn: Function) => (tokens: any, idx: any, options: any, env: any, slf: any) => {
if (tokens[idx].attrIndex('title') < 0) {
tokens[idx].attrJoin('class', 'copy-inner-text')
tokens[idx].attrPush(['title', getKeyLabel('CtrlCmd') + ' + 单击复制'])
}
return (fn)(tokens, idx, options, env, slf)
}
md.renderer.rules.code_inline = Fun(md.renderer.rules.code_inline!!.bind(md.renderer.rules))
})
}
} as Plugin
+104
View File
@@ -0,0 +1,104 @@
import { Plugin, Ctx } from '@/useful/plugin'
import store from '@/store'
import env from '@/useful/env'
import file from '@/useful/file'
const handleLink = (link: HTMLAnchorElement, ctx: Ctx) => {
const { currentFile } = store.state
const { repo: fileRepo, path: filePath } = currentFile
// 系统中打开附件
if (link.classList.contains('open')) {
fetch(link.href.replace('api/attachment', 'api/open'))
return true
}
const href = link.getAttribute('href') || ''
if (/^(http:|https:|ftp:)\/\//i.test(href)) { // 处理外链
// Electron 中打开外链
if (env.isElectron) {
env.require && env.require('opn')(link.href)
return true
}
} else { // 处理相对链接
if (/(\.md$|\.md#)/.test(href)) { // 处理打开相对 md 文件
const tmp = decodeURI(href).split('#')
let path = tmp[0]
if (path.startsWith('.')) { // 将相对路径转换为绝对路径
path = file.dirname(filePath || '') + path.replace('.', '')
}
// 打开文件
store.commit('setCurrentFile', {
path,
name: file.basename(path),
repo: fileRepo,
type: 'file'
})
// 跳转锚点
const hash = tmp.slice(1).join('#')
if (hash) {
ctx.registerHook('ON_VIEW_RENDERED', () => {
const el = document.getElementById(hash) ||
document.getElementById(encodeURIComponent(hash))
if (el) {
// 如果是标题的话,也顺便将编辑器滚动到可视区域
if (hash.startsWith('h-')) {
el.click()
}
el.scrollIntoView()
}
}, true)
}
return true
} else if (href && href.startsWith('#')) { // 处理 TOC 跳转
const el = document.getElementById(href.replace(/^#/, ''))
if (el) {
el.scrollIntoView()
}
return true
}
return false
}
}
export default {
name: 'markdown-link',
register: (ctx: Ctx) => {
ctx.registerHook('ON_VIEW_ELEMENT_CLICK', async (e: MouseEvent) => {
const target = e.target as HTMLElement
const preventEvent = () => {
e.preventDefault()
e.stopPropagation()
return true
}
if (target.tagName === 'A' || target.parentElement?.tagName === 'A') {
if (handleLink(target as HTMLAnchorElement, ctx)) {
return preventEvent()
} else {
return true
}
}
return false
})
ctx.registerMarkdownItPlugin(md => {
md.renderer.rules.link_open = (tokens, idx, options, _, slf) => {
if (tokens[idx].attrIndex('target') < 0) {
tokens[idx].attrPush(['target', '_blank'])
}
return slf.renderToken(tokens, idx, options)
}
})
}
} as Plugin
@@ -1,4 +1,4 @@
import Markdown from 'markdown-it'
import { Plugin } from '@/useful/plugin'
import Renderer from 'markdown-it/lib/renderer'
const buildFunction = (className: string): Renderer.RenderRule => {
@@ -16,12 +16,15 @@ const buildFunction = (className: string): Renderer.RenderRule => {
export const injectLineNumbers = buildFunction('source-line')
const SourceLinePlugin = (md: Markdown) => {
md.renderer.rules.paragraph_open = injectLineNumbers
md.renderer.rules.heading_open = injectLineNumbers
md.renderer.rules.list_item_open = injectLineNumbers
md.renderer.rules.table_open = injectLineNumbers
md.renderer.rules.td_open = buildFunction('yank-td')
}
export default SourceLinePlugin
export default {
name: 'markdown-source-line',
register: ctx => {
ctx.registerMarkdownItPlugin(md => {
md.renderer.rules.paragraph_open = injectLineNumbers
md.renderer.rules.heading_open = injectLineNumbers
md.renderer.rules.list_item_open = injectLineNumbers
md.renderer.rules.table_open = injectLineNumbers
md.renderer.rules.td_open = buildFunction('yank-td')
})
}
} as Plugin
@@ -2,8 +2,9 @@
import Markdown from 'markdown-it'
import { getKeyLabel } from '@/useful/shortcut'
import { Plugin } from '@/useful/plugin'
import { injectLineNumbers } from './SourceLinePlugin'
import { injectLineNumbers } from './markdown-source-line'
const slugify = (s: string) => 'h-' + encodeURIComponent(String(s).trim().toLowerCase().replace(/\s+/g, '-'))
@@ -19,7 +20,7 @@ const defaults = {
containerFooterHtml: undefined
}
export default (md: Markdown, o: any) => {
const MarkdownItPlugin = (md: Markdown, o: any) => {
const options = Object.assign({}, defaults, o)
const tocRegexp = options.markerPattern
let gstate: any
@@ -201,3 +202,10 @@ export default (md: Markdown, o: any) => {
// Insert TOC
md.inline.ruler.after('emphasis', 'toc', toc)
}
export default {
name: 'markdown-toc',
register: ctx => {
ctx.registerMarkdownItPlugin(MarkdownItPlugin)
}
} as Plugin
@@ -1,5 +1,6 @@
import Markdown from 'markdown-it'
import mermaid from 'mermaid'
import { Plugin } from '@/useful/plugin'
const mermaidChart = (code: string) => {
try {
@@ -28,8 +29,10 @@ const MermaidPlugin = (md: Markdown) => {
}
}
MermaidPlugin.update = () => {
mermaid.init('.mermaid')
}
export default MermaidPlugin
export default {
name: 'mermaid',
register: ctx => {
ctx.registerMarkdownItPlugin(MermaidPlugin)
ctx.registerHook('ON_VIEW_RENDER', () => mermaid.init('.mermaid'))
}
} as Plugin
@@ -1,5 +1,6 @@
import Markdown from 'markdown-it'
import lodash from 'lodash'
import Renderer from 'markdown-it/lib/renderer'
import { Plugin } from '@/useful/plugin'
import crypto from '@/useful/crypto'
import { dataURItoBlobLink, openInNewWindow } from '@/useful/utils'
@@ -21,10 +22,6 @@ const renderRule: Renderer.RenderRule = (tokens, idx, options, { source }, slf)
return slf.renderToken(tokens, idx, options)
}
const Plugin = (md: Markdown) => {
md.renderer.rules.bullet_list_open = renderRule
}
const buildSrcdoc = (json: string, btns: string) => {
return `
<html>
@@ -87,7 +84,7 @@ const buildSrcdoc = (json: string, btns: string) => {
`
}
Plugin.render = async (ele: HTMLElement) => {
const render = async (ele: HTMLElement) => {
const code = (ele.dataset.mindmapSource || '').trim()
const div = document.createElement('div')
@@ -189,4 +186,19 @@ Plugin.render = async (ele: HTMLElement) => {
div.appendChild(action)
}
export default Plugin
export default {
name: 'mind-map',
register: ctx => {
ctx.registerMarkdownItPlugin(md => {
md.renderer.rules.bullet_list_open = renderRule
})
function renderMindMap ({ getViewDom }: any) {
const refView: HTMLElement = getViewDom()
const nodes = refView.querySelectorAll<HTMLElement>('.mindmap-list[data-mindmap-source]')
nodes.forEach(render)
}
ctx.registerHook('ON_VIEW_RENDER', lodash.debounce(renderMindMap, 1000, { leading: true }))
}
} as Plugin
@@ -1,7 +1,9 @@
// https://github.com/DeepElement/markdown-it-plantuml-offline
import lodash from 'lodash'
import Markdown from 'markdown-it'
import { Plugin } from '@/useful/plugin'
export default function umlPlugin (md: Markdown, options: any) {
const MarkdownItPlugin = function umlPlugin (md: Markdown, options: any) {
function generateSourceDefault (umlCode: string) {
return 'api/plantuml/png?data=' + encodeURIComponent(umlCode)
}
@@ -136,3 +138,30 @@ export default function umlPlugin (md: Markdown, options: any) {
})
md.renderer.rules.uml_diagram = render
}
export default {
name: 'markdown-plantuml',
register: ctx => {
ctx.registerMarkdownItPlugin(MarkdownItPlugin)
const updatePlantuml = ({ getViewDom }: any) => {
const refView: HTMLElement = getViewDom()
const nodes = refView.querySelectorAll<HTMLImageElement>('img[data-plantuml-src]')
nodes.forEach(el => {
el.src = el.dataset.plantumlSrc || ''
})
}
const quickUpdateFun = (params: any) => {
setTimeout(() => {
ctx.registerHook('ON_VIEW_RENDERED', () => {
updatePlantuml(params)
}, true)
}, 0)
}
ctx.registerHook('ON_VIEW_MOUNTED', quickUpdateFun)
ctx.registerHook('ON_VIEW_FILE_CHANGE', quickUpdateFun)
ctx.registerHook('ON_VIEW_RENDERED', lodash.debounce(updatePlantuml, 3000, { leading: true }))
}
} as Plugin
@@ -1,5 +1,6 @@
import CryptoJS from 'crypto-js'
import Markdown from 'markdown-it'
import { Plugin } from '@/useful/plugin'
const cachePrefix = 'run_code_result_'
@@ -60,7 +61,7 @@ const RunPlugin = (md: Markdown) => {
}
}
RunPlugin.clearCache = () => {
const clearCache = () => {
Object.keys(localStorage).forEach(key => {
if (key.startsWith(cachePrefix)) {
localStorage.removeItem(key)
@@ -68,4 +69,10 @@ RunPlugin.clearCache = () => {
})
}
export default RunPlugin
export default {
name: 'run-code',
register: ctx => {
ctx.registerMarkdownItPlugin(RunPlugin)
ctx.registerHook('ON_STARTUP', clearCache)
}
} as Plugin
+36
View File
@@ -0,0 +1,36 @@
import { Plugin, Ctx } from '@/useful/plugin'
import { useBus } from '@/useful/bus'
import dayjs from 'dayjs'
export default {
name: 'switch-todo',
register: (ctx: Ctx) => {
ctx.registerHook('ON_VIEW_ELEMENT_CLICK', async (e: MouseEvent) => {
const target = e.target as HTMLElement
const bus = useBus()
const preventEvent = () => {
e.preventDefault()
e.stopPropagation()
return true
}
function switchTodo (line: number, checked: boolean) {
let lineText = ''
bus.emit('editor-get-line', { line, callback: (val: string) => { lineText = val } })
const value = checked
? lineText.replace('[ ]', `[x] ~~${dayjs().format('YYYY-MM-DD HH:mm')}~~`)
: lineText.replace(/(\[x\] ~~[\d-: ]+~~|\[x\])/, '[ ]')
bus.emit('editor-replace-line', { line, value })
}
if (target.tagName === 'INPUT' && target.parentElement!!.classList.contains('source-line')) {
switchTodo(parseInt(target.parentElement!!.dataset.sourceLine || '0'), (target as HTMLInputElement).checked)
return preventEvent()
}
return false
})
}
} as Plugin
+116
View File
@@ -0,0 +1,116 @@
import { useBus } from '@/useful/bus'
import { useModal } from '@/useful/modal'
import { Plugin, Ctx } from '@/useful/plugin'
import { hasCtrlCmd } from '@/useful/shortcut'
import { useToast } from '@/useful/toast'
const editTableCell = async (start: number, end: number, cellIndex: number) => {
const toast = useToast()
const modal = useModal()
const bus = useBus()
if (end - start !== 1) {
toast.show('warning', '暂只支持编辑单行文本')
return
}
const escapedSplit = (str: string) => {
const result = []
const max = str.length
let pos = 0
let ch = str.charCodeAt(pos)
let isEscaped = false
let lastPos = 0
let current = ''
while (pos < max) {
if (ch === 0x7c/* | */) {
if (!isEscaped) {
// pipe separating cells, '|'
result.push(current + str.substring(lastPos, pos))
current = ''
lastPos = pos + 1
} else {
// escaped pipe, '\|'
current += str.substring(lastPos, pos - 1)
lastPos = pos
}
}
isEscaped = (ch === 0x5c/* \ */)
pos++
ch = str.charCodeAt(pos)
}
result.push(current + str.substring(lastPos))
if (result.length && result[0] === '') result.shift()
if (result.length && result[result.length - 1] === '') result.pop()
return result
}
let text = ''
bus.emit('editor-get-line', { line: start, callback: (val: string) => { text = val.trim() } })
const columns = escapedSplit(text)
const cellText = columns[cellIndex]
if (typeof cellText !== 'string') {
toast.show('warning', '编辑错误')
return
}
let value = await modal.input({
title: '编辑单元格',
type: 'textarea',
value: cellText,
modalWidth: '600px',
hint: '单元格内容',
})
if (typeof value !== 'string') {
toast.show('warning', '取消编辑')
return
}
if (!value.startsWith(' ') && cellIndex > 0) value = ' ' + value
if (!value.endsWith(' ') && cellIndex < columns.length - 1) value += ' '
columns[cellIndex] = value.replace(/\|/g, '\\|').replace(/\n/g, ' ')
let content = columns.join('|').trim()
if (text.startsWith('|')) content = '| ' + content
if (text.endsWith('|')) content += ' |'
bus.emit('editor-replace-line', { line: start, value: content })
}
export default {
name: 'table-cell-edit',
register: (ctx: Ctx) => {
ctx.registerHook('ON_VIEW_ELEMENT_CLICK', async (e: MouseEvent) => {
const target = e.target as HTMLElement
const preventEvent = () => {
e.preventDefault()
e.stopPropagation()
return true
}
if (target.tagName === 'TD' && target.classList.contains('yank-td') && hasCtrlCmd(e)) {
const start = parseInt(target.dataset.sourceLine || '0')
const end = parseInt(target.dataset.sourceLineEnd || '0')
const td = target as HTMLTableDataCellElement
const cellIndex = [...td.parentElement!.children as any]
.slice(0, td.cellIndex)
.reduce((prev, current) => prev + current.colSpan, 0)
editTableCell(start, end, cellIndex)
return preventEvent()
}
return false
})
}
} as Plugin
+49 -9
View File
@@ -1,30 +1,50 @@
import { addAction } from './shortcut'
import { getLogger } from './utils'
import Markdown from 'markdown-it'
export type HookType = 'ON_VIEW_ELEMENT_CLICK'
export type HookType = 'ON_STARTUP'
| 'ON_VIEW_ELEMENT_CLICK'
| 'ON_VIEW_KEY_DOWN'
| 'ON_VIEW_RENDER'
| 'ON_VIEW_RENDERED'
| 'ON_VIEW_MOUNTED'
| 'ON_VIEW_FILE_CHANGE'
| 'ON_VIEW_BEFORE_CONVERT'
| 'ON_TREE_NODE_SELECT'
export interface Plugin {
name: string;
register?: (ctx: Ctx) => void;
}
type HookFun = (...args: any[]) => boolean | void | Promise<boolean | void>
const logger = getLogger('plugin')
const plugins: {[name: string]: Plugin} = {}
const hooks: { [key in HookType]?: ((...args: any[]) => boolean | void | Promise<boolean | void>)[] } = {}
const hooks: { [key in HookType]?: {fun: HookFun; once: boolean}[] } = {}
const markdownItPlugins: {plugin: any; params: any}[] = []
const ctx = {
registerShortcutAction (name: string, keys: (string | number)[]) {
addAction(name, keys)
},
registerHook: (type: HookType, fun: (...args: any[]) => void) => {
registerHook: (type: HookType, fun: HookFun, once = false) => {
if (Array.isArray(hooks[type])) {
hooks[type] = [...hooks[type]!!, fun]
hooks[type] = [...hooks[type]!!, { fun, once }]
} else {
hooks[type] = [fun]
hooks[type] = [{ fun, once }]
}
},
removeHook: (type: HookType, fun: HookFun) => {
if (Array.isArray(hooks[type])) {
hooks[type] = hooks[type]!!.filter(x => x.fun !== fun)
}
},
registerMarkdownItPlugin: (plugin: (md: Markdown, ...args: any) => void, params?: any) => {
markdownItPlugins.push({ plugin, params })
}
}
@@ -40,7 +60,22 @@ export const init = () => {
logger.debug('init')
const localPlugins = [
require('@/plugins/TransformImgOutLink')
require('@/plugins/markdown-source-line'),
require('@/plugins/markdown-toc'),
require('@/plugins/markdown-code'),
require('@/plugins/markdown-link'),
require('@/plugins/code-line-number'),
require('@/plugins/transform-img-out-link'),
require('@/plugins/copy-text'),
require('@/plugins/table-cell-edit'),
require('@/plugins/switch-todo'),
require('@/plugins/run-code'),
require('@/plugins/plantuml'),
require('@/plugins/drawio'),
require('@/plugins/mind-map'),
require('@/plugins/mermaid'),
require('@/plugins/applet'),
require('@/plugins/echarts'),
]
localPlugins.forEach((plugin) => {
@@ -53,12 +88,17 @@ export const init = () => {
}
export const triggerHook = async (type: HookType, ...args: any[]) => {
logger.debug('triggerHook', type, hooks, args)
for (const fun of (hooks[type] || [])) {
logger.debug('triggerHook', type, args)
for (const { fun, once } of (hooks[type] || [])) {
once && ctx.removeHook(type, fun)
if (await fun(...args)) {
break
return true
}
}
return false
}
export const getMarkdownItPlugins = () => markdownItPlugins
init()
+2 -5
View File
@@ -1,15 +1,14 @@
import { useStore } from 'vuex'
import { useBus } from './bus'
import RunPlugin from '@/plugins/RunPlugin'
import { copyText } from './copy-text'
import env from './env'
import { hasCtrlCmd } from './shortcut'
import { triggerHook } from './plugin'
export default function startup () {
const bus = useBus()
const store = useStore()
RunPlugin.clearCache()
triggerHook('ON_STARTUP')
bus.on('editor-ready', () => {
const { currentFile } = store.state
@@ -20,8 +19,6 @@ export default function startup () {
}
})
bus.on('copy-text', copyText)
// 在 Electron 环境中开启缩放页面功能
if (env.isElectron) {
const webContents = env.require('electron').remote.getCurrentWebContents()
+1 -2
View File
@@ -19,8 +19,7 @@
<template v-slot:preview>
<Preview
ref="refPreview"
@sync-scroll="line => refEditor.revealLineInCenter(line)"
@switch-todo="(line, checked) => refEditor.switchTodo(line, checked)" />
@sync-scroll="line => refEditor.revealLineInCenter(line)" />
</template>
</Layout>
<XFilter />
+5 -5
View File
@@ -6441,11 +6441,6 @@ loader-utils@^2.0.0:
emojis-list "^3.0.0"
json5 "^2.1.2"
loadsh@^0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/loadsh/-/loadsh-0.0.4.tgz#5314babd12bb13315dde024a4ca70758c5489d2d"
integrity sha512-U+wLL8InpfRalWrr+0SuhWgGt10M4OyAk6G8xCYo2rwpiHtxZkWiFpjei0vO463ghW8LPCdhqQxXlMy2qicAEw==
locate-path@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
@@ -6514,6 +6509,11 @@ lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
log-symbols@2.2.0, log-symbols@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
+5
View File
@@ -1799,6 +1799,11 @@ lodash@^4.17.10:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"