@tnotesjs/core 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/config/defaultConfig.ts +33 -4
- package/config/index.ts +6 -0
- package/core/NoteIndexCache.ts +7 -1
- package/core/NoteManager.ts +18 -30
- package/dist/{chunk-5Y5IYS6C.js → chunk-UJG3UU5X.js} +21 -5
- package/dist/{chunk-5QN44ES5.js → chunk-XCWFZLER.js} +48 -368
- package/dist/cli/index.js +26 -78
- package/dist/index.js +1 -1
- package/dist/vitepress/config/index.js +159 -18
- package/package.json +3 -6
- package/services/index.ts +0 -1
- package/services/init-sub-repo/initSubRepoLogic.ts +0 -1
- package/services/note/service.ts +0 -1
- package/types/config.ts +0 -3
- package/types/note.ts +0 -2
- package/vitepress/assets/icons/icon__cursor.svg +4 -0
- package/vitepress/assets/icons/index.ts +1 -0
- package/vitepress/components/Layout/AboutPanel.vue +0 -24
- package/vitepress/components/Layout/DocBeforeControls.vue +6 -14
- package/vitepress/components/Layout/DocFooter.vue +3 -3
- package/vitepress/components/Layout/Layout.vue +0 -30
- package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +20 -11
- package/vitepress/components/Layout/homeReadme.data.ts +0 -48
- package/vitepress/components/MindmapPreview/InlineRuns.ts +25 -0
- package/vitepress/components/MindmapPreview/MindmapOutlineNode.vue +62 -0
- package/vitepress/components/MindmapPreview/MindmapPreview.vue +287 -0
- package/vitepress/components/MindmapPreview/compat.test.ts +93 -0
- package/vitepress/components/MindmapPreview/compat.ts +128 -0
- package/vitepress/components/MindmapPreview/expandLevel.test.ts +40 -0
- package/vitepress/components/MindmapPreview/expandLevel.ts +28 -0
- package/vitepress/components/MindmapPreview/legacyCorpus.test.ts +57 -0
- package/vitepress/components/Settings/Settings.vue +76 -73
- package/vitepress/components/SidebarCard/FolderTreeItems.vue +16 -6
- package/vitepress/components/SidebarCard/SidebarCard.vue +21 -27
- package/vitepress/components/composables/useLocalIde.ts +83 -0
- package/vitepress/components/constants.ts +5 -6
- package/vitepress/components/utils/vscodePaths.test.ts +22 -0
- package/vitepress/components/utils/vscodePaths.ts +24 -2
- package/vitepress/configs/markdown.config.ts +77 -20
- package/vitepress/theme/index.ts +4 -2
- package/vitepress/theme/styles/doc-layout.scss +4 -4
- package/vitepress/theme/styles/index.scss +0 -1
- package/services/timestamp/index.ts +0 -7
- package/services/timestamp/service.ts +0 -465
- package/vitepress/components/MarkMap/MarkMap.vue +0 -625
- package/vitepress/theme/styles/components/markmap.scss +0 -122
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
normalizeMindmapMarkdown,
|
|
5
|
+
parseMarkmapFence,
|
|
6
|
+
parseMindmapReference,
|
|
7
|
+
} from './compat'
|
|
8
|
+
|
|
9
|
+
describe('parseMarkmapFence', () => {
|
|
10
|
+
it.each([
|
|
11
|
+
['```markmap', {}],
|
|
12
|
+
['```markmap 2', { initialExpandLevel: 2 }],
|
|
13
|
+
['```markmap {2}', { initialExpandLevel: 2 }],
|
|
14
|
+
['```markmap {initialExpandLevel=3}', { initialExpandLevel: 3 }],
|
|
15
|
+
['```markmap [ESM 模块] 2', { title: 'ESM 模块', initialExpandLevel: 2 }],
|
|
16
|
+
['```markmap 2 [ESM 模块]', { title: 'ESM 模块', initialExpandLevel: 2 }],
|
|
17
|
+
['```mindmap', {}],
|
|
18
|
+
['```mindmap [项目架构] 1', { title: '项目架构', initialExpandLevel: 1 }],
|
|
19
|
+
])('parses %s', (input, expected) => {
|
|
20
|
+
expect(parseMarkmapFence(input)).toEqual(expected)
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('parseMindmapReference', () => {
|
|
25
|
+
it.each([
|
|
26
|
+
['<<< ./assets/tree.md', { path: './assets/tree.md' }],
|
|
27
|
+
['<<< ./assets/tree.md [项目架构]', { path: './assets/tree.md', title: '项目架构' }],
|
|
28
|
+
['<<< "./assets/tree with spaces.md" [项目架构]', { path: './assets/tree with spaces.md', title: '项目架构' }],
|
|
29
|
+
])('parses %s', (input, expected) => {
|
|
30
|
+
expect(parseMindmapReference(input)).toEqual(expected)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('normalizeMindmapMarkdown', () => {
|
|
35
|
+
it('injects the default root for historical unordered-list input', () => {
|
|
36
|
+
expect(normalizeMindmapMarkdown('- A\n - B')).toBe('# root\n\n- A\n - B\n')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('promotes a single legacy root item instead of rendering duplicate roots', () => {
|
|
40
|
+
expect(normalizeMindmapMarkdown('- root\n - item1\n - item2')).toBe(
|
|
41
|
+
'# root\n\n- item1\n- item2\n',
|
|
42
|
+
)
|
|
43
|
+
expect(normalizeMindmapMarkdown('- Custom\n - item', { title: 'Custom' })).toBe(
|
|
44
|
+
'# Custom\n\n- item\n',
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('does not promote a matching root item when it has top-level siblings', () => {
|
|
49
|
+
expect(normalizeMindmapMarkdown('- root\n - nested\n- sibling')).toBe(
|
|
50
|
+
'# root\n\n- root\n - nested\n- sibling\n',
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('preserves an existing H1 and lets explicit fence metadata override it', () => {
|
|
55
|
+
const source = '# Existing\n\n- A\n'
|
|
56
|
+
expect(normalizeMindmapMarkdown(source)).toBe(source)
|
|
57
|
+
expect(normalizeMindmapMarkdown(source, { title: 'Explicit' })).toBe('# Explicit\n\n- A\n')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('converts legacy H2/H3 sections into a nested unordered-list tree', () => {
|
|
61
|
+
const source = [
|
|
62
|
+
'# ESM',
|
|
63
|
+
'',
|
|
64
|
+
'## 基本语法',
|
|
65
|
+
'',
|
|
66
|
+
'### 导出方式',
|
|
67
|
+
'',
|
|
68
|
+
'- 命名导出',
|
|
69
|
+
' - 统一导出',
|
|
70
|
+
'### 导入方式',
|
|
71
|
+
'- 默认导入',
|
|
72
|
+
].join('\n')
|
|
73
|
+
|
|
74
|
+
expect(normalizeMindmapMarkdown(source)).toBe([
|
|
75
|
+
'# ESM',
|
|
76
|
+
'',
|
|
77
|
+
'- 基本语法',
|
|
78
|
+
'',
|
|
79
|
+
' - 导出方式',
|
|
80
|
+
'',
|
|
81
|
+
' - 命名导出',
|
|
82
|
+
' - 统一导出',
|
|
83
|
+
' - 导入方式',
|
|
84
|
+
' - 默认导入',
|
|
85
|
+
'',
|
|
86
|
+
].join('\n'))
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('is stable when normalized more than once', () => {
|
|
90
|
+
const once = normalizeMindmapMarkdown('- A\n - B')
|
|
91
|
+
expect(normalizeMindmapMarkdown(once)).toBe(once)
|
|
92
|
+
})
|
|
93
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export interface MarkmapFenceOptions {
|
|
2
|
+
title?: string
|
|
3
|
+
initialExpandLevel?: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface MindmapReference {
|
|
7
|
+
path: string
|
|
8
|
+
title?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function cleanHeadingText(value: string): string {
|
|
12
|
+
return value.trim().replace(/\s+#+\s*$/, '').trim()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function promoteLegacyRootList(body: string[], rootTitle: string): string[] {
|
|
16
|
+
const firstContentIndex = body.findIndex((line) => line.trim() !== '')
|
|
17
|
+
if (firstContentIndex < 0) return body
|
|
18
|
+
|
|
19
|
+
const firstItem = body[firstContentIndex].match(/^[-+*]\s+(.+?)\s*$/)
|
|
20
|
+
if (!firstItem || cleanHeadingText(firstItem[1]) !== rootTitle) return body
|
|
21
|
+
|
|
22
|
+
const descendants = body.slice(firstContentIndex + 1)
|
|
23
|
+
if (descendants.some((line) => line.trim() !== '' && !/^\s{2,}/.test(line))) return body
|
|
24
|
+
|
|
25
|
+
return [
|
|
26
|
+
...body.slice(0, firstContentIndex),
|
|
27
|
+
...descendants.map((line) => line.replace(/^ {2}/, '')),
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Parse the opening fence while retaining every legacy expand-level spelling. */
|
|
32
|
+
export function parseMarkmapFence(openLine: string): MarkmapFenceOptions {
|
|
33
|
+
const fenceBody = openLine.trim().replace(/^`+\s*/, '')
|
|
34
|
+
const nameMatch = fenceBody.match(/^(mindmap|markmap)(?=\s|\{|\[|$)/)
|
|
35
|
+
if (!nameMatch) return {}
|
|
36
|
+
let rest = fenceBody.slice(nameMatch[1].length).trim()
|
|
37
|
+
const options: MarkmapFenceOptions = {}
|
|
38
|
+
|
|
39
|
+
const titleMatch = rest.match(/\[([^\]]+)\]/)
|
|
40
|
+
if (titleMatch) {
|
|
41
|
+
options.title = titleMatch[1].trim()
|
|
42
|
+
rest = `${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const braceMatch = rest.match(/\{([^}]*)\}/)
|
|
46
|
+
const paramPart = braceMatch ? braceMatch[1].trim() : rest
|
|
47
|
+
const tokens = paramPart.match(/"[^"]*"|'[^']*'|\S+/g) ?? []
|
|
48
|
+
for (const [index, token] of tokens.entries()) {
|
|
49
|
+
if (/^\d+$/.test(token) && index === 0) {
|
|
50
|
+
options.initialExpandLevel = Number(token)
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
const pair = token.match(/^([^=:\s]+)\s*(?:=|:)\s*(.+)$/)
|
|
54
|
+
if (!pair || pair[1] !== 'initialExpandLevel') continue
|
|
55
|
+
const value = pair[2].replace(/^['"]|['"]$/g, '')
|
|
56
|
+
if (/^\d+$/.test(value)) options.initialExpandLevel = Number(value)
|
|
57
|
+
}
|
|
58
|
+
return options
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Parse `<<< file.md [title]`; the title is optional and paths may be quoted. */
|
|
62
|
+
export function parseMindmapReference(line: string): MindmapReference | null {
|
|
63
|
+
const match = line.trim().match(/^<<<\s+(.+?)\s*$/)
|
|
64
|
+
if (!match) return null
|
|
65
|
+
|
|
66
|
+
let rest = match[1].trim()
|
|
67
|
+
let title: string | undefined
|
|
68
|
+
const titleMatch = rest.match(/\s+\[([^\]]+)\]\s*$/)
|
|
69
|
+
if (titleMatch) {
|
|
70
|
+
title = cleanHeadingText(titleMatch[1]) || undefined
|
|
71
|
+
rest = rest.slice(0, titleMatch.index).trim()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const path = rest.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, '$1$2').trim()
|
|
75
|
+
return path ? { path, title } : null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface NormalizeMindmapOptions {
|
|
79
|
+
title?: string
|
|
80
|
+
defaultTitle?: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Convert historical VitePress markmap input into the strict mindmap-core format. */
|
|
84
|
+
export function normalizeMindmapMarkdown(
|
|
85
|
+
source: string,
|
|
86
|
+
options: NormalizeMindmapOptions = {},
|
|
87
|
+
): string {
|
|
88
|
+
const lines = source.replace(/\r\n?/g, '\n').split('\n')
|
|
89
|
+
let existingTitle = ''
|
|
90
|
+
let rootIndex = -1
|
|
91
|
+
|
|
92
|
+
for (let index = 0; index < lines.length; index++) {
|
|
93
|
+
const match = lines[index].match(/^\s{0,3}#(?!#)\s+(.+?)\s*$/)
|
|
94
|
+
if (!match) continue
|
|
95
|
+
existingTitle = cleanHeadingText(match[1])
|
|
96
|
+
rootIndex = index
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const rootTitle = cleanHeadingText(options.title || existingTitle || options.defaultTitle || 'root') || 'root'
|
|
101
|
+
const body: string[] = []
|
|
102
|
+
let headingDepth: number | null = null
|
|
103
|
+
|
|
104
|
+
for (let index = 0; index < lines.length; index++) {
|
|
105
|
+
if (index === rootIndex) continue
|
|
106
|
+
const line = lines[index]
|
|
107
|
+
const heading = line.match(/^\s{0,3}(#{2,6})\s+(.+?)\s*$/)
|
|
108
|
+
if (heading) {
|
|
109
|
+
headingDepth = heading[1].length - 2
|
|
110
|
+
body.push(`${' '.repeat(headingDepth)}- ${cleanHeadingText(heading[2])}`)
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const listItem = line.match(/^(\s*)([-+*])\s+(.+)$/)
|
|
115
|
+
if (listItem && headingDepth !== null) {
|
|
116
|
+
body.push(`${' '.repeat(headingDepth + 1)}${line}`)
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
body.push(line)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const normalizedBody = promoteLegacyRootList(body, rootTitle)
|
|
123
|
+
while (normalizedBody[0]?.trim() === '') normalizedBody.shift()
|
|
124
|
+
while (normalizedBody[normalizedBody.length - 1]?.trim() === '') normalizedBody.pop()
|
|
125
|
+
return normalizedBody.length > 0
|
|
126
|
+
? `# ${rootTitle}\n\n${normalizedBody.join('\n')}\n`
|
|
127
|
+
: `# ${rootTitle}\n`
|
|
128
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { MindmapSession } from '@tnotesjs/mindmap-core'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { applyInitialExpandLevel, normalizeExpandLevel } from './expandLevel'
|
|
5
|
+
|
|
6
|
+
function createSession(): MindmapSession {
|
|
7
|
+
return new MindmapSession({
|
|
8
|
+
markdown: '# root\n\n- 一级\n - 二级\n - 三级\n',
|
|
9
|
+
fileName: 'expand-level.tn-mindmap.md',
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function visibleLabels(session: MindmapSession): string[] {
|
|
14
|
+
const result: string[] = []
|
|
15
|
+
const visit = (node: typeof session.document.root) => {
|
|
16
|
+
result.push(node.content.text)
|
|
17
|
+
if (!node.collapsed) node.children.forEach(visit)
|
|
18
|
+
}
|
|
19
|
+
visit(session.document.root)
|
|
20
|
+
return result
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('applyInitialExpandLevel', () => {
|
|
24
|
+
it('clamps the minimum level to one', () => {
|
|
25
|
+
expect(normalizeExpandLevel(0)).toBe(1)
|
|
26
|
+
expect(normalizeExpandLevel(-2)).toBe(1)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('shows only root and first-level children for level one', () => {
|
|
30
|
+
const session = createSession()
|
|
31
|
+
applyInitialExpandLevel(session, 1)
|
|
32
|
+
expect(visibleLabels(session)).toEqual(['root', '一级'])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('shows root, first-level and second-level children for level two', () => {
|
|
36
|
+
const session = createSession()
|
|
37
|
+
applyInitialExpandLevel(session, 2)
|
|
38
|
+
expect(visibleLabels(session)).toEqual(['root', '一级', '二级'])
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { MindmapNode, MindmapSession } from '@tnotesjs/mindmap-core'
|
|
2
|
+
|
|
3
|
+
export function normalizeExpandLevel(value: number): number {
|
|
4
|
+
return Math.max(1, Math.trunc(Number(value) || 1))
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function childLevel(node: MindmapNode, root: MindmapNode): number {
|
|
8
|
+
let level = 0
|
|
9
|
+
let current: MindmapNode | null = node
|
|
10
|
+
while (current && current !== root) {
|
|
11
|
+
level += 1
|
|
12
|
+
current = current.parent
|
|
13
|
+
}
|
|
14
|
+
return level
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Level 1 renders root + direct children; level 2 additionally renders
|
|
19
|
+
* grandchildren. Nodes at the last visible level are collapsed.
|
|
20
|
+
*/
|
|
21
|
+
export function applyInitialExpandLevel(session: MindmapSession, value: number): void {
|
|
22
|
+
const visibleLevel = normalizeExpandLevel(value)
|
|
23
|
+
const root = session.document.root
|
|
24
|
+
session.document.traverse((node) => {
|
|
25
|
+
if (node === root || node.children.length === 0) return
|
|
26
|
+
session.setCollapsed(node.id, childLevel(node, root) >= visibleLevel)
|
|
27
|
+
})
|
|
28
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { parseMarkdown } from '@tnotesjs/mindmap-core'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
normalizeMindmapMarkdown,
|
|
9
|
+
parseMarkmapFence,
|
|
10
|
+
parseMindmapReference,
|
|
11
|
+
} from './compat'
|
|
12
|
+
|
|
13
|
+
const tnotesRoot = path.resolve(fileURLToPath(new URL('../../../../', import.meta.url)))
|
|
14
|
+
const noteFiles = [
|
|
15
|
+
'TNotes.canvas/notes/0035. 使用 ctx.drawImage 引入图像/README.md',
|
|
16
|
+
'TNotes.docs/notes/0013. Mindmap/README.md',
|
|
17
|
+
'TNotes.docs/notes/0014. 分仓库模式/README.md',
|
|
18
|
+
'TNotes.javascript/notes/0070. CommonJS/README.md',
|
|
19
|
+
'TNotes.javascript/notes/0071. ESM/README.md',
|
|
20
|
+
'TNotes.sql/notes/0001. MySQL 8 从入门到精通/README.md',
|
|
21
|
+
'TNotes.vite/notes/0014. vite 思维导图/README.md',
|
|
22
|
+
].map((relative) => path.join(tnotesRoot, relative))
|
|
23
|
+
|
|
24
|
+
const corpusAvailable = noteFiles.every((file) => fs.existsSync(file))
|
|
25
|
+
|
|
26
|
+
describe.skipIf(!corpusAvailable)('legacy TNotes markmap corpus', () => {
|
|
27
|
+
it('normalizes every historical block into valid mindmap-core Markdown', () => {
|
|
28
|
+
const diagnostics: string[] = []
|
|
29
|
+
let blockCount = 0
|
|
30
|
+
|
|
31
|
+
for (const noteFile of noteFiles) {
|
|
32
|
+
const note = fs.readFileSync(noteFile, 'utf8')
|
|
33
|
+
const blocks = note.matchAll(/^(```(?:mindmap|markmap)[^\n]*)\n([\s\S]*?)^```\s*$/gm)
|
|
34
|
+
for (const match of blocks) {
|
|
35
|
+
blockCount += 1
|
|
36
|
+
const options = parseMarkmapFence(match[1])
|
|
37
|
+
let source = match[2]
|
|
38
|
+
const reference = parseMindmapReference(
|
|
39
|
+
source.split('\n').find((line) => line.trim()) ?? '',
|
|
40
|
+
)
|
|
41
|
+
if (reference) {
|
|
42
|
+
source = fs.readFileSync(path.resolve(path.dirname(noteFile), reference.path), 'utf8')
|
|
43
|
+
}
|
|
44
|
+
const normalized = normalizeMindmapMarkdown(source, {
|
|
45
|
+
title: options.title || reference?.title,
|
|
46
|
+
})
|
|
47
|
+
const result = parseMarkdown(normalized, path.basename(noteFile))
|
|
48
|
+
if (!result.valid) {
|
|
49
|
+
diagnostics.push(`${noteFile}\n${result.diagnostics.map((item) => item.message).join('\n')}`)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
expect(blockCount).toBe(16)
|
|
55
|
+
expect(diagnostics).toEqual([])
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -10,33 +10,60 @@ vitepress/components/Settings/Settings.vue
|
|
|
10
10
|
<div class="settingItem">
|
|
11
11
|
<div class="settingMeta">
|
|
12
12
|
<div class="labelLine">
|
|
13
|
-
<span class="itemName"
|
|
13
|
+
<span class="itemName">本地 IDE 打开</span>
|
|
14
14
|
<span class="infoWrap">
|
|
15
15
|
<span class="infoIcon">?</span>
|
|
16
16
|
<span class="tooltip">
|
|
17
|
-
适用于 PC
|
|
17
|
+
适用于 PC 桌面环境。配置本地知识库 notes 目录路径,并选择 VS Code 或 Cursor,即可从页面快速打开知识库或笔记。
|
|
18
18
|
</span>
|
|
19
19
|
</span>
|
|
20
20
|
</div>
|
|
21
21
|
<span v-if="path" class="statusText">已配置</span>
|
|
22
22
|
</div>
|
|
23
23
|
|
|
24
|
-
<div class="
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
24
|
+
<div class="fieldStack">
|
|
25
|
+
<div class="controlRow">
|
|
26
|
+
<span class="controlLabel">IDE</span>
|
|
27
|
+
<div class="segmented segmented-two">
|
|
28
|
+
<label
|
|
29
|
+
v-for="option in localIdeOptions"
|
|
30
|
+
:key="option.value"
|
|
31
|
+
:class="[
|
|
32
|
+
'segment',
|
|
33
|
+
{ activeSegment: localIde === option.value },
|
|
34
|
+
]"
|
|
35
|
+
>
|
|
36
|
+
<input
|
|
37
|
+
v-model="localIde"
|
|
38
|
+
type="radio"
|
|
39
|
+
name="local-ide"
|
|
40
|
+
:value="option.value"
|
|
41
|
+
/>
|
|
42
|
+
<span>{{ option.label }}</span>
|
|
43
|
+
</label>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<label class="field">
|
|
48
|
+
<span class="controlLabel">知识库路径</span>
|
|
49
|
+
<div class="inputGroup">
|
|
50
|
+
<input
|
|
51
|
+
v-model="path"
|
|
52
|
+
type="text"
|
|
53
|
+
placeholder="/Users/username/notes"
|
|
54
|
+
class="input pathInput"
|
|
55
|
+
/>
|
|
56
|
+
<button
|
|
57
|
+
v-if="path"
|
|
58
|
+
@click="clearPath"
|
|
59
|
+
class="clearBtn"
|
|
60
|
+
title="清空"
|
|
61
|
+
type="button"
|
|
62
|
+
>
|
|
63
|
+
×
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
</label>
|
|
40
67
|
</div>
|
|
41
68
|
</div>
|
|
42
69
|
|
|
@@ -128,36 +155,6 @@ vitepress/components/Settings/Settings.vue
|
|
|
128
155
|
</div>
|
|
129
156
|
</div>
|
|
130
157
|
</div>
|
|
131
|
-
|
|
132
|
-
<div class="settingItem">
|
|
133
|
-
<div class="settingMeta">
|
|
134
|
-
<div class="labelLine">
|
|
135
|
-
<span class="itemName">笔记内 MarkMap</span>
|
|
136
|
-
<span class="infoWrap">
|
|
137
|
-
<span class="infoIcon">?</span>
|
|
138
|
-
<span class="tooltip">
|
|
139
|
-
配置笔记中 markmap 代码块的初始展开层级。
|
|
140
|
-
</span>
|
|
141
|
-
</span>
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
144
|
-
|
|
145
|
-
<div class="inlineGrid">
|
|
146
|
-
<label class="field">
|
|
147
|
-
<span class="controlLabel">展开层级</span>
|
|
148
|
-
<div class="inputWithUnit">
|
|
149
|
-
<input
|
|
150
|
-
v-model.number="markmapExpandLevel"
|
|
151
|
-
type="number"
|
|
152
|
-
min="1"
|
|
153
|
-
max="100"
|
|
154
|
-
class="input"
|
|
155
|
-
/>
|
|
156
|
-
<span class="unit">层</span>
|
|
157
|
-
</div>
|
|
158
|
-
</label>
|
|
159
|
-
</div>
|
|
160
|
-
</div>
|
|
161
158
|
</section>
|
|
162
159
|
|
|
163
160
|
<div class="actionBar">
|
|
@@ -185,12 +182,18 @@ import { ref, computed, onMounted } from 'vue'
|
|
|
185
182
|
|
|
186
183
|
import {
|
|
187
184
|
NOTES_DIR_KEY,
|
|
188
|
-
|
|
185
|
+
LOCAL_IDE_KEY,
|
|
189
186
|
SIDEBAR_SHOW_NOTE_ID_KEY,
|
|
190
187
|
SIDEBAR_DONE_PREFIX_KEY,
|
|
191
188
|
SIDEBAR_UNDONE_PREFIX_KEY,
|
|
192
189
|
CONTENT_WIDTH_MODE_KEY,
|
|
193
190
|
} from '../constants'
|
|
191
|
+
import { useLocalIde } from '../composables/useLocalIde'
|
|
192
|
+
import {
|
|
193
|
+
DEFAULT_LOCAL_IDE,
|
|
194
|
+
normalizeLocalIde,
|
|
195
|
+
type LocalIdeId,
|
|
196
|
+
} from '../utils/vscodePaths'
|
|
194
197
|
// @ts-expect-error - VitePress Data Loader
|
|
195
198
|
import { data as tnotesConfig } from '../tnotes-config.data'
|
|
196
199
|
|
|
@@ -200,6 +203,8 @@ const DEFAULT_CONTENT_WIDTH_MODE: ContentWidthMode = 'wide'
|
|
|
200
203
|
const DEFAULT_DONE_PREFIX = '✅'
|
|
201
204
|
const DEFAULT_UNDONE_PREFIX = '⏰'
|
|
202
205
|
|
|
206
|
+
const { setLocalIde } = useLocalIde()
|
|
207
|
+
|
|
203
208
|
const contentWidthModeOptions: Array<{
|
|
204
209
|
label: string
|
|
205
210
|
value: ContentWidthMode
|
|
@@ -208,10 +213,15 @@ const contentWidthModeOptions: Array<{
|
|
|
208
213
|
{ label: '标准页宽', value: 'standard' },
|
|
209
214
|
]
|
|
210
215
|
|
|
216
|
+
const localIdeOptions: Array<{ label: string; value: LocalIdeId }> = [
|
|
217
|
+
{ label: 'VS Code', value: 'vscode' },
|
|
218
|
+
{ label: 'Cursor', value: 'cursor' },
|
|
219
|
+
]
|
|
220
|
+
|
|
211
221
|
const path = ref('')
|
|
212
222
|
const originalPath = ref('')
|
|
213
|
-
const
|
|
214
|
-
const
|
|
223
|
+
const localIde = ref<LocalIdeId>(DEFAULT_LOCAL_IDE)
|
|
224
|
+
const originalLocalIde = ref<LocalIdeId>(DEFAULT_LOCAL_IDE)
|
|
215
225
|
const showNoteId = ref(false)
|
|
216
226
|
const originalShowNoteId = ref(false)
|
|
217
227
|
const donePrefix = ref(DEFAULT_DONE_PREFIX)
|
|
@@ -227,7 +237,7 @@ const showSuccessToast = ref(false)
|
|
|
227
237
|
const hasChanges = computed(
|
|
228
238
|
() =>
|
|
229
239
|
path.value !== originalPath.value ||
|
|
230
|
-
|
|
240
|
+
localIde.value !== originalLocalIde.value ||
|
|
231
241
|
showNoteId.value !== originalShowNoteId.value ||
|
|
232
242
|
donePrefix.value !== originalDonePrefix.value ||
|
|
233
243
|
undonePrefix.value !== originalUndonePrefix.value ||
|
|
@@ -246,9 +256,9 @@ onMounted(() => {
|
|
|
246
256
|
path.value = savedPath
|
|
247
257
|
originalPath.value = savedPath
|
|
248
258
|
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
259
|
+
const savedLocalIde = normalizeLocalIde(localStorage.getItem(LOCAL_IDE_KEY))
|
|
260
|
+
localIde.value = savedLocalIde
|
|
261
|
+
originalLocalIde.value = savedLocalIde
|
|
252
262
|
|
|
253
263
|
const savedShowNoteId = localStorage.getItem(SIDEBAR_SHOW_NOTE_ID_KEY)
|
|
254
264
|
showNoteId.value =
|
|
@@ -293,17 +303,14 @@ function save() {
|
|
|
293
303
|
contentWidthMode.value !== originalContentWidthMode.value
|
|
294
304
|
|
|
295
305
|
localStorage.setItem(NOTES_DIR_KEY, path.value)
|
|
296
|
-
|
|
297
|
-
MARKMAP_EXPAND_LEVEL_KEY,
|
|
298
|
-
markmapExpandLevel.value.toString(),
|
|
299
|
-
)
|
|
306
|
+
setLocalIde(localIde.value)
|
|
300
307
|
localStorage.setItem(SIDEBAR_SHOW_NOTE_ID_KEY, showNoteId.value.toString())
|
|
301
308
|
localStorage.setItem(SIDEBAR_DONE_PREFIX_KEY, donePrefix.value)
|
|
302
309
|
localStorage.setItem(SIDEBAR_UNDONE_PREFIX_KEY, undonePrefix.value)
|
|
303
310
|
localStorage.setItem(CONTENT_WIDTH_MODE_KEY, contentWidthMode.value)
|
|
304
311
|
|
|
305
312
|
originalPath.value = path.value
|
|
306
|
-
|
|
313
|
+
originalLocalIde.value = localIde.value
|
|
307
314
|
originalShowNoteId.value = showNoteId.value
|
|
308
315
|
originalDonePrefix.value = donePrefix.value
|
|
309
316
|
originalUndonePrefix.value = undonePrefix.value
|
|
@@ -327,7 +334,7 @@ function save() {
|
|
|
327
334
|
|
|
328
335
|
function reset() {
|
|
329
336
|
path.value = originalPath.value
|
|
330
|
-
|
|
337
|
+
localIde.value = originalLocalIde.value
|
|
331
338
|
showNoteId.value = originalShowNoteId.value
|
|
332
339
|
donePrefix.value = originalDonePrefix.value
|
|
333
340
|
undonePrefix.value = originalUndonePrefix.value
|
|
@@ -400,10 +407,12 @@ function reset() {
|
|
|
400
407
|
|
|
401
408
|
.tooltip {
|
|
402
409
|
position: absolute;
|
|
403
|
-
left:
|
|
404
|
-
|
|
405
|
-
|
|
410
|
+
left: 0;
|
|
411
|
+
top: calc(100% + 8px);
|
|
412
|
+
bottom: auto;
|
|
413
|
+
z-index: 20;
|
|
406
414
|
width: 240px;
|
|
415
|
+
max-width: min(240px, calc(100vw - 48px));
|
|
407
416
|
padding: 8px 10px;
|
|
408
417
|
color: var(--vp-c-text-1);
|
|
409
418
|
background: var(--vp-c-bg-elv);
|
|
@@ -414,7 +423,7 @@ function reset() {
|
|
|
414
423
|
line-height: 18px;
|
|
415
424
|
opacity: 0;
|
|
416
425
|
pointer-events: none;
|
|
417
|
-
transform:
|
|
426
|
+
transform: translateY(-4px);
|
|
418
427
|
transition:
|
|
419
428
|
opacity 0.15s ease,
|
|
420
429
|
transform 0.15s ease;
|
|
@@ -423,7 +432,7 @@ function reset() {
|
|
|
423
432
|
.infoWrap:hover .tooltip,
|
|
424
433
|
.infoWrap:focus-within .tooltip {
|
|
425
434
|
opacity: 1;
|
|
426
|
-
transform:
|
|
435
|
+
transform: translateY(0);
|
|
427
436
|
}
|
|
428
437
|
|
|
429
438
|
.inputGroup {
|
|
@@ -699,14 +708,8 @@ function reset() {
|
|
|
699
708
|
}
|
|
700
709
|
|
|
701
710
|
.tooltip {
|
|
702
|
-
left: 0;
|
|
703
711
|
width: min(260px, calc(100vw - 48px));
|
|
704
|
-
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
.infoWrap:hover .tooltip,
|
|
708
|
-
.infoWrap:focus-within .tooltip {
|
|
709
|
-
transform: translate(0, 0);
|
|
712
|
+
max-width: min(260px, calc(100vw - 48px));
|
|
710
713
|
}
|
|
711
714
|
|
|
712
715
|
.actionBar {
|
|
@@ -6,8 +6,8 @@ vitepress/components/SidebarCard/FolderTreeItems.vue
|
|
|
6
6
|
import {
|
|
7
7
|
icon__sidebar_collapsed,
|
|
8
8
|
icon__sidebar_opened,
|
|
9
|
-
icon__vscode,
|
|
10
9
|
} from '../../assets/icons'
|
|
10
|
+
import { useLocalIde } from '../composables/useLocalIde'
|
|
11
11
|
|
|
12
12
|
const props = defineProps({
|
|
13
13
|
nodes: {
|
|
@@ -30,6 +30,8 @@ const props = defineProps({
|
|
|
30
30
|
|
|
31
31
|
const emit = defineEmits(['toggle-node', 'open-vscode'])
|
|
32
32
|
|
|
33
|
+
const { icon: localIdeIcon, openNoteTitle } = useLocalIde()
|
|
34
|
+
|
|
33
35
|
function hasChildren(node) {
|
|
34
36
|
return node.children?.length > 0
|
|
35
37
|
}
|
|
@@ -109,21 +111,24 @@ function onOpenVscode(node, event) {
|
|
|
109
111
|
<span class="tree-title">{{ getDisplayTitle(node) }}</span>
|
|
110
112
|
</a>
|
|
111
113
|
|
|
112
|
-
<
|
|
114
|
+
<button
|
|
113
115
|
v-else-if="hasChildren(node)"
|
|
116
|
+
type="button"
|
|
114
117
|
class="tree-link tree-link-folder"
|
|
118
|
+
:title="isExpanded(node) ? '折叠分组' : '展开分组'"
|
|
119
|
+
@click="onToggle(node)"
|
|
115
120
|
>
|
|
116
121
|
<span class="tree-title">{{ getDisplayTitle(node) }}</span>
|
|
117
|
-
</
|
|
122
|
+
</button>
|
|
118
123
|
|
|
119
124
|
<button
|
|
120
125
|
v-if="node.relativePath"
|
|
121
126
|
class="vscode-article"
|
|
122
127
|
type="button"
|
|
123
|
-
title="
|
|
128
|
+
:title="openNoteTitle"
|
|
124
129
|
@click="onOpenVscode(node, $event)"
|
|
125
130
|
>
|
|
126
|
-
<img :src="
|
|
131
|
+
<img :src="localIdeIcon" :alt="openNoteTitle" />
|
|
127
132
|
</button>
|
|
128
133
|
</div>
|
|
129
134
|
|
|
@@ -225,9 +230,14 @@ export default {
|
|
|
225
230
|
}
|
|
226
231
|
|
|
227
232
|
.tree-link-folder {
|
|
228
|
-
cursor:
|
|
233
|
+
cursor: pointer;
|
|
229
234
|
font-weight: 600;
|
|
230
235
|
color: var(--vp-c-text-1);
|
|
236
|
+
border: none;
|
|
237
|
+
background: none;
|
|
238
|
+
padding: 0;
|
|
239
|
+
text-align: left;
|
|
240
|
+
font: inherit;
|
|
231
241
|
}
|
|
232
242
|
|
|
233
243
|
.tree-status {
|