@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.
- package/dist/ai-tools.d.ts +8 -0
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/build-page.d.ts +16 -0
- package/dist/build-page.d.ts.map +1 -1
- package/dist/content.d.ts +24 -0
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +40 -0
- package/dist/content.js.map +1 -1
- package/dist/lists/index.d.ts +2 -0
- package/dist/lists/index.d.ts.map +1 -1
- package/dist/lists/index.js +2 -0
- package/dist/lists/index.js.map +1 -1
- package/dist/lists/list-type.d.ts +4 -1
- package/dist/lists/list-type.d.ts.map +1 -1
- package/dist/lists/list-type.js +2 -0
- package/dist/lists/list-type.js.map +1 -1
- package/dist/lists/paginated-render.d.ts +38 -0
- package/dist/lists/paginated-render.d.ts.map +1 -0
- package/dist/lists/paginated-render.js +45 -0
- package/dist/lists/paginated-render.js.map +1 -0
- package/dist/lists/pagination.d.ts +95 -0
- package/dist/lists/pagination.d.ts.map +1 -0
- package/dist/lists/pagination.js +136 -0
- package/dist/lists/pagination.js.map +1 -0
- package/dist/lists/resolve.d.ts +65 -0
- package/dist/lists/resolve.d.ts.map +1 -1
- package/dist/lists/resolve.js +166 -20
- package/dist/lists/resolve.js.map +1 -1
- package/dist/live-events.d.ts.map +1 -1
- package/dist/live-events.js +12 -1
- package/dist/live-events.js.map +1 -1
- package/dist/markup-structure.d.ts +25 -0
- package/dist/markup-structure.d.ts.map +1 -1
- package/dist/markup-structure.js +88 -1
- package/dist/markup-structure.js.map +1 -1
- package/dist/media.d.ts +13 -0
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +40 -0
- package/dist/media.js.map +1 -1
- package/dist/nested-module-render.d.ts +13 -0
- package/dist/nested-module-render.d.ts.map +1 -1
- package/dist/nested-module-render.js +22 -1
- package/dist/nested-module-render.js.map +1 -1
- package/dist/template-engine.d.ts +4 -1
- package/dist/template-engine.d.ts.map +1 -1
- package/dist/template-engine.js +6 -4
- package/dist/template-engine.js.map +1 -1
- package/dist/template-sections.d.ts.map +1 -1
- package/dist/template-sections.js +1 -0
- package/dist/template-sections.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/content.ts +43 -0
- package/src/lists/index.ts +2 -0
- package/src/lists/list-type.ts +2 -0
- package/src/lists/paginated-render.ts +77 -0
- package/src/lists/pagination.test.ts +422 -0
- package/src/lists/pagination.ts +160 -0
- package/src/lists/resolve.ts +254 -22
- package/src/live-events.ts +12 -1
- package/src/markup-structure.test.ts +27 -0
- package/src/markup-structure.ts +96 -1
- package/src/media.ts +43 -0
- package/src/nested-module-render.ts +25 -1
- package/src/template-engine.ts +9 -4
- package/src/template-sections.ts +1 -0
- 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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"}
|
package/dist/lists/resolve.d.ts
CHANGED
|
@@ -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":"AAEA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/lists/resolve.ts"],"names":[],"mappings":"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"}
|
package/dist/lists/resolve.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
|
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) {
|