@backmatter/core 0.0.25 → 0.0.26
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/package.json +1 -1
- package/src/config.ts +2 -0
- package/src/index.ts +10 -0
- package/src/media-url.ts +10 -2
- package/src/node/media-cli.ts +1 -1
- package/src/page-nav.ts +67 -0
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { detectGitBranch } from "./sync-schema";
|
|
|
7
7
|
export type DevBarHostConfig = {
|
|
8
8
|
/** Draw a viewport ring and top pill while the site session is in preview. */
|
|
9
9
|
previewFrame?: boolean;
|
|
10
|
+
/** Bottom corner radius (px) for the preview ring (host window lip below the chrome). */
|
|
11
|
+
previewFrameRadius?: number;
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
export type BackmatterDirs = {
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,8 @@ export type {
|
|
|
10
10
|
export { field, serializeField, parseFieldSnapshot } from "./fields";
|
|
11
11
|
export {
|
|
12
12
|
MAX_MEDIA_BYTES,
|
|
13
|
+
MAX_MEDIA_TEXT,
|
|
14
|
+
MAX_VARIANT_WIDTH,
|
|
13
15
|
MEDIA_CONTENT_TYPES,
|
|
14
16
|
MEDIA_WIDTHS,
|
|
15
17
|
isMediaContentType,
|
|
@@ -92,6 +94,14 @@ export {
|
|
|
92
94
|
sortByRouteSpecificity,
|
|
93
95
|
} from "./routes";
|
|
94
96
|
|
|
97
|
+
export type { PageNavDoc, PageNavItem } from "./page-nav";
|
|
98
|
+
export {
|
|
99
|
+
PAGE_NAV_DEFAULT_LIMIT,
|
|
100
|
+
PAGE_NAV_MAX_LIMIT,
|
|
101
|
+
pageNavItems,
|
|
102
|
+
paginatePageNavItems,
|
|
103
|
+
} from "./page-nav";
|
|
104
|
+
|
|
95
105
|
export type { PathMatch, ContentSource, ParsedContentKey } from "./path";
|
|
96
106
|
export {
|
|
97
107
|
compilePathPattern,
|
package/src/media-url.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
// packages/core/src/media-url.ts
|
|
2
2
|
|
|
3
|
-
/** Widths the catalog lists for the gallery.
|
|
3
|
+
/** Widths the catalog lists for the gallery. */
|
|
4
4
|
export const MEDIA_WIDTHS = [400, 800, 1200, 2000] as const;
|
|
5
5
|
|
|
6
|
+
/** Widest variant the CDN will resize. A wider request is refused. */
|
|
7
|
+
export const MAX_VARIANT_WIDTH = 2000;
|
|
8
|
+
|
|
6
9
|
export type MediaWidth = (typeof MEDIA_WIDTHS)[number];
|
|
7
10
|
|
|
8
11
|
/** Bytes the catalog accepts for one file. */
|
|
9
12
|
export const MAX_MEDIA_BYTES = 20 * 1024 * 1024;
|
|
10
13
|
|
|
14
|
+
/** Characters of alt text or caption the catalog stores on one image. */
|
|
15
|
+
export const MAX_MEDIA_TEXT = 500;
|
|
16
|
+
|
|
11
17
|
export const MEDIA_CONTENT_TYPES = [
|
|
12
18
|
"image/jpeg",
|
|
13
19
|
"image/png",
|
|
@@ -39,7 +45,9 @@ export function mediaVariantUrl(
|
|
|
39
45
|
id: string,
|
|
40
46
|
width: number,
|
|
41
47
|
): string | null {
|
|
42
|
-
if (!isMediaId(id) || !Number.isInteger(width) || width < 1)
|
|
48
|
+
if (!isMediaId(id) || !Number.isInteger(width) || width < 1 || width > MAX_VARIANT_WIDTH) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
43
51
|
return `${origin.replace(/\/$/, "")}/img/${id}/width=${width},format=auto`;
|
|
44
52
|
}
|
|
45
53
|
|
package/src/node/media-cli.ts
CHANGED
|
@@ -112,7 +112,7 @@ async function media(
|
|
|
112
112
|
if (flags.width) {
|
|
113
113
|
const width = Number(flags.width);
|
|
114
114
|
const url = mediaVariantUrl(origin, id, width);
|
|
115
|
-
if (!url) throw new Error("width must be a positive integer");
|
|
115
|
+
if (!url) throw new Error("width must be a positive integer up to 2000");
|
|
116
116
|
io.log(url);
|
|
117
117
|
return 0;
|
|
118
118
|
}
|
package/src/page-nav.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// packages/core/src/page-nav.ts
|
|
2
|
+
import { isRoutable, routeFor, type RouteModel } from "./routes";
|
|
3
|
+
|
|
4
|
+
export type PageNavDoc = {
|
|
5
|
+
locale: string;
|
|
6
|
+
type: string;
|
|
7
|
+
slug: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type PageNavItem = {
|
|
11
|
+
key: string;
|
|
12
|
+
label: string;
|
|
13
|
+
href: string;
|
|
14
|
+
current: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Routable documents, in label order. Unroutable models stay out of the menu. */
|
|
18
|
+
export function pageNavItems(input: {
|
|
19
|
+
docs: readonly PageNavDoc[];
|
|
20
|
+
models: readonly RouteModel[];
|
|
21
|
+
current: PageNavDoc | null;
|
|
22
|
+
}): PageNavItem[] {
|
|
23
|
+
const byType = new Map(input.models.map((model) => [model.type, model]));
|
|
24
|
+
const locales = new Set(input.docs.map((doc) => doc.locale));
|
|
25
|
+
const showLocale = locales.size > 1;
|
|
26
|
+
const items: PageNavItem[] = [];
|
|
27
|
+
for (const doc of input.docs) {
|
|
28
|
+
const model = byType.get(doc.type);
|
|
29
|
+
if (!model || !isRoutable(model)) continue;
|
|
30
|
+
const href = routeFor(model, doc.locale, doc.slug);
|
|
31
|
+
if (!href) continue;
|
|
32
|
+
const name = `${doc.type}/${doc.slug}`;
|
|
33
|
+
items.push({
|
|
34
|
+
key: `${doc.locale}/${doc.type}/${doc.slug}`,
|
|
35
|
+
label: showLocale ? `${name} · ${doc.locale}` : name,
|
|
36
|
+
href,
|
|
37
|
+
current: Boolean(
|
|
38
|
+
input.current &&
|
|
39
|
+
input.current.locale === doc.locale &&
|
|
40
|
+
input.current.type === doc.type &&
|
|
41
|
+
input.current.slug === doc.slug,
|
|
42
|
+
),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
items.sort((a, b) => a.label.localeCompare(b.label));
|
|
46
|
+
return items;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const PAGE_NAV_DEFAULT_LIMIT = 10;
|
|
50
|
+
export const PAGE_NAV_MAX_LIMIT = 50;
|
|
51
|
+
|
|
52
|
+
export function paginatePageNavItems(
|
|
53
|
+
items: readonly PageNavItem[],
|
|
54
|
+
query: { q?: string; limit?: number; offset?: number },
|
|
55
|
+
): { items: PageNavItem[]; total: number } {
|
|
56
|
+
const needle = query.q?.trim().toLowerCase() ?? "";
|
|
57
|
+
const filtered = needle ? items.filter((item) => item.label.toLowerCase().includes(needle)) : [...items];
|
|
58
|
+
const limit = Math.min(
|
|
59
|
+
PAGE_NAV_MAX_LIMIT,
|
|
60
|
+
Math.max(1, query.limit ?? PAGE_NAV_DEFAULT_LIMIT),
|
|
61
|
+
);
|
|
62
|
+
const offset = Math.max(0, query.offset ?? 0);
|
|
63
|
+
return {
|
|
64
|
+
items: filtered.slice(offset, offset + limit),
|
|
65
|
+
total: filtered.length,
|
|
66
|
+
};
|
|
67
|
+
}
|