@wdprlib/parser 5.2.0 → 5.3.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
@@ -82,8 +82,8 @@ module.exports = __toCommonJS(exports_src);
82
82
 
83
83
  // packages/parser/src/build-info.generated.ts
84
84
  var buildInfo = Object.freeze({
85
- version: "5.2.0",
86
- sha: "cc290b0d882de3ed9ea33ec060ddfc8a7b9da59a",
85
+ version: "5.3.0",
86
+ sha: "b71b84c705e05717f5ea12fd89684a91b669dbff",
87
87
  dirty: false
88
88
  });
89
89
 
@@ -6253,9 +6253,9 @@ function renderListPagesItems(module2, data, compiledTemplate, parse) {
6253
6253
  continue;
6254
6254
  const ctx = {
6255
6255
  page,
6256
- index: i + 1,
6256
+ index: (data.pagination?.offset ?? 0) + i + 1,
6257
6257
  total: data.totalCount,
6258
- limit: module2.limit,
6258
+ limit: data.pagination ? data.pagination.limit : module2.limit,
6259
6259
  site: data.site
6260
6260
  };
6261
6261
  const itemAst = getModuleParseAst(parse(compiledTemplate(ctx)));
@@ -6275,9 +6275,46 @@ function renderListPagesItems(module2, data, compiledTemplate, parse) {
6275
6275
  return items;
6276
6276
  }
6277
6277
 
6278
+ // packages/parser/src/parser/rules/block/module/listpages/resolution/pager.ts
6279
+ function createListPagesPager(data) {
6280
+ const pagination = data.pagination;
6281
+ if (!pagination || pagination.totalPages <= 1)
6282
+ return [];
6283
+ const { currentPage, totalPages, urlPath, parameter } = pagination;
6284
+ const pages = new Set([1, totalPages]);
6285
+ for (let page = Math.max(1, currentPage - 2);page <= Math.min(totalPages, currentPage + 2); page++) {
6286
+ pages.add(page);
6287
+ }
6288
+ return [
6289
+ {
6290
+ element: "pager",
6291
+ data: {
6292
+ currentPage,
6293
+ totalPages,
6294
+ pages: [...pages].sort((a, b) => a - b).map((page) => ({
6295
+ page,
6296
+ href: pageUrl(urlPath, parameter, page)
6297
+ }))
6298
+ }
6299
+ }
6300
+ ];
6301
+ }
6302
+ function pageUrl(urlPath, parameter, page) {
6303
+ const suffixStart = urlPath.search(/[?#]/);
6304
+ const path = suffixStart === -1 ? urlPath : urlPath.slice(0, suffixStart);
6305
+ const suffix = suffixStart === -1 ? "" : urlPath.slice(suffixStart);
6306
+ const parts = path.split("/");
6307
+ const result = parts.slice(0, 2);
6308
+ for (let index = 2;index < parts.length; index += 2) {
6309
+ if (parts[index] !== parameter)
6310
+ result.push(...parts.slice(index, index + 2));
6311
+ }
6312
+ return `${result.join("/").replace(/\/$/, "")}/${parameter}/${page}${suffix}`;
6313
+ }
6314
+
6278
6315
  // packages/parser/src/parser/rules/block/module/listpages/resolution/wrapper.ts
6279
- function wrapListPagesResult(module2, items, parse) {
6280
- if (items.length === 0) {
6316
+ function wrapListPagesResult(module2, items, parse, data) {
6317
+ if (items.length === 0 && (!data || data.pages.length === 0)) {
6281
6318
  return [];
6282
6319
  }
6283
6320
  const result = [];
@@ -6290,6 +6327,8 @@ function wrapListPagesResult(module2, items, parse) {
6290
6327
  const appendAst = getModuleParseAst(parse(module2["append-line"]));
6291
6328
  result.push(...appendAst.elements);
6292
6329
  }
6330
+ if (data)
6331
+ result.push(...createListPagesPager(data));
6293
6332
  if (module2.wrapper) {
6294
6333
  return [
6295
6334
  {
@@ -6311,7 +6350,7 @@ function isListPagesModule2(module2) {
6311
6350
  }
6312
6351
  function resolveListPages(module2, data, compiledTemplate, parse) {
6313
6352
  const items = renderListPagesItems(module2, data, compiledTemplate, parse);
6314
- return wrapListPagesResult(module2, items, parse);
6353
+ return wrapListPagesResult(module2, items, parse, data);
6315
6354
  }
6316
6355
  // packages/parser/src/parser/rules/block/module/listpages/types/external-data.ts
6317
6356
  function definePageData(input) {
@@ -6408,10 +6447,10 @@ var URL_RESOLVABLE_FIELDS = [
6408
6447
  ];
6409
6448
 
6410
6449
  // packages/parser/src/parser/rules/block/module/listpages/url-resolution/params.ts
6411
- function parseUrlParams(url) {
6450
+ function parseUrlParams(url, hasPageName = false) {
6412
6451
  const params = new Map;
6413
- const parts = url.split("/").filter(Boolean);
6414
- const pairStart = isUrlParameter(parts[0]) ? 0 : 1;
6452
+ const parts = url.split(/[?#]/, 1)[0].split("/").filter(Boolean);
6453
+ const pairStart = !hasPageName && isUrlParameter(parts[0]) ? 0 : 1;
6415
6454
  for (let i = pairStart;i < parts.length - 1; i += 2) {
6416
6455
  const key = parts[i];
6417
6456
  const value = parts[i + 1];
@@ -7509,18 +7548,66 @@ function resolveListUsers(_module, data, compiledTemplate, parse) {
7509
7548
  return itemAst.elements;
7510
7549
  }
7511
7550
  // packages/parser/src/parser/rules/block/module/resolution/data-maps.ts
7512
- async function buildListPagesDataMap(dataProvider, requirements, urlPath) {
7551
+ async function buildListPagesDataMap(dataProvider, requirements, urlPath, paginationState = { nextUnprefixed: 1 }) {
7513
7552
  const dataMap = new Map;
7514
- const urlParams = parseUrlParams(urlPath ?? "");
7553
+ const urlParams = parseUrlParams(urlPath ?? "", true);
7515
7554
  for (const req of requirements) {
7516
7555
  const normalizedQuery = resolveAndNormalizeQuery(req, urlParams);
7517
- const data = await dataProvider.fetchListPages?.(normalizedQuery, req);
7556
+ if (!urlPath || !urlPath.startsWith("/") || urlPath.startsWith("//")) {
7557
+ const data2 = await dataProvider.fetchListPages?.(normalizedQuery, req);
7558
+ if (data2)
7559
+ dataMap.set(req.id, data2);
7560
+ continue;
7561
+ }
7562
+ const ordinal = req.urlAttrPrefix ? 0 : paginationState.nextUnprefixed++;
7563
+ const parameter = req.urlAttrPrefix ? `${encodeURIComponent(req.urlAttrPrefix)}_p` : ordinal === 1 ? "p" : `p${ordinal}`;
7564
+ const perPage = Math.min(250, positiveInteger(normalizedQuery.perPage, 20));
7565
+ const baseOffset = positiveInteger(normalizedQuery.offset, 0);
7566
+ const limit = normalizedQuery.limit !== undefined && normalizedQuery.limit >= 0 ? Math.floor(normalizedQuery.limit) : undefined;
7567
+ const rawPage = urlParams.get(parameter) ?? "1";
7568
+ let currentPage = /^\d+$/.test(rawPage) ? positiveInteger(Number(rawPage), 1) : 1;
7569
+ currentPage = Math.min(currentPage, Math.floor((Number.MAX_SAFE_INTEGER - baseOffset) / perPage) + 1);
7570
+ if (limit !== undefined)
7571
+ currentPage = Math.min(currentPage, Math.max(1, Math.ceil(limit / perPage)));
7572
+ const fetchPage = () => {
7573
+ const relativeOffset = (currentPage - 1) * perPage;
7574
+ return dataProvider.fetchListPages?.({
7575
+ ...normalizedQuery,
7576
+ offset: baseOffset + relativeOffset,
7577
+ limit: Math.min(perPage, limit === undefined ? perPage : Math.max(0, limit - relativeOffset)),
7578
+ perPage
7579
+ }, req);
7580
+ };
7581
+ let data = await fetchPage();
7518
7582
  if (data) {
7519
- dataMap.set(req.id, data);
7583
+ const count = Math.min(Math.max(0, data.totalCount - baseOffset), limit ?? Infinity);
7584
+ const totalPages = Math.ceil(count / perPage);
7585
+ const lastPage = Math.max(1, totalPages);
7586
+ if (currentPage > lastPage) {
7587
+ currentPage = lastPage;
7588
+ data = await fetchPage();
7589
+ }
7590
+ if (data)
7591
+ dataMap.set(req.id, {
7592
+ ...data,
7593
+ pages: data.pages.slice(0, Math.min(perPage, Math.max(0, count - (currentPage - 1) * perPage))),
7594
+ pagination: {
7595
+ currentPage,
7596
+ perPage,
7597
+ totalPages,
7598
+ offset: baseOffset + (currentPage - 1) * perPage,
7599
+ limit,
7600
+ urlPath,
7601
+ parameter
7602
+ }
7603
+ });
7520
7604
  }
7521
7605
  }
7522
7606
  return dataMap;
7523
7607
  }
7608
+ function positiveInteger(value, fallback) {
7609
+ return value !== undefined && Number.isSafeInteger(value) && value > 0 ? value : fallback;
7610
+ }
7524
7611
  async function buildListUsersDataMap(dataProvider, requirements) {
7525
7612
  const dataMap = new Map;
7526
7613
  for (const req of requirements) {
@@ -14388,7 +14475,7 @@ async function resolveModulesWithAsyncParse(ast, dataProvider, options) {
14388
14475
  registry.register(ast);
14389
14476
  const parse2 = async (source) => registry.register(await options.parse(source));
14390
14477
  const [listPagesData, listUsersData, tagCloudData] = await Promise.all([
14391
- buildListPagesDataMap(dataProvider, options.requirements.listPages ?? [], options.urlPath),
14478
+ buildListPagesDataMap(dataProvider, options.requirements.listPages ?? [], options.urlPath, options.paginationState),
14392
14479
  buildListUsersDataMap(dataProvider, options.requirements.listUsers ?? []),
14393
14480
  buildTagCloudDataMap(dataProvider, options.requirements.tagCloud ?? [])
14394
14481
  ]);
@@ -14476,9 +14563,9 @@ async function resolveListPagesAsync(module2, data, template, parse2) {
14476
14563
  continue;
14477
14564
  const variableContext = {
14478
14565
  page,
14479
- index: i + 1,
14566
+ index: (data.pagination?.offset ?? 0) + i + 1,
14480
14567
  total: data.totalCount,
14481
- limit: module2.limit,
14568
+ limit: data.pagination ? data.pagination.limit : module2.limit,
14482
14569
  site: data.site
14483
14570
  };
14484
14571
  const parsed = await parse2(template(variableContext));
@@ -14498,6 +14585,7 @@ async function resolveListPagesAsync(module2, data, template, parse2) {
14498
14585
  if (module2["append-line"] && !module2.separate) {
14499
14586
  result.push(...(await parse2(module2["append-line"])).elements);
14500
14587
  }
14588
+ result.push(...createListPagesPager(data));
14501
14589
  return module2.wrapper ? [
14502
14590
  {
14503
14591
  element: "container",
@@ -14598,6 +14686,9 @@ async function processWikitext(source, options) {
14598
14686
  const dataProvider = createModuleDataProvider(options, callbackContext);
14599
14687
  const parseFragment = parseSource2;
14600
14688
  let ast = initial.ast;
14689
+ const paginationState = { nextUnprefixed: 1 };
14690
+ const requestedPath = options.page.urlPath;
14691
+ const urlPath = !requestedPath || /^\/(?:[?#]|$)/.test(requestedPath) ? `/${options.page.fullName}${requestedPath?.slice(1) ?? ""}` : requestedPath;
14601
14692
  for (let pass = 0;pass < DEFAULT_MODULE_MAX_PASSES; pass++) {
14602
14693
  const extraction = extractDataRequirements(ast);
14603
14694
  if (pass > 0 && !hasResolvableRequirements(extraction.requirements, options.dataProvider)) {
@@ -14608,7 +14699,8 @@ async function processWikitext(source, options) {
14608
14699
  compiledListPagesTemplates: extraction.compiledListPagesTemplates,
14609
14700
  compiledListUsersTemplates: extraction.compiledListUsersTemplates,
14610
14701
  requirements: extraction.requirements,
14611
- urlPath: options.page.urlPath,
14702
+ urlPath,
14703
+ paginationState,
14612
14704
  pageTags: options.page.tags
14613
14705
  });
14614
14706
  ast = resolved.ast;
package/dist/index.d.cts CHANGED
@@ -691,8 +691,21 @@ interface SiteContext {
691
691
  */
692
692
  interface ListPagesExternalData {
693
693
  pages: PageData[];
694
+ /** Matching items before offset and limit; use the same visibility filters as pages. */
694
695
  totalCount: number;
695
696
  site: SiteContext;
697
+ /** Filled by module resolution when a page URL is available. */
698
+ pagination?: {
699
+ currentPage: number;
700
+ perPage: number;
701
+ totalPages: number;
702
+ /** Actual fetch offset, including the module's starting offset. */
703
+ offset: number;
704
+ /** Total item limit after URL resolution, not the fetch batch size. */
705
+ limit?: number;
706
+ urlPath: string;
707
+ parameter: string;
708
+ };
696
709
  }
697
710
  /**
698
711
  * Normalized tags selector.
package/dist/index.d.ts CHANGED
@@ -691,8 +691,21 @@ interface SiteContext {
691
691
  */
692
692
  interface ListPagesExternalData {
693
693
  pages: PageData[];
694
+ /** Matching items before offset and limit; use the same visibility filters as pages. */
694
695
  totalCount: number;
695
696
  site: SiteContext;
697
+ /** Filled by module resolution when a page URL is available. */
698
+ pagination?: {
699
+ currentPage: number;
700
+ perPage: number;
701
+ totalPages: number;
702
+ /** Actual fetch offset, including the module's starting offset. */
703
+ offset: number;
704
+ /** Total item limit after URL resolution, not the fetch batch size. */
705
+ limit?: number;
706
+ urlPath: string;
707
+ parameter: string;
708
+ };
696
709
  }
697
710
  /**
698
711
  * Normalized tags selector.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // packages/parser/src/build-info.generated.ts
2
2
  var buildInfo = Object.freeze({
3
- version: "5.2.0",
4
- sha: "cc290b0d882de3ed9ea33ec060ddfc8a7b9da59a",
3
+ version: "5.3.0",
4
+ sha: "b71b84c705e05717f5ea12fd89684a91b669dbff",
5
5
  dirty: false
6
6
  });
7
7
 
@@ -6186,9 +6186,9 @@ function renderListPagesItems(module, data, compiledTemplate, parse) {
6186
6186
  continue;
6187
6187
  const ctx = {
6188
6188
  page,
6189
- index: i + 1,
6189
+ index: (data.pagination?.offset ?? 0) + i + 1,
6190
6190
  total: data.totalCount,
6191
- limit: module.limit,
6191
+ limit: data.pagination ? data.pagination.limit : module.limit,
6192
6192
  site: data.site
6193
6193
  };
6194
6194
  const itemAst = getModuleParseAst(parse(compiledTemplate(ctx)));
@@ -6208,9 +6208,46 @@ function renderListPagesItems(module, data, compiledTemplate, parse) {
6208
6208
  return items;
6209
6209
  }
6210
6210
 
6211
+ // packages/parser/src/parser/rules/block/module/listpages/resolution/pager.ts
6212
+ function createListPagesPager(data) {
6213
+ const pagination = data.pagination;
6214
+ if (!pagination || pagination.totalPages <= 1)
6215
+ return [];
6216
+ const { currentPage, totalPages, urlPath, parameter } = pagination;
6217
+ const pages = new Set([1, totalPages]);
6218
+ for (let page = Math.max(1, currentPage - 2);page <= Math.min(totalPages, currentPage + 2); page++) {
6219
+ pages.add(page);
6220
+ }
6221
+ return [
6222
+ {
6223
+ element: "pager",
6224
+ data: {
6225
+ currentPage,
6226
+ totalPages,
6227
+ pages: [...pages].sort((a, b) => a - b).map((page) => ({
6228
+ page,
6229
+ href: pageUrl(urlPath, parameter, page)
6230
+ }))
6231
+ }
6232
+ }
6233
+ ];
6234
+ }
6235
+ function pageUrl(urlPath, parameter, page) {
6236
+ const suffixStart = urlPath.search(/[?#]/);
6237
+ const path = suffixStart === -1 ? urlPath : urlPath.slice(0, suffixStart);
6238
+ const suffix = suffixStart === -1 ? "" : urlPath.slice(suffixStart);
6239
+ const parts = path.split("/");
6240
+ const result = parts.slice(0, 2);
6241
+ for (let index = 2;index < parts.length; index += 2) {
6242
+ if (parts[index] !== parameter)
6243
+ result.push(...parts.slice(index, index + 2));
6244
+ }
6245
+ return `${result.join("/").replace(/\/$/, "")}/${parameter}/${page}${suffix}`;
6246
+ }
6247
+
6211
6248
  // packages/parser/src/parser/rules/block/module/listpages/resolution/wrapper.ts
6212
- function wrapListPagesResult(module, items, parse) {
6213
- if (items.length === 0) {
6249
+ function wrapListPagesResult(module, items, parse, data) {
6250
+ if (items.length === 0 && (!data || data.pages.length === 0)) {
6214
6251
  return [];
6215
6252
  }
6216
6253
  const result = [];
@@ -6223,6 +6260,8 @@ function wrapListPagesResult(module, items, parse) {
6223
6260
  const appendAst = getModuleParseAst(parse(module["append-line"]));
6224
6261
  result.push(...appendAst.elements);
6225
6262
  }
6263
+ if (data)
6264
+ result.push(...createListPagesPager(data));
6226
6265
  if (module.wrapper) {
6227
6266
  return [
6228
6267
  {
@@ -6244,7 +6283,7 @@ function isListPagesModule2(module) {
6244
6283
  }
6245
6284
  function resolveListPages(module, data, compiledTemplate, parse) {
6246
6285
  const items = renderListPagesItems(module, data, compiledTemplate, parse);
6247
- return wrapListPagesResult(module, items, parse);
6286
+ return wrapListPagesResult(module, items, parse, data);
6248
6287
  }
6249
6288
  // packages/parser/src/parser/rules/block/module/listpages/types/external-data.ts
6250
6289
  function definePageData(input) {
@@ -6341,10 +6380,10 @@ var URL_RESOLVABLE_FIELDS = [
6341
6380
  ];
6342
6381
 
6343
6382
  // packages/parser/src/parser/rules/block/module/listpages/url-resolution/params.ts
6344
- function parseUrlParams(url) {
6383
+ function parseUrlParams(url, hasPageName = false) {
6345
6384
  const params = new Map;
6346
- const parts = url.split("/").filter(Boolean);
6347
- const pairStart = isUrlParameter(parts[0]) ? 0 : 1;
6385
+ const parts = url.split(/[?#]/, 1)[0].split("/").filter(Boolean);
6386
+ const pairStart = !hasPageName && isUrlParameter(parts[0]) ? 0 : 1;
6348
6387
  for (let i = pairStart;i < parts.length - 1; i += 2) {
6349
6388
  const key = parts[i];
6350
6389
  const value = parts[i + 1];
@@ -7442,18 +7481,66 @@ function resolveListUsers(_module, data, compiledTemplate, parse) {
7442
7481
  return itemAst.elements;
7443
7482
  }
7444
7483
  // packages/parser/src/parser/rules/block/module/resolution/data-maps.ts
7445
- async function buildListPagesDataMap(dataProvider, requirements, urlPath) {
7484
+ async function buildListPagesDataMap(dataProvider, requirements, urlPath, paginationState = { nextUnprefixed: 1 }) {
7446
7485
  const dataMap = new Map;
7447
- const urlParams = parseUrlParams(urlPath ?? "");
7486
+ const urlParams = parseUrlParams(urlPath ?? "", true);
7448
7487
  for (const req of requirements) {
7449
7488
  const normalizedQuery = resolveAndNormalizeQuery(req, urlParams);
7450
- const data = await dataProvider.fetchListPages?.(normalizedQuery, req);
7489
+ if (!urlPath || !urlPath.startsWith("/") || urlPath.startsWith("//")) {
7490
+ const data2 = await dataProvider.fetchListPages?.(normalizedQuery, req);
7491
+ if (data2)
7492
+ dataMap.set(req.id, data2);
7493
+ continue;
7494
+ }
7495
+ const ordinal = req.urlAttrPrefix ? 0 : paginationState.nextUnprefixed++;
7496
+ const parameter = req.urlAttrPrefix ? `${encodeURIComponent(req.urlAttrPrefix)}_p` : ordinal === 1 ? "p" : `p${ordinal}`;
7497
+ const perPage = Math.min(250, positiveInteger(normalizedQuery.perPage, 20));
7498
+ const baseOffset = positiveInteger(normalizedQuery.offset, 0);
7499
+ const limit = normalizedQuery.limit !== undefined && normalizedQuery.limit >= 0 ? Math.floor(normalizedQuery.limit) : undefined;
7500
+ const rawPage = urlParams.get(parameter) ?? "1";
7501
+ let currentPage = /^\d+$/.test(rawPage) ? positiveInteger(Number(rawPage), 1) : 1;
7502
+ currentPage = Math.min(currentPage, Math.floor((Number.MAX_SAFE_INTEGER - baseOffset) / perPage) + 1);
7503
+ if (limit !== undefined)
7504
+ currentPage = Math.min(currentPage, Math.max(1, Math.ceil(limit / perPage)));
7505
+ const fetchPage = () => {
7506
+ const relativeOffset = (currentPage - 1) * perPage;
7507
+ return dataProvider.fetchListPages?.({
7508
+ ...normalizedQuery,
7509
+ offset: baseOffset + relativeOffset,
7510
+ limit: Math.min(perPage, limit === undefined ? perPage : Math.max(0, limit - relativeOffset)),
7511
+ perPage
7512
+ }, req);
7513
+ };
7514
+ let data = await fetchPage();
7451
7515
  if (data) {
7452
- dataMap.set(req.id, data);
7516
+ const count = Math.min(Math.max(0, data.totalCount - baseOffset), limit ?? Infinity);
7517
+ const totalPages = Math.ceil(count / perPage);
7518
+ const lastPage = Math.max(1, totalPages);
7519
+ if (currentPage > lastPage) {
7520
+ currentPage = lastPage;
7521
+ data = await fetchPage();
7522
+ }
7523
+ if (data)
7524
+ dataMap.set(req.id, {
7525
+ ...data,
7526
+ pages: data.pages.slice(0, Math.min(perPage, Math.max(0, count - (currentPage - 1) * perPage))),
7527
+ pagination: {
7528
+ currentPage,
7529
+ perPage,
7530
+ totalPages,
7531
+ offset: baseOffset + (currentPage - 1) * perPage,
7532
+ limit,
7533
+ urlPath,
7534
+ parameter
7535
+ }
7536
+ });
7453
7537
  }
7454
7538
  }
7455
7539
  return dataMap;
7456
7540
  }
7541
+ function positiveInteger(value, fallback) {
7542
+ return value !== undefined && Number.isSafeInteger(value) && value > 0 ? value : fallback;
7543
+ }
7457
7544
  async function buildListUsersDataMap(dataProvider, requirements) {
7458
7545
  const dataMap = new Map;
7459
7546
  for (const req of requirements) {
@@ -14326,7 +14413,7 @@ async function resolveModulesWithAsyncParse(ast, dataProvider, options) {
14326
14413
  registry.register(ast);
14327
14414
  const parse2 = async (source) => registry.register(await options.parse(source));
14328
14415
  const [listPagesData, listUsersData, tagCloudData] = await Promise.all([
14329
- buildListPagesDataMap(dataProvider, options.requirements.listPages ?? [], options.urlPath),
14416
+ buildListPagesDataMap(dataProvider, options.requirements.listPages ?? [], options.urlPath, options.paginationState),
14330
14417
  buildListUsersDataMap(dataProvider, options.requirements.listUsers ?? []),
14331
14418
  buildTagCloudDataMap(dataProvider, options.requirements.tagCloud ?? [])
14332
14419
  ]);
@@ -14414,9 +14501,9 @@ async function resolveListPagesAsync(module, data, template, parse2) {
14414
14501
  continue;
14415
14502
  const variableContext = {
14416
14503
  page,
14417
- index: i + 1,
14504
+ index: (data.pagination?.offset ?? 0) + i + 1,
14418
14505
  total: data.totalCount,
14419
- limit: module.limit,
14506
+ limit: data.pagination ? data.pagination.limit : module.limit,
14420
14507
  site: data.site
14421
14508
  };
14422
14509
  const parsed = await parse2(template(variableContext));
@@ -14436,6 +14523,7 @@ async function resolveListPagesAsync(module, data, template, parse2) {
14436
14523
  if (module["append-line"] && !module.separate) {
14437
14524
  result.push(...(await parse2(module["append-line"])).elements);
14438
14525
  }
14526
+ result.push(...createListPagesPager(data));
14439
14527
  return module.wrapper ? [
14440
14528
  {
14441
14529
  element: "container",
@@ -14536,6 +14624,9 @@ async function processWikitext(source, options) {
14536
14624
  const dataProvider = createModuleDataProvider(options, callbackContext);
14537
14625
  const parseFragment = parseSource2;
14538
14626
  let ast = initial.ast;
14627
+ const paginationState = { nextUnprefixed: 1 };
14628
+ const requestedPath = options.page.urlPath;
14629
+ const urlPath = !requestedPath || /^\/(?:[?#]|$)/.test(requestedPath) ? `/${options.page.fullName}${requestedPath?.slice(1) ?? ""}` : requestedPath;
14539
14630
  for (let pass = 0;pass < DEFAULT_MODULE_MAX_PASSES; pass++) {
14540
14631
  const extraction = extractDataRequirements(ast);
14541
14632
  if (pass > 0 && !hasResolvableRequirements(extraction.requirements, options.dataProvider)) {
@@ -14546,7 +14637,8 @@ async function processWikitext(source, options) {
14546
14637
  compiledListPagesTemplates: extraction.compiledListPagesTemplates,
14547
14638
  compiledListUsersTemplates: extraction.compiledListUsersTemplates,
14548
14639
  requirements: extraction.requirements,
14549
- urlPath: options.page.urlPath,
14640
+ urlPath,
14641
+ paginationState,
14550
14642
  pageTags: options.page.tags
14551
14643
  });
14552
14644
  ast = resolved.ast;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wdprlib/parser",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "Parser for Wikidot markup",
5
5
  "keywords": [
6
6
  "ast",
@@ -44,6 +44,6 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@braintree/sanitize-url": "^7.1.1",
47
- "@wdprlib/ast": "4.1.0"
47
+ "@wdprlib/ast": "4.2.0"
48
48
  }
49
49
  }
@@ -5,7 +5,7 @@ export const buildInfo: Readonly<{
5
5
  sha: string | null;
6
6
  dirty: boolean | null;
7
7
  }> = Object.freeze({
8
- version: "5.2.0",
9
- sha: "cc290b0d882de3ed9ea33ec060ddfc8a7b9da59a",
8
+ version: "5.3.0",
9
+ sha: "b71b84c705e05717f5ea12fd89684a91b669dbff",
10
10
  dirty: false,
11
11
  });
@@ -17,9 +17,9 @@ export function renderListPagesItems(
17
17
 
18
18
  const ctx: VariableContext = {
19
19
  page,
20
- index: i + 1,
20
+ index: (data.pagination?.offset ?? 0) + i + 1,
21
21
  total: data.totalCount,
22
- limit: module.limit,
22
+ limit: data.pagination ? data.pagination.limit : module.limit,
23
23
  site: data.site,
24
24
  };
25
25
 
@@ -0,0 +1,43 @@
1
+ import type { Element } from "@wdprlib/ast";
2
+ import type { ListPagesExternalData } from "../types";
3
+
4
+ export function createListPagesPager(data: ListPagesExternalData): Element[] {
5
+ const pagination = data.pagination;
6
+ if (!pagination || pagination.totalPages <= 1) return [];
7
+ const { currentPage, totalPages, urlPath, parameter } = pagination;
8
+ const pages = new Set([1, totalPages]);
9
+ for (
10
+ let page = Math.max(1, currentPage - 2);
11
+ page <= Math.min(totalPages, currentPage + 2);
12
+ page++
13
+ ) {
14
+ pages.add(page);
15
+ }
16
+ return [
17
+ {
18
+ element: "pager",
19
+ data: {
20
+ currentPage,
21
+ totalPages,
22
+ pages: [...pages]
23
+ .sort((a, b) => a - b)
24
+ .map((page) => ({
25
+ page,
26
+ href: pageUrl(urlPath, parameter, page),
27
+ })),
28
+ },
29
+ },
30
+ ];
31
+ }
32
+
33
+ function pageUrl(urlPath: string, parameter: string, page: number): string {
34
+ const suffixStart = urlPath.search(/[?#]/);
35
+ const path = suffixStart === -1 ? urlPath : urlPath.slice(0, suffixStart);
36
+ const suffix = suffixStart === -1 ? "" : urlPath.slice(suffixStart);
37
+ const parts = path.split("/");
38
+ const result = parts.slice(0, 2);
39
+ for (let index = 2; index < parts.length; index += 2) {
40
+ if (parts[index] !== parameter) result.push(...parts.slice(index, index + 2));
41
+ }
42
+ return `${result.join("/").replace(/\/$/, "")}/${parameter}/${page}${suffix}`;
43
+ }
@@ -1,13 +1,16 @@
1
1
  import type { Element } from "@wdprlib/ast";
2
2
  import { getModuleParseAst, type ParseFunction } from "../../types";
3
3
  import type { ListPagesModuleData } from "../resolve";
4
+ import type { ListPagesExternalData } from "../types";
5
+ import { createListPagesPager } from "./pager";
4
6
 
5
7
  export function wrapListPagesResult(
6
8
  module: ListPagesModuleData,
7
9
  items: Element[],
8
10
  parse: ParseFunction,
11
+ data?: ListPagesExternalData,
9
12
  ): Element[] {
10
- if (items.length === 0) {
13
+ if (items.length === 0 && (!data || data.pages.length === 0)) {
11
14
  return [];
12
15
  }
13
16
 
@@ -25,6 +28,8 @@ export function wrapListPagesResult(
25
28
  result.push(...appendAst.elements);
26
29
  }
27
30
 
31
+ if (data) result.push(...createListPagesPager(data));
32
+
28
33
  if (module.wrapper) {
29
34
  return [
30
35
  {
@@ -56,5 +56,5 @@ export function resolveListPages(
56
56
  parse: ParseFunction,
57
57
  ): Element[] {
58
58
  const items = renderListPagesItems(module, data, compiledTemplate, parse);
59
- return wrapListPagesResult(module, items, parse);
59
+ return wrapListPagesResult(module, items, parse, data);
60
60
  }
@@ -94,6 +94,19 @@ export interface SiteContext {
94
94
  */
95
95
  export interface ListPagesExternalData {
96
96
  pages: PageData[];
97
+ /** Matching items before offset and limit; use the same visibility filters as pages. */
97
98
  totalCount: number;
98
99
  site: SiteContext;
100
+ /** Filled by module resolution when a page URL is available. */
101
+ pagination?: {
102
+ currentPage: number;
103
+ perPage: number;
104
+ totalPages: number;
105
+ /** Actual fetch offset, including the module's starting offset. */
106
+ offset: number;
107
+ /** Total item limit after URL resolution, not the fetch batch size. */
108
+ limit?: number;
109
+ urlPath: string;
110
+ parameter: string;
111
+ };
99
112
  }
@@ -2,12 +2,13 @@ import { URL_RESOLVABLE_FIELDS } from "./fields";
2
2
 
3
3
  /**
4
4
  * Parse URL path parameters like /offset/1/page2_limit/1.
5
- * Returns a map of parameter name -> value.
5
+ * Returns a map of parameter name -> value. Set hasPageName for a full page path;
6
+ * otherwise the legacy parameter-only-path heuristic is used.
6
7
  */
7
- export function parseUrlParams(url: string): Map<string, string> {
8
+ export function parseUrlParams(url: string, hasPageName = false): Map<string, string> {
8
9
  const params = new Map<string, string>();
9
- const parts = url.split("/").filter(Boolean);
10
- const pairStart = isUrlParameter(parts[0]) ? 0 : 1;
10
+ const parts = url.split(/[?#]/, 1)[0]!.split("/").filter(Boolean);
11
+ const pairStart = !hasPageName && isUrlParameter(parts[0]) ? 0 : 1;
11
12
 
12
13
  // Skip the page name (first part), parse key/value pairs.
13
14
  for (let i = pairStart; i < parts.length - 1; i += 2) {
@@ -4,25 +4,97 @@ import type { ListUsersDataRequirement, ListUsersExternalData } from "../listuse
4
4
  import type { TagCloudDataRequirement, TagCloudExternalData } from "../tagcloud/types";
5
5
  import { parseUrlParams, resolveAndNormalizeQuery } from "../listpages/url-resolver";
6
6
 
7
+ export interface ListPagesPaginationState {
8
+ nextUnprefixed: number;
9
+ }
10
+
7
11
  export async function buildListPagesDataMap(
8
12
  dataProvider: DataProvider,
9
13
  requirements: ListPagesDataRequirement[],
10
14
  urlPath: string | undefined,
15
+ paginationState: ListPagesPaginationState = { nextUnprefixed: 1 },
11
16
  ): Promise<Map<number, ListPagesExternalData>> {
12
17
  const dataMap = new Map<number, ListPagesExternalData>();
13
- const urlParams = parseUrlParams(urlPath ?? "");
18
+ const urlParams = parseUrlParams(urlPath ?? "", true);
14
19
 
15
20
  for (const req of requirements) {
16
21
  const normalizedQuery = resolveAndNormalizeQuery(req, urlParams);
17
- const data = await dataProvider.fetchListPages?.(normalizedQuery, req);
22
+ if (!urlPath || !urlPath.startsWith("/") || urlPath.startsWith("//")) {
23
+ const data = await dataProvider.fetchListPages?.(normalizedQuery, req);
24
+ if (data) dataMap.set(req.id, data);
25
+ continue;
26
+ }
27
+ const ordinal = req.urlAttrPrefix ? 0 : paginationState.nextUnprefixed++;
28
+ const parameter = req.urlAttrPrefix
29
+ ? `${encodeURIComponent(req.urlAttrPrefix)}_p`
30
+ : ordinal === 1
31
+ ? "p"
32
+ : `p${ordinal}`;
33
+ const perPage = Math.min(250, positiveInteger(normalizedQuery.perPage, 20));
34
+ const baseOffset = positiveInteger(normalizedQuery.offset, 0);
35
+ const limit =
36
+ normalizedQuery.limit !== undefined && normalizedQuery.limit >= 0
37
+ ? Math.floor(normalizedQuery.limit)
38
+ : undefined;
39
+ const rawPage = urlParams.get(parameter) ?? "1";
40
+ let currentPage = /^\d+$/.test(rawPage) ? positiveInteger(Number(rawPage), 1) : 1;
41
+ currentPage = Math.min(
42
+ currentPage,
43
+ Math.floor((Number.MAX_SAFE_INTEGER - baseOffset) / perPage) + 1,
44
+ );
45
+ if (limit !== undefined)
46
+ currentPage = Math.min(currentPage, Math.max(1, Math.ceil(limit / perPage)));
47
+ const fetchPage = () => {
48
+ const relativeOffset = (currentPage - 1) * perPage;
49
+ return dataProvider.fetchListPages?.(
50
+ {
51
+ ...normalizedQuery,
52
+ offset: baseOffset + relativeOffset,
53
+ limit: Math.min(
54
+ perPage,
55
+ limit === undefined ? perPage : Math.max(0, limit - relativeOffset),
56
+ ),
57
+ perPage,
58
+ },
59
+ req,
60
+ );
61
+ };
62
+ let data = await fetchPage();
18
63
  if (data) {
19
- dataMap.set(req.id, data);
64
+ const count = Math.min(Math.max(0, data.totalCount - baseOffset), limit ?? Infinity);
65
+ const totalPages = Math.ceil(count / perPage);
66
+ const lastPage = Math.max(1, totalPages);
67
+ if (currentPage > lastPage) {
68
+ currentPage = lastPage;
69
+ data = await fetchPage();
70
+ }
71
+ if (data)
72
+ dataMap.set(req.id, {
73
+ ...data,
74
+ pages: data.pages.slice(
75
+ 0,
76
+ Math.min(perPage, Math.max(0, count - (currentPage - 1) * perPage)),
77
+ ),
78
+ pagination: {
79
+ currentPage,
80
+ perPage,
81
+ totalPages,
82
+ offset: baseOffset + (currentPage - 1) * perPage,
83
+ limit,
84
+ urlPath,
85
+ parameter,
86
+ },
87
+ });
20
88
  }
21
89
  }
22
90
 
23
91
  return dataMap;
24
92
  }
25
93
 
94
+ function positiveInteger(value: number | undefined, fallback: number): number {
95
+ return value !== undefined && Number.isSafeInteger(value) && value > 0 ? value : fallback;
96
+ }
97
+
26
98
  export async function buildListUsersDataMap(
27
99
  dataProvider: DataProvider,
28
100
  requirements: ListUsersDataRequirement[],
@@ -26,6 +26,8 @@ import {
26
26
  import { buildListPagesDataMap, buildListUsersDataMap, buildTagCloudDataMap } from "./data-maps";
27
27
  import { ModuleDocumentRegistry } from "./document";
28
28
  import { collectStyles, mergeCollectedStyles } from "./styles";
29
+ import { createListPagesPager } from "../listpages/resolution/pager";
30
+ import type { ListPagesPaginationState } from "./data-maps";
29
31
 
30
32
  export type AsyncModuleParseFunction = (source: string) => Promise<ModuleParseResult>;
31
33
 
@@ -40,6 +42,7 @@ export interface ResolveModulesWithAsyncParseOptions {
40
42
  };
41
43
  urlPath?: string;
42
44
  pageTags: string[];
45
+ paginationState?: ListPagesPaginationState;
43
46
  }
44
47
 
45
48
  export interface AsyncModuleResolutionResult {
@@ -73,7 +76,12 @@ export async function resolveModulesWithAsyncParse(
73
76
  registry.register(await options.parse(source));
74
77
 
75
78
  const [listPagesData, listUsersData, tagCloudData] = await Promise.all([
76
- buildListPagesDataMap(dataProvider, options.requirements.listPages ?? [], options.urlPath),
79
+ buildListPagesDataMap(
80
+ dataProvider,
81
+ options.requirements.listPages ?? [],
82
+ options.urlPath,
83
+ options.paginationState,
84
+ ),
77
85
  buildListUsersDataMap(dataProvider, options.requirements.listUsers ?? []),
78
86
  buildTagCloudDataMap(dataProvider, options.requirements.tagCloud ?? []),
79
87
  ]);
@@ -175,9 +183,9 @@ async function resolveListPagesAsync(
175
183
  if (!page) continue;
176
184
  const variableContext: VariableContext = {
177
185
  page,
178
- index: i + 1,
186
+ index: (data.pagination?.offset ?? 0) + i + 1,
179
187
  total: data.totalCount,
180
- limit: module.limit,
188
+ limit: data.pagination ? data.pagination.limit : module.limit,
181
189
  site: data.site,
182
190
  };
183
191
  const parsed = await parse(template(variableContext));
@@ -197,6 +205,7 @@ async function resolveListPagesAsync(
197
205
  if (module["append-line"] && !module.separate) {
198
206
  result.push(...(await parse(module["append-line"])).elements);
199
207
  }
208
+ result.push(...createListPagesPager(data));
200
209
 
201
210
  return module.wrapper
202
211
  ? [
@@ -82,6 +82,12 @@ export async function processWikitext<TPage extends WikitextPageContext>(
82
82
  const dataProvider = createModuleDataProvider(options, callbackContext);
83
83
  const parseFragment = parseSource;
84
84
  let ast = initial.ast;
85
+ const paginationState = { nextUnprefixed: 1 };
86
+ const requestedPath = options.page.urlPath;
87
+ const urlPath =
88
+ !requestedPath || /^\/(?:[?#]|$)/.test(requestedPath)
89
+ ? `/${options.page.fullName}${requestedPath?.slice(1) ?? ""}`
90
+ : requestedPath;
85
91
 
86
92
  for (let pass = 0; pass < DEFAULT_MODULE_MAX_PASSES; pass++) {
87
93
  const extraction = extractDataRequirements(ast);
@@ -94,7 +100,8 @@ export async function processWikitext<TPage extends WikitextPageContext>(
94
100
  compiledListPagesTemplates: extraction.compiledListPagesTemplates,
95
101
  compiledListUsersTemplates: extraction.compiledListUsersTemplates,
96
102
  requirements: extraction.requirements,
97
- urlPath: options.page.urlPath,
103
+ urlPath,
104
+ paginationState,
98
105
  pageTags: options.page.tags,
99
106
  });
100
107
  ast = resolved.ast;