@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 +36 -2
- package/dist/index.cjs +289 -29
- package/dist/index.d.ts +16 -0
- package/dist/index.js +288 -31
- package/dist/index.style.css +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @deot/docs-markdown
|
|
2
2
|
|
|
3
|
-
`@deot/docs-markdown` 提供 Vue 3 Markdown 渲染组件,内置文档指示器、标题锚点、链接识别、tip/warning
|
|
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
|
|
77
|
+
const playgroundReg = new RegExp(`^${PLAYGROUND}\\s*(.*)$`);
|
|
76
78
|
$md.use(markdown_it_container.default, PLAYGROUND, { validate(params) {
|
|
77
|
-
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());
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
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 =
|
|
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 { }
|