@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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Fragment, computed, createApp, createBlock, createCommentVNode, createElementBlock, createElementVNode, defineComponent, h, inject, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, openBlock, ref, renderList, shallowRef, toDisplayString, unref, watch, withDirectives } from "vue";
1
+ import { Fragment, computed, createApp, createBlock, createCommentVNode, createElementBlock, createElementVNode, defineComponent, h, inject, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, ref, renderList, shallowRef, toDisplayString, unref, watch, withDirectives } from "vue";
2
2
  import { provideLocale, useLocale } from "@deot/docs-locale";
3
3
  import Config from "markdown-it-chain";
4
4
  import anchor from "markdown-it-anchor";
@@ -9,9 +9,11 @@ import { getScroller } from "@deot/helper-dom";
9
9
  //#region packages/markdown/src/markdown.ts
10
10
  var HTML_MD_SIGN = "md";
11
11
  var PLAYGROUND = "playground";
12
+ var TABS = "tabs";
12
13
  var TIP = "tip";
13
14
  var WARNING = "warning";
14
15
  var CALLOUT_FILLED_RE = /(^|\s)filled(\s|$)/;
16
+ var TAB_MARKDOWN_INFO_RE = /^markdown(?:\s+(.*))?$/i;
15
17
  var CALLOUT_INFO_ICON = [
16
18
  "<svg width=\"22\" height=\"28\" viewBox=\"0 0 22 28\" fill=\"none\" aria-hidden=\"true\">",
17
19
  "<circle class=\"docs-markdown-callout-icon__fill\" cx=\"11\" cy=\"14\" r=\"11\"></circle>",
@@ -44,9 +46,13 @@ config.options.html(true).typographer(true).linkify(true).end().plugin("anchor")
44
46
  placement: "before",
45
47
  symbol: "#"
46
48
  }) }]).end().plugin("container").use(($md) => {
47
- const reg = new RegExp(`^${PLAYGROUND}\\s*(.*)$`);
49
+ const playgroundReg = new RegExp(`^${PLAYGROUND}\\s*(.*)$`);
48
50
  $md.use(mdContainer, PLAYGROUND, { validate(params) {
49
- return params.trim().match(reg);
51
+ return params.trim().match(playgroundReg);
52
+ } });
53
+ const tabsReg = new RegExp(`^${TABS}\\s*$`);
54
+ $md.use(mdContainer, TABS, { validate(params) {
55
+ return tabsReg.test(params.trim());
50
56
  } });
51
57
  $md.use(mdContainer, TIP, { render: renderCallout(TIP, CALLOUT_INFO_ICON) });
52
58
  $md.use(mdContainer, WARNING, { render: renderCallout(WARNING, CALLOUT_WARNING_ICON) });
@@ -54,9 +60,33 @@ config.options.html(true).typographer(true).linkify(true).end().plugin("anchor")
54
60
  var md = config.toMd(markdownIt);
55
61
  var playgroundOpen = `container_${PLAYGROUND}_open`;
56
62
  var playgroundClose = `container_${PLAYGROUND}_close`;
63
+ var tabsOpen = `container_${TABS}_open`;
64
+ var tabsClose = `container_${TABS}_close`;
57
65
  var htmlCommentRE = /<!--([\s\S]*?)-->/g;
58
66
  var runtimeConfigRE = /<config\s+lang\s*=\s*["']json5["']\s*>([\s\S]*?)<\/config>/i;
59
67
  var renderPlaygroundError = (message) => `<div class="docs-playground-error">PLAYGROUND: ${md.utils.escapeHtml(message)}</div>\n`;
68
+ var renderTabsError = (message) => `<div class="docs-markdown-tabs-error">TABS: ${md.utils.escapeHtml(message)}</div>\n`;
69
+ /**
70
+ * 从 fence info 解析 Tab 显示名:去掉可选的 `markdown` 语言前缀。
71
+ * @param info fence 的 info 字符串,如 `markdown Linux` 或 `Android`。
72
+ * @returns 显示标题(可能为空)。
73
+ */
74
+ var parseTabFenceTitle = (info) => {
75
+ const trimmed = info.trim();
76
+ const match = trimmed.match(TAB_MARKDOWN_INFO_RE);
77
+ if (match) return (match[1] || "").trim();
78
+ return trimmed;
79
+ };
80
+ /**
81
+ * 生成 `?tab=` 使用的 id:小写、空白变 `-`;含非 ASCII 时与标题锚点一致做 encodeURIComponent。
82
+ * @param title Tab 显示名。
83
+ * @returns 可写入 query 的 id;标题为空时返回空串。
84
+ */
85
+ var toMarkdownTabId = (title) => {
86
+ const normalized = title.trim().toLowerCase().replace(/\s+/gu, "-");
87
+ if (!normalized) return "";
88
+ return /[^\x20-\x7E]/u.test(normalized) ? encodeURIComponent(normalized) : normalized;
89
+ };
60
90
  var PLAYGROUND_VIEWS = ["runtime", "files"];
61
91
  var isPlaygroundView = (value) => typeof value === "string" && PLAYGROUND_VIEWS.includes(value);
62
92
  var isRuntimeViewport = (viewport) => {
@@ -113,6 +143,11 @@ var validateTitleId = (propsData) => {
113
143
  if (typeof propsData.id === "string") return "";
114
144
  return "id 必须是字符串";
115
145
  };
146
+ var validatePreviewScroller = (propsData) => {
147
+ if (!("previewScroller" in propsData)) return "";
148
+ if (typeof propsData.previewScroller === "boolean") return "";
149
+ return "previewScroller 必须是布尔值";
150
+ };
116
151
  var parseRuntimeProps = (tokens) => {
117
152
  for (const token of tokens) {
118
153
  const sources = [];
@@ -134,7 +169,61 @@ var parseRuntimeProps = (tokens) => {
134
169
  return {};
135
170
  };
136
171
  var renderPlaygroundAttrs = (propsData) => `data-props="${md.utils.escapeHtml(JSON.stringify(propsData))}"`;
137
- md.core.ruler.after("block", "runtime-files", (state) => {
172
+ var renderTabsHtml = (panels) => {
173
+ const parts = ["<div class=\"docs-markdown-tabs\" md data-tabs>", "<div class=\"docs-markdown-tabs__nav\" data-tabs-nav></div>"];
174
+ panels.forEach((panel, index) => {
175
+ const hidden = index === 0 ? "" : " hidden";
176
+ const id = md.utils.escapeHtml(panel.id);
177
+ const title = md.utils.escapeHtml(panel.title);
178
+ parts.push(`<div class="docs-markdown-tabs__panel" role="tabpanel" data-tab="${id}" data-tab-title="${title}"${hidden}>`, md.render(panel.content), "</div>");
179
+ });
180
+ parts.push("</div>\n");
181
+ return parts.join("");
182
+ };
183
+ md.core.ruler.after("block", "markdown-tabs", (state) => {
184
+ const panelSources = Array.isArray(state.env?.tabsPanelSources) ? state.env.tabsPanelSources : void 0;
185
+ for (let index = 0; index < state.tokens.length; index++) {
186
+ if (state.tokens[index].type !== tabsOpen) continue;
187
+ let depth = 1;
188
+ let closeIndex = index + 1;
189
+ for (; closeIndex < state.tokens.length; closeIndex++) {
190
+ if (state.tokens[closeIndex].type === tabsOpen) depth++;
191
+ if (state.tokens[closeIndex].type === tabsClose) depth--;
192
+ if (depth === 0) break;
193
+ }
194
+ const fences = state.tokens.slice(index + 1, closeIndex).filter((token) => token.type === "fence");
195
+ const placeholder = new state.Token("html_block", "", 0);
196
+ placeholder.block = true;
197
+ if (!fences.length) placeholder.content = renderTabsError("至少需要声明一个分栏");
198
+ else {
199
+ const panels = [];
200
+ const seen = /* @__PURE__ */ new Set();
201
+ let error = "";
202
+ for (const fence of fences) {
203
+ const title = parseTabFenceTitle(fence.info);
204
+ const id = toMarkdownTabId(title);
205
+ if (!title || !id) {
206
+ error = "每个分栏都必须声明标题";
207
+ break;
208
+ }
209
+ if (seen.has(id)) {
210
+ error = `分栏 ${title} 的 id 重复`;
211
+ break;
212
+ }
213
+ seen.add(id);
214
+ panels.push({
215
+ id,
216
+ title,
217
+ content: fence.content
218
+ });
219
+ panelSources?.push(fence.content);
220
+ }
221
+ placeholder.content = error ? renderTabsError(error) : renderTabsHtml(panels);
222
+ }
223
+ state.tokens.splice(index, closeIndex - index + 1, placeholder);
224
+ }
225
+ });
226
+ md.core.ruler.after("markdown-tabs", "runtime-files", (state) => {
138
227
  for (let index = 0; index < state.tokens.length; index++) {
139
228
  if (state.tokens[index].type !== playgroundOpen) continue;
140
229
  let depth = 1;
@@ -149,7 +238,7 @@ md.core.ruler.after("block", "runtime-files", (state) => {
149
238
  const placeholder = new state.Token("html_block", "", 0);
150
239
  placeholder.block = true;
151
240
  const propsData = parseRuntimeProps(innerTokens);
152
- const propsError = validateRuntimeViews(propsData) || validateRuntimeViewport(propsData) || validatePreviewInset(propsData) || validateExpandable(propsData) || validateTitle(propsData) || validateTitleId(propsData);
241
+ const propsError = validateRuntimeViews(propsData) || validateRuntimeViewport(propsData) || validatePreviewInset(propsData) || validateExpandable(propsData) || validateTitle(propsData) || validateTitleId(propsData) || validatePreviewScroller(propsData);
153
242
  const propsAttr = renderPlaygroundAttrs(propsData);
154
243
  if (!fences.length) placeholder.content = renderPlaygroundError("至少需要声明一个代码文件");
155
244
  else if (propsError) placeholder.content = renderPlaygroundError(propsError);
@@ -187,7 +276,7 @@ md.core.ruler.after("block", "runtime-files", (state) => {
187
276
  });
188
277
  var renderAttrs = md.renderer.renderAttrs;
189
278
  md.renderer.renderAttrs = (token) => {
190
- const reg = new RegExp(`container_${PLAYGROUND}|fence|text`);
279
+ const reg = new RegExp(`container_${PLAYGROUND}|container_${TABS}|fence|text`);
191
280
  if (token.nesting !== -1 && token.type && !reg.test(token.type)) token.attrPush([HTML_MD_SIGN, ""]);
192
281
  return renderAttrs(token);
193
282
  };
@@ -215,32 +304,39 @@ var getInlineSearchText = (token) => {
215
304
  * @returns 文档标题、正文和按标题划分的小节。
216
305
  */
217
306
  var parseMarkdownSearchSections = (content) => {
218
- const tokens = md.parse(content, {});
219
307
  const sections = [];
220
308
  const documentParts = [];
221
309
  let activeSection;
222
- for (let index = 0; index < tokens.length; index++) {
223
- const token = tokens[index];
224
- if (token.type === "heading_open") {
225
- const inline = tokens[index + 1];
226
- if (inline?.type !== "inline") continue;
227
- const title = getInlineSearchText(inline);
228
- if (!title) continue;
229
- activeSection = {
230
- title,
231
- anchor: token.attrGet("id") || "",
232
- level: Number(token.tag.slice(1)) || 1,
233
- text: ""
234
- };
235
- sections.push(activeSection);
236
- continue;
237
- }
238
- if (token.type !== "inline" || tokens[index - 1]?.type === "heading_open") continue;
239
- const text = getInlineSearchText(token);
240
- if (!text) continue;
310
+ const consumeInline = (text) => {
311
+ if (!text) return;
241
312
  documentParts.push(text);
242
313
  if (activeSection) activeSection.text = normalizeSearchText(`${activeSection.text} ${text}`);
243
- }
314
+ };
315
+ const walk = (source, withHeadings) => {
316
+ const tabsPanelSources = [];
317
+ const tokens = md.parse(source, { tabsPanelSources });
318
+ for (let index = 0; index < tokens.length; index++) {
319
+ const token = tokens[index];
320
+ if (withHeadings && token.type === "heading_open") {
321
+ const inline = tokens[index + 1];
322
+ if (inline?.type !== "inline") continue;
323
+ const title = getInlineSearchText(inline);
324
+ if (!title) continue;
325
+ activeSection = {
326
+ title,
327
+ anchor: token.attrGet("id") || "",
328
+ level: Number(token.tag.slice(1)) || 1,
329
+ text: ""
330
+ };
331
+ sections.push(activeSection);
332
+ continue;
333
+ }
334
+ if (token.type !== "inline" || tokens[index - 1]?.type === "heading_open") continue;
335
+ consumeInline(getInlineSearchText(token));
336
+ }
337
+ for (const panelSource of tabsPanelSources) walk(panelSource, false);
338
+ };
339
+ walk(content, true);
244
340
  return {
245
341
  title: sections.find((section) => section.level === 1)?.title || sections[0]?.title || "",
246
342
  text: normalizeSearchText(documentParts.join(" ")),
@@ -248,11 +344,137 @@ var parseMarkdownSearchSections = (content) => {
248
344
  };
249
345
  };
250
346
  //#endregion
347
+ //#region packages/markdown/src/tab-query.ts
348
+ var markdownTabQueryKey = Symbol("docs-markdown-tab-query");
349
+ var TAB_QUERY = "tab";
350
+ /**
351
+ * 不依赖 Vue Router 的默认适配器:读写 `location.search` 的 `tab`,
352
+ * 并用 `history.replaceState` 更新(供 examples / 单测使用)。
353
+ * @returns 可注入 TabsNav 的 get/set/subscribe 适配器。
354
+ */
355
+ var createHistoryTabQueryAdapter = () => {
356
+ const read = () => {
357
+ try {
358
+ return new URLSearchParams(window.location.search).get(TAB_QUERY) || "";
359
+ } catch {
360
+ return "";
361
+ }
362
+ };
363
+ const write = (tab) => {
364
+ try {
365
+ const url = new URL(window.location.href);
366
+ if (tab) url.searchParams.set(TAB_QUERY, tab);
367
+ else url.searchParams.delete(TAB_QUERY);
368
+ window.history.replaceState(window.history.state, "", `${url.pathname}${url.search}${url.hash}`);
369
+ } catch {}
370
+ };
371
+ return {
372
+ get: read,
373
+ set: write,
374
+ subscribe: (listener) => {
375
+ const onPopState = () => listener();
376
+ window.addEventListener("popstate", onPopState);
377
+ return () => window.removeEventListener("popstate", onPopState);
378
+ }
379
+ };
380
+ };
381
+ //#endregion
382
+ //#region packages/markdown/src/tabs-nav.vue?vue&type=script&setup=true&lang.ts
383
+ var _hoisted_1$1 = ["aria-label"];
384
+ var _hoisted_2$1 = [
385
+ "id",
386
+ "aria-selected",
387
+ "aria-controls",
388
+ "tabindex",
389
+ "onClick"
390
+ ];
391
+ //#endregion
392
+ //#region packages/markdown/src/tabs-nav.vue
393
+ var tabs_nav_default = /* @__PURE__ */ defineComponent({
394
+ __name: "tabs-nav",
395
+ props: {
396
+ items: {},
397
+ label: {},
398
+ adapter: {},
399
+ root: {},
400
+ groupId: {}
401
+ },
402
+ setup(__props) {
403
+ const props = __props;
404
+ const activeId = ref("");
405
+ let unsubscribe;
406
+ const tabButtonId = (id) => `${props.groupId}-tab-${id}`;
407
+ const tabPanelId = (id) => `${props.groupId}-panel-${id}`;
408
+ const resolveActive = () => {
409
+ const fromQuery = props.adapter.get();
410
+ const matched = props.items.find((item) => item.id === fromQuery);
411
+ activeId.value = matched?.id || props.items[0]?.id || "";
412
+ };
413
+ const syncPanels = () => {
414
+ props.root.querySelectorAll(".docs-markdown-tabs__panel").forEach((panel) => {
415
+ const id = panel.dataset.tab || "";
416
+ const selected = id === activeId.value;
417
+ panel.hidden = !selected;
418
+ panel.setAttribute("aria-labelledby", tabButtonId(id));
419
+ if (selected) {
420
+ panel.removeAttribute("hidden");
421
+ panel.id = tabPanelId(id);
422
+ } else {
423
+ panel.setAttribute("hidden", "");
424
+ panel.id = tabPanelId(id);
425
+ }
426
+ });
427
+ };
428
+ const select = (id) => {
429
+ if (!id) return;
430
+ activeId.value = id;
431
+ props.adapter.set(id);
432
+ syncPanels();
433
+ };
434
+ onMounted(() => {
435
+ resolveActive();
436
+ syncPanels();
437
+ unsubscribe = props.adapter.subscribe?.(() => {
438
+ resolveActive();
439
+ syncPanels();
440
+ });
441
+ });
442
+ onBeforeUnmount(() => {
443
+ unsubscribe?.();
444
+ unsubscribe = void 0;
445
+ });
446
+ watch(() => props.items.map((item) => item.id).join("\0"), () => {
447
+ resolveActive();
448
+ syncPanels();
449
+ });
450
+ return (_ctx, _cache) => {
451
+ return openBlock(), createElementBlock("div", {
452
+ class: "docs-markdown-tabs__list",
453
+ role: "tablist",
454
+ "aria-label": __props.label
455
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.items, (item) => {
456
+ return openBlock(), createElementBlock("button", {
457
+ key: item.id,
458
+ type: "button",
459
+ class: normalizeClass(["docs-markdown-tabs__tab", { "docs-markdown-tabs__tab--active": item.id === activeId.value }]),
460
+ role: "tab",
461
+ id: tabButtonId(item.id),
462
+ "aria-selected": item.id === activeId.value,
463
+ "aria-controls": tabPanelId(item.id),
464
+ tabindex: item.id === activeId.value ? 0 : -1,
465
+ onClick: ($event) => select(item.id)
466
+ }, toDisplayString(item.title), 11, _hoisted_2$1);
467
+ }), 128))], 8, _hoisted_1$1);
468
+ };
469
+ }
470
+ });
471
+ //#endregion
251
472
  //#region packages/markdown/src/directive.ts
252
473
  var mountedApps = /* @__PURE__ */ new WeakMap();
253
474
  var renderVersions = /* @__PURE__ */ new WeakMap();
254
475
  var playgroundModule;
255
476
  var loadPlayground = () => playgroundModule ||= import("@deot/docs-playground");
477
+ var defaultTabQuery = createHistoryTabQueryAdapter();
256
478
  var cleanup = (el) => {
257
479
  for (const app of mountedApps.get(el) || []) app.unmount();
258
480
  mountedApps.delete(el);
@@ -261,12 +483,36 @@ var resolveBlockLanguage = (block) => {
261
483
  return [...block.classList].find((className) => className.startsWith("language-"))?.slice(9) || "";
262
484
  };
263
485
  var serializePlaygroundDefaults = (value) => JSON.stringify(value || {});
486
+ var mountTabs = (el, apps, locale, adapter) => {
487
+ const label = locale.markdown?.tabs?.label || "Tabs";
488
+ el.querySelectorAll("[data-tabs]").forEach((root, index) => {
489
+ const nav = root.querySelector("[data-tabs-nav]");
490
+ if (!nav) return;
491
+ const items = [...root.querySelectorAll(".docs-markdown-tabs__panel")].map((panel) => ({
492
+ id: panel.dataset.tab || "",
493
+ title: panel.dataset.tabTitle || panel.dataset.tab || ""
494
+ })).filter((item) => item.id && item.title);
495
+ if (!items.length) return;
496
+ const groupId = `docs-markdown-tabs-${index}`;
497
+ const app = createApp({ render: () => h(tabs_nav_default, {
498
+ items,
499
+ label,
500
+ adapter,
501
+ root,
502
+ groupId
503
+ }) });
504
+ app.mount(nav);
505
+ apps.push(app);
506
+ });
507
+ };
264
508
  var render = async (el, binding) => {
265
509
  cleanup(el);
266
510
  const version = (renderVersions.get(el) || 0) + 1;
267
511
  renderVersions.set(el, version);
268
512
  el.innerHTML = binding.value.source ? Markdown.render(binding.value.source) : "";
269
513
  const apps = [];
514
+ const adapter = binding.value.tabQuery || defaultTabQuery;
515
+ mountTabs(el, apps, binding.value.locale, adapter);
270
516
  const blocks = el.querySelectorAll("pre code");
271
517
  const playgrounds = el.querySelectorAll("div[data-playground]");
272
518
  if (!blocks.length && !playgrounds.length) {
@@ -316,7 +562,7 @@ var render = async (el, binding) => {
316
562
  mountedApps.set(el, apps);
317
563
  };
318
564
  var update = (el, binding) => {
319
- if (binding.value.source !== binding.oldValue?.source || binding.value.locale !== binding.oldValue?.locale || serializePlaygroundDefaults(binding.value.playground) !== serializePlaygroundDefaults(binding.oldValue?.playground)) render(el, binding);
565
+ 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);
320
566
  };
321
567
  var vMarkdown = {
322
568
  mounted: (el, binding) => void render(el, binding),
@@ -427,7 +673,13 @@ var getIndicatorInlineOffset = (root, rail, side, viewportWidth) => {
427
673
  //#endregion
428
674
  //#region packages/markdown/src/headings.ts
429
675
  var HEADING_SELECTOR = "h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]";
430
- var SKIP_CLOSEST = "pre, .docs-markdown-code-preview, [data-playground][data-code], [data-playground][data-files]";
676
+ var SKIP_CLOSEST = [
677
+ "pre",
678
+ ".docs-markdown-code-preview",
679
+ "[data-playground][data-code]",
680
+ "[data-playground][data-files]",
681
+ ".docs-markdown-tabs__panel[hidden]"
682
+ ].join(", ");
431
683
  /**
432
684
  * 去掉 markdown-it-anchor 的井号后,读取标题的可见文案。
433
685
  * @param element 当前标题节点。
@@ -972,6 +1224,7 @@ var wrapper_default = /* @__PURE__ */ defineComponent({
972
1224
  locale: {},
973
1225
  theme: { default: "default" },
974
1226
  playground: { default: () => ({}) },
1227
+ tabQuery: {},
975
1228
  modelValue: {},
976
1229
  value: {}
977
1230
  },
@@ -987,11 +1240,15 @@ var wrapper_default = /* @__PURE__ */ defineComponent({
987
1240
  return typeof props.indicator === "object" ? props.indicator : {};
988
1241
  });
989
1242
  const resolvedTheme = computed(() => props.theme === "traditional" ? "traditional" : "default");
1243
+ const injectedTabQuery = inject(markdownTabQueryKey, null);
1244
+ const historyTabQuery = createHistoryTabQueryAdapter();
1245
+ const resolvedTabQuery = computed(() => props.tabQuery || injectedTabQuery || historyTabQuery);
990
1246
  const source = computed(() => typeof props.modelValue === "string" ? props.modelValue : props.value);
991
1247
  const markdownBinding = computed(() => ({
992
1248
  source: source.value,
993
1249
  locale: locale.value,
994
- playground: props.playground
1250
+ playground: props.playground,
1251
+ tabQuery: resolvedTabQuery.value
995
1252
  }));
996
1253
  return (_ctx, _cache) => {
997
1254
  return openBlock(), createElementBlock("div", { class: normalizeClass(["docs-markdown", `docs-markdown--${resolvedTheme.value}`]) }, [indicatorOptions.value && content.value ? (openBlock(), createBlock(indicator_default, {
@@ -1007,4 +1264,4 @@ var wrapper_default = /* @__PURE__ */ defineComponent({
1007
1264
  }
1008
1265
  });
1009
1266
  //#endregion
1010
- export { wrapper_default as Markdown, collectMarkdownHeadings, getMarkdownHeadingTitle, parseMarkdownSearchSections };
1267
+ export { wrapper_default as Markdown, collectMarkdownHeadings, createHistoryTabQueryAdapter, getMarkdownHeadingTitle, markdownTabQueryKey, parseMarkdownSearchSections, toMarkdownTabId };