@deot/docs-markdown 1.0.10 → 1.1.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 +15 -8
- package/dist/index.cjs +490 -291
- package/dist/index.d.ts +21 -2
- package/dist/index.js +490 -293
- package/dist/index.style.css +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fragment, computed, createApp, createBlock, createCommentVNode, createElementBlock, createElementVNode,
|
|
1
|
+
import { Fragment, computed, createApp, createBlock, createCommentVNode, createElementBlock, createElementVNode, defineComponent, h, inject, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, openBlock, ref, renderList, shallowRef, toDisplayString, unref, watch, withDirectives } from "vue";
|
|
2
2
|
import { provideLocale, useLocale } from "@deot/docs-locale";
|
|
3
3
|
import Config from "markdown-it-chain";
|
|
4
4
|
import anchor from "markdown-it-anchor";
|
|
@@ -6,12 +6,39 @@ import mdContainer from "markdown-it-container";
|
|
|
6
6
|
import markdownIt from "markdown-it";
|
|
7
7
|
import JSON5 from "json5";
|
|
8
8
|
import { getScroller } from "@deot/helper-dom";
|
|
9
|
-
import { Scroller } from "@deot/vc";
|
|
10
9
|
//#region packages/markdown/src/markdown.ts
|
|
11
10
|
var HTML_MD_SIGN = "md";
|
|
12
11
|
var PLAYGROUND = "playground";
|
|
13
12
|
var TIP = "tip";
|
|
14
13
|
var WARNING = "warning";
|
|
14
|
+
var CALLOUT_FILLED_RE = /(^|\s)filled(\s|$)/;
|
|
15
|
+
var CALLOUT_INFO_ICON = [
|
|
16
|
+
"<svg width=\"22\" height=\"28\" viewBox=\"0 0 22 28\" fill=\"none\" aria-hidden=\"true\">",
|
|
17
|
+
"<circle class=\"docs-markdown-callout-icon__fill\" cx=\"11\" cy=\"14\" r=\"11\"></circle>",
|
|
18
|
+
"<circle class=\"docs-markdown-callout-icon__ring\" cx=\"11\" cy=\"14\" r=\"10.5\"></circle>",
|
|
19
|
+
"<path class=\"docs-markdown-callout-icon__mark\"",
|
|
20
|
+
" d=\"m12.5 19-1.011.337a1 1 0 0 1-1.253-1.3l1.528-4.074a1 1 0 0 0-1.253-1.3L9.5 13\"",
|
|
21
|
+
" stroke-width=\"1.5\" stroke-linecap=\"round\"></path>",
|
|
22
|
+
"<path class=\"docs-markdown-callout-icon__mark\" d=\"M12 9a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0Z\" stroke-width=\"1.5\"></path>",
|
|
23
|
+
"</svg>"
|
|
24
|
+
].join("");
|
|
25
|
+
var CALLOUT_WARNING_ICON = [
|
|
26
|
+
"<svg width=\"22\" height=\"28\" viewBox=\"0 0 22 28\" fill=\"none\" aria-hidden=\"true\">",
|
|
27
|
+
"<circle class=\"docs-markdown-callout-icon__fill\" cx=\"11\" cy=\"14\" r=\"11\"></circle>",
|
|
28
|
+
"<circle class=\"docs-markdown-callout-icon__ring\" cx=\"11\" cy=\"14\" r=\"10.5\"></circle>",
|
|
29
|
+
"<path class=\"docs-markdown-callout-icon__mark\" d=\"M11 10v5.5\" stroke-width=\"1.5\" stroke-linecap=\"round\"></path>",
|
|
30
|
+
"<path class=\"docs-markdown-callout-icon__mark\" d=\"M11.5 18.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0Z\" stroke-width=\"1.5\"></path>",
|
|
31
|
+
"</svg>"
|
|
32
|
+
].join("");
|
|
33
|
+
var renderCallout = (name, icon) => (tokens, idx) => {
|
|
34
|
+
if (tokens[idx].nesting === 1) return [
|
|
35
|
+
`<div class="${CALLOUT_FILLED_RE.test(tokens[idx].info) ? `${name} ${name}--filled` : name}" md>`,
|
|
36
|
+
"<span class=\"docs-markdown-callout-icon\" aria-hidden=\"true\">",
|
|
37
|
+
icon,
|
|
38
|
+
"</span><div class=\"docs-markdown-callout-body\">\n"
|
|
39
|
+
].join("");
|
|
40
|
+
return "</div></div>\n";
|
|
41
|
+
};
|
|
15
42
|
var config = new Config();
|
|
16
43
|
config.options.html(true).typographer(true).linkify(true).end().plugin("anchor").use(anchor, [{ permalink: anchor.permalink.linkInsideHeader({
|
|
17
44
|
placement: "before",
|
|
@@ -21,8 +48,8 @@ config.options.html(true).typographer(true).linkify(true).end().plugin("anchor")
|
|
|
21
48
|
$md.use(mdContainer, PLAYGROUND, { validate(params) {
|
|
22
49
|
return params.trim().match(reg);
|
|
23
50
|
} });
|
|
24
|
-
$md.use(mdContainer, TIP);
|
|
25
|
-
$md.use(mdContainer, WARNING);
|
|
51
|
+
$md.use(mdContainer, TIP, { render: renderCallout(TIP, CALLOUT_INFO_ICON) });
|
|
52
|
+
$md.use(mdContainer, WARNING, { render: renderCallout(WARNING, CALLOUT_WARNING_ICON) });
|
|
26
53
|
}).end();
|
|
27
54
|
var md = config.toMd(markdownIt);
|
|
28
55
|
var playgroundOpen = `container_${PLAYGROUND}_open`;
|
|
@@ -69,12 +96,22 @@ var validatePreviewInset = (propsData) => {
|
|
|
69
96
|
if (Array.isArray(inset) && inset.length === 2 && inset.every(isPreviewInsetValue)) return "";
|
|
70
97
|
return "previewInset 必须是非负数或 [垂直,水平] 非负数数组";
|
|
71
98
|
};
|
|
72
|
-
var
|
|
73
|
-
if (!("
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
if (typeof
|
|
77
|
-
return "
|
|
99
|
+
var validateExpandable = (propsData) => {
|
|
100
|
+
if (!("expandable" in propsData)) return "";
|
|
101
|
+
const expandable = propsData.expandable;
|
|
102
|
+
if (expandable === true) return "";
|
|
103
|
+
if (typeof expandable === "number" && Number.isFinite(expandable) && expandable > 0) return "";
|
|
104
|
+
return "expandable 必须是 true 或正数";
|
|
105
|
+
};
|
|
106
|
+
var validateTitle = (propsData) => {
|
|
107
|
+
if (!("title" in propsData)) return "";
|
|
108
|
+
if (typeof propsData.title === "string") return "";
|
|
109
|
+
return "title 必须是字符串";
|
|
110
|
+
};
|
|
111
|
+
var validateTitleId = (propsData) => {
|
|
112
|
+
if (!("id" in propsData)) return "";
|
|
113
|
+
if (typeof propsData.id === "string") return "";
|
|
114
|
+
return "id 必须是字符串";
|
|
78
115
|
};
|
|
79
116
|
var parseRuntimeProps = (tokens) => {
|
|
80
117
|
for (const token of tokens) {
|
|
@@ -112,7 +149,7 @@ md.core.ruler.after("block", "runtime-files", (state) => {
|
|
|
112
149
|
const placeholder = new state.Token("html_block", "", 0);
|
|
113
150
|
placeholder.block = true;
|
|
114
151
|
const propsData = parseRuntimeProps(innerTokens);
|
|
115
|
-
const propsError = validateRuntimeViews(propsData) || validateRuntimeViewport(propsData) || validatePreviewInset(propsData) ||
|
|
152
|
+
const propsError = validateRuntimeViews(propsData) || validateRuntimeViewport(propsData) || validatePreviewInset(propsData) || validateExpandable(propsData) || validateTitle(propsData) || validateTitleId(propsData);
|
|
116
153
|
const propsAttr = renderPlaygroundAttrs(propsData);
|
|
117
154
|
if (!fences.length) placeholder.content = renderPlaygroundError("至少需要声明一个代码文件");
|
|
118
155
|
else if (propsError) placeholder.content = renderPlaygroundError(propsError);
|
|
@@ -223,6 +260,7 @@ var cleanup = (el) => {
|
|
|
223
260
|
var resolveBlockLanguage = (block) => {
|
|
224
261
|
return [...block.classList].find((className) => className.startsWith("language-"))?.slice(9) || "";
|
|
225
262
|
};
|
|
263
|
+
var serializePlaygroundDefaults = (value) => JSON.stringify(value || {});
|
|
226
264
|
var render = async (el, binding) => {
|
|
227
265
|
cleanup(el);
|
|
228
266
|
const version = (renderVersions.get(el) || 0) + 1;
|
|
@@ -251,6 +289,7 @@ var render = async (el, binding) => {
|
|
|
251
289
|
app.mount(mountPoint);
|
|
252
290
|
apps.push(app);
|
|
253
291
|
});
|
|
292
|
+
const sitePlaygroundDefaults = binding.value.playground && typeof binding.value.playground === "object" ? binding.value.playground : {};
|
|
254
293
|
playgrounds.forEach((item) => {
|
|
255
294
|
const code = item.dataset.code;
|
|
256
295
|
let files;
|
|
@@ -266,6 +305,7 @@ var render = async (el, binding) => {
|
|
|
266
305
|
entry: item.dataset.entry
|
|
267
306
|
} : { modelValue: code };
|
|
268
307
|
const app = createApp(() => h(DocsPlayground.Playground, {
|
|
308
|
+
...sitePlaygroundDefaults,
|
|
269
309
|
...typeof propsData === "object" ? propsData : {},
|
|
270
310
|
...runtimeProps,
|
|
271
311
|
locale: binding.value.locale
|
|
@@ -276,7 +316,7 @@ var render = async (el, binding) => {
|
|
|
276
316
|
mountedApps.set(el, apps);
|
|
277
317
|
};
|
|
278
318
|
var update = (el, binding) => {
|
|
279
|
-
if (binding.value.source !== binding.oldValue?.source || binding.value.locale !== binding.oldValue?.locale) render(el, binding);
|
|
319
|
+
if (binding.value.source !== binding.oldValue?.source || binding.value.locale !== binding.oldValue?.locale || serializePlaygroundDefaults(binding.value.playground) !== serializePlaygroundDefaults(binding.oldValue?.playground)) render(el, binding);
|
|
280
320
|
};
|
|
281
321
|
var vMarkdown = {
|
|
282
322
|
mounted: (el, binding) => void render(el, binding),
|
|
@@ -286,17 +326,148 @@ var vMarkdown = {
|
|
|
286
326
|
cleanup(el);
|
|
287
327
|
}
|
|
288
328
|
};
|
|
329
|
+
/**
|
|
330
|
+
* 指示器高度不能超过正文实际高度,也不能超出当前滚动容器可视区域。
|
|
331
|
+
* 与文档壳大纲的封顶规则一致。
|
|
332
|
+
* @param articleHeight 正文高度。
|
|
333
|
+
* @param viewportHeight 主滚动容器可视高度。
|
|
334
|
+
* @returns 指示器最大高度,单位 px。
|
|
335
|
+
*/
|
|
336
|
+
var capIndicatorMaxHeight = (articleHeight, viewportHeight) => {
|
|
337
|
+
const article = Math.max(0, articleHeight);
|
|
338
|
+
const viewport = Math.max(0, viewportHeight);
|
|
339
|
+
if (!article) return viewport;
|
|
340
|
+
if (!viewport) return article;
|
|
341
|
+
return Math.min(article, viewport);
|
|
342
|
+
};
|
|
343
|
+
/**
|
|
344
|
+
* 滑轨高度再受 sticky 父级剩余空间约束,避免滚到文末盖住 footer。
|
|
345
|
+
* @param cap 正文/视口封顶后的高度。
|
|
346
|
+
* @param available 指示器顶部到父级底部的剩余像素;非有限值表示不额外约束。
|
|
347
|
+
* @returns 最终高度,单位 px。
|
|
348
|
+
*/
|
|
349
|
+
var capStickyAvailableHeight = (cap, available) => {
|
|
350
|
+
const limited = Math.max(0, cap);
|
|
351
|
+
if (!Number.isFinite(available)) return limited;
|
|
352
|
+
return Math.min(limited, Math.max(0, available));
|
|
353
|
+
};
|
|
354
|
+
/**
|
|
355
|
+
* 把主滚动位置映射成小地图可视窗口(相对轨道的 0–1 比例)。
|
|
356
|
+
* @param scrollTop 当前滚动偏移。
|
|
357
|
+
* @param clientHeight 滚动容器可视高度。
|
|
358
|
+
* @param scrollHeight 滚动内容总高度。
|
|
359
|
+
* @returns 窗口顶部与高度比例。
|
|
360
|
+
*/
|
|
361
|
+
var getMinimapWindow = (scrollTop, clientHeight, scrollHeight) => {
|
|
362
|
+
const total = Math.max(0, scrollHeight);
|
|
363
|
+
if (!total) return {
|
|
364
|
+
height: 1,
|
|
365
|
+
top: 0
|
|
366
|
+
};
|
|
367
|
+
const height = Math.min(1, Math.max(0, clientHeight / total));
|
|
368
|
+
const maxTop = Math.max(0, 1 - height);
|
|
369
|
+
return {
|
|
370
|
+
height,
|
|
371
|
+
top: maxTop <= 0 ? 0 : Math.min(maxTop, Math.max(0, scrollTop / total))
|
|
372
|
+
};
|
|
373
|
+
};
|
|
374
|
+
/**
|
|
375
|
+
* 按小地图窗口顶部选出当前阅读刻度:最后一个仍不晚于窗口顶的块。
|
|
376
|
+
* 滚到末屏时固定最后一条,避免末块永远到不了窗口顶。
|
|
377
|
+
* @param ratios 刻度在正文中的 0–1 比例,按出现顺序。
|
|
378
|
+
* @param windowTop 可视窗口顶部比例。
|
|
379
|
+
* @param windowHeight 可视窗口高度比例。
|
|
380
|
+
* @returns 当前刻度序号;没有刻度时为 -1。
|
|
381
|
+
*/
|
|
382
|
+
var findActiveMinimapIndex = (ratios, windowTop, windowHeight) => {
|
|
383
|
+
if (!ratios.length) return -1;
|
|
384
|
+
if (windowTop + windowHeight >= .999) return ratios.length - 1;
|
|
385
|
+
let active = 0;
|
|
386
|
+
for (let index = 0; index < ratios.length; index++) if ((ratios[index] || 0) <= windowTop) active = index;
|
|
387
|
+
return active;
|
|
388
|
+
};
|
|
389
|
+
/**
|
|
390
|
+
* 判断刻度是否落在当前可视窗口内。
|
|
391
|
+
* @param ratio 刻度比例。
|
|
392
|
+
* @param windowTop 可视窗口顶部比例。
|
|
393
|
+
* @param windowHeight 可视窗口高度比例。
|
|
394
|
+
* @returns 是否位于窗口内。
|
|
395
|
+
*/
|
|
396
|
+
var isMinimapMarkerInView = (ratio, windowTop, windowHeight) => {
|
|
397
|
+
const top = Math.max(0, windowTop);
|
|
398
|
+
const bottom = top + Math.max(0, windowHeight);
|
|
399
|
+
return ratio >= top && ratio <= bottom;
|
|
400
|
+
};
|
|
401
|
+
/**
|
|
402
|
+
* 把指针纵坐标映射成轨道上的 0–1 比例。
|
|
403
|
+
* @param clientY 指针视口纵坐标。
|
|
404
|
+
* @param trackTop 轨道顶部。
|
|
405
|
+
* @param trackHeight 轨道高度。
|
|
406
|
+
* @returns 夹紧后的比例。
|
|
407
|
+
*/
|
|
408
|
+
var getMinimapPointerRatio = (clientY, trackTop, trackHeight) => {
|
|
409
|
+
if (trackHeight <= 0) return 0;
|
|
410
|
+
return Math.min(1, Math.max(0, (clientY - trackTop) / trackHeight));
|
|
411
|
+
};
|
|
412
|
+
/**
|
|
413
|
+
* 把指示器刻度对齐到文档壳斜纹轨的内侧,并留出 `INDICATOR_RAIL_GAP`。
|
|
414
|
+
* 左侧靠近 `rail--start` 的右缘,右侧靠近 `rail--end` 的左缘。
|
|
415
|
+
* 没有可见轨道时返回 undefined,由样式回退到正文边缘。
|
|
416
|
+
* @param root 指示器根节点相对视口的盒子。
|
|
417
|
+
* @param rail 当前侧斜纹轨的盒子。
|
|
418
|
+
* @param side 指示器所在侧。
|
|
419
|
+
* @param viewportWidth 刻度轨道宽度。
|
|
420
|
+
* @returns 相对指示器根节点的 left 偏移,单位 px。
|
|
421
|
+
*/
|
|
422
|
+
var getIndicatorInlineOffset = (root, rail, side, viewportWidth) => {
|
|
423
|
+
if (!rail || rail.width <= 0 || rail.height <= 0) return;
|
|
424
|
+
if (side === "left") return rail.right - root.left + 8;
|
|
425
|
+
return rail.left - root.left - viewportWidth - 8;
|
|
426
|
+
};
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region packages/markdown/src/headings.ts
|
|
429
|
+
var HEADING_SELECTOR = "h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]";
|
|
430
|
+
var SKIP_CLOSEST = "pre, .docs-markdown-code-preview, [data-playground][data-code], [data-playground][data-files]";
|
|
431
|
+
/**
|
|
432
|
+
* 去掉 markdown-it-anchor 的井号后,读取标题的可见文案。
|
|
433
|
+
* @param element 当前标题节点。
|
|
434
|
+
* @returns 单行章节标题。
|
|
435
|
+
*/
|
|
436
|
+
var getMarkdownHeadingTitle = (element) => {
|
|
437
|
+
const clone = element.cloneNode(true);
|
|
438
|
+
clone.querySelectorAll(".header-anchor").forEach((node) => node.remove());
|
|
439
|
+
return (clone.textContent || "").replace(/\s+/g, " ").trim();
|
|
440
|
+
};
|
|
441
|
+
/**
|
|
442
|
+
* 从已渲染的 Markdown 中收集带 id 的标题,规则与文档壳大纲一致:
|
|
443
|
+
* 忽略代码块 / Playground 内部标题;有章节标题时不把页面 h1 算进去。
|
|
444
|
+
* @param root Markdown 所在容器。
|
|
445
|
+
* @returns 大纲条目,按出现顺序排列。
|
|
446
|
+
*/
|
|
447
|
+
var collectMarkdownHeadings = (root) => {
|
|
448
|
+
if (!root || !("querySelectorAll" in root)) return [];
|
|
449
|
+
return [...root.querySelectorAll(HEADING_SELECTOR)].filter((element) => !element.closest(SKIP_CLOSEST) && Boolean(getMarkdownHeadingTitle(element))).map((element) => ({
|
|
450
|
+
element,
|
|
451
|
+
id: element.id,
|
|
452
|
+
level: Number(element.tagName.slice(1)),
|
|
453
|
+
text: getMarkdownHeadingTitle(element)
|
|
454
|
+
})).filter((item, _, headings) => item.level > 1 || headings.every((heading) => heading.level === 1));
|
|
455
|
+
};
|
|
289
456
|
//#endregion
|
|
290
457
|
//#region packages/markdown/src/indicator.vue?vue&type=script&setup=true&lang.ts
|
|
291
|
-
var _hoisted_1
|
|
292
|
-
var _hoisted_2 =
|
|
293
|
-
var _hoisted_3 =
|
|
294
|
-
var _hoisted_4 = {
|
|
295
|
-
var _hoisted_5 = {
|
|
458
|
+
var _hoisted_1 = ["aria-label"];
|
|
459
|
+
var _hoisted_2 = ["aria-valuenow", "aria-valuetext"];
|
|
460
|
+
var _hoisted_3 = { class: "docs-markdown-indicator__preview-title" };
|
|
461
|
+
var _hoisted_4 = {
|
|
296
462
|
key: 0,
|
|
297
463
|
class: "docs-markdown-indicator__preview-content"
|
|
298
464
|
};
|
|
299
|
-
var
|
|
465
|
+
var LAYOUT_SELECTOR = ".docs-layout";
|
|
466
|
+
var RAIL_START_SELECTOR = ".docs-layout__rail--start";
|
|
467
|
+
var RAIL_END_SELECTOR = ".docs-layout__rail--end";
|
|
468
|
+
var VIEWPORT_WIDTH = 40;
|
|
469
|
+
/** 与 docs-page-outline 上下 padding 一致,需从轨道高度中扣除。 */
|
|
470
|
+
var INDICATOR_CHROME = 136;
|
|
300
471
|
//#endregion
|
|
301
472
|
//#region packages/markdown/src/indicator.vue
|
|
302
473
|
var indicator_default = /* @__PURE__ */ defineComponent({
|
|
@@ -306,61 +477,82 @@ var indicator_default = /* @__PURE__ */ defineComponent({
|
|
|
306
477
|
target: {}
|
|
307
478
|
},
|
|
308
479
|
setup(__props) {
|
|
309
|
-
const BLOCK_SELECTOR = [
|
|
310
|
-
"h1",
|
|
311
|
-
"h2",
|
|
312
|
-
"h3",
|
|
313
|
-
"h4",
|
|
314
|
-
"h5",
|
|
315
|
-
"h6",
|
|
316
|
-
"p",
|
|
317
|
-
"li",
|
|
318
|
-
"pre",
|
|
319
|
-
"blockquote",
|
|
320
|
-
"table",
|
|
321
|
-
".tip",
|
|
322
|
-
".warning",
|
|
323
|
-
".docs-markdown-code-preview",
|
|
324
|
-
"[data-playground]"
|
|
325
|
-
].join(",");
|
|
326
|
-
const CONTAINER_SELECTOR = [
|
|
327
|
-
"li",
|
|
328
|
-
"blockquote",
|
|
329
|
-
"table",
|
|
330
|
-
".tip",
|
|
331
|
-
".warning",
|
|
332
|
-
".docs-markdown-code-preview",
|
|
333
|
-
"[data-playground]"
|
|
334
|
-
].join(",");
|
|
335
480
|
const props = __props;
|
|
336
481
|
const { lang: localeName, t } = useLocale();
|
|
337
482
|
const parentScroller = inject("vc-scroller", null);
|
|
338
483
|
const indicatorRoot = ref();
|
|
339
484
|
const viewport = ref();
|
|
340
485
|
const rail = ref();
|
|
341
|
-
const indicatorScroller = ref();
|
|
342
486
|
const markers = shallowRef([]);
|
|
343
|
-
const activeIndex = ref(0);
|
|
344
487
|
const hoverIndex = ref();
|
|
488
|
+
const activeIndex = ref(-1);
|
|
345
489
|
const previewTop = ref(0);
|
|
346
490
|
const dragging = ref(false);
|
|
347
|
-
const
|
|
491
|
+
const cappedHeight = ref(0);
|
|
492
|
+
const inlineAnchor = ref();
|
|
493
|
+
const windowBox = ref(getMinimapWindow(0, 0, 0));
|
|
348
494
|
let observer;
|
|
349
495
|
let hostResizeObserver;
|
|
350
496
|
let scrollHost;
|
|
351
497
|
let usesParentScroller = false;
|
|
352
498
|
let captureTarget;
|
|
353
499
|
let refreshFrame = 0;
|
|
354
|
-
let
|
|
500
|
+
let windowFrame = 0;
|
|
355
501
|
let targetGeneration = 0;
|
|
356
502
|
const toCssLength = (value, fallback) => typeof value === "number" ? `${value}px` : value || fallback;
|
|
503
|
+
const resolvedHeight = computed(() => {
|
|
504
|
+
const cap = cappedHeight.value;
|
|
505
|
+
const configured = props.options.height;
|
|
506
|
+
if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) return `${cap > 0 ? Math.min(configured, cap) : configured}px`;
|
|
507
|
+
if (typeof configured === "string" && configured.trim()) return configured;
|
|
508
|
+
if (cap > 0) return `${cap}px`;
|
|
509
|
+
return "calc(100svh - 60px)";
|
|
510
|
+
});
|
|
511
|
+
/** 扣除上下 padding 后的轨道高度。小地图轨高等于该值,不再内部滚动。 */
|
|
512
|
+
const resolvedTrackHeight = computed(() => {
|
|
513
|
+
const height = resolvedHeight.value;
|
|
514
|
+
if (height.endsWith("px")) {
|
|
515
|
+
const total = Number.parseFloat(height);
|
|
516
|
+
if (Number.isFinite(total)) return `${Math.max(0, total - INDICATOR_CHROME)}px`;
|
|
517
|
+
}
|
|
518
|
+
return `calc(${height} - ${INDICATOR_CHROME}px)`;
|
|
519
|
+
});
|
|
357
520
|
const rootStyle = computed(() => ({
|
|
358
|
-
"--docs-markdown-indicator-height":
|
|
359
|
-
"--docs-markdown-indicator-
|
|
521
|
+
"--docs-markdown-indicator-height": resolvedHeight.value,
|
|
522
|
+
"--docs-markdown-indicator-track-height": resolvedTrackHeight.value,
|
|
360
523
|
"--docs-markdown-indicator-top": toCssLength(props.options.top, "0px")
|
|
361
524
|
}));
|
|
362
525
|
const hoveredMarker = computed(() => typeof hoverIndex.value === "number" ? markers.value[hoverIndex.value] : void 0);
|
|
363
|
-
const
|
|
526
|
+
const isMarkerInView = (index) => {
|
|
527
|
+
const marker = markers.value[index];
|
|
528
|
+
if (!marker) return false;
|
|
529
|
+
return isMinimapMarkerInView(marker.ratio, windowBox.value.top, windowBox.value.height);
|
|
530
|
+
};
|
|
531
|
+
const resolvedPosition = computed(() => props.options.position === "right" ? "right" : "left");
|
|
532
|
+
const scrollProgress = computed(() => Math.round(windowBox.value.top * 100));
|
|
533
|
+
const viewportStyle = computed(() => {
|
|
534
|
+
if (typeof inlineAnchor.value !== "number") return;
|
|
535
|
+
return {
|
|
536
|
+
left: `${inlineAnchor.value}px`,
|
|
537
|
+
right: "auto"
|
|
538
|
+
};
|
|
539
|
+
});
|
|
540
|
+
const windowStyle = computed(() => ({
|
|
541
|
+
height: `${windowBox.value.height * 100}%`,
|
|
542
|
+
top: `${windowBox.value.top * 100}%`
|
|
543
|
+
}));
|
|
544
|
+
const previewStyle = computed(() => {
|
|
545
|
+
const style = { top: `${previewTop.value}px` };
|
|
546
|
+
if (typeof inlineAnchor.value !== "number") return style;
|
|
547
|
+
if (resolvedPosition.value === "left") {
|
|
548
|
+
style.left = `${inlineAnchor.value + 24}px`;
|
|
549
|
+
style.right = "auto";
|
|
550
|
+
} else {
|
|
551
|
+
style.left = "auto";
|
|
552
|
+
style.right = `calc(100% - ${inlineAnchor.value}px)`;
|
|
553
|
+
}
|
|
554
|
+
return style;
|
|
555
|
+
});
|
|
364
556
|
/**
|
|
365
557
|
* 保留文档块的有效换行,同时折叠每行内部的多余空白。
|
|
366
558
|
* @param element 当前文档块。
|
|
@@ -368,30 +560,21 @@ var indicator_default = /* @__PURE__ */ defineComponent({
|
|
|
368
560
|
*/
|
|
369
561
|
const getBlockText = (element) => (element.textContent || "").split(/\n+/).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
|
|
370
562
|
/**
|
|
371
|
-
*
|
|
372
|
-
* @param
|
|
373
|
-
* @
|
|
374
|
-
|
|
375
|
-
const getHeadingTitle = (element) => getBlockText(element).replace(/^#\s*/, "").replace(/\s+/g, " ").trim() || t("markdown.indicator.untitled");
|
|
376
|
-
/**
|
|
377
|
-
* 为无障碍名称限制长度,避免长代码块生成过大的属性值。
|
|
378
|
-
* @param title 当前章节标题。
|
|
379
|
-
* @param content 当前文档块内容。
|
|
380
|
-
* @returns 标题和内容组成的简短名称。
|
|
381
|
-
*/
|
|
382
|
-
const getAriaLabel = (title, content) => {
|
|
383
|
-
const value = content ? `${title}: ${content.replace(/\s+/g, " ")}` : title;
|
|
384
|
-
if (value.length <= MAX_ARIA_LABEL_LENGTH) return value;
|
|
385
|
-
return `${value.slice(0, MAX_ARIA_LABEL_LENGTH).trim()}…`;
|
|
386
|
-
};
|
|
387
|
-
/**
|
|
388
|
-
* 判断候选节点是否已经包含在另一个可独立定位的文档块内。
|
|
389
|
-
* @param element 当前候选节点。
|
|
390
|
-
* @returns 是否应由外层文档块统一表示。
|
|
563
|
+
* 取当前标题到下一标题之间的第一段正文,作为悬停摘要。
|
|
564
|
+
* @param heading 当前标题。
|
|
565
|
+
* @param nextHeading 下一个标题,没有则为空。
|
|
566
|
+
* @returns 摘要文本。
|
|
391
567
|
*/
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
568
|
+
const getPreviewContent = (heading, nextHeading) => {
|
|
569
|
+
let node = heading.nextElementSibling;
|
|
570
|
+
while (node && node !== nextHeading) {
|
|
571
|
+
if (!/^H[1-6]$/.test(node.tagName)) {
|
|
572
|
+
const text = getBlockText(node);
|
|
573
|
+
if (text) return text;
|
|
574
|
+
}
|
|
575
|
+
node = node.nextElementSibling;
|
|
576
|
+
}
|
|
577
|
+
return "";
|
|
395
578
|
};
|
|
396
579
|
/**
|
|
397
580
|
* 获取 Markdown 使用的原生滚动宿主。
|
|
@@ -400,77 +583,118 @@ var indicator_default = /* @__PURE__ */ defineComponent({
|
|
|
400
583
|
*/
|
|
401
584
|
const getScrollHost = () => getScroller(props.target) || window;
|
|
402
585
|
/**
|
|
403
|
-
*
|
|
404
|
-
* @returns
|
|
586
|
+
* 读取当前滚动容器的可视高度。
|
|
587
|
+
* @returns 滚动容器 clientHeight,窗口滚动时回退到 innerHeight。
|
|
405
588
|
*/
|
|
406
|
-
const
|
|
407
|
-
if (usesParentScroller) return parentScroller?.wrapper?.
|
|
408
|
-
|
|
589
|
+
const getScrollHostClientHeight = () => {
|
|
590
|
+
if (usesParentScroller) return parentScroller?.wrapper?.clientHeight || parentScroller?.clientHeight || window.innerHeight;
|
|
591
|
+
if (scrollHost instanceof HTMLElement) return scrollHost.clientHeight;
|
|
592
|
+
return window.innerHeight;
|
|
409
593
|
};
|
|
410
594
|
/**
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
* @returns 是否位于可滚动内容末尾。
|
|
595
|
+
* 读取主滚动的位置与尺寸,供可视窗口和 scrub 共用。
|
|
596
|
+
* @returns 当前滚动偏移、可视高度和内容总高度。
|
|
414
597
|
*/
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
return scrollHeight > clientHeight && scrollTop + clientHeight >= scrollHeight - 1;
|
|
598
|
+
const getScrollMetrics = () => {
|
|
599
|
+
if (usesParentScroller) return {
|
|
600
|
+
clientHeight: parentScroller?.clientHeight || parentScroller?.wrapper?.clientHeight || 0,
|
|
601
|
+
scrollHeight: parentScroller?.scrollHeight || parentScroller?.wrapper?.scrollHeight || 0,
|
|
602
|
+
scrollTop: parentScroller?.scrollTop || 0
|
|
603
|
+
};
|
|
604
|
+
if (scrollHost instanceof HTMLElement) return {
|
|
605
|
+
clientHeight: scrollHost.clientHeight,
|
|
606
|
+
scrollHeight: scrollHost.scrollHeight,
|
|
607
|
+
scrollTop: scrollHost.scrollTop
|
|
608
|
+
};
|
|
609
|
+
return {
|
|
610
|
+
clientHeight: window.innerHeight,
|
|
611
|
+
scrollHeight: document.documentElement.scrollHeight,
|
|
612
|
+
scrollTop: window.scrollY
|
|
613
|
+
};
|
|
433
614
|
};
|
|
434
615
|
/**
|
|
435
|
-
*
|
|
436
|
-
* @
|
|
616
|
+
* 按正文、主滚动可视高度,以及 sticky 父级剩余空间封顶,避免盖住 footer。
|
|
617
|
+
* @returns 指示器可用的最大高度,单位 px。
|
|
437
618
|
*/
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const padding = 16;
|
|
446
|
-
if (markerRect.top >= wrapperRect.top + padding && markerRect.bottom <= wrapperRect.bottom - padding) return;
|
|
447
|
-
const markerCenter = markerRect.top + markerRect.height / 2;
|
|
448
|
-
const wrapperCenter = wrapperRect.top + wrapperRect.height / 2;
|
|
449
|
-
indicatorScroller.value?.setScrollTop(Math.max(0, wrapper.scrollTop + markerCenter - wrapperCenter));
|
|
619
|
+
const getStickyAvailableHeight = () => {
|
|
620
|
+
const root = indicatorRoot.value;
|
|
621
|
+
const parent = root?.parentElement;
|
|
622
|
+
if (!root || !parent) return Number.POSITIVE_INFINITY;
|
|
623
|
+
const parentBox = parent.getBoundingClientRect();
|
|
624
|
+
if (parentBox.height <= 0 && parentBox.bottom === 0) return Number.POSITIVE_INFINITY;
|
|
625
|
+
return parentBox.bottom - root.getBoundingClientRect().top;
|
|
450
626
|
};
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
627
|
+
const updateCappedHeight = () => {
|
|
628
|
+
cappedHeight.value = capStickyAvailableHeight(capIndicatorMaxHeight(props.target?.offsetHeight || 0, getScrollHostClientHeight()), getStickyAvailableHeight());
|
|
629
|
+
};
|
|
630
|
+
/** 根据主滚动位置更新小地图可视窗口和当前刻度。 */
|
|
631
|
+
const updateWindow = () => {
|
|
632
|
+
windowFrame = 0;
|
|
633
|
+
const metrics = getScrollMetrics();
|
|
634
|
+
windowBox.value = getMinimapWindow(metrics.scrollTop, metrics.clientHeight, metrics.scrollHeight);
|
|
635
|
+
activeIndex.value = findActiveMinimapIndex(markers.value.map((marker) => marker.ratio), windowBox.value.top, windowBox.value.height);
|
|
636
|
+
};
|
|
637
|
+
/**
|
|
638
|
+
* 有文档壳斜纹轨时,把刻度靠近轨道内侧并留出间距;否则清空偏移,沿用正文边缘定位。
|
|
639
|
+
*/
|
|
640
|
+
const updateInlineAnchor = () => {
|
|
641
|
+
const root = indicatorRoot.value;
|
|
642
|
+
if (!root) {
|
|
643
|
+
inlineAnchor.value = void 0;
|
|
644
|
+
return;
|
|
463
645
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
646
|
+
const layoutRail = root.closest(LAYOUT_SELECTOR)?.querySelector(resolvedPosition.value === "left" ? RAIL_START_SELECTOR : RAIL_END_SELECTOR);
|
|
647
|
+
const railRect = layoutRail && getComputedStyle(layoutRail).display !== "none" ? layoutRail.getBoundingClientRect() : void 0;
|
|
648
|
+
inlineAnchor.value = getIndicatorInlineOffset(root.getBoundingClientRect(), railRect, resolvedPosition.value, viewport.value?.offsetWidth || VIEWPORT_WIDTH);
|
|
649
|
+
};
|
|
650
|
+
/** 同步高度封顶、斜纹轨锚点、刻度比例和可视窗口。 */
|
|
651
|
+
const updateLayout = () => {
|
|
652
|
+
updateCappedHeight();
|
|
653
|
+
updateInlineAnchor();
|
|
654
|
+
if (props.target && markers.value.length) {
|
|
655
|
+
const article = props.target;
|
|
656
|
+
markers.value = markers.value.map((marker) => ({
|
|
657
|
+
...marker,
|
|
658
|
+
ratio: getBlockRatio(marker.element, article)
|
|
659
|
+
}));
|
|
467
660
|
}
|
|
661
|
+
updateWindow();
|
|
468
662
|
};
|
|
469
|
-
/**
|
|
470
|
-
const
|
|
471
|
-
|
|
663
|
+
/** 跟随滚动容器、正文和指示器自身的尺寸变化。 */
|
|
664
|
+
const observeHostSize = () => {
|
|
665
|
+
hostResizeObserver?.disconnect();
|
|
666
|
+
hostResizeObserver = void 0;
|
|
667
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
668
|
+
hostResizeObserver = new ResizeObserver(updateLayout);
|
|
669
|
+
const host = usesParentScroller ? parentScroller?.wrapper : scrollHost instanceof HTMLElement ? scrollHost : void 0;
|
|
670
|
+
if (host) hostResizeObserver.observe(host);
|
|
671
|
+
if (props.target) hostResizeObserver.observe(props.target);
|
|
672
|
+
if (viewport.value) hostResizeObserver.observe(viewport.value);
|
|
673
|
+
if (indicatorRoot.value) hostResizeObserver.observe(indicatorRoot.value);
|
|
674
|
+
const parent = indicatorRoot.value?.parentElement;
|
|
675
|
+
if (parent) hostResizeObserver.observe(parent);
|
|
676
|
+
const layout = indicatorRoot.value?.closest(LAYOUT_SELECTOR);
|
|
677
|
+
if (layout instanceof HTMLElement) hostResizeObserver.observe(layout);
|
|
678
|
+
const startRail = layout?.querySelector(RAIL_START_SELECTOR);
|
|
679
|
+
const endRail = layout?.querySelector(RAIL_END_SELECTOR);
|
|
680
|
+
if (startRail instanceof HTMLElement) hostResizeObserver.observe(startRail);
|
|
681
|
+
if (endRail instanceof HTMLElement) hostResizeObserver.observe(endRail);
|
|
682
|
+
}
|
|
683
|
+
updateLayout();
|
|
472
684
|
};
|
|
473
|
-
/**
|
|
685
|
+
/**
|
|
686
|
+
* 计算文档块相对正文顶部的比例,用于铺在固定高度轨道上。
|
|
687
|
+
* @param element 当前文档块。
|
|
688
|
+
* @param article 正文根节点。
|
|
689
|
+
* @returns 0–1 的纵向比例。
|
|
690
|
+
*/
|
|
691
|
+
const getBlockRatio = (element, article) => {
|
|
692
|
+
const articleHeight = article.offsetHeight || article.getBoundingClientRect().height;
|
|
693
|
+
if (articleHeight <= 0) return 0;
|
|
694
|
+
const top = element.getBoundingClientRect().top - article.getBoundingClientRect().top;
|
|
695
|
+
return Math.min(1, Math.max(0, top / articleHeight));
|
|
696
|
+
};
|
|
697
|
+
/** 从最新渲染的 Markdown DOM 重建小地图刻度。 */
|
|
474
698
|
const refreshMarkers = () => {
|
|
475
699
|
refreshFrame = 0;
|
|
476
700
|
const target = props.target;
|
|
@@ -478,67 +702,28 @@ var indicator_default = /* @__PURE__ */ defineComponent({
|
|
|
478
702
|
markers.value = [];
|
|
479
703
|
return;
|
|
480
704
|
}
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
content,
|
|
491
|
-
element,
|
|
492
|
-
id: `${index}-${element.id || element.tagName}`,
|
|
493
|
-
title: sectionTitle
|
|
494
|
-
};
|
|
495
|
-
});
|
|
705
|
+
const headings = collectMarkdownHeadings(target);
|
|
706
|
+
markers.value = headings.map((heading, index) => ({
|
|
707
|
+
content: getPreviewContent(heading.element, headings[index + 1]?.element),
|
|
708
|
+
element: heading.element,
|
|
709
|
+
id: heading.id || `${index}`,
|
|
710
|
+
isHeading: true,
|
|
711
|
+
ratio: getBlockRatio(heading.element, target),
|
|
712
|
+
title: heading.text
|
|
713
|
+
}));
|
|
496
714
|
hoverIndex.value = void 0;
|
|
497
|
-
|
|
498
|
-
followActiveMarker(activeIndex.value);
|
|
715
|
+
updateWindow();
|
|
499
716
|
};
|
|
500
717
|
/** 合并 MutationObserver 的密集通知,避免 Playground 挂载时重复扫描。 */
|
|
501
718
|
const scheduleRefresh = () => {
|
|
502
719
|
if (!refreshFrame) refreshFrame = requestAnimationFrame(refreshMarkers);
|
|
503
720
|
};
|
|
504
|
-
/**
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
if (usesParentScroller) return parentScroller?.wrapper?.clientHeight || parentScroller?.clientHeight || window.innerHeight;
|
|
511
|
-
if (scrollHost instanceof HTMLElement) return scrollHost.clientHeight;
|
|
512
|
-
return window.innerHeight;
|
|
513
|
-
};
|
|
514
|
-
/**
|
|
515
|
-
* 读取指示器可视高度:数字配置优先,否则测量已挂载节点或回退默认值。
|
|
516
|
-
* @returns 指示器高度,单位 px。
|
|
517
|
-
*/
|
|
518
|
-
const resolveIndicatorHeight = () => {
|
|
519
|
-
const height = props.options.height;
|
|
520
|
-
if (typeof height === "number" && Number.isFinite(height) && height > 0) return height;
|
|
521
|
-
const measured = viewport.value?.getBoundingClientRect().height || 0;
|
|
522
|
-
if (measured > 0) return measured;
|
|
523
|
-
return Math.min(window.innerHeight * .72, 600);
|
|
524
|
-
};
|
|
525
|
-
/** 把指示器钉在滚动容器可视区域的垂直中心。 */
|
|
526
|
-
const updateStickyInset = () => {
|
|
527
|
-
const hostHeight = getScrollHostClientHeight();
|
|
528
|
-
const indicatorHeight = resolveIndicatorHeight();
|
|
529
|
-
stickyInset.value = hostHeight > 0 ? `${Math.max(0, (hostHeight - indicatorHeight) / 2)}px` : "0px";
|
|
530
|
-
};
|
|
531
|
-
/** 跟随滚动容器和指示器自身的尺寸变化,保持垂直居中。 */
|
|
532
|
-
const observeHostSize = () => {
|
|
533
|
-
hostResizeObserver?.disconnect();
|
|
534
|
-
hostResizeObserver = void 0;
|
|
535
|
-
if (typeof ResizeObserver !== "undefined") {
|
|
536
|
-
hostResizeObserver = new ResizeObserver(updateStickyInset);
|
|
537
|
-
const host = usesParentScroller ? parentScroller?.wrapper : scrollHost instanceof HTMLElement ? scrollHost : void 0;
|
|
538
|
-
if (host) hostResizeObserver.observe(host);
|
|
539
|
-
if (viewport.value) hostResizeObserver.observe(viewport.value);
|
|
540
|
-
}
|
|
541
|
-
updateStickyInset();
|
|
721
|
+
/** 将连续滚动事件合并到下一帧:更新可视窗口,并按父级底部收缩高度。 */
|
|
722
|
+
const handleScroll = () => {
|
|
723
|
+
if (!windowFrame) windowFrame = requestAnimationFrame(() => {
|
|
724
|
+
updateCappedHeight();
|
|
725
|
+
updateWindow();
|
|
726
|
+
});
|
|
542
727
|
};
|
|
543
728
|
/** 解除旧文档的观察和滚动监听。 */
|
|
544
729
|
const cleanupTarget = () => {
|
|
@@ -546,7 +731,7 @@ var indicator_default = /* @__PURE__ */ defineComponent({
|
|
|
546
731
|
observer = void 0;
|
|
547
732
|
hostResizeObserver?.disconnect();
|
|
548
733
|
hostResizeObserver = void 0;
|
|
549
|
-
window.removeEventListener("resize",
|
|
734
|
+
window.removeEventListener("resize", updateLayout);
|
|
550
735
|
if (usesParentScroller) parentScroller?.off?.(handleScroll);
|
|
551
736
|
else if (scrollHost) scrollHost.removeEventListener("scroll", handleScroll);
|
|
552
737
|
usesParentScroller = false;
|
|
@@ -570,7 +755,7 @@ var indicator_default = /* @__PURE__ */ defineComponent({
|
|
|
570
755
|
scrollHost = getScrollHost();
|
|
571
756
|
scrollHost.addEventListener("scroll", handleScroll, { passive: true });
|
|
572
757
|
}
|
|
573
|
-
window.addEventListener("resize",
|
|
758
|
+
window.addEventListener("resize", updateLayout, { passive: true });
|
|
574
759
|
observer = new MutationObserver(scheduleRefresh);
|
|
575
760
|
observer.observe(props.target, {
|
|
576
761
|
childList: true,
|
|
@@ -581,18 +766,16 @@ var indicator_default = /* @__PURE__ */ defineComponent({
|
|
|
581
766
|
observeHostSize();
|
|
582
767
|
};
|
|
583
768
|
/**
|
|
584
|
-
*
|
|
585
|
-
* @param
|
|
586
|
-
* @returns
|
|
769
|
+
* 把轨道上的指针位置映射到最近的刻度,只用于悬停预览。
|
|
770
|
+
* @param ratio 轨道 0–1 比例。
|
|
771
|
+
* @returns 最近刻度序号。
|
|
587
772
|
*/
|
|
588
|
-
const
|
|
589
|
-
if (!
|
|
590
|
-
const elements = [...rail.value.querySelectorAll(".docs-markdown-indicator__marker")];
|
|
773
|
+
const getIndexByRatio = (ratio) => {
|
|
774
|
+
if (!markers.value.length) return 0;
|
|
591
775
|
let closestIndex = 0;
|
|
592
776
|
let closestDistance = Number.POSITIVE_INFINITY;
|
|
593
|
-
|
|
594
|
-
const
|
|
595
|
-
const distance = Math.abs(event.clientY - (bounds.top + bounds.height / 2));
|
|
777
|
+
markers.value.forEach((marker, index) => {
|
|
778
|
+
const distance = Math.abs(marker.ratio - ratio);
|
|
596
779
|
if (distance < closestDistance) {
|
|
597
780
|
closestIndex = index;
|
|
598
781
|
closestDistance = distance;
|
|
@@ -601,66 +784,73 @@ var indicator_default = /* @__PURE__ */ defineComponent({
|
|
|
601
784
|
return closestIndex;
|
|
602
785
|
};
|
|
603
786
|
/**
|
|
604
|
-
*
|
|
605
|
-
* @param
|
|
787
|
+
* 读取指针相对轨道的比例。
|
|
788
|
+
* @param event 当前指针事件。
|
|
789
|
+
* @returns 0–1 比例。
|
|
790
|
+
*/
|
|
791
|
+
const getPointerRatio = (event) => {
|
|
792
|
+
const bounds = rail.value?.getBoundingClientRect();
|
|
793
|
+
if (!bounds) return 0;
|
|
794
|
+
return getMinimapPointerRatio(event.clientY, bounds.top, bounds.height);
|
|
795
|
+
};
|
|
796
|
+
/**
|
|
797
|
+
* 让摘要跟随指针,同时限制在指示器可视高度内。
|
|
606
798
|
* @param clientY 指针的视口纵坐标。
|
|
607
799
|
*/
|
|
608
|
-
const updatePreviewPosition = (
|
|
800
|
+
const updatePreviewPosition = (clientY) => {
|
|
609
801
|
const rootBounds = indicatorRoot.value?.getBoundingClientRect();
|
|
610
802
|
const viewportBounds = viewport.value?.getBoundingClientRect();
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const markerBounds = marker.getBoundingClientRect();
|
|
614
|
-
const relativeY = (clientY ?? markerBounds.top + markerBounds.height / 2) - rootBounds.top;
|
|
615
|
-
previewTop.value = Math.min(viewportBounds.height - 48, Math.max(48, relativeY));
|
|
803
|
+
if (!rootBounds || !viewportBounds) return;
|
|
804
|
+
previewTop.value = Math.min(viewportBounds.height - 48, Math.max(48, clientY - rootBounds.top));
|
|
616
805
|
};
|
|
617
806
|
/**
|
|
618
|
-
*
|
|
619
|
-
* @param
|
|
807
|
+
* 按轨道比例滚动正文,不改 hash。
|
|
808
|
+
* @param ratio 轨道 0–1 比例。
|
|
620
809
|
* @param behavior 滚动行为。
|
|
621
810
|
*/
|
|
622
|
-
const
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
811
|
+
const scrollToRatio = (ratio, behavior) => {
|
|
812
|
+
const metrics = getScrollMetrics();
|
|
813
|
+
const nextTop = ratio * Math.max(0, metrics.scrollHeight - metrics.clientHeight);
|
|
814
|
+
if (usesParentScroller && parentScroller?.setScrollTop) {
|
|
815
|
+
parentScroller.setScrollTop(nextTop);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if (scrollHost instanceof HTMLElement) {
|
|
629
819
|
scrollHost.scrollTo({
|
|
630
|
-
|
|
631
|
-
|
|
820
|
+
behavior,
|
|
821
|
+
top: nextTop
|
|
632
822
|
});
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
window.scrollTo({
|
|
826
|
+
behavior,
|
|
827
|
+
top: nextTop
|
|
636
828
|
});
|
|
637
|
-
activeIndex.value = index;
|
|
638
|
-
followActiveMarker(index);
|
|
639
829
|
};
|
|
640
830
|
/**
|
|
641
|
-
*
|
|
831
|
+
* 指针移动时展示摘要;按下拖动时按比例 scrub 正文。
|
|
642
832
|
* @param event 当前指针事件。
|
|
643
833
|
*/
|
|
644
834
|
const handlePointerMove = (event) => {
|
|
645
|
-
const
|
|
646
|
-
hoverIndex.value =
|
|
647
|
-
updatePreviewPosition(
|
|
648
|
-
if (dragging.value)
|
|
835
|
+
const ratio = getPointerRatio(event);
|
|
836
|
+
hoverIndex.value = getIndexByRatio(ratio);
|
|
837
|
+
updatePreviewPosition(event.clientY);
|
|
838
|
+
if (dragging.value) scrollToRatio(ratio, "auto");
|
|
649
839
|
};
|
|
650
840
|
/**
|
|
651
|
-
*
|
|
841
|
+
* 开始 scrub:点击即按比例定位,可拖动时再捕获指针。
|
|
652
842
|
* @param event 当前指针事件。
|
|
653
843
|
*/
|
|
654
844
|
const handlePointerDown = (event) => {
|
|
655
845
|
if (event.button !== 0) return;
|
|
846
|
+
const ratio = getPointerRatio(event);
|
|
847
|
+
hoverIndex.value = getIndexByRatio(ratio);
|
|
848
|
+
updatePreviewPosition(event.clientY);
|
|
849
|
+
scrollToRatio(ratio, "auto");
|
|
656
850
|
dragging.value = props.options.draggable !== false;
|
|
657
|
-
const index = getIndexByPointer(event);
|
|
658
|
-
hoverIndex.value = index;
|
|
659
|
-
updatePreviewPosition(index, event.clientY);
|
|
660
851
|
if (dragging.value) {
|
|
661
852
|
captureTarget = event.currentTarget;
|
|
662
853
|
captureTarget.setPointerCapture?.(event.pointerId);
|
|
663
|
-
scrollToMarker(index, "auto");
|
|
664
854
|
event.preventDefault();
|
|
665
855
|
}
|
|
666
856
|
};
|
|
@@ -678,96 +868,99 @@ var indicator_default = /* @__PURE__ */ defineComponent({
|
|
|
678
868
|
if (!dragging.value) hoverIndex.value = void 0;
|
|
679
869
|
};
|
|
680
870
|
/**
|
|
681
|
-
*
|
|
682
|
-
* @param index 目标文档块序号。
|
|
683
|
-
*/
|
|
684
|
-
const handleClick = (index) => {
|
|
685
|
-
hoverIndex.value = index;
|
|
686
|
-
updatePreviewPosition(index);
|
|
687
|
-
scrollToMarker(index, "smooth");
|
|
688
|
-
};
|
|
689
|
-
/**
|
|
690
|
-
* 计算刻度位置、基础宽度和靠近指针时的鱼眼展开宽度。
|
|
871
|
+
* 计算刻度在轨道上的位置、标题/正文宽度,以及靠近指针时的鱼眼展开。
|
|
691
872
|
* @param index 当前刻度序号。
|
|
692
873
|
* @returns 刻度的定位和宽度样式。
|
|
693
874
|
*/
|
|
694
875
|
const getMarkerStyle = (index) => {
|
|
876
|
+
const marker = markers.value[index];
|
|
695
877
|
const distance = typeof hoverIndex.value === "number" ? Math.abs(index - hoverIndex.value) : Number.POSITIVE_INFINITY;
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
878
|
+
const base = marker?.isHeading ? 10 : 6;
|
|
879
|
+
const expandedWidths = [
|
|
880
|
+
base + 6,
|
|
881
|
+
base + 4,
|
|
882
|
+
base + 2,
|
|
883
|
+
base + 1
|
|
884
|
+
];
|
|
885
|
+
const hoverWidth = distance <= 3 ? expandedWidths[distance] : base;
|
|
886
|
+
const activeWidth = index === activeIndex.value ? base + 8 : base;
|
|
887
|
+
return {
|
|
888
|
+
top: `${(marker?.ratio || 0) * 100}%`,
|
|
889
|
+
width: `${Math.max(hoverWidth, activeWidth)}px`
|
|
890
|
+
};
|
|
702
891
|
};
|
|
703
|
-
watch([() => props.target, localeName], setupTarget, {
|
|
704
|
-
|
|
892
|
+
watch([() => props.target, localeName], setupTarget, {
|
|
893
|
+
immediate: true,
|
|
894
|
+
flush: "post"
|
|
895
|
+
});
|
|
896
|
+
watch(indicatorRoot, (root) => {
|
|
897
|
+
if (root) observeHostSize();
|
|
898
|
+
}, { flush: "post" });
|
|
899
|
+
watch([
|
|
900
|
+
() => props.options.height,
|
|
901
|
+
() => props.options.position,
|
|
902
|
+
viewport
|
|
903
|
+
], () => {
|
|
705
904
|
if (viewport.value && hostResizeObserver) hostResizeObserver.observe(viewport.value);
|
|
706
|
-
|
|
905
|
+
updateLayout();
|
|
707
906
|
});
|
|
708
907
|
onBeforeUnmount(() => {
|
|
709
908
|
targetGeneration++;
|
|
710
909
|
cleanupTarget();
|
|
711
910
|
if (refreshFrame) cancelAnimationFrame(refreshFrame);
|
|
712
|
-
if (
|
|
911
|
+
if (windowFrame) cancelAnimationFrame(windowFrame);
|
|
713
912
|
});
|
|
714
913
|
return (_ctx, _cache) => {
|
|
715
|
-
return markers.value.length
|
|
914
|
+
return markers.value.length ? (openBlock(), createElementBlock("aside", {
|
|
716
915
|
key: 0,
|
|
717
916
|
ref_key: "indicatorRoot",
|
|
718
917
|
ref: indicatorRoot,
|
|
719
|
-
class: normalizeClass(["docs-markdown-indicator", `is-${
|
|
918
|
+
class: normalizeClass(["docs-markdown-indicator", `is-${resolvedPosition.value}`]),
|
|
720
919
|
style: normalizeStyle(rootStyle.value),
|
|
721
920
|
"aria-label": unref(t)("markdown.indicator.label")
|
|
722
921
|
}, [createElementVNode("div", {
|
|
723
922
|
ref_key: "viewport",
|
|
724
923
|
ref: viewport,
|
|
725
|
-
class: "docs-markdown-indicator__viewport",
|
|
924
|
+
class: normalizeClass(["docs-markdown-indicator__viewport", { "is-dragging": dragging.value }]),
|
|
925
|
+
role: "slider",
|
|
926
|
+
"aria-orientation": "vertical",
|
|
927
|
+
"aria-valuemin": 0,
|
|
928
|
+
"aria-valuemax": 100,
|
|
929
|
+
"aria-valuenow": scrollProgress.value,
|
|
930
|
+
"aria-valuetext": `${scrollProgress.value}%`,
|
|
931
|
+
style: normalizeStyle(viewportStyle.value),
|
|
726
932
|
onPointermove: handlePointerMove,
|
|
727
933
|
onPointerleave: handlePointerLeave,
|
|
728
934
|
onPointerdown: handlePointerDown,
|
|
729
935
|
onPointerup: handlePointerUp,
|
|
730
936
|
onPointercancel: handlePointerUp
|
|
731
|
-
}, [
|
|
732
|
-
ref_key: "
|
|
733
|
-
ref:
|
|
734
|
-
class: "docs-markdown-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
"
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
"is-active": index === activeIndex.value,
|
|
751
|
-
"is-hovered": index === hoverIndex.value
|
|
752
|
-
}]),
|
|
753
|
-
style: normalizeStyle(getMarkerStyle(index)),
|
|
754
|
-
"aria-label": marker.ariaLabel,
|
|
755
|
-
onClick: ($event) => handleClick(index)
|
|
756
|
-
}, null, 14, _hoisted_3);
|
|
757
|
-
}), 128))])], 512)]),
|
|
758
|
-
_: 1
|
|
759
|
-
}, 512)], 544), __props.options.preview !== false && hoveredMarker.value ? (openBlock(), createElementBlock("div", {
|
|
937
|
+
}, [createElementVNode("div", {
|
|
938
|
+
ref_key: "rail",
|
|
939
|
+
ref: rail,
|
|
940
|
+
class: "docs-markdown-indicator__rail"
|
|
941
|
+
}, [createElementVNode("div", {
|
|
942
|
+
class: "docs-markdown-indicator__window",
|
|
943
|
+
style: normalizeStyle(windowStyle.value)
|
|
944
|
+
}, null, 4), (openBlock(true), createElementBlock(Fragment, null, renderList(markers.value, (marker, index) => {
|
|
945
|
+
return openBlock(), createElementBlock("div", {
|
|
946
|
+
key: marker.id,
|
|
947
|
+
class: normalizeClass(["docs-markdown-indicator__marker", {
|
|
948
|
+
"is-heading": marker.isHeading,
|
|
949
|
+
"is-hovered": index === hoverIndex.value,
|
|
950
|
+
"is-active": index === activeIndex.value,
|
|
951
|
+
"is-in-view": isMarkerInView(index)
|
|
952
|
+
}]),
|
|
953
|
+
style: normalizeStyle(getMarkerStyle(index))
|
|
954
|
+
}, null, 6);
|
|
955
|
+
}), 128))], 512)], 46, _hoisted_2), __props.options.preview !== false && !dragging.value && hoveredMarker.value ? (openBlock(), createElementBlock("div", {
|
|
760
956
|
key: 0,
|
|
761
957
|
class: "docs-markdown-indicator__preview",
|
|
762
958
|
style: normalizeStyle(previewStyle.value)
|
|
763
|
-
}, [createElementVNode("div",
|
|
959
|
+
}, [createElementVNode("div", _hoisted_3, toDisplayString(hoveredMarker.value.title), 1), hoveredMarker.value.content ? (openBlock(), createElementBlock("div", _hoisted_4, toDisplayString(hoveredMarker.value.content), 1)) : createCommentVNode("v-if", true)], 4)) : createCommentVNode("v-if", true)], 14, _hoisted_1)) : createCommentVNode("v-if", true);
|
|
764
960
|
};
|
|
765
961
|
}
|
|
766
962
|
});
|
|
767
963
|
//#endregion
|
|
768
|
-
//#region packages/markdown/src/wrapper.vue?vue&type=script&setup=true&lang.ts
|
|
769
|
-
var _hoisted_1 = { class: "docs-markdown" };
|
|
770
|
-
//#endregion
|
|
771
964
|
//#region packages/markdown/src/wrapper.vue
|
|
772
965
|
var wrapper_default = /* @__PURE__ */ defineComponent({
|
|
773
966
|
__name: "wrapper",
|
|
@@ -777,6 +970,8 @@ var wrapper_default = /* @__PURE__ */ defineComponent({
|
|
|
777
970
|
default: true
|
|
778
971
|
},
|
|
779
972
|
locale: {},
|
|
973
|
+
theme: { default: "default" },
|
|
974
|
+
playground: { default: () => ({}) },
|
|
780
975
|
modelValue: {},
|
|
781
976
|
value: {}
|
|
782
977
|
},
|
|
@@ -791,13 +986,15 @@ var wrapper_default = /* @__PURE__ */ defineComponent({
|
|
|
791
986
|
if (props.indicator === false) return void 0;
|
|
792
987
|
return typeof props.indicator === "object" ? props.indicator : {};
|
|
793
988
|
});
|
|
989
|
+
const resolvedTheme = computed(() => props.theme === "traditional" ? "traditional" : "default");
|
|
794
990
|
const source = computed(() => typeof props.modelValue === "string" ? props.modelValue : props.value);
|
|
795
991
|
const markdownBinding = computed(() => ({
|
|
796
992
|
source: source.value,
|
|
797
|
-
locale: locale.value
|
|
993
|
+
locale: locale.value,
|
|
994
|
+
playground: props.playground
|
|
798
995
|
}));
|
|
799
996
|
return (_ctx, _cache) => {
|
|
800
|
-
return openBlock(), createElementBlock("div",
|
|
997
|
+
return openBlock(), createElementBlock("div", { class: normalizeClass(["docs-markdown", `docs-markdown--${resolvedTheme.value}`]) }, [indicatorOptions.value && content.value ? (openBlock(), createBlock(indicator_default, {
|
|
801
998
|
key: 0,
|
|
802
999
|
target: content.value,
|
|
803
1000
|
options: indicatorOptions.value
|
|
@@ -805,9 +1002,9 @@ var wrapper_default = /* @__PURE__ */ defineComponent({
|
|
|
805
1002
|
ref_key: "content",
|
|
806
1003
|
ref: content,
|
|
807
1004
|
class: "docs-markdown-reset"
|
|
808
|
-
}, null, 512), [[unref(vMarkdown), markdownBinding.value]])]);
|
|
1005
|
+
}, null, 512), [[unref(vMarkdown), markdownBinding.value]])], 2);
|
|
809
1006
|
};
|
|
810
1007
|
}
|
|
811
1008
|
});
|
|
812
1009
|
//#endregion
|
|
813
|
-
export { wrapper_default as Markdown, parseMarkdownSearchSections };
|
|
1010
|
+
export { wrapper_default as Markdown, collectMarkdownHeadings, getMarkdownHeadingTitle, parseMarkdownSearchSections };
|