mirror of
https://github.com/purocean/yn.git
synced 2026-10-13 07:38:54 +08:00
Merge branch 'release/2.6.0'
This commit is contained in:
@@ -77,12 +77,17 @@
|
||||
## 更新日志
|
||||
[最新发布](https://github.com/purocean/yn/releases)
|
||||
|
||||
### [v2.5.5](https://github.com/purocean/yn/releases/tag/v2.5.5) 2021-02-03
|
||||
1. 调整预览文字选择颜色
|
||||
### [v2.6.0](https://github.com/purocean/yn/releases/tag/v2.6.0) 2021-03-04
|
||||
1. 内部功能插件化,增强拓展性
|
||||
2. 微调界面样式
|
||||
3. 修复复制代码快捷键不正确问题
|
||||
|
||||
<details>
|
||||
<summary>展开查看更多版本记录</summary>
|
||||
|
||||
### [v2.5.5](https://github.com/purocean/yn/releases/tag/v2.5.5) 2021-02-03
|
||||
1. 调整预览文字选择颜色
|
||||
|
||||
### [v2.5.4](https://github.com/purocean/yn/releases/tag/v2.5.4) 2021-01-31
|
||||
1. 调整 macOS 上应用边框样式
|
||||
2. macOS 打包增加签名公证
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -109,7 +109,7 @@ export default defineComponent({
|
||||
left: -99999px;
|
||||
top: -99999px;
|
||||
visibility: hidden;
|
||||
background: #696969;
|
||||
background: #3d4042;
|
||||
border: 1px #333 solid;
|
||||
border-left: 0;
|
||||
border-top: 0;
|
||||
@@ -120,8 +120,8 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.menu > li.separator {
|
||||
border-top: 1px #888888 solid;
|
||||
border-bottom: 1px #646464 solid;
|
||||
border-top: 1px #666 solid;
|
||||
border-bottom: 1px #444 solid;
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -188,7 +188,7 @@ export default defineComponent({
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 4px;
|
||||
cursor: w-resize;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.sash-top {
|
||||
@@ -198,7 +198,7 @@ export default defineComponent({
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 4px;
|
||||
cursor: s-resize;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.right {
|
||||
|
||||
@@ -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,10 +38,9 @@
|
||||
</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 mime from 'mime-types'
|
||||
import Markdown from 'markdown-it'
|
||||
import TaskLists from 'markdown-it-task-lists'
|
||||
import katex from 'markdown-it-katex'
|
||||
@@ -50,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 { 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 { useToast } from '../useful/toast'
|
||||
import { hasCtrlCmd, isAction } from '../useful/shortcut'
|
||||
|
||||
HighlightLineNumber.addStyles()
|
||||
|
||||
const markdown = Markdown({
|
||||
linkify: true,
|
||||
@@ -90,28 +72,20 @@ 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',
|
||||
setup (_, { emit }) {
|
||||
const bus = useBus()
|
||||
const store = useStore()
|
||||
const toast = useToast()
|
||||
|
||||
const { currentContent, currentFile, autoPreview } = toRefs(store.state)
|
||||
const fileRepo = computed(() => currentFile.value?.repo)
|
||||
@@ -122,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<{
|
||||
@@ -140,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
|
||||
@@ -210,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(','))
|
||||
@@ -244,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
|
||||
@@ -259,77 +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 transformImgOutLink (img: HTMLImageElement) {
|
||||
const transform = (ximg: HTMLImageElement): Promise<string> => {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = ximg.naturalWidth
|
||||
canvas.height = ximg.naturalHeight
|
||||
canvas.getContext('2d')!!.drawImage(ximg, 0, 0)
|
||||
return new Promise((resolve, reject) => {
|
||||
canvas.toBlob(async blob => {
|
||||
try {
|
||||
const imgFile = new File([blob!!], 'file.png')
|
||||
const { relativePath } = await file.upload(fileRepo.value, filePath.value, imgFile)
|
||||
resolve(relativePath)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
let replacedLink = ''
|
||||
const imgAttrSrc = img.getAttribute('src') || ''
|
||||
if (img.src.startsWith('data:')) {
|
||||
replacedLink = await transform(img)
|
||||
} else if (imgAttrSrc.startsWith('http://') || imgAttrSrc.startsWith('https://')) {
|
||||
const res = await window.fetch(`api/proxy?url=${encodeURIComponent(img.src)}&headers=${img.getAttribute('headers') || ''}`)
|
||||
const blob = await res.blob()
|
||||
const imgFile = new File([blob!!], 'file.' + mime.extension(res.headers.get('content-type')!!))
|
||||
const { relativePath } = await file.upload(fileRepo.value, filePath.value, imgFile)
|
||||
replacedLink = relativePath
|
||||
}
|
||||
|
||||
if (replacedLink) {
|
||||
return { oldLink: img.src, replacedLink: encodeMarkdownLink(replacedLink) }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async function keydownHandler (e: KeyboardEvent) {
|
||||
// 转换所有外链图片到本地
|
||||
if (isAction(e, 'transform-img-link')) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const result = []
|
||||
const imgList = refView.value!!.querySelectorAll('img')
|
||||
for (let i = 0; i < imgList.length; i++) {
|
||||
toast.show('info', `正在转换外链图片 ${i + 1}/${imgList.length}`)
|
||||
|
||||
const img = imgList[i]
|
||||
const data = await transformImgOutLink(img)
|
||||
if (data) {
|
||||
result.push(data)
|
||||
}
|
||||
}
|
||||
result.forEach(data => bus.emit('editor-replace-value', { search: data.oldLink, replace: data.replacedLink }))
|
||||
bus.emit('tree-refresh')
|
||||
}
|
||||
triggerHook('ON_VIEW_KEY_DOWN', e, getViewDom())
|
||||
}
|
||||
|
||||
function handleScroll (e: any) {
|
||||
@@ -340,133 +202,16 @@ 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)
|
||||
}
|
||||
|
||||
async function handleClick (e: MouseEvent) {
|
||||
triggerHook('ON_VIEW_ELEMENT_CLICK', e)
|
||||
|
||||
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 === 'IMG') {
|
||||
const img = target as HTMLImageElement
|
||||
if (isAction(e, 'transform-img-link-by-click')) { // 转换外链图片到本地
|
||||
const data = await transformImgOutLink(img)
|
||||
if (data) {
|
||||
bus.emit('tree-refresh')
|
||||
bus.emit('editor-replace-value', { search: data.oldLink, replace: data.replacedLink })
|
||||
}
|
||||
} else if (img.parentElement!!.tagName === 'A') {
|
||||
handleLink(img.parentElement as HTMLAnchorElement)
|
||||
} else {
|
||||
env.openAlwaysOnTopWindow(img.src)
|
||||
}
|
||||
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -491,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('/')) + '/'
|
||||
|
||||
@@ -505,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)
|
||||
@@ -516,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)
|
||||
@@ -537,9 +278,7 @@ export default defineComponent({
|
||||
watch(filePath, () => {
|
||||
// 切换文件后,开启自动预览
|
||||
store.commit('setAutoPreview', true)
|
||||
setTimeout(() => {
|
||||
updatePlantuml()
|
||||
}, 100)
|
||||
triggerHook('ON_VIEW_FILE_CHANGE', { getViewDom })
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
<template>
|
||||
<div class="repository-switch">
|
||||
<div class="current" v-if="currentRepo">仓库:{{currentRepo.name}}</div>
|
||||
<div class="current" v-else>未选择仓库</div>
|
||||
<ul class="list">
|
||||
<li v-for="(path, name) in repositories" :key="name" :title="path" @click="choose({name, path})">{{name}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onBeforeMount, onBeforeUnmount, toRefs, watch } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import file from '../useful/file'
|
||||
import { useBus } from '../useful/bus'
|
||||
import { $args } from '../useful/global-args'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'repository-switch',
|
||||
setup () {
|
||||
const store = useStore()
|
||||
const bus = useBus()
|
||||
|
||||
const { currentRepo, repositories } = toRefs(store.state)
|
||||
|
||||
function choose (repo: any) {
|
||||
if (repo.name !== currentRepo.value?.name) {
|
||||
store.commit('setCurrentRepo', repo)
|
||||
}
|
||||
}
|
||||
|
||||
function chooseRepoByName (name?: string) {
|
||||
if (name && repositories.value[name]) {
|
||||
choose({ name, path: repositories.value[name] })
|
||||
}
|
||||
}
|
||||
|
||||
function initRepo () {
|
||||
const initRepoName = $args().get('init-repo')
|
||||
const initFilePath = $args().get('init-file')
|
||||
|
||||
if (initRepoName) {
|
||||
chooseRepoByName(initRepoName)
|
||||
}
|
||||
|
||||
if (initFilePath) {
|
||||
store.commit('setCurrentFile', { repo: currentRepo.value.name, name: file.basename(initFilePath), path: initFilePath })
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
bus.on('switch-repo-by-name', chooseRepoByName)
|
||||
bus.on('editor-ready', initRepo)
|
||||
store.dispatch('fetchRepositories')
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
bus.off('switch-repo-by-name', chooseRepoByName)
|
||||
bus.off('editor-ready', initRepo)
|
||||
})
|
||||
|
||||
watch(repositories, val => {
|
||||
const keys = Object.keys(val)
|
||||
if (!currentRepo.value || keys.indexOf(currentRepo.value.name) < 0) {
|
||||
if (keys.length > 0) {
|
||||
const name = keys[0]
|
||||
store.commit('setCurrentRepo', { name, path: val[name] })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
currentRepo,
|
||||
repositories,
|
||||
choose,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.repository-switch {
|
||||
width: 100px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.current {
|
||||
padding: 0 .3em;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.repository-switch:hover {
|
||||
background: #333030;
|
||||
}
|
||||
|
||||
.repository-switch:hover .list {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.list {
|
||||
width: 100px;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
background: #333030;
|
||||
padding: 4px 0;
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.list li {
|
||||
padding: 4px .6em;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list li:hover {
|
||||
background: #252525;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="status-bar">
|
||||
<RepositorySwitch class="left"></RepositorySwitch>
|
||||
<StatusBarMenu class="left" position="left" />
|
||||
<div class="right">
|
||||
<div class="document-info">
|
||||
<span>行:{{documentInfo.line}}</span>
|
||||
@@ -9,12 +9,7 @@
|
||||
<span>字符数:{{documentInfo.textLength}}</span>
|
||||
<span v-if="documentInfo.selectedLength > 0">已选中:{{documentInfo.selectedLength}}</span>
|
||||
</div>
|
||||
<div class="action" @click="toggleSide" :title="getActionLabel('toggle-side')">切换侧栏</div>
|
||||
<div class="action" @click="toggleWrap" :title="getActionLabel('toggle-wrap')">切换换行</div>
|
||||
<div class="action" @click="toggleView" :title="getActionLabel('toggle-view')">切换预览</div>
|
||||
<div class="action" @click="toggleXterm" :title="getActionLabel('toggle-xterm')">切换终端</div>
|
||||
<div class="action" @click="toggleReadme" :title="getActionLabel('toggle-readme')">README</div>
|
||||
<div class="action" @click="toggleFeature">特色功能说明</div>
|
||||
<StatusBarMenu class="right" position="right" />
|
||||
<div class="action" @click="toggleRender">{{autoPreview ? '同步渲染-已开启' : '同步渲染-已关闭'}}</div>
|
||||
<svg-icon v-if="!autoPreview" class="action action-icon" name="sync-alt-solid" @click="rerenderView" title="强制重新渲染" />
|
||||
</div>
|
||||
@@ -22,45 +17,20 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onBeforeUnmount, onMounted, toRefs } from 'vue'
|
||||
import { defineComponent, toRefs } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useBus } from '../useful/bus'
|
||||
import RepositorySwitch from './RepositorySwitch.vue'
|
||||
import StatusBarMenu from './StatusBarMenu.vue'
|
||||
import SvgIcon from '../components/SvgIcon.vue'
|
||||
import { isAction, getActionLabel } from '../useful/shortcut'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'status-bar',
|
||||
components: { RepositorySwitch, SvgIcon },
|
||||
components: { StatusBarMenu, SvgIcon },
|
||||
setup () {
|
||||
const bus = useBus()
|
||||
const store = useStore()
|
||||
const { documentInfo, autoPreview } = toRefs(store.state)
|
||||
|
||||
function toggleSide () {
|
||||
bus.emit('toggle-side')
|
||||
}
|
||||
|
||||
function toggleView () {
|
||||
bus.emit('toggle-view')
|
||||
}
|
||||
|
||||
function toggleXterm () {
|
||||
bus.emit('toggle-xterm')
|
||||
}
|
||||
|
||||
function toggleFeature () {
|
||||
store.dispatch('showHelp', 'FEATURES.md')
|
||||
}
|
||||
|
||||
function toggleReadme () {
|
||||
store.dispatch('showHelp', 'README.md')
|
||||
}
|
||||
|
||||
function toggleWrap () {
|
||||
bus.emit('editor-toggle-wrap')
|
||||
}
|
||||
|
||||
function toggleRender () {
|
||||
store.commit('setAutoPreview', !autoPreview.value)
|
||||
}
|
||||
@@ -69,58 +39,11 @@ export default defineComponent({
|
||||
bus.emit('view-rerender')
|
||||
}
|
||||
|
||||
function keydownHandler (e: KeyboardEvent) {
|
||||
if (isAction(e, 'toggle-side')) {
|
||||
toggleSide()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
if (isAction(e, 'toggle-view')) {
|
||||
toggleView()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
if (isAction(e, 'toggle-xterm')) {
|
||||
toggleXterm()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
if (isAction(e, 'toggle-wrap')) {
|
||||
toggleWrap()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
if (isAction(e, 'toggle-readme')) {
|
||||
toggleReadme()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', keydownHandler, true)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', keydownHandler)
|
||||
})
|
||||
|
||||
return {
|
||||
documentInfo,
|
||||
autoPreview,
|
||||
toggleSide,
|
||||
toggleWrap,
|
||||
toggleView,
|
||||
toggleXterm,
|
||||
toggleReadme,
|
||||
toggleFeature,
|
||||
toggleRender,
|
||||
rerenderView,
|
||||
getActionLabel,
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -151,6 +74,7 @@ export default defineComponent({
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 0.3em;
|
||||
}
|
||||
|
||||
.document-info > span {
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="status-bar-menu-wrapper">
|
||||
<div class="status-bar-menu" v-for="menu in list" :key="menu.id">
|
||||
<div class="title">{{menu.title}}</div>
|
||||
<ul class="list" v-if="showList">
|
||||
<li v-for="item in menu.list" :key="item.id" :title="item.tips" @click="handleItemClick(item)">{{item.title}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onBeforeUnmount, ref } from 'vue'
|
||||
import { useBus } from '../useful/bus'
|
||||
import { getStatusBarMenus, StatusBarMenuItem } from '../useful/plugin'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'status-bar-menu',
|
||||
props: {
|
||||
position: {
|
||||
type: String,
|
||||
default: 'left'
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
const bus = useBus()
|
||||
const list = ref(getStatusBarMenus('status-bar', props.position))
|
||||
const showList = ref(true)
|
||||
|
||||
const handleItemClick = (item: StatusBarMenuItem) => {
|
||||
item.onClick && item.onClick(item)
|
||||
showList.value = false
|
||||
setTimeout(() => {
|
||||
showList.value = true
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const updateMenu = () => {
|
||||
list.value = getStatusBarMenus('status-bar', props.position)
|
||||
}
|
||||
|
||||
bus.on('status-bar-menu-update', updateMenu)
|
||||
onBeforeUnmount(() => {
|
||||
bus.off('status-bar-menu-update', updateMenu)
|
||||
})
|
||||
|
||||
return {
|
||||
list,
|
||||
showList,
|
||||
handleItemClick
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.status-bar-menu {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
z-index: 9999999999;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
padding: 0 .8em;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.status-bar-menu:hover {
|
||||
background: #2f3031;
|
||||
}
|
||||
|
||||
.status-bar-menu:hover .list {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.list {
|
||||
width: 100px;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
background: #37393a;
|
||||
padding: 4px 0;
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.list li {
|
||||
padding: 4px .8em;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list li:hover {
|
||||
background: #2f3031;
|
||||
}
|
||||
</style>
|
||||
@@ -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') && hasCtrlCmd(e)) {
|
||||
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,54 @@
|
||||
import { Plugin } from '@/useful/plugin'
|
||||
import { Alt, getActionLabel, isAction } from '@/useful/shortcut'
|
||||
import store from '@/store'
|
||||
|
||||
export default {
|
||||
name: 'status-bar-view',
|
||||
register: ctx => {
|
||||
const actions = {
|
||||
'toggle-readme': {
|
||||
title: 'README',
|
||||
action: () => store.dispatch('showHelp', 'README.md'),
|
||||
shortcut: [Alt, 'h'],
|
||||
},
|
||||
'toggle-features': {
|
||||
title: '特色功能说明',
|
||||
action: () => store.dispatch('showHelp', 'FEATURES.md'),
|
||||
shortcut: null,
|
||||
},
|
||||
}
|
||||
|
||||
Object.entries(actions).forEach(([key, item]) => {
|
||||
item.shortcut && ctx.registerShortcutAction(key, item.shortcut)
|
||||
})
|
||||
|
||||
function keydownHandler (e: KeyboardEvent) {
|
||||
for (const [key, item] of Object.entries(actions)) {
|
||||
if (isAction(e, key)) {
|
||||
item.action()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', keydownHandler, true)
|
||||
|
||||
ctx.updateStatusBarMenu({
|
||||
id: 'status-bar-help',
|
||||
location: 'status-bar',
|
||||
position: 'right',
|
||||
title: '帮助',
|
||||
list: Object.entries(actions).map(([key, item]) => {
|
||||
return {
|
||||
id: key,
|
||||
type: 'normal',
|
||||
title: item.title,
|
||||
tips: item.shortcut ? getActionLabel(key) : '',
|
||||
onClick: item.action
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
} as Plugin
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Plugin, StatusBarMenu } from '@/useful/plugin'
|
||||
import store from '@/store'
|
||||
import { useBus } from '@/useful/bus'
|
||||
import { $args } from '@/useful/global-args'
|
||||
import file from '@/useful/file'
|
||||
|
||||
export default {
|
||||
name: 'status-bar-repository-switch',
|
||||
register: ctx => {
|
||||
const bus = useBus()
|
||||
|
||||
function choose (repo: any) {
|
||||
const { currentRepo } = store.state
|
||||
if (repo.name !== currentRepo.name) {
|
||||
store.commit('setCurrentRepo', repo)
|
||||
}
|
||||
}
|
||||
|
||||
function chooseRepoByName (name?: string) {
|
||||
const { repositories } = store.state
|
||||
if (name && repositories[name]) {
|
||||
choose({ name, path: repositories[name] })
|
||||
}
|
||||
}
|
||||
|
||||
function initRepo () {
|
||||
const { currentRepo } = store.state
|
||||
|
||||
const initRepoName = $args().get('init-repo')
|
||||
const initFilePath = $args().get('init-file')
|
||||
|
||||
if (initRepoName) {
|
||||
chooseRepoByName(initRepoName)
|
||||
}
|
||||
|
||||
if (initFilePath) {
|
||||
store.commit('setCurrentFile', { repo: currentRepo.name, name: file.basename(initFilePath), path: initFilePath })
|
||||
}
|
||||
}
|
||||
|
||||
const menu = {
|
||||
id: 'status-bar-repository-switch',
|
||||
location: 'status-bar',
|
||||
position: 'left',
|
||||
title: '仓库',
|
||||
} as StatusBarMenu
|
||||
|
||||
function updateMenu () {
|
||||
const { currentRepo, repositories } = store.state
|
||||
ctx.updateStatusBarMenu({
|
||||
...menu,
|
||||
title: currentRepo ? `仓库: ${currentRepo.name}` : '未选择仓库',
|
||||
list: Object.keys(repositories).map(name => {
|
||||
const path = repositories[name]
|
||||
|
||||
return {
|
||||
id: name,
|
||||
type: 'normal',
|
||||
title: name,
|
||||
tips: path,
|
||||
onClick: () => choose({ name, path })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
ctx.updateStatusBarMenu(menu)
|
||||
|
||||
bus.on('switch-repo-by-name', chooseRepoByName)
|
||||
bus.on('editor-ready', initRepo)
|
||||
store.dispatch('fetchRepositories')
|
||||
|
||||
store.watch(() => store.state.repositories, updateMenu)
|
||||
store.watch(() => store.state.currentRepo, updateMenu)
|
||||
|
||||
store.watch(() => store.state.repositories, val => {
|
||||
const { currentRepo } = store.state
|
||||
const keys = Object.keys(val)
|
||||
if (!currentRepo || keys.indexOf(currentRepo.name) < 0) {
|
||||
if (keys.length > 0) {
|
||||
const name = keys[0]
|
||||
store.commit('setCurrentRepo', { name, path: val[name] })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
} as Plugin
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Plugin } from '@/useful/plugin'
|
||||
import { useBus } from '@/useful/bus'
|
||||
import { Alt, getActionLabel, isAction } from '@/useful/shortcut'
|
||||
|
||||
export default {
|
||||
name: 'status-bar-view',
|
||||
register: ctx => {
|
||||
const bus = useBus()
|
||||
|
||||
const actions = {
|
||||
'toggle-side': {
|
||||
title: '切换侧栏',
|
||||
action: () => bus.emit('toggle-side'),
|
||||
shortcut: [Alt, 'e'],
|
||||
},
|
||||
'toggle-view': {
|
||||
title: '切换预览',
|
||||
action: () => bus.emit('toggle-view'),
|
||||
shortcut: [Alt, 'v'],
|
||||
},
|
||||
'toggle-xterm': {
|
||||
title: '切换终端',
|
||||
action: () => bus.emit('toggle-xterm'),
|
||||
shortcut: [Alt, 't'],
|
||||
},
|
||||
'toggle-wrap': {
|
||||
title: '切换换行',
|
||||
action: () => bus.emit('editor-toggle-wrap'),
|
||||
shortcut: [Alt, 'w'],
|
||||
},
|
||||
}
|
||||
|
||||
Object.entries(actions).forEach(([key, item]) => ctx.registerShortcutAction(key, item.shortcut))
|
||||
|
||||
function keydownHandler (e: KeyboardEvent) {
|
||||
for (const [key, item] of Object.entries(actions)) {
|
||||
if (isAction(e, key)) {
|
||||
item.action()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', keydownHandler, true)
|
||||
|
||||
ctx.updateStatusBarMenu({
|
||||
id: 'status-bar-view',
|
||||
location: 'status-bar',
|
||||
position: 'left',
|
||||
title: '视图',
|
||||
list: Object.entries(actions).map(([key, item]) => {
|
||||
return {
|
||||
id: key,
|
||||
type: 'normal',
|
||||
title: item.title,
|
||||
tips: getActionLabel(key),
|
||||
onClick: item.action
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
} 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
|
||||
@@ -0,0 +1,108 @@
|
||||
import mime from 'mime-types'
|
||||
import file from '@/useful/file'
|
||||
import { Plugin, Ctx } from '@/useful/plugin'
|
||||
import { CtrlCmd, isAction, LeftClick, Shift } from '@/useful/shortcut'
|
||||
import { encodeMarkdownLink } from '@/useful/utils'
|
||||
import { useBus } from '@/useful/bus'
|
||||
import env from '@/useful/env'
|
||||
import { useToast } from '@/useful/toast'
|
||||
import store from '@/store'
|
||||
|
||||
async function transformImgOutLink (img: HTMLImageElement) {
|
||||
const { currentFile } = store.state
|
||||
const { repo, path } = currentFile
|
||||
|
||||
const transform = (ximg: HTMLImageElement): Promise<string> => {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = ximg.naturalWidth
|
||||
canvas.height = ximg.naturalHeight
|
||||
canvas.getContext('2d')!!.drawImage(ximg, 0, 0)
|
||||
return new Promise((resolve, reject) => {
|
||||
canvas.toBlob(async blob => {
|
||||
try {
|
||||
const imgFile = new File([blob!!], 'file.png')
|
||||
const { relativePath } = await file.upload(repo, path, imgFile)
|
||||
resolve(relativePath)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
let replacedLink = ''
|
||||
const imgAttrSrc = img.getAttribute('src') || ''
|
||||
if (img.src.startsWith('data:')) {
|
||||
replacedLink = await transform(img)
|
||||
} else if (imgAttrSrc.startsWith('http://') || imgAttrSrc.startsWith('https://')) {
|
||||
const res = await window.fetch(`api/proxy?url=${encodeURIComponent(img.src)}&headers=${img.getAttribute('headers') || ''}`)
|
||||
const blob = await res.blob()
|
||||
const imgFile = new File([blob!!], 'file.' + mime.extension(res.headers.get('content-type')!!))
|
||||
const { relativePath } = await file.upload(repo, path, imgFile)
|
||||
replacedLink = relativePath
|
||||
}
|
||||
|
||||
if (replacedLink) {
|
||||
return { oldLink: img.src, replacedLink: encodeMarkdownLink(replacedLink) }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const actionKeydown = 'transform-img-link'
|
||||
const actionClick = 'transform-img-link-by-click'
|
||||
|
||||
export default {
|
||||
name: 'transform-img-out-link',
|
||||
register: (ctx: Ctx) => {
|
||||
ctx.registerShortcutAction(actionKeydown, [CtrlCmd, Shift, 'l'])
|
||||
ctx.registerShortcutAction(actionClick, [CtrlCmd, Shift, LeftClick])
|
||||
|
||||
ctx.registerHook('ON_VIEW_KEY_DOWN', async (e: KeyboardEvent, refView: HTMLElement) => {
|
||||
const toast = useToast()
|
||||
const bus = useBus()
|
||||
|
||||
if (isAction(e, actionKeydown)) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const result = []
|
||||
const imgList = refView.querySelectorAll('img')
|
||||
for (let i = 0; i < imgList.length; i++) {
|
||||
toast.show('info', `正在转换外链图片 ${i + 1}/${imgList.length}`)
|
||||
|
||||
const img = imgList[i]
|
||||
const data = await transformImgOutLink(img)
|
||||
if (data) {
|
||||
result.push(data)
|
||||
}
|
||||
}
|
||||
result.forEach(data => bus.emit('editor-replace-value', { search: data.oldLink, replace: data.replacedLink }))
|
||||
bus.emit('tree-refresh')
|
||||
}
|
||||
})
|
||||
|
||||
ctx.registerHook('ON_VIEW_ELEMENT_CLICK', async (e: MouseEvent) => {
|
||||
const bus = useBus()
|
||||
const target = e.target as HTMLElement
|
||||
if (target.tagName !== 'IMG') {
|
||||
return false
|
||||
}
|
||||
|
||||
const img = target as HTMLImageElement
|
||||
if (isAction(e, actionClick)) { // 转换外链图片到本地
|
||||
const data = await transformImgOutLink(img)
|
||||
if (data) {
|
||||
bus.emit('tree-refresh')
|
||||
bus.emit('editor-replace-value', { search: data.oldLink, replace: data.replacedLink })
|
||||
}
|
||||
} else {
|
||||
env.openAlwaysOnTopWindow(img.src)
|
||||
}
|
||||
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
} as Plugin
|
||||
@@ -36,7 +36,7 @@ const getLastOpenFile = (repoName?: string) => {
|
||||
|
||||
export default createStore({
|
||||
state: {
|
||||
repositories: {},
|
||||
repositories: {} as any,
|
||||
tree: null,
|
||||
showSide: Storage.get('showSide', true),
|
||||
showView: Storage.get('showView', true),
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { addAction } from './shortcut'
|
||||
import { getLogger } from './utils'
|
||||
import Markdown from 'markdown-it'
|
||||
import { useBus } from './bus'
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export type HookFun = (...args: any[]) => boolean | void | Promise<boolean | void>
|
||||
|
||||
export type StatusBarMenuItem = {
|
||||
id: string;
|
||||
type: 'normal';
|
||||
title: string;
|
||||
tips?: string;
|
||||
onClick?: (item: StatusBarMenuItem) => void;
|
||||
}
|
||||
|
||||
export type MenuLocation = 'status-bar'
|
||||
|
||||
export interface StatusBarMenu {
|
||||
id: string;
|
||||
title: string;
|
||||
tips?: string;
|
||||
location: MenuLocation;
|
||||
position: 'left' | 'right';
|
||||
onClick?: (menu: StatusBarMenu) => void;
|
||||
list?: (StatusBarMenuItem | { type: 'separator' })[];
|
||||
}
|
||||
|
||||
const logger = getLogger('plugin')
|
||||
const bus = useBus()
|
||||
|
||||
const plugins: {[name: string]: Plugin} = {}
|
||||
const hooks: { [key in HookType]?: {fun: HookFun; once: boolean}[] } = {}
|
||||
const markdownItPlugins: {plugin: any; params: any}[] = []
|
||||
const statusBarMenus: { [key: string]: StatusBarMenu } = {}
|
||||
|
||||
const ctx = {
|
||||
registerShortcutAction (name: string, keys: (string | number)[]) {
|
||||
addAction(name, keys)
|
||||
},
|
||||
registerHook: (type: HookType, fun: HookFun, once = false) => {
|
||||
if (Array.isArray(hooks[type])) {
|
||||
hooks[type] = [...hooks[type]!!, { fun, once }]
|
||||
} else {
|
||||
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 })
|
||||
},
|
||||
updateStatusBarMenu: (menu: StatusBarMenu) => {
|
||||
statusBarMenus[menu.id] = menu
|
||||
bus.emit('status-bar-menu-update', menu)
|
||||
},
|
||||
removeStatusBarMenu: (id: string) => {
|
||||
delete statusBarMenus[id]
|
||||
bus.emit('status-bar-menu-update')
|
||||
}
|
||||
}
|
||||
|
||||
export type Ctx = typeof ctx;
|
||||
|
||||
export const register = (plugin: Plugin) => {
|
||||
logger.debug('register', plugin)
|
||||
plugins[plugin.name] = plugin
|
||||
plugin.register && plugin.register(ctx)
|
||||
}
|
||||
|
||||
export const init = () => {
|
||||
logger.debug('init')
|
||||
|
||||
const localPlugins = [
|
||||
require('@/plugins/status-bar-repository-switch'),
|
||||
require('@/plugins/status-bar-view'),
|
||||
require('@/plugins/status-bar-help'),
|
||||
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) => {
|
||||
if (plugin.__esModule && plugin.default) {
|
||||
register(plugin.default)
|
||||
} else {
|
||||
register(plugin)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const triggerHook = async (type: HookType, ...args: any[]) => {
|
||||
logger.debug('triggerHook', type, args)
|
||||
for (const { fun, once } of (hooks[type] || [])) {
|
||||
once && ctx.removeHook(type, fun)
|
||||
if (await fun(...args)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export const getMarkdownItPlugins = () => markdownItPlugins
|
||||
export const getStatusBarMenus = (location: MenuLocation, position: string) =>
|
||||
Object.values(statusBarMenus).filter(x => x.location === location && x.position === position)
|
||||
|
||||
init()
|
||||
@@ -4,29 +4,26 @@ import env from './env'
|
||||
const logger = getLogger('shortcut')
|
||||
|
||||
const isMacOS = env.isMacOS
|
||||
const Ctrl = 'Ctrl'
|
||||
const CtrlCmd = 'CtrlCmd'
|
||||
const Alt = 'Alt'
|
||||
const Shift = 'Shift'
|
||||
const LeftClick = 0
|
||||
export const Ctrl = 'Ctrl'
|
||||
export const CtrlCmd = 'CtrlCmd'
|
||||
export const Alt = 'Alt'
|
||||
export const Shift = 'Shift'
|
||||
export const LeftClick = 0
|
||||
|
||||
type XKey = typeof Ctrl | typeof CtrlCmd | typeof Alt | typeof Shift
|
||||
|
||||
const actions = {
|
||||
'toggle-side': [Alt, 'e'],
|
||||
'toggle-wrap': [Alt, 'w'],
|
||||
'toggle-view': [Alt, 'v'],
|
||||
'toggle-xterm': [Alt, 't'],
|
||||
'toggle-readme': [Alt, 'h'],
|
||||
const defaultActions = {
|
||||
'insert-document': [CtrlCmd, Alt, 'i'],
|
||||
'show-quick-open': [CtrlCmd, 'p'],
|
||||
'transform-img-link-by-click': [CtrlCmd, Shift, LeftClick],
|
||||
'transform-img-link': [CtrlCmd, Shift, 'l'],
|
||||
'file-tabs-switch-left': [Ctrl, Alt, 'ArrowLeft'],
|
||||
'file-tabs-switch-right': [Ctrl, Alt, 'ArrowRight'],
|
||||
}
|
||||
|
||||
type ActionName = keyof typeof actions
|
||||
const actions: {[key: string]: (string | number)[]} = {
|
||||
...defaultActions
|
||||
}
|
||||
|
||||
type ActionName = keyof typeof defaultActions
|
||||
|
||||
export const hasCtrlCmd = (e: KeyboardEvent | MouseEvent) => isMacOS ? e.metaKey : e.ctrlKey
|
||||
|
||||
@@ -43,11 +40,6 @@ export const getKeyLabel = (key: XKey | string | number) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const getActionLabel = (name: ActionName) => {
|
||||
const keys: any[] = actions[name]
|
||||
return keys.map(getKeyLabel).join(' + ')
|
||||
}
|
||||
|
||||
export const matchKeys = (e: KeyboardEvent | MouseEvent, keys: (string | number)[]) => {
|
||||
for (const key of keys) {
|
||||
switch (key) {
|
||||
@@ -80,7 +72,22 @@ export const getCurrentAction = (e: KeyboardEvent | MouseEvent) => {
|
||||
return (Object.keys(actions) as ActionName[]).find(action => matchKeys(e, actions[action]))
|
||||
}
|
||||
|
||||
export const isAction = (e: KeyboardEvent | MouseEvent, name: ActionName) => {
|
||||
export function isAction (e: KeyboardEvent | MouseEvent, name: ActionName): boolean
|
||||
export function isAction (e: KeyboardEvent | MouseEvent, name: string): boolean
|
||||
export function isAction (e: KeyboardEvent | MouseEvent, name: string) {
|
||||
logger.debug('isAction', name, e)
|
||||
return matchKeys(e, actions[name])
|
||||
}
|
||||
|
||||
export function getActionLabel (name: ActionName): string
|
||||
export function getActionLabel (name: string): string
|
||||
export function getActionLabel (name: string): string {
|
||||
const keys: any[] = actions[name]
|
||||
return keys.map(getKeyLabel).join(' + ')
|
||||
}
|
||||
|
||||
export function addAction (name: ActionName, keys: (string | number)[]): void
|
||||
export function addAction (name: string, keys: (string | number)[]): void
|
||||
export function addAction (name: string, keys: (string | number)[]): void {
|
||||
actions[name] = keys
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yank.note",
|
||||
"version": "2.5.5",
|
||||
"version": "2.6.0",
|
||||
"description": "Yank Note 一款面向程序员的 Markdown 笔记应用",
|
||||
"main": "dist/app.js",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -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