From 77461658ad205386637283a16183a58f9aa63e76 Mon Sep 17 00:00:00 2001 From: purocean Date: Thu, 23 May 2024 15:36:46 +0800 Subject: [PATCH 01/26] fix: add CSS rule to avoid page break inside code blocks --- src/renderer/plugins/markdown-code-wrap.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/renderer/plugins/markdown-code-wrap.ts b/src/renderer/plugins/markdown-code-wrap.ts index 0e5a5bf42..87cb35a97 100644 --- a/src/renderer/plugins/markdown-code-wrap.ts +++ b/src/renderer/plugins/markdown-code-wrap.ts @@ -11,6 +11,11 @@ export default { overflow-wrap: anywhere; } + .markdown-view .markdown-body pre > code.${ctx.args.DOM_CLASS_NAME.AVOID_PAGE_BREAK} { + page-break-inside: avoid; + display: block; + } + @media print { .markdown-view .markdown-body pre > code, .markdown-view .markdown-body .p-mcr-run-code-result { From 0dfd4a0b034dd0a06c3fae0c4b7b2ff53213196d Mon Sep 17 00:00:00 2001 From: purocean Date: Wed, 29 May 2024 11:10:03 +0800 Subject: [PATCH 02/26] chore: add error handling for load node-pty package --- src/main/server/index.ts | 52 ++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/src/main/server/index.ts b/src/main/server/index.ts index c99a7719c..941d1051e 100644 --- a/src/main/server/index.ts +++ b/src/main/server/index.ts @@ -2,6 +2,7 @@ import * as os from 'os' import ip from 'ip' import * as fs from 'fs-extra' import uniq from 'lodash/uniq' +import type NodePty from 'node-pty' import isEqual from 'lodash/isEqual' import * as path from 'path' import Koa from 'koa' @@ -693,7 +694,14 @@ const server = (port = 3000) => { // eslint-disable-next-line @typescript-eslint/no-var-requires const io = require('socket.io')(server, { path: '/ws' }) // eslint-disable-next-line @typescript-eslint/no-var-requires - const pty = require('node-pty') + + let pty: typeof NodePty | null = null + + try { + pty = require('node-pty') + } catch (error) { + console.error(error) + } io.on('connection', (socket: any) => { if (!isLocalhost(socket.client.conn.remoteAddress)) { @@ -701,25 +709,29 @@ const server = (port = 3000) => { return } - const ptyProcess = pty.spawn(shell.getShell(), [], { - name: 'xterm-color', - cols: 80, - rows: 24, - cwd: socket.handshake.query.cwd || HOME_DIR, - env: process.env, - useConpty: false, - }) - ptyProcess.onData((data: any) => socket.emit('output', data)) - ptyProcess.onExit(() => socket.disconnect()) - socket.on('input', (data: any) => { - if (data.startsWith(shell.CD_COMMAND_PREFIX)) { - ptyProcess.write(shell.transformCdCommand(data.toString())) - } else { - ptyProcess.write(data) - } - }) - socket.on('resize', (size: any) => ptyProcess.resize(size[0], size[1])) - socket.on('disconnect', () => ptyProcess.kill()) + if (pty) { + const ptyProcess = pty.spawn(shell.getShell(), [], { + name: 'xterm-color', + cols: 80, + rows: 24, + cwd: socket.handshake.query.cwd || HOME_DIR, + env: process.env, + useConpty: false, + }) + ptyProcess.onData((data: any) => socket.emit('output', data)) + ptyProcess.onExit(() => socket.disconnect()) + socket.on('input', (data: any) => { + if (data.startsWith(shell.CD_COMMAND_PREFIX)) { + ptyProcess.write(shell.transformCdCommand(data.toString())) + } else { + ptyProcess.write(data) + } + }) + socket.on('resize', (size: any) => ptyProcess.resize(size[0], size[1])) + socket.on('disconnect', () => ptyProcess.kill()) + } else { + socket.emit('output', 'node-pty is not compatible with this platform. Please install another version from GitHub https://github.com/purocean/yn/releases') + } }) const host = config.get('server.host', 'localhost') From f0f4cedf55bae2ff18710da9ebf7b87261111eb5 Mon Sep 17 00:00:00 2001 From: purocean Date: Wed, 22 May 2024 18:27:30 +0800 Subject: [PATCH 03/26] feat: add support for additional file extensions This commit adds support for the following file extensions: - .bib - .plantuml - .dot - .gv - .puml These extensions are now recognized by the application. --- src/renderer/others/file-extensions.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/renderer/others/file-extensions.ts b/src/renderer/others/file-extensions.ts index 03ea8dbf8..eb12f8899 100644 --- a/src/renderer/others/file-extensions.ts +++ b/src/renderer/others/file-extensions.ts @@ -141,7 +141,12 @@ const extensions = [ '.azcli', '.cls', '.graphql', - '.gql' + '.gql', + '.bib', + '.plantuml', + '.dot', + '.gv', + '.puml', ] const supported = (name: string) => { From 872ff652ce2aa101d614b43f43a326be7354df31 Mon Sep 17 00:00:00 2001 From: purocean Date: Mon, 3 Jun 2024 21:04:07 +0800 Subject: [PATCH 04/26] feat: upgrade electron --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 048eda4d5..d11ec6247 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "canvas-confetti": "^1.6.0", "crypto-js": "^4.2.0", "dom-to-image": "^2.6.0", - "electron": "28.3.1", + "electron": "28.3.3", "electron-builder": "^23.6.0", "eslint": "^8.56.0", "eslint-plugin-import": "^2.29.1", diff --git a/yarn.lock b/yarn.lock index 78696b84e..b3e8f2802 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4021,10 +4021,10 @@ electron-updater@^6.1.1: semver "^7.3.8" typed-emitter "^2.1.0" -electron@28.3.1: - version "28.3.1" - resolved "https://registry.yarnpkg.com/electron/-/electron-28.3.1.tgz#babb3ff8e246336e9cd1c1966f16a55ba723ea06" - integrity sha512-aF9fONuhVDJlctJS7YOw76ynxVAQdfIWmlhRMKits24tDcdSL0eMHUS0wWYiRfGWbQnUKB6V49Rf17o32f4/fg== +electron@28.3.3: + version "28.3.3" + resolved "https://registry.yarnpkg.com/electron/-/electron-28.3.3.tgz#2df898f653c4f77b66b4cf3eeba79d8bea6d03c0" + integrity sha512-ObKMLSPNhomtCOBAxFS8P2DW/4umkh72ouZUlUKzXGtYuPzgr1SYhskhFWgzAsPtUzhL2CzyV2sfbHcEW4CXqw== dependencies: "@electron/get" "^2.0.0" "@types/node" "^18.11.18" From e001626cfe411612a019112d84f4c897198498fd Mon Sep 17 00:00:00 2001 From: purocean Date: Mon, 3 Jun 2024 21:04:21 +0800 Subject: [PATCH 05/26] chore: bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d11ec6247..40e3f617b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "yank.note", - "version": "3.70.4", + "version": "3.71.0", "description": "Yank Note: A highly extensible Markdown editor, designed for productivity.", "main": "dist/main/app.js", "license": "AGPL-3.0", From fb93783d121c9e932530402cbbb49bc201aa1495 Mon Sep 17 00:00:00 2001 From: purocean Date: Tue, 11 Jun 2024 12:40:06 +0800 Subject: [PATCH 06/26] feat: add font ligatures setting to editor --- .node-version | 1 + src/renderer/others/setting-schema.ts | 8 ++++++++ src/renderer/services/editor.ts | 1 + src/renderer/types.ts | 1 + src/share/i18n/languages/en.ts | 1 + src/share/i18n/languages/zh-CN.ts | 1 + src/share/i18n/languages/zh-TW.ts | 1 + 7 files changed, 14 insertions(+) create mode 100644 .node-version diff --git a/.node-version b/.node-version new file mode 100644 index 000000000..fb3e6603b --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +v18.20.2 diff --git a/src/renderer/others/setting-schema.ts b/src/renderer/others/setting-schema.ts index 8bf3b5d84..90fcd4311 100644 --- a/src/renderer/others/setting-schema.ts +++ b/src/renderer/others/setting-schema.ts @@ -200,6 +200,14 @@ const schema: SettingSchema = ({ inputAttributes: { placeholder: 'e.g., \'Courier New\', monospace' } }, }, + 'editor.font-ligatures': { + defaultValue: false, + title: 'T_setting-panel.schema.editor.font-ligatures', + type: 'boolean', + format: 'checkbox', + group: 'editor', + required: true, + }, 'editor.mouse-wheel-zoom': { defaultValue: true, title: 'T_setting-panel.schema.editor.mouse-wheel-zoom', diff --git a/src/renderer/services/editor.ts b/src/renderer/services/editor.ts index 4eef064a5..0d9019798 100644 --- a/src/renderer/services/editor.ts +++ b/src/renderer/services/editor.ts @@ -92,6 +92,7 @@ export const getDefaultOptions = (): Monaco.editor.IStandaloneEditorConstruction renderLineHighlight: 'all', stickyScroll: { enabled: getSetting('editor.sticky-scroll-enabled', true) }, lightbulb: { enabled: 'on' as any }, + fontLigatures: getSetting('editor.font-ligatures', false), wordSeparators: '`~!@#$%^&*()-=+[{]}\\|;:\'",.<>/?。?!,、;:“”‘’()《》〈〉【】『』「」﹃﹄〔〕' }) diff --git a/src/renderer/types.ts b/src/renderer/types.ts index dff35e95c..f56e01021 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -311,6 +311,7 @@ export interface BuildInSettings { 'envs': string, 'editor.mouse-wheel-zoom': boolean, 'editor.font-size': number, + 'editor.font-ligatures': boolean, 'editor.tab-size': 2 | 4, 'editor.ordered-list-completion': 'auto' | 'increase' | 'one', 'editor.minimap': boolean, diff --git a/src/share/i18n/languages/en.ts b/src/share/i18n/languages/en.ts index f48f1c0ed..4a33ae2c9 100644 --- a/src/share/i18n/languages/en.ts +++ b/src/share/i18n/languages/en.ts @@ -384,6 +384,7 @@ const data = { 'line-numbers': 'Line Numbers', 'enable-preview': 'Enable Preview - Open new files using temporary tabs, double click the tab to change', 'font-family': 'Font Family', + 'font-ligatures': 'Font Ligatures', 'complete-emoji': 'Complete Emoji - Input : to display emoji list', 'todo-with-time': 'Add time when checking todo', 'suggest-on-trigger-characters': 'Suggest on trigger characters - You can also use Ctrl+Space (depending on the shortcuts) to trigger', diff --git a/src/share/i18n/languages/zh-CN.ts b/src/share/i18n/languages/zh-CN.ts index 4c8419dd6..ed62e665f 100644 --- a/src/share/i18n/languages/zh-CN.ts +++ b/src/share/i18n/languages/zh-CN.ts @@ -375,6 +375,7 @@ const data: BaseLanguage = { 'line-numbers': '行号', 'enable-preview': '开启预览 - 打开新文件使用临时标签,双击标签以更改', 'font-family': '字体', + 'font-ligatures': '字体连字', 'complete-emoji': '自动补全 Emoji - 输入 : 时显示 Emoji 列表', 'todo-with-time': '勾选待办事项时自动添加时间', 'suggest-on-trigger-characters': '自动提示 - 输入提示符时显示提示。如果禁用,仍可使用 Ctrl+Space (取决于快捷键配置) 触发', diff --git a/src/share/i18n/languages/zh-TW.ts b/src/share/i18n/languages/zh-TW.ts index c3baf395d..f6ccdf151 100644 --- a/src/share/i18n/languages/zh-TW.ts +++ b/src/share/i18n/languages/zh-TW.ts @@ -375,6 +375,7 @@ const data: BaseLanguage = { 'line-numbers': '行號', 'enable-preview': '開啟預覽 - 開啟新檔案使用暫存標籤,雙擊標籤以變更', 'font-family': '字體', + 'font-ligatures': '字體連字', 'complete-emoji': '自動補全 Emoji - 輸入 : 時顯示 Emoji 清單', 'todo-with-time': '勾選待辦事項時自動加入時間', 'suggest-on-trigger-characters': '自動提示 - 輸入提示符號時顯示提示。若停用,仍可使用 Ctrl+Space (依據快速鍵設定) 觸發', From c1d76679264b7671981cfb3946dd9a31c91412a7 Mon Sep 17 00:00:00 2001 From: purocean Date: Tue, 11 Jun 2024 14:21:12 +0800 Subject: [PATCH 07/26] refactor: improve code completion logic in MdSyntaxCompletionProvider --- src/renderer/plugins/editor-md-syntax.ts | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/renderer/plugins/editor-md-syntax.ts b/src/renderer/plugins/editor-md-syntax.ts index ccc5d913f..7342126ce 100644 --- a/src/renderer/plugins/editor-md-syntax.ts +++ b/src/renderer/plugins/editor-md-syntax.ts @@ -76,7 +76,37 @@ class MdSyntaxCompletionProvider implements Monaco.languages.CompletionItemProvi return 0 } + private async provideSelectionCompletionItems (selection: Monaco.Selection): Promise { + const items = this.ctx.editor.getSimpleCompletionItems().filter(item => item.insertText.includes('${TM_SELECTED_TEXT}')) + + const result: Monaco.languages.CompletionItem[] = items.map((item, i) => { + const range = new this.monaco.Range( + selection.startLineNumber, + selection.startColumn, + selection.endLineNumber, + selection.endColumn, + ) + + return { + label: { label: item.label }, + kind: item.kind || this.monaco.languages.CompletionItemKind.Keyword, + insertText: item.insertText, + insertTextRules: this.monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + sortText: i.toString().padStart(7), + detail: item.detail, + } + }) + + return { suggestions: result } + } + public async provideCompletionItems (model: Monaco.editor.IModel, position: Monaco.Position): Promise { + const selection = this.ctx.editor.getEditor().getSelection()! + if (!selection.isEmpty()) { + return this.provideSelectionCompletionItems(selection) + } + const line = model.getLineContent(position.lineNumber) const cursor = position.column - 1 const linePrefixText = line.slice(0, cursor) From 5491df64a825afc3e6caffd3d6735dc10a8c138b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B4=8B=E5=AD=90?= Date: Tue, 11 Jun 2024 14:46:18 +0800 Subject: [PATCH 08/26] Update release.yml --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e1ed1e6f..275b798ab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -133,7 +133,7 @@ jobs: coscmd upload -r out / --include out/Yank-Note*.*,out/latest*.yml - name: GH Release - uses: softprops/action-gh-release@v0.1.5 + uses: softprops/action-gh-release@v2.0.5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: From 2845f0509103accef921afe09af10bbfbedcf730 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Jun 2024 06:46:50 +0000 Subject: [PATCH 09/26] chore(deps): bump braces from 3.0.2 to 3.0.3 Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3. - [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3) --- updated-dependencies: - dependency-name: braces dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 47 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/yarn.lock b/yarn.lock index b3e8f2802..d93456341 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2821,11 +2821,11 @@ brace-expansion@^2.0.1: balanced-match "^1.0.0" braces@^3.0.1, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" browserslist@^4.17.5: version "4.19.1" @@ -4683,10 +4683,10 @@ filenamify@^5.1.0: strip-outer "^2.0.0" trim-repeated "^2.0.0" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -8405,7 +8405,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8473,7 +8482,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -8487,6 +8496,13 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -9233,7 +9249,7 @@ wide-align@^1.1.5: dependencies: string-width "^1.0.2 || 2 || 3 || 4" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -9251,6 +9267,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 2e478f0bbdae547cee925ea191e817340bc58169 Mon Sep 17 00:00:00 2001 From: purocean Date: Fri, 14 Jun 2024 20:07:06 +0800 Subject: [PATCH 10/26] feat: add support for Wiki Links in Markdown syntax This commit modifies the Markdown syntax in the editor to support Wiki Links. It adds a new token pattern for [[...]] syntax and updates the completion provider to handle Wiki Link contexts. Now, when a user types [[, the editor will suggest Wiki Link completions. The changes include: - Adding a new token pattern for [[...]] syntax - Updating the completion provider to handle Wiki Link contexts These modifications enhance the editing experience by allowing users to easily create Wiki Links within their Markdown documents. --- src/renderer/plugins/editor-md-syntax.ts | 2 ++ .../plugins/editor-path-completion.ts | 32 +++++++++++++++++-- src/renderer/services/markdown.ts | 31 ++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/src/renderer/plugins/editor-md-syntax.ts b/src/renderer/plugins/editor-md-syntax.ts index 7342126ce..7490926e2 100644 --- a/src/renderer/plugins/editor-md-syntax.ts +++ b/src/renderer/plugins/editor-md-syntax.ts @@ -199,6 +199,7 @@ export default { { label: '/ __ Bold', insertText: '__$1__' }, { label: '/ ~~ Delete', insertText: '~~$1~~' }, { label: '/ == Mark', insertText: '==$1==' }, + { label: '/ [[]] Wiki Link', insertText: '[[$1]]' }, { label: '/ ``` Fence', insertText: '```$1\n$2\n```\n' }, { label: '/ ||| Table', insertText: '| ${1:TH} | ${2:TH} | ${3:TH} |\n| -- | -- | -- |\n| TD | TD | TD |' }, { label: '/ ||| Small Table', insertText: '| ${1:TH} | ${2:TH} | ${3:TH} |\n| -- | -- | -- |\n| TD | TD | TD |\n{.small}' }, @@ -211,6 +212,7 @@ export default { ctx.editor.tapMarkdownMonarchLanguage(mdLanguage => { mdLanguage.tokenizer.root.unshift( [/==\S.*\S?==/, 'keyword'], + [/\[\[[^[\]]+\]\]/, 'string'], [/~\S[^~]*\S?~/, 'string'], [/\^\S[^^]*\S?\^/, 'string'], ) diff --git a/src/renderer/plugins/editor-path-completion.ts b/src/renderer/plugins/editor-path-completion.ts index 68e2f871a..6297282d2 100644 --- a/src/renderer/plugins/editor-path-completion.ts +++ b/src/renderer/plugins/editor-path-completion.ts @@ -9,6 +9,8 @@ import type Token from 'markdown-it/lib/token' enum CompletionContextKind { Link, // [...](|) + WikiLink, // [[|]] + ReferenceLink, // [...][|] LinkDefinition, // []: | // TODO: not implemented @@ -89,6 +91,7 @@ class CompletionProvider implements Monaco.languages.CompletionItemProvider { } case CompletionContextKind.LinkDefinition: + case CompletionContextKind.WikiLink: case CompletionContextKind.Link: { const items: Monaco.languages.CompletionItem[] = [] @@ -151,6 +154,9 @@ class CompletionProvider implements Monaco.languages.CompletionItemProvider { /// [...](...| private readonly linkStartPattern = /\[([^\]]*?)\]\(\s*([^\s()]*)$/ + /// [[...| + private readonly wikiLinkStartPattern = /\[\[\s*([^\s[\]]*)$/ + /// [...| private readonly referenceLinkStartPattern = /\[\s*([^\s[\]]*)$/ @@ -186,6 +192,19 @@ class CompletionProvider implements Monaco.languages.CompletionItemProvider { } } + const wikiLinkPrefixMatch = linePrefixText.match(this.wikiLinkStartPattern) + if (wikiLinkPrefixMatch) { + const prefix = wikiLinkPrefixMatch[1] + const suffix = lineSuffixText.match(/^[^\]]*/) + return { + kind: CompletionContextKind.WikiLink, + linkPrefix: prefix, + linkTextStartPosition: position.delta(0, -prefix.length), + linkSuffix: suffix ? suffix[0] : '', + anchorInfo: this.getAnchorContext(prefix), + } + } + const definitionLinkPrefixMatch = linePrefixText.match(this.definitionPattern) if (definitionLinkPrefixMatch) { const prefix = definitionLinkPrefixMatch[1] @@ -230,14 +249,21 @@ class CompletionProvider implements Monaco.languages.CompletionItemProvider { if (!anchorMatch) { return undefined } + + let beforeAnchor = anchorMatch[1] + + if (anchorMatch[1] && !this.ctx.utils.path.extname(beforeAnchor)) { + beforeAnchor += '.md' + } + return { - beforeAnchor: anchorMatch[1], + beforeAnchor, anchorPrefix: anchorMatch[2], } } private async * providePathSuggestions (position: Monaco.Position, context: CompletionContext): AsyncIterable { - const valueBeforeLastSlash = context.linkPrefix.substring(0, context.linkPrefix.lastIndexOf('/') + 1) || '.' // keep the last slash + const valueBeforeLastSlash = context.linkPrefix.substring(0, context.linkPrefix.lastIndexOf('/') + 1) // keep the last slash const currentFile = this.ctx.store.state.currentFile if (!currentFile) { @@ -246,7 +272,7 @@ class CompletionProvider implements Monaco.languages.CompletionItemProvider { const parentDir = this.ctx.utils.path.resolve( this.ctx.utils.path.dirname(currentFile.path), - valueBeforeLastSlash + valueBeforeLastSlash || '.' ) const pathSegmentStart = position.delta(0, valueBeforeLastSlash.length - context.linkPrefix.length) diff --git a/src/renderer/services/markdown.ts b/src/renderer/services/markdown.ts index 759159421..6b9fb242b 100644 --- a/src/renderer/services/markdown.ts +++ b/src/renderer/services/markdown.ts @@ -73,3 +73,34 @@ markdown.core.ruler.after('normalize', 'after_normalize', state => { state.env.tokens = state.tokens return true }) + +markdown.linkify.add('[[', { + validate: /^\s*([^[\]]+)\s*\]\]/, + normalize: (match) => { + const parts = match.raw.slice(2, -2).split('|') + const url = parts[0].trim() + + // external link + if (/^[a-zA-Z]{1,8}:\/\/.*/.test(url)) { + match.url = url + match.text = parts[1] || url + return + } + + const [path, hash] = url.split('#') + const hashStr = hash ? `#${hash}` : '' + + const name = parts[1] || (path.split('/').pop() + hashStr) + match.text = name || url + + // has extension name + if (/\.[^/]+$/.test(path)) { + match.url = url + } else if (path) { + match.url = `${path}.md${hashStr}` + } else { + match.url = hashStr + match.text = name || hash || url + } + } +}) From 880b5b5fa5caf1ad09b495612ea0e6440192af6b Mon Sep 17 00:00:00 2001 From: purocean Date: Fri, 14 Jun 2024 20:18:05 +0800 Subject: [PATCH 11/26] refactor: fix link text in markdown.ts --- src/renderer/services/markdown.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/services/markdown.ts b/src/renderer/services/markdown.ts index 6b9fb242b..f226cd5fb 100644 --- a/src/renderer/services/markdown.ts +++ b/src/renderer/services/markdown.ts @@ -100,7 +100,7 @@ markdown.linkify.add('[[', { match.url = `${path}.md${hashStr}` } else { match.url = hashStr - match.text = name || hash || url + match.text = parts[1] || hash || url } } }) From fb758c22d0907c3ee874415432b1165ddbcaf546 Mon Sep 17 00:00:00 2001 From: purocean Date: Fri, 14 Jun 2024 20:44:20 +0800 Subject: [PATCH 12/26] refactor: improve Markdown syntax for predefined keywords --- src/renderer/plugins/editor-md-syntax.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/plugins/editor-md-syntax.ts b/src/renderer/plugins/editor-md-syntax.ts index 7490926e2..45b2feb4f 100644 --- a/src/renderer/plugins/editor-md-syntax.ts +++ b/src/renderer/plugins/editor-md-syntax.ts @@ -212,7 +212,7 @@ export default { ctx.editor.tapMarkdownMonarchLanguage(mdLanguage => { mdLanguage.tokenizer.root.unshift( [/==\S.*\S?==/, 'keyword'], - [/\[\[[^[\]]+\]\]/, 'string'], + [/(\[\[)([^[\]]+)(\]\])/, ['keyword.predefined', 'string', 'keyword.predefined']], [/~\S[^~]*\S?~/, 'string'], [/\^\S[^^]*\S?\^/, 'string'], ) From f2018cd82942fe5e94b19f292beb9d152374ad6c Mon Sep 17 00:00:00 2001 From: purocean Date: Sat, 15 Jun 2024 01:10:13 +0800 Subject: [PATCH 13/26] feat: add markdown-wiki-links plugin --- src/renderer/plugins.ts | 2 + src/renderer/plugins/markdown-wiki-links.ts | 66 +++++++++++++++++++++ src/renderer/services/markdown.ts | 31 ---------- 3 files changed, 68 insertions(+), 31 deletions(-) create mode 100644 src/renderer/plugins/markdown-wiki-links.ts diff --git a/src/renderer/plugins.ts b/src/renderer/plugins.ts index 580fbf985..213ab9d83 100644 --- a/src/renderer/plugins.ts +++ b/src/renderer/plugins.ts @@ -33,6 +33,7 @@ import switchTodo from '@fe/plugins/switch-todo' import imageViewer from '@fe/plugins/image-viewer' import emoji from '@fe/plugins/emoji' import getStarted from '@fe/plugins/get-started' +import markdownWikiLinks from '@fe/plugins/markdown-wiki-links' import markdownHtml from '@fe/plugins/markdown-html' import markdownRenderVnode from '@fe/plugins/markdown-render-vnode' import markdownMacro from '@fe/plugins/markdown-macro' @@ -106,6 +107,7 @@ export default [ imageViewer, emoji, getStarted, + markdownWikiLinks, markdownHtml, markdownRenderVnode, markdownMacro, diff --git a/src/renderer/plugins/markdown-wiki-links.ts b/src/renderer/plugins/markdown-wiki-links.ts new file mode 100644 index 000000000..06d7634eb --- /dev/null +++ b/src/renderer/plugins/markdown-wiki-links.ts @@ -0,0 +1,66 @@ +import { Plugin } from '@fe/context' +import type StateInline from 'markdown-it/lib/rules_inline/state_inline' + +const reMatch = /^\s*([^[#|]+)(?:#([^|]*))?(?:\|([^\]]*))?\s*$/ +const reExternalLink = /^[a-zA-Z]{1,8}:\/\/.*/ +const reExtName = /\.[^/]+$/ + +function wikiLinks (state: StateInline, silent?: boolean) { + // check [[ + if (state.src.charCodeAt(state.pos) !== 0x5B/* [ */ || state.src.charCodeAt(state.pos + 1) !== 0x5B/* [ */) { + return false + } + + const endPos = state.src.indexOf(']]', state.pos + 2) + if (endPos === -1 || endPos === state.pos + 2) { + return false + } + + const content = state.src.slice(state.pos + 2, endPos) + const parts = content.match(reMatch) + if (!parts) { + return false + } + + const link = parts[1].trim() + const hash = parts[2] || '' + const label = parts[3] || '' + + const hashStr = hash ? `#${hash}` : '' + + let url = link + hashStr + let text = label || url + + // internal link + if (!reExternalLink.test(link)) { + const fileName = link.split('/').pop() + text = label || (fileName ? fileName + hashStr : url) + + // no extension name + if (link && !reExtName.test(link)) { + url = `${link}.md${hashStr}` + } else if (!link) { + url = hashStr + text = label || hash || url + } + } + + if (!silent) { + state.push('link_open', 'a', 1).attrs = [['href', url]] + state.push('text', '', 0).content = text + state.push('link_close', 'a', -1) + } + + state.pos = endPos + 2 + + return true +} + +export default { + name: 'markdown-wiki-links', + register: ctx => { + ctx.markdown.registerPlugin(md => { + md.inline.ruler.after('link', 'wiki-links', wikiLinks) + }) + } +} satisfies Plugin diff --git a/src/renderer/services/markdown.ts b/src/renderer/services/markdown.ts index f226cd5fb..759159421 100644 --- a/src/renderer/services/markdown.ts +++ b/src/renderer/services/markdown.ts @@ -73,34 +73,3 @@ markdown.core.ruler.after('normalize', 'after_normalize', state => { state.env.tokens = state.tokens return true }) - -markdown.linkify.add('[[', { - validate: /^\s*([^[\]]+)\s*\]\]/, - normalize: (match) => { - const parts = match.raw.slice(2, -2).split('|') - const url = parts[0].trim() - - // external link - if (/^[a-zA-Z]{1,8}:\/\/.*/.test(url)) { - match.url = url - match.text = parts[1] || url - return - } - - const [path, hash] = url.split('#') - const hashStr = hash ? `#${hash}` : '' - - const name = parts[1] || (path.split('/').pop() + hashStr) - match.text = name || url - - // has extension name - if (/\.[^/]+$/.test(path)) { - match.url = url - } else if (path) { - match.url = `${path}.md${hashStr}` - } else { - match.url = hashStr - match.text = parts[1] || hash || url - } - } -}) From 89128087bf60fc94d9fef23587f59b73287d8494 Mon Sep 17 00:00:00 2001 From: purocean Date: Sat, 15 Jun 2024 01:19:48 +0800 Subject: [PATCH 14/26] refactor: fix regex pattern for markdown-wiki-links plugin --- src/renderer/plugins/markdown-wiki-links.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/plugins/markdown-wiki-links.ts b/src/renderer/plugins/markdown-wiki-links.ts index 06d7634eb..f859a1dd7 100644 --- a/src/renderer/plugins/markdown-wiki-links.ts +++ b/src/renderer/plugins/markdown-wiki-links.ts @@ -1,7 +1,7 @@ import { Plugin } from '@fe/context' import type StateInline from 'markdown-it/lib/rules_inline/state_inline' -const reMatch = /^\s*([^[#|]+)(?:#([^|]*))?(?:\|([^\]]*))?\s*$/ +const reMatch = /^\s*([^[#|]*)?(?:#([^|]*))?(?:\|([^\]]*))?\s*$/ const reExternalLink = /^[a-zA-Z]{1,8}:\/\/.*/ const reExtName = /\.[^/]+$/ @@ -22,7 +22,7 @@ function wikiLinks (state: StateInline, silent?: boolean) { return false } - const link = parts[1].trim() + const link = (parts[1] || '').trim() const hash = parts[2] || '' const label = parts[3] || '' From 5db7f564dbca6f726db2a5c6f91eca91dd78e950 Mon Sep 17 00:00:00 2001 From: purocean Date: Mon, 17 Jun 2024 19:20:30 +0800 Subject: [PATCH 15/26] feat: optimize wiki links use experience --- help/FEATURES.md | 1 + help/FEATURES_ZH-CN.md | 1 + src/renderer/others/setting-schema.ts | 8 ++ .../plugins/editor-path-completion.ts | 14 +++- src/renderer/plugins/markdown-link.ts | 76 +++++++++++++++++-- src/renderer/plugins/markdown-wiki-links.ts | 4 +- src/renderer/services/markdown.ts | 1 + src/renderer/support/args.ts | 1 + src/renderer/types.ts | 1 + src/share/i18n/languages/en.ts | 1 + src/share/i18n/languages/zh-CN.ts | 1 + src/share/i18n/languages/zh-TW.ts | 1 + 12 files changed, 100 insertions(+), 10 deletions(-) diff --git a/help/FEATURES.md b/help/FEATURES.md index b6219c590..03c78b9dd 100644 --- a/help/FEATURES.md +++ b/help/FEATURES.md @@ -69,6 +69,7 @@ Type '/' in the editor to get prompts *[HTML]: Hyper Text Markup Language *[W3C]: World Wide Web Consortium The HTML specification is maintained by the W3C. ++ Wiki Link: Supports using `[[filename#anchor|display text]]` syntax to link documents, such as [[README#Highlights|Highlights]] ## Github Alerts diff --git a/help/FEATURES_ZH-CN.md b/help/FEATURES_ZH-CN.md index 82248f619..7b3e7190a 100644 --- a/help/FEATURES_ZH-CN.md +++ b/help/FEATURES_ZH-CN.md @@ -70,6 +70,7 @@ define: *[HTML]: Hyper Text Markup Language *[W3C]: World Wide Web Consortium The HTML specification is maintained by the W3C. ++ Wiki 链接:支持使用 `[[文件名#锚点|显示文本]]` 语法来链接文档,如 [[README#Highlights|特色功能]] ## Github Alerts diff --git a/src/renderer/others/setting-schema.ts b/src/renderer/others/setting-schema.ts index 90fcd4311..f0ece2260 100644 --- a/src/renderer/others/setting-schema.ts +++ b/src/renderer/others/setting-schema.ts @@ -280,6 +280,14 @@ const schema: SettingSchema = ({ group: 'render', required: true, }, + 'render.md-wiki-links': { + defaultValue: true, + title: 'T_setting-panel.schema.render.md-wiki-links', + type: 'boolean', + format: 'checkbox', + group: 'render', + required: true, + }, 'render.md-typographer': { defaultValue: false, title: 'T_setting-panel.schema.render.md-typographer', diff --git a/src/renderer/plugins/editor-path-completion.ts b/src/renderer/plugins/editor-path-completion.ts index 6297282d2..c99deac4c 100644 --- a/src/renderer/plugins/editor-path-completion.ts +++ b/src/renderer/plugins/editor-path-completion.ts @@ -155,7 +155,7 @@ class CompletionProvider implements Monaco.languages.CompletionItemProvider { private readonly linkStartPattern = /\[([^\]]*?)\]\(\s*([^\s()]*)$/ /// [[...| - private readonly wikiLinkStartPattern = /\[\[\s*([^\s[\]]*)$/ + private readonly wikiLinkStartPattern = /\[\[\s*([^[\]]*)$/ /// [...| private readonly referenceLinkStartPattern = /\[\s*([^\s[\]]*)$/ @@ -297,10 +297,18 @@ class CompletionProvider implements Monaco.languages.CompletionItemProvider { for (const item of items) { i++ const isDir = item.type === 'dir' - const label = isDir ? item.name + '/' : item.name + let label = isDir ? item.name + '/' : item.name + let insertText = this.ctx.utils.encodeMarkdownLink(label) + + // Remove extension for wiki links + if (context.kind === CompletionContextKind.WikiLink) { + label = label.replace(/\.(md|markdown)$/, '') + insertText = label.replaceAll(']', ']').replaceAll('[', '[') + } + yield { label, - insertText: this.ctx.utils.encodeMarkdownLink(label), + insertText, kind: isDir ? this.monaco.languages.CompletionItemKind.Folder : this.monaco.languages.CompletionItemKind.File, range: { insert: insertRange, diff --git a/src/renderer/plugins/markdown-link.ts b/src/renderer/plugins/markdown-link.ts index d86f83b81..624b7c422 100644 --- a/src/renderer/plugins/markdown-link.ts +++ b/src/renderer/plugins/markdown-link.ts @@ -6,12 +6,14 @@ import { removeQuery, sleep } from '@fe/utils' import { isElectron, isWindows } from '@fe/support/env' import { useToast } from '@fe/support/ui/toast' import { DOM_ATTR_NAME, DOM_CLASS_NAME } from '@fe/support/args' -import { basename, dirname, join, resolve } from '@fe/utils/path' +import { basename, dirname, join, normalizeSep, 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 { fetchTree } from '@fe/support/api' import type { Doc } from '@share/types' +import type { Components } from '@fe/types' async function getElement (id: string) { id = id.replaceAll('%28', '(').replaceAll('%29', ')') @@ -28,6 +30,55 @@ async function getElement (id: string) { return _find(id) || _find(id.toUpperCase()) } +async function getFirstMatchPath (repo: string, dir: string, path: string) { + if (path.includes('/')) { + return path + } + + const findInDir = (items: Components.Tree.Node[]): string | null => { + for (const item of items) { + const p = normalizeSep(item.path) + if ( + item.type === 'file' && + (p === normalizeSep(join(dir, path)) || + p === normalizeSep(join(dir, `${path}.md`))) + ) { + return item.path + } + + if (item.children) { + const found = findInDir(item.children) + if (found) { + return found + } + } + } + + return null + } + + const findByName = (items: Components.Tree.Node[]): string | null => { + for (const item of items) { + if (item.type === 'file' && (item.name === path || item.name === `${path}.md`)) { + return item.path + } + + if (item.children) { + const found = findByName(item.children) + if (found) { + return found + } + } + } + + return null + } + + const tree = await fetchTree(repo, { by: 'mtime', order: 'desc' }) + + return findInDir(tree) || findByName(tree) +} + function getAnchorElement (target: HTMLElement) { let cur: HTMLElement | null = target while (cur && cur.tagName !== 'A' && cur.tagName !== 'ARTICLE') { @@ -92,18 +143,33 @@ function handleLink (link: HTMLAnchorElement): boolean { const tmp = decodeURI(href).split('#') - let path = tmp[0] - if (!path.startsWith('/')) { // to absolute path - path = join(dirname(filePath || ''), path) + const _switchDoc = async () => { + let path = normalizeSep(tmp[0]) + + const dir = dirname(filePath || '') + + // wiki link + if (link.getAttribute(DOM_ATTR_NAME.WIKI_LINK)) { + path = (await getFirstMatchPath(fileRepo, dir, path)) || path + } + + if (!path.startsWith('/')) { // to absolute path + path = join(dir, path) + } + + const file: Doc = { path, type: 'file', name: basename(path), repo: fileRepo } + + return switchDoc(file) } + const path = normalizeSep(tmp[0]) 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 () => { + _switchDoc().then(async () => { const hash = tmp.slice(1).join('#') // jump anchor if (hash) { diff --git a/src/renderer/plugins/markdown-wiki-links.ts b/src/renderer/plugins/markdown-wiki-links.ts index f859a1dd7..203bc416d 100644 --- a/src/renderer/plugins/markdown-wiki-links.ts +++ b/src/renderer/plugins/markdown-wiki-links.ts @@ -1,4 +1,4 @@ -import { Plugin } from '@fe/context' +import ctx, { Plugin } from '@fe/context' import type StateInline from 'markdown-it/lib/rules_inline/state_inline' const reMatch = /^\s*([^[#|]*)?(?:#([^|]*))?(?:\|([^\]]*))?\s*$/ @@ -46,7 +46,7 @@ function wikiLinks (state: StateInline, silent?: boolean) { } if (!silent) { - state.push('link_open', 'a', 1).attrs = [['href', url]] + state.push('link_open', 'a', 1).attrs = [['href', url], [ctx.args.DOM_ATTR_NAME.WIKI_LINK, 'true']] state.push('text', '', 0).content = text state.push('link_close', 'a', -1) } diff --git a/src/renderer/services/markdown.ts b/src/renderer/services/markdown.ts index 759159421..3524d5dc3 100644 --- a/src/renderer/services/markdown.ts +++ b/src/renderer/services/markdown.ts @@ -36,6 +36,7 @@ markdown.render = (src: string, env?: any) => { ;(getSetting('render.md-sup', true) ? enabledRules : disabledRules).push('sup') ;(getSetting('render.md-sub', true) ? enabledRules : disabledRules).push('sub') + ;(getSetting('render.md-wiki-links', true) ? enabledRules : disabledRules).push('wiki-links') markdown.enable(enabledRules, true) markdown.disable(disabledRules, true) diff --git a/src/renderer/support/args.ts b/src/renderer/support/args.ts index 143f46922..1c6582ec2 100644 --- a/src/renderer/support/args.ts +++ b/src/renderer/support/args.ts @@ -38,6 +38,7 @@ export const DOM_ATTR_NAME = { ONLY_CHILD: 'auto-center', TOKEN_IDX: 'data-token-idx', DISPLAY_NONE: 'display-none', + WIKI_LINK: 'wiki-link', } export const DOM_CLASS_NAME = { diff --git a/src/renderer/types.ts b/src/renderer/types.ts index f56e01021..aa47dfa64 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -326,6 +326,7 @@ export interface BuildInSettings { 'render.md-html': boolean, 'render.md-breaks': boolean, 'render.md-linkify': boolean, + 'render.md-wiki-links': boolean, 'render.md-typographer': boolean, 'render.md-emoji': boolean, 'render.md-sub': boolean, diff --git a/src/share/i18n/languages/en.ts b/src/share/i18n/languages/en.ts index 4a33ae2c9..fe2bf5e68 100644 --- a/src/share/i18n/languages/en.ts +++ b/src/share/i18n/languages/en.ts @@ -395,6 +395,7 @@ const data = { 'md-html': 'Enable HTML', 'md-breaks': 'Convert \\n to <br>', 'md-linkify': 'Auto convert URL-like text to links', + 'md-wiki-links': 'Enable Wiki Links - [[link]]', 'md-typographer': 'Enable some language-neutral replacement + quotes beautification', 'md-sup': 'Enable sup syntax: 29^th^', 'md-sub': 'Enable sub syntax: H~2~O', diff --git a/src/share/i18n/languages/zh-CN.ts b/src/share/i18n/languages/zh-CN.ts index ed62e665f..be7d35780 100644 --- a/src/share/i18n/languages/zh-CN.ts +++ b/src/share/i18n/languages/zh-CN.ts @@ -386,6 +386,7 @@ const data: BaseLanguage = { 'md-html': '启用 HTML', 'md-breaks': '将 \\n 转换为 <br>', 'md-linkify': '自动将类似 URL 的文本转换为链接', + 'md-wiki-links': '启用 Wiki 链接 - [[link]]', 'md-typographer': '启用排版美化,如 (c) -> ©', 'md-sup': '启用上标语法: 29^th^', 'md-sub': '启用下标语法: H~2~O', diff --git a/src/share/i18n/languages/zh-TW.ts b/src/share/i18n/languages/zh-TW.ts index f6ccdf151..35fee4765 100644 --- a/src/share/i18n/languages/zh-TW.ts +++ b/src/share/i18n/languages/zh-TW.ts @@ -386,6 +386,7 @@ const data: BaseLanguage = { 'md-html': '啟用 HTML', 'md-breaks': '將 \\n 轉換為 <br>', 'md-linkify': '自動將類似 URL 的文字轉換為連結', + 'md-wiki-links': '啟用 Wiki 連結 - [[link]]', 'md-typographer': '啟用排版美化,如 (c) -> ©', 'md-sup': '啟用上標語法: 29^th^', 'md-sub': '啟用下標語法: H~2~O', From 74ebb1155560a1ba2c46dd386af72b1d735d3521 Mon Sep 17 00:00:00 2001 From: purocean Date: Mon, 17 Jun 2024 19:41:27 +0800 Subject: [PATCH 16/26] refactor: handle error gracefully in markdown-link.ts --- src/renderer/plugins/markdown-link.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/plugins/markdown-link.ts b/src/renderer/plugins/markdown-link.ts index 624b7c422..1aea53de4 100644 --- a/src/renderer/plugins/markdown-link.ts +++ b/src/renderer/plugins/markdown-link.ts @@ -74,7 +74,7 @@ async function getFirstMatchPath (repo: string, dir: string, path: string) { return null } - const tree = await fetchTree(repo, { by: 'mtime', order: 'desc' }) + const tree = await fetchTree(repo, { by: 'mtime', order: 'desc' }).catch(() => []) return findInDir(tree) || findByName(tree) } From 03f0dcd82417115ae4409cabdd32eabd71fb8a0f Mon Sep 17 00:00:00 2001 From: purocean Date: Tue, 18 Jun 2024 00:48:10 +0800 Subject: [PATCH 17/26] feat: enhance Wiki Link syntax to support line and column positions --- help/FEATURES.md | 2 +- help/FEATURES_ZH-CN.md | 2 +- .../components/DefaultPreviewerRender.ce.vue | 1 + src/renderer/plugins/markdown-link.ts | 39 ++++++++++++++++--- src/renderer/plugins/markdown-wiki-links.ts | 18 ++++++--- 5 files changed, 50 insertions(+), 12 deletions(-) diff --git a/help/FEATURES.md b/help/FEATURES.md index 03c78b9dd..bb4c0f966 100644 --- a/help/FEATURES.md +++ b/help/FEATURES.md @@ -69,7 +69,7 @@ Type '/' in the editor to get prompts *[HTML]: Hyper Text Markup Language *[W3C]: World Wide Web Consortium The HTML specification is maintained by the W3C. -+ Wiki Link: Supports using `[[filename#anchor|display text]]` syntax to link documents, such as [[README#Highlights|Highlights]] ++ Wiki Link: Supports using `[[filename#anchor|display text]]` or `[[filename:line,column|display text]]` syntax to link documents, such as [[README#Highlights|Features]] [[README:3,4]] ## Github Alerts diff --git a/help/FEATURES_ZH-CN.md b/help/FEATURES_ZH-CN.md index 7b3e7190a..2f8e1eeef 100644 --- a/help/FEATURES_ZH-CN.md +++ b/help/FEATURES_ZH-CN.md @@ -70,7 +70,7 @@ define: *[HTML]: Hyper Text Markup Language *[W3C]: World Wide Web Consortium The HTML specification is maintained by the W3C. -+ Wiki 链接:支持使用 `[[文件名#锚点|显示文本]]` 语法来链接文档,如 [[README#Highlights|特色功能]] ++ Wiki 链接:支持使用 `[[文件名#锚点|显示文本]]` 或 `[[文件名:行,列|显示文本]]` 语法来链接文档,如 [[README#Highlights|特色功能]] [[README:3,4]] ## Github Alerts diff --git a/src/renderer/components/DefaultPreviewerRender.ce.vue b/src/renderer/components/DefaultPreviewerRender.ce.vue index 29b806ea0..9c0d180d1 100644 --- a/src/renderer/components/DefaultPreviewerRender.ce.vue +++ b/src/renderer/components/DefaultPreviewerRender.ce.vue @@ -358,6 +358,7 @@ body.find-in-preview-highlight ::selection { } a[href$=".md"], + a[href*=".md:"], a[href*=".md#"] { &:after { content: '\200D\2002'; diff --git a/src/renderer/plugins/markdown-link.ts b/src/renderer/plugins/markdown-link.ts index 1aea53de4..168ca373d 100644 --- a/src/renderer/plugins/markdown-link.ts +++ b/src/renderer/plugins/markdown-link.ts @@ -1,6 +1,6 @@ import StateCore from 'markdown-it/lib/rules_core/state_core' import Token from 'markdown-it/lib/token' -import { Plugin } from '@fe/context' +import ctx, { Plugin } from '@fe/context' import store from '@fe/support/store' import { removeQuery, sleep } from '@fe/utils' import { isElectron, isWindows } from '@fe/support/env' @@ -142,10 +142,33 @@ function handleLink (link: HTMLAnchorElement): boolean { } const tmp = decodeURI(href).split('#') + const rePos = /:([0-9]+),?([0-9]+)?$/ + + const setPosition = (line: number, column: number) => { + ctx.view.disableSyncScrollAwhile(() => { + if (ctx.editor.isDefault()) { + ctx.editor.highlightLine(line, true, 1000) + ctx.editor.getEditor().setPosition({ lineNumber: line, column }) + ctx.editor.getEditor().focus() + } - const _switchDoc = async () => { - let path = normalizeSep(tmp[0]) + ctx.view.highlightLine(line, true, 1000) + }) + } + + const parsePathPos = (path: string): {pos: [number, number] | null, path: string} => { + const match = path.match(rePos) + let pos: [number, number] | null = null + if (match) { + path = path.replace(rePos, '') + pos = [parseInt(match[1]), match[2] ? parseInt(match[2]) : 1] + } + return { pos, path } + } + + const _switchDoc = async () => { + let { path, pos } = parsePathPos(normalizeSep(tmp[0])) const dir = dirname(filePath || '') // wiki link @@ -159,13 +182,15 @@ function handleLink (link: HTMLAnchorElement): boolean { const file: Doc = { path, type: 'file', name: basename(path), repo: fileRepo } - return switchDoc(file) + return switchDoc(file).then(() => { + pos && setPosition(pos[0], pos[1]) + }) } const path = normalizeSep(tmp[0]) const file: Doc = { path, type: 'file', name: basename(path), repo: fileRepo } - const isMarkdownFile = /(\.md$|\.md#)/.test(href) + const isMarkdownFile = /(\.md$|\.md#|\.md:)/.test(href) const supportOpenDirectly = isMarkdownFile || getAllCustomEditors().some(x => x.when?.({ doc: file })) if (supportOpenDirectly) { @@ -194,6 +219,10 @@ function handleLink (link: HTMLAnchorElement): boolean { el && scrollIntoView(el) }) return true + } else if (href && href.startsWith(':') && rePos.test(href)) { // for pos + const { pos } = parsePathPos(href) + pos && setPosition(pos[0], pos[1]) + return true } else { return false } diff --git a/src/renderer/plugins/markdown-wiki-links.ts b/src/renderer/plugins/markdown-wiki-links.ts index 203bc416d..3990e485d 100644 --- a/src/renderer/plugins/markdown-wiki-links.ts +++ b/src/renderer/plugins/markdown-wiki-links.ts @@ -1,9 +1,10 @@ import ctx, { Plugin } from '@fe/context' import type StateInline from 'markdown-it/lib/rules_inline/state_inline' -const reMatch = /^\s*([^[#|]*)?(?:#([^|]*))?(?:\|([^\]]*))?\s*$/ +const reMatch = /^([^[#|]*)?(?:#([^#|]*))?(?:\|([^\]]*))?$/ const reExternalLink = /^[a-zA-Z]{1,8}:\/\/.*/ const reExtName = /\.[^/]+$/ +const rePos = /(:\d{1,6},?\d{0,6})$/ function wikiLinks (state: StateInline, silent?: boolean) { // check [[ @@ -22,7 +23,7 @@ function wikiLinks (state: StateInline, silent?: boolean) { return false } - const link = (parts[1] || '').trim() + let link = (parts[1] || '').trim() const hash = parts[2] || '' const label = parts[3] || '' @@ -33,14 +34,21 @@ function wikiLinks (state: StateInline, silent?: boolean) { // internal link if (!reExternalLink.test(link)) { + let posStr = '' + const posMatch = link.match(rePos) + if (posMatch) { + link = link.replace(rePos, '') + posStr = posMatch[1] + } + const fileName = link.split('/').pop() - text = label || (fileName ? fileName + hashStr : url) + text = label || (fileName ? (fileName + posStr + hashStr) : url) // no extension name if (link && !reExtName.test(link)) { - url = `${link}.md${hashStr}` + url = `${link}.md${posStr}${hashStr}` } else if (!link) { - url = hashStr + url = posStr + hashStr text = label || hash || url } } From 3430bfe3886d4e0246eb1a91c3d74a484817ad3f Mon Sep 17 00:00:00 2001 From: purocean Date: Tue, 18 Jun 2024 00:49:02 +0800 Subject: [PATCH 18/26] chore: update markdown-it-attributes to version 1.2.0 --- package.json | 2 +- yarn.lock | 39 +++++++-------------------------------- 2 files changed, 8 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 40e3f617b..757f90666 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "lodash-es": "^4.17.21", "markdown-it": "^14.1.0", "markdown-it-abbr": "^2.0.0", - "markdown-it-attributes": "^1.1.1", + "markdown-it-attributes": "^1.2.0", "markdown-it-container": "^4.0.0", "markdown-it-emoji": "^3.0.0", "markdown-it-github-alerts": "^0.3.0", diff --git a/yarn.lock b/yarn.lock index d93456341..f528ac079 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6700,10 +6700,10 @@ markdown-it-abbr@^2.0.0: resolved "https://registry.yarnpkg.com/markdown-it-abbr/-/markdown-it-abbr-2.0.0.tgz#aa01d8ff23ebf672f9a7db02a974e59254ce7acb" integrity sha512-of7C8pXSjXjDojW4neNP+jD7inUYH/DO0Ca+K/4FUEccg0oHAEX/nfscw0jfz66PJbYWOAT9U8mjO21X5p6aAw== -markdown-it-attributes@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/markdown-it-attributes/-/markdown-it-attributes-1.1.1.tgz#b52cc6bbc7c2a8d1700c8eb11a6c0e56a7443fcb" - integrity sha512-oiOH6eb3WIXQ4XF+uyiMjP+scV0inkTIl+fGTXrXfDSD8cn1UJV+ZGVtN/ytukJ576MZssZUXCBRaL6SEQMxDQ== +markdown-it-attributes@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/markdown-it-attributes/-/markdown-it-attributes-1.2.0.tgz#1f3403fb7bd510c6ca4eeaa58c4106313f643234" + integrity sha512-FDdqndq26KbWrO9ymFz7JyhnxXMP4vmPek7iAMNuQlfvgxO7zeAU8JfARzqcUYBiqsTc+H7wa9ybhIZVKAAWug== markdown-it-container@^4.0.0: version "4.0.0" @@ -8405,16 +8405,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8482,7 +8473,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -8496,13 +8487,6 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -9249,7 +9233,7 @@ wide-align@^1.1.5: dependencies: string-width "^1.0.2 || 2 || 3 || 4" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -9267,15 +9251,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From cd211d812239152c3f88e76fbbc42fb56bc45983 Mon Sep 17 00:00:00 2001 From: purocean Date: Tue, 18 Jun 2024 22:02:51 +0800 Subject: [PATCH 19/26] feat: Add support for stop button in code runner --- src/main/server/run.ts | 33 ++++++++++++--- src/renderer/plugins/code-runners.tsx | 1 + src/renderer/plugins/markdown-code-run.ts | 51 ++++++++++++++++++++--- src/renderer/support/api.ts | 14 ++++--- src/renderer/types.ts | 3 +- src/share/i18n/languages/en.ts | 1 + src/share/i18n/languages/zh-CN.ts | 1 + src/share/i18n/languages/zh-TW.ts | 1 + 8 files changed, 88 insertions(+), 17 deletions(-) diff --git a/src/main/server/run.ts b/src/main/server/run.ts index dde1f0fba..34c3f0b4d 100644 --- a/src/main/server/run.ts +++ b/src/main/server/run.ts @@ -17,17 +17,38 @@ function execFile (file: string, args: string[], options?: cp.ExecFileOptions) { }) process.on('spawn', () => { - const output = [process.stdout, process.stderr].filter(Boolean) as NodeJS.ReadableStream[] - resolve(mergeStreams(output)) + const output = mergeStreams([process.stdout, process.stderr].filter(Boolean) as NodeJS.ReadableStream[]) + + output.on('close', () => { + console.log('execFile process closed') + process.kill() + }) + + resolve(output) }) }) } function execCmd (cmd: string, options?: cp.ExecOptions, onComplete?: () => void) { - const process = cp.exec(cmd, { timeout: 300 * 1000, ...options }) - onComplete && process.on('close', onComplete) - const output = [process.stdout, process.stderr].filter(Boolean) as NodeJS.ReadableStream[] - return mergeStreams(output) + return new Promise((resolve) => { + const process = cp.exec(cmd, { timeout: 300 * 1000, ...options }) + + process.on('error', error => { + resolve(error.message) + }) + + process.on('spawn', () => { + const output = mergeStreams([process.stdout, process.stderr].filter(Boolean) as NodeJS.ReadableStream[]) + + output.on('close', () => { + console.log('execCmd process closed') + onComplete && onComplete() + process.kill() + }) + + resolve(output) + }) + }) } const runCode = async (cmd: { cmd: string, args: string[] } | string, code: string): Promise => { diff --git a/src/renderer/plugins/code-runners.tsx b/src/renderer/plugins/code-runners.tsx index 763632b50..e8e26bb1e 100644 --- a/src/renderer/plugins/code-runners.tsx +++ b/src/renderer/plugins/code-runners.tsx @@ -111,6 +111,7 @@ export default { ctx.runner.registerRunner({ name: 'javascript', order: 100, + nonInterruptible: true, match (language) { return ['js', 'javascript'].includes(language.toLowerCase()) }, diff --git a/src/renderer/plugins/markdown-code-run.ts b/src/renderer/plugins/markdown-code-run.ts index 71e5da5a2..f08f0ebb4 100644 --- a/src/renderer/plugins/markdown-code-run.ts +++ b/src/renderer/plugins/markdown-code-run.ts @@ -1,4 +1,4 @@ -import { computed, defineComponent, getCurrentInstance, h, onBeforeUnmount, ref, VNode, watch, watchEffect } from 'vue' +import { computed, defineComponent, getCurrentInstance, h, onBeforeUnmount, ref, shallowRef, VNode, watch, watchEffect } from 'vue' import Markdown from 'markdown-it' import { escape } from 'lodash-es' import { Plugin } from '@fe/context' @@ -30,8 +30,10 @@ const RunCode = defineComponent({ const { t } = useI18n() const instance = getCurrentInstance() const result = ref('') + const abortController = shallowRef() const hash = computed(() => md5(props.language + props.code)) const runner = ref() + const status = computed(() => runner.value?.nonInterruptible ? 'idle' : (abortController.value ? 'running' : 'idle')) const getTerminalCmd = computed(() => runner.value?.getTerminalCmd(props.language!, props.firstLine!)) let hasResult = false @@ -51,7 +53,22 @@ const RunCode = defineComponent({ cache[hash.value] = result.value } + const abort = () => { + if (abortController.value && !abortController.value.signal.aborted) { + abortController.value.abort() + abortController.value = undefined + if (!hasResult) { + result.value = '' + } + } + } + const run = async () => { + if (status.value === 'running') { + abort() + return + } + const { code, language } = props hasResult = false @@ -64,7 +81,11 @@ const RunCode = defineComponent({ result.value = t('code-run.running') try { - const { type, value: val } = await runner.value.run(language!, code) + if (!runner.value.nonInterruptible) { + abortController.value = new AbortController() + } + + const { type, value: val } = await runner.value.run(language!, code, { signal: abortController.value?.signal }) if (typeof val === 'string') { appendLog?.(type, val) @@ -73,6 +94,10 @@ const RunCode = defineComponent({ // read stream while (true) { + if (abortController.value && abortController.value.signal.aborted) { + break + } + const { done, value } = await val.read() if (done) { logger.debug('run code done >', value) @@ -99,7 +124,11 @@ const RunCode = defineComponent({ appendLog(type, valStr) } } catch (error: any) { - result.value = error.message + if (error.name !== 'AbortError') { + appendLog?.('plain', error.message) + } + } finally { + abort() } } @@ -146,8 +175,8 @@ const RunCode = defineComponent({ return [ h('div', { class: 'p-mcr-run-code-action skip-export' }, [ h('div', { - title: t('code-run.run'), - class: 'p-mcr-run-btn', + title: status.value !== 'running' ? t('code-run.run') : t('code-run.stop'), + class: 'p-mcr-run-btn' + (status.value === 'running' ? ' p-mcr-run-btn-stop' : ''), onClick: run }), h('div', { @@ -220,6 +249,18 @@ export default { outline: none; } + .markdown-view .markdown-body .p-mcr-run-btn.p-mcr-run-btn-stop { + border: .56em #b7b3b3 solid; + border-radius: 4px; + animation: p-mcr-run-btn-stop 1.5s infinite; + } + + @keyframes p-mcr-run-btn-stop { + 0% { border-color: #b7b3b3; } + 50% { border-color: #b7c3f3; } + 100% { border-color: #b7b3b3; } + } + .markdown-view .markdown-body .p-mcr-run-xterm-btn { position: absolute; top: -.5em; diff --git a/src/renderer/support/api.ts b/src/renderer/support/api.ts index 7939aef8a..c43f5326d 100644 --- a/src/renderer/support/api.ts +++ b/src/renderer/support/api.ts @@ -481,18 +481,22 @@ export async function convertFile ( * Run code. * @param cmd * @param code - * @param outputStream + * @param opts * @returns result */ -export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, outputStream: true): Promise -export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, outputStream?: false): Promise -export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, outputStream = false): Promise { +export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, opts?: { stream?: false, signal?: AbortSignal }): Promise +export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, opts?: { stream?: false, signal?: AbortSignal }): Promise +export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, opts?: { stream?: false, signal?: AbortSignal }): Promise { const response = await fetchHttp('/api/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ cmd, code }) + body: JSON.stringify({ cmd, code }), + signal: opts?.signal }) + // compatible with old version + const outputStream = typeof opts === 'boolean' ? opts : opts?.stream + if (outputStream) { return response.body.getReader() } else { diff --git a/src/renderer/types.ts b/src/renderer/types.ts index aa47dfa64..ba1694a49 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -516,12 +516,13 @@ export type Renderer = { export interface CodeRunner { name: string; order?: number; + nonInterruptible?: boolean match: (language: string, magicComment: string) => boolean; getTerminalCmd: (language: string, magicComment: string) => { start: string, exit: string, } | null; - run: (language: string, code: string) => Promise<{ + run: (language: string, code: string, opts?: { signal?: AbortSignal }) => Promise<{ type: 'html' | 'plain', value: ReadableStreamDefaultReader | string, }>; diff --git a/src/share/i18n/languages/en.ts b/src/share/i18n/languages/en.ts index fe2bf5e68..6fbe527bb 100644 --- a/src/share/i18n/languages/en.ts +++ b/src/share/i18n/languages/en.ts @@ -493,6 +493,7 @@ const data = { }, 'code-run': { 'run': 'Run', + 'stop': 'Stop', 'run-in-xterm-tips': 'Run code in terminal, %s + click do not exit', 'run-in-xterm': 'Run in terminal', 'running': 'Running...', diff --git a/src/share/i18n/languages/zh-CN.ts b/src/share/i18n/languages/zh-CN.ts index be7d35780..9d26e5f12 100644 --- a/src/share/i18n/languages/zh-CN.ts +++ b/src/share/i18n/languages/zh-CN.ts @@ -494,6 +494,7 @@ const data: BaseLanguage = { }, 'code-run': { 'run': '运行', + 'stop': '停止', 'run-in-xterm-tips': '在终端中运行代码,%s + 单击不退出解释器', 'run-in-xterm': '终端中运行', 'running': '运行中……', diff --git a/src/share/i18n/languages/zh-TW.ts b/src/share/i18n/languages/zh-TW.ts index 35fee4765..f3f560256 100644 --- a/src/share/i18n/languages/zh-TW.ts +++ b/src/share/i18n/languages/zh-TW.ts @@ -494,6 +494,7 @@ const data: BaseLanguage = { }, 'code-run': { 'run': '執行', + 'stop': '停止', 'run-in-xterm-tips': '在終端中執行程式碼,%s + 點擊不退出解釋器', 'run-in-xterm': '終端中執行', 'running': '執行中……', From 8b7a69a464d9ba11c346abdd09d1decda75d80bb Mon Sep 17 00:00:00 2001 From: purocean Date: Tue, 18 Jun 2024 22:58:57 +0800 Subject: [PATCH 20/26] refactor: Add key prop to RunCode component in markdown-code-run.ts --- src/renderer/plugins/markdown-code-run.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/renderer/plugins/markdown-code-run.ts b/src/renderer/plugins/markdown-code-run.ts index f08f0ebb4..4856fa392 100644 --- a/src/renderer/plugins/markdown-code-run.ts +++ b/src/renderer/plugins/markdown-code-run.ts @@ -166,6 +166,7 @@ const RunCode = defineComponent({ onBeforeUnmount(() => { appendLog = undefined + abort() removeHook('CODE_RUNNER_CHANGE', refreshRunner) }) @@ -214,6 +215,7 @@ const RunPlugin = (md: Markdown) => { if (codeNode && Array.isArray(codeNode.children)) { codeNode.children.push(h(RunCode, { + key: code, code, firstLine, language: token.info, From a478d03326e4e80ca0ca5719e720bb02ae1f6940 Mon Sep 17 00:00:00 2001 From: purocean Date: Tue, 18 Jun 2024 22:59:03 +0800 Subject: [PATCH 21/26] chore: fix types --- src/renderer/support/api.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderer/support/api.ts b/src/renderer/support/api.ts index c43f5326d..e9a933d42 100644 --- a/src/renderer/support/api.ts +++ b/src/renderer/support/api.ts @@ -484,9 +484,9 @@ export async function convertFile ( * @param opts * @returns result */ -export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, opts?: { stream?: false, signal?: AbortSignal }): Promise -export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, opts?: { stream?: false, signal?: AbortSignal }): Promise -export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, opts?: { stream?: false, signal?: AbortSignal }): Promise { +export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, opts?: { stream?: boolean, signal?: AbortSignal }): Promise +export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, opts?: { stream?: boolean, signal?: AbortSignal }): Promise +export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, opts?: { stream?: boolean, signal?: AbortSignal }): Promise { const response = await fetchHttp('/api/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, From 676ad073b871ec3505310e7516b771850e373d4d Mon Sep 17 00:00:00 2001 From: purocean Date: Tue, 18 Jun 2024 23:51:20 +0800 Subject: [PATCH 22/26] fix: emit 'finish' event when response is closed in transformProtocolRequest --- src/main/protocol.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/protocol.ts b/src/main/protocol.ts index 394a48fd5..2aa292873 100644 --- a/src/main/protocol.ts +++ b/src/main/protocol.ts @@ -72,5 +72,9 @@ export async function transformProtocolRequest (request: ProtocolRequest) { }, }) + res.on('close', () => { + res.emit('finish') + }) + return { req, res, out } } From 6893b4061d07c838b7f6aaf5a3a19cebc86f1e68 Mon Sep 17 00:00:00 2001 From: purocean Date: Wed, 19 Jun 2024 01:02:30 +0800 Subject: [PATCH 23/26] feat: improve handling of wiki links and anchors in markdown rendering --- .../components/DefaultPreviewerRender.ce.vue | 6 ++- src/renderer/plugins/markdown-link.ts | 50 +++++++++++-------- src/renderer/plugins/markdown-wiki-links.ts | 19 ++++--- src/renderer/support/args.ts | 1 + 4 files changed, 48 insertions(+), 28 deletions(-) diff --git a/src/renderer/components/DefaultPreviewerRender.ce.vue b/src/renderer/components/DefaultPreviewerRender.ce.vue index 9c0d180d1..c8c63e303 100644 --- a/src/renderer/components/DefaultPreviewerRender.ce.vue +++ b/src/renderer/components/DefaultPreviewerRender.ce.vue @@ -340,7 +340,8 @@ body.find-in-preview-highlight ::selection { } } - a:not([href^="#fn"])[href^="#"]:after { + a:not([href^="#fn"])[href^="#"]:after, + a[is-anchor]:after { content: '\200D\2002'; padding: 2px; background-repeat: no-repeat; @@ -359,7 +360,8 @@ body.find-in-preview-highlight ::selection { a[href$=".md"], a[href*=".md:"], - a[href*=".md#"] { + a[href*=".md#"], + a:not([is-anchor])[wiki-link] { &:after { content: '\200D\2002'; padding: 1px; diff --git a/src/renderer/plugins/markdown-link.ts b/src/renderer/plugins/markdown-link.ts index 168ca373d..a00517130 100644 --- a/src/renderer/plugins/markdown-link.ts +++ b/src/renderer/plugins/markdown-link.ts @@ -30,9 +30,9 @@ async function getElement (id: string) { return _find(id) || _find(id.toUpperCase()) } -async function getFirstMatchPath (repo: string, dir: string, path: string) { - if (path.includes('/')) { - return path +async function getFirstMatchPath (repo: string, dir: string, fileName: string) { + if (fileName.includes('/')) { + return fileName } const findInDir = (items: Components.Tree.Node[]): string | null => { @@ -40,8 +40,8 @@ async function getFirstMatchPath (repo: string, dir: string, path: string) { const p = normalizeSep(item.path) if ( item.type === 'file' && - (p === normalizeSep(join(dir, path)) || - p === normalizeSep(join(dir, `${path}.md`))) + (p === normalizeSep(join(dir, fileName)) || + p === normalizeSep(join(dir, `${fileName}.md`))) ) { return item.path } @@ -59,7 +59,7 @@ async function getFirstMatchPath (repo: string, dir: string, path: string) { const findByName = (items: Components.Tree.Node[]): string | null => { for (const item of items) { - if (item.type === 'file' && (item.name === path || item.name === `${path}.md`)) { + if (item.type === 'file' && (item.name === fileName || item.name === `${fileName}.md`)) { return item.path } @@ -167,13 +167,17 @@ function handleLink (link: HTMLAnchorElement): boolean { return { pos, path } } + const isWikiLink = !!link.getAttribute(DOM_ATTR_NAME.WIKI_LINK) + const _switchDoc = async () => { let { path, pos } = parsePathPos(normalizeSep(tmp[0])) const dir = dirname(filePath || '') - // wiki link - if (link.getAttribute(DOM_ATTR_NAME.WIKI_LINK)) { - path = (await getFirstMatchPath(fileRepo, dir, path)) || path + if (isWikiLink) { + path = normalizeSep(path) + path = path.replace(/:/g, '/') // replace all ':' to '/' + path = await getFirstMatchPath(fileRepo, dir, path) || path + path = path.endsWith('.md') ? path : `${path}.md` } if (!path.startsWith('/')) { // to absolute path @@ -184,17 +188,7 @@ function handleLink (link: HTMLAnchorElement): boolean { return switchDoc(file).then(() => { pos && setPosition(pos[0], pos[1]) - }) - } - - const path = normalizeSep(tmp[0]) - const file: Doc = { path, type: 'file', name: basename(path), repo: fileRepo } - - const isMarkdownFile = /(\.md$|\.md#|\.md:)/.test(href) - const supportOpenDirectly = isMarkdownFile || getAllCustomEditors().some(x => x.when?.({ doc: file })) - - if (supportOpenDirectly) { - _switchDoc().then(async () => { + }).then(async () => { const hash = tmp.slice(1).join('#') // jump anchor if (hash) { @@ -212,7 +206,16 @@ function handleLink (link: HTMLAnchorElement): boolean { } } }) + } + const path = normalizeSep(tmp[0]) + const file: Doc = { path, type: 'file', name: basename(path), repo: fileRepo } + + const isMarkdownFile = /(\.md$|\.md#|\.md:)/.test(href) + const supportOpenDirectly = isMarkdownFile || getAllCustomEditors().some(x => x.when?.({ doc: file })) + + if (supportOpenDirectly) { + _switchDoc() return true } else if (href && href.startsWith('#')) { // for anchor getElement(href.replace(/^#/, '')).then(el => { @@ -223,6 +226,9 @@ function handleLink (link: HTMLAnchorElement): boolean { const { pos } = parsePathPos(href) pos && setPosition(pos[0], pos[1]) return true + } else if (isWikiLink) { + _switchDoc() + return true } else { return false } @@ -238,6 +244,10 @@ function convertLink (state: StateCore) { } const link = (token: Token) => { + if (token.attrGet(DOM_ATTR_NAME.WIKI_LINK)) { + return + } + const isAnchor = token.tag === 'a' const attrName = isAnchor ? 'href' : 'src' const attrVal = decodeURIComponent(token.attrGet(attrName) || '') diff --git a/src/renderer/plugins/markdown-wiki-links.ts b/src/renderer/plugins/markdown-wiki-links.ts index 3990e485d..b606776ad 100644 --- a/src/renderer/plugins/markdown-wiki-links.ts +++ b/src/renderer/plugins/markdown-wiki-links.ts @@ -3,7 +3,6 @@ import type StateInline from 'markdown-it/lib/rules_inline/state_inline' const reMatch = /^([^[#|]*)?(?:#([^#|]*))?(?:\|([^\]]*))?$/ const reExternalLink = /^[a-zA-Z]{1,8}:\/\/.*/ -const reExtName = /\.[^/]+$/ const rePos = /(:\d{1,6},?\d{0,6})$/ function wikiLinks (state: StateInline, silent?: boolean) { @@ -31,6 +30,7 @@ function wikiLinks (state: StateInline, silent?: boolean) { let url = link + hashStr let text = label || url + let isAnchor = false // internal link if (!reExternalLink.test(link)) { @@ -44,17 +44,24 @@ function wikiLinks (state: StateInline, silent?: boolean) { const fileName = link.split('/').pop() text = label || (fileName ? (fileName + posStr + hashStr) : url) - // no extension name - if (link && !reExtName.test(link)) { - url = `${link}.md${posStr}${hashStr}` - } else if (!link) { + if (!link) { url = posStr + hashStr text = label || hash || url + isAnchor = true } } if (!silent) { - state.push('link_open', 'a', 1).attrs = [['href', url], [ctx.args.DOM_ATTR_NAME.WIKI_LINK, 'true']] + const attrs: [string, string][] = [ + ['href', url], + [ctx.args.DOM_ATTR_NAME.WIKI_LINK, 'true'], + ] + + if (isAnchor) { + attrs.push([ctx.args.DOM_ATTR_NAME.IS_ANCHOR, 'true']) + } + + state.push('link_open', 'a', 1).attrs = attrs state.push('text', '', 0).content = text state.push('link_close', 'a', -1) } diff --git a/src/renderer/support/args.ts b/src/renderer/support/args.ts index 1c6582ec2..a6ff41b59 100644 --- a/src/renderer/support/args.ts +++ b/src/renderer/support/args.ts @@ -39,6 +39,7 @@ export const DOM_ATTR_NAME = { TOKEN_IDX: 'data-token-idx', DISPLAY_NONE: 'display-none', WIKI_LINK: 'wiki-link', + IS_ANCHOR: 'is-anchor', } export const DOM_CLASS_NAME = { From 774e59e229ef1a5844fdccb97acf368cf04397ad Mon Sep 17 00:00:00 2001 From: purocean Date: Wed, 19 Jun 2024 01:29:03 +0800 Subject: [PATCH 24/26] refactor: improve filename validation --- src/renderer/services/document.ts | 6 ++++-- src/share/i18n/languages/en.ts | 1 + src/share/i18n/languages/zh-CN.ts | 1 + src/share/i18n/languages/zh-TW.ts | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/renderer/services/document.ts b/src/renderer/services/document.ts index d3951a70a..c9c034029 100644 --- a/src/renderer/services/document.ts +++ b/src/renderer/services/document.ts @@ -41,8 +41,10 @@ function encrypt (content: any, password: string) { } function checkFilePath (path: string) { - if (path.includes('#')) { - throw new Error('Path should not contain #') + // check filename is valid + const filename = basename(path) + if (/[<>:"|?*#]/.test(filename)) { + throw new Error(t('document.invalid-filename', '< > : " / \\ | ? * #')) } } diff --git a/src/share/i18n/languages/en.ts b/src/share/i18n/languages/en.ts index 6fbe527bb..cb2cedadd 100644 --- a/src/share/i18n/languages/en.ts +++ b/src/share/i18n/languages/en.ts @@ -177,6 +177,7 @@ const data = { }, 'document': { 'current-path': 'Current Path: %s', + 'invalid-filename': 'File name cannot contain the following characters: %s', 'password-create': '[Create] Please enter a password', 'password-save': '[Save] Please enter password of the file', 'password-open': '[Open] Please enter password of the file', diff --git a/src/share/i18n/languages/zh-CN.ts b/src/share/i18n/languages/zh-CN.ts index 9d26e5f12..def348184 100644 --- a/src/share/i18n/languages/zh-CN.ts +++ b/src/share/i18n/languages/zh-CN.ts @@ -178,6 +178,7 @@ const data: BaseLanguage = { }, 'document': { 'current-path': '当前路径: %s', + 'invalid-filename': '文件名不能包含以下字符: %s', 'password-create': '[创建] 请输入密码', 'password-save': '[保存] 请输入密码', 'password-open': '[打开] 请输入密码', diff --git a/src/share/i18n/languages/zh-TW.ts b/src/share/i18n/languages/zh-TW.ts index f3f560256..662fc6c86 100644 --- a/src/share/i18n/languages/zh-TW.ts +++ b/src/share/i18n/languages/zh-TW.ts @@ -178,6 +178,7 @@ const data: BaseLanguage = { }, 'document': { 'current-path': '當前路徑: %s', + 'invalid-filename': '檔案名不能包含以下字符: %s', 'password-create': '[創建] 請輸入密碼', 'password-save': '[保存] 請輸入密碼', 'password-open': '[打開] 請輸入密碼', From 8acc67fc51ca9e1e949d85bbc643533bb43f4dab Mon Sep 17 00:00:00 2001 From: purocean Date: Wed, 19 Jun 2024 02:04:36 +0800 Subject: [PATCH 25/26] refactor: improve trigger characters for path completion and handle wiki links in emoji completion --- .../plugins/editor-path-completion.ts | 19 +++++++++++++------ src/renderer/plugins/emoji.ts | 5 +++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/renderer/plugins/editor-path-completion.ts b/src/renderer/plugins/editor-path-completion.ts index c99deac4c..8552fa67a 100644 --- a/src/renderer/plugins/editor-path-completion.ts +++ b/src/renderer/plugins/editor-path-completion.ts @@ -62,7 +62,7 @@ interface CompletionContext { } class CompletionProvider implements Monaco.languages.CompletionItemProvider { - triggerCharacters = ['/', '#', '['] + triggerCharacters = ['/', ':', '#', '['] private readonly monaco: typeof Monaco private readonly ctx: Ctx @@ -263,17 +263,24 @@ class CompletionProvider implements Monaco.languages.CompletionItemProvider { } private async * providePathSuggestions (position: Monaco.Position, context: CompletionContext): AsyncIterable { - const valueBeforeLastSlash = context.linkPrefix.substring(0, context.linkPrefix.lastIndexOf('/') + 1) // keep the last slash + let idx = context.linkPrefix.lastIndexOf('/') + + if (context.kind === CompletionContextKind.WikiLink) { + idx = Math.max(context.linkPrefix.lastIndexOf(':'), idx) + } + + const valueBeforeLastSlash = context.linkPrefix.substring(0, idx + 1) // keep the last slash const currentFile = this.ctx.store.state.currentFile if (!currentFile) { return } - const parentDir = this.ctx.utils.path.resolve( - this.ctx.utils.path.dirname(currentFile.path), - valueBeforeLastSlash || '.' - ) + const basePath = this.ctx.utils.path.dirname(currentFile.path) + + const parentDir = context.kind === CompletionContextKind.WikiLink + ? this.ctx.utils.path.resolve(basePath, valueBeforeLastSlash.replace(/:/g, '/') || '.') + : this.ctx.utils.path.resolve(basePath, valueBeforeLastSlash || '.') const pathSegmentStart = position.delta(0, valueBeforeLastSlash.length - context.linkPrefix.length) const insertRange = new this.monaco.Range( diff --git a/src/renderer/plugins/emoji.ts b/src/renderer/plugins/emoji.ts index 07a547a66..db04924ae 100644 --- a/src/renderer/plugins/emoji.ts +++ b/src/renderer/plugins/emoji.ts @@ -25,6 +25,11 @@ class EmojiCompletionProvider implements Monaco.languages.CompletionItemProvider const cursor = position.column - 1 const linePrefixText = line.slice(0, cursor) + // Check if the cursor is in a wiki link + if (linePrefixText.lastIndexOf('[[') > linePrefixText.lastIndexOf(']]')) { + return { suggestions: [] } + } + const match = linePrefixText.match(/:[a-zA-Z0-9]*$/) if (!match || linePrefixText.charAt(linePrefixText.length - match[0].length - 1) === ':') { return { suggestions: [] } From c897422795998f2ee6e02554229e49d3876d2569 Mon Sep 17 00:00:00 2001 From: purocean Date: Wed, 19 Jun 2024 19:33:14 +0800 Subject: [PATCH 26/26] chore: bump version --- README.md | 17 ++++++++--------- README_ZH-CN.md | 17 ++++++++--------- package.json | 2 +- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 19e7bfbcb..4310b8bca 100644 --- a/README.md +++ b/README.md @@ -76,17 +76,16 @@ For more information on how to use the following functions, please see [characte ## Changelogs -### [v3.70.4](https://github.com/purocean/yn/releases/tag/v3.70.4) 2024-05-21 +### [v3.71.1](https://github.com/purocean/yn/releases/tag/v3.71.1) 2024-06-19 -[Windows](https://github.com/purocean/yn/releases/download/v3.70.4/Yank-Note-win-x64-3.70.4.exe) | [macOS arm64](https://github.com/purocean/yn/releases/download/v3.70.4/Yank-Note-mac-arm64-3.70.4.dmg) | [macOS x64](https://github.com/purocean/yn/releases/download/v3.70.4/Yank-Note-mac-x64-3.70.4.dmg) | [Linux AppImage](https://github.com/purocean/yn/releases/download/v3.70.4/Yank-Note-linux-x86_64-3.70.4.AppImage) | [Linux deb](https://github.com/purocean/yn/releases/download/v3.70.4/Yank-Note-linux-amd64-3.70.4.deb) +[Windows](https://github.com/purocean/yn/releases/download/v3.71.1/Yank-Note-win-x64-3.71.1.exe) | [macOS arm64](https://github.com/purocean/yn/releases/download/v3.71.1/Yank-Note-mac-arm64-3.71.1.dmg) | [macOS x64](https://github.com/purocean/yn/releases/download/v3.71.1/Yank-Note-mac-x64-3.71.1.dmg) | [Linux AppImage](https://github.com/purocean/yn/releases/download/v3.71.1/Yank-Note-linux-x86_64-3.71.1.AppImage) | [Linux deb](https://github.com/purocean/yn/releases/download/v3.71.1/Yank-Note-linux-amd64-3.71.1.deb) -1. feat: Support pasting rich text as Markdown with table support -2. feat: Support keyboard navigation in context menu -3. feat: Optimize keyboard accessibility in dialogs -4. perf: Hide image navigation bar in image previewer when there are too many images in the document -5. refactor: Replace `request` library with `undici` -6. refactor(plugin): Add `ctx.api.proxyFetch` method, remove `ctx.api.proxyRequest` method for better SSE support -7. chore: Upgrade Electron to 28.3.1 +1. feat: support for linking documents using the syntax `[[filename#anchor|display text]]` or `[[filename:line,column|display text]]`. +2. feat: running shell code now supports stopping the execution. +3. feat: files with extensions `.bib`, `.plantuml`, `.dot`, `.gv`, `.puml` are considered as text files and are editable. +4. feat: editor configuration now supports ligatures for fonts. +5. fix: fixed an issue where code blocks did not adhere to the `.avoid-page-break` style, causing incorrect pagination during printing. +6. chore: upgraded Electron to version 28.3.3. [More release notes](https://github.com/purocean/yn/releases) diff --git a/README_ZH-CN.md b/README_ZH-CN.md index 62b29ecb0..3173ddd7a 100644 --- a/README_ZH-CN.md +++ b/README_ZH-CN.md @@ -76,17 +76,16 @@ ## 更新日志 -### [v3.70.4](https://github.com/purocean/yn/releases/tag/v3.70.4) 2024-05-21 +### [v3.71.1](https://github.com/purocean/yn/releases/tag/v3.71.1) 2024-06-19 -[Windows](https://github.com/purocean/yn/releases/download/v3.70.4/Yank-Note-win-x64-3.70.4.exe) | [macOS arm64](https://github.com/purocean/yn/releases/download/v3.70.4/Yank-Note-mac-arm64-3.70.4.dmg) | [macOS x64](https://github.com/purocean/yn/releases/download/v3.70.4/Yank-Note-mac-x64-3.70.4.dmg) | [Linux AppImage](https://github.com/purocean/yn/releases/download/v3.70.4/Yank-Note-linux-x86_64-3.70.4.AppImage) | [Linux deb](https://github.com/purocean/yn/releases/download/v3.70.4/Yank-Note-linux-amd64-3.70.4.deb) +[Windows](https://github.com/purocean/yn/releases/download/v3.71.1/Yank-Note-win-x64-3.71.1.exe) | [macOS arm64](https://github.com/purocean/yn/releases/download/v3.71.1/Yank-Note-mac-arm64-3.71.1.dmg) | [macOS x64](https://github.com/purocean/yn/releases/download/v3.71.1/Yank-Note-mac-x64-3.71.1.dmg) | [Linux AppImage](https://github.com/purocean/yn/releases/download/v3.71.1/Yank-Note-linux-x86_64-3.71.1.AppImage) | [Linux deb](https://github.com/purocean/yn/releases/download/v3.71.1/Yank-Note-linux-amd64-3.71.1.deb) -1. feat: 富文本粘贴为 Markdown 支持表格 -2. feat: 上下文菜单支持使用键盘导航 -3. feat: 优化对话框键盘访问 -4. perf: 当文档图片过多时,图片预览器不再显示图片导航栏 -5. refactor: 使用 `undici` 代替 `request` 库 -6. refactor(plugin): 增加 `ctx.api.proxyFetch` 方法,移除 `ctx.api.proxyRequest` 方法,更好的支持 SSE -7. chore: 升级 Electron 到 28.3.1 +1. feat: 支持使用 `[[文件名#锚点|显示文本]]` 或 `[[文件名:行,列|显示文本]]` 语法来链接文档 +2. feat: 运行 shell 代码支持停止运行 +3. feat: `.bib`, `.plantuml`, `.dot`, `.gv`, `.puml` 文件视为文本文件,可编辑 +4. feat: 编辑器配置支持字体连字 +5. fix: 修复代码块不遵守 `.avoid-page-break` 样式,导致打印时分页不正确的问题 +6. chore: 升级 Electron 到 28.3.3 [更多发布说明](https://github.com/purocean/yn/releases) diff --git a/package.json b/package.json index 757f90666..013c6c886 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "yank.note", - "version": "3.71.0", + "version": "3.71.1", "description": "Yank Note: A highly extensible Markdown editor, designed for productivity.", "main": "dist/main/app.js", "license": "AGPL-3.0",