@deot/docs-markdown 1.0.10 → 1.1.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 +50 -10
- package/dist/index.cjs +770 -316
- package/dist/index.d.ts +35 -2
- package/dist/index.js +767 -318
- package/dist/index.style.css +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -34,30 +34,87 @@ markdown_it = __toESM(markdown_it);
|
|
|
34
34
|
let json5 = require("json5");
|
|
35
35
|
json5 = __toESM(json5);
|
|
36
36
|
let _deot_helper_dom = require("@deot/helper-dom");
|
|
37
|
-
let _deot_vc = require("@deot/vc");
|
|
38
37
|
//#region packages/markdown/src/markdown.ts
|
|
39
38
|
var HTML_MD_SIGN = "md";
|
|
40
39
|
var PLAYGROUND = "playground";
|
|
40
|
+
var TABS = "tabs";
|
|
41
41
|
var TIP = "tip";
|
|
42
42
|
var WARNING = "warning";
|
|
43
|
+
var CALLOUT_FILLED_RE = /(^|\s)filled(\s|$)/;
|
|
44
|
+
var TAB_MARKDOWN_INFO_RE = /^markdown(?:\s+(.*))?$/i;
|
|
45
|
+
var CALLOUT_INFO_ICON = [
|
|
46
|
+
"<svg width=\"22\" height=\"28\" viewBox=\"0 0 22 28\" fill=\"none\" aria-hidden=\"true\">",
|
|
47
|
+
"<circle class=\"docs-markdown-callout-icon__fill\" cx=\"11\" cy=\"14\" r=\"11\"></circle>",
|
|
48
|
+
"<circle class=\"docs-markdown-callout-icon__ring\" cx=\"11\" cy=\"14\" r=\"10.5\"></circle>",
|
|
49
|
+
"<path class=\"docs-markdown-callout-icon__mark\"",
|
|
50
|
+
" 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\"",
|
|
51
|
+
" stroke-width=\"1.5\" stroke-linecap=\"round\"></path>",
|
|
52
|
+
"<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>",
|
|
53
|
+
"</svg>"
|
|
54
|
+
].join("");
|
|
55
|
+
var CALLOUT_WARNING_ICON = [
|
|
56
|
+
"<svg width=\"22\" height=\"28\" viewBox=\"0 0 22 28\" fill=\"none\" aria-hidden=\"true\">",
|
|
57
|
+
"<circle class=\"docs-markdown-callout-icon__fill\" cx=\"11\" cy=\"14\" r=\"11\"></circle>",
|
|
58
|
+
"<circle class=\"docs-markdown-callout-icon__ring\" cx=\"11\" cy=\"14\" r=\"10.5\"></circle>",
|
|
59
|
+
"<path class=\"docs-markdown-callout-icon__mark\" d=\"M11 10v5.5\" stroke-width=\"1.5\" stroke-linecap=\"round\"></path>",
|
|
60
|
+
"<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>",
|
|
61
|
+
"</svg>"
|
|
62
|
+
].join("");
|
|
63
|
+
var renderCallout = (name, icon) => (tokens, idx) => {
|
|
64
|
+
if (tokens[idx].nesting === 1) return [
|
|
65
|
+
`<div class="${CALLOUT_FILLED_RE.test(tokens[idx].info) ? `${name} ${name}--filled` : name}" md>`,
|
|
66
|
+
"<span class=\"docs-markdown-callout-icon\" aria-hidden=\"true\">",
|
|
67
|
+
icon,
|
|
68
|
+
"</span><div class=\"docs-markdown-callout-body\">\n"
|
|
69
|
+
].join("");
|
|
70
|
+
return "</div></div>\n";
|
|
71
|
+
};
|
|
43
72
|
var config = new markdown_it_chain.default();
|
|
44
73
|
config.options.html(true).typographer(true).linkify(true).end().plugin("anchor").use(markdown_it_anchor.default, [{ permalink: markdown_it_anchor.default.permalink.linkInsideHeader({
|
|
45
74
|
placement: "before",
|
|
46
75
|
symbol: "#"
|
|
47
76
|
}) }]).end().plugin("container").use(($md) => {
|
|
48
|
-
const
|
|
77
|
+
const playgroundReg = new RegExp(`^${PLAYGROUND}\\s*(.*)$`);
|
|
49
78
|
$md.use(markdown_it_container.default, PLAYGROUND, { validate(params) {
|
|
50
|
-
return params.trim().match(
|
|
79
|
+
return params.trim().match(playgroundReg);
|
|
80
|
+
} });
|
|
81
|
+
const tabsReg = new RegExp(`^${TABS}\\s*$`);
|
|
82
|
+
$md.use(markdown_it_container.default, TABS, { validate(params) {
|
|
83
|
+
return tabsReg.test(params.trim());
|
|
51
84
|
} });
|
|
52
|
-
$md.use(markdown_it_container.default, TIP);
|
|
53
|
-
$md.use(markdown_it_container.default, WARNING);
|
|
85
|
+
$md.use(markdown_it_container.default, TIP, { render: renderCallout(TIP, CALLOUT_INFO_ICON) });
|
|
86
|
+
$md.use(markdown_it_container.default, WARNING, { render: renderCallout(WARNING, CALLOUT_WARNING_ICON) });
|
|
54
87
|
}).end();
|
|
55
88
|
var md = config.toMd(markdown_it.default);
|
|
56
89
|
var playgroundOpen = `container_${PLAYGROUND}_open`;
|
|
57
90
|
var playgroundClose = `container_${PLAYGROUND}_close`;
|
|
91
|
+
var tabsOpen = `container_${TABS}_open`;
|
|
92
|
+
var tabsClose = `container_${TABS}_close`;
|
|
58
93
|
var htmlCommentRE = /<!--([\s\S]*?)-->/g;
|
|
59
94
|
var runtimeConfigRE = /<config\s+lang\s*=\s*["']json5["']\s*>([\s\S]*?)<\/config>/i;
|
|
60
95
|
var renderPlaygroundError = (message) => `<div class="docs-playground-error">PLAYGROUND: ${md.utils.escapeHtml(message)}</div>\n`;
|
|
96
|
+
var renderTabsError = (message) => `<div class="docs-markdown-tabs-error">TABS: ${md.utils.escapeHtml(message)}</div>\n`;
|
|
97
|
+
/**
|
|
98
|
+
* 从 fence info 解析 Tab 显示名:去掉可选的 `markdown` 语言前缀。
|
|
99
|
+
* @param info fence 的 info 字符串,如 `markdown Linux` 或 `Android`。
|
|
100
|
+
* @returns 显示标题(可能为空)。
|
|
101
|
+
*/
|
|
102
|
+
var parseTabFenceTitle = (info) => {
|
|
103
|
+
const trimmed = info.trim();
|
|
104
|
+
const match = trimmed.match(TAB_MARKDOWN_INFO_RE);
|
|
105
|
+
if (match) return (match[1] || "").trim();
|
|
106
|
+
return trimmed;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* 生成 `?tab=` 使用的 id:小写、空白变 `-`;含非 ASCII 时与标题锚点一致做 encodeURIComponent。
|
|
110
|
+
* @param title Tab 显示名。
|
|
111
|
+
* @returns 可写入 query 的 id;标题为空时返回空串。
|
|
112
|
+
*/
|
|
113
|
+
var toMarkdownTabId = (title) => {
|
|
114
|
+
const normalized = title.trim().toLowerCase().replace(/\s+/gu, "-");
|
|
115
|
+
if (!normalized) return "";
|
|
116
|
+
return /[^\x20-\x7E]/u.test(normalized) ? encodeURIComponent(normalized) : normalized;
|
|
117
|
+
};
|
|
61
118
|
var PLAYGROUND_VIEWS = ["runtime", "files"];
|
|
62
119
|
var isPlaygroundView = (value) => typeof value === "string" && PLAYGROUND_VIEWS.includes(value);
|
|
63
120
|
var isRuntimeViewport = (viewport) => {
|
|
@@ -97,12 +154,22 @@ var validatePreviewInset = (propsData) => {
|
|
|
97
154
|
if (Array.isArray(inset) && inset.length === 2 && inset.every(isPreviewInsetValue)) return "";
|
|
98
155
|
return "previewInset 必须是非负数或 [垂直,水平] 非负数数组";
|
|
99
156
|
};
|
|
100
|
-
var
|
|
101
|
-
if (!("
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
104
|
-
if (typeof
|
|
105
|
-
return "
|
|
157
|
+
var validateExpandable = (propsData) => {
|
|
158
|
+
if (!("expandable" in propsData)) return "";
|
|
159
|
+
const expandable = propsData.expandable;
|
|
160
|
+
if (expandable === true) return "";
|
|
161
|
+
if (typeof expandable === "number" && Number.isFinite(expandable) && expandable > 0) return "";
|
|
162
|
+
return "expandable 必须是 true 或正数";
|
|
163
|
+
};
|
|
164
|
+
var validateTitle = (propsData) => {
|
|
165
|
+
if (!("title" in propsData)) return "";
|
|
166
|
+
if (typeof propsData.title === "string") return "";
|
|
167
|
+
return "title 必须是字符串";
|
|
168
|
+
};
|
|
169
|
+
var validateTitleId = (propsData) => {
|
|
170
|
+
if (!("id" in propsData)) return "";
|
|
171
|
+
if (typeof propsData.id === "string") return "";
|
|
172
|
+
return "id 必须是字符串";
|
|
106
173
|
};
|
|
107
174
|
var parseRuntimeProps = (tokens) => {
|
|
108
175
|
for (const token of tokens) {
|
|
@@ -125,7 +192,61 @@ var parseRuntimeProps = (tokens) => {
|
|
|
125
192
|
return {};
|
|
126
193
|
};
|
|
127
194
|
var renderPlaygroundAttrs = (propsData) => `data-props="${md.utils.escapeHtml(JSON.stringify(propsData))}"`;
|
|
128
|
-
|
|
195
|
+
var renderTabsHtml = (panels) => {
|
|
196
|
+
const parts = ["<div class=\"docs-markdown-tabs\" md data-tabs>", "<div class=\"docs-markdown-tabs__nav\" data-tabs-nav></div>"];
|
|
197
|
+
panels.forEach((panel, index) => {
|
|
198
|
+
const hidden = index === 0 ? "" : " hidden";
|
|
199
|
+
const id = md.utils.escapeHtml(panel.id);
|
|
200
|
+
const title = md.utils.escapeHtml(panel.title);
|
|
201
|
+
parts.push(`<div class="docs-markdown-tabs__panel" role="tabpanel" data-tab="${id}" data-tab-title="${title}"${hidden}>`, md.render(panel.content), "</div>");
|
|
202
|
+
});
|
|
203
|
+
parts.push("</div>\n");
|
|
204
|
+
return parts.join("");
|
|
205
|
+
};
|
|
206
|
+
md.core.ruler.after("block", "markdown-tabs", (state) => {
|
|
207
|
+
const panelSources = Array.isArray(state.env?.tabsPanelSources) ? state.env.tabsPanelSources : void 0;
|
|
208
|
+
for (let index = 0; index < state.tokens.length; index++) {
|
|
209
|
+
if (state.tokens[index].type !== tabsOpen) continue;
|
|
210
|
+
let depth = 1;
|
|
211
|
+
let closeIndex = index + 1;
|
|
212
|
+
for (; closeIndex < state.tokens.length; closeIndex++) {
|
|
213
|
+
if (state.tokens[closeIndex].type === tabsOpen) depth++;
|
|
214
|
+
if (state.tokens[closeIndex].type === tabsClose) depth--;
|
|
215
|
+
if (depth === 0) break;
|
|
216
|
+
}
|
|
217
|
+
const fences = state.tokens.slice(index + 1, closeIndex).filter((token) => token.type === "fence");
|
|
218
|
+
const placeholder = new state.Token("html_block", "", 0);
|
|
219
|
+
placeholder.block = true;
|
|
220
|
+
if (!fences.length) placeholder.content = renderTabsError("至少需要声明一个分栏");
|
|
221
|
+
else {
|
|
222
|
+
const panels = [];
|
|
223
|
+
const seen = /* @__PURE__ */ new Set();
|
|
224
|
+
let error = "";
|
|
225
|
+
for (const fence of fences) {
|
|
226
|
+
const title = parseTabFenceTitle(fence.info);
|
|
227
|
+
const id = toMarkdownTabId(title);
|
|
228
|
+
if (!title || !id) {
|
|
229
|
+
error = "每个分栏都必须声明标题";
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
if (seen.has(id)) {
|
|
233
|
+
error = `分栏 ${title} 的 id 重复`;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
seen.add(id);
|
|
237
|
+
panels.push({
|
|
238
|
+
id,
|
|
239
|
+
title,
|
|
240
|
+
content: fence.content
|
|
241
|
+
});
|
|
242
|
+
panelSources?.push(fence.content);
|
|
243
|
+
}
|
|
244
|
+
placeholder.content = error ? renderTabsError(error) : renderTabsHtml(panels);
|
|
245
|
+
}
|
|
246
|
+
state.tokens.splice(index, closeIndex - index + 1, placeholder);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
md.core.ruler.after("markdown-tabs", "runtime-files", (state) => {
|
|
129
250
|
for (let index = 0; index < state.tokens.length; index++) {
|
|
130
251
|
if (state.tokens[index].type !== playgroundOpen) continue;
|
|
131
252
|
let depth = 1;
|
|
@@ -140,7 +261,7 @@ md.core.ruler.after("block", "runtime-files", (state) => {
|
|
|
140
261
|
const placeholder = new state.Token("html_block", "", 0);
|
|
141
262
|
placeholder.block = true;
|
|
142
263
|
const propsData = parseRuntimeProps(innerTokens);
|
|
143
|
-
const propsError = validateRuntimeViews(propsData) || validateRuntimeViewport(propsData) || validatePreviewInset(propsData) ||
|
|
264
|
+
const propsError = validateRuntimeViews(propsData) || validateRuntimeViewport(propsData) || validatePreviewInset(propsData) || validateExpandable(propsData) || validateTitle(propsData) || validateTitleId(propsData);
|
|
144
265
|
const propsAttr = renderPlaygroundAttrs(propsData);
|
|
145
266
|
if (!fences.length) placeholder.content = renderPlaygroundError("至少需要声明一个代码文件");
|
|
146
267
|
else if (propsError) placeholder.content = renderPlaygroundError(propsError);
|
|
@@ -178,7 +299,7 @@ md.core.ruler.after("block", "runtime-files", (state) => {
|
|
|
178
299
|
});
|
|
179
300
|
var renderAttrs = md.renderer.renderAttrs;
|
|
180
301
|
md.renderer.renderAttrs = (token) => {
|
|
181
|
-
const reg = new RegExp(`container_${PLAYGROUND}|fence|text`);
|
|
302
|
+
const reg = new RegExp(`container_${PLAYGROUND}|container_${TABS}|fence|text`);
|
|
182
303
|
if (token.nesting !== -1 && token.type && !reg.test(token.type)) token.attrPush([HTML_MD_SIGN, ""]);
|
|
183
304
|
return renderAttrs(token);
|
|
184
305
|
};
|
|
@@ -206,32 +327,39 @@ var getInlineSearchText = (token) => {
|
|
|
206
327
|
* @returns 文档标题、正文和按标题划分的小节。
|
|
207
328
|
*/
|
|
208
329
|
var parseMarkdownSearchSections = (content) => {
|
|
209
|
-
const tokens = md.parse(content, {});
|
|
210
330
|
const sections = [];
|
|
211
331
|
const documentParts = [];
|
|
212
332
|
let activeSection;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (token.type === "heading_open") {
|
|
216
|
-
const inline = tokens[index + 1];
|
|
217
|
-
if (inline?.type !== "inline") continue;
|
|
218
|
-
const title = getInlineSearchText(inline);
|
|
219
|
-
if (!title) continue;
|
|
220
|
-
activeSection = {
|
|
221
|
-
title,
|
|
222
|
-
anchor: token.attrGet("id") || "",
|
|
223
|
-
level: Number(token.tag.slice(1)) || 1,
|
|
224
|
-
text: ""
|
|
225
|
-
};
|
|
226
|
-
sections.push(activeSection);
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
if (token.type !== "inline" || tokens[index - 1]?.type === "heading_open") continue;
|
|
230
|
-
const text = getInlineSearchText(token);
|
|
231
|
-
if (!text) continue;
|
|
333
|
+
const consumeInline = (text) => {
|
|
334
|
+
if (!text) return;
|
|
232
335
|
documentParts.push(text);
|
|
233
336
|
if (activeSection) activeSection.text = normalizeSearchText(`${activeSection.text} ${text}`);
|
|
234
|
-
}
|
|
337
|
+
};
|
|
338
|
+
const walk = (source, withHeadings) => {
|
|
339
|
+
const tabsPanelSources = [];
|
|
340
|
+
const tokens = md.parse(source, { tabsPanelSources });
|
|
341
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
342
|
+
const token = tokens[index];
|
|
343
|
+
if (withHeadings && token.type === "heading_open") {
|
|
344
|
+
const inline = tokens[index + 1];
|
|
345
|
+
if (inline?.type !== "inline") continue;
|
|
346
|
+
const title = getInlineSearchText(inline);
|
|
347
|
+
if (!title) continue;
|
|
348
|
+
activeSection = {
|
|
349
|
+
title,
|
|
350
|
+
anchor: token.attrGet("id") || "",
|
|
351
|
+
level: Number(token.tag.slice(1)) || 1,
|
|
352
|
+
text: ""
|
|
353
|
+
};
|
|
354
|
+
sections.push(activeSection);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (token.type !== "inline" || tokens[index - 1]?.type === "heading_open") continue;
|
|
358
|
+
consumeInline(getInlineSearchText(token));
|
|
359
|
+
}
|
|
360
|
+
for (const panelSource of tabsPanelSources) walk(panelSource, false);
|
|
361
|
+
};
|
|
362
|
+
walk(content, true);
|
|
235
363
|
return {
|
|
236
364
|
title: sections.find((section) => section.level === 1)?.title || sections[0]?.title || "",
|
|
237
365
|
text: normalizeSearchText(documentParts.join(" ")),
|
|
@@ -239,11 +367,137 @@ var parseMarkdownSearchSections = (content) => {
|
|
|
239
367
|
};
|
|
240
368
|
};
|
|
241
369
|
//#endregion
|
|
370
|
+
//#region packages/markdown/src/tab-query.ts
|
|
371
|
+
var markdownTabQueryKey = Symbol("docs-markdown-tab-query");
|
|
372
|
+
var TAB_QUERY = "tab";
|
|
373
|
+
/**
|
|
374
|
+
* 不依赖 Vue Router 的默认适配器:读写 `location.search` 的 `tab`,
|
|
375
|
+
* 并用 `history.replaceState` 更新(供 examples / 单测使用)。
|
|
376
|
+
* @returns 可注入 TabsNav 的 get/set/subscribe 适配器。
|
|
377
|
+
*/
|
|
378
|
+
var createHistoryTabQueryAdapter = () => {
|
|
379
|
+
const read = () => {
|
|
380
|
+
try {
|
|
381
|
+
return new URLSearchParams(window.location.search).get(TAB_QUERY) || "";
|
|
382
|
+
} catch {
|
|
383
|
+
return "";
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
const write = (tab) => {
|
|
387
|
+
try {
|
|
388
|
+
const url = new URL(window.location.href);
|
|
389
|
+
if (tab) url.searchParams.set(TAB_QUERY, tab);
|
|
390
|
+
else url.searchParams.delete(TAB_QUERY);
|
|
391
|
+
window.history.replaceState(window.history.state, "", `${url.pathname}${url.search}${url.hash}`);
|
|
392
|
+
} catch {}
|
|
393
|
+
};
|
|
394
|
+
return {
|
|
395
|
+
get: read,
|
|
396
|
+
set: write,
|
|
397
|
+
subscribe: (listener) => {
|
|
398
|
+
const onPopState = () => listener();
|
|
399
|
+
window.addEventListener("popstate", onPopState);
|
|
400
|
+
return () => window.removeEventListener("popstate", onPopState);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
};
|
|
404
|
+
//#endregion
|
|
405
|
+
//#region packages/markdown/src/tabs-nav.vue?vue&type=script&setup=true&lang.ts
|
|
406
|
+
var _hoisted_1$1 = ["aria-label"];
|
|
407
|
+
var _hoisted_2$1 = [
|
|
408
|
+
"id",
|
|
409
|
+
"aria-selected",
|
|
410
|
+
"aria-controls",
|
|
411
|
+
"tabindex",
|
|
412
|
+
"onClick"
|
|
413
|
+
];
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region packages/markdown/src/tabs-nav.vue
|
|
416
|
+
var tabs_nav_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
417
|
+
__name: "tabs-nav",
|
|
418
|
+
props: {
|
|
419
|
+
items: {},
|
|
420
|
+
label: {},
|
|
421
|
+
adapter: {},
|
|
422
|
+
root: {},
|
|
423
|
+
groupId: {}
|
|
424
|
+
},
|
|
425
|
+
setup(__props) {
|
|
426
|
+
const props = __props;
|
|
427
|
+
const activeId = (0, vue.ref)("");
|
|
428
|
+
let unsubscribe;
|
|
429
|
+
const tabButtonId = (id) => `${props.groupId}-tab-${id}`;
|
|
430
|
+
const tabPanelId = (id) => `${props.groupId}-panel-${id}`;
|
|
431
|
+
const resolveActive = () => {
|
|
432
|
+
const fromQuery = props.adapter.get();
|
|
433
|
+
const matched = props.items.find((item) => item.id === fromQuery);
|
|
434
|
+
activeId.value = matched?.id || props.items[0]?.id || "";
|
|
435
|
+
};
|
|
436
|
+
const syncPanels = () => {
|
|
437
|
+
props.root.querySelectorAll(".docs-markdown-tabs__panel").forEach((panel) => {
|
|
438
|
+
const id = panel.dataset.tab || "";
|
|
439
|
+
const selected = id === activeId.value;
|
|
440
|
+
panel.hidden = !selected;
|
|
441
|
+
panel.setAttribute("aria-labelledby", tabButtonId(id));
|
|
442
|
+
if (selected) {
|
|
443
|
+
panel.removeAttribute("hidden");
|
|
444
|
+
panel.id = tabPanelId(id);
|
|
445
|
+
} else {
|
|
446
|
+
panel.setAttribute("hidden", "");
|
|
447
|
+
panel.id = tabPanelId(id);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
};
|
|
451
|
+
const select = (id) => {
|
|
452
|
+
if (!id) return;
|
|
453
|
+
activeId.value = id;
|
|
454
|
+
props.adapter.set(id);
|
|
455
|
+
syncPanels();
|
|
456
|
+
};
|
|
457
|
+
(0, vue.onMounted)(() => {
|
|
458
|
+
resolveActive();
|
|
459
|
+
syncPanels();
|
|
460
|
+
unsubscribe = props.adapter.subscribe?.(() => {
|
|
461
|
+
resolveActive();
|
|
462
|
+
syncPanels();
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
(0, vue.onBeforeUnmount)(() => {
|
|
466
|
+
unsubscribe?.();
|
|
467
|
+
unsubscribe = void 0;
|
|
468
|
+
});
|
|
469
|
+
(0, vue.watch)(() => props.items.map((item) => item.id).join("\0"), () => {
|
|
470
|
+
resolveActive();
|
|
471
|
+
syncPanels();
|
|
472
|
+
});
|
|
473
|
+
return (_ctx, _cache) => {
|
|
474
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
475
|
+
class: "docs-markdown-tabs__list",
|
|
476
|
+
role: "tablist",
|
|
477
|
+
"aria-label": __props.label
|
|
478
|
+
}, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.items, (item) => {
|
|
479
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
480
|
+
key: item.id,
|
|
481
|
+
type: "button",
|
|
482
|
+
class: (0, vue.normalizeClass)(["docs-markdown-tabs__tab", { "docs-markdown-tabs__tab--active": item.id === activeId.value }]),
|
|
483
|
+
role: "tab",
|
|
484
|
+
id: tabButtonId(item.id),
|
|
485
|
+
"aria-selected": item.id === activeId.value,
|
|
486
|
+
"aria-controls": tabPanelId(item.id),
|
|
487
|
+
tabindex: item.id === activeId.value ? 0 : -1,
|
|
488
|
+
onClick: ($event) => select(item.id)
|
|
489
|
+
}, (0, vue.toDisplayString)(item.title), 11, _hoisted_2$1);
|
|
490
|
+
}), 128))], 8, _hoisted_1$1);
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
//#endregion
|
|
242
495
|
//#region packages/markdown/src/directive.ts
|
|
243
496
|
var mountedApps = /* @__PURE__ */ new WeakMap();
|
|
244
497
|
var renderVersions = /* @__PURE__ */ new WeakMap();
|
|
245
498
|
var playgroundModule;
|
|
246
499
|
var loadPlayground = () => playgroundModule ||= import("@deot/docs-playground");
|
|
500
|
+
var defaultTabQuery = createHistoryTabQueryAdapter();
|
|
247
501
|
var cleanup = (el) => {
|
|
248
502
|
for (const app of mountedApps.get(el) || []) app.unmount();
|
|
249
503
|
mountedApps.delete(el);
|
|
@@ -251,12 +505,37 @@ var cleanup = (el) => {
|
|
|
251
505
|
var resolveBlockLanguage = (block) => {
|
|
252
506
|
return [...block.classList].find((className) => className.startsWith("language-"))?.slice(9) || "";
|
|
253
507
|
};
|
|
508
|
+
var serializePlaygroundDefaults = (value) => JSON.stringify(value || {});
|
|
509
|
+
var mountTabs = (el, apps, locale, adapter) => {
|
|
510
|
+
const label = locale.markdown?.tabs?.label || "Tabs";
|
|
511
|
+
el.querySelectorAll("[data-tabs]").forEach((root, index) => {
|
|
512
|
+
const nav = root.querySelector("[data-tabs-nav]");
|
|
513
|
+
if (!nav) return;
|
|
514
|
+
const items = [...root.querySelectorAll(".docs-markdown-tabs__panel")].map((panel) => ({
|
|
515
|
+
id: panel.dataset.tab || "",
|
|
516
|
+
title: panel.dataset.tabTitle || panel.dataset.tab || ""
|
|
517
|
+
})).filter((item) => item.id && item.title);
|
|
518
|
+
if (!items.length) return;
|
|
519
|
+
const groupId = `docs-markdown-tabs-${index}`;
|
|
520
|
+
const app = (0, vue.createApp)({ render: () => (0, vue.h)(tabs_nav_default, {
|
|
521
|
+
items,
|
|
522
|
+
label,
|
|
523
|
+
adapter,
|
|
524
|
+
root,
|
|
525
|
+
groupId
|
|
526
|
+
}) });
|
|
527
|
+
app.mount(nav);
|
|
528
|
+
apps.push(app);
|
|
529
|
+
});
|
|
530
|
+
};
|
|
254
531
|
var render = async (el, binding) => {
|
|
255
532
|
cleanup(el);
|
|
256
533
|
const version = (renderVersions.get(el) || 0) + 1;
|
|
257
534
|
renderVersions.set(el, version);
|
|
258
535
|
el.innerHTML = binding.value.source ? Markdown.render(binding.value.source) : "";
|
|
259
536
|
const apps = [];
|
|
537
|
+
const adapter = binding.value.tabQuery || defaultTabQuery;
|
|
538
|
+
mountTabs(el, apps, binding.value.locale, adapter);
|
|
260
539
|
const blocks = el.querySelectorAll("pre code");
|
|
261
540
|
const playgrounds = el.querySelectorAll("div[data-playground]");
|
|
262
541
|
if (!blocks.length && !playgrounds.length) {
|
|
@@ -279,6 +558,7 @@ var render = async (el, binding) => {
|
|
|
279
558
|
app.mount(mountPoint);
|
|
280
559
|
apps.push(app);
|
|
281
560
|
});
|
|
561
|
+
const sitePlaygroundDefaults = binding.value.playground && typeof binding.value.playground === "object" ? binding.value.playground : {};
|
|
282
562
|
playgrounds.forEach((item) => {
|
|
283
563
|
const code = item.dataset.code;
|
|
284
564
|
let files;
|
|
@@ -294,6 +574,7 @@ var render = async (el, binding) => {
|
|
|
294
574
|
entry: item.dataset.entry
|
|
295
575
|
} : { modelValue: code };
|
|
296
576
|
const app = (0, vue.createApp)(() => (0, vue.h)(DocsPlayground.Playground, {
|
|
577
|
+
...sitePlaygroundDefaults,
|
|
297
578
|
...typeof propsData === "object" ? propsData : {},
|
|
298
579
|
...runtimeProps,
|
|
299
580
|
locale: binding.value.locale
|
|
@@ -304,7 +585,7 @@ var render = async (el, binding) => {
|
|
|
304
585
|
mountedApps.set(el, apps);
|
|
305
586
|
};
|
|
306
587
|
var update = (el, binding) => {
|
|
307
|
-
if (binding.value.source !== binding.oldValue?.source || binding.value.locale !== binding.oldValue?.locale) render(el, binding);
|
|
588
|
+
if (binding.value.source !== binding.oldValue?.source || binding.value.locale !== binding.oldValue?.locale || binding.value.tabQuery !== binding.oldValue?.tabQuery || serializePlaygroundDefaults(binding.value.playground) !== serializePlaygroundDefaults(binding.oldValue?.playground)) render(el, binding);
|
|
308
589
|
};
|
|
309
590
|
var vMarkdown = {
|
|
310
591
|
mounted: (el, binding) => void render(el, binding),
|
|
@@ -314,17 +595,154 @@ var vMarkdown = {
|
|
|
314
595
|
cleanup(el);
|
|
315
596
|
}
|
|
316
597
|
};
|
|
598
|
+
/**
|
|
599
|
+
* 指示器高度不能超过正文实际高度,也不能超出当前滚动容器可视区域。
|
|
600
|
+
* 与文档壳大纲的封顶规则一致。
|
|
601
|
+
* @param articleHeight 正文高度。
|
|
602
|
+
* @param viewportHeight 主滚动容器可视高度。
|
|
603
|
+
* @returns 指示器最大高度,单位 px。
|
|
604
|
+
*/
|
|
605
|
+
var capIndicatorMaxHeight = (articleHeight, viewportHeight) => {
|
|
606
|
+
const article = Math.max(0, articleHeight);
|
|
607
|
+
const viewport = Math.max(0, viewportHeight);
|
|
608
|
+
if (!article) return viewport;
|
|
609
|
+
if (!viewport) return article;
|
|
610
|
+
return Math.min(article, viewport);
|
|
611
|
+
};
|
|
612
|
+
/**
|
|
613
|
+
* 滑轨高度再受 sticky 父级剩余空间约束,避免滚到文末盖住 footer。
|
|
614
|
+
* @param cap 正文/视口封顶后的高度。
|
|
615
|
+
* @param available 指示器顶部到父级底部的剩余像素;非有限值表示不额外约束。
|
|
616
|
+
* @returns 最终高度,单位 px。
|
|
617
|
+
*/
|
|
618
|
+
var capStickyAvailableHeight = (cap, available) => {
|
|
619
|
+
const limited = Math.max(0, cap);
|
|
620
|
+
if (!Number.isFinite(available)) return limited;
|
|
621
|
+
return Math.min(limited, Math.max(0, available));
|
|
622
|
+
};
|
|
623
|
+
/**
|
|
624
|
+
* 把主滚动位置映射成小地图可视窗口(相对轨道的 0–1 比例)。
|
|
625
|
+
* @param scrollTop 当前滚动偏移。
|
|
626
|
+
* @param clientHeight 滚动容器可视高度。
|
|
627
|
+
* @param scrollHeight 滚动内容总高度。
|
|
628
|
+
* @returns 窗口顶部与高度比例。
|
|
629
|
+
*/
|
|
630
|
+
var getMinimapWindow = (scrollTop, clientHeight, scrollHeight) => {
|
|
631
|
+
const total = Math.max(0, scrollHeight);
|
|
632
|
+
if (!total) return {
|
|
633
|
+
height: 1,
|
|
634
|
+
top: 0
|
|
635
|
+
};
|
|
636
|
+
const height = Math.min(1, Math.max(0, clientHeight / total));
|
|
637
|
+
const maxTop = Math.max(0, 1 - height);
|
|
638
|
+
return {
|
|
639
|
+
height,
|
|
640
|
+
top: maxTop <= 0 ? 0 : Math.min(maxTop, Math.max(0, scrollTop / total))
|
|
641
|
+
};
|
|
642
|
+
};
|
|
643
|
+
/**
|
|
644
|
+
* 按小地图窗口顶部选出当前阅读刻度:最后一个仍不晚于窗口顶的块。
|
|
645
|
+
* 滚到末屏时固定最后一条,避免末块永远到不了窗口顶。
|
|
646
|
+
* @param ratios 刻度在正文中的 0–1 比例,按出现顺序。
|
|
647
|
+
* @param windowTop 可视窗口顶部比例。
|
|
648
|
+
* @param windowHeight 可视窗口高度比例。
|
|
649
|
+
* @returns 当前刻度序号;没有刻度时为 -1。
|
|
650
|
+
*/
|
|
651
|
+
var findActiveMinimapIndex = (ratios, windowTop, windowHeight) => {
|
|
652
|
+
if (!ratios.length) return -1;
|
|
653
|
+
if (windowTop + windowHeight >= .999) return ratios.length - 1;
|
|
654
|
+
let active = 0;
|
|
655
|
+
for (let index = 0; index < ratios.length; index++) if ((ratios[index] || 0) <= windowTop) active = index;
|
|
656
|
+
return active;
|
|
657
|
+
};
|
|
658
|
+
/**
|
|
659
|
+
* 判断刻度是否落在当前可视窗口内。
|
|
660
|
+
* @param ratio 刻度比例。
|
|
661
|
+
* @param windowTop 可视窗口顶部比例。
|
|
662
|
+
* @param windowHeight 可视窗口高度比例。
|
|
663
|
+
* @returns 是否位于窗口内。
|
|
664
|
+
*/
|
|
665
|
+
var isMinimapMarkerInView = (ratio, windowTop, windowHeight) => {
|
|
666
|
+
const top = Math.max(0, windowTop);
|
|
667
|
+
const bottom = top + Math.max(0, windowHeight);
|
|
668
|
+
return ratio >= top && ratio <= bottom;
|
|
669
|
+
};
|
|
670
|
+
/**
|
|
671
|
+
* 把指针纵坐标映射成轨道上的 0–1 比例。
|
|
672
|
+
* @param clientY 指针视口纵坐标。
|
|
673
|
+
* @param trackTop 轨道顶部。
|
|
674
|
+
* @param trackHeight 轨道高度。
|
|
675
|
+
* @returns 夹紧后的比例。
|
|
676
|
+
*/
|
|
677
|
+
var getMinimapPointerRatio = (clientY, trackTop, trackHeight) => {
|
|
678
|
+
if (trackHeight <= 0) return 0;
|
|
679
|
+
return Math.min(1, Math.max(0, (clientY - trackTop) / trackHeight));
|
|
680
|
+
};
|
|
681
|
+
/**
|
|
682
|
+
* 把指示器刻度对齐到文档壳斜纹轨的内侧,并留出 `INDICATOR_RAIL_GAP`。
|
|
683
|
+
* 左侧靠近 `rail--start` 的右缘,右侧靠近 `rail--end` 的左缘。
|
|
684
|
+
* 没有可见轨道时返回 undefined,由样式回退到正文边缘。
|
|
685
|
+
* @param root 指示器根节点相对视口的盒子。
|
|
686
|
+
* @param rail 当前侧斜纹轨的盒子。
|
|
687
|
+
* @param side 指示器所在侧。
|
|
688
|
+
* @param viewportWidth 刻度轨道宽度。
|
|
689
|
+
* @returns 相对指示器根节点的 left 偏移,单位 px。
|
|
690
|
+
*/
|
|
691
|
+
var getIndicatorInlineOffset = (root, rail, side, viewportWidth) => {
|
|
692
|
+
if (!rail || rail.width <= 0 || rail.height <= 0) return;
|
|
693
|
+
if (side === "left") return rail.right - root.left + 8;
|
|
694
|
+
return rail.left - root.left - viewportWidth - 8;
|
|
695
|
+
};
|
|
696
|
+
//#endregion
|
|
697
|
+
//#region packages/markdown/src/headings.ts
|
|
698
|
+
var HEADING_SELECTOR = "h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]";
|
|
699
|
+
var SKIP_CLOSEST = [
|
|
700
|
+
"pre",
|
|
701
|
+
".docs-markdown-code-preview",
|
|
702
|
+
"[data-playground][data-code]",
|
|
703
|
+
"[data-playground][data-files]",
|
|
704
|
+
".docs-markdown-tabs__panel[hidden]"
|
|
705
|
+
].join(", ");
|
|
706
|
+
/**
|
|
707
|
+
* 去掉 markdown-it-anchor 的井号后,读取标题的可见文案。
|
|
708
|
+
* @param element 当前标题节点。
|
|
709
|
+
* @returns 单行章节标题。
|
|
710
|
+
*/
|
|
711
|
+
var getMarkdownHeadingTitle = (element) => {
|
|
712
|
+
const clone = element.cloneNode(true);
|
|
713
|
+
clone.querySelectorAll(".header-anchor").forEach((node) => node.remove());
|
|
714
|
+
return (clone.textContent || "").replace(/\s+/g, " ").trim();
|
|
715
|
+
};
|
|
716
|
+
/**
|
|
717
|
+
* 从已渲染的 Markdown 中收集带 id 的标题,规则与文档壳大纲一致:
|
|
718
|
+
* 忽略代码块 / Playground 内部标题;有章节标题时不把页面 h1 算进去。
|
|
719
|
+
* @param root Markdown 所在容器。
|
|
720
|
+
* @returns 大纲条目,按出现顺序排列。
|
|
721
|
+
*/
|
|
722
|
+
var collectMarkdownHeadings = (root) => {
|
|
723
|
+
if (!root || !("querySelectorAll" in root)) return [];
|
|
724
|
+
return [...root.querySelectorAll(HEADING_SELECTOR)].filter((element) => !element.closest(SKIP_CLOSEST) && Boolean(getMarkdownHeadingTitle(element))).map((element) => ({
|
|
725
|
+
element,
|
|
726
|
+
id: element.id,
|
|
727
|
+
level: Number(element.tagName.slice(1)),
|
|
728
|
+
text: getMarkdownHeadingTitle(element)
|
|
729
|
+
})).filter((item, _, headings) => item.level > 1 || headings.every((heading) => heading.level === 1));
|
|
730
|
+
};
|
|
317
731
|
//#endregion
|
|
318
732
|
//#region packages/markdown/src/indicator.vue?vue&type=script&setup=true&lang.ts
|
|
319
|
-
var _hoisted_1
|
|
320
|
-
var _hoisted_2 =
|
|
321
|
-
var _hoisted_3 =
|
|
322
|
-
var _hoisted_4 = {
|
|
323
|
-
var _hoisted_5 = {
|
|
733
|
+
var _hoisted_1 = ["aria-label"];
|
|
734
|
+
var _hoisted_2 = ["aria-valuenow", "aria-valuetext"];
|
|
735
|
+
var _hoisted_3 = { class: "docs-markdown-indicator__preview-title" };
|
|
736
|
+
var _hoisted_4 = {
|
|
324
737
|
key: 0,
|
|
325
738
|
class: "docs-markdown-indicator__preview-content"
|
|
326
739
|
};
|
|
327
|
-
var
|
|
740
|
+
var LAYOUT_SELECTOR = ".docs-layout";
|
|
741
|
+
var RAIL_START_SELECTOR = ".docs-layout__rail--start";
|
|
742
|
+
var RAIL_END_SELECTOR = ".docs-layout__rail--end";
|
|
743
|
+
var VIEWPORT_WIDTH = 40;
|
|
744
|
+
/** 与 docs-page-outline 上下 padding 一致,需从轨道高度中扣除。 */
|
|
745
|
+
var INDICATOR_CHROME = 136;
|
|
328
746
|
//#endregion
|
|
329
747
|
//#region packages/markdown/src/indicator.vue
|
|
330
748
|
var indicator_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
@@ -334,61 +752,82 @@ var indicator_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
334
752
|
target: {}
|
|
335
753
|
},
|
|
336
754
|
setup(__props) {
|
|
337
|
-
const BLOCK_SELECTOR = [
|
|
338
|
-
"h1",
|
|
339
|
-
"h2",
|
|
340
|
-
"h3",
|
|
341
|
-
"h4",
|
|
342
|
-
"h5",
|
|
343
|
-
"h6",
|
|
344
|
-
"p",
|
|
345
|
-
"li",
|
|
346
|
-
"pre",
|
|
347
|
-
"blockquote",
|
|
348
|
-
"table",
|
|
349
|
-
".tip",
|
|
350
|
-
".warning",
|
|
351
|
-
".docs-markdown-code-preview",
|
|
352
|
-
"[data-playground]"
|
|
353
|
-
].join(",");
|
|
354
|
-
const CONTAINER_SELECTOR = [
|
|
355
|
-
"li",
|
|
356
|
-
"blockquote",
|
|
357
|
-
"table",
|
|
358
|
-
".tip",
|
|
359
|
-
".warning",
|
|
360
|
-
".docs-markdown-code-preview",
|
|
361
|
-
"[data-playground]"
|
|
362
|
-
].join(",");
|
|
363
755
|
const props = __props;
|
|
364
756
|
const { lang: localeName, t } = (0, _deot_docs_locale.useLocale)();
|
|
365
757
|
const parentScroller = (0, vue.inject)("vc-scroller", null);
|
|
366
758
|
const indicatorRoot = (0, vue.ref)();
|
|
367
759
|
const viewport = (0, vue.ref)();
|
|
368
760
|
const rail = (0, vue.ref)();
|
|
369
|
-
const indicatorScroller = (0, vue.ref)();
|
|
370
761
|
const markers = (0, vue.shallowRef)([]);
|
|
371
|
-
const activeIndex = (0, vue.ref)(0);
|
|
372
762
|
const hoverIndex = (0, vue.ref)();
|
|
763
|
+
const activeIndex = (0, vue.ref)(-1);
|
|
373
764
|
const previewTop = (0, vue.ref)(0);
|
|
374
765
|
const dragging = (0, vue.ref)(false);
|
|
375
|
-
const
|
|
766
|
+
const cappedHeight = (0, vue.ref)(0);
|
|
767
|
+
const inlineAnchor = (0, vue.ref)();
|
|
768
|
+
const windowBox = (0, vue.ref)(getMinimapWindow(0, 0, 0));
|
|
376
769
|
let observer;
|
|
377
770
|
let hostResizeObserver;
|
|
378
771
|
let scrollHost;
|
|
379
772
|
let usesParentScroller = false;
|
|
380
773
|
let captureTarget;
|
|
381
774
|
let refreshFrame = 0;
|
|
382
|
-
let
|
|
775
|
+
let windowFrame = 0;
|
|
383
776
|
let targetGeneration = 0;
|
|
384
777
|
const toCssLength = (value, fallback) => typeof value === "number" ? `${value}px` : value || fallback;
|
|
778
|
+
const resolvedHeight = (0, vue.computed)(() => {
|
|
779
|
+
const cap = cappedHeight.value;
|
|
780
|
+
const configured = props.options.height;
|
|
781
|
+
if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) return `${cap > 0 ? Math.min(configured, cap) : configured}px`;
|
|
782
|
+
if (typeof configured === "string" && configured.trim()) return configured;
|
|
783
|
+
if (cap > 0) return `${cap}px`;
|
|
784
|
+
return "calc(100svh - 60px)";
|
|
785
|
+
});
|
|
786
|
+
/** 扣除上下 padding 后的轨道高度。小地图轨高等于该值,不再内部滚动。 */
|
|
787
|
+
const resolvedTrackHeight = (0, vue.computed)(() => {
|
|
788
|
+
const height = resolvedHeight.value;
|
|
789
|
+
if (height.endsWith("px")) {
|
|
790
|
+
const total = Number.parseFloat(height);
|
|
791
|
+
if (Number.isFinite(total)) return `${Math.max(0, total - INDICATOR_CHROME)}px`;
|
|
792
|
+
}
|
|
793
|
+
return `calc(${height} - ${INDICATOR_CHROME}px)`;
|
|
794
|
+
});
|
|
385
795
|
const rootStyle = (0, vue.computed)(() => ({
|
|
386
|
-
"--docs-markdown-indicator-height":
|
|
387
|
-
"--docs-markdown-indicator-
|
|
796
|
+
"--docs-markdown-indicator-height": resolvedHeight.value,
|
|
797
|
+
"--docs-markdown-indicator-track-height": resolvedTrackHeight.value,
|
|
388
798
|
"--docs-markdown-indicator-top": toCssLength(props.options.top, "0px")
|
|
389
799
|
}));
|
|
390
800
|
const hoveredMarker = (0, vue.computed)(() => typeof hoverIndex.value === "number" ? markers.value[hoverIndex.value] : void 0);
|
|
391
|
-
const
|
|
801
|
+
const isMarkerInView = (index) => {
|
|
802
|
+
const marker = markers.value[index];
|
|
803
|
+
if (!marker) return false;
|
|
804
|
+
return isMinimapMarkerInView(marker.ratio, windowBox.value.top, windowBox.value.height);
|
|
805
|
+
};
|
|
806
|
+
const resolvedPosition = (0, vue.computed)(() => props.options.position === "right" ? "right" : "left");
|
|
807
|
+
const scrollProgress = (0, vue.computed)(() => Math.round(windowBox.value.top * 100));
|
|
808
|
+
const viewportStyle = (0, vue.computed)(() => {
|
|
809
|
+
if (typeof inlineAnchor.value !== "number") return;
|
|
810
|
+
return {
|
|
811
|
+
left: `${inlineAnchor.value}px`,
|
|
812
|
+
right: "auto"
|
|
813
|
+
};
|
|
814
|
+
});
|
|
815
|
+
const windowStyle = (0, vue.computed)(() => ({
|
|
816
|
+
height: `${windowBox.value.height * 100}%`,
|
|
817
|
+
top: `${windowBox.value.top * 100}%`
|
|
818
|
+
}));
|
|
819
|
+
const previewStyle = (0, vue.computed)(() => {
|
|
820
|
+
const style = { top: `${previewTop.value}px` };
|
|
821
|
+
if (typeof inlineAnchor.value !== "number") return style;
|
|
822
|
+
if (resolvedPosition.value === "left") {
|
|
823
|
+
style.left = `${inlineAnchor.value + 24}px`;
|
|
824
|
+
style.right = "auto";
|
|
825
|
+
} else {
|
|
826
|
+
style.left = "auto";
|
|
827
|
+
style.right = `calc(100% - ${inlineAnchor.value}px)`;
|
|
828
|
+
}
|
|
829
|
+
return style;
|
|
830
|
+
});
|
|
392
831
|
/**
|
|
393
832
|
* 保留文档块的有效换行,同时折叠每行内部的多余空白。
|
|
394
833
|
* @param element 当前文档块。
|
|
@@ -396,30 +835,21 @@ var indicator_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
396
835
|
*/
|
|
397
836
|
const getBlockText = (element) => (element.textContent || "").split(/\n+/).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
|
|
398
837
|
/**
|
|
399
|
-
*
|
|
400
|
-
* @param
|
|
401
|
-
* @
|
|
838
|
+
* 取当前标题到下一标题之间的第一段正文,作为悬停摘要。
|
|
839
|
+
* @param heading 当前标题。
|
|
840
|
+
* @param nextHeading 下一个标题,没有则为空。
|
|
841
|
+
* @returns 摘要文本。
|
|
402
842
|
*/
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
return `${value.slice(0, MAX_ARIA_LABEL_LENGTH).trim()}…`;
|
|
414
|
-
};
|
|
415
|
-
/**
|
|
416
|
-
* 判断候选节点是否已经包含在另一个可独立定位的文档块内。
|
|
417
|
-
* @param element 当前候选节点。
|
|
418
|
-
* @returns 是否应由外层文档块统一表示。
|
|
419
|
-
*/
|
|
420
|
-
const isNestedBlock = (element) => {
|
|
421
|
-
const container = element.closest(CONTAINER_SELECTOR);
|
|
422
|
-
return Boolean(container && container !== element);
|
|
843
|
+
const getPreviewContent = (heading, nextHeading) => {
|
|
844
|
+
let node = heading.nextElementSibling;
|
|
845
|
+
while (node && node !== nextHeading) {
|
|
846
|
+
if (!/^H[1-6]$/.test(node.tagName)) {
|
|
847
|
+
const text = getBlockText(node);
|
|
848
|
+
if (text) return text;
|
|
849
|
+
}
|
|
850
|
+
node = node.nextElementSibling;
|
|
851
|
+
}
|
|
852
|
+
return "";
|
|
423
853
|
};
|
|
424
854
|
/**
|
|
425
855
|
* 获取 Markdown 使用的原生滚动宿主。
|
|
@@ -428,77 +858,118 @@ var indicator_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
428
858
|
*/
|
|
429
859
|
const getScrollHost = () => (0, _deot_helper_dom.getScroller)(props.target) || window;
|
|
430
860
|
/**
|
|
431
|
-
*
|
|
432
|
-
* @returns
|
|
861
|
+
* 读取当前滚动容器的可视高度。
|
|
862
|
+
* @returns 滚动容器 clientHeight,窗口滚动时回退到 innerHeight。
|
|
433
863
|
*/
|
|
434
|
-
const
|
|
435
|
-
if (usesParentScroller) return parentScroller?.wrapper?.
|
|
436
|
-
|
|
864
|
+
const getScrollHostClientHeight = () => {
|
|
865
|
+
if (usesParentScroller) return parentScroller?.wrapper?.clientHeight || parentScroller?.clientHeight || window.innerHeight;
|
|
866
|
+
if (scrollHost instanceof HTMLElement) return scrollHost.clientHeight;
|
|
867
|
+
return window.innerHeight;
|
|
437
868
|
};
|
|
438
869
|
/**
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
* @returns 是否位于可滚动内容末尾。
|
|
870
|
+
* 读取主滚动的位置与尺寸,供可视窗口和 scrub 共用。
|
|
871
|
+
* @returns 当前滚动偏移、可视高度和内容总高度。
|
|
442
872
|
*/
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
return scrollHeight > clientHeight && scrollTop + clientHeight >= scrollHeight - 1;
|
|
873
|
+
const getScrollMetrics = () => {
|
|
874
|
+
if (usesParentScroller) return {
|
|
875
|
+
clientHeight: parentScroller?.clientHeight || parentScroller?.wrapper?.clientHeight || 0,
|
|
876
|
+
scrollHeight: parentScroller?.scrollHeight || parentScroller?.wrapper?.scrollHeight || 0,
|
|
877
|
+
scrollTop: parentScroller?.scrollTop || 0
|
|
878
|
+
};
|
|
879
|
+
if (scrollHost instanceof HTMLElement) return {
|
|
880
|
+
clientHeight: scrollHost.clientHeight,
|
|
881
|
+
scrollHeight: scrollHost.scrollHeight,
|
|
882
|
+
scrollTop: scrollHost.scrollTop
|
|
883
|
+
};
|
|
884
|
+
return {
|
|
885
|
+
clientHeight: window.innerHeight,
|
|
886
|
+
scrollHeight: document.documentElement.scrollHeight,
|
|
887
|
+
scrollTop: window.scrollY
|
|
888
|
+
};
|
|
461
889
|
};
|
|
462
890
|
/**
|
|
463
|
-
*
|
|
464
|
-
* @
|
|
891
|
+
* 按正文、主滚动可视高度,以及 sticky 父级剩余空间封顶,避免盖住 footer。
|
|
892
|
+
* @returns 指示器可用的最大高度,单位 px。
|
|
465
893
|
*/
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const padding = 16;
|
|
474
|
-
if (markerRect.top >= wrapperRect.top + padding && markerRect.bottom <= wrapperRect.bottom - padding) return;
|
|
475
|
-
const markerCenter = markerRect.top + markerRect.height / 2;
|
|
476
|
-
const wrapperCenter = wrapperRect.top + wrapperRect.height / 2;
|
|
477
|
-
indicatorScroller.value?.setScrollTop(Math.max(0, wrapper.scrollTop + markerCenter - wrapperCenter));
|
|
894
|
+
const getStickyAvailableHeight = () => {
|
|
895
|
+
const root = indicatorRoot.value;
|
|
896
|
+
const parent = root?.parentElement;
|
|
897
|
+
if (!root || !parent) return Number.POSITIVE_INFINITY;
|
|
898
|
+
const parentBox = parent.getBoundingClientRect();
|
|
899
|
+
if (parentBox.height <= 0 && parentBox.bottom === 0) return Number.POSITIVE_INFINITY;
|
|
900
|
+
return parentBox.bottom - root.getBoundingClientRect().top;
|
|
478
901
|
};
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
902
|
+
const updateCappedHeight = () => {
|
|
903
|
+
cappedHeight.value = capStickyAvailableHeight(capIndicatorMaxHeight(props.target?.offsetHeight || 0, getScrollHostClientHeight()), getStickyAvailableHeight());
|
|
904
|
+
};
|
|
905
|
+
/** 根据主滚动位置更新小地图可视窗口和当前刻度。 */
|
|
906
|
+
const updateWindow = () => {
|
|
907
|
+
windowFrame = 0;
|
|
908
|
+
const metrics = getScrollMetrics();
|
|
909
|
+
windowBox.value = getMinimapWindow(metrics.scrollTop, metrics.clientHeight, metrics.scrollHeight);
|
|
910
|
+
activeIndex.value = findActiveMinimapIndex(markers.value.map((marker) => marker.ratio), windowBox.value.top, windowBox.value.height);
|
|
911
|
+
};
|
|
912
|
+
/**
|
|
913
|
+
* 有文档壳斜纹轨时,把刻度靠近轨道内侧并留出间距;否则清空偏移,沿用正文边缘定位。
|
|
914
|
+
*/
|
|
915
|
+
const updateInlineAnchor = () => {
|
|
916
|
+
const root = indicatorRoot.value;
|
|
917
|
+
if (!root) {
|
|
918
|
+
inlineAnchor.value = void 0;
|
|
919
|
+
return;
|
|
491
920
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
921
|
+
const layoutRail = root.closest(LAYOUT_SELECTOR)?.querySelector(resolvedPosition.value === "left" ? RAIL_START_SELECTOR : RAIL_END_SELECTOR);
|
|
922
|
+
const railRect = layoutRail && getComputedStyle(layoutRail).display !== "none" ? layoutRail.getBoundingClientRect() : void 0;
|
|
923
|
+
inlineAnchor.value = getIndicatorInlineOffset(root.getBoundingClientRect(), railRect, resolvedPosition.value, viewport.value?.offsetWidth || VIEWPORT_WIDTH);
|
|
924
|
+
};
|
|
925
|
+
/** 同步高度封顶、斜纹轨锚点、刻度比例和可视窗口。 */
|
|
926
|
+
const updateLayout = () => {
|
|
927
|
+
updateCappedHeight();
|
|
928
|
+
updateInlineAnchor();
|
|
929
|
+
if (props.target && markers.value.length) {
|
|
930
|
+
const article = props.target;
|
|
931
|
+
markers.value = markers.value.map((marker) => ({
|
|
932
|
+
...marker,
|
|
933
|
+
ratio: getBlockRatio(marker.element, article)
|
|
934
|
+
}));
|
|
495
935
|
}
|
|
936
|
+
updateWindow();
|
|
496
937
|
};
|
|
497
|
-
/**
|
|
498
|
-
const
|
|
499
|
-
|
|
938
|
+
/** 跟随滚动容器、正文和指示器自身的尺寸变化。 */
|
|
939
|
+
const observeHostSize = () => {
|
|
940
|
+
hostResizeObserver?.disconnect();
|
|
941
|
+
hostResizeObserver = void 0;
|
|
942
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
943
|
+
hostResizeObserver = new ResizeObserver(updateLayout);
|
|
944
|
+
const host = usesParentScroller ? parentScroller?.wrapper : scrollHost instanceof HTMLElement ? scrollHost : void 0;
|
|
945
|
+
if (host) hostResizeObserver.observe(host);
|
|
946
|
+
if (props.target) hostResizeObserver.observe(props.target);
|
|
947
|
+
if (viewport.value) hostResizeObserver.observe(viewport.value);
|
|
948
|
+
if (indicatorRoot.value) hostResizeObserver.observe(indicatorRoot.value);
|
|
949
|
+
const parent = indicatorRoot.value?.parentElement;
|
|
950
|
+
if (parent) hostResizeObserver.observe(parent);
|
|
951
|
+
const layout = indicatorRoot.value?.closest(LAYOUT_SELECTOR);
|
|
952
|
+
if (layout instanceof HTMLElement) hostResizeObserver.observe(layout);
|
|
953
|
+
const startRail = layout?.querySelector(RAIL_START_SELECTOR);
|
|
954
|
+
const endRail = layout?.querySelector(RAIL_END_SELECTOR);
|
|
955
|
+
if (startRail instanceof HTMLElement) hostResizeObserver.observe(startRail);
|
|
956
|
+
if (endRail instanceof HTMLElement) hostResizeObserver.observe(endRail);
|
|
957
|
+
}
|
|
958
|
+
updateLayout();
|
|
500
959
|
};
|
|
501
|
-
/**
|
|
960
|
+
/**
|
|
961
|
+
* 计算文档块相对正文顶部的比例,用于铺在固定高度轨道上。
|
|
962
|
+
* @param element 当前文档块。
|
|
963
|
+
* @param article 正文根节点。
|
|
964
|
+
* @returns 0–1 的纵向比例。
|
|
965
|
+
*/
|
|
966
|
+
const getBlockRatio = (element, article) => {
|
|
967
|
+
const articleHeight = article.offsetHeight || article.getBoundingClientRect().height;
|
|
968
|
+
if (articleHeight <= 0) return 0;
|
|
969
|
+
const top = element.getBoundingClientRect().top - article.getBoundingClientRect().top;
|
|
970
|
+
return Math.min(1, Math.max(0, top / articleHeight));
|
|
971
|
+
};
|
|
972
|
+
/** 从最新渲染的 Markdown DOM 重建小地图刻度。 */
|
|
502
973
|
const refreshMarkers = () => {
|
|
503
974
|
refreshFrame = 0;
|
|
504
975
|
const target = props.target;
|
|
@@ -506,67 +977,28 @@ var indicator_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
506
977
|
markers.value = [];
|
|
507
978
|
return;
|
|
508
979
|
}
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
content,
|
|
519
|
-
element,
|
|
520
|
-
id: `${index}-${element.id || element.tagName}`,
|
|
521
|
-
title: sectionTitle
|
|
522
|
-
};
|
|
523
|
-
});
|
|
980
|
+
const headings = collectMarkdownHeadings(target);
|
|
981
|
+
markers.value = headings.map((heading, index) => ({
|
|
982
|
+
content: getPreviewContent(heading.element, headings[index + 1]?.element),
|
|
983
|
+
element: heading.element,
|
|
984
|
+
id: heading.id || `${index}`,
|
|
985
|
+
isHeading: true,
|
|
986
|
+
ratio: getBlockRatio(heading.element, target),
|
|
987
|
+
title: heading.text
|
|
988
|
+
}));
|
|
524
989
|
hoverIndex.value = void 0;
|
|
525
|
-
|
|
526
|
-
followActiveMarker(activeIndex.value);
|
|
990
|
+
updateWindow();
|
|
527
991
|
};
|
|
528
992
|
/** 合并 MutationObserver 的密集通知,避免 Playground 挂载时重复扫描。 */
|
|
529
993
|
const scheduleRefresh = () => {
|
|
530
994
|
if (!refreshFrame) refreshFrame = requestAnimationFrame(refreshMarkers);
|
|
531
995
|
};
|
|
532
|
-
/**
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if (usesParentScroller) return parentScroller?.wrapper?.clientHeight || parentScroller?.clientHeight || window.innerHeight;
|
|
539
|
-
if (scrollHost instanceof HTMLElement) return scrollHost.clientHeight;
|
|
540
|
-
return window.innerHeight;
|
|
541
|
-
};
|
|
542
|
-
/**
|
|
543
|
-
* 读取指示器可视高度:数字配置优先,否则测量已挂载节点或回退默认值。
|
|
544
|
-
* @returns 指示器高度,单位 px。
|
|
545
|
-
*/
|
|
546
|
-
const resolveIndicatorHeight = () => {
|
|
547
|
-
const height = props.options.height;
|
|
548
|
-
if (typeof height === "number" && Number.isFinite(height) && height > 0) return height;
|
|
549
|
-
const measured = viewport.value?.getBoundingClientRect().height || 0;
|
|
550
|
-
if (measured > 0) return measured;
|
|
551
|
-
return Math.min(window.innerHeight * .72, 600);
|
|
552
|
-
};
|
|
553
|
-
/** 把指示器钉在滚动容器可视区域的垂直中心。 */
|
|
554
|
-
const updateStickyInset = () => {
|
|
555
|
-
const hostHeight = getScrollHostClientHeight();
|
|
556
|
-
const indicatorHeight = resolveIndicatorHeight();
|
|
557
|
-
stickyInset.value = hostHeight > 0 ? `${Math.max(0, (hostHeight - indicatorHeight) / 2)}px` : "0px";
|
|
558
|
-
};
|
|
559
|
-
/** 跟随滚动容器和指示器自身的尺寸变化,保持垂直居中。 */
|
|
560
|
-
const observeHostSize = () => {
|
|
561
|
-
hostResizeObserver?.disconnect();
|
|
562
|
-
hostResizeObserver = void 0;
|
|
563
|
-
if (typeof ResizeObserver !== "undefined") {
|
|
564
|
-
hostResizeObserver = new ResizeObserver(updateStickyInset);
|
|
565
|
-
const host = usesParentScroller ? parentScroller?.wrapper : scrollHost instanceof HTMLElement ? scrollHost : void 0;
|
|
566
|
-
if (host) hostResizeObserver.observe(host);
|
|
567
|
-
if (viewport.value) hostResizeObserver.observe(viewport.value);
|
|
568
|
-
}
|
|
569
|
-
updateStickyInset();
|
|
996
|
+
/** 将连续滚动事件合并到下一帧:更新可视窗口,并按父级底部收缩高度。 */
|
|
997
|
+
const handleScroll = () => {
|
|
998
|
+
if (!windowFrame) windowFrame = requestAnimationFrame(() => {
|
|
999
|
+
updateCappedHeight();
|
|
1000
|
+
updateWindow();
|
|
1001
|
+
});
|
|
570
1002
|
};
|
|
571
1003
|
/** 解除旧文档的观察和滚动监听。 */
|
|
572
1004
|
const cleanupTarget = () => {
|
|
@@ -574,7 +1006,7 @@ var indicator_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
574
1006
|
observer = void 0;
|
|
575
1007
|
hostResizeObserver?.disconnect();
|
|
576
1008
|
hostResizeObserver = void 0;
|
|
577
|
-
window.removeEventListener("resize",
|
|
1009
|
+
window.removeEventListener("resize", updateLayout);
|
|
578
1010
|
if (usesParentScroller) parentScroller?.off?.(handleScroll);
|
|
579
1011
|
else if (scrollHost) scrollHost.removeEventListener("scroll", handleScroll);
|
|
580
1012
|
usesParentScroller = false;
|
|
@@ -598,7 +1030,7 @@ var indicator_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
598
1030
|
scrollHost = getScrollHost();
|
|
599
1031
|
scrollHost.addEventListener("scroll", handleScroll, { passive: true });
|
|
600
1032
|
}
|
|
601
|
-
window.addEventListener("resize",
|
|
1033
|
+
window.addEventListener("resize", updateLayout, { passive: true });
|
|
602
1034
|
observer = new MutationObserver(scheduleRefresh);
|
|
603
1035
|
observer.observe(props.target, {
|
|
604
1036
|
childList: true,
|
|
@@ -609,18 +1041,16 @@ var indicator_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
609
1041
|
observeHostSize();
|
|
610
1042
|
};
|
|
611
1043
|
/**
|
|
612
|
-
*
|
|
613
|
-
* @param
|
|
614
|
-
* @returns
|
|
1044
|
+
* 把轨道上的指针位置映射到最近的刻度,只用于悬停预览。
|
|
1045
|
+
* @param ratio 轨道 0–1 比例。
|
|
1046
|
+
* @returns 最近刻度序号。
|
|
615
1047
|
*/
|
|
616
|
-
const
|
|
617
|
-
if (!
|
|
618
|
-
const elements = [...rail.value.querySelectorAll(".docs-markdown-indicator__marker")];
|
|
1048
|
+
const getIndexByRatio = (ratio) => {
|
|
1049
|
+
if (!markers.value.length) return 0;
|
|
619
1050
|
let closestIndex = 0;
|
|
620
1051
|
let closestDistance = Number.POSITIVE_INFINITY;
|
|
621
|
-
|
|
622
|
-
const
|
|
623
|
-
const distance = Math.abs(event.clientY - (bounds.top + bounds.height / 2));
|
|
1052
|
+
markers.value.forEach((marker, index) => {
|
|
1053
|
+
const distance = Math.abs(marker.ratio - ratio);
|
|
624
1054
|
if (distance < closestDistance) {
|
|
625
1055
|
closestIndex = index;
|
|
626
1056
|
closestDistance = distance;
|
|
@@ -629,66 +1059,73 @@ var indicator_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
629
1059
|
return closestIndex;
|
|
630
1060
|
};
|
|
631
1061
|
/**
|
|
632
|
-
*
|
|
633
|
-
* @param
|
|
1062
|
+
* 读取指针相对轨道的比例。
|
|
1063
|
+
* @param event 当前指针事件。
|
|
1064
|
+
* @returns 0–1 比例。
|
|
1065
|
+
*/
|
|
1066
|
+
const getPointerRatio = (event) => {
|
|
1067
|
+
const bounds = rail.value?.getBoundingClientRect();
|
|
1068
|
+
if (!bounds) return 0;
|
|
1069
|
+
return getMinimapPointerRatio(event.clientY, bounds.top, bounds.height);
|
|
1070
|
+
};
|
|
1071
|
+
/**
|
|
1072
|
+
* 让摘要跟随指针,同时限制在指示器可视高度内。
|
|
634
1073
|
* @param clientY 指针的视口纵坐标。
|
|
635
1074
|
*/
|
|
636
|
-
const updatePreviewPosition = (
|
|
1075
|
+
const updatePreviewPosition = (clientY) => {
|
|
637
1076
|
const rootBounds = indicatorRoot.value?.getBoundingClientRect();
|
|
638
1077
|
const viewportBounds = viewport.value?.getBoundingClientRect();
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const markerBounds = marker.getBoundingClientRect();
|
|
642
|
-
const relativeY = (clientY ?? markerBounds.top + markerBounds.height / 2) - rootBounds.top;
|
|
643
|
-
previewTop.value = Math.min(viewportBounds.height - 48, Math.max(48, relativeY));
|
|
1078
|
+
if (!rootBounds || !viewportBounds) return;
|
|
1079
|
+
previewTop.value = Math.min(viewportBounds.height - 48, Math.max(48, clientY - rootBounds.top));
|
|
644
1080
|
};
|
|
645
1081
|
/**
|
|
646
|
-
*
|
|
647
|
-
* @param
|
|
1082
|
+
* 按轨道比例滚动正文,不改 hash。
|
|
1083
|
+
* @param ratio 轨道 0–1 比例。
|
|
648
1084
|
* @param behavior 滚动行为。
|
|
649
1085
|
*/
|
|
650
|
-
const
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
1086
|
+
const scrollToRatio = (ratio, behavior) => {
|
|
1087
|
+
const metrics = getScrollMetrics();
|
|
1088
|
+
const nextTop = ratio * Math.max(0, metrics.scrollHeight - metrics.clientHeight);
|
|
1089
|
+
if (usesParentScroller && parentScroller?.setScrollTop) {
|
|
1090
|
+
parentScroller.setScrollTop(nextTop);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
if (scrollHost instanceof HTMLElement) {
|
|
657
1094
|
scrollHost.scrollTo({
|
|
658
|
-
|
|
659
|
-
|
|
1095
|
+
behavior,
|
|
1096
|
+
top: nextTop
|
|
660
1097
|
});
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
window.scrollTo({
|
|
1101
|
+
behavior,
|
|
1102
|
+
top: nextTop
|
|
664
1103
|
});
|
|
665
|
-
activeIndex.value = index;
|
|
666
|
-
followActiveMarker(index);
|
|
667
1104
|
};
|
|
668
1105
|
/**
|
|
669
|
-
*
|
|
1106
|
+
* 指针移动时展示摘要;按下拖动时按比例 scrub 正文。
|
|
670
1107
|
* @param event 当前指针事件。
|
|
671
1108
|
*/
|
|
672
1109
|
const handlePointerMove = (event) => {
|
|
673
|
-
const
|
|
674
|
-
hoverIndex.value =
|
|
675
|
-
updatePreviewPosition(
|
|
676
|
-
if (dragging.value)
|
|
1110
|
+
const ratio = getPointerRatio(event);
|
|
1111
|
+
hoverIndex.value = getIndexByRatio(ratio);
|
|
1112
|
+
updatePreviewPosition(event.clientY);
|
|
1113
|
+
if (dragging.value) scrollToRatio(ratio, "auto");
|
|
677
1114
|
};
|
|
678
1115
|
/**
|
|
679
|
-
*
|
|
1116
|
+
* 开始 scrub:点击即按比例定位,可拖动时再捕获指针。
|
|
680
1117
|
* @param event 当前指针事件。
|
|
681
1118
|
*/
|
|
682
1119
|
const handlePointerDown = (event) => {
|
|
683
1120
|
if (event.button !== 0) return;
|
|
1121
|
+
const ratio = getPointerRatio(event);
|
|
1122
|
+
hoverIndex.value = getIndexByRatio(ratio);
|
|
1123
|
+
updatePreviewPosition(event.clientY);
|
|
1124
|
+
scrollToRatio(ratio, "auto");
|
|
684
1125
|
dragging.value = props.options.draggable !== false;
|
|
685
|
-
const index = getIndexByPointer(event);
|
|
686
|
-
hoverIndex.value = index;
|
|
687
|
-
updatePreviewPosition(index, event.clientY);
|
|
688
1126
|
if (dragging.value) {
|
|
689
1127
|
captureTarget = event.currentTarget;
|
|
690
1128
|
captureTarget.setPointerCapture?.(event.pointerId);
|
|
691
|
-
scrollToMarker(index, "auto");
|
|
692
1129
|
event.preventDefault();
|
|
693
1130
|
}
|
|
694
1131
|
};
|
|
@@ -706,96 +1143,99 @@ var indicator_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
706
1143
|
if (!dragging.value) hoverIndex.value = void 0;
|
|
707
1144
|
};
|
|
708
1145
|
/**
|
|
709
|
-
*
|
|
710
|
-
* @param index 目标文档块序号。
|
|
711
|
-
*/
|
|
712
|
-
const handleClick = (index) => {
|
|
713
|
-
hoverIndex.value = index;
|
|
714
|
-
updatePreviewPosition(index);
|
|
715
|
-
scrollToMarker(index, "smooth");
|
|
716
|
-
};
|
|
717
|
-
/**
|
|
718
|
-
* 计算刻度位置、基础宽度和靠近指针时的鱼眼展开宽度。
|
|
1146
|
+
* 计算刻度在轨道上的位置、标题/正文宽度,以及靠近指针时的鱼眼展开。
|
|
719
1147
|
* @param index 当前刻度序号。
|
|
720
1148
|
* @returns 刻度的定位和宽度样式。
|
|
721
1149
|
*/
|
|
722
1150
|
const getMarkerStyle = (index) => {
|
|
1151
|
+
const marker = markers.value[index];
|
|
723
1152
|
const distance = typeof hoverIndex.value === "number" ? Math.abs(index - hoverIndex.value) : Number.POSITIVE_INFINITY;
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
1153
|
+
const base = marker?.isHeading ? 10 : 6;
|
|
1154
|
+
const expandedWidths = [
|
|
1155
|
+
base + 6,
|
|
1156
|
+
base + 4,
|
|
1157
|
+
base + 2,
|
|
1158
|
+
base + 1
|
|
1159
|
+
];
|
|
1160
|
+
const hoverWidth = distance <= 3 ? expandedWidths[distance] : base;
|
|
1161
|
+
const activeWidth = index === activeIndex.value ? base + 8 : base;
|
|
1162
|
+
return {
|
|
1163
|
+
top: `${(marker?.ratio || 0) * 100}%`,
|
|
1164
|
+
width: `${Math.max(hoverWidth, activeWidth)}px`
|
|
1165
|
+
};
|
|
730
1166
|
};
|
|
731
|
-
(0, vue.watch)([() => props.target, localeName], setupTarget, {
|
|
732
|
-
|
|
1167
|
+
(0, vue.watch)([() => props.target, localeName], setupTarget, {
|
|
1168
|
+
immediate: true,
|
|
1169
|
+
flush: "post"
|
|
1170
|
+
});
|
|
1171
|
+
(0, vue.watch)(indicatorRoot, (root) => {
|
|
1172
|
+
if (root) observeHostSize();
|
|
1173
|
+
}, { flush: "post" });
|
|
1174
|
+
(0, vue.watch)([
|
|
1175
|
+
() => props.options.height,
|
|
1176
|
+
() => props.options.position,
|
|
1177
|
+
viewport
|
|
1178
|
+
], () => {
|
|
733
1179
|
if (viewport.value && hostResizeObserver) hostResizeObserver.observe(viewport.value);
|
|
734
|
-
|
|
1180
|
+
updateLayout();
|
|
735
1181
|
});
|
|
736
1182
|
(0, vue.onBeforeUnmount)(() => {
|
|
737
1183
|
targetGeneration++;
|
|
738
1184
|
cleanupTarget();
|
|
739
1185
|
if (refreshFrame) cancelAnimationFrame(refreshFrame);
|
|
740
|
-
if (
|
|
1186
|
+
if (windowFrame) cancelAnimationFrame(windowFrame);
|
|
741
1187
|
});
|
|
742
1188
|
return (_ctx, _cache) => {
|
|
743
|
-
return markers.value.length
|
|
1189
|
+
return markers.value.length ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("aside", {
|
|
744
1190
|
key: 0,
|
|
745
1191
|
ref_key: "indicatorRoot",
|
|
746
1192
|
ref: indicatorRoot,
|
|
747
|
-
class: (0, vue.normalizeClass)(["docs-markdown-indicator", `is-${
|
|
1193
|
+
class: (0, vue.normalizeClass)(["docs-markdown-indicator", `is-${resolvedPosition.value}`]),
|
|
748
1194
|
style: (0, vue.normalizeStyle)(rootStyle.value),
|
|
749
1195
|
"aria-label": (0, vue.unref)(t)("markdown.indicator.label")
|
|
750
1196
|
}, [(0, vue.createElementVNode)("div", {
|
|
751
1197
|
ref_key: "viewport",
|
|
752
1198
|
ref: viewport,
|
|
753
|
-
class: "docs-markdown-indicator__viewport",
|
|
1199
|
+
class: (0, vue.normalizeClass)(["docs-markdown-indicator__viewport", { "is-dragging": dragging.value }]),
|
|
1200
|
+
role: "slider",
|
|
1201
|
+
"aria-orientation": "vertical",
|
|
1202
|
+
"aria-valuemin": 0,
|
|
1203
|
+
"aria-valuemax": 100,
|
|
1204
|
+
"aria-valuenow": scrollProgress.value,
|
|
1205
|
+
"aria-valuetext": `${scrollProgress.value}%`,
|
|
1206
|
+
style: (0, vue.normalizeStyle)(viewportStyle.value),
|
|
754
1207
|
onPointermove: handlePointerMove,
|
|
755
1208
|
onPointerleave: handlePointerLeave,
|
|
756
1209
|
onPointerdown: handlePointerDown,
|
|
757
1210
|
onPointerup: handlePointerUp,
|
|
758
1211
|
onPointercancel: handlePointerUp
|
|
759
|
-
}, [(0, vue.
|
|
760
|
-
ref_key: "
|
|
761
|
-
ref:
|
|
762
|
-
class: "docs-markdown-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
"
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
"is-active": index === activeIndex.value,
|
|
779
|
-
"is-hovered": index === hoverIndex.value
|
|
780
|
-
}]),
|
|
781
|
-
style: (0, vue.normalizeStyle)(getMarkerStyle(index)),
|
|
782
|
-
"aria-label": marker.ariaLabel,
|
|
783
|
-
onClick: ($event) => handleClick(index)
|
|
784
|
-
}, null, 14, _hoisted_3);
|
|
785
|
-
}), 128))])], 512)]),
|
|
786
|
-
_: 1
|
|
787
|
-
}, 512)], 544), __props.options.preview !== false && hoveredMarker.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
1212
|
+
}, [(0, vue.createElementVNode)("div", {
|
|
1213
|
+
ref_key: "rail",
|
|
1214
|
+
ref: rail,
|
|
1215
|
+
class: "docs-markdown-indicator__rail"
|
|
1216
|
+
}, [(0, vue.createElementVNode)("div", {
|
|
1217
|
+
class: "docs-markdown-indicator__window",
|
|
1218
|
+
style: (0, vue.normalizeStyle)(windowStyle.value)
|
|
1219
|
+
}, null, 4), ((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(markers.value, (marker, index) => {
|
|
1220
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
1221
|
+
key: marker.id,
|
|
1222
|
+
class: (0, vue.normalizeClass)(["docs-markdown-indicator__marker", {
|
|
1223
|
+
"is-heading": marker.isHeading,
|
|
1224
|
+
"is-hovered": index === hoverIndex.value,
|
|
1225
|
+
"is-active": index === activeIndex.value,
|
|
1226
|
+
"is-in-view": isMarkerInView(index)
|
|
1227
|
+
}]),
|
|
1228
|
+
style: (0, vue.normalizeStyle)(getMarkerStyle(index))
|
|
1229
|
+
}, null, 6);
|
|
1230
|
+
}), 128))], 512)], 46, _hoisted_2), __props.options.preview !== false && !dragging.value && hoveredMarker.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
788
1231
|
key: 0,
|
|
789
1232
|
class: "docs-markdown-indicator__preview",
|
|
790
1233
|
style: (0, vue.normalizeStyle)(previewStyle.value)
|
|
791
|
-
}, [(0, vue.createElementVNode)("div",
|
|
1234
|
+
}, [(0, vue.createElementVNode)("div", _hoisted_3, (0, vue.toDisplayString)(hoveredMarker.value.title), 1), hoveredMarker.value.content ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_4, (0, vue.toDisplayString)(hoveredMarker.value.content), 1)) : (0, vue.createCommentVNode)("v-if", true)], 4)) : (0, vue.createCommentVNode)("v-if", true)], 14, _hoisted_1)) : (0, vue.createCommentVNode)("v-if", true);
|
|
792
1235
|
};
|
|
793
1236
|
}
|
|
794
1237
|
});
|
|
795
1238
|
//#endregion
|
|
796
|
-
//#region packages/markdown/src/wrapper.vue?vue&type=script&setup=true&lang.ts
|
|
797
|
-
var _hoisted_1 = { class: "docs-markdown" };
|
|
798
|
-
//#endregion
|
|
799
1239
|
//#region packages/markdown/src/wrapper.vue
|
|
800
1240
|
var wrapper_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
801
1241
|
__name: "wrapper",
|
|
@@ -805,6 +1245,9 @@ var wrapper_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
805
1245
|
default: true
|
|
806
1246
|
},
|
|
807
1247
|
locale: {},
|
|
1248
|
+
theme: { default: "default" },
|
|
1249
|
+
playground: { default: () => ({}) },
|
|
1250
|
+
tabQuery: {},
|
|
808
1251
|
modelValue: {},
|
|
809
1252
|
value: {}
|
|
810
1253
|
},
|
|
@@ -819,13 +1262,19 @@ var wrapper_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
819
1262
|
if (props.indicator === false) return void 0;
|
|
820
1263
|
return typeof props.indicator === "object" ? props.indicator : {};
|
|
821
1264
|
});
|
|
1265
|
+
const resolvedTheme = (0, vue.computed)(() => props.theme === "traditional" ? "traditional" : "default");
|
|
1266
|
+
const injectedTabQuery = (0, vue.inject)(markdownTabQueryKey, null);
|
|
1267
|
+
const historyTabQuery = createHistoryTabQueryAdapter();
|
|
1268
|
+
const resolvedTabQuery = (0, vue.computed)(() => props.tabQuery || injectedTabQuery || historyTabQuery);
|
|
822
1269
|
const source = (0, vue.computed)(() => typeof props.modelValue === "string" ? props.modelValue : props.value);
|
|
823
1270
|
const markdownBinding = (0, vue.computed)(() => ({
|
|
824
1271
|
source: source.value,
|
|
825
|
-
locale: locale.value
|
|
1272
|
+
locale: locale.value,
|
|
1273
|
+
playground: props.playground,
|
|
1274
|
+
tabQuery: resolvedTabQuery.value
|
|
826
1275
|
}));
|
|
827
1276
|
return (_ctx, _cache) => {
|
|
828
|
-
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div",
|
|
1277
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", { class: (0, vue.normalizeClass)(["docs-markdown", `docs-markdown--${resolvedTheme.value}`]) }, [indicatorOptions.value && content.value ? ((0, vue.openBlock)(), (0, vue.createBlock)(indicator_default, {
|
|
829
1278
|
key: 0,
|
|
830
1279
|
target: content.value,
|
|
831
1280
|
options: indicatorOptions.value
|
|
@@ -833,10 +1282,15 @@ var wrapper_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
833
1282
|
ref_key: "content",
|
|
834
1283
|
ref: content,
|
|
835
1284
|
class: "docs-markdown-reset"
|
|
836
|
-
}, null, 512), [[(0, vue.unref)(vMarkdown), markdownBinding.value]])]);
|
|
1285
|
+
}, null, 512), [[(0, vue.unref)(vMarkdown), markdownBinding.value]])], 2);
|
|
837
1286
|
};
|
|
838
1287
|
}
|
|
839
1288
|
});
|
|
840
1289
|
//#endregion
|
|
841
1290
|
exports.Markdown = wrapper_default;
|
|
1291
|
+
exports.collectMarkdownHeadings = collectMarkdownHeadings;
|
|
1292
|
+
exports.createHistoryTabQueryAdapter = createHistoryTabQueryAdapter;
|
|
1293
|
+
exports.getMarkdownHeadingTitle = getMarkdownHeadingTitle;
|
|
1294
|
+
exports.markdownTabQueryKey = markdownTabQueryKey;
|
|
842
1295
|
exports.parseMarkdownSearchSections = parseMarkdownSearchSections;
|
|
1296
|
+
exports.toMarkdownTabId = toMarkdownTabId;
|