@tnotesjs/ui 0.1.5 → 0.2.1
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 +26 -9
- package/src/browser/clipboard.ts +16 -19
- package/src/browser/theme.ts +10 -15
- 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.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/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.ts +224 -0
- package/src/excalidraw/exporter.ts +50 -0
- package/src/excalidraw/focus.ts +33 -0
- package/src/excalidraw/fonts.ts +92 -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.ts +4 -13
- package/src/styles/code.css +5 -5
- package/src/styles/prose.css +1 -1
- package/src/styles/swiper.css +1 -1
- package/src/styles/tokens.css +5 -6
- package/src/swiper/hydrate.ts +91 -102
- package/src/code/highlight.test.ts +0 -55
- package/src/components/Mermaid/Mermaid.test.ts +0 -54
- package/src/components/Mermaid/adoptLiveSvg.test.ts +0 -16
- package/src/components/Mindmap/FocusBreadcrumbs.test.ts +0 -265
- package/src/components/sharedMedia.test.ts +0 -139
- package/src/markdown/image.test.ts +0 -88
- package/src/swiper/hydrate.test.ts +0 -65
package/src/markdown/image.ts
CHANGED
|
@@ -76,13 +76,9 @@ export function normalizeImageWidth(raw: string): string {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/** Logical CSS px for a pasted bitmap, folding device pixel ratio (4K/retina). */
|
|
79
|
-
export function pasteDisplayWidthPx(
|
|
80
|
-
naturalWidth: number,
|
|
81
|
-
devicePixelRatio = 1
|
|
82
|
-
): string {
|
|
79
|
+
export function pasteDisplayWidthPx(naturalWidth: number, devicePixelRatio = 1): string {
|
|
83
80
|
if (!Number.isFinite(naturalWidth) || naturalWidth <= 0) return ''
|
|
84
|
-
const dpr =
|
|
85
|
-
Number.isFinite(devicePixelRatio) && devicePixelRatio > 0 ? devicePixelRatio : 1
|
|
81
|
+
const dpr = Number.isFinite(devicePixelRatio) && devicePixelRatio > 0 ? devicePixelRatio : 1
|
|
86
82
|
return normalizeImageWidth(`${Math.round(naturalWidth / dpr)}px`)
|
|
87
83
|
}
|
|
88
84
|
|
|
@@ -90,10 +86,7 @@ export function formatImageWidthAttr(width: string): string {
|
|
|
90
86
|
return formatImageAttrs({ width, align: 'left' })
|
|
91
87
|
}
|
|
92
88
|
|
|
93
|
-
export function formatImageAttrs(attrs: {
|
|
94
|
-
width?: string
|
|
95
|
-
align?: ImageAlign | string
|
|
96
|
-
}): string {
|
|
89
|
+
export function formatImageAttrs(attrs: { width?: string; align?: ImageAlign | string }): string {
|
|
97
90
|
const width = normalizeImageWidth(attrs.width ?? '')
|
|
98
91
|
const align = normalizeImageAlign(attrs.align ?? '')
|
|
99
92
|
const parts: string[] = []
|
|
@@ -104,9 +97,7 @@ export function formatImageAttrs(attrs: {
|
|
|
104
97
|
|
|
105
98
|
export function serializeImageMarkdown(image: ImageMarkdown): string {
|
|
106
99
|
const alt = image.alt.replaceAll('[', '').replaceAll(']', '')
|
|
107
|
-
const title = image.title?.trim()
|
|
108
|
-
? ` "${image.title.replaceAll('"', '')}"`
|
|
109
|
-
: ''
|
|
100
|
+
const title = image.title?.trim() ? ` "${image.title.replaceAll('"', '')}"` : ''
|
|
110
101
|
const suffix = formatImageAttrs({
|
|
111
102
|
width: image.width ?? '',
|
|
112
103
|
align: image.align ?? 'left'
|
package/src/styles/code.css
CHANGED
|
@@ -105,13 +105,13 @@
|
|
|
105
105
|
color: var(--shiki-light);
|
|
106
106
|
background-color: transparent;
|
|
107
107
|
}
|
|
108
|
-
html.dark:not([data-theme=
|
|
109
|
-
html[data-theme=
|
|
108
|
+
html.dark:not([data-theme='light']) .tn-code-highlight,
|
|
109
|
+
html[data-theme='dark'] .tn-code-highlight {
|
|
110
110
|
color: var(--shiki-dark);
|
|
111
111
|
background-color: var(--tn-c-bg-soft);
|
|
112
112
|
}
|
|
113
|
-
html.dark:not([data-theme=
|
|
114
|
-
html[data-theme=
|
|
113
|
+
html.dark:not([data-theme='light']) .tn-code-highlight span:not(.line),
|
|
114
|
+
html[data-theme='dark'] .tn-code-highlight span:not(.line) {
|
|
115
115
|
color: var(--shiki-dark);
|
|
116
116
|
background-color: transparent;
|
|
117
117
|
}
|
|
@@ -151,7 +151,7 @@ html[data-theme="dark"] .tn-code-highlight span:not(.line) {
|
|
|
151
151
|
border-bottom: 2px solid transparent;
|
|
152
152
|
padding: 4px 10px;
|
|
153
153
|
}
|
|
154
|
-
.tn-code-group__tabs button[aria-selected=
|
|
154
|
+
.tn-code-group__tabs button[aria-selected='true'] {
|
|
155
155
|
color: var(--tn-c-brand);
|
|
156
156
|
border-bottom-color: var(--tn-c-brand);
|
|
157
157
|
}
|
package/src/styles/prose.css
CHANGED
package/src/styles/swiper.css
CHANGED
package/src/styles/tokens.css
CHANGED
|
@@ -24,10 +24,9 @@
|
|
|
24
24
|
--tn-c-default-soft: #e8e8e9;
|
|
25
25
|
--tn-shadow-2: 0 4px 12px rgba(0, 0, 0, 0.12);
|
|
26
26
|
--tn-font-sans:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
--tn-font-mono:
|
|
30
|
-
"SFMono-Regular", "Cascadia Code", "Roboto Mono", Consolas, monospace;
|
|
27
|
+
'Punctuation SC', Inter, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
|
|
28
|
+
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
|
29
|
+
--tn-font-mono: 'SFMono-Regular', 'Cascadia Code', 'Roboto Mono', Consolas, monospace;
|
|
31
30
|
--tn-prose-size: 16px;
|
|
32
31
|
--tn-prose-line-height: 28px;
|
|
33
32
|
--tn-heading-weight: 600;
|
|
@@ -46,8 +45,8 @@
|
|
|
46
45
|
color-scheme: light;
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
:root.dark:not([data-theme=
|
|
50
|
-
:root[data-theme=
|
|
48
|
+
:root.dark:not([data-theme='light']),
|
|
49
|
+
:root[data-theme='dark'] {
|
|
51
50
|
--tn-c-brand-strong: #a8b1ff;
|
|
52
51
|
--tn-c-text: #dfdfd6;
|
|
53
52
|
--tn-c-text-2: #98989f;
|
package/src/swiper/hydrate.ts
CHANGED
|
@@ -5,56 +5,49 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export function applySwiperTabsPadding(tabs: HTMLElement, hasNav: boolean): void {
|
|
8
|
-
tabs.style.padding = hasNav ?
|
|
8
|
+
tabs.style.padding = hasNav ? '0 0.8rem 0 3rem' : '0 0.8rem'
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export function createSwiperTabNav(handlers: {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
prev: HTMLButtonElement;
|
|
16
|
-
line: HTMLSpanElement;
|
|
17
|
-
next: HTMLButtonElement;
|
|
11
|
+
export function createSwiperTabNav(handlers: { onPrev: () => void; onNext: () => void }): {
|
|
12
|
+
prev: HTMLButtonElement
|
|
13
|
+
line: HTMLSpanElement
|
|
14
|
+
next: HTMLButtonElement
|
|
18
15
|
} {
|
|
19
|
-
const prev = document.createElement(
|
|
20
|
-
prev.type =
|
|
21
|
-
prev.className =
|
|
22
|
-
prev.textContent =
|
|
23
|
-
prev.title =
|
|
24
|
-
prev.addEventListener(
|
|
25
|
-
event.preventDefault()
|
|
26
|
-
event.stopPropagation()
|
|
27
|
-
handlers.onPrev()
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
const line = document.createElement(
|
|
31
|
-
line.className =
|
|
32
|
-
line.textContent =
|
|
33
|
-
|
|
34
|
-
const next = document.createElement(
|
|
35
|
-
next.type =
|
|
36
|
-
next.className =
|
|
37
|
-
next.textContent =
|
|
38
|
-
next.title =
|
|
39
|
-
next.addEventListener(
|
|
40
|
-
event.preventDefault()
|
|
41
|
-
event.stopPropagation()
|
|
42
|
-
handlers.onNext()
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
return { prev, line, next }
|
|
16
|
+
const prev = document.createElement('button')
|
|
17
|
+
prev.type = 'button'
|
|
18
|
+
prev.className = 'tn-tab-nav tn-tab-prev'
|
|
19
|
+
prev.textContent = '<'
|
|
20
|
+
prev.title = '上一页'
|
|
21
|
+
prev.addEventListener('click', (event) => {
|
|
22
|
+
event.preventDefault()
|
|
23
|
+
event.stopPropagation()
|
|
24
|
+
handlers.onPrev()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const line = document.createElement('span')
|
|
28
|
+
line.className = 'tn-tab-nav tab-tab-line'
|
|
29
|
+
line.textContent = '/'
|
|
30
|
+
|
|
31
|
+
const next = document.createElement('button')
|
|
32
|
+
next.type = 'button'
|
|
33
|
+
next.className = 'tn-tab-nav tn-tab-next'
|
|
34
|
+
next.textContent = '>'
|
|
35
|
+
next.title = '下一页'
|
|
36
|
+
next.addEventListener('click', (event) => {
|
|
37
|
+
event.preventDefault()
|
|
38
|
+
event.stopPropagation()
|
|
39
|
+
handlers.onNext()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return { prev, line, next }
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
export function wrapSlideIndex(
|
|
49
|
-
|
|
50
|
-
length
|
|
51
|
-
delta: -1 | 1,
|
|
52
|
-
): number {
|
|
53
|
-
if (length <= 0) return 0;
|
|
54
|
-
return (index + delta + length) % length;
|
|
45
|
+
export function wrapSlideIndex(index: number, length: number, delta: -1 | 1): number {
|
|
46
|
+
if (length <= 0) return 0
|
|
47
|
+
return (index + delta + length) % length
|
|
55
48
|
}
|
|
56
49
|
|
|
57
|
-
const READY =
|
|
50
|
+
const READY = 'tnSwiperReady'
|
|
58
51
|
|
|
59
52
|
/**
|
|
60
53
|
* Hydrate every `.tn-swiper` under `root` (or `document`). Idempotent:
|
|
@@ -62,103 +55,99 @@ const READY = "tnSwiperReady";
|
|
|
62
55
|
* When `root` itself is a `.tn-swiper`, it is included.
|
|
63
56
|
*/
|
|
64
57
|
export function hydrateTnSwipers(root: ParentNode = document): number {
|
|
65
|
-
const hosts: HTMLElement[] = []
|
|
66
|
-
if (root instanceof HTMLElement && root.classList.contains(
|
|
67
|
-
hosts.push(root)
|
|
58
|
+
const hosts: HTMLElement[] = []
|
|
59
|
+
if (root instanceof HTMLElement && root.classList.contains('tn-swiper')) {
|
|
60
|
+
hosts.push(root)
|
|
68
61
|
}
|
|
69
|
-
hosts.push(...root.querySelectorAll<HTMLElement>(
|
|
62
|
+
hosts.push(...root.querySelectorAll<HTMLElement>('.tn-swiper'))
|
|
70
63
|
|
|
71
|
-
let count = 0
|
|
72
|
-
const seen = new Set<HTMLElement>()
|
|
64
|
+
let count = 0
|
|
65
|
+
const seen = new Set<HTMLElement>()
|
|
73
66
|
for (const host of hosts) {
|
|
74
|
-
if (seen.has(host)) continue
|
|
75
|
-
seen.add(host)
|
|
76
|
-
if (host.dataset[READY] ===
|
|
67
|
+
if (seen.has(host)) continue
|
|
68
|
+
seen.add(host)
|
|
69
|
+
if (host.dataset[READY] === 'true') continue
|
|
77
70
|
if (hydrateOne(host)) {
|
|
78
|
-
host.dataset[READY] =
|
|
79
|
-
count += 1
|
|
71
|
+
host.dataset[READY] = 'true'
|
|
72
|
+
count += 1
|
|
80
73
|
}
|
|
81
74
|
}
|
|
82
|
-
return count
|
|
75
|
+
return count
|
|
83
76
|
}
|
|
84
77
|
|
|
85
78
|
function hydrateOne(host: HTMLElement): boolean {
|
|
86
|
-
const tabs = host.querySelector<HTMLElement>(
|
|
87
|
-
const wrapper = host.querySelector<HTMLElement>(
|
|
88
|
-
if (!wrapper) return false
|
|
79
|
+
const tabs = host.querySelector<HTMLElement>('.tn-swiper-tabs')
|
|
80
|
+
const wrapper = host.querySelector<HTMLElement>('.swiper-wrapper')
|
|
81
|
+
if (!wrapper) return false
|
|
89
82
|
|
|
90
|
-
const slides = [
|
|
91
|
-
...wrapper.querySelectorAll<HTMLElement>(":scope > .swiper-slide"),
|
|
92
|
-
];
|
|
83
|
+
const slides = [...wrapper.querySelectorAll<HTMLElement>(':scope > .swiper-slide')]
|
|
93
84
|
if (slides.length === 0) {
|
|
94
85
|
// Empty shell — still mark ready so we don't loop.
|
|
95
|
-
return true
|
|
86
|
+
return true
|
|
96
87
|
}
|
|
97
88
|
|
|
98
89
|
// Ensure tabs host exists (site always emits it; Desk may omit for 1 slide).
|
|
99
|
-
let tabsEl = tabs
|
|
90
|
+
let tabsEl = tabs
|
|
100
91
|
if (!tabsEl) {
|
|
101
|
-
tabsEl = document.createElement(
|
|
102
|
-
tabsEl.className =
|
|
103
|
-
host.prepend(tabsEl)
|
|
92
|
+
tabsEl = document.createElement('div')
|
|
93
|
+
tabsEl.className = 'tn-swiper-tabs'
|
|
94
|
+
host.prepend(tabsEl)
|
|
104
95
|
}
|
|
105
96
|
|
|
106
|
-
const useTabs = slides.length > 1
|
|
107
|
-
tabsEl.replaceChildren()
|
|
108
|
-
applySwiperTabsPadding(tabsEl, useTabs)
|
|
97
|
+
const useTabs = slides.length > 1
|
|
98
|
+
tabsEl.replaceChildren()
|
|
99
|
+
applySwiperTabsPadding(tabsEl, useTabs)
|
|
109
100
|
|
|
110
|
-
const tabButtons: HTMLButtonElement[] = []
|
|
101
|
+
const tabButtons: HTMLButtonElement[] = []
|
|
111
102
|
|
|
112
103
|
const activate = (index: number): void => {
|
|
113
104
|
tabButtons.forEach((button, buttonIndex) =>
|
|
114
|
-
button.classList.toggle(
|
|
115
|
-
)
|
|
105
|
+
button.classList.toggle('active', buttonIndex === index)
|
|
106
|
+
)
|
|
116
107
|
slides.forEach((slide, slideIndex) => {
|
|
117
|
-
const on = slideIndex === index
|
|
118
|
-
slide.classList.toggle(
|
|
119
|
-
slide.hidden = !on
|
|
120
|
-
})
|
|
121
|
-
}
|
|
108
|
+
const on = slideIndex === index
|
|
109
|
+
slide.classList.toggle('is-active', on)
|
|
110
|
+
slide.hidden = !on
|
|
111
|
+
})
|
|
112
|
+
}
|
|
122
113
|
|
|
123
114
|
const activeIndex = (): number => {
|
|
124
|
-
const found = tabButtons.findIndex((button) =>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return found >= 0 ? found : 0;
|
|
128
|
-
};
|
|
115
|
+
const found = tabButtons.findIndex((button) => button.classList.contains('active'))
|
|
116
|
+
return found >= 0 ? found : 0
|
|
117
|
+
}
|
|
129
118
|
|
|
130
119
|
if (useTabs) {
|
|
131
120
|
const nav = createSwiperTabNav({
|
|
132
121
|
onPrev: () => activate(wrapSlideIndex(activeIndex(), slides.length, -1)),
|
|
133
|
-
onNext: () => activate(wrapSlideIndex(activeIndex(), slides.length, 1))
|
|
134
|
-
})
|
|
135
|
-
tabsEl.append(nav.prev, nav.line)
|
|
122
|
+
onNext: () => activate(wrapSlideIndex(activeIndex(), slides.length, 1))
|
|
123
|
+
})
|
|
124
|
+
tabsEl.append(nav.prev, nav.line)
|
|
136
125
|
|
|
137
126
|
slides.forEach((slide, index) => {
|
|
138
127
|
const title =
|
|
139
128
|
slide.dataset.title?.trim() ||
|
|
140
|
-
slide.querySelector(
|
|
141
|
-
|
|
142
|
-
slide.dataset.title = title
|
|
143
|
-
const tab = document.createElement(
|
|
144
|
-
tab.type =
|
|
145
|
-
tab.className =
|
|
146
|
-
tab.textContent = title
|
|
147
|
-
tab.addEventListener(
|
|
148
|
-
tabButtons.push(tab)
|
|
149
|
-
tabsEl!.append(tab)
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
tabsEl.append(nav.next)
|
|
129
|
+
slide.querySelector('img')?.getAttribute('alt')?.trim() ||
|
|
130
|
+
'img'
|
|
131
|
+
slide.dataset.title = title
|
|
132
|
+
const tab = document.createElement('button')
|
|
133
|
+
tab.type = 'button'
|
|
134
|
+
tab.className = 'tn-tab'
|
|
135
|
+
tab.textContent = title
|
|
136
|
+
tab.addEventListener('click', () => activate(index))
|
|
137
|
+
tabButtons.push(tab)
|
|
138
|
+
tabsEl!.append(tab)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
tabsEl.append(nav.next)
|
|
153
142
|
// Multi-slide: tabs stay above the container.
|
|
154
143
|
if (tabsEl.parentElement !== host || host.firstElementChild !== tabsEl) {
|
|
155
|
-
host.prepend(tabsEl)
|
|
144
|
+
host.prepend(tabsEl)
|
|
156
145
|
}
|
|
157
146
|
} else {
|
|
158
147
|
// Single slide: drop empty tab bar (Desk parity).
|
|
159
|
-
tabsEl.remove()
|
|
148
|
+
tabsEl.remove()
|
|
160
149
|
}
|
|
161
150
|
|
|
162
|
-
activate(0)
|
|
163
|
-
return true
|
|
151
|
+
activate(0)
|
|
152
|
+
return true
|
|
164
153
|
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { highlightCode, highlightCodeSync, parseCodeMeta, prepareCodeHighlighter } from './highlight'
|
|
3
|
-
|
|
4
|
-
describe('shared code rendering', () => {
|
|
5
|
-
it('parses titles, line offsets, highlight ranges and per-block overrides', () => {
|
|
6
|
-
expect(parseCodeMeta('ts:line-numbers=12 {1,3-5} [demo.ts]')).toEqual({
|
|
7
|
-
language: 'ts', title: 'demo.ts', startLine: 12, lineNumbers: true,
|
|
8
|
-
highlightedLines: [1, 3, 4, 5]
|
|
9
|
-
})
|
|
10
|
-
expect(parseCodeMeta('js {1-2} [setup.js]')).toEqual({
|
|
11
|
-
language: 'js', title: 'setup.js', startLine: 1, lineNumbers: true,
|
|
12
|
-
highlightedLines: [1, 2]
|
|
13
|
-
})
|
|
14
|
-
expect(parseCodeMeta('txt {3}')).toEqual({
|
|
15
|
-
language: 'txt', title: '', startLine: 1, lineNumbers: true,
|
|
16
|
-
highlightedLines: [3]
|
|
17
|
-
})
|
|
18
|
-
expect(parseCodeMeta('text:no-line-numbers').lineNumbers).toBe(false)
|
|
19
|
-
expect(parseCodeMeta('text {0,-1,8-3,1-9999999}').highlightedLines).toEqual([])
|
|
20
|
-
})
|
|
21
|
-
it('shares one lazy engine and matches build-time/browser output byte for byte', async () => {
|
|
22
|
-
const [a, b] = await Promise.all([prepareCodeHighlighter(['js']), prepareCodeHighlighter(['typescript'])])
|
|
23
|
-
expect(a).toBe(b)
|
|
24
|
-
const source = 'const answer = 42\nconsole.log(answer)\n'
|
|
25
|
-
const runtime = await highlightCode(source, 'js {2}')
|
|
26
|
-
expect(runtime).toBe(highlightCodeSync(source, 'js {2}'))
|
|
27
|
-
expect(runtime).toContain('--shiki-light:')
|
|
28
|
-
expect(runtime).toContain('--shiki-dark:')
|
|
29
|
-
expect(runtime).toContain('class="line highlighted"')
|
|
30
|
-
expect(runtime).toContain('data-line="2"')
|
|
31
|
-
expect(a.getLoadedLanguages()).not.toContain('cobol')
|
|
32
|
-
})
|
|
33
|
-
it('escapes unknown-language code instead of executing HTML', async () => {
|
|
34
|
-
const html = await highlightCode('<script>alert(1)</script> {{ injected }}', 'unknown-label')
|
|
35
|
-
expect(html).not.toContain('<script>')
|
|
36
|
-
const host = document.createElement('div')
|
|
37
|
-
host.innerHTML = html
|
|
38
|
-
expect(host.querySelector('script')).toBeNull()
|
|
39
|
-
expect(host.textContent).toBe('<script>alert(1)</script> {{ injected }}')
|
|
40
|
-
expect(html).toContain('{{ injected }}')
|
|
41
|
-
})
|
|
42
|
-
it('applies fence line highlights and offsets without Shiki notation', async () => {
|
|
43
|
-
const withNotation = await highlightCode(
|
|
44
|
-
'const x = 1 // [!code ++]\nconst y = 2 // [!code focus]',
|
|
45
|
-
'js:line-numbers=5 {2}',
|
|
46
|
-
)
|
|
47
|
-
// Magic comments stay as source text; Desk has no annotation write path.
|
|
48
|
-
expect(withNotation).toContain('[!code ++]')
|
|
49
|
-
expect(withNotation).not.toContain('diff add')
|
|
50
|
-
expect(withNotation).not.toContain('focused')
|
|
51
|
-
expect(withNotation).toContain('data-line="5"')
|
|
52
|
-
expect(withNotation).toContain('data-line="6"')
|
|
53
|
-
expect(withNotation).toContain('class="line highlighted"')
|
|
54
|
-
})
|
|
55
|
-
})
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
// @vitest-environment happy-dom
|
|
2
|
-
|
|
3
|
-
import { createApp, defineComponent, h, nextTick, type Component } from 'vue'
|
|
4
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
-
|
|
6
|
-
import Mermaid from './Mermaid.vue'
|
|
7
|
-
|
|
8
|
-
const cleanups: Array<() => void> = []
|
|
9
|
-
|
|
10
|
-
function mount(component: Component, props: Record<string, unknown> = {}): HTMLElement {
|
|
11
|
-
const host = document.createElement('div')
|
|
12
|
-
document.body.append(host)
|
|
13
|
-
const app = createApp(defineComponent({ render: () => h(component, props) }))
|
|
14
|
-
app.mount(host)
|
|
15
|
-
cleanups.push(() => {
|
|
16
|
-
app.unmount()
|
|
17
|
-
host.remove()
|
|
18
|
-
})
|
|
19
|
-
return host
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
cleanups
|
|
24
|
-
.splice(0)
|
|
25
|
-
.reverse()
|
|
26
|
-
.forEach((cleanup) => cleanup())
|
|
27
|
-
document.body.innerHTML = ''
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
describe('Mermaid preview host', () => {
|
|
31
|
-
it('keeps the diagram host laid out while loading (note 0020 diagrams)', async () => {
|
|
32
|
-
const sources = [
|
|
33
|
-
`flowchart LR\n Desk --> UI`,
|
|
34
|
-
`sequenceDiagram\n participant Desk\n participant Site\n Desk->>Site: hi`,
|
|
35
|
-
`graph TD\n A[Start] --> B{ok}`
|
|
36
|
-
]
|
|
37
|
-
for (const source of sources) {
|
|
38
|
-
const host = mount(Mermaid, { source })
|
|
39
|
-
await nextTick()
|
|
40
|
-
const diagram = host.querySelector('.tn-mermaid__diagram')
|
|
41
|
-
expect(diagram).not.toBeNull()
|
|
42
|
-
// Mermaid 11 needs layout during mermaid.render(); do not visibility:hidden the host.
|
|
43
|
-
expect(diagram!.classList.contains('is-obscured')).toBe(false)
|
|
44
|
-
}
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('only obscures the host for empty or error states', async () => {
|
|
48
|
-
const emptyHost = mount(Mermaid, { source: '' })
|
|
49
|
-
await nextTick()
|
|
50
|
-
expect(emptyHost.querySelector('.tn-mermaid__diagram')!.classList.contains('is-obscured')).toBe(
|
|
51
|
-
true
|
|
52
|
-
)
|
|
53
|
-
})
|
|
54
|
-
})
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { adoptLiveSvg } from './adoptLiveSvg'
|
|
4
|
-
|
|
5
|
-
describe('adoptLiveSvg', () => {
|
|
6
|
-
it('renames the root svg id and scoped theme selectors', () => {
|
|
7
|
-
const svg = `<svg id="tn-mermaid-1" xmlns="http://www.w3.org/2000/svg"><style>#tn-mermaid-1{fill:#ccc;}#tn-mermaid-1 .node{stroke:#fff;}#tn-mermaid-1_flowchart-v2-pointEnd{fill:red;}</style><path id="tn-mermaid-1_flowchart-v2-pointEnd"/></svg>`
|
|
8
|
-
const out = adoptLiveSvg(svg, 'tn-mermaid-1')
|
|
9
|
-
expect(out).toContain('id="tn-mermaid-1-live"')
|
|
10
|
-
expect(out).toContain('#tn-mermaid-1-live{fill:#ccc;}')
|
|
11
|
-
expect(out).toContain('#tn-mermaid-1-live .node{stroke:#fff;}')
|
|
12
|
-
expect(out).toContain('id="tn-mermaid-1_flowchart-v2-pointEnd"')
|
|
13
|
-
expect(out).toContain('#tn-mermaid-1_flowchart-v2-pointEnd{fill:red;}')
|
|
14
|
-
expect(out).not.toMatch(/id="tn-mermaid-1"/)
|
|
15
|
-
})
|
|
16
|
-
})
|