@tnotesjs/ui 0.2.0 → 0.3.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 +81 -0
- package/dist/code/highlight.d.ts +39 -1
- package/dist/code/highlight.js +54 -1
- package/package.json +21 -7
- package/src/code/collapse.ts +79 -0
- package/src/code/highlight.ts +12 -0
- package/src/components/CodeBlock/CodeBlock.vue +39 -5
- package/src/components/CodeGroup/CodeGroup.vue +70 -7
- package/src/components/Mindmap/Mindmap.vue +7 -6
- package/src/components/Mindmap/editor/CanvasContextMenu.vue +77 -68
- package/src/components/Mindmap/editor/CollapseMenu.vue +113 -0
- package/src/components/Mindmap/editor/IconButton.vue +63 -0
- package/src/components/Mindmap/editor/LinkPopover.vue +50 -46
- package/src/components/Mindmap/editor/MarkdownView.vue +5 -3
- package/src/components/Mindmap/editor/MindmapView.vue +11 -1
- package/src/components/Mindmap/editor/SearchBar.vue +672 -0
- package/src/components/Mindmap/editor/SelectionToolbar.vue +1 -1
- package/src/entries/excalidraw-view.ts +5 -0
- package/src/entries/mindmap-editor.ts +37 -0
- package/src/excalidraw/exporter.ts +1 -0
- package/src/excalidraw/fonts.ts +21 -1
- package/src/styles/code.css +74 -2
- package/src/styles/prose.css +45 -0
- package/src/styles/tokens.css +6 -0
- package/src/code/highlight.test.ts +0 -72
- 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 -275
- package/src/components/Mindmap/editor/canvasClipboard.ts +0 -2
- package/src/components/sharedMedia.test.ts +0 -121
- package/src/excalidraw/editorSession.test.ts +0 -205
- package/src/excalidraw/focus.test.ts +0 -60
- package/src/excalidraw/fonts.test.ts +0 -66
- package/src/excalidraw/renderer.test.ts +0 -148
- package/src/markdown/image.test.ts +0 -86
- package/src/swiper/hydrate.test.ts +0 -61
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 思维导图编辑器外壳入口:Web 与 VS Code 扩展共用同一套三视图 UI。
|
|
3
|
+
*
|
|
4
|
+
* 与 `./mindmap` 的分工:`./mindmap` 是 SSG / Desk 笔记里内嵌的「预览 + 可选编辑」块
|
|
5
|
+
* (`components/Mindmap/Mindmap.vue`,自带 `--mm-*` 主题变量与只读渲染);本入口导出它
|
|
6
|
+
* 内部复用的编辑器组件,供自带页面外壳的宿主(Web 整页、VS Code webview)直接组装。
|
|
7
|
+
*
|
|
8
|
+
* 宿主职责:`MindmapSession` 归属、`--mm-*` 变量定义、落盘 / 剪贴板 / 图片写入
|
|
9
|
+
* (`apps/mindmap-web`、`apps/mindmap-vscode`)。浮层默认不 teleport,嵌入笔记的宿主
|
|
10
|
+
* 保持默认即可;整页宿主传 `teleport-to="body"`。
|
|
11
|
+
*/
|
|
12
|
+
export { default as AppIcon } from '../components/Mindmap/editor/AppIcon.vue'
|
|
13
|
+
export { default as CanvasContextMenu } from '../components/Mindmap/editor/CanvasContextMenu.vue'
|
|
14
|
+
export { default as CollapseMenu } from '../components/Mindmap/editor/CollapseMenu.vue'
|
|
15
|
+
export { default as IconButton } from '../components/Mindmap/editor/IconButton.vue'
|
|
16
|
+
export { default as LinkPopover } from '../components/Mindmap/editor/LinkPopover.vue'
|
|
17
|
+
export { default as MarkdownView } from '../components/Mindmap/editor/MarkdownView.vue'
|
|
18
|
+
export { default as MindmapView } from '../components/Mindmap/editor/MindmapView.vue'
|
|
19
|
+
export { default as OutlineView } from '../components/Mindmap/editor/OutlineView.vue'
|
|
20
|
+
export { default as RichInlineEditor } from '../components/Mindmap/editor/RichInlineEditor.vue'
|
|
21
|
+
export { default as SearchBar } from '../components/Mindmap/editor/SearchBar.vue'
|
|
22
|
+
export { default as SelectionToolbar } from '../components/Mindmap/editor/SelectionToolbar.vue'
|
|
23
|
+
export { default as FocusBreadcrumbs } from '../components/Mindmap/FocusBreadcrumbs.vue'
|
|
24
|
+
|
|
25
|
+
export { insertImageIntoSource } from '../components/Mindmap/editor/imagePaste'
|
|
26
|
+
export {
|
|
27
|
+
pasteCanvasOutline,
|
|
28
|
+
readMindmapClipboard,
|
|
29
|
+
writeMindmapClipboard
|
|
30
|
+
} from '../components/Mindmap/editor/mindmapClipboard'
|
|
31
|
+
export { resolveAfterDropLevel } from '../components/Mindmap/editor/outlineDrag'
|
|
32
|
+
export type { DropLevel } from '../components/Mindmap/editor/outlineDrag'
|
|
33
|
+
export {
|
|
34
|
+
altShortcut,
|
|
35
|
+
isApplePlatform,
|
|
36
|
+
primaryShortcut
|
|
37
|
+
} from '../components/Mindmap/editor/platform'
|
|
@@ -37,6 +37,7 @@ export async function renderExcalidrawSvg(
|
|
|
37
37
|
},
|
|
38
38
|
files: (scene.files ?? {}) as never
|
|
39
39
|
})
|
|
40
|
+
|
|
40
41
|
// 只读路径必须自包含:把 CDN/相对字体换成 data URL,离线与 <img> 渲染才不掉字体
|
|
41
42
|
const inlined = await inlineExcalidrawFonts(svg.outerHTML, {
|
|
42
43
|
base: options.fontBase ?? getExcalidrawAssetPath(),
|
package/src/excalidraw/fonts.ts
CHANGED
|
@@ -28,6 +28,19 @@ export interface InlineFontsResult {
|
|
|
28
28
|
|
|
29
29
|
const FONT_URL_PATTERN = /url\((?:'|")?([^'")]+\.woff2?)(?:'|")?\)/g
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* 字体 data URL 缓存。
|
|
33
|
+
*
|
|
34
|
+
* 派生产物是「编辑期间反复重导出」的热路径,每 200ms 重新 fetch + base64 一遍
|
|
35
|
+
* 字体文件(Excalifont/Xiaolai 等十几 KB)纯属浪费;字体在进程生命周期内不会变。
|
|
36
|
+
*/
|
|
37
|
+
const fontDataUrlCache = new Map<string, string>()
|
|
38
|
+
|
|
39
|
+
/** 便于测试与部署后换基址:清掉缓存 */
|
|
40
|
+
export function clearExcalidrawFontCache(): void {
|
|
41
|
+
fontDataUrlCache.clear()
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
/** 把 CDN / 相对基址统一成本地基址下的字体路径。 */
|
|
32
45
|
export function resolveFontUrl(rawUrl: string, base?: string): string {
|
|
33
46
|
const fileName = rawUrl.split('/fonts/').pop() ?? rawUrl.split('/').pop() ?? rawUrl
|
|
@@ -63,11 +76,18 @@ export async function inlineExcalidrawFonts(
|
|
|
63
76
|
const remaining: string[] = []
|
|
64
77
|
for (const original of urls) {
|
|
65
78
|
const target = resolveFontUrl(original, options.base)
|
|
79
|
+
const cached = fontDataUrlCache.get(target)
|
|
80
|
+
if (cached !== undefined) {
|
|
81
|
+
replacements.set(original, cached)
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
66
84
|
try {
|
|
67
85
|
const response = await fetchImpl(target)
|
|
68
86
|
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
69
87
|
const bytes = new Uint8Array(await response.arrayBuffer())
|
|
70
|
-
|
|
88
|
+
const dataUrl = `data:${mimeFor(target)};base64,${toBase64(bytes)}`
|
|
89
|
+
fontDataUrlCache.set(target, dataUrl)
|
|
90
|
+
replacements.set(original, dataUrl)
|
|
71
91
|
} catch {
|
|
72
92
|
remaining.push(target)
|
|
73
93
|
}
|
package/src/styles/code.css
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
background: var(--tn-c-bg-soft);
|
|
6
6
|
color: var(--tn-c-text);
|
|
7
7
|
overflow: hidden;
|
|
8
|
+
/* 宽度只由正文容器决定:长行走块内横向滚动,永远不把页面撑宽 */
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
max-width: 100%;
|
|
11
|
+
min-width: 0;
|
|
8
12
|
}
|
|
9
13
|
.tn-code-block__header,
|
|
10
14
|
.tn-code-fullscreen > header {
|
|
@@ -18,6 +22,19 @@
|
|
|
18
22
|
border-bottom: 1px solid var(--tn-c-divider);
|
|
19
23
|
font: 13px/24px var(--tn-font-sans);
|
|
20
24
|
}
|
|
25
|
+
.tn-code-block__collapse-btn,
|
|
26
|
+
.tn-code-group__collapse-btn {
|
|
27
|
+
flex: none;
|
|
28
|
+
}
|
|
29
|
+
.tn-code-block__collapse-btn svg,
|
|
30
|
+
.tn-code-group__collapse-btn svg {
|
|
31
|
+
display: block;
|
|
32
|
+
transition: transform 0.15s ease;
|
|
33
|
+
}
|
|
34
|
+
.tn-code-block__collapse-btn.is-collapsed svg,
|
|
35
|
+
.tn-code-group__collapse-btn.is-collapsed svg {
|
|
36
|
+
transform: rotate(-90deg);
|
|
37
|
+
}
|
|
21
38
|
.tn-code-block__title,
|
|
22
39
|
.tn-code-fullscreen > header > span {
|
|
23
40
|
flex: 1;
|
|
@@ -67,12 +84,20 @@
|
|
|
67
84
|
outline: 2px solid var(--tn-c-brand);
|
|
68
85
|
outline-offset: -2px;
|
|
69
86
|
}
|
|
87
|
+
/*
|
|
88
|
+
* 默认(VitePress 同款宽高处理):不管多少行都由内容撑开,不设 max-height、
|
|
89
|
+
* 不出现内部纵向滚动;宽度不超出容器,超长单行在代码块内横向滚动;
|
|
90
|
+
* 原始换行与缩进原样保留。「收起」是页面内的视图状态(见文件末尾 .is-collapsed)。
|
|
91
|
+
*/
|
|
70
92
|
.tn-code-block pre {
|
|
71
93
|
background: transparent;
|
|
72
94
|
margin: 0;
|
|
73
95
|
padding: 8px 0;
|
|
74
|
-
max-height:
|
|
75
|
-
|
|
96
|
+
max-height: none;
|
|
97
|
+
max-width: 100%;
|
|
98
|
+
overflow-x: auto;
|
|
99
|
+
overflow-y: hidden;
|
|
100
|
+
white-space: pre;
|
|
76
101
|
font: 14px/24px var(--tn-font-mono);
|
|
77
102
|
tab-size: 2;
|
|
78
103
|
}
|
|
@@ -155,12 +180,49 @@ html[data-theme='dark'] .tn-code-highlight span:not(.line) {
|
|
|
155
180
|
color: var(--tn-c-brand);
|
|
156
181
|
border-bottom-color: var(--tn-c-brand);
|
|
157
182
|
}
|
|
183
|
+
/*
|
|
184
|
+
* 代码分组:折叠 Icon 放在 tab 行最左侧——面板自带的那个会被 tab 行盖住,
|
|
185
|
+
* 所以带 tab 的分组统一用 tab 行里这颗(点它作用于**当前**面板)。
|
|
186
|
+
*/
|
|
187
|
+
.tn-code-group__tabs .tn-code-group__collapse-btn {
|
|
188
|
+
position: sticky;
|
|
189
|
+
left: 0;
|
|
190
|
+
z-index: 1;
|
|
191
|
+
box-sizing: border-box;
|
|
192
|
+
display: inline-flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
justify-content: center;
|
|
195
|
+
width: 28px;
|
|
196
|
+
height: 28px;
|
|
197
|
+
padding: 4px;
|
|
198
|
+
border: 0;
|
|
199
|
+
border-radius: 4px;
|
|
200
|
+
background: var(--tn-c-bg-soft);
|
|
201
|
+
color: var(--tn-c-text-2);
|
|
202
|
+
}
|
|
203
|
+
.tn-code-group__tabs .tn-code-group__collapse-btn:hover {
|
|
204
|
+
background: var(--tn-c-hover);
|
|
205
|
+
color: var(--tn-c-text);
|
|
206
|
+
}
|
|
207
|
+
.tn-code-group__collapse-btn[hidden] {
|
|
208
|
+
display: none;
|
|
209
|
+
}
|
|
210
|
+
.tn-code-group.has-tabs .tn-code-block__collapse-btn {
|
|
211
|
+
display: none;
|
|
212
|
+
}
|
|
158
213
|
.tn-code-group .tn-code-block__title {
|
|
159
214
|
visibility: hidden;
|
|
160
215
|
}
|
|
161
216
|
.tn-code-group__panel > .tn-code-block {
|
|
162
217
|
margin: 0;
|
|
163
218
|
}
|
|
219
|
+
/*
|
|
220
|
+
* tab 行是绝对定位(叠在面板头部上)。面板里代码块收起后只剩标题栏那 38px,
|
|
221
|
+
* 这里兜一个下限:万一头部被隐藏,tab 行也不会压住后面的正文。
|
|
222
|
+
*/
|
|
223
|
+
.tn-code-group__panels > .tn-code-group__panel {
|
|
224
|
+
min-height: 38px;
|
|
225
|
+
}
|
|
164
226
|
.tn-code-fullscreen {
|
|
165
227
|
position: fixed;
|
|
166
228
|
inset: 0;
|
|
@@ -182,3 +244,13 @@ html[data-theme='dark'] .tn-code-highlight span:not(.line) {
|
|
|
182
244
|
.tn-code-fullscreen .tn-code-block pre {
|
|
183
245
|
max-height: none;
|
|
184
246
|
}
|
|
247
|
+
|
|
248
|
+
/* -------------------------- 收起(视图状态,不落库) -------------------------- */
|
|
249
|
+
|
|
250
|
+
/*
|
|
251
|
+
* 收起 = 代码内容整体隐藏,只剩标题栏(跟折叠标题一样),不是「露出前几行」。
|
|
252
|
+
* 展开态完全不做高度限制。
|
|
253
|
+
*/
|
|
254
|
+
.tn-code-block.is-collapsed .tn-code-block__content {
|
|
255
|
+
display: none;
|
|
256
|
+
}
|
package/src/styles/prose.css
CHANGED
|
@@ -70,6 +70,41 @@
|
|
|
70
70
|
:is(.tn-prose, .vp-doc) :where(li > ul, li > ol) {
|
|
71
71
|
margin: 0;
|
|
72
72
|
}
|
|
73
|
+
/* 任务列表复选框:对齐语雀的方框(16px / 圆角 4px / 选中填充品牌绿 + 白色对勾 /
|
|
74
|
+
`transition: all .3s`)。markdown-it-task-lists 只输出原生 `input`,这里用
|
|
75
|
+
`appearance: none` 自绘;同时去掉 `.task-list-item` 的项目符号,避免出现
|
|
76
|
+
「◦ ☐ 文本」两层符号。对勾用 data-URI 而不是 `::after`:`input` 是替换元素,
|
|
77
|
+
Firefox 不生成伪元素。 */
|
|
78
|
+
:is(.tn-prose, .vp-doc) :where(.task-list-item) {
|
|
79
|
+
list-style: none;
|
|
80
|
+
}
|
|
81
|
+
:is(.tn-prose, .vp-doc) :where(.task-list-item-checkbox) {
|
|
82
|
+
appearance: none;
|
|
83
|
+
-webkit-appearance: none;
|
|
84
|
+
box-sizing: border-box;
|
|
85
|
+
position: relative;
|
|
86
|
+
top: 0.2em;
|
|
87
|
+
width: 16px;
|
|
88
|
+
height: 16px;
|
|
89
|
+
margin: 0 4px 0 0;
|
|
90
|
+
border: 1px solid var(--tn-c-task-check-border);
|
|
91
|
+
border-radius: 4px;
|
|
92
|
+
background-color: var(--tn-c-task-check-bg);
|
|
93
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.3 6.35 4.85 8.9 9.7 3.1' fill='none' stroke='%23fff' stroke-width='2'/%3E%3C/svg%3E");
|
|
94
|
+
background-repeat: no-repeat;
|
|
95
|
+
background-position: center;
|
|
96
|
+
background-size: 0 0;
|
|
97
|
+
transition: all 0.3s;
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
}
|
|
100
|
+
:is(.tn-prose, .vp-doc) :where(.task-list-item-checkbox:hover) {
|
|
101
|
+
border-color: var(--tn-c-task-check);
|
|
102
|
+
}
|
|
103
|
+
:is(.tn-prose, .vp-doc) :where(.task-list-item-checkbox:checked) {
|
|
104
|
+
border-color: var(--tn-c-task-check);
|
|
105
|
+
background-color: var(--tn-c-task-check);
|
|
106
|
+
background-size: 12px 12px;
|
|
107
|
+
}
|
|
73
108
|
:is(.tn-prose, .vp-doc) :where(:not(pre) > code) {
|
|
74
109
|
color: var(--tn-c-brand);
|
|
75
110
|
background: var(--tn-c-default-soft);
|
|
@@ -86,6 +121,11 @@
|
|
|
86
121
|
:is(.tn-prose, .vp-doc) :where(table) {
|
|
87
122
|
border-collapse: collapse;
|
|
88
123
|
margin: 16px 0;
|
|
124
|
+
/* 铺满内容列:与 Desk 可视化编辑器里的表格宽度一致(编辑器里表格宽度 = 内容列宽)。
|
|
125
|
+
不加这条时站点表格按内容收缩(实测 3 列表只有 182px,挤在 892px 的阅读列里),
|
|
126
|
+
两边观感不一致。列宽仍由 table-layout: auto 按内容分配。 */
|
|
127
|
+
width: 100%;
|
|
128
|
+
max-width: 100%;
|
|
89
129
|
}
|
|
90
130
|
:is(.tn-prose, .vp-doc) :where(th, td) {
|
|
91
131
|
border: 1px solid var(--tn-c-divider);
|
|
@@ -96,6 +136,11 @@
|
|
|
96
136
|
:is(.tn-prose, .vp-doc) :where(th) {
|
|
97
137
|
font-weight: 600;
|
|
98
138
|
background: var(--tn-c-bg-soft);
|
|
139
|
+
/* 表头默认左对齐:浏览器对 <th> 的 UA 默认是居中,而编辑器里 Milkdown 给单元格写了
|
|
140
|
+
内联 `text-align: left`,于是「站点表头居中 / 编辑器左对齐」两边不一致。
|
|
141
|
+
表格里显式写的 `:---:` / `:---:` / `---:` 会渲染成单元格内联样式,优先级高于本规则,
|
|
142
|
+
因此显式对齐不受影响(实测保留 left / center / right)。 */
|
|
143
|
+
text-align: left;
|
|
99
144
|
}
|
|
100
145
|
:is(.tn-prose, .vp-doc) :where(td p, th p) {
|
|
101
146
|
margin: 0;
|
package/src/styles/tokens.css
CHANGED
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
--tn-c-warning-soft: rgba(234, 179, 8, 0.14);
|
|
23
23
|
--tn-c-danger-soft: rgba(244, 63, 94, 0.14);
|
|
24
24
|
--tn-c-default-soft: #e8e8e9;
|
|
25
|
+
/* 任务列表复选框(对齐语雀):16px 方框、圆角 4px、选中填充品牌绿 + 白色对勾。 */
|
|
26
|
+
--tn-c-task-check: #00b96b;
|
|
27
|
+
--tn-c-task-check-border: #d9d9d9;
|
|
28
|
+
--tn-c-task-check-bg: #ffffff;
|
|
25
29
|
--tn-shadow-2: 0 4px 12px rgba(0, 0, 0, 0.12);
|
|
26
30
|
--tn-font-sans:
|
|
27
31
|
'Punctuation SC', Inter, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
|
|
@@ -65,6 +69,8 @@
|
|
|
65
69
|
--tn-c-warning-soft: rgba(234, 179, 8, 0.16);
|
|
66
70
|
--tn-c-danger-soft: rgba(244, 63, 94, 0.14);
|
|
67
71
|
--tn-c-default-soft: #2e2e32;
|
|
72
|
+
--tn-c-task-check-border: #4a4d54;
|
|
73
|
+
--tn-c-task-check-bg: transparent;
|
|
68
74
|
--tn-shadow-2: 0 4px 12px rgb(0 0 0 / 0.35);
|
|
69
75
|
color-scheme: dark;
|
|
70
76
|
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
highlightCode,
|
|
4
|
-
highlightCodeSync,
|
|
5
|
-
parseCodeMeta,
|
|
6
|
-
prepareCodeHighlighter
|
|
7
|
-
} from './highlight'
|
|
8
|
-
|
|
9
|
-
describe('shared code rendering', () => {
|
|
10
|
-
it('parses titles, line offsets, highlight ranges and per-block overrides', () => {
|
|
11
|
-
expect(parseCodeMeta('ts:line-numbers=12 {1,3-5} [demo.ts]')).toEqual({
|
|
12
|
-
language: 'ts',
|
|
13
|
-
title: 'demo.ts',
|
|
14
|
-
startLine: 12,
|
|
15
|
-
lineNumbers: true,
|
|
16
|
-
highlightedLines: [1, 3, 4, 5]
|
|
17
|
-
})
|
|
18
|
-
expect(parseCodeMeta('js {1-2} [setup.js]')).toEqual({
|
|
19
|
-
language: 'js',
|
|
20
|
-
title: 'setup.js',
|
|
21
|
-
startLine: 1,
|
|
22
|
-
lineNumbers: true,
|
|
23
|
-
highlightedLines: [1, 2]
|
|
24
|
-
})
|
|
25
|
-
expect(parseCodeMeta('txt {3}')).toEqual({
|
|
26
|
-
language: 'txt',
|
|
27
|
-
title: '',
|
|
28
|
-
startLine: 1,
|
|
29
|
-
lineNumbers: true,
|
|
30
|
-
highlightedLines: [3]
|
|
31
|
-
})
|
|
32
|
-
expect(parseCodeMeta('text:no-line-numbers').lineNumbers).toBe(false)
|
|
33
|
-
expect(parseCodeMeta('text {0,-1,8-3,1-9999999}').highlightedLines).toEqual([])
|
|
34
|
-
})
|
|
35
|
-
it('shares one lazy engine and matches build-time/browser output byte for byte', async () => {
|
|
36
|
-
const [a, b] = await Promise.all([
|
|
37
|
-
prepareCodeHighlighter(['js']),
|
|
38
|
-
prepareCodeHighlighter(['typescript'])
|
|
39
|
-
])
|
|
40
|
-
expect(a).toBe(b)
|
|
41
|
-
const source = 'const answer = 42\nconsole.log(answer)\n'
|
|
42
|
-
const runtime = await highlightCode(source, 'js {2}')
|
|
43
|
-
expect(runtime).toBe(highlightCodeSync(source, 'js {2}'))
|
|
44
|
-
expect(runtime).toContain('--shiki-light:')
|
|
45
|
-
expect(runtime).toContain('--shiki-dark:')
|
|
46
|
-
expect(runtime).toContain('class="line highlighted"')
|
|
47
|
-
expect(runtime).toContain('data-line="2"')
|
|
48
|
-
expect(a.getLoadedLanguages()).not.toContain('cobol')
|
|
49
|
-
})
|
|
50
|
-
it('escapes unknown-language code instead of executing HTML', async () => {
|
|
51
|
-
const html = await highlightCode('<script>alert(1)</script> {{ injected }}', 'unknown-label')
|
|
52
|
-
expect(html).not.toContain('<script>')
|
|
53
|
-
const host = document.createElement('div')
|
|
54
|
-
host.innerHTML = html
|
|
55
|
-
expect(host.querySelector('script')).toBeNull()
|
|
56
|
-
expect(host.textContent).toBe('<script>alert(1)</script> {{ injected }}')
|
|
57
|
-
expect(html).toContain('{{ injected }}')
|
|
58
|
-
})
|
|
59
|
-
it('applies fence line highlights and offsets without Shiki notation', async () => {
|
|
60
|
-
const withNotation = await highlightCode(
|
|
61
|
-
'const x = 1 // [!code ++]\nconst y = 2 // [!code focus]',
|
|
62
|
-
'js:line-numbers=5 {2}'
|
|
63
|
-
)
|
|
64
|
-
// Magic comments stay as source text; Desk has no annotation write path.
|
|
65
|
-
expect(withNotation).toContain('[!code ++]')
|
|
66
|
-
expect(withNotation).not.toContain('diff add')
|
|
67
|
-
expect(withNotation).not.toContain('focused')
|
|
68
|
-
expect(withNotation).toContain('data-line="5"')
|
|
69
|
-
expect(withNotation).toContain('data-line="6"')
|
|
70
|
-
expect(withNotation).toContain('class="line highlighted"')
|
|
71
|
-
})
|
|
72
|
-
})
|
|
@@ -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
|
-
})
|