@tnotesjs/ui 0.1.4 → 0.2.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 +30 -97
- package/dist/code/highlight.js +6 -15
- package/dist/components/Footprints/parse.js +1 -3
- package/dist/components/Mindmap/markdown.d.ts +1 -7
- package/dist/components/Mindmap/markdown.js +1 -15
- package/package.json +28 -13
- package/src/browser/clipboard.ts +16 -19
- package/src/browser/theme.ts +10 -15
- package/src/code/highlight.test.ts +23 -6
- package/src/code/highlight.ts +89 -105
- package/src/components/Badge/Badge.vue +4 -4
- package/src/components/CodeBlock/CodeBlock.vue +43 -53
- package/src/components/CodeGroup/CodeGroup.vue +32 -35
- package/src/components/Footprints/Footprints.vue +2 -14
- package/src/components/Footprints/parse.ts +1 -3
- package/src/components/ImagePreview/ImagePreview.vue +101 -126
- package/src/components/Mermaid/Mermaid.vue +9 -12
- package/src/components/Mindmap/FocusBreadcrumbs.test.ts +40 -30
- package/src/components/Mindmap/FocusBreadcrumbs.vue +31 -28
- package/src/components/Mindmap/InlineRuns.ts +25 -16
- package/src/components/Mindmap/Mindmap.vue +147 -81
- package/src/components/Mindmap/MindmapOutlineNode.vue +4 -1
- package/src/components/Mindmap/editor/AppIcon.vue +32 -11
- package/src/components/Mindmap/editor/CanvasContextMenu.vue +126 -49
- package/src/components/Mindmap/editor/LinkPopover.vue +101 -23
- package/src/components/Mindmap/editor/MarkdownView.vue +7 -4
- package/src/components/Mindmap/editor/MindmapView.vue +11 -3
- package/src/components/Mindmap/editor/OutlineView.vue +144 -58
- package/src/components/Mindmap/editor/RichInlineEditor.vue +93 -44
- package/src/components/Mindmap/editor/SelectionToolbar.vue +133 -41
- package/src/components/Mindmap/editor/canvasClipboard.ts +1 -5
- package/src/components/Mindmap/editor/imagePaste.ts +1 -1
- package/src/components/Mindmap/editor/mindmapClipboard.ts +17 -5
- package/src/components/Mindmap/editor/outlineDrag.ts +1 -1
- package/src/components/Mindmap/markdown.ts +10 -29
- package/src/components/WordList/RightClickMenu.vue +10 -35
- package/src/components/WordList/WordList.vue +19 -42
- package/src/components/sharedMedia.test.ts +94 -112
- package/src/entries/bilibili-video.ts +1 -1
- package/src/entries/excalidraw-editor.ts +20 -0
- package/src/entries/excalidraw-view.ts +15 -0
- package/src/entries/footprints.ts +1 -1
- package/src/entries/mermaid.ts +1 -1
- package/src/entries/mindmap.ts +1 -1
- package/src/entries/notes-table.ts +2 -2
- package/src/entries/word-list.ts +1 -1
- package/src/excalidraw/ExcalidrawSvg.vue +125 -0
- package/src/excalidraw/editorHost.ts +110 -0
- package/src/excalidraw/editorSession.test.ts +205 -0
- package/src/excalidraw/editorSession.ts +224 -0
- package/src/excalidraw/exporter.ts +50 -0
- package/src/excalidraw/focus.test.ts +60 -0
- package/src/excalidraw/focus.ts +33 -0
- package/src/excalidraw/fonts.test.ts +66 -0
- package/src/excalidraw/fonts.ts +92 -0
- package/src/excalidraw/renderer.test.ts +148 -0
- package/src/excalidraw/renderer.ts +105 -0
- package/src/excalidraw/scene.ts +93 -0
- package/src/index.ts +25 -33
- package/src/markdown/image.test.ts +1 -3
- package/src/markdown/image.ts +4 -13
- package/src/styles/code.css +5 -5
- package/src/styles/prose.css +2 -2
- package/src/styles/swiper.css +1 -1
- package/src/styles/tokens.css +5 -6
- package/src/swiper/hydrate.test.ts +37 -41
- package/src/swiper/hydrate.ts +91 -102
package/src/code/highlight.ts
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type ShikiTransformer,
|
|
5
|
-
} from "shiki/core";
|
|
6
|
-
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
|
|
7
|
-
import { bundledLanguages, bundledLanguagesAlias } from "shiki/langs";
|
|
1
|
+
import { createHighlighterCore, type HighlighterCore, type ShikiTransformer } from 'shiki/core'
|
|
2
|
+
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
|
|
3
|
+
import { bundledLanguages, bundledLanguagesAlias } from 'shiki/langs'
|
|
8
4
|
|
|
9
5
|
export interface CodeMeta {
|
|
10
|
-
language: string
|
|
11
|
-
title: string
|
|
12
|
-
lineNumbers: boolean
|
|
13
|
-
startLine: number
|
|
14
|
-
highlightedLines: number[]
|
|
6
|
+
language: string
|
|
7
|
+
title: string
|
|
8
|
+
lineNumbers: boolean
|
|
9
|
+
startLine: number
|
|
10
|
+
highlightedLines: number[]
|
|
15
11
|
}
|
|
16
12
|
|
|
17
13
|
/**
|
|
@@ -20,113 +16,103 @@ export interface CodeMeta {
|
|
|
20
16
|
*/
|
|
21
17
|
export const SHARED_CODE_GROUP_CONTRACT = {
|
|
22
18
|
source: [
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
].join(
|
|
19
|
+
'::: code-group',
|
|
20
|
+
'```js:line-numbers=4 {2} [one.js]',
|
|
21
|
+
'console.log(1)',
|
|
22
|
+
'console.log(2)',
|
|
23
|
+
'```',
|
|
24
|
+
'```ts:no-line-numbers [two.ts]',
|
|
25
|
+
'const value: number = 2',
|
|
26
|
+
'```',
|
|
27
|
+
':::'
|
|
28
|
+
].join('\n'),
|
|
33
29
|
items: [
|
|
34
30
|
{
|
|
35
|
-
info:
|
|
36
|
-
code:
|
|
37
|
-
title:
|
|
38
|
-
language:
|
|
31
|
+
info: 'js:line-numbers=4 {2} [one.js]',
|
|
32
|
+
code: 'console.log(1)\nconsole.log(2)',
|
|
33
|
+
title: 'one.js',
|
|
34
|
+
language: 'js',
|
|
39
35
|
lineNumbers: true,
|
|
40
36
|
startLine: 4,
|
|
41
|
-
highlightedLines: [2]
|
|
37
|
+
highlightedLines: [2]
|
|
42
38
|
},
|
|
43
39
|
{
|
|
44
|
-
info:
|
|
45
|
-
code:
|
|
46
|
-
title:
|
|
47
|
-
language:
|
|
40
|
+
info: 'ts:no-line-numbers [two.ts]',
|
|
41
|
+
code: 'const value: number = 2',
|
|
42
|
+
title: 'two.ts',
|
|
43
|
+
language: 'ts',
|
|
48
44
|
lineNumbers: false,
|
|
49
45
|
startLine: 1,
|
|
50
|
-
highlightedLines: []
|
|
51
|
-
}
|
|
52
|
-
]
|
|
53
|
-
} as const
|
|
46
|
+
highlightedLines: []
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
} as const
|
|
54
50
|
|
|
55
|
-
export function parseCodeMeta(info =
|
|
51
|
+
export function parseCodeMeta(info = '', defaultLineNumbers = true): CodeMeta {
|
|
56
52
|
const language =
|
|
57
53
|
info
|
|
58
54
|
.trim()
|
|
59
55
|
.match(/^[^\s{\[]+/)?.[0]
|
|
60
|
-
?.split(
|
|
61
|
-
const highlightedLines = new Set<number>()
|
|
62
|
-
const ranges = info.match(/\{([\d,\s-]+)\}/)?.[1] ||
|
|
63
|
-
for (const range of ranges.split(
|
|
64
|
-
const match = range.trim().match(/^(\d+)(?:-(\d+))?$/)
|
|
65
|
-
if (!match) continue
|
|
66
|
-
const start = Number(match[1])
|
|
67
|
-
const end = Number(match[2] ?? start)
|
|
68
|
-
if (start < 1 || end < start || end - start > 10000) continue
|
|
69
|
-
for (let line = start; line <= end; line++) highlightedLines.add(line)
|
|
56
|
+
?.split(':')[0] || 'text'
|
|
57
|
+
const highlightedLines = new Set<number>()
|
|
58
|
+
const ranges = info.match(/\{([\d,\s-]+)\}/)?.[1] || ''
|
|
59
|
+
for (const range of ranges.split(',')) {
|
|
60
|
+
const match = range.trim().match(/^(\d+)(?:-(\d+))?$/)
|
|
61
|
+
if (!match) continue
|
|
62
|
+
const start = Number(match[1])
|
|
63
|
+
const end = Number(match[2] ?? start)
|
|
64
|
+
if (start < 1 || end < start || end - start > 10000) continue
|
|
65
|
+
for (let line = start; line <= end; line++) highlightedLines.add(line)
|
|
70
66
|
}
|
|
71
67
|
return {
|
|
72
68
|
language,
|
|
73
|
-
title: info.match(/\[([^\]]*)\]/)?.[1]?.trim() ||
|
|
69
|
+
title: info.match(/\[([^\]]*)\]/)?.[1]?.trim() || '',
|
|
74
70
|
lineNumbers: /:no-line-numbers\b/.test(info)
|
|
75
71
|
? false
|
|
76
72
|
: /:line-numbers\b/.test(info) || defaultLineNumbers,
|
|
77
73
|
startLine: Math.max(1, Number(info.match(/:line-numbers=(\d+)/)?.[1] || 1)),
|
|
78
|
-
highlightedLines: [...highlightedLines].sort((a, b) => a - b)
|
|
79
|
-
}
|
|
74
|
+
highlightedLines: [...highlightedLines].sort((a, b) => a - b)
|
|
75
|
+
}
|
|
80
76
|
}
|
|
81
77
|
|
|
82
|
-
let singleton: Promise<HighlighterCore> | undefined
|
|
83
|
-
let ready: HighlighterCore | undefined
|
|
84
|
-
const loading = new Map<string, Promise<void>>()
|
|
78
|
+
let singleton: Promise<HighlighterCore> | undefined
|
|
79
|
+
let ready: HighlighterCore | undefined
|
|
80
|
+
const loading = new Map<string, Promise<void>>()
|
|
85
81
|
|
|
86
82
|
export function normalizeCodeLanguage(language: string): string {
|
|
87
|
-
const id = language.toLowerCase()
|
|
88
|
-
if (id ===
|
|
89
|
-
|
|
90
|
-
if (id in bundledLanguages || id in bundledLanguagesAlias) return id;
|
|
83
|
+
const id = language.toLowerCase()
|
|
84
|
+
if (id === 'plain' || id === 'txt' || id === 'plaintext' || !id) return 'text'
|
|
85
|
+
if (id in bundledLanguages || id in bundledLanguagesAlias) return id
|
|
91
86
|
// Unknown labels retain their caption, but display safely as plain text.
|
|
92
|
-
return
|
|
87
|
+
return 'text'
|
|
93
88
|
}
|
|
94
89
|
|
|
95
90
|
/** One engine and one grammar cache for build-time and browser consumers. */
|
|
96
|
-
export async function prepareCodeHighlighter(
|
|
97
|
-
languages: string[] = [],
|
|
98
|
-
): Promise<HighlighterCore> {
|
|
91
|
+
export async function prepareCodeHighlighter(languages: string[] = []): Promise<HighlighterCore> {
|
|
99
92
|
singleton ??= createHighlighterCore({
|
|
100
93
|
engine: createJavaScriptRegexEngine(),
|
|
101
|
-
themes: [
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
langs: [],
|
|
106
|
-
}).then((highlighter) => (ready = highlighter));
|
|
107
|
-
const highlighter = await singleton;
|
|
94
|
+
themes: [import('shiki/themes/github-light.mjs'), import('shiki/themes/github-dark.mjs')],
|
|
95
|
+
langs: []
|
|
96
|
+
}).then((highlighter) => (ready = highlighter))
|
|
97
|
+
const highlighter = await singleton
|
|
108
98
|
await Promise.all(
|
|
109
99
|
languages.map(async (requested) => {
|
|
110
|
-
const language = normalizeCodeLanguage(requested)
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
highlighter.getLoadedLanguages().includes(language)
|
|
114
|
-
)
|
|
115
|
-
return;
|
|
116
|
-
let pending = loading.get(language);
|
|
100
|
+
const language = normalizeCodeLanguage(requested)
|
|
101
|
+
if (language === 'text' || highlighter.getLoadedLanguages().includes(language)) return
|
|
102
|
+
let pending = loading.get(language)
|
|
117
103
|
if (!pending) {
|
|
118
104
|
const loader =
|
|
119
105
|
bundledLanguages[language as keyof typeof bundledLanguages] ??
|
|
120
|
-
bundledLanguagesAlias[language as keyof typeof bundledLanguagesAlias]
|
|
121
|
-
if (!loader) return
|
|
122
|
-
pending = highlighter.loadLanguage(loader).then(() => undefined)
|
|
123
|
-
loading.set(language, pending)
|
|
124
|
-
pending.catch(() => loading.delete(language))
|
|
106
|
+
bundledLanguagesAlias[language as keyof typeof bundledLanguagesAlias]
|
|
107
|
+
if (!loader) return
|
|
108
|
+
pending = highlighter.loadLanguage(loader).then(() => undefined)
|
|
109
|
+
loading.set(language, pending)
|
|
110
|
+
pending.catch(() => loading.delete(language))
|
|
125
111
|
}
|
|
126
|
-
await pending
|
|
127
|
-
})
|
|
128
|
-
)
|
|
129
|
-
return highlighter
|
|
112
|
+
await pending
|
|
113
|
+
})
|
|
114
|
+
)
|
|
115
|
+
return highlighter
|
|
130
116
|
}
|
|
131
117
|
|
|
132
118
|
function transformers(meta: CodeMeta): ShikiTransformer[] {
|
|
@@ -134,38 +120,36 @@ function transformers(meta: CodeMeta): ShikiTransformer[] {
|
|
|
134
120
|
// notation transformers (`[!code ++]` etc.); Desk has no write path for them.
|
|
135
121
|
return [
|
|
136
122
|
{
|
|
137
|
-
name:
|
|
123
|
+
name: 'tnotes-code-meta',
|
|
138
124
|
pre(node) {
|
|
139
|
-
this.addClassToHast(node,
|
|
140
|
-
node.properties.tabindex = 0
|
|
125
|
+
this.addClassToHast(node, 'tn-code-highlight')
|
|
126
|
+
node.properties.tabindex = 0
|
|
141
127
|
},
|
|
142
128
|
line(node, line) {
|
|
143
|
-
node.properties[
|
|
144
|
-
if (meta.highlightedLines.includes(line))
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
];
|
|
129
|
+
node.properties['data-line'] = String(meta.startLine + line - 1)
|
|
130
|
+
if (meta.highlightedLines.includes(line)) this.addClassToHast(node, 'highlighted')
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
]
|
|
149
134
|
}
|
|
150
135
|
|
|
151
136
|
/** Markdown renderers prewarm document languages before their synchronous render pass. */
|
|
152
|
-
export function highlightCodeSync(code: string, info =
|
|
153
|
-
if (!ready)
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
const normalizedLanguage = normalizeCodeLanguage(meta.language);
|
|
137
|
+
export function highlightCodeSync(code: string, info = ''): string {
|
|
138
|
+
if (!ready) throw new Error('Call prepareCodeHighlighter before rendering Markdown')
|
|
139
|
+
const meta = parseCodeMeta(info)
|
|
140
|
+
const normalizedLanguage = normalizeCodeLanguage(meta.language)
|
|
157
141
|
const language = ready.getLoadedLanguages().includes(normalizedLanguage)
|
|
158
142
|
? normalizedLanguage
|
|
159
|
-
:
|
|
160
|
-
return ready.codeToHtml(code.replace(/\n$/,
|
|
143
|
+
: 'text'
|
|
144
|
+
return ready.codeToHtml(code.replace(/\n$/, ''), {
|
|
161
145
|
lang: language,
|
|
162
|
-
themes: { light:
|
|
146
|
+
themes: { light: 'github-light', dark: 'github-dark' },
|
|
163
147
|
defaultColor: false,
|
|
164
|
-
transformers: transformers(meta)
|
|
165
|
-
})
|
|
148
|
+
transformers: transformers(meta)
|
|
149
|
+
})
|
|
166
150
|
}
|
|
167
151
|
|
|
168
|
-
export async function highlightCode(code: string, info =
|
|
169
|
-
await prepareCodeHighlighter([parseCodeMeta(info).language])
|
|
170
|
-
return highlightCodeSync(code, info)
|
|
152
|
+
export async function highlightCode(code: string, info = ''): Promise<string> {
|
|
153
|
+
await prepareCodeHighlighter([parseCodeMeta(info).language])
|
|
154
|
+
return highlightCodeSync(code, info)
|
|
171
155
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
withDefaults(
|
|
3
3
|
defineProps<{
|
|
4
|
-
text?: string
|
|
5
|
-
type?:
|
|
4
|
+
text?: string
|
|
5
|
+
type?: 'info' | 'tip' | 'warning' | 'danger'
|
|
6
6
|
}>(),
|
|
7
|
-
{ text:
|
|
8
|
-
)
|
|
7
|
+
{ text: '', type: 'info' }
|
|
8
|
+
)
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
11
|
<template>
|
|
@@ -1,73 +1,70 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, onBeforeUnmount, onServerPrefetch, ref, watch } from
|
|
3
|
-
import { highlightCode, parseCodeMeta } from
|
|
4
|
-
import { copyText } from
|
|
2
|
+
import { computed, onBeforeUnmount, onServerPrefetch, ref, watch } from 'vue'
|
|
3
|
+
import { highlightCode, parseCodeMeta } from '../../code/highlight'
|
|
4
|
+
import { copyText } from '../../browser/clipboard'
|
|
5
5
|
|
|
6
6
|
const props = withDefaults(
|
|
7
7
|
defineProps<{
|
|
8
|
-
code: string
|
|
9
|
-
info?: string
|
|
10
|
-
highlightedHtml?: string
|
|
11
|
-
lineNumbers?: boolean
|
|
12
|
-
title?: string
|
|
8
|
+
code: string
|
|
9
|
+
info?: string
|
|
10
|
+
highlightedHtml?: string
|
|
11
|
+
lineNumbers?: boolean
|
|
12
|
+
title?: string
|
|
13
13
|
}>(),
|
|
14
|
-
{ info:
|
|
15
|
-
)
|
|
16
|
-
const meta = computed(() => parseCodeMeta(props.info, props.lineNumbers))
|
|
17
|
-
const html = ref(props.highlightedHtml ||
|
|
18
|
-
const error = ref(
|
|
19
|
-
const copied = ref(false)
|
|
20
|
-
const fullscreen = ref(false)
|
|
21
|
-
const trigger = ref<HTMLButtonElement>()
|
|
22
|
-
let generation = 0
|
|
23
|
-
let copyTimer: ReturnType<typeof setTimeout> | undefined
|
|
14
|
+
{ info: '', lineNumbers: true }
|
|
15
|
+
)
|
|
16
|
+
const meta = computed(() => parseCodeMeta(props.info, props.lineNumbers))
|
|
17
|
+
const html = ref(props.highlightedHtml || '')
|
|
18
|
+
const error = ref('')
|
|
19
|
+
const copied = ref(false)
|
|
20
|
+
const fullscreen = ref(false)
|
|
21
|
+
const trigger = ref<HTMLButtonElement>()
|
|
22
|
+
let generation = 0
|
|
23
|
+
let copyTimer: ReturnType<typeof setTimeout> | undefined
|
|
24
24
|
|
|
25
25
|
async function renderCode(): Promise<void> {
|
|
26
|
-
const version = ++generation
|
|
27
|
-
error.value =
|
|
26
|
+
const version = ++generation
|
|
27
|
+
error.value = ''
|
|
28
28
|
if (props.highlightedHtml !== undefined) {
|
|
29
|
-
html.value = props.highlightedHtml
|
|
30
|
-
return
|
|
29
|
+
html.value = props.highlightedHtml
|
|
30
|
+
return
|
|
31
31
|
}
|
|
32
32
|
try {
|
|
33
|
-
const output = await highlightCode(props.code, props.info)
|
|
34
|
-
if (version === generation) html.value = output
|
|
33
|
+
const output = await highlightCode(props.code, props.info)
|
|
34
|
+
if (version === generation) html.value = output
|
|
35
35
|
} catch (cause) {
|
|
36
36
|
if (version === generation) {
|
|
37
|
-
html.value =
|
|
38
|
-
error.value = `高亮失败:${cause instanceof Error ? cause.message : String(cause)}
|
|
37
|
+
html.value = ''
|
|
38
|
+
error.value = `高亮失败:${cause instanceof Error ? cause.message : String(cause)}`
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
watch(() => [props.code, props.info, props.highlightedHtml], renderCode, {
|
|
43
|
-
immediate: true
|
|
44
|
-
})
|
|
45
|
-
onServerPrefetch(renderCode)
|
|
43
|
+
immediate: true
|
|
44
|
+
})
|
|
45
|
+
onServerPrefetch(renderCode)
|
|
46
46
|
async function copy(): Promise<void> {
|
|
47
47
|
try {
|
|
48
|
-
await copyText(props.code.replace(/\n$/,
|
|
49
|
-
copied.value = true
|
|
50
|
-
clearTimeout(copyTimer)
|
|
51
|
-
copyTimer = setTimeout(() => (copied.value = false), 1500)
|
|
48
|
+
await copyText(props.code.replace(/\n$/, ''))
|
|
49
|
+
copied.value = true
|
|
50
|
+
clearTimeout(copyTimer)
|
|
51
|
+
copyTimer = setTimeout(() => (copied.value = false), 1500)
|
|
52
52
|
} catch {
|
|
53
|
-
error.value =
|
|
53
|
+
error.value = '复制失败,请检查剪贴板权限'
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
function closeFullscreen(): void {
|
|
57
|
-
fullscreen.value = false
|
|
58
|
-
trigger.value?.focus({ preventScroll: true })
|
|
57
|
+
fullscreen.value = false
|
|
58
|
+
trigger.value?.focus({ preventScroll: true })
|
|
59
59
|
}
|
|
60
60
|
onBeforeUnmount(() => {
|
|
61
|
-
generation
|
|
62
|
-
clearTimeout(copyTimer)
|
|
63
|
-
})
|
|
61
|
+
generation++
|
|
62
|
+
clearTimeout(copyTimer)
|
|
63
|
+
})
|
|
64
64
|
</script>
|
|
65
65
|
|
|
66
66
|
<template>
|
|
67
|
-
<section
|
|
68
|
-
class="tn-code-block"
|
|
69
|
-
:class="{ 'has-line-numbers': meta.lineNumbers }"
|
|
70
|
-
>
|
|
67
|
+
<section class="tn-code-block" :class="{ 'has-line-numbers': meta.lineNumbers }">
|
|
71
68
|
<header class="tn-code-block__header">
|
|
72
69
|
<span class="tn-code-block__title">{{ title || meta.title }}</span>
|
|
73
70
|
<slot name="language" :language="meta.language"
|
|
@@ -108,9 +105,7 @@ onBeforeUnmount(() => {
|
|
|
108
105
|
aria-hidden="true"
|
|
109
106
|
>
|
|
110
107
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
111
|
-
<path
|
|
112
|
-
d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
|
|
113
|
-
/>
|
|
108
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
114
109
|
</svg>
|
|
115
110
|
</button>
|
|
116
111
|
<button
|
|
@@ -155,14 +150,9 @@ onBeforeUnmount(() => {
|
|
|
155
150
|
>
|
|
156
151
|
<header>
|
|
157
152
|
<span>{{ title || meta.title || meta.language }}</span
|
|
158
|
-
><button type="button" autofocus @click="closeFullscreen">
|
|
159
|
-
关闭(Esc)
|
|
160
|
-
</button>
|
|
153
|
+
><button type="button" autofocus @click="closeFullscreen">关闭(Esc)</button>
|
|
161
154
|
</header>
|
|
162
|
-
<div
|
|
163
|
-
class="tn-code-block"
|
|
164
|
-
:class="{ 'has-line-numbers': meta.lineNumbers }"
|
|
165
|
-
>
|
|
155
|
+
<div class="tn-code-block" :class="{ 'has-line-numbers': meta.lineNumbers }">
|
|
166
156
|
<div v-if="html" class="tn-code-block__content" v-html="html" />
|
|
167
157
|
<pre v-else><code>{{ code }}</code></pre>
|
|
168
158
|
</div>
|
|
@@ -1,48 +1,47 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { nextTick, onMounted, ref, watch } from
|
|
3
|
-
import CodeBlock from
|
|
4
|
-
import { parseCodeMeta } from
|
|
2
|
+
import { nextTick, onMounted, ref, watch } from 'vue'
|
|
3
|
+
import CodeBlock from '../CodeBlock/CodeBlock.vue'
|
|
4
|
+
import { parseCodeMeta } from '../../code/highlight'
|
|
5
5
|
|
|
6
6
|
export interface CodeGroupItem {
|
|
7
|
-
code?: string
|
|
8
|
-
info?: string
|
|
9
|
-
highlightedHtml?: string
|
|
10
|
-
key?: string
|
|
7
|
+
code?: string
|
|
8
|
+
info?: string
|
|
9
|
+
highlightedHtml?: string
|
|
10
|
+
key?: string
|
|
11
11
|
}
|
|
12
|
-
const props = defineProps<{ items: CodeGroupItem[] }>()
|
|
13
|
-
const root = ref<HTMLElement>()
|
|
14
|
-
const active = ref(0)
|
|
12
|
+
const props = defineProps<{ items: CodeGroupItem[] }>()
|
|
13
|
+
const root = ref<HTMLElement>()
|
|
14
|
+
const active = ref(0)
|
|
15
15
|
function syncPanels(): void {
|
|
16
16
|
const panels =
|
|
17
17
|
root.value?.querySelectorAll<HTMLElement>(
|
|
18
|
-
|
|
19
|
-
) ?? []
|
|
18
|
+
':scope > .tn-code-group__panels > .tn-code-group__panel'
|
|
19
|
+
) ?? []
|
|
20
20
|
panels.forEach((panel, index) => {
|
|
21
|
-
panel.hidden = index !== active.value
|
|
22
|
-
})
|
|
21
|
+
panel.hidden = index !== active.value
|
|
22
|
+
})
|
|
23
23
|
}
|
|
24
|
-
watch(active, syncPanels, { flush:
|
|
24
|
+
watch(active, syncPanels, { flush: 'post' })
|
|
25
25
|
watch(
|
|
26
26
|
() => props.items.length,
|
|
27
27
|
(length) => {
|
|
28
|
-
active.value = Math.max(0, Math.min(active.value, length - 1))
|
|
29
|
-
nextTick(syncPanels)
|
|
30
|
-
}
|
|
31
|
-
)
|
|
32
|
-
onMounted(syncPanels)
|
|
28
|
+
active.value = Math.max(0, Math.min(active.value, length - 1))
|
|
29
|
+
nextTick(syncPanels)
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
onMounted(syncPanels)
|
|
33
33
|
function navigate(event: KeyboardEvent, index: number): void {
|
|
34
|
-
let next = index
|
|
35
|
-
if (event.key ===
|
|
36
|
-
else if (event.key ===
|
|
37
|
-
|
|
38
|
-
else if (event.key ===
|
|
39
|
-
else
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
?.
|
|
45
|
-
[next]?.focus();
|
|
34
|
+
let next = index
|
|
35
|
+
if (event.key === 'ArrowRight') next = (index + 1) % props.items.length
|
|
36
|
+
else if (event.key === 'ArrowLeft') next = (index - 1 + props.items.length) % props.items.length
|
|
37
|
+
else if (event.key === 'Home') next = 0
|
|
38
|
+
else if (event.key === 'End') next = props.items.length - 1
|
|
39
|
+
else return
|
|
40
|
+
event.preventDefault()
|
|
41
|
+
active.value = next
|
|
42
|
+
;(event.currentTarget as HTMLElement).parentElement
|
|
43
|
+
?.querySelectorAll<HTMLButtonElement>('button')
|
|
44
|
+
[next]?.focus()
|
|
46
45
|
}
|
|
47
46
|
</script>
|
|
48
47
|
|
|
@@ -67,9 +66,7 @@ function navigate(event: KeyboardEvent, index: number): void {
|
|
|
67
66
|
@keydown="navigate($event, index)"
|
|
68
67
|
>
|
|
69
68
|
{{
|
|
70
|
-
parseCodeMeta(item.info).title ||
|
|
71
|
-
parseCodeMeta(item.info).language ||
|
|
72
|
-
`代码 ${index + 1}`
|
|
69
|
+
parseCodeMeta(item.info).title || parseCodeMeta(item.info).language || `代码 ${index + 1}`
|
|
73
70
|
}}
|
|
74
71
|
</button>
|
|
75
72
|
</div>
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
computed,
|
|
4
|
-
nextTick,
|
|
5
|
-
onBeforeUnmount,
|
|
6
|
-
onMounted,
|
|
7
|
-
ref,
|
|
8
|
-
useSlots,
|
|
9
|
-
watch
|
|
10
|
-
} from 'vue'
|
|
2
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, useSlots, watch } from 'vue'
|
|
11
3
|
|
|
12
4
|
const props = withDefaults(
|
|
13
5
|
defineProps<{
|
|
@@ -141,11 +133,7 @@ watch(
|
|
|
141
133
|
<!-- tn-preview-ignore: opt out of Layout ImagePreview so only the in-component modal opens -->
|
|
142
134
|
<div class="tn-footprints tn-preview-ignore">
|
|
143
135
|
<div class="tn-footprints__text">
|
|
144
|
-
<div
|
|
145
|
-
ref="textEl"
|
|
146
|
-
class="tn-footprints__text-body"
|
|
147
|
-
:class="{ 'is-collapsed': isCollapsed }"
|
|
148
|
-
>
|
|
136
|
+
<div ref="textEl" class="tn-footprints__text-body" :class="{ 'is-collapsed': isCollapsed }">
|
|
149
137
|
<slot name="text-area">
|
|
150
138
|
<p v-for="(line, i) in paragraphs" :key="i">{{ line }}</p>
|
|
151
139
|
</slot>
|
|
@@ -11,9 +11,7 @@ export interface FootprintsPayload {
|
|
|
11
11
|
export function parseFootprintsDatetime(meta: string): number[] {
|
|
12
12
|
const m = meta
|
|
13
13
|
.trim()
|
|
14
|
-
.match(
|
|
15
|
-
/^(\d{4})-(\d{1,2})-(\d{1,2})(?:\s+(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?$/
|
|
16
|
-
)
|
|
14
|
+
.match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:\s+(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?$/)
|
|
17
15
|
if (!m) return []
|
|
18
16
|
const parts = [Number(m[1]), Number(m[2]), Number(m[3])]
|
|
19
17
|
if (m[4] !== undefined) {
|