@deot/docs-markdown 1.1.0 → 1.1.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @deot/docs-markdown
2
2
 
3
- `@deot/docs-markdown` 提供 Vue 3 Markdown 渲染组件,内置文档指示器、标题锚点、链接识别、tip/warning 容器、代码高亮预览和按需加载的 Playground 容器。
3
+ `@deot/docs-markdown` 提供 Vue 3 Markdown 渲染组件,内置文档指示器、标题锚点、链接识别、tip/warning 容器、tabs 分栏、代码高亮预览和按需加载的 Playground 容器。
4
4
 
5
5
  ## 安装
6
6
 
@@ -73,6 +73,37 @@ const source = ref('# Hello @deot/docs');
73
73
  :::
74
74
  ````
75
75
 
76
+ ### Tabs 分栏
77
+
78
+ 每一栏用 **4 个反引号** 的 fence(info 为 `markdown <标题>`),这样栏内还能写普通 ` ``` ` 代码块。切换时写入 `?tab=<id>`(标题小写、空白变 `-`;非 ASCII 与标题锚点一致做 `encodeURIComponent`)。
79
+
80
+ `````markdown
81
+ :::tabs
82
+ ````markdown Linux
83
+ Linux 安装步骤,可含普通代码块:
84
+
85
+ ```ts
86
+ const value = 1;
87
+ ```
88
+ ````
89
+ ````markdown Android
90
+ Android 安装步骤。
91
+ ````
92
+ :::
93
+ `````
94
+
95
+ 栏内若再嵌套 `:::tip` / `:::playground` / `:::warning`,外层必须加长冒号(`::::tabs` … `::::`)。`markdown-it-container` 会按「足够长的纯 `:::` 行」闭合外层,内层结束标记会提前关掉 `:::tabs`。
96
+
97
+ ``````markdown
98
+ ::::tabs
99
+ ````markdown Demo
100
+ :::tip
101
+ 嵌套提示。
102
+ :::
103
+ ````
104
+ ::::
105
+ ``````
106
+
76
107
  ### 单文件 Playground
77
108
 
78
109
  ````markdown
@@ -119,6 +150,7 @@ createApp(App).mount('#app');
119
150
  支持的 `views` 值为 `runtime` 和 `files`。`viewport` 可为 `auto`、正数宽度或 `[width, height]`。
120
151
  `title` 为可选字符串,显示在运行时顶栏左侧;空串或不传不渲染。有标题时会生成锚点 id(可用 `#...` 跳转),规则与 markdown heading 一致;也可用 `id` 显式覆盖。
121
152
  `expandable` 为可选:`true` 展开到剩余视口,正数为展开目标高度(px);未配置时不显示展开控件。
153
+ `previewScroller` 为可选布尔值:开启后在 iframe 内用 `@deot/vc` Scroller 替换原生滚动条;会动态探测 `Scroller`,不存在则回退。
122
154
 
123
155
  Playground 会在浏览器里编译 Vue SFC 的 `<style lang="scss">` 以及独立 `.scss` / `.sass` 文件。下划线 partial(如 `_variables.scss`)只作为 `@use` 依赖,不会单独注入预览。
124
156
 
@@ -130,8 +162,10 @@ Playground 只在页面出现 fence 或 `:::playground` 时动态加载。使用
130
162
  | --- | --- |
131
163
  | `Markdown` | Vue Markdown 渲染组件。 |
132
164
  | `parseMarkdownSearchSections(content)` | 使用渲染器相同的标题锚点规则提取文档标题、正文和小节,供搜索索引使用。 |
165
+ | `toMarkdownTabId(title)` | 将 Tab 标题转为 `?tab=` id。 |
166
+ | `markdownTabQueryKey` / `createHistoryTabQueryAdapter` | 文档壳可 `provide` Router 版适配器;独立预览回退 history。 |
133
167
 
134
- 类型:`MarkdownSearchDocument`、`MarkdownSearchSection`、`MarkdownIndicatorConfig`、`MarkdownIndicatorOptions`、`MarkdownPlaygroundConfig`、`MarkdownPlaygroundMountProps`。
168
+ 类型:`MarkdownSearchDocument`、`MarkdownSearchSection`、`MarkdownIndicatorConfig`、`MarkdownIndicatorOptions`、`MarkdownPlaygroundConfig`、`MarkdownPlaygroundMountProps`、`MarkdownTabQueryAdapter`。
135
169
 
136
170
  底层 markdown-it 实例和内部指令不是包入口的公共导出。搜索解析结果不会包含代码围栏和 HTML 内容。
137
171
 
package/dist/index.cjs CHANGED
@@ -37,9 +37,11 @@ let _deot_helper_dom = require("@deot/helper-dom");
37
37
  //#region packages/markdown/src/markdown.ts
38
38
  var HTML_MD_SIGN = "md";
39
39
  var PLAYGROUND = "playground";
40
+ var TABS = "tabs";
40
41
  var TIP = "tip";
41
42
  var WARNING = "warning";
42
43
  var CALLOUT_FILLED_RE = /(^|\s)filled(\s|$)/;
44
+ var TAB_MARKDOWN_INFO_RE = /^markdown(?:\s+(.*))?$/i;
43
45
  var CALLOUT_INFO_ICON = [
44
46
  "<svg width=\"22\" height=\"28\" viewBox=\"0 0 22 28\" fill=\"none\" aria-hidden=\"true\">",
45
47
  "<circle class=\"docs-markdown-callout-icon__fill\" cx=\"11\" cy=\"14\" r=\"11\"></circle>",
@@ -72,9 +74,13 @@ config.options.html(true).typographer(true).linkify(true).end().plugin("anchor")
72
74
  placement: "before",
73
75
  symbol: "#"
74
76
  }) }]).end().plugin("container").use(($md) => {
75
- const reg = new RegExp(`^${PLAYGROUND}\\s*(.*)$`);
77
+ const playgroundReg = new RegExp(`^${PLAYGROUND}\\s*(.*)$`);
76
78
  $md.use(markdown_it_container.default, PLAYGROUND, { validate(params) {
77
- return params.trim().match(reg);
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());
78
84
  } });
79
85
  $md.use(markdown_it_container.default, TIP, { render: renderCallout(TIP, CALLOUT_INFO_ICON) });
80
86
  $md.use(markdown_it_container.default, WARNING, { render: renderCallout(WARNING, CALLOUT_WARNING_ICON) });
@@ -82,9 +88,33 @@ config.options.html(true).typographer(true).linkify(true).end().plugin("anchor")
82
88
  var md = config.toMd(markdown_it.default);
83
89
  var playgroundOpen = `container_${PLAYGROUND}_open`;
84
90
  var playgroundClose = `container_${PLAYGROUND}_close`;
91
+ var tabsOpen = `container_${TABS}_open`;
92
+ var tabsClose = `container_${TABS}_close`;
85
93
  var htmlCommentRE = /<!--([\s\S]*?)-->/g;
86
94
  var runtimeConfigRE = /<config\s+lang\s*=\s*["']json5["']\s*>([\s\S]*?)<\/config>/i;
87
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
+ };
88
118
  var PLAYGROUND_VIEWS = ["runtime", "files"];
89
119
  var isPlaygroundView = (value) => typeof value === "string" && PLAYGROUND_VIEWS.includes(value);
90
120
  var isRuntimeViewport = (viewport) => {
@@ -141,6 +171,11 @@ var validateTitleId = (propsData) => {
141
171
  if (typeof propsData.id === "string") return "";
142
172
  return "id 必须是字符串";
143
173
  };
174
+ var validatePreviewScroller = (propsData) => {
175
+ if (!("previewScroller" in propsData)) return "";
176
+ if (typeof propsData.previewScroller === "boolean") return "";
177
+ return "previewScroller 必须是布尔值";
178
+ };
144
179
  var parseRuntimeProps = (tokens) => {
145
180
  for (const token of tokens) {
146
181
  const sources = [];
@@ -162,7 +197,61 @@ var parseRuntimeProps = (tokens) => {
162
197
  return {};
163
198
  };
164
199
  var renderPlaygroundAttrs = (propsData) => `data-props="${md.utils.escapeHtml(JSON.stringify(propsData))}"`;
165
- md.core.ruler.after("block", "runtime-files", (state) => {
200
+ var renderTabsHtml = (panels) => {
201
+ const parts = ["<div class=\"docs-markdown-tabs\" md data-tabs>", "<div class=\"docs-markdown-tabs__nav\" data-tabs-nav></div>"];
202
+ panels.forEach((panel, index) => {
203
+ const hidden = index === 0 ? "" : " hidden";
204
+ const id = md.utils.escapeHtml(panel.id);
205
+ const title = md.utils.escapeHtml(panel.title);
206
+ parts.push(`<div class="docs-markdown-tabs__panel" role="tabpanel" data-tab="${id}" data-tab-title="${title}"${hidden}>`, md.render(panel.content), "</div>");
207
+ });
208
+ parts.push("</div>\n");
209
+ return parts.join("");
210
+ };
211
+ md.core.ruler.after("block", "markdown-tabs", (state) => {
212
+ const panelSources = Array.isArray(state.env?.tabsPanelSources) ? state.env.tabsPanelSources : void 0;
213
+ for (let index = 0; index < state.tokens.length; index++) {
214
+ if (state.tokens[index].type !== tabsOpen) continue;
215
+ let depth = 1;
216
+ let closeIndex = index + 1;
217
+ for (; closeIndex < state.tokens.length; closeIndex++) {
218
+ if (state.tokens[closeIndex].type === tabsOpen) depth++;
219
+ if (state.tokens[closeIndex].type === tabsClose) depth--;
220
+ if (depth === 0) break;
221
+ }
222
+ const fences = state.tokens.slice(index + 1, closeIndex).filter((token) => token.type === "fence");
223
+ const placeholder = new state.Token("html_block", "", 0);
224
+ placeholder.block = true;
225
+ if (!fences.length) placeholder.content = renderTabsError("至少需要声明一个分栏");
226
+ else {
227
+ const panels = [];
228
+ const seen = /* @__PURE__ */ new Set();
229
+ let error = "";
230
+ for (const fence of fences) {
231
+ const title = parseTabFenceTitle(fence.info);
232
+ const id = toMarkdownTabId(title);
233
+ if (!title || !id) {
234
+ error = "每个分栏都必须声明标题";
235
+ break;
236
+ }
237
+ if (seen.has(id)) {
238
+ error = `分栏 ${title} 的 id 重复`;
239
+ break;
240
+ }
241
+ seen.add(id);
242
+ panels.push({
243
+ id,
244
+ title,
245
+ content: fence.content
246
+ });
247
+ panelSources?.push(fence.content);
248
+ }
249
+ placeholder.content = error ? renderTabsError(error) : renderTabsHtml(panels);
250
+ }
251
+ state.tokens.splice(index, closeIndex - index + 1, placeholder);
252
+ }
253
+ });
254
+ md.core.ruler.after("markdown-tabs", "runtime-files", (state) => {
166
255
  for (let index = 0; index < state.tokens.length; index++) {
167
256
  if (state.tokens[index].type !== playgroundOpen) continue;
168
257
  let depth = 1;
@@ -177,7 +266,7 @@ md.core.ruler.after("block", "runtime-files", (state) => {
177
266
  const placeholder = new state.Token("html_block", "", 0);
178
267
  placeholder.block = true;
179
268
  const propsData = parseRuntimeProps(innerTokens);
180
- const propsError = validateRuntimeViews(propsData) || validateRuntimeViewport(propsData) || validatePreviewInset(propsData) || validateExpandable(propsData) || validateTitle(propsData) || validateTitleId(propsData);
269
+ const propsError = validateRuntimeViews(propsData) || validateRuntimeViewport(propsData) || validatePreviewInset(propsData) || validateExpandable(propsData) || validateTitle(propsData) || validateTitleId(propsData) || validatePreviewScroller(propsData);
181
270
  const propsAttr = renderPlaygroundAttrs(propsData);
182
271
  if (!fences.length) placeholder.content = renderPlaygroundError("至少需要声明一个代码文件");
183
272
  else if (propsError) placeholder.content = renderPlaygroundError(propsError);
@@ -215,7 +304,7 @@ md.core.ruler.after("block", "runtime-files", (state) => {
215
304
  });
216
305
  var renderAttrs = md.renderer.renderAttrs;
217
306
  md.renderer.renderAttrs = (token) => {
218
- const reg = new RegExp(`container_${PLAYGROUND}|fence|text`);
307
+ const reg = new RegExp(`container_${PLAYGROUND}|container_${TABS}|fence|text`);
219
308
  if (token.nesting !== -1 && token.type && !reg.test(token.type)) token.attrPush([HTML_MD_SIGN, ""]);
220
309
  return renderAttrs(token);
221
310
  };
@@ -243,32 +332,39 @@ var getInlineSearchText = (token) => {
243
332
  * @returns 文档标题、正文和按标题划分的小节。
244
333
  */
245
334
  var parseMarkdownSearchSections = (content) => {
246
- const tokens = md.parse(content, {});
247
335
  const sections = [];
248
336
  const documentParts = [];
249
337
  let activeSection;
250
- for (let index = 0; index < tokens.length; index++) {
251
- const token = tokens[index];
252
- if (token.type === "heading_open") {
253
- const inline = tokens[index + 1];
254
- if (inline?.type !== "inline") continue;
255
- const title = getInlineSearchText(inline);
256
- if (!title) continue;
257
- activeSection = {
258
- title,
259
- anchor: token.attrGet("id") || "",
260
- level: Number(token.tag.slice(1)) || 1,
261
- text: ""
262
- };
263
- sections.push(activeSection);
264
- continue;
265
- }
266
- if (token.type !== "inline" || tokens[index - 1]?.type === "heading_open") continue;
267
- const text = getInlineSearchText(token);
268
- if (!text) continue;
338
+ const consumeInline = (text) => {
339
+ if (!text) return;
269
340
  documentParts.push(text);
270
341
  if (activeSection) activeSection.text = normalizeSearchText(`${activeSection.text} ${text}`);
271
- }
342
+ };
343
+ const walk = (source, withHeadings) => {
344
+ const tabsPanelSources = [];
345
+ const tokens = md.parse(source, { tabsPanelSources });
346
+ for (let index = 0; index < tokens.length; index++) {
347
+ const token = tokens[index];
348
+ if (withHeadings && token.type === "heading_open") {
349
+ const inline = tokens[index + 1];
350
+ if (inline?.type !== "inline") continue;
351
+ const title = getInlineSearchText(inline);
352
+ if (!title) continue;
353
+ activeSection = {
354
+ title,
355
+ anchor: token.attrGet("id") || "",
356
+ level: Number(token.tag.slice(1)) || 1,
357
+ text: ""
358
+ };
359
+ sections.push(activeSection);
360
+ continue;
361
+ }
362
+ if (token.type !== "inline" || tokens[index - 1]?.type === "heading_open") continue;
363
+ consumeInline(getInlineSearchText(token));
364
+ }
365
+ for (const panelSource of tabsPanelSources) walk(panelSource, false);
366
+ };
367
+ walk(content, true);
272
368
  return {
273
369
  title: sections.find((section) => section.level === 1)?.title || sections[0]?.title || "",
274
370
  text: normalizeSearchText(documentParts.join(" ")),
@@ -276,11 +372,137 @@ var parseMarkdownSearchSections = (content) => {
276
372
  };
277
373
  };
278
374
  //#endregion
375
+ //#region packages/markdown/src/tab-query.ts
376
+ var markdownTabQueryKey = Symbol("docs-markdown-tab-query");
377
+ var TAB_QUERY = "tab";
378
+ /**
379
+ * 不依赖 Vue Router 的默认适配器:读写 `location.search` 的 `tab`,
380
+ * 并用 `history.replaceState` 更新(供 examples / 单测使用)。
381
+ * @returns 可注入 TabsNav 的 get/set/subscribe 适配器。
382
+ */
383
+ var createHistoryTabQueryAdapter = () => {
384
+ const read = () => {
385
+ try {
386
+ return new URLSearchParams(window.location.search).get(TAB_QUERY) || "";
387
+ } catch {
388
+ return "";
389
+ }
390
+ };
391
+ const write = (tab) => {
392
+ try {
393
+ const url = new URL(window.location.href);
394
+ if (tab) url.searchParams.set(TAB_QUERY, tab);
395
+ else url.searchParams.delete(TAB_QUERY);
396
+ window.history.replaceState(window.history.state, "", `${url.pathname}${url.search}${url.hash}`);
397
+ } catch {}
398
+ };
399
+ return {
400
+ get: read,
401
+ set: write,
402
+ subscribe: (listener) => {
403
+ const onPopState = () => listener();
404
+ window.addEventListener("popstate", onPopState);
405
+ return () => window.removeEventListener("popstate", onPopState);
406
+ }
407
+ };
408
+ };
409
+ //#endregion
410
+ //#region packages/markdown/src/tabs-nav.vue?vue&type=script&setup=true&lang.ts
411
+ var _hoisted_1$1 = ["aria-label"];
412
+ var _hoisted_2$1 = [
413
+ "id",
414
+ "aria-selected",
415
+ "aria-controls",
416
+ "tabindex",
417
+ "onClick"
418
+ ];
419
+ //#endregion
420
+ //#region packages/markdown/src/tabs-nav.vue
421
+ var tabs_nav_default = /* @__PURE__ */ (0, vue.defineComponent)({
422
+ __name: "tabs-nav",
423
+ props: {
424
+ items: {},
425
+ label: {},
426
+ adapter: {},
427
+ root: {},
428
+ groupId: {}
429
+ },
430
+ setup(__props) {
431
+ const props = __props;
432
+ const activeId = (0, vue.ref)("");
433
+ let unsubscribe;
434
+ const tabButtonId = (id) => `${props.groupId}-tab-${id}`;
435
+ const tabPanelId = (id) => `${props.groupId}-panel-${id}`;
436
+ const resolveActive = () => {
437
+ const fromQuery = props.adapter.get();
438
+ const matched = props.items.find((item) => item.id === fromQuery);
439
+ activeId.value = matched?.id || props.items[0]?.id || "";
440
+ };
441
+ const syncPanels = () => {
442
+ props.root.querySelectorAll(".docs-markdown-tabs__panel").forEach((panel) => {
443
+ const id = panel.dataset.tab || "";
444
+ const selected = id === activeId.value;
445
+ panel.hidden = !selected;
446
+ panel.setAttribute("aria-labelledby", tabButtonId(id));
447
+ if (selected) {
448
+ panel.removeAttribute("hidden");
449
+ panel.id = tabPanelId(id);
450
+ } else {
451
+ panel.setAttribute("hidden", "");
452
+ panel.id = tabPanelId(id);
453
+ }
454
+ });
455
+ };
456
+ const select = (id) => {
457
+ if (!id) return;
458
+ activeId.value = id;
459
+ props.adapter.set(id);
460
+ syncPanels();
461
+ };
462
+ (0, vue.onMounted)(() => {
463
+ resolveActive();
464
+ syncPanels();
465
+ unsubscribe = props.adapter.subscribe?.(() => {
466
+ resolveActive();
467
+ syncPanels();
468
+ });
469
+ });
470
+ (0, vue.onBeforeUnmount)(() => {
471
+ unsubscribe?.();
472
+ unsubscribe = void 0;
473
+ });
474
+ (0, vue.watch)(() => props.items.map((item) => item.id).join("\0"), () => {
475
+ resolveActive();
476
+ syncPanels();
477
+ });
478
+ return (_ctx, _cache) => {
479
+ return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
480
+ class: "docs-markdown-tabs__list",
481
+ role: "tablist",
482
+ "aria-label": __props.label
483
+ }, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.items, (item) => {
484
+ return (0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
485
+ key: item.id,
486
+ type: "button",
487
+ class: (0, vue.normalizeClass)(["docs-markdown-tabs__tab", { "docs-markdown-tabs__tab--active": item.id === activeId.value }]),
488
+ role: "tab",
489
+ id: tabButtonId(item.id),
490
+ "aria-selected": item.id === activeId.value,
491
+ "aria-controls": tabPanelId(item.id),
492
+ tabindex: item.id === activeId.value ? 0 : -1,
493
+ onClick: ($event) => select(item.id)
494
+ }, (0, vue.toDisplayString)(item.title), 11, _hoisted_2$1);
495
+ }), 128))], 8, _hoisted_1$1);
496
+ };
497
+ }
498
+ });
499
+ //#endregion
279
500
  //#region packages/markdown/src/directive.ts
280
501
  var mountedApps = /* @__PURE__ */ new WeakMap();
281
502
  var renderVersions = /* @__PURE__ */ new WeakMap();
282
503
  var playgroundModule;
283
504
  var loadPlayground = () => playgroundModule ||= import("@deot/docs-playground");
505
+ var defaultTabQuery = createHistoryTabQueryAdapter();
284
506
  var cleanup = (el) => {
285
507
  for (const app of mountedApps.get(el) || []) app.unmount();
286
508
  mountedApps.delete(el);
@@ -289,12 +511,36 @@ var resolveBlockLanguage = (block) => {
289
511
  return [...block.classList].find((className) => className.startsWith("language-"))?.slice(9) || "";
290
512
  };
291
513
  var serializePlaygroundDefaults = (value) => JSON.stringify(value || {});
514
+ var mountTabs = (el, apps, locale, adapter) => {
515
+ const label = locale.markdown?.tabs?.label || "Tabs";
516
+ el.querySelectorAll("[data-tabs]").forEach((root, index) => {
517
+ const nav = root.querySelector("[data-tabs-nav]");
518
+ if (!nav) return;
519
+ const items = [...root.querySelectorAll(".docs-markdown-tabs__panel")].map((panel) => ({
520
+ id: panel.dataset.tab || "",
521
+ title: panel.dataset.tabTitle || panel.dataset.tab || ""
522
+ })).filter((item) => item.id && item.title);
523
+ if (!items.length) return;
524
+ const groupId = `docs-markdown-tabs-${index}`;
525
+ const app = (0, vue.createApp)({ render: () => (0, vue.h)(tabs_nav_default, {
526
+ items,
527
+ label,
528
+ adapter,
529
+ root,
530
+ groupId
531
+ }) });
532
+ app.mount(nav);
533
+ apps.push(app);
534
+ });
535
+ };
292
536
  var render = async (el, binding) => {
293
537
  cleanup(el);
294
538
  const version = (renderVersions.get(el) || 0) + 1;
295
539
  renderVersions.set(el, version);
296
540
  el.innerHTML = binding.value.source ? Markdown.render(binding.value.source) : "";
297
541
  const apps = [];
542
+ const adapter = binding.value.tabQuery || defaultTabQuery;
543
+ mountTabs(el, apps, binding.value.locale, adapter);
298
544
  const blocks = el.querySelectorAll("pre code");
299
545
  const playgrounds = el.querySelectorAll("div[data-playground]");
300
546
  if (!blocks.length && !playgrounds.length) {
@@ -344,7 +590,7 @@ var render = async (el, binding) => {
344
590
  mountedApps.set(el, apps);
345
591
  };
346
592
  var update = (el, binding) => {
347
- if (binding.value.source !== binding.oldValue?.source || binding.value.locale !== binding.oldValue?.locale || serializePlaygroundDefaults(binding.value.playground) !== serializePlaygroundDefaults(binding.oldValue?.playground)) render(el, binding);
593
+ 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);
348
594
  };
349
595
  var vMarkdown = {
350
596
  mounted: (el, binding) => void render(el, binding),
@@ -455,7 +701,13 @@ var getIndicatorInlineOffset = (root, rail, side, viewportWidth) => {
455
701
  //#endregion
456
702
  //#region packages/markdown/src/headings.ts
457
703
  var HEADING_SELECTOR = "h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]";
458
- var SKIP_CLOSEST = "pre, .docs-markdown-code-preview, [data-playground][data-code], [data-playground][data-files]";
704
+ var SKIP_CLOSEST = [
705
+ "pre",
706
+ ".docs-markdown-code-preview",
707
+ "[data-playground][data-code]",
708
+ "[data-playground][data-files]",
709
+ ".docs-markdown-tabs__panel[hidden]"
710
+ ].join(", ");
459
711
  /**
460
712
  * 去掉 markdown-it-anchor 的井号后,读取标题的可见文案。
461
713
  * @param element 当前标题节点。
@@ -1000,6 +1252,7 @@ var wrapper_default = /* @__PURE__ */ (0, vue.defineComponent)({
1000
1252
  locale: {},
1001
1253
  theme: { default: "default" },
1002
1254
  playground: { default: () => ({}) },
1255
+ tabQuery: {},
1003
1256
  modelValue: {},
1004
1257
  value: {}
1005
1258
  },
@@ -1015,11 +1268,15 @@ var wrapper_default = /* @__PURE__ */ (0, vue.defineComponent)({
1015
1268
  return typeof props.indicator === "object" ? props.indicator : {};
1016
1269
  });
1017
1270
  const resolvedTheme = (0, vue.computed)(() => props.theme === "traditional" ? "traditional" : "default");
1271
+ const injectedTabQuery = (0, vue.inject)(markdownTabQueryKey, null);
1272
+ const historyTabQuery = createHistoryTabQueryAdapter();
1273
+ const resolvedTabQuery = (0, vue.computed)(() => props.tabQuery || injectedTabQuery || historyTabQuery);
1018
1274
  const source = (0, vue.computed)(() => typeof props.modelValue === "string" ? props.modelValue : props.value);
1019
1275
  const markdownBinding = (0, vue.computed)(() => ({
1020
1276
  source: source.value,
1021
1277
  locale: locale.value,
1022
- playground: props.playground
1278
+ playground: props.playground,
1279
+ tabQuery: resolvedTabQuery.value
1023
1280
  }));
1024
1281
  return (_ctx, _cache) => {
1025
1282
  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, {
@@ -1037,5 +1294,8 @@ var wrapper_default = /* @__PURE__ */ (0, vue.defineComponent)({
1037
1294
  //#endregion
1038
1295
  exports.Markdown = wrapper_default;
1039
1296
  exports.collectMarkdownHeadings = collectMarkdownHeadings;
1297
+ exports.createHistoryTabQueryAdapter = createHistoryTabQueryAdapter;
1040
1298
  exports.getMarkdownHeadingTitle = getMarkdownHeadingTitle;
1299
+ exports.markdownTabQueryKey = markdownTabQueryKey;
1041
1300
  exports.parseMarkdownSearchSections = parseMarkdownSearchSections;
1301
+ exports.toMarkdownTabId = toMarkdownTabId;
package/dist/index.d.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  import { ComponentOptionsMixin } from 'vue';
2
2
  import { ComponentProvideOptions } from 'vue';
3
3
  import { DefineComponent } from 'vue';
4
+ import type { InjectionKey } from 'vue';
4
5
  import type { Language } from '@deot/docs-locale';
5
6
  import type { PlaygroundExpandable } from '@deot/docs-playground';
6
7
  import type { PlaygroundFiles } from '@deot/docs-playground';
7
8
  import type { PlaygroundOptions } from '@deot/docs-playground';
8
9
  import type { PlaygroundPreviewInset } from '@deot/docs-playground';
9
10
  import type { PlaygroundPreviewOptions } from '@deot/docs-playground';
11
+ import type { PlaygroundPreviewScroller } from '@deot/docs-playground';
10
12
  import type { PlaygroundView } from '@deot/docs-playground';
11
13
  import type { PlaygroundViewport } from '@deot/docs-playground';
12
14
  import { PublicProps } from 'vue';
@@ -28,12 +30,15 @@ declare type __VLS_Props = {
28
30
  locale?: Language;
29
31
  theme?: MarkdownTheme;
30
32
  playground?: MarkdownPlaygroundConfig;
33
+ tabQuery?: MarkdownTabQueryAdapter;
31
34
  modelValue?: string;
32
35
  value?: string;
33
36
  };
34
37
 
35
38
  export declare const collectMarkdownHeadings: (root: ParentNode | null | undefined) => MarkdownHeading[];
36
39
 
40
+ export declare const createHistoryTabQueryAdapter: () => MarkdownTabQueryAdapter;
41
+
37
42
  export declare const getMarkdownHeadingTitle: (element: HTMLElement) => string;
38
43
 
39
44
  export declare const Markdown: typeof __VLS_export;
@@ -66,6 +71,7 @@ export declare interface MarkdownPlaygroundConfig {
66
71
  options?: PlaygroundOptions;
67
72
  previewInset?: PlaygroundPreviewInset;
68
73
  expandable?: PlaygroundExpandable;
74
+ previewScroller?: PlaygroundPreviewScroller;
69
75
  previewOptions?: PlaygroundPreviewOptions;
70
76
  }
71
77
 
@@ -93,8 +99,18 @@ export declare interface MarkdownSearchSection {
93
99
  text: string;
94
100
  }
95
101
 
102
+ export declare interface MarkdownTabQueryAdapter {
103
+ get: () => string;
104
+ set: (tab: string) => void;
105
+ subscribe?: (listener: () => void) => () => void;
106
+ }
107
+
108
+ export declare const markdownTabQueryKey: InjectionKey<MarkdownTabQueryAdapter>;
109
+
96
110
  export declare type MarkdownTheme = 'default' | 'traditional';
97
111
 
98
112
  export declare const parseMarkdownSearchSections: (content: string) => MarkdownSearchDocument;
99
113
 
114
+ export declare const toMarkdownTabId: (title: string) => string;
115
+
100
116
  export { }