@wdprlib/parser 4.3.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wdprlib/parser",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "Parser for Wikidot markup",
5
5
  "keywords": [
6
6
  "ast",
@@ -41,6 +41,6 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@braintree/sanitize-url": "^7.1.1",
44
- "@wdprlib/ast": "2.3.0"
44
+ "@wdprlib/ast": "2.4.0"
45
45
  }
46
46
  }
package/src/index.ts CHANGED
@@ -98,6 +98,15 @@ export { Lexer, tokenize, createToken } from "./lexer";
98
98
  export type { ParserOptions } from "./parser";
99
99
  export { Parser, parse } from "./parser";
100
100
 
101
+ // High-level parser pipeline
102
+ export { processWikitext } from "./pipeline";
103
+ export type {
104
+ ProcessedWikitextDocument,
105
+ ProcessWikitextCallbackContext,
106
+ ProcessWikitextDataProvider,
107
+ ProcessWikitextOptions,
108
+ } from "./pipeline";
109
+
101
110
  // Modules (ListPages, ListUsers, IfTags, Include, etc.)
102
111
  export type {
103
112
  // ListPages query types
@@ -121,6 +130,7 @@ export type {
121
130
  ExtractionResult,
122
131
  // Resolution types
123
132
  ParseFunction,
133
+ ModuleParseResult,
124
134
  ModuleSourceTransform,
125
135
  ResolveOptions,
126
136
  // Include resolution
@@ -163,6 +173,7 @@ export {
163
173
  extractIncludeReferences,
164
174
  resolveIncludes,
165
175
  resolveIncludesAsync,
176
+ resolveIncludesAsyncWithTrace,
166
177
  resolveIncludesWithTrace,
167
178
  // IfTags source-level preprocessing (run between include expansion and parse)
168
179
  preprocessIftags,
@@ -10,6 +10,7 @@ export function createParseContext(tokens: Token[], options: ParserOptions = {})
10
10
  version: options.version ?? "wikidot",
11
11
  trackPositions: options.trackPositions ?? true,
12
12
  settings: options.settings ?? DEFAULT_SETTINGS,
13
+ appendImplicitFootnoteBlock: options.appendImplicitFootnoteBlock ?? true,
13
14
  footnotes: [],
14
15
  tocEntries: [],
15
16
  codeBlocks: [],
@@ -31,4 +31,10 @@ export interface ParserOptions {
31
31
  * - `string[]`: every iftags block is evaluated against the given tags eagerly.
32
32
  */
33
33
  pageTags?: string[] | null;
34
+ /**
35
+ * Append the implicit document-level footnote block when no explicit block
36
+ * exists. Defaults to true. Fragment pipelines can disable this and add one
37
+ * block after all fragments have been merged.
38
+ */
39
+ appendImplicitFootnoteBlock?: boolean;
34
40
  }
@@ -10,7 +10,9 @@ import { containsFootnoteBlock } from "./footnotes";
10
10
 
11
11
  export function finalizeParseResult(ctx: ParseContext, children: Element[]): ParseResult {
12
12
  const cleanedChildren = postprocessChildren(children);
13
- appendImplicitFootnoteBlock(cleanedChildren);
13
+ if (ctx.appendImplicitFootnoteBlock) {
14
+ appendImplicitFootnoteBlock(cleanedChildren);
15
+ }
14
16
 
15
17
  return {
16
18
  ast: buildSyntaxTree(ctx, cleanedChildren),
@@ -16,7 +16,12 @@
16
16
  * @module
17
17
  */
18
18
 
19
- export { resolveIncludes, resolveIncludesAsync, resolveIncludesWithTrace } from "./resolve";
19
+ export {
20
+ resolveIncludes,
21
+ resolveIncludesAsync,
22
+ resolveIncludesAsyncWithTrace,
23
+ resolveIncludesWithTrace,
24
+ } from "./resolve";
20
25
  export { extractIncludeReferences } from "./references";
21
26
  export type {
22
27
  IncludeFetcher,
@@ -7,7 +7,11 @@
7
7
 
8
8
  import type { PageRef } from "@wdprlib/ast";
9
9
  import { createCachedAsyncIncludeFetcher, createCachedIncludeFetcher } from "./cache";
10
- import { expandIterative, expandIterativeAsync, expandIterativeWithTrace } from "./iterate";
10
+ import {
11
+ expandIterative,
12
+ expandIterativeAsyncWithTrace,
13
+ expandIterativeWithTrace,
14
+ } from "./iterate";
11
15
  import type {
12
16
  AsyncIncludeFetcher,
13
17
  IncludeFetcher,
@@ -84,13 +88,29 @@ export async function resolveIncludesAsync(
84
88
  fetcher: AsyncIncludeFetcher,
85
89
  options?: ResolveIncludesOptions,
86
90
  ): Promise<string> {
91
+ return (await resolveIncludesAsyncWithTrace(source, fetcher, options)).source;
92
+ }
93
+
94
+ /**
95
+ * Async include expansion with dependency and iteration trace data.
96
+ */
97
+ export async function resolveIncludesAsyncWithTrace(
98
+ source: string,
99
+ fetcher: AsyncIncludeFetcher,
100
+ options?: ResolveIncludesOptions,
101
+ ): Promise<ResolveIncludesTraceResult> {
87
102
  if (options?.settings && !options.settings.enablePageSyntax) {
88
- return source;
103
+ return {
104
+ source,
105
+ dependencies: [],
106
+ iterations: [],
107
+ reachedMaxIterations: false,
108
+ };
89
109
  }
90
110
 
91
111
  const maxIterations = options?.maxIterations ?? 10;
92
112
  const cachedFetcher = createCachedAsyncIncludeFetcher(fetcher, normalizePageKey);
93
- return expandIterativeAsync(source, cachedFetcher, maxIterations);
113
+ return expandIterativeAsyncWithTrace(source, cachedFetcher, maxIterations);
94
114
  }
95
115
 
96
116
  /**
@@ -102,6 +102,41 @@ export async function expandIterativeAsync(
102
102
  return current;
103
103
  }
104
104
 
105
+ /** Async counterpart of {@link expandIterativeWithTrace}. */
106
+ export async function expandIterativeAsyncWithTrace(
107
+ source: string,
108
+ fetcher: AsyncIncludeFetcher,
109
+ maxIterations: number,
110
+ ): Promise<ResolveIncludesTraceResult> {
111
+ let current = source;
112
+ const replacementCache = new Map<string, Promise<string>>();
113
+ const dependencies: IncludeDependency[] = [];
114
+ const iterations: ResolveIncludesTraceResult["iterations"] = [];
115
+
116
+ for (let i = 0; i < maxIterations; i++) {
117
+ const expanded = await expandOneIterationAsyncWithTrace(current, fetcher, replacementCache, i);
118
+ if (expanded === null) break;
119
+
120
+ dependencies.push(...expanded.dependencies);
121
+ iterations.push({
122
+ iteration: i,
123
+ directives: expanded.references,
124
+ changed: expanded.source !== current,
125
+ });
126
+
127
+ if (expanded.source === current) break;
128
+ current = expanded.source;
129
+ }
130
+
131
+ return {
132
+ source: current,
133
+ dependencies,
134
+ iterations,
135
+ reachedMaxIterations:
136
+ iterations.length === maxIterations && scanIncludeDirectives(current).length > 0,
137
+ };
138
+ }
139
+
105
140
  function expandOneIteration(
106
141
  source: string,
107
142
  fetcher: IncludeFetcher,
@@ -179,6 +214,42 @@ async function expandOneIterationAsync(
179
214
  return parts.join("");
180
215
  }
181
216
 
217
+ async function expandOneIterationAsyncWithTrace(
218
+ source: string,
219
+ fetcher: AsyncIncludeFetcher,
220
+ replacementCache: Map<string, Promise<string>>,
221
+ iteration: number,
222
+ ): Promise<{
223
+ source: string;
224
+ references: IncludeReference[];
225
+ dependencies: IncludeDependency[];
226
+ } | null> {
227
+ if (!MAYBE_INCLUDE_PATTERN.test(source)) return null;
228
+
229
+ const directives = scanIncludeDirectives(source);
230
+ if (directives.length === 0) return null;
231
+
232
+ const references = directives.map(createIncludeReference);
233
+ const replacements = await Promise.all(
234
+ directives.map(({ inner }) => replaceCachedAsync(inner, fetcher, replacementCache)),
235
+ );
236
+ const parts: string[] = [];
237
+ let lastPos = 0;
238
+
239
+ for (let i = 0; i < directives.length; i++) {
240
+ const { start, end } = directives[i]!;
241
+ parts.push(source.slice(lastPos, start), replacements[i]!);
242
+ lastPos = end;
243
+ }
244
+ parts.push(source.slice(lastPos));
245
+
246
+ return {
247
+ source: parts.join(""),
248
+ references,
249
+ dependencies: references.map((reference) => ({ ...reference, iteration })),
250
+ };
251
+ }
252
+
182
253
  function replaceCached(inner: string, fetcher: IncludeFetcher, cache: Map<string, string>): string {
183
254
  const cached = cache.get(inner);
184
255
  if (cached !== undefined) return cached;
@@ -20,7 +20,7 @@
20
20
  */
21
21
 
22
22
  // Module rule types and registry
23
- export type { ModuleRule } from "./types";
23
+ export type { ModuleParseResult, ModuleRule } from "./types";
24
24
  export { MODULE_RULES, getModuleRuleByName } from "./mapping";
25
25
  export { moduleRule } from "./rule";
26
26
 
@@ -106,6 +106,7 @@ export {
106
106
  extractIncludeReferences,
107
107
  resolveIncludes,
108
108
  resolveIncludesAsync,
109
+ resolveIncludesAsyncWithTrace,
109
110
  resolveIncludesWithTrace,
110
111
  } from "./include";
111
112
 
@@ -1,6 +1,6 @@
1
1
  import type { Element } from "@wdprlib/ast";
2
2
  import type { CompiledTemplate, ListPagesExternalData, VariableContext } from "../types";
3
- import type { ParseFunction } from "../../types";
3
+ import { getModuleParseAst, type ParseFunction } from "../../types";
4
4
  import type { ListPagesModuleData } from "../resolve";
5
5
 
6
6
  export function renderListPagesItems(
@@ -23,7 +23,7 @@ export function renderListPagesItems(
23
23
  site: data.site,
24
24
  };
25
25
 
26
- const itemAst = parse(compiledTemplate(ctx));
26
+ const itemAst = getModuleParseAst(parse(compiledTemplate(ctx)));
27
27
 
28
28
  if (module.separate) {
29
29
  items.push({
@@ -1,5 +1,5 @@
1
1
  import type { Element } from "@wdprlib/ast";
2
- import type { ParseFunction } from "../../types";
2
+ import { getModuleParseAst, type ParseFunction } from "../../types";
3
3
  import type { ListPagesModuleData } from "../resolve";
4
4
 
5
5
  export function wrapListPagesResult(
@@ -14,14 +14,14 @@ export function wrapListPagesResult(
14
14
  const result: Element[] = [];
15
15
 
16
16
  if (module["prepend-line"] && !module.separate) {
17
- const prependAst = parse(module["prepend-line"]);
17
+ const prependAst = getModuleParseAst(parse(module["prepend-line"]));
18
18
  result.push(...prependAst.elements);
19
19
  }
20
20
 
21
21
  result.push(...items);
22
22
 
23
23
  if (module["append-line"] && !module.separate) {
24
- const appendAst = parse(module["append-line"]);
24
+ const appendAst = getModuleParseAst(parse(module["append-line"]));
25
25
  result.push(...appendAst.elements);
26
26
  }
27
27
 
@@ -15,7 +15,7 @@ import type {
15
15
  ListUsersCompiledTemplate,
16
16
  ListUsersVariableContext,
17
17
  } from "./types";
18
- import type { ParseFunction } from "../types";
18
+ import { getModuleParseAst, type ParseFunction } from "../types";
19
19
 
20
20
  /**
21
21
  * Narrowed type for the list-users variant of the Module discriminated union.
@@ -53,6 +53,6 @@ export function resolveListUsers(
53
53
  ): Element[] {
54
54
  const ctx: ListUsersVariableContext = { user: data.user };
55
55
  const substituted = compiledTemplate(ctx);
56
- const itemAst = parse(substituted);
56
+ const itemAst = getModuleParseAst(parse(substituted));
57
57
  return itemAst.elements;
58
58
  }
@@ -0,0 +1,357 @@
1
+ import {
2
+ evaluateExpression,
3
+ isTruthy,
4
+ type Diagnostic,
5
+ type Element,
6
+ type ParseResult,
7
+ type SyntaxTree,
8
+ type TocEntry,
9
+ } from "@wdprlib/ast";
10
+ import { extractHeadingText } from "../../heading/toc-text";
11
+ import { buildTableOfContents } from "../../../../toc";
12
+ import { resolveIfTags } from "../iftags/resolve";
13
+ import {
14
+ getGenericElementChildren,
15
+ listElement,
16
+ withGenericElementChildren,
17
+ } from "../walk/children";
18
+ import type { ModuleParseResult } from "../types";
19
+
20
+ interface RegisterOptions {
21
+ stripLegacyImplicitFootnoteBlock?: boolean;
22
+ }
23
+
24
+ /** Tracks parse-local side channels while module fragments are merged. */
25
+ export class ModuleDocumentRegistry {
26
+ private readonly footnotesByElement = new WeakMap<Element, Element[]>();
27
+ readonly diagnostics: Diagnostic[] = [];
28
+
29
+ register(result: ModuleParseResult, options: RegisterOptions = {}): SyntaxTree {
30
+ const parseResult = isParseResult(result) ? result : null;
31
+ const ast: SyntaxTree = parseResult ? parseResult.ast : (result as SyntaxTree);
32
+ const elements = options.stripLegacyImplicitFootnoteBlock
33
+ ? stripTrailingDefaultFootnoteBlock(ast.elements)
34
+ : ast.elements;
35
+ const registeredAst = elements === ast.elements ? ast : { ...ast, elements };
36
+
37
+ if (parseResult) {
38
+ this.diagnostics.push(...parseResult.diagnostics);
39
+ }
40
+ this.registerFootnotes(registeredAst);
41
+ return registeredAst;
42
+ }
43
+
44
+ finalize(
45
+ ast: SyntaxTree,
46
+ elements: Element[],
47
+ pageTags: string[] | null,
48
+ ensureFootnoteBlock = true,
49
+ ): SyntaxTree {
50
+ const normalizedElements = normalizeFootnoteBlocks(elements, ensureFootnoteBlock);
51
+ const footnotes: Element[][] = [];
52
+ const htmlBlocks: string[] = [];
53
+ const codeBlocks: NonNullable<SyntaxTree["code-blocks"]> = [];
54
+ const tocEntries: TocEntry[] = [];
55
+
56
+ const collectElement = (element: Element): void => {
57
+ if (element.element === "footnote") {
58
+ const content = this.footnotesByElement.get(element);
59
+ if (content) footnotes.push(content);
60
+ } else if (element.element === "html") {
61
+ htmlBlocks.push(element.data.contents);
62
+ } else if (element.element === "code") {
63
+ codeBlocks.push(element.data);
64
+ } else if (isTocHeading(element)) {
65
+ tocEntries.push({
66
+ level: element.data.type.header.level,
67
+ text: extractHeadingText(element.data.elements),
68
+ });
69
+ }
70
+ };
71
+ walkRenderOrder(normalizedElements, pageTags, collectElement);
72
+
73
+ const result: SyntaxTree = { ...ast, elements: normalizedElements };
74
+ setOptionalArray(result, "footnotes", footnotes);
75
+ setOptionalArray(result, "html-blocks", htmlBlocks);
76
+ setOptionalArray(result, "code-blocks", codeBlocks);
77
+ setOptionalArray(result, "table-of-contents", buildTableOfContents(tocEntries));
78
+ return result;
79
+ }
80
+
81
+ private registerFootnotes(ast: SyntaxTree): void {
82
+ const footnoteElements: Element[] = [];
83
+ walkSyntaxOrder(ast.elements, (element) => {
84
+ if (element.element === "footnote") footnoteElements.push(element);
85
+ });
86
+
87
+ for (let i = 0; i < footnoteElements.length; i++) {
88
+ const content = ast.footnotes?.[i];
89
+ if (content) this.footnotesByElement.set(footnoteElements[i]!, content);
90
+ }
91
+ }
92
+ }
93
+
94
+ function isParseResult(result: ModuleParseResult): result is ParseResult {
95
+ return "ast" in result && "diagnostics" in result;
96
+ }
97
+
98
+ function setOptionalArray<K extends keyof SyntaxTree>(
99
+ tree: SyntaxTree,
100
+ key: K,
101
+ value: NonNullable<SyntaxTree[K]>,
102
+ ): void {
103
+ if ((value as unknown[]).length > 0) {
104
+ tree[key] = value;
105
+ } else {
106
+ delete tree[key];
107
+ }
108
+ }
109
+
110
+ function stripTrailingDefaultFootnoteBlock(elements: Element[]): Element[] {
111
+ const last = elements.at(-1);
112
+ if (last?.element !== "footnote-block" || last.data.title !== null || last.data.hide === true) {
113
+ return elements;
114
+ }
115
+ return elements.slice(0, -1);
116
+ }
117
+
118
+ function normalizeFootnoteBlocks(elements: Element[], ensureFootnoteBlock: boolean): Element[] {
119
+ const state = { found: false };
120
+ const normalized = mapSyntaxElements(elements, (element) => {
121
+ if (element.element !== "footnote-block") return element;
122
+ if (state.found) return null;
123
+ state.found = true;
124
+ return element;
125
+ });
126
+
127
+ if (!state.found && ensureFootnoteBlock) {
128
+ normalized.push({
129
+ element: "footnote-block",
130
+ data: { title: null, hide: false },
131
+ });
132
+ }
133
+ return normalized;
134
+ }
135
+
136
+ export function containsSyntaxFootnoteBlock(elements: Element[]): boolean {
137
+ let found = false;
138
+ walkSyntaxOrder(elements, (element) => {
139
+ if (element.element === "footnote-block") found = true;
140
+ });
141
+ return found;
142
+ }
143
+
144
+ function mapSyntaxElements(
145
+ elements: Element[],
146
+ transform: (element: Element) => Element | null,
147
+ ): Element[] {
148
+ const result: Element[] = [];
149
+ for (const original of elements) {
150
+ const element = transform(original);
151
+ if (!element) continue;
152
+ result.push(mapSyntaxChildren(element, (children) => mapSyntaxElements(children, transform)));
153
+ }
154
+ return result;
155
+ }
156
+
157
+ function mapSyntaxChildren(
158
+ element: Element,
159
+ transform: (elements: Element[]) => Element[],
160
+ ): Element {
161
+ if (element.element === "if") {
162
+ return {
163
+ ...element,
164
+ data: {
165
+ ...element.data,
166
+ // oxlint-disable-next-line unicorn/no-thenable -- `then` is part of the public AST schema
167
+ then: transform(element.data.then),
168
+ else: transform(element.data.else),
169
+ },
170
+ };
171
+ }
172
+ if (element.element === "ifexpr") {
173
+ return {
174
+ ...element,
175
+ data: {
176
+ ...element.data,
177
+ // oxlint-disable-next-line unicorn/no-thenable -- `then` is part of the public AST schema
178
+ then: transform(element.data.then),
179
+ else: transform(element.data.else),
180
+ },
181
+ };
182
+ }
183
+ if (element.element === "bibliography-block") {
184
+ return {
185
+ ...element,
186
+ data: {
187
+ ...element.data,
188
+ entries: element.data.entries.map((entry) => ({
189
+ ...entry,
190
+ key: transform(entry.key),
191
+ value: transform(entry.value),
192
+ })),
193
+ },
194
+ };
195
+ }
196
+ if (element.element === "list") {
197
+ return {
198
+ ...element,
199
+ data: {
200
+ ...element.data,
201
+ items: element.data.items.map((item) =>
202
+ item["item-type"] === "elements"
203
+ ? { ...item, elements: transform(item.elements) }
204
+ : mapSubList(item, transform),
205
+ ),
206
+ },
207
+ };
208
+ }
209
+ if (element.element === "table") {
210
+ return {
211
+ ...element,
212
+ data: {
213
+ ...element.data,
214
+ rows: element.data.rows.map((row) => ({
215
+ ...row,
216
+ cells: row.cells.map((cell) => ({ ...cell, elements: transform(cell.elements) })),
217
+ })),
218
+ },
219
+ };
220
+ }
221
+ if (element.element === "definition-list") {
222
+ return {
223
+ ...element,
224
+ data: element.data.map((entry) => ({
225
+ ...entry,
226
+ key: transform(entry.key),
227
+ value: transform(entry.value),
228
+ })),
229
+ };
230
+ }
231
+ if (element.element === "tab-view") {
232
+ return {
233
+ ...element,
234
+ data: element.data.map((tab) => ({ ...tab, elements: transform(tab.elements) })),
235
+ };
236
+ }
237
+
238
+ const children = getGenericElementChildren(element);
239
+ return children === null ? element : withGenericElementChildren(element, transform(children));
240
+ }
241
+
242
+ function mapSubList(
243
+ item: Extract<ElementOfListItem, { "item-type": "sub-list" }>,
244
+ transform: (elements: Element[]) => Element[],
245
+ ): ElementOfListItem {
246
+ const transformed = transform([listElement(item.data)])[0];
247
+ return transformed?.element === "list" ? { ...item, data: transformed.data } : item;
248
+ }
249
+
250
+ type ElementOfListItem = Extract<Element, { element: "list" }>["data"]["items"][number];
251
+
252
+ function walkSyntaxOrder(elements: Element[], callback: (element: Element) => void): void {
253
+ for (const element of elements) {
254
+ callback(element);
255
+ walkAllSyntaxChildren(element, (children) => walkSyntaxOrder(children, callback));
256
+ }
257
+ }
258
+
259
+ function walkRenderOrder(
260
+ elements: Element[],
261
+ pageTags: string[] | null,
262
+ callback: (element: Element) => void,
263
+ ): void {
264
+ for (const element of elements) {
265
+ callback(element);
266
+ if (element.element === "if") {
267
+ walkRenderOrder(
268
+ isTruthy(element.data.condition) ? element.data.then : element.data.else,
269
+ pageTags,
270
+ callback,
271
+ );
272
+ continue;
273
+ }
274
+ if (element.element === "ifexpr") {
275
+ const evaluated = evaluateExpression(element.data.expression);
276
+ if (evaluated.success) {
277
+ walkRenderOrder(
278
+ evaluated.value !== 0 ? element.data.then : element.data.else,
279
+ pageTags,
280
+ callback,
281
+ );
282
+ }
283
+ continue;
284
+ }
285
+ if (element.element === "if-tags") {
286
+ const resolved = resolveIfTags(element.data, pageTags);
287
+ if (!resolved.evaluated || resolved.matched) {
288
+ walkRenderOrder(element.data.elements, pageTags, callback);
289
+ }
290
+ continue;
291
+ }
292
+ if (element.element === "bibliography-block") {
293
+ if (!element.data.hide) {
294
+ for (const entry of element.data.entries) {
295
+ walkRenderOrder(entry.value, pageTags, callback);
296
+ }
297
+ }
298
+ continue;
299
+ }
300
+ walkBasicChildren(element, (children) => walkRenderOrder(children, pageTags, callback));
301
+ }
302
+ }
303
+
304
+ function walkAllSyntaxChildren(element: Element, callback: (elements: Element[]) => void): void {
305
+ if (element.element === "if" || element.element === "ifexpr") {
306
+ callback(element.data.then);
307
+ callback(element.data.else);
308
+ return;
309
+ }
310
+ if (element.element === "bibliography-block") {
311
+ for (const entry of element.data.entries) {
312
+ callback(entry.key);
313
+ callback(entry.value);
314
+ }
315
+ return;
316
+ }
317
+ walkBasicChildren(element, callback);
318
+ }
319
+
320
+ function walkBasicChildren(element: Element, callback: (elements: Element[]) => void): void {
321
+ if (element.element === "list") {
322
+ for (const item of element.data.items) {
323
+ callback(item["item-type"] === "elements" ? item.elements : [listElement(item.data)]);
324
+ }
325
+ return;
326
+ }
327
+ if (element.element === "table") {
328
+ for (const row of element.data.rows) {
329
+ for (const cell of row.cells) callback(cell.elements);
330
+ }
331
+ return;
332
+ }
333
+ if (element.element === "definition-list") {
334
+ for (const entry of element.data) {
335
+ callback(entry.key);
336
+ callback(entry.value);
337
+ }
338
+ return;
339
+ }
340
+ if (element.element === "tab-view") {
341
+ for (const tab of element.data) callback(tab.elements);
342
+ return;
343
+ }
344
+ const children = getGenericElementChildren(element);
345
+ if (children !== null) callback(children);
346
+ }
347
+
348
+ function isTocHeading(element: Element): element is Extract<Element, { element: "container" }> & {
349
+ data: { type: { header: { level: number; "has-toc": true } } };
350
+ } {
351
+ return (
352
+ element.element === "container" &&
353
+ typeof element.data.type === "object" &&
354
+ "header" in element.data.type &&
355
+ element.data.type.header["has-toc"]
356
+ );
357
+ }