@backmatter/core 0.0.24 → 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/bin/backmatter.js +3 -1
- 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/cli.ts +1 -1
- package/src/node/media-cli.ts +5 -3
- package/src/page-nav.ts +67 -0
package/bin/backmatter.js
CHANGED
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/cli.ts
CHANGED
|
@@ -554,7 +554,7 @@ export async function runInit(args: CliArgs): Promise<string[]> {
|
|
|
554
554
|
return notes;
|
|
555
555
|
}
|
|
556
556
|
|
|
557
|
-
async function main(argv: string[]): Promise<number> {
|
|
557
|
+
export async function main(argv: string[]): Promise<number> {
|
|
558
558
|
if (argv[0] === "login" || argv[0] === "media") {
|
|
559
559
|
return runMediaCli(argv);
|
|
560
560
|
}
|
package/src/node/media-cli.ts
CHANGED
|
@@ -12,6 +12,7 @@ export const MEDIA_USAGE = `Usage:
|
|
|
12
12
|
backmatter media url <id> [--width <px>] [--origin <url>]
|
|
13
13
|
|
|
14
14
|
login opens the browser and stores a personal token in ~/.backmatter/credentials.json.
|
|
15
|
+
media upload and list use --workspace, else BACKMATTER_WORKSPACE, else BACKMATTER_WORKSPACE_ID.
|
|
15
16
|
media url prints a CDN url. --width <px> appends width and format=auto.`;
|
|
16
17
|
|
|
17
18
|
type Credentials = { api: string; token: string; origin?: string };
|
|
@@ -111,7 +112,7 @@ async function media(
|
|
|
111
112
|
if (flags.width) {
|
|
112
113
|
const width = Number(flags.width);
|
|
113
114
|
const url = mediaVariantUrl(origin, id, width);
|
|
114
|
-
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");
|
|
115
116
|
io.log(url);
|
|
116
117
|
return 0;
|
|
117
118
|
}
|
|
@@ -121,9 +122,10 @@ async function media(
|
|
|
121
122
|
const creds = await readCredentials(io.home);
|
|
122
123
|
const api = (flags.api || creds?.api || io.env.BACKMATTER_API_URL || "").replace(/\/$/, "");
|
|
123
124
|
const token = creds?.token;
|
|
124
|
-
const workspace =
|
|
125
|
+
const workspace =
|
|
126
|
+
flags.workspace || io.env.BACKMATTER_WORKSPACE || io.env.BACKMATTER_WORKSPACE_ID;
|
|
125
127
|
if (!api || !token) throw new Error("run backmatter login first");
|
|
126
|
-
if (!workspace) throw new Error("pass --workspace <id>");
|
|
128
|
+
if (!workspace) throw new Error("pass --workspace <id> or set BACKMATTER_WORKSPACE_ID");
|
|
127
129
|
const fetchImpl = io.fetch ?? fetch;
|
|
128
130
|
if (sub === "list") {
|
|
129
131
|
const params = new URLSearchParams();
|
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
|
+
}
|