@wdprlib/parser 4.4.0 → 5.0.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
@@ -50,6 +50,7 @@ __export(exports_src, {
50
50
  parse: () => parse,
51
51
  paragraph: () => import_ast8.paragraph,
52
52
  normalizeQuery: () => normalizeQuery,
53
+ matchesListPagesSelectors: () => matchesListPagesSelectors,
53
54
  listItemSubList: () => import_ast8.listItemSubList,
54
55
  listItemElements: () => import_ast8.listItemElements,
55
56
  list: () => import_ast8.list,
@@ -63,6 +64,7 @@ __export(exports_src, {
63
64
  extractListUsersVariables: () => extractListUsersVariables,
64
65
  extractIncludeReferences: () => extractIncludeReferences,
65
66
  extractDataRequirements: () => extractDataRequirements,
67
+ definePageData: () => definePageData,
66
68
  createToken: () => createToken,
67
69
  createSettings: () => import_ast9.createSettings,
68
70
  createPosition: () => import_ast8.createPosition,
@@ -5919,6 +5921,60 @@ function resolveListPages(module2, data, compiledTemplate, parse) {
5919
5921
  const items = renderListPagesItems(module2, data, compiledTemplate, parse);
5920
5922
  return wrapListPagesResult(module2, items, parse);
5921
5923
  }
5924
+ // packages/parser/src/parser/rules/block/module/listpages/types/external-data.ts
5925
+ function definePageData(input) {
5926
+ const separator = input.fullname.indexOf(":");
5927
+ const category = separator === -1 ? "_default" : input.fullname.slice(0, separator);
5928
+ const name = separator === -1 ? input.fullname : input.fullname.slice(separator + 1);
5929
+ return {
5930
+ ...input,
5931
+ name: input.name ?? name,
5932
+ category: input.category ?? category,
5933
+ hiddenTags: input.hiddenTags ?? [],
5934
+ children: input.children ?? 0,
5935
+ comments: input.comments ?? 0,
5936
+ size: input.size ?? 0,
5937
+ rating: input.rating ?? 0,
5938
+ ratingVotes: input.ratingVotes ?? 0,
5939
+ revisions: input.revisions ?? 0
5940
+ };
5941
+ }
5942
+ // packages/parser/src/parser/rules/block/module/listpages/selectors.ts
5943
+ function matchesListPagesSelectors(page, query, currentPage) {
5944
+ return matchesCategory(page.category, query.category, currentPage.category) && matchesTags(page, query.tags, currentPage);
5945
+ }
5946
+ function matchesCategory(category, selector, currentCategory) {
5947
+ if (!selector)
5948
+ return category === currentCategory;
5949
+ if (selector.exclude.includes(category))
5950
+ return false;
5951
+ const hasPositiveSelector = selector.all || selector.current || selector.include.length > 0;
5952
+ return !hasPositiveSelector || selector.all || selector.current && category === currentCategory || selector.include.includes(category);
5953
+ }
5954
+ function matchesTags(page, selector, currentPage) {
5955
+ if (!selector)
5956
+ return true;
5957
+ const allTags = new Set([...page.tags, ...page.hiddenTags]);
5958
+ if (selector.all.some((tag) => !allTags.has(tag)))
5959
+ return false;
5960
+ if (selector.any.length > 0 && !selector.any.some((tag) => allTags.has(tag)))
5961
+ return false;
5962
+ if (selector.none.some((tag) => allTags.has(tag)))
5963
+ return false;
5964
+ if (selector.special === "none")
5965
+ return allTags.size === 0;
5966
+ const currentTags = new Set(currentPage.tags.filter((tag) => !tag.startsWith("_")));
5967
+ if (selector.special === "same-visible") {
5968
+ return page.tags.some((tag) => currentTags.has(tag));
5969
+ }
5970
+ if (selector.special === "same-all") {
5971
+ return setsEqual(new Set(page.tags), currentTags);
5972
+ }
5973
+ return true;
5974
+ }
5975
+ function setsEqual(left, right) {
5976
+ return left.size === right.size && [...left].every((value) => right.has(value));
5977
+ }
5922
5978
  // packages/parser/src/parser/rules/block/module/listpages/url-resolution/fields.ts
5923
5979
  var URL_RESOLVABLE_FIELDS = [
5924
5980
  { attr: "offset", queryKey: "offset", type: "number" },
@@ -6348,13 +6404,20 @@ var BASE_PLACEHOLDER_OPEN = "";
6348
6404
  var BASE_PLACEHOLDER_CLOSE = "";
6349
6405
  var RAW_BLOCK_OPEN_PATTERN = /\[\[\s*(code|html)\b[^\]]*\]\]/iy;
6350
6406
  function makeUniqueSentinels(source) {
6351
- let open = BASE_PLACEHOLDER_OPEN;
6352
- let close = BASE_PLACEHOLDER_CLOSE;
6353
- while (source.includes(open) || source.includes(close)) {
6354
- open += BASE_PLACEHOLDER_OPEN;
6355
- close += BASE_PLACEHOLDER_CLOSE;
6407
+ let openRun = 0;
6408
+ let closeRun = 0;
6409
+ let longestOpenRun = 0;
6410
+ let longestCloseRun = 0;
6411
+ for (const char of source) {
6412
+ openRun = char === BASE_PLACEHOLDER_OPEN ? openRun + 1 : 0;
6413
+ closeRun = char === BASE_PLACEHOLDER_CLOSE ? closeRun + 1 : 0;
6414
+ longestOpenRun = Math.max(longestOpenRun, openRun);
6415
+ longestCloseRun = Math.max(longestCloseRun, closeRun);
6356
6416
  }
6357
- return { open, close };
6417
+ return {
6418
+ open: BASE_PLACEHOLDER_OPEN.repeat(longestOpenRun + 1),
6419
+ close: BASE_PLACEHOLDER_CLOSE.repeat(longestCloseRun + 1)
6420
+ };
6358
6421
  }
6359
6422
  function maskRawRegions(source, sentinels) {
6360
6423
  const placeholders = [];
@@ -13754,30 +13817,6 @@ function preprocess(text) {
13754
13817
  return result;
13755
13818
  }
13756
13819
 
13757
- // packages/parser/src/parser/preprocess/expr/evaluate.ts
13758
- var import_ast6 = require("@wdprlib/ast");
13759
- function evaluateDirective(kind, match) {
13760
- if (kind === "expr") {
13761
- const result2 = import_ast6.evaluateExpression(match.head);
13762
- if (result2.success)
13763
- return import_ast6.formatExprValue(result2.value);
13764
- if (result2.error === "empty expression")
13765
- return "";
13766
- return "ERROR";
13767
- }
13768
- if (kind === "if") {
13769
- if (!match.hasPipe)
13770
- return "";
13771
- return import_ast6.isTruthy(match.head) ? match.thenText : match.elseText;
13772
- }
13773
- if (!match.hasPipe)
13774
- return "";
13775
- const result = import_ast6.evaluateExpression(match.head);
13776
- if (!result.success)
13777
- return "ERROR";
13778
- return result.value !== 0 && !Number.isNaN(result.value) ? match.thenText : match.elseText;
13779
- }
13780
-
13781
13820
  // packages/parser/src/parser/preprocess/expr/chars.ts
13782
13821
  function isWhitespace(ch) {
13783
13822
  return ch === " " || ch === "\t" || ch === `
@@ -13806,6 +13845,30 @@ function matchDirectiveKind(source, i) {
13806
13845
  return null;
13807
13846
  }
13808
13847
 
13848
+ // packages/parser/src/parser/preprocess/expr/evaluate.ts
13849
+ var import_ast6 = require("@wdprlib/ast");
13850
+ function evaluateDirective(kind, match) {
13851
+ if (kind === "expr") {
13852
+ const result2 = import_ast6.evaluateExpression(match.head);
13853
+ if (result2.success)
13854
+ return import_ast6.formatExprValue(result2.value);
13855
+ if (result2.error === "empty expression")
13856
+ return "";
13857
+ return "ERROR";
13858
+ }
13859
+ if (kind === "if") {
13860
+ if (!match.hasPipe)
13861
+ return "";
13862
+ return import_ast6.isTruthy(match.head) ? match.thenText : match.elseText;
13863
+ }
13864
+ if (!match.hasPipe)
13865
+ return "";
13866
+ const result = import_ast6.evaluateExpression(match.head);
13867
+ if (!result.success)
13868
+ return "ERROR";
13869
+ return result.value !== 0 && !Number.isNaN(result.value) ? match.thenText : match.elseText;
13870
+ }
13871
+
13809
13872
  // packages/parser/src/parser/preprocess/expr/parse.ts
13810
13873
  function tryParseInnermostDirective(source, start, kind) {
13811
13874
  const keywordLen = kind === "ifexpr" ? 6 : kind === "expr" ? 4 : 2;
@@ -13907,11 +13970,14 @@ function expandInnermost(source) {
13907
13970
  }
13908
13971
 
13909
13972
  // packages/parser/src/parser/preprocess/expr/index.ts
13973
+ var MAX_EXPR_NESTING = 64;
13910
13974
  function preprocessExpr(source) {
13911
13975
  if (!source.includes("[[#"))
13912
13976
  return source;
13913
13977
  const sentinels = makeUniqueSentinels(source);
13914
13978
  const { masked, placeholders } = maskRawRegions(source, sentinels);
13979
+ if (exceedsExprNestingLimit(masked))
13980
+ return source;
13915
13981
  const reduced = reduceExpr(masked);
13916
13982
  return restorePlaceholders(reduced, placeholders, sentinels);
13917
13983
  }
@@ -13926,6 +13992,26 @@ function reduceExpr(source) {
13926
13992
  }
13927
13993
  return current2;
13928
13994
  }
13995
+ function exceedsExprNestingLimit(source) {
13996
+ const expressionStack = [];
13997
+ let expressionDepth = 0;
13998
+ for (let i = 0;i < source.length; i++) {
13999
+ if (source.startsWith("[[", i)) {
14000
+ const isExpression = matchDirectiveKind(source, i) !== null;
14001
+ expressionStack.push(isExpression);
14002
+ if (isExpression && ++expressionDepth > MAX_EXPR_NESTING)
14003
+ return true;
14004
+ i++;
14005
+ continue;
14006
+ }
14007
+ if (source.startsWith("]]", i)) {
14008
+ if (expressionStack.pop())
14009
+ expressionDepth--;
14010
+ i++;
14011
+ }
14012
+ }
14013
+ return false;
14014
+ }
13929
14015
 
13930
14016
  // packages/parser/src/parser/parse/source.ts
13931
14017
  function prepareSourceForParse(source, options) {
@@ -14129,6 +14215,11 @@ async function resolveElementChildren(element, context, state) {
14129
14215
  }
14130
14216
 
14131
14217
  // packages/parser/src/pipeline/process.ts
14218
+ var DEFAULT_MODULE_MAX_PASSES = 5;
14219
+ var PIPELINE_DIAGNOSTIC_POSITION = {
14220
+ start: { line: 1, column: 1, offset: 0 },
14221
+ end: { line: 1, column: 1, offset: 0 }
14222
+ };
14132
14223
  async function processWikitext(source, options) {
14133
14224
  const settings = options.settings ?? import_ast7.DEFAULT_SETTINGS;
14134
14225
  const callbackContext = {
@@ -14136,6 +14227,7 @@ async function processWikitext(source, options) {
14136
14227
  settings
14137
14228
  };
14138
14229
  const dependencies = [];
14230
+ const diagnostics = [];
14139
14231
  const fetchInclude = createRequestIncludeFetcher(options.dataProvider?.fetchInclude ? (pageRef) => options.dataProvider.fetchInclude(pageRef, callbackContext) : undefined);
14140
14232
  const resolveSource = async (input) => {
14141
14233
  if (!fetchInclude)
@@ -14145,6 +14237,9 @@ async function processWikitext(source, options) {
14145
14237
  settings
14146
14238
  });
14147
14239
  dependencies.push(...resolution.dependencies);
14240
+ if (resolution.reachedMaxIterations) {
14241
+ diagnostics.push(createLimitDiagnostic("include-resolution-limit", `Include expansion stopped after ${options.includeMaxIterations ?? 10} iterations.`));
14242
+ }
14148
14243
  return resolution.source;
14149
14244
  };
14150
14245
  const expandedSource = await resolveSource(source);
@@ -14153,28 +14248,52 @@ async function processWikitext(source, options) {
14153
14248
  pageTags: options.page.tags,
14154
14249
  appendImplicitFootnoteBlock: false
14155
14250
  });
14156
- const extraction = extractDataRequirements(initial.ast);
14251
+ diagnostics.push(...initial.diagnostics);
14157
14252
  const dataProvider = createModuleDataProvider(options, callbackContext);
14158
- const resolved = await resolveModulesWithAsyncParse(initial.ast, dataProvider, {
14159
- parse: async (fragmentSource) => parse(await resolveSource(fragmentSource), {
14160
- settings,
14161
- pageTags: options.page.tags,
14162
- appendImplicitFootnoteBlock: false
14163
- }),
14164
- compiledListPagesTemplates: extraction.compiledListPagesTemplates,
14165
- compiledListUsersTemplates: extraction.compiledListUsersTemplates,
14166
- requirements: extraction.requirements,
14167
- urlPath: options.page.urlPath,
14168
- pageTags: options.page.tags
14253
+ const parseFragment = async (fragmentSource) => parse(await resolveSource(fragmentSource), {
14254
+ settings,
14255
+ pageTags: options.page.tags,
14256
+ appendImplicitFootnoteBlock: false
14169
14257
  });
14258
+ let ast = initial.ast;
14259
+ for (let pass = 0;pass < DEFAULT_MODULE_MAX_PASSES; pass++) {
14260
+ const extraction = extractDataRequirements(ast);
14261
+ if (pass > 0 && !hasResolvableRequirements(extraction.requirements, options.dataProvider)) {
14262
+ break;
14263
+ }
14264
+ const resolved = await resolveModulesWithAsyncParse(ast, dataProvider, {
14265
+ parse: parseFragment,
14266
+ compiledListPagesTemplates: extraction.compiledListPagesTemplates,
14267
+ compiledListUsersTemplates: extraction.compiledListUsersTemplates,
14268
+ requirements: extraction.requirements,
14269
+ urlPath: options.page.urlPath,
14270
+ pageTags: options.page.tags
14271
+ });
14272
+ ast = resolved.ast;
14273
+ diagnostics.push(...resolved.diagnostics);
14274
+ }
14275
+ if (hasResolvableRequirements(extractDataRequirements(ast).requirements, options.dataProvider)) {
14276
+ diagnostics.push(createLimitDiagnostic("module-resolution-limit", `Module resolution stopped after ${DEFAULT_MODULE_MAX_PASSES} passes.`));
14277
+ }
14170
14278
  return {
14171
- ast: resolved.ast,
14279
+ ast,
14172
14280
  page: options.page,
14173
14281
  settings,
14174
- diagnostics: [...initial.diagnostics, ...resolved.diagnostics],
14282
+ diagnostics,
14175
14283
  dependencies
14176
14284
  };
14177
14285
  }
14286
+ function hasResolvableRequirements(requirements, provider) {
14287
+ return Boolean(provider?.fetchListPages && requirements.listPages.length > 0 || provider?.fetchListUsers && requirements.listUsers.length > 0 || provider?.fetchTagCloud && requirements.tagCloud.length > 0);
14288
+ }
14289
+ function createLimitDiagnostic(code, message) {
14290
+ return {
14291
+ severity: "warning",
14292
+ code,
14293
+ message,
14294
+ position: PIPELINE_DIAGNOSTIC_POSITION
14295
+ };
14296
+ }
14178
14297
  function createModuleDataProvider(options, context) {
14179
14298
  const provider = options.dataProvider;
14180
14299
  return {
package/dist/index.d.cts CHANGED
@@ -674,6 +674,8 @@ interface PageData {
674
674
  ratingPercent?: number;
675
675
  revisions: number;
676
676
  }
677
+ type PageDataInput = Pick<PageData, "fullname" | "title" | "createdAt" | "updatedAt" | "tags"> & Partial<Omit<PageData, "fullname" | "title" | "createdAt" | "updatedAt" | "tags">>;
678
+ declare function definePageData(input: PageDataInput): PageData;
677
679
  /**
678
680
  * Site context information.
679
681
  */
@@ -967,6 +969,12 @@ interface ExtractionResult {
967
969
  * @returns Extraction result containing requirements and compiled templates
968
970
  */
969
971
  declare function extractDataRequirements(ast: SyntaxTree3): ExtractionResult;
972
+ type SelectorPage = Pick<PageData, "category" | "tags" | "hiddenTags">;
973
+ type CurrentPage = {
974
+ category: string;
975
+ tags: readonly string[];
976
+ };
977
+ declare function matchesListPagesSelectors(page: SelectorPage, query: Pick<NormalizedListPagesQuery, "category" | "tags">, currentPage: CurrentPage): boolean;
970
978
  /**
971
979
  * Compile a ListPages template string into an executable function.
972
980
  *
@@ -1192,4 +1200,4 @@ interface ResolveOptions {
1192
1200
  */
1193
1201
  declare function resolveModules(ast: SyntaxTree4, dataProvider: DataProvider, options: ResolveOptions): Promise<SyntaxTree4>;
1194
1202
  import { STYLE_SLOT_PREFIX } from "@wdprlib/ast";
1195
- export { tokenize, text, resolveTagCloud, resolveModules, resolveListUsers, resolveIncludesWithTrace, resolveIncludesAsyncWithTrace, resolveIncludesAsync, resolveIncludes, processWikitext, preprocessIftags, parseTags, parseParent, parseOrder, parseNumericSelector, parseDateSelector, parseCategory, parse, paragraph, normalizeQuery, listItemSubList, listItemElements, list, link, lineBreak, italics, isTagCloudModule, isListUsersModule, horizontalRule, heading, extractListUsersVariables, extractIncludeReferences, extractDataRequirements, createToken, createSettings, createPosition, createPoint, container, compileTemplate, compileListUsersTemplate, bold, WikitextSettings5 as WikitextSettings, WikitextMode, Version2 as Version, VariableMap, VariableContext, UserInfo, TokenType, Token, TocEntry2 as TocEntry, TagCloudTagData, TagCloudModuleData, TagCloudExternalData, TagCloudDataRequirement, TagCloudDataFetcher, TableRow, TableData, TableCell, TabData, SyntaxTree5 as SyntaxTree, SiteContext, STYLE_SLOT_PREFIX, ResolveOptions, ResolveIncludesTraceResult, ResolveIncludesOptions, ProcessedWikitextDocument, ProcessWikitextOptions, ProcessWikitextDataProvider, ProcessWikitextCallbackContext, Position2 as Position, Point, ParserOptions, Parser, ParseResult4 as ParseResult, ParseFunction, PageRef4 as PageRef, PageData, NormalizedTags, NormalizedParent, NormalizedOrder, NormalizedNumericSelector, NormalizedListPagesQuery, NormalizedDateSelector, NormalizedCategory, ModuleSourceTransform, ModuleParseResult, Module5 as Module, ListUsersVariableContext, ListUsersVariable, ListUsersUserData, ListUsersExternalData, ListUsersDataRequirement, ListUsersDataFetcher, ListUsersCompiledTemplate, ListType, ListPagesVariable, ListPagesQuery, ListPagesExternalData, ListPagesDataRequirement, ListPagesDataFetcher, ListItem, ListData, LinkType, LinkLocation, LinkLabel, LexerOptions, Lexer, IncludeReference, IncludeIterationTrace, IncludeFetcher, IncludeDependency, ImageSource, HeadingLevel, Heading, HeaderType, GallerySize, GalleryOrder, GalleryItem, GalleryData, GalleryContent, FloatAlignment, ExtractionResult, Embed, Element8 as Element, DiagnosticSeverity, Diagnostic4 as Diagnostic, DefinitionListItem, DateItem, DataRequirements, DataProvider, DEFAULT_SETTINGS, ContainerType, ContainerData, CompiledTemplate, CollapsibleData, CodeBlockData2 as CodeBlockData, ClearFloat, AttributeMap, AsyncIncludeFetcher, AnchorTarget, Alignment, AlignType };
1203
+ export { tokenize, text, resolveTagCloud, resolveModules, resolveListUsers, resolveIncludesWithTrace, resolveIncludesAsyncWithTrace, resolveIncludesAsync, resolveIncludes, processWikitext, preprocessIftags, parseTags, parseParent, parseOrder, parseNumericSelector, parseDateSelector, parseCategory, parse, paragraph, normalizeQuery, matchesListPagesSelectors, listItemSubList, listItemElements, list, link, lineBreak, italics, isTagCloudModule, isListUsersModule, horizontalRule, heading, extractListUsersVariables, extractIncludeReferences, extractDataRequirements, definePageData, createToken, createSettings, createPosition, createPoint, container, compileTemplate, compileListUsersTemplate, bold, WikitextSettings5 as WikitextSettings, WikitextMode, Version2 as Version, VariableMap, VariableContext, UserInfo, TokenType, Token, TocEntry2 as TocEntry, TagCloudTagData, TagCloudModuleData, TagCloudExternalData, TagCloudDataRequirement, TagCloudDataFetcher, TableRow, TableData, TableCell, TabData, SyntaxTree5 as SyntaxTree, SiteContext, STYLE_SLOT_PREFIX, ResolveOptions, ResolveIncludesTraceResult, ResolveIncludesOptions, ProcessedWikitextDocument, ProcessWikitextOptions, ProcessWikitextDataProvider, ProcessWikitextCallbackContext, Position2 as Position, Point, ParserOptions, Parser, ParseResult4 as ParseResult, ParseFunction, PageRef4 as PageRef, PageData, NormalizedTags, NormalizedParent, NormalizedOrder, NormalizedNumericSelector, NormalizedListPagesQuery, NormalizedDateSelector, NormalizedCategory, ModuleSourceTransform, ModuleParseResult, Module5 as Module, ListUsersVariableContext, ListUsersVariable, ListUsersUserData, ListUsersExternalData, ListUsersDataRequirement, ListUsersDataFetcher, ListUsersCompiledTemplate, ListType, ListPagesVariable, ListPagesQuery, ListPagesExternalData, ListPagesDataRequirement, ListPagesDataFetcher, ListItem, ListData, LinkType, LinkLocation, LinkLabel, LexerOptions, Lexer, IncludeReference, IncludeIterationTrace, IncludeFetcher, IncludeDependency, ImageSource, HeadingLevel, Heading, HeaderType, GallerySize, GalleryOrder, GalleryItem, GalleryData, GalleryContent, FloatAlignment, ExtractionResult, Embed, Element8 as Element, DiagnosticSeverity, Diagnostic4 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
@@ -674,6 +674,8 @@ interface PageData {
674
674
  ratingPercent?: number;
675
675
  revisions: number;
676
676
  }
677
+ type PageDataInput = Pick<PageData, "fullname" | "title" | "createdAt" | "updatedAt" | "tags"> & Partial<Omit<PageData, "fullname" | "title" | "createdAt" | "updatedAt" | "tags">>;
678
+ declare function definePageData(input: PageDataInput): PageData;
677
679
  /**
678
680
  * Site context information.
679
681
  */
@@ -967,6 +969,12 @@ interface ExtractionResult {
967
969
  * @returns Extraction result containing requirements and compiled templates
968
970
  */
969
971
  declare function extractDataRequirements(ast: SyntaxTree3): ExtractionResult;
972
+ type SelectorPage = Pick<PageData, "category" | "tags" | "hiddenTags">;
973
+ type CurrentPage = {
974
+ category: string;
975
+ tags: readonly string[];
976
+ };
977
+ declare function matchesListPagesSelectors(page: SelectorPage, query: Pick<NormalizedListPagesQuery, "category" | "tags">, currentPage: CurrentPage): boolean;
970
978
  /**
971
979
  * Compile a ListPages template string into an executable function.
972
980
  *
@@ -1192,4 +1200,4 @@ interface ResolveOptions {
1192
1200
  */
1193
1201
  declare function resolveModules(ast: SyntaxTree4, dataProvider: DataProvider, options: ResolveOptions): Promise<SyntaxTree4>;
1194
1202
  import { STYLE_SLOT_PREFIX } from "@wdprlib/ast";
1195
- export { tokenize, text, resolveTagCloud, resolveModules, resolveListUsers, resolveIncludesWithTrace, resolveIncludesAsyncWithTrace, resolveIncludesAsync, resolveIncludes, processWikitext, preprocessIftags, parseTags, parseParent, parseOrder, parseNumericSelector, parseDateSelector, parseCategory, parse, paragraph, normalizeQuery, listItemSubList, listItemElements, list, link, lineBreak, italics, isTagCloudModule, isListUsersModule, horizontalRule, heading, extractListUsersVariables, extractIncludeReferences, extractDataRequirements, createToken, createSettings, createPosition, createPoint, container, compileTemplate, compileListUsersTemplate, bold, WikitextSettings5 as WikitextSettings, WikitextMode, Version2 as Version, VariableMap, VariableContext, UserInfo, TokenType, Token, TocEntry2 as TocEntry, TagCloudTagData, TagCloudModuleData, TagCloudExternalData, TagCloudDataRequirement, TagCloudDataFetcher, TableRow, TableData, TableCell, TabData, SyntaxTree5 as SyntaxTree, SiteContext, STYLE_SLOT_PREFIX, ResolveOptions, ResolveIncludesTraceResult, ResolveIncludesOptions, ProcessedWikitextDocument, ProcessWikitextOptions, ProcessWikitextDataProvider, ProcessWikitextCallbackContext, Position2 as Position, Point, ParserOptions, Parser, ParseResult4 as ParseResult, ParseFunction, PageRef4 as PageRef, PageData, NormalizedTags, NormalizedParent, NormalizedOrder, NormalizedNumericSelector, NormalizedListPagesQuery, NormalizedDateSelector, NormalizedCategory, ModuleSourceTransform, ModuleParseResult, Module5 as Module, ListUsersVariableContext, ListUsersVariable, ListUsersUserData, ListUsersExternalData, ListUsersDataRequirement, ListUsersDataFetcher, ListUsersCompiledTemplate, ListType, ListPagesVariable, ListPagesQuery, ListPagesExternalData, ListPagesDataRequirement, ListPagesDataFetcher, ListItem, ListData, LinkType, LinkLocation, LinkLabel, LexerOptions, Lexer, IncludeReference, IncludeIterationTrace, IncludeFetcher, IncludeDependency, ImageSource, HeadingLevel, Heading, HeaderType, GallerySize, GalleryOrder, GalleryItem, GalleryData, GalleryContent, FloatAlignment, ExtractionResult, Embed, Element8 as Element, DiagnosticSeverity, Diagnostic4 as Diagnostic, DefinitionListItem, DateItem, DataRequirements, DataProvider, DEFAULT_SETTINGS, ContainerType, ContainerData, CompiledTemplate, CollapsibleData, CodeBlockData2 as CodeBlockData, ClearFloat, AttributeMap, AsyncIncludeFetcher, AnchorTarget, Alignment, AlignType };
1203
+ export { tokenize, text, resolveTagCloud, resolveModules, resolveListUsers, resolveIncludesWithTrace, resolveIncludesAsyncWithTrace, resolveIncludesAsync, resolveIncludes, processWikitext, preprocessIftags, parseTags, parseParent, parseOrder, parseNumericSelector, parseDateSelector, parseCategory, parse, paragraph, normalizeQuery, matchesListPagesSelectors, listItemSubList, listItemElements, list, link, lineBreak, italics, isTagCloudModule, isListUsersModule, horizontalRule, heading, extractListUsersVariables, extractIncludeReferences, extractDataRequirements, definePageData, createToken, createSettings, createPosition, createPoint, container, compileTemplate, compileListUsersTemplate, bold, WikitextSettings5 as WikitextSettings, WikitextMode, Version2 as Version, VariableMap, VariableContext, UserInfo, TokenType, Token, TocEntry2 as TocEntry, TagCloudTagData, TagCloudModuleData, TagCloudExternalData, TagCloudDataRequirement, TagCloudDataFetcher, TableRow, TableData, TableCell, TabData, SyntaxTree5 as SyntaxTree, SiteContext, STYLE_SLOT_PREFIX, ResolveOptions, ResolveIncludesTraceResult, ResolveIncludesOptions, ProcessedWikitextDocument, ProcessWikitextOptions, ProcessWikitextDataProvider, ProcessWikitextCallbackContext, Position2 as Position, Point, ParserOptions, Parser, ParseResult4 as ParseResult, ParseFunction, PageRef4 as PageRef, PageData, NormalizedTags, NormalizedParent, NormalizedOrder, NormalizedNumericSelector, NormalizedListPagesQuery, NormalizedDateSelector, NormalizedCategory, ModuleSourceTransform, ModuleParseResult, Module5 as Module, ListUsersVariableContext, ListUsersVariable, ListUsersUserData, ListUsersExternalData, ListUsersDataRequirement, ListUsersDataFetcher, ListUsersCompiledTemplate, ListType, ListPagesVariable, ListPagesQuery, ListPagesExternalData, ListPagesDataRequirement, ListPagesDataFetcher, ListItem, ListData, LinkType, LinkLocation, LinkLabel, LexerOptions, Lexer, IncludeReference, IncludeIterationTrace, IncludeFetcher, IncludeDependency, ImageSource, HeadingLevel, Heading, HeaderType, GallerySize, GalleryOrder, GalleryItem, GalleryData, GalleryContent, FloatAlignment, ExtractionResult, Embed, Element8 as Element, DiagnosticSeverity, Diagnostic4 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
@@ -5856,6 +5856,60 @@ function resolveListPages(module, data, compiledTemplate, parse) {
5856
5856
  const items = renderListPagesItems(module, data, compiledTemplate, parse);
5857
5857
  return wrapListPagesResult(module, items, parse);
5858
5858
  }
5859
+ // packages/parser/src/parser/rules/block/module/listpages/types/external-data.ts
5860
+ function definePageData(input) {
5861
+ const separator = input.fullname.indexOf(":");
5862
+ const category = separator === -1 ? "_default" : input.fullname.slice(0, separator);
5863
+ const name = separator === -1 ? input.fullname : input.fullname.slice(separator + 1);
5864
+ return {
5865
+ ...input,
5866
+ name: input.name ?? name,
5867
+ category: input.category ?? category,
5868
+ hiddenTags: input.hiddenTags ?? [],
5869
+ children: input.children ?? 0,
5870
+ comments: input.comments ?? 0,
5871
+ size: input.size ?? 0,
5872
+ rating: input.rating ?? 0,
5873
+ ratingVotes: input.ratingVotes ?? 0,
5874
+ revisions: input.revisions ?? 0
5875
+ };
5876
+ }
5877
+ // packages/parser/src/parser/rules/block/module/listpages/selectors.ts
5878
+ function matchesListPagesSelectors(page, query, currentPage) {
5879
+ return matchesCategory(page.category, query.category, currentPage.category) && matchesTags(page, query.tags, currentPage);
5880
+ }
5881
+ function matchesCategory(category, selector, currentCategory) {
5882
+ if (!selector)
5883
+ return category === currentCategory;
5884
+ if (selector.exclude.includes(category))
5885
+ return false;
5886
+ const hasPositiveSelector = selector.all || selector.current || selector.include.length > 0;
5887
+ return !hasPositiveSelector || selector.all || selector.current && category === currentCategory || selector.include.includes(category);
5888
+ }
5889
+ function matchesTags(page, selector, currentPage) {
5890
+ if (!selector)
5891
+ return true;
5892
+ const allTags = new Set([...page.tags, ...page.hiddenTags]);
5893
+ if (selector.all.some((tag) => !allTags.has(tag)))
5894
+ return false;
5895
+ if (selector.any.length > 0 && !selector.any.some((tag) => allTags.has(tag)))
5896
+ return false;
5897
+ if (selector.none.some((tag) => allTags.has(tag)))
5898
+ return false;
5899
+ if (selector.special === "none")
5900
+ return allTags.size === 0;
5901
+ const currentTags = new Set(currentPage.tags.filter((tag) => !tag.startsWith("_")));
5902
+ if (selector.special === "same-visible") {
5903
+ return page.tags.some((tag) => currentTags.has(tag));
5904
+ }
5905
+ if (selector.special === "same-all") {
5906
+ return setsEqual(new Set(page.tags), currentTags);
5907
+ }
5908
+ return true;
5909
+ }
5910
+ function setsEqual(left, right) {
5911
+ return left.size === right.size && [...left].every((value) => right.has(value));
5912
+ }
5859
5913
  // packages/parser/src/parser/rules/block/module/listpages/url-resolution/fields.ts
5860
5914
  var URL_RESOLVABLE_FIELDS = [
5861
5915
  { attr: "offset", queryKey: "offset", type: "number" },
@@ -6285,13 +6339,20 @@ var BASE_PLACEHOLDER_OPEN = "";
6285
6339
  var BASE_PLACEHOLDER_CLOSE = "";
6286
6340
  var RAW_BLOCK_OPEN_PATTERN = /\[\[\s*(code|html)\b[^\]]*\]\]/iy;
6287
6341
  function makeUniqueSentinels(source) {
6288
- let open = BASE_PLACEHOLDER_OPEN;
6289
- let close = BASE_PLACEHOLDER_CLOSE;
6290
- while (source.includes(open) || source.includes(close)) {
6291
- open += BASE_PLACEHOLDER_OPEN;
6292
- close += BASE_PLACEHOLDER_CLOSE;
6342
+ let openRun = 0;
6343
+ let closeRun = 0;
6344
+ let longestOpenRun = 0;
6345
+ let longestCloseRun = 0;
6346
+ for (const char of source) {
6347
+ openRun = char === BASE_PLACEHOLDER_OPEN ? openRun + 1 : 0;
6348
+ closeRun = char === BASE_PLACEHOLDER_CLOSE ? closeRun + 1 : 0;
6349
+ longestOpenRun = Math.max(longestOpenRun, openRun);
6350
+ longestCloseRun = Math.max(longestCloseRun, closeRun);
6293
6351
  }
6294
- return { open, close };
6352
+ return {
6353
+ open: BASE_PLACEHOLDER_OPEN.repeat(longestOpenRun + 1),
6354
+ close: BASE_PLACEHOLDER_CLOSE.repeat(longestCloseRun + 1)
6355
+ };
6295
6356
  }
6296
6357
  function maskRawRegions(source, sentinels) {
6297
6358
  const placeholders = [];
@@ -13694,30 +13755,6 @@ function preprocess(text) {
13694
13755
  return result;
13695
13756
  }
13696
13757
 
13697
- // packages/parser/src/parser/preprocess/expr/evaluate.ts
13698
- import { evaluateExpression as evaluateExpression2, formatExprValue, isTruthy as isTruthy2 } from "@wdprlib/ast";
13699
- function evaluateDirective(kind, match) {
13700
- if (kind === "expr") {
13701
- const result2 = evaluateExpression2(match.head);
13702
- if (result2.success)
13703
- return formatExprValue(result2.value);
13704
- if (result2.error === "empty expression")
13705
- return "";
13706
- return "ERROR";
13707
- }
13708
- if (kind === "if") {
13709
- if (!match.hasPipe)
13710
- return "";
13711
- return isTruthy2(match.head) ? match.thenText : match.elseText;
13712
- }
13713
- if (!match.hasPipe)
13714
- return "";
13715
- const result = evaluateExpression2(match.head);
13716
- if (!result.success)
13717
- return "ERROR";
13718
- return result.value !== 0 && !Number.isNaN(result.value) ? match.thenText : match.elseText;
13719
- }
13720
-
13721
13758
  // packages/parser/src/parser/preprocess/expr/chars.ts
13722
13759
  function isWhitespace(ch) {
13723
13760
  return ch === " " || ch === "\t" || ch === `
@@ -13746,6 +13783,30 @@ function matchDirectiveKind(source, i) {
13746
13783
  return null;
13747
13784
  }
13748
13785
 
13786
+ // packages/parser/src/parser/preprocess/expr/evaluate.ts
13787
+ import { evaluateExpression as evaluateExpression2, formatExprValue, isTruthy as isTruthy2 } from "@wdprlib/ast";
13788
+ function evaluateDirective(kind, match) {
13789
+ if (kind === "expr") {
13790
+ const result2 = evaluateExpression2(match.head);
13791
+ if (result2.success)
13792
+ return formatExprValue(result2.value);
13793
+ if (result2.error === "empty expression")
13794
+ return "";
13795
+ return "ERROR";
13796
+ }
13797
+ if (kind === "if") {
13798
+ if (!match.hasPipe)
13799
+ return "";
13800
+ return isTruthy2(match.head) ? match.thenText : match.elseText;
13801
+ }
13802
+ if (!match.hasPipe)
13803
+ return "";
13804
+ const result = evaluateExpression2(match.head);
13805
+ if (!result.success)
13806
+ return "ERROR";
13807
+ return result.value !== 0 && !Number.isNaN(result.value) ? match.thenText : match.elseText;
13808
+ }
13809
+
13749
13810
  // packages/parser/src/parser/preprocess/expr/parse.ts
13750
13811
  function tryParseInnermostDirective(source, start, kind) {
13751
13812
  const keywordLen = kind === "ifexpr" ? 6 : kind === "expr" ? 4 : 2;
@@ -13847,11 +13908,14 @@ function expandInnermost(source) {
13847
13908
  }
13848
13909
 
13849
13910
  // packages/parser/src/parser/preprocess/expr/index.ts
13911
+ var MAX_EXPR_NESTING = 64;
13850
13912
  function preprocessExpr(source) {
13851
13913
  if (!source.includes("[[#"))
13852
13914
  return source;
13853
13915
  const sentinels = makeUniqueSentinels(source);
13854
13916
  const { masked, placeholders } = maskRawRegions(source, sentinels);
13917
+ if (exceedsExprNestingLimit(masked))
13918
+ return source;
13855
13919
  const reduced = reduceExpr(masked);
13856
13920
  return restorePlaceholders(reduced, placeholders, sentinels);
13857
13921
  }
@@ -13866,6 +13930,26 @@ function reduceExpr(source) {
13866
13930
  }
13867
13931
  return current2;
13868
13932
  }
13933
+ function exceedsExprNestingLimit(source) {
13934
+ const expressionStack = [];
13935
+ let expressionDepth = 0;
13936
+ for (let i = 0;i < source.length; i++) {
13937
+ if (source.startsWith("[[", i)) {
13938
+ const isExpression = matchDirectiveKind(source, i) !== null;
13939
+ expressionStack.push(isExpression);
13940
+ if (isExpression && ++expressionDepth > MAX_EXPR_NESTING)
13941
+ return true;
13942
+ i++;
13943
+ continue;
13944
+ }
13945
+ if (source.startsWith("]]", i)) {
13946
+ if (expressionStack.pop())
13947
+ expressionDepth--;
13948
+ i++;
13949
+ }
13950
+ }
13951
+ return false;
13952
+ }
13869
13953
 
13870
13954
  // packages/parser/src/parser/parse/source.ts
13871
13955
  function prepareSourceForParse(source, options) {
@@ -13893,7 +13977,9 @@ function parse(source, options) {
13893
13977
  return new Parser(tokens, options).parse();
13894
13978
  }
13895
13979
  // packages/parser/src/pipeline/process.ts
13896
- import { DEFAULT_SETTINGS as DEFAULT_SETTINGS2 } from "@wdprlib/ast";
13980
+ import {
13981
+ DEFAULT_SETTINGS as DEFAULT_SETTINGS2
13982
+ } from "@wdprlib/ast";
13897
13983
 
13898
13984
  // packages/parser/src/parser/rules/block/module/resolution/resolve-async.ts
13899
13985
  async function resolveModulesWithAsyncParse(ast, dataProvider, options) {
@@ -14069,6 +14155,11 @@ async function resolveElementChildren(element, context, state) {
14069
14155
  }
14070
14156
 
14071
14157
  // packages/parser/src/pipeline/process.ts
14158
+ var DEFAULT_MODULE_MAX_PASSES = 5;
14159
+ var PIPELINE_DIAGNOSTIC_POSITION = {
14160
+ start: { line: 1, column: 1, offset: 0 },
14161
+ end: { line: 1, column: 1, offset: 0 }
14162
+ };
14072
14163
  async function processWikitext(source, options) {
14073
14164
  const settings = options.settings ?? DEFAULT_SETTINGS2;
14074
14165
  const callbackContext = {
@@ -14076,6 +14167,7 @@ async function processWikitext(source, options) {
14076
14167
  settings
14077
14168
  };
14078
14169
  const dependencies = [];
14170
+ const diagnostics = [];
14079
14171
  const fetchInclude = createRequestIncludeFetcher(options.dataProvider?.fetchInclude ? (pageRef) => options.dataProvider.fetchInclude(pageRef, callbackContext) : undefined);
14080
14172
  const resolveSource = async (input) => {
14081
14173
  if (!fetchInclude)
@@ -14085,6 +14177,9 @@ async function processWikitext(source, options) {
14085
14177
  settings
14086
14178
  });
14087
14179
  dependencies.push(...resolution.dependencies);
14180
+ if (resolution.reachedMaxIterations) {
14181
+ diagnostics.push(createLimitDiagnostic("include-resolution-limit", `Include expansion stopped after ${options.includeMaxIterations ?? 10} iterations.`));
14182
+ }
14088
14183
  return resolution.source;
14089
14184
  };
14090
14185
  const expandedSource = await resolveSource(source);
@@ -14093,28 +14188,52 @@ async function processWikitext(source, options) {
14093
14188
  pageTags: options.page.tags,
14094
14189
  appendImplicitFootnoteBlock: false
14095
14190
  });
14096
- const extraction = extractDataRequirements(initial.ast);
14191
+ diagnostics.push(...initial.diagnostics);
14097
14192
  const dataProvider = createModuleDataProvider(options, callbackContext);
14098
- const resolved = await resolveModulesWithAsyncParse(initial.ast, dataProvider, {
14099
- parse: async (fragmentSource) => parse(await resolveSource(fragmentSource), {
14100
- settings,
14101
- pageTags: options.page.tags,
14102
- appendImplicitFootnoteBlock: false
14103
- }),
14104
- compiledListPagesTemplates: extraction.compiledListPagesTemplates,
14105
- compiledListUsersTemplates: extraction.compiledListUsersTemplates,
14106
- requirements: extraction.requirements,
14107
- urlPath: options.page.urlPath,
14108
- pageTags: options.page.tags
14193
+ const parseFragment = async (fragmentSource) => parse(await resolveSource(fragmentSource), {
14194
+ settings,
14195
+ pageTags: options.page.tags,
14196
+ appendImplicitFootnoteBlock: false
14109
14197
  });
14198
+ let ast = initial.ast;
14199
+ for (let pass = 0;pass < DEFAULT_MODULE_MAX_PASSES; pass++) {
14200
+ const extraction = extractDataRequirements(ast);
14201
+ if (pass > 0 && !hasResolvableRequirements(extraction.requirements, options.dataProvider)) {
14202
+ break;
14203
+ }
14204
+ const resolved = await resolveModulesWithAsyncParse(ast, dataProvider, {
14205
+ parse: parseFragment,
14206
+ compiledListPagesTemplates: extraction.compiledListPagesTemplates,
14207
+ compiledListUsersTemplates: extraction.compiledListUsersTemplates,
14208
+ requirements: extraction.requirements,
14209
+ urlPath: options.page.urlPath,
14210
+ pageTags: options.page.tags
14211
+ });
14212
+ ast = resolved.ast;
14213
+ diagnostics.push(...resolved.diagnostics);
14214
+ }
14215
+ if (hasResolvableRequirements(extractDataRequirements(ast).requirements, options.dataProvider)) {
14216
+ diagnostics.push(createLimitDiagnostic("module-resolution-limit", `Module resolution stopped after ${DEFAULT_MODULE_MAX_PASSES} passes.`));
14217
+ }
14110
14218
  return {
14111
- ast: resolved.ast,
14219
+ ast,
14112
14220
  page: options.page,
14113
14221
  settings,
14114
- diagnostics: [...initial.diagnostics, ...resolved.diagnostics],
14222
+ diagnostics,
14115
14223
  dependencies
14116
14224
  };
14117
14225
  }
14226
+ function hasResolvableRequirements(requirements, provider) {
14227
+ return Boolean(provider?.fetchListPages && requirements.listPages.length > 0 || provider?.fetchListUsers && requirements.listUsers.length > 0 || provider?.fetchTagCloud && requirements.tagCloud.length > 0);
14228
+ }
14229
+ function createLimitDiagnostic(code, message) {
14230
+ return {
14231
+ severity: "warning",
14232
+ code,
14233
+ message,
14234
+ position: PIPELINE_DIAGNOSTIC_POSITION
14235
+ };
14236
+ }
14118
14237
  function createModuleDataProvider(options, context) {
14119
14238
  const provider = options.dataProvider;
14120
14239
  return {
@@ -14159,6 +14278,7 @@ export {
14159
14278
  parse,
14160
14279
  paragraph,
14161
14280
  normalizeQuery,
14281
+ matchesListPagesSelectors,
14162
14282
  listItemSubList,
14163
14283
  listItemElements,
14164
14284
  list,
@@ -14172,6 +14292,7 @@ export {
14172
14292
  extractListUsersVariables,
14173
14293
  extractIncludeReferences,
14174
14294
  extractDataRequirements,
14295
+ definePageData,
14175
14296
  createToken,
14176
14297
  createSettings,
14177
14298
  createPosition,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wdprlib/parser",
3
- "version": "4.4.0",
3
+ "version": "5.0.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.4.0"
44
+ "@wdprlib/ast": "3.0.0"
45
45
  }
46
46
  }
package/src/index.ts CHANGED
@@ -185,6 +185,8 @@ export {
185
185
  parseParent,
186
186
  parseDateSelector,
187
187
  parseNumericSelector,
188
+ definePageData,
189
+ matchesListPagesSelectors,
188
190
  // ListUsers
189
191
  extractListUsersVariables,
190
192
  compileListUsersTemplate,
@@ -7,8 +7,11 @@
7
7
  */
8
8
 
9
9
  import { makeUniqueSentinels, maskRawRegions, restorePlaceholders } from "../utils";
10
+ import { matchDirectiveKind } from "./kind";
10
11
  import { expandInnermost } from "./scan";
11
12
 
13
+ const MAX_EXPR_NESTING = 64;
14
+
12
15
  /**
13
16
  * Resolve every `[[#if]]` / `[[#ifexpr]]` / `[[#expr]]` that sits inside
14
17
  * another block's opener (depth > 0). Top-level directives are left for
@@ -21,6 +24,7 @@ export function preprocessExpr(source: string): string {
21
24
 
22
25
  const sentinels = makeUniqueSentinels(source);
23
26
  const { masked, placeholders } = maskRawRegions(source, sentinels);
27
+ if (exceedsExprNestingLimit(masked)) return source;
24
28
  const reduced = reduceExpr(masked);
25
29
  return restorePlaceholders(reduced, placeholders, sentinels);
26
30
  }
@@ -43,3 +47,25 @@ function reduceExpr(source: string): string {
43
47
  }
44
48
  return current;
45
49
  }
50
+
51
+ function exceedsExprNestingLimit(source: string): boolean {
52
+ const expressionStack: boolean[] = [];
53
+ let expressionDepth = 0;
54
+
55
+ for (let i = 0; i < source.length; i++) {
56
+ if (source.startsWith("[[", i)) {
57
+ const isExpression = matchDirectiveKind(source, i) !== null;
58
+ expressionStack.push(isExpression);
59
+ if (isExpression && ++expressionDepth > MAX_EXPR_NESTING) return true;
60
+ i++;
61
+ continue;
62
+ }
63
+
64
+ if (source.startsWith("]]", i)) {
65
+ if (expressionStack.pop()) expressionDepth--;
66
+ i++;
67
+ }
68
+ }
69
+
70
+ return false;
71
+ }
@@ -13,16 +13,25 @@ export interface Sentinels {
13
13
  * Choose sentinel strings that are guaranteed not to appear in `source`.
14
14
  * The placeholders we splice into the masked source have the form
15
15
  * `<open><digits><close>`, so the restore pass must not confuse them
16
- * with content. Extends both sentinel characters until neither appears.
16
+ * with content.
17
17
  */
18
18
  export function makeUniqueSentinels(source: string): Sentinels {
19
- let open = BASE_PLACEHOLDER_OPEN;
20
- let close = BASE_PLACEHOLDER_CLOSE;
21
- while (source.includes(open) || source.includes(close)) {
22
- open += BASE_PLACEHOLDER_OPEN;
23
- close += BASE_PLACEHOLDER_CLOSE;
19
+ let openRun = 0;
20
+ let closeRun = 0;
21
+ let longestOpenRun = 0;
22
+ let longestCloseRun = 0;
23
+
24
+ for (const char of source) {
25
+ openRun = char === BASE_PLACEHOLDER_OPEN ? openRun + 1 : 0;
26
+ closeRun = char === BASE_PLACEHOLDER_CLOSE ? closeRun + 1 : 0;
27
+ longestOpenRun = Math.max(longestOpenRun, openRun);
28
+ longestCloseRun = Math.max(longestCloseRun, closeRun);
24
29
  }
25
- return { open, close };
30
+
31
+ return {
32
+ open: BASE_PLACEHOLDER_OPEN.repeat(longestOpenRun + 1),
33
+ close: BASE_PLACEHOLDER_CLOSE.repeat(longestCloseRun + 1),
34
+ };
26
35
  }
27
36
 
28
37
  /**
@@ -82,6 +82,8 @@ export {
82
82
  parseParent,
83
83
  parseDateSelector,
84
84
  parseNumericSelector,
85
+ definePageData,
86
+ matchesListPagesSelectors,
85
87
  } from "./listpages";
86
88
 
87
89
  // IfTags module
@@ -59,6 +59,8 @@ export { extractDataRequirements } from "./extract";
59
59
  // Resolution
60
60
  export type { ParseFunction, ListPagesModuleData } from "./resolve";
61
61
  export { isListPagesModule, resolveListPages } from "./resolve";
62
+ export { definePageData } from "./types/external-data";
63
+ export { matchesListPagesSelectors } from "./selectors";
62
64
 
63
65
  // Compiler
64
66
  export { compileTemplate } from "./compiler";
@@ -0,0 +1,64 @@
1
+ import type { PageData } from "./types/external-data";
2
+ import type {
3
+ NormalizedCategory,
4
+ NormalizedListPagesQuery,
5
+ NormalizedTags,
6
+ } from "./types/normalized-query";
7
+
8
+ type SelectorPage = Pick<PageData, "category" | "tags" | "hiddenTags">;
9
+ type CurrentPage = { category: string; tags: readonly string[] };
10
+
11
+ export function matchesListPagesSelectors(
12
+ page: SelectorPage,
13
+ query: Pick<NormalizedListPagesQuery, "category" | "tags">,
14
+ currentPage: CurrentPage,
15
+ ): boolean {
16
+ return (
17
+ matchesCategory(page.category, query.category, currentPage.category) &&
18
+ matchesTags(page, query.tags, currentPage)
19
+ );
20
+ }
21
+
22
+ function matchesCategory(
23
+ category: string,
24
+ selector: NormalizedCategory | undefined,
25
+ currentCategory: string,
26
+ ): boolean {
27
+ if (!selector) return category === currentCategory;
28
+ if (selector.exclude.includes(category)) return false;
29
+
30
+ const hasPositiveSelector = selector.all || selector.current || selector.include.length > 0;
31
+ return (
32
+ !hasPositiveSelector ||
33
+ selector.all ||
34
+ (selector.current && category === currentCategory) ||
35
+ selector.include.includes(category)
36
+ );
37
+ }
38
+
39
+ function matchesTags(
40
+ page: Pick<SelectorPage, "tags" | "hiddenTags">,
41
+ selector: NormalizedTags | undefined,
42
+ currentPage: Pick<CurrentPage, "tags">,
43
+ ): boolean {
44
+ if (!selector) return true;
45
+
46
+ const allTags = new Set([...page.tags, ...page.hiddenTags]);
47
+ if (selector.all.some((tag) => !allTags.has(tag))) return false;
48
+ if (selector.any.length > 0 && !selector.any.some((tag) => allTags.has(tag))) return false;
49
+ if (selector.none.some((tag) => allTags.has(tag))) return false;
50
+
51
+ if (selector.special === "none") return allTags.size === 0;
52
+ const currentTags = new Set(currentPage.tags.filter((tag) => !tag.startsWith("_")));
53
+ if (selector.special === "same-visible") {
54
+ return page.tags.some((tag) => currentTags.has(tag));
55
+ }
56
+ if (selector.special === "same-all") {
57
+ return setsEqual(new Set(page.tags), currentTags);
58
+ }
59
+ return true;
60
+ }
61
+
62
+ function setsEqual(left: ReadonlySet<string>, right: ReadonlySet<string>): boolean {
63
+ return left.size === right.size && [...left].every((value) => right.has(value));
64
+ }
@@ -58,6 +58,28 @@ export interface PageData {
58
58
  revisions: number;
59
59
  }
60
60
 
61
+ type PageDataInput = Pick<PageData, "fullname" | "title" | "createdAt" | "updatedAt" | "tags"> &
62
+ Partial<Omit<PageData, "fullname" | "title" | "createdAt" | "updatedAt" | "tags">>;
63
+
64
+ export function definePageData(input: PageDataInput): PageData {
65
+ const separator = input.fullname.indexOf(":");
66
+ const category = separator === -1 ? "_default" : input.fullname.slice(0, separator);
67
+ const name = separator === -1 ? input.fullname : input.fullname.slice(separator + 1);
68
+
69
+ return {
70
+ ...input,
71
+ name: input.name ?? name,
72
+ category: input.category ?? category,
73
+ hiddenTags: input.hiddenTags ?? [],
74
+ children: input.children ?? 0,
75
+ comments: input.comments ?? 0,
76
+ size: input.size ?? 0,
77
+ rating: input.rating ?? 0,
78
+ ratingVotes: input.ratingVotes ?? 0,
79
+ revisions: input.revisions ?? 0,
80
+ };
81
+ }
82
+
61
83
  /**
62
84
  * Site context information.
63
85
  */
@@ -1,4 +1,9 @@
1
- import { DEFAULT_SETTINGS, type PageRef, type WikitextPageContext } from "@wdprlib/ast";
1
+ import {
2
+ DEFAULT_SETTINGS,
3
+ type Diagnostic,
4
+ type PageRef,
5
+ type WikitextPageContext,
6
+ } from "@wdprlib/ast";
2
7
  import { parse } from "../parser";
3
8
  import { extractDataRequirements } from "../parser/rules/block/module/listpages/extract";
4
9
  import {
@@ -14,6 +19,12 @@ import type {
14
19
  ProcessWikitextOptions,
15
20
  } from "./types";
16
21
 
22
+ const DEFAULT_MODULE_MAX_PASSES = 5;
23
+ const PIPELINE_DIAGNOSTIC_POSITION = {
24
+ start: { line: 1, column: 1, offset: 0 },
25
+ end: { line: 1, column: 1, offset: 0 },
26
+ };
27
+
17
28
  export async function processWikitext<TPage extends WikitextPageContext>(
18
29
  source: string,
19
30
  options: ProcessWikitextOptions<TPage>,
@@ -24,6 +35,7 @@ export async function processWikitext<TPage extends WikitextPageContext>(
24
35
  settings,
25
36
  };
26
37
  const dependencies: IncludeDependency[] = [];
38
+ const diagnostics: Diagnostic[] = [];
27
39
  const fetchInclude = createRequestIncludeFetcher(
28
40
  options.dataProvider?.fetchInclude
29
41
  ? (pageRef) => options.dataProvider!.fetchInclude!(pageRef, callbackContext)
@@ -36,6 +48,14 @@ export async function processWikitext<TPage extends WikitextPageContext>(
36
48
  settings,
37
49
  });
38
50
  dependencies.push(...resolution.dependencies);
51
+ if (resolution.reachedMaxIterations) {
52
+ diagnostics.push(
53
+ createLimitDiagnostic(
54
+ "include-resolution-limit",
55
+ `Include expansion stopped after ${options.includeMaxIterations ?? 10} iterations.`,
56
+ ),
57
+ );
58
+ }
39
59
  return resolution.source;
40
60
  };
41
61
 
@@ -45,31 +65,78 @@ export async function processWikitext<TPage extends WikitextPageContext>(
45
65
  pageTags: options.page.tags,
46
66
  appendImplicitFootnoteBlock: false,
47
67
  });
48
- const extraction = extractDataRequirements(initial.ast);
68
+ diagnostics.push(...initial.diagnostics);
49
69
  const dataProvider = createModuleDataProvider(options, callbackContext);
50
- const resolved = await resolveModulesWithAsyncParse(initial.ast, dataProvider, {
51
- parse: async (fragmentSource) =>
52
- parse(await resolveSource(fragmentSource), {
53
- settings,
54
- pageTags: options.page.tags,
55
- appendImplicitFootnoteBlock: false,
56
- }),
57
- compiledListPagesTemplates: extraction.compiledListPagesTemplates,
58
- compiledListUsersTemplates: extraction.compiledListUsersTemplates,
59
- requirements: extraction.requirements,
60
- urlPath: options.page.urlPath,
61
- pageTags: options.page.tags,
62
- });
70
+ const parseFragment = async (fragmentSource: string) =>
71
+ parse(await resolveSource(fragmentSource), {
72
+ settings,
73
+ pageTags: options.page.tags,
74
+ appendImplicitFootnoteBlock: false,
75
+ });
76
+ let ast = initial.ast;
77
+
78
+ for (let pass = 0; pass < DEFAULT_MODULE_MAX_PASSES; pass++) {
79
+ const extraction = extractDataRequirements(ast);
80
+ if (pass > 0 && !hasResolvableRequirements(extraction.requirements, options.dataProvider)) {
81
+ break;
82
+ }
83
+
84
+ const resolved = await resolveModulesWithAsyncParse(ast, dataProvider, {
85
+ parse: parseFragment,
86
+ compiledListPagesTemplates: extraction.compiledListPagesTemplates,
87
+ compiledListUsersTemplates: extraction.compiledListUsersTemplates,
88
+ requirements: extraction.requirements,
89
+ urlPath: options.page.urlPath,
90
+ pageTags: options.page.tags,
91
+ });
92
+ ast = resolved.ast;
93
+ diagnostics.push(...resolved.diagnostics);
94
+ }
95
+
96
+ if (hasResolvableRequirements(extractDataRequirements(ast).requirements, options.dataProvider)) {
97
+ diagnostics.push(
98
+ createLimitDiagnostic(
99
+ "module-resolution-limit",
100
+ `Module resolution stopped after ${DEFAULT_MODULE_MAX_PASSES} passes.`,
101
+ ),
102
+ );
103
+ }
63
104
 
64
105
  return {
65
- ast: resolved.ast,
106
+ ast,
66
107
  page: options.page,
67
108
  settings,
68
- diagnostics: [...initial.diagnostics, ...resolved.diagnostics],
109
+ diagnostics,
69
110
  dependencies,
70
111
  };
71
112
  }
72
113
 
114
+ function hasResolvableRequirements(
115
+ requirements: ReturnType<typeof extractDataRequirements>["requirements"],
116
+ provider:
117
+ | {
118
+ fetchListPages?: unknown;
119
+ fetchListUsers?: unknown;
120
+ fetchTagCloud?: unknown;
121
+ }
122
+ | undefined,
123
+ ): boolean {
124
+ return Boolean(
125
+ (provider?.fetchListPages && requirements.listPages.length > 0) ||
126
+ (provider?.fetchListUsers && requirements.listUsers.length > 0) ||
127
+ (provider?.fetchTagCloud && requirements.tagCloud.length > 0),
128
+ );
129
+ }
130
+
131
+ function createLimitDiagnostic(code: string, message: string): Diagnostic {
132
+ return {
133
+ severity: "warning",
134
+ code,
135
+ message,
136
+ position: PIPELINE_DIAGNOSTIC_POSITION,
137
+ };
138
+ }
139
+
73
140
  function createModuleDataProvider<TPage extends WikitextPageContext>(
74
141
  options: ProcessWikitextOptions<TPage>,
75
142
  context: ProcessWikitextCallbackContext<TPage>,