mirror of
https://github.com/purocean/yn.git
synced 2026-10-12 23:31:39 +08:00
插件改造
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
+13
-1
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
+13
-10
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user