@caelo-cms/shared 0.11.3 → 0.11.4

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.
Files changed (68) hide show
  1. package/dist/ai-tools.d.ts +8 -0
  2. package/dist/ai-tools.d.ts.map +1 -1
  3. package/dist/build-page.d.ts +16 -0
  4. package/dist/build-page.d.ts.map +1 -1
  5. package/dist/content.d.ts +24 -0
  6. package/dist/content.d.ts.map +1 -1
  7. package/dist/content.js +40 -0
  8. package/dist/content.js.map +1 -1
  9. package/dist/lists/index.d.ts +2 -0
  10. package/dist/lists/index.d.ts.map +1 -1
  11. package/dist/lists/index.js +2 -0
  12. package/dist/lists/index.js.map +1 -1
  13. package/dist/lists/list-type.d.ts +4 -1
  14. package/dist/lists/list-type.d.ts.map +1 -1
  15. package/dist/lists/list-type.js +2 -0
  16. package/dist/lists/list-type.js.map +1 -1
  17. package/dist/lists/paginated-render.d.ts +38 -0
  18. package/dist/lists/paginated-render.d.ts.map +1 -0
  19. package/dist/lists/paginated-render.js +45 -0
  20. package/dist/lists/paginated-render.js.map +1 -0
  21. package/dist/lists/pagination.d.ts +95 -0
  22. package/dist/lists/pagination.d.ts.map +1 -0
  23. package/dist/lists/pagination.js +136 -0
  24. package/dist/lists/pagination.js.map +1 -0
  25. package/dist/lists/resolve.d.ts +65 -0
  26. package/dist/lists/resolve.d.ts.map +1 -1
  27. package/dist/lists/resolve.js +166 -20
  28. package/dist/lists/resolve.js.map +1 -1
  29. package/dist/live-events.d.ts.map +1 -1
  30. package/dist/live-events.js +12 -1
  31. package/dist/live-events.js.map +1 -1
  32. package/dist/markup-structure.d.ts +25 -0
  33. package/dist/markup-structure.d.ts.map +1 -1
  34. package/dist/markup-structure.js +88 -1
  35. package/dist/markup-structure.js.map +1 -1
  36. package/dist/media.d.ts +13 -0
  37. package/dist/media.d.ts.map +1 -1
  38. package/dist/media.js +40 -0
  39. package/dist/media.js.map +1 -1
  40. package/dist/nested-module-render.d.ts +13 -0
  41. package/dist/nested-module-render.d.ts.map +1 -1
  42. package/dist/nested-module-render.js +22 -1
  43. package/dist/nested-module-render.js.map +1 -1
  44. package/dist/template-engine.d.ts +4 -1
  45. package/dist/template-engine.d.ts.map +1 -1
  46. package/dist/template-engine.js +6 -4
  47. package/dist/template-engine.js.map +1 -1
  48. package/dist/template-sections.d.ts.map +1 -1
  49. package/dist/template-sections.js +1 -0
  50. package/dist/template-sections.js.map +1 -1
  51. package/dist/version.d.ts +2 -2
  52. package/dist/version.js +1 -1
  53. package/package.json +1 -1
  54. package/src/content.ts +43 -0
  55. package/src/lists/index.ts +2 -0
  56. package/src/lists/list-type.ts +2 -0
  57. package/src/lists/paginated-render.ts +77 -0
  58. package/src/lists/pagination.test.ts +422 -0
  59. package/src/lists/pagination.ts +160 -0
  60. package/src/lists/resolve.ts +254 -22
  61. package/src/live-events.ts +12 -1
  62. package/src/markup-structure.test.ts +27 -0
  63. package/src/markup-structure.ts +96 -1
  64. package/src/media.ts +43 -0
  65. package/src/nested-module-render.ts +25 -1
  66. package/src/template-engine.ts +9 -4
  67. package/src/template-sections.ts +1 -0
  68. package/src/version.ts +1 -1
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Paginated page lists (#1257, `docs/lists.md` §6 "Pagination"). An
3
+ * overview page (a guides hub, a category landing page) lists every page
4
+ * a query matches: its paginated `list` field shows `per_page` entries,
5
+ * page 1 at the page's own URL and every further page at
6
+ * `<page path>/page/<n>/`. The static generator and the editor preview
7
+ * emit those follow-up pages; the module draws its own pager from the
8
+ * `pagination` system source.
9
+ *
10
+ * Decisions (maintainer, #1257):
11
+ *
12
+ * - Follow-up URLs are path segments, not `?page=n`: a static host serves
13
+ * files, and a query string would serve page 1 for every page.
14
+ * `/page/1/` is never emitted — page 1 has exactly one URL.
15
+ * - Follow-up pages are indexable and self-canonical by default (Google:
16
+ * do not canonicalise page 2… to page 1); `follow_ups: "noindex"` keeps
17
+ * them out of search (robots noindex, no sitemap entry) per overview.
18
+ * - Page size defaults to the maximum ({@link LIST_QUERY_MAX_LIMIT}):
19
+ * static HTML costs nothing per entry, and fewer follow-up pages mean
20
+ * fewer thin pages.
21
+ * - One paginated list per page: two would need two independent page
22
+ * numbers in one URL.
23
+ */
24
+ import { z } from "zod";
25
+ import type { PageUrlStyle } from "../seo.js";
26
+ /** The URL segment in front of the page number: `/guides/page/2/`. */
27
+ export declare const PAGINATION_SEGMENT = "page";
28
+ /** Who may see follow-up pages in search. */
29
+ export declare const PAGINATION_FOLLOW_UPS: readonly ["index", "noindex"];
30
+ export type PaginationFollowUps = (typeof PAGINATION_FOLLOW_UPS)[number];
31
+ /**
32
+ * Pagination settings — on a module `list` field (`paginate`, opts the
33
+ * field in and sets the defaults) and in a placement's value
34
+ * (`paginate`, overrides them for that overview).
35
+ */
36
+ export declare const listPaginationSchema: z.ZodObject<{
37
+ per_page: z.ZodOptional<z.ZodNumber>;
38
+ follow_ups: z.ZodOptional<z.ZodEnum<{
39
+ index: "index";
40
+ noindex: "noindex";
41
+ }>>;
42
+ }, z.core.$strict>;
43
+ export type ListPaginationSettings = z.infer<typeof listPaginationSchema>;
44
+ /** The settings an overview renders with. */
45
+ export interface EffectivePagination {
46
+ readonly perPage: number;
47
+ readonly followUps: PaginationFollowUps;
48
+ }
49
+ /**
50
+ * The placement value of a paginated field split into its pagination
51
+ * overrides and the rest (query knobs / `terms`). The rest is what the
52
+ * source reads; `paginate` never reaches the query parser.
53
+ */
54
+ export declare function splitPaginationValue(value: unknown): {
55
+ readonly paginate: unknown;
56
+ readonly rest: unknown;
57
+ };
58
+ /**
59
+ * Field settings with the placement's overrides applied; null when the
60
+ * overrides are malformed (the render reports `list-malformed`).
61
+ */
62
+ export declare function effectivePagination(field: ListPaginationSettings, overrides: unknown): EffectivePagination | null;
63
+ /** How many pages `total` entries need at `perPage` (at least one). */
64
+ export declare function pageCountOf(total: number, perPage: number): number;
65
+ /**
66
+ * The public path of page `n` (≥ 2) of the overview at `path`:
67
+ * `/guides` → `/guides/page/2`, `/` → `/page/2`. No trailing slash: the
68
+ * target's URL style adds it (`pageHrefForStyle`, `pageOutputPath`).
69
+ */
70
+ export declare function pagedPath(path: string, n: number): string;
71
+ /**
72
+ * The URL of page `n` (≥ 2) from the overview's own URL (absolute or a
73
+ * path, as the link resolver or the public URL builder made it) in the
74
+ * target's URL style. Query and fragment are dropped: they belong to
75
+ * page 1's link, not to the page sequence.
76
+ */
77
+ export declare function pagedUrl(url: string, n: number, style: PageUrlStyle): string;
78
+ /**
79
+ * Split a requested path into the overview's path and a follow-up page
80
+ * number: `/guides/page/3/` → `{ basePath: "/guides", page: 3 }`. Null
81
+ * when the path names no follow-up page (`/page/1` included — page 1 is
82
+ * only served at the overview's own path).
83
+ */
84
+ export declare function parsePagedPath(path: string): {
85
+ basePath: string;
86
+ page: number;
87
+ } | null;
88
+ /**
89
+ * The page path a follow-up page of `overviewPath` would collide with:
90
+ * `pagePath` is `<overview>/page` or below it. A page there would either
91
+ * shadow a follow-up page or, in the no-extension URL style, need a file
92
+ * and a directory of the same name.
93
+ */
94
+ export declare function collidesWithFollowUps(overviewPath: string, pagePath: string): boolean;
95
+ //# sourceMappingURL=pagination.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../../src/lists/pagination.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAI9C,sEAAsE;AACtE,eAAO,MAAM,kBAAkB,SAAS,CAAC;AAEzC,6CAA6C;AAC7C,eAAO,MAAM,qBAAqB,+BAAgC,CAAC;AACnE,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzE;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;kBAOtB,CAAC;AACZ,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAE1E,6CAA6C;AAC7C,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,mBAAmB,CAAC;CACzC;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG;IACpD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB,CAMA;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,sBAAsB,EAC7B,SAAS,EAAE,OAAO,GACjB,mBAAmB,GAAG,IAAI,CAO5B;AAED,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAIzD;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,MAAM,CAQ5E;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAOtF;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAIrF"}
@@ -0,0 +1,136 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+ /**
3
+ * Paginated page lists (#1257, `docs/lists.md` §6 "Pagination"). An
4
+ * overview page (a guides hub, a category landing page) lists every page
5
+ * a query matches: its paginated `list` field shows `per_page` entries,
6
+ * page 1 at the page's own URL and every further page at
7
+ * `<page path>/page/<n>/`. The static generator and the editor preview
8
+ * emit those follow-up pages; the module draws its own pager from the
9
+ * `pagination` system source.
10
+ *
11
+ * Decisions (maintainer, #1257):
12
+ *
13
+ * - Follow-up URLs are path segments, not `?page=n`: a static host serves
14
+ * files, and a query string would serve page 1 for every page.
15
+ * `/page/1/` is never emitted — page 1 has exactly one URL.
16
+ * - Follow-up pages are indexable and self-canonical by default (Google:
17
+ * do not canonicalise page 2… to page 1); `follow_ups: "noindex"` keeps
18
+ * them out of search (robots noindex, no sitemap entry) per overview.
19
+ * - Page size defaults to the maximum ({@link LIST_QUERY_MAX_LIMIT}):
20
+ * static HTML costs nothing per entry, and fewer follow-up pages mean
21
+ * fewer thin pages.
22
+ * - One paginated list per page: two would need two independent page
23
+ * numbers in one URL.
24
+ */
25
+ import { z } from "zod";
26
+ import { trimSlashes } from "../url.js";
27
+ import { LIST_QUERY_MAX_LIMIT } from "./list-type.js";
28
+ /** The URL segment in front of the page number: `/guides/page/2/`. */
29
+ export const PAGINATION_SEGMENT = "page";
30
+ /** Who may see follow-up pages in search. */
31
+ export const PAGINATION_FOLLOW_UPS = ["index", "noindex"];
32
+ /**
33
+ * Pagination settings — on a module `list` field (`paginate`, opts the
34
+ * field in and sets the defaults) and in a placement's value
35
+ * (`paginate`, overrides them for that overview).
36
+ */
37
+ export const listPaginationSchema = z
38
+ .object({
39
+ /** Entries per page, 1..50; default 50. */
40
+ per_page: z.number().int().min(1).max(LIST_QUERY_MAX_LIMIT).optional(),
41
+ /** Follow-up pages indexable (default) or `noindex` and out of the sitemap. */
42
+ follow_ups: z.enum(PAGINATION_FOLLOW_UPS).optional(),
43
+ })
44
+ .strict();
45
+ /**
46
+ * The placement value of a paginated field split into its pagination
47
+ * overrides and the rest (query knobs / `terms`). The rest is what the
48
+ * source reads; `paginate` never reaches the query parser.
49
+ */
50
+ export function splitPaginationValue(value) {
51
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
52
+ return { paginate: undefined, rest: value };
53
+ }
54
+ const { paginate, ...rest } = value;
55
+ return { paginate, rest: Object.keys(rest).length > 0 ? rest : undefined };
56
+ }
57
+ /**
58
+ * Field settings with the placement's overrides applied; null when the
59
+ * overrides are malformed (the render reports `list-malformed`).
60
+ */
61
+ export function effectivePagination(field, overrides) {
62
+ const parsed = listPaginationSchema.safeParse(overrides ?? {});
63
+ if (!parsed.success)
64
+ return null;
65
+ return {
66
+ perPage: parsed.data.per_page ?? field.per_page ?? LIST_QUERY_MAX_LIMIT,
67
+ followUps: parsed.data.follow_ups ?? field.follow_ups ?? "index",
68
+ };
69
+ }
70
+ /** How many pages `total` entries need at `perPage` (at least one). */
71
+ export function pageCountOf(total, perPage) {
72
+ return Math.max(1, Math.ceil(total / perPage));
73
+ }
74
+ /**
75
+ * The public path of page `n` (≥ 2) of the overview at `path`:
76
+ * `/guides` → `/guides/page/2`, `/` → `/page/2`. No trailing slash: the
77
+ * target's URL style adds it (`pageHrefForStyle`, `pageOutputPath`).
78
+ */
79
+ export function pagedPath(path, n) {
80
+ assertFollowUp(n);
81
+ const trimmed = trimSlashes(path);
82
+ return `${trimmed.length > 0 ? `/${trimmed}` : ""}/${PAGINATION_SEGMENT}/${n}`;
83
+ }
84
+ /**
85
+ * The URL of page `n` (≥ 2) from the overview's own URL (absolute or a
86
+ * path, as the link resolver or the public URL builder made it) in the
87
+ * target's URL style. Query and fragment are dropped: they belong to
88
+ * page 1's link, not to the page sequence.
89
+ */
90
+ export function pagedUrl(url, n, style) {
91
+ assertFollowUp(n);
92
+ // Trailing slashes cut by index, not a regex: no backtracking on input.
93
+ let base = url.split(/[?#]/, 1)[0] ?? url;
94
+ let end = base.length;
95
+ while (end > 0 && base.charCodeAt(end - 1) === 47)
96
+ end -= 1;
97
+ base = base.slice(0, end);
98
+ return `${base}/${PAGINATION_SEGMENT}/${n}${style === "directory" ? "/" : ""}`;
99
+ }
100
+ /**
101
+ * Split a requested path into the overview's path and a follow-up page
102
+ * number: `/guides/page/3/` → `{ basePath: "/guides", page: 3 }`. Null
103
+ * when the path names no follow-up page (`/page/1` included — page 1 is
104
+ * only served at the overview's own path).
105
+ */
106
+ export function parsePagedPath(path) {
107
+ const m = /^(.*?)\/page\/([1-9][0-9]{0,5})\/?$/.exec(path);
108
+ if (!m)
109
+ return null;
110
+ const page = Number(m[2]);
111
+ if (page < 2)
112
+ return null;
113
+ const base = trimSlashes(m[1] ?? "");
114
+ return { basePath: base.length === 0 ? "/" : `/${base}`, page };
115
+ }
116
+ /**
117
+ * The page path a follow-up page of `overviewPath` would collide with:
118
+ * `pagePath` is `<overview>/page` or below it. A page there would either
119
+ * shadow a follow-up page or, in the no-extension URL style, need a file
120
+ * and a directory of the same name.
121
+ */
122
+ export function collidesWithFollowUps(overviewPath, pagePath) {
123
+ const prefix = pagedPrefix(overviewPath);
124
+ const p = `/${trimSlashes(pagePath)}`;
125
+ return p === prefix || p.startsWith(`${prefix}/`);
126
+ }
127
+ function pagedPrefix(path) {
128
+ const trimmed = trimSlashes(path);
129
+ return `${trimmed.length > 0 ? `/${trimmed}` : ""}/${PAGINATION_SEGMENT}`;
130
+ }
131
+ function assertFollowUp(n) {
132
+ if (!Number.isInteger(n) || n < 2) {
133
+ throw new Error(`pagination: follow-up page numbers start at 2 (got ${n}).`);
134
+ }
135
+ }
136
+ //# sourceMappingURL=pagination.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.js","sourceRoot":"","sources":["../../src/lists/pagination.ts"],"names":[],"mappings":"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"}
@@ -20,7 +20,9 @@
20
20
  * raw.
21
21
  */
22
22
  import type { LinkResolver } from "../link-refs.js";
23
+ import type { PageUrlStyle } from "../seo.js";
23
24
  import { type SourceDef } from "./list-type.js";
25
+ import { type ListPaginationSettings, type PaginationFollowUps } from "./pagination.js";
24
26
  import type { ListRenderData, ListRenderDeps, PluginSourceEntry } from "./render-data.js";
25
27
  import type { ScopeEntry, ScopeValue } from "./scope.js";
26
28
  /** True for a list failure marker — the editor's missing-content surface shows these. */
@@ -33,6 +35,8 @@ export interface ListFieldSpec {
33
35
  readonly list?: string;
34
36
  readonly source?: SourceDef;
35
37
  readonly default?: unknown;
38
+ /** #1257 — the field lists every match across follow-up pages. */
39
+ readonly paginate?: ListPaginationSettings;
36
40
  }
37
41
  /** Result of resolving one list field for one page. */
38
42
  export type ListFieldResolution = {
@@ -63,7 +67,64 @@ export interface ListPageOptions {
63
67
  readonly linkResolver?: LinkResolver;
64
68
  /** Entries of every ACTIVE plugin list source for this page, keyed by source name. */
65
69
  readonly pluginSources?: Readonly<Record<string, readonly PluginSourceEntry[]>>;
70
+ /**
71
+ * #1257 — the target's URL style, for the pager's links to follow-up
72
+ * pages. Without it a rendered `pagination` source is
73
+ * `list-context-unavailable:pagination`.
74
+ */
75
+ readonly pageUrlStyle?: PageUrlStyle;
76
+ /**
77
+ * #1257 — which page of the paginated list this render is (1 = the
78
+ * page's own URL; default 1), and — once a first render found it —
79
+ * how many pages there are, so a pager rendered BEFORE the list knows.
80
+ */
81
+ readonly pagination?: {
82
+ readonly page: number;
83
+ readonly pageCount?: number;
84
+ };
85
+ }
86
+ /**
87
+ * #1257 — the page's paginated list as one render saw it
88
+ * ({@link ListPageContext.pagination}).
89
+ */
90
+ export interface ListPaginationState {
91
+ /** The paginated module field. */
92
+ readonly field: string;
93
+ readonly perPage: number;
94
+ /** Every match of the query. */
95
+ readonly total: number;
96
+ readonly pageCount: number;
97
+ /** The page this render showed. */
98
+ readonly page: number;
99
+ readonly followUps: PaginationFollowUps;
100
+ /** A `pagination` source rendered on this page. */
101
+ readonly pagerShown: boolean;
102
+ /**
103
+ * The pager rendered before the list was resolved, so it did not know
104
+ * the page count yet: render the page again with
105
+ * `pagination: { page, pageCount }`.
106
+ */
107
+ readonly pagerEarly: boolean;
66
108
  }
109
+ /**
110
+ * #1257 — a list cut at its limit: more pages match than it shows. Never
111
+ * silent: the Stage result and `preview_list_source` report it.
112
+ */
113
+ export interface TruncatedList {
114
+ /** The module field (or source node's field) that rendered the list. */
115
+ readonly field: string;
116
+ readonly shown: number;
117
+ readonly total: number;
118
+ /** The limit that cut it (the query's `limit`; 50 for term_pages). */
119
+ readonly limit: number;
120
+ }
121
+ /**
122
+ * The markers a finished render's pagination asks for: a paginated page
123
+ * that shows no pager strands every entry after the first page
124
+ * (`list-pager-missing:<field>`). Exported so the preview and the
125
+ * generator apply one rule.
126
+ */
127
+ export declare function paginationMarkers(state: ListPaginationState | null): string[];
67
128
  /** The list view of ONE rendering page, handed to the template engine. */
68
129
  export interface ListPageContext {
69
130
  /** The rendering page, or null for a render outside any page. */
@@ -95,6 +156,10 @@ export interface ListPageContext {
95
156
  deps(): ListRenderDeps;
96
157
  /** Overlay values this page's render refused (also the render-wide ones). */
97
158
  overlayIssues(): readonly ListOverlayIssue[];
159
+ /** #1257 — the paginated list this render resolved, or null. */
160
+ pagination(): ListPaginationState | null;
161
+ /** #1257 — lists this render cut at their limit (not paginated). */
162
+ truncatedLists(): readonly TruncatedList[];
98
163
  }
99
164
  /** Render-wide options of {@link createListRenderContext}. */
100
165
  export interface ListRenderContextOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/lists/resolve.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/lists/resolve.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,7 @@ import { parseLinkRef } from "../link-refs.js";
3
3
  import { SITE_TEXTS_LIST_TYPE } from "./default-types.js";
4
4
  import { TERM_REF_RE, validateFieldValue } from "./fields.js";
5
5
  import { effectiveMaxDepth, LIST_QUERY_MAX_LIMIT, pageQuerySchema, } from "./list-type.js";
6
+ import { effectivePagination, listPaginationSchema, pageCountOf, pagedUrl, splitPaginationValue, } from "./pagination.js";
6
7
  import { parseListRef } from "./refs.js";
7
8
  import { bool, dateValue, EMPTY_LIST, EMPTY_TEXT, fieldValue, linkValue, listValue, mediaValue, positionValue, selectFlag, text, untypedValue, } from "./resolve-values.js";
8
9
  /** Prefixes of the list failure markers (`docs/lists.md` §8). */
@@ -13,11 +14,25 @@ const LIST_MARKER_PREFIXES = [
13
14
  "list-source-unavailable:",
14
15
  "list-malformed:",
15
16
  "list-context-unavailable:",
17
+ // #1257 — a second paginated list on one page; a paginated page without a pager.
18
+ "list-pagination-conflict:",
19
+ "list-pager-missing:",
16
20
  ];
17
21
  /** True for a list failure marker — the editor's missing-content surface shows these. */
18
22
  export function isListFailureMarker(marker) {
19
23
  return LIST_MARKER_PREFIXES.some((p) => marker.startsWith(p));
20
24
  }
25
+ /**
26
+ * The markers a finished render's pagination asks for: a paginated page
27
+ * that shows no pager strands every entry after the first page
28
+ * (`list-pager-missing:<field>`). Exported so the preview and the
29
+ * generator apply one rule.
30
+ */
31
+ export function paginationMarkers(state) {
32
+ if (state === null || state.pageCount <= 1 || state.pagerShown)
33
+ return [];
34
+ return [`list-pager-missing:${state.field}`];
35
+ }
21
36
  /** Resolve one list field for a page (thin wrapper, `docs/lists.md` §10). */
22
37
  export function resolveListField(field, value, page, holder = { contentInstanceId: null }) {
23
38
  return page.resolveField(field, value, holder);
@@ -212,6 +227,12 @@ function createPageContext(s, pageId, opts, renderIssues) {
212
227
  let depTree = false;
213
228
  const depPages = new Set();
214
229
  let trail = null;
230
+ // #1257 — pagination and cut lists of this render.
231
+ const requestedPage = opts.pagination?.page ?? 1;
232
+ let paged = null;
233
+ let pagerShown = false;
234
+ let pagerEarly = false;
235
+ const truncated = new Map();
215
236
  /** Entries of a page-shaped system source: their pages become deps. */
216
237
  const readPages = (pages) => {
217
238
  for (const pg of pages)
@@ -559,7 +580,8 @@ function createPageContext(s, pageId, opts, renderIssues) {
559
580
  return null;
560
581
  };
561
582
  const sameVariant = (p) => variant === null || data.pageVariants?.get(p.id)?.variant === variant;
562
- const runQuery = (q) => {
583
+ /** Every page a query matches, in its order (deps recorded). */
584
+ const queryHits = (q) => {
563
585
  const parent = q.parent
564
586
  ? (parseLinkRef(q.parent)?.pageId ?? null)
565
587
  : null;
@@ -580,7 +602,96 @@ function createPageContext(s, pageId, opts, renderIssues) {
580
602
  ? terms.every((t) => p.terms.includes(t))
581
603
  : terms.some((t) => p.terms.includes(t)))));
582
604
  hits.sort(pageOrder(q.order, s.collator));
583
- return listValue(pageEntries(hits.slice(0, q.limit)));
605
+ return hits;
606
+ };
607
+ /** The first `limit` hits; a cut is recorded, never silent (#1257). */
608
+ const cutAt = (hits, limit, field) => {
609
+ if (hits.length > limit) {
610
+ const key = `${field}:${limit}:${hits.length}`;
611
+ if (!truncated.has(key))
612
+ truncated.set(key, { field, shown: limit, total: hits.length, limit });
613
+ }
614
+ return listValue(pageEntries(hits.slice(0, limit)));
615
+ };
616
+ const runQuery = (q, field) => cutAt(queryHits(q), q.limit, field);
617
+ /**
618
+ * The slice of a paginated list this render shows. One paginated list
619
+ * per page: a second one (another query or page size) is refused.
620
+ */
621
+ const paginate = (field, key, hits, eff) => {
622
+ const fullKey = JSON.stringify([key, eff.perPage, eff.followUps]);
623
+ if (paged !== null && paged.key !== fullKey) {
624
+ return { kind: "failed", marker: `list-pagination-conflict:${field}` };
625
+ }
626
+ paged = {
627
+ key: fullKey,
628
+ field,
629
+ perPage: eff.perPage,
630
+ total: hits.length,
631
+ pageCount: pageCountOf(hits.length, eff.perPage),
632
+ followUps: eff.followUps,
633
+ };
634
+ const from = (requestedPage - 1) * eff.perPage;
635
+ return listValue(pageEntries(hits.slice(from, from + eff.perPage)));
636
+ };
637
+ /** Hits of the term_pages source for `terms` (deps recorded). */
638
+ const termPageHits = (terms) => {
639
+ depTree = true;
640
+ if (terms.length === 0)
641
+ return [];
642
+ const sorted = [...terms].sort();
643
+ const key = JSON.stringify([null, sorted, null]);
644
+ if (!depQueries.has(key))
645
+ depQueries.set(key, { pageType: null, terms: sorted, parent: null });
646
+ return s.pageList
647
+ .filter((p) => p.inBuild && sameVariant(p) && p.id !== pageId && terms.some((t) => p.terms.includes(t)))
648
+ .sort(byTitle);
649
+ };
650
+ /**
651
+ * The `pagination` source: one entry per page of the page's paginated
652
+ * list (`number`, `link`, `is_current`, `is_prev`, `is_next`); empty
653
+ * while everything fits on one page.
654
+ */
655
+ const pagerEntries = () => {
656
+ pagerShown = true;
657
+ const known = opts.pagination?.pageCount ?? paged?.pageCount;
658
+ if (known === undefined) {
659
+ // The list renders later on this page: the caller renders again
660
+ // with the page count (ListPaginationState.pagerEarly).
661
+ pagerEarly = true;
662
+ return EMPTY_LIST;
663
+ }
664
+ if (known <= 1)
665
+ return EMPTY_LIST;
666
+ const self = pageId === null ? null : parseLinkRef(`caelo:page/${pageId}`);
667
+ const r = self !== null && resolver ? resolver.resolve(self) : null;
668
+ const style = opts.pageUrlStyle;
669
+ if (r === null ||
670
+ style === undefined ||
671
+ (r.status !== "resolved" && r.status !== "unpublished")) {
672
+ return { kind: "failed", marker: "list-context-unavailable:pagination" };
673
+ }
674
+ const entries = [];
675
+ for (let n = 1; n <= known; n += 1) {
676
+ const href = n === 1 ? r.url : pagedUrl(r.url, n, style);
677
+ const pos = { index: n - 1, count: known, depth: 1 };
678
+ const values = {
679
+ number: text(String(n)),
680
+ link: linkValue({ href }, null),
681
+ is_current: bool(n === requestedPage),
682
+ is_prev: bool(n === requestedPage - 1),
683
+ is_next: bool(n === requestedPage + 1),
684
+ is_ancestor: bool(false),
685
+ };
686
+ entries.push({
687
+ id: `page-${n}`,
688
+ itemAttr: null,
689
+ childrenKey: null,
690
+ depth: 1,
691
+ get: (key) => values[key] ?? positionValue(key, pos),
692
+ });
693
+ }
694
+ return listValue(entries);
584
695
  };
585
696
  const byTitle = (a, b) => s.collator.compare(a.title, b.title) || compareIds(a.id, b.id);
586
697
  const systemSource = (name, terms, vocabularyIds, field) => {
@@ -626,22 +737,10 @@ function createPageContext(s, pageId, opts, renderIssues) {
626
737
  depLists.add(id);
627
738
  return termEntries(current.terms.filter((t) => ids === null || ids.has(TERM_REF_RE.exec(t)?.[1] ?? "")), field);
628
739
  }
629
- case "term_pages": {
630
- depTree = true;
631
- if (terms.length === 0)
632
- return EMPTY_LIST;
633
- const sorted = [...terms].sort();
634
- const key = JSON.stringify([null, sorted, null]);
635
- if (!depQueries.has(key))
636
- depQueries.set(key, { pageType: null, terms: sorted, parent: null });
637
- const hits = s.pageList
638
- .filter((p) => p.inBuild &&
639
- sameVariant(p) &&
640
- p.id !== pageId &&
641
- terms.some((t) => p.terms.includes(t)))
642
- .sort(byTitle);
643
- return listValue(pageEntries(hits.slice(0, LIST_QUERY_MAX_LIMIT)));
644
- }
740
+ case "term_pages":
741
+ return cutAt(termPageHits(terms), LIST_QUERY_MAX_LIMIT, field);
742
+ case "pagination":
743
+ return pagerEntries();
645
744
  }
646
745
  };
647
746
  const pluginEntries = (name) => {
@@ -689,7 +788,7 @@ function createPageContext(s, pageId, opts, renderIssues) {
689
788
  switch (source.kind) {
690
789
  case "query": {
691
790
  const q = mergedQuery(source, item.query, field, `${item.id}.query`);
692
- return "marker" in q ? { kind: "failed", marker: q.marker } : runQuery(q);
791
+ return "marker" in q ? { kind: "failed", marker: q.marker } : runQuery(q, field);
693
792
  }
694
793
  case "system": {
695
794
  const termFields = node.fields.filter((f) => f.type === "term");
@@ -710,8 +809,40 @@ function createPageContext(s, pageId, opts, renderIssues) {
710
809
  const fromValue = (v) => v.kind === "failed"
711
810
  ? { ok: false, marker: v.marker }
712
811
  : { ok: true, entries: v.kind === "list" ? v.entries : [] };
812
+ /**
813
+ * A paginated source field (#1257): a query, or the term_pages system
814
+ * source. The page size replaces the query's `limit`; the placement
815
+ * value may carry `paginate` overrides next to its knobs.
816
+ */
817
+ const resolvePaginated = (field, settings, value) => {
818
+ const { paginate: overrides, rest } = splitPaginationValue(value);
819
+ const valid = listPaginationSchema.safeParse(settings);
820
+ const eff = valid.success ? effectivePagination(valid.data, overrides) : null;
821
+ if (eff === null)
822
+ return { ok: false, marker: `list-malformed:${field.name}[paginate]` };
823
+ const source = field.source;
824
+ if (source?.kind === "query") {
825
+ const q = mergedQuery({ ...source, defaults: { ...source.defaults, limit: eff.perPage } }, rest, field.name, "query");
826
+ if ("marker" in q)
827
+ return { ok: false, marker: q.marker };
828
+ const key = JSON.stringify(["query", { ...q, limit: null }]);
829
+ return fromValue(paginate(field.name, key, queryHits(q), eff));
830
+ }
831
+ if (source?.kind === "system" && source.name === "term_pages") {
832
+ const terms = rest?.terms;
833
+ const list = Array.isArray(terms)
834
+ ? terms.filter((t) => typeof t === "string")
835
+ : [];
836
+ const key = JSON.stringify(["term_pages", [...list].sort()]);
837
+ return fromValue(paginate(field.name, key, termPageHits(list), eff));
838
+ }
839
+ return { ok: false, marker: `list-malformed:${field.name}[paginate]` };
840
+ };
713
841
  const resolveField = (field, value, holder) => {
714
842
  const bind = field.bind ?? "local";
843
+ if (bind === "source" && field.paginate !== undefined) {
844
+ return resolvePaginated(field, field.paginate, value);
845
+ }
715
846
  if (bind === "source") {
716
847
  const source = field.source;
717
848
  if (!source)
@@ -719,7 +850,9 @@ function createPageContext(s, pageId, opts, renderIssues) {
719
850
  switch (source.kind) {
720
851
  case "query": {
721
852
  const q = mergedQuery(source, value, field.name, "query");
722
- return "marker" in q ? { ok: false, marker: q.marker } : fromValue(runQuery(q));
853
+ return "marker" in q
854
+ ? { ok: false, marker: q.marker }
855
+ : fromValue(runQuery(q, field.name));
723
856
  }
724
857
  case "system": {
725
858
  const overrides = (value ?? {});
@@ -901,6 +1034,19 @@ function createPageContext(s, pageId, opts, renderIssues) {
901
1034
  pages: [...depPages].sort(),
902
1035
  }),
903
1036
  overlayIssues: () => [...renderIssues, ...pageIssues],
1037
+ pagination: () => paged === null
1038
+ ? null
1039
+ : {
1040
+ field: paged.field,
1041
+ perPage: paged.perPage,
1042
+ total: paged.total,
1043
+ pageCount: paged.pageCount,
1044
+ page: requestedPage,
1045
+ followUps: paged.followUps,
1046
+ pagerShown,
1047
+ pagerEarly,
1048
+ },
1049
+ truncatedLists: () => [...truncated.values()],
904
1050
  };
905
1051
  }
906
1052
  function normaliseUrl(url) {