mirror of
https://github.com/purocean/yn.git
synced 2026-08-31 01:55:43 +08:00
Merge branch 'release/3.67.0'
This commit is contained in:
@@ -76,16 +76,23 @@ For more information on how to use the following functions, please see [characte
|
||||
|
||||
## Changelogs
|
||||
|
||||
### [v3.66.1](https://github.com/purocean/yn/releases/tag/v3.66.1) 2024-01-11
|
||||
### [v3.67.0](https://github.com/purocean/yn/releases/tag/v3.67.0) 2024-01-11
|
||||
|
||||
[Windows](https://github.com/purocean/yn/releases/download/v3.66.1/Yank-Note-win-x64-3.66.1.exe) | [macOS arm64](https://github.com/purocean/yn/releases/download/v3.66.1/Yank-Note-mac-arm64-3.66.1.dmg) | [macOS x64](https://github.com/purocean/yn/releases/download/v3.66.1/Yank-Note-mac-x64-3.66.1.dmg) | [Linux AppImage](https://github.com/purocean/yn/releases/download/v3.66.1/Yank-Note-linux-x86_64-3.66.1.AppImage) | [Linux deb](https://github.com/purocean/yn/releases/download/v3.66.1/Yank-Note-linux-amd64-3.66.1.deb)
|
||||
[Windows](https://github.com/purocean/yn/releases/download/v3.67.0/Yank-Note-win-x64-3.67.0.exe) | [macOS arm64](https://github.com/purocean/yn/releases/download/v3.67.0/Yank-Note-mac-arm64-3.67.0.dmg) | [macOS x64](https://github.com/purocean/yn/releases/download/v3.67.0/Yank-Note-mac-x64-3.67.0.dmg) | [Linux AppImage](https://github.com/purocean/yn/releases/download/v3.67.0/Yank-Note-linux-x86_64-3.67.0.AppImage) | [Linux deb](https://github.com/purocean/yn/releases/download/v3.67.0/Yank-Note-linux-amd64-3.67.0.deb)
|
||||
|
||||
1. feat: Support [Github-style warning prompts](https://github.com/orgs/community/discussions/16925)
|
||||
2. feat: Support displaying open windows in the Dock menu on macOS
|
||||
3. feat: Support rendering PlantUML using code blocks
|
||||
4. fix: Fix the issue of abnormal line number display when the code block exceeds 100 lines
|
||||
5. fix: Fix the issue of not being able to jump to the corresponding position in the preview area after selecting text in the editor
|
||||
6. feat(plugin): Custom editor supports defining using classes
|
||||
1. feat: Add "Services" menu on macOS
|
||||
2. feat: Add HTML file preview functionality
|
||||
3. feat: Allow parsing of Markdown `style` tags
|
||||
4. feat: Support Vue code block highlighting in the editor (HTML mode)
|
||||
5. fix: Fix slight performance degradation in Markdown rendering after introducing custom editor
|
||||
6. fix: Fix caching issue with updated images
|
||||
7. fix: Fix potential unresponsiveness of built-in terminal to window size changes
|
||||
8. fix: Fix incorrect indentation of Python code in terminal execution
|
||||
9. fix: Fix editor loading issue when opening files supported by custom editor
|
||||
10. fix: Fix correct path retrieval when opening files from the system
|
||||
11. feat(plugin): Support registering custom renderer `ctx.renderer.registerRenderer`
|
||||
12. refactor(plugin): Rename `ctx.api.watchFile` to `ctx.api.watchFs`
|
||||
13. refactor(plugin): Rename `ctx.markdown.getRenderCache` to `ctx.renderer.getRenderCache`
|
||||
|
||||
[More release notes](https://github.com/purocean/yn/releases)
|
||||
|
||||
|
||||
+15
-8
@@ -76,16 +76,23 @@
|
||||
|
||||
## 更新日志
|
||||
|
||||
### [v3.66.1](https://github.com/purocean/yn/releases/tag/v3.66.1) 2024-01-11
|
||||
### [v3.67.0](https://github.com/purocean/yn/releases/tag/v3.67.0) 2024-01-11
|
||||
|
||||
[Windows](https://github.com/purocean/yn/releases/download/v3.66.1/Yank-Note-win-x64-3.66.1.exe) | [macOS arm64](https://github.com/purocean/yn/releases/download/v3.66.1/Yank-Note-mac-arm64-3.66.1.dmg) | [macOS x64](https://github.com/purocean/yn/releases/download/v3.66.1/Yank-Note-mac-x64-3.66.1.dmg) | [Linux AppImage](https://github.com/purocean/yn/releases/download/v3.66.1/Yank-Note-linux-x86_64-3.66.1.AppImage) | [Linux deb](https://github.com/purocean/yn/releases/download/v3.66.1/Yank-Note-linux-amd64-3.66.1.deb)
|
||||
[Windows](https://github.com/purocean/yn/releases/download/v3.67.0/Yank-Note-win-x64-3.67.0.exe) | [macOS arm64](https://github.com/purocean/yn/releases/download/v3.67.0/Yank-Note-mac-arm64-3.67.0.dmg) | [macOS x64](https://github.com/purocean/yn/releases/download/v3.67.0/Yank-Note-mac-x64-3.67.0.dmg) | [Linux AppImage](https://github.com/purocean/yn/releases/download/v3.67.0/Yank-Note-linux-x86_64-3.67.0.AppImage) | [Linux deb](https://github.com/purocean/yn/releases/download/v3.67.0/Yank-Note-linux-amd64-3.67.0.deb)
|
||||
|
||||
1. feat: 支持 [Github 风格的警告提示](https://github.com/orgs/community/discussions/16925)
|
||||
2. feat: 在 macOS 上支持在 Dock 菜单中显示打开的窗口
|
||||
3. feat: 支持使用代码块的方式渲染 PlantUML
|
||||
4. fix: 修复代码块超过 100 行后,行号显示异常问题
|
||||
5. fix: 修复编辑器选中文字后,点击预览区不能跳转到对应位置的问题
|
||||
6. feat(plugin): 自定义编辑器支持使用类的方式定义
|
||||
1. feat: 在 macOS 上增加“服务”菜单
|
||||
2. feat: 增加 HTML 文件预览功能
|
||||
3. feat: 放开 Markdown `style` 标签解析
|
||||
4. feat: 编辑器中支持 Vue 代码块高亮(HTML模式)
|
||||
5. fix: 修复引入自定义编辑器后 Markdown 渲染性能轻微退化
|
||||
6. fix: 修复图片更新后,图片被缓存的问题
|
||||
7. fix: 修复内置终端可能不能响应窗口尺寸变化问题
|
||||
8. fix: 修复终端运行 python 代码缩进不正确问题
|
||||
9. fix: 修复打开自定义编辑器支持的文件,可能编辑器加载异常问题
|
||||
10. fix: 修复从系统中打开文件,路径获取正确问题
|
||||
11. feat(plugin): 支持注册自定义渲染器 `ctx.renderer.registerRenderer`
|
||||
12. refactor(plugin): `ctx.api.watchFile` 被命名为 `ctx.api.watchFs`
|
||||
13. refactor(plugin): `ctx.markdown.getRenderCache` 被命名为 `ctx.renderer.getRenderCache`
|
||||
|
||||
[更多发布说明](https://github.com/purocean/yn/releases)
|
||||
|
||||
|
||||
@@ -399,6 +399,8 @@ If there is no title, there will be no outer border decoration
|
||||
<button onclick="ctx.ui.useToast().show(`info`, `HELLOWORLD!`)">HELLO</button>
|
||||
```
|
||||
|
||||
You can also *<a href="javascript: ctx.showExtensionManager('@yank-note/extension-repl')">install and activate the Vue Repl extension</a>* to write applet using Vue SFC.
|
||||
|
||||
## ECharts
|
||||
|
||||
The string containing `--echarts--` in the first line of the Js code block will be resolved into ECharts graphics, the example is as follows
|
||||
|
||||
@@ -397,6 +397,8 @@ function run (type) {
|
||||
<button onclick="ctx.ui.useToast().show(`info`, `HELLOWORLD!`)">HELLO</button>
|
||||
```
|
||||
|
||||
你也可以 *<a href="javascript: ctx.showExtensionManager('@yank-note/extension-repl')">安装并启用 Vue Repl 扩展</a>*,使用 Vue SFC 来编写小工具。
|
||||
|
||||
## ECharts 图形
|
||||
|
||||
Js 代码块第一行包含以 `--echarts--` 字符串会被解析成 ECharts 图形,示例如下
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yank.note",
|
||||
"version": "3.66.1",
|
||||
"version": "3.67.0",
|
||||
"description": "Yank Note: A highly extensible Markdown editor, designed for productivity.",
|
||||
"main": "dist/main/app.js",
|
||||
"license": "AGPL-3.0",
|
||||
@@ -148,7 +148,7 @@
|
||||
"typescript": "^4.9.5",
|
||||
"utility-types": "^3.10.0",
|
||||
"viewerjs": "^1.11.0",
|
||||
"vite": "^4.5.1",
|
||||
"vite": "^4.5.2",
|
||||
"vue": "^3.3.13",
|
||||
"vue-router": "^4.0.0-0",
|
||||
"vue-tsc": "^1.8.26",
|
||||
|
||||
+4
-1
@@ -11,7 +11,10 @@ export const getMainMenus = () => process.platform === 'darwin' ? Menu.buildFrom
|
||||
submenu: [
|
||||
{ type: 'normal', label: $t('app.preferences'), click: () => getAction('show-main-window-setting')() },
|
||||
{ type: 'normal', label: $t('app.toggle-fullscreen'), accelerator: 'Ctrl+command+F', click: () => getAction('toggle-fullscreen')() },
|
||||
{ type: 'normal', label: $t('app.quit'), accelerator: 'Command+Q', click: () => getAction('quit')() }
|
||||
{ type: 'separator' },
|
||||
{ role: 'services', submenu: [] },
|
||||
{ type: 'separator' },
|
||||
{ type: 'normal', label: $t('app.quit'), accelerator: 'Command+Q', click: () => getAction('quit')() },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -208,6 +208,7 @@ const attachment = async (ctx: any, next: any) => {
|
||||
|
||||
checkPrivateRepo(ctx, repo)
|
||||
|
||||
noCache(ctx)
|
||||
ctx.type = mime.getType(path)
|
||||
ctx.body = await file.read(repo, path)
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import { FileTabs } from '@fe/services/workbench'
|
||||
import { isMarkdownFile } from '@fe/services/document'
|
||||
import store from '@fe/support/store'
|
||||
import type { Components } from '@fe/types'
|
||||
import { CSS_VAR_NAME } from '@fe/support/args'
|
||||
|
||||
import DefaultPreviewerRender from './DefaultPreviewerRender.ce.vue'
|
||||
import SvgIcon from './SvgIcon.vue'
|
||||
@@ -264,6 +265,12 @@ onBeforeUnmount(() => {
|
||||
FileTabs.removeActionBtnTapper(tabsActionBtnTapper)
|
||||
})
|
||||
|
||||
watch([container, height], () => {
|
||||
if (container.value) {
|
||||
container.value.ownerDocument.documentElement.style.setProperty(CSS_VAR_NAME.PREVIEWER_HEIGHT, height.value + 'px')
|
||||
}
|
||||
})
|
||||
|
||||
async function scrollToTop () {
|
||||
disableSyncScrollAwhile(() => {
|
||||
scrollTopTo(0)
|
||||
|
||||
@@ -9,12 +9,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { debounce } from 'lodash-es'
|
||||
import { computed, defineComponent, h, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { isElectron } from '@fe/support/env'
|
||||
import { markdown } from '@fe/services/markdown'
|
||||
import { triggerHook } from '@fe/core/hook'
|
||||
import { registerAction, removeAction } from '@fe/core/action'
|
||||
import { CtrlCmd } from '@fe/core/keybinding'
|
||||
import { toUri, isMarkdownFile, isOutOfRepo } from '@fe/services/document'
|
||||
import { toUri, isOutOfRepo } from '@fe/services/document'
|
||||
import * as renderer from '@fe/services/renderer'
|
||||
import { getContextMenuItems, getRenderIframe, scrollTopTo } from '@fe/services/view'
|
||||
import { useContextMenu } from '@fe/support/ui/context-menu'
|
||||
import { DOM_ATTR_NAME } from '@fe/support/args'
|
||||
@@ -80,12 +79,7 @@ async function render (checkInComposition = false) {
|
||||
updateRender = debounce(render, 25)
|
||||
}
|
||||
|
||||
let content = currentContent.value
|
||||
|
||||
if (currentFile.value && !isMarkdownFile(currentFile.value)) {
|
||||
content = `## ${currentFile.value.name} \n *Not a markdown file.*`
|
||||
currentFile.value.name
|
||||
}
|
||||
const content = currentContent.value
|
||||
|
||||
const file = currentFile.value || null
|
||||
const safeMode = isOutOfRepo(file) // enable safe mode for root repo
|
||||
@@ -93,7 +87,7 @@ async function render (checkInComposition = false) {
|
||||
const startTime = performance.now()
|
||||
renderEnv = { tokens: [], source: content, file, renderCount, safeMode }
|
||||
try {
|
||||
renderContent.value = markdown.render(content, renderEnv)
|
||||
renderContent.value = renderer.render(content, renderEnv)
|
||||
} catch (error: any) {
|
||||
logger.error('render', error)
|
||||
renderContent.value = h('div', [
|
||||
@@ -132,25 +126,21 @@ function handleClick (e: MouseEvent) {
|
||||
}
|
||||
|
||||
function handleContextMenu (e: MouseEvent) {
|
||||
const tagName = (e.target as HTMLElement).tagName
|
||||
const allowTags = ['TD', 'TH']
|
||||
if (isElectron || e.altKey || allowTags.includes(tagName)) {
|
||||
const contextMenuItems = getContextMenuItems(e)
|
||||
if (contextMenuItems.length > 0) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
const contextMenuItems = getContextMenuItems(e)
|
||||
if (contextMenuItems.length > 0) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
|
||||
const clientX = e.clientX
|
||||
const clientY = e.clientY
|
||||
const clientX = e.clientX
|
||||
const clientY = e.clientY
|
||||
|
||||
getRenderIframe().then((iframe) => {
|
||||
const iframeRect = iframe.getBoundingClientRect()
|
||||
useContextMenu().show(contextMenuItems, {
|
||||
mouseX: iframeRect.left + clientX,
|
||||
mouseY: iframeRect.top + clientY,
|
||||
})
|
||||
getRenderIframe().then((iframe) => {
|
||||
const iframeRect = iframe.getBoundingClientRect()
|
||||
useContextMenu().show(contextMenuItems, {
|
||||
mouseX: iframeRect.left + clientX,
|
||||
mouseY: iframeRect.top + clientY,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,7 +254,13 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
|
||||
watch([currentContent, fileUri, inComposition], () => {
|
||||
autoPreview.value && updateRender()
|
||||
if (autoPreview.value) {
|
||||
if (renderCount === 0) {
|
||||
render()
|
||||
} else {
|
||||
updateRender()
|
||||
}
|
||||
}
|
||||
}, { flush: 'post' })
|
||||
|
||||
watch(fileUri, () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, shallowRef, watchEffect } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, shallowRef, watch } from 'vue'
|
||||
import { getAllCustomEditors, isDefault, getValue, setValue, switchEditor } from '@fe/services/editor'
|
||||
import { registerAction, removeAction } from '@fe/core/action'
|
||||
import { registerHook, removeHook, triggerHook } from '@fe/core/hook'
|
||||
@@ -145,7 +145,7 @@ onBeforeUnmount(() => {
|
||||
FileTabs.removeActionBtnTapper(tabsActionBtnTapper)
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
watch(() => currentEditor.value?.name, () => {
|
||||
triggerHook('EDITOR_CURRENT_EDITOR_CHANGE', { current: currentEditor.value })
|
||||
FileTabs.refreshActionBtns()
|
||||
recordEditorUsageTime(currentEditor.value?.name || 'default')
|
||||
|
||||
@@ -406,6 +406,7 @@ async function fetchContent (type: 'readme' | 'changelog', extension: Extension)
|
||||
})
|
||||
|
||||
contentMap.value[type][extension.id] = `
|
||||
<base target="_blank" />
|
||||
<link rel="stylesheet" href="${location.origin}/github.css">
|
||||
<div style="padding: 12px" class="markdown-body">
|
||||
${markdownIt.render(markdown.replaceAll(/<small>([^<]+)<\/small>/g, '**$1**'))}
|
||||
|
||||
@@ -114,6 +114,9 @@ export default defineComponent({
|
||||
})
|
||||
}
|
||||
|
||||
// force trigger resize event
|
||||
xterm.resize(xterm.cols, xterm.cols)
|
||||
|
||||
if (!socket.connected) {
|
||||
socket.io.opts.query = query
|
||||
socket.connect()
|
||||
@@ -148,7 +151,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
cmd.code.split('\n').forEach(x => {
|
||||
input(x.trim())
|
||||
input(x)
|
||||
input(eol)
|
||||
})
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import * as theme from '@fe/services/theme'
|
||||
import * as setting from '@fe/services/setting'
|
||||
import * as i18n from '@fe/services/i18n'
|
||||
import * as runner from '@fe/services/runner'
|
||||
import * as renderer from '@fe/services/renderer'
|
||||
import * as exportDoc from '@fe/services/export'
|
||||
import * as directives from '@fe/directives/index'
|
||||
import * as lib from './lib'
|
||||
@@ -51,6 +52,7 @@ const ctx = Object.freeze({
|
||||
markdown,
|
||||
statusBar,
|
||||
runner,
|
||||
renderer,
|
||||
layout,
|
||||
editor,
|
||||
view,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import buildInRenderers from '@fe/plugins/build-in-renderers'
|
||||
import customStyles from '@fe/plugins/custom-styles'
|
||||
import customKeybindings from '@fe/plugins/custom-keybindings'
|
||||
import electronZoom from '@fe/plugins/electron-zoom'
|
||||
@@ -70,6 +71,7 @@ import dropToOpenFile from '@fe/plugins/drop-to-open-file'
|
||||
import recordRecentDocument from '@fe/plugins/record-recent-document'
|
||||
|
||||
export default [
|
||||
buildInRenderers,
|
||||
customStyles,
|
||||
customKeybindings,
|
||||
electronZoom,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Plugin } from '@fe/context'
|
||||
|
||||
export default {
|
||||
name: 'build-in-renderers',
|
||||
register: (ctx) => {
|
||||
ctx.renderer.registerRenderer({
|
||||
name: 'not-markdown-file',
|
||||
order: 999999999,
|
||||
when (env) {
|
||||
return !env.file || !ctx.doc.isMarkdownFile(env.file)
|
||||
},
|
||||
render (_, env) {
|
||||
const { h } = ctx.lib.vue
|
||||
return [
|
||||
h('h1', env.file?.name),
|
||||
h('p', [h('em', 'Not a markdown file.')])
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
ctx.renderer.registerRenderer({
|
||||
name: 'markdown',
|
||||
order: -255,
|
||||
when (env) {
|
||||
return !!(env.file && ctx.doc.isMarkdownFile(env.file))
|
||||
},
|
||||
render (src, env) {
|
||||
return ctx.markdown.markdown.render(src, env)
|
||||
},
|
||||
})
|
||||
|
||||
ctx.renderer.registerRenderer({
|
||||
name: 'html',
|
||||
when (env) {
|
||||
return !!(env.file && !env.safeMode && env.file.path.toLowerCase().endsWith('.html'))
|
||||
},
|
||||
render (src) {
|
||||
const iframeProps = { style: `background: #fff; position: fixed; left: 0; top: 0; height: var(${ctx.args.CSS_VAR_NAME.PREVIEWER_HEIGHT})` }
|
||||
return ctx.lib.vue.h(ctx.embed.IFrame, { html: src, triggerParentKeyBoardEvent: true, iframeProps })
|
||||
},
|
||||
})
|
||||
}
|
||||
} as Plugin
|
||||
@@ -208,7 +208,7 @@ export default {
|
||||
items.push({
|
||||
id: 'plugin.image-hosting-picgo.upload-single-image',
|
||||
type: 'normal',
|
||||
ellipsis: true,
|
||||
ellipsis: false,
|
||||
label: ctx.i18n.t('upload-image') + ' (PicGo)',
|
||||
onClick: async () => {
|
||||
try {
|
||||
|
||||
@@ -21,7 +21,7 @@ const comment = '(?:<!(--)|(--)>)'
|
||||
|
||||
const HTML_TAG_RE = new RegExp('^(?:' + comment + '|' + openTag + '|' + closeTag + ')')
|
||||
const HTML_SELF_CLOSE_TAG_RE = new RegExp('^' + selfCloseTag, 'i')
|
||||
const INVALID_HTML_TAG_NAME_RE = /script|style/i
|
||||
const INVALID_HTML_TAG_NAME_RE = /script/i
|
||||
const INVALID_ATTR_NAME_RE = /^on|^xmlns$|^xml$|^aria-|^srcdoc$/i
|
||||
|
||||
const SAFE_MODE_ALLOWED_TAGS = [
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { Plugin } from '@fe/context'
|
||||
import type Token from 'markdown-it/lib/token'
|
||||
import styles from 'katex/dist/katex.min.css?inline'
|
||||
import monacoLatex from '@fe/others/monaco-latex'
|
||||
import { getRenderCache } from '@fe/services/markdown'
|
||||
import { getRenderCache } from '@fe/services/renderer'
|
||||
import { triggerHook } from '@fe/core/hook'
|
||||
|
||||
// Test if potential opening or closing delimieter
|
||||
|
||||
@@ -10,6 +10,8 @@ import { basename, dirname, join, resolve } from '@fe/utils/path'
|
||||
import { switchDoc } from '@fe/services/document'
|
||||
import { getAttachmentURL, getRepo, openExternal, openPath } from '@fe/services/base'
|
||||
import { getRenderIframe } from '@fe/services/view'
|
||||
import { getAllCustomEditors } from '@fe/services/editor'
|
||||
import type { Doc } from '@share/types'
|
||||
|
||||
async function getElement (id: string) {
|
||||
id = id.replaceAll('%28', '(').replaceAll('%29', ')')
|
||||
@@ -88,20 +90,20 @@ function handleLink (link: HTMLAnchorElement): boolean {
|
||||
el.classList.remove(DOM_CLASS_NAME.PREVIEW_HIGHLIGHT)
|
||||
}
|
||||
|
||||
if (/(\.md$|\.md#)/.test(href)) { // markdown file
|
||||
const tmp = decodeURI(href).split('#')
|
||||
const tmp = decodeURI(href).split('#')
|
||||
|
||||
let path = tmp[0]
|
||||
if (!path.startsWith('/')) { // to absolute path
|
||||
path = join(dirname(filePath || ''), path)
|
||||
}
|
||||
let path = tmp[0]
|
||||
if (!path.startsWith('/')) { // to absolute path
|
||||
path = join(dirname(filePath || ''), path)
|
||||
}
|
||||
|
||||
switchDoc({
|
||||
path,
|
||||
name: basename(path),
|
||||
repo: fileRepo,
|
||||
type: 'file'
|
||||
}).then(async () => {
|
||||
const file: Doc = { path, type: 'file', name: basename(path), repo: fileRepo }
|
||||
|
||||
const isMarkdownFile = /(\.md$|\.md#)/.test(href)
|
||||
const supportOpenDirectly = isMarkdownFile || getAllCustomEditors().some(x => x.when?.({ doc: file }))
|
||||
|
||||
if (supportOpenDirectly) {
|
||||
switchDoc(file).then(async () => {
|
||||
const hash = tmp.slice(1).join('#')
|
||||
// jump anchor
|
||||
if (hash) {
|
||||
@@ -133,7 +135,7 @@ function handleLink (link: HTMLAnchorElement): boolean {
|
||||
}
|
||||
|
||||
function convertLink (state: StateCore) {
|
||||
const tags = ['audio', 'img', 'source', 'video', 'track', 'a', 'iframe']
|
||||
const tags = ['audio', 'img', 'source', 'video', 'track', 'a', 'iframe', 'embed']
|
||||
|
||||
const { repo, path, name } = state.env.file || {}
|
||||
if (!repo || !path || !name) {
|
||||
@@ -166,6 +168,12 @@ function convertLink (state: StateCore) {
|
||||
return
|
||||
}
|
||||
|
||||
// support custom editor
|
||||
const file: Doc = { path: resolve(basePath, attrVal), type: 'file', name: fileName, repo }
|
||||
if (getAllCustomEditors().some(x => x.when?.({ doc: file }))) {
|
||||
return
|
||||
}
|
||||
|
||||
// open other file in os
|
||||
token.attrJoin('class', DOM_CLASS_NAME.MARK_OPEN)
|
||||
} else {
|
||||
|
||||
@@ -309,7 +309,7 @@ export default {
|
||||
return false
|
||||
}
|
||||
|
||||
const cache = ctx.markdown.getRenderCache('plugin-macro', 'cache', { $define: {} } as CacheItem)
|
||||
const cache = ctx.renderer.getRenderCache('plugin-macro', 'cache', { $define: {} } as CacheItem)
|
||||
|
||||
const options = {
|
||||
purchased: getPurchased() || file.repo === ctx.args.HELP_REPO_NAME,
|
||||
|
||||
@@ -15,7 +15,7 @@ export default {
|
||||
const lock = new AsyncLock()
|
||||
|
||||
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
|
||||
let handler: Awaited<ReturnType<typeof ctx.api.watchFile>> | null = null
|
||||
let handler: Awaited<ReturnType<typeof ctx.api.watchFs>> | null = null
|
||||
|
||||
function stopWatch () {
|
||||
logger.debug('stopWatch', !!handler)
|
||||
@@ -33,7 +33,7 @@ export default {
|
||||
|
||||
const { repo, path } = doc
|
||||
logger.debug('startWatch', repo, path)
|
||||
const watchHandler = await ctx.api.watchFile(
|
||||
const watchHandler = await ctx.api.watchFs(
|
||||
repo,
|
||||
path,
|
||||
{ awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 50 }, alwaysStat: true },
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useModal } from '@fe/support/ui/modal'
|
||||
import { useToast } from '@fe/support/ui/toast'
|
||||
import store from '@fe/support/store'
|
||||
import type { Doc, PathItem } from '@fe/types'
|
||||
import { basename, dirname, extname, isBelongTo, join, normalizeSep, relative } from '@fe/utils/path'
|
||||
import { basename, dirname, extname, isBelongTo, join, normalizeSep, relative, resolve } from '@fe/utils/path'
|
||||
import { getActionHandler } from '@fe/core/action'
|
||||
import { triggerHook } from '@fe/core/hook'
|
||||
import { FLAG_MAS, HELP_REPO_NAME } from '@fe/support/args'
|
||||
@@ -701,7 +701,7 @@ export async function switchDocByPath (path: string): Promise<void> {
|
||||
type: 'file',
|
||||
repo: repo.name,
|
||||
name: basename(path),
|
||||
path: relative(repo.path, path)
|
||||
path: resolve(relative(repo.path, path))
|
||||
})
|
||||
} else {
|
||||
if (FLAG_MAS) {
|
||||
|
||||
@@ -527,6 +527,8 @@ registerHook('MONACO_BEFORE_INIT', ({ monaco }) => {
|
||||
lang.aliases?.push('node')
|
||||
} else if (lang.id === 'shell') {
|
||||
lang.aliases?.push('bash')
|
||||
} else if (lang.id === 'html') {
|
||||
lang.aliases?.push('vue')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import MarkdownItMark from 'markdown-it-mark'
|
||||
import MarkdownItAbbr from 'markdown-it-abbr'
|
||||
import MarkdownItAttributes from 'markdown-it-attributes'
|
||||
import MarkdownItMultimdTable from 'markdown-it-multimd-table'
|
||||
import { registerHook, triggerHook } from '@fe/core/hook'
|
||||
import { triggerHook } from '@fe/core/hook'
|
||||
import { getSetting } from './setting'
|
||||
|
||||
/**
|
||||
@@ -22,60 +22,10 @@ export function registerPlugin (plugin: (md: Markdown, ...args: any) => void, pa
|
||||
markdown.use(plugin, params)
|
||||
}
|
||||
|
||||
const renderCache: Map<string, Map<string, any>> = new Map()
|
||||
|
||||
/**
|
||||
* Get render cache
|
||||
* @param domain
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
export function getRenderCache (domain: string): Map<string, any>
|
||||
export function getRenderCache<T> (domain: string, key: string, fallback?: T | (() => T)): T
|
||||
export function getRenderCache (domain: string, key?: string, fallback?: any) {
|
||||
if (!domain) {
|
||||
throw new Error('Domain is required')
|
||||
}
|
||||
|
||||
if (!renderCache.has(domain)) {
|
||||
renderCache.set(domain, new Map())
|
||||
}
|
||||
|
||||
const cache = renderCache.get(domain)!
|
||||
|
||||
if (!key) {
|
||||
return cache
|
||||
}
|
||||
|
||||
const value = cache.get(key)
|
||||
if (value) {
|
||||
return value
|
||||
}
|
||||
|
||||
const newValue = typeof fallback === 'function' ? fallback() : fallback
|
||||
cache.set(key, newValue)
|
||||
return newValue
|
||||
}
|
||||
|
||||
registerHook('VIEW_BEFORE_REFRESH', () => {
|
||||
renderCache.clear()
|
||||
})
|
||||
|
||||
const render = markdown.render
|
||||
markdown.render = (src: string, env?: any) => {
|
||||
triggerHook('MARKDOWN_BEFORE_RENDER', { src, env, md: markdown })
|
||||
|
||||
// build render cache
|
||||
if (env.file) {
|
||||
const cacheKey = `__file_${env.file.repo}:${env.file.path}`
|
||||
if (!renderCache.has(cacheKey)) {
|
||||
renderCache.clear()
|
||||
renderCache.set(cacheKey, new Map())
|
||||
}
|
||||
} else {
|
||||
renderCache.clear()
|
||||
}
|
||||
|
||||
markdown.options.html = getSetting('render.md-html', true)
|
||||
markdown.options.breaks = getSetting('render.md-breaks', true)
|
||||
markdown.options.linkify = getSetting('render.md-linkify', true)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { VNode } from 'vue'
|
||||
import { registerHook } from '@fe/core/hook'
|
||||
import type { RenderEnv, Renderer } from '@fe/types'
|
||||
import * as ioc from '@fe/core/ioc'
|
||||
|
||||
const renderCache: Map<string, Map<string, any>> = new Map()
|
||||
|
||||
registerHook('VIEW_BEFORE_REFRESH', () => {
|
||||
renderCache.clear()
|
||||
})
|
||||
|
||||
/**
|
||||
* Get render cache
|
||||
* @param domain
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
export function getRenderCache (domain: string): Map<string, any>
|
||||
export function getRenderCache<T> (domain: string, key: string, fallback?: T | (() => T)): T
|
||||
export function getRenderCache (domain: string, key?: string, fallback?: any) {
|
||||
if (!domain) {
|
||||
throw new Error('Domain is required')
|
||||
}
|
||||
|
||||
if (!renderCache.has(domain)) {
|
||||
renderCache.set(domain, new Map())
|
||||
}
|
||||
|
||||
const cache = renderCache.get(domain)!
|
||||
|
||||
if (!key) {
|
||||
return cache
|
||||
}
|
||||
|
||||
const value = cache.get(key)
|
||||
if (value) {
|
||||
return value
|
||||
}
|
||||
|
||||
const newValue = typeof fallback === 'function' ? fallback() : fallback
|
||||
cache.set(key, newValue)
|
||||
return newValue
|
||||
}
|
||||
|
||||
export function render (src: string, env: RenderEnv): VNode | VNode[] | string {
|
||||
// build render cache
|
||||
if (env.file) {
|
||||
const cacheKey = `__file_${env.file.repo}:${env.file.path}`
|
||||
if (!renderCache.has(cacheKey)) {
|
||||
renderCache.clear()
|
||||
renderCache.set(cacheKey, new Map())
|
||||
}
|
||||
} else {
|
||||
renderCache.clear()
|
||||
}
|
||||
|
||||
const renderer = ioc.get('RENDERERS').find(x => x.when(env))
|
||||
|
||||
if (!renderer) {
|
||||
throw new Error('No renderer found')
|
||||
}
|
||||
|
||||
return renderer.render(src, env)
|
||||
}
|
||||
|
||||
export function registerRenderer (renderer: Renderer) {
|
||||
ioc.register('RENDERERS', renderer)
|
||||
|
||||
// sort renderers by order
|
||||
ioc.getRaw('RENDERERS')?.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
}
|
||||
|
||||
export function removeRenderer (name: string) {
|
||||
ioc.removeWhen('RENDERERS', item => item.name === name)
|
||||
}
|
||||
@@ -324,16 +324,16 @@ export async function search (controller: AbortController, query: ITextQuery): P
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch a file.
|
||||
* Watch file or dir.
|
||||
* @param controller
|
||||
* @param query
|
||||
* @returns
|
||||
*/
|
||||
export async function watchFile (
|
||||
export async function watchFs (
|
||||
repo: string,
|
||||
path: string,
|
||||
options: WatchOptions,
|
||||
onResult: (result: { eventName: 'add' | 'change' | 'unlink', path: string, stats?: Stats }) => void,
|
||||
onResult: (result: { eventName: 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir', path: string, stats?: Stats }) => void,
|
||||
onError: (error: Error) => void
|
||||
) {
|
||||
const controller: AbortController = new AbortController()
|
||||
|
||||
@@ -25,6 +25,10 @@ export const HELP_REPO_NAME = '__help__'
|
||||
|
||||
export const MONACO_EDITOR_NLS = { de: 'Deutsch', es: 'Español', fr: 'Français', it: 'Italiano', ja: '日本語', ko: '한국어', ru: 'Русский', 'zh-cn': '简体中文' }
|
||||
|
||||
export const CSS_VAR_NAME = {
|
||||
PREVIEWER_HEIGHT: '--previewer-height'
|
||||
}
|
||||
|
||||
export const DOM_ATTR_NAME = {
|
||||
SOURCE_LINE_START: 'data-source-line',
|
||||
SOURCE_LINE_END: 'data-source-line-end',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { VNode } from 'vue'
|
||||
import type { Language, MsgPath } from '@share/i18n'
|
||||
import type { Doc, FileItem, Repo } from '@share/types'
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
@@ -502,6 +503,13 @@ export type CustomEditor = {
|
||||
getIsDirty?: () => boolean | Promise<boolean>,
|
||||
}
|
||||
|
||||
export type Renderer = {
|
||||
name: string,
|
||||
order?: number,
|
||||
when: (env: RenderEnv) => boolean | Promise<boolean>,
|
||||
render (src: string, env: RenderEnv): string | VNode | VNode[]
|
||||
}
|
||||
|
||||
export interface CodeRunner {
|
||||
name: string;
|
||||
order?: number;
|
||||
@@ -527,6 +535,7 @@ export type BuildInIOCTypes = { [key in keyof BuildInHookTypes]: any; } & {
|
||||
THEME_STYLES: any;
|
||||
VIEW_PREVIEWER: Previewer;
|
||||
EDITOR_CUSTOM_EDITOR: CustomEditor,
|
||||
RENDERERS: Renderer,
|
||||
CODE_RUNNER: CodeRunner;
|
||||
}
|
||||
|
||||
|
||||
@@ -9063,10 +9063,10 @@ viewerjs@^1.11.0:
|
||||
resolved "https://registry.yarnpkg.com/viewerjs/-/viewerjs-1.11.0.tgz#eda25d96998356b4576fe25ef8d7cd6fd795a374"
|
||||
integrity sha512-TE3XvfEXqKV+LdHsRdMSPsCaq0PfKDXm4SjGzfty7CzVxlDbBbT4x38vAqfXfNITmHhHUQefdxdg0Vv4ia+Z9w==
|
||||
|
||||
vite@^4.5.1:
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.1.tgz#3370986e1ed5dbabbf35a6c2e1fb1e18555b968a"
|
||||
integrity sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==
|
||||
vite@^4.5.2:
|
||||
version "4.5.2"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.2.tgz#d6ea8610e099851dad8c7371599969e0f8b97e82"
|
||||
integrity sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==
|
||||
dependencies:
|
||||
esbuild "^0.18.10"
|
||||
postcss "^8.4.27"
|
||||
|
||||
Reference in New Issue
Block a user