@wdprlib/parser 2.0.9 → 2.1.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/dist/index.cjs CHANGED
@@ -34,6 +34,7 @@ __export(exports_src, {
34
34
  text: () => import_ast5.text,
35
35
  resolveModules: () => resolveModules,
36
36
  resolveListUsers: () => resolveListUsers,
37
+ resolveIncludesAsync: () => resolveIncludesAsync,
37
38
  resolveIncludes: () => resolveIncludes,
38
39
  parseTags: () => parseTags,
39
40
  parseParent: () => parseParent,
@@ -646,7 +647,139 @@ var BLOCK_START_TOKENS = [
646
647
  "CLEAR_FLOAT_RIGHT"
647
648
  ];
648
649
 
650
+ // packages/parser/src/parser/rules/utils.ts
651
+ var SAFE_ATTRIBUTES = new Set([
652
+ "accept",
653
+ "align",
654
+ "alt",
655
+ "autocapitalize",
656
+ "autoplay",
657
+ "background",
658
+ "bgcolor",
659
+ "border",
660
+ "buffered",
661
+ "checked",
662
+ "cite",
663
+ "class",
664
+ "cols",
665
+ "colspan",
666
+ "contenteditable",
667
+ "controls",
668
+ "coords",
669
+ "datetime",
670
+ "decoding",
671
+ "default",
672
+ "dir",
673
+ "dirname",
674
+ "disabled",
675
+ "download",
676
+ "draggable",
677
+ "for",
678
+ "form",
679
+ "headers",
680
+ "height",
681
+ "hidden",
682
+ "high",
683
+ "href",
684
+ "hreflang",
685
+ "id",
686
+ "inputmode",
687
+ "ismap",
688
+ "itemprop",
689
+ "kind",
690
+ "label",
691
+ "lang",
692
+ "list",
693
+ "loop",
694
+ "low",
695
+ "max",
696
+ "maxlength",
697
+ "min",
698
+ "minlength",
699
+ "multiple",
700
+ "muted",
701
+ "name",
702
+ "optimum",
703
+ "pattern",
704
+ "placeholder",
705
+ "poster",
706
+ "preload",
707
+ "readonly",
708
+ "required",
709
+ "reversed",
710
+ "role",
711
+ "rows",
712
+ "rowspan",
713
+ "scope",
714
+ "selected",
715
+ "shape",
716
+ "size",
717
+ "sizes",
718
+ "span",
719
+ "spellcheck",
720
+ "src",
721
+ "srclang",
722
+ "srcset",
723
+ "start",
724
+ "step",
725
+ "style",
726
+ "tabindex",
727
+ "target",
728
+ "title",
729
+ "translate",
730
+ "type",
731
+ "usemap",
732
+ "value",
733
+ "width",
734
+ "wrap"
735
+ ]);
736
+ function filterUnsafeAttributes(attrs) {
737
+ const result = {};
738
+ for (const [key, value] of Object.entries(attrs)) {
739
+ const lower = key.toLowerCase();
740
+ if (lower.startsWith("on"))
741
+ continue;
742
+ if (lower.startsWith("aria-") || lower.startsWith("data-")) {
743
+ result[key] = value;
744
+ continue;
745
+ }
746
+ if (!SAFE_ATTRIBUTES.has(lower))
747
+ continue;
748
+ if (lower === "id") {
749
+ result[key] = value.startsWith("u-") ? value : `u-${value}`;
750
+ continue;
751
+ }
752
+ result[key] = value;
753
+ }
754
+ return result;
755
+ }
756
+ function parseBlockName(ctx, startPos) {
757
+ let pos = startPos;
758
+ let consumed = 0;
759
+ const token = ctx.tokens[pos];
760
+ if (!token || token.type !== "TEXT" && token.type !== "IDENTIFIER") {
761
+ return null;
762
+ }
763
+ let name = token.value.toLowerCase();
764
+ consumed++;
765
+ pos++;
766
+ if (ctx.tokens[pos]?.type === "UNDERSCORE") {
767
+ name += "_";
768
+ consumed++;
769
+ }
770
+ return { name, consumed };
771
+ }
772
+
649
773
  // packages/parser/src/parser/rules/inline/utils.ts
774
+ function isExcludedBlockToken(ctx, tokenPos) {
775
+ if (!ctx.excludedBlockNames?.size)
776
+ return false;
777
+ const token = ctx.tokens[tokenPos];
778
+ if (token?.type !== "BLOCK_OPEN" && token?.type !== "BLOCK_END_OPEN")
779
+ return false;
780
+ const nameResult = parseBlockName(ctx, tokenPos + 1);
781
+ return nameResult !== null && ctx.excludedBlockNames.has(nameResult.name);
782
+ }
650
783
  function canApplyInlineRule(rule, token) {
651
784
  if (rule.startTokens.length === 0) {
652
785
  return true;
@@ -738,7 +871,8 @@ function parseInlineUntil(ctx, endType) {
738
871
  isInvalidHeading = true;
739
872
  }
740
873
  }
741
- const isBlockStart = nextMeaningfulToken && BLOCK_START_TOKENS.includes(nextMeaningfulToken.type) && nextMeaningfulToken.lineStart && !isOrphanCloseSpan && !isAnchorName && !isInvalidBlockOpen && !isInvalidHeading;
874
+ const isExcludedBlock = (nextMeaningfulToken?.type === "BLOCK_OPEN" || nextMeaningfulToken?.type === "BLOCK_END_OPEN") && isExcludedBlockToken(ctx, pos + lookAhead);
875
+ const isBlockStart = nextMeaningfulToken && BLOCK_START_TOKENS.includes(nextMeaningfulToken.type) && nextMeaningfulToken.lineStart && !isOrphanCloseSpan && !isAnchorName && !isInvalidBlockOpen && !isInvalidHeading && !isExcludedBlock;
742
876
  if (!nextMeaningfulToken || nextMeaningfulToken.type === "NEWLINE" || nextMeaningfulToken.type === "EOF" || isBlockStart) {
743
877
  if (isBlockStart && nodes.length > 0) {
744
878
  const nextPos = pos + lookAhead;
@@ -1076,129 +1210,6 @@ function buildListData(topLtype, list) {
1076
1210
  };
1077
1211
  }
1078
1212
 
1079
- // packages/parser/src/parser/rules/utils.ts
1080
- var SAFE_ATTRIBUTES = new Set([
1081
- "accept",
1082
- "align",
1083
- "alt",
1084
- "autocapitalize",
1085
- "autoplay",
1086
- "background",
1087
- "bgcolor",
1088
- "border",
1089
- "buffered",
1090
- "checked",
1091
- "cite",
1092
- "class",
1093
- "cols",
1094
- "colspan",
1095
- "contenteditable",
1096
- "controls",
1097
- "coords",
1098
- "datetime",
1099
- "decoding",
1100
- "default",
1101
- "dir",
1102
- "dirname",
1103
- "disabled",
1104
- "download",
1105
- "draggable",
1106
- "for",
1107
- "form",
1108
- "headers",
1109
- "height",
1110
- "hidden",
1111
- "high",
1112
- "href",
1113
- "hreflang",
1114
- "id",
1115
- "inputmode",
1116
- "ismap",
1117
- "itemprop",
1118
- "kind",
1119
- "label",
1120
- "lang",
1121
- "list",
1122
- "loop",
1123
- "low",
1124
- "max",
1125
- "maxlength",
1126
- "min",
1127
- "minlength",
1128
- "multiple",
1129
- "muted",
1130
- "name",
1131
- "optimum",
1132
- "pattern",
1133
- "placeholder",
1134
- "poster",
1135
- "preload",
1136
- "readonly",
1137
- "required",
1138
- "reversed",
1139
- "role",
1140
- "rows",
1141
- "rowspan",
1142
- "scope",
1143
- "selected",
1144
- "shape",
1145
- "size",
1146
- "sizes",
1147
- "span",
1148
- "spellcheck",
1149
- "src",
1150
- "srclang",
1151
- "srcset",
1152
- "start",
1153
- "step",
1154
- "style",
1155
- "tabindex",
1156
- "target",
1157
- "title",
1158
- "translate",
1159
- "type",
1160
- "usemap",
1161
- "value",
1162
- "width",
1163
- "wrap"
1164
- ]);
1165
- function filterUnsafeAttributes(attrs) {
1166
- const result = {};
1167
- for (const [key, value] of Object.entries(attrs)) {
1168
- const lower = key.toLowerCase();
1169
- if (lower.startsWith("on"))
1170
- continue;
1171
- if (lower.startsWith("aria-") || lower.startsWith("data-")) {
1172
- result[key] = value;
1173
- continue;
1174
- }
1175
- if (!SAFE_ATTRIBUTES.has(lower))
1176
- continue;
1177
- if (lower === "id") {
1178
- result[key] = value.startsWith("u-") ? value : `u-${value}`;
1179
- continue;
1180
- }
1181
- result[key] = value;
1182
- }
1183
- return result;
1184
- }
1185
- function parseBlockName(ctx, startPos) {
1186
- let pos = startPos;
1187
- let consumed = 0;
1188
- const token = ctx.tokens[pos];
1189
- if (!token || token.type !== "TEXT" && token.type !== "IDENTIFIER") {
1190
- return null;
1191
- }
1192
- let name = token.value.toLowerCase();
1193
- consumed++;
1194
- pos++;
1195
- if (ctx.tokens[pos]?.type === "UNDERSCORE") {
1196
- name += "_";
1197
- consumed++;
1198
- }
1199
- return { name, consumed };
1200
- }
1201
-
1202
1213
  // packages/parser/src/parser/rules/block/utils.ts
1203
1214
  function canApplyBlockRule(rule, token) {
1204
1215
  if (rule.requiresLineStart && !token.lineStart) {
@@ -1209,11 +1220,13 @@ function canApplyBlockRule(rule, token) {
1209
1220
  }
1210
1221
  return rule.startTokens.includes(token.type);
1211
1222
  }
1212
- function parseBlocksUntil(ctx, closeCondition) {
1223
+ function parseBlocksUntil(ctx, closeCondition, options) {
1213
1224
  const elements = [];
1214
1225
  let consumed = 0;
1215
1226
  let pos = ctx.pos;
1216
- const { blockRules, blockFallbackRule } = ctx;
1227
+ const excluded = options?.excludedBlockNames;
1228
+ const blockRules = excluded ? ctx.blockRules.filter((r) => !excluded.has(r.name)) : ctx.blockRules;
1229
+ const { blockFallbackRule } = ctx;
1217
1230
  while (pos < ctx.tokens.length) {
1218
1231
  const token = ctx.tokens[pos];
1219
1232
  if (!token || token.type === "EOF") {
@@ -1234,7 +1247,13 @@ function parseBlocksUntil(ctx, closeCondition) {
1234
1247
  continue;
1235
1248
  }
1236
1249
  let matched = false;
1237
- const blockCtx = { ...ctx, pos, blockCloseCondition: closeCondition };
1250
+ const blockCtx = {
1251
+ ...ctx,
1252
+ pos,
1253
+ blockRules,
1254
+ blockCloseCondition: closeCondition,
1255
+ excludedBlockNames: excluded
1256
+ };
1238
1257
  for (const rule of blockRules) {
1239
1258
  if (canApplyBlockRule(rule, token)) {
1240
1259
  const result = rule.parse(blockCtx);
@@ -2619,44 +2638,7 @@ function consumeCloseTag2(ctx, pos) {
2619
2638
  closeConsumed++;
2620
2639
  return closeConsumed;
2621
2640
  }
2622
- function mergeParagraphs(elements) {
2623
- const result = [];
2624
- let mergedElements = [];
2625
- for (const elem of elements) {
2626
- if (elem.element === "container" && elem.data && typeof elem.data === "object" && "type" in elem.data && elem.data.type === "paragraph") {
2627
- if (mergedElements.length > 0) {
2628
- mergedElements.push({ element: "line-break" });
2629
- }
2630
- if ("elements" in elem.data && Array.isArray(elem.data.elements)) {
2631
- mergedElements.push(...elem.data.elements);
2632
- }
2633
- } else {
2634
- if (mergedElements.length > 0) {
2635
- result.push({
2636
- element: "container",
2637
- data: {
2638
- type: "paragraph",
2639
- attributes: {},
2640
- elements: mergedElements
2641
- }
2642
- });
2643
- mergedElements = [];
2644
- }
2645
- result.push(elem);
2646
- }
2647
- }
2648
- if (mergedElements.length > 0) {
2649
- result.push({
2650
- element: "container",
2651
- data: {
2652
- type: "paragraph",
2653
- attributes: {},
2654
- elements: mergedElements
2655
- }
2656
- });
2657
- }
2658
- return result;
2659
- }
2641
+ var EXCLUDED_BLOCKS = new Set(["collapsible"]);
2660
2642
  var collapsibleRule = {
2661
2643
  name: "collapsible",
2662
2644
  startTokens: ["BLOCK_OPEN"],
@@ -2720,18 +2702,16 @@ var collapsibleRule = {
2720
2702
  bodyElements = [];
2721
2703
  }
2722
2704
  } else {
2723
- const bodyCtx = {
2724
- ...ctx,
2725
- pos,
2726
- blockRules: ctx.blockRules.filter((r) => r.name !== "collapsible")
2727
- };
2705
+ const bodyCtx = { ...ctx, pos };
2728
2706
  const closeCondition = (checkCtx) => {
2729
2707
  return isCollapsibleClose(checkCtx, checkCtx.pos);
2730
2708
  };
2731
- const bodyResult = parseBlocksUntil(bodyCtx, closeCondition);
2709
+ const bodyResult = parseBlocksUntil(bodyCtx, closeCondition, {
2710
+ excludedBlockNames: EXCLUDED_BLOCKS
2711
+ });
2732
2712
  consumed += bodyResult.consumed;
2733
2713
  pos += bodyResult.consumed;
2734
- bodyElements = mergeParagraphs(bodyResult.elements);
2714
+ bodyElements = bodyResult.elements;
2735
2715
  }
2736
2716
  if (!isCollapsibleClose(ctx, pos)) {
2737
2717
  ctx.diagnostics.push({
@@ -9989,7 +9969,7 @@ function resolveIncludes(source, fetcher, options) {
9989
9969
  if (options?.settings && !options.settings.enablePageSyntax) {
9990
9970
  return source;
9991
9971
  }
9992
- const maxDepth = options?.maxDepth ?? 5;
9972
+ const maxIterations = options?.maxIterations ?? 10;
9993
9973
  const cache = new Map;
9994
9974
  const cachedFetcher = (pageRef) => {
9995
9975
  const key = normalizePageKey(pageRef);
@@ -10005,7 +9985,29 @@ function resolveIncludes(source, fetcher, options) {
10005
9985
  cache.set(key, result);
10006
9986
  return result;
10007
9987
  };
10008
- return expandText(source, cachedFetcher, 0, maxDepth, []);
9988
+ return expandIterative(source, cachedFetcher, maxIterations);
9989
+ }
9990
+ async function resolveIncludesAsync(source, fetcher, options) {
9991
+ if (options?.settings && !options.settings.enablePageSyntax) {
9992
+ return source;
9993
+ }
9994
+ const maxIterations = options?.maxIterations ?? 10;
9995
+ const cache = new Map;
9996
+ const cachedFetcher = async (pageRef) => {
9997
+ const key = normalizePageKey(pageRef);
9998
+ if (cache.has(key)) {
9999
+ return cache.get(key);
10000
+ }
10001
+ let result;
10002
+ try {
10003
+ result = await fetcher(pageRef);
10004
+ } catch {
10005
+ result = null;
10006
+ }
10007
+ cache.set(key, result);
10008
+ return result;
10009
+ };
10010
+ return expandIterativeAsync(source, cachedFetcher, maxIterations);
10009
10011
  }
10010
10012
  var INCLUDE_PATTERN = /^\[\[include\s([^\]]*(?:\](?!\])[^\]]*)*)\]\]/gim;
10011
10013
  function parseIncludeDirective(inner) {
@@ -10055,26 +10057,55 @@ function parseIncludeDirective(inner) {
10055
10057
  }
10056
10058
  return { location, variables };
10057
10059
  }
10058
- function expandText(source, fetcher, depth, maxDepth, trace) {
10059
- if (depth >= maxDepth)
10060
- return source;
10061
- return source.replace(INCLUDE_PATTERN, (_match, inner) => {
10062
- const { location, variables } = parseIncludeDirective(inner);
10063
- const pageKey = normalizePageKey(location);
10064
- if (trace.includes(pageKey)) {
10065
- return `[[div class="error-block"]]
10066
- Circular include detected: "${location.page}"
10060
+ function replaceOneInclude(_match, inner, fetcher) {
10061
+ const { location, variables } = parseIncludeDirective(inner);
10062
+ const content = fetcher(location);
10063
+ if (content === null) {
10064
+ return `[[div class="error-block"]]
10065
+ Page to be included "${location.page}" cannot be found!
10067
10066
  [[/div]]`;
10068
- }
10069
- const content = fetcher(location);
10070
- if (content === null) {
10071
- return `[[div class="error-block"]]
10067
+ }
10068
+ return substituteVariables(content, variables);
10069
+ }
10070
+ function expandIterative(source, fetcher, maxIterations) {
10071
+ let current = source;
10072
+ for (let i = 0;i < maxIterations; i++) {
10073
+ const previous = current;
10074
+ current = current.replace(INCLUDE_PATTERN, (_match, inner) => replaceOneInclude(_match, inner, fetcher));
10075
+ if (current === previous)
10076
+ break;
10077
+ }
10078
+ return current;
10079
+ }
10080
+ async function expandIterativeAsync(source, fetcher, maxIterations) {
10081
+ let current = source;
10082
+ for (let i = 0;i < maxIterations; i++) {
10083
+ const previous = current;
10084
+ const pattern = new RegExp(INCLUDE_PATTERN.source, INCLUDE_PATTERN.flags);
10085
+ let result = "";
10086
+ let lastPos = 0;
10087
+ let match;
10088
+ while ((match = pattern.exec(current)) !== null) {
10089
+ const fullMatch = match[0];
10090
+ const inner = match[1];
10091
+ result += current.slice(lastPos, match.index);
10092
+ const { location, variables } = parseIncludeDirective(inner);
10093
+ const content = await fetcher(location);
10094
+ if (content === null) {
10095
+ result += `[[div class="error-block"]]
10072
10096
  Page to be included "${location.page}" cannot be found!
10073
10097
  [[/div]]`;
10098
+ } else {
10099
+ result += substituteVariables(content, variables);
10100
+ }
10101
+ lastPos = match.index + fullMatch.length;
10074
10102
  }
10075
- const substituted = substituteVariables(content, variables);
10076
- return expandText(substituted, fetcher, depth + 1, maxDepth, [...trace, pageKey]);
10077
- });
10103
+ result += current.slice(lastPos);
10104
+ current = result;
10105
+ if (current === previous)
10106
+ break;
10107
+ }
10108
+ return current;
10078
10109
  }
10079
10110
  function normalizePageKey(location) {
10080
10111
  const site = location.site ?? "";
package/dist/index.d.cts CHANGED
@@ -859,21 +859,36 @@ import { PageRef, WikitextSettings as WikitextSettings3 } from "@wdprlib/ast";
859
859
  */
860
860
  type IncludeFetcher = (pageRef: PageRef) => string | null;
861
861
  /**
862
- * Options for resolveIncludes
862
+ * Async callback to fetch page content for include resolution.
863
+ * Returns a promise of the wikitext source, or null if the page does not exist.
864
+ *
865
+ * @security The fetcher is called with user-provided page references.
866
+ * Implementations should validate and sanitize page references before
867
+ * using them in database queries or file system access.
868
+ */
869
+ type AsyncIncludeFetcher = (pageRef: PageRef) => Promise<string | null>;
870
+ /**
871
+ * Options for resolveIncludes / resolveIncludesAsync
863
872
  */
864
873
  interface ResolveIncludesOptions {
865
- /** Maximum recursion depth for nested includes (default: 5) */
866
- maxDepth?: number;
874
+ /**
875
+ * Maximum number of expansion iterations (default: 10).
876
+ *
877
+ * Each iteration replaces all `[[include]]` directives in the current
878
+ * source with fetched content. Iteration stops when the source is
879
+ * unchanged or this limit is reached.
880
+ */
881
+ maxIterations?: number;
867
882
  /** Wikitext settings. If enablePageSyntax is false, includes are not expanded. */
868
883
  settings?: WikitextSettings3;
869
884
  }
870
885
  /**
871
886
  * Expand all [[include]] directives in the source text.
872
887
  *
873
- * Include directives are treated as macro expansions: `[[include page]]`
874
- * is replaced with the fetched page content (after variable substitution).
875
- * The result is a single expanded text that can be parsed as a whole,
876
- * allowing block structures (like div) to span across include boundaries.
888
+ * Uses Wikidot-compatible iterative expansion: each iteration replaces
889
+ * all include directives in the current source with fetched (and
890
+ * variable-substituted) content. Iteration continues until no further
891
+ * changes occur or `maxIterations` is reached.
877
892
  *
878
893
  * @example
879
894
  * ```ts
@@ -883,6 +898,21 @@ interface ResolveIncludesOptions {
883
898
  */
884
899
  declare function resolveIncludes(source: string, fetcher: IncludeFetcher, options?: ResolveIncludesOptions): string;
885
900
  /**
901
+ * Async version of {@link resolveIncludes}.
902
+ *
903
+ * Expand all [[include]] directives using an async fetcher, allowing
904
+ * page content to be loaded from async sources such as databases.
905
+ *
906
+ * @example
907
+ * ```ts
908
+ * const expanded = await resolveIncludesAsync(source, async (ref) => {
909
+ * return await db.getPageContent(ref.page);
910
+ * });
911
+ * const ast = parse(expanded);
912
+ * ```
913
+ */
914
+ declare function resolveIncludesAsync(source: string, fetcher: AsyncIncludeFetcher, options?: ResolveIncludesOptions): Promise<string>;
915
+ /**
886
916
  * Compile a ListUsers template string into an executable function.
887
917
  *
888
918
  * The template is split into alternating static strings and dynamic getter
@@ -981,4 +1011,4 @@ interface ResolveOptions {
981
1011
  */
982
1012
  declare function resolveModules(ast: SyntaxTree2, dataProvider: DataProvider, options: ResolveOptions): Promise<SyntaxTree2>;
983
1013
  import { STYLE_SLOT_PREFIX } from "@wdprlib/ast";
984
- export { tokenize, text, resolveModules, resolveListUsers, resolveIncludes, parseTags, parseParent, parseOrder, parseNumericSelector, parseDateSelector, parseCategory, parse, paragraph, normalizeQuery, listItemSubList, listItemElements, list, link, lineBreak, italics, isListUsersModule, horizontalRule, heading, extractListUsersVariables, extractDataRequirements, createToken, createSettings, createPosition, createPoint, container, compileTemplate, compileListUsersTemplate, bold, WikitextSettings4 as WikitextSettings, WikitextMode, Version2 as Version, VariableMap, VariableContext, UserInfo, TokenType, Token, TocEntry2 as TocEntry, TableRow, TableData, TableCell, TabData, SyntaxTree3 as SyntaxTree, SiteContext, STYLE_SLOT_PREFIX, ResolveOptions, ResolveIncludesOptions, Position2 as Position, Point, ParserOptions, Parser, ParseResult2 as ParseResult, ParseFunction, PageRef2 as PageRef, PageData, NormalizedTags, NormalizedParent, NormalizedOrder, NormalizedNumericSelector, NormalizedListPagesQuery, NormalizedDateSelector, NormalizedCategory, Module4 as Module, ListUsersVariableContext, ListUsersVariable, ListUsersUserData, ListUsersExternalData, ListUsersDataRequirement, ListUsersDataFetcher, ListUsersCompiledTemplate, ListType, ListPagesVariable, ListPagesQuery, ListPagesExternalData, ListPagesDataRequirement, ListPagesDataFetcher, ListItem, ListData, LinkType, LinkLocation, LinkLabel, LexerOptions, Lexer, IncludeFetcher, ImageSource, HeadingLevel, Heading, HeaderType, FloatAlignment, ExtractionResult, Embed, Element6 as Element, DiagnosticSeverity, Diagnostic2 as Diagnostic, DefinitionListItem, DateItem, DataRequirements, DataProvider, DEFAULT_SETTINGS, ContainerType, ContainerData, CompiledTemplate, CollapsibleData, CodeBlockData2 as CodeBlockData, ClearFloat, AttributeMap, AnchorTarget, Alignment, AlignType };
1014
+ export { tokenize, text, resolveModules, resolveListUsers, resolveIncludesAsync, resolveIncludes, parseTags, parseParent, parseOrder, parseNumericSelector, parseDateSelector, parseCategory, parse, paragraph, normalizeQuery, listItemSubList, listItemElements, list, link, lineBreak, italics, isListUsersModule, horizontalRule, heading, extractListUsersVariables, extractDataRequirements, createToken, createSettings, createPosition, createPoint, container, compileTemplate, compileListUsersTemplate, bold, WikitextSettings4 as WikitextSettings, WikitextMode, Version2 as Version, VariableMap, VariableContext, UserInfo, TokenType, Token, TocEntry2 as TocEntry, TableRow, TableData, TableCell, TabData, SyntaxTree3 as SyntaxTree, SiteContext, STYLE_SLOT_PREFIX, ResolveOptions, ResolveIncludesOptions, Position2 as Position, Point, ParserOptions, Parser, ParseResult2 as ParseResult, ParseFunction, PageRef2 as PageRef, PageData, NormalizedTags, NormalizedParent, NormalizedOrder, NormalizedNumericSelector, NormalizedListPagesQuery, NormalizedDateSelector, NormalizedCategory, Module4 as Module, ListUsersVariableContext, ListUsersVariable, ListUsersUserData, ListUsersExternalData, ListUsersDataRequirement, ListUsersDataFetcher, ListUsersCompiledTemplate, ListType, ListPagesVariable, ListPagesQuery, ListPagesExternalData, ListPagesDataRequirement, ListPagesDataFetcher, ListItem, ListData, LinkType, LinkLocation, LinkLabel, LexerOptions, Lexer, IncludeFetcher, ImageSource, HeadingLevel, Heading, HeaderType, FloatAlignment, ExtractionResult, Embed, Element6 as Element, DiagnosticSeverity, Diagnostic2 as Diagnostic, DefinitionListItem, DateItem, DataRequirements, DataProvider, DEFAULT_SETTINGS, ContainerType, ContainerData, CompiledTemplate, CollapsibleData, CodeBlockData2 as CodeBlockData, ClearFloat, AttributeMap, AsyncIncludeFetcher, AnchorTarget, Alignment, AlignType };
package/dist/index.d.ts CHANGED
@@ -859,21 +859,36 @@ import { PageRef, WikitextSettings as WikitextSettings3 } from "@wdprlib/ast";
859
859
  */
860
860
  type IncludeFetcher = (pageRef: PageRef) => string | null;
861
861
  /**
862
- * Options for resolveIncludes
862
+ * Async callback to fetch page content for include resolution.
863
+ * Returns a promise of the wikitext source, or null if the page does not exist.
864
+ *
865
+ * @security The fetcher is called with user-provided page references.
866
+ * Implementations should validate and sanitize page references before
867
+ * using them in database queries or file system access.
868
+ */
869
+ type AsyncIncludeFetcher = (pageRef: PageRef) => Promise<string | null>;
870
+ /**
871
+ * Options for resolveIncludes / resolveIncludesAsync
863
872
  */
864
873
  interface ResolveIncludesOptions {
865
- /** Maximum recursion depth for nested includes (default: 5) */
866
- maxDepth?: number;
874
+ /**
875
+ * Maximum number of expansion iterations (default: 10).
876
+ *
877
+ * Each iteration replaces all `[[include]]` directives in the current
878
+ * source with fetched content. Iteration stops when the source is
879
+ * unchanged or this limit is reached.
880
+ */
881
+ maxIterations?: number;
867
882
  /** Wikitext settings. If enablePageSyntax is false, includes are not expanded. */
868
883
  settings?: WikitextSettings3;
869
884
  }
870
885
  /**
871
886
  * Expand all [[include]] directives in the source text.
872
887
  *
873
- * Include directives are treated as macro expansions: `[[include page]]`
874
- * is replaced with the fetched page content (after variable substitution).
875
- * The result is a single expanded text that can be parsed as a whole,
876
- * allowing block structures (like div) to span across include boundaries.
888
+ * Uses Wikidot-compatible iterative expansion: each iteration replaces
889
+ * all include directives in the current source with fetched (and
890
+ * variable-substituted) content. Iteration continues until no further
891
+ * changes occur or `maxIterations` is reached.
877
892
  *
878
893
  * @example
879
894
  * ```ts
@@ -883,6 +898,21 @@ interface ResolveIncludesOptions {
883
898
  */
884
899
  declare function resolveIncludes(source: string, fetcher: IncludeFetcher, options?: ResolveIncludesOptions): string;
885
900
  /**
901
+ * Async version of {@link resolveIncludes}.
902
+ *
903
+ * Expand all [[include]] directives using an async fetcher, allowing
904
+ * page content to be loaded from async sources such as databases.
905
+ *
906
+ * @example
907
+ * ```ts
908
+ * const expanded = await resolveIncludesAsync(source, async (ref) => {
909
+ * return await db.getPageContent(ref.page);
910
+ * });
911
+ * const ast = parse(expanded);
912
+ * ```
913
+ */
914
+ declare function resolveIncludesAsync(source: string, fetcher: AsyncIncludeFetcher, options?: ResolveIncludesOptions): Promise<string>;
915
+ /**
886
916
  * Compile a ListUsers template string into an executable function.
887
917
  *
888
918
  * The template is split into alternating static strings and dynamic getter
@@ -981,4 +1011,4 @@ interface ResolveOptions {
981
1011
  */
982
1012
  declare function resolveModules(ast: SyntaxTree2, dataProvider: DataProvider, options: ResolveOptions): Promise<SyntaxTree2>;
983
1013
  import { STYLE_SLOT_PREFIX } from "@wdprlib/ast";
984
- export { tokenize, text, resolveModules, resolveListUsers, resolveIncludes, parseTags, parseParent, parseOrder, parseNumericSelector, parseDateSelector, parseCategory, parse, paragraph, normalizeQuery, listItemSubList, listItemElements, list, link, lineBreak, italics, isListUsersModule, horizontalRule, heading, extractListUsersVariables, extractDataRequirements, createToken, createSettings, createPosition, createPoint, container, compileTemplate, compileListUsersTemplate, bold, WikitextSettings4 as WikitextSettings, WikitextMode, Version2 as Version, VariableMap, VariableContext, UserInfo, TokenType, Token, TocEntry2 as TocEntry, TableRow, TableData, TableCell, TabData, SyntaxTree3 as SyntaxTree, SiteContext, STYLE_SLOT_PREFIX, ResolveOptions, ResolveIncludesOptions, Position2 as Position, Point, ParserOptions, Parser, ParseResult2 as ParseResult, ParseFunction, PageRef2 as PageRef, PageData, NormalizedTags, NormalizedParent, NormalizedOrder, NormalizedNumericSelector, NormalizedListPagesQuery, NormalizedDateSelector, NormalizedCategory, Module4 as Module, ListUsersVariableContext, ListUsersVariable, ListUsersUserData, ListUsersExternalData, ListUsersDataRequirement, ListUsersDataFetcher, ListUsersCompiledTemplate, ListType, ListPagesVariable, ListPagesQuery, ListPagesExternalData, ListPagesDataRequirement, ListPagesDataFetcher, ListItem, ListData, LinkType, LinkLocation, LinkLabel, LexerOptions, Lexer, IncludeFetcher, ImageSource, HeadingLevel, Heading, HeaderType, FloatAlignment, ExtractionResult, Embed, Element6 as Element, DiagnosticSeverity, Diagnostic2 as Diagnostic, DefinitionListItem, DateItem, DataRequirements, DataProvider, DEFAULT_SETTINGS, ContainerType, ContainerData, CompiledTemplate, CollapsibleData, CodeBlockData2 as CodeBlockData, ClearFloat, AttributeMap, AnchorTarget, Alignment, AlignType };
1014
+ export { tokenize, text, resolveModules, resolveListUsers, resolveIncludesAsync, resolveIncludes, parseTags, parseParent, parseOrder, parseNumericSelector, parseDateSelector, parseCategory, parse, paragraph, normalizeQuery, listItemSubList, listItemElements, list, link, lineBreak, italics, isListUsersModule, horizontalRule, heading, extractListUsersVariables, extractDataRequirements, createToken, createSettings, createPosition, createPoint, container, compileTemplate, compileListUsersTemplate, bold, WikitextSettings4 as WikitextSettings, WikitextMode, Version2 as Version, VariableMap, VariableContext, UserInfo, TokenType, Token, TocEntry2 as TocEntry, TableRow, TableData, TableCell, TabData, SyntaxTree3 as SyntaxTree, SiteContext, STYLE_SLOT_PREFIX, ResolveOptions, ResolveIncludesOptions, Position2 as Position, Point, ParserOptions, Parser, ParseResult2 as ParseResult, ParseFunction, PageRef2 as PageRef, PageData, NormalizedTags, NormalizedParent, NormalizedOrder, NormalizedNumericSelector, NormalizedListPagesQuery, NormalizedDateSelector, NormalizedCategory, Module4 as Module, ListUsersVariableContext, ListUsersVariable, ListUsersUserData, ListUsersExternalData, ListUsersDataRequirement, ListUsersDataFetcher, ListUsersCompiledTemplate, ListType, ListPagesVariable, ListPagesQuery, ListPagesExternalData, ListPagesDataRequirement, ListPagesDataFetcher, ListItem, ListData, LinkType, LinkLocation, LinkLabel, LexerOptions, Lexer, IncludeFetcher, ImageSource, HeadingLevel, Heading, HeaderType, FloatAlignment, ExtractionResult, Embed, Element6 as Element, DiagnosticSeverity, Diagnostic2 as Diagnostic, DefinitionListItem, DateItem, DataRequirements, DataProvider, DEFAULT_SETTINGS, ContainerType, ContainerData, CompiledTemplate, CollapsibleData, CodeBlockData2 as CodeBlockData, ClearFloat, AttributeMap, AsyncIncludeFetcher, AnchorTarget, Alignment, AlignType };
package/dist/index.js CHANGED
@@ -591,7 +591,139 @@ var BLOCK_START_TOKENS = [
591
591
  "CLEAR_FLOAT_RIGHT"
592
592
  ];
593
593
 
594
+ // packages/parser/src/parser/rules/utils.ts
595
+ var SAFE_ATTRIBUTES = new Set([
596
+ "accept",
597
+ "align",
598
+ "alt",
599
+ "autocapitalize",
600
+ "autoplay",
601
+ "background",
602
+ "bgcolor",
603
+ "border",
604
+ "buffered",
605
+ "checked",
606
+ "cite",
607
+ "class",
608
+ "cols",
609
+ "colspan",
610
+ "contenteditable",
611
+ "controls",
612
+ "coords",
613
+ "datetime",
614
+ "decoding",
615
+ "default",
616
+ "dir",
617
+ "dirname",
618
+ "disabled",
619
+ "download",
620
+ "draggable",
621
+ "for",
622
+ "form",
623
+ "headers",
624
+ "height",
625
+ "hidden",
626
+ "high",
627
+ "href",
628
+ "hreflang",
629
+ "id",
630
+ "inputmode",
631
+ "ismap",
632
+ "itemprop",
633
+ "kind",
634
+ "label",
635
+ "lang",
636
+ "list",
637
+ "loop",
638
+ "low",
639
+ "max",
640
+ "maxlength",
641
+ "min",
642
+ "minlength",
643
+ "multiple",
644
+ "muted",
645
+ "name",
646
+ "optimum",
647
+ "pattern",
648
+ "placeholder",
649
+ "poster",
650
+ "preload",
651
+ "readonly",
652
+ "required",
653
+ "reversed",
654
+ "role",
655
+ "rows",
656
+ "rowspan",
657
+ "scope",
658
+ "selected",
659
+ "shape",
660
+ "size",
661
+ "sizes",
662
+ "span",
663
+ "spellcheck",
664
+ "src",
665
+ "srclang",
666
+ "srcset",
667
+ "start",
668
+ "step",
669
+ "style",
670
+ "tabindex",
671
+ "target",
672
+ "title",
673
+ "translate",
674
+ "type",
675
+ "usemap",
676
+ "value",
677
+ "width",
678
+ "wrap"
679
+ ]);
680
+ function filterUnsafeAttributes(attrs) {
681
+ const result = {};
682
+ for (const [key, value] of Object.entries(attrs)) {
683
+ const lower = key.toLowerCase();
684
+ if (lower.startsWith("on"))
685
+ continue;
686
+ if (lower.startsWith("aria-") || lower.startsWith("data-")) {
687
+ result[key] = value;
688
+ continue;
689
+ }
690
+ if (!SAFE_ATTRIBUTES.has(lower))
691
+ continue;
692
+ if (lower === "id") {
693
+ result[key] = value.startsWith("u-") ? value : `u-${value}`;
694
+ continue;
695
+ }
696
+ result[key] = value;
697
+ }
698
+ return result;
699
+ }
700
+ function parseBlockName(ctx, startPos) {
701
+ let pos = startPos;
702
+ let consumed = 0;
703
+ const token = ctx.tokens[pos];
704
+ if (!token || token.type !== "TEXT" && token.type !== "IDENTIFIER") {
705
+ return null;
706
+ }
707
+ let name = token.value.toLowerCase();
708
+ consumed++;
709
+ pos++;
710
+ if (ctx.tokens[pos]?.type === "UNDERSCORE") {
711
+ name += "_";
712
+ consumed++;
713
+ }
714
+ return { name, consumed };
715
+ }
716
+
594
717
  // packages/parser/src/parser/rules/inline/utils.ts
718
+ function isExcludedBlockToken(ctx, tokenPos) {
719
+ if (!ctx.excludedBlockNames?.size)
720
+ return false;
721
+ const token = ctx.tokens[tokenPos];
722
+ if (token?.type !== "BLOCK_OPEN" && token?.type !== "BLOCK_END_OPEN")
723
+ return false;
724
+ const nameResult = parseBlockName(ctx, tokenPos + 1);
725
+ return nameResult !== null && ctx.excludedBlockNames.has(nameResult.name);
726
+ }
595
727
  function canApplyInlineRule(rule, token) {
596
728
  if (rule.startTokens.length === 0) {
597
729
  return true;
@@ -683,7 +815,8 @@ function parseInlineUntil(ctx, endType) {
683
815
  isInvalidHeading = true;
684
816
  }
685
817
  }
686
- const isBlockStart = nextMeaningfulToken && BLOCK_START_TOKENS.includes(nextMeaningfulToken.type) && nextMeaningfulToken.lineStart && !isOrphanCloseSpan && !isAnchorName && !isInvalidBlockOpen && !isInvalidHeading;
818
+ const isExcludedBlock = (nextMeaningfulToken?.type === "BLOCK_OPEN" || nextMeaningfulToken?.type === "BLOCK_END_OPEN") && isExcludedBlockToken(ctx, pos + lookAhead);
819
+ const isBlockStart = nextMeaningfulToken && BLOCK_START_TOKENS.includes(nextMeaningfulToken.type) && nextMeaningfulToken.lineStart && !isOrphanCloseSpan && !isAnchorName && !isInvalidBlockOpen && !isInvalidHeading && !isExcludedBlock;
687
820
  if (!nextMeaningfulToken || nextMeaningfulToken.type === "NEWLINE" || nextMeaningfulToken.type === "EOF" || isBlockStart) {
688
821
  if (isBlockStart && nodes.length > 0) {
689
822
  const nextPos = pos + lookAhead;
@@ -1021,129 +1154,6 @@ function buildListData(topLtype, list) {
1021
1154
  };
1022
1155
  }
1023
1156
 
1024
- // packages/parser/src/parser/rules/utils.ts
1025
- var SAFE_ATTRIBUTES = new Set([
1026
- "accept",
1027
- "align",
1028
- "alt",
1029
- "autocapitalize",
1030
- "autoplay",
1031
- "background",
1032
- "bgcolor",
1033
- "border",
1034
- "buffered",
1035
- "checked",
1036
- "cite",
1037
- "class",
1038
- "cols",
1039
- "colspan",
1040
- "contenteditable",
1041
- "controls",
1042
- "coords",
1043
- "datetime",
1044
- "decoding",
1045
- "default",
1046
- "dir",
1047
- "dirname",
1048
- "disabled",
1049
- "download",
1050
- "draggable",
1051
- "for",
1052
- "form",
1053
- "headers",
1054
- "height",
1055
- "hidden",
1056
- "high",
1057
- "href",
1058
- "hreflang",
1059
- "id",
1060
- "inputmode",
1061
- "ismap",
1062
- "itemprop",
1063
- "kind",
1064
- "label",
1065
- "lang",
1066
- "list",
1067
- "loop",
1068
- "low",
1069
- "max",
1070
- "maxlength",
1071
- "min",
1072
- "minlength",
1073
- "multiple",
1074
- "muted",
1075
- "name",
1076
- "optimum",
1077
- "pattern",
1078
- "placeholder",
1079
- "poster",
1080
- "preload",
1081
- "readonly",
1082
- "required",
1083
- "reversed",
1084
- "role",
1085
- "rows",
1086
- "rowspan",
1087
- "scope",
1088
- "selected",
1089
- "shape",
1090
- "size",
1091
- "sizes",
1092
- "span",
1093
- "spellcheck",
1094
- "src",
1095
- "srclang",
1096
- "srcset",
1097
- "start",
1098
- "step",
1099
- "style",
1100
- "tabindex",
1101
- "target",
1102
- "title",
1103
- "translate",
1104
- "type",
1105
- "usemap",
1106
- "value",
1107
- "width",
1108
- "wrap"
1109
- ]);
1110
- function filterUnsafeAttributes(attrs) {
1111
- const result = {};
1112
- for (const [key, value] of Object.entries(attrs)) {
1113
- const lower = key.toLowerCase();
1114
- if (lower.startsWith("on"))
1115
- continue;
1116
- if (lower.startsWith("aria-") || lower.startsWith("data-")) {
1117
- result[key] = value;
1118
- continue;
1119
- }
1120
- if (!SAFE_ATTRIBUTES.has(lower))
1121
- continue;
1122
- if (lower === "id") {
1123
- result[key] = value.startsWith("u-") ? value : `u-${value}`;
1124
- continue;
1125
- }
1126
- result[key] = value;
1127
- }
1128
- return result;
1129
- }
1130
- function parseBlockName(ctx, startPos) {
1131
- let pos = startPos;
1132
- let consumed = 0;
1133
- const token = ctx.tokens[pos];
1134
- if (!token || token.type !== "TEXT" && token.type !== "IDENTIFIER") {
1135
- return null;
1136
- }
1137
- let name = token.value.toLowerCase();
1138
- consumed++;
1139
- pos++;
1140
- if (ctx.tokens[pos]?.type === "UNDERSCORE") {
1141
- name += "_";
1142
- consumed++;
1143
- }
1144
- return { name, consumed };
1145
- }
1146
-
1147
1157
  // packages/parser/src/parser/rules/block/utils.ts
1148
1158
  function canApplyBlockRule(rule, token) {
1149
1159
  if (rule.requiresLineStart && !token.lineStart) {
@@ -1154,11 +1164,13 @@ function canApplyBlockRule(rule, token) {
1154
1164
  }
1155
1165
  return rule.startTokens.includes(token.type);
1156
1166
  }
1157
- function parseBlocksUntil(ctx, closeCondition) {
1167
+ function parseBlocksUntil(ctx, closeCondition, options) {
1158
1168
  const elements = [];
1159
1169
  let consumed = 0;
1160
1170
  let pos = ctx.pos;
1161
- const { blockRules, blockFallbackRule } = ctx;
1171
+ const excluded = options?.excludedBlockNames;
1172
+ const blockRules = excluded ? ctx.blockRules.filter((r) => !excluded.has(r.name)) : ctx.blockRules;
1173
+ const { blockFallbackRule } = ctx;
1162
1174
  while (pos < ctx.tokens.length) {
1163
1175
  const token = ctx.tokens[pos];
1164
1176
  if (!token || token.type === "EOF") {
@@ -1179,7 +1191,13 @@ function parseBlocksUntil(ctx, closeCondition) {
1179
1191
  continue;
1180
1192
  }
1181
1193
  let matched = false;
1182
- const blockCtx = { ...ctx, pos, blockCloseCondition: closeCondition };
1194
+ const blockCtx = {
1195
+ ...ctx,
1196
+ pos,
1197
+ blockRules,
1198
+ blockCloseCondition: closeCondition,
1199
+ excludedBlockNames: excluded
1200
+ };
1183
1201
  for (const rule of blockRules) {
1184
1202
  if (canApplyBlockRule(rule, token)) {
1185
1203
  const result = rule.parse(blockCtx);
@@ -2564,44 +2582,7 @@ function consumeCloseTag2(ctx, pos) {
2564
2582
  closeConsumed++;
2565
2583
  return closeConsumed;
2566
2584
  }
2567
- function mergeParagraphs(elements) {
2568
- const result = [];
2569
- let mergedElements = [];
2570
- for (const elem of elements) {
2571
- if (elem.element === "container" && elem.data && typeof elem.data === "object" && "type" in elem.data && elem.data.type === "paragraph") {
2572
- if (mergedElements.length > 0) {
2573
- mergedElements.push({ element: "line-break" });
2574
- }
2575
- if ("elements" in elem.data && Array.isArray(elem.data.elements)) {
2576
- mergedElements.push(...elem.data.elements);
2577
- }
2578
- } else {
2579
- if (mergedElements.length > 0) {
2580
- result.push({
2581
- element: "container",
2582
- data: {
2583
- type: "paragraph",
2584
- attributes: {},
2585
- elements: mergedElements
2586
- }
2587
- });
2588
- mergedElements = [];
2589
- }
2590
- result.push(elem);
2591
- }
2592
- }
2593
- if (mergedElements.length > 0) {
2594
- result.push({
2595
- element: "container",
2596
- data: {
2597
- type: "paragraph",
2598
- attributes: {},
2599
- elements: mergedElements
2600
- }
2601
- });
2602
- }
2603
- return result;
2604
- }
2585
+ var EXCLUDED_BLOCKS = new Set(["collapsible"]);
2605
2586
  var collapsibleRule = {
2606
2587
  name: "collapsible",
2607
2588
  startTokens: ["BLOCK_OPEN"],
@@ -2665,18 +2646,16 @@ var collapsibleRule = {
2665
2646
  bodyElements = [];
2666
2647
  }
2667
2648
  } else {
2668
- const bodyCtx = {
2669
- ...ctx,
2670
- pos,
2671
- blockRules: ctx.blockRules.filter((r) => r.name !== "collapsible")
2672
- };
2649
+ const bodyCtx = { ...ctx, pos };
2673
2650
  const closeCondition = (checkCtx) => {
2674
2651
  return isCollapsibleClose(checkCtx, checkCtx.pos);
2675
2652
  };
2676
- const bodyResult = parseBlocksUntil(bodyCtx, closeCondition);
2653
+ const bodyResult = parseBlocksUntil(bodyCtx, closeCondition, {
2654
+ excludedBlockNames: EXCLUDED_BLOCKS
2655
+ });
2677
2656
  consumed += bodyResult.consumed;
2678
2657
  pos += bodyResult.consumed;
2679
- bodyElements = mergeParagraphs(bodyResult.elements);
2658
+ bodyElements = bodyResult.elements;
2680
2659
  }
2681
2660
  if (!isCollapsibleClose(ctx, pos)) {
2682
2661
  ctx.diagnostics.push({
@@ -9934,7 +9913,7 @@ function resolveIncludes(source, fetcher, options) {
9934
9913
  if (options?.settings && !options.settings.enablePageSyntax) {
9935
9914
  return source;
9936
9915
  }
9937
- const maxDepth = options?.maxDepth ?? 5;
9916
+ const maxIterations = options?.maxIterations ?? 10;
9938
9917
  const cache = new Map;
9939
9918
  const cachedFetcher = (pageRef) => {
9940
9919
  const key = normalizePageKey(pageRef);
@@ -9950,7 +9929,29 @@ function resolveIncludes(source, fetcher, options) {
9950
9929
  cache.set(key, result);
9951
9930
  return result;
9952
9931
  };
9953
- return expandText(source, cachedFetcher, 0, maxDepth, []);
9932
+ return expandIterative(source, cachedFetcher, maxIterations);
9933
+ }
9934
+ async function resolveIncludesAsync(source, fetcher, options) {
9935
+ if (options?.settings && !options.settings.enablePageSyntax) {
9936
+ return source;
9937
+ }
9938
+ const maxIterations = options?.maxIterations ?? 10;
9939
+ const cache = new Map;
9940
+ const cachedFetcher = async (pageRef) => {
9941
+ const key = normalizePageKey(pageRef);
9942
+ if (cache.has(key)) {
9943
+ return cache.get(key);
9944
+ }
9945
+ let result;
9946
+ try {
9947
+ result = await fetcher(pageRef);
9948
+ } catch {
9949
+ result = null;
9950
+ }
9951
+ cache.set(key, result);
9952
+ return result;
9953
+ };
9954
+ return expandIterativeAsync(source, cachedFetcher, maxIterations);
9954
9955
  }
9955
9956
  var INCLUDE_PATTERN = /^\[\[include\s([^\]]*(?:\](?!\])[^\]]*)*)\]\]/gim;
9956
9957
  function parseIncludeDirective(inner) {
@@ -10000,26 +10001,55 @@ function parseIncludeDirective(inner) {
10000
10001
  }
10001
10002
  return { location, variables };
10002
10003
  }
10003
- function expandText(source, fetcher, depth, maxDepth, trace) {
10004
- if (depth >= maxDepth)
10005
- return source;
10006
- return source.replace(INCLUDE_PATTERN, (_match, inner) => {
10007
- const { location, variables } = parseIncludeDirective(inner);
10008
- const pageKey = normalizePageKey(location);
10009
- if (trace.includes(pageKey)) {
10010
- return `[[div class="error-block"]]
10011
- Circular include detected: "${location.page}"
10004
+ function replaceOneInclude(_match, inner, fetcher) {
10005
+ const { location, variables } = parseIncludeDirective(inner);
10006
+ const content = fetcher(location);
10007
+ if (content === null) {
10008
+ return `[[div class="error-block"]]
10009
+ Page to be included "${location.page}" cannot be found!
10012
10010
  [[/div]]`;
10013
- }
10014
- const content = fetcher(location);
10015
- if (content === null) {
10016
- return `[[div class="error-block"]]
10011
+ }
10012
+ return substituteVariables(content, variables);
10013
+ }
10014
+ function expandIterative(source, fetcher, maxIterations) {
10015
+ let current = source;
10016
+ for (let i = 0;i < maxIterations; i++) {
10017
+ const previous = current;
10018
+ current = current.replace(INCLUDE_PATTERN, (_match, inner) => replaceOneInclude(_match, inner, fetcher));
10019
+ if (current === previous)
10020
+ break;
10021
+ }
10022
+ return current;
10023
+ }
10024
+ async function expandIterativeAsync(source, fetcher, maxIterations) {
10025
+ let current = source;
10026
+ for (let i = 0;i < maxIterations; i++) {
10027
+ const previous = current;
10028
+ const pattern = new RegExp(INCLUDE_PATTERN.source, INCLUDE_PATTERN.flags);
10029
+ let result = "";
10030
+ let lastPos = 0;
10031
+ let match;
10032
+ while ((match = pattern.exec(current)) !== null) {
10033
+ const fullMatch = match[0];
10034
+ const inner = match[1];
10035
+ result += current.slice(lastPos, match.index);
10036
+ const { location, variables } = parseIncludeDirective(inner);
10037
+ const content = await fetcher(location);
10038
+ if (content === null) {
10039
+ result += `[[div class="error-block"]]
10017
10040
  Page to be included "${location.page}" cannot be found!
10018
10041
  [[/div]]`;
10042
+ } else {
10043
+ result += substituteVariables(content, variables);
10044
+ }
10045
+ lastPos = match.index + fullMatch.length;
10019
10046
  }
10020
- const substituted = substituteVariables(content, variables);
10021
- return expandText(substituted, fetcher, depth + 1, maxDepth, [...trace, pageKey]);
10022
- });
10047
+ result += current.slice(lastPos);
10048
+ current = result;
10049
+ if (current === previous)
10050
+ break;
10051
+ }
10052
+ return current;
10023
10053
  }
10024
10054
  function normalizePageKey(location) {
10025
10055
  const site = location.site ?? "";
@@ -10248,6 +10278,7 @@ export {
10248
10278
  text,
10249
10279
  resolveModules,
10250
10280
  resolveListUsers,
10281
+ resolveIncludesAsync,
10251
10282
  resolveIncludes,
10252
10283
  parseTags,
10253
10284
  parseParent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wdprlib/parser",
3
- "version": "2.0.9",
3
+ "version": "2.1.0",
4
4
  "description": "Parser for Wikidot markup",
5
5
  "keywords": [
6
6
  "ast",